context-planning 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +454 -0
- package/bin/commands/_helpers.js +53 -0
- package/bin/commands/_usage.js +67 -0
- package/bin/commands/capture.js +46 -0
- package/bin/commands/codebase-status.js +41 -0
- package/bin/commands/complete-milestone.js +57 -0
- package/bin/commands/config.js +70 -0
- package/bin/commands/doctor.js +139 -0
- package/bin/commands/gsd-import.js +90 -0
- package/bin/commands/inbox.js +81 -0
- package/bin/commands/index.js +33 -0
- package/bin/commands/init.js +87 -0
- package/bin/commands/install.js +43 -0
- package/bin/commands/scaffold-codebase.js +53 -0
- package/bin/commands/scaffold-milestone.js +58 -0
- package/bin/commands/scaffold-phase.js +65 -0
- package/bin/commands/status.js +42 -0
- package/bin/commands/statusline.js +108 -0
- package/bin/commands/tick.js +49 -0
- package/bin/commands/version.js +9 -0
- package/bin/commands/worktree.js +218 -0
- package/bin/commands/write-summary.js +54 -0
- package/bin/cp.cmd +2 -0
- package/bin/cp.js +54 -0
- package/commands/cp/capture.md +107 -0
- package/commands/cp/complete-milestone.md +166 -0
- package/commands/cp/execute-phase.md +220 -0
- package/commands/cp/map-codebase.md +211 -0
- package/commands/cp/new-milestone.md +136 -0
- package/commands/cp/new-project.md +132 -0
- package/commands/cp/plan-phase.md +195 -0
- package/commands/cp/progress.md +147 -0
- package/commands/cp/quick.md +104 -0
- package/commands/cp/resume.md +125 -0
- package/commands/cp/write-summary.md +33 -0
- package/docs/MIGRATION-v0.5.md +140 -0
- package/docs/architecture.md +189 -0
- package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-01-design-md-infrastructure.md +1064 -0
- package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-02-review-log-infrastructure.md +418 -0
- package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-03-key-decisions-hard-block.md +295 -0
- package/docs/superpowers/specs/2026-05-20-generic-provider-harness-detection-design.md +380 -0
- package/docs/superpowers/specs/2026-05-20-v0-7-design-capture-design.md +400 -0
- package/docs/writing-providers.md +76 -0
- package/install/aider.js +204 -0
- package/install/claude.js +116 -0
- package/install/common.js +65 -0
- package/install/copilot.js +86 -0
- package/install/cursor.js +120 -0
- package/install/echo-provider.js +50 -0
- package/lib/codebase-mapper.js +169 -0
- package/lib/detect.js +280 -0
- package/lib/frontmatter.js +72 -0
- package/lib/gsd-compat.js +165 -0
- package/lib/import.js +543 -0
- package/lib/inbox.js +226 -0
- package/lib/lifecycle.js +929 -0
- package/lib/merge.js +157 -0
- package/lib/milestone.js +595 -0
- package/lib/paths.js +191 -0
- package/lib/provider.js +168 -0
- package/lib/roadmap.js +134 -0
- package/lib/state.js +99 -0
- package/lib/worktree.js +253 -0
- package/package.json +45 -0
- package/templates/DESIGN.md +78 -0
- package/templates/INBOX.md +13 -0
- package/templates/MILESTONE-CONTEXT.md +40 -0
- package/templates/MILESTONES.md +29 -0
- package/templates/PLAN.md +84 -0
- package/templates/PROJECT.md +43 -0
- package/templates/REVIEW-LOG.md +38 -0
- package/templates/ROADMAP.md +34 -0
- package/templates/STATE.md +78 -0
- package/templates/SUMMARY.md +75 -0
- package/templates/codebase/ARCHITECTURE.md +30 -0
- package/templates/codebase/CONCERNS.md +30 -0
- package/templates/codebase/CONVENTIONS.md +30 -0
- package/templates/codebase/INTEGRATIONS.md +30 -0
- package/templates/codebase/STACK.md +26 -0
- package/templates/codebase/STRUCTURE.md +32 -0
- package/templates/codebase/TESTING.md +39 -0
- package/templates/config.json +173 -0
- package/templates/phase-PLAN.md +32 -0
- package/templates/quick-PLAN.md +24 -0
- package/templates/quick-SUMMARY.md +25 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { repoRoot } = require('../../lib/paths');
|
|
5
|
+
const lifecycle = require('../../lib/lifecycle');
|
|
6
|
+
const codebaseMapper = require('../../lib/codebase-mapper');
|
|
7
|
+
|
|
8
|
+
function run(args = []) {
|
|
9
|
+
const root = repoRoot();
|
|
10
|
+
let dryRun = false;
|
|
11
|
+
let force = false;
|
|
12
|
+
let noCommit = false;
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const a = args[i];
|
|
15
|
+
if (a === '--dry-run') dryRun = true;
|
|
16
|
+
else if (a === '--force') force = true;
|
|
17
|
+
else if (a === '--no-commit') noCommit = true;
|
|
18
|
+
else if (a.startsWith('-')) { console.error(`unknown option: ${a}`); process.exit(2); }
|
|
19
|
+
else { console.error(`unexpected arg: ${a}`); process.exit(2); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let r;
|
|
23
|
+
try {
|
|
24
|
+
r = codebaseMapper.scaffoldCodebase(root, { dryRun, force });
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error(`scaffold-codebase: ${e.message}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const a of r.actions) {
|
|
31
|
+
const rel = path.relative(root, a.path);
|
|
32
|
+
const mark = dryRun ? '·' : (a.kind === 'mkdir' ? '+' : '✓');
|
|
33
|
+
console.log(`${mark} ${a.kind.padEnd(5)} ${rel}`);
|
|
34
|
+
}
|
|
35
|
+
if (r.skipped.length) {
|
|
36
|
+
console.log(`\nSkipped ${r.skipped.length} existing file(s) — use --force to overwrite:`);
|
|
37
|
+
for (const s of r.skipped) console.log(` = ${s}`);
|
|
38
|
+
}
|
|
39
|
+
console.log(`\nCodebase dir: ${path.relative(root, r.codebaseDir)}`);
|
|
40
|
+
console.log(`Created: ${r.created.length} stub(s)`);
|
|
41
|
+
if (dryRun) return;
|
|
42
|
+
if (!noCommit && r.actions.some((a) => a.kind === 'write' || a.kind === 'mkdir')) {
|
|
43
|
+
const commit = lifecycle.gitCommit(root, `cp: scaffold-codebase (${r.created.length} stubs)`, {
|
|
44
|
+
paths: lifecycle.pathsFromActions(r.actions),
|
|
45
|
+
});
|
|
46
|
+
if (commit) console.log(`committed ${commit}`);
|
|
47
|
+
}
|
|
48
|
+
if (r.created.length > 0) {
|
|
49
|
+
console.log(`\nNext: run /cp-map-codebase to fill the stubs with a real analysis.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { name: 'scaffold-codebase', run };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { repoRoot } = require('../../lib/paths');
|
|
5
|
+
const lifecycle = require('../../lib/lifecycle');
|
|
6
|
+
|
|
7
|
+
function run(args = []) {
|
|
8
|
+
const root = repoRoot();
|
|
9
|
+
let name = null;
|
|
10
|
+
let dryRun = false;
|
|
11
|
+
let noCommit = false;
|
|
12
|
+
let status = 'in-progress';
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const a = args[i];
|
|
15
|
+
if (a === '--dry-run') dryRun = true;
|
|
16
|
+
else if (a === '--no-commit') noCommit = true;
|
|
17
|
+
else if (a === '--planned') status = 'planned';
|
|
18
|
+
else if (a === '--status') status = args[++i];
|
|
19
|
+
else if (a.startsWith('-')) { console.error(`unknown option: ${a}`); process.exit(2); }
|
|
20
|
+
else if (!name) name = a;
|
|
21
|
+
else { console.error(`unexpected arg: ${a}`); process.exit(2); }
|
|
22
|
+
}
|
|
23
|
+
if (!name) {
|
|
24
|
+
console.error('Usage: cp scaffold-milestone <name> [--planned] [--no-commit] [--dry-run]');
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let r;
|
|
29
|
+
try {
|
|
30
|
+
r = lifecycle.scaffoldMilestone(root, name, { dryRun, status });
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(`scaffold-milestone: ${e.message}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!r.ok) {
|
|
37
|
+
console.error(`scaffold-milestone: ${r.reason}`);
|
|
38
|
+
if (r.reason === 'milestone-exists') {
|
|
39
|
+
console.error(` "${r.milestone}" already exists (status: ${r.status}).`);
|
|
40
|
+
}
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const a of r.actions) {
|
|
45
|
+
const rel = path.relative(root, a.path);
|
|
46
|
+
console.log(`${dryRun ? '·' : '✓'} ${rel}`);
|
|
47
|
+
}
|
|
48
|
+
console.log(`Milestone: ${r.milestone} [${r.status}]`);
|
|
49
|
+
if (dryRun) return;
|
|
50
|
+
if (!noCommit) {
|
|
51
|
+
const commit = lifecycle.gitCommit(root, `cp: scaffold-milestone ${r.milestone}`, {
|
|
52
|
+
paths: lifecycle.pathsFromActions(r.actions),
|
|
53
|
+
});
|
|
54
|
+
if (commit) console.log(`committed ${commit}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { name: 'scaffold-milestone', run };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { repoRoot } = require('../../lib/paths');
|
|
5
|
+
const lifecycle = require('../../lib/lifecycle');
|
|
6
|
+
|
|
7
|
+
function run(args = []) {
|
|
8
|
+
const root = repoRoot();
|
|
9
|
+
let num = null;
|
|
10
|
+
let name = null;
|
|
11
|
+
let plans = 0;
|
|
12
|
+
let milestoneName = null;
|
|
13
|
+
let dryRun = false;
|
|
14
|
+
let noCommit = false;
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
const a = args[i];
|
|
17
|
+
if (a === '--name') name = args[++i];
|
|
18
|
+
else if (a === '--plans') plans = parseInt(args[++i], 10) || 0;
|
|
19
|
+
else if (a === '--milestone') milestoneName = args[++i];
|
|
20
|
+
else if (a === '--dry-run') dryRun = true;
|
|
21
|
+
else if (a === '--no-commit') noCommit = true;
|
|
22
|
+
else if (a.startsWith('-')) { console.error(`unknown option: ${a}`); process.exit(2); }
|
|
23
|
+
else if (!num) num = a;
|
|
24
|
+
else { console.error(`unexpected arg: ${a}`); process.exit(2); }
|
|
25
|
+
}
|
|
26
|
+
if (!num || !name) {
|
|
27
|
+
console.error('Usage: cp scaffold-phase <N> --name <name> [--plans <count>] [--milestone <name>] [--no-commit] [--dry-run]');
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let r;
|
|
32
|
+
try {
|
|
33
|
+
r = lifecycle.scaffoldPhase(root, num, { dryRun, name, plans, milestone: milestoneName });
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error(`scaffold-phase: ${e.message}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!r.ok) {
|
|
40
|
+
console.error(`scaffold-phase: ${r.reason}`);
|
|
41
|
+
if (r.reason === 'phase-exists') {
|
|
42
|
+
console.error(` ${path.relative(root, r.phaseDir)} already exists.`);
|
|
43
|
+
} else if (r.reason === 'milestone-not-found') {
|
|
44
|
+
console.error(` No milestone named "${milestoneName}" in ROADMAP.md.`);
|
|
45
|
+
} else if (r.reason === 'no-active-milestone') {
|
|
46
|
+
console.error(` No in-progress milestone. Run \`cp scaffold-milestone <name>\` first or pass --milestone.`);
|
|
47
|
+
}
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const a of r.actions) {
|
|
52
|
+
const rel = path.relative(root, a.path);
|
|
53
|
+
console.log(`${dryRun ? '·' : '✓'} ${rel}`);
|
|
54
|
+
}
|
|
55
|
+
console.log(`Phase ${r.phaseNum} added to milestone "${r.milestone}"${r.plans.length ? ` (${r.plans.length} plan${r.plans.length === 1 ? '' : 's'}: ${r.plans.join(', ')})` : ''}`);
|
|
56
|
+
if (dryRun) return;
|
|
57
|
+
if (!noCommit) {
|
|
58
|
+
const commit = lifecycle.gitCommit(root, `cp: scaffold-phase ${r.phaseNum} (${name})`, {
|
|
59
|
+
paths: lifecycle.pathsFromActions(r.actions),
|
|
60
|
+
});
|
|
61
|
+
if (commit) console.log(`committed ${commit}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { name: 'scaffold-phase', run };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pkg = require('../../package.json');
|
|
4
|
+
const { repoRoot } = require('../../lib/paths');
|
|
5
|
+
const lifecycle = require('../../lib/lifecycle');
|
|
6
|
+
|
|
7
|
+
function run(args = []) {
|
|
8
|
+
const root = repoRoot();
|
|
9
|
+
const json = args.includes('--json');
|
|
10
|
+
const r = lifecycle.statusReport(root);
|
|
11
|
+
if (json) {
|
|
12
|
+
console.log(JSON.stringify(r, null, 2));
|
|
13
|
+
process.exit(r.ok ? 0 : 1);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!r.ok) {
|
|
17
|
+
console.error(r.error);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
console.log(`cp v${pkg.version}`);
|
|
21
|
+
console.log(`Repo: ${root}`);
|
|
22
|
+
console.log(`Milestone: ${r.milestone || '(none in-progress)'}${r.milestoneStatus ? ` [${r.milestoneStatus}]` : ''}`);
|
|
23
|
+
if (r.phases.length === 0) {
|
|
24
|
+
console.log('Phases: (none yet — run `/cp-plan-phase 1`)');
|
|
25
|
+
} else {
|
|
26
|
+
console.log('Phases:');
|
|
27
|
+
for (const p of r.phases) {
|
|
28
|
+
const bar = p.total > 0 ? `${p.done}/${p.total}` : '0/0';
|
|
29
|
+
const mark = p.total > 0 && p.done === p.total ? '✓' : '·';
|
|
30
|
+
console.log(` ${mark} Phase ${p.num} ${p.name}: ${bar} plans done`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (r.nextPlan) {
|
|
34
|
+
console.log(`\nNext plan: ${r.nextPlan.planId} (Phase ${r.nextPlan.phaseNum}: ${r.nextPlan.phaseName})`);
|
|
35
|
+
console.log(` ${r.nextPlan.desc}`);
|
|
36
|
+
console.log(`\nDo: /cp-execute-phase ${r.nextPlan.phaseNum}`);
|
|
37
|
+
} else if (r.milestone) {
|
|
38
|
+
console.log(`\nAll plans done. Run \`cp complete-milestone\` (or \`/cp-complete-milestone\`).`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { name: 'status', run };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { repoRoot, planningDir } = require('../../lib/paths');
|
|
6
|
+
const lifecycle = require('../../lib/lifecycle');
|
|
7
|
+
|
|
8
|
+
function run(args = []) {
|
|
9
|
+
let format = null;
|
|
10
|
+
let json = false;
|
|
11
|
+
let noColor = false;
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const a = args[i];
|
|
14
|
+
if (a === '--json') json = true;
|
|
15
|
+
else if (a === '--no-color') noColor = true;
|
|
16
|
+
else if (a === '--format') {
|
|
17
|
+
format = args[++i];
|
|
18
|
+
if (format == null) { console.error('--format requires a value'); process.exit(2); }
|
|
19
|
+
} else { console.error(`unknown option: ${a}`); process.exit(2); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Stay silent outside a cp/git project. Used in shell prompts — must never
|
|
23
|
+
// emit noise that breaks PS1.
|
|
24
|
+
let root;
|
|
25
|
+
try { root = repoRoot(); }
|
|
26
|
+
catch { process.exit(0); }
|
|
27
|
+
const planning = planningDir(root);
|
|
28
|
+
if (!fs.existsSync(path.join(planning, 'ROADMAP.md'))) process.exit(0);
|
|
29
|
+
|
|
30
|
+
let r;
|
|
31
|
+
try { r = lifecycle.statusReport(root); }
|
|
32
|
+
catch { process.exit(0); }
|
|
33
|
+
if (!r.ok) process.exit(0);
|
|
34
|
+
|
|
35
|
+
// Compute the active phase: first phase with done < total. Fall back to
|
|
36
|
+
// last phase if all done.
|
|
37
|
+
let activePhase = r.phases.find((p) => p.done < p.total);
|
|
38
|
+
if (!activePhase && r.phases.length > 0) activePhase = r.phases[r.phases.length - 1];
|
|
39
|
+
|
|
40
|
+
const phaseLabel = activePhase
|
|
41
|
+
? `${String(activePhase.num).padStart(2, '0')}-${activePhase.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')}`
|
|
42
|
+
: null;
|
|
43
|
+
const doneTotal = activePhase ? `${activePhase.done}/${activePhase.total}` : null;
|
|
44
|
+
const nextId = r.nextPlan ? r.nextPlan.planId : null;
|
|
45
|
+
|
|
46
|
+
// Try to grab the current git branch (best-effort, silent on failure).
|
|
47
|
+
let branch = '';
|
|
48
|
+
try {
|
|
49
|
+
branch = require('child_process')
|
|
50
|
+
.execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
51
|
+
.toString().trim();
|
|
52
|
+
} catch { branch = ''; }
|
|
53
|
+
|
|
54
|
+
if (json) {
|
|
55
|
+
console.log(JSON.stringify({
|
|
56
|
+
milestone: r.milestone,
|
|
57
|
+
milestoneStatus: r.milestoneStatus,
|
|
58
|
+
phase: activePhase ? { num: activePhase.num, name: activePhase.name, label: phaseLabel, done: activePhase.done, total: activePhase.total } : null,
|
|
59
|
+
nextPlan: r.nextPlan,
|
|
60
|
+
branch,
|
|
61
|
+
}, null, 2));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Color helpers (ANSI). Detection is conservative: only emit when stdout
|
|
66
|
+
// is a TTY AND --no-color was NOT passed AND NO_COLOR env is unset.
|
|
67
|
+
const useColor = !noColor && process.stdout.isTTY && !process.env.NO_COLOR;
|
|
68
|
+
const c = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
69
|
+
const dim = (s) => c('2', s);
|
|
70
|
+
const bold = (s) => c('1', s);
|
|
71
|
+
const cyan = (s) => c('36', s);
|
|
72
|
+
const green = (s) => c('32', s);
|
|
73
|
+
const yellow = (s) => c('33', s);
|
|
74
|
+
|
|
75
|
+
// Custom format support: %M %P %D %N %B tokens.
|
|
76
|
+
if (format) {
|
|
77
|
+
const out = format
|
|
78
|
+
.replace(/%M/g, r.milestone || '')
|
|
79
|
+
.replace(/%P/g, phaseLabel || '')
|
|
80
|
+
.replace(/%D/g, doneTotal || '')
|
|
81
|
+
.replace(/%N/g, nextId || '')
|
|
82
|
+
.replace(/%B/g, branch || '');
|
|
83
|
+
console.log(out);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Default format. Examples:
|
|
88
|
+
// cp ▸ v0.4 ▸ 02-mvp 1/3 → 02-02
|
|
89
|
+
// cp ▸ v0.4 ▸ ✓ done
|
|
90
|
+
// cp ▸ (no milestone)
|
|
91
|
+
const arrow = dim('▸');
|
|
92
|
+
const prefix = bold(cyan('cp'));
|
|
93
|
+
if (!r.milestone) {
|
|
94
|
+
console.log(`${prefix} ${arrow} ${dim('(no milestone)')}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const mPart = green(r.milestone);
|
|
98
|
+
if (!activePhase) {
|
|
99
|
+
console.log(`${prefix} ${arrow} ${mPart}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const phasePart = activePhase.done >= activePhase.total
|
|
103
|
+
? `${green('✓')} ${dim('done')}`
|
|
104
|
+
: `${yellow(phaseLabel)} ${dim(doneTotal)}${nextId ? ' ' + arrow + ' ' + cyan(nextId) : ''}`;
|
|
105
|
+
console.log(`${prefix} ${arrow} ${mPart} ${arrow} ${phasePart}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { name: 'statusline', run };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { repoRoot } = require('../../lib/paths');
|
|
5
|
+
const lifecycle = require('../../lib/lifecycle');
|
|
6
|
+
|
|
7
|
+
function run(args = []) {
|
|
8
|
+
const root = repoRoot();
|
|
9
|
+
let planId = null;
|
|
10
|
+
let undo = false;
|
|
11
|
+
let noCommit = false;
|
|
12
|
+
let dryRun = false;
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const a = args[i];
|
|
15
|
+
if (a === '--undo') undo = true;
|
|
16
|
+
else if (a === '--no-commit') noCommit = true;
|
|
17
|
+
else if (a === '--dry-run') dryRun = true;
|
|
18
|
+
else if (a.startsWith('-')) { console.error(`unknown option: ${a}`); process.exit(2); }
|
|
19
|
+
else if (!planId) planId = a;
|
|
20
|
+
else { console.error(`unexpected arg: ${a}`); process.exit(2); }
|
|
21
|
+
}
|
|
22
|
+
if (!planId) { console.error('Usage: cp tick <plan-id> [--undo] [--no-commit] [--dry-run]'); process.exit(2); }
|
|
23
|
+
|
|
24
|
+
let result;
|
|
25
|
+
try {
|
|
26
|
+
result = lifecycle.tickPlan(root, planId, { dryRun, done: !undo });
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error(`tick: ${e.message}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
for (const a of result.actions) {
|
|
32
|
+
const rel = path.relative(root, a.path);
|
|
33
|
+
console.log(`${dryRun ? '·' : '✓'} ${rel}`);
|
|
34
|
+
}
|
|
35
|
+
if (result.actions.length === 0) {
|
|
36
|
+
console.log(`(no change — plan ${planId} already ${undo ? 'unticked' : 'ticked'})`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (dryRun) return;
|
|
40
|
+
if (!noCommit) {
|
|
41
|
+
const verb = undo ? 'untick' : 'tick';
|
|
42
|
+
const commit = lifecycle.gitCommit(root, `cp: ${verb} plan ${planId}`, {
|
|
43
|
+
paths: lifecycle.pathsFromActions(result.actions),
|
|
44
|
+
});
|
|
45
|
+
if (commit) console.log(`committed ${commit}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { name: 'tick', run };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { repoRoot } = require('../../lib/paths');
|
|
6
|
+
const provider = require('../../lib/provider');
|
|
7
|
+
const lifecycle = require('../../lib/lifecycle');
|
|
8
|
+
const worktree = require('../../lib/worktree');
|
|
9
|
+
|
|
10
|
+
// Best-effort canonical path: collapses Windows short names (FOO~1) and
|
|
11
|
+
// resolves symlinks. Falls back to path.resolve() if the path doesn't
|
|
12
|
+
// exist (which can happen for registry entries whose worktree was
|
|
13
|
+
// removed externally).
|
|
14
|
+
//
|
|
15
|
+
// On Windows, the JS realpathSync historically preserves 8.3 short
|
|
16
|
+
// names while realpathSync.native (GetFinalPathNameByHandle) expands
|
|
17
|
+
// them. Try .native first.
|
|
18
|
+
function canonical(p) {
|
|
19
|
+
const realNative = fs.realpathSync.native;
|
|
20
|
+
if (realNative) {
|
|
21
|
+
try { return realNative(p); } catch { /* fall through */ }
|
|
22
|
+
}
|
|
23
|
+
try { return fs.realpathSync(p); }
|
|
24
|
+
catch { return path.resolve(p); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function samePath(a, b) {
|
|
28
|
+
const ca = canonical(a);
|
|
29
|
+
const cb = canonical(b);
|
|
30
|
+
if (process.platform === 'win32') {
|
|
31
|
+
return ca.toLowerCase() === cb.toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
return ca === cb;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function runCreate(args, root) {
|
|
37
|
+
let name = null;
|
|
38
|
+
let branch = null;
|
|
39
|
+
let from = null;
|
|
40
|
+
let wpath = null;
|
|
41
|
+
let phase = null;
|
|
42
|
+
let noCreate = false;
|
|
43
|
+
let noCommit = false;
|
|
44
|
+
let dryRun = false;
|
|
45
|
+
let useProvider = false;
|
|
46
|
+
for (let i = 0; i < args.length; i++) {
|
|
47
|
+
const a = args[i];
|
|
48
|
+
if (a === '--branch') branch = args[++i];
|
|
49
|
+
else if (a === '--from') from = args[++i];
|
|
50
|
+
else if (a === '--path') wpath = args[++i];
|
|
51
|
+
else if (a === '--phase') phase = args[++i];
|
|
52
|
+
else if (a === '--no-create') noCreate = true;
|
|
53
|
+
else if (a === '--no-commit') noCommit = true;
|
|
54
|
+
else if (a === '--dry-run') dryRun = true;
|
|
55
|
+
else if (a === '--use-provider') useProvider = true;
|
|
56
|
+
else if (a.startsWith('-')) { console.error(`unknown option: ${a}`); process.exit(2); }
|
|
57
|
+
else if (!name) name = a;
|
|
58
|
+
else { console.error(`unexpected arg: ${a}`); process.exit(2); }
|
|
59
|
+
}
|
|
60
|
+
if (!name) { console.error('Usage: cp worktree create <name> [--branch <b>] [--from <base>] [--path <dir>] [--phase <N>]'); process.exit(2); }
|
|
61
|
+
|
|
62
|
+
const slug = worktree.slugify(name);
|
|
63
|
+
const finalPath = wpath ? path.resolve(wpath) : worktree.defaultWorktreePath(root, slug);
|
|
64
|
+
const finalBranch = branch || worktree.defaultBranchName(slug);
|
|
65
|
+
|
|
66
|
+
// Provider delegation: when --use-provider is set, resolve the worktree
|
|
67
|
+
// role and emit a hand-off line. The harness is responsible for
|
|
68
|
+
// invoking the named skill; cp still records the registry entry so
|
|
69
|
+
// future `cp worktree list` shows it.
|
|
70
|
+
if (useProvider) {
|
|
71
|
+
const skill = provider.resolveSkill('worktree', root);
|
|
72
|
+
if (skill.installed && skill.skill) {
|
|
73
|
+
console.log(`Provider hand-off:`);
|
|
74
|
+
console.log(` provider: ${skill.name}${skill.fallback ? ' (fallback)' : ''}`);
|
|
75
|
+
console.log(` skill: ${skill.skill}`);
|
|
76
|
+
console.log(` invoke: load the "${skill.skill}" skill, then run:`);
|
|
77
|
+
console.log(` git worktree add ${finalPath} -b ${finalBranch}${from ? ' ' + from : ''}`);
|
|
78
|
+
console.log(` registry: cp will record .planning/WORKTREES.md when you re-run without --use-provider`);
|
|
79
|
+
console.log(` (cp did NOT create the worktree itself — provider in charge)`);
|
|
80
|
+
process.exitCode = 0;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.error(`--use-provider: no worktree skill available (configured: ${skill.name}, installed: ${skill.installed}).`);
|
|
84
|
+
console.error(`Falling back to cp-native worktree creation. Re-run without --use-provider to silence this message.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Native path: shell out via lib/worktree.runGitWorktreeAdd (v0.4.4).
|
|
88
|
+
const created = worktree.isoDay();
|
|
89
|
+
if (!noCreate && !dryRun) {
|
|
90
|
+
const r = worktree.runGitWorktreeAdd(root, { worktreePath: finalPath, branch: finalBranch, from });
|
|
91
|
+
if (r.status !== 0) {
|
|
92
|
+
const gitArgs = ['worktree', 'add', finalPath, '-b', finalBranch].concat(from ? [from] : []);
|
|
93
|
+
console.error(`git ${gitArgs.join(' ')} failed:`);
|
|
94
|
+
if (r.stdout) process.stderr.write(r.stdout);
|
|
95
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write(r.stdout || '');
|
|
99
|
+
} else if (noCreate) {
|
|
100
|
+
console.log(`(--no-create) skipping git worktree add ${finalPath} -b ${finalBranch}${from ? ' ' + from : ''}`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(`(dry-run) would run: git worktree add ${finalPath} -b ${finalBranch}${from ? ' ' + from : ''}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const entry = {
|
|
106
|
+
slug,
|
|
107
|
+
branch: finalBranch,
|
|
108
|
+
path: finalPath,
|
|
109
|
+
phase: phase || null,
|
|
110
|
+
created,
|
|
111
|
+
notes: '',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const r = worktree.addRegistryEntry(root, entry);
|
|
115
|
+
if (r.alreadyPresent) {
|
|
116
|
+
console.log(`(note: an entry for slug "${slug}" was already in WORKTREES.md — updated path/branch in place)`);
|
|
117
|
+
}
|
|
118
|
+
if (dryRun) {
|
|
119
|
+
console.log(`(dry-run) would update ${path.relative(root, worktree.worktreesPath(root))}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
lifecycle.writeBatch(r.actions);
|
|
123
|
+
console.log(`✓ worktree registered: ${slug} → ${finalPath} (branch ${finalBranch})`);
|
|
124
|
+
|
|
125
|
+
if (!noCommit) {
|
|
126
|
+
const commit = lifecycle.gitCommit(root, `cp: register worktree ${slug}`, {
|
|
127
|
+
paths: lifecycle.pathsFromActions(r.actions),
|
|
128
|
+
});
|
|
129
|
+
if (commit) console.log(`committed ${commit}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function runList(args, root) {
|
|
134
|
+
const json = args.includes('--json');
|
|
135
|
+
const entries = worktree.listRegistry(root);
|
|
136
|
+
|
|
137
|
+
// Cross-reference with git's view via lib/worktree.listGitWorktrees (v0.4.4).
|
|
138
|
+
const gitTrees = worktree.listGitWorktrees(root);
|
|
139
|
+
|
|
140
|
+
if (json) {
|
|
141
|
+
console.log(JSON.stringify({ registered: entries, git: gitTrees }, null, 2));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (entries.length === 0) {
|
|
146
|
+
console.log(`No cp-tracked worktrees yet.`);
|
|
147
|
+
console.log(`Create one with: cp worktree create <name>`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`Registered worktrees (.planning/WORKTREES.md):`);
|
|
152
|
+
for (const e of entries) {
|
|
153
|
+
const onDisk = gitTrees.find((g) => samePath(g.path, e.path));
|
|
154
|
+
const status = onDisk ? '✓ on disk' : '✗ missing';
|
|
155
|
+
console.log(` ${e.slug.padEnd(20)} ${e.branch.padEnd(28)} ${status}`);
|
|
156
|
+
console.log(` ${e.path}${e.phase ? ' [phase ' + e.phase + ']' : ''}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function runRemove(args, root) {
|
|
161
|
+
let slug = null;
|
|
162
|
+
let force = false;
|
|
163
|
+
let noCommit = false;
|
|
164
|
+
for (let i = 0; i < args.length; i++) {
|
|
165
|
+
const a = args[i];
|
|
166
|
+
if (a === '--force') force = true;
|
|
167
|
+
else if (a === '--no-commit') noCommit = true;
|
|
168
|
+
else if (a.startsWith('-')) { console.error(`unknown option: ${a}`); process.exit(2); }
|
|
169
|
+
else if (!slug) slug = a;
|
|
170
|
+
else { console.error(`unexpected arg: ${a}`); process.exit(2); }
|
|
171
|
+
}
|
|
172
|
+
if (!slug) { console.error('Usage: cp worktree remove <slug> [--force]'); process.exit(2); }
|
|
173
|
+
|
|
174
|
+
const r = worktree.removeRegistryEntry(root, slug);
|
|
175
|
+
if (!r.removed) {
|
|
176
|
+
console.error(`No worktree registered under slug "${slug}". Run \`cp worktree list\` to see slugs.`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Shell out via lib/worktree.runGitWorktreeRemove (v0.4.4).
|
|
181
|
+
const gr = worktree.runGitWorktreeRemove(root, { worktreePath: r.removed.path, force });
|
|
182
|
+
if (gr.status !== 0) {
|
|
183
|
+
const gitArgs = ['worktree', 'remove'].concat(force ? ['--force'] : []).concat([r.removed.path]);
|
|
184
|
+
console.error(`git ${gitArgs.join(' ')} failed:`);
|
|
185
|
+
if (gr.stdout) process.stderr.write(gr.stdout);
|
|
186
|
+
if (gr.stderr) process.stderr.write(gr.stderr);
|
|
187
|
+
console.error(`\n(Registry entry NOT removed. Pass --force to remove anyway, or clean up the worktree first.)`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
if (gr.stdout) process.stdout.write(gr.stdout);
|
|
191
|
+
|
|
192
|
+
lifecycle.writeBatch(r.actions);
|
|
193
|
+
console.log(`✓ worktree removed: ${slug} (${r.removed.path})`);
|
|
194
|
+
|
|
195
|
+
if (!noCommit) {
|
|
196
|
+
const commit = lifecycle.gitCommit(root, `cp: remove worktree ${slug}`, {
|
|
197
|
+
paths: lifecycle.pathsFromActions(r.actions),
|
|
198
|
+
});
|
|
199
|
+
if (commit) console.log(`committed ${commit}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function run(args = []) {
|
|
204
|
+
const sub = args.shift();
|
|
205
|
+
if (!sub) {
|
|
206
|
+
console.error('Usage: cp worktree <create|list|remove> [...]');
|
|
207
|
+
process.exit(2);
|
|
208
|
+
}
|
|
209
|
+
const root = repoRoot();
|
|
210
|
+
|
|
211
|
+
if (sub === 'create') return runCreate(args, root);
|
|
212
|
+
if (sub === 'list') return runList(args, root);
|
|
213
|
+
if (sub === 'remove' || sub === 'rm') return runRemove(args, root);
|
|
214
|
+
console.error(`Unknown worktree subcommand: ${sub}`);
|
|
215
|
+
process.exit(2);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { name: 'worktree', run };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { repoRoot } = require('../../lib/paths');
|
|
6
|
+
const milestone = require('../../lib/milestone');
|
|
7
|
+
|
|
8
|
+
function run(args = []) {
|
|
9
|
+
const root = repoRoot();
|
|
10
|
+
let planId = null;
|
|
11
|
+
let fromPath = null;
|
|
12
|
+
let bodyPath = null;
|
|
13
|
+
let overwrite = false;
|
|
14
|
+
let dryRun = false;
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
const a = args[i];
|
|
17
|
+
if (a === '--from') fromPath = args[++i];
|
|
18
|
+
else if (a === '--body') bodyPath = args[++i];
|
|
19
|
+
else if (a === '--overwrite') overwrite = true;
|
|
20
|
+
else if (a === '--dry-run') dryRun = true;
|
|
21
|
+
else if (a.startsWith('-')) { console.error(`unknown option: ${a}`); process.exit(2); }
|
|
22
|
+
else if (!planId) planId = a;
|
|
23
|
+
else { console.error(`unexpected arg: ${a}`); process.exit(2); }
|
|
24
|
+
}
|
|
25
|
+
if (!planId || !fromPath) {
|
|
26
|
+
console.error('Usage: cp write-summary <plan-id> --from <json> [--body <md>] [--overwrite] [--dry-run]');
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
let data;
|
|
30
|
+
try {
|
|
31
|
+
data = JSON.parse(fs.readFileSync(fromPath, 'utf8'));
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error(`failed to read JSON from ${fromPath}: ${e.message}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const body = bodyPath ? fs.readFileSync(bodyPath, 'utf8') : undefined;
|
|
37
|
+
let r;
|
|
38
|
+
try {
|
|
39
|
+
r = milestone.writeSummary(root, planId, data, { dryRun, body, overwrite });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err && (err.name === 'ValidationError' || err.code === 'EVALIDATION')) {
|
|
42
|
+
process.stderr.write(err.message + '\n');
|
|
43
|
+
process.exit(2);
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
console.log(`${dryRun ? '·' : '✓'} ${path.relative(root, r.path)}`);
|
|
48
|
+
if (dryRun) {
|
|
49
|
+
console.log('--- normalised frontmatter ---');
|
|
50
|
+
console.log(JSON.stringify(r.fm, null, 2));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { name: 'write-summary', run };
|
package/bin/cp.cmd
ADDED