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.
Files changed (76) hide show
  1. package/README.md +30 -7
  2. package/bin/plankit.js +1 -0
  3. package/package.json +1 -1
  4. package/src/agents.js +98 -0
  5. package/src/cli.js +180 -62
  6. package/src/commandBundles.js +50 -0
  7. package/src/config.js +31 -5
  8. package/src/templates.js +2 -0
  9. package/templates/angular-commands/codex/angular-clone-pattern.md +23 -0
  10. package/templates/angular-commands/codex/angular-component-from.md +26 -0
  11. package/templates/angular-commands/codex/angular-design-match.md +20 -0
  12. package/templates/angular-commands/codex/angular-extract-component.md +21 -0
  13. package/templates/angular-commands/codex/angular-extract-store.md +19 -0
  14. package/templates/angular-commands/codex/angular-feature.md +24 -0
  15. package/templates/angular-commands/codex/angular-fixtures.md +23 -0
  16. package/templates/angular-commands/codex/angular-form-from-model.md +22 -0
  17. package/templates/angular-commands/codex/angular-generate-tests.md +20 -0
  18. package/templates/angular-commands/codex/angular-modernize.md +20 -0
  19. package/templates/angular-commands/codex/angular-page-from-api.md +23 -0
  20. package/templates/angular-commands/codex/angular-project-discovery.md +39 -0
  21. package/templates/angular-commands/codex/angular-responsive.md +21 -0
  22. package/templates/angular-commands/codex/angular-signalize.md +20 -0
  23. package/templates/angular-commands/codex/angular-split-component.md +21 -0
  24. package/templates/angular-commands/codex/angular-table-from-model.md +22 -0
  25. package/templates/angular-commands/codex/angular-wire-api.md +24 -0
  26. package/templates/angular-commands/cursor/angular-clone-pattern.md +24 -0
  27. package/templates/angular-commands/cursor/angular-component-from.md +27 -0
  28. package/templates/angular-commands/cursor/angular-design-match.md +21 -0
  29. package/templates/angular-commands/cursor/angular-extract-component.md +22 -0
  30. package/templates/angular-commands/cursor/angular-extract-store.md +20 -0
  31. package/templates/angular-commands/cursor/angular-feature.md +25 -0
  32. package/templates/angular-commands/cursor/angular-fixtures.md +24 -0
  33. package/templates/angular-commands/cursor/angular-form-from-model.md +23 -0
  34. package/templates/angular-commands/cursor/angular-generate-tests.md +21 -0
  35. package/templates/angular-commands/cursor/angular-modernize.md +21 -0
  36. package/templates/angular-commands/cursor/angular-page-from-api.md +24 -0
  37. package/templates/angular-commands/cursor/angular-project-discovery.md +39 -0
  38. package/templates/angular-commands/cursor/angular-responsive.md +22 -0
  39. package/templates/angular-commands/cursor/angular-signalize.md +21 -0
  40. package/templates/angular-commands/cursor/angular-split-component.md +22 -0
  41. package/templates/angular-commands/cursor/angular-table-from-model.md +23 -0
  42. package/templates/angular-commands/cursor/angular-wire-api.md +25 -0
  43. package/templates/angular-commands/opencode/angular-clone-pattern.md +21 -0
  44. package/templates/angular-commands/opencode/angular-component-from.md +24 -0
  45. package/templates/angular-commands/opencode/angular-design-match.md +18 -0
  46. package/templates/angular-commands/opencode/angular-extract-component.md +19 -0
  47. package/templates/angular-commands/opencode/angular-extract-store.md +17 -0
  48. package/templates/angular-commands/opencode/angular-feature.md +22 -0
  49. package/templates/angular-commands/opencode/angular-fixtures.md +21 -0
  50. package/templates/angular-commands/opencode/angular-form-from-model.md +20 -0
  51. package/templates/angular-commands/opencode/angular-generate-tests.md +18 -0
  52. package/templates/angular-commands/opencode/angular-modernize.md +18 -0
  53. package/templates/angular-commands/opencode/angular-page-from-api.md +21 -0
  54. package/templates/angular-commands/opencode/angular-project-discovery.md +39 -0
  55. package/templates/angular-commands/opencode/angular-responsive.md +19 -0
  56. package/templates/angular-commands/opencode/angular-signalize.md +18 -0
  57. package/templates/angular-commands/opencode/angular-split-component.md +19 -0
  58. package/templates/angular-commands/opencode/angular-table-from-model.md +20 -0
  59. package/templates/angular-commands/opencode/angular-wire-api.md +22 -0
  60. package/templates/angular-skills/gemini/angular-clone-pattern/SKILL.md +24 -0
  61. package/templates/angular-skills/gemini/angular-component-from/SKILL.md +27 -0
  62. package/templates/angular-skills/gemini/angular-design-match/SKILL.md +21 -0
  63. package/templates/angular-skills/gemini/angular-extract-component/SKILL.md +22 -0
  64. package/templates/angular-skills/gemini/angular-extract-store/SKILL.md +20 -0
  65. package/templates/angular-skills/gemini/angular-feature/SKILL.md +25 -0
  66. package/templates/angular-skills/gemini/angular-fixtures/SKILL.md +24 -0
  67. package/templates/angular-skills/gemini/angular-form-from-model/SKILL.md +23 -0
  68. package/templates/angular-skills/gemini/angular-generate-tests/SKILL.md +21 -0
  69. package/templates/angular-skills/gemini/angular-modernize/SKILL.md +21 -0
  70. package/templates/angular-skills/gemini/angular-page-from-api/SKILL.md +24 -0
  71. package/templates/angular-skills/gemini/angular-project-discovery/SKILL.md +41 -0
  72. package/templates/angular-skills/gemini/angular-responsive/SKILL.md +22 -0
  73. package/templates/angular-skills/gemini/angular-signalize/SKILL.md +21 -0
  74. package/templates/angular-skills/gemini/angular-split-component/SKILL.md +22 -0
  75. package/templates/angular-skills/gemini/angular-table-from-model/SKILL.md +23 -0
  76. 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 bindings. |
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
- "codex": true,
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 --agents codex,opencode
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
@@ -3,3 +3,4 @@
3
3
  import { runCli } from '../src/cli.js';
4
4
 
5
5
  runCli(process.argv.slice(2));
6
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plankit-cli",
3
- "version": "1.0.1",
3
+ "version": "1.3.0",
4
4
  "description": "PlanKit Command Suite CLI for AI Coding Assistants (Antigravity, OpenCode, Codex, Cursor)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
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 agents = selectedAgents(parsed, config);
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
- if (agents.gemini) {
84
+ for (const binding of agent.bindings) {
82
85
  results.push(writeManagedFile(
83
- safeJoin(io.cwd, '.gemini', 'instructions.md'),
84
- renderTemplate(config, 'gemini-instructions.md', { artifactsDir: config.artifactsDir }, io.cwd),
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
- if (agents.cursor) {
90
- results.push(writeManagedFile(
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 (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
- ));
96
+ if (parsed.flags['global-gemini-skills'] && agent.id === 'gemini') {
97
+ results.push(...installGlobalGeminiSkills(config, options));
103
98
  }
104
99
 
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
- ));
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 (parsed.flags['global-gemini-skills']) {
123
- results.push(...installGlobalGeminiSkills(config, options));
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
- io.stdout('PlanKit initialized.');
127
- printResults(results, io);
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
- 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(
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 selectedAgents(parsed, config) {
431
- if (!parsed.flags.agents) {
432
- return config.agents;
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
- 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
- };
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
- return {
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
- --agents codex,cursor,opencode Select generated agent integrations during init
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
- codex: true,
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 = { ...DEFAULT_CONFIG.agents, ...(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.