flanders 0.1.0 → 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.
Files changed (60) hide show
  1. package/lib/ai/AiRunner.d.ts +24 -0
  2. package/lib/ai/AiRunner.js +100 -0
  3. package/lib/ai/AiSession.d.ts +32 -0
  4. package/lib/ai/AiSession.js +143 -0
  5. package/lib/ai/ClaudeAdapter.d.ts +12 -0
  6. package/lib/ai/ClaudeAdapter.js +345 -0
  7. package/lib/ai/CodexAdapter.d.ts +13 -0
  8. package/lib/ai/CodexAdapter.js +354 -0
  9. package/lib/ai/ToolAdapter.d.ts +52 -0
  10. package/lib/ai/ToolAdapter.js +2 -0
  11. package/lib/cli.js +56 -35
  12. package/lib/commands/Implement.d.ts +22 -8
  13. package/lib/commands/Implement.js +296 -171
  14. package/lib/commands/Install.d.ts +31 -3
  15. package/lib/commands/Install.js +649 -49
  16. package/lib/commands/InstallAvailabilityCheck.d.ts +8 -0
  17. package/lib/commands/InstallAvailabilityCheck.js +45 -0
  18. package/lib/commands/InstallModelProbe.d.ts +2 -0
  19. package/lib/commands/InstallModelProbe.js +74 -0
  20. package/lib/contexts.d.ts +5 -4
  21. package/lib/index.d.ts +5 -3
  22. package/lib/{PlanFile.d.ts → plan/PlanFile.d.ts} +8 -1
  23. package/lib/{PlanFile.js → plan/PlanFile.js} +44 -5
  24. package/lib/prompts/prompts.d.ts +48 -0
  25. package/lib/prompts/prompts.js +328 -0
  26. package/lib/prompts/skills.d.ts +3 -0
  27. package/lib/prompts/skills.js +463 -0
  28. package/lib/{Git.d.ts → system/Git.d.ts} +4 -2
  29. package/lib/{Git.js → system/Git.js} +63 -3
  30. package/lib/{ScriptRunner.d.ts → system/ScriptRunner.d.ts} +1 -1
  31. package/lib/system/ShellScriptContext.d.ts +36 -0
  32. package/lib/system/ShellScriptContext.js +70 -0
  33. package/lib/{fsUtils.d.ts → system/fsUtils.d.ts} +1 -1
  34. package/lib/{wait.d.ts → system/wait.d.ts} +1 -1
  35. package/lib/ui/BottomBlock.d.ts +9 -1
  36. package/lib/ui/BottomBlock.js +30 -14
  37. package/lib/ui/PromptHelper.d.ts +12 -0
  38. package/lib/ui/PromptHelper.js +32 -0
  39. package/lib/ui/TerminalSizeSource.d.ts +19 -0
  40. package/lib/ui/TerminalSizeSource.js +77 -0
  41. package/lib/ui/formatters.d.ts +14 -0
  42. package/lib/ui/formatters.js +65 -2
  43. package/lib/workspace/FlandersConfig.d.ts +23 -0
  44. package/lib/workspace/FlandersConfig.js +90 -0
  45. package/lib/workspace/SpecDiscovery.d.ts +12 -0
  46. package/lib/workspace/SpecDiscovery.js +40 -0
  47. package/lib/{Workspace.d.ts → workspace/Workspace.d.ts} +10 -2
  48. package/lib/{Workspace.js → workspace/Workspace.js} +52 -6
  49. package/package.json +3 -2
  50. package/lib/Claude.d.ts +0 -129
  51. package/lib/Claude.js +0 -387
  52. package/lib/ClaudeSession.d.ts +0 -34
  53. package/lib/ClaudeSession.js +0 -292
  54. package/lib/prompts.d.ts +0 -18
  55. package/lib/prompts.js +0 -221
  56. package/lib/skills.d.ts +0 -3
  57. package/lib/skills.js +0 -267
  58. /package/lib/{ScriptRunner.js → system/ScriptRunner.js} +0 -0
  59. /package/lib/{fsUtils.js → system/fsUtils.js} +0 -0
  60. /package/lib/{wait.js → system/wait.js} +0 -0
@@ -1,82 +1,697 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Install = void 0;
4
- const fsUtils_1 = require("../fsUtils");
5
- const skills_1 = require("../skills");
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-contract", folder: "flanders-contract", body: skills_1.contractSkillBody },
8
- { name: "flanders-plan", folder: "flanders-plan", body: skills_1.planSkillBody },
9
- { name: "flanders-rule", folder: "flanders-rule", body: skills_1.ruleSkillBody }
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 hasGlobal = rawArgs.includes("--global");
23
- const hasProject = rawArgs.includes("--project");
24
- if (hasGlobal && hasProject) {
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
- let mode;
29
- if (hasGlobal) {
30
- mode = "global";
453
+ const answers = parsed.answers;
454
+ let skillsTool;
455
+ if (answers.skillsTool !== undefined) {
456
+ skillsTool = answers.skillsTool;
31
457
  }
32
- else if (hasProject) {
33
- mode = "project";
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
- const picked = await this._promptDestination(contexts);
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
- if (!picked) {
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
- mode = picked;
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
- const skillsRoot = mode === "global"
49
- ? (0, fsUtils_1.joinPath)(contexts.platform.homedir(), ".claude/skills")
50
- : (0, fsUtils_1.joinPath)(options.projectRoot, ".claude/skills");
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
- for (const skill of SKILLS) {
59
- if (this._disposed) {
60
- return 1;
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
- const skillFolder = (0, fsUtils_1.joinPath)(skillsRoot, skill.folder);
653
+ }
654
+ if (writeCodex) {
655
+ const codexPromptsRoot = (0, fsUtils_1.joinPath)(scopeRoot, ".codex/prompts");
63
656
  try {
64
- await contexts.fs.mkdir(skillFolder, { recursive: true });
657
+ await contexts.fs.mkdir(codexPromptsRoot, { recursive: true });
65
658
  }
66
659
  catch {
67
- contexts.output.writeError(`Cannot create destination: ${skillFolder}\n`);
660
+ contexts.output.writeError(`Cannot create destination: ${codexPromptsRoot}\n`);
68
661
  return 1;
69
662
  }
70
- const filePath = (0, fsUtils_1.joinPath)(skillFolder, "SKILL.md");
71
- try {
72
- await contexts.fs.writeFile(filePath, skill.body);
73
- writtenPaths.push(filePath);
74
- }
75
- catch {
76
- contexts.output.writeError(`Cannot write file: ${filePath}\n`);
77
- return 1;
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 {