fix: resolve gsd-tools.cjs from repo-local installation before global fallback (#1425)
Probe <projectDir>/.claude/get-shit-done/bin/gsd-tools.cjs before falling back to ~/.claude/get-shit-done/bin/gsd-tools.cjs, fixing MODULE_NOT_FOUND for repo-local GSD installations. Also adds repo-local agent definition path. Closes #1424 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { GSDTools, GSDToolsError } from './gsd-tools.js';
|
||||
import { GSDTools, GSDToolsError, resolveGsdToolsPath } from './gsd-tools.js';
|
||||
import { mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { tmpdir, homedir } from 'node:os';
|
||||
|
||||
describe('GSDTools', () => {
|
||||
let tmpDir: string;
|
||||
@@ -318,6 +318,42 @@ describe('GSDTools', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── resolveGsdToolsPath() tests ────────────────────────────────────────
|
||||
|
||||
describe('resolveGsdToolsPath()', () => {
|
||||
it('returns repo-local path when it exists', async () => {
|
||||
const localBinDir = join(tmpDir, '.claude', 'get-shit-done', 'bin');
|
||||
await mkdir(localBinDir, { recursive: true });
|
||||
await writeFile(join(localBinDir, 'gsd-tools.cjs'), '// stub');
|
||||
|
||||
const result = resolveGsdToolsPath(tmpDir);
|
||||
expect(result).toBe(join(localBinDir, 'gsd-tools.cjs'));
|
||||
});
|
||||
|
||||
it('falls back to global path when repo-local does not exist', () => {
|
||||
const result = resolveGsdToolsPath(tmpDir);
|
||||
expect(result).toBe(
|
||||
join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
);
|
||||
});
|
||||
|
||||
it('constructor uses repo-local path when available', async () => {
|
||||
const localBinDir = join(tmpDir, '.claude', 'get-shit-done', 'bin');
|
||||
await mkdir(localBinDir, { recursive: true });
|
||||
const scriptPath = join(localBinDir, 'gsd-tools.cjs');
|
||||
await writeFile(
|
||||
scriptPath,
|
||||
`process.stdout.write(JSON.stringify({ source: "local" }));`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
|
||||
// No explicit gsdToolsPath — should auto-resolve to local
|
||||
const tools = new GSDTools({ projectDir: tmpDir });
|
||||
const result = await tools.exec('test', []);
|
||||
expect(result).toEqual({ source: 'local' });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── configSet() tests ─────────────────────────────────────────────────
|
||||
|
||||
describe('configSet()', () => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { execFile } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import type { InitNewProjectInfo, PhaseOpInfo, PhasePlanIndex, RoadmapAnalysis } from './types.js';
|
||||
@@ -42,8 +43,7 @@ export class GSDTools {
|
||||
}) {
|
||||
this.projectDir = opts.projectDir;
|
||||
this.gsdToolsPath =
|
||||
opts.gsdToolsPath ??
|
||||
join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
opts.gsdToolsPath ?? resolveGsdToolsPath(opts.projectDir);
|
||||
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
@@ -281,3 +281,15 @@ export class GSDTools {
|
||||
return this.execRaw('config-set', [key, value]);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Path resolution ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve gsd-tools.cjs path with repo-local fallback.
|
||||
* Probe order: repo-local → global home directory.
|
||||
*/
|
||||
export function resolveGsdToolsPath(projectDir: string): string {
|
||||
const localPath = join(projectDir, '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
if (existsSync(localPath)) return localPath;
|
||||
return join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import type { GSDOptions, PlanResult, SessionOptions, GSDEvent, TransportHandler
|
||||
import { GSDEventType } from './types.js';
|
||||
import { parsePlan, parsePlanFile } from './plan-parser.js';
|
||||
import { loadConfig } from './config.js';
|
||||
import { GSDTools } from './gsd-tools.js';
|
||||
import { GSDTools, resolveGsdToolsPath } from './gsd-tools.js';
|
||||
import { runPlanSession } from './session-runner.js';
|
||||
import { buildExecutorPrompt, parseAgentTools } from './prompt-builder.js';
|
||||
import { GSDEventStream } from './event-stream.js';
|
||||
@@ -49,8 +49,7 @@ export class GSD {
|
||||
constructor(options: GSDOptions) {
|
||||
this.projectDir = resolve(options.projectDir);
|
||||
this.gsdToolsPath =
|
||||
options.gsdToolsPath ??
|
||||
join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
options.gsdToolsPath ?? resolveGsdToolsPath(this.projectDir);
|
||||
this.defaultModel = options.model;
|
||||
this.defaultMaxBudgetUsd = options.maxBudgetUsd ?? 5.0;
|
||||
this.defaultMaxTurns = options.maxTurns ?? 50;
|
||||
@@ -261,6 +260,11 @@ export class GSD {
|
||||
*/
|
||||
private async loadAgentDefinition(): Promise<string | undefined> {
|
||||
const paths = [
|
||||
// Repo-local GSD installation
|
||||
join(this.projectDir, '.claude', 'get-shit-done', 'agents', 'gsd-executor.md'),
|
||||
// Repo-local agents directory
|
||||
join(this.projectDir, '.claude', 'agents', 'gsd-executor.md'),
|
||||
// Global home directory
|
||||
join(homedir(), '.claude', 'agents', 'gsd-executor.md'),
|
||||
join(this.projectDir, 'agents', 'gsd-executor.md'),
|
||||
];
|
||||
@@ -282,7 +286,7 @@ export class GSD {
|
||||
export { parsePlan, parsePlanFile } from './plan-parser.js';
|
||||
export { loadConfig } from './config.js';
|
||||
export type { GSDConfig } from './config.js';
|
||||
export { GSDTools, GSDToolsError } from './gsd-tools.js';
|
||||
export { GSDTools, GSDToolsError, resolveGsdToolsPath } from './gsd-tools.js';
|
||||
export { runPlanSession, runPhaseStepSession } from './session-runner.js';
|
||||
export { buildExecutorPrompt, parseAgentTools } from './prompt-builder.js';
|
||||
export * from './types.js';
|
||||
|
||||
@@ -71,6 +71,7 @@ vi.mock('./gsd-tools.js', () => ({
|
||||
GSDToolsError: class extends Error {
|
||||
name = 'GSDToolsError';
|
||||
},
|
||||
resolveGsdToolsPath: vi.fn().mockReturnValue('/mock/gsd-tools.cjs'),
|
||||
}));
|
||||
|
||||
import { GSD } from './index.js';
|
||||
|
||||
@@ -197,7 +197,7 @@ export interface PlanResult {
|
||||
export interface GSDOptions {
|
||||
/** Root directory of the project. */
|
||||
projectDir: string;
|
||||
/** Path to gsd-tools.cjs. Falls back to ~/.claude/get-shit-done/bin/gsd-tools.cjs. */
|
||||
/** Path to gsd-tools.cjs. Falls back to <projectDir>/.claude/ then ~/.claude/. */
|
||||
gsdToolsPath?: string;
|
||||
/** Model to use for execution sessions. */
|
||||
model?: string;
|
||||
|
||||
Reference in New Issue
Block a user