Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
75 kB · 2443 lines
TypeScript
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444import { type ChildProcess, spawn } from "node:child_process";import { createHash } from "node:crypto";import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";import { homedir, tmpdir } from "node:os";
function getEnv(): NodeJS.ProcessEnv { if (process.platform !== "linux" || Object.keys(process.env).length > 0) { return process.env; } try { const data = readFileSync("/proc/self/environ", "utf-8"); const env: NodeJS.ProcessEnv = {}; for (const entry of data.split("\0")) { const idx = entry.indexOf("="); if (idx > 0) { env[entry.slice(0, idx)] = entry.slice(idx + 1); } } return env; } catch { return process.env; }}
import { basename, dirname, join, relative, resolve, sep } from "node:path";import { globSync } from "glob";import ignore from "ignore";import { minimatch } from "minimatch";import { CONFIG_DIR_NAME, getBundledSkillsDir } from "../config.js";import { shouldUseWindowsShell, spawnHidden, spawnSyncHidden } from "../utils/child-process.js";import { type GitSource, parseGitUrl } from "../utils/git.js";import { canonicalizePath, isLocalPath } from "../utils/paths.js";import type { ResourceDiagnostic } from "./diagnostics.js";import { isStdoutTakenOver } from "./output-guard.js";import type { PackageSource, SettingsManager } from "./settings-manager.js";
const NETWORK_TIMEOUT_MS = 10000;const UPDATE_CHECK_CONCURRENCY = 4;const GIT_UPDATE_CONCURRENCY = 4;
function isOfflineModeEnabled(): boolean { const value = process.env.PI_OFFLINE; if (!value) return false; return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";}
export interface PathMetadata { source: string; scope: SourceScope; origin: "package" | "top-level"; baseDir?: string;}
export interface ResolvedResource { path: string; enabled: boolean; metadata: PathMetadata;}
export interface ResolvedPaths { extensions: ResolvedResource[]; skills: ResolvedResource[]; prompts: ResolvedResource[]; themes: ResolvedResource[]; diagnostics: ResourceDiagnostic[];}
export type MissingSourceAction = "install" | "skip" | "error";
export interface ProgressEvent { type: "start" | "progress" | "complete" | "error"; action: "install" | "remove" | "update" | "clone" | "pull"; source: string; message?: string;}
export type ProgressCallback = (event: ProgressEvent) => void;
export interface PackageUpdate { source: string; displayName: string; type: "npm" | "git"; scope: Exclude<SourceScope, "temporary">;}
export interface ConfiguredPackage { source: string; scope: "user" | "project"; filtered: boolean; installedPath?: string;}
export interface PackageManager { resolve(onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths>; install(source: string, options?: { local?: boolean }): Promise<void>; installAndPersist(source: string, options?: { local?: boolean }): Promise<void>; remove(source: string, options?: { local?: boolean }): Promise<void>; removeAndPersist(source: string, options?: { local?: boolean }): Promise<boolean>; update(source?: string): Promise<void>; listConfiguredPackages(): ConfiguredPackage[]; resolveExtensionSources( sources: string[], options?: { local?: boolean; temporary?: boolean }, ): Promise<ResolvedPaths>; addSourceToSettings(source: string, options?: { local?: boolean }): boolean; removeSourceFromSettings(source: string, options?: { local?: boolean }): boolean; setProgressCallback(callback: ProgressCallback | undefined): void; getInstalledPath(source: string, scope: "user" | "project"): string | undefined;}
interface PackageManagerOptions { cwd: string; agentDir: string; settingsManager: SettingsManager; /** Directory of built-in skills shipped with the package. Defaults to the bundled skills dir; pass null to disable. */ bundledSkillsDir?: string | null; /** Extra force-exclude patterns for built-in skills (e.g. unauthenticated MCP integrations). */ extraBuiltinSkillOverrides?: () => string[];}
type SourceScope = "user" | "project" | "temporary";
type NpmSource = { type: "npm"; spec: string; name: string; pinned: boolean;};
type LocalSource = { type: "local"; path: string;};
type ParsedSource = NpmSource | GitSource | LocalSource;
type InstalledSourceScope = Exclude<SourceScope, "temporary">;
interface ConfiguredUpdateSource { source: string; scope: InstalledSourceScope;}
interface NpmUpdateTarget extends ConfiguredUpdateSource { parsed: NpmSource;}
interface GitUpdateTarget extends ConfiguredUpdateSource { parsed: GitSource;}
interface PiManifest { extensions?: string[]; skills?: string[]; prompts?: string[]; themes?: string[];}
interface ResourceAccumulator { extensions: Map<string, { metadata: PathMetadata; enabled: boolean }>; skills: Map<string, { metadata: PathMetadata; enabled: boolean }>; prompts: Map<string, { metadata: PathMetadata; enabled: boolean }>; themes: Map<string, { metadata: PathMetadata; enabled: boolean }>; diagnostics: ResourceDiagnostic[];}
/** * Compute a numeric precedence rank for a resource based on its metadata. * Lower rank = higher precedence. Used to sort resolved resources so that * name-collision resolution ("first wins") produces the correct outcome. * * Precedence (highest to lowest): * 0 project + settings entry (source: "local", scope: "project") * 1 project + auto-discovered (source: "auto", scope: "project") * 2 user + settings entry (source: "local", scope: "user") * 3 user + auto-discovered (source: "auto", scope: "user") * 4 package resource (origin: "package") * 5 built-in resource shipped with prime-agent (source: "builtin") */function resourcePrecedenceRank(m: PathMetadata): number { if (m.source === "builtin") return 5; if (m.origin === "package") return 4; const scopeBase = m.scope === "project" ? 0 : 2; return scopeBase + (m.source === "local" ? 0 : 1);}
interface PackageFilter { extensions?: string[]; skills?: string[]; prompts?: string[]; themes?: string[];}
type ResourceType = "extensions" | "skills" | "prompts" | "themes";
const RESOURCE_TYPES: ResourceType[] = ["extensions", "skills", "prompts", "themes"];
const FILE_PATTERNS: Record<ResourceType, RegExp> = { extensions: /\.(ts|js)$/, skills: /\.md$/, prompts: /\.md$/, themes: /\.json$/,};
const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"];
type IgnoreMatcher = ReturnType<typeof ignore>;
function toPosixPath(p: string): string { return p.split(sep).join("/");}
function getHomeDir(): string { return process.env.HOME || homedir();}
function prefixIgnorePattern(line: string, prefix: string): string | null { const trimmed = line.trim(); if (!trimmed) return null; if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) return null;
let pattern = line; let negated = false;
if (pattern.startsWith("!")) { negated = true; pattern = pattern.slice(1); } else if (pattern.startsWith("\\!")) { pattern = pattern.slice(1); }
if (pattern.startsWith("/")) { pattern = pattern.slice(1); }
const prefixed = prefix ? `${prefix}${pattern}` : pattern; return negated ? `!${prefixed}` : prefixed;}
function addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void { const relativeDir = relative(rootDir, dir); const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : "";
for (const filename of IGNORE_FILE_NAMES) { const ignorePath = join(dir, filename); if (!existsSync(ignorePath)) continue; try { const content = readFileSync(ignorePath, "utf-8"); const patterns = content .split(/\r?\n/) .map((line) => prefixIgnorePattern(line, prefix)) .filter((line): line is string => Boolean(line)); if (patterns.length > 0) { ig.add(patterns); } } catch { // Unreadable ignore file: skip it rather than failing resource loading. } }}
function isPattern(s: string): boolean { return s.startsWith("!") || s.startsWith("+") || s.startsWith("-") || s.includes("*") || s.includes("?");}
function isOverridePattern(s: string): boolean { return s.startsWith("!") || s.startsWith("+") || s.startsWith("-");}
function hasGlobPattern(s: string): boolean { return s.includes("*") || s.includes("?");}
function splitPatterns(entries: string[]): { plain: string[]; patterns: string[] } { const plain: string[] = []; const patterns: string[] = []; for (const entry of entries) { if (isPattern(entry)) { patterns.push(entry); } else { plain.push(entry); } } return { plain, patterns };}
function collectFiles( dir: string, filePattern: RegExp, skipNodeModules = true, ignoreMatcher?: IgnoreMatcher, rootDir?: string,): string[] { const files: string[] = []; if (!existsSync(dir)) return files;
const root = rootDir ?? dir; const ig = ignoreMatcher ?? ignore(); addIgnoreRules(ig, dir, root);
try { const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith(".")) continue; if (skipNodeModules && entry.name === "node_modules") continue;
const fullPath = join(dir, entry.name); let isDir = entry.isDirectory(); let isFile = entry.isFile();
if (entry.isSymbolicLink()) { try { const stats = statSync(fullPath); isDir = stats.isDirectory(); isFile = stats.isFile(); } catch { continue; } }
const relPath = toPosixPath(relative(root, fullPath)); const ignorePath = isDir ? `${relPath}/` : relPath; if (ig.ignores(ignorePath)) continue;
if (isDir) { files.push(...collectFiles(fullPath, filePattern, skipNodeModules, ig, root)); } else if (isFile && filePattern.test(entry.name)) { files.push(fullPath); } } } catch { // Ignore unreadable directories during file discovery. }
return files;}
type SkillDiscoveryMode = "pi" | "agents";
function collectSkillEntries( dir: string, mode: SkillDiscoveryMode, ignoreMatcher?: IgnoreMatcher, rootDir?: string,): string[] { const entries: string[] = []; if (!existsSync(dir)) return entries;
const root = rootDir ?? dir; const ig = ignoreMatcher ?? ignore(); addIgnoreRules(ig, dir, root);
try { const dirEntries = readdirSync(dir, { withFileTypes: true });
for (const entry of dirEntries) { if (entry.name !== "SKILL.md") { continue; }
const fullPath = join(dir, entry.name); let isFile = entry.isFile(); if (entry.isSymbolicLink()) { try { isFile = statSync(fullPath).isFile(); } catch { continue; } }
const relPath = toPosixPath(relative(root, fullPath)); if (isFile && !ig.ignores(relPath)) { entries.push(fullPath); return entries; } }
for (const entry of dirEntries) { if (entry.name.startsWith(".")) continue; if (entry.name === "node_modules") continue;
const fullPath = join(dir, entry.name); let isDir = entry.isDirectory(); let isFile = entry.isFile();
if (entry.isSymbolicLink()) { try { const stats = statSync(fullPath); isDir = stats.isDirectory(); isFile = stats.isFile(); } catch { continue; } }
const relPath = toPosixPath(relative(root, fullPath)); if (mode === "pi" && dir === root && isFile && entry.name.endsWith(".md") && !ig.ignores(relPath)) { entries.push(fullPath); continue; }
if (!isDir) continue; if (ig.ignores(`${relPath}/`)) continue;
entries.push(...collectSkillEntries(fullPath, mode, ig, root)); } } catch { // Ignore unreadable directories during skill discovery. }
return entries;}
function collectAutoSkillEntries(dir: string, mode: SkillDiscoveryMode): string[] { return collectSkillEntries(dir, mode);}
function findGitRepoRoot(startDir: string): string | null { let dir = resolve(startDir); while (true) { if (existsSync(join(dir, ".git"))) { return dir; } const parent = dirname(dir); if (parent === dir) { return null; } dir = parent; }}
function collectAncestorAgentsSkillDirs(startDir: string): string[] { const skillDirs: string[] = []; const resolvedStartDir = resolve(startDir); const gitRepoRoot = findGitRepoRoot(resolvedStartDir);
let dir = resolvedStartDir; while (true) { skillDirs.push(join(dir, ".agents", "skills")); if (gitRepoRoot && dir === gitRepoRoot) { break; } const parent = dirname(dir); if (parent === dir) { break; } dir = parent; }
return skillDirs;}
function collectAutoPromptEntries(dir: string): string[] { const entries: string[] = []; if (!existsSync(dir)) return entries;
const ig = ignore(); addIgnoreRules(ig, dir, dir);
try { const dirEntries = readdirSync(dir, { withFileTypes: true }); for (const entry of dirEntries) { if (entry.name.startsWith(".")) continue; if (entry.name === "node_modules") continue;
const fullPath = join(dir, entry.name); let isFile = entry.isFile(); if (entry.isSymbolicLink()) { try { isFile = statSync(fullPath).isFile(); } catch { continue; } }
const relPath = toPosixPath(relative(dir, fullPath)); if (ig.ignores(relPath)) continue;
if (isFile && entry.name.endsWith(".md")) { entries.push(fullPath); } } } catch { // Ignore unreadable directories during prompt discovery. }
return entries;}
function collectAutoThemeEntries(dir: string): string[] { const entries: string[] = []; if (!existsSync(dir)) return entries;
const ig = ignore(); addIgnoreRules(ig, dir, dir);
try { const dirEntries = readdirSync(dir, { withFileTypes: true }); for (const entry of dirEntries) { if (entry.name.startsWith(".")) continue; if (entry.name === "node_modules") continue;
const fullPath = join(dir, entry.name); let isFile = entry.isFile(); if (entry.isSymbolicLink()) { try { isFile = statSync(fullPath).isFile(); } catch { continue; } }
const relPath = toPosixPath(relative(dir, fullPath)); if (ig.ignores(relPath)) continue;
if (isFile && entry.name.endsWith(".json")) { entries.push(fullPath); } } } catch { // Ignore unreadable directories during theme discovery. }
return entries;}
function readPiManifestFile(packageJsonPath: string): PiManifest | null { try { const content = readFileSync(packageJsonPath, "utf-8"); const pkg = JSON.parse(content) as { pi?: PiManifest }; return pkg.pi ?? null; } catch { return null; }}
function resolveExtensionEntries(dir: string): string[] | null { const packageJsonPath = join(dir, "package.json"); if (existsSync(packageJsonPath)) { const manifest = readPiManifestFile(packageJsonPath); if (manifest?.extensions?.length) { const entries: string[] = []; for (const extPath of manifest.extensions) { const resolvedExtPath = resolve(dir, extPath); if (existsSync(resolvedExtPath)) { entries.push(resolvedExtPath); } } if (entries.length > 0) { return entries; } } }
const indexTs = join(dir, "index.ts"); const indexJs = join(dir, "index.js"); if (existsSync(indexTs)) { return [indexTs]; } if (existsSync(indexJs)) { return [indexJs]; }
return null;}
function collectAutoExtensionEntries(dir: string): string[] { const entries: string[] = []; if (!existsSync(dir)) return entries; const rootEntries = resolveExtensionEntries(dir); if (rootEntries) { return rootEntries; } const ig = ignore(); addIgnoreRules(ig, dir, dir);
try { const dirEntries = readdirSync(dir, { withFileTypes: true }); for (const entry of dirEntries) { if (entry.name.startsWith(".")) continue; if (entry.name === "node_modules") continue;
const fullPath = join(dir, entry.name); let isDir = entry.isDirectory(); let isFile = entry.isFile();
if (entry.isSymbolicLink()) { try { const stats = statSync(fullPath); isDir = stats.isDirectory(); isFile = stats.isFile(); } catch { continue; } }
const relPath = toPosixPath(relative(dir, fullPath)); const ignorePath = isDir ? `${relPath}/` : relPath; if (ig.ignores(ignorePath)) continue;
if (isFile && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) { entries.push(fullPath); } else if (isDir) { const resolvedEntries = resolveExtensionEntries(fullPath); if (resolvedEntries) { entries.push(...resolvedEntries); } } } } catch { // Ignore unreadable directories during extension discovery. }
return entries;}
/** * Collect resource files from a directory based on resource type. * Extensions use smart discovery (index.ts in subdirs), others use recursive collection. */function collectResourceFiles(dir: string, resourceType: ResourceType): string[] { if (resourceType === "skills") { return collectSkillEntries(dir, "pi"); } if (resourceType === "extensions") { return collectAutoExtensionEntries(dir); } return collectFiles(dir, FILE_PATTERNS[resourceType]);}
function matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean { const rel = toPosixPath(relative(baseDir, filePath)); const name = basename(filePath); const filePathPosix = toPosixPath(filePath); const isSkillFile = name === "SKILL.md"; const parentDir = isSkillFile ? dirname(filePath) : undefined; const parentRel = isSkillFile ? toPosixPath(relative(baseDir, parentDir!)) : undefined; const parentName = isSkillFile ? basename(parentDir!) : undefined; const parentDirPosix = isSkillFile ? toPosixPath(parentDir!) : undefined;
return patterns.some((pattern) => { const normalizedPattern = toPosixPath(pattern); if ( minimatch(rel, normalizedPattern) || minimatch(name, normalizedPattern) || minimatch(filePathPosix, normalizedPattern) ) { return true; } if (!isSkillFile) return false; return ( minimatch(parentRel!, normalizedPattern) || minimatch(parentName!, normalizedPattern) || minimatch(parentDirPosix!, normalizedPattern) ); });}
function normalizeExactPattern(pattern: string): string { const normalized = pattern.startsWith("./") || pattern.startsWith(".\\") ? pattern.slice(2) : pattern; return toPosixPath(normalized);}
function matchesAnyExactPattern(filePath: string, patterns: string[], baseDir: string): boolean { if (patterns.length === 0) return false; const rel = toPosixPath(relative(baseDir, filePath)); const name = basename(filePath); const filePathPosix = toPosixPath(filePath); const isSkillFile = name === "SKILL.md"; const parentDir = isSkillFile ? dirname(filePath) : undefined; const parentRel = isSkillFile ? toPosixPath(relative(baseDir, parentDir!)) : undefined; const parentDirPosix = isSkillFile ? toPosixPath(parentDir!) : undefined;
return patterns.some((pattern) => { const normalized = normalizeExactPattern(pattern); if (normalized === rel || normalized === filePathPosix) { return true; } if (!isSkillFile) return false; return normalized === parentRel || normalized === parentDirPosix; });}
function getOverridePatterns(entries: string[]): string[] { return entries.filter((pattern) => pattern.startsWith("!") || pattern.startsWith("+") || pattern.startsWith("-"));}
function isEnabledByOverrides(filePath: string, patterns: string[], baseDir: string): boolean { const overrides = getOverridePatterns(patterns); const excludes = overrides.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1)); const forceIncludes = overrides.filter((pattern) => pattern.startsWith("+")).map((pattern) => pattern.slice(1)); const forceExcludes = overrides.filter((pattern) => pattern.startsWith("-")).map((pattern) => pattern.slice(1));
let enabled = true; if (excludes.length > 0 && matchesAnyPattern(filePath, excludes, baseDir)) { enabled = false; } if (forceIncludes.length > 0 && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) { enabled = true; } if (forceExcludes.length > 0 && matchesAnyExactPattern(filePath, forceExcludes, baseDir)) { enabled = false; } return enabled;}
/** * Apply patterns to paths and return a Set of enabled paths. * Pattern types: * - Plain patterns: include matching paths * - `!pattern`: exclude matching paths * - `+path`: force-include exact path (overrides exclusions) * - `-path`: force-exclude exact path (overrides force-includes) */function applyPatterns(allPaths: string[], patterns: string[], baseDir: string): Set<string> { const includes: string[] = []; const excludes: string[] = []; const forceIncludes: string[] = []; const forceExcludes: string[] = [];
for (const p of patterns) { if (p.startsWith("+")) { forceIncludes.push(p.slice(1)); } else if (p.startsWith("-")) { forceExcludes.push(p.slice(1)); } else if (p.startsWith("!")) { excludes.push(p.slice(1)); } else { includes.push(p); } } // Apply patterns in order: includes, excludes, force-includes, then force-excludes. let result: string[]; if (includes.length === 0) { result = [...allPaths]; } else { result = allPaths.filter((filePath) => matchesAnyPattern(filePath, includes, baseDir)); } if (excludes.length > 0) { result = result.filter((filePath) => !matchesAnyPattern(filePath, excludes, baseDir)); } if (forceIncludes.length > 0) { for (const filePath of allPaths) { if (!result.includes(filePath) && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) { result.push(filePath); } } } if (forceExcludes.length > 0) { result = result.filter((filePath) => !matchesAnyExactPattern(filePath, forceExcludes, baseDir)); }
return new Set(result);}
export class DefaultPackageManager implements PackageManager { private cwd: string; private agentDir: string; private settingsManager: SettingsManager; private bundledSkillsDir: string | null; private extraBuiltinSkillOverrides: () => string[]; private globalNpmRoot: string | undefined; private globalNpmRootCommandKey: string | undefined; private progressCallback: ProgressCallback | undefined;
constructor(options: PackageManagerOptions) { this.cwd = options.cwd; this.agentDir = options.agentDir; this.settingsManager = options.settingsManager; this.bundledSkillsDir = options.bundledSkillsDir === undefined ? getBundledSkillsDir() : options.bundledSkillsDir; this.extraBuiltinSkillOverrides = options.extraBuiltinSkillOverrides ?? (() => []); }
setProgressCallback(callback: ProgressCallback | undefined): void { this.progressCallback = callback; }
addSourceToSettings(source: string, options?: { local?: boolean }): boolean { const scope: SourceScope = options?.local ? "project" : "user"; const currentSettings = scope === "project" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings(); const currentPackages = currentSettings.packages ?? []; const normalizedSource = this.normalizePackageSourceForSettings(source, scope); const exists = currentPackages.some((existing) => this.packageSourcesMatch(existing, source, scope)); if (exists) { return false; } const nextPackages = [...currentPackages, normalizedSource]; if (scope === "project") { this.settingsManager.setProjectPackages(nextPackages); } else { this.settingsManager.setPackages(nextPackages); } return true; }
removeSourceFromSettings(source: string, options?: { local?: boolean }): boolean { const scope: SourceScope = options?.local ? "project" : "user"; const currentSettings = scope === "project" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings(); const currentPackages = currentSettings.packages ?? []; const nextPackages = currentPackages.filter((existing) => !this.packageSourcesMatch(existing, source, scope)); const changed = nextPackages.length !== currentPackages.length; if (!changed) { return false; } if (scope === "project") { this.settingsManager.setProjectPackages(nextPackages); } else { this.settingsManager.setPackages(nextPackages); } return true; }
getInstalledPath(source: string, scope: "user" | "project"): string | undefined { const parsed = this.parseSource(source); if (parsed.type === "npm") { const path = this.getNpmInstallPath(parsed, scope); return existsSync(path) ? path : undefined; } if (parsed.type === "git") { const path = this.getGitInstallPath(parsed, scope); return existsSync(path) ? path : undefined; } if (parsed.type === "local") { const baseDir = this.getBaseDirForScope(scope); const path = this.resolvePathFromBase(parsed.path, baseDir); return existsSync(path) ? path : undefined; } return undefined; }
private emitProgress(event: ProgressEvent): void { this.progressCallback?.(event); }
private async withProgress( action: ProgressEvent["action"], source: string, message: string, operation: () => Promise<void>, ): Promise<void> { this.emitProgress({ type: "start", action, source, message }); try { await operation(); this.emitProgress({ type: "complete", action, source }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.emitProgress({ type: "error", action, source, message: errorMessage }); throw error; } }
async resolve(onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths> { const accumulator = this.createAccumulator(); const globalSettings = this.settingsManager.getGlobalSettings(); const projectSettings = this.settingsManager.getProjectSettings(); // Project resources win collisions with global resources. const allPackages: Array<{ pkg: PackageSource; scope: SourceScope }> = []; for (const pkg of projectSettings.packages ?? []) { allPackages.push({ pkg, scope: "project" }); } for (const pkg of globalSettings.packages ?? []) { allPackages.push({ pkg, scope: "user" }); } // Deduplicate by package identity after recording project precedence. const packageSources = this.dedupePackages(allPackages); await this.resolvePackageSources(packageSources, accumulator, onMissing);
const globalBaseDir = this.agentDir; const projectBaseDir = join(this.cwd, CONFIG_DIR_NAME);
for (const resourceType of RESOURCE_TYPES) { const target = this.getTargetMap(accumulator, resourceType); const globalEntries = (globalSettings[resourceType] ?? []) as string[]; const projectEntries = (projectSettings[resourceType] ?? []) as string[]; this.resolveLocalEntries( projectEntries, resourceType, target, { source: "local", scope: "project", origin: "top-level", }, projectBaseDir, ); this.resolveLocalEntries( globalEntries, resourceType, target, { source: "local", scope: "user", origin: "top-level", }, globalBaseDir, ); }
this.addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDir);
return this.toResolvedPaths(accumulator); }
async resolveExtensionSources( sources: string[], options?: { local?: boolean; temporary?: boolean }, ): Promise<ResolvedPaths> { const accumulator = this.createAccumulator(); const scope: SourceScope = options?.temporary ? "temporary" : options?.local ? "project" : "user"; const packageSources = sources.map((source) => ({ pkg: source as PackageSource, scope })); await this.resolvePackageSources(packageSources, accumulator); return this.toResolvedPaths(accumulator); }
listConfiguredPackages(): ConfiguredPackage[] { const globalSettings = this.settingsManager.getGlobalSettings(); const projectSettings = this.settingsManager.getProjectSettings(); const configuredPackages: ConfiguredPackage[] = [];
for (const pkg of globalSettings.packages ?? []) { const source = typeof pkg === "string" ? pkg : pkg.source; configuredPackages.push({ source, scope: "user", filtered: typeof pkg === "object", installedPath: this.getInstalledPath(source, "user"), }); }
for (const pkg of projectSettings.packages ?? []) { const source = typeof pkg === "string" ? pkg : pkg.source; configuredPackages.push({ source, scope: "project", filtered: typeof pkg === "object", installedPath: this.getInstalledPath(source, "project"), }); }
return configuredPackages; }
async install(source: string, options?: { local?: boolean }): Promise<void> { const parsed = this.parseSource(source); const scope: SourceScope = options?.local ? "project" : "user"; await this.withProgress("install", source, `Installing ${source}...`, async () => { if (parsed.type === "npm") { await this.installNpm(parsed, scope, false); return; } if (parsed.type === "git") { await this.installGit(parsed, scope); return; } if (parsed.type === "local") { const resolved = this.resolvePath(parsed.path); if (!existsSync(resolved)) { throw new Error(`Path does not exist: ${resolved}`); } return; } throw new Error(`Unsupported install source: ${source}`); }); }
async installAndPersist(source: string, options?: { local?: boolean }): Promise<void> { await this.install(source, options); this.addSourceToSettings(source, options); }
async remove(source: string, options?: { local?: boolean }): Promise<void> { const parsed = this.parseSource(source); const scope: SourceScope = options?.local ? "project" : "user"; await this.withProgress("remove", source, `Removing ${source}...`, async () => { if (parsed.type === "npm") { await this.uninstallNpm(parsed, scope); return; } if (parsed.type === "git") { await this.removeGit(parsed, scope); return; } if (parsed.type === "local") { return; } throw new Error(`Unsupported remove source: ${source}`); }); }
async removeAndPersist(source: string, options?: { local?: boolean }): Promise<boolean> { await this.remove(source, options); return this.removeSourceFromSettings(source, options); }
async update(source?: string): Promise<void> { const globalSettings = this.settingsManager.getGlobalSettings(); const projectSettings = this.settingsManager.getProjectSettings(); const identity = source ? this.getPackageIdentity(source) : undefined; let matched = false; const updateSources: ConfiguredUpdateSource[] = [];
for (const pkg of globalSettings.packages ?? []) { const sourceStr = typeof pkg === "string" ? pkg : pkg.source; if (identity && this.getPackageIdentity(sourceStr, "user") !== identity) continue; matched = true; updateSources.push({ source: sourceStr, scope: "user" }); } for (const pkg of projectSettings.packages ?? []) { const sourceStr = typeof pkg === "string" ? pkg : pkg.source; if (identity && this.getPackageIdentity(sourceStr, "project") !== identity) continue; matched = true; updateSources.push({ source: sourceStr, scope: "project" }); }
if (source && !matched) { throw new Error( this.buildNoMatchingPackageMessage(source, [ ...(globalSettings.packages ?? []), ...(projectSettings.packages ?? []), ]), ); }
await this.updateConfiguredSources(updateSources); }
private async updateConfiguredSources(sources: ConfiguredUpdateSource[]): Promise<void> { if (isOfflineModeEnabled() || sources.length === 0) { return; }
const npmCandidates: NpmUpdateTarget[] = []; const gitCandidates: GitUpdateTarget[] = [];
for (const entry of sources) { const parsed = this.parseSource(entry.source); if (parsed.type === "local" || parsed.pinned) { continue; } if (parsed.type === "npm") { npmCandidates.push({ ...entry, parsed }); continue; } gitCandidates.push({ ...entry, parsed }); }
const npmCheckTasks = npmCandidates.map((entry) => async () => ({ entry, shouldUpdate: await this.shouldUpdateNpmSource(entry.parsed, entry.scope), })); const npmCheckResults = await this.runWithConcurrency(npmCheckTasks, UPDATE_CHECK_CONCURRENCY); const userNpmUpdates: NpmUpdateTarget[] = []; const projectNpmUpdates: NpmUpdateTarget[] = []; for (const result of npmCheckResults) { if (!result.shouldUpdate) { continue; } if (result.entry.scope === "user") { userNpmUpdates.push(result.entry); } else { projectNpmUpdates.push(result.entry); } }
const tasks: Promise<void>[] = []; if (userNpmUpdates.length > 0) { tasks.push(this.updateNpmBatch(userNpmUpdates, "user")); } if (projectNpmUpdates.length > 0) { tasks.push(this.updateNpmBatch(projectNpmUpdates, "project")); } if (gitCandidates.length > 0) { const gitTasks = gitCandidates.map( (entry) => async () => this.withProgress("update", entry.source, `Updating ${entry.source}...`, async () => { await this.updateGit(entry.parsed, entry.scope); }), ); tasks.push(this.runWithConcurrency(gitTasks, GIT_UPDATE_CONCURRENCY).then(() => {})); }
await Promise.all(tasks); }
private async shouldUpdateNpmSource(source: NpmSource, scope: InstalledSourceScope): Promise<boolean> { const installedPath = this.getNpmInstallPath(source, scope); const installedVersion = existsSync(installedPath) ? this.getInstalledNpmVersion(installedPath) : undefined; if (!installedVersion) { return true; }
try { const latestVersion = await this.getLatestNpmVersion(source.name); return latestVersion !== installedVersion; } catch { // Preserve the existing update policy when version lookup fails. return true; } }
private async updateNpmBatch(sources: NpmUpdateTarget[], scope: InstalledSourceScope): Promise<void> { if (sources.length === 0) { return; }
const sourceLabel = sources.length === 1 ? sources[0].source : `${scope} npm packages`; const message = sources.length === 1 ? `Updating ${sources[0].source}...` : `Updating ${scope} npm packages...`; const specs = sources.map((entry) => `${entry.parsed.name}@latest`);
await this.withProgress("update", sourceLabel, message, async () => { await this.installNpmBatch(specs, scope); }); }
private async installNpmBatch(specs: string[], scope: InstalledSourceScope): Promise<void> { if (scope === "user") { await this.runNpmCommand(["install", "-g", ...specs]); return; } const installRoot = this.getNpmInstallRoot(scope, false); this.ensureNpmProject(installRoot); await this.runNpmCommand(["install", ...specs, "--prefix", installRoot]); }
async checkForAvailableUpdates(): Promise<PackageUpdate[]> { if (isOfflineModeEnabled()) { return []; }
const globalSettings = this.settingsManager.getGlobalSettings(); const projectSettings = this.settingsManager.getProjectSettings(); const allPackages: Array<{ pkg: PackageSource; scope: SourceScope }> = []; for (const pkg of projectSettings.packages ?? []) { allPackages.push({ pkg, scope: "project" }); } for (const pkg of globalSettings.packages ?? []) { allPackages.push({ pkg, scope: "user" }); }
const packageSources = this.dedupePackages(allPackages); const checks = packageSources .filter( (entry): entry is { pkg: PackageSource; scope: Exclude<SourceScope, "temporary"> } => entry.scope !== "temporary", ) .map((entry) => async (): Promise<PackageUpdate | undefined> => { const source = typeof entry.pkg === "string" ? entry.pkg : entry.pkg.source; const parsed = this.parseSource(source); if (parsed.type === "local" || parsed.pinned) { return undefined; }
if (parsed.type === "npm") { const installedPath = this.getNpmInstallPath(parsed, entry.scope); if (!existsSync(installedPath)) { return undefined; } const hasUpdate = await this.npmHasAvailableUpdate(parsed, installedPath); if (!hasUpdate) { return undefined; } return { source, displayName: parsed.name, type: "npm", scope: entry.scope, }; }
const installedPath = this.getGitInstallPath(parsed, entry.scope); if (!existsSync(installedPath)) { return undefined; } const hasUpdate = await this.gitHasAvailableUpdate(installedPath); if (!hasUpdate) { return undefined; } return { source, displayName: `${parsed.host}/${parsed.path}`, type: "git", scope: entry.scope, }; });
const results = await this.runWithConcurrency(checks, UPDATE_CHECK_CONCURRENCY); return results.filter((result): result is PackageUpdate => result !== undefined); }
private async resolvePackageSources( sources: Array<{ pkg: PackageSource; scope: SourceScope }>, accumulator: ResourceAccumulator, onMissing?: (source: string) => Promise<MissingSourceAction>, ): Promise<void> { for (const { pkg, scope } of sources) { const sourceStr = typeof pkg === "string" ? pkg : pkg.source; const filter = typeof pkg === "object" ? pkg : undefined; const parsed = this.parseSource(sourceStr); const metadata: PathMetadata = { source: sourceStr, scope, origin: "package" };
if (parsed.type === "local") { const baseDir = this.getBaseDirForScope(scope); this.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir); continue; }
const installMissing = async (): Promise<boolean> => { if (isOfflineModeEnabled()) { return false; } if (!onMissing) { await this.installParsedSource(parsed, scope); return true; } const action = await onMissing(sourceStr); if (action === "skip") return false; if (action === "error") throw new Error(`Missing source: ${sourceStr}`); await this.installParsedSource(parsed, scope); return true; };
if (parsed.type === "npm") { const installedPath = this.getNpmInstallPath(parsed, scope); const needsInstall = !existsSync(installedPath) || (parsed.pinned && !(await this.installedNpmMatchesPinnedVersion(parsed, installedPath))); if (needsInstall) { const installed = await installMissing(); if (!installed) continue; } metadata.baseDir = installedPath; this.collectPackageResources(installedPath, accumulator, filter, metadata); continue; }
if (parsed.type === "git") { const installedPath = this.getGitInstallPath(parsed, scope); if (!existsSync(installedPath)) { const installed = await installMissing(); if (!installed) continue; } else if (scope === "temporary" && !parsed.pinned && !isOfflineModeEnabled()) { await this.refreshTemporaryGitSource(parsed, sourceStr); } metadata.baseDir = installedPath; this.collectPackageResources(installedPath, accumulator, filter, metadata); } } }
private resolveLocalExtensionSource( source: LocalSource, accumulator: ResourceAccumulator, filter: PackageFilter | undefined, metadata: PathMetadata, baseDir: string, ): void { const resolved = this.resolvePathFromBase(source.path, baseDir); if (!existsSync(resolved)) { return; }
try { const stats = statSync(resolved); if (stats.isFile()) { metadata.baseDir = dirname(resolved); this.addResource(accumulator.extensions, resolved, metadata, true); return; } if (stats.isDirectory()) { metadata.baseDir = resolved; const resources = this.collectPackageResources(resolved, accumulator, filter, metadata); if (!resources) { this.addResource(accumulator.extensions, resolved, metadata, true); } } } catch { return; } }
private async installParsedSource(parsed: ParsedSource, scope: SourceScope): Promise<void> { if (parsed.type === "npm") { await this.installNpm(parsed, scope, scope === "temporary"); return; } if (parsed.type === "git") { await this.installGit(parsed, scope); return; } }
private getPackageSourceString(pkg: PackageSource): string { return typeof pkg === "string" ? pkg : pkg.source; }
private getSourceMatchKeyForInput(source: string): string { const parsed = this.parseSource(source); if (parsed.type === "npm") { return `npm:${parsed.name}`; } if (parsed.type === "git") { return `git:${parsed.host}/${parsed.path}`; } return `local:${this.resolvePath(parsed.path)}`; }
private getSourceMatchKeyForSettings(source: string, scope: SourceScope): string { const parsed = this.parseSource(source); if (parsed.type === "npm") { return `npm:${parsed.name}`; } if (parsed.type === "git") { return `git:${parsed.host}/${parsed.path}`; } const baseDir = this.getBaseDirForScope(scope); return `local:${this.resolvePathFromBase(parsed.path, baseDir)}`; }
private buildNoMatchingPackageMessage(source: string, configuredPackages: PackageSource[]): string { const suggestion = this.findSuggestedConfiguredSource(source, configuredPackages); if (!suggestion) { return `No matching package found for ${source}`; } return `No matching package found for ${source}. Did you mean ${suggestion}?`; }
private findSuggestedConfiguredSource(source: string, configuredPackages: PackageSource[]): string | undefined { const trimmedSource = source.trim(); const suggestions = new Set<string>();
for (const pkg of configuredPackages) { const sourceStr = this.getPackageSourceString(pkg); const parsed = this.parseSource(sourceStr); if (parsed.type === "npm") { if (trimmedSource === parsed.name || trimmedSource === parsed.spec) { suggestions.add(sourceStr); } continue; } if (parsed.type === "git") { const shorthand = `${parsed.host}/${parsed.path}`; const shorthandWithRef = parsed.ref ? `${shorthand}@${parsed.ref}` : undefined; if (trimmedSource === shorthand || (shorthandWithRef && trimmedSource === shorthandWithRef)) { suggestions.add(sourceStr); } } }
return suggestions.values().next().value; }
private packageSourcesMatch(existing: PackageSource, inputSource: string, scope: SourceScope): boolean { const left = this.getSourceMatchKeyForSettings(this.getPackageSourceString(existing), scope); const right = this.getSourceMatchKeyForInput(inputSource); return left === right; }
private normalizePackageSourceForSettings(source: string, scope: SourceScope): string { const parsed = this.parseSource(source); if (parsed.type !== "local") { return source; } const baseDir = this.getBaseDirForScope(scope); const resolved = this.resolvePath(parsed.path); const rel = relative(baseDir, resolved); return rel || "."; }
private parseSource(source: string): ParsedSource { if (source.startsWith("npm:")) { const spec = source.slice("npm:".length).trim(); const { name, version } = this.parseNpmSpec(spec); return { type: "npm", spec, name, pinned: Boolean(version), }; }
if (isLocalPath(source)) { return { type: "local", path: source }; } const gitParsed = parseGitUrl(source); if (gitParsed) { return gitParsed; }
return { type: "local", path: source }; }
private async installedNpmMatchesPinnedVersion(source: NpmSource, installedPath: string): Promise<boolean> { const installedVersion = this.getInstalledNpmVersion(installedPath); if (!installedVersion) { return false; }
const { version: pinnedVersion } = this.parseNpmSpec(source.spec); if (!pinnedVersion) { return true; }
return installedVersion === pinnedVersion; }
private async npmHasAvailableUpdate(source: NpmSource, installedPath: string): Promise<boolean> { if (isOfflineModeEnabled()) { return false; }
const installedVersion = this.getInstalledNpmVersion(installedPath); if (!installedVersion) { return false; }
try { const latestVersion = await this.getLatestNpmVersion(source.name); return latestVersion !== installedVersion; } catch { return false; } }
private getInstalledNpmVersion(installedPath: string): string | undefined { const packageJsonPath = join(installedPath, "package.json"); if (!existsSync(packageJsonPath)) return undefined; try { const content = readFileSync(packageJsonPath, "utf-8"); const pkg = JSON.parse(content) as { version?: string }; return pkg.version; } catch { return undefined; } }
private async getLatestNpmVersion(packageName: string): Promise<string> { const npmCommand = this.getNpmCommand(); const stdout = await this.runCommandCapture( npmCommand.command, [...npmCommand.args, "view", packageName, "version", "--json"], { cwd: this.cwd, timeoutMs: NETWORK_TIMEOUT_MS }, ); const raw = stdout.trim(); if (!raw) throw new Error("Empty response from npm view"); return JSON.parse(raw); }
private async gitHasAvailableUpdate(installedPath: string): Promise<boolean> { if (isOfflineModeEnabled()) { return false; }
try { const localHead = await this.runCommandCapture("git", ["rev-parse", "HEAD"], { cwd: installedPath, timeoutMs: NETWORK_TIMEOUT_MS, }); const remoteHead = await this.getRemoteGitHead(installedPath); return localHead.trim() !== remoteHead.trim(); } catch { return false; } }
private async getRemoteGitHead(installedPath: string): Promise<string> { const upstreamRef = await this.getGitUpstreamRef(installedPath); if (upstreamRef) { const remoteHead = await this.runGitRemoteCommand(installedPath, ["ls-remote", "origin", upstreamRef]); const match = remoteHead.match(/^([0-9a-f]{40})\s+/m); if (match?.[1]) { return match[1]; } }
const remoteHead = await this.runGitRemoteCommand(installedPath, ["ls-remote", "origin", "HEAD"]); const match = remoteHead.match(/^([0-9a-f]{40})\s+HEAD$/m); if (!match?.[1]) { throw new Error("Failed to determine remote HEAD"); } return match[1]; }
private async getLocalGitUpdateTarget( installedPath: string, ): Promise<{ ref: string; head: string; fetchArgs: string[] }> { try { const upstream = await this.runCommandCapture("git", ["rev-parse", "--abbrev-ref", "@{upstream}"], { cwd: installedPath, timeoutMs: NETWORK_TIMEOUT_MS, }); const trimmedUpstream = upstream.trim(); if (!trimmedUpstream.startsWith("origin/")) { throw new Error(`Unsupported upstream remote: ${trimmedUpstream}`); } const branch = trimmedUpstream.slice("origin/".length); if (!branch) { throw new Error("Missing upstream branch name"); } const head = await this.runCommandCapture("git", ["rev-parse", "@{upstream}"], { cwd: installedPath, timeoutMs: NETWORK_TIMEOUT_MS, }); return { ref: "@{upstream}", head, fetchArgs: [ "fetch", "--prune", "--no-tags", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`, ], }; } catch { await this.runCommand("git", ["remote", "set-head", "origin", "-a"], { cwd: installedPath }).catch(() => {}); const head = await this.runCommandCapture("git", ["rev-parse", "origin/HEAD"], { cwd: installedPath, timeoutMs: NETWORK_TIMEOUT_MS, }); const originHeadRef = await this.runCommandCapture("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: installedPath, timeoutMs: NETWORK_TIMEOUT_MS, }).catch(() => ""); const branch = originHeadRef.trim().replace(/^refs\/remotes\/origin\//, ""); if (branch) { return { ref: "origin/HEAD", head, fetchArgs: [ "fetch", "--prune", "--no-tags", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`, ], }; } return { ref: "origin/HEAD", head, fetchArgs: ["fetch", "--prune", "--no-tags", "origin", "+HEAD:refs/remotes/origin/HEAD"], }; } }
private async getGitUpstreamRef(installedPath: string): Promise<string | undefined> { try { const upstream = await this.runCommandCapture("git", ["rev-parse", "--abbrev-ref", "@{upstream}"], { cwd: installedPath, timeoutMs: NETWORK_TIMEOUT_MS, }); const trimmed = upstream.trim(); if (!trimmed.startsWith("origin/")) { return undefined; } const branch = trimmed.slice("origin/".length); return branch ? `refs/heads/${branch}` : undefined; } catch { return undefined; } }
private runGitRemoteCommand(installedPath: string, args: string[]): Promise<string> { return this.runCommandCapture("git", args, { cwd: installedPath, timeoutMs: NETWORK_TIMEOUT_MS, env: { GIT_TERMINAL_PROMPT: "0", }, }); }
private async runWithConcurrency<T>(tasks: Array<() => Promise<T>>, limit: number): Promise<T[]> { if (tasks.length === 0) { return []; }
const results: T[] = new Array(tasks.length); let nextIndex = 0; const workerCount = Math.max(1, Math.min(limit, tasks.length));
const worker = async () => { while (true) { const index = nextIndex; nextIndex += 1; if (index >= tasks.length) { return; } results[index] = await tasks[index](); } };
await Promise.all(Array.from({ length: workerCount }, () => worker())); return results; }
/** * Get a unique identity for a package, ignoring version/ref. * Used to detect when the same package is in both global and project settings. * For git packages, uses normalized host/path to ensure SSH and HTTPS URLs * for the same repository are treated as identical. */ private getPackageIdentity(source: string, scope?: SourceScope): string { const parsed = this.parseSource(source); if (parsed.type === "npm") { return `npm:${parsed.name}`; } if (parsed.type === "git") { // Use host/path for identity to normalize SSH and HTTPS return `git:${parsed.host}/${parsed.path}`; } if (scope) { const baseDir = this.getBaseDirForScope(scope); return `local:${this.resolvePathFromBase(parsed.path, baseDir)}`; } return `local:${this.resolvePath(parsed.path)}`; }
/** * Dedupe packages: if same package identity appears in both global and project, * keep only the project one (project wins). */ private dedupePackages( packages: Array<{ pkg: PackageSource; scope: SourceScope }>, ): Array<{ pkg: PackageSource; scope: SourceScope }> { const seen = new Map<string, { pkg: PackageSource; scope: SourceScope }>();
for (const entry of packages) { const sourceStr = typeof entry.pkg === "string" ? entry.pkg : entry.pkg.source; const identity = this.getPackageIdentity(sourceStr, entry.scope);
const existing = seen.get(identity); if (!existing) { seen.set(identity, entry); } else if (entry.scope === "project" && existing.scope === "user") { seen.set(identity, entry); } }
return Array.from(seen.values()); }
private parseNpmSpec(spec: string): { name: string; version?: string } { const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/); if (!match) { return { name: spec }; } const name = match[1] ?? spec; const version = match[2]; return { name, version }; }
private getNpmCommand(): { command: string; args: string[] } { const configuredCommand = this.settingsManager.getNpmCommand(); if (!configuredCommand || configuredCommand.length === 0) { return { command: "npm", args: [] }; } const [command, ...args] = configuredCommand; if (!command) { throw new Error("Invalid npmCommand: first array entry must be a non-empty command"); } return { command, args }; }
private async runNpmCommand(args: string[], options?: { cwd?: string }): Promise<void> { const npmCommand = this.getNpmCommand(); await this.runCommand(npmCommand.command, [...npmCommand.args, ...args], options); }
private getGitDependencyInstallArgs(): string[] { const configuredCommand = this.settingsManager.getNpmCommand(); if (configuredCommand && configuredCommand.length > 0) { return ["install"]; } return ["install", "--omit=dev"]; }
private runNpmCommandSync(args: string[]): string { const npmCommand = this.getNpmCommand(); return this.runCommandSync(npmCommand.command, [...npmCommand.args, ...args]); }
private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> { if (scope === "user" && !temporary) { await this.runNpmCommand(["install", "-g", source.spec]); return; } const installRoot = this.getNpmInstallRoot(scope, temporary); this.ensureNpmProject(installRoot); await this.runNpmCommand(["install", source.spec, "--prefix", installRoot]); }
private async uninstallNpm(source: NpmSource, scope: SourceScope): Promise<void> { if (scope === "user") { await this.runNpmCommand(["uninstall", "-g", source.name]); return; } const installRoot = this.getNpmInstallRoot(scope, false); if (!existsSync(installRoot)) { return; } await this.runNpmCommand(["uninstall", source.name, "--prefix", installRoot]); }
private async installGit(source: GitSource, scope: SourceScope): Promise<void> { const targetDir = this.getGitInstallPath(source, scope); if (existsSync(targetDir)) { return; } const gitRoot = this.getGitInstallRoot(scope); if (gitRoot) { this.ensureGitIgnore(gitRoot); } mkdirSync(dirname(targetDir), { recursive: true });
await this.runCommand("git", ["clone", source.repo, targetDir]); if (source.ref) { await this.runCommand("git", ["checkout", source.ref], { cwd: targetDir }); } const packageJsonPath = join(targetDir, "package.json"); if (existsSync(packageJsonPath)) { await this.runNpmCommand(this.getGitDependencyInstallArgs(), { cwd: targetDir }); } }
private async updateGit(source: GitSource, scope: SourceScope): Promise<void> { const targetDir = this.getGitInstallPath(source, scope); if (!existsSync(targetDir)) { await this.installGit(source, scope); return; }
const target = await this.getLocalGitUpdateTarget(targetDir);
await this.runCommand("git", target.fetchArgs, { cwd: targetDir });
const localHead = await this.runCommandCapture("git", ["rev-parse", "HEAD"], { cwd: targetDir, timeoutMs: NETWORK_TIMEOUT_MS, }); const refreshedTargetHead = await this.runCommandCapture("git", ["rev-parse", target.ref], { cwd: targetDir, timeoutMs: NETWORK_TIMEOUT_MS, }); if (localHead.trim() === refreshedTargetHead.trim()) { return; }
await this.runCommand("git", ["reset", "--hard", target.ref], { cwd: targetDir });
// Extension checkouts must be pristine after an update. await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir });
const packageJsonPath = join(targetDir, "package.json"); if (existsSync(packageJsonPath)) { await this.runNpmCommand(this.getGitDependencyInstallArgs(), { cwd: targetDir }); } }
private async refreshTemporaryGitSource(source: GitSource, sourceStr: string): Promise<void> { if (isOfflineModeEnabled()) { return; } try { await this.withProgress("pull", sourceStr, `Refreshing ${sourceStr}...`, async () => { await this.updateGit(source, "temporary"); }); } catch { // Keep the cached temporary checkout if refresh fails. } }
private async removeGit(source: GitSource, scope: SourceScope): Promise<void> { const targetDir = this.getGitInstallPath(source, scope); if (!existsSync(targetDir)) return; rmSync(targetDir, { recursive: true, force: true }); this.pruneEmptyGitParents(targetDir, this.getGitInstallRoot(scope)); }
private pruneEmptyGitParents(targetDir: string, installRoot: string | undefined): void { if (!installRoot) return; const resolvedRoot = resolve(installRoot); let current = dirname(targetDir); while (current.startsWith(resolvedRoot) && current !== resolvedRoot) { if (!existsSync(current)) { current = dirname(current); continue; } const entries = readdirSync(current); if (entries.length > 0) { break; } try { rmSync(current, { recursive: true, force: true }); } catch { break; } current = dirname(current); } }
private ensureNpmProject(installRoot: string): void { if (!existsSync(installRoot)) { mkdirSync(installRoot, { recursive: true }); } this.ensureGitIgnore(installRoot); const packageJsonPath = join(installRoot, "package.json"); if (!existsSync(packageJsonPath)) { const pkgJson = { name: "pi-extensions", private: true }; writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2), "utf-8"); } }
private ensureGitIgnore(dir: string): void { if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } const ignorePath = join(dir, ".gitignore"); if (!existsSync(ignorePath)) { writeFileSync(ignorePath, "*\n!.gitignore\n", "utf-8"); } }
private getNpmInstallRoot(scope: SourceScope, temporary: boolean): string { if (temporary) { return this.getTemporaryDir("npm"); } if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME, "npm"); } return join(this.getGlobalNpmRoot(), ".."); }
private getGlobalNpmRoot(): string { const npmCommand = this.getNpmCommand(); const commandKey = [npmCommand.command, ...npmCommand.args].join("\0"); if (this.globalNpmRoot && this.globalNpmRootCommandKey === commandKey) { return this.globalNpmRoot; } const isBunPackageManager = npmCommand.command === "bun"; if (isBunPackageManager) { const binDir = this.runNpmCommandSync(["pm", "bin", "-g"]).trim(); this.globalNpmRoot = join(dirname(binDir), "install", "global", "node_modules"); } else { this.globalNpmRoot = this.runNpmCommandSync(["root", "-g"]).trim(); } this.globalNpmRootCommandKey = commandKey; return this.globalNpmRoot; }
private getNpmInstallPath(source: NpmSource, scope: SourceScope): string { if (scope === "temporary") { return join(this.getTemporaryDir("npm"), "node_modules", source.name); } if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name); } return join(this.getGlobalNpmRoot(), source.name); }
private getGitInstallPath(source: GitSource, scope: SourceScope): string { if (scope === "temporary") { return this.getTemporaryDir(`git-${source.host}`, source.path); } if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME, "git", source.host, source.path); } return join(this.agentDir, "git", source.host, source.path); }
private getGitInstallRoot(scope: SourceScope): string | undefined { if (scope === "temporary") { return undefined; } if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME, "git"); } return join(this.agentDir, "git"); }
private getTemporaryDir(prefix: string, suffix?: string): string { const hash = createHash("sha256") .update(`${prefix}-${suffix ?? ""}`) .digest("hex") .slice(0, 8); return join(tmpdir(), "pi-extensions", prefix, hash, suffix ?? ""); }
private getBaseDirForScope(scope: SourceScope): string { if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME); } if (scope === "user") { return this.agentDir; } return this.cwd; }
private resolvePath(input: string): string { const trimmed = input.trim(); if (trimmed === "~") return getHomeDir(); if (trimmed.startsWith("~/")) return join(getHomeDir(), trimmed.slice(2)); if (trimmed.startsWith("~")) return join(getHomeDir(), trimmed.slice(1)); return resolve(this.cwd, trimmed); }
private resolvePathFromBase(input: string, baseDir: string): string { const trimmed = input.trim(); if (trimmed === "~") return getHomeDir(); if (trimmed.startsWith("~/")) return join(getHomeDir(), trimmed.slice(2)); if (trimmed.startsWith("~")) return join(getHomeDir(), trimmed.slice(1)); return resolve(baseDir, trimmed); }
private collectPackageResources( packageRoot: string, accumulator: ResourceAccumulator, filter: PackageFilter | undefined, metadata: PathMetadata, ): boolean { if (filter) { for (const resourceType of RESOURCE_TYPES) { const patterns = filter[resourceType as keyof PackageFilter]; const target = this.getTargetMap(accumulator, resourceType); if (patterns !== undefined) { this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata); } else { this.collectDefaultResources(packageRoot, resourceType, target, metadata); } } return true; }
const manifest = this.readPiManifest(packageRoot); if (manifest) { for (const resourceType of RESOURCE_TYPES) { const entries = manifest[resourceType as keyof PiManifest]; this.addManifestEntries( entries, packageRoot, resourceType, this.getTargetMap(accumulator, resourceType), metadata, ); } return true; }
let hasAnyDir = false; for (const resourceType of RESOURCE_TYPES) { const dir = join(packageRoot, resourceType); if (existsSync(dir)) { const files = collectResourceFiles(dir, resourceType); for (const f of files) { this.addResource(this.getTargetMap(accumulator, resourceType), f, metadata, true); } hasAnyDir = true; } } return hasAnyDir; }
private collectDefaultResources( packageRoot: string, resourceType: ResourceType, target: Map<string, { metadata: PathMetadata; enabled: boolean }>, metadata: PathMetadata, ): void { const manifest = this.readPiManifest(packageRoot); const entries = manifest?.[resourceType as keyof PiManifest]; if (entries) { this.addManifestEntries(entries, packageRoot, resourceType, target, metadata); return; } const dir = join(packageRoot, resourceType); if (existsSync(dir)) { const files = collectResourceFiles(dir, resourceType); for (const f of files) { this.addResource(target, f, metadata, true); } } }
private applyPackageFilter( packageRoot: string, userPatterns: string[], resourceType: ResourceType, target: Map<string, { metadata: PathMetadata; enabled: boolean }>, metadata: PathMetadata, ): void { const { allFiles } = this.collectManifestFiles(packageRoot, resourceType);
// An explicit empty list disables this resource type. if (userPatterns.length === 0) { for (const f of allFiles) { this.addResource(target, f, metadata, false); } return; } const enabledByUser = applyPatterns(allFiles, userPatterns, packageRoot);
for (const f of allFiles) { const enabled = enabledByUser.has(f); this.addResource(target, f, metadata, enabled); } }
/** * Collect all files from a package for a resource type, applying manifest patterns. * Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files * that pass the manifest's own patterns. */ private collectManifestFiles( packageRoot: string, resourceType: ResourceType, ): { allFiles: string[]; enabledByManifest: Set<string> } { const manifest = this.readPiManifest(packageRoot); const entries = manifest?.[resourceType as keyof PiManifest]; if (entries && entries.length > 0) { const allFiles = this.collectFilesFromManifestEntries(entries, packageRoot, resourceType); const manifestPatterns = entries.filter(isOverridePattern); const enabledByManifest = manifestPatterns.length > 0 ? applyPatterns(allFiles, manifestPatterns, packageRoot) : new Set(allFiles); return { allFiles: Array.from(enabledByManifest), enabledByManifest }; }
const conventionDir = join(packageRoot, resourceType); if (!existsSync(conventionDir)) { return { allFiles: [], enabledByManifest: new Set() }; } const allFiles = collectResourceFiles(conventionDir, resourceType); return { allFiles, enabledByManifest: new Set(allFiles) }; }
private readPiManifest(packageRoot: string): PiManifest | null { const packageJsonPath = join(packageRoot, "package.json"); if (!existsSync(packageJsonPath)) { return null; }
try { const content = readFileSync(packageJsonPath, "utf-8"); const pkg = JSON.parse(content) as { pi?: PiManifest }; return pkg.pi ?? null; } catch { return null; } }
private addManifestEntries( entries: string[] | undefined, root: string, resourceType: ResourceType, target: Map<string, { metadata: PathMetadata; enabled: boolean }>, metadata: PathMetadata, ): void { if (!entries) return;
const allFiles = this.collectFilesFromManifestEntries(entries, root, resourceType); const patterns = entries.filter(isOverridePattern); const enabledPaths = applyPatterns(allFiles, patterns, root);
for (const f of allFiles) { if (enabledPaths.has(f)) { this.addResource(target, f, metadata, true); } } }
private collectFilesFromManifestEntries(entries: string[], root: string, resourceType: ResourceType): string[] { const sourceEntries = entries.filter((entry) => !isOverridePattern(entry)); const resolved = sourceEntries.flatMap((entry) => { if (!hasGlobPattern(entry)) { return [resolve(root, entry)]; }
return globSync(entry, { cwd: root, absolute: true, dot: false, nodir: false, }).map((match) => resolve(match)); }); return this.collectFilesFromPaths(resolved, resourceType); }
private resolveLocalEntries( entries: string[], resourceType: ResourceType, target: Map<string, { metadata: PathMetadata; enabled: boolean }>, metadata: PathMetadata, baseDir: string, ): void { if (entries.length === 0) return; const { plain, patterns } = splitPatterns(entries); const resolvedPlain = plain.map((p) => this.resolvePathFromBase(p, baseDir)); const allFiles = this.collectFilesFromPaths(resolvedPlain, resourceType); const enabledPaths = applyPatterns(allFiles, patterns, baseDir); for (const f of allFiles) { this.addResource(target, f, metadata, enabledPaths.has(f)); } }
private addAutoDiscoveredResources( accumulator: ResourceAccumulator, globalSettings: ReturnType<SettingsManager["getGlobalSettings"]>, projectSettings: ReturnType<SettingsManager["getProjectSettings"]>, globalBaseDir: string, projectBaseDir: string, ): void { const userMetadata: PathMetadata = { source: "auto", scope: "user", origin: "top-level", baseDir: globalBaseDir, }; const projectMetadata: PathMetadata = { source: "auto", scope: "project", origin: "top-level", baseDir: projectBaseDir, };
const userOverrides = { extensions: (globalSettings.extensions ?? []) as string[], skills: (globalSettings.skills ?? []) as string[], prompts: (globalSettings.prompts ?? []) as string[], themes: (globalSettings.themes ?? []) as string[], }; const projectOverrides = { extensions: (projectSettings.extensions ?? []) as string[], skills: (projectSettings.skills ?? []) as string[], prompts: (projectSettings.prompts ?? []) as string[], themes: (projectSettings.themes ?? []) as string[], };
const userDirs = { extensions: join(globalBaseDir, "extensions"), skills: join(globalBaseDir, "skills"), prompts: join(globalBaseDir, "prompts"), themes: join(globalBaseDir, "themes"), }; const projectDirs = { extensions: join(projectBaseDir, "extensions"), skills: join(projectBaseDir, "skills"), prompts: join(projectBaseDir, "prompts"), themes: join(projectBaseDir, "themes"), }; const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills"); const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd).filter( (dir) => resolve(dir) !== resolve(userAgentsSkillsDir), );
const addResources = ( resourceType: ResourceType, paths: string[], metadata: PathMetadata, overrides: string[], baseDir: string, ) => { const target = this.getTargetMap(accumulator, resourceType); for (const path of paths) { const enabled = isEnabledByOverrides(path, overrides, baseDir); this.addResource(target, path, metadata, enabled); } };
addResources( "extensions", collectAutoExtensionEntries(projectDirs.extensions), projectMetadata, projectOverrides.extensions, projectBaseDir, ); addResources( "skills", [ ...collectAutoSkillEntries(projectDirs.skills, "pi"), ...projectAgentsSkillDirs.flatMap((dir) => collectAutoSkillEntries(dir, "agents")), ], projectMetadata, projectOverrides.skills, projectBaseDir, ); addResources( "prompts", collectAutoPromptEntries(projectDirs.prompts), projectMetadata, projectOverrides.prompts, projectBaseDir, ); addResources( "themes", collectAutoThemeEntries(projectDirs.themes), projectMetadata, projectOverrides.themes, projectBaseDir, );
addResources( "extensions", collectAutoExtensionEntries(userDirs.extensions), userMetadata, userOverrides.extensions, globalBaseDir, ); addResources( "skills", [...collectAutoSkillEntries(userDirs.skills, "pi"), ...collectAutoSkillEntries(userAgentsSkillsDir, "agents")], userMetadata, userOverrides.skills, globalBaseDir, );
if (this.bundledSkillsDir && this.settingsManager.getEnableBuiltinSkills()) { const builtinMetadata: PathMetadata = { source: "builtin", scope: "user", origin: "top-level", baseDir: this.bundledSkillsDir, }; const builtinEntries = collectAutoSkillEntries(this.bundledSkillsDir, "pi"); // Bundled skills must ship with the package; warn instead of silently exposing none. if (builtinEntries.length === 0) { accumulator.diagnostics.push({ type: "warning", message: existsSync(this.bundledSkillsDir) ? "built-in skills directory contains no skills; this build may be packaged incorrectly" : "built-in skills directory not found; this build may be packaged incorrectly", path: this.bundledSkillsDir, }); } const builtinSkillOverrides = [ ...userOverrides.skills, // Web search stays disabled until explicitly enabled. ...(this.settingsManager.getBundledWebsearchEnabled() ? [] : ["-websearch/SKILL.md"]), // MCP integration skills stay disabled until their authentication is available. ...this.extraBuiltinSkillOverrides(), ]; addResources("skills", builtinEntries, builtinMetadata, builtinSkillOverrides, this.bundledSkillsDir); } addResources( "prompts", collectAutoPromptEntries(userDirs.prompts), userMetadata, userOverrides.prompts, globalBaseDir, ); addResources( "themes", collectAutoThemeEntries(userDirs.themes), userMetadata, userOverrides.themes, globalBaseDir, ); }
private collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] { const files: string[] = []; for (const p of paths) { if (!existsSync(p)) continue;
try { const stats = statSync(p); if (stats.isFile()) { files.push(p); } else if (stats.isDirectory()) { files.push(...collectResourceFiles(p, resourceType)); } } catch { // Ignore inaccessible resource paths. } } return files; }
private getTargetMap( accumulator: ResourceAccumulator, resourceType: ResourceType, ): Map<string, { metadata: PathMetadata; enabled: boolean }> { switch (resourceType) { case "extensions": return accumulator.extensions; case "skills": return accumulator.skills; case "prompts": return accumulator.prompts; case "themes": return accumulator.themes; default: throw new Error(`Unknown resource type: ${resourceType}`); } }
private addResource( map: Map<string, { metadata: PathMetadata; enabled: boolean }>, path: string, metadata: PathMetadata, enabled: boolean, ): void { if (!path) return; if (!map.has(path)) { map.set(path, { metadata, enabled }); } }
private createAccumulator(): ResourceAccumulator { return { extensions: new Map(), skills: new Map(), prompts: new Map(), themes: new Map(), diagnostics: [], }; }
private toResolvedPaths(accumulator: ResourceAccumulator): ResolvedPaths { const mapToResolved = ( entries: Map<string, { metadata: PathMetadata; enabled: boolean }>, ): ResolvedResource[] => { const resolved = Array.from(entries.entries()).map(([path, { metadata, enabled }]) => ({ path, enabled, metadata, })); resolved.sort((a, b) => resourcePrecedenceRank(a.metadata) - resourcePrecedenceRank(b.metadata));
const seen = new Set<string>(); return resolved.filter((entry) => { const canonicalPath = canonicalizePath(entry.path); if (seen.has(canonicalPath)) return false; seen.add(canonicalPath); return true; }); };
return { extensions: mapToResolved(accumulator.extensions), skills: mapToResolved(accumulator.skills), prompts: mapToResolved(accumulator.prompts), themes: mapToResolved(accumulator.themes), diagnostics: accumulator.diagnostics, }; }
private spawnCommand(command: string, args: string[], options?: { cwd?: string }): ChildProcess { return spawn(command, args, { cwd: options?.cwd, stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit", shell: shouldUseWindowsShell(command), env: getEnv(), }); }
private spawnCaptureCommand( command: string, args: string[], options?: { cwd?: string; env?: Record<string, string> }, ): ChildProcess { const baseEnv = getEnv(); return spawnHidden(command, args, { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"], shell: shouldUseWindowsShell(command), env: options?.env ? { ...baseEnv, ...options.env } : baseEnv, }); }
private runCommandCapture( command: string, args: string[], options?: { cwd?: string; timeoutMs?: number; env?: Record<string, string> }, ): Promise<string> { return new Promise((resolvePromise, reject) => { const child = this.spawnCaptureCommand(command, args, options); let stdout = ""; let stderr = ""; let timedOut = false; const timeout = typeof options?.timeoutMs === "number" ? setTimeout(() => { timedOut = true; child.kill(); }, options.timeoutMs) : undefined;
child.stdout?.on("data", (data) => { stdout += data.toString(); }); child.stderr?.on("data", (data) => { stderr += data.toString(); }); child.once("error", (error) => { if (timeout) clearTimeout(timeout); reject(error); }); child.once("close", (code, signal) => { if (timeout) clearTimeout(timeout); if (timedOut) { reject(new Error(`${command} ${args.join(" ")} timed out after ${options?.timeoutMs}ms`)); return; } if (code === 0) { resolvePromise(stdout.trim()); return; } const exitStatus = code === null ? `signal ${signal ?? "unknown"}` : `code ${code}`; reject(new Error(`${command} ${args.join(" ")} failed with ${exitStatus}: ${stderr || stdout}`)); }); }); }
private runCommand(command: string, args: string[], options?: { cwd?: string }): Promise<void> { return new Promise((resolvePromise, reject) => { const child = this.spawnCommand(command, args, options); child.on("error", reject); child.on("exit", (code) => { if (code === 0) { resolvePromise(); } else { reject(new Error(`${command} ${args.join(" ")} failed with code ${code}`)); } }); }); }
private runCommandSync(command: string, args: string[]): string { const result = spawnSyncHidden(command, args, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", shell: shouldUseWindowsShell(command), env: getEnv(), }); if (result.error || result.status !== 0) { throw new Error( `Failed to run ${command} ${args.join(" ")}: ${result.error?.message || result.stderr || result.stdout}`, ); } return (result.stdout || result.stderr || "").trim(); }}