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/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
- AGENTS_MD_TEMPLATE,
6
- CURSORRULES_TEMPLATE,
7
- GEMINI_INSTRUCTIONS_TEMPLATE,
8
- OPENCODE_COMMAND_PLAN,
9
- OPENCODE_COMMAND_CLARIFY,
10
- OPENCODE_COMMAND_IMPLEMENT,
11
- OPENCODE_COMMAND_REVIEW,
12
- OPENCODE_COMMAND_MASTER,
13
- PLANKIT_SPEC_TEMPLATE
14
- } from './templates.js';
15
-
16
- export function runCli(args) {
17
- const command = args[0];
18
- const target = args[1];
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 userHome = os.homedir();
93
- const globalSkillsDir = path.join(userHome, '.gemini', 'skills');
94
-
95
- const skillsToCreate = [
96
- { name: 'plankit', desc: 'PlanKit Command Suite master skill.' },
97
- { name: 'plankit-plan', desc: 'Initialize feature under artifacts/current/<feature-name>/.' },
98
- { name: 'plankit-clarify', desc: 'Analyze requirements and ask targeted clarifying questions.' },
99
- { name: 'plankit-implement', desc: 'Execute Phase N reading spec and previous outputs.' },
100
- { name: 'plankit-review', desc: 'Run test & build verification, validate DoD, and archive feature.' }
101
- ];
102
-
103
- for (const skill of skillsToCreate) {
104
- const skillPath = path.join(globalSkillsDir, skill.name, 'SKILL.md');
105
- const skillContent = `---\nname: ${skill.name}\ndescription: ${skill.desc}\n---\n\n# ${skill.name}\n${skill.desc}\n`;
106
- writeFile(skillPath, skillContent);
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
- console.log('\n✅ PlanKit successfully initialized!');
114
- console.log('\nCreated / Updated configuration files:');
115
- console.log(' - artifacts/PLANKIT.md');
116
- console.log(' - .gemini/instructions.md');
117
- console.log(' - .cursorrules');
118
- console.log(' - AGENTS.md');
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(featureName) {
125
- const cwd = process.cwd();
126
- const featureDir = path.join(cwd, 'artifacts', 'current', featureName);
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
- console.log(`⚠️ Feature folder "${featureName}" already exists at artifacts/current/${featureName}`);
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
- console.log(`🚀 Creating new feature workspace for "${featureName}"...`);
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
- ensureDir(path.join(featureDir, 'phases'));
136
- ensureDir(path.join(featureDir, 'outputs'));
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 readmeContent = `# Feature: ${featureName}
303
+ const { config } = loadConfig(io.cwd);
304
+ moveFeatureToArchive(io.cwd, config, featureName, parsed.dryRun);
305
+ io.stdout(`Feature "${featureName}" archived.`);
306
+ }
139
307
 
140
- ## Overview
141
- Detailed description of the feature goal and scope.
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
- ## Phase Breakdown & Progress Status
312
+ if (fs.existsSync(targetDir)) {
313
+ throw new Error(`Archived feature "${featureName}" already exists.`);
314
+ }
144
315
 
145
- - [ ] Phase 1: Foundation & Setup — ([Spec](phases/phase-1-foundation.md))
146
- - [ ] Phase 2: Core Implementation
147
- - [ ] Phase 3: Testing & Verification
316
+ if (!dryRun) {
317
+ ensureDir(path.dirname(targetDir));
318
+ fs.renameSync(sourceDir, targetDir);
319
+ }
320
+ }
148
321
 
149
- ## Phase Execution Matrix
322
+ function requireFeatureDir(cwd, config, featureName) {
323
+ assertSafeName(featureName, 'feature name');
324
+ const featureDir = safeJoin(cwd, config.artifactsDir, 'current', featureName);
150
325
 
151
- | Phase # | Phase Title | Spec Path | Output Path | Status |
152
- |---|---|---|---|---|
153
- | Phase 1 | Foundation & Setup | \`phases/phase-1-foundation.md\` | \`outputs/phase-1-output.md\` | **PENDING** |
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
- const phase1Spec = `# Phase 1: Foundation & Setup
330
+ return featureDir;
331
+ }
157
332
 
158
- ## 1. Objective
159
- Establish base structure and foundational dependencies for ${featureName}.
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
- ## 2. Tasks
162
- - [ ] Initial setup
163
- - [ ] Basic implementation
164
- `;
338
+ if (!file) {
339
+ throw new Error(`Phase ${phaseNumber} spec not found under ${path.relative(process.cwd(), phasesDir)}`);
340
+ }
165
341
 
166
- writeFile(path.join(featureDir, 'README.md'), readmeContent);
167
- writeFile(path.join(featureDir, 'phases', 'phase-1-foundation.md'), phase1Spec);
168
- writeFile(path.join(featureDir, 'outputs', '.gitkeep'), '');
342
+ return path.join(phasesDir, file);
343
+ }
169
344
 
170
- console.log(`\n✅ Feature "${featureName}" initialized successfully!`);
171
- console.log(` - Spec: artifacts/current/${featureName}/phases/phase-1-foundation.md`);
172
- console.log(` - Overview: artifacts/current/${featureName}/README.md`);
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 showStatus() {
176
- const cwd = process.cwd();
177
- const currentDir = path.join(cwd, 'artifacts', 'current');
178
- const archivedDir = path.join(cwd, 'artifacts', 'archived');
350
+ function markReadmeArchived(readmePath, dryRun = false) {
351
+ if (!fs.existsSync(readmePath) || dryRun) {
352
+ return;
353
+ }
179
354
 
180
- console.log('📊 PlanKit Feature Status Summary:\n');
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
- if (fs.existsSync(currentDir)) {
183
- const currentFeatures = fs.readdirSync(currentDir).filter(f => !f.startsWith('.'));
184
- console.log('📌 ACTIVE FEATURES (artifacts/current/):');
185
- if (currentFeatures.length === 0) {
186
- console.log(' (none)');
187
- } else {
188
- currentFeatures.forEach(f => console.log(` - 🟡 ${f}`));
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
- console.log('');
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
- if (fs.existsSync(archivedDir)) {
195
- const archivedFeatures = fs.readdirSync(archivedDir).filter(f => !f.startsWith('.'));
196
- console.log('📦 ARCHIVED FEATURES (artifacts/archived/):');
197
- if (archivedFeatures.length === 0) {
198
- console.log(' (none)');
199
- } else {
200
- archivedFeatures.forEach(f => console.log(` - 🟢 ${f}`));
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 archiveFeature(featureName) {
206
- const cwd = process.cwd();
207
- const sourceDir = path.join(cwd, 'artifacts', 'current', featureName);
208
- const targetDir = path.join(cwd, 'artifacts', 'archived', featureName);
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
- if (!fs.existsSync(sourceDir)) {
211
- console.error(`Error: Active feature "${featureName}" not found in artifacts/current/`);
212
- process.exit(1);
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
- ensureDir(path.dirname(targetDir));
216
- fs.renameSync(sourceDir, targetDir);
486
+ return {
487
+ command,
488
+ flags,
489
+ positionals,
490
+ dryRun: Boolean(flags['dry-run'])
491
+ };
492
+ }
217
493
 
218
- console.log(`✅ Archived feature "${featureName}" -> artifacts/archived/${featureName}`);
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
- console.log(`
503
+ function showHelp(io) {
504
+ io.stdout(`
223
505
  PlanKit CLI — Command Suite for AI Coding Assistants
224
506
 
225
507
  Usage:
226
- npx plankit <command> [options]
508
+ plankit <command> [options]
227
509
 
228
510
  Commands:
229
- init Initialize PlanKit rules & commands in the current project
230
- plan <feature-name> Create a new feature workspace under artifacts/current/<feature-name>/
231
- status Show status of all active and archived features
232
- archive <feature-name> Move a completed feature to artifacts/archived/<feature-name>/
233
- help Show this help message
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
  }