cli-five 0.2.15 → 0.2.17
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 +95 -21
- package/package.json +1 -1
- package/src/addons/codegraph-command.mjs +47 -0
- package/src/addons/codegraph.mjs +124 -0
- package/src/addons/jev.mjs +98 -0
- package/src/addons/registry.mjs +126 -0
- package/src/cli.mjs +30 -5
- package/src/commands/add.mjs +55 -0
- package/src/commands/init.mjs +105 -41
- package/src/commands/list-addons.mjs +49 -0
- package/src/steps/interview.mjs +59 -0
- package/src/steps/platform.mjs +46 -3
- package/src/steps/scaffold.mjs +11 -46
- package/src/util/merge.mjs +170 -0
- package/src/util/project.mjs +140 -0
- package/templates/AGENTS.md.tmpl +0 -1
- package/templates/opencode/plugin/jev-tier-router/index.js +187 -0
- package/templates/opencode/plugin/jev-tier-router/package.json +9 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import { log } from '../util/log.mjs';
|
|
3
|
+
import { ADDON_NAMES, getAddon, listAddons } from '../addons/registry.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `cli-five add <name>` — dispatch to a registered add-on.
|
|
7
|
+
*
|
|
8
|
+
* This pass ships the dispatcher only. Registered targets without a `run`
|
|
9
|
+
* function respond with a clear stub so the mechanism can be exercised
|
|
10
|
+
* end-to-end without pretending an integration exists.
|
|
11
|
+
*/
|
|
12
|
+
export async function add(args) {
|
|
13
|
+
const name = args._[1];
|
|
14
|
+
|
|
15
|
+
if (!name || name === '--help' || name === '-h' || name === 'help') {
|
|
16
|
+
printAddHelp();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const addon = getAddon(name);
|
|
21
|
+
if (!addon) {
|
|
22
|
+
log.err(`Unknown add-on: ${name}`);
|
|
23
|
+
printAvailable(addon => addon.name);
|
|
24
|
+
process.exitCode = 2;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof addon.run !== 'function') {
|
|
29
|
+
log.warn(`${addon.label} is registered but not implemented yet.`);
|
|
30
|
+
log.info('The add dispatcher works — this target is reserved for a future release.');
|
|
31
|
+
if (addon.note || addon.status) log.dim(addon.note || addon.status);
|
|
32
|
+
log.dim('Nothing was written to your project.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
log.step(`add ${addon.name}`);
|
|
37
|
+
await addon.run({ cwd: args.cwd, args });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printAddHelp() {
|
|
41
|
+
process.stdout.write(`${kleur.bold('cli-five add')} — install an optional add-on\n\n`);
|
|
42
|
+
process.stdout.write(`${kleur.bold('Usage')}\n npx cli-five add <name>\n\n`);
|
|
43
|
+
printAvailable();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function printAvailable() {
|
|
47
|
+
process.stdout.write(`${kleur.bold('Available add-ons')}\n`);
|
|
48
|
+
for (const addon of listAddons()) {
|
|
49
|
+
const addable = typeof addon.run === 'function';
|
|
50
|
+
const statusText = (addable ? 'available' : 'planned').padEnd(10);
|
|
51
|
+
const status = addable ? kleur.green(statusText) : kleur.yellow(statusText);
|
|
52
|
+
process.stdout.write(` ${addon.name.padEnd(12)} ${status} ${kleur.dim(addon.description)}\n`);
|
|
53
|
+
}
|
|
54
|
+
process.stdout.write(`\n${kleur.dim(`Known names: ${ADDON_NAMES.join(', ')}`)}\n`);
|
|
55
|
+
}
|
package/src/commands/init.mjs
CHANGED
|
@@ -5,18 +5,30 @@ 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 } from '../steps/platform.mjs';
|
|
12
|
+
import { choosePlatform, chooseModels, resolveCodegraphDefault } from '../steps/platform.mjs';
|
|
13
13
|
import { isGitRepo, gitInit } from '../util/git.mjs';
|
|
14
14
|
import { platformLabel } from '../util/platforms.mjs';
|
|
15
|
+
import { autoProjectInfo } from '../util/project.mjs';
|
|
15
16
|
|
|
16
17
|
export async function init(args) {
|
|
17
18
|
const cwd = args.cwd;
|
|
18
19
|
log.raw(kleur.bold().magenta('\ncli-five init') + kleur.gray(` ${cwd}`));
|
|
19
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
|
+
|
|
20
32
|
// 1. Detect
|
|
21
33
|
log.step('1/8 Detect workspace');
|
|
22
34
|
const detected = detect(cwd);
|
|
@@ -27,10 +39,17 @@ export async function init(args) {
|
|
|
27
39
|
|
|
28
40
|
// 2. Platform + CodeGraph
|
|
29
41
|
log.step('2/8 Choose platform');
|
|
30
|
-
const
|
|
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
|
+
});
|
|
31
48
|
log.info(`Target: ${kleur.bold(platformLabel(platform))}`);
|
|
32
49
|
if (codegraph) log.info(`CodeGraph: ${kleur.green('enabled')}`);
|
|
33
|
-
else log.info('CodeGraph: disabled');
|
|
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');
|
|
34
53
|
|
|
35
54
|
// 3. git init if needed
|
|
36
55
|
if (!detected.hasGit) {
|
|
@@ -51,43 +70,57 @@ export async function init(args) {
|
|
|
51
70
|
}
|
|
52
71
|
if (!detected.hasAgents && !detected.hasCopilotInstructions) log.dim('No collisions.');
|
|
53
72
|
|
|
54
|
-
// 5.
|
|
55
|
-
|
|
73
|
+
// 5. Project info + model configuration
|
|
74
|
+
args.__platform = platform;
|
|
56
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
|
+
}
|
|
57
91
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
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}`);
|
|
63
96
|
}
|
|
64
|
-
|
|
65
|
-
|
|
97
|
+
|
|
98
|
+
log.step('5/8 Model configuration');
|
|
99
|
+
modelConfig = await chooseModels(platform, args);
|
|
66
100
|
} else {
|
|
67
|
-
|
|
68
|
-
|
|
101
|
+
log.step('4/8 Project info');
|
|
102
|
+
docHints = autoProjectInfo(cwd);
|
|
103
|
+
logAutoProjectInfo(docHints);
|
|
69
104
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
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 });
|
|
74
108
|
}
|
|
75
109
|
|
|
76
|
-
// 6. Model configuration
|
|
77
|
-
log.step('5/8 Model configuration');
|
|
78
|
-
const modelConfig = await chooseModels(platform, args);
|
|
79
110
|
log.info(`Provider: ${kleur.bold(modelConfig.provider)}`);
|
|
80
111
|
if (modelConfig.customized) log.info('Models: customized');
|
|
81
112
|
else log.info('Models: defaults');
|
|
82
113
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
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);
|
|
86
118
|
|
|
87
|
-
// CLI
|
|
119
|
+
// CLI overrides — apply to both paths.
|
|
88
120
|
if (args.costMode && ['premium', 'cheap', 'mixed'].includes(args.costMode)) {
|
|
89
121
|
answers.costMode = args.costMode;
|
|
90
122
|
}
|
|
123
|
+
if (args.persona !== null) answers.snark = Boolean(args.persona);
|
|
91
124
|
|
|
92
125
|
// Attach platform/model choices to answers so scaffold can use them.
|
|
93
126
|
answers.platform = platform;
|
|
@@ -101,30 +134,56 @@ export async function init(args) {
|
|
|
101
134
|
}
|
|
102
135
|
if (answers.frameworks.length) log.info(`Stack: ${answers.stack.join(', ')} + ${answers.frameworks.join(', ')}`);
|
|
103
136
|
|
|
104
|
-
//
|
|
137
|
+
// 7. Scaffold
|
|
105
138
|
log.step('6/8 Scaffold');
|
|
106
139
|
const written = scaffold({ cwd, answers, args });
|
|
107
140
|
if (args.dryRun) log.warn('--dry-run: no files written. Plan:');
|
|
108
141
|
log.raw(summarize(written, cwd));
|
|
109
142
|
if (!args.dryRun) log.ok(`Wrote ${written.length} files.`);
|
|
110
143
|
|
|
111
|
-
//
|
|
144
|
+
// 8. Skill discovery
|
|
112
145
|
log.step('7/8 Skill discovery');
|
|
113
|
-
|
|
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
|
+
}
|
|
114
151
|
|
|
115
|
-
//
|
|
152
|
+
// 9. Custom instructions
|
|
116
153
|
log.step('8/8 Custom instructions');
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
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' : ''}.`);
|
|
122
162
|
}
|
|
123
|
-
|
|
163
|
+
} else {
|
|
164
|
+
log.dim('Skipped (minimal init). Enable with --instructions or --full-interview.');
|
|
124
165
|
}
|
|
125
166
|
|
|
126
|
-
//
|
|
127
|
-
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
|
+
}
|
|
128
187
|
}
|
|
129
188
|
|
|
130
189
|
async function ask(message, initial = false) {
|
|
@@ -132,7 +191,7 @@ async function ask(message, initial = false) {
|
|
|
132
191
|
return Boolean(v);
|
|
133
192
|
}
|
|
134
193
|
|
|
135
|
-
function printNextSteps(answers) {
|
|
194
|
+
function printNextSteps(answers, { generatedInstructions = false } = {}) {
|
|
136
195
|
const hasDocs = answers.docFiles?.length > 0;
|
|
137
196
|
const platform = answers.platform || 'copilot';
|
|
138
197
|
const codegraph = answers.codegraph;
|
|
@@ -165,8 +224,10 @@ function printNextSteps(answers) {
|
|
|
165
224
|
} else {
|
|
166
225
|
log.raw(kleur.gray(` read PROJECT.md and implement Phase 1.`));
|
|
167
226
|
}
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
}
|
|
170
231
|
|
|
171
232
|
if (codegraph) {
|
|
172
233
|
log.raw('');
|
|
@@ -183,6 +244,9 @@ function printNextSteps(answers) {
|
|
|
183
244
|
log.raw('');
|
|
184
245
|
log.raw(kleur.dim('Edit agent models anytime by changing `model:` in .opencode/agents/*.md.'));
|
|
185
246
|
}
|
|
247
|
+
|
|
248
|
+
log.raw('');
|
|
249
|
+
log.raw(kleur.dim('Optional integrations: npx cli-five list-addons'));
|
|
186
250
|
log.raw('');
|
|
187
251
|
}
|
|
188
252
|
|
|
@@ -0,0 +1,49 @@
|
|
|
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. Status text is
|
|
9
|
+
* deliberately honest: an add-on that ships partial capability (e.g. jev's
|
|
10
|
+
* tier-routing without the parked test-gate) must not read as fully installed.
|
|
11
|
+
*/
|
|
12
|
+
export function listAddonsCommand(args) {
|
|
13
|
+
const cwd = args.cwd;
|
|
14
|
+
log.raw(kleur.bold().magenta('\ncli-five list-addons') + kleur.gray(` ${cwd}`));
|
|
15
|
+
log.raw('');
|
|
16
|
+
|
|
17
|
+
log.raw(` ${kleur.gray(pad('ADD-ON', 12))} ${kleur.gray(pad('STATUS', 14))} ${kleur.gray(pad('ADD', 10))} ${kleur.gray('DETAIL')}`);
|
|
18
|
+
log.raw(` ${'─'.repeat(12)} ${'─'.repeat(14)} ${'─'.repeat(10)} ${'─'.repeat(30)}`);
|
|
19
|
+
|
|
20
|
+
for (const addon of listAddons()) {
|
|
21
|
+
const signals = detectAddon(addon, cwd);
|
|
22
|
+
const installed = signals.length > 0;
|
|
23
|
+
const addable = typeof addon.run === 'function';
|
|
24
|
+
|
|
25
|
+
// An installed add-on with a `capability` note is partial — say so.
|
|
26
|
+
const statusLabel = installed
|
|
27
|
+
? (addon.capability ? `installed (${addon.capability})` : 'installed')
|
|
28
|
+
: 'not found';
|
|
29
|
+
|
|
30
|
+
const detail = installed
|
|
31
|
+
? [addon.status || signals.join(', ')].filter(Boolean).join(' — ')
|
|
32
|
+
: addon.note || addon.status || '';
|
|
33
|
+
|
|
34
|
+
const statusText = pad(statusLabel, 14);
|
|
35
|
+
const status = installed ? kleur.green(statusText) : kleur.gray(statusText);
|
|
36
|
+
const addableText = pad(addable ? 'available' : 'planned', 10);
|
|
37
|
+
const addableColored = addable ? kleur.green(addableText) : kleur.yellow(addableText);
|
|
38
|
+
|
|
39
|
+
log.raw(` ${pad(addon.name, 12)} ${status} ${addableColored} ${kleur.dim(detail)}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
log.raw('');
|
|
43
|
+
log.dim('Install with `npx cli-five add <name>`.');
|
|
44
|
+
log.raw('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function pad(value, width) {
|
|
48
|
+
return String(value).padEnd(width);
|
|
49
|
+
}
|
package/src/steps/interview.mjs
CHANGED
|
@@ -195,6 +195,65 @@ export async function interview(detected, args, docHints = {}) {
|
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
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
|
+
|
|
198
257
|
/** Default stack is first preset when nothing is detected and --yes is used. */
|
|
199
258
|
function defaults(detected, docHints = {}, platform = 'copilot') {
|
|
200
259
|
const hasDetected = detected.stacks.length > 0;
|
package/src/steps/platform.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
2
|
import prompts from 'prompts';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
3
5
|
import {
|
|
4
6
|
PLATFORM_COPILOT,
|
|
5
7
|
PLATFORM_OPENCODE,
|
|
@@ -21,20 +23,50 @@ import {
|
|
|
21
23
|
|
|
22
24
|
const CUSTOM_SENTINEL = '__custom__';
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Resolve whether CodeGraph should be enabled when no explicit `--codegraph`
|
|
28
|
+
* / `--no-codegraph` value was passed.
|
|
29
|
+
*
|
|
30
|
+
* The registration/opt-out mechanism is unchanged — this only decides the
|
|
31
|
+
* default: on for the full interview, off for minimal init.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveCodegraphDefault(args, fullInterview) {
|
|
34
|
+
if (args.codegraph === true || args.codegraph === false) return args.codegraph;
|
|
35
|
+
return Boolean(fullInterview);
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
/**
|
|
25
39
|
* Ask the user to choose a target platform.
|
|
26
40
|
* If args.target is a valid platform, skip the prompt.
|
|
41
|
+
*
|
|
42
|
+
* Options:
|
|
43
|
+
* autoDetect — infer the platform from an existing scaffold before prompting
|
|
44
|
+
* askCodegraph — whether to ask the CodeGraph opt-out question (full interview)
|
|
45
|
+
* codegraphDefault — resolved default when no explicit flag was passed
|
|
46
|
+
*
|
|
47
|
+
* Note: CodeGraph registration and the `--no-codegraph` opt-out are unchanged;
|
|
48
|
+
* this only controls whether the question is asked / what the default is.
|
|
27
49
|
*/
|
|
28
|
-
export async function choosePlatform(args) {
|
|
50
|
+
export async function choosePlatform(args, { autoDetect = false, askCodegraph = true, codegraphDefault } = {}) {
|
|
51
|
+
if (codegraphDefault === undefined) codegraphDefault = args.codegraph !== false;
|
|
52
|
+
|
|
29
53
|
if (args.target) {
|
|
30
54
|
const t = String(args.target).toLowerCase();
|
|
31
55
|
if (PLATFORMS.includes(t)) {
|
|
32
|
-
return { platform: t, codegraph:
|
|
56
|
+
return { platform: t, codegraph: codegraphDefault };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (autoDetect) {
|
|
61
|
+
const existing = detectExistingPlatform(args.cwd);
|
|
62
|
+
if (existing) {
|
|
63
|
+
log.dim(`Detected existing ${platformLabel(existing)} scaffold.`);
|
|
64
|
+
return { platform: existing, codegraph: codegraphDefault };
|
|
33
65
|
}
|
|
34
66
|
}
|
|
35
67
|
|
|
36
68
|
if (args.yes) {
|
|
37
|
-
return { platform: PLATFORM_COPILOT, codegraph:
|
|
69
|
+
return { platform: PLATFORM_COPILOT, codegraph: codegraphDefault };
|
|
38
70
|
}
|
|
39
71
|
|
|
40
72
|
const { platform } = await prompts({
|
|
@@ -60,6 +92,10 @@ export async function choosePlatform(args) {
|
|
|
60
92
|
throw new Error('Platform selection cancelled. Nothing was written.');
|
|
61
93
|
}
|
|
62
94
|
|
|
95
|
+
if (!askCodegraph) {
|
|
96
|
+
return { platform, codegraph: codegraphDefault };
|
|
97
|
+
}
|
|
98
|
+
|
|
63
99
|
const { codegraph } = await prompts({
|
|
64
100
|
type: 'confirm',
|
|
65
101
|
name: 'codegraph',
|
|
@@ -70,6 +106,13 @@ export async function choosePlatform(args) {
|
|
|
70
106
|
return { platform, codegraph: codegraph !== false };
|
|
71
107
|
}
|
|
72
108
|
|
|
109
|
+
/** Infer an existing scaffold's platform, or null when there is no scaffold. */
|
|
110
|
+
export function detectExistingPlatform(cwd) {
|
|
111
|
+
if (existsSync(join(cwd, '.opencode', 'agents'))) return PLATFORM_OPENCODE;
|
|
112
|
+
if (existsSync(join(cwd, '.github', 'agents', 'orchestrator.agent.md'))) return PLATFORM_COPILOT;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
73
116
|
/**
|
|
74
117
|
* Ask the user whether to customize models, pick a provider, and optionally
|
|
75
118
|
* override per-agent models.
|
package/src/steps/scaffold.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { log } from '../util/log.mjs';
|
|
|
3
3
|
import { readTemplate, render, writeFile, listFilesRecursive, relTo, templatePath } from '../util/fs.mjs';
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import { PLATFORM_COPILOT, PLATFORM_OPENCODE, agentDirFor, agentFileFor } from '../util/platforms.mjs';
|
|
6
|
+
import { addCodegraphTo } from '../addons/codegraph.mjs';
|
|
6
7
|
|
|
7
8
|
const AGENT_NAMES = ['orchestrator', 'planner', 'coder', 'designer', 'reviewer'];
|
|
8
9
|
const HISTORY_FILES = ['orchestrator.md', 'planner.md', 'coder.md', 'designer.md', 'reviewer.md'];
|
|
@@ -44,6 +45,9 @@ export function scaffold({ cwd, answers, args }) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
// Shared memory primitives
|
|
48
|
+
// NOTE: AGENTS.md is written WITHOUT the CodeGraph block here. When CodeGraph
|
|
49
|
+
// is enabled, the block is merged in afterward by addCodegraphTo() — the same
|
|
50
|
+
// code path `add codegraph` uses (one implementation, two entry points).
|
|
47
51
|
for (const tmpl of [
|
|
48
52
|
'AGENTS.md.tmpl',
|
|
49
53
|
'PROJECT.md.tmpl',
|
|
@@ -61,6 +65,13 @@ export function scaffold({ cwd, answers, args }) {
|
|
|
61
65
|
written.push(writeFile(join(cwd, 'histories', file), readTemplate('histories', file), args));
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
// CodeGraph — delegated to the shared add codegraph implementation.
|
|
69
|
+
if (answers.codegraph) {
|
|
70
|
+
for (const t of addCodegraphTo({ cwd, platform, dryRun: args.dryRun })) {
|
|
71
|
+
written.push({ path: t.path, written: !args.dryRun && t.action !== 'unchanged' });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
return written;
|
|
65
76
|
}
|
|
66
77
|
|
|
@@ -93,11 +104,6 @@ function scaffoldCopilot({ cwd, answers, args, vars }) {
|
|
|
93
104
|
writeFile(join(cwd, '.github', 'skills', 'README.md'), readTemplate('.github', 'skills', 'README.md'), args),
|
|
94
105
|
);
|
|
95
106
|
|
|
96
|
-
// CodeGraph MCP for VS Code Copilot
|
|
97
|
-
if (answers.codegraph) {
|
|
98
|
-
written.push(writeFile(join(cwd, '.vscode', 'mcp.json'), JSON.stringify(codegraphMcpJson(), null, 2), args));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
107
|
return written;
|
|
102
108
|
}
|
|
103
109
|
|
|
@@ -144,34 +150,10 @@ function buildOpencodeConfig({ answers, orchestratorModel }) {
|
|
|
144
150
|
subagent_depth: 2,
|
|
145
151
|
};
|
|
146
152
|
|
|
147
|
-
if (answers.codegraph) {
|
|
148
|
-
config.mcp = {
|
|
149
|
-
codegraph: {
|
|
150
|
-
type: 'local',
|
|
151
|
-
command: ['codegraph', 'serve', '--mcp'],
|
|
152
|
-
enabled: true,
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
153
|
return config;
|
|
158
154
|
}
|
|
159
155
|
|
|
160
|
-
function codegraphMcpJson() {
|
|
161
|
-
return {
|
|
162
|
-
inputs: [],
|
|
163
|
-
servers: {
|
|
164
|
-
codegraph: {
|
|
165
|
-
command: 'codegraph',
|
|
166
|
-
args: ['serve', '--mcp'],
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
156
|
function buildVars(a) {
|
|
173
|
-
const codegraphBlock = a.codegraph ? CODEGRAPH_BLOCK : '';
|
|
174
|
-
|
|
175
157
|
return {
|
|
176
158
|
PROJECT_NAME: a.projectName,
|
|
177
159
|
ONE_LINER: a.oneLiner || 'TODO — write a one-line vision statement.',
|
|
@@ -190,7 +172,6 @@ ${a.docs}
|
|
|
190
172
|
` : '',
|
|
191
173
|
DATE: new Date().toISOString().slice(0, 10),
|
|
192
174
|
PERSONA_BLOCK: a.snark ? PERSONA_BLOCK : '',
|
|
193
|
-
CODEGRAPH_BLOCK: codegraphBlock,
|
|
194
175
|
};
|
|
195
176
|
}
|
|
196
177
|
|
|
@@ -221,22 +202,6 @@ const PERSONA_BLOCK = `# Persona
|
|
|
221
202
|
|
|
222
203
|
`;
|
|
223
204
|
|
|
224
|
-
const CODEGRAPH_BLOCK = `
|
|
225
|
-
<!-- CODEGRAPH_START -->
|
|
226
|
-
## CodeGraph
|
|
227
|
-
|
|
228
|
-
This project is configured to use [CodeGraph](https://codegraph.ru) for graph-backed codebase context.
|
|
229
|
-
When you need to understand relationships, call paths, or impacts, use:
|
|
230
|
-
|
|
231
|
-
\`\`\`
|
|
232
|
-
codegraph explore "<your question>"
|
|
233
|
-
\`\`\`
|
|
234
|
-
|
|
235
|
-
The CodeGraph MCP server is registered in the project config. Run \`codegraph init\` in this directory
|
|
236
|
-
if the project has not been indexed yet.
|
|
237
|
-
<!-- CODEGRAPH_END -->
|
|
238
|
-
`;
|
|
239
|
-
|
|
240
205
|
export function summarize(written, cwd) {
|
|
241
206
|
const lines = [];
|
|
242
207
|
for (const w of written) {
|