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.
- package/README.md +144 -32
- package/package.json +14 -3
- package/plugin-agents/designer.agent.md +2 -2
- package/plugin-agents/planner.agent.md +1 -1
- package/src/addons/registry.mjs +96 -0
- package/src/cli.mjs +47 -4
- package/src/commands/add.mjs +55 -0
- package/src/commands/doctor.mjs +82 -15
- package/src/commands/init.mjs +164 -51
- package/src/commands/list-addons.mjs +40 -0
- package/src/steps/confirm.mjs +2 -0
- package/src/steps/detect.mjs +4 -0
- package/src/steps/interview.mjs +99 -34
- package/src/steps/platform.mjs +239 -0
- package/src/steps/scaffold.mjs +135 -34
- package/src/util/agents.mjs +63 -11
- package/src/util/merge.mjs +170 -0
- package/src/util/models.mjs +148 -0
- package/src/util/platforms.mjs +25 -0
- package/src/util/project.mjs +140 -0
- package/templates/AGENTS.md.tmpl +2 -0
- package/templates/opencode/agents/coder.md +74 -0
- package/templates/opencode/agents/designer.md +65 -0
- package/templates/opencode/agents/orchestrator.md +92 -0
- package/templates/opencode/agents/planner.md +76 -0
- package/templates/opencode/agents/reviewer.md +94 -0
package/src/commands/doctor.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
package/src/commands/init.mjs
CHANGED
|
@@ -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/
|
|
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.
|
|
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
|
-
//
|
|
37
|
-
log.step('
|
|
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
|
-
//
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
97
|
+
|
|
98
|
+
log.step('5/8 Model configuration');
|
|
99
|
+
modelConfig = await chooseModels(platform, args);
|
|
57
100
|
} else {
|
|
58
|
-
|
|
59
|
-
|
|
101
|
+
log.step('4/8 Project info');
|
|
102
|
+
docHints = autoProjectInfo(cwd);
|
|
103
|
+
logAutoProjectInfo(docHints);
|
|
60
104
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
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
|
-
//
|
|
81
|
-
log.step('
|
|
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
|
-
//
|
|
88
|
-
log.step('
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
163
|
+
} else {
|
|
164
|
+
log.dim('Skipped (minimal init). Enable with --instructions or --full-interview.');
|
|
100
165
|
}
|
|
101
166
|
|
|
102
|
-
//
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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('
|
|
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
|
+
}
|
package/src/steps/confirm.mjs
CHANGED
|
@@ -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
|
}
|
package/src/steps/detect.mjs
CHANGED
|
@@ -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
|
};
|
package/src/steps/interview.mjs
CHANGED
|
@@ -69,7 +69,8 @@ const COST_MODES = [
|
|
|
69
69
|
];
|
|
70
70
|
|
|
71
71
|
export async function interview(detected, args, docHints = {}) {
|
|
72
|
-
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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,
|