@vandeepunk/pi-coding-agent 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +6 -6
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +2 -2
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/extensions/loader.d.ts.map +1 -1
  8. package/dist/core/extensions/loader.js.map +1 -1
  9. package/dist/core/package-manager.d.ts +1 -1
  10. package/dist/core/package-manager.d.ts.map +1 -1
  11. package/dist/core/package-manager.js +12 -12
  12. package/dist/core/package-manager.js.map +1 -1
  13. package/dist/core/prompt-templates.d.ts +3 -3
  14. package/dist/core/prompt-templates.d.ts.map +1 -1
  15. package/dist/core/prompt-templates.js +15 -15
  16. package/dist/core/prompt-templates.js.map +1 -1
  17. package/dist/core/resource-loader.d.ts.map +1 -1
  18. package/dist/core/resource-loader.js +6 -6
  19. package/dist/core/resource-loader.js.map +1 -1
  20. package/dist/core/settings-manager.d.ts +2 -2
  21. package/dist/core/settings-manager.d.ts.map +1 -1
  22. package/dist/core/settings-manager.js +4 -4
  23. package/dist/core/settings-manager.js.map +1 -1
  24. package/dist/core/slash-commands.d.ts.map +1 -1
  25. package/dist/core/slash-commands.js +1 -1
  26. package/dist/core/slash-commands.js.map +1 -1
  27. package/dist/migrations.d.ts.map +1 -1
  28. package/dist/migrations.js +11 -11
  29. package/dist/migrations.js.map +1 -1
  30. package/dist/modes/interactive/components/config-selector.d.ts +1 -1
  31. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  32. package/dist/modes/interactive/components/config-selector.js +6 -6
  33. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  34. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  35. package/dist/modes/interactive/interactive-mode.js +2 -2
  36. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  37. package/docs/packages.md +3 -3
  38. package/docs/prompt-templates.md +6 -6
  39. package/docs/rpc.md +1 -1
  40. package/docs/sdk.md +5 -3
  41. package/docs/settings.md +2 -2
  42. package/examples/extensions/subagent/README.md +4 -4
  43. package/examples/sdk/08-prompt-templates.ts +2 -2
  44. package/package.json +1 -1
  45. /package/examples/extensions/subagent/{prompts → commands}/implement-and-review.md +0 -0
  46. /package/examples/extensions/subagent/{prompts → commands}/implement.md +0 -0
  47. /package/examples/extensions/subagent/{prompts → commands}/scout-and-plan.md +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"package-manager.d.ts","sourceRoot":"","sources":["../../src/core/package-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport ignore from \"ignore\";\nimport { minimatch } from \"minimatch\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\nimport { looksLikeGitUrl } from \"../utils/git.js\";\nimport type { PackageSource, SettingsManager } from \"./settings-manager.js\";\n\nexport interface PathMetadata {\n\tsource: string;\n\tscope: SourceScope;\n\torigin: \"package\" | \"top-level\";\n\tbaseDir?: string;\n}\n\nexport interface ResolvedResource {\n\tpath: string;\n\tenabled: boolean;\n\tmetadata: PathMetadata;\n}\n\nexport interface ResolvedPaths {\n\textensions: ResolvedResource[];\n\tskills: ResolvedResource[];\n\tprompts: ResolvedResource[];\n\tthemes: ResolvedResource[];\n}\n\nexport type MissingSourceAction = \"install\" | \"skip\" | \"error\";\n\nexport interface ProgressEvent {\n\ttype: \"start\" | \"progress\" | \"complete\" | \"error\";\n\taction: \"install\" | \"remove\" | \"update\" | \"clone\" | \"pull\";\n\tsource: string;\n\tmessage?: string;\n}\n\nexport type ProgressCallback = (event: ProgressEvent) => void;\n\nexport interface PackageManager {\n\tresolve(onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths>;\n\tinstall(source: string, options?: { local?: boolean }): Promise<void>;\n\tremove(source: string, options?: { local?: boolean }): Promise<void>;\n\tupdate(source?: string): Promise<void>;\n\tresolveExtensionSources(\n\t\tsources: string[],\n\t\toptions?: { local?: boolean; temporary?: boolean },\n\t): Promise<ResolvedPaths>;\n\tsetProgressCallback(callback: ProgressCallback | undefined): void;\n\tgetInstalledPath(source: string, scope: \"user\" | \"project\"): string | undefined;\n}\n\ninterface PackageManagerOptions {\n\tcwd: string;\n\tagentDir: string;\n\tsettingsManager: SettingsManager;\n}\n\ntype SourceScope = \"user\" | \"project\" | \"temporary\";\n\ntype NpmSource = {\n\ttype: \"npm\";\n\tspec: string;\n\tname: string;\n\tpinned: boolean;\n};\n\ntype GitSource = {\n\ttype: \"git\";\n\trepo: string;\n\thost: string;\n\tpath: string;\n\tref?: string;\n\tpinned: boolean;\n};\n\ntype LocalSource = {\n\ttype: \"local\";\n\tpath: string;\n};\n\ntype ParsedSource = NpmSource | GitSource | LocalSource;\n\ninterface PiManifest {\n\textensions?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n\tthemes?: string[];\n}\n\ninterface ResourceAccumulator {\n\textensions: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n\tskills: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n\tprompts: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n\tthemes: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n}\n\ninterface PackageFilter {\n\textensions?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n\tthemes?: string[];\n}\n\ntype ResourceType = \"extensions\" | \"skills\" | \"prompts\" | \"themes\";\n\nconst RESOURCE_TYPES: ResourceType[] = [\"extensions\", \"skills\", \"prompts\", \"themes\"];\n\nconst FILE_PATTERNS: Record<ResourceType, RegExp> = {\n\textensions: /\\.(ts|js)$/,\n\tskills: /\\.md$/,\n\tprompts: /\\.md$/,\n\tthemes: /\\.json$/,\n};\n\nconst IGNORE_FILE_NAMES = [\".gitignore\", \".ignore\", \".fdignore\"];\n\ntype IgnoreMatcher = ReturnType<typeof ignore>;\n\nfunction toPosixPath(p: string): string {\n\treturn p.split(sep).join(\"/\");\n}\n\nfunction prefixIgnorePattern(line: string, prefix: string): string | null {\n\tconst trimmed = line.trim();\n\tif (!trimmed) return null;\n\tif (trimmed.startsWith(\"#\") && !trimmed.startsWith(\"\\\\#\")) return null;\n\n\tlet pattern = line;\n\tlet negated = false;\n\n\tif (pattern.startsWith(\"!\")) {\n\t\tnegated = true;\n\t\tpattern = pattern.slice(1);\n\t} else if (pattern.startsWith(\"\\\\!\")) {\n\t\tpattern = pattern.slice(1);\n\t}\n\n\tif (pattern.startsWith(\"/\")) {\n\t\tpattern = pattern.slice(1);\n\t}\n\n\tconst prefixed = prefix ? `${prefix}${pattern}` : pattern;\n\treturn negated ? `!${prefixed}` : prefixed;\n}\n\nfunction addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void {\n\tconst relativeDir = relative(rootDir, dir);\n\tconst prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : \"\";\n\n\tfor (const filename of IGNORE_FILE_NAMES) {\n\t\tconst ignorePath = join(dir, filename);\n\t\tif (!existsSync(ignorePath)) continue;\n\t\ttry {\n\t\t\tconst content = readFileSync(ignorePath, \"utf-8\");\n\t\t\tconst patterns = content\n\t\t\t\t.split(/\\r?\\n/)\n\t\t\t\t.map((line) => prefixIgnorePattern(line, prefix))\n\t\t\t\t.filter((line): line is string => Boolean(line));\n\t\t\tif (patterns.length > 0) {\n\t\t\t\tig.add(patterns);\n\t\t\t}\n\t\t} catch {}\n\t}\n}\n\nfunction isPattern(s: string): boolean {\n\treturn s.startsWith(\"!\") || s.startsWith(\"+\") || s.startsWith(\"-\") || s.includes(\"*\") || s.includes(\"?\");\n}\n\nfunction splitPatterns(entries: string[]): { plain: string[]; patterns: string[] } {\n\tconst plain: string[] = [];\n\tconst patterns: string[] = [];\n\tfor (const entry of entries) {\n\t\tif (isPattern(entry)) {\n\t\t\tpatterns.push(entry);\n\t\t} else {\n\t\t\tplain.push(entry);\n\t\t}\n\t}\n\treturn { plain, patterns };\n}\n\nfunction collectFiles(\n\tdir: string,\n\tfilePattern: RegExp,\n\tskipNodeModules = true,\n\tignoreMatcher?: IgnoreMatcher,\n\trootDir?: string,\n): string[] {\n\tconst files: string[] = [];\n\tif (!existsSync(dir)) return files;\n\n\tconst root = rootDir ?? dir;\n\tconst ig = ignoreMatcher ?? ignore();\n\taddIgnoreRules(ig, dir, root);\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (skipNodeModules && entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isDir = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDir = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(root, fullPath));\n\t\t\tconst ignorePath = isDir ? `${relPath}/` : relPath;\n\t\t\tif (ig.ignores(ignorePath)) continue;\n\n\t\t\tif (isDir) {\n\t\t\t\tfiles.push(...collectFiles(fullPath, filePattern, skipNodeModules, ig, root));\n\t\t\t} else if (isFile && filePattern.test(entry.name)) {\n\t\t\t\tfiles.push(fullPath);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn files;\n}\n\nfunction collectSkillEntries(\n\tdir: string,\n\tincludeRootFiles = true,\n\tignoreMatcher?: IgnoreMatcher,\n\trootDir?: string,\n): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\tconst root = rootDir ?? dir;\n\tconst ig = ignoreMatcher ?? ignore();\n\taddIgnoreRules(ig, dir, root);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isDir = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDir = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(root, fullPath));\n\t\t\tconst ignorePath = isDir ? `${relPath}/` : relPath;\n\t\t\tif (ig.ignores(ignorePath)) continue;\n\n\t\t\tif (isDir) {\n\t\t\t\tentries.push(...collectSkillEntries(fullPath, false, ig, root));\n\t\t\t} else if (isFile) {\n\t\t\t\tconst isRootMd = includeRootFiles && entry.name.endsWith(\".md\");\n\t\t\t\tconst isSkillMd = !includeRootFiles && entry.name === \"SKILL.md\";\n\t\t\t\tif (isRootMd || isSkillMd) {\n\t\t\t\t\tentries.push(fullPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\nfunction collectAutoSkillEntries(dir: string, includeRootFiles = true): string[] {\n\treturn collectSkillEntries(dir, includeRootFiles);\n}\n\nfunction collectAutoPromptEntries(dir: string): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\tconst ig = ignore();\n\taddIgnoreRules(ig, dir, dir);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tisFile = statSync(fullPath).isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(dir, fullPath));\n\t\t\tif (ig.ignores(relPath)) continue;\n\n\t\t\tif (isFile && entry.name.endsWith(\".md\")) {\n\t\t\t\tentries.push(fullPath);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\nfunction collectAutoThemeEntries(dir: string): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\tconst ig = ignore();\n\taddIgnoreRules(ig, dir, dir);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tisFile = statSync(fullPath).isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(dir, fullPath));\n\t\t\tif (ig.ignores(relPath)) continue;\n\n\t\t\tif (isFile && entry.name.endsWith(\".json\")) {\n\t\t\t\tentries.push(fullPath);\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\nfunction readPiManifestFile(packageJsonPath: string): PiManifest | null {\n\ttry {\n\t\tconst content = readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content) as { pi?: PiManifest };\n\t\treturn pkg.pi ?? null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\tconst packageJsonPath = join(dir, \"package.json\");\n\tif (existsSync(packageJsonPath)) {\n\t\tconst manifest = readPiManifestFile(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = resolve(dir, extPath);\n\t\t\t\tif (existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst indexTs = join(dir, \"index.ts\");\n\tconst indexJs = join(dir, \"index.js\");\n\tif (existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\nfunction collectAutoExtensionEntries(dir: string): string[] {\n\tconst entries: string[] = [];\n\tif (!existsSync(dir)) return entries;\n\n\tconst ig = ignore();\n\taddIgnoreRules(ig, dir, dir);\n\n\ttry {\n\t\tconst dirEntries = readdirSync(dir, { withFileTypes: true });\n\t\tfor (const entry of dirEntries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (entry.name === \"node_modules\") continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\t\t\tlet isDir = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDir = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst relPath = toPosixPath(relative(dir, fullPath));\n\t\t\tconst ignorePath = isDir ? `${relPath}/` : relPath;\n\t\t\tif (ig.ignores(ignorePath)) continue;\n\n\t\t\tif (isFile && (entry.name.endsWith(\".ts\") || entry.name.endsWith(\".js\"))) {\n\t\t\t\tentries.push(fullPath);\n\t\t\t} else if (isDir) {\n\t\t\t\tconst resolvedEntries = resolveExtensionEntries(fullPath);\n\t\t\t\tif (resolvedEntries) {\n\t\t\t\t\tentries.push(...resolvedEntries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\n\treturn entries;\n}\n\n/**\n * Collect resource files from a directory based on resource type.\n * Extensions use smart discovery (index.ts in subdirs), others use recursive collection.\n */\nfunction collectResourceFiles(dir: string, resourceType: ResourceType): string[] {\n\tif (resourceType === \"skills\") {\n\t\treturn collectSkillEntries(dir);\n\t}\n\tif (resourceType === \"extensions\") {\n\t\treturn collectAutoExtensionEntries(dir);\n\t}\n\treturn collectFiles(dir, FILE_PATTERNS[resourceType]);\n}\n\nfunction matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean {\n\tconst rel = relative(baseDir, filePath);\n\tconst name = basename(filePath);\n\tconst isSkillFile = name === \"SKILL.md\";\n\tconst parentDir = isSkillFile ? dirname(filePath) : undefined;\n\tconst parentRel = isSkillFile ? relative(baseDir, parentDir!) : undefined;\n\tconst parentName = isSkillFile ? basename(parentDir!) : undefined;\n\n\treturn patterns.some((pattern) => {\n\t\tif (minimatch(rel, pattern) || minimatch(name, pattern) || minimatch(filePath, pattern)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (!isSkillFile) return false;\n\t\treturn minimatch(parentRel!, pattern) || minimatch(parentName!, pattern) || minimatch(parentDir!, pattern);\n\t});\n}\n\nfunction normalizeExactPattern(pattern: string): string {\n\tif (pattern.startsWith(\"./\") || pattern.startsWith(\".\\\\\")) {\n\t\treturn pattern.slice(2);\n\t}\n\treturn pattern;\n}\n\nfunction matchesAnyExactPattern(filePath: string, patterns: string[], baseDir: string): boolean {\n\tif (patterns.length === 0) return false;\n\tconst rel = relative(baseDir, filePath);\n\tconst name = basename(filePath);\n\tconst isSkillFile = name === \"SKILL.md\";\n\tconst parentDir = isSkillFile ? dirname(filePath) : undefined;\n\tconst parentRel = isSkillFile ? relative(baseDir, parentDir!) : undefined;\n\n\treturn patterns.some((pattern) => {\n\t\tconst normalized = normalizeExactPattern(pattern);\n\t\tif (normalized === rel || normalized === filePath) {\n\t\t\treturn true;\n\t\t}\n\t\tif (!isSkillFile) return false;\n\t\treturn normalized === parentRel || normalized === parentDir;\n\t});\n}\n\nfunction getOverridePatterns(entries: string[]): string[] {\n\treturn entries.filter((pattern) => pattern.startsWith(\"!\") || pattern.startsWith(\"+\") || pattern.startsWith(\"-\"));\n}\n\nfunction isEnabledByOverrides(filePath: string, patterns: string[], baseDir: string): boolean {\n\tconst overrides = getOverridePatterns(patterns);\n\tconst excludes = overrides.filter((pattern) => pattern.startsWith(\"!\")).map((pattern) => pattern.slice(1));\n\tconst forceIncludes = overrides.filter((pattern) => pattern.startsWith(\"+\")).map((pattern) => pattern.slice(1));\n\tconst forceExcludes = overrides.filter((pattern) => pattern.startsWith(\"-\")).map((pattern) => pattern.slice(1));\n\n\tlet enabled = true;\n\tif (excludes.length > 0 && matchesAnyPattern(filePath, excludes, baseDir)) {\n\t\tenabled = false;\n\t}\n\tif (forceIncludes.length > 0 && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {\n\t\tenabled = true;\n\t}\n\tif (forceExcludes.length > 0 && matchesAnyExactPattern(filePath, forceExcludes, baseDir)) {\n\t\tenabled = false;\n\t}\n\treturn enabled;\n}\n\n/**\n * Apply patterns to paths and return a Set of enabled paths.\n * Pattern types:\n * - Plain patterns: include matching paths\n * - `!pattern`: exclude matching paths\n * - `+path`: force-include exact path (overrides exclusions)\n * - `-path`: force-exclude exact path (overrides force-includes)\n */\nfunction applyPatterns(allPaths: string[], patterns: string[], baseDir: string): Set<string> {\n\tconst includes: string[] = [];\n\tconst excludes: string[] = [];\n\tconst forceIncludes: string[] = [];\n\tconst forceExcludes: string[] = [];\n\n\tfor (const p of patterns) {\n\t\tif (p.startsWith(\"+\")) {\n\t\t\tforceIncludes.push(p.slice(1));\n\t\t} else if (p.startsWith(\"-\")) {\n\t\t\tforceExcludes.push(p.slice(1));\n\t\t} else if (p.startsWith(\"!\")) {\n\t\t\texcludes.push(p.slice(1));\n\t\t} else {\n\t\t\tincludes.push(p);\n\t\t}\n\t}\n\n\t// Step 1: Apply includes (or all if no includes)\n\tlet result: string[];\n\tif (includes.length === 0) {\n\t\tresult = [...allPaths];\n\t} else {\n\t\tresult = allPaths.filter((filePath) => matchesAnyPattern(filePath, includes, baseDir));\n\t}\n\n\t// Step 2: Apply excludes\n\tif (excludes.length > 0) {\n\t\tresult = result.filter((filePath) => !matchesAnyPattern(filePath, excludes, baseDir));\n\t}\n\n\t// Step 3: Force-include (add back from allPaths, overriding exclusions)\n\tif (forceIncludes.length > 0) {\n\t\tfor (const filePath of allPaths) {\n\t\t\tif (!result.includes(filePath) && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {\n\t\t\t\tresult.push(filePath);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Step 4: Force-exclude (remove even if included or force-included)\n\tif (forceExcludes.length > 0) {\n\t\tresult = result.filter((filePath) => !matchesAnyExactPattern(filePath, forceExcludes, baseDir));\n\t}\n\n\treturn new Set(result);\n}\n\nexport class DefaultPackageManager implements PackageManager {\n\tprivate cwd: string;\n\tprivate agentDir: string;\n\tprivate settingsManager: SettingsManager;\n\tprivate globalNpmRoot: string | undefined;\n\tprivate progressCallback: ProgressCallback | undefined;\n\n\tconstructor(options: PackageManagerOptions) {\n\t\tthis.cwd = options.cwd;\n\t\tthis.agentDir = options.agentDir;\n\t\tthis.settingsManager = options.settingsManager;\n\t}\n\n\tsetProgressCallback(callback: ProgressCallback | undefined): void {\n\t\tthis.progressCallback = callback;\n\t}\n\n\tgetInstalledPath(source: string, scope: \"user\" | \"project\"): string | undefined {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\tconst path = this.getNpmInstallPath(parsed, scope);\n\t\t\treturn existsSync(path) ? path : undefined;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\tconst path = this.getGitInstallPath(parsed, scope);\n\t\t\treturn existsSync(path) ? path : undefined;\n\t\t}\n\t\tif (parsed.type === \"local\") {\n\t\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\t\tconst path = this.resolvePathFromBase(parsed.path, baseDir);\n\t\t\treturn existsSync(path) ? path : undefined;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate emitProgress(event: ProgressEvent): void {\n\t\tthis.progressCallback?.(event);\n\t}\n\n\tprivate async withProgress(\n\t\taction: ProgressEvent[\"action\"],\n\t\tsource: string,\n\t\tmessage: string,\n\t\toperation: () => Promise<void>,\n\t): Promise<void> {\n\t\tthis.emitProgress({ type: \"start\", action, source, message });\n\t\ttry {\n\t\t\tawait operation();\n\t\t\tthis.emitProgress({ type: \"complete\", action, source });\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\tthis.emitProgress({ type: \"error\", action, source, message: errorMessage });\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync resolve(onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths> {\n\t\tconst accumulator = this.createAccumulator();\n\t\tconst globalSettings = this.settingsManager.getGlobalSettings();\n\t\tconst projectSettings = this.settingsManager.getProjectSettings();\n\n\t\t// Collect all packages with scope\n\t\tconst allPackages: Array<{ pkg: PackageSource; scope: SourceScope }> = [];\n\t\tfor (const pkg of globalSettings.packages ?? []) {\n\t\t\tallPackages.push({ pkg, scope: \"user\" });\n\t\t}\n\t\tfor (const pkg of projectSettings.packages ?? []) {\n\t\t\tallPackages.push({ pkg, scope: \"project\" });\n\t\t}\n\n\t\t// Dedupe: project scope wins over global for same package identity\n\t\tconst packageSources = this.dedupePackages(allPackages);\n\t\tawait this.resolvePackageSources(packageSources, accumulator, onMissing);\n\n\t\tconst globalBaseDir = this.agentDir;\n\t\tconst projectBaseDir = join(this.cwd, CONFIG_DIR_NAME);\n\n\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\tconst target = this.getTargetMap(accumulator, resourceType);\n\t\t\tconst globalEntries = (globalSettings[resourceType] ?? []) as string[];\n\t\t\tconst projectEntries = (projectSettings[resourceType] ?? []) as string[];\n\t\t\tthis.resolveLocalEntries(\n\t\t\t\tglobalEntries,\n\t\t\t\tresourceType,\n\t\t\t\ttarget,\n\t\t\t\t{\n\t\t\t\t\tsource: \"local\",\n\t\t\t\t\tscope: \"user\",\n\t\t\t\t\torigin: \"top-level\",\n\t\t\t\t},\n\t\t\t\tglobalBaseDir,\n\t\t\t);\n\t\t\tthis.resolveLocalEntries(\n\t\t\t\tprojectEntries,\n\t\t\t\tresourceType,\n\t\t\t\ttarget,\n\t\t\t\t{\n\t\t\t\t\tsource: \"local\",\n\t\t\t\t\tscope: \"project\",\n\t\t\t\t\torigin: \"top-level\",\n\t\t\t\t},\n\t\t\t\tprojectBaseDir,\n\t\t\t);\n\t\t}\n\n\t\tthis.addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDir);\n\n\t\treturn this.toResolvedPaths(accumulator);\n\t}\n\n\tasync resolveExtensionSources(\n\t\tsources: string[],\n\t\toptions?: { local?: boolean; temporary?: boolean },\n\t): Promise<ResolvedPaths> {\n\t\tconst accumulator = this.createAccumulator();\n\t\tconst scope: SourceScope = options?.temporary ? \"temporary\" : options?.local ? \"project\" : \"user\";\n\t\tconst packageSources = sources.map((source) => ({ pkg: source as PackageSource, scope }));\n\t\tawait this.resolvePackageSources(packageSources, accumulator);\n\t\treturn this.toResolvedPaths(accumulator);\n\t}\n\n\tasync install(source: string, options?: { local?: boolean }): Promise<void> {\n\t\tconst parsed = this.parseSource(source);\n\t\tconst scope: SourceScope = options?.local ? \"project\" : \"user\";\n\t\tawait this.withProgress(\"install\", source, `Installing ${source}...`, async () => {\n\t\t\tif (parsed.type === \"npm\") {\n\t\t\t\tawait this.installNpm(parsed, scope, false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"git\") {\n\t\t\t\tawait this.installGit(parsed, scope);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"local\") {\n\t\t\t\tconst resolved = this.resolvePath(parsed.path);\n\t\t\t\tif (!existsSync(resolved)) {\n\t\t\t\t\tthrow new Error(`Path does not exist: ${resolved}`);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new Error(`Unsupported install source: ${source}`);\n\t\t});\n\t}\n\n\tasync remove(source: string, options?: { local?: boolean }): Promise<void> {\n\t\tconst parsed = this.parseSource(source);\n\t\tconst scope: SourceScope = options?.local ? \"project\" : \"user\";\n\t\tawait this.withProgress(\"remove\", source, `Removing ${source}...`, async () => {\n\t\t\tif (parsed.type === \"npm\") {\n\t\t\t\tawait this.uninstallNpm(parsed, scope);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"git\") {\n\t\t\t\tawait this.removeGit(parsed, scope);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsed.type === \"local\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new Error(`Unsupported remove source: ${source}`);\n\t\t});\n\t}\n\n\tasync update(source?: string): Promise<void> {\n\t\tconst globalSettings = this.settingsManager.getGlobalSettings();\n\t\tconst projectSettings = this.settingsManager.getProjectSettings();\n\t\tconst identity = source ? this.getPackageIdentity(source) : undefined;\n\n\t\tfor (const pkg of globalSettings.packages ?? []) {\n\t\t\tconst sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n\t\t\tif (identity && this.getPackageIdentity(sourceStr, \"user\") !== identity) continue;\n\t\t\tawait this.updateSourceForScope(sourceStr, \"user\");\n\t\t}\n\t\tfor (const pkg of projectSettings.packages ?? []) {\n\t\t\tconst sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n\t\t\tif (identity && this.getPackageIdentity(sourceStr, \"project\") !== identity) continue;\n\t\t\tawait this.updateSourceForScope(sourceStr, \"project\");\n\t\t}\n\t}\n\n\tprivate async updateSourceForScope(source: string, scope: SourceScope): Promise<void> {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\tif (parsed.pinned) return;\n\t\t\tawait this.withProgress(\"update\", source, `Updating ${source}...`, async () => {\n\t\t\t\tawait this.installNpm(parsed, scope, false);\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\tif (parsed.pinned) return;\n\t\t\tawait this.withProgress(\"update\", source, `Updating ${source}...`, async () => {\n\t\t\t\tawait this.updateGit(parsed, scope);\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate async resolvePackageSources(\n\t\tsources: Array<{ pkg: PackageSource; scope: SourceScope }>,\n\t\taccumulator: ResourceAccumulator,\n\t\tonMissing?: (source: string) => Promise<MissingSourceAction>,\n\t): Promise<void> {\n\t\tfor (const { pkg, scope } of sources) {\n\t\t\tconst sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n\t\t\tconst filter = typeof pkg === \"object\" ? pkg : undefined;\n\t\t\tconst parsed = this.parseSource(sourceStr);\n\t\t\tconst metadata: PathMetadata = { source: sourceStr, scope, origin: \"package\" };\n\n\t\t\tif (parsed.type === \"local\") {\n\t\t\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\t\t\tthis.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst installMissing = async (): Promise<boolean> => {\n\t\t\t\tif (!onMissing) {\n\t\t\t\t\tawait this.installParsedSource(parsed, scope);\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tconst action = await onMissing(sourceStr);\n\t\t\t\tif (action === \"skip\") return false;\n\t\t\t\tif (action === \"error\") throw new Error(`Missing source: ${sourceStr}`);\n\t\t\t\tawait this.installParsedSource(parsed, scope);\n\t\t\t\treturn true;\n\t\t\t};\n\n\t\t\tif (parsed.type === \"npm\") {\n\t\t\t\tconst installedPath = this.getNpmInstallPath(parsed, scope);\n\t\t\t\tconst needsInstall = !existsSync(installedPath) || (await this.npmNeedsUpdate(parsed, installedPath));\n\t\t\t\tif (needsInstall) {\n\t\t\t\t\tconst installed = await installMissing();\n\t\t\t\t\tif (!installed) continue;\n\t\t\t\t}\n\t\t\t\tmetadata.baseDir = installedPath;\n\t\t\t\tthis.collectPackageResources(installedPath, accumulator, filter, metadata);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (parsed.type === \"git\") {\n\t\t\t\tconst installedPath = this.getGitInstallPath(parsed, scope);\n\t\t\t\tif (!existsSync(installedPath)) {\n\t\t\t\t\tconst installed = await installMissing();\n\t\t\t\t\tif (!installed) continue;\n\t\t\t\t}\n\t\t\t\tmetadata.baseDir = installedPath;\n\t\t\t\tthis.collectPackageResources(installedPath, accumulator, filter, metadata);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate resolveLocalExtensionSource(\n\t\tsource: LocalSource,\n\t\taccumulator: ResourceAccumulator,\n\t\tfilter: PackageFilter | undefined,\n\t\tmetadata: PathMetadata,\n\t\tbaseDir: string,\n\t): void {\n\t\tconst resolved = this.resolvePathFromBase(source.path, baseDir);\n\t\tif (!existsSync(resolved)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(resolved);\n\t\t\tif (stats.isFile()) {\n\t\t\t\tmetadata.baseDir = dirname(resolved);\n\t\t\t\tthis.addResource(accumulator.extensions, resolved, metadata, true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (stats.isDirectory()) {\n\t\t\t\tmetadata.baseDir = resolved;\n\t\t\t\tconst resources = this.collectPackageResources(resolved, accumulator, filter, metadata);\n\t\t\t\tif (!resources) {\n\t\t\t\t\tthis.addResource(accumulator.extensions, resolved, metadata, true);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate async installParsedSource(parsed: ParsedSource, scope: SourceScope): Promise<void> {\n\t\tif (parsed.type === \"npm\") {\n\t\t\tawait this.installNpm(parsed, scope, scope === \"temporary\");\n\t\t\treturn;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\tawait this.installGit(parsed, scope);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate parseSource(source: string): ParsedSource {\n\t\tif (source.startsWith(\"npm:\")) {\n\t\t\tconst spec = source.slice(\"npm:\".length).trim();\n\t\t\tconst { name, version } = this.parseNpmSpec(spec);\n\t\t\treturn {\n\t\t\t\ttype: \"npm\",\n\t\t\t\tspec,\n\t\t\t\tname,\n\t\t\t\tpinned: Boolean(version),\n\t\t\t};\n\t\t}\n\n\t\tif (source.startsWith(\"git:\") || looksLikeGitUrl(source)) {\n\t\t\tconst repoSpec = source.startsWith(\"git:\") ? source.slice(\"git:\".length).trim() : source;\n\t\t\tconst [repo, ref] = repoSpec.split(\"@\");\n\t\t\tconst normalized = repo.replace(/^https?:\\/\\//, \"\").replace(/\\.git$/, \"\");\n\t\t\tconst parts = normalized.split(\"/\");\n\t\t\tconst host = parts.shift() ?? \"\";\n\t\t\tconst repoPath = parts.join(\"/\");\n\t\t\treturn {\n\t\t\t\ttype: \"git\",\n\t\t\t\trepo: normalized,\n\t\t\t\thost,\n\t\t\t\tpath: repoPath,\n\t\t\t\tref,\n\t\t\t\tpinned: Boolean(ref),\n\t\t\t};\n\t\t}\n\n\t\treturn { type: \"local\", path: source };\n\t}\n\n\t/**\n\t * Check if an npm package needs to be updated.\n\t * - For unpinned packages: check if registry has a newer version\n\t * - For pinned packages: check if installed version matches the pinned version\n\t */\n\tprivate async npmNeedsUpdate(source: NpmSource, installedPath: string): Promise<boolean> {\n\t\tconst installedVersion = this.getInstalledNpmVersion(installedPath);\n\t\tif (!installedVersion) return true;\n\n\t\tconst { version: pinnedVersion } = this.parseNpmSpec(source.spec);\n\t\tif (pinnedVersion) {\n\t\t\t// Pinned: check if installed matches pinned (exact match for now)\n\t\t\treturn installedVersion !== pinnedVersion;\n\t\t}\n\n\t\t// Unpinned: check registry for latest version\n\t\ttry {\n\t\t\tconst latestVersion = await this.getLatestNpmVersion(source.name);\n\t\t\treturn latestVersion !== installedVersion;\n\t\t} catch {\n\t\t\t// If we can't check registry, assume it's fine\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate getInstalledNpmVersion(installedPath: string): string | undefined {\n\t\tconst packageJsonPath = join(installedPath, \"package.json\");\n\t\tif (!existsSync(packageJsonPath)) return undefined;\n\t\ttry {\n\t\t\tconst content = readFileSync(packageJsonPath, \"utf-8\");\n\t\t\tconst pkg = JSON.parse(content) as { version?: string };\n\t\t\treturn pkg.version;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async getLatestNpmVersion(packageName: string): Promise<string> {\n\t\tconst response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);\n\t\tif (!response.ok) throw new Error(`Failed to fetch npm registry: ${response.status}`);\n\t\tconst data = (await response.json()) as { version: string };\n\t\treturn data.version;\n\t}\n\n\t/**\n\t * Get a unique identity for a package, ignoring version/ref.\n\t * Used to detect when the same package is in both global and project settings.\n\t */\n\tprivate getPackageIdentity(source: string, scope?: SourceScope): string {\n\t\tconst parsed = this.parseSource(source);\n\t\tif (parsed.type === \"npm\") {\n\t\t\treturn `npm:${parsed.name}`;\n\t\t}\n\t\tif (parsed.type === \"git\") {\n\t\t\treturn `git:${parsed.repo}`;\n\t\t}\n\t\tif (scope) {\n\t\t\tconst baseDir = this.getBaseDirForScope(scope);\n\t\t\treturn `local:${this.resolvePathFromBase(parsed.path, baseDir)}`;\n\t\t}\n\t\treturn `local:${this.resolvePath(parsed.path)}`;\n\t}\n\n\t/**\n\t * Dedupe packages: if same package identity appears in both global and project,\n\t * keep only the project one (project wins).\n\t */\n\tprivate dedupePackages(\n\t\tpackages: Array<{ pkg: PackageSource; scope: SourceScope }>,\n\t): Array<{ pkg: PackageSource; scope: SourceScope }> {\n\t\tconst seen = new Map<string, { pkg: PackageSource; scope: SourceScope }>();\n\n\t\tfor (const entry of packages) {\n\t\t\tconst sourceStr = typeof entry.pkg === \"string\" ? entry.pkg : entry.pkg.source;\n\t\t\tconst identity = this.getPackageIdentity(sourceStr, entry.scope);\n\n\t\t\tconst existing = seen.get(identity);\n\t\t\tif (!existing) {\n\t\t\t\tseen.set(identity, entry);\n\t\t\t} else if (entry.scope === \"project\" && existing.scope === \"user\") {\n\t\t\t\t// Project wins over user\n\t\t\t\tseen.set(identity, entry);\n\t\t\t}\n\t\t\t// If existing is project and new is global, keep existing (project)\n\t\t\t// If both are same scope, keep first one\n\t\t}\n\n\t\treturn Array.from(seen.values());\n\t}\n\n\tprivate parseNpmSpec(spec: string): { name: string; version?: string } {\n\t\tconst match = spec.match(/^(@?[^@]+(?:\\/[^@]+)?)(?:@(.+))?$/);\n\t\tif (!match) {\n\t\t\treturn { name: spec };\n\t\t}\n\t\tconst name = match[1] ?? spec;\n\t\tconst version = match[2];\n\t\treturn { name, version };\n\t}\n\n\tprivate async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> {\n\t\tif (scope === \"user\" && !temporary) {\n\t\t\tawait this.runCommand(\"npm\", [\"install\", \"-g\", source.spec]);\n\t\t\treturn;\n\t\t}\n\t\tconst installRoot = this.getNpmInstallRoot(scope, temporary);\n\t\tthis.ensureNpmProject(installRoot);\n\t\tawait this.runCommand(\"npm\", [\"install\", source.spec, \"--prefix\", installRoot]);\n\t}\n\n\tprivate async uninstallNpm(source: NpmSource, scope: SourceScope): Promise<void> {\n\t\tif (scope === \"user\") {\n\t\t\tawait this.runCommand(\"npm\", [\"uninstall\", \"-g\", source.name]);\n\t\t\treturn;\n\t\t}\n\t\tconst installRoot = this.getNpmInstallRoot(scope, false);\n\t\tif (!existsSync(installRoot)) {\n\t\t\treturn;\n\t\t}\n\t\tawait this.runCommand(\"npm\", [\"uninstall\", source.name, \"--prefix\", installRoot]);\n\t}\n\n\tprivate async installGit(source: GitSource, scope: SourceScope): Promise<void> {\n\t\tconst targetDir = this.getGitInstallPath(source, scope);\n\t\tif (existsSync(targetDir)) {\n\t\t\treturn;\n\t\t}\n\t\tconst gitRoot = this.getGitInstallRoot(scope);\n\t\tif (gitRoot) {\n\t\t\tthis.ensureGitIgnore(gitRoot);\n\t\t}\n\t\tmkdirSync(dirname(targetDir), { recursive: true });\n\t\tconst cloneUrl = source.repo.startsWith(\"http\") ? source.repo : `https://${source.repo}`;\n\t\tawait this.runCommand(\"git\", [\"clone\", cloneUrl, targetDir]);\n\t\tif (source.ref) {\n\t\t\tawait this.runCommand(\"git\", [\"checkout\", source.ref], { cwd: targetDir });\n\t\t}\n\t\tconst packageJsonPath = join(targetDir, \"package.json\");\n\t\tif (existsSync(packageJsonPath)) {\n\t\t\tawait this.runCommand(\"npm\", [\"install\"], { cwd: targetDir });\n\t\t}\n\t}\n\n\tprivate async updateGit(source: GitSource, scope: SourceScope): Promise<void> {\n\t\tconst targetDir = this.getGitInstallPath(source, scope);\n\t\tif (!existsSync(targetDir)) {\n\t\t\tawait this.installGit(source, scope);\n\t\t\treturn;\n\t\t}\n\n\t\t// Fetch latest from remote (handles force-push by getting new history)\n\t\tawait this.runCommand(\"git\", [\"fetch\", \"--prune\", \"origin\"], { cwd: targetDir });\n\n\t\t// Reset to upstream tracking branch (handles force-push gracefully)\n\t\tawait this.runCommand(\"git\", [\"reset\", \"--hard\", \"@{upstream}\"], { cwd: targetDir });\n\n\t\t// Clean untracked files (extensions should be pristine)\n\t\tawait this.runCommand(\"git\", [\"clean\", \"-fdx\"], { cwd: targetDir });\n\n\t\tconst packageJsonPath = join(targetDir, \"package.json\");\n\t\tif (existsSync(packageJsonPath)) {\n\t\t\tawait this.runCommand(\"npm\", [\"install\"], { cwd: targetDir });\n\t\t}\n\t}\n\n\tprivate async removeGit(source: GitSource, scope: SourceScope): Promise<void> {\n\t\tconst targetDir = this.getGitInstallPath(source, scope);\n\t\tif (!existsSync(targetDir)) return;\n\t\trmSync(targetDir, { recursive: true, force: true });\n\t\tthis.pruneEmptyGitParents(targetDir, this.getGitInstallRoot(scope));\n\t}\n\n\tprivate pruneEmptyGitParents(targetDir: string, installRoot: string | undefined): void {\n\t\tif (!installRoot) return;\n\t\tconst resolvedRoot = resolve(installRoot);\n\t\tlet current = dirname(targetDir);\n\t\twhile (current.startsWith(resolvedRoot) && current !== resolvedRoot) {\n\t\t\tif (!existsSync(current)) {\n\t\t\t\tcurrent = dirname(current);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst entries = readdirSync(current);\n\t\t\tif (entries.length > 0) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\trmSync(current, { recursive: true, force: true });\n\t\t\t} catch {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcurrent = dirname(current);\n\t\t}\n\t}\n\n\tprivate ensureNpmProject(installRoot: string): void {\n\t\tif (!existsSync(installRoot)) {\n\t\t\tmkdirSync(installRoot, { recursive: true });\n\t\t}\n\t\tthis.ensureGitIgnore(installRoot);\n\t\tconst packageJsonPath = join(installRoot, \"package.json\");\n\t\tif (!existsSync(packageJsonPath)) {\n\t\t\tconst pkgJson = { name: \"pi-extensions\", private: true };\n\t\t\twriteFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2), \"utf-8\");\n\t\t}\n\t}\n\n\tprivate ensureGitIgnore(dir: string): void {\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\tconst ignorePath = join(dir, \".gitignore\");\n\t\tif (!existsSync(ignorePath)) {\n\t\t\twriteFileSync(ignorePath, \"*\\n!.gitignore\\n\", \"utf-8\");\n\t\t}\n\t}\n\n\tprivate getNpmInstallRoot(scope: SourceScope, temporary: boolean): string {\n\t\tif (temporary) {\n\t\t\treturn this.getTemporaryDir(\"npm\");\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"npm\");\n\t\t}\n\t\treturn join(this.getGlobalNpmRoot(), \"..\");\n\t}\n\n\tprivate getGlobalNpmRoot(): string {\n\t\tif (this.globalNpmRoot) {\n\t\t\treturn this.globalNpmRoot;\n\t\t}\n\t\tconst result = this.runCommandSync(\"npm\", [\"root\", \"-g\"]);\n\t\tthis.globalNpmRoot = result.trim();\n\t\treturn this.globalNpmRoot;\n\t}\n\n\tprivate getNpmInstallPath(source: NpmSource, scope: SourceScope): string {\n\t\tif (scope === \"temporary\") {\n\t\t\treturn join(this.getTemporaryDir(\"npm\"), \"node_modules\", source.name);\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"npm\", \"node_modules\", source.name);\n\t\t}\n\t\treturn join(this.getGlobalNpmRoot(), source.name);\n\t}\n\n\tprivate getGitInstallPath(source: GitSource, scope: SourceScope): string {\n\t\tif (scope === \"temporary\") {\n\t\t\treturn this.getTemporaryDir(`git-${source.host}`, source.path);\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"git\", source.host, source.path);\n\t\t}\n\t\treturn join(this.agentDir, \"git\", source.host, source.path);\n\t}\n\n\tprivate getGitInstallRoot(scope: SourceScope): string | undefined {\n\t\tif (scope === \"temporary\") {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"git\");\n\t\t}\n\t\treturn join(this.agentDir, \"git\");\n\t}\n\n\tprivate getTemporaryDir(prefix: string, suffix?: string): string {\n\t\tconst hash = createHash(\"sha256\")\n\t\t\t.update(`${prefix}-${suffix ?? \"\"}`)\n\t\t\t.digest(\"hex\")\n\t\t\t.slice(0, 8);\n\t\treturn join(tmpdir(), \"pi-extensions\", prefix, hash, suffix ?? \"\");\n\t}\n\n\tprivate getBaseDirForScope(scope: SourceScope): string {\n\t\tif (scope === \"project\") {\n\t\t\treturn join(this.cwd, CONFIG_DIR_NAME);\n\t\t}\n\t\tif (scope === \"user\") {\n\t\t\treturn this.agentDir;\n\t\t}\n\t\treturn this.cwd;\n\t}\n\n\tprivate resolvePath(input: string): string {\n\t\tconst trimmed = input.trim();\n\t\tif (trimmed === \"~\") return homedir();\n\t\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\t\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\t\treturn resolve(this.cwd, trimmed);\n\t}\n\n\tprivate resolvePathFromBase(input: string, baseDir: string): string {\n\t\tconst trimmed = input.trim();\n\t\tif (trimmed === \"~\") return homedir();\n\t\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\t\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\t\treturn resolve(baseDir, trimmed);\n\t}\n\n\tprivate collectPackageResources(\n\t\tpackageRoot: string,\n\t\taccumulator: ResourceAccumulator,\n\t\tfilter: PackageFilter | undefined,\n\t\tmetadata: PathMetadata,\n\t): boolean {\n\t\tif (filter) {\n\t\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\t\tconst patterns = filter[resourceType as keyof PackageFilter];\n\t\t\t\tconst target = this.getTargetMap(accumulator, resourceType);\n\t\t\t\tif (patterns !== undefined) {\n\t\t\t\t\tthis.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata);\n\t\t\t\t} else {\n\t\t\t\t\tthis.collectDefaultResources(packageRoot, resourceType, target, metadata);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tconst manifest = this.readPiManifest(packageRoot);\n\t\tif (manifest) {\n\t\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\t\tconst entries = manifest[resourceType as keyof PiManifest];\n\t\t\t\tthis.addManifestEntries(\n\t\t\t\t\tentries,\n\t\t\t\t\tpackageRoot,\n\t\t\t\t\tresourceType,\n\t\t\t\t\tthis.getTargetMap(accumulator, resourceType),\n\t\t\t\t\tmetadata,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tlet hasAnyDir = false;\n\t\tfor (const resourceType of RESOURCE_TYPES) {\n\t\t\tconst dir = join(packageRoot, resourceType);\n\t\t\tif (existsSync(dir)) {\n\t\t\t\t// Collect all files from the directory (all enabled by default)\n\t\t\t\tconst files = collectResourceFiles(dir, resourceType);\n\t\t\t\tfor (const f of files) {\n\t\t\t\t\tthis.addResource(this.getTargetMap(accumulator, resourceType), f, metadata, true);\n\t\t\t\t}\n\t\t\t\thasAnyDir = true;\n\t\t\t}\n\t\t}\n\t\treturn hasAnyDir;\n\t}\n\n\tprivate collectDefaultResources(\n\t\tpackageRoot: string,\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t): void {\n\t\tconst manifest = this.readPiManifest(packageRoot);\n\t\tconst entries = manifest?.[resourceType as keyof PiManifest];\n\t\tif (entries) {\n\t\t\tthis.addManifestEntries(entries, packageRoot, resourceType, target, metadata);\n\t\t\treturn;\n\t\t}\n\t\tconst dir = join(packageRoot, resourceType);\n\t\tif (existsSync(dir)) {\n\t\t\t// Collect all files from the directory (all enabled by default)\n\t\t\tconst files = collectResourceFiles(dir, resourceType);\n\t\t\tfor (const f of files) {\n\t\t\t\tthis.addResource(target, f, metadata, true);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate applyPackageFilter(\n\t\tpackageRoot: string,\n\t\tuserPatterns: string[],\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t): void {\n\t\tconst { allFiles } = this.collectManifestFiles(packageRoot, resourceType);\n\n\t\tif (userPatterns.length === 0) {\n\t\t\t// Empty array explicitly disables all resources of this type\n\t\t\tfor (const f of allFiles) {\n\t\t\t\tthis.addResource(target, f, metadata, false);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Apply user patterns\n\t\tconst enabledByUser = applyPatterns(allFiles, userPatterns, packageRoot);\n\n\t\tfor (const f of allFiles) {\n\t\t\tconst enabled = enabledByUser.has(f);\n\t\t\tthis.addResource(target, f, metadata, enabled);\n\t\t}\n\t}\n\n\t/**\n\t * Collect all files from a package for a resource type, applying manifest patterns.\n\t * Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files\n\t * that pass the manifest's own patterns.\n\t */\n\tprivate collectManifestFiles(\n\t\tpackageRoot: string,\n\t\tresourceType: ResourceType,\n\t): { allFiles: string[]; enabledByManifest: Set<string> } {\n\t\tconst manifest = this.readPiManifest(packageRoot);\n\t\tconst entries = manifest?.[resourceType as keyof PiManifest];\n\t\tif (entries && entries.length > 0) {\n\t\t\tconst allFiles = this.collectFilesFromManifestEntries(entries, packageRoot, resourceType);\n\t\t\tconst manifestPatterns = entries.filter(isPattern);\n\t\t\tconst enabledByManifest =\n\t\t\t\tmanifestPatterns.length > 0 ? applyPatterns(allFiles, manifestPatterns, packageRoot) : new Set(allFiles);\n\t\t\treturn { allFiles: Array.from(enabledByManifest), enabledByManifest };\n\t\t}\n\n\t\tconst conventionDir = join(packageRoot, resourceType);\n\t\tif (!existsSync(conventionDir)) {\n\t\t\treturn { allFiles: [], enabledByManifest: new Set() };\n\t\t}\n\t\tconst allFiles = collectResourceFiles(conventionDir, resourceType);\n\t\treturn { allFiles, enabledByManifest: new Set(allFiles) };\n\t}\n\n\tprivate readPiManifest(packageRoot: string): PiManifest | null {\n\t\tconst packageJsonPath = join(packageRoot, \"package.json\");\n\t\tif (!existsSync(packageJsonPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(packageJsonPath, \"utf-8\");\n\t\t\tconst pkg = JSON.parse(content) as { pi?: PiManifest };\n\t\t\treturn pkg.pi ?? null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate addManifestEntries(\n\t\tentries: string[] | undefined,\n\t\troot: string,\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t): void {\n\t\tif (!entries) return;\n\n\t\tconst allFiles = this.collectFilesFromManifestEntries(entries, root, resourceType);\n\t\tconst patterns = entries.filter(isPattern);\n\t\tconst enabledPaths = applyPatterns(allFiles, patterns, root);\n\n\t\tfor (const f of allFiles) {\n\t\t\tif (enabledPaths.has(f)) {\n\t\t\t\tthis.addResource(target, f, metadata, true);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate collectFilesFromManifestEntries(entries: string[], root: string, resourceType: ResourceType): string[] {\n\t\tconst plain = entries.filter((entry) => !isPattern(entry));\n\t\tconst resolved = plain.map((entry) => resolve(root, entry));\n\t\treturn this.collectFilesFromPaths(resolved, resourceType);\n\t}\n\n\tprivate resolveLocalEntries(\n\t\tentries: string[],\n\t\tresourceType: ResourceType,\n\t\ttarget: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tmetadata: PathMetadata,\n\t\tbaseDir: string,\n\t): void {\n\t\tif (entries.length === 0) return;\n\n\t\t// Collect all files from plain entries (non-pattern entries)\n\t\tconst { plain, patterns } = splitPatterns(entries);\n\t\tconst resolvedPlain = plain.map((p) => this.resolvePathFromBase(p, baseDir));\n\t\tconst allFiles = this.collectFilesFromPaths(resolvedPlain, resourceType);\n\n\t\t// Determine which files are enabled based on patterns\n\t\tconst enabledPaths = applyPatterns(allFiles, patterns, baseDir);\n\n\t\t// Add all files with their enabled state\n\t\tfor (const f of allFiles) {\n\t\t\tthis.addResource(target, f, metadata, enabledPaths.has(f));\n\t\t}\n\t}\n\n\tprivate addAutoDiscoveredResources(\n\t\taccumulator: ResourceAccumulator,\n\t\tglobalSettings: ReturnType<SettingsManager[\"getGlobalSettings\"]>,\n\t\tprojectSettings: ReturnType<SettingsManager[\"getProjectSettings\"]>,\n\t\tglobalBaseDir: string,\n\t\tprojectBaseDir: string,\n\t): void {\n\t\tconst userMetadata: PathMetadata = {\n\t\t\tsource: \"auto\",\n\t\t\tscope: \"user\",\n\t\t\torigin: \"top-level\",\n\t\t\tbaseDir: globalBaseDir,\n\t\t};\n\t\tconst projectMetadata: PathMetadata = {\n\t\t\tsource: \"auto\",\n\t\t\tscope: \"project\",\n\t\t\torigin: \"top-level\",\n\t\t\tbaseDir: projectBaseDir,\n\t\t};\n\n\t\tconst userOverrides = {\n\t\t\textensions: (globalSettings.extensions ?? []) as string[],\n\t\t\tskills: (globalSettings.skills ?? []) as string[],\n\t\t\tprompts: (globalSettings.prompts ?? []) as string[],\n\t\t\tthemes: (globalSettings.themes ?? []) as string[],\n\t\t};\n\t\tconst projectOverrides = {\n\t\t\textensions: (projectSettings.extensions ?? []) as string[],\n\t\t\tskills: (projectSettings.skills ?? []) as string[],\n\t\t\tprompts: (projectSettings.prompts ?? []) as string[],\n\t\t\tthemes: (projectSettings.themes ?? []) as string[],\n\t\t};\n\n\t\tconst userDirs = {\n\t\t\textensions: join(globalBaseDir, \"extensions\"),\n\t\t\tskills: join(globalBaseDir, \"skills\"),\n\t\t\tprompts: join(globalBaseDir, \"prompts\"),\n\t\t\tthemes: join(globalBaseDir, \"themes\"),\n\t\t};\n\t\tconst projectDirs = {\n\t\t\textensions: join(projectBaseDir, \"extensions\"),\n\t\t\tskills: join(projectBaseDir, \"skills\"),\n\t\t\tprompts: join(projectBaseDir, \"prompts\"),\n\t\t\tthemes: join(projectBaseDir, \"themes\"),\n\t\t};\n\n\t\tconst addResources = (\n\t\t\tresourceType: ResourceType,\n\t\t\tpaths: string[],\n\t\t\tmetadata: PathMetadata,\n\t\t\toverrides: string[],\n\t\t\tbaseDir: string,\n\t\t) => {\n\t\t\tconst target = this.getTargetMap(accumulator, resourceType);\n\t\t\tfor (const path of paths) {\n\t\t\t\tconst enabled = isEnabledByOverrides(path, overrides, baseDir);\n\t\t\t\tthis.addResource(target, path, metadata, enabled);\n\t\t\t}\n\t\t};\n\n\t\taddResources(\n\t\t\t\"extensions\",\n\t\t\tcollectAutoExtensionEntries(userDirs.extensions),\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.extensions,\n\t\t\tglobalBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"skills\",\n\t\t\tcollectAutoSkillEntries(userDirs.skills),\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.skills,\n\t\t\tglobalBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"prompts\",\n\t\t\tcollectAutoPromptEntries(userDirs.prompts),\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.prompts,\n\t\t\tglobalBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"themes\",\n\t\t\tcollectAutoThemeEntries(userDirs.themes),\n\t\t\tuserMetadata,\n\t\t\tuserOverrides.themes,\n\t\t\tglobalBaseDir,\n\t\t);\n\n\t\taddResources(\n\t\t\t\"extensions\",\n\t\t\tcollectAutoExtensionEntries(projectDirs.extensions),\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.extensions,\n\t\t\tprojectBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"skills\",\n\t\t\tcollectAutoSkillEntries(projectDirs.skills),\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.skills,\n\t\t\tprojectBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"prompts\",\n\t\t\tcollectAutoPromptEntries(projectDirs.prompts),\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.prompts,\n\t\t\tprojectBaseDir,\n\t\t);\n\t\taddResources(\n\t\t\t\"themes\",\n\t\t\tcollectAutoThemeEntries(projectDirs.themes),\n\t\t\tprojectMetadata,\n\t\t\tprojectOverrides.themes,\n\t\t\tprojectBaseDir,\n\t\t);\n\t}\n\n\tprivate collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] {\n\t\tconst files: string[] = [];\n\t\tfor (const p of paths) {\n\t\t\tif (!existsSync(p)) continue;\n\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(p);\n\t\t\t\tif (stats.isFile()) {\n\t\t\t\t\tfiles.push(p);\n\t\t\t\t} else if (stats.isDirectory()) {\n\t\t\t\t\tfiles.push(...collectResourceFiles(p, resourceType));\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore errors\n\t\t\t}\n\t\t}\n\t\treturn files;\n\t}\n\n\tprivate getTargetMap(\n\t\taccumulator: ResourceAccumulator,\n\t\tresourceType: ResourceType,\n\t): Map<string, { metadata: PathMetadata; enabled: boolean }> {\n\t\tswitch (resourceType) {\n\t\t\tcase \"extensions\":\n\t\t\t\treturn accumulator.extensions;\n\t\t\tcase \"skills\":\n\t\t\t\treturn accumulator.skills;\n\t\t\tcase \"prompts\":\n\t\t\t\treturn accumulator.prompts;\n\t\t\tcase \"themes\":\n\t\t\t\treturn accumulator.themes;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown resource type: ${resourceType}`);\n\t\t}\n\t}\n\n\tprivate addResource(\n\t\tmap: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n\t\tpath: string,\n\t\tmetadata: PathMetadata,\n\t\tenabled: boolean,\n\t): void {\n\t\tif (!path) return;\n\t\tif (!map.has(path)) {\n\t\t\tmap.set(path, { metadata, enabled });\n\t\t}\n\t}\n\n\tprivate createAccumulator(): ResourceAccumulator {\n\t\treturn {\n\t\t\textensions: new Map(),\n\t\t\tskills: new Map(),\n\t\t\tprompts: new Map(),\n\t\t\tthemes: new Map(),\n\t\t};\n\t}\n\n\tprivate toResolvedPaths(accumulator: ResourceAccumulator): ResolvedPaths {\n\t\tconst toResolved = (entries: Map<string, { metadata: PathMetadata; enabled: boolean }>): ResolvedResource[] => {\n\t\t\treturn Array.from(entries.entries()).map(([path, { metadata, enabled }]) => ({\n\t\t\t\tpath,\n\t\t\t\tenabled,\n\t\t\t\tmetadata,\n\t\t\t}));\n\t\t};\n\n\t\treturn {\n\t\t\textensions: toResolved(accumulator.extensions),\n\t\t\tskills: toResolved(accumulator.skills),\n\t\t\tprompts: toResolved(accumulator.prompts),\n\t\t\tthemes: toResolved(accumulator.themes),\n\t\t};\n\t}\n\n\tprivate runCommand(command: string, args: string[], options?: { cwd?: string }): Promise<void> {\n\t\treturn new Promise((resolvePromise, reject) => {\n\t\t\tconst child = spawn(command, args, {\n\t\t\t\tcwd: options?.cwd,\n\t\t\t\tstdio: \"inherit\",\n\t\t\t\tshell: process.platform === \"win32\",\n\t\t\t});\n\t\t\tchild.on(\"error\", reject);\n\t\t\tchild.on(\"exit\", (code) => {\n\t\t\t\tif (code === 0) {\n\t\t\t\t\tresolvePromise();\n\t\t\t\t} else {\n\t\t\t\t\treject(new Error(`${command} ${args.join(\" \")} failed with code ${code}`));\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate runCommandSync(command: string, args: string[]): string {\n\t\tconst result = spawnSync(command, args, {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tencoding: \"utf-8\",\n\t\t\tshell: process.platform === \"win32\",\n\t\t});\n\t\tif (result.status !== 0) {\n\t\t\tthrow new Error(`Failed to run ${command} ${args.join(\" \")}: ${result.stderr || result.stdout}`);\n\t\t}\n\t\treturn (result.stdout || result.stderr || \"\").trim();\n\t}\n}\n"]}
1
+ {"version":3,"file":"package-manager.d.ts","sourceRoot":"","sources":["../../src/core/package-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport ignore from \"ignore\";\nimport { minimatch } from \"minimatch\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\nimport { looksLikeGitUrl } from \"../utils/git.js\";\nimport type { PackageSource, SettingsManager } from \"./settings-manager.js\";\n\nexport interface PathMetadata {\n source: string;\n scope: SourceScope;\n origin: \"package\" | \"top-level\";\n baseDir?: string;\n}\n\nexport interface ResolvedResource {\n path: string;\n enabled: boolean;\n metadata: PathMetadata;\n}\n\nexport interface ResolvedPaths {\n extensions: ResolvedResource[];\n skills: ResolvedResource[];\n commands: ResolvedResource[];\n themes: ResolvedResource[];\n}\n\nexport type MissingSourceAction = \"install\" | \"skip\" | \"error\";\n\nexport interface ProgressEvent {\n type: \"start\" | \"progress\" | \"complete\" | \"error\";\n action: \"install\" | \"remove\" | \"update\" | \"clone\" | \"pull\";\n source: string;\n message?: string;\n}\n\nexport type ProgressCallback = (event: ProgressEvent) => void;\n\nexport interface PackageManager {\n resolve(onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths>;\n install(source: string, options?: { local?: boolean }): Promise<void>;\n remove(source: string, options?: { local?: boolean }): Promise<void>;\n update(source?: string): Promise<void>;\n resolveExtensionSources(\n sources: string[],\n options?: { local?: boolean; temporary?: boolean },\n ): Promise<ResolvedPaths>;\n setProgressCallback(callback: ProgressCallback | undefined): void;\n getInstalledPath(source: string, scope: \"user\" | \"project\"): string | undefined;\n}\n\ninterface PackageManagerOptions {\n cwd: string;\n agentDir: string;\n settingsManager: SettingsManager;\n}\n\ntype SourceScope = \"user\" | \"project\" | \"temporary\";\n\ntype NpmSource = {\n type: \"npm\";\n spec: string;\n name: string;\n pinned: boolean;\n};\n\ntype GitSource = {\n type: \"git\";\n repo: string;\n host: string;\n path: string;\n ref?: string;\n pinned: boolean;\n};\n\ntype LocalSource = {\n type: \"local\";\n path: string;\n};\n\ntype ParsedSource = NpmSource | GitSource | LocalSource;\n\ninterface PiManifest {\n extensions?: string[];\n skills?: string[];\n prompts?: string[];\n themes?: string[];\n}\n\ninterface ResourceAccumulator {\n extensions: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n skills: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n commands: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n themes: Map<string, { metadata: PathMetadata; enabled: boolean }>;\n}\n\ninterface PackageFilter {\n extensions?: string[];\n skills?: string[];\n prompts?: string[];\n themes?: string[];\n}\n\ntype ResourceType = \"extensions\" | \"skills\" | \"commands\" | \"themes\";\n\nconst RESOURCE_TYPES: ResourceType[] = [\"extensions\", \"skills\", \"commands\", \"themes\"];\n\nconst FILE_PATTERNS: Record<ResourceType, RegExp> = {\n extensions: /\\.(ts|js)$/,\n skills: /\\.md$/,\n commands: /\\.md$/,\n themes: /\\.json$/,\n};\n\nconst IGNORE_FILE_NAMES = [\".gitignore\", \".ignore\", \".fdignore\"];\n\ntype IgnoreMatcher = ReturnType<typeof ignore>;\n\nfunction toPosixPath(p: string): string {\n return p.split(sep).join(\"/\");\n}\n\nfunction prefixIgnorePattern(line: string, prefix: string): string | null {\n const trimmed = line.trim();\n if (!trimmed) return null;\n if (trimmed.startsWith(\"#\") && !trimmed.startsWith(\"\\\\#\")) return null;\n\n let pattern = line;\n let negated = false;\n\n if (pattern.startsWith(\"!\")) {\n negated = true;\n pattern = pattern.slice(1);\n } else if (pattern.startsWith(\"\\\\!\")) {\n pattern = pattern.slice(1);\n }\n\n if (pattern.startsWith(\"/\")) {\n pattern = pattern.slice(1);\n }\n\n const prefixed = prefix ? `${prefix}${pattern}` : pattern;\n return negated ? `!${prefixed}` : prefixed;\n}\n\nfunction addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void {\n const relativeDir = relative(rootDir, dir);\n const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : \"\";\n\n for (const filename of IGNORE_FILE_NAMES) {\n const ignorePath = join(dir, filename);\n if (!existsSync(ignorePath)) continue;\n try {\n const content = readFileSync(ignorePath, \"utf-8\");\n const patterns = content\n .split(/\\r?\\n/)\n .map((line) => prefixIgnorePattern(line, prefix))\n .filter((line): line is string => Boolean(line));\n if (patterns.length > 0) {\n ig.add(patterns);\n }\n } catch {}\n }\n}\n\nfunction isPattern(s: string): boolean {\n return s.startsWith(\"!\") || s.startsWith(\"+\") || s.startsWith(\"-\") || s.includes(\"*\") || s.includes(\"?\");\n}\n\nfunction splitPatterns(entries: string[]): { plain: string[]; patterns: string[] } {\n const plain: string[] = [];\n const patterns: string[] = [];\n for (const entry of entries) {\n if (isPattern(entry)) {\n patterns.push(entry);\n } else {\n plain.push(entry);\n }\n }\n return { plain, patterns };\n}\n\nfunction collectFiles(\n dir: string,\n filePattern: RegExp,\n skipNodeModules = true,\n ignoreMatcher?: IgnoreMatcher,\n rootDir?: string,\n): string[] {\n const files: string[] = [];\n if (!existsSync(dir)) return files;\n\n const root = rootDir ?? dir;\n const ig = ignoreMatcher ?? ignore();\n addIgnoreRules(ig, dir, root);\n\n try {\n const entries = readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\")) continue;\n if (skipNodeModules && entry.name === \"node_modules\") continue;\n\n const fullPath = join(dir, entry.name);\n let isDir = entry.isDirectory();\n let isFile = entry.isFile();\n\n if (entry.isSymbolicLink()) {\n try {\n const stats = statSync(fullPath);\n isDir = stats.isDirectory();\n isFile = stats.isFile();\n } catch {\n continue;\n }\n }\n\n const relPath = toPosixPath(relative(root, fullPath));\n const ignorePath = isDir ? `${relPath}/` : relPath;\n if (ig.ignores(ignorePath)) continue;\n\n if (isDir) {\n files.push(...collectFiles(fullPath, filePattern, skipNodeModules, ig, root));\n } else if (isFile && filePattern.test(entry.name)) {\n files.push(fullPath);\n }\n }\n } catch {\n // Ignore errors\n }\n\n return files;\n}\n\nfunction collectSkillEntries(\n dir: string,\n includeRootFiles = true,\n ignoreMatcher?: IgnoreMatcher,\n rootDir?: string,\n): string[] {\n const entries: string[] = [];\n if (!existsSync(dir)) return entries;\n\n const root = rootDir ?? dir;\n const ig = ignoreMatcher ?? ignore();\n addIgnoreRules(ig, dir, root);\n\n try {\n const dirEntries = readdirSync(dir, { withFileTypes: true });\n for (const entry of dirEntries) {\n if (entry.name.startsWith(\".\")) continue;\n if (entry.name === \"node_modules\") continue;\n\n const fullPath = join(dir, entry.name);\n let isDir = entry.isDirectory();\n let isFile = entry.isFile();\n\n if (entry.isSymbolicLink()) {\n try {\n const stats = statSync(fullPath);\n isDir = stats.isDirectory();\n isFile = stats.isFile();\n } catch {\n continue;\n }\n }\n\n const relPath = toPosixPath(relative(root, fullPath));\n const ignorePath = isDir ? `${relPath}/` : relPath;\n if (ig.ignores(ignorePath)) continue;\n\n if (isDir) {\n entries.push(...collectSkillEntries(fullPath, false, ig, root));\n } else if (isFile) {\n const isRootMd = includeRootFiles && entry.name.endsWith(\".md\");\n const isSkillMd = !includeRootFiles && entry.name === \"SKILL.md\";\n if (isRootMd || isSkillMd) {\n entries.push(fullPath);\n }\n }\n }\n } catch {\n // Ignore errors\n }\n\n return entries;\n}\n\nfunction collectAutoSkillEntries(dir: string, includeRootFiles = true): string[] {\n return collectSkillEntries(dir, includeRootFiles);\n}\n\nfunction collectAutoPromptEntries(dir: string): string[] {\n const entries: string[] = [];\n if (!existsSync(dir)) return entries;\n\n const ig = ignore();\n addIgnoreRules(ig, dir, dir);\n\n try {\n const dirEntries = readdirSync(dir, { withFileTypes: true });\n for (const entry of dirEntries) {\n if (entry.name.startsWith(\".\")) continue;\n if (entry.name === \"node_modules\") continue;\n\n const fullPath = join(dir, entry.name);\n let isFile = entry.isFile();\n if (entry.isSymbolicLink()) {\n try {\n isFile = statSync(fullPath).isFile();\n } catch {\n continue;\n }\n }\n\n const relPath = toPosixPath(relative(dir, fullPath));\n if (ig.ignores(relPath)) continue;\n\n if (isFile && entry.name.endsWith(\".md\")) {\n entries.push(fullPath);\n }\n }\n } catch {\n // Ignore errors\n }\n\n return entries;\n}\n\nfunction collectAutoThemeEntries(dir: string): string[] {\n const entries: string[] = [];\n if (!existsSync(dir)) return entries;\n\n const ig = ignore();\n addIgnoreRules(ig, dir, dir);\n\n try {\n const dirEntries = readdirSync(dir, { withFileTypes: true });\n for (const entry of dirEntries) {\n if (entry.name.startsWith(\".\")) continue;\n if (entry.name === \"node_modules\") continue;\n\n const fullPath = join(dir, entry.name);\n let isFile = entry.isFile();\n if (entry.isSymbolicLink()) {\n try {\n isFile = statSync(fullPath).isFile();\n } catch {\n continue;\n }\n }\n\n const relPath = toPosixPath(relative(dir, fullPath));\n if (ig.ignores(relPath)) continue;\n\n if (isFile && entry.name.endsWith(\".json\")) {\n entries.push(fullPath);\n }\n }\n } catch {\n // Ignore errors\n }\n\n return entries;\n}\n\nfunction readPiManifestFile(packageJsonPath: string): PiManifest | null {\n try {\n const content = readFileSync(packageJsonPath, \"utf-8\");\n const pkg = JSON.parse(content) as { pi?: PiManifest };\n return pkg.pi ?? null;\n } catch {\n return null;\n }\n}\n\nfunction resolveExtensionEntries(dir: string): string[] | null {\n const packageJsonPath = join(dir, \"package.json\");\n if (existsSync(packageJsonPath)) {\n const manifest = readPiManifestFile(packageJsonPath);\n if (manifest?.extensions?.length) {\n const entries: string[] = [];\n for (const extPath of manifest.extensions) {\n const resolvedExtPath = resolve(dir, extPath);\n if (existsSync(resolvedExtPath)) {\n entries.push(resolvedExtPath);\n }\n }\n if (entries.length > 0) {\n return entries;\n }\n }\n }\n\n const indexTs = join(dir, \"index.ts\");\n const indexJs = join(dir, \"index.js\");\n if (existsSync(indexTs)) {\n return [indexTs];\n }\n if (existsSync(indexJs)) {\n return [indexJs];\n }\n\n return null;\n}\n\nfunction collectAutoExtensionEntries(dir: string): string[] {\n const entries: string[] = [];\n if (!existsSync(dir)) return entries;\n\n const ig = ignore();\n addIgnoreRules(ig, dir, dir);\n\n try {\n const dirEntries = readdirSync(dir, { withFileTypes: true });\n for (const entry of dirEntries) {\n if (entry.name.startsWith(\".\")) continue;\n if (entry.name === \"node_modules\") continue;\n\n const fullPath = join(dir, entry.name);\n let isDir = entry.isDirectory();\n let isFile = entry.isFile();\n\n if (entry.isSymbolicLink()) {\n try {\n const stats = statSync(fullPath);\n isDir = stats.isDirectory();\n isFile = stats.isFile();\n } catch {\n continue;\n }\n }\n\n const relPath = toPosixPath(relative(dir, fullPath));\n const ignorePath = isDir ? `${relPath}/` : relPath;\n if (ig.ignores(ignorePath)) continue;\n\n if (isFile && (entry.name.endsWith(\".ts\") || entry.name.endsWith(\".js\"))) {\n entries.push(fullPath);\n } else if (isDir) {\n const resolvedEntries = resolveExtensionEntries(fullPath);\n if (resolvedEntries) {\n entries.push(...resolvedEntries);\n }\n }\n }\n } catch {\n // Ignore errors\n }\n\n return entries;\n}\n\n/**\n * Collect resource files from a directory based on resource type.\n * Extensions use smart discovery (index.ts in subdirs), others use recursive collection.\n */\nfunction collectResourceFiles(dir: string, resourceType: ResourceType): string[] {\n if (resourceType === \"skills\") {\n return collectSkillEntries(dir);\n }\n if (resourceType === \"extensions\") {\n return collectAutoExtensionEntries(dir);\n }\n return collectFiles(dir, FILE_PATTERNS[resourceType]);\n}\n\nfunction matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean {\n const rel = relative(baseDir, filePath);\n const name = basename(filePath);\n const isSkillFile = name === \"SKILL.md\";\n const parentDir = isSkillFile ? dirname(filePath) : undefined;\n const parentRel = isSkillFile ? relative(baseDir, parentDir!) : undefined;\n const parentName = isSkillFile ? basename(parentDir!) : undefined;\n\n return patterns.some((pattern) => {\n if (minimatch(rel, pattern) || minimatch(name, pattern) || minimatch(filePath, pattern)) {\n return true;\n }\n if (!isSkillFile) return false;\n return minimatch(parentRel!, pattern) || minimatch(parentName!, pattern) || minimatch(parentDir!, pattern);\n });\n}\n\nfunction normalizeExactPattern(pattern: string): string {\n if (pattern.startsWith(\"./\") || pattern.startsWith(\".\\\\\")) {\n return pattern.slice(2);\n }\n return pattern;\n}\n\nfunction matchesAnyExactPattern(filePath: string, patterns: string[], baseDir: string): boolean {\n if (patterns.length === 0) return false;\n const rel = relative(baseDir, filePath);\n const name = basename(filePath);\n const isSkillFile = name === \"SKILL.md\";\n const parentDir = isSkillFile ? dirname(filePath) : undefined;\n const parentRel = isSkillFile ? relative(baseDir, parentDir!) : undefined;\n\n return patterns.some((pattern) => {\n const normalized = normalizeExactPattern(pattern);\n if (normalized === rel || normalized === filePath) {\n return true;\n }\n if (!isSkillFile) return false;\n return normalized === parentRel || normalized === parentDir;\n });\n}\n\nfunction getOverridePatterns(entries: string[]): string[] {\n return entries.filter((pattern) => pattern.startsWith(\"!\") || pattern.startsWith(\"+\") || pattern.startsWith(\"-\"));\n}\n\nfunction isEnabledByOverrides(filePath: string, patterns: string[], baseDir: string): boolean {\n const overrides = getOverridePatterns(patterns);\n const excludes = overrides.filter((pattern) => pattern.startsWith(\"!\")).map((pattern) => pattern.slice(1));\n const forceIncludes = overrides.filter((pattern) => pattern.startsWith(\"+\")).map((pattern) => pattern.slice(1));\n const forceExcludes = overrides.filter((pattern) => pattern.startsWith(\"-\")).map((pattern) => pattern.slice(1));\n\n let enabled = true;\n if (excludes.length > 0 && matchesAnyPattern(filePath, excludes, baseDir)) {\n enabled = false;\n }\n if (forceIncludes.length > 0 && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {\n enabled = true;\n }\n if (forceExcludes.length > 0 && matchesAnyExactPattern(filePath, forceExcludes, baseDir)) {\n enabled = false;\n }\n return enabled;\n}\n\n/**\n * Apply patterns to paths and return a Set of enabled paths.\n * Pattern types:\n * - Plain patterns: include matching paths\n * - `!pattern`: exclude matching paths\n * - `+path`: force-include exact path (overrides exclusions)\n * - `-path`: force-exclude exact path (overrides force-includes)\n */\nfunction applyPatterns(allPaths: string[], patterns: string[], baseDir: string): Set<string> {\n const includes: string[] = [];\n const excludes: string[] = [];\n const forceIncludes: string[] = [];\n const forceExcludes: string[] = [];\n\n for (const p of patterns) {\n if (p.startsWith(\"+\")) {\n forceIncludes.push(p.slice(1));\n } else if (p.startsWith(\"-\")) {\n forceExcludes.push(p.slice(1));\n } else if (p.startsWith(\"!\")) {\n excludes.push(p.slice(1));\n } else {\n includes.push(p);\n }\n }\n\n // Step 1: Apply includes (or all if no includes)\n let result: string[];\n if (includes.length === 0) {\n result = [...allPaths];\n } else {\n result = allPaths.filter((filePath) => matchesAnyPattern(filePath, includes, baseDir));\n }\n\n // Step 2: Apply excludes\n if (excludes.length > 0) {\n result = result.filter((filePath) => !matchesAnyPattern(filePath, excludes, baseDir));\n }\n\n // Step 3: Force-include (add back from allPaths, overriding exclusions)\n if (forceIncludes.length > 0) {\n for (const filePath of allPaths) {\n if (!result.includes(filePath) && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {\n result.push(filePath);\n }\n }\n }\n\n // Step 4: Force-exclude (remove even if included or force-included)\n if (forceExcludes.length > 0) {\n result = result.filter((filePath) => !matchesAnyExactPattern(filePath, forceExcludes, baseDir));\n }\n\n return new Set(result);\n}\n\nexport class DefaultPackageManager implements PackageManager {\n private cwd: string;\n private agentDir: string;\n private settingsManager: SettingsManager;\n private globalNpmRoot: string | undefined;\n private progressCallback: ProgressCallback | undefined;\n\n constructor(options: PackageManagerOptions) {\n this.cwd = options.cwd;\n this.agentDir = options.agentDir;\n this.settingsManager = options.settingsManager;\n }\n\n setProgressCallback(callback: ProgressCallback | undefined): void {\n this.progressCallback = callback;\n }\n\n getInstalledPath(source: string, scope: \"user\" | \"project\"): string | undefined {\n const parsed = this.parseSource(source);\n if (parsed.type === \"npm\") {\n const path = this.getNpmInstallPath(parsed, scope);\n return existsSync(path) ? path : undefined;\n }\n if (parsed.type === \"git\") {\n const path = this.getGitInstallPath(parsed, scope);\n return existsSync(path) ? path : undefined;\n }\n if (parsed.type === \"local\") {\n const baseDir = this.getBaseDirForScope(scope);\n const path = this.resolvePathFromBase(parsed.path, baseDir);\n return existsSync(path) ? path : undefined;\n }\n return undefined;\n }\n\n private emitProgress(event: ProgressEvent): void {\n this.progressCallback?.(event);\n }\n\n private async withProgress(\n action: ProgressEvent[\"action\"],\n source: string,\n message: string,\n operation: () => Promise<void>,\n ): Promise<void> {\n this.emitProgress({ type: \"start\", action, source, message });\n try {\n await operation();\n this.emitProgress({ type: \"complete\", action, source });\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n this.emitProgress({ type: \"error\", action, source, message: errorMessage });\n throw error;\n }\n }\n\n async resolve(onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths> {\n const accumulator = this.createAccumulator();\n const globalSettings = this.settingsManager.getGlobalSettings();\n const projectSettings = this.settingsManager.getProjectSettings();\n\n // Collect all packages with scope\n const allPackages: Array<{ pkg: PackageSource; scope: SourceScope }> = [];\n for (const pkg of globalSettings.packages ?? []) {\n allPackages.push({ pkg, scope: \"user\" });\n }\n for (const pkg of projectSettings.packages ?? []) {\n allPackages.push({ pkg, scope: \"project\" });\n }\n\n // Dedupe: project scope wins over global for same package identity\n const packageSources = this.dedupePackages(allPackages);\n await this.resolvePackageSources(packageSources, accumulator, onMissing);\n\n const globalBaseDir = this.agentDir;\n const projectBaseDir = join(this.cwd, CONFIG_DIR_NAME);\n\n for (const resourceType of RESOURCE_TYPES) {\n const target = this.getTargetMap(accumulator, resourceType);\n const globalEntries = (globalSettings[resourceType] ?? []) as string[];\n const projectEntries = (projectSettings[resourceType] ?? []) as string[];\n this.resolveLocalEntries(\n globalEntries,\n resourceType,\n target,\n {\n source: \"local\",\n scope: \"user\",\n origin: \"top-level\",\n },\n globalBaseDir,\n );\n this.resolveLocalEntries(\n projectEntries,\n resourceType,\n target,\n {\n source: \"local\",\n scope: \"project\",\n origin: \"top-level\",\n },\n projectBaseDir,\n );\n }\n\n this.addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDir);\n\n return this.toResolvedPaths(accumulator);\n }\n\n async resolveExtensionSources(\n sources: string[],\n options?: { local?: boolean; temporary?: boolean },\n ): Promise<ResolvedPaths> {\n const accumulator = this.createAccumulator();\n const scope: SourceScope = options?.temporary ? \"temporary\" : options?.local ? \"project\" : \"user\";\n const packageSources = sources.map((source) => ({ pkg: source as PackageSource, scope }));\n await this.resolvePackageSources(packageSources, accumulator);\n return this.toResolvedPaths(accumulator);\n }\n\n async install(source: string, options?: { local?: boolean }): Promise<void> {\n const parsed = this.parseSource(source);\n const scope: SourceScope = options?.local ? \"project\" : \"user\";\n await this.withProgress(\"install\", source, `Installing ${source}...`, async () => {\n if (parsed.type === \"npm\") {\n await this.installNpm(parsed, scope, false);\n return;\n }\n if (parsed.type === \"git\") {\n await this.installGit(parsed, scope);\n return;\n }\n if (parsed.type === \"local\") {\n const resolved = this.resolvePath(parsed.path);\n if (!existsSync(resolved)) {\n throw new Error(`Path does not exist: ${resolved}`);\n }\n return;\n }\n throw new Error(`Unsupported install source: ${source}`);\n });\n }\n\n async remove(source: string, options?: { local?: boolean }): Promise<void> {\n const parsed = this.parseSource(source);\n const scope: SourceScope = options?.local ? \"project\" : \"user\";\n await this.withProgress(\"remove\", source, `Removing ${source}...`, async () => {\n if (parsed.type === \"npm\") {\n await this.uninstallNpm(parsed, scope);\n return;\n }\n if (parsed.type === \"git\") {\n await this.removeGit(parsed, scope);\n return;\n }\n if (parsed.type === \"local\") {\n return;\n }\n throw new Error(`Unsupported remove source: ${source}`);\n });\n }\n\n async update(source?: string): Promise<void> {\n const globalSettings = this.settingsManager.getGlobalSettings();\n const projectSettings = this.settingsManager.getProjectSettings();\n const identity = source ? this.getPackageIdentity(source) : undefined;\n\n for (const pkg of globalSettings.packages ?? []) {\n const sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n if (identity && this.getPackageIdentity(sourceStr, \"user\") !== identity) continue;\n await this.updateSourceForScope(sourceStr, \"user\");\n }\n for (const pkg of projectSettings.packages ?? []) {\n const sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n if (identity && this.getPackageIdentity(sourceStr, \"project\") !== identity) continue;\n await this.updateSourceForScope(sourceStr, \"project\");\n }\n }\n\n private async updateSourceForScope(source: string, scope: SourceScope): Promise<void> {\n const parsed = this.parseSource(source);\n if (parsed.type === \"npm\") {\n if (parsed.pinned) return;\n await this.withProgress(\"update\", source, `Updating ${source}...`, async () => {\n await this.installNpm(parsed, scope, false);\n });\n return;\n }\n if (parsed.type === \"git\") {\n if (parsed.pinned) return;\n await this.withProgress(\"update\", source, `Updating ${source}...`, async () => {\n await this.updateGit(parsed, scope);\n });\n return;\n }\n }\n\n private async resolvePackageSources(\n sources: Array<{ pkg: PackageSource; scope: SourceScope }>,\n accumulator: ResourceAccumulator,\n onMissing?: (source: string) => Promise<MissingSourceAction>,\n ): Promise<void> {\n for (const { pkg, scope } of sources) {\n const sourceStr = typeof pkg === \"string\" ? pkg : pkg.source;\n const filter = typeof pkg === \"object\" ? pkg : undefined;\n const parsed = this.parseSource(sourceStr);\n const metadata: PathMetadata = { source: sourceStr, scope, origin: \"package\" };\n\n if (parsed.type === \"local\") {\n const baseDir = this.getBaseDirForScope(scope);\n this.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir);\n continue;\n }\n\n const installMissing = async (): Promise<boolean> => {\n if (!onMissing) {\n await this.installParsedSource(parsed, scope);\n return true;\n }\n const action = await onMissing(sourceStr);\n if (action === \"skip\") return false;\n if (action === \"error\") throw new Error(`Missing source: ${sourceStr}`);\n await this.installParsedSource(parsed, scope);\n return true;\n };\n\n if (parsed.type === \"npm\") {\n const installedPath = this.getNpmInstallPath(parsed, scope);\n const needsInstall = !existsSync(installedPath) || (await this.npmNeedsUpdate(parsed, installedPath));\n if (needsInstall) {\n const installed = await installMissing();\n if (!installed) continue;\n }\n metadata.baseDir = installedPath;\n this.collectPackageResources(installedPath, accumulator, filter, metadata);\n continue;\n }\n\n if (parsed.type === \"git\") {\n const installedPath = this.getGitInstallPath(parsed, scope);\n if (!existsSync(installedPath)) {\n const installed = await installMissing();\n if (!installed) continue;\n }\n metadata.baseDir = installedPath;\n this.collectPackageResources(installedPath, accumulator, filter, metadata);\n }\n }\n }\n\n private resolveLocalExtensionSource(\n source: LocalSource,\n accumulator: ResourceAccumulator,\n filter: PackageFilter | undefined,\n metadata: PathMetadata,\n baseDir: string,\n ): void {\n const resolved = this.resolvePathFromBase(source.path, baseDir);\n if (!existsSync(resolved)) {\n return;\n }\n\n try {\n const stats = statSync(resolved);\n if (stats.isFile()) {\n metadata.baseDir = dirname(resolved);\n this.addResource(accumulator.extensions, resolved, metadata, true);\n return;\n }\n if (stats.isDirectory()) {\n metadata.baseDir = resolved;\n const resources = this.collectPackageResources(resolved, accumulator, filter, metadata);\n if (!resources) {\n this.addResource(accumulator.extensions, resolved, metadata, true);\n }\n }\n } catch {\n return;\n }\n }\n\n private async installParsedSource(parsed: ParsedSource, scope: SourceScope): Promise<void> {\n if (parsed.type === \"npm\") {\n await this.installNpm(parsed, scope, scope === \"temporary\");\n return;\n }\n if (parsed.type === \"git\") {\n await this.installGit(parsed, scope);\n return;\n }\n }\n\n private parseSource(source: string): ParsedSource {\n if (source.startsWith(\"npm:\")) {\n const spec = source.slice(\"npm:\".length).trim();\n const { name, version } = this.parseNpmSpec(spec);\n return {\n type: \"npm\",\n spec,\n name,\n pinned: Boolean(version),\n };\n }\n\n if (source.startsWith(\"git:\") || looksLikeGitUrl(source)) {\n const repoSpec = source.startsWith(\"git:\") ? source.slice(\"git:\".length).trim() : source;\n const [repo, ref] = repoSpec.split(\"@\");\n const normalized = repo.replace(/^https?:\\/\\//, \"\").replace(/\\.git$/, \"\");\n const parts = normalized.split(\"/\");\n const host = parts.shift() ?? \"\";\n const repoPath = parts.join(\"/\");\n return {\n type: \"git\",\n repo: normalized,\n host,\n path: repoPath,\n ref,\n pinned: Boolean(ref),\n };\n }\n\n return { type: \"local\", path: source };\n }\n\n /**\n * Check if an npm package needs to be updated.\n * - For unpinned packages: check if registry has a newer version\n * - For pinned packages: check if installed version matches the pinned version\n */\n private async npmNeedsUpdate(source: NpmSource, installedPath: string): Promise<boolean> {\n const installedVersion = this.getInstalledNpmVersion(installedPath);\n if (!installedVersion) return true;\n\n const { version: pinnedVersion } = this.parseNpmSpec(source.spec);\n if (pinnedVersion) {\n // Pinned: check if installed matches pinned (exact match for now)\n return installedVersion !== pinnedVersion;\n }\n\n // Unpinned: check registry for latest version\n try {\n const latestVersion = await this.getLatestNpmVersion(source.name);\n return latestVersion !== installedVersion;\n } catch {\n // If we can't check registry, assume it's fine\n return false;\n }\n }\n\n private getInstalledNpmVersion(installedPath: string): string | undefined {\n const packageJsonPath = join(installedPath, \"package.json\");\n if (!existsSync(packageJsonPath)) return undefined;\n try {\n const content = readFileSync(packageJsonPath, \"utf-8\");\n const pkg = JSON.parse(content) as { version?: string };\n return pkg.version;\n } catch {\n return undefined;\n }\n }\n\n private async getLatestNpmVersion(packageName: string): Promise<string> {\n const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);\n if (!response.ok) throw new Error(`Failed to fetch npm registry: ${response.status}`);\n const data = (await response.json()) as { version: string };\n return data.version;\n }\n\n /**\n * Get a unique identity for a package, ignoring version/ref.\n * Used to detect when the same package is in both global and project settings.\n */\n private getPackageIdentity(source: string, scope?: SourceScope): string {\n const parsed = this.parseSource(source);\n if (parsed.type === \"npm\") {\n return `npm:${parsed.name}`;\n }\n if (parsed.type === \"git\") {\n return `git:${parsed.repo}`;\n }\n if (scope) {\n const baseDir = this.getBaseDirForScope(scope);\n return `local:${this.resolvePathFromBase(parsed.path, baseDir)}`;\n }\n return `local:${this.resolvePath(parsed.path)}`;\n }\n\n /**\n * Dedupe packages: if same package identity appears in both global and project,\n * keep only the project one (project wins).\n */\n private dedupePackages(\n packages: Array<{ pkg: PackageSource; scope: SourceScope }>,\n ): Array<{ pkg: PackageSource; scope: SourceScope }> {\n const seen = new Map<string, { pkg: PackageSource; scope: SourceScope }>();\n\n for (const entry of packages) {\n const sourceStr = typeof entry.pkg === \"string\" ? entry.pkg : entry.pkg.source;\n const identity = this.getPackageIdentity(sourceStr, entry.scope);\n\n const existing = seen.get(identity);\n if (!existing) {\n seen.set(identity, entry);\n } else if (entry.scope === \"project\" && existing.scope === \"user\") {\n // Project wins over user\n seen.set(identity, entry);\n }\n // If existing is project and new is global, keep existing (project)\n // If both are same scope, keep first one\n }\n\n return Array.from(seen.values());\n }\n\n private parseNpmSpec(spec: string): { name: string; version?: string } {\n const match = spec.match(/^(@?[^@]+(?:\\/[^@]+)?)(?:@(.+))?$/);\n if (!match) {\n return { name: spec };\n }\n const name = match[1] ?? spec;\n const version = match[2];\n return { name, version };\n }\n\n private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> {\n if (scope === \"user\" && !temporary) {\n await this.runCommand(\"npm\", [\"install\", \"-g\", source.spec]);\n return;\n }\n const installRoot = this.getNpmInstallRoot(scope, temporary);\n this.ensureNpmProject(installRoot);\n await this.runCommand(\"npm\", [\"install\", source.spec, \"--prefix\", installRoot]);\n }\n\n private async uninstallNpm(source: NpmSource, scope: SourceScope): Promise<void> {\n if (scope === \"user\") {\n await this.runCommand(\"npm\", [\"uninstall\", \"-g\", source.name]);\n return;\n }\n const installRoot = this.getNpmInstallRoot(scope, false);\n if (!existsSync(installRoot)) {\n return;\n }\n await this.runCommand(\"npm\", [\"uninstall\", source.name, \"--prefix\", installRoot]);\n }\n\n private async installGit(source: GitSource, scope: SourceScope): Promise<void> {\n const targetDir = this.getGitInstallPath(source, scope);\n if (existsSync(targetDir)) {\n return;\n }\n const gitRoot = this.getGitInstallRoot(scope);\n if (gitRoot) {\n this.ensureGitIgnore(gitRoot);\n }\n mkdirSync(dirname(targetDir), { recursive: true });\n const cloneUrl = source.repo.startsWith(\"http\") ? source.repo : `https://${source.repo}`;\n await this.runCommand(\"git\", [\"clone\", cloneUrl, targetDir]);\n if (source.ref) {\n await this.runCommand(\"git\", [\"checkout\", source.ref], { cwd: targetDir });\n }\n const packageJsonPath = join(targetDir, \"package.json\");\n if (existsSync(packageJsonPath)) {\n await this.runCommand(\"npm\", [\"install\"], { cwd: targetDir });\n }\n }\n\n private async updateGit(source: GitSource, scope: SourceScope): Promise<void> {\n const targetDir = this.getGitInstallPath(source, scope);\n if (!existsSync(targetDir)) {\n await this.installGit(source, scope);\n return;\n }\n\n // Fetch latest from remote (handles force-push by getting new history)\n await this.runCommand(\"git\", [\"fetch\", \"--prune\", \"origin\"], { cwd: targetDir });\n\n // Reset to upstream tracking branch (handles force-push gracefully)\n await this.runCommand(\"git\", [\"reset\", \"--hard\", \"@{upstream}\"], { cwd: targetDir });\n\n // Clean untracked files (extensions should be pristine)\n await this.runCommand(\"git\", [\"clean\", \"-fdx\"], { cwd: targetDir });\n\n const packageJsonPath = join(targetDir, \"package.json\");\n if (existsSync(packageJsonPath)) {\n await this.runCommand(\"npm\", [\"install\"], { cwd: targetDir });\n }\n }\n\n private async removeGit(source: GitSource, scope: SourceScope): Promise<void> {\n const targetDir = this.getGitInstallPath(source, scope);\n if (!existsSync(targetDir)) return;\n rmSync(targetDir, { recursive: true, force: true });\n this.pruneEmptyGitParents(targetDir, this.getGitInstallRoot(scope));\n }\n\n private pruneEmptyGitParents(targetDir: string, installRoot: string | undefined): void {\n if (!installRoot) return;\n const resolvedRoot = resolve(installRoot);\n let current = dirname(targetDir);\n while (current.startsWith(resolvedRoot) && current !== resolvedRoot) {\n if (!existsSync(current)) {\n current = dirname(current);\n continue;\n }\n const entries = readdirSync(current);\n if (entries.length > 0) {\n break;\n }\n try {\n rmSync(current, { recursive: true, force: true });\n } catch {\n break;\n }\n current = dirname(current);\n }\n }\n\n private ensureNpmProject(installRoot: string): void {\n if (!existsSync(installRoot)) {\n mkdirSync(installRoot, { recursive: true });\n }\n this.ensureGitIgnore(installRoot);\n const packageJsonPath = join(installRoot, \"package.json\");\n if (!existsSync(packageJsonPath)) {\n const pkgJson = { name: \"pi-extensions\", private: true };\n writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2), \"utf-8\");\n }\n }\n\n private ensureGitIgnore(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const ignorePath = join(dir, \".gitignore\");\n if (!existsSync(ignorePath)) {\n writeFileSync(ignorePath, \"*\\n!.gitignore\\n\", \"utf-8\");\n }\n }\n\n private getNpmInstallRoot(scope: SourceScope, temporary: boolean): string {\n if (temporary) {\n return this.getTemporaryDir(\"npm\");\n }\n if (scope === \"project\") {\n return join(this.cwd, CONFIG_DIR_NAME, \"npm\");\n }\n return join(this.getGlobalNpmRoot(), \"..\");\n }\n\n private getGlobalNpmRoot(): string {\n if (this.globalNpmRoot) {\n return this.globalNpmRoot;\n }\n const result = this.runCommandSync(\"npm\", [\"root\", \"-g\"]);\n this.globalNpmRoot = result.trim();\n return this.globalNpmRoot;\n }\n\n private getNpmInstallPath(source: NpmSource, scope: SourceScope): string {\n if (scope === \"temporary\") {\n return join(this.getTemporaryDir(\"npm\"), \"node_modules\", source.name);\n }\n if (scope === \"project\") {\n return join(this.cwd, CONFIG_DIR_NAME, \"npm\", \"node_modules\", source.name);\n }\n return join(this.getGlobalNpmRoot(), source.name);\n }\n\n private getGitInstallPath(source: GitSource, scope: SourceScope): string {\n if (scope === \"temporary\") {\n return this.getTemporaryDir(`git-${source.host}`, source.path);\n }\n if (scope === \"project\") {\n return join(this.cwd, CONFIG_DIR_NAME, \"git\", source.host, source.path);\n }\n return join(this.agentDir, \"git\", source.host, source.path);\n }\n\n private getGitInstallRoot(scope: SourceScope): string | undefined {\n if (scope === \"temporary\") {\n return undefined;\n }\n if (scope === \"project\") {\n return join(this.cwd, CONFIG_DIR_NAME, \"git\");\n }\n return join(this.agentDir, \"git\");\n }\n\n private getTemporaryDir(prefix: string, suffix?: string): string {\n const hash = createHash(\"sha256\")\n .update(`${prefix}-${suffix ?? \"\"}`)\n .digest(\"hex\")\n .slice(0, 8);\n return join(tmpdir(), \"pi-extensions\", prefix, hash, suffix ?? \"\");\n }\n\n private getBaseDirForScope(scope: SourceScope): string {\n if (scope === \"project\") {\n return join(this.cwd, CONFIG_DIR_NAME);\n }\n if (scope === \"user\") {\n return this.agentDir;\n }\n return this.cwd;\n }\n\n private resolvePath(input: string): string {\n const trimmed = input.trim();\n if (trimmed === \"~\") return homedir();\n if (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n if (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n return resolve(this.cwd, trimmed);\n }\n\n private resolvePathFromBase(input: string, baseDir: string): string {\n const trimmed = input.trim();\n if (trimmed === \"~\") return homedir();\n if (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n if (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n return resolve(baseDir, trimmed);\n }\n\n private collectPackageResources(\n packageRoot: string,\n accumulator: ResourceAccumulator,\n filter: PackageFilter | undefined,\n metadata: PathMetadata,\n ): boolean {\n if (filter) {\n for (const resourceType of RESOURCE_TYPES) {\n const patterns = filter[resourceType as keyof PackageFilter];\n const target = this.getTargetMap(accumulator, resourceType);\n if (patterns !== undefined) {\n this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata);\n } else {\n this.collectDefaultResources(packageRoot, resourceType, target, metadata);\n }\n }\n return true;\n }\n\n const manifest = this.readPiManifest(packageRoot);\n if (manifest) {\n for (const resourceType of RESOURCE_TYPES) {\n const entries = manifest[resourceType as keyof PiManifest];\n this.addManifestEntries(\n entries,\n packageRoot,\n resourceType,\n this.getTargetMap(accumulator, resourceType),\n metadata,\n );\n }\n return true;\n }\n\n let hasAnyDir = false;\n for (const resourceType of RESOURCE_TYPES) {\n const dir = join(packageRoot, resourceType);\n if (existsSync(dir)) {\n // Collect all files from the directory (all enabled by default)\n const files = collectResourceFiles(dir, resourceType);\n for (const f of files) {\n this.addResource(this.getTargetMap(accumulator, resourceType), f, metadata, true);\n }\n hasAnyDir = true;\n }\n }\n return hasAnyDir;\n }\n\n private collectDefaultResources(\n packageRoot: string,\n resourceType: ResourceType,\n target: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n metadata: PathMetadata,\n ): void {\n const manifest = this.readPiManifest(packageRoot);\n const entries = manifest?.[resourceType as keyof PiManifest];\n if (entries) {\n this.addManifestEntries(entries, packageRoot, resourceType, target, metadata);\n return;\n }\n const dir = join(packageRoot, resourceType);\n if (existsSync(dir)) {\n // Collect all files from the directory (all enabled by default)\n const files = collectResourceFiles(dir, resourceType);\n for (const f of files) {\n this.addResource(target, f, metadata, true);\n }\n }\n }\n\n private applyPackageFilter(\n packageRoot: string,\n userPatterns: string[],\n resourceType: ResourceType,\n target: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n metadata: PathMetadata,\n ): void {\n const { allFiles } = this.collectManifestFiles(packageRoot, resourceType);\n\n if (userPatterns.length === 0) {\n // Empty array explicitly disables all resources of this type\n for (const f of allFiles) {\n this.addResource(target, f, metadata, false);\n }\n return;\n }\n\n // Apply user patterns\n const enabledByUser = applyPatterns(allFiles, userPatterns, packageRoot);\n\n for (const f of allFiles) {\n const enabled = enabledByUser.has(f);\n this.addResource(target, f, metadata, enabled);\n }\n }\n\n /**\n * Collect all files from a package for a resource type, applying manifest patterns.\n * Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files\n * that pass the manifest's own patterns.\n */\n private collectManifestFiles(\n packageRoot: string,\n resourceType: ResourceType,\n ): { allFiles: string[]; enabledByManifest: Set<string> } {\n const manifest = this.readPiManifest(packageRoot);\n const entries = manifest?.[resourceType as keyof PiManifest];\n if (entries && entries.length > 0) {\n const allFiles = this.collectFilesFromManifestEntries(entries, packageRoot, resourceType);\n const manifestPatterns = entries.filter(isPattern);\n const enabledByManifest =\n manifestPatterns.length > 0 ? applyPatterns(allFiles, manifestPatterns, packageRoot) : new Set(allFiles);\n return { allFiles: Array.from(enabledByManifest), enabledByManifest };\n }\n\n const conventionDir = join(packageRoot, resourceType);\n if (!existsSync(conventionDir)) {\n return { allFiles: [], enabledByManifest: new Set() };\n }\n const allFiles = collectResourceFiles(conventionDir, resourceType);\n return { allFiles, enabledByManifest: new Set(allFiles) };\n }\n\n private readPiManifest(packageRoot: string): PiManifest | null {\n const packageJsonPath = join(packageRoot, \"package.json\");\n if (!existsSync(packageJsonPath)) {\n return null;\n }\n\n try {\n const content = readFileSync(packageJsonPath, \"utf-8\");\n const pkg = JSON.parse(content) as { pi?: PiManifest };\n return pkg.pi ?? null;\n } catch {\n return null;\n }\n }\n\n private addManifestEntries(\n entries: string[] | undefined,\n root: string,\n resourceType: ResourceType,\n target: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n metadata: PathMetadata,\n ): void {\n if (!entries) return;\n\n const allFiles = this.collectFilesFromManifestEntries(entries, root, resourceType);\n const patterns = entries.filter(isPattern);\n const enabledPaths = applyPatterns(allFiles, patterns, root);\n\n for (const f of allFiles) {\n if (enabledPaths.has(f)) {\n this.addResource(target, f, metadata, true);\n }\n }\n }\n\n private collectFilesFromManifestEntries(entries: string[], root: string, resourceType: ResourceType): string[] {\n const plain = entries.filter((entry) => !isPattern(entry));\n const resolved = plain.map((entry) => resolve(root, entry));\n return this.collectFilesFromPaths(resolved, resourceType);\n }\n\n private resolveLocalEntries(\n entries: string[],\n resourceType: ResourceType,\n target: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n metadata: PathMetadata,\n baseDir: string,\n ): void {\n if (entries.length === 0) return;\n\n // Collect all files from plain entries (non-pattern entries)\n const { plain, patterns } = splitPatterns(entries);\n const resolvedPlain = plain.map((p) => this.resolvePathFromBase(p, baseDir));\n const allFiles = this.collectFilesFromPaths(resolvedPlain, resourceType);\n\n // Determine which files are enabled based on patterns\n const enabledPaths = applyPatterns(allFiles, patterns, baseDir);\n\n // Add all files with their enabled state\n for (const f of allFiles) {\n this.addResource(target, f, metadata, enabledPaths.has(f));\n }\n }\n\n private addAutoDiscoveredResources(\n accumulator: ResourceAccumulator,\n globalSettings: ReturnType<SettingsManager[\"getGlobalSettings\"]>,\n projectSettings: ReturnType<SettingsManager[\"getProjectSettings\"]>,\n globalBaseDir: string,\n projectBaseDir: string,\n ): void {\n const userMetadata: PathMetadata = {\n source: \"auto\",\n scope: \"user\",\n origin: \"top-level\",\n baseDir: globalBaseDir,\n };\n const projectMetadata: PathMetadata = {\n source: \"auto\",\n scope: \"project\",\n origin: \"top-level\",\n baseDir: projectBaseDir,\n };\n\n const userOverrides = {\n extensions: (globalSettings.extensions ?? []) as string[],\n skills: (globalSettings.skills ?? []) as string[],\n commands: (globalSettings.commands ?? []) as string[],\n themes: (globalSettings.themes ?? []) as string[],\n };\n const projectOverrides = {\n extensions: (projectSettings.extensions ?? []) as string[],\n skills: (projectSettings.skills ?? []) as string[],\n commands: (projectSettings.commands ?? []) as string[],\n themes: (projectSettings.themes ?? []) as string[],\n };\n\n const userDirs = {\n extensions: join(globalBaseDir, \"extensions\"),\n skills: join(globalBaseDir, \"skills\"),\n commands: join(globalBaseDir, \"commands\"),\n themes: join(globalBaseDir, \"themes\"),\n };\n const projectDirs = {\n extensions: join(projectBaseDir, \"extensions\"),\n skills: join(projectBaseDir, \"skills\"),\n commands: join(projectBaseDir, \"commands\"),\n themes: join(projectBaseDir, \"themes\"),\n };\n\n const addResources = (\n resourceType: ResourceType,\n paths: string[],\n metadata: PathMetadata,\n overrides: string[],\n baseDir: string,\n ) => {\n const target = this.getTargetMap(accumulator, resourceType);\n for (const path of paths) {\n const enabled = isEnabledByOverrides(path, overrides, baseDir);\n this.addResource(target, path, metadata, enabled);\n }\n };\n\n addResources(\n \"extensions\",\n collectAutoExtensionEntries(userDirs.extensions),\n userMetadata,\n userOverrides.extensions,\n globalBaseDir,\n );\n addResources(\n \"skills\",\n collectAutoSkillEntries(userDirs.skills),\n userMetadata,\n userOverrides.skills,\n globalBaseDir,\n );\n addResources(\n \"commands\",\n collectAutoPromptEntries(userDirs.commands),\n userMetadata,\n userOverrides.commands,\n globalBaseDir,\n );\n addResources(\n \"themes\",\n collectAutoThemeEntries(userDirs.themes),\n userMetadata,\n userOverrides.themes,\n globalBaseDir,\n );\n\n addResources(\n \"extensions\",\n collectAutoExtensionEntries(projectDirs.extensions),\n projectMetadata,\n projectOverrides.extensions,\n projectBaseDir,\n );\n addResources(\n \"skills\",\n collectAutoSkillEntries(projectDirs.skills),\n projectMetadata,\n projectOverrides.skills,\n projectBaseDir,\n );\n addResources(\n \"commands\",\n collectAutoPromptEntries(projectDirs.commands),\n projectMetadata,\n projectOverrides.commands,\n projectBaseDir,\n );\n addResources(\n \"themes\",\n collectAutoThemeEntries(projectDirs.themes),\n projectMetadata,\n projectOverrides.themes,\n projectBaseDir,\n );\n }\n\n private collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] {\n const files: string[] = [];\n for (const p of paths) {\n if (!existsSync(p)) continue;\n\n try {\n const stats = statSync(p);\n if (stats.isFile()) {\n files.push(p);\n } else if (stats.isDirectory()) {\n files.push(...collectResourceFiles(p, resourceType));\n }\n } catch {\n // Ignore errors\n }\n }\n return files;\n }\n\n private getTargetMap(\n accumulator: ResourceAccumulator,\n resourceType: ResourceType,\n ): Map<string, { metadata: PathMetadata; enabled: boolean }> {\n switch (resourceType) {\n case \"extensions\":\n return accumulator.extensions;\n case \"skills\":\n return accumulator.skills;\n case \"commands\":\n return accumulator.commands;\n case \"themes\":\n return accumulator.themes;\n default:\n throw new Error(`Unknown resource type: ${resourceType}`);\n }\n }\n\n private addResource(\n map: Map<string, { metadata: PathMetadata; enabled: boolean }>,\n path: string,\n metadata: PathMetadata,\n enabled: boolean,\n ): void {\n if (!path) return;\n if (!map.has(path)) {\n map.set(path, { metadata, enabled });\n }\n }\n\n private createAccumulator(): ResourceAccumulator {\n return {\n extensions: new Map(),\n skills: new Map(),\n commands: new Map(),\n themes: new Map(),\n };\n }\n\n private toResolvedPaths(accumulator: ResourceAccumulator): ResolvedPaths {\n const toResolved = (entries: Map<string, { metadata: PathMetadata; enabled: boolean }>): ResolvedResource[] => {\n return Array.from(entries.entries()).map(([path, { metadata, enabled }]) => ({\n path,\n enabled,\n metadata,\n }));\n };\n\n return {\n extensions: toResolved(accumulator.extensions),\n skills: toResolved(accumulator.skills),\n commands: toResolved(accumulator.commands),\n themes: toResolved(accumulator.themes),\n };\n }\n\n private runCommand(command: string, args: string[], options?: { cwd?: string }): Promise<void> {\n return new Promise((resolvePromise, reject) => {\n const child = spawn(command, args, {\n cwd: options?.cwd,\n stdio: \"inherit\",\n shell: process.platform === \"win32\",\n });\n child.on(\"error\", reject);\n child.on(\"exit\", (code) => {\n if (code === 0) {\n resolvePromise();\n } else {\n reject(new Error(`${command} ${args.join(\" \")} failed with code ${code}`));\n }\n });\n });\n }\n\n private runCommandSync(command: string, args: string[]): string {\n const result = spawnSync(command, args, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n encoding: \"utf-8\",\n shell: process.platform === \"win32\",\n });\n if (result.status !== 0) {\n throw new Error(`Failed to run ${command} ${args.join(\" \")}: ${result.stderr || result.stdout}`);\n }\n return (result.stdout || result.stderr || \"\").trim();\n }\n}\n"]}
@@ -7,11 +7,11 @@ import ignore from "ignore";
7
7
  import { minimatch } from "minimatch";
8
8
  import { CONFIG_DIR_NAME } from "../config.js";
9
9
  import { looksLikeGitUrl } from "../utils/git.js";
10
- const RESOURCE_TYPES = ["extensions", "skills", "prompts", "themes"];
10
+ const RESOURCE_TYPES = ["extensions", "skills", "commands", "themes"];
11
11
  const FILE_PATTERNS = {
12
12
  extensions: /\.(ts|js)$/,
13
13
  skills: /\.md$/,
14
- prompts: /\.md$/,
14
+ commands: /\.md$/,
15
15
  themes: /\.json$/,
16
16
  };
17
17
  const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"];
@@ -1156,25 +1156,25 @@ export class DefaultPackageManager {
1156
1156
  const userOverrides = {
1157
1157
  extensions: (globalSettings.extensions ?? []),
1158
1158
  skills: (globalSettings.skills ?? []),
1159
- prompts: (globalSettings.prompts ?? []),
1159
+ commands: (globalSettings.commands ?? []),
1160
1160
  themes: (globalSettings.themes ?? []),
1161
1161
  };
1162
1162
  const projectOverrides = {
1163
1163
  extensions: (projectSettings.extensions ?? []),
1164
1164
  skills: (projectSettings.skills ?? []),
1165
- prompts: (projectSettings.prompts ?? []),
1165
+ commands: (projectSettings.commands ?? []),
1166
1166
  themes: (projectSettings.themes ?? []),
1167
1167
  };
1168
1168
  const userDirs = {
1169
1169
  extensions: join(globalBaseDir, "extensions"),
1170
1170
  skills: join(globalBaseDir, "skills"),
1171
- prompts: join(globalBaseDir, "prompts"),
1171
+ commands: join(globalBaseDir, "commands"),
1172
1172
  themes: join(globalBaseDir, "themes"),
1173
1173
  };
1174
1174
  const projectDirs = {
1175
1175
  extensions: join(projectBaseDir, "extensions"),
1176
1176
  skills: join(projectBaseDir, "skills"),
1177
- prompts: join(projectBaseDir, "prompts"),
1177
+ commands: join(projectBaseDir, "commands"),
1178
1178
  themes: join(projectBaseDir, "themes"),
1179
1179
  };
1180
1180
  const addResources = (resourceType, paths, metadata, overrides, baseDir) => {
@@ -1186,11 +1186,11 @@ export class DefaultPackageManager {
1186
1186
  };
1187
1187
  addResources("extensions", collectAutoExtensionEntries(userDirs.extensions), userMetadata, userOverrides.extensions, globalBaseDir);
1188
1188
  addResources("skills", collectAutoSkillEntries(userDirs.skills), userMetadata, userOverrides.skills, globalBaseDir);
1189
- addResources("prompts", collectAutoPromptEntries(userDirs.prompts), userMetadata, userOverrides.prompts, globalBaseDir);
1189
+ addResources("commands", collectAutoPromptEntries(userDirs.commands), userMetadata, userOverrides.commands, globalBaseDir);
1190
1190
  addResources("themes", collectAutoThemeEntries(userDirs.themes), userMetadata, userOverrides.themes, globalBaseDir);
1191
1191
  addResources("extensions", collectAutoExtensionEntries(projectDirs.extensions), projectMetadata, projectOverrides.extensions, projectBaseDir);
1192
1192
  addResources("skills", collectAutoSkillEntries(projectDirs.skills), projectMetadata, projectOverrides.skills, projectBaseDir);
1193
- addResources("prompts", collectAutoPromptEntries(projectDirs.prompts), projectMetadata, projectOverrides.prompts, projectBaseDir);
1193
+ addResources("commands", collectAutoPromptEntries(projectDirs.commands), projectMetadata, projectOverrides.commands, projectBaseDir);
1194
1194
  addResources("themes", collectAutoThemeEntries(projectDirs.themes), projectMetadata, projectOverrides.themes, projectBaseDir);
1195
1195
  }
1196
1196
  collectFilesFromPaths(paths, resourceType) {
@@ -1219,8 +1219,8 @@ export class DefaultPackageManager {
1219
1219
  return accumulator.extensions;
1220
1220
  case "skills":
1221
1221
  return accumulator.skills;
1222
- case "prompts":
1223
- return accumulator.prompts;
1222
+ case "commands":
1223
+ return accumulator.commands;
1224
1224
  case "themes":
1225
1225
  return accumulator.themes;
1226
1226
  default:
@@ -1238,7 +1238,7 @@ export class DefaultPackageManager {
1238
1238
  return {
1239
1239
  extensions: new Map(),
1240
1240
  skills: new Map(),
1241
- prompts: new Map(),
1241
+ commands: new Map(),
1242
1242
  themes: new Map(),
1243
1243
  };
1244
1244
  }
@@ -1253,7 +1253,7 @@ export class DefaultPackageManager {
1253
1253
  return {
1254
1254
  extensions: toResolved(accumulator.extensions),
1255
1255
  skills: toResolved(accumulator.skills),
1256
- prompts: toResolved(accumulator.prompts),
1256
+ commands: toResolved(accumulator.commands),
1257
1257
  themes: toResolved(accumulator.themes),
1258
1258
  };
1259
1259
  }