plankit-cli 1.0.0 → 1.0.1
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/README.md +80 -50
- package/package.json +10 -1
- package/src/cli.js +483 -185
- package/src/config.js +208 -0
- package/src/templates.js +25 -149
- package/templates/AGENTS.md +12 -0
- package/templates/PLANKIT.md +40 -0
- package/templates/clarifications.md +16 -0
- package/templates/cursorrules +10 -0
- package/templates/feature-readme.md +22 -0
- package/templates/gemini-instructions.md +28 -0
- package/templates/opencode/plankit-clarify.md +10 -0
- package/templates/opencode/plankit-implement.md +14 -0
- package/templates/opencode/plankit-plan.md +13 -0
- package/templates/opencode/plankit-review.md +11 -0
- package/templates/opencode/plankit.md +12 -0
- package/templates/output-report.md +17 -0
- package/templates/phase-spec.md +19 -0
package/src/cli.js
CHANGED
|
@@ -1,235 +1,533 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from './
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
switch (command) {
|
|
21
|
-
case 'init':
|
|
22
|
-
initProject();
|
|
23
|
-
break;
|
|
24
|
-
case 'plan':
|
|
25
|
-
if (!target) {
|
|
26
|
-
console.error('Error: Please specify a feature name. Example: npx plankit plan my-feature');
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
planFeature(target);
|
|
30
|
-
break;
|
|
31
|
-
case 'status':
|
|
32
|
-
showStatus();
|
|
33
|
-
break;
|
|
34
|
-
case 'archive':
|
|
35
|
-
if (!target) {
|
|
36
|
-
console.error('Error: Please specify a feature name. Example: npx plankit archive my-feature');
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
archiveFeature(target);
|
|
40
|
-
break;
|
|
41
|
-
case 'help':
|
|
42
|
-
case '--help':
|
|
43
|
-
case '-h':
|
|
44
|
-
default:
|
|
45
|
-
showHelp();
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function ensureDir(dirPath) {
|
|
51
|
-
if (!fs.existsSync(dirPath)) {
|
|
52
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function writeFile(filePath, content) {
|
|
57
|
-
ensureDir(path.dirname(filePath));
|
|
58
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function initProject() {
|
|
62
|
-
console.log('🚀 Initializing PlanKit Command Suite in project...');
|
|
63
|
-
|
|
64
|
-
const cwd = process.cwd();
|
|
65
|
-
|
|
66
|
-
// 1. Artifacts directories & spec files
|
|
67
|
-
ensureDir(path.join(cwd, 'artifacts', 'current'));
|
|
68
|
-
ensureDir(path.join(cwd, 'artifacts', 'archived'));
|
|
69
|
-
writeFile(path.join(cwd, 'artifacts', 'PLANKIT.md'), PLANKIT_SPEC_TEMPLATE);
|
|
70
|
-
writeFile(path.join(cwd, 'artifacts', 'current', '.gitkeep'), '');
|
|
71
|
-
writeFile(path.join(cwd, 'artifacts', 'archived', '.gitkeep'), '');
|
|
72
|
-
|
|
73
|
-
// 2. Gemini / Antigravity instructions
|
|
74
|
-
writeFile(path.join(cwd, '.gemini', 'instructions.md'), GEMINI_INSTRUCTIONS_TEMPLATE);
|
|
75
|
-
|
|
76
|
-
// 3. Cursor rules
|
|
77
|
-
writeFile(path.join(cwd, '.cursorrules'), CURSORRULES_TEMPLATE);
|
|
78
|
-
|
|
79
|
-
// 4. AGENTS.md
|
|
80
|
-
writeFile(path.join(cwd, 'AGENTS.md'), AGENTS_MD_TEMPLATE);
|
|
81
|
-
|
|
82
|
-
// 5. OpenCode command markdown files
|
|
83
|
-
const opencodeDir = path.join(cwd, '.opencode', 'commands');
|
|
84
|
-
writeFile(path.join(opencodeDir, 'plankit-plan.md'), OPENCODE_COMMAND_PLAN);
|
|
85
|
-
writeFile(path.join(opencodeDir, 'plankit-clarify.md'), OPENCODE_COMMAND_CLARIFY);
|
|
86
|
-
writeFile(path.join(opencodeDir, 'plankit-implement.md'), OPENCODE_COMMAND_IMPLEMENT);
|
|
87
|
-
writeFile(path.join(opencodeDir, 'plankit-review.md'), OPENCODE_COMMAND_REVIEW);
|
|
88
|
-
writeFile(path.join(opencodeDir, 'plankit.md'), OPENCODE_COMMAND_MASTER);
|
|
89
|
-
|
|
90
|
-
// 6. Global Gemini Skills setup
|
|
6
|
+
assertSafeName,
|
|
7
|
+
createDefaultConfigFile,
|
|
8
|
+
ensureDir,
|
|
9
|
+
listDirectories,
|
|
10
|
+
loadConfig,
|
|
11
|
+
normalizePhases,
|
|
12
|
+
parsePhaseFlag,
|
|
13
|
+
safeJoin,
|
|
14
|
+
writeManagedFile
|
|
15
|
+
} from './config.js';
|
|
16
|
+
import { renderList, renderTemplate } from './templates.js';
|
|
17
|
+
|
|
18
|
+
export function runCli(args, context = {}) {
|
|
19
|
+
const io = createIo(context);
|
|
20
|
+
|
|
91
21
|
try {
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
22
|
+
const parsed = parseArgs(args);
|
|
23
|
+
const command = parsed.command;
|
|
24
|
+
|
|
25
|
+
switch (command) {
|
|
26
|
+
case 'init':
|
|
27
|
+
initProject(parsed, io);
|
|
28
|
+
break;
|
|
29
|
+
case 'plan':
|
|
30
|
+
planFeature(parsed, io);
|
|
31
|
+
break;
|
|
32
|
+
case 'clarify':
|
|
33
|
+
clarifyFeature(parsed, io);
|
|
34
|
+
break;
|
|
35
|
+
case 'implement':
|
|
36
|
+
prepareImplementation(parsed, io);
|
|
37
|
+
break;
|
|
38
|
+
case 'review':
|
|
39
|
+
reviewFeature(parsed, io);
|
|
40
|
+
break;
|
|
41
|
+
case 'status':
|
|
42
|
+
showStatus(parsed, io);
|
|
43
|
+
break;
|
|
44
|
+
case 'archive':
|
|
45
|
+
archiveFeature(parsed, io);
|
|
46
|
+
break;
|
|
47
|
+
case 'help':
|
|
48
|
+
case '--help':
|
|
49
|
+
case '-h':
|
|
50
|
+
case undefined:
|
|
51
|
+
showHelp(io);
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unknown command "${command}". Run "plankit help" for usage.`);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
io.stderr(`Error: ${error.message}`);
|
|
58
|
+
io.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function initProject(parsed, io) {
|
|
63
|
+
const { config } = loadConfig(io.cwd);
|
|
64
|
+
const options = writeOptions(parsed, config);
|
|
65
|
+
const agents = selectedAgents(parsed, config);
|
|
66
|
+
const results = [];
|
|
67
|
+
const artifactsDir = safeJoin(io.cwd, config.artifactsDir);
|
|
68
|
+
|
|
69
|
+
ensureDir(path.join(artifactsDir, 'current'), options);
|
|
70
|
+
ensureDir(path.join(artifactsDir, 'archived'), options);
|
|
71
|
+
|
|
72
|
+
results.push(writeManagedFile(
|
|
73
|
+
path.join(artifactsDir, 'PLANKIT.md'),
|
|
74
|
+
renderTemplate(config, 'PLANKIT.md', { artifactsDir: config.artifactsDir }, io.cwd),
|
|
75
|
+
options
|
|
76
|
+
));
|
|
77
|
+
results.push(writeManagedFile(path.join(artifactsDir, 'current', '.gitkeep'), '', options));
|
|
78
|
+
results.push(writeManagedFile(path.join(artifactsDir, 'archived', '.gitkeep'), '', options));
|
|
79
|
+
results.push(createDefaultConfigFile(io.cwd, options));
|
|
80
|
+
|
|
81
|
+
if (agents.gemini) {
|
|
82
|
+
results.push(writeManagedFile(
|
|
83
|
+
safeJoin(io.cwd, '.gemini', 'instructions.md'),
|
|
84
|
+
renderTemplate(config, 'gemini-instructions.md', { artifactsDir: config.artifactsDir }, io.cwd),
|
|
85
|
+
options
|
|
86
|
+
));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (agents.cursor) {
|
|
90
|
+
results.push(writeManagedFile(
|
|
91
|
+
safeJoin(io.cwd, '.cursorrules'),
|
|
92
|
+
renderTemplate(config, 'cursorrules', { artifactsDir: config.artifactsDir }, io.cwd),
|
|
93
|
+
options
|
|
94
|
+
));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (agents.codex) {
|
|
98
|
+
results.push(writeManagedFile(
|
|
99
|
+
safeJoin(io.cwd, 'AGENTS.md'),
|
|
100
|
+
renderTemplate(config, 'AGENTS.md', { artifactsDir: config.artifactsDir }, io.cwd),
|
|
101
|
+
options
|
|
102
|
+
));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (agents.opencode) {
|
|
106
|
+
const commandsDir = safeJoin(io.cwd, '.opencode', 'commands');
|
|
107
|
+
for (const fileName of [
|
|
108
|
+
'plankit-plan.md',
|
|
109
|
+
'plankit-clarify.md',
|
|
110
|
+
'plankit-implement.md',
|
|
111
|
+
'plankit-review.md',
|
|
112
|
+
'plankit.md'
|
|
113
|
+
]) {
|
|
114
|
+
results.push(writeManagedFile(
|
|
115
|
+
path.join(commandsDir, fileName),
|
|
116
|
+
renderTemplate(config, path.join('opencode', fileName), { artifactsDir: config.artifactsDir }, io.cwd),
|
|
117
|
+
options
|
|
118
|
+
));
|
|
107
119
|
}
|
|
108
|
-
console.log(' ✓ Installed global Gemini skills in ~/.gemini/skills/');
|
|
109
|
-
} catch (e) {
|
|
110
|
-
// Ignore global home dir write errors if restricted
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.log(' - .opencode/commands/*.md');
|
|
120
|
-
console.log('\nNext steps:');
|
|
121
|
-
console.log(' Run "npx plankit plan <feature-name>" to start a new feature.');
|
|
122
|
+
if (parsed.flags['global-gemini-skills']) {
|
|
123
|
+
results.push(...installGlobalGeminiSkills(config, options));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
io.stdout('PlanKit initialized.');
|
|
127
|
+
printResults(results, io);
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
function planFeature(
|
|
125
|
-
const
|
|
126
|
-
|
|
130
|
+
function planFeature(parsed, io) {
|
|
131
|
+
const featureName = parsed.positionals[0];
|
|
132
|
+
if (!featureName) {
|
|
133
|
+
throw new Error('Please specify a feature name. Example: plankit plan my-feature');
|
|
134
|
+
}
|
|
135
|
+
assertSafeName(featureName, 'feature name');
|
|
136
|
+
|
|
137
|
+
const { config } = loadConfig(io.cwd);
|
|
138
|
+
const phases = parsePhaseFlag(parsed.flags.phases) || normalizePhases(config.defaultPhases);
|
|
139
|
+
const requirements = parsed.flags.requirements || parsed.positionals.slice(1).join(' ') || 'Detailed description of the feature goal and scope.';
|
|
140
|
+
const options = writeOptions(parsed, config);
|
|
141
|
+
const featureDir = safeJoin(io.cwd, config.artifactsDir, 'current', featureName);
|
|
127
142
|
|
|
128
143
|
if (fs.existsSync(featureDir)) {
|
|
129
|
-
|
|
144
|
+
throw new Error(`Feature "${featureName}" already exists at ${path.join(config.artifactsDir, 'current', featureName)}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
ensureDir(path.join(featureDir, 'phases'), options);
|
|
148
|
+
ensureDir(path.join(featureDir, 'outputs'), options);
|
|
149
|
+
|
|
150
|
+
const phaseChecklist = renderList(phases, (phase, index) => (
|
|
151
|
+
`- [ ] Phase ${index + 1}: ${phase.title} — ([Spec](phases/${phaseFileName(index + 1, phase)}))`
|
|
152
|
+
));
|
|
153
|
+
const phaseMatrix = renderList(phases, (phase, index) => {
|
|
154
|
+
const phaseNumber = index + 1;
|
|
155
|
+
const specPath = `phases/${phaseFileName(phaseNumber, phase)}`;
|
|
156
|
+
const outputPath = `outputs/phase-${phaseNumber}-output.md`;
|
|
157
|
+
return `| Phase ${phaseNumber} | ${phase.title} | \`${specPath}\` | \`${outputPath}\` | **PENDING** |`;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const results = [];
|
|
161
|
+
results.push(writeManagedFile(
|
|
162
|
+
path.join(featureDir, 'README.md'),
|
|
163
|
+
renderTemplate(config, 'feature-readme.md', {
|
|
164
|
+
featureName,
|
|
165
|
+
requirements,
|
|
166
|
+
phaseChecklist,
|
|
167
|
+
phaseMatrix
|
|
168
|
+
}, io.cwd),
|
|
169
|
+
options
|
|
170
|
+
));
|
|
171
|
+
|
|
172
|
+
results.push(writeManagedFile(
|
|
173
|
+
path.join(featureDir, 'plankit.json'),
|
|
174
|
+
`${JSON.stringify(createFeatureState(featureName, phases), null, 2)}\n`,
|
|
175
|
+
options
|
|
176
|
+
));
|
|
177
|
+
|
|
178
|
+
phases.forEach((phase, index) => {
|
|
179
|
+
const phaseNumber = index + 1;
|
|
180
|
+
const tasks = phase.tasks.map((task) => `- [ ] ${task}`).join('\n');
|
|
181
|
+
results.push(writeManagedFile(
|
|
182
|
+
path.join(featureDir, 'phases', phaseFileName(phaseNumber, phase)),
|
|
183
|
+
renderTemplate(config, 'phase-spec.md', {
|
|
184
|
+
phaseNumber,
|
|
185
|
+
previousPhaseNumber: Math.max(phaseNumber - 1, 0),
|
|
186
|
+
phaseTitle: phase.title,
|
|
187
|
+
objective: phase.objective,
|
|
188
|
+
tasks
|
|
189
|
+
}, io.cwd),
|
|
190
|
+
options
|
|
191
|
+
));
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
results.push(writeManagedFile(path.join(featureDir, 'outputs', '.gitkeep'), '', options));
|
|
195
|
+
|
|
196
|
+
io.stdout(`Feature "${featureName}" planned with ${phases.length} phase(s).`);
|
|
197
|
+
printResults(results, io);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function clarifyFeature(parsed, io) {
|
|
201
|
+
const { featureName, phaseNumber } = readFeatureAndPhaseArgs(parsed, { phaseOptional: true });
|
|
202
|
+
const { config } = loadConfig(io.cwd);
|
|
203
|
+
const options = writeOptions(parsed, config);
|
|
204
|
+
const featureDir = requireFeatureDir(io.cwd, config, featureName);
|
|
205
|
+
const phaseLabel = phaseNumber ? `Phase ${phaseNumber}` : 'Unspecified phase';
|
|
206
|
+
|
|
207
|
+
const result = writeManagedFile(
|
|
208
|
+
path.join(featureDir, 'clarifications.md'),
|
|
209
|
+
renderTemplate(config, 'clarifications.md', { featureName, phaseLabel }, io.cwd),
|
|
210
|
+
options
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
io.stdout(`Clarification artifact prepared for "${featureName}".`);
|
|
214
|
+
printResults([result], io);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function prepareImplementation(parsed, io) {
|
|
218
|
+
const { featureName, phaseNumber } = readFeatureAndPhaseArgs(parsed);
|
|
219
|
+
const { config } = loadConfig(io.cwd);
|
|
220
|
+
const options = writeOptions(parsed, config);
|
|
221
|
+
const featureDir = requireFeatureDir(io.cwd, config, featureName);
|
|
222
|
+
const phaseSpec = findPhaseSpec(featureDir, phaseNumber);
|
|
223
|
+
const previousMissing = [];
|
|
224
|
+
|
|
225
|
+
for (let index = 1; index < phaseNumber; index += 1) {
|
|
226
|
+
const outputPath = path.join(featureDir, 'outputs', `phase-${index}-output.md`);
|
|
227
|
+
if (!fs.existsSync(outputPath)) {
|
|
228
|
+
previousMissing.push(path.relative(featureDir, outputPath));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (previousMissing.length > 0) {
|
|
233
|
+
throw new Error(`Cannot prepare Phase ${phaseNumber}; missing previous output(s): ${previousMissing.join(', ')}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const phaseTitle = readPhaseTitle(phaseSpec, phaseNumber);
|
|
237
|
+
const outputPath = path.join(featureDir, 'outputs', `phase-${phaseNumber}-output.md`);
|
|
238
|
+
const result = writeManagedFile(
|
|
239
|
+
outputPath,
|
|
240
|
+
renderTemplate(config, 'output-report.md', { phaseNumber, phaseTitle }, io.cwd),
|
|
241
|
+
options
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
io.stdout(`Implementation context validated for "${featureName}" Phase ${phaseNumber}.`);
|
|
245
|
+
io.stdout(`Phase spec: ${path.relative(io.cwd, phaseSpec)}`);
|
|
246
|
+
printResults([result], io);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function reviewFeature(parsed, io) {
|
|
250
|
+
const featureName = parsed.positionals[0];
|
|
251
|
+
if (!featureName) {
|
|
252
|
+
throw new Error('Please specify a feature name. Example: plankit review my-feature');
|
|
253
|
+
}
|
|
254
|
+
assertSafeName(featureName, 'feature name');
|
|
255
|
+
|
|
256
|
+
const { config } = loadConfig(io.cwd);
|
|
257
|
+
const featureDir = requireFeatureDir(io.cwd, config, featureName);
|
|
258
|
+
const testCommand = parsed.flags['test-command'] ?? config.verification.testCommand;
|
|
259
|
+
const buildCommand = parsed.flags['build-command'] ?? config.verification.buildCommand;
|
|
260
|
+
|
|
261
|
+
if (!parsed.flags['skip-test'] && testCommand) {
|
|
262
|
+
runVerificationCommand(testCommand, io);
|
|
263
|
+
}
|
|
264
|
+
if (!parsed.flags['skip-build'] && buildCommand) {
|
|
265
|
+
runVerificationCommand(buildCommand, io);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
markReadmeArchived(path.join(featureDir, 'README.md'), parsed.dryRun);
|
|
269
|
+
moveFeatureToArchive(io.cwd, config, featureName, parsed.dryRun);
|
|
270
|
+
io.stdout(`Feature "${featureName}" reviewed and archived.`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function showStatus(parsed, io) {
|
|
274
|
+
const { config } = loadConfig(io.cwd);
|
|
275
|
+
const currentDir = safeJoin(io.cwd, config.artifactsDir, 'current');
|
|
276
|
+
const archivedDir = safeJoin(io.cwd, config.artifactsDir, 'archived');
|
|
277
|
+
const status = {
|
|
278
|
+
active: listDirectories(currentDir),
|
|
279
|
+
archived: listDirectories(archivedDir)
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
if (parsed.flags.json) {
|
|
283
|
+
io.stdout(JSON.stringify(status, null, 2));
|
|
130
284
|
return;
|
|
131
285
|
}
|
|
132
286
|
|
|
133
|
-
|
|
287
|
+
io.stdout('PlanKit Feature Status Summary:');
|
|
288
|
+
io.stdout('');
|
|
289
|
+
io.stdout(`ACTIVE FEATURES (${path.join(config.artifactsDir, 'current')}/):`);
|
|
290
|
+
io.stdout(status.active.length ? status.active.map((feature) => ` - ${feature}`).join('\n') : ' (none)');
|
|
291
|
+
io.stdout('');
|
|
292
|
+
io.stdout(`ARCHIVED FEATURES (${path.join(config.artifactsDir, 'archived')}/):`);
|
|
293
|
+
io.stdout(status.archived.length ? status.archived.map((feature) => ` - ${feature}`).join('\n') : ' (none)');
|
|
294
|
+
}
|
|
134
295
|
|
|
135
|
-
|
|
136
|
-
|
|
296
|
+
function archiveFeature(parsed, io) {
|
|
297
|
+
const featureName = parsed.positionals[0];
|
|
298
|
+
if (!featureName) {
|
|
299
|
+
throw new Error('Please specify a feature name. Example: plankit archive my-feature');
|
|
300
|
+
}
|
|
301
|
+
assertSafeName(featureName, 'feature name');
|
|
137
302
|
|
|
138
|
-
const
|
|
303
|
+
const { config } = loadConfig(io.cwd);
|
|
304
|
+
moveFeatureToArchive(io.cwd, config, featureName, parsed.dryRun);
|
|
305
|
+
io.stdout(`Feature "${featureName}" archived.`);
|
|
306
|
+
}
|
|
139
307
|
|
|
140
|
-
|
|
141
|
-
|
|
308
|
+
function moveFeatureToArchive(cwd, config, featureName, dryRun = false) {
|
|
309
|
+
const sourceDir = requireFeatureDir(cwd, config, featureName);
|
|
310
|
+
const targetDir = safeJoin(cwd, config.artifactsDir, 'archived', featureName);
|
|
142
311
|
|
|
143
|
-
|
|
312
|
+
if (fs.existsSync(targetDir)) {
|
|
313
|
+
throw new Error(`Archived feature "${featureName}" already exists.`);
|
|
314
|
+
}
|
|
144
315
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
316
|
+
if (!dryRun) {
|
|
317
|
+
ensureDir(path.dirname(targetDir));
|
|
318
|
+
fs.renameSync(sourceDir, targetDir);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
148
321
|
|
|
149
|
-
|
|
322
|
+
function requireFeatureDir(cwd, config, featureName) {
|
|
323
|
+
assertSafeName(featureName, 'feature name');
|
|
324
|
+
const featureDir = safeJoin(cwd, config.artifactsDir, 'current', featureName);
|
|
150
325
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
`;
|
|
326
|
+
if (!fs.existsSync(featureDir)) {
|
|
327
|
+
throw new Error(`Active feature "${featureName}" not found in ${path.join(config.artifactsDir, 'current')}/`);
|
|
328
|
+
}
|
|
155
329
|
|
|
156
|
-
|
|
330
|
+
return featureDir;
|
|
331
|
+
}
|
|
157
332
|
|
|
158
|
-
|
|
159
|
-
|
|
333
|
+
function findPhaseSpec(featureDir, phaseNumber) {
|
|
334
|
+
const phasesDir = path.join(featureDir, 'phases');
|
|
335
|
+
const prefix = `phase-${phaseNumber}-`;
|
|
336
|
+
const file = fs.readdirSync(phasesDir).find((name) => name.startsWith(prefix) && name.endsWith('.md'));
|
|
160
337
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
`;
|
|
338
|
+
if (!file) {
|
|
339
|
+
throw new Error(`Phase ${phaseNumber} spec not found under ${path.relative(process.cwd(), phasesDir)}`);
|
|
340
|
+
}
|
|
165
341
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
writeFile(path.join(featureDir, 'outputs', '.gitkeep'), '');
|
|
342
|
+
return path.join(phasesDir, file);
|
|
343
|
+
}
|
|
169
344
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
345
|
+
function readPhaseTitle(phaseSpec, phaseNumber) {
|
|
346
|
+
const firstLine = fs.readFileSync(phaseSpec, 'utf8').split(/\r?\n/, 1)[0];
|
|
347
|
+
return firstLine.replace(/^#\s*Phase\s+\d+:\s*/i, '') || `Phase ${phaseNumber}`;
|
|
173
348
|
}
|
|
174
349
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
350
|
+
function markReadmeArchived(readmePath, dryRun = false) {
|
|
351
|
+
if (!fs.existsSync(readmePath) || dryRun) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
179
354
|
|
|
180
|
-
|
|
355
|
+
const content = fs.readFileSync(readmePath, 'utf8');
|
|
356
|
+
const updated = content.replace(/^# Feature: (.+?)(?: \[ARCHIVED\])?$/m, '# Feature: $1 [ARCHIVED]');
|
|
357
|
+
fs.writeFileSync(readmePath, updated, 'utf8');
|
|
358
|
+
}
|
|
181
359
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
360
|
+
function runVerificationCommand(command, io) {
|
|
361
|
+
io.stdout(`Running: ${command}`);
|
|
362
|
+
const result = spawnSync(command, {
|
|
363
|
+
cwd: io.cwd,
|
|
364
|
+
shell: true,
|
|
365
|
+
stdio: 'inherit'
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (result.status !== 0) {
|
|
369
|
+
throw new Error(`Verification command failed: ${command}`);
|
|
190
370
|
}
|
|
371
|
+
}
|
|
191
372
|
|
|
192
|
-
|
|
373
|
+
function installGlobalGeminiSkills(config, options) {
|
|
374
|
+
const globalSkillsDir = path.join(os.homedir(), '.gemini', 'skills');
|
|
375
|
+
const skills = [
|
|
376
|
+
['plankit', 'PlanKit Command Suite master skill.'],
|
|
377
|
+
['plankit-plan', 'Initialize a feature workspace.'],
|
|
378
|
+
['plankit-clarify', 'Analyze requirements and ask targeted clarifying questions.'],
|
|
379
|
+
['plankit-implement', 'Prepare phase implementation context and output artifacts.'],
|
|
380
|
+
['plankit-review', 'Run verification and archive completed features.']
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
return skills.map(([name, description]) => writeManagedFile(
|
|
384
|
+
path.join(globalSkillsDir, name, 'SKILL.md'),
|
|
385
|
+
`---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n\n${description}\n\nSpecification: ${config.artifactsDir}/PLANKIT.md\n`,
|
|
386
|
+
options
|
|
387
|
+
));
|
|
388
|
+
}
|
|
193
389
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
390
|
+
function createFeatureState(featureName, phases) {
|
|
391
|
+
return {
|
|
392
|
+
feature: featureName,
|
|
393
|
+
status: 'active',
|
|
394
|
+
createdAt: new Date().toISOString(),
|
|
395
|
+
phases: phases.map((phase, index) => ({
|
|
396
|
+
number: index + 1,
|
|
397
|
+
title: phase.title,
|
|
398
|
+
specPath: `phases/${phaseFileName(index + 1, phase)}`,
|
|
399
|
+
outputPath: `outputs/phase-${index + 1}-output.md`,
|
|
400
|
+
status: 'pending'
|
|
401
|
+
}))
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function phaseFileName(phaseNumber, phase) {
|
|
406
|
+
return `phase-${phaseNumber}-${phase.slug}.md`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function readFeatureAndPhaseArgs(parsed, options = {}) {
|
|
410
|
+
const featureName = parsed.positionals[0];
|
|
411
|
+
const rawPhase = parsed.positionals[1] || parsed.flags.phase;
|
|
412
|
+
|
|
413
|
+
if (!featureName) {
|
|
414
|
+
throw new Error('Please specify a feature name.');
|
|
415
|
+
}
|
|
416
|
+
assertSafeName(featureName, 'feature name');
|
|
417
|
+
|
|
418
|
+
if (!rawPhase && options.phaseOptional) {
|
|
419
|
+
return { featureName, phaseNumber: null };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const phaseNumber = Number.parseInt(rawPhase, 10);
|
|
423
|
+
if (!Number.isInteger(phaseNumber) || phaseNumber <= 0) {
|
|
424
|
+
throw new Error('Please specify a positive phase number.');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { featureName, phaseNumber };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function selectedAgents(parsed, config) {
|
|
431
|
+
if (!parsed.flags.agents) {
|
|
432
|
+
return config.agents;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const requested = new Set(String(parsed.flags.agents).split(',').map((item) => item.trim()).filter(Boolean));
|
|
436
|
+
return {
|
|
437
|
+
codex: requested.has('codex'),
|
|
438
|
+
cursor: requested.has('cursor'),
|
|
439
|
+
opencode: requested.has('opencode'),
|
|
440
|
+
gemini: requested.has('gemini') || requested.has('antigravity')
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function writeOptions(parsed, config) {
|
|
445
|
+
return {
|
|
446
|
+
force: Boolean(parsed.flags.force) || config.overwrite === 'force',
|
|
447
|
+
dryRun: Boolean(parsed.flags['dry-run'])
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function printResults(results, io) {
|
|
452
|
+
for (const result of results.filter(Boolean)) {
|
|
453
|
+
io.stdout(` ${result.action.padEnd(9)} ${path.relative(io.cwd, result.path) || result.path}`);
|
|
202
454
|
}
|
|
203
455
|
}
|
|
204
456
|
|
|
205
|
-
function
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const
|
|
457
|
+
function parseArgs(args) {
|
|
458
|
+
const [command, ...rest] = args;
|
|
459
|
+
const flags = {};
|
|
460
|
+
const positionals = [];
|
|
461
|
+
|
|
462
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
463
|
+
const arg = rest[index];
|
|
464
|
+
|
|
465
|
+
if (!arg.startsWith('--')) {
|
|
466
|
+
positionals.push(arg);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const flag = arg.slice(2);
|
|
471
|
+
const [inlineName, inlineValue] = flag.split('=', 2);
|
|
472
|
+
if (inlineValue !== undefined) {
|
|
473
|
+
flags[inlineName] = inlineValue;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
209
476
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
477
|
+
const next = rest[index + 1];
|
|
478
|
+
if (next && !next.startsWith('--')) {
|
|
479
|
+
flags[inlineName] = next;
|
|
480
|
+
index += 1;
|
|
481
|
+
} else {
|
|
482
|
+
flags[inlineName] = true;
|
|
483
|
+
}
|
|
213
484
|
}
|
|
214
485
|
|
|
215
|
-
|
|
216
|
-
|
|
486
|
+
return {
|
|
487
|
+
command,
|
|
488
|
+
flags,
|
|
489
|
+
positionals,
|
|
490
|
+
dryRun: Boolean(flags['dry-run'])
|
|
491
|
+
};
|
|
492
|
+
}
|
|
217
493
|
|
|
218
|
-
|
|
494
|
+
function createIo(context) {
|
|
495
|
+
return {
|
|
496
|
+
cwd: context.cwd || process.cwd(),
|
|
497
|
+
stdout: context.stdout || ((message) => console.log(message)),
|
|
498
|
+
stderr: context.stderr || ((message) => console.error(message)),
|
|
499
|
+
exit: context.exit || ((code) => process.exit(code))
|
|
500
|
+
};
|
|
219
501
|
}
|
|
220
502
|
|
|
221
|
-
function showHelp() {
|
|
222
|
-
|
|
503
|
+
function showHelp(io) {
|
|
504
|
+
io.stdout(`
|
|
223
505
|
PlanKit CLI — Command Suite for AI Coding Assistants
|
|
224
506
|
|
|
225
507
|
Usage:
|
|
226
|
-
|
|
508
|
+
plankit <command> [options]
|
|
227
509
|
|
|
228
510
|
Commands:
|
|
229
|
-
init
|
|
230
|
-
plan <feature
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
511
|
+
init Initialize PlanKit rules, config, templates, and artifact folders
|
|
512
|
+
plan <feature> [requirements] Create a feature workspace under artifacts/current/<feature>/
|
|
513
|
+
clarify <feature> [phase] Create a clarification artifact for a feature
|
|
514
|
+
implement <feature> <phase> Validate phase context and prepare phase output artifact
|
|
515
|
+
review <feature> Run verification and archive a completed feature
|
|
516
|
+
status [--json] Show active and archived features
|
|
517
|
+
archive <feature> Move a feature to archived without running verification
|
|
518
|
+
help Show this help message
|
|
519
|
+
|
|
520
|
+
Options:
|
|
521
|
+
--force Overwrite generated files where safe
|
|
522
|
+
--dry-run Show intended actions without writing
|
|
523
|
+
--agents codex,cursor,opencode Select generated agent integrations during init
|
|
524
|
+
--global-gemini-skills Also install Gemini skills under the user profile
|
|
525
|
+
--phases 3 Generate N default phases during plan
|
|
526
|
+
--phases "Design,Build,Test" Generate named phases during plan
|
|
527
|
+
--requirements "text" Set feature requirements during plan
|
|
528
|
+
--test-command "command" Override review test command
|
|
529
|
+
--build-command "command" Override review build command
|
|
530
|
+
--skip-test Skip review test command
|
|
531
|
+
--skip-build Skip review build command
|
|
234
532
|
`);
|
|
235
533
|
}
|