mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 18:01:22 +00:00
feat(coding-agent): resolve resources with enabled state
This commit is contained in:
parent
9dc2b9b163
commit
3e8eb956b3
4 changed files with 389 additions and 180 deletions
|
|
@ -14,12 +14,17 @@ export interface PathMetadata {
|
|||
origin: "package" | "top-level";
|
||||
}
|
||||
|
||||
export interface ResolvedResource {
|
||||
path: string;
|
||||
enabled: boolean;
|
||||
metadata: PathMetadata;
|
||||
}
|
||||
|
||||
export interface ResolvedPaths {
|
||||
extensions: string[];
|
||||
skills: string[];
|
||||
prompts: string[];
|
||||
themes: string[];
|
||||
metadata: Map<string, PathMetadata>;
|
||||
extensions: ResolvedResource[];
|
||||
skills: ResolvedResource[];
|
||||
prompts: ResolvedResource[];
|
||||
themes: ResolvedResource[];
|
||||
}
|
||||
|
||||
export type MissingSourceAction = "install" | "skip" | "error";
|
||||
|
|
@ -85,11 +90,10 @@ interface PiManifest {
|
|||
}
|
||||
|
||||
interface ResourceAccumulator {
|
||||
extensions: Set<string>;
|
||||
skills: Set<string>;
|
||||
prompts: Set<string>;
|
||||
themes: Set<string>;
|
||||
metadata: Map<string, PathMetadata>;
|
||||
extensions: Map<string, { metadata: PathMetadata; enabled: boolean }>;
|
||||
skills: Map<string, { metadata: PathMetadata; enabled: boolean }>;
|
||||
prompts: Map<string, { metadata: PathMetadata; enabled: boolean }>;
|
||||
themes: Map<string, { metadata: PathMetadata; enabled: boolean }>;
|
||||
}
|
||||
|
||||
interface PackageFilter {
|
||||
|
|
@ -111,11 +115,7 @@ const FILE_PATTERNS: Record<ResourceType, RegExp> = {
|
|||
};
|
||||
|
||||
function isPattern(s: string): boolean {
|
||||
return s.startsWith("!") || s.includes("*") || s.includes("?");
|
||||
}
|
||||
|
||||
function hasPatterns(entries: string[]): boolean {
|
||||
return entries.some(isPattern);
|
||||
return s.startsWith("!") || s.startsWith("+") || s.includes("*") || s.includes("?");
|
||||
}
|
||||
|
||||
function splitPatterns(entries: string[]): { plain: string[]; patterns: string[] } {
|
||||
|
|
@ -210,42 +210,59 @@ function collectSkillEntries(dir: string): string[] {
|
|||
return entries;
|
||||
}
|
||||
|
||||
function applyPatterns(allPaths: string[], patterns: string[], baseDir: string): string[] {
|
||||
function matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean {
|
||||
const rel = relative(baseDir, filePath);
|
||||
const name = basename(filePath);
|
||||
return patterns.some(
|
||||
(pattern) => minimatch(rel, pattern) || minimatch(name, pattern) || minimatch(filePath, pattern),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply patterns to paths and return a Set of enabled paths.
|
||||
* Pattern types:
|
||||
* - Plain patterns: include matching paths
|
||||
* - `!pattern`: exclude matching paths
|
||||
* - `+pattern`: force-include matching paths (overrides exclusions)
|
||||
*/
|
||||
function applyPatterns(allPaths: string[], patterns: string[], baseDir: string): Set<string> {
|
||||
const includes: string[] = [];
|
||||
const excludes: string[] = [];
|
||||
const forceIncludes: string[] = [];
|
||||
|
||||
for (const p of patterns) {
|
||||
if (p.startsWith("!")) {
|
||||
if (p.startsWith("+")) {
|
||||
forceIncludes.push(p.slice(1));
|
||||
} else if (p.startsWith("!")) {
|
||||
excludes.push(p.slice(1));
|
||||
} else {
|
||||
includes.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Apply includes (or all if no includes)
|
||||
let result: string[];
|
||||
if (includes.length === 0) {
|
||||
result = [...allPaths];
|
||||
} else {
|
||||
result = allPaths.filter((filePath) => {
|
||||
const rel = relative(baseDir, filePath);
|
||||
const name = basename(filePath);
|
||||
return includes.some((pattern) => {
|
||||
return minimatch(rel, pattern) || minimatch(name, pattern) || minimatch(filePath, pattern);
|
||||
});
|
||||
});
|
||||
result = allPaths.filter((filePath) => matchesAnyPattern(filePath, includes, baseDir));
|
||||
}
|
||||
|
||||
// Step 2: Apply excludes
|
||||
if (excludes.length > 0) {
|
||||
result = result.filter((filePath) => {
|
||||
const rel = relative(baseDir, filePath);
|
||||
const name = basename(filePath);
|
||||
return !excludes.some((pattern) => {
|
||||
return minimatch(rel, pattern) || minimatch(name, pattern) || minimatch(filePath, pattern);
|
||||
});
|
||||
});
|
||||
result = result.filter((filePath) => !matchesAnyPattern(filePath, excludes, baseDir));
|
||||
}
|
||||
|
||||
return result;
|
||||
// Step 3: Force-include (add back from allPaths, overriding exclusions)
|
||||
if (forceIncludes.length > 0) {
|
||||
for (const filePath of allPaths) {
|
||||
if (!result.includes(filePath) && matchesAnyPattern(filePath, forceIncludes, baseDir)) {
|
||||
result.push(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Set(result);
|
||||
}
|
||||
|
||||
export class DefaultPackageManager implements PackageManager {
|
||||
|
|
@ -322,23 +339,19 @@ export class DefaultPackageManager implements PackageManager {
|
|||
await this.resolvePackageSources(packageSources, accumulator, onMissing);
|
||||
|
||||
for (const resourceType of RESOURCE_TYPES) {
|
||||
const target = this.getTargetSet(accumulator, resourceType);
|
||||
const target = this.getTargetMap(accumulator, resourceType);
|
||||
const globalEntries = (globalSettings[resourceType] ?? []) as string[];
|
||||
const projectEntries = (projectSettings[resourceType] ?? []) as string[];
|
||||
this.resolveLocalEntries(
|
||||
globalEntries,
|
||||
resourceType,
|
||||
target,
|
||||
{ source: "local", scope: "user", origin: "top-level" },
|
||||
accumulator,
|
||||
);
|
||||
this.resolveLocalEntries(
|
||||
projectEntries,
|
||||
resourceType,
|
||||
target,
|
||||
{ source: "local", scope: "project", origin: "top-level" },
|
||||
accumulator,
|
||||
);
|
||||
this.resolveLocalEntries(globalEntries, resourceType, target, {
|
||||
source: "local",
|
||||
scope: "user",
|
||||
origin: "top-level",
|
||||
});
|
||||
this.resolveLocalEntries(projectEntries, resourceType, target, {
|
||||
source: "local",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
});
|
||||
}
|
||||
|
||||
return this.toResolvedPaths(accumulator);
|
||||
|
|
@ -486,13 +499,13 @@ export class DefaultPackageManager implements PackageManager {
|
|||
try {
|
||||
const stats = statSync(resolved);
|
||||
if (stats.isFile()) {
|
||||
this.addPath(accumulator.extensions, resolved, metadata, accumulator);
|
||||
this.addResource(accumulator.extensions, resolved, metadata, true);
|
||||
return;
|
||||
}
|
||||
if (stats.isDirectory()) {
|
||||
const resources = this.collectPackageResources(resolved, accumulator, filter, metadata);
|
||||
if (!resources) {
|
||||
this.addPath(accumulator.extensions, resolved, metadata, accumulator);
|
||||
this.addResource(accumulator.extensions, resolved, metadata, true);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -798,11 +811,11 @@ export class DefaultPackageManager implements PackageManager {
|
|||
if (filter) {
|
||||
for (const resourceType of RESOURCE_TYPES) {
|
||||
const patterns = filter[resourceType as keyof PackageFilter];
|
||||
const target = this.getTargetSet(accumulator, resourceType);
|
||||
const target = this.getTargetMap(accumulator, resourceType);
|
||||
if (patterns !== undefined) {
|
||||
this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata, accumulator);
|
||||
this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata);
|
||||
} else {
|
||||
this.collectDefaultResources(packageRoot, resourceType, target, metadata, accumulator);
|
||||
this.collectDefaultResources(packageRoot, resourceType, target, metadata);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
|
@ -816,9 +829,8 @@ export class DefaultPackageManager implements PackageManager {
|
|||
entries,
|
||||
packageRoot,
|
||||
resourceType,
|
||||
this.getTargetSet(accumulator, resourceType),
|
||||
this.getTargetMap(accumulator, resourceType),
|
||||
metadata,
|
||||
accumulator,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
|
|
@ -828,7 +840,12 @@ export class DefaultPackageManager implements PackageManager {
|
|||
for (const resourceType of RESOURCE_TYPES) {
|
||||
const dir = join(packageRoot, resourceType);
|
||||
if (existsSync(dir)) {
|
||||
this.addPath(this.getTargetSet(accumulator, resourceType), dir, metadata, accumulator);
|
||||
// Collect all files from the directory (all enabled by default)
|
||||
const files =
|
||||
resourceType === "skills" ? collectSkillEntries(dir) : collectFiles(dir, FILE_PATTERNS[resourceType]);
|
||||
for (const f of files) {
|
||||
this.addResource(this.getTargetMap(accumulator, resourceType), f, metadata, true);
|
||||
}
|
||||
hasAnyDir = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -838,19 +855,23 @@ export class DefaultPackageManager implements PackageManager {
|
|||
private collectDefaultResources(
|
||||
packageRoot: string,
|
||||
resourceType: ResourceType,
|
||||
target: Set<string>,
|
||||
target: Map<string, { metadata: PathMetadata; enabled: boolean }>,
|
||||
metadata: PathMetadata,
|
||||
accumulator: ResourceAccumulator,
|
||||
): void {
|
||||
const manifest = this.readPiManifest(packageRoot);
|
||||
const entries = manifest?.[resourceType as keyof PiManifest];
|
||||
if (entries) {
|
||||
this.addManifestEntries(entries, packageRoot, resourceType, target, metadata, accumulator);
|
||||
this.addManifestEntries(entries, packageRoot, resourceType, target, metadata);
|
||||
return;
|
||||
}
|
||||
const dir = join(packageRoot, resourceType);
|
||||
if (existsSync(dir)) {
|
||||
this.addPath(target, dir, metadata, accumulator);
|
||||
// Collect all files from the directory (all enabled by default)
|
||||
const files =
|
||||
resourceType === "skills" ? collectSkillEntries(dir) : collectFiles(dir, FILE_PATTERNS[resourceType]);
|
||||
for (const f of files) {
|
||||
this.addResource(target, f, metadata, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -858,37 +879,64 @@ export class DefaultPackageManager implements PackageManager {
|
|||
packageRoot: string,
|
||||
userPatterns: string[],
|
||||
resourceType: ResourceType,
|
||||
target: Set<string>,
|
||||
target: Map<string, { metadata: PathMetadata; enabled: boolean }>,
|
||||
metadata: PathMetadata,
|
||||
accumulator: ResourceAccumulator,
|
||||
): void {
|
||||
const { allFiles, enabledByManifest } = this.collectManifestFiles(packageRoot, resourceType);
|
||||
|
||||
if (userPatterns.length === 0) {
|
||||
// No user patterns, just use manifest filtering
|
||||
for (const f of allFiles) {
|
||||
this.addResource(target, f, metadata, enabledByManifest.has(f));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const manifestFiles = this.collectManifestFilteredFiles(packageRoot, resourceType);
|
||||
const filtered = applyPatterns(manifestFiles, userPatterns, packageRoot);
|
||||
for (const f of filtered) {
|
||||
this.addPath(target, f, metadata, accumulator);
|
||||
// Apply user patterns on top of manifest-enabled files
|
||||
const enabledByUser = applyPatterns(Array.from(enabledByManifest), userPatterns, packageRoot);
|
||||
|
||||
// A file is enabled if it passes both manifest AND user patterns
|
||||
// But force-include (+) in user patterns can bring back files excluded by manifest
|
||||
const forceIncludePatterns = userPatterns.filter((p) => p.startsWith("+")).map((p) => p.slice(1));
|
||||
const forceEnabled =
|
||||
forceIncludePatterns.length > 0
|
||||
? new Set(allFiles.filter((f) => matchesAnyPattern(f, forceIncludePatterns, packageRoot)))
|
||||
: new Set<string>();
|
||||
|
||||
for (const f of allFiles) {
|
||||
const enabled = enabledByUser.has(f) || forceEnabled.has(f);
|
||||
this.addResource(target, f, metadata, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
private collectManifestFilteredFiles(packageRoot: string, resourceType: ResourceType): string[] {
|
||||
/**
|
||||
* Collect all files from a package for a resource type, applying manifest patterns.
|
||||
* Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files
|
||||
* that pass the manifest's own patterns.
|
||||
*/
|
||||
private collectManifestFiles(
|
||||
packageRoot: string,
|
||||
resourceType: ResourceType,
|
||||
): { allFiles: string[]; enabledByManifest: Set<string> } {
|
||||
const manifest = this.readPiManifest(packageRoot);
|
||||
const entries = manifest?.[resourceType as keyof PiManifest];
|
||||
if (entries && entries.length > 0) {
|
||||
const allFiles = this.collectFilesFromManifestEntries(entries, packageRoot, resourceType);
|
||||
const manifestPatterns = entries.filter(isPattern);
|
||||
return manifestPatterns.length > 0 ? applyPatterns(allFiles, manifestPatterns, packageRoot) : allFiles;
|
||||
const enabledByManifest =
|
||||
manifestPatterns.length > 0 ? applyPatterns(allFiles, manifestPatterns, packageRoot) : new Set(allFiles);
|
||||
return { allFiles, enabledByManifest };
|
||||
}
|
||||
|
||||
const conventionDir = join(packageRoot, resourceType);
|
||||
if (!existsSync(conventionDir)) {
|
||||
return [];
|
||||
return { allFiles: [], enabledByManifest: new Set() };
|
||||
}
|
||||
return resourceType === "skills"
|
||||
? collectSkillEntries(conventionDir)
|
||||
: collectFiles(conventionDir, FILE_PATTERNS[resourceType]);
|
||||
const allFiles =
|
||||
resourceType === "skills"
|
||||
? collectSkillEntries(conventionDir)
|
||||
: collectFiles(conventionDir, FILE_PATTERNS[resourceType]);
|
||||
return { allFiles, enabledByManifest: new Set(allFiles) };
|
||||
}
|
||||
|
||||
private readPiManifest(packageRoot: string): PiManifest | null {
|
||||
|
|
@ -910,25 +958,17 @@ export class DefaultPackageManager implements PackageManager {
|
|||
entries: string[] | undefined,
|
||||
root: string,
|
||||
resourceType: ResourceType,
|
||||
target: Set<string>,
|
||||
target: Map<string, { metadata: PathMetadata; enabled: boolean }>,
|
||||
metadata: PathMetadata,
|
||||
accumulator: ResourceAccumulator,
|
||||
): void {
|
||||
if (!entries) return;
|
||||
|
||||
if (!hasPatterns(entries)) {
|
||||
for (const entry of entries) {
|
||||
const resolved = resolve(root, entry);
|
||||
this.addPath(target, resolved, metadata, accumulator);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const allFiles = this.collectFilesFromManifestEntries(entries, root, resourceType);
|
||||
const patterns = entries.filter(isPattern);
|
||||
const filtered = applyPatterns(allFiles, patterns, root);
|
||||
for (const f of filtered) {
|
||||
this.addPath(target, f, metadata, accumulator);
|
||||
const enabledPaths = applyPatterns(allFiles, patterns, root);
|
||||
|
||||
for (const f of allFiles) {
|
||||
this.addResource(target, f, metadata, enabledPaths.has(f));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -941,28 +981,22 @@ export class DefaultPackageManager implements PackageManager {
|
|||
private resolveLocalEntries(
|
||||
entries: string[],
|
||||
resourceType: ResourceType,
|
||||
target: Set<string>,
|
||||
target: Map<string, { metadata: PathMetadata; enabled: boolean }>,
|
||||
metadata: PathMetadata,
|
||||
accumulator: ResourceAccumulator,
|
||||
): void {
|
||||
if (entries.length === 0) return;
|
||||
|
||||
if (!hasPatterns(entries)) {
|
||||
for (const entry of entries) {
|
||||
const resolved = this.resolvePath(entry);
|
||||
if (existsSync(resolved)) {
|
||||
this.addPath(target, resolved, metadata, accumulator);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect all files from plain entries (non-pattern entries)
|
||||
const { plain, patterns } = splitPatterns(entries);
|
||||
const resolvedPlain = plain.map((p) => this.resolvePath(p));
|
||||
const allFiles = this.collectFilesFromPaths(resolvedPlain, resourceType);
|
||||
const filtered = applyPatterns(allFiles, patterns, this.cwd);
|
||||
for (const f of filtered) {
|
||||
this.addPath(target, f, metadata, accumulator);
|
||||
|
||||
// Determine which files are enabled based on patterns
|
||||
const enabledPaths = applyPatterns(allFiles, patterns, this.cwd);
|
||||
|
||||
// Add all files with their enabled state
|
||||
for (const f of allFiles) {
|
||||
this.addResource(target, f, metadata, enabledPaths.has(f));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -989,7 +1023,10 @@ export class DefaultPackageManager implements PackageManager {
|
|||
return files;
|
||||
}
|
||||
|
||||
private getTargetSet(accumulator: ResourceAccumulator, resourceType: ResourceType): Set<string> {
|
||||
private getTargetMap(
|
||||
accumulator: ResourceAccumulator,
|
||||
resourceType: ResourceType,
|
||||
): Map<string, { metadata: PathMetadata; enabled: boolean }> {
|
||||
switch (resourceType) {
|
||||
case "extensions":
|
||||
return accumulator.extensions;
|
||||
|
|
@ -1004,31 +1041,41 @@ export class DefaultPackageManager implements PackageManager {
|
|||
}
|
||||
}
|
||||
|
||||
private addPath(set: Set<string>, value: string, metadata?: PathMetadata, accumulator?: ResourceAccumulator): void {
|
||||
if (!value) return;
|
||||
set.add(value);
|
||||
if (metadata && accumulator && !accumulator.metadata.has(value)) {
|
||||
accumulator.metadata.set(value, metadata);
|
||||
private addResource(
|
||||
map: Map<string, { metadata: PathMetadata; enabled: boolean }>,
|
||||
path: string,
|
||||
metadata: PathMetadata,
|
||||
enabled: boolean,
|
||||
): void {
|
||||
if (!path) return;
|
||||
if (!map.has(path)) {
|
||||
map.set(path, { metadata, enabled });
|
||||
}
|
||||
}
|
||||
|
||||
private createAccumulator(): ResourceAccumulator {
|
||||
return {
|
||||
extensions: new Set<string>(),
|
||||
skills: new Set<string>(),
|
||||
prompts: new Set<string>(),
|
||||
themes: new Set<string>(),
|
||||
metadata: new Map<string, PathMetadata>(),
|
||||
extensions: new Map(),
|
||||
skills: new Map(),
|
||||
prompts: new Map(),
|
||||
themes: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
private toResolvedPaths(accumulator: ResourceAccumulator): ResolvedPaths {
|
||||
const toResolved = (entries: Map<string, { metadata: PathMetadata; enabled: boolean }>): ResolvedResource[] => {
|
||||
return Array.from(entries.entries()).map(([path, { metadata, enabled }]) => ({
|
||||
path,
|
||||
enabled,
|
||||
metadata,
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
extensions: Array.from(accumulator.extensions),
|
||||
skills: Array.from(accumulator.skills),
|
||||
prompts: Array.from(accumulator.prompts),
|
||||
themes: Array.from(accumulator.themes),
|
||||
metadata: accumulator.metadata,
|
||||
extensions: toResolved(accumulator.extensions),
|
||||
skills: toResolved(accumulator.skills),
|
||||
prompts: toResolved(accumulator.prompts),
|
||||
themes: toResolved(accumulator.themes),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -272,23 +272,45 @@ export class DefaultResourceLoader implements ResourceLoader {
|
|||
temporary: true,
|
||||
});
|
||||
|
||||
// Store metadata from resolved paths
|
||||
this.pathMetadata = new Map(resolvedPaths.metadata);
|
||||
// Helper to extract enabled paths and store metadata
|
||||
const getEnabledPaths = (
|
||||
resources: Array<{ path: string; enabled: boolean; metadata: PathMetadata }>,
|
||||
): string[] => {
|
||||
for (const r of resources) {
|
||||
if (!this.pathMetadata.has(r.path)) {
|
||||
this.pathMetadata.set(r.path, r.metadata);
|
||||
}
|
||||
}
|
||||
return resources.filter((r) => r.enabled).map((r) => r.path);
|
||||
};
|
||||
|
||||
// Store metadata and get enabled paths
|
||||
this.pathMetadata = new Map();
|
||||
const enabledExtensions = getEnabledPaths(resolvedPaths.extensions);
|
||||
const enabledSkills = getEnabledPaths(resolvedPaths.skills);
|
||||
const enabledPrompts = getEnabledPaths(resolvedPaths.prompts);
|
||||
const enabledThemes = getEnabledPaths(resolvedPaths.themes);
|
||||
|
||||
// Add CLI paths metadata
|
||||
for (const p of cliExtensionPaths.extensions) {
|
||||
if (!this.pathMetadata.has(p)) {
|
||||
this.pathMetadata.set(p, { source: "cli", scope: "temporary", origin: "top-level" });
|
||||
for (const r of cliExtensionPaths.extensions) {
|
||||
if (!this.pathMetadata.has(r.path)) {
|
||||
this.pathMetadata.set(r.path, { source: "cli", scope: "temporary", origin: "top-level" });
|
||||
}
|
||||
}
|
||||
for (const p of cliExtensionPaths.skills) {
|
||||
if (!this.pathMetadata.has(p)) {
|
||||
this.pathMetadata.set(p, { source: "cli", scope: "temporary", origin: "top-level" });
|
||||
for (const r of cliExtensionPaths.skills) {
|
||||
if (!this.pathMetadata.has(r.path)) {
|
||||
this.pathMetadata.set(r.path, { source: "cli", scope: "temporary", origin: "top-level" });
|
||||
}
|
||||
}
|
||||
|
||||
const cliEnabledExtensions = getEnabledPaths(cliExtensionPaths.extensions);
|
||||
const cliEnabledSkills = getEnabledPaths(cliExtensionPaths.skills);
|
||||
const cliEnabledPrompts = getEnabledPaths(cliExtensionPaths.prompts);
|
||||
const cliEnabledThemes = getEnabledPaths(cliExtensionPaths.themes);
|
||||
|
||||
const extensionPaths = this.noExtensions
|
||||
? cliExtensionPaths.extensions
|
||||
: this.mergePaths(resolvedPaths.extensions, cliExtensionPaths.extensions);
|
||||
? cliEnabledExtensions
|
||||
: this.mergePaths(enabledExtensions, cliEnabledExtensions);
|
||||
|
||||
let extensionsResult: LoadExtensionsResult;
|
||||
if (this.noExtensions) {
|
||||
|
|
@ -313,8 +335,8 @@ export class DefaultResourceLoader implements ResourceLoader {
|
|||
this.extensionsResult = this.extensionsOverride ? this.extensionsOverride(extensionsResult) : extensionsResult;
|
||||
|
||||
const skillPaths = this.noSkills
|
||||
? this.mergePaths(cliExtensionPaths.skills, this.additionalSkillPaths)
|
||||
: this.mergePaths([...resolvedPaths.skills, ...cliExtensionPaths.skills], this.additionalSkillPaths);
|
||||
? this.mergePaths(cliEnabledSkills, this.additionalSkillPaths)
|
||||
: this.mergePaths([...enabledSkills, ...cliEnabledSkills], this.additionalSkillPaths);
|
||||
|
||||
let skillsResult: { skills: Skill[]; diagnostics: ResourceDiagnostic[] };
|
||||
if (this.noSkills && skillPaths.length === 0) {
|
||||
|
|
@ -334,11 +356,8 @@ export class DefaultResourceLoader implements ResourceLoader {
|
|||
}
|
||||
|
||||
const promptPaths = this.noPromptTemplates
|
||||
? this.mergePaths(cliExtensionPaths.prompts, this.additionalPromptTemplatePaths)
|
||||
: this.mergePaths(
|
||||
[...resolvedPaths.prompts, ...cliExtensionPaths.prompts],
|
||||
this.additionalPromptTemplatePaths,
|
||||
);
|
||||
? this.mergePaths(cliEnabledPrompts, this.additionalPromptTemplatePaths)
|
||||
: this.mergePaths([...enabledPrompts, ...cliEnabledPrompts], this.additionalPromptTemplatePaths);
|
||||
|
||||
let promptsResult: { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] };
|
||||
if (this.noPromptTemplates && promptPaths.length === 0) {
|
||||
|
|
@ -359,8 +378,8 @@ export class DefaultResourceLoader implements ResourceLoader {
|
|||
}
|
||||
|
||||
const themePaths = this.noThemes
|
||||
? this.mergePaths(cliExtensionPaths.themes, this.additionalThemePaths)
|
||||
: this.mergePaths([...resolvedPaths.themes, ...cliExtensionPaths.themes], this.additionalThemePaths);
|
||||
? this.mergePaths(cliEnabledThemes, this.additionalThemePaths)
|
||||
: this.mergePaths([...enabledThemes, ...cliEnabledThemes], this.additionalThemePaths);
|
||||
|
||||
let themesResult: { themes: Theme[]; diagnostics: ResourceDiagnostic[] };
|
||||
if (this.noThemes && themePaths.length === 0) {
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ export type {
|
|||
ProgressCallback,
|
||||
ProgressEvent,
|
||||
ResolvedPaths,
|
||||
ResolvedResource,
|
||||
} from "./core/package-manager.js";
|
||||
export { DefaultPackageManager } from "./core/package-manager.js";
|
||||
export type { ResourceCollision, ResourceDiagnostic, ResourceLoader } from "./core/resource-loader.js";
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { DefaultPackageManager, type ProgressEvent } from "../src/core/package-manager.js";
|
||||
import { DefaultPackageManager, type ProgressEvent, type ResolvedResource } from "../src/core/package-manager.js";
|
||||
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||
|
||||
// Helper to check if a resource is enabled
|
||||
const isEnabled = (r: ResolvedResource, pathMatch: string, matchFn: "endsWith" | "includes" = "endsWith") =>
|
||||
matchFn === "endsWith" ? r.path.endsWith(pathMatch) && r.enabled : r.path.includes(pathMatch) && r.enabled;
|
||||
|
||||
const isDisabled = (r: ResolvedResource, pathMatch: string, matchFn: "endsWith" | "includes" = "endsWith") =>
|
||||
matchFn === "endsWith" ? r.path.endsWith(pathMatch) && !r.enabled : r.path.includes(pathMatch) && !r.enabled;
|
||||
|
||||
describe("DefaultPackageManager", () => {
|
||||
let tempDir: string;
|
||||
let settingsManager: SettingsManager;
|
||||
|
|
@ -43,11 +50,11 @@ describe("DefaultPackageManager", () => {
|
|||
settingsManager.setExtensionPaths([extPath]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.extensions).toContain(extPath);
|
||||
expect(result.extensions.some((r) => r.path === extPath && r.enabled)).toBe(true);
|
||||
});
|
||||
|
||||
it("should resolve skill paths from settings", async () => {
|
||||
const skillDir = join(tempDir, "skills");
|
||||
const skillDir = join(tempDir, "skills", "my-skill");
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(skillDir, "SKILL.md"),
|
||||
|
|
@ -58,10 +65,11 @@ description: A test skill
|
|||
Content`,
|
||||
);
|
||||
|
||||
settingsManager.setSkillPaths([skillDir]);
|
||||
settingsManager.setSkillPaths([join(tempDir, "skills")]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.skills).toContain(skillDir);
|
||||
// Skills with SKILL.md are returned as directory paths
|
||||
expect(result.skills.some((r) => r.path === skillDir && r.enabled)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -71,7 +79,7 @@ Content`,
|
|||
writeFileSync(extPath, "export default function() {}");
|
||||
|
||||
const result = await packageManager.resolveExtensionSources([extPath]);
|
||||
expect(result.extensions).toContain(extPath);
|
||||
expect(result.extensions.some((r) => r.path === extPath && r.enabled)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle directories with pi manifest", async () => {
|
||||
|
|
@ -89,11 +97,16 @@ Content`,
|
|||
);
|
||||
mkdirSync(join(pkgDir, "src"), { recursive: true });
|
||||
writeFileSync(join(pkgDir, "src", "index.ts"), "export default function() {}");
|
||||
mkdirSync(join(pkgDir, "skills"), { recursive: true });
|
||||
mkdirSync(join(pkgDir, "skills", "my-skill"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(pkgDir, "skills", "my-skill", "SKILL.md"),
|
||||
"---\nname: my-skill\ndescription: Test\n---\nContent",
|
||||
);
|
||||
|
||||
const result = await packageManager.resolveExtensionSources([pkgDir]);
|
||||
expect(result.extensions).toContain(join(pkgDir, "src", "index.ts"));
|
||||
expect(result.skills).toContain(join(pkgDir, "skills"));
|
||||
expect(result.extensions.some((r) => r.path === join(pkgDir, "src", "index.ts") && r.enabled)).toBe(true);
|
||||
// Skills with SKILL.md are returned as directory paths
|
||||
expect(result.skills.some((r) => r.path === join(pkgDir, "skills", "my-skill") && r.enabled)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle directories with auto-discovery layout", async () => {
|
||||
|
|
@ -104,8 +117,8 @@ Content`,
|
|||
writeFileSync(join(pkgDir, "themes", "dark.json"), "{}");
|
||||
|
||||
const result = await packageManager.resolveExtensionSources([pkgDir]);
|
||||
expect(result.extensions).toContain(join(pkgDir, "extensions"));
|
||||
expect(result.themes).toContain(join(pkgDir, "themes"));
|
||||
expect(result.extensions.some((r) => r.path.endsWith("main.ts") && r.enabled)).toBe(true);
|
||||
expect(result.themes.some((r) => r.path.endsWith("dark.json") && r.enabled)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -169,8 +182,8 @@ Content`,
|
|||
settingsManager.setExtensionPaths([extDir, "!**/remove.ts"]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.extensions.some((p) => p.endsWith("keep.ts"))).toBe(true);
|
||||
expect(result.extensions.some((p) => p.endsWith("remove.ts"))).toBe(false);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "keep.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isDisabled(r, "remove.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter themes with glob patterns", async () => {
|
||||
|
|
@ -183,9 +196,9 @@ Content`,
|
|||
settingsManager.setThemePaths([themesDir, "!funky.json"]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.themes.some((p) => p.endsWith("dark.json"))).toBe(true);
|
||||
expect(result.themes.some((p) => p.endsWith("light.json"))).toBe(true);
|
||||
expect(result.themes.some((p) => p.endsWith("funky.json"))).toBe(false);
|
||||
expect(result.themes.some((r) => isEnabled(r, "dark.json"))).toBe(true);
|
||||
expect(result.themes.some((r) => isEnabled(r, "light.json"))).toBe(true);
|
||||
expect(result.themes.some((r) => isDisabled(r, "funky.json"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter prompts with exclusion pattern", async () => {
|
||||
|
|
@ -197,8 +210,8 @@ Content`,
|
|||
settingsManager.setPromptTemplatePaths([promptsDir, "!explain.md"]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.prompts.some((p) => p.endsWith("review.md"))).toBe(true);
|
||||
expect(result.prompts.some((p) => p.endsWith("explain.md"))).toBe(false);
|
||||
expect(result.prompts.some((r) => isEnabled(r, "review.md"))).toBe(true);
|
||||
expect(result.prompts.some((r) => isDisabled(r, "explain.md"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter skills with exclusion pattern", async () => {
|
||||
|
|
@ -217,8 +230,8 @@ Content`,
|
|||
settingsManager.setSkillPaths([skillsDir, "!**/bad-skill"]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.skills.some((p) => p.includes("good-skill"))).toBe(true);
|
||||
expect(result.skills.some((p) => p.includes("bad-skill"))).toBe(false);
|
||||
expect(result.skills.some((r) => isEnabled(r, "good-skill", "includes"))).toBe(true);
|
||||
expect(result.skills.some((r) => isDisabled(r, "bad-skill", "includes"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should work without patterns (backward compatible)", async () => {
|
||||
|
|
@ -228,7 +241,7 @@ Content`,
|
|||
settingsManager.setExtensionPaths([extPath]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.extensions).toContain(extPath);
|
||||
expect(result.extensions.some((r) => r.path === extPath && r.enabled)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -251,9 +264,9 @@ Content`,
|
|||
);
|
||||
|
||||
const result = await packageManager.resolveExtensionSources([pkgDir]);
|
||||
expect(result.extensions.some((p) => p.endsWith("local.ts"))).toBe(true);
|
||||
expect(result.extensions.some((p) => p.endsWith("remote.ts"))).toBe(true);
|
||||
expect(result.extensions.some((p) => p.endsWith("skip.ts"))).toBe(false);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "local.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "remote.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isDisabled(r, "skip.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should support glob patterns in manifest skills", async () => {
|
||||
|
|
@ -279,8 +292,8 @@ Content`,
|
|||
);
|
||||
|
||||
const result = await packageManager.resolveExtensionSources([pkgDir]);
|
||||
expect(result.skills.some((p) => p.includes("good-skill"))).toBe(true);
|
||||
expect(result.skills.some((p) => p.includes("bad-skill"))).toBe(false);
|
||||
expect(result.skills.some((r) => isEnabled(r, "good-skill", "includes"))).toBe(true);
|
||||
expect(result.skills.some((r) => isDisabled(r, "bad-skill", "includes"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -316,11 +329,11 @@ Content`,
|
|||
|
||||
const result = await packageManager.resolve();
|
||||
// foo.ts should be included (not excluded by anyone)
|
||||
expect(result.extensions.some((p) => p.endsWith("foo.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "foo.ts"))).toBe(true);
|
||||
// bar.ts should be excluded (by user)
|
||||
expect(result.extensions.some((p) => p.endsWith("bar.ts"))).toBe(false);
|
||||
expect(result.extensions.some((r) => isDisabled(r, "bar.ts"))).toBe(true);
|
||||
// baz.ts should be excluded (by manifest)
|
||||
expect(result.extensions.some((p) => p.endsWith("baz.ts"))).toBe(false);
|
||||
expect(result.extensions.some((r) => isDisabled(r, "baz.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should exclude extensions from package with ! pattern", async () => {
|
||||
|
|
@ -341,9 +354,9 @@ Content`,
|
|||
]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.extensions.some((p) => p.endsWith("foo.ts"))).toBe(true);
|
||||
expect(result.extensions.some((p) => p.endsWith("bar.ts"))).toBe(true);
|
||||
expect(result.extensions.some((p) => p.endsWith("baz.ts"))).toBe(false);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "foo.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "bar.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isDisabled(r, "baz.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter themes from package", async () => {
|
||||
|
|
@ -363,8 +376,8 @@ Content`,
|
|||
]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.themes.some((p) => p.endsWith("nice.json"))).toBe(true);
|
||||
expect(result.themes.some((p) => p.endsWith("ugly.json"))).toBe(false);
|
||||
expect(result.themes.some((r) => isEnabled(r, "nice.json"))).toBe(true);
|
||||
expect(result.themes.some((r) => isDisabled(r, "ugly.json"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should combine include and exclude patterns", async () => {
|
||||
|
|
@ -385,9 +398,9 @@ Content`,
|
|||
]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.extensions.some((p) => p.endsWith("alpha.ts"))).toBe(true);
|
||||
expect(result.extensions.some((p) => p.endsWith("beta.ts"))).toBe(false);
|
||||
expect(result.extensions.some((p) => p.endsWith("gamma.ts"))).toBe(false);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "alpha.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isDisabled(r, "beta.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isDisabled(r, "gamma.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should work with direct paths (no patterns)", async () => {
|
||||
|
|
@ -407,8 +420,140 @@ Content`,
|
|||
]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.extensions.some((p) => p.endsWith("one.ts"))).toBe(true);
|
||||
expect(result.extensions.some((p) => p.endsWith("two.ts"))).toBe(false);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "one.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isDisabled(r, "two.ts"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("force-include patterns", () => {
|
||||
it("should force-include extensions with + pattern after exclusion", async () => {
|
||||
const extDir = join(tempDir, "extensions");
|
||||
mkdirSync(extDir, { recursive: true });
|
||||
writeFileSync(join(extDir, "keep.ts"), "export default function() {}");
|
||||
writeFileSync(join(extDir, "excluded.ts"), "export default function() {}");
|
||||
writeFileSync(join(extDir, "force-back.ts"), "export default function() {}");
|
||||
|
||||
// Exclude all, then force-include one back
|
||||
settingsManager.setExtensionPaths([extDir, "!**/*.ts", "+**/force-back.ts"]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.extensions.some((r) => isDisabled(r, "keep.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isDisabled(r, "excluded.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "force-back.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should force-include overrides exclude in package filters", async () => {
|
||||
const pkgDir = join(tempDir, "force-pkg");
|
||||
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
||||
writeFileSync(join(pkgDir, "extensions", "alpha.ts"), "export default function() {}");
|
||||
writeFileSync(join(pkgDir, "extensions", "beta.ts"), "export default function() {}");
|
||||
writeFileSync(join(pkgDir, "extensions", "gamma.ts"), "export default function() {}");
|
||||
|
||||
settingsManager.setPackages([
|
||||
{
|
||||
source: pkgDir,
|
||||
extensions: ["!*", "+**/beta.ts"],
|
||||
skills: [],
|
||||
prompts: [],
|
||||
themes: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.extensions.some((r) => isDisabled(r, "alpha.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "beta.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isDisabled(r, "gamma.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should force-include multiple resources", async () => {
|
||||
const pkgDir = join(tempDir, "multi-force-pkg");
|
||||
mkdirSync(join(pkgDir, "skills/skill-a"), { recursive: true });
|
||||
mkdirSync(join(pkgDir, "skills/skill-b"), { recursive: true });
|
||||
mkdirSync(join(pkgDir, "skills/skill-c"), { recursive: true });
|
||||
writeFileSync(join(pkgDir, "skills/skill-a", "SKILL.md"), "---\nname: skill-a\ndescription: A\n---\nContent");
|
||||
writeFileSync(join(pkgDir, "skills/skill-b", "SKILL.md"), "---\nname: skill-b\ndescription: B\n---\nContent");
|
||||
writeFileSync(join(pkgDir, "skills/skill-c", "SKILL.md"), "---\nname: skill-c\ndescription: C\n---\nContent");
|
||||
|
||||
settingsManager.setPackages([
|
||||
{
|
||||
source: pkgDir,
|
||||
extensions: [],
|
||||
skills: ["!*", "+**/skill-a", "+**/skill-c"],
|
||||
prompts: [],
|
||||
themes: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.skills.some((r) => isEnabled(r, "skill-a", "includes"))).toBe(true);
|
||||
expect(result.skills.some((r) => isDisabled(r, "skill-b", "includes"))).toBe(true);
|
||||
expect(result.skills.some((r) => isEnabled(r, "skill-c", "includes"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should force-include after specific exclusion", async () => {
|
||||
const extDir = join(tempDir, "specific-force");
|
||||
mkdirSync(extDir, { recursive: true });
|
||||
writeFileSync(join(extDir, "a.ts"), "export default function() {}");
|
||||
writeFileSync(join(extDir, "b.ts"), "export default function() {}");
|
||||
|
||||
// Specifically exclude b.ts, then force it back
|
||||
settingsManager.setExtensionPaths([extDir, "!**/b.ts", "+**/b.ts"]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.extensions.some((r) => isEnabled(r, "a.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "b.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle force-include in manifest patterns", async () => {
|
||||
const pkgDir = join(tempDir, "manifest-force-pkg");
|
||||
mkdirSync(join(pkgDir, "extensions"), { recursive: true });
|
||||
writeFileSync(join(pkgDir, "extensions", "one.ts"), "export default function() {}");
|
||||
writeFileSync(join(pkgDir, "extensions", "two.ts"), "export default function() {}");
|
||||
writeFileSync(join(pkgDir, "extensions", "three.ts"), "export default function() {}");
|
||||
writeFileSync(
|
||||
join(pkgDir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "manifest-force-pkg",
|
||||
pi: {
|
||||
extensions: ["extensions", "!**/two.ts", "+**/two.ts"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await packageManager.resolveExtensionSources([pkgDir]);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "one.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "two.ts"))).toBe(true);
|
||||
expect(result.extensions.some((r) => isEnabled(r, "three.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should force-include themes", async () => {
|
||||
const themesDir = join(tempDir, "force-themes");
|
||||
mkdirSync(themesDir, { recursive: true });
|
||||
writeFileSync(join(themesDir, "dark.json"), "{}");
|
||||
writeFileSync(join(themesDir, "light.json"), "{}");
|
||||
writeFileSync(join(themesDir, "special.json"), "{}");
|
||||
|
||||
settingsManager.setThemePaths([themesDir, "!*.json", "+special.json"]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.themes.some((r) => isDisabled(r, "dark.json"))).toBe(true);
|
||||
expect(result.themes.some((r) => isDisabled(r, "light.json"))).toBe(true);
|
||||
expect(result.themes.some((r) => isEnabled(r, "special.json"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should force-include prompts", async () => {
|
||||
const promptsDir = join(tempDir, "force-prompts");
|
||||
mkdirSync(promptsDir, { recursive: true });
|
||||
writeFileSync(join(promptsDir, "review.md"), "Review");
|
||||
writeFileSync(join(promptsDir, "explain.md"), "Explain");
|
||||
writeFileSync(join(promptsDir, "debug.md"), "Debug");
|
||||
|
||||
settingsManager.setPromptTemplatePaths([promptsDir, "!*", "+debug.md"]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
expect(result.prompts.some((r) => isDisabled(r, "review.md"))).toBe(true);
|
||||
expect(result.prompts.some((r) => isDisabled(r, "explain.md"))).toBe(true);
|
||||
expect(result.prompts.some((r) => isEnabled(r, "debug.md"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -429,12 +574,10 @@ Content`,
|
|||
expect(projectSettings.packages).toEqual([pkgDir]);
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
// Auto-discovery returns directories, not individual files
|
||||
// Should only appear once (deduped), with project scope
|
||||
const sharedPaths = result.extensions.filter((p) => p.includes("shared-pkg"));
|
||||
const sharedPaths = result.extensions.filter((r) => r.path.includes("shared-pkg"));
|
||||
expect(sharedPaths.length).toBe(1);
|
||||
const meta = result.metadata.get(sharedPaths[0]);
|
||||
expect(meta?.scope).toBe("project");
|
||||
expect(sharedPaths[0].metadata.scope).toBe("project");
|
||||
});
|
||||
|
||||
it("should keep both if different packages", async () => {
|
||||
|
|
@ -449,9 +592,8 @@ Content`,
|
|||
settingsManager.setProjectPackages([pkg2Dir]); // project
|
||||
|
||||
const result = await packageManager.resolve();
|
||||
// Auto-discovery returns directories, not individual files
|
||||
expect(result.extensions.some((p) => p.includes("pkg1"))).toBe(true);
|
||||
expect(result.extensions.some((p) => p.includes("pkg2"))).toBe(true);
|
||||
expect(result.extensions.some((r) => r.path.includes("pkg1"))).toBe(true);
|
||||
expect(result.extensions.some((r) => r.path.includes("pkg2"))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue