cli-five 0.2.13 → 0.2.16

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.
@@ -2,9 +2,15 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import kleur from 'kleur';
4
4
  import { log } from '../util/log.mjs';
5
- import { AGENT_FILES, validateAgentSource } from '../util/agents.mjs';
5
+ import {
6
+ AGENT_FILES,
7
+ OPENCODE_AGENT_FILES,
8
+ validateAgentSource,
9
+ validateOpenCodeAgentSource,
10
+ } from '../util/agents.mjs';
11
+ import { PLATFORM_COPILOT, PLATFORM_OPENCODE } from '../util/platforms.mjs';
6
12
 
7
- const REQUIRED = [
13
+ const COPILOT_REQUIRED = [
8
14
  '.github/copilot-instructions.md',
9
15
  '.github/agents/orchestrator.agent.md',
10
16
  '.github/agents/planner.agent.md',
@@ -17,6 +23,19 @@ const REQUIRED = [
17
23
  'agent-diary.md',
18
24
  ];
19
25
 
26
+ const OPENCODE_REQUIRED = [
27
+ 'opencode.json',
28
+ '.opencode/agents/orchestrator.md',
29
+ '.opencode/agents/planner.md',
30
+ '.opencode/agents/coder.md',
31
+ '.opencode/agents/designer.md',
32
+ '.opencode/agents/reviewer.md',
33
+ 'PROJECT.md',
34
+ 'STATE.md',
35
+ 'decisions.md',
36
+ 'agent-diary.md',
37
+ ];
38
+
20
39
  const OPTIONAL = [
21
40
  '.github/instructions',
22
41
  '.github/skills',
@@ -26,11 +45,15 @@ const OPTIONAL = [
26
45
 
27
46
  export async function doctor(args) {
28
47
  const cwd = args.cwd;
48
+ const platform = detectPlatform(cwd);
29
49
  log.raw(kleur.bold().magenta('\ncli-five doctor') + kleur.gray(` ${cwd}`));
50
+ log.info(`Detected platform: ${kleur.bold(platform)}`);
30
51
  let fail = 0;
31
52
 
53
+ const required = platform === PLATFORM_OPENCODE ? OPENCODE_REQUIRED : COPILOT_REQUIRED;
54
+
32
55
  log.step('Required');
33
- for (const p of REQUIRED) {
56
+ for (const p of required) {
34
57
  const ok = existsSync(join(cwd, p));
35
58
  if (ok) log.ok(p);
36
59
  else {
@@ -40,22 +63,60 @@ export async function doctor(args) {
40
63
  }
41
64
 
42
65
  log.step('Agent integrity');
43
- for (const file of AGENT_FILES) {
44
- const relPath = join('.github', 'agents', file);
45
- const fullPath = join(cwd, relPath);
46
- if (!existsSync(fullPath)) continue;
66
+ if (platform === PLATFORM_OPENCODE) {
67
+ for (const file of OPENCODE_AGENT_FILES) {
68
+ const relPath = join('.opencode', 'agents', file);
69
+ const fullPath = join(cwd, relPath);
70
+ if (!existsSync(fullPath)) continue;
47
71
 
48
- const errors = validateAgentSource(readFileSync(fullPath, 'utf8'), file);
49
- if (errors.length === 0) {
50
- log.ok(relPath);
51
- continue;
72
+ const errors = validateOpenCodeAgentSource(readFileSync(fullPath, 'utf8'), file);
73
+ if (errors.length === 0) {
74
+ log.ok(relPath);
75
+ continue;
76
+ }
77
+
78
+ log.err(relPath);
79
+ for (const error of errors) {
80
+ log.raw(kleur.red(` - ${error}`));
81
+ }
82
+ fail++;
52
83
  }
84
+ } else {
85
+ for (const file of AGENT_FILES) {
86
+ const relPath = join('.github', 'agents', file);
87
+ const fullPath = join(cwd, relPath);
88
+ if (!existsSync(fullPath)) continue;
89
+
90
+ const errors = validateAgentSource(readFileSync(fullPath, 'utf8'), file);
91
+ if (errors.length === 0) {
92
+ log.ok(relPath);
93
+ continue;
94
+ }
95
+
96
+ log.err(relPath);
97
+ for (const error of errors) {
98
+ log.raw(kleur.red(` - ${error}`));
99
+ }
100
+ fail++;
101
+ }
102
+ }
53
103
 
54
- log.err(relPath);
55
- for (const error of errors) {
56
- log.raw(kleur.red(` - ${error}`));
104
+ if (platform === PLATFORM_OPENCODE) {
105
+ log.step('OpenCode config');
106
+ const opencodePath = join(cwd, 'opencode.json');
107
+ if (existsSync(opencodePath)) {
108
+ try {
109
+ const cfg = JSON.parse(readFileSync(opencodePath, 'utf8'));
110
+ if (cfg.mcp?.codegraph) {
111
+ log.ok('opencode.json has CodeGraph MCP entry');
112
+ } else {
113
+ log.warn('opencode.json missing CodeGraph MCP entry (ok if CodeGraph disabled)');
114
+ }
115
+ } catch (err) {
116
+ log.err(`opencode.json is not valid JSON: ${err.message}`);
117
+ fail++;
118
+ }
57
119
  }
58
- fail++;
59
120
  }
60
121
 
61
122
  log.step('Optional');
@@ -72,3 +133,9 @@ export async function doctor(args) {
72
133
  process.exit(1);
73
134
  }
74
135
  }
136
+
137
+ function detectPlatform(cwd) {
138
+ if (existsSync(join(cwd, '.opencode', 'agents'))) return PLATFORM_OPENCODE;
139
+ if (existsSync(join(cwd, '.github', 'agents', 'orchestrator.agent.md'))) return PLATFORM_COPILOT;
140
+ return PLATFORM_COPILOT;
141
+ }
@@ -5,25 +5,53 @@ import { resolve, basename, join, extname } from 'node:path';
5
5
  import { log } from '../util/log.mjs';
6
6
  import { detect } from '../steps/detect.mjs';
7
7
  import { confirmOverwriteIfNeeded } from '../steps/confirm.mjs';
8
- import { interview } from '../steps/interview.mjs';
8
+ import { interview, minimalInterview } from '../steps/interview.mjs';
9
9
  import { scaffold, summarize } from '../steps/scaffold.mjs';
10
10
  import { skillDiscovery } from '../steps/skills.mjs';
11
11
  import { instructionGeneration } from '../steps/instructions.mjs';
12
+ import { choosePlatform, chooseModels, resolveCodegraphDefault } from '../steps/platform.mjs';
12
13
  import { isGitRepo, gitInit } from '../util/git.mjs';
14
+ import { platformLabel } from '../util/platforms.mjs';
15
+ import { autoProjectInfo } from '../util/project.mjs';
13
16
 
14
17
  export async function init(args) {
15
18
  const cwd = args.cwd;
16
19
  log.raw(kleur.bold().magenta('\ncli-five init') + kleur.gray(` ${cwd}`));
17
20
 
21
+ // ── Mode ───────────────────────────────────────────────────────────
22
+ // Default init is minimal: the 5 agents + required tooling, asking only for
23
+ // platform and (when needed) name/one-liner. The legacy interview — docs,
24
+ // goals/constraints/persona, model customization, skills, instructions — is
25
+ // opt-in via --full-interview (or --doc). Per-feature flags can also force
26
+ // skills/instructions/persona without the whole interview.
27
+ const docs = Array.isArray(args.docs) ? args.docs : [];
28
+ const fullInterview = Boolean(args.fullInterview) || docs.length > 0;
29
+ const runSkills = args.skills !== null ? Boolean(args.skills) : fullInterview;
30
+ const runInstructions = args.instructions !== null ? Boolean(args.instructions) : fullInterview;
31
+
18
32
  // 1. Detect
19
- log.step('1/6 Detect workspace');
33
+ log.step('1/8 Detect workspace');
20
34
  const detected = detect(cwd);
21
35
  log.info(`Project: ${kleur.bold(detected.projectName)}`);
22
36
  log.info(`Mode: ${detected.isBrownfield ? kleur.yellow('brownfield') : kleur.green('greenfield')}`);
23
37
  if (detected.stacks.length) log.info(`Stack: ${detected.stacks.map((s) => s.label).join(', ')}`);
24
38
  if (!detected.hasGit) log.warn('Not a git repository.');
25
39
 
26
- // 2. git init if needed
40
+ // 2. Platform + CodeGraph
41
+ log.step('2/8 Choose platform');
42
+ const codegraphDefault = resolveCodegraphDefault(args, fullInterview);
43
+ const { platform, codegraph } = await choosePlatform(args, {
44
+ autoDetect: !fullInterview,
45
+ askCodegraph: fullInterview && args.codegraph === null,
46
+ codegraphDefault,
47
+ });
48
+ log.info(`Target: ${kleur.bold(platformLabel(platform))}`);
49
+ if (codegraph) log.info(`CodeGraph: ${kleur.green('enabled')}`);
50
+ else if (fullInterview || args.codegraph === false) log.info('CodeGraph: disabled');
51
+ else log.info(`CodeGraph: ${kleur.gray('disabled')} ${kleur.dim('(enable with --codegraph or --full-interview)')}`);
52
+ if (!fullInterview) log.dim('Minimal init. Full interview: npx cli-five init --full-interview');
53
+
54
+ // 3. git init if needed
27
55
  if (!detected.hasGit) {
28
56
  if (args.yes || (await ask('Run `git init`?', true))) {
29
57
  gitInit(cwd);
@@ -33,8 +61,8 @@ export async function init(args) {
33
61
  }
34
62
  }
35
63
 
36
- // 3. Overwrite gate
37
- log.step('2/6 Confirm overwrites');
64
+ // 4. Overwrite gate
65
+ log.step('3/8 Confirm overwrites');
38
66
  const ok = await confirmOverwriteIfNeeded(detected, args);
39
67
  if (!ok) {
40
68
  log.warn('Aborted. Nothing written.');
@@ -42,65 +70,120 @@ export async function init(args) {
42
70
  }
43
71
  if (!detected.hasAgents && !detected.hasCopilotInstructions) log.dim('No collisions.');
44
72
 
45
- // 4. Input mode — docs or manual interview
46
- log.step('3/6 Project info');
73
+ // 5. Project info + model configuration
74
+ args.__platform = platform;
47
75
  let docHints;
76
+ let modelConfig;
77
+
78
+ if (fullInterview) {
79
+ log.step('4/8 Project info');
80
+ if (docs.length > 0) {
81
+ // --doc was passed on the CLI — validate with retry
82
+ docHints = loadDocs(docs, cwd);
83
+ if (docHints.files.length === 0) {
84
+ log.warn('None of the --doc files could be loaded.');
85
+ }
86
+ } else if (args.yes) {
87
+ docHints = loadDocs([], cwd);
88
+ } else {
89
+ docHints = await collectDocFiles(cwd);
90
+ }
48
91
 
49
- if (args.docs.length > 0) {
50
- // --doc was passed on the CLI — validate with retry
51
- docHints = loadDocs(args.docs, cwd);
52
- if (docHints.files.length === 0) {
53
- log.warn('None of the --doc files could be loaded.');
92
+ if (docHints.files.length > 0 && docs.length > 0) {
93
+ log.info(`Loaded ${docHints.files.length} doc${docHints.files.length > 1 ? 's' : ''}: ${docHints.files.join(', ')}`);
94
+ if (docHints.projectName) log.dim(` → project name: ${docHints.projectName}`);
95
+ if (docHints.oneLiner) log.dim(` → description: ${docHints.oneLiner}`);
54
96
  }
55
- } else if (args.yes) {
56
- docHints = loadDocs([], cwd);
97
+
98
+ log.step('5/8 Model configuration');
99
+ modelConfig = await chooseModels(platform, args);
57
100
  } else {
58
- docHints = await collectDocFiles(cwd);
59
- }
101
+ log.step('4/8 Project info');
102
+ docHints = autoProjectInfo(cwd);
103
+ logAutoProjectInfo(docHints);
60
104
 
61
- if (docHints.files.length > 0 && args.docs.length > 0) {
62
- log.info(`Loaded ${docHints.files.length} doc${docHints.files.length > 1 ? 's' : ''}: ${docHints.files.join(', ')}`);
63
- if (docHints.projectName) log.dim(` → project name: ${docHints.projectName}`);
64
- if (docHints.oneLiner) log.dim(` → description: ${docHints.oneLiner}`);
105
+ log.step('5/8 Model configuration');
106
+ // Minimal path uses provider defaults without prompting (still honours --provider).
107
+ modelConfig = await chooseModels(platform, { ...args, yes: true });
65
108
  }
66
109
 
67
- // 5. Interview (pre-filled from docs if available, otherwise manual)
68
- const answers = await interview(detected, args, docHints);
110
+ log.info(`Provider: ${kleur.bold(modelConfig.provider)}`);
111
+ if (modelConfig.customized) log.info('Models: customized');
112
+ else log.info('Models: defaults');
113
+
114
+ // 6. Interview (minimal by default, full behind --full-interview)
115
+ const answers = fullInterview
116
+ ? await interview(detected, args, docHints)
117
+ : await minimalInterview(detected, args, docHints);
69
118
 
70
- // CLI --cost-mode override
119
+ // CLI overrides — apply to both paths.
71
120
  if (args.costMode && ['premium', 'cheap', 'mixed'].includes(args.costMode)) {
72
121
  answers.costMode = args.costMode;
73
122
  }
123
+ if (args.persona !== null) answers.snark = Boolean(args.persona);
124
+
125
+ // Attach platform/model choices to answers so scaffold can use them.
126
+ answers.platform = platform;
127
+ answers.codegraph = codegraph;
128
+ answers.provider = modelConfig.provider;
129
+ answers.modelMap = modelConfig.modelMap;
130
+ answers.customizedModels = modelConfig.customized;
74
131
 
75
132
  if (answers.presetId && answers.presetId !== 'custom') {
76
133
  log.info(`Preset: ${answers.presetId}`);
77
134
  }
78
135
  if (answers.frameworks.length) log.info(`Stack: ${answers.stack.join(', ')} + ${answers.frameworks.join(', ')}`);
79
136
 
80
- // 5. Scaffold
81
- log.step('4/6 Scaffold');
137
+ // 7. Scaffold
138
+ log.step('6/8 Scaffold');
82
139
  const written = scaffold({ cwd, answers, args });
83
140
  if (args.dryRun) log.warn('--dry-run: no files written. Plan:');
84
141
  log.raw(summarize(written, cwd));
85
142
  if (!args.dryRun) log.ok(`Wrote ${written.length} files.`);
86
143
 
87
- // 6. Skill discovery
88
- log.step('5/6 Skill discovery');
89
- await skillDiscovery({ cwd, answers, args });
90
-
91
- // 7. Custom instructions
92
- log.step('6/6 Custom instructions');
93
- const instrWritten = await instructionGeneration({ cwd, answers, args });
94
- if (instrWritten && instrWritten.length > 0) {
95
- if (args.dryRun) log.warn('--dry-run: instruction plan:');
96
- for (const w of instrWritten) {
97
- log.raw(` ${w.written ? '+' : '~'} ${w.path.replace(cwd + '/', '')}`);
144
+ // 8. Skill discovery
145
+ log.step('7/8 Skill discovery');
146
+ if (runSkills) {
147
+ await skillDiscovery({ cwd, answers, args: { ...args, skills: true } });
148
+ } else {
149
+ log.dim('Skipped (minimal init). Enable with --skills or --full-interview.');
150
+ }
151
+
152
+ // 9. Custom instructions
153
+ log.step('8/8 Custom instructions');
154
+ if (runInstructions) {
155
+ const instrWritten = await instructionGeneration({ cwd, answers, args });
156
+ if (instrWritten && instrWritten.length > 0) {
157
+ if (args.dryRun) log.warn('--dry-run: instruction plan:');
158
+ for (const w of instrWritten) {
159
+ log.raw(` ${w.written ? '+' : '~'} ${w.path.replace(cwd + '/', '')}`);
160
+ }
161
+ if (!args.dryRun) log.ok(`Wrote ${instrWritten.length} instruction file${instrWritten.length > 1 ? 's' : ''}.`);
98
162
  }
99
- if (!args.dryRun) log.ok(`Wrote ${instrWritten.length} instruction file${instrWritten.length > 1 ? 's' : ''}.`);
163
+ } else {
164
+ log.dim('Skipped (minimal init). Enable with --instructions or --full-interview.');
100
165
  }
101
166
 
102
- // 8. Next steps
103
- printNextSteps(answers);
167
+ // 10. Next steps
168
+ printNextSteps(answers, { generatedInstructions: runInstructions });
169
+ }
170
+
171
+ /** Log which project fields were auto-pulled from the workspace. */
172
+ function logAutoProjectInfo(info) {
173
+ const name = info?.name || {};
174
+ const oneLiner = info?.oneLiner || {};
175
+
176
+ if (name.value && !name.ambiguous) {
177
+ log.info(`Name: ${kleur.bold(name.value)} ${kleur.gray(`(${name.sources[0].source})`)}`);
178
+ } else if (name.ambiguous) {
179
+ log.warn(`Multiple project names found (${name.sources.map((s) => s.source).join(', ')}) — asking.`);
180
+ }
181
+
182
+ if (oneLiner.value && !oneLiner.ambiguous) {
183
+ log.info(`Tagline: ${oneLiner.value} ${kleur.gray(`(${oneLiner.sources[0].source})`)}`);
184
+ } else if (oneLiner.ambiguous) {
185
+ log.warn(`Multiple descriptions found (${oneLiner.sources.map((s) => s.source).join(', ')}) — asking.`);
186
+ }
104
187
  }
105
188
 
106
189
  async function ask(message, initial = false) {
@@ -108,17 +191,31 @@ async function ask(message, initial = false) {
108
191
  return Boolean(v);
109
192
  }
110
193
 
111
- function printNextSteps(answers) {
194
+ function printNextSteps(answers, { generatedInstructions = false } = {}) {
112
195
  const hasDocs = answers.docFiles?.length > 0;
196
+ const platform = answers.platform || 'copilot';
197
+ const codegraph = answers.codegraph;
113
198
 
114
199
  log.raw('');
115
200
  log.raw(kleur.bold().green('Done. Next steps:'));
116
201
  log.raw('');
117
- log.raw(` 1. Open this folder in VS Code Insiders.`);
118
- log.raw(` 2. Enable Copilot subagent invocations (settings.json):`);
119
- log.raw(kleur.gray(` "chat.subagents.allowInvocationsFromSubagents": true`));
120
- log.raw(` 3. Open Copilot Chat — select an agent from the dropdown (${kleur.bold('not')} @mention).`);
121
- log.raw(` 4. Select ${kleur.bold('Orchestrator')} — it routes tasks autonomously to Planner, Coder, Designer, and Reviewer.`);
202
+
203
+ if (platform === 'opencode') {
204
+ log.raw(` 1. Install the CodeGraph CLI if you haven't:`);
205
+ log.raw(kleur.gray(` npm i -g @colbymchenry/codegraph`));
206
+ log.raw(` 2. Index this project with CodeGraph:`);
207
+ log.raw(kleur.gray(` codegraph init`));
208
+ log.raw(` 3. Run OpenCode from this directory:`);
209
+ log.raw(kleur.gray(` opencode`));
210
+ log.raw(` 4. Select ${kleur.bold('Orchestrator')} and describe what you want built.`);
211
+ } else {
212
+ log.raw(` 1. Open this folder in VS Code Insiders.`);
213
+ log.raw(` 2. Enable Copilot subagent invocations (settings.json):`);
214
+ log.raw(kleur.gray(` "chat.subagents.allowInvocationsFromSubagents": true`));
215
+ log.raw(` 3. Open Copilot Chat — select an agent from the dropdown (${kleur.bold('not')} @mention).`);
216
+ log.raw(` 4. Select ${kleur.bold('Orchestrator')} — it routes tasks autonomously to Planner, Coder, Designer, and Reviewer.`);
217
+ }
218
+
122
219
  log.raw(` 5. ${hasDocs ? 'Kickoff prompt (paste this into Orchestrator):' : 'Start building:'}`);
123
220
  if (hasDocs) {
124
221
  const docList = answers.docFiles.join(', ');
@@ -127,13 +224,29 @@ function printNextSteps(answers) {
127
224
  } else {
128
225
  log.raw(kleur.gray(` read PROJECT.md and implement Phase 1.`));
129
226
  }
130
- log.raw(` 6. Review generated instruction files in .github/instructions/.`);
131
- log.raw(kleur.gray(` Edit applyTo globs and guidelines to fit your project.`));
132
- log.raw('');
133
- log.raw(kleur.dim(' Quick plugin install (personal, no project config):'));
134
- log.raw(kleur.dim(' copilot plugin install idusortus/cli-five'));
227
+ if (generatedInstructions) {
228
+ log.raw(` 6. Review generated instruction files in .github/instructions/.`);
229
+ log.raw(kleur.gray(` Edit applyTo globs and guidelines to fit your project.`));
230
+ }
231
+
232
+ if (codegraph) {
233
+ log.raw('');
234
+ log.raw(kleur.dim(' CodeGraph is configured. Remember to run `codegraph init` before asking agents to explore the codebase.'));
235
+ }
236
+
237
+ if (platform === 'copilot') {
238
+ log.raw('');
239
+ log.raw(kleur.dim(' Quick plugin install (personal, no project config):'));
240
+ log.raw(kleur.dim(' copilot plugin install idusortus/cli-five'));
241
+ log.raw('');
242
+ log.raw(kleur.dim('Edit cost mode anytime by changing `model:` in .github/agents/*.agent.md.'));
243
+ } else {
244
+ log.raw('');
245
+ log.raw(kleur.dim('Edit agent models anytime by changing `model:` in .opencode/agents/*.md.'));
246
+ }
247
+
135
248
  log.raw('');
136
- log.raw(kleur.dim('Edit cost mode anytime by changing `model:` in .github/agents/*.agent.md.'));
249
+ log.raw(kleur.dim('Optional integrations: npx cli-five list-addons'));
137
250
  log.raw('');
138
251
  }
139
252
 
@@ -0,0 +1,40 @@
1
+ import kleur from 'kleur';
2
+ import { log } from '../util/log.mjs';
3
+ import { detectAddon, listAddons } from '../addons/registry.mjs';
4
+
5
+ /**
6
+ * `cli-five list-addons` — show what is installed vs. available.
7
+ *
8
+ * "Installed" is detected read-only from workspace artifacts. CodeGraph is
9
+ * listed honestly even though its `add` plumbing has not moved yet.
10
+ */
11
+ export function listAddonsCommand(args) {
12
+ const cwd = args.cwd;
13
+ log.raw(kleur.bold().magenta('\ncli-five list-addons') + kleur.gray(` ${cwd}`));
14
+ log.raw('');
15
+
16
+ log.raw(` ${kleur.gray(pad('ADD-ON', 12))} ${kleur.gray(pad('STATUS', 12))} ${kleur.gray(pad('ADD', 10))} ${kleur.gray('DETAIL')}`);
17
+ log.raw(` ${'─'.repeat(12)} ${'─'.repeat(12)} ${'─'.repeat(10)} ${'─'.repeat(30)}`);
18
+
19
+ for (const addon of listAddons()) {
20
+ const signals = detectAddon(addon, cwd);
21
+ const installed = signals.length > 0;
22
+ const addable = typeof addon.run === 'function';
23
+ const detail = installed ? signals.join(', ') : addon.note || '';
24
+
25
+ const statusText = pad(installed ? 'installed' : 'not found', 12);
26
+ const status = installed ? kleur.green(statusText) : kleur.gray(statusText);
27
+ const addableText = pad(addable ? 'available' : 'planned', 10);
28
+ const addableColored = addable ? kleur.green(addableText) : kleur.yellow(addableText);
29
+
30
+ log.raw(` ${pad(addon.name, 12)} ${status} ${addableColored} ${kleur.dim(detail)}`);
31
+ }
32
+
33
+ log.raw('');
34
+ log.dim('Install with `npx cli-five add <name>` once a target is available.');
35
+ log.raw('');
36
+ }
37
+
38
+ function pad(value, width) {
39
+ return String(value).padEnd(width);
40
+ }
@@ -34,5 +34,7 @@ function collideList(detected) {
34
34
  const out = [];
35
35
  if (detected.hasAgents) out.push('.github/agents/');
36
36
  if (detected.hasCopilotInstructions) out.push('.github/copilot-instructions.md');
37
+ if (detected.hasOpencodeAgents) out.push('.opencode/agents/');
38
+ if (detected.hasOpencodeConfig) out.push('opencode.json');
37
39
  return out;
38
40
  }
@@ -29,6 +29,8 @@ export function detect(cwd) {
29
29
  const hasGithub = names.has('.github');
30
30
  const hasAgents = existsSync(join(cwd, '.github', 'agents'));
31
31
  const hasCopilotInstructions = existsSync(join(cwd, '.github', 'copilot-instructions.md'));
32
+ const hasOpencodeAgents = existsSync(join(cwd, '.opencode', 'agents'));
33
+ const hasOpencodeConfig = existsSync(join(cwd, 'opencode.json'));
32
34
  const projectName = guessName(cwd, names);
33
35
 
34
36
  return {
@@ -38,6 +40,8 @@ export function detect(cwd) {
38
40
  hasGithub,
39
41
  hasAgents,
40
42
  hasCopilotInstructions,
43
+ hasOpencodeAgents,
44
+ hasOpencodeConfig,
41
45
  isBrownfield: stacks.length > 0,
42
46
  stacks,
43
47
  };
@@ -69,7 +69,8 @@ const COST_MODES = [
69
69
  ];
70
70
 
71
71
  export async function interview(detected, args, docHints = {}) {
72
- if (args.yes) return defaults(detected, docHints);
72
+ const platform = args.__platform || 'copilot';
73
+ if (args.yes) return defaults(detected, docHints, platform);
73
74
 
74
75
  const onCancel = () => {
75
76
  throw new Error('Interview cancelled. Nothing was written.');
@@ -146,39 +147,44 @@ export async function interview(detected, args, docHints = {}) {
146
147
  }
147
148
 
148
149
  // ── Remaining questions ───────────────────────────────────────────
149
- const rest = await prompts(
150
- [
151
- {
152
- type: 'text',
153
- name: 'goals',
154
- message: `Primary goal of this project (one sentence)${skipHint}`,
155
- initial: docHints.goals || '',
156
- },
157
- {
158
- type: 'text',
159
- name: 'constraints',
160
- message: `Hard constraints (perf, deps, deploy, compliance — one sentence, optional)${skipHint}`,
161
- initial: docHints.constraints || '',
162
- },
163
- {
164
- type: 'select',
165
- name: 'costMode',
166
- message: 'Agent cost mode',
167
- choices: COST_MODES,
168
- initial: 0,
169
- },
170
- {
171
- type: 'confirm',
172
- name: 'snark',
173
- message: 'Include the snarky persona block in copilot-instructions.md?',
174
- initial: true,
175
- },
176
- ],
177
- { onCancel },
178
- );
150
+ const remainingQuestions = [
151
+ {
152
+ type: 'text',
153
+ name: 'goals',
154
+ message: `Primary goal of this project (one sentence)${skipHint}`,
155
+ initial: docHints.goals || '',
156
+ },
157
+ {
158
+ type: 'text',
159
+ name: 'constraints',
160
+ message: `Hard constraints (perf, deps, deploy, compliance — one sentence, optional)${skipHint}`,
161
+ initial: docHints.constraints || '',
162
+ },
163
+ ];
164
+
165
+ if (platform === 'copilot') {
166
+ remainingQuestions.push({
167
+ type: 'select',
168
+ name: 'costMode',
169
+ message: 'Agent cost mode',
170
+ choices: COST_MODES,
171
+ initial: 0,
172
+ });
173
+ }
174
+
175
+ remainingQuestions.push({
176
+ type: 'confirm',
177
+ name: 'snark',
178
+ message: platform === 'copilot'
179
+ ? 'Include the snarky persona block in copilot-instructions.md?'
180
+ : 'Include the snarky persona block in AGENTS.md?',
181
+ initial: true,
182
+ });
183
+
184
+ const rest = await prompts(remainingQuestions, { onCancel });
179
185
 
180
186
  return normalize({
181
- ...defaults(detected, docHints),
187
+ ...defaults(detected, docHints, platform),
182
188
  ...basic,
183
189
  ...stackAnswers,
184
190
  ...rest,
@@ -189,8 +195,67 @@ export async function interview(detected, args, docHints = {}) {
189
195
  });
190
196
  }
191
197
 
198
+ /**
199
+ * Minimal interview — the default `init` path.
200
+ *
201
+ * Asks for the project name and one-liner only, and only when `projectInfo`
202
+ * could not confidently supply them. Everything else (stack, goals,
203
+ * constraints, persona, cost mode) falls back to `defaults`.
204
+ *
205
+ * `projectInfo` is the shape returned by `autoProjectInfo(cwd)`.
206
+ */
207
+ export async function minimalInterview(detected, args, projectInfo = {}) {
208
+ const platform = args.__platform || 'copilot';
209
+ const nameInfo = projectInfo.name || {};
210
+ const oneLinerInfo = projectInfo.oneLiner || {};
211
+
212
+ let projectName = nameInfo.value || '';
213
+ let oneLiner = oneLinerInfo.value || '';
214
+
215
+ if (!args.yes) {
216
+ const questions = [];
217
+
218
+ if (!projectName || nameInfo.ambiguous) {
219
+ questions.push({
220
+ type: 'text',
221
+ name: 'projectName',
222
+ message: 'Project name',
223
+ initial: projectName || detected.projectName,
224
+ });
225
+ }
226
+
227
+ if (!oneLiner || oneLinerInfo.ambiguous) {
228
+ questions.push({
229
+ type: 'text',
230
+ name: 'oneLiner',
231
+ message: 'One-line description (becomes PROJECT.md vision)',
232
+ initial: oneLiner || '',
233
+ });
234
+ }
235
+
236
+ if (questions.length > 0) {
237
+ const answers = await prompts(questions, {
238
+ onCancel: () => {
239
+ throw new Error('Interview cancelled. Nothing was written.');
240
+ },
241
+ });
242
+ if (answers.projectName !== undefined) projectName = answers.projectName;
243
+ if (answers.oneLiner !== undefined) oneLiner = answers.oneLiner;
244
+ }
245
+ }
246
+
247
+ const base = defaults(detected, { projectName, oneLiner }, platform);
248
+ return normalize({
249
+ ...base,
250
+ projectName: (projectName || detected.projectName || '').trim(),
251
+ oneLiner: (oneLiner || '').trim(),
252
+ // Persona is opt-in on the minimal path (--persona / --full-interview).
253
+ snark: args.persona === true,
254
+ });
255
+ }
256
+
192
257
  /** Default stack is first preset when nothing is detected and --yes is used. */
193
- function defaults(detected, docHints = {}) {
258
+ function defaults(detected, docHints = {}, platform = 'copilot') {
194
259
  const hasDetected = detected.stacks.length > 0;
195
260
  const fallback = STACK_PRESETS[0];
196
261
  return {
@@ -200,7 +265,7 @@ function defaults(detected, docHints = {}) {
200
265
  frameworks: hasDetected ? [] : fallback.frameworks,
201
266
  goals: docHints.goals || '',
202
267
  constraints: docHints.constraints || '',
203
- costMode: 'premium',
268
+ costMode: platform === 'copilot' ? 'premium' : 'none',
204
269
  snark: true,
205
270
  presetId: hasDetected ? 'custom' : fallback.value,
206
271
  quickstart: hasDetected ? '' : fallback.quickstart,