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 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,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
@@ -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.2.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,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
- 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
+ if (entry.global && options.dryRun) {
118
+ return { action: 'info', path: bundleName, message: 'would install to user profile (dry run)' };
124
119
  }
125
120
 
126
- io.stdout('PlanKit initialized.');
127
- printResults(results, io);
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
- 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(
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 selectedAgents(parsed, config) {
431
- if (!parsed.flags.agents) {
432
- return config.agents;
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
- 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
- };
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
- return {
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
- --agents codex,cursor,opencode Select generated agent integrations during init
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
- 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)