diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index bdd50bb7c..daf673ca5 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -6,6 +6,13 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +// ─── Path helpers ──────────────────────────────────────────────────────────── + +/** Normalize a relative path to always use forward slashes (cross-platform). */ +function toPosixPath(p) { + return p.split(path.sep).join('/'); +} + // ─── Model Profile Table ───────────────────────────────────────────────────── const MODEL_PROFILES = { @@ -218,7 +225,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) { return { found: true, - directory: path.join(relBase, match), + directory: toPosixPath(path.join(relBase, match)), phase_number: phaseNumber, phase_name: phaseName, phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null, @@ -241,7 +248,7 @@ function findPhaseInternal(cwd, phase) { const normalized = normalizePhaseName(phase); // Search current phases first - const current = searchPhaseInDir(phasesDir, path.join('.planning', 'phases'), normalized); + const current = searchPhaseInDir(phasesDir, '.planning/phases', normalized); if (current) return current; // Search archived milestone phases (newest first) @@ -259,7 +266,7 @@ function findPhaseInternal(cwd, phase) { for (const archiveName of archiveDirs) { const version = archiveName.match(/^(v[\d.]+)-phases$/)[1]; const archivePath = path.join(milestonesDir, archiveName); - const relBase = path.join('.planning', 'milestones', archiveName); + const relBase = '.planning/milestones/' + archiveName; const result = searchPhaseInDir(archivePath, relBase, normalized); if (result) { result.archived = version; @@ -419,4 +426,5 @@ module.exports = { pathExistsInternal, generateSlugInternal, getMilestoneInfo, + toPosixPath, }; diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index c2933a8b0..7e551a01f 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -5,7 +5,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, output, error } = require('./core.cjs'); +const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs'); function cmdInitExecutePhase(cwd, phase, raw) { if (!phase) { @@ -139,19 +139,19 @@ function cmdInitPlanPhase(cwd, phase, raw) { const files = fs.readdirSync(phaseDirFull); const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'); if (contextFile) { - result.context_path = path.join(phaseInfo.directory, contextFile); + result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile)); } const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'); if (researchFile) { - result.research_path = path.join(phaseInfo.directory, researchFile); + result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile)); } const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'); if (verificationFile) { - result.verification_path = path.join(phaseInfo.directory, verificationFile); + result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile)); } const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md'); if (uatFile) { - result.uat_path = path.join(phaseInfo.directory, uatFile); + result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile)); } } catch {} } @@ -422,19 +422,19 @@ function cmdInitPhaseOp(cwd, phase, raw) { const files = fs.readdirSync(phaseDirFull); const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'); if (contextFile) { - result.context_path = path.join(phaseInfo.directory, contextFile); + result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile)); } const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'); if (researchFile) { - result.research_path = path.join(phaseInfo.directory, researchFile); + result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile)); } const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'); if (verificationFile) { - result.verification_path = path.join(phaseInfo.directory, verificationFile); + result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile)); } const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md'); if (uatFile) { - result.uat_path = path.join(phaseInfo.directory, uatFile); + result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile)); } } catch {} } @@ -469,7 +469,7 @@ function cmdInitTodos(cwd, area, raw) { created: createdMatch ? createdMatch[1].trim() : 'unknown', title: titleMatch ? titleMatch[1].trim() : 'Untitled', area: todoArea, - path: path.join('.planning', 'todos', 'pending', file), + path: '.planning/todos/pending/' + file, }); } catch {} } @@ -629,7 +629,7 @@ function cmdInitProgress(cwd, raw) { const phaseInfo = { number: phaseNumber, name: phaseName, - directory: path.join('.planning', 'phases', dir), + directory: '.planning/phases/' + dir, status, plan_count: plans.length, summary_count: summaries.length, diff --git a/tests/frontmatter-cli.test.cjs b/tests/frontmatter-cli.test.cjs index de014b796..a07ec4366 100644 --- a/tests/frontmatter-cli.test.cjs +++ b/tests/frontmatter-cli.test.cjs @@ -107,7 +107,7 @@ describe('frontmatter set', () => { test('handles JSON array value', () => { const file = writeTempFile('---\nphase: 01\n---\nbody'); - const result = runGsdTools(`frontmatter set ${file} --field tags --value '["a","b"]'`); + const result = runGsdTools(['frontmatter', 'set', file, '--field', 'tags', '--value', '["a","b"]']); assert.ok(result.success, `Command failed: ${result.error}`); const content = fs.readFileSync(file, 'utf-8'); @@ -140,7 +140,7 @@ describe('frontmatter set', () => { describe('frontmatter merge', () => { test('merges multiple fields into frontmatter', () => { const file = writeTempFile('---\nphase: 01\n---\nbody'); - const result = runGsdTools(`frontmatter merge ${file} --data '{"plan":"02","type":"tdd"}'`); + const result = runGsdTools(['frontmatter', 'merge', file, '--data', '{"plan":"02","type":"tdd"}']); assert.ok(result.success, `Command failed: ${result.error}`); const content = fs.readFileSync(file, 'utf-8'); @@ -153,7 +153,7 @@ describe('frontmatter merge', () => { test('overwrites existing fields on conflict', () => { const file = writeTempFile('---\nphase: 01\ntype: execute\n---\nbody'); - const result = runGsdTools(`frontmatter merge ${file} --data '{"phase":"02"}'`); + const result = runGsdTools(['frontmatter', 'merge', file, '--data', '{"phase":"02"}']); assert.ok(result.success, `Command failed: ${result.error}`); const content = fs.readFileSync(file, 'utf-8'); diff --git a/tests/helpers.cjs b/tests/helpers.cjs index 68b773af7..4dddcf461 100644 --- a/tests/helpers.cjs +++ b/tests/helpers.cjs @@ -2,20 +2,35 @@ * GSD Tools Test Helpers */ -const { execSync } = require('child_process'); +const { execSync, execFileSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const TOOLS_PATH = path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs'); -// Helper to run gsd-tools command +/** + * Run gsd-tools command. + * + * @param {string|string[]} args - Command string (shell-interpreted) or array + * of arguments (shell-bypassed via execFileSync, safe for JSON and dollar signs). + * @param {string} cwd - Working directory. + */ function runGsdTools(args, cwd = process.cwd()) { try { - const result = execSync(`node "${TOOLS_PATH}" ${args}`, { - cwd, - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }); + let result; + if (Array.isArray(args)) { + result = execFileSync(process.execPath, [TOOLS_PATH, ...args], { + cwd, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + }); + } else { + result = execSync(`node "${TOOLS_PATH}" ${args}`, { + cwd, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + }); + } return { success: true, output: result.trim() }; } catch (err) { return { diff --git a/tests/state.test.cjs b/tests/state.test.cjs index ff70b02a0..a8b2100e8 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -205,7 +205,7 @@ None ); const result = runGsdTools( - "state add-decision --phase 11-01 --summary 'Benchmark prices moved from $0.50 to $2.00 to $5.00' --rationale 'track cost growth'", + ['state', 'add-decision', '--phase', '11-01', '--summary', 'Benchmark prices moved from $0.50 to $2.00 to $5.00', '--rationale', 'track cost growth'], tmpDir ); assert.ok(result.success, `Command failed: ${result.error}`); @@ -233,7 +233,7 @@ None ` ); - const result = runGsdTools("state add-blocker --text 'Waiting on vendor quote $1.00 before approval'", tmpDir); + const result = runGsdTools(['state', 'add-blocker', '--text', 'Waiting on vendor quote $1.00 before approval'], tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');