feat: add Kilo CLI runtime support

This commit is contained in:
Alex Alecu
2026-03-31 15:59:31 +03:00
parent 1421dc07bc
commit ac4836d270
38 changed files with 1191 additions and 292 deletions

View File

@@ -1,8 +1,14 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { GSDTools, GSDToolsError, resolveGsdToolsPath } from './gsd-tools.js';
import { mkdir, writeFile, rm } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir, homedir } from 'node:os';
import { fileURLToPath } from 'node:url';
const BUNDLED_GSD_TOOLS_PATH = fileURLToPath(
new URL('../../get-shit-done/bin/gsd-tools.cjs', import.meta.url),
);
describe('GSDTools', () => {
let tmpDir: string;
@@ -330,11 +336,13 @@ describe('GSDTools', () => {
expect(result).toBe(join(localBinDir, 'gsd-tools.cjs'));
});
it('falls back to global path when repo-local does not exist', () => {
it('falls back to bundled repo path when repo-local does not exist', () => {
const result = resolveGsdToolsPath(tmpDir);
expect(result).toBe(
join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
);
const expected = existsSync(BUNDLED_GSD_TOOLS_PATH)
? BUNDLED_GSD_TOOLS_PATH
: join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
expect(result).toBe(expected);
});
it('constructor uses repo-local path when available', async () => {

View File

@@ -10,6 +10,7 @@ import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { fileURLToPath } from 'node:url';
import type { InitNewProjectInfo, PhaseOpInfo, PhasePlanIndex, RoadmapAnalysis } from './types.js';
// ─── Error type ──────────────────────────────────────────────────────────────
@@ -30,6 +31,9 @@ export class GSDToolsError extends Error {
// ─── GSDTools class ──────────────────────────────────────────────────────────
const DEFAULT_TIMEOUT_MS = 30_000;
const BUNDLED_GSD_TOOLS_PATH = fileURLToPath(
new URL('../../get-shit-done/bin/gsd-tools.cjs', import.meta.url),
);
export class GSDTools {
private readonly projectDir: string;
@@ -58,7 +62,7 @@ export class GSDTools {
return new Promise<unknown>((resolve, reject) => {
const child = execFile(
'node',
process.execPath,
fullArgs,
{
cwd: this.projectDir,
@@ -160,7 +164,7 @@ export class GSDTools {
return new Promise<string>((resolve, reject) => {
const child = execFile(
'node',
process.execPath,
fullArgs,
{
cwd: this.projectDir,
@@ -285,11 +289,15 @@ export class GSDTools {
// ─── Path resolution ────────────────────────────────────────────────────────
/**
* Resolve gsd-tools.cjs path with repo-local fallback.
* Probe order: repo-local → global home directory.
* Resolve gsd-tools.cjs path with bundled-repo fallback.
* Probe order: project-local → repo-bundled → 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');
const candidates = [
join(projectDir, '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
BUNDLED_GSD_TOOLS_PATH,
join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
];
return candidates.find(candidate => existsSync(candidate)) ?? candidates[candidates.length - 1]!;
}

View File

@@ -13,13 +13,13 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { execSync } from 'node:child_process';
import { mkdtemp, rm, readFile, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { homedir } from 'node:os';
import { InitRunner } from './init-runner.js';
import { GSDTools } from './gsd-tools.js';
import { GSDTools, resolveGsdToolsPath } from './gsd-tools.js';
import { GSDEventStream } from './event-stream.js';
import { GSDEventType } from './types.js';
import type { GSDEvent } from './types.js';
@@ -36,11 +36,12 @@ try {
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const sdkPromptsDir = join(__dirname, '..', 'prompts');
const GSD_TOOLS_PATH = join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
const GSD_TOOLS_PATH = resolveGsdToolsPath(process.cwd());
const gsdToolsAvailable = existsSync(GSD_TOOLS_PATH);
// ─── Test suite ──────────────────────────────────────────────────────────────
describe.skipIf(!cliAvailable)('E2E: InitRunner.run() full workflow', () => {
describe.skipIf(!cliAvailable || !gsdToolsAvailable)('E2E: InitRunner.run() full workflow', () => {
let tmpDir: string;
let events: GSDEvent[];

View File

@@ -14,14 +14,14 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { execSync } from 'node:child_process';
import { mkdtemp, rm, readFile, stat, readdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { homedir } from 'node:os';
import { GSD } from './index.js';
import { InitRunner } from './init-runner.js';
import { GSDTools } from './gsd-tools.js';
import { GSDTools, resolveGsdToolsPath } from './gsd-tools.js';
import { GSDEventStream } from './event-stream.js';
import { GSDEventType, PhaseStepType } from './types.js';
import type { GSDEvent, PhaseRunnerResult, RoadmapAnalysis } from './types.js';
@@ -38,7 +38,8 @@ try {
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const sdkPromptsDir = join(__dirname, '..', 'prompts');
const GSD_TOOLS_PATH = join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
const GSD_TOOLS_PATH = resolveGsdToolsPath(process.cwd());
const gsdToolsAvailable = existsSync(GSD_TOOLS_PATH);
// ─── Lifecycle step ordering for monotonicity check ──────────────────────────
@@ -54,7 +55,7 @@ const STEP_ORDER: Record<string, number> = {
// ─── Test suite ──────────────────────────────────────────────────────────────
describe.skipIf(!cliAvailable)('E2E Lifecycle: InitRunner → GSD.runPhase() full lifecycle', () => {
describe.skipIf(!cliAvailable || !gsdToolsAvailable)('E2E Lifecycle: InitRunner → GSD.runPhase() full lifecycle', () => {
let tmpDir: string;
let initSuccess: boolean = false;
let phase1Number: string | null = null;

View File

@@ -8,11 +8,11 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { homedir } from 'node:os';
import { GSDTools } from './gsd-tools.js';
import { GSDTools, resolveGsdToolsPath } from './gsd-tools.js';
import { PhaseRunner } from './phase-runner.js';
import type { PhaseRunnerDeps } from './phase-runner.js';
import { ContextEngine } from './context-engine.js';
@@ -24,7 +24,8 @@ import { GSDEventType, PhaseStepType } from './types.js';
// ─── Helpers ─────────────────────────────────────────────────────────────────
const GSD_TOOLS_PATH = join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
const GSD_TOOLS_PATH = resolveGsdToolsPath(process.cwd());
const gsdToolsAvailable = existsSync(GSD_TOOLS_PATH);
async function createTempPlanningDir(): Promise<string> {
const tmpDir = await mkdtemp(join(tmpdir(), 'gsd-sdk-phase-int-'));
@@ -65,7 +66,7 @@ async function createTempPlanningDir(): Promise<string> {
// ─── Test suite ──────────────────────────────────────────────────────────────
describe('Integration: PhaseRunner against real gsd-tools.cjs', () => {
describe.skipIf(!gsdToolsAvailable)('Integration: PhaseRunner against real gsd-tools.cjs', () => {
let tmpDir: string;
let tools: GSDTools;
@@ -303,7 +304,7 @@ must_haves:
return tmpDir;
}
describe('Integration: phasePlanIndex and wave execution', () => {
describe.skipIf(!gsdToolsAvailable)('Integration: phasePlanIndex and wave execution', () => {
let tmpDir: string;
let tools: GSDTools;

View File

@@ -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 <projectDir>/.claude/ then ~/.claude/. */
/** Path to gsd-tools.cjs. Falls back to <projectDir>/.claude/, then the bundled repo path, then ~/.claude/. */
gsdToolsPath?: string;
/** Model to use for execution sessions. */
model?: string;