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) <noreply@anthropic.com>
This commit is contained in:
ace-pm 2026-04-15 11:44:52 +02:00
parent 2f2f1845f7
commit 6612456934
4 changed files with 58 additions and 7 deletions

View file

@ -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,
})

View file

@ -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<string> {
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 {

View file

@ -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 [];

View file

@ -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"]
}
}