plankit-cli 1.0.1 → 1.2.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/README.md +24 -7
- package/bin/plankit.js +1 -0
- package/package.json +1 -1
- package/src/agents.js +98 -0
- package/src/cli.js +184 -62
- package/src/commandBundles.js +51 -0
- package/src/config.js +31 -5
- package/src/templates.js +2 -0
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ plankit plan my-feature
|
|
|
23
23
|
|
|
24
24
|
| Command | Action |
|
|
25
25
|
|---|---|
|
|
26
|
-
| `plankit init` | Create PlanKit config, artifact folders, spec, and selected agent
|
|
26
|
+
| `plankit init [--agent <name>]` | Create PlanKit config, artifact folders, spec, and the **selected agent's** folders/commands. Prompts for the agent when none is specified. |
|
|
27
27
|
| `plankit plan <feature> [requirements]` | Create `artifacts/current/<feature>/` with README, phase specs, outputs folder, and state metadata. |
|
|
28
28
|
| `plankit clarify <feature> [phase]` | Create a clarification artifact for requirements and design decisions. |
|
|
29
29
|
| `plankit implement <feature> <phase>` | Validate phase context and prepare `outputs/phase-N-output.md`. |
|
|
@@ -33,17 +33,14 @@ plankit plan my-feature
|
|
|
33
33
|
|
|
34
34
|
## Customization
|
|
35
35
|
|
|
36
|
-
`plankit init` creates `plankit.config.json
|
|
36
|
+
`plankit init` creates `plankit.config.json`. By default **only one agent** is installed — the one you select (interactively, via `--agent`, or the default `opencode`).
|
|
37
37
|
|
|
38
38
|
```json
|
|
39
39
|
{
|
|
40
40
|
"artifactsDir": "artifacts",
|
|
41
41
|
"overwrite": "skip",
|
|
42
42
|
"agents": {
|
|
43
|
-
"
|
|
44
|
-
"cursor": true,
|
|
45
|
-
"opencode": true,
|
|
46
|
-
"gemini": true
|
|
43
|
+
"default": "opencode"
|
|
47
44
|
},
|
|
48
45
|
"defaultPhases": [
|
|
49
46
|
{
|
|
@@ -64,7 +61,8 @@ plankit plan my-feature
|
|
|
64
61
|
Useful options:
|
|
65
62
|
|
|
66
63
|
```bash
|
|
67
|
-
plankit init --
|
|
64
|
+
plankit init --agent opencode
|
|
65
|
+
plankit init --agent gemini --global-gemini-skills
|
|
68
66
|
plankit init --force
|
|
69
67
|
plankit plan my-feature --phases 4
|
|
70
68
|
plankit plan my-feature --phases "Design,Build,Test"
|
|
@@ -73,6 +71,25 @@ plankit review my-feature --skip-build
|
|
|
73
71
|
plankit status --json
|
|
74
72
|
```
|
|
75
73
|
|
|
74
|
+
### Agent selection
|
|
75
|
+
|
|
76
|
+
`plankit init` installs **only** the folders/commands for the agent you select. Supported agents:
|
|
77
|
+
|
|
78
|
+
| Agent | What is copied |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `opencode` (default) | `AGENTS.md`-style slash commands under `.opencode/commands/` |
|
|
81
|
+
| `gemini` / `antigravity` | `.gemini/instructions.md` (plus `--global-gemini-skills` under your profile) |
|
|
82
|
+
| `codex` | `AGENTS.md` |
|
|
83
|
+
| `cursor` | `.cursorrules` |
|
|
84
|
+
|
|
85
|
+
Selection order:
|
|
86
|
+
1. `--agent <name>` flag (non-interactive / CI).
|
|
87
|
+
2. A previously configured `agents.default` in `plankit.config.json`.
|
|
88
|
+
3. An interactive numbered menu (when run in a terminal).
|
|
89
|
+
4. The `opencode` default — announced explicitly so the behavior is never silent.
|
|
90
|
+
|
|
91
|
+
When the Angular command/skill library ships, each agent's `commandBundles` entry installs that agent's `commands/`/`skills/` folder automatically with no CLI changes.
|
|
92
|
+
|
|
76
93
|
## Template overrides
|
|
77
94
|
|
|
78
95
|
Default templates live in the package `templates/` directory. To override them in a project:
|
package/bin/plankit.js
CHANGED
package/package.json
CHANGED
package/src/agents.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_AGENT = 'opencode';
|
|
5
|
+
|
|
6
|
+
export const AGENT_ORDER = ['opencode', 'gemini', 'codex', 'cursor'];
|
|
7
|
+
|
|
8
|
+
export const GEMINI_SKILL_ENTRIES = [
|
|
9
|
+
['plankit', 'PlanKit Command Suite master skill.'],
|
|
10
|
+
['plankit-plan', 'Initialize a feature workspace.'],
|
|
11
|
+
['plankit-clarify', 'Analyze requirements and ask targeted clarifying questions.'],
|
|
12
|
+
['plankit-implement', 'Prepare phase implementation context and output artifacts.'],
|
|
13
|
+
['plankit-review', 'Run verification and archive completed features.']
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const AGENT_DESCRIPTIONS = {
|
|
17
|
+
codex: 'Copies the PlanKit Command Suite into AGENTS.md (Codex reads AGENTS.md).',
|
|
18
|
+
cursor: 'Copies the PlanKit rules into .cursorrules for Cursor AI.',
|
|
19
|
+
opencode: 'Installs the PlanKit slash commands into .opencode/commands/ for OpenCode.',
|
|
20
|
+
gemini: 'Installs .gemini/instructions.md and optional global Gemini skills (Antigravity alias).'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const AGENTS = {
|
|
24
|
+
opencode: {
|
|
25
|
+
id: 'opencode',
|
|
26
|
+
label: 'OpenCode',
|
|
27
|
+
description: AGENT_DESCRIPTIONS.opencode,
|
|
28
|
+
aliases: [],
|
|
29
|
+
bindings: [
|
|
30
|
+
{ template: 'opencode/plankit.md', dest: ['.opencode', 'commands', 'plankit.md'] },
|
|
31
|
+
{ template: 'opencode/plankit-plan.md', dest: ['.opencode', 'commands', 'plankit-plan.md'] },
|
|
32
|
+
{ template: 'opencode/plankit-clarify.md', dest: ['.opencode', 'commands', 'plankit-clarify.md'] },
|
|
33
|
+
{ template: 'opencode/plankit-implement.md', dest: ['.opencode', 'commands', 'plankit-implement.md'] },
|
|
34
|
+
{ template: 'opencode/plankit-review.md', dest: ['.opencode', 'commands', 'plankit-review.md'] }
|
|
35
|
+
],
|
|
36
|
+
commandBundles: ['opencode-angular'],
|
|
37
|
+
globalSkillEntries: null
|
|
38
|
+
},
|
|
39
|
+
gemini: {
|
|
40
|
+
id: 'gemini',
|
|
41
|
+
label: 'Antigravity / Gemini',
|
|
42
|
+
description: AGENT_DESCRIPTIONS.gemini,
|
|
43
|
+
aliases: ['antigravity'],
|
|
44
|
+
bindings: [
|
|
45
|
+
{ template: 'gemini-instructions.md', dest: ['.gemini', 'instructions.md'] }
|
|
46
|
+
],
|
|
47
|
+
commandBundles: ['gemini-angular'],
|
|
48
|
+
globalSkillEntries: GEMINI_SKILL_ENTRIES
|
|
49
|
+
},
|
|
50
|
+
codex: {
|
|
51
|
+
id: 'codex',
|
|
52
|
+
label: 'Codex',
|
|
53
|
+
description: AGENT_DESCRIPTIONS.codex,
|
|
54
|
+
aliases: [],
|
|
55
|
+
bindings: [
|
|
56
|
+
{ template: 'AGENTS.md', dest: ['AGENTS.md'] }
|
|
57
|
+
],
|
|
58
|
+
commandBundles: ['codex-angular'],
|
|
59
|
+
globalSkillEntries: null
|
|
60
|
+
},
|
|
61
|
+
cursor: {
|
|
62
|
+
id: 'cursor',
|
|
63
|
+
label: 'Cursor',
|
|
64
|
+
description: AGENT_DESCRIPTIONS.cursor,
|
|
65
|
+
aliases: [],
|
|
66
|
+
bindings: [
|
|
67
|
+
{ template: 'cursorrules', dest: ['.cursorrules'] }
|
|
68
|
+
],
|
|
69
|
+
commandBundles: ['cursor-angular'],
|
|
70
|
+
globalSkillEntries: null
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function listAgents() {
|
|
75
|
+
return AGENT_ORDER.map((id) => AGENTS[id]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function validAgentNames() {
|
|
79
|
+
return AGENT_ORDER.map((id) => AGENTS[id].aliases.concat(id)).flat().join(', ');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getAgent(input) {
|
|
83
|
+
const name = String(input).trim().toLowerCase();
|
|
84
|
+
if (!name) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
for (const id of AGENT_ORDER) {
|
|
88
|
+
const agent = AGENTS[id];
|
|
89
|
+
if (id === name || agent.aliases.includes(name)) {
|
|
90
|
+
return agent;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Unknown agent "${input}". Valid agents: ${validAgentNames()}.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function agentBindingsExist(agent) {
|
|
97
|
+
return Array.isArray(agent.bindings) && agent.bindings.length > 0;
|
|
98
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -10,12 +10,15 @@ import {
|
|
|
10
10
|
loadConfig,
|
|
11
11
|
normalizePhases,
|
|
12
12
|
parsePhaseFlag,
|
|
13
|
+
resolveConfiguredAgent,
|
|
13
14
|
safeJoin,
|
|
14
15
|
writeManagedFile
|
|
15
16
|
} from './config.js';
|
|
16
|
-
import { renderList, renderTemplate } from './templates.js';
|
|
17
|
+
import { renderList, renderTemplate, packageRoot } from './templates.js';
|
|
18
|
+
import { DEFAULT_AGENT, GEMINI_SKILL_ENTRIES, getAgent, listAgents, validAgentNames } from './agents.js';
|
|
19
|
+
import { bundleEntry, bundleSourceExists } from './commandBundles.js';
|
|
17
20
|
|
|
18
|
-
export function runCli(args, context = {}) {
|
|
21
|
+
export async function runCli(args, context = {}) {
|
|
19
22
|
const io = createIo(context);
|
|
20
23
|
|
|
21
24
|
try {
|
|
@@ -24,7 +27,7 @@ export function runCli(args, context = {}) {
|
|
|
24
27
|
|
|
25
28
|
switch (command) {
|
|
26
29
|
case 'init':
|
|
27
|
-
initProject(parsed, io);
|
|
30
|
+
await initProject(parsed, io);
|
|
28
31
|
break;
|
|
29
32
|
case 'plan':
|
|
30
33
|
planFeature(parsed, io);
|
|
@@ -59,10 +62,10 @@ export function runCli(args, context = {}) {
|
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
function initProject(parsed, io) {
|
|
63
|
-
const { config } = loadConfig(io.cwd);
|
|
65
|
+
async function initProject(parsed, io) {
|
|
66
|
+
const { config, path: configPath } = loadConfig(io.cwd);
|
|
64
67
|
const options = writeOptions(parsed, config);
|
|
65
|
-
const
|
|
68
|
+
const agent = await resolveAgent(parsed, config, io, configPath);
|
|
66
69
|
const results = [];
|
|
67
70
|
const artifactsDir = safeJoin(io.cwd, config.artifactsDir);
|
|
68
71
|
|
|
@@ -78,53 +81,98 @@ function initProject(parsed, io) {
|
|
|
78
81
|
results.push(writeManagedFile(path.join(artifactsDir, 'archived', '.gitkeep'), '', options));
|
|
79
82
|
results.push(createDefaultConfigFile(io.cwd, options));
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
for (const binding of agent.bindings) {
|
|
82
85
|
results.push(writeManagedFile(
|
|
83
|
-
safeJoin(io.cwd,
|
|
84
|
-
renderTemplate(config,
|
|
86
|
+
safeJoin(io.cwd, ...binding.dest),
|
|
87
|
+
renderTemplate(config, binding.template, { artifactsDir: config.artifactsDir, agent: agent.id }, io.cwd),
|
|
85
88
|
options
|
|
86
89
|
));
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
results.push(
|
|
91
|
-
safeJoin(io.cwd, '.cursorrules'),
|
|
92
|
-
renderTemplate(config, 'cursorrules', { artifactsDir: config.artifactsDir }, io.cwd),
|
|
93
|
-
options
|
|
94
|
-
));
|
|
92
|
+
for (const bundleName of agent.commandBundles) {
|
|
93
|
+
results.push(installCommandBundle(agent, bundleName, config, options, io));
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
if (
|
|
98
|
-
results.push(
|
|
99
|
-
safeJoin(io.cwd, 'AGENTS.md'),
|
|
100
|
-
renderTemplate(config, 'AGENTS.md', { artifactsDir: config.artifactsDir }, io.cwd),
|
|
101
|
-
options
|
|
102
|
-
));
|
|
96
|
+
if (parsed.flags['global-gemini-skills'] && agent.id === 'gemini') {
|
|
97
|
+
results.push(...installGlobalGeminiSkills(config, options));
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
100
|
+
io.stdout(`PlanKit initialized for agent "${agent.id}".`);
|
|
101
|
+
printResults(results, io);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function installCommandBundle(agent, bundleName, config, options, io) {
|
|
105
|
+
const entry = bundleEntry(bundleName);
|
|
106
|
+
if (!entry) {
|
|
107
|
+
return { action: 'info', path: bundleName, message: `unknown bundle for ${agent.id}` };
|
|
108
|
+
}
|
|
109
|
+
if (!bundleSourceExists(bundleName, io.cwd, config)) {
|
|
110
|
+
return {
|
|
111
|
+
action: 'info',
|
|
112
|
+
path: bundleName,
|
|
113
|
+
message: `${entry.label} for ${agent.id} not shipped yet`
|
|
114
|
+
};
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
if (
|
|
123
|
-
|
|
117
|
+
if (entry.global && options.dryRun) {
|
|
118
|
+
return { action: 'info', path: bundleName, message: 'would install to user profile (dry run)' };
|
|
124
119
|
}
|
|
125
120
|
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
const targetRoot = entry.global
|
|
122
|
+
? path.join(os.homedir(), ...entry.dest)
|
|
123
|
+
: safeJoin(io.cwd, ...entry.dest);
|
|
124
|
+
|
|
125
|
+
const sourceRoot = resolveBundleSource(entry.src, io.cwd, config);
|
|
126
|
+
ensureDir(targetRoot, options);
|
|
127
|
+
return copyBundleRecursive(sourceRoot, targetRoot, options);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveBundleSource(src, cwd, config) {
|
|
131
|
+
const customPath = config.templatesDir
|
|
132
|
+
? path.resolve(cwd, config.templatesDir, src)
|
|
133
|
+
: null;
|
|
134
|
+
if (customPath && fs.existsSync(customPath)) {
|
|
135
|
+
return customPath;
|
|
136
|
+
}
|
|
137
|
+
return path.join(packageRoot, 'templates', src);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function copyBundleRecursive(sourceRoot, targetRoot, options) {
|
|
141
|
+
const copyResults = [];
|
|
142
|
+
const files = collectFiles(sourceRoot);
|
|
143
|
+
for (const rel of files) {
|
|
144
|
+
const srcFile = path.join(sourceRoot, rel);
|
|
145
|
+
const destFile = path.join(targetRoot, rel);
|
|
146
|
+
const rendered = renderTemplateFromFile(srcFile);
|
|
147
|
+
copyResults.push(writeManagedFile(destFile, rendered, options));
|
|
148
|
+
}
|
|
149
|
+
const wroteAny = copyResults.some((result) => result && result.action !== 'skipped' && result.action !== 'unchanged');
|
|
150
|
+
return {
|
|
151
|
+
action: wroteAny ? 'copied' : 'unchanged',
|
|
152
|
+
path: targetRoot,
|
|
153
|
+
copies: copyResults
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function collectFiles(dir) {
|
|
158
|
+
const results = [];
|
|
159
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
160
|
+
const full = path.join(dir, entry.name);
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
results.push(...collectFiles(full).map((rel) => path.join(entry.name, rel)));
|
|
163
|
+
} else {
|
|
164
|
+
results.push(entry.name);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function renderTemplateFromFile(filePath) {
|
|
171
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
172
|
+
.replace(/\{\{([a-zA-Z0-9_.-]+)\}\}/g, (_, key) => {
|
|
173
|
+
const maps = { artifactsDir: 'artifacts' };
|
|
174
|
+
return key in maps ? maps[key] : '';
|
|
175
|
+
});
|
|
128
176
|
}
|
|
129
177
|
|
|
130
178
|
function planFeature(parsed, io) {
|
|
@@ -372,15 +420,8 @@ function runVerificationCommand(command, io) {
|
|
|
372
420
|
|
|
373
421
|
function installGlobalGeminiSkills(config, options) {
|
|
374
422
|
const globalSkillsDir = path.join(os.homedir(), '.gemini', 'skills');
|
|
375
|
-
|
|
376
|
-
|
|
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(
|
|
423
|
+
|
|
424
|
+
return GEMINI_SKILL_ENTRIES.map(([name, description]) => writeManagedFile(
|
|
384
425
|
path.join(globalSkillsDir, name, 'SKILL.md'),
|
|
385
426
|
`---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n\n${description}\n\nSpecification: ${config.artifactsDir}/PLANKIT.md\n`,
|
|
386
427
|
options
|
|
@@ -427,18 +468,57 @@ function readFeatureAndPhaseArgs(parsed, options = {}) {
|
|
|
427
468
|
return { featureName, phaseNumber };
|
|
428
469
|
}
|
|
429
470
|
|
|
430
|
-
function
|
|
431
|
-
|
|
432
|
-
|
|
471
|
+
async function resolveAgent(parsed, config, io, configPath = null) {
|
|
472
|
+
const fromFlag = parsed.flags.agent || parsed.flags.agents;
|
|
473
|
+
if (fromFlag) {
|
|
474
|
+
const parts = String(fromFlag).split(',').map((item) => item.trim()).filter(Boolean);
|
|
475
|
+
if (parts.length > 1) {
|
|
476
|
+
throw new Error(`Select a single agent, got ${parts.length}. Use --agent <name>. Valid agents: ${validAgentNames()}.`);
|
|
477
|
+
}
|
|
478
|
+
const agent = getAgent(parts[0]);
|
|
479
|
+
io.stdout(`Using agent from --agent flag: "${agent.id}".`);
|
|
480
|
+
return agent;
|
|
433
481
|
}
|
|
434
482
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
483
|
+
if (configPath) {
|
|
484
|
+
const configured = resolveConfiguredAgent(config);
|
|
485
|
+
if (configured) {
|
|
486
|
+
io.stdout(`Using configured agent "${configured.id}" from plan config.`);
|
|
487
|
+
return configured;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (io.isTTY && io.promptChoice) {
|
|
492
|
+
const agent = await promptAgent(io);
|
|
493
|
+
if (agent) {
|
|
494
|
+
io.stdout(`Selected agent: "${agent.id}".`);
|
|
495
|
+
return agent;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const agent = getAgent(DEFAULT_AGENT);
|
|
500
|
+
io.stdout(`No agent selected — using default agent "${agent.id}". Pass --agent <name> to change it.`);
|
|
501
|
+
return agent;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function promptAgent(io) {
|
|
505
|
+
const agents = listAgents();
|
|
506
|
+
const header = agents.map((agent, index) => (
|
|
507
|
+
` ${index + 1}. ${agent.label} — ${agent.description}`
|
|
508
|
+
)).join('\n');
|
|
509
|
+
const answer = await io.promptChoice(
|
|
510
|
+
`Select your coding agent:\n${header}\n\nEnter a number 1-${agents.length} or a name`,
|
|
511
|
+
agents.map((agent) => agent.id)
|
|
512
|
+
);
|
|
513
|
+
if (!answer) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
const trimmed = answer.trim().toLowerCase();
|
|
517
|
+
const num = Number.parseInt(trimmed, 10);
|
|
518
|
+
if (Number.isInteger(num) && num >= 1 && num <= agents.length) {
|
|
519
|
+
return agents[num - 1];
|
|
520
|
+
}
|
|
521
|
+
return getAgent(trimmed) || null;
|
|
442
522
|
}
|
|
443
523
|
|
|
444
524
|
function writeOptions(parsed, config) {
|
|
@@ -450,6 +530,17 @@ function writeOptions(parsed, config) {
|
|
|
450
530
|
|
|
451
531
|
function printResults(results, io) {
|
|
452
532
|
for (const result of results.filter(Boolean)) {
|
|
533
|
+
if (result.action === 'info') {
|
|
534
|
+
io.stdout(` ${'info'.padEnd(9)} ${path.relative(io.cwd, result.path) || result.path} (${result.message || ''})`);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (result.action === 'copied' && Array.isArray(result.copies)) {
|
|
538
|
+
io.stdout(` ${'copied'.padEnd(9)} ${path.relative(io.cwd, result.path) || result.path}/`);
|
|
539
|
+
for (const copy of result.copies.filter(Boolean)) {
|
|
540
|
+
io.stdout(` ${' '.padEnd(9)} ${path.relative(io.cwd, copy.path) || copy.path}`);
|
|
541
|
+
}
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
453
544
|
io.stdout(` ${result.action.padEnd(9)} ${path.relative(io.cwd, result.path) || result.path}`);
|
|
454
545
|
}
|
|
455
546
|
}
|
|
@@ -492,11 +583,41 @@ function parseArgs(args) {
|
|
|
492
583
|
}
|
|
493
584
|
|
|
494
585
|
function createIo(context) {
|
|
495
|
-
|
|
586
|
+
const input = context.input || process.stdin;
|
|
587
|
+
const output = context.output || process.stdout;
|
|
588
|
+
const io = {
|
|
496
589
|
cwd: context.cwd || process.cwd(),
|
|
497
590
|
stdout: context.stdout || ((message) => console.log(message)),
|
|
498
591
|
stderr: context.stderr || ((message) => console.error(message)),
|
|
499
|
-
exit: context.exit || ((code) => process.exit(code))
|
|
592
|
+
exit: context.exit || ((code) => process.exit(code)),
|
|
593
|
+
isTTY: context.isTTY !== undefined ? context.isTTY : Boolean(output && output.isTTY)
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
if (context.promptResponses !== undefined || input && input.isTTY) {
|
|
597
|
+
io.promptChoice = makePromptChoice(input, io.stdout, context.promptResponses);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return io;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function makePromptChoice(input, write, promptResponses) {
|
|
604
|
+
if (Array.isArray(promptResponses)) {
|
|
605
|
+
let index = 0;
|
|
606
|
+
return (prompt) => {
|
|
607
|
+
write(prompt);
|
|
608
|
+
const answer = promptResponses[index];
|
|
609
|
+
index += 1;
|
|
610
|
+
return answer === undefined ? '' : String(answer);
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return async (prompt) => {
|
|
615
|
+
write(prompt);
|
|
616
|
+
const readline = (await import('node:readline/promises')).default;
|
|
617
|
+
const rl = readline.createInterface({ input, output: process.stdout });
|
|
618
|
+
const answer = await rl.question('');
|
|
619
|
+
rl.close();
|
|
620
|
+
return answer;
|
|
500
621
|
};
|
|
501
622
|
}
|
|
502
623
|
|
|
@@ -520,7 +641,8 @@ Commands:
|
|
|
520
641
|
Options:
|
|
521
642
|
--force Overwrite generated files where safe
|
|
522
643
|
--dry-run Show intended actions without writing
|
|
523
|
-
--
|
|
644
|
+
--agent <name> Select your coding agent during init
|
|
645
|
+
(opencode, gemini, codex, cursor; default: opencode)
|
|
524
646
|
--global-gemini-skills Also install Gemini skills under the user profile
|
|
525
647
|
--phases 3 Generate N default phases during plan
|
|
526
648
|
--phases "Design,Build,Test" Generate named phases during plan
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
|
+
|
|
7
|
+
export const COMMAND_BUNDLES = {
|
|
8
|
+
'opencode-angular': {
|
|
9
|
+
label: 'Angular command suite',
|
|
10
|
+
src: path.join('angular-commands', 'opencode'),
|
|
11
|
+
dest: ['.opencode', 'commands'],
|
|
12
|
+
kind: 'folder'
|
|
13
|
+
},
|
|
14
|
+
'gemini-angular': {
|
|
15
|
+
label: 'Angular skill suite',
|
|
16
|
+
src: path.join('angular-skills', 'gemini'),
|
|
17
|
+
dest: ['.gemini', 'skills'],
|
|
18
|
+
kind: 'folder',
|
|
19
|
+
global: true
|
|
20
|
+
},
|
|
21
|
+
'codex-angular': {
|
|
22
|
+
label: 'Angular command suite',
|
|
23
|
+
src: path.join('angular-commands', 'codex'),
|
|
24
|
+
dest: ['.codex'],
|
|
25
|
+
kind: 'folder'
|
|
26
|
+
},
|
|
27
|
+
'cursor-angular': {
|
|
28
|
+
label: 'Angular command suite',
|
|
29
|
+
src: path.join('angular-commands', 'cursor'),
|
|
30
|
+
dest: ['.cursor'],
|
|
31
|
+
kind: 'folder'
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function bundleEntry(bundleName) {
|
|
36
|
+
return COMMAND_BUNDLES[bundleName] || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function bundleSourceExists(bundleName, cwd, config) {
|
|
40
|
+
const entry = COMMAND_BUNDLES[bundleName];
|
|
41
|
+
if (!entry) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const customPath = config.templatesDir
|
|
45
|
+
? path.resolve(cwd, config.templatesDir, entry.src)
|
|
46
|
+
: null;
|
|
47
|
+
return Boolean(
|
|
48
|
+
customPath && fs.existsSync(customPath) &&
|
|
49
|
+
typeof customPath === 'string' && customPath.length
|
|
50
|
+
) || fs.existsSync(path.join(packageRoot, 'templates', entry.src));
|
|
51
|
+
}
|
package/src/config.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { AGENT_ORDER, DEFAULT_AGENT, getAgent } from './agents.js';
|
|
3
4
|
|
|
4
5
|
export const DEFAULT_CONFIG = {
|
|
5
6
|
artifactsDir: 'artifacts',
|
|
6
7
|
overwrite: 'skip',
|
|
7
8
|
agents: {
|
|
8
|
-
|
|
9
|
-
cursor: true,
|
|
10
|
-
opencode: true,
|
|
11
|
-
gemini: true
|
|
9
|
+
default: DEFAULT_AGENT
|
|
12
10
|
},
|
|
13
11
|
defaultPhases: [
|
|
14
12
|
{
|
|
@@ -70,11 +68,39 @@ export function normalizeConfig(config) {
|
|
|
70
68
|
normalized.defaultPhases = normalizePhases(normalized.defaultPhases);
|
|
71
69
|
normalized.artifactsDir = normalized.artifactsDir || DEFAULT_CONFIG.artifactsDir;
|
|
72
70
|
normalized.overwrite = normalized.overwrite || DEFAULT_CONFIG.overwrite;
|
|
73
|
-
normalized.agents =
|
|
71
|
+
normalized.agents = normalizeAgents(normalized.agents);
|
|
74
72
|
normalized.verification = { ...DEFAULT_CONFIG.verification, ...(normalized.verification || {}) };
|
|
75
73
|
return normalized;
|
|
76
74
|
}
|
|
77
75
|
|
|
76
|
+
export function normalizeAgents(agents) {
|
|
77
|
+
if (!agents || typeof agents !== 'object') {
|
|
78
|
+
return { ...DEFAULT_CONFIG.agents };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const hasDefault = typeof agents.default === 'string' && agents.default.length > 0;
|
|
82
|
+
if (hasDefault) {
|
|
83
|
+
return { default: agents.default };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const legacyEnabled = AGENT_ORDER.filter((id) => agents[id] === true);
|
|
87
|
+
if (legacyEnabled.length > 0) {
|
|
88
|
+
return { default: legacyEnabled[0] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { ...DEFAULT_CONFIG.agents };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveConfiguredAgent(config) {
|
|
95
|
+
const configured = config.agents && config.agents.default;
|
|
96
|
+
try {
|
|
97
|
+
return configured ? getAgent(configured) : null;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
78
104
|
export function normalizePhases(phases) {
|
|
79
105
|
if (!Array.isArray(phases) || phases.length === 0) {
|
|
80
106
|
return DEFAULT_CONFIG.defaultPhases;
|
package/src/templates.js
CHANGED
|
@@ -4,6 +4,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
|
|
5
5
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
6
|
|
|
7
|
+
export { packageRoot };
|
|
8
|
+
|
|
7
9
|
export function renderTemplate(config, templateName, variables = {}, cwd = process.cwd()) {
|
|
8
10
|
const customPath = config.templatesDir
|
|
9
11
|
? path.resolve(cwd, config.templatesDir, templateName)
|