plankit-cli 1.0.1 → 1.3.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 +30 -7
- package/bin/plankit.js +1 -0
- package/package.json +1 -1
- package/src/agents.js +98 -0
- package/src/cli.js +180 -62
- package/src/commandBundles.js +50 -0
- package/src/config.js +31 -5
- package/src/templates.js +2 -0
- package/templates/angular-commands/codex/angular-clone-pattern.md +23 -0
- package/templates/angular-commands/codex/angular-component-from.md +26 -0
- package/templates/angular-commands/codex/angular-design-match.md +20 -0
- package/templates/angular-commands/codex/angular-extract-component.md +21 -0
- package/templates/angular-commands/codex/angular-extract-store.md +19 -0
- package/templates/angular-commands/codex/angular-feature.md +24 -0
- package/templates/angular-commands/codex/angular-fixtures.md +23 -0
- package/templates/angular-commands/codex/angular-form-from-model.md +22 -0
- package/templates/angular-commands/codex/angular-generate-tests.md +20 -0
- package/templates/angular-commands/codex/angular-modernize.md +20 -0
- package/templates/angular-commands/codex/angular-page-from-api.md +23 -0
- package/templates/angular-commands/codex/angular-project-discovery.md +39 -0
- package/templates/angular-commands/codex/angular-responsive.md +21 -0
- package/templates/angular-commands/codex/angular-signalize.md +20 -0
- package/templates/angular-commands/codex/angular-split-component.md +21 -0
- package/templates/angular-commands/codex/angular-table-from-model.md +22 -0
- package/templates/angular-commands/codex/angular-wire-api.md +24 -0
- package/templates/angular-commands/cursor/angular-clone-pattern.md +24 -0
- package/templates/angular-commands/cursor/angular-component-from.md +27 -0
- package/templates/angular-commands/cursor/angular-design-match.md +21 -0
- package/templates/angular-commands/cursor/angular-extract-component.md +22 -0
- package/templates/angular-commands/cursor/angular-extract-store.md +20 -0
- package/templates/angular-commands/cursor/angular-feature.md +25 -0
- package/templates/angular-commands/cursor/angular-fixtures.md +24 -0
- package/templates/angular-commands/cursor/angular-form-from-model.md +23 -0
- package/templates/angular-commands/cursor/angular-generate-tests.md +21 -0
- package/templates/angular-commands/cursor/angular-modernize.md +21 -0
- package/templates/angular-commands/cursor/angular-page-from-api.md +24 -0
- package/templates/angular-commands/cursor/angular-project-discovery.md +39 -0
- package/templates/angular-commands/cursor/angular-responsive.md +22 -0
- package/templates/angular-commands/cursor/angular-signalize.md +21 -0
- package/templates/angular-commands/cursor/angular-split-component.md +22 -0
- package/templates/angular-commands/cursor/angular-table-from-model.md +23 -0
- package/templates/angular-commands/cursor/angular-wire-api.md +25 -0
- package/templates/angular-commands/opencode/angular-clone-pattern.md +21 -0
- package/templates/angular-commands/opencode/angular-component-from.md +24 -0
- package/templates/angular-commands/opencode/angular-design-match.md +18 -0
- package/templates/angular-commands/opencode/angular-extract-component.md +19 -0
- package/templates/angular-commands/opencode/angular-extract-store.md +17 -0
- package/templates/angular-commands/opencode/angular-feature.md +22 -0
- package/templates/angular-commands/opencode/angular-fixtures.md +21 -0
- package/templates/angular-commands/opencode/angular-form-from-model.md +20 -0
- package/templates/angular-commands/opencode/angular-generate-tests.md +18 -0
- package/templates/angular-commands/opencode/angular-modernize.md +18 -0
- package/templates/angular-commands/opencode/angular-page-from-api.md +21 -0
- package/templates/angular-commands/opencode/angular-project-discovery.md +39 -0
- package/templates/angular-commands/opencode/angular-responsive.md +19 -0
- package/templates/angular-commands/opencode/angular-signalize.md +18 -0
- package/templates/angular-commands/opencode/angular-split-component.md +19 -0
- package/templates/angular-commands/opencode/angular-table-from-model.md +20 -0
- package/templates/angular-commands/opencode/angular-wire-api.md +22 -0
- package/templates/angular-skills/gemini/angular-clone-pattern/SKILL.md +24 -0
- package/templates/angular-skills/gemini/angular-component-from/SKILL.md +27 -0
- package/templates/angular-skills/gemini/angular-design-match/SKILL.md +21 -0
- package/templates/angular-skills/gemini/angular-extract-component/SKILL.md +22 -0
- package/templates/angular-skills/gemini/angular-extract-store/SKILL.md +20 -0
- package/templates/angular-skills/gemini/angular-feature/SKILL.md +25 -0
- package/templates/angular-skills/gemini/angular-fixtures/SKILL.md +24 -0
- package/templates/angular-skills/gemini/angular-form-from-model/SKILL.md +23 -0
- package/templates/angular-skills/gemini/angular-generate-tests/SKILL.md +21 -0
- package/templates/angular-skills/gemini/angular-modernize/SKILL.md +21 -0
- package/templates/angular-skills/gemini/angular-page-from-api/SKILL.md +24 -0
- package/templates/angular-skills/gemini/angular-project-discovery/SKILL.md +41 -0
- package/templates/angular-skills/gemini/angular-responsive/SKILL.md +22 -0
- package/templates/angular-skills/gemini/angular-signalize/SKILL.md +21 -0
- package/templates/angular-skills/gemini/angular-split-component/SKILL.md +22 -0
- package/templates/angular-skills/gemini/angular-table-from-model/SKILL.md +23 -0
- package/templates/angular-skills/gemini/angular-wire-api/SKILL.md +25 -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,31 @@ 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) | Slash commands under `.opencode/commands/` |
|
|
81
|
+
| `gemini` / `antigravity` | `.gemini/instructions.md` + `angular-*` skills (plus `--global-gemini-skills` under your profile) |
|
|
82
|
+
| `codex` | `AGENTS.md` + Angular commands under `.codex/commands/` |
|
|
83
|
+
| `cursor` | `.cursorrules` + Angular command rules under `.cursor/rules/` |
|
|
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
|
+
### Angular command suite
|
|
92
|
+
|
|
93
|
+
Alongside the phase workflow, `init` installs a per-agent Angular command library that gives your AI assistant 16 deterministic Angular engineering workflows, driven by repository discovery rather than generic prompts:
|
|
94
|
+
|
|
95
|
+
`component-from`, `feature`, `clone-pattern`, `design-match`, `page-from-api`, `form-from-model`, `table-from-model`, `wire-api`, `signalize`, `extract-component`, `split-component`, `extract-store`, `modernize`, `responsive`, `generate-tests`, `fixtures` — plus a shared `angular-project-discovery` reference.
|
|
96
|
+
|
|
97
|
+
Every command ships with the exact per-agent binding, so only your selected agent's folder/commands are copied. To include Angular command templates in a custom `templatesDir`, mirror the `templates/angular-commands/` and `templates/angular-skills/` folder layout.
|
|
98
|
+
|
|
76
99
|
## Template overrides
|
|
77
100
|
|
|
78
101
|
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,94 @@ 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
|
-
|
|
123
|
-
|
|
117
|
+
const targetRoot = entry.global
|
|
118
|
+
? path.join(os.homedir(), ...entry.dest)
|
|
119
|
+
: safeJoin(io.cwd, ...entry.dest);
|
|
120
|
+
|
|
121
|
+
const sourceRoot = resolveBundleSource(entry.src, io.cwd, config);
|
|
122
|
+
ensureDir(targetRoot, options);
|
|
123
|
+
return copyBundleRecursive(sourceRoot, targetRoot, options);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveBundleSource(src, cwd, config) {
|
|
127
|
+
const customPath = config.templatesDir
|
|
128
|
+
? path.resolve(cwd, config.templatesDir, src)
|
|
129
|
+
: null;
|
|
130
|
+
if (customPath && fs.existsSync(customPath)) {
|
|
131
|
+
return customPath;
|
|
124
132
|
}
|
|
133
|
+
return path.join(packageRoot, 'templates', src);
|
|
134
|
+
}
|
|
125
135
|
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
function copyBundleRecursive(sourceRoot, targetRoot, options) {
|
|
137
|
+
const copyResults = [];
|
|
138
|
+
const files = collectFiles(sourceRoot);
|
|
139
|
+
for (const rel of files) {
|
|
140
|
+
const srcFile = path.join(sourceRoot, rel);
|
|
141
|
+
const destFile = path.join(targetRoot, rel);
|
|
142
|
+
const rendered = renderTemplateFromFile(srcFile);
|
|
143
|
+
copyResults.push(writeManagedFile(destFile, rendered, options));
|
|
144
|
+
}
|
|
145
|
+
const wroteAny = copyResults.some((result) => result && result.action !== 'skipped' && result.action !== 'unchanged');
|
|
146
|
+
return {
|
|
147
|
+
action: wroteAny ? 'copied' : 'unchanged',
|
|
148
|
+
path: targetRoot,
|
|
149
|
+
copies: copyResults
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function collectFiles(dir) {
|
|
154
|
+
const results = [];
|
|
155
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
156
|
+
const full = path.join(dir, entry.name);
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
results.push(...collectFiles(full).map((rel) => path.join(entry.name, rel)));
|
|
159
|
+
} else {
|
|
160
|
+
results.push(entry.name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function renderTemplateFromFile(filePath) {
|
|
167
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
168
|
+
.replace(/\{\{([a-zA-Z0-9_.-]+)\}\}/g, (_, key) => {
|
|
169
|
+
const maps = { artifactsDir: 'artifacts' };
|
|
170
|
+
return key in maps ? maps[key] : '';
|
|
171
|
+
});
|
|
128
172
|
}
|
|
129
173
|
|
|
130
174
|
function planFeature(parsed, io) {
|
|
@@ -372,15 +416,8 @@ function runVerificationCommand(command, io) {
|
|
|
372
416
|
|
|
373
417
|
function installGlobalGeminiSkills(config, options) {
|
|
374
418
|
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(
|
|
419
|
+
|
|
420
|
+
return GEMINI_SKILL_ENTRIES.map(([name, description]) => writeManagedFile(
|
|
384
421
|
path.join(globalSkillsDir, name, 'SKILL.md'),
|
|
385
422
|
`---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n\n${description}\n\nSpecification: ${config.artifactsDir}/PLANKIT.md\n`,
|
|
386
423
|
options
|
|
@@ -427,18 +464,57 @@ function readFeatureAndPhaseArgs(parsed, options = {}) {
|
|
|
427
464
|
return { featureName, phaseNumber };
|
|
428
465
|
}
|
|
429
466
|
|
|
430
|
-
function
|
|
431
|
-
|
|
432
|
-
|
|
467
|
+
async function resolveAgent(parsed, config, io, configPath = null) {
|
|
468
|
+
const fromFlag = parsed.flags.agent || parsed.flags.agents;
|
|
469
|
+
if (fromFlag) {
|
|
470
|
+
const parts = String(fromFlag).split(',').map((item) => item.trim()).filter(Boolean);
|
|
471
|
+
if (parts.length > 1) {
|
|
472
|
+
throw new Error(`Select a single agent, got ${parts.length}. Use --agent <name>. Valid agents: ${validAgentNames()}.`);
|
|
473
|
+
}
|
|
474
|
+
const agent = getAgent(parts[0]);
|
|
475
|
+
io.stdout(`Using agent from --agent flag: "${agent.id}".`);
|
|
476
|
+
return agent;
|
|
433
477
|
}
|
|
434
478
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
479
|
+
if (configPath) {
|
|
480
|
+
const configured = resolveConfiguredAgent(config);
|
|
481
|
+
if (configured) {
|
|
482
|
+
io.stdout(`Using configured agent "${configured.id}" from plan config.`);
|
|
483
|
+
return configured;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (io.isTTY && io.promptChoice) {
|
|
488
|
+
const agent = await promptAgent(io);
|
|
489
|
+
if (agent) {
|
|
490
|
+
io.stdout(`Selected agent: "${agent.id}".`);
|
|
491
|
+
return agent;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const agent = getAgent(DEFAULT_AGENT);
|
|
496
|
+
io.stdout(`No agent selected — using default agent "${agent.id}". Pass --agent <name> to change it.`);
|
|
497
|
+
return agent;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function promptAgent(io) {
|
|
501
|
+
const agents = listAgents();
|
|
502
|
+
const header = agents.map((agent, index) => (
|
|
503
|
+
` ${index + 1}. ${agent.label} — ${agent.description}`
|
|
504
|
+
)).join('\n');
|
|
505
|
+
const answer = await io.promptChoice(
|
|
506
|
+
`Select your coding agent:\n${header}\n\nEnter a number 1-${agents.length} or a name`,
|
|
507
|
+
agents.map((agent) => agent.id)
|
|
508
|
+
);
|
|
509
|
+
if (!answer) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
const trimmed = answer.trim().toLowerCase();
|
|
513
|
+
const num = Number.parseInt(trimmed, 10);
|
|
514
|
+
if (Number.isInteger(num) && num >= 1 && num <= agents.length) {
|
|
515
|
+
return agents[num - 1];
|
|
516
|
+
}
|
|
517
|
+
return getAgent(trimmed) || null;
|
|
442
518
|
}
|
|
443
519
|
|
|
444
520
|
function writeOptions(parsed, config) {
|
|
@@ -450,6 +526,17 @@ function writeOptions(parsed, config) {
|
|
|
450
526
|
|
|
451
527
|
function printResults(results, io) {
|
|
452
528
|
for (const result of results.filter(Boolean)) {
|
|
529
|
+
if (result.action === 'info') {
|
|
530
|
+
io.stdout(` ${'info'.padEnd(9)} ${path.relative(io.cwd, result.path) || result.path} (${result.message || ''})`);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (result.action === 'copied' && Array.isArray(result.copies)) {
|
|
534
|
+
io.stdout(` ${'copied'.padEnd(9)} ${path.relative(io.cwd, result.path) || result.path}/`);
|
|
535
|
+
for (const copy of result.copies.filter(Boolean)) {
|
|
536
|
+
io.stdout(` ${' '.padEnd(9)} ${path.relative(io.cwd, copy.path) || copy.path}`);
|
|
537
|
+
}
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
453
540
|
io.stdout(` ${result.action.padEnd(9)} ${path.relative(io.cwd, result.path) || result.path}`);
|
|
454
541
|
}
|
|
455
542
|
}
|
|
@@ -492,11 +579,41 @@ function parseArgs(args) {
|
|
|
492
579
|
}
|
|
493
580
|
|
|
494
581
|
function createIo(context) {
|
|
495
|
-
|
|
582
|
+
const input = context.input || process.stdin;
|
|
583
|
+
const output = context.output || process.stdout;
|
|
584
|
+
const io = {
|
|
496
585
|
cwd: context.cwd || process.cwd(),
|
|
497
586
|
stdout: context.stdout || ((message) => console.log(message)),
|
|
498
587
|
stderr: context.stderr || ((message) => console.error(message)),
|
|
499
|
-
exit: context.exit || ((code) => process.exit(code))
|
|
588
|
+
exit: context.exit || ((code) => process.exit(code)),
|
|
589
|
+
isTTY: context.isTTY !== undefined ? context.isTTY : Boolean(output && output.isTTY)
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
if (context.promptResponses !== undefined || input && input.isTTY) {
|
|
593
|
+
io.promptChoice = makePromptChoice(input, io.stdout, context.promptResponses);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return io;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function makePromptChoice(input, write, promptResponses) {
|
|
600
|
+
if (Array.isArray(promptResponses)) {
|
|
601
|
+
let index = 0;
|
|
602
|
+
return (prompt) => {
|
|
603
|
+
write(prompt);
|
|
604
|
+
const answer = promptResponses[index];
|
|
605
|
+
index += 1;
|
|
606
|
+
return answer === undefined ? '' : String(answer);
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return async (prompt) => {
|
|
611
|
+
write(prompt);
|
|
612
|
+
const readline = (await import('node:readline/promises')).default;
|
|
613
|
+
const rl = readline.createInterface({ input, output: process.stdout });
|
|
614
|
+
const answer = await rl.question('');
|
|
615
|
+
rl.close();
|
|
616
|
+
return answer;
|
|
500
617
|
};
|
|
501
618
|
}
|
|
502
619
|
|
|
@@ -520,7 +637,8 @@ Commands:
|
|
|
520
637
|
Options:
|
|
521
638
|
--force Overwrite generated files where safe
|
|
522
639
|
--dry-run Show intended actions without writing
|
|
523
|
-
--
|
|
640
|
+
--agent <name> Select your coding agent during init
|
|
641
|
+
(opencode, gemini, codex, cursor; default: opencode)
|
|
524
642
|
--global-gemini-skills Also install Gemini skills under the user profile
|
|
525
643
|
--phases 3 Generate N default phases during plan
|
|
526
644
|
--phases "Design,Build,Test" Generate named phases during plan
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
},
|
|
20
|
+
'codex-angular': {
|
|
21
|
+
label: 'Angular command suite',
|
|
22
|
+
src: path.join('angular-commands', 'codex'),
|
|
23
|
+
dest: ['.codex'],
|
|
24
|
+
kind: 'folder'
|
|
25
|
+
},
|
|
26
|
+
'cursor-angular': {
|
|
27
|
+
label: 'Angular command suite',
|
|
28
|
+
src: path.join('angular-commands', 'cursor'),
|
|
29
|
+
dest: ['.cursor', 'rules'],
|
|
30
|
+
kind: 'folder'
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function bundleEntry(bundleName) {
|
|
35
|
+
return COMMAND_BUNDLES[bundleName] || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function bundleSourceExists(bundleName, cwd, config) {
|
|
39
|
+
const entry = COMMAND_BUNDLES[bundleName];
|
|
40
|
+
if (!entry) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const customPath = config.templatesDir
|
|
44
|
+
? path.resolve(cwd, config.templatesDir, entry.src)
|
|
45
|
+
: null;
|
|
46
|
+
return Boolean(
|
|
47
|
+
customPath && fs.existsSync(customPath) &&
|
|
48
|
+
typeof customPath === 'string' && customPath.length
|
|
49
|
+
) || fs.existsSync(path.join(packageRoot, 'templates', entry.src));
|
|
50
|
+
}
|
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)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# angular-clone-pattern
|
|
2
|
+
> Clone the pattern of an existing feature/page/dialog/store/form/table/service or route config to a new target.
|
|
3
|
+
|
|
4
|
+
# /clone-pattern
|
|
5
|
+
|
|
6
|
+
Core primitive: clone an architectural pattern to a new target.
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
1. Build a PATTERN MODEL (type, architecture, state style, API style, routing, styling, testing).
|
|
10
|
+
2. Apply: PATTERN + TARGET DOMAIN = IMPLEMENTATION.
|
|
11
|
+
|
|
12
|
+
## Categories
|
|
13
|
+
- COPY: folder structure, naming conventions
|
|
14
|
+
- ADAPT: model, inputs, routes
|
|
15
|
+
- IGNORE: business logic, temporary workarounds, hardcoded styling
|
|
16
|
+
|
|
17
|
+
## Guardrail
|
|
18
|
+
Identify and refuse to propagate suspicious patterns (e.g. manual subscriptions without teardown) when the dominant repository pattern differs. Never build /clone-tech-debt.
|
|
19
|
+
|
|
20
|
+
## Execution
|
|
21
|
+
1. Run Angular PROJECT DISCOVERY against the repository.
|
|
22
|
+
2. Apply the workflow above with the provided target.
|
|
23
|
+
3. Report files created/modified and patterns reused.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# angular-component-from
|
|
2
|
+
> Generate a new Angular component using an existing component as the architectural and stylistic reference.
|
|
3
|
+
|
|
4
|
+
# /component-from
|
|
5
|
+
|
|
6
|
+
Generate a new component by learning from an existing one instead of blind clone-and-rename.
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
1. Analyze the source component and classify what is reusable vs business-specific.
|
|
10
|
+
2. Identify: component structure, file layout, input/output style, inject() convention, tokens, shared components, testing style.
|
|
11
|
+
3. Understand the target domain (optionally from --model).
|
|
12
|
+
4. Generate the new component using the reusable pattern.
|
|
13
|
+
|
|
14
|
+
## Guardrail
|
|
15
|
+
Never literal clone-and-rename (Product->User). Extract the pattern, understand the target domain, then generate.
|
|
16
|
+
|
|
17
|
+
## Options
|
|
18
|
+
- --model <Type> inspect the model to derive appropriate inputs
|
|
19
|
+
- --no-tests skip generating a spec
|
|
20
|
+
- --interactive ask before writing
|
|
21
|
+
- --dry-run show files to create only
|
|
22
|
+
|
|
23
|
+
## Execution
|
|
24
|
+
1. Run Angular PROJECT DISCOVERY against the repository.
|
|
25
|
+
2. Apply the workflow above with the provided target.
|
|
26
|
+
3. Report files created/modified and patterns reused.
|