From a3000d463000b653df86c0ff0dc284d2d0654355 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 1 Aug 2026 05:40:06 +0800 Subject: [PATCH] fix(skillpack): resolve gbrain root from module path when cwd walk fails (#3144) Co-Authored-By: Time Attakc <89218912+time-attack@users.noreply.github.com> --- src/core/skillpack/bundle.ts | 49 ++++++++++++++++++++++++++-------- test/skillpack-install.test.ts | 17 ++++++++++++ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/core/skillpack/bundle.ts b/src/core/skillpack/bundle.ts index b35ea93a5..61db20dd9 100644 --- a/src/core/skillpack/bundle.ts +++ b/src/core/skillpack/bundle.ts @@ -9,6 +9,7 @@ import { existsSync, readFileSync, statSync, readdirSync } from 'fs'; import { join, dirname, isAbsolute, resolve } from 'path'; +import { fileURLToPath } from 'url'; import { parseMarkdown } from '../markdown.ts'; @@ -38,19 +39,45 @@ export class BundleError extends Error { /** * Walk up from `start` (default cwd) looking for an `openclaw.plugin.json` * sibling to `src/cli.ts`. That pair identifies a gbrain repo root. + * + * When no explicit `start` is given and the cwd walk fails (e.g. gbrain was + * installed globally via `bun install -g` and the user is in an unrelated + * directory, #1917), fall back to walking up from this module's own location + * and from the running entrypoint (`process.argv[1]`). Both resolve the + * bun-global layout (~/.bun/install/global/node_modules/gbrain/) and the + * in-repo compiled binary (bin/gbrain). */ -export function findGbrainRoot(start: string = process.cwd()): string | null { - let dir = resolve(start); - for (let i = 0; i < 10; i++) { - if ( - existsSync(join(dir, 'openclaw.plugin.json')) && - existsSync(join(dir, 'src', 'cli.ts')) - ) { - return dir; +export function findGbrainRoot(start?: string): string | null { + const walkUp = (from: string): string | null => { + let dir = resolve(from); + for (let i = 0; i < 10; i++) { + if ( + existsSync(join(dir, 'openclaw.plugin.json')) && + existsSync(join(dir, 'src', 'cli.ts')) + ) { + return dir; + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; } - const parent = dirname(dir); - if (parent === dir) break; - dir = parent; + return null; + }; + + const found = walkUp(start ?? process.cwd()); + if (found !== null || start !== undefined) return found; + + const fallbacks: string[] = []; + try { + // Not a file:// URL inside a compiled binary; skip on error. + fallbacks.push(dirname(fileURLToPath(import.meta.url))); + } catch { + /* ignore */ + } + if (process.argv[1]) fallbacks.push(dirname(resolve(process.argv[1]))); + for (const candidate of fallbacks) { + const root = walkUp(candidate); + if (root !== null) return root; } return null; } diff --git a/test/skillpack-install.test.ts b/test/skillpack-install.test.ts index 0d5345a11..1420357ca 100644 --- a/test/skillpack-install.test.ts +++ b/test/skillpack-install.test.ts @@ -125,6 +125,23 @@ describe('findGbrainRoot', () => { it('returns null when no gbrain root above', () => { expect(findGbrainRoot('/tmp/definitely-not-a-gbrain-repo-XYZ')).toBeNull(); }); + it('falls back to the module location when cwd has no markers (#1917)', () => { + // Simulate a bun-global install: cwd is an unrelated directory with no + // gbrain markers anywhere above it. The no-arg call must still resolve + // via bundle.ts's own location (which lives in the real repo). + const elsewhere = mkdtempSync(join(tmpdir(), 'skillpack-elsewhere-')); + created.push(elsewhere); + const prevCwd = process.cwd(); + try { + process.chdir(elsewhere); + const root = findGbrainRoot(); + expect(root).not.toBeNull(); + expect(existsSync(join(root!, 'openclaw.plugin.json'))).toBe(true); + expect(existsSync(join(root!, 'src', 'cli.ts'))).toBe(true); + } finally { + process.chdir(prevCwd); + } + }); }); describe('loadBundleManifest', () => {