From 66124569343122e2a9206fb7173a4e5b68fda4b3 Mon Sep 17 00:00:00 2001 From: ace-pm Date: Wed, 15 Apr 2026 11:44:52 +0200 Subject: [PATCH] fix(extensions): route print mode through buildResourceLoader Print mode was constructing DefaultResourceLoader directly, which bypassed the GSD extension registry filter and let disabled bundled extensions leak through. With the community @0xkobold/pi-ollama installed, every `gsd -p` invocation printed an /ollama command conflict because the bundled ollama extension (explicitly disabled in ~/.gsd/extensions/registry.json) was still being loaded. - Add extension-manifest.json for the bundled ollama extension so the registry's id-keyed disable entry can actually target it. - Extend buildResourceLoader() with an options bag for print-mode callers (additionalExtensionPaths, appendSystemPrompt). - Switch print mode to buildResourceLoader() so the registry filter (extensionPathsTransform) runs in both TUI and print paths. Also fix a stderr leak in the GSD codebase-generator: execSync("git ls-files") was inheriting stderr to the parent, so running gsd from a non-repo cwd (e.g. $HOME) printed "fatal: not a git repository" before the catch silently returned []. Pipe stderr so it lands in the thrown Error instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli.ts | 8 +++-- src/resource-loader.ts | 35 ++++++++++++++++--- .../extensions/gsd/codebase-generator.ts | 11 +++++- .../extensions/ollama/extension-manifest.json | 11 ++++++ 4 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 src/resources/extensions/ollama/extension-manifest.json diff --git a/src/cli.ts b/src/cli.ts index 5c128053d..a516a7e59 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -544,8 +544,12 @@ if (isPrintMode) { exitIfManagedResourcesAreNewer(agentDir) initResources(agentDir) markStartup('initResources') - const resourceLoader = new DefaultResourceLoader({ - agentDir, + // Route print mode through buildResourceLoader so the GSD extension registry + // filter (extensionPathsTransform) is applied consistently with TUI mode. + // Constructing DefaultResourceLoader directly bypassed the filter and let + // disabled bundled extensions (e.g. `ollama` superseded by `@0xkobold/pi-ollama`) + // leak through and emit `/ollama` command conflicts on every print invocation. + const resourceLoader = buildResourceLoader(agentDir, { additionalExtensionPaths: cliFlags.extensions.length > 0 ? cliFlags.extensions : undefined, appendSystemPrompt, }) diff --git a/src/resource-loader.ts b/src/resource-loader.ts index 48a07fc35..38885f4b4 100644 --- a/src/resource-loader.ts +++ b/src/resource-loader.ts @@ -1,4 +1,5 @@ import { DefaultResourceLoader, sortExtensionPaths } from '@gsd/pi-coding-agent' +if (process.env.GSD_DEBUG_EXTENSIONS) process.stderr.write("[gsd-debug] resource-loader.ts loaded\n") import { createHash } from 'node:crypto' import { homedir } from 'node:os' import { chmodSync, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, openSync, closeSync, readFileSync, readlinkSync, readdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs' @@ -730,7 +731,22 @@ function getBundledExtensionKeys(): Set { return _bundledExtensionKeys } -export function buildResourceLoader(agentDir: string): DefaultResourceLoader { +/** + * Optional overrides passed through to DefaultResourceLoader. Print mode + * needs these — it used to construct DefaultResourceLoader directly, which + * bypassed buildResourceLoader's extensionPathsTransform (= the GSD registry + * filter) and let disabled bundled extensions like `ollama` leak through and + * conflict with community replacements such as `@0xkobold/pi-ollama`. + */ +export interface BuildResourceLoaderOptions { + additionalExtensionPaths?: string[] + appendSystemPrompt?: string +} + +export function buildResourceLoader( + agentDir: string, + options: BuildResourceLoaderOptions = {}, +): DefaultResourceLoader { const registry = loadRegistry() const piAgentDir = join(homedir(), '.pi', 'agent') const piExtensionsDir = join(piAgentDir, 'extensions') @@ -743,19 +759,30 @@ export function buildResourceLoader(agentDir: string): DefaultResourceLoader { return isExtensionEnabled(registry, manifest.id) }) + // Print-mode callers pass their own additional extension paths (e.g. --extension + // flags). Non-print mode uses the implicit pi-extensions discovery above. + const additionalExtensionPaths = + options.additionalExtensionPaths && options.additionalExtensionPaths.length > 0 + ? options.additionalExtensionPaths + : piExtensionPaths + return new DefaultResourceLoader({ agentDir, - additionalExtensionPaths: piExtensionPaths, + additionalExtensionPaths, + appendSystemPrompt: options.appendSystemPrompt, bundledExtensionKeys: bundledKeys, extensionPathsTransform: (paths: string[]) => { - // 1. Filter community extensions through the GSD registry + // Filter community + bundled extensions through the GSD registry so + // explicitly-disabled entries (e.g. bundled `ollama` superseded by + // `@0xkobold/pi-ollama`) never reach the runtime and trigger command + // conflicts. const filteredPaths = paths.filter((entryPath) => { const manifest = readManifestFromEntryPath(entryPath) if (!manifest) return true // no manifest = always load return isExtensionEnabled(registry, manifest.id) }) - // 2. Sort in topological dependency order + // Sort in topological dependency order const { sortedPaths, warnings } = sortExtensionPaths(filteredPaths) return { diff --git a/src/resources/extensions/gsd/codebase-generator.ts b/src/resources/extensions/gsd/codebase-generator.ts index b291c3c1f..b0e777c85 100644 --- a/src/resources/extensions/gsd/codebase-generator.ts +++ b/src/resources/extensions/gsd/codebase-generator.ts @@ -199,7 +199,16 @@ function shouldExclude(filePath: string, excludes: string[]): boolean { function lsFiles(basePath: string): string[] { try { - const result = execSync("git ls-files", { cwd: basePath, encoding: "utf-8", timeout: 10000 }); + // stdio: "pipe" captures stderr into the thrown Error instead of + // inheriting it to the parent. Without it, running gsd from a non-repo + // cwd (e.g. `$HOME`) leaks a "fatal: not a git repository" line to the + // user's terminal before the catch silently falls through to []. + const result = execSync("git ls-files", { + cwd: basePath, + encoding: "utf-8", + timeout: 10000, + stdio: ["ignore", "pipe", "pipe"], + }); return result.split("\n").filter(Boolean); } catch { return []; diff --git a/src/resources/extensions/ollama/extension-manifest.json b/src/resources/extensions/ollama/extension-manifest.json new file mode 100644 index 000000000..aeab597fc --- /dev/null +++ b/src/resources/extensions/ollama/extension-manifest.json @@ -0,0 +1,11 @@ +{ + "id": "ollama", + "name": "Ollama", + "version": "1.0.0", + "description": "Local Ollama model discovery and /ollama command", + "tier": "bundled", + "requires": { "platform": ">=2.29.0" }, + "provides": { + "commands": ["ollama"] + } +}