flanders 0.0.2 → 0.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/lib/ai/AiRunner.d.ts +24 -0
- package/lib/ai/AiRunner.js +100 -0
- package/lib/ai/AiSession.d.ts +32 -0
- package/lib/ai/AiSession.js +143 -0
- package/lib/ai/ClaudeAdapter.d.ts +12 -0
- package/lib/ai/ClaudeAdapter.js +345 -0
- package/lib/ai/CodexAdapter.d.ts +13 -0
- package/lib/ai/CodexAdapter.js +354 -0
- package/lib/ai/ToolAdapter.d.ts +52 -0
- package/lib/ai/ToolAdapter.js +2 -0
- package/lib/cli.js +56 -35
- package/lib/commands/Implement.d.ts +22 -8
- package/lib/commands/Implement.js +296 -171
- package/lib/commands/Install.d.ts +31 -3
- package/lib/commands/Install.js +649 -49
- package/lib/commands/InstallAvailabilityCheck.d.ts +8 -0
- package/lib/commands/InstallAvailabilityCheck.js +45 -0
- package/lib/commands/InstallModelProbe.d.ts +2 -0
- package/lib/commands/InstallModelProbe.js +74 -0
- package/lib/contexts.d.ts +5 -4
- package/lib/index.d.ts +5 -3
- package/lib/{PlanFile.d.ts → plan/PlanFile.d.ts} +8 -1
- package/lib/{PlanFile.js → plan/PlanFile.js} +44 -5
- package/lib/prompts/prompts.d.ts +48 -0
- package/lib/prompts/prompts.js +328 -0
- package/lib/prompts/skills.d.ts +3 -0
- package/lib/prompts/skills.js +463 -0
- package/lib/{Git.d.ts → system/Git.d.ts} +4 -2
- package/lib/{Git.js → system/Git.js} +63 -3
- package/lib/{ScriptRunner.d.ts → system/ScriptRunner.d.ts} +1 -1
- package/lib/system/ShellScriptContext.d.ts +36 -0
- package/lib/system/ShellScriptContext.js +70 -0
- package/lib/{fsUtils.d.ts → system/fsUtils.d.ts} +1 -1
- package/lib/{wait.d.ts → system/wait.d.ts} +1 -1
- package/lib/ui/BottomBlock.d.ts +9 -1
- package/lib/ui/BottomBlock.js +30 -14
- package/lib/ui/PromptHelper.d.ts +12 -0
- package/lib/ui/PromptHelper.js +32 -0
- package/lib/ui/TerminalSizeSource.d.ts +19 -0
- package/lib/ui/TerminalSizeSource.js +77 -0
- package/lib/ui/formatters.d.ts +14 -0
- package/lib/ui/formatters.js +65 -2
- package/lib/workspace/FlandersConfig.d.ts +23 -0
- package/lib/workspace/FlandersConfig.js +90 -0
- package/lib/workspace/SpecDiscovery.d.ts +12 -0
- package/lib/workspace/SpecDiscovery.js +40 -0
- package/lib/{Workspace.d.ts → workspace/Workspace.d.ts} +10 -2
- package/lib/{Workspace.js → workspace/Workspace.js} +52 -6
- package/package.json +3 -2
- package/lib/Claude.d.ts +0 -129
- package/lib/Claude.js +0 -387
- package/lib/ClaudeSession.d.ts +0 -34
- package/lib/ClaudeSession.js +0 -292
- package/lib/prompts.d.ts +0 -18
- package/lib/prompts.js +0 -221
- package/lib/skills.d.ts +0 -3
- package/lib/skills.js +0 -256
- /package/lib/{ScriptRunner.js → system/ScriptRunner.js} +0 -0
- /package/lib/{fsUtils.js → system/fsUtils.js} +0 -0
- /package/lib/{wait.js → system/wait.js} +0 -0
package/lib/commands/Install.js
CHANGED
|
@@ -1,82 +1,697 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Install = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
exports.stripYamlFrontmatter = stripYamlFrontmatter;
|
|
5
|
+
exports.parseInstallFlags = parseInstallFlags;
|
|
6
|
+
const FlandersConfig_1 = require("../workspace/FlandersConfig");
|
|
7
|
+
const PromptHelper_1 = require("../ui/PromptHelper");
|
|
8
|
+
const fsUtils_1 = require("../system/fsUtils");
|
|
9
|
+
const skills_1 = require("../prompts/skills");
|
|
10
|
+
const InstallAvailabilityCheck_1 = require("./InstallAvailabilityCheck");
|
|
11
|
+
const InstallModelProbe_1 = require("./InstallModelProbe");
|
|
6
12
|
const SKILLS = [
|
|
7
|
-
{ name: "flanders-
|
|
8
|
-
{ name: "flanders-plan",
|
|
9
|
-
{ name: "flanders-
|
|
13
|
+
{ name: "flanders-spec", body: skills_1.specSkillBody },
|
|
14
|
+
{ name: "flanders-plan", body: skills_1.planSkillBody },
|
|
15
|
+
{ name: "flanders-work", body: skills_1.workSkillBody }
|
|
10
16
|
];
|
|
17
|
+
function stripYamlFrontmatter(body) {
|
|
18
|
+
if (!body.startsWith("---\n") && !body.startsWith("---\r\n")) {
|
|
19
|
+
return body;
|
|
20
|
+
}
|
|
21
|
+
const newlineAfterOpener = body.indexOf("\n") + 1;
|
|
22
|
+
const closerIndex = body.indexOf("\n---\n", newlineAfterOpener);
|
|
23
|
+
if (closerIndex === -1) {
|
|
24
|
+
const closerCrlf = body.indexOf("\n---\r\n", newlineAfterOpener);
|
|
25
|
+
if (closerCrlf === -1) {
|
|
26
|
+
return body;
|
|
27
|
+
}
|
|
28
|
+
return body.slice(closerCrlf + "\n---\r\n".length);
|
|
29
|
+
}
|
|
30
|
+
return body.slice(closerIndex + "\n---\n".length);
|
|
31
|
+
}
|
|
32
|
+
async function promptChoice(ask, args) {
|
|
33
|
+
try {
|
|
34
|
+
return await (0, PromptHelper_1.askChoice)(ask, args);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
if (e instanceof Error && e.name === "AbortError") {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function promptText(ask, args) {
|
|
44
|
+
try {
|
|
45
|
+
return await (0, PromptHelper_1.askText)(ask, args);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
if (e instanceof Error && e.name === "AbortError") {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
throw e;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function extractFlagValue(rawArgs, flag) {
|
|
55
|
+
const prefix = flag + "=";
|
|
56
|
+
for (const arg of rawArgs) {
|
|
57
|
+
if (arg.startsWith(prefix)) {
|
|
58
|
+
return arg.slice(prefix.length);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
function validateClosedSet(value, allowed, flagName) {
|
|
64
|
+
if (allowed.includes(value)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return `Invalid value for ${flagName}: "${value}". Allowed values: ${allowed.join(", ")}.\n`;
|
|
68
|
+
}
|
|
69
|
+
const CODEX_EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh"];
|
|
70
|
+
const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
71
|
+
const CLAUDE_MODEL_FAMILIES = [
|
|
72
|
+
{
|
|
73
|
+
family: "Opus",
|
|
74
|
+
entries: [
|
|
75
|
+
{ label: "Latest Opus", value: "opus" },
|
|
76
|
+
{ label: "Latest Opus [1m context]", value: "opus[1m]" },
|
|
77
|
+
{ label: "Opus 4.8", value: "claude-opus-4-8" },
|
|
78
|
+
{ label: "Opus 4.8 [1m context]", value: "claude-opus-4-8[1m]" },
|
|
79
|
+
{ label: "Opus 4.7", value: "claude-opus-4-7" },
|
|
80
|
+
{ label: "Opus 4.7 [1m context]", value: "claude-opus-4-7[1m]" },
|
|
81
|
+
{ label: "Opus 4.6", value: "claude-opus-4-6" },
|
|
82
|
+
{ label: "Opus 4.6 [1m context]", value: "claude-opus-4-6[1m]" }
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
family: "Sonnet",
|
|
87
|
+
entries: [
|
|
88
|
+
{ label: "Latest Sonnet", value: "sonnet" },
|
|
89
|
+
{ label: "Latest Sonnet [1m context]", value: "sonnet[1m]" },
|
|
90
|
+
{ label: "Sonnet 4.6", value: "claude-sonnet-4-6" },
|
|
91
|
+
{ label: "Sonnet 4.6 [1m context]", value: "claude-sonnet-4-6[1m]" },
|
|
92
|
+
{ label: "Sonnet 4.5", value: "claude-sonnet-4-5" },
|
|
93
|
+
{ label: "Sonnet 4.5 [1m context]", value: "claude-sonnet-4-5[1m]" }
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
family: "Haiku",
|
|
98
|
+
entries: [
|
|
99
|
+
{ label: "Latest Haiku", value: "haiku" },
|
|
100
|
+
{ label: "Haiku 4.5", value: "claude-haiku-4-5-20251001" }
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
family: "Fable",
|
|
105
|
+
entries: [
|
|
106
|
+
{ label: "Latest Fable", value: "fable" },
|
|
107
|
+
{ label: "Fable 5", value: "claude-fable-5" }
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
];
|
|
111
|
+
const CLAUDE_CROSS_FAMILY_ALIASES = [
|
|
112
|
+
{ label: "Best (auto-pick)", value: "best" }
|
|
113
|
+
];
|
|
114
|
+
const CLAUDE_BACK_LABEL = "← back";
|
|
115
|
+
const REVIEWER_INDEXED_RE = /^--reviewer-(\d+)-(tool|model|effort)=/;
|
|
116
|
+
function parseInstallFlags(rawArgs) {
|
|
117
|
+
var _a;
|
|
118
|
+
const hasGlobal = rawArgs.includes("--global");
|
|
119
|
+
const hasProject = rawArgs.includes("--project");
|
|
120
|
+
if (hasGlobal && hasProject) {
|
|
121
|
+
return { ok: false, diagnostic: "Conflicting flags: --global and --project cannot be used together.\n" };
|
|
122
|
+
}
|
|
123
|
+
const answers = {};
|
|
124
|
+
if (hasProject)
|
|
125
|
+
answers.scope = "project";
|
|
126
|
+
if (hasGlobal)
|
|
127
|
+
answers.scope = "global";
|
|
128
|
+
const skillsTool = extractFlagValue(rawArgs, "--skills-tool");
|
|
129
|
+
if (skillsTool !== undefined) {
|
|
130
|
+
const error = validateClosedSet(skillsTool, ["claude", "codex", "both"], "--skills-tool");
|
|
131
|
+
if (error)
|
|
132
|
+
return { ok: false, diagnostic: error };
|
|
133
|
+
answers.skillsTool = skillsTool;
|
|
134
|
+
}
|
|
135
|
+
const workerTool = extractFlagValue(rawArgs, "--worker-tool");
|
|
136
|
+
if (workerTool !== undefined) {
|
|
137
|
+
const error = validateClosedSet(workerTool, ["claude", "codex"], "--worker-tool");
|
|
138
|
+
if (error)
|
|
139
|
+
return { ok: false, diagnostic: error };
|
|
140
|
+
answers.workerTool = workerTool;
|
|
141
|
+
}
|
|
142
|
+
const workerModel = extractFlagValue(rawArgs, "--worker-model");
|
|
143
|
+
if (workerModel !== undefined) {
|
|
144
|
+
answers.workerModel = workerModel;
|
|
145
|
+
}
|
|
146
|
+
const workerEffort = extractFlagValue(rawArgs, "--worker-effort");
|
|
147
|
+
if (workerEffort !== undefined) {
|
|
148
|
+
if (workerEffort !== "" && answers.workerTool === "codex") {
|
|
149
|
+
const error = validateClosedSet(workerEffort, CODEX_EFFORT_LEVELS, "--worker-effort");
|
|
150
|
+
if (error)
|
|
151
|
+
return { ok: false, diagnostic: error };
|
|
152
|
+
}
|
|
153
|
+
answers.workerEffort = workerEffort;
|
|
154
|
+
}
|
|
155
|
+
const reviewerIndices = new Map();
|
|
156
|
+
const reviewer1Tool = extractFlagValue(rawArgs, "--reviewer-tool");
|
|
157
|
+
const reviewer1Model = extractFlagValue(rawArgs, "--reviewer-model");
|
|
158
|
+
const reviewer1Effort = extractFlagValue(rawArgs, "--reviewer-effort");
|
|
159
|
+
if (reviewer1Tool !== undefined) {
|
|
160
|
+
const error = validateClosedSet(reviewer1Tool, ["claude", "codex"], "--reviewer-tool");
|
|
161
|
+
if (error)
|
|
162
|
+
return { ok: false, diagnostic: error };
|
|
163
|
+
if (!reviewerIndices.has(1))
|
|
164
|
+
reviewerIndices.set(1, {});
|
|
165
|
+
reviewerIndices.get(1).tool = reviewer1Tool;
|
|
166
|
+
}
|
|
167
|
+
if (reviewer1Model !== undefined) {
|
|
168
|
+
if (!reviewerIndices.has(1))
|
|
169
|
+
reviewerIndices.set(1, {});
|
|
170
|
+
reviewerIndices.get(1).model = reviewer1Model;
|
|
171
|
+
}
|
|
172
|
+
if (reviewer1Effort !== undefined) {
|
|
173
|
+
const tool1 = (_a = reviewerIndices.get(1)) === null || _a === void 0 ? void 0 : _a.tool;
|
|
174
|
+
if (reviewer1Effort !== "" && tool1 === "codex") {
|
|
175
|
+
const error = validateClosedSet(reviewer1Effort, CODEX_EFFORT_LEVELS, "--reviewer-effort");
|
|
176
|
+
if (error)
|
|
177
|
+
return { ok: false, diagnostic: error };
|
|
178
|
+
}
|
|
179
|
+
if (!reviewerIndices.has(1))
|
|
180
|
+
reviewerIndices.set(1, {});
|
|
181
|
+
reviewerIndices.get(1).effort = reviewer1Effort;
|
|
182
|
+
}
|
|
183
|
+
for (const arg of rawArgs) {
|
|
184
|
+
const match = arg.match(REVIEWER_INDEXED_RE);
|
|
185
|
+
if (!match)
|
|
186
|
+
continue;
|
|
187
|
+
const idx = Number(match[1]);
|
|
188
|
+
if (idx < 2) {
|
|
189
|
+
return { ok: false, diagnostic: `Invalid reviewer flag: "${arg}". Reviewer 1 uses --reviewer-tool/-model/-effort; --reviewer-N-* requires N >= 2.\n` };
|
|
190
|
+
}
|
|
191
|
+
const field = match[2];
|
|
192
|
+
const value = arg.slice(arg.indexOf("=") + 1);
|
|
193
|
+
if (!reviewerIndices.has(idx))
|
|
194
|
+
reviewerIndices.set(idx, {});
|
|
195
|
+
const entry = reviewerIndices.get(idx);
|
|
196
|
+
if (field === "tool") {
|
|
197
|
+
const error = validateClosedSet(value, ["claude", "codex"], `--reviewer-${idx}-tool`);
|
|
198
|
+
if (error)
|
|
199
|
+
return { ok: false, diagnostic: error };
|
|
200
|
+
entry.tool = value;
|
|
201
|
+
}
|
|
202
|
+
else if (field === "model") {
|
|
203
|
+
entry.model = value;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
entry.effort = value;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const [idx, entry] of reviewerIndices) {
|
|
210
|
+
if (entry.effort !== undefined && entry.effort !== "" && entry.tool === "codex") {
|
|
211
|
+
const error = validateClosedSet(entry.effort, CODEX_EFFORT_LEVELS, `--reviewer-${idx}-effort`);
|
|
212
|
+
if (error)
|
|
213
|
+
return { ok: false, diagnostic: error };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (reviewerIndices.size > 0) {
|
|
217
|
+
const sortedIndices = [...reviewerIndices.keys()].sort((a, b) => a - b);
|
|
218
|
+
const maxIdx = sortedIndices[sortedIndices.length - 1];
|
|
219
|
+
for (let i = 1; i <= maxIdx; i++) {
|
|
220
|
+
if (!reviewerIndices.has(i)) {
|
|
221
|
+
return { ok: false, diagnostic: `Reviewer flag indices are not contiguous: missing reviewer ${i}. Indexed reviewer flags must form a contiguous run starting at reviewer 1.\n` };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const list = [];
|
|
225
|
+
for (let i = 1; i <= maxIdx; i++) {
|
|
226
|
+
list.push(reviewerIndices.get(i));
|
|
227
|
+
}
|
|
228
|
+
answers.reviewers = list;
|
|
229
|
+
}
|
|
230
|
+
return { ok: true, answers };
|
|
231
|
+
}
|
|
11
232
|
class Install {
|
|
12
233
|
constructor(rawArgs, _options, _contexts) {
|
|
13
234
|
this._disposed = false;
|
|
235
|
+
this._modelProbeCache = new Map();
|
|
14
236
|
this._runPromise = this._run(rawArgs, _options, _contexts);
|
|
15
237
|
this._runPromise.catch(() => { });
|
|
16
238
|
}
|
|
17
239
|
result() {
|
|
18
240
|
return this._runPromise;
|
|
19
241
|
}
|
|
242
|
+
async _resolveCuratedChoice(headerLabel, question, curatedValues, defaultLabel, customLabel, customPlaceholder, contexts) {
|
|
243
|
+
const options = curatedValues.map(v => ({ label: v }));
|
|
244
|
+
options.push({ label: defaultLabel });
|
|
245
|
+
options.push({ label: customLabel });
|
|
246
|
+
const option = await promptChoice(contexts.ask, {
|
|
247
|
+
header: headerLabel,
|
|
248
|
+
question,
|
|
249
|
+
options
|
|
250
|
+
});
|
|
251
|
+
if (!option) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
if (this._disposed) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
if (option.label === defaultLabel) {
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
if (option.label === customLabel) {
|
|
261
|
+
const text = await promptText(contexts.ask, {
|
|
262
|
+
question,
|
|
263
|
+
placeholder: customPlaceholder
|
|
264
|
+
});
|
|
265
|
+
if (text === null) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
if (this._disposed) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return text;
|
|
272
|
+
}
|
|
273
|
+
return option.label;
|
|
274
|
+
}
|
|
275
|
+
async _resolveClaudeModel(roleLabel, headerLabel, contexts) {
|
|
276
|
+
const question = `Which model should ${roleLabel} use?`;
|
|
277
|
+
topLevel: for (;;) {
|
|
278
|
+
const topOptions = [
|
|
279
|
+
...CLAUDE_MODEL_FAMILIES.map(family => ({ label: family.family })),
|
|
280
|
+
...CLAUDE_CROSS_FAMILY_ALIASES.map(alias => ({ label: alias.label })),
|
|
281
|
+
{ label: "default configured model" },
|
|
282
|
+
{ label: "enter a custom value…" }
|
|
283
|
+
];
|
|
284
|
+
const top = await promptChoice(contexts.ask, {
|
|
285
|
+
header: headerLabel,
|
|
286
|
+
question,
|
|
287
|
+
options: topOptions
|
|
288
|
+
});
|
|
289
|
+
if (!top) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
if (this._disposed) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
if (top.label === "default configured model") {
|
|
296
|
+
return "";
|
|
297
|
+
}
|
|
298
|
+
if (top.label === "enter a custom value…") {
|
|
299
|
+
const text = await promptText(contexts.ask, {
|
|
300
|
+
question,
|
|
301
|
+
placeholder: "leave empty for the default configured model"
|
|
302
|
+
});
|
|
303
|
+
if (text === null) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
if (this._disposed) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return text;
|
|
310
|
+
}
|
|
311
|
+
const crossFamily = CLAUDE_CROSS_FAMILY_ALIASES.find(alias => alias.label === top.label);
|
|
312
|
+
if (crossFamily) {
|
|
313
|
+
return crossFamily.value;
|
|
314
|
+
}
|
|
315
|
+
const family = CLAUDE_MODEL_FAMILIES.find(f => f.family === top.label);
|
|
316
|
+
for (;;) {
|
|
317
|
+
const familyOptions = family.entries.map(entry => ({ label: entry.label }));
|
|
318
|
+
familyOptions.push({ label: CLAUDE_BACK_LABEL });
|
|
319
|
+
const choice = await promptChoice(contexts.ask, {
|
|
320
|
+
header: headerLabel,
|
|
321
|
+
question: `Which ${family.family} model should ${roleLabel} use?`,
|
|
322
|
+
options: familyOptions
|
|
323
|
+
});
|
|
324
|
+
if (!choice) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
if (this._disposed) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
if (choice.label === CLAUDE_BACK_LABEL) {
|
|
331
|
+
continue topLevel;
|
|
332
|
+
}
|
|
333
|
+
const entry = family.entries.find(e => e.label === choice.label);
|
|
334
|
+
return entry.value;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async _resolveRoleModel(roleLabel, headerLabel, tool, suppliedModel, contexts) {
|
|
339
|
+
if (suppliedModel !== undefined) {
|
|
340
|
+
return suppliedModel;
|
|
341
|
+
}
|
|
342
|
+
if (this._disposed) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
if (tool === "claude") {
|
|
346
|
+
return await this._resolveClaudeModel(roleLabel, headerLabel, contexts);
|
|
347
|
+
}
|
|
348
|
+
if (!this._modelProbeCache.has(tool)) {
|
|
349
|
+
const models = await (0, InstallModelProbe_1.probeModelList)(contexts.script);
|
|
350
|
+
if (this._disposed) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
this._modelProbeCache.set(tool, models);
|
|
354
|
+
}
|
|
355
|
+
const probeResult = this._modelProbeCache.get(tool);
|
|
356
|
+
if (probeResult && probeResult.length > 0) {
|
|
357
|
+
const options = probeResult.map(m => ({ label: m }));
|
|
358
|
+
options.push({ label: "default configured model" });
|
|
359
|
+
const option = await promptChoice(contexts.ask, {
|
|
360
|
+
header: headerLabel,
|
|
361
|
+
question: `Which model should ${roleLabel} use?`,
|
|
362
|
+
options
|
|
363
|
+
});
|
|
364
|
+
if (!option) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
if (this._disposed) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
return option.label === "default configured model" ? "" : option.label;
|
|
371
|
+
}
|
|
372
|
+
const text = await promptText(contexts.ask, {
|
|
373
|
+
question: `Which model should ${roleLabel} use?`,
|
|
374
|
+
placeholder: "leave empty for the default configured model"
|
|
375
|
+
});
|
|
376
|
+
if (text === null) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
if (this._disposed) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
return text;
|
|
383
|
+
}
|
|
384
|
+
async _resolveRoleEffort(roleLabel, headerLabel, tool, suppliedEffort, contexts) {
|
|
385
|
+
if (suppliedEffort !== undefined) {
|
|
386
|
+
return suppliedEffort;
|
|
387
|
+
}
|
|
388
|
+
if (this._disposed) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
if (tool === "codex") {
|
|
392
|
+
const options = CODEX_EFFORT_LEVELS.map(e => ({ label: e }));
|
|
393
|
+
options.push({ label: "default configured effort" });
|
|
394
|
+
const option = await promptChoice(contexts.ask, {
|
|
395
|
+
header: headerLabel,
|
|
396
|
+
question: `What effort level should ${roleLabel} use?`,
|
|
397
|
+
options
|
|
398
|
+
});
|
|
399
|
+
if (!option) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
if (this._disposed) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return option.label === "default configured effort" ? "" : option.label;
|
|
406
|
+
}
|
|
407
|
+
return await this._resolveCuratedChoice(headerLabel, `What effort level should ${roleLabel} use?`, CLAUDE_EFFORT_LEVELS, "default configured effort", "enter a custom value…", "leave empty for the default configured effort", contexts);
|
|
408
|
+
}
|
|
409
|
+
async _resolveReviewer(idx, supplied, contexts) {
|
|
410
|
+
const ordinal = idx === 1 ? "" : ` ${idx}`;
|
|
411
|
+
const roleLabel = `reviewer${ordinal}`;
|
|
412
|
+
let tool;
|
|
413
|
+
if ((supplied === null || supplied === void 0 ? void 0 : supplied.tool) !== undefined) {
|
|
414
|
+
tool = supplied.tool;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
if (this._disposed) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
const option = await promptChoice(contexts.ask, {
|
|
421
|
+
header: `Reviewer${ordinal} tool`,
|
|
422
|
+
question: `Which AI tool should ${roleLabel} use?`,
|
|
423
|
+
options: [
|
|
424
|
+
{ label: "claude", description: "Use Claude Code" },
|
|
425
|
+
{ label: "codex", description: "Use Codex CLI" }
|
|
426
|
+
]
|
|
427
|
+
});
|
|
428
|
+
if (!option) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
if (this._disposed) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
tool = option.label;
|
|
435
|
+
}
|
|
436
|
+
const model = await this._resolveRoleModel(roleLabel, `Reviewer${ordinal} model`, tool, supplied === null || supplied === void 0 ? void 0 : supplied.model, contexts);
|
|
437
|
+
if (model === null) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const effort = await this._resolveRoleEffort(roleLabel, `Reviewer${ordinal} effort`, tool, supplied === null || supplied === void 0 ? void 0 : supplied.effort, contexts);
|
|
441
|
+
if (effort === null) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
return { tool, model, effort };
|
|
445
|
+
}
|
|
20
446
|
async _run(rawArgs, options, contexts) {
|
|
21
447
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
contexts.output.writeError("Conflicting flags: --global and --project cannot be used together.\n");
|
|
448
|
+
const parsed = parseInstallFlags(rawArgs);
|
|
449
|
+
if (!parsed.ok) {
|
|
450
|
+
contexts.output.writeError(parsed.diagnostic);
|
|
26
451
|
return 1;
|
|
27
452
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
453
|
+
const answers = parsed.answers;
|
|
454
|
+
let skillsTool;
|
|
455
|
+
if (answers.skillsTool !== undefined) {
|
|
456
|
+
skillsTool = answers.skillsTool;
|
|
31
457
|
}
|
|
32
|
-
else
|
|
33
|
-
|
|
458
|
+
else {
|
|
459
|
+
if (this._disposed) {
|
|
460
|
+
return 1;
|
|
461
|
+
}
|
|
462
|
+
const option = await promptChoice(contexts.ask, {
|
|
463
|
+
header: "Skills tool",
|
|
464
|
+
question: "Which AI tool(s) should the skills be installed for?",
|
|
465
|
+
options: [
|
|
466
|
+
{ label: "claude", description: "Install skills for Claude Code" },
|
|
467
|
+
{ label: "codex", description: "Install skills for Codex CLI" },
|
|
468
|
+
{ label: "both", description: "Install skills for both Claude Code and Codex CLI" }
|
|
469
|
+
]
|
|
470
|
+
});
|
|
471
|
+
if (!option) {
|
|
472
|
+
return 1;
|
|
473
|
+
}
|
|
474
|
+
if (this._disposed) {
|
|
475
|
+
return 1;
|
|
476
|
+
}
|
|
477
|
+
skillsTool = option.label;
|
|
478
|
+
}
|
|
479
|
+
let mode;
|
|
480
|
+
if (answers.scope) {
|
|
481
|
+
mode = answers.scope;
|
|
34
482
|
}
|
|
35
483
|
else {
|
|
36
484
|
if (this._disposed) {
|
|
37
485
|
return 1;
|
|
38
486
|
}
|
|
39
|
-
|
|
487
|
+
let projectDescription;
|
|
488
|
+
let globalDescription;
|
|
489
|
+
if (skillsTool === "claude") {
|
|
490
|
+
projectDescription = "Install in .claude/skills/ relative to CWD";
|
|
491
|
+
globalDescription = "Install in ~/.claude/skills/";
|
|
492
|
+
}
|
|
493
|
+
else if (skillsTool === "codex") {
|
|
494
|
+
projectDescription = "Install in .codex/prompts/ relative to CWD";
|
|
495
|
+
globalDescription = "Install in ~/.codex/prompts/";
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
projectDescription = "Install in .claude/skills/ and .codex/prompts/ relative to CWD";
|
|
499
|
+
globalDescription = "Install in ~/.claude/skills/ and ~/.codex/prompts/";
|
|
500
|
+
}
|
|
501
|
+
const option = await promptChoice(contexts.ask, {
|
|
502
|
+
header: "Install destination",
|
|
503
|
+
question: "Where should Flanders skills be installed?",
|
|
504
|
+
options: [
|
|
505
|
+
{ label: "project", description: projectDescription },
|
|
506
|
+
{ label: "global", description: globalDescription }
|
|
507
|
+
]
|
|
508
|
+
});
|
|
509
|
+
if (!option) {
|
|
510
|
+
return 1;
|
|
511
|
+
}
|
|
40
512
|
if (this._disposed) {
|
|
41
513
|
return 1;
|
|
42
514
|
}
|
|
43
|
-
|
|
515
|
+
mode = option.label;
|
|
516
|
+
}
|
|
517
|
+
let workerTool;
|
|
518
|
+
if (answers.workerTool !== undefined) {
|
|
519
|
+
workerTool = answers.workerTool;
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
if (this._disposed) {
|
|
44
523
|
return 1;
|
|
45
524
|
}
|
|
46
|
-
|
|
525
|
+
const option = await promptChoice(contexts.ask, {
|
|
526
|
+
header: "Worker tool",
|
|
527
|
+
question: "Which AI tool should the worker use?",
|
|
528
|
+
options: [
|
|
529
|
+
{ label: "claude", description: "Use Claude Code" },
|
|
530
|
+
{ label: "codex", description: "Use Codex CLI" }
|
|
531
|
+
]
|
|
532
|
+
});
|
|
533
|
+
if (!option) {
|
|
534
|
+
return 1;
|
|
535
|
+
}
|
|
536
|
+
if (this._disposed) {
|
|
537
|
+
return 1;
|
|
538
|
+
}
|
|
539
|
+
workerTool = option.label;
|
|
540
|
+
}
|
|
541
|
+
const workerModel = await this._resolveRoleModel("the worker", "Worker model", workerTool, answers.workerModel, contexts);
|
|
542
|
+
if (workerModel === null) {
|
|
543
|
+
return 1;
|
|
544
|
+
}
|
|
545
|
+
const workerEffort = await this._resolveRoleEffort("the worker", "Worker effort", workerTool, answers.workerEffort, contexts);
|
|
546
|
+
if (workerEffort === null) {
|
|
547
|
+
return 1;
|
|
548
|
+
}
|
|
549
|
+
const suppliedReviewers = answers.reviewers;
|
|
550
|
+
const reviewers = [];
|
|
551
|
+
if (suppliedReviewers && suppliedReviewers.length > 0) {
|
|
552
|
+
for (let i = 0; i < suppliedReviewers.length; i++) {
|
|
553
|
+
const reviewer = await this._resolveReviewer(i + 1, suppliedReviewers[i], contexts);
|
|
554
|
+
if (reviewer === null) {
|
|
555
|
+
return 1;
|
|
556
|
+
}
|
|
557
|
+
reviewers.push(reviewer);
|
|
558
|
+
}
|
|
47
559
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
560
|
+
else {
|
|
561
|
+
let idx = 1;
|
|
562
|
+
for (;;) {
|
|
563
|
+
const reviewer = await this._resolveReviewer(idx, undefined, contexts);
|
|
564
|
+
if (reviewer === null) {
|
|
565
|
+
return 1;
|
|
566
|
+
}
|
|
567
|
+
reviewers.push(reviewer);
|
|
568
|
+
if (this._disposed) {
|
|
569
|
+
return 1;
|
|
570
|
+
}
|
|
571
|
+
const more = await promptChoice(contexts.ask, {
|
|
572
|
+
header: "Configure another reviewer?",
|
|
573
|
+
question: "Configure another reviewer?",
|
|
574
|
+
options: [
|
|
575
|
+
{ label: "no", description: "Stop adding reviewers" },
|
|
576
|
+
{ label: "yes", description: "Configure another reviewer in the ordered list" }
|
|
577
|
+
]
|
|
578
|
+
});
|
|
579
|
+
if (!more) {
|
|
580
|
+
return 1;
|
|
581
|
+
}
|
|
582
|
+
if (this._disposed) {
|
|
583
|
+
return 1;
|
|
584
|
+
}
|
|
585
|
+
if (more.label === "no") {
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
idx++;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const selectedTools = new Set();
|
|
592
|
+
if (skillsTool === "both") {
|
|
593
|
+
selectedTools.add("claude");
|
|
594
|
+
selectedTools.add("codex");
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
selectedTools.add(skillsTool);
|
|
598
|
+
}
|
|
599
|
+
selectedTools.add(workerTool);
|
|
600
|
+
for (const r of reviewers) {
|
|
601
|
+
selectedTools.add(r.tool);
|
|
602
|
+
}
|
|
603
|
+
if (this._disposed) {
|
|
604
|
+
return 1;
|
|
605
|
+
}
|
|
606
|
+
const report = await (0, InstallAvailabilityCheck_1.verifyToolAvailability)(selectedTools, contexts.script);
|
|
607
|
+
if (this._disposed) {
|
|
608
|
+
return 1;
|
|
609
|
+
}
|
|
610
|
+
const missing = report.filter(e => !e.available);
|
|
611
|
+
if (missing.length > 0) {
|
|
612
|
+
for (const entry of missing) {
|
|
613
|
+
contexts.output.writeError(`${entry.reason}\n`);
|
|
614
|
+
}
|
|
615
|
+
return 1;
|
|
616
|
+
}
|
|
617
|
+
const scopeRoot = mode === "global"
|
|
618
|
+
? contexts.platform.homedir()
|
|
619
|
+
: options.projectRoot;
|
|
51
620
|
for (const skill of SKILLS) {
|
|
52
621
|
if (!skill.body) {
|
|
53
622
|
contexts.output.writeError(`Skill "${skill.name}" has no content.\n`);
|
|
54
623
|
return 1;
|
|
55
624
|
}
|
|
56
625
|
}
|
|
626
|
+
const writeClaude = skillsTool === "claude" || skillsTool === "both";
|
|
627
|
+
const writeCodex = skillsTool === "codex" || skillsTool === "both";
|
|
57
628
|
const writtenPaths = [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
629
|
+
if (writeClaude) {
|
|
630
|
+
const claudeSkillsRoot = (0, fsUtils_1.joinPath)(scopeRoot, ".claude/skills");
|
|
631
|
+
for (const skill of SKILLS) {
|
|
632
|
+
if (this._disposed) {
|
|
633
|
+
return 1;
|
|
634
|
+
}
|
|
635
|
+
const skillFolder = (0, fsUtils_1.joinPath)(claudeSkillsRoot, skill.name);
|
|
636
|
+
try {
|
|
637
|
+
await contexts.fs.mkdir(skillFolder, { recursive: true });
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
contexts.output.writeError(`Cannot create destination: ${skillFolder}\n`);
|
|
641
|
+
return 1;
|
|
642
|
+
}
|
|
643
|
+
const filePath = (0, fsUtils_1.joinPath)(skillFolder, "SKILL.md");
|
|
644
|
+
try {
|
|
645
|
+
await contexts.fs.writeFile(filePath, skill.body);
|
|
646
|
+
writtenPaths.push(filePath);
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
contexts.output.writeError(`Cannot write file: ${filePath}\n`);
|
|
650
|
+
return 1;
|
|
651
|
+
}
|
|
61
652
|
}
|
|
62
|
-
|
|
653
|
+
}
|
|
654
|
+
if (writeCodex) {
|
|
655
|
+
const codexPromptsRoot = (0, fsUtils_1.joinPath)(scopeRoot, ".codex/prompts");
|
|
63
656
|
try {
|
|
64
|
-
await contexts.fs.mkdir(
|
|
657
|
+
await contexts.fs.mkdir(codexPromptsRoot, { recursive: true });
|
|
65
658
|
}
|
|
66
659
|
catch {
|
|
67
|
-
contexts.output.writeError(`Cannot create destination: ${
|
|
660
|
+
contexts.output.writeError(`Cannot create destination: ${codexPromptsRoot}\n`);
|
|
68
661
|
return 1;
|
|
69
662
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
663
|
+
for (const skill of SKILLS) {
|
|
664
|
+
if (this._disposed) {
|
|
665
|
+
return 1;
|
|
666
|
+
}
|
|
667
|
+
const filePath = (0, fsUtils_1.joinPath)(codexPromptsRoot, `${skill.name}.md`);
|
|
668
|
+
try {
|
|
669
|
+
await contexts.fs.writeFile(filePath, stripYamlFrontmatter(skill.body));
|
|
670
|
+
writtenPaths.push(filePath);
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
contexts.output.writeError(`Cannot write file: ${filePath}\n`);
|
|
674
|
+
return 1;
|
|
675
|
+
}
|
|
78
676
|
}
|
|
79
677
|
}
|
|
678
|
+
if (this._disposed) {
|
|
679
|
+
return 1;
|
|
680
|
+
}
|
|
681
|
+
const config = {
|
|
682
|
+
worker: { tool: workerTool, model: workerModel, effort: workerEffort },
|
|
683
|
+
reviewers
|
|
684
|
+
};
|
|
685
|
+
const configWrittenPath = await (0, FlandersConfig_1.write)(contexts.fs, {
|
|
686
|
+
scope: mode,
|
|
687
|
+
projectRoot: options.projectRoot,
|
|
688
|
+
homeDir: contexts.platform.homedir(),
|
|
689
|
+
config
|
|
690
|
+
});
|
|
691
|
+
if (this._disposed) {
|
|
692
|
+
return 1;
|
|
693
|
+
}
|
|
694
|
+
writtenPaths.push(configWrittenPath);
|
|
80
695
|
for (const p of writtenPaths) {
|
|
81
696
|
contexts.output.write(`${p}\n`);
|
|
82
697
|
}
|
|
@@ -89,21 +704,6 @@ class Install {
|
|
|
89
704
|
return 1;
|
|
90
705
|
}
|
|
91
706
|
}
|
|
92
|
-
async _promptDestination(contexts) {
|
|
93
|
-
const [answer] = await contexts.ask.askChoices([{
|
|
94
|
-
header: "Install destination",
|
|
95
|
-
question: "Where should Flanders skills be installed?",
|
|
96
|
-
options: [
|
|
97
|
-
{ label: "project", description: "Install in .claude/skills/ relative to CWD" },
|
|
98
|
-
{ label: "global", description: "Install in ~/.claude/skills/" }
|
|
99
|
-
],
|
|
100
|
-
multiSelect: false
|
|
101
|
-
}]);
|
|
102
|
-
if (!answer || answer.picked.length === 0) {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
return answer.picked[0].label;
|
|
106
|
-
}
|
|
107
707
|
async dispose() {
|
|
108
708
|
if (this._disposed) {
|
|
109
709
|
try {
|