UNPKG

@earendil-works/pi-coding-agent

Version:

Coding agent CLI with read, bash, edit, write tools and session management

2,383 lines • 99.5 kB
import { createHash } from "node:crypto";
import { chmodSync, existsSync, globSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
import { homedir } from "node:os";
function getEnv() {
    if (process.platform !== "linux" || Object.keys(process.env).length > 0) {
        return process.env;
    }
    try {
        const data = readFileSync("/proc/self/environ", "utf-8");
        const env = {};
        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, isAbsolute, join, relative, resolve, sep } from "node:path";
import ignore from "ignore";
import { minimatch } from "minimatch";
import { gt, maxSatisfying, rcompare, satisfies, valid, validRange } from "semver";
import { CONFIG_DIR_NAME } from "../config.js";
import { spawnProcess, spawnProcessSync } from "../utils/child-process.js";
import { parseGitUrl } from "../utils/git.js";
import { canonicalizePath, getPathComparisonKey, isLocalPath, markPathIgnoredByCloudSync, normalizePath, pathsEqual, resolvePath, } from "../utils/paths.js";
import { stripBom } from "../utils/text.js";
import { isStdoutTakenOver } from "./output-guard.js";
import { readPiManifest } from "./pi-manifest.js";
import { BUILTIN_PATH_PREFIX, getResourcePathKey } from "./source-info.js";
const NETWORK_TIMEOUT_MS = 10000;
const UPDATE_CHECK_CONCURRENCY = 4;
const GIT_UPDATE_CONCURRENCY = 4;
/** Whether PI_OFFLINE is set; package installs and updates then skip all network access. */
export function isOfflineModeEnabled() {
    const value = process.env.PI_OFFLINE;
    if (!value)
        return false;
    return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
}
function isExactNpmVersion(version) {
    return valid(version ?? "") !== null;
}
function getNpmVersionRange(version) {
    return version ? (validRange(version) ?? undefined) : undefined;
}
/**
 * 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")
 */
function resourcePrecedenceRank(m) {
    // Built-in extensions load after file and package extensions, whichever scope enables them.
    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);
}
const RESOURCE_TYPES = ["extensions", "skills", "prompts", "themes"];
const FILE_PATTERNS = {
    extensions: /\.(ts|js)$/,
    skills: /\.md$/,
    prompts: /\.md$/,
    themes: /\.json$/,
};
const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"];
function toPosixPath(p) {
    return p.split(sep).join("/");
}
function getHomeDir() {
    return process.env.HOME || homedir();
}
export function getExtensionTempFolder(agentDir) {
    const tempFolder = join(agentDir, "tmp", "extensions");
    mkdirSync(tempFolder, { recursive: true, mode: 0o700 });
    chmodSync(tempFolder, 0o700);
    return tempFolder;
}
function prefixIgnorePattern(line, prefix) {
    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, dir, rootDir) {
    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) => Boolean(line));
            if (patterns.length > 0) {
                ig.add(patterns);
            }
        }
        catch { }
    }
}
function isPattern(s) {
    return s.startsWith("!") || s.startsWith("+") || s.startsWith("-") || s.includes("*") || s.includes("?");
}
function isOverridePattern(s) {
    return s.startsWith("!") || s.startsWith("+") || s.startsWith("-");
}
function hasGlobPattern(s) {
    return s.includes("*") || s.includes("?");
}
/** Glob entries discover visible paths; exact entries can target dot paths or symlinked trees. */
function expandPackageGlob(pattern, root) {
    return globSync(pattern, { cwd: root })
        .map((match) => resolve(root, match))
        .filter((path) => relative(root, path)
        .split(sep)
        .every((segment) => segment === ".." || !segment.startsWith(".")))
        .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
}
function splitPatterns(entries) {
    const plain = [];
    const patterns = [];
    for (const entry of entries) {
        if (isPattern(entry)) {
            patterns.push(entry);
        }
        else {
            plain.push(entry);
        }
    }
    return { plain, patterns };
}
function collectFiles(dir, filePattern, skipNodeModules = true, ignoreMatcher, rootDir) {
    const files = [];
    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 errors
    }
    return files;
}
function collectSkillEntries(dir, mode, ignoreMatcher, rootDir) {
    const entries = [];
    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));
            const shouldIncludeMarkdownFile = isFile &&
                entry.name.endsWith(".md") &&
                !ig.ignores(relPath) &&
                ((mode === "pi" && dir === root) || (mode === "agents" && dir !== root));
            if (shouldIncludeMarkdownFile) {
                entries.push(fullPath);
                continue;
            }
            if (!isDir)
                continue;
            if (ig.ignores(`${relPath}/`))
                continue;
            entries.push(...collectSkillEntries(fullPath, mode, ig, root));
        }
    }
    catch {
        // Ignore errors
    }
    return entries;
}
function collectAutoSkillEntries(dir, mode) {
    return collectSkillEntries(dir, mode);
}
function findGitRepoRoot(startDir) {
    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) {
    const skillDirs = [];
    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) {
    const entries = [];
    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 errors
    }
    return entries;
}
function collectAutoThemeEntries(dir) {
    const entries = [];
    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 errors
    }
    return entries;
}
function resolveExtensionEntries(dir) {
    const packageJsonPath = join(dir, "package.json");
    if (existsSync(packageJsonPath)) {
        const manifest = readPiManifest(packageJsonPath);
        if (manifest?.extensions?.length) {
            const entries = [];
            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) {
    const entries = [];
    if (!existsSync(dir))
        return entries;
    // First check if this directory itself has explicit extension entries (package.json or index)
    const rootEntries = resolveExtensionEntries(dir);
    if (rootEntries) {
        return rootEntries;
    }
    // Otherwise, discover extensions from directory contents
    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 errors
    }
    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, resourceType) {
    if (resourceType === "skills") {
        return collectSkillEntries(dir, "pi");
    }
    if (resourceType === "extensions") {
        return collectAutoExtensionEntries(dir);
    }
    return collectFiles(dir, FILE_PATTERNS[resourceType]);
}
// A `pi` object counts as an explicit resource manifest only when it lists resources. Packages whose
// `pi` object holds only metadata (changelogPath, gallery image/video) still use directory auto-discovery.
function hasManifestResourceFields(manifest) {
    return RESOURCE_TYPES.some((resourceType) => manifest[resourceType] !== undefined);
}
function isPathInside(parent, child) {
    const relativePath = relative(parent, child);
    return (relativePath !== "" && relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));
}
function resolvePackageChangelogPath(packageRoot, manifest) {
    const changelogPath = manifest?.changelogPath ?? "CHANGELOG.md";
    if (!changelogPath)
        return undefined;
    const resolvedPath = resolve(packageRoot, changelogPath);
    if (!existsSync(resolvedPath))
        return undefined;
    const canonicalPackageRoot = canonicalizePath(packageRoot);
    const canonicalChangelogPath = canonicalizePath(resolvedPath);
    if (!isPathInside(canonicalPackageRoot, canonicalChangelogPath))
        return undefined;
    try {
        return statSync(resolvedPath).isFile() ? resolvedPath : undefined;
    }
    catch {
        return undefined;
    }
}
function matchesAnyPattern(filePath, patterns, baseDir) {
    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(normalizePath(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) {
    const normalized = pattern.startsWith("./") || pattern.startsWith(".\\") ? pattern.slice(2) : pattern;
    return toPosixPath(normalizePath(normalized));
}
function matchesAnyExactPattern(filePath, patterns, baseDir) {
    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) {
    return entries.filter((pattern) => pattern.startsWith("!") || pattern.startsWith("+") || pattern.startsWith("-"));
}
function isEnabledByOverrides(filePath, patterns, baseDir) {
    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, patterns, baseDir) {
    const includes = [];
    const excludes = [];
    const forceIncludes = [];
    const forceExcludes = [];
    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);
        }
    }
    // Step 1: Apply includes (or all if no includes)
    let result;
    if (includes.length === 0) {
        result = [...allPaths];
    }
    else {
        result = allPaths.filter((filePath) => matchesAnyPattern(filePath, includes, baseDir));
    }
    // Step 2: Apply excludes
    if (excludes.length > 0) {
        result = result.filter((filePath) => !matchesAnyPattern(filePath, excludes, baseDir));
    }
    // Step 3: Force-include (add back from allPaths, overriding exclusions)
    if (forceIncludes.length > 0) {
        for (const filePath of allPaths) {
            if (!result.includes(filePath) && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) {
                result.push(filePath);
            }
        }
    }
    // Step 4: Force-exclude (remove even if included or force-included)
    if (forceExcludes.length > 0) {
        result = result.filter((filePath) => !matchesAnyExactPattern(filePath, forceExcludes, baseDir));
    }
    return new Set(result);
}
function applyAutoloadDisabledPatterns(allPaths, patterns, baseDir) {
    const result = new Map();
    for (const pattern of patterns) {
        const target = pattern.slice(pattern.startsWith("+") || pattern.startsWith("-") || pattern.startsWith("!") ? 1 : 0);
        const enabled = !pattern.startsWith("-") && !pattern.startsWith("!");
        const exact = pattern.startsWith("+") || pattern.startsWith("-");
        for (const filePath of allPaths) {
            if (exact ? matchesAnyExactPattern(filePath, [target], baseDir) : matchesAnyPattern(filePath, [target], baseDir)) {
                result.set(filePath, enabled);
            }
        }
    }
    return result;
}
export class DefaultPackageManager {
    cwd;
    agentDir;
    settingsManager;
    builtinExtensions;
    globalNpmRoot;
    globalNpmRootCommandKey;
    progressCallback;
    constructor(options) {
        this.cwd = resolvePath(options.cwd);
        this.agentDir = resolvePath(options.agentDir);
        this.settingsManager = options.settingsManager;
        this.builtinExtensions = options.builtinExtensions ?? [];
    }
    setProgressCallback(callback) {
        this.progressCallback = callback;
    }
    addSourceToSettings(source, options) {
        const scope = 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 matchIndex = currentPackages.findIndex((existing) => this.packageSourcesMatch(existing, source, scope));
        if (matchIndex !== -1) {
            const existing = currentPackages[matchIndex];
            if (this.getPackageSourceString(existing) === normalizedSource) {
                return false;
            }
            const nextPackages = [...currentPackages];
            nextPackages[matchIndex] =
                typeof existing === "string" ? normalizedSource : { ...existing, source: normalizedSource };
            if (scope === "project") {
                this.settingsManager.setProjectPackages(nextPackages);
            }
            else {
                this.settingsManager.setPackages(nextPackages);
            }
            return true;
        }
        const nextPackages = [...currentPackages, normalizedSource];
        if (scope === "project") {
            this.settingsManager.setProjectPackages(nextPackages);
        }
        else {
            this.settingsManager.setPackages(nextPackages);
        }
        return true;
    }
    removeSourceFromSettings(source, options) {
        const scope = 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, scope) {
        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;
    }
    emitProgress(event) {
        this.progressCallback?.(event);
    }
    async withProgress(action, source, message, operation) {
        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) {
        const accumulator = this.createAccumulator();
        const globalSettings = this.settingsManager.getGlobalSettings();
        const projectSettings = this.settingsManager.getProjectSettings();
        // Collect all packages with scope (project first so cwd resources win collisions)
        const allPackages = [];
        for (const pkg of projectSettings.packages ?? []) {
            allPackages.push({ pkg, scope: "project" });
        }
        for (const pkg of globalSettings.packages ?? []) {
            allPackages.push({ pkg, scope: "user" });
        }
        // Dedupe: project scope wins over global for same package identity
        const packageSources = this.dedupePackages(allPackages);
        const installFailures = 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] ?? []);
            const projectEntries = (projectSettings[resourceType] ?? []);
            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);
        // Built-in extensions are enabled unless the user `extensions` setting excludes them, for example
        // with `-builtin:mcp`. A matching `+`, `-`, or `!` entry in the project setting overrides that.
        for (const name of this.builtinExtensions) {
            const path = `${BUILTIN_PATH_PREFIX}${name}`;
            const projectEnabled = applyAutoloadDisabledPatterns([path], getOverridePatterns(projectSettings.extensions ?? []), projectBaseDir).get(path);
            this.addResource(accumulator.extensions, path, { source: "builtin", scope: projectEnabled === undefined ? "user" : "project", origin: "top-level" }, projectEnabled ?? isEnabledByOverrides(path, globalSettings.extensions ?? [], globalBaseDir));
        }
        return { ...this.toResolvedPaths(accumulator), installFailures };
    }
    async resolveExtensionSources(sources, options) {
        const accumulator = this.createAccumulator();
        const scope = options?.temporary ? "temporary" : options?.local ? "project" : "user";
        // `-e builtin:<name>` loads a built-in extension; the resource loader reports unknown names.
        for (const source of sources.filter((source) => source.startsWith(BUILTIN_PATH_PREFIX))) {
            this.addResource(accumulator.extensions, source, { source: "builtin", scope, origin: "top-level" }, true);
        }
        const packageSources = sources
            .filter((source) => !source.startsWith(BUILTIN_PATH_PREFIX))
            .map((source) => ({ pkg: source, scope }));
        const installFailures = await this.resolvePackageSources(packageSources, accumulator);
        return { ...this.toResolvedPaths(accumulator), installFailures };
    }
    listConfiguredPackages() {
        const globalSettings = this.settingsManager.getGlobalSettings();
        const projectSettings = this.settingsManager.getProjectSettings();
        const configuredPackages = [];
        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, options) {
        const parsed = this.parseSource(source);
        const scope = options?.local ? "project" : "user";
        this.assertProjectTrustedForScope(scope);
        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, options) {
        await this.install(source, options);
        this.addSourceToSettings(source, options);
    }
    async remove(source, options) {
        const parsed = this.parseSource(source);
        const scope = options?.local ? "project" : "user";
        this.assertProjectTrustedForScope(scope);
        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, options) {
        await this.remove(source, options);
        return this.removeSourceFromSettings(source, options);
    }
    async update(source) {
        const globalSettings = this.settingsManager.getGlobalSettings();
        const projectSettings = this.settingsManager.getProjectSettings();
        const identity = source ? this.getPackageIdentity(source) : undefined;
        let matched = false;
        const updateSources = [];
        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);
    }
    async updateConfiguredSources(sources) {
        if (isOfflineModeEnabled() || sources.length === 0) {
            return;
        }
        const npmCandidates = [];
        const gitCandidates = [];
        for (const entry of sources) {
            const parsed = this.parseSource(entry.source);
            // Pinned npm versions are fixed. Pinned git refs are configured checkout targets,
            // so include them to reconcile an existing clone when the configured ref changes.
            if (parsed.type === "npm") {
                if (!parsed.pinned) {
                    npmCandidates.push({ ...entry, parsed });
                }
            }
            else if (parsed.type === "git") {
                gitCandidates.push({ ...entry, parsed });
            }
        }
        const npmCheckTasks = npmCandidates.map((entry) => async () => {
            const check = await this.shouldUpdateNpmSource(entry.parsed, entry.scope);
            return {
                entry: { ...entry, installedVersion: check.installedVersion, targetVersion: check.targetVersion },
                shouldUpdate: check.shouldUpdate,
            };
        });
        const npmCheckResults = await this.runWithConcurrency(npmCheckTasks, UPDATE_CHECK_CONCURRENCY);
        const userNpmUpdates = [];
        const projectNpmUpdates = [];
        const npmUpdates = npmCheckResults.filter((result) => result.shouldUpdate);
        for (const result of npmUpdates) {
            if (result.entry.scope === "user") {
                userNpmUpdates.push(result.entry);
            }
            else {
                projectNpmUpdates.push(result.entry);
            }
        }
        const tasks = [];
        if (userNpmUpdates.length > 0) {
            tasks.push(this.updateNpmBatch(userNpmUpdates, "user"));
        }
        if (projectNpmUpdates.length > 0) {
            tasks.push(this.updateNpmBatch(projectNpmUpdates, "project"));
        }
        const gitUpdates = [];
        if (gitCandidates.length > 0) {
            const gitTasks = gitCandidates.map((entry) => async () => this.withProgress("update", entry.source, `Updating ${entry.source}...`, async () => {
                const targetDir = this.getGitInstallPath(entry.parsed, entry.scope);
                const from = await this.getGitHead(targetDir);
                await this.updateGit(entry.parsed, entry.scope);
                const to = await this.getGitHead(targetDir);
                if (from !== to) {
                    gitUpdates.push({ source: entry.source, type: "git", scope: entry.scope, from, to });
                }
            }));
            tasks.push(this.runWithConcurrency(gitTasks, GIT_UPDATE_CONCURRENCY).then(() => { }));
        }
        await Promise.all(tasks);
        const updated = [];
        for (const { entry } of npmUpdates) {
            const installedPath = this.getManagedNpmInstallPath(entry.parsed, entry.scope);
            const to = existsSync(installedPath) ? this.getInstalledNpmVersion(installedPath) : undefined;
            if (to !== entry.installedVersion) {
                updated.push({ source: entry.source, type: "npm", scope: entry.scope, from: entry.installedVersion, to });
            }
        }
        // Keep git results in configured order, independent of completion order.
        for (const entry of gitCandidates) {
            const update = gitUpdates.find((u) => u.source === entry.source && u.scope === entry.scope);
            if (update)
                updated.push(update);
        }
        // Report each changed package, e.g. "npm:foo 1.2.0 -> 1.3.0".
        for (const pkg of updated) {
            const format = (value) => value === undefined ? "(none)" : pkg.type === "git" ? value.slice(0, 7) : value;
            const scope = pkg.scope === "project" ? " (project)" : "";
            this.emitProgress({
                type: "result",
                action: "update",
                source: pkg.source,
                message: `${pkg.source}${scope} ${format(pkg.from)} -> ${format(pkg.to)}`,
            });
        }
    }
    async getGitHead(targetDir) {
        if (!existsSync(targetDir))
            return undefined;
        try {
            const head = await this.runCommandCapture("git", ["rev-parse", "HEAD"], {
                cwd: targetDir,
                timeoutMs: NETWORK_TIMEOUT_MS,
            });
            return head.trim() || undefined;
        }
        catch {
            return undefined;
        }
    }
    async shouldUpdateNpmSource(source, scope) {
        const installedPath = this.getManagedNpmInstallPath(source, scope);
        const installedVersion = existsSync(installedPath) ? this.getInstalledNpmVersion(installedPath) : undefined;
        if (!installedVersion) {
            return { shouldUpdate: true };
        }
        try {
            const targetVersion = await this.getLatestNpmVersion(source.version ? source.spec : source.name, source.range);
            return { shouldUpdate: gt(targetVersion, installedVersion), installedVersion, targetVersion };
        }
        catch {
            // Preserve existing update behavior when version lookup fails.
            return { shouldUpdate: true, installedVersion };
        }
    }
    async updateNpmBatch(sources, scope) {
        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.version ? entry.parsed.spec : `${entry.parsed.name}@latest`));
        await this.withProgress("update", sourceLabel, message, async () => {
            await this.installNpmBatch(specs, scope);
        });
        this.reportUnchangedNpmUpdates(sources, scope);
    }
    /**
     * npm can resolve stale registry metadata shortly after a publish and install the old
     * version without an error. Warn when an announced update did not change the installed version.
     */
    reportUnchangedNpmUpdates(sources, scope) {
        for (const entry of sources) {
            if (!entry.installedVersion || !entry.targetVersion) {
                continue;
            }
            const installedPath = this.getManagedNpmInstallPath(entry.parsed, scope);
            const versionAfter = this.getInstalledNpmVersion(installedPath);
            if (versionAfter !== entry.installedVersion) {
                continue;
            }
            this.emitProgress({
                type: "progress",
                action: "update",
                source: entry.source,
                message: `${entry.source} is still ${versionAfter} (registry reports ${entry.targetVersion}); npm registry metadata may be stale, try again in a few minutes`,
            });
        }
    }
    async installNpmBatch(specs, scope) {
        const installRoot = this.getNpmInstallRoot(scope, false);
        this.ensureNpmProject(installRoot);
        await this.runNpmCommand(this.getNpmInstallArgs(specs, installRoot));
    }
    async checkForAvailableUpdates() {
        if (isOfflineModeEnabled()) {
            return [];
        }
        const globalSettings = this.settingsManager.getGlobalSettings();
        const projectSettings = this.settingsManager.getProjectSettings();
        const allPackages = [];
        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.scope !== "temporary")
            .map((entry) => async () => {
            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 !== undefined);
    }
    async resolvePackageSources(sources, accumulator, onMissing) {
        const installFailures = [];
        for (const { pkg, scope } of sources) {
            const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
            const filter = typeof pkg === "object" ? pkg : undefined;
            const deltaBase = this.findAutoloadDeltaBase(pkg, scope, sources);
            const resolvedSource = deltaBase?.source ?? sourceStr;
            const resolvedScope = deltaBase?.scope ?? scope;
            const parsed = this.parseSource(resolvedSource);
            const metadata = { source: sourceStr, scope, origin: "package" };
            if (parsed.type === "local") {
                const baseDir = this.getBaseDirForScope(resolvedScope);
                this.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir);
                continue;
            }
            const installMissing = async () => {
                if (isOfflineModeEnabled())
                    return false;
                if (onMissing) {
                    const action = await onMissing(resolvedSource);
                    if (action === "skip")
                        return false;
                    if (action === "error")
                        throw new Error(`Missing source: ${resolvedSource}`);
                }
                // A source that cannot be installed (no network, missing package, failing git prepare step)
                // must not abort startup. Skip its resources, like offline mode does, and report the failure.
                try {
                    await this.installParsedSource(parsed, resolvedScope);
                    return true;
                }
                catch (error) {
                    installFailures.push({
                        source: resolvedSource,
                        scope: resolvedScope,
                        message: error instanceof Error ? error.message : String(error),
                    });
                    return false;
                }
            };
            if (parsed.type === "npm") {
                let installedPath = this.getNpmInstallPath(parsed, resolvedScope);
                const needsInstall = !existsSync(installedPath) || !(await this.installedNpmMatchesConfiguredVersion(parsed, installedPath));
                if (!needsInstall &&
                    resolvedScope === "temporary" &&
                    !parsed.pinned &&
                    (await this.npmHasAvailableUpdate(parsed, installedPath))) {
                    // Temporary (-e) sources have no `pi update` path, so refresh unpinned ones on load like
                    // temporary git sources. Like `pi update`, install `name@latest` for unversioned specs because a bare
                    // `npm install name` keeps the range already saved in the temporary package.json.
                    const spec = parsed.version ? parsed.spec : `${parsed.name}@latest`;
                    try {
                        await this.installNpm({ ...parsed, spec }, resolvedScope, true);
                    }
                    catch {
                        // Keep the cached install if the refresh fails.
                    }
                }
                if (needsInstall) {
                    const installed = await installMissing();
                    if (!installed)
                        continue;
                    installedPath = this.getNpmInstallPath(parsed, resolvedScope);
                }
                metadata.baseDir = installedPath;
                metadata.packageRoot = installedPath;
                this.collectPackageResources(installedPath, accumulator, filter, metadata);
                continue;
            }
            if (parsed.type === "git") {
                const installedPath = this.getGitInstallPath(parsed, resolvedScope);
                if (!existsSync(installedPath)) {
                    const installed = await installMissing();
                    if (!installed)
                        continue;
                }
                else if (resolvedScope === "temporary" && !isOfflineModeEnabled()) {
                    await this.refreshTemporaryGitSource(parsed, resolvedSource, installedPath);
                }
                metadata.baseDir = installedPath;
                metadata.packageRoot = installedPath;
                this.collectPackageResources(installedPath, accumulator, filter, metadata);
            }
        }
        return installFailures;
    }
    findAutoloadDeltaBase(pkg, scope, sources) {
        if (scope !== "project" || typeof pkg !== "object" || pkg.autoload !== false)
            return undefined;
        const identity = this.getPackageIdentity(pkg.source, scope);
        const userEntry = sources.find((entry) => entry.scope === "user" &&
            this.getPackageIdentity(this.getPackageSourceString(entry.pkg), "user") === identity);
        return userEntry ? { source: this.getPackageSourceString(userEntry.pkg), scope: "user" } : undefined;
    }
    resolveLocalExtensionSource(source, accumulator, filter, metadata, baseDir) {
        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;
                metadata.packageRoot = resolved;
                const resources = this.collectPackageResources(resolved, accumulator, filter, metadata);
                if (!resources) {
                    this.addResource(accumulator.extensions, resolved, metadata, true);
                }
            }
        }
        catch {
            return;
        }
    }
    async installParsedSource(parsed, scope) {
        if (parsed.type === "npm") {
            await this.installNpm(parsed, scope, scope === "temporary");
            return;
        }
        if (parsed.type === "git") {
            await this.installGit(parsed, scope);
            return;
        }
    }
    getPackageSourceString(pkg) {
        return typeof pkg === "string" ? pkg : pkg.source;
    }
    getSourceMatchKeyForInput(source) {
        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:${getPathComparisonKey(this.resolvePath(parsed.path))}`;
    }
    getSourceMatchKeyForSettings(source, scope) {
        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:${getPathComparisonKey(this.resolvePathFromBase(parsed.path, baseDir))}`;
    }
    buildNoMatchingPackageMessage(source, configuredPackages) {
        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}?`;
    }
    findSuggestedConfiguredSource(source, configuredPackages) {
        const trimmedSource = source.trim();
        const suggestions = new Set();
        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;
    }
    packageSourcesMatch(existing, inputSource, scope) {
        const left = this.getSourceMatchKeyForSettings(this.getPackageSourceString(existing), scope);
        const right = this.getSourceMatchKeyForInput(inputSource);
        return left === right;
    }
    normalizePackageSourceForSettings(source, scope) {
        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 || ".";
    }
    parseSource(source) {
        if (source.startsWith("npm:")) {
            const spec = source.slice("npm:".length).trim();
            const { name, version } = this.parseNpmSpec(spec);
            return {
                type: "npm",
                spec,
                name,
                version,
                range: getNpmVersionRange(version),
                pinned: isExactNpmVersion(version),
            };
        }
        if (isLocalPath(source)) {
            return { type: "local", path: source };
        }
        // Try parsing as git URL
        const gitParsed = parseGitUrl(source);
        if (gitParsed) {
            return gitParsed;
        }
        return { type: "local", path: source };
    }
    async installedNpmMatchesConfiguredVersion(source, installedPath) {
        const installedVersion = this.getInstalledNpmVersion(installedPath);
        if (!installedVersion) {
            return false;
        }
        return source.range ? satisfies(installedVersion, source.range) : true;
    }
    async npmHasAvailableUpdate(source, installedPath) {
        if (isOfflineModeEnabled()) {
            return false;
        }
        const installedVersion = this.getInstalledNpmVersion(installedPath);
        if (!installedVersion) {
            return false;
        }
        try {
            const targetVersion = await this.getLatestNpmVersion(source.version ? source.spec : source.name, source.range);
            return gt(targetVersion, installedVersion);
        }
        catch {
            return false;
        }
    }
    getInstalledNpmVersion(installedPath) {
        const packageJsonPath = join(installedPath, "package.json");
        if (!existsSync(packageJsonPath))
            return undefined;
        try {
            const content = readFileSync(packageJsonPath, "utf-8");
            const pkg = JSON.parse(stripBom(content));
            return pkg.version;
        }
        catch {
            return undefined;
        }
    }
    async getLatestNpmVersion(packageSpec, range) {
        const npmCommand = this.getNpmCommand();
        const stdout = await this.runCommandCapture(npmCommand.command, [...npmCommand.args, "view", packageSpec, "version", "--json"], { cwd: this.cwd, timeoutMs: NETWORK_TIMEOUT_MS });
        const raw = stdout.trim();
        if (!raw)
            throw new Error("Empty response from npm view");
        const parsed = JSON.parse(raw);
        if (typeof parsed === "string") {
            return parsed;
        }
        if (Array.isArray(parsed)) {
            const versions = parsed.filter((value) => typeof value === "string" && value.length > 0);
            const latest = range ? maxSatisfying(versions, range) : [...versions].sort(rcompare)[0];
            if (latest)
                return latest;
        }
        throw new Error("Unexpected response from npm view");
    }
    async gitHasAvailableUpdate(installedPath) {
        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;
        }
    }
    async getRemoteGitHead(installedPath) {
        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];
    }
    async getLocalGitUpdateTarget(installedPath) {
        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"],
            };
        }
    }
    async getGitUpstreamRef(installedPath) {
        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;
        }
    }
    runGitRemoteCommand(installedPath, args) {
        return this.runCommandCapture("git", args, {
            cwd: installedPath,
            timeoutMs: NETWORK_TIMEOUT_MS,
            env: {
                GIT_TERMINAL_PROMPT: "0",
            },
        });
    }
    async runWithConcurrency(tasks, limit) {
        if (tasks.length === 0) {
            return [];
        }
        const results = 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.
     */
    getPackageIdentity(source, scope) {
        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:${getPathComparisonKey(this.resolvePathFromBase(parsed.path, baseDir))}`;
        }
        return `local:${getPathComparisonKey(this.resolvePath(parsed.path))}`;
    }
    /**
     * Dedupe packages: if same package identity appears in both global and project,
     * keep only the project one (project wins). A project entry with autoload=false
     * is a delta over the global entry, so both are kept (delta first).
     */
    dedupePackages(packages) {
        const result = [];
        const seen = new Map();
        for (const entry of packages) {
            const identity = this.getPackageIdentity(this.getPackageSourceString(entry.pkg), entry.scope);
            const index = seen.get(identity);
            if (index === undefined) {
                seen.set(identity, result.length);
                result.push(entry);
                continue;
            }
            const existing = result[index];
            if (existing?.scope === "project" && entry.scope === "user") {
                if (typeof existing.pkg === "object" && existing.pkg.autoload === false)
                    result.push(entry);
            }
            else if (entry.scope === "project") {
                result[index] = entry;
            }
        }
        return result;
    }
    parseNpmSpec(spec) {
        const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/);
        if (!match) {
            return { name: spec };
        }
        const name = match[1] ?? spec;
        const version = match[2];
        return { name, version };
    }
    assertProjectTrustedForScope(scope) {
        if (scope === "project" && !this.settingsManager.isProjectTrusted()) {
            throw new Error("Project is not trusted; refusing to access project package storage");
        }
    }
    getNpmCommand() {
        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 };
    }
    getPackageManagerName() {
        const npmCommand = this.getNpmCommand();
        const normalizeCommandName = (command) => basename(command).replace(/\.(cmd|exe)$/i, "");
        const supportedPackageManagers = new Set(["npm", "pnpm", "bun"]);
        const directCommand = normalizeCommandName(npmCommand.command);
        const separatorIndex = npmCommand.args.lastIndexOf("--");
        if (separatorIndex >= 0) {
            const wrappedCommand = npmCommand.args[separatorIndex + 1];
            return wrappedCommand ? normalizeCommandName(wrappedCommand) : directCommand;
        }
        if (supportedPackageManagers.has(directCommand))
            return directCommand;
        const wrappedPackageManagers = [
            ...new Set(npmCommand.args.map(normalizeCommandName).filter((command) => supportedPackageManagers.has(command))),
        ];
        if (wrappedPackageManagers.length > 1) {
            throw new Error(`Ambiguous npmCommand package managers: ${wrappedPackageManagers.join(", ")}`);
        }
        return wrappedPackageManagers[0] ?? directCommand;
    }
    async runNpmCommand(args, options) {
        const npmCommand = this.getNpmCommand();
        await this.runCommand(npmCommand.command, [...npmCommand.args, ...args], options);
    }
    getGitDependencyInstallArgs() {
        switch (this.getPackageManagerName()) {
            case "bun":
                return ["install", "--omit=dev", "--omit=peer"];
            case "pnpm":
                return [
                    "install",
                    "--prod",
                    "--config.auto-install-peers=false",
                    "--config.strict-peer-dependencies=false",
                    "--config.strict-dep-builds=false",
                ];
            case "npm":
                return ["install", "--omit=dev", "--legacy-peer-deps", "--no-audit", "--no-fund"];
            default:
                return ["install"];
        }
    }
    runNpmCommandSync(args) {
        const npmCommand = this.getNpmCommand();
        return this.runCommandSync(npmCommand.command, [...npmCommand.args, ...args]);
    }
    getNpmInstallArgs(specs, root) {
        // npm writes broken nested package-lock.json paths when --prefix goes through a symlink.
        const installRoot = existsSync(root) ? realpathSync(root) : root;
        const packageManagerName = this.getPackageManagerName();
        // Extension packages run inside pi and resolve pi APIs through loader aliases/virtual modules.
        // Disable peer dependency resolution for managed installs (npm's --legacy-peer-deps, and
        // equivalent bun/pnpm settings) so package managers do not install or solve host-provided
        // @earendil-works/pi-* peers. Stale auto-installed pi peers can otherwise block updates.
        if (packageManagerName === "bun") {
            return ["install", ...specs, "--cwd", installRoot, "--omit=peer"];
        }
        if (packageManagerName === "pnpm") {
            return [
                "install",
                ...specs,
                "--prefix",
                installRoot,
                "--config.auto-install-peers=false",
                "--config.strict-peer-dependencies=false",
                "--config.strict-dep-builds=false",
            ];
        }
        // npm audit fix would run without --legacy-peer-deps and install pi peers, so skip audit advice.
        return ["install", ...specs, "--prefix", installRoot, "--legacy-peer-deps", "--no-audit", "--no-fund"];
    }
    async installNpm(source, scope, temporary) {
        const installRoot = this.getNpmInstallRoot(scope, temporary);
        this.ensureNpmProject(installRoot);
        await this.runNpmCommand(this.getNpmInstallArgs([source.spec], installRoot));
    }
    async uninstallNpm(source, scope) {
        const root = this.getNpmInstallRoot(scope, false);
        if (!existsSync(root)) {
            return;
        }
        // npm writes broken nested package-lock.json paths when --prefix goes through a symlink.
        const installRoot = realpathSync(root);
        const packageManagerName = this.getPackageManagerName();
        if (packageManagerName === "bun") {
            await this.runNpmCommand(["uninstall", source.name, "--cwd", installRoot]);
            return;
        }
        const args = ["uninstall", source.name, "--prefix", installRoot];
        if (packageManagerName !== "pnpm") {
            args.push("--legacy-peer-deps", "--no-audit", "--no-fund");
        }
        await this.runNpmCommand(args);
    }
    async installGit(source, scope) {
        const targetDir = this.getGitInstallPath(source, scope);
        if (existsSync(targetDir)) {
            if (source.ref) {
                await this.ensureGitRef(targetDir, ["fetch", "origin", source.ref], "FETCH_HEAD");
                return;
            }
            const target = await this.getLocalGitUpdateTarget(targetDir);
            await this.ensureGitRef(targetDir, target.fetchArgs, target.ref);
            return;
        }
        const gitRoot = this.getGitInstallRoot(scope);
        if (gitRoot) {
            this.ensureGitIgnore(gitRoot);
        }
        mkdirSync(dirname(targetDir), { recursive: true });
        rmSync(this.getGitUpdateMarkerPath(targetDir), { force: true });
        try {
            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 });
            }
        }
        catch (error) {
            rmSync(targetDir, { recursive: true, force: true });
            this.pruneEmptyGitParents(targetDir, gitRoot);
            throw error;
        }
    }
    async updateGit(source, scope) {
        const targetDir = this.getGitInstallPath(source, scope);
        if (!existsSync(targetDir)) {
            await this.installGit(source, scope);
            return;
        }
        if (source.ref) {
            await this.ensureGitRef(targetDir, ["fetch", "origin", source.ref], "FETCH_HEAD");
            return;
        }
        const target = await this.getLocalGitUpdateTarget(targetDir);
        await this.ensureGitRef(targetDir, target.fetchArgs, target.ref);
    }
    hasMissingGitDependencies(targetDir) {
        const packageJsonPath = join(targetDir, "package.json");
        if (!existsSync(packageJsonPath))
            return false;
        try {
            const manifest = JSON.parse(stripBom(readFileSync(packageJsonPath, "utf-8")));
            if (!manifest.dependencies ||
                typeof manifest.dependencies !== "object" ||
                Array.isArray(manifest.dependencies)) {
                return false;
            }
            const nodeModulesDir = resolve(targetDir, "node_modules");
            return Object.keys(manifest.dependencies).some((name) => {
                const dependencyPath = resolve(nodeModulesDir, name);
                if (!dependencyPath.startsWith(`${nodeModulesDir}${sep}`))
                    return false;
                return !existsSync(dependencyPath);
            });
        }
        catch {
            return false;
        }
    }
    async repairMissingGitDependencies(targetDir) {
        if (!this.hasMissingGitDependencies(targetDir))
            return;
        await this.runNpmCommand(this.getGitDependencyInstallArgs(), { cwd: targetDir });
    }
    getGitUpdateMarkerPath(targetDir) {
        return join(dirname(targetDir), `.${basename(targetDir)}.pi-update-incomplete`);
    }
    async cleanAndInstallGitDependencies(targetDir, markerPath) {
        // Clean untracked files (extensions should be pristine). On Windows, files loaded by
        // another process (e.g. a native module of this extension in another pi session) cannot
        // be deleted, so git clean fails. Still install dependencies for the new commit, but keep
        // the marker so the next update retries the clean.
        let cleaned = true;
        try {
            await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir });
        }
        catch (error) {
            cleaned = false;
            const reason = error instanceof Error ? error.message : String(error);
            this.emitProgress({
                type: "progress",
                action: "update",
                source: targetDir,
                message: `Could not remove all untracked files in ${targetDir} (${reason}). Files may be in use by another pi session; close it and run the update again.`,
            });
        }
        const packageJsonPath = join(targetDir, "package.json");
        if (existsSync(packageJsonPath)) {
            await this.runNpmCommand(this.getGitDependencyInstallArgs(), { cwd: targetDir });
        }
        if (cleaned) {
            rmSync(markerPath, { force: true });
        }
    }
    async ensureGitRef(targetDir, fetchArgs, ref) {
        // Fetch only the ref we will reset to, avoiding unrelated branch/tag noise.
        await this.runCommand("git", fetchArgs, { cwd: targetDir });
        const localHead = await this.runCommandCapture("git", ["rev-parse", "HEAD"], {
            cwd: targetDir,
            timeoutMs: NETWORK_TIMEOUT_MS,
        });
        const commitRef = `${ref}^{commit}`;
        const targetHead = await this.runCommandCapture("git", ["rev-parse", commitRef], {
            cwd: targetDir,
            timeoutMs: NETWORK_TIMEOUT_MS,
        });
        const markerPath = this.getGitUpdateMarkerPath(targetDir);
        if (localHead.trim() === targetHead.trim()) {
            if (existsSync(markerPath)) {
                await this.cleanAndInstallGitDependencies(targetDir, markerPath);
            }
            else {
                await this.repairMissingGitDependencies(targetDir);
            }
            return;
        }
        writeFileSync(markerPath, "", "utf-8");
        await this.runCommand("git", ["reset", "--hard", commitRef], { cwd: targetDir });
        await this.cleanAndInstallGitDependencies(targetDir, markerPath);
    }
    async refreshTemporaryGitSource(source, sourceStr, installedPath) {
        if (isOfflineModeEnabled()) {
            return;
        }
        try {
            // A ref can name a moving branch, so refresh refs too. Skip the network call only when the
            // ref is a commit hash that is already checked out.
            if (source.ref && /^[0-9a-f]{7,64}$/i.test(source.ref)) {
                const head = await this.runCommandCapture("git", ["rev-parse", "HEAD"], {
                    cwd: installedPath,
                    timeoutMs: NETWORK_TIMEOUT_MS,
                });
                if (head.trim().toLowerCase().startsWith(source.ref.toLowerCase())) {
                    return;
                }
            }
            await this.withProgress("pull", sourceStr, `Refreshing ${sourceStr}...`, async () => {
                await this.updateGit(source, "temporary");
            });
        }
        catch {
            // Keep cached temporary checkout if refresh fails.
        }
    }
    async removeGit(source, scope) {
        const targetDir = this.getGitInstallPath(source, scope);
        rmSync(targetDir, { recursive: true, force: true });
        rmSync(this.getGitUpdateMarkerPath(targetDir), { force: true });
        this.pruneEmptyGitParents(targetDir, this.getGitInstallRoot(scope));
    }
    pruneEmptyGitParents(targetDir, installRoot) {
        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);
        }
    }
    ensureNpmProject(installRoot) {
        if (!existsSync(installRoot)) {
            mkdirSync(installRoot, { recursive: true });
        }
        markPathIgnoredByCloudSync(installRoot);
        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");
        }
    }
    ensureGitIgnore(dir) {
        if (!existsSync(dir)) {
            mkdirSync(dir, { recursive: true });
        }
        const ignorePath = join(dir, ".gitignore");
        if (!existsSync(ignorePath)) {
            writeFileSync(ignorePath, "*\n!.gitignore\n", "utf-8");
        }
    }
    getNpmInstallRoot(scope, temporary) {
        if (temporary) {
            return this.getTemporaryDir("npm");
        }
        if (scope === "project") {
            this.assertProjectTrustedForScope(scope);
            return join(this.cwd, CONFIG_DIR_NAME, "npm");
        }
        return join(this.agentDir, "npm");
    }
    getGlobalNpmRoot() {
        const npmCommand = this.getNpmCommand();
        const commandKey = [npmCommand.command, ...npmCommand.args].join("\0");
        if (this.globalNpmRoot && this.globalNpmRootCommandKey === commandKey) {
            return this.globalNpmRoot;
        }
        if (this.getPackageManagerName() === "bun") {
            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;
    }
    getPnpmGlobalPackagePath(packageName) {
        if (this.getPackageManagerName() !== "pnpm") {
            return undefined;
        }
        const output = this.runNpmCommandSync(["list", "-g", "--depth", "0", "--json"]);
        const entries = JSON.parse(output);
        for (const entry of entries) {
            const path = entry.dependencies?.[packageName]?.path;
            if (path)
                return path;
        }
        return undefined;
    }
    getManagedNpmInstallPath(source, scope) {
        if (scope === "temporary") {
            return join(this.getTemporaryDir("npm"), "node_modules", source.name);
        }
        if (scope === "project") {
            this.assertProjectTrustedForScope(scope);
            return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name);
        }
        return join(this.agentDir, "npm", "node_modules", source.name);
    }
    getLegacyGlobalNpmInstallPath(source) {
        try {
            return this.getPnpmGlobalPackagePath(source.name) ?? join(this.getGlobalNpmRoot(), source.name);
        }
        catch {
            return undefined;
        }
    }
    getNpmInstallPath(source, scope) {
        const managedPath = this.getManagedNpmInstallPath(source, scope);
        if (scope !== "user" || existsSync(managedPath)) {
            return managedPath;
        }
        const legacyPath = this.getLegacyGlobalNpmInstallPath(source);
        return legacyPath && existsSync(legacyPath) ? legacyPath : managedPath;
    }
    getGitInstallPath(source, scope) {
        if (scope === "temporary") {
            // Include the ref in the hash so each pinned ref gets its own checkout.
            return this.getTemporaryDir(`git-${source.host}`, source.path, source.ref);
        }
        const installRoot = this.getGitInstallRoot(scope);
        if (!installRoot) {
            throw new Error("Missing git install root");
        }
        return this.resolveManagedPath(installRoot, source.host, source.path);
    }
    getGitInstallRoot(scope) {
        if (scope === "temporary") {
            return undefined;
        }
        if (scope === "project") {
            this.assertProjectTrustedForScope(scope);
            return join(this.cwd, CONFIG_DIR_NAME, "git");
        }
        return join(this.agentDir, "git");
    }
    getTemporaryDir(prefix, suffix, ref) {
        const root = this.resolveManagedPath(getExtensionTempFolder(this.agentDir), prefix);
        const hash = createHash("sha256")
            .update(`${prefix}-${suffix ?? ""}${ref ? `@${ref}` : ""}`)
            .digest("hex")
            .slice(0, 8);
        return this.resolveManagedPath(root, hash, suffix ?? "");
    }
    resolveManagedPath(root, ...parts) {
        const resolvedRoot = resolve(root);
        const resolvedPath = resolve(resolvedRoot, ...parts);
        if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${sep}`)) {
            throw new Error(`Refusing to use path outside package install root: ${resolvedPath}`);
        }
        return resolvedPath;
    }
    getBaseDirForScope(scope) {
        if (scope === "project") {
            this.assertProjectTrustedForScope(scope);
            return join(this.cwd, CONFIG_DIR_NAME);
        }
        if (scope === "user") {
            return this.agentDir;
        }
        return this.cwd;
    }
    resolvePath(input) {
        return resolvePath(input, this.cwd, { homeDir: getHomeDir(), trim: true });
    }
    resolvePathFromBase(input, baseDir) {
        return resolvePath(input, baseDir, { homeDir: getHomeDir(), trim: true });
    }
    collectPackageResources(packageRoot, accumulator, filter, metadata) {
        const manifest = readPiManifest(join(packageRoot, "package.json"));
        const changelogPath = resolvePackageChangelogPath(packageRoot, manifest);
        if (changelogPath) {
            metadata.changelogPath = changelogPath;
        }
        else {
            delete metadata.changelogPath;
        }
        if (filter) {
            for (const resourceType of RESOURCE_TYPES) {
                const patterns = filter[resourceType];
                const target = this.getTargetMap(accumulator, resourceType);
                if (filter.autoload === false) {
                    this.applyPackageDeltaFilter(packageRoot, patterns ?? [], resourceType, target, metadata);
                }
                else if (patterns !== undefined) {
                    this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata);
                }
                else {
                    this.collectDefaultResources(packageRoot, resourceType, target, metadata);
                }
            }
            return true;
        }
        if (manifest && hasManifestResourceFields(manifest)) {
            for (const resourceType of RESOURCE_TYPES) {
                const entries = manifest[resourceType];
                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)) {
                // Collect all files from the directory (all enabled by default)
                const files = collectResourceFiles(dir, resourceType);
                for (const f of files) {
                    this.addResource(this.getTargetMap(accumulator, resourceType), f, metadata, true);
                }
                hasAnyDir = true;
            }
        }
        return hasAnyDir;
    }
    collectDefaultResources(packageRoot, resourceType, target, metadata) {
        const manifest = readPiManifest(join(packageRoot, "package.json"));
        const entries = manifest?.[resourceType];
        if (entries) {
            this.addManifestEntries(entries, packageRoot, resourceType, target, metadata);
            return;
        }
        const dir = join(packageRoot, resourceType);
        if (existsSync(dir)) {
            // Collect all files from the directory (all enabled by default)
            const files = collectResourceFiles(dir, resourceType);
            for (const f of files) {
                this.addResource(target, f, metadata, true);
            }
        }
    }
    applyPackageFilter(packageRoot, userPatterns, resourceType, target, metadata) {
        const { allFiles } = this.collectManifestFiles(packageRoot, resourceType);
        if (userPatterns.length === 0) {
            // Empty array explicitly disables all resources of this type
            for (const f of allFiles) {
                this.addResource(target, f, metadata, false);
            }
            return;
        }
        // Apply user patterns
        const enabledByUser = applyPatterns(allFiles, userPatterns, packageRoot);
        for (const f of allFiles) {
            const enabled = enabledByUser.has(f);
            this.addResource(target, f, metadata, enabled);
        }
    }
    applyPackageDeltaFilter(packageRoot, userPatterns, resourceType, target, metadata) {
        if (userPatterns.length === 0) {
            return;
        }
        const { allFiles } = this.collectManifestFiles(packageRoot, resourceType);
        const enabledByUser = applyAutoloadDisabledPatterns(allFiles, userPatterns, packageRoot);
        for (const [filePath, enabled] of enabledByUser) {
            this.addResource(target, filePath, 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.
     */
    collectManifestFiles(packageRoot, resourceType) {
        const manifest = readPiManifest(join(packageRoot, "package.json"));
        const entries = manifest?.[resourceType];
        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) };
    }
    addManifestEntries(entries, root, resourceType, target, metadata) {
        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);
            }
        }
    }
    collectFilesFromManifestEntries(entries, root, resourceType) {
        const sourceEntries = entries.filter((entry) => !isOverridePattern(entry));
        const resolved = sourceEntries.flatMap((entry) => {
            if (!hasGlobPattern(entry)) {
                return [resolve(root, entry)];
            }
            return expandPackageGlob(entry, root);
        });
        return this.collectFilesFromPaths(resolved, resourceType);
    }
    resolveLocalEntries(entries, resourceType, target, metadata, baseDir) {
        if (entries.length === 0)
            return;
        // Collect all files from plain entries (non-pattern entries)
        const { plain, patterns } = splitPatterns(entries);
        const resolvedPlain = plain.map((p) => this.resolvePathFromBase(p, baseDir));
        const allFiles = this.collectFilesFromPaths(resolvedPlain, resourceType);
        // Determine which files are enabled based on patterns
        const enabledPaths = applyPatterns(allFiles, patterns, baseDir);
        // Add all files with their enabled state
        for (const f of allFiles) {
            this.addResource(target, f, metadata, enabledPaths.has(f));
        }
    }
    addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDir) {
        const userMetadata = {
            source: "auto",
            scope: "user",
            origin: "top-level",
            baseDir: globalBaseDir,
        };
        const projectMetadata = {
            source: "auto",
            scope: "project",
            origin: "top-level",
            baseDir: projectBaseDir,
        };
        const userOverrides = {
            extensions: (globalSettings.extensions ?? []),
            skills: (globalSettings.skills ?? []),
            prompts: (globalSettings.prompts ?? []),
            themes: (globalSettings.themes ?? []),
        };
        const projectOverrides = {
            extensions: (projectSettings.extensions ?? []),
            skills: (projectSettings.skills ?? []),
            prompts: (projectSettings.prompts ?? []),
            themes: (projectSettings.themes ?? []),
        };
        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 projectTrusted = this.settingsManager.isProjectTrusted();
        const projectAgentsSkillDirs = projectTrusted
            ? collectAncestorAgentsSkillDirs(this.cwd).filter((dir) => !pathsEqual(dir, userAgentsSkillsDir))
            : [];
        const addResources = (resourceType, paths, metadata, overrides, baseDir) => {
            const target = this.getTargetMap(accumulator, resourceType);
            for (const path of paths) {
                const enabled = isEnabledByOverrides(path, overrides, baseDir);
                this.addResource(target, path, metadata, enabled);
            }
        };
        if (projectTrusted) {
            // Project extensions from .pi/
            addResources("extensions", collectAutoExtensionEntries(projectDirs.extensions), projectMetadata, projectOverrides.extensions, projectBaseDir);
            // Project skills from .pi/
            addResources("skills", collectAutoSkillEntries(projectDirs.skills, "pi"), projectMetadata, projectOverrides.skills, projectBaseDir);
        }
        // Project skills from .agents/ (each with its own baseDir)
        for (const agentsSkillsDir of projectAgentsSkillDirs) {
            const agentsBaseDir = dirname(agentsSkillsDir); // the .agents directory
            const agentsMetadata = {
                ...projectMetadata,
                baseDir: agentsBaseDir,
            };
            addResources("skills", collectAutoSkillEntries(agentsSkillsDir, "agents"), agentsMetadata, projectOverrides.skills, agentsBaseDir);
        }
        if (projectTrusted) {
            addResources("prompts", collectAutoPromptEntries(projectDirs.prompts), projectMetadata, projectOverrides.prompts, projectBaseDir);
            addResources("themes", collectAutoThemeEntries(projectDirs.themes), projectMetadata, projectOverrides.themes, projectBaseDir);
        }
        // User extensions from ~/.pi/agent/
        addResources("extensions", collectAutoExtensionEntries(userDirs.extensions), userMetadata, userOverrides.extensions, globalBaseDir);
        // User skills from ~/.pi/agent/
        addResources("skills", collectAutoSkillEntries(userDirs.skills, "pi"), userMetadata, userOverrides.skills, globalBaseDir);
        // User skills from ~/.agents/ (with its own baseDir)
        const userAgentsBaseDir = dirname(userAgentsSkillsDir);
        const userAgentsMetadata = {
            ...userMetadata,
            baseDir: userAgentsBaseDir,
        };
        addResources("skills", collectAutoSkillEntries(userAgentsSkillsDir, "agents"), userAgentsMetadata, userOverrides.skills, userAgentsBaseDir);
        addResources("prompts", collectAutoPromptEntries(userDirs.prompts), userMetadata, userOverrides.prompts, globalBaseDir);
        addResources("themes", collectAutoThemeEntries(userDirs.themes), userMetadata, userOverrides.themes, globalBaseDir);
    }
    collectFilesFromPaths(paths, resourceType) {
        const files = [];
        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 errors
            }
        }
        return files;
    }
    getTargetMap(accumulator, resourceType) {
        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}`);
        }
    }
    addResource(map, path, metadata, enabled) {
        if (!path)
            return;
        if (!map.has(path)) {
            map.set(path, { metadata, enabled });
        }
    }
    createAccumulator() {
        return {
            extensions: new Map(),
            skills: new Map(),
            prompts: new Map(),
            themes: new Map(),
        };
    }
    toResolvedPaths(accumulator) {
        const mapToResolved = (entries) => {
            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();
            return resolved.filter((entry) => {
                const pathKey = getResourcePathKey(entry.path);
                if (seen.has(pathKey))
                    return false;
                seen.add(pathKey);
                return true;
            });
        };
        return {
            extensions: mapToResolved(accumulator.extensions),
            skills: mapToResolved(accumulator.skills),
            prompts: mapToResolved(accumulator.prompts),
            themes: mapToResolved(accumulator.themes),
        };
    }
    spawnCommand(command, args, options) {
        const env = getEnv();
        return spawnProcess(command, args, {
            cwd: options?.cwd,
            stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit",
            env,
        });
    }
    spawnCaptureCommand(command, args, options) {
        const baseEnv = getEnv();
        const env = options?.env ? { ...baseEnv, ...options.env } : baseEnv;
        return spawnProcess(command, args, {
            cwd: options?.cwd,
            stdio: ["ignore", "pipe", "pipe"],
            env,
        });
    }
    runCommandCapture(command, args, options) {
        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}`));
            });
        });
    }
    runCommand(command, args, options) {
        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}`));
                }
            });
        });
    }
    runCommandSync(command, args) {
        const env = getEnv();
        const result = spawnProcessSync(command, args, {
            stdio: ["ignore", "pipe", "pipe"],
            encoding: "utf-8",
            env,
        });
        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();
    }
}
//# sourceMappingURL=package-manager.js.map