whale-igniter 1.2.3 → 1.3.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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://cdn.jsdelivr.net/npm/whale-igniter@1.2.1/brand/lockup.svg" alt="Whale Igniter" width="640">
2
+ <img src="https://cdn.jsdelivr.net/npm/whale-igniter@1.3.0/brand/lockup.svg" alt="Whale Igniter" width="640">
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -50,16 +50,33 @@ my-app/
50
50
  `intelligence/*.json` is the source of truth. Markdown files are rendered
51
51
  from it so agents always read current project state.
52
52
 
53
+ ## What's new in v1.3
54
+
55
+ **UI references library** — curated, copy-paste component references ship with the tool. Run `whale references add forms` (or `--all`) to install them into your project. CLAUDE.md and MCP tools automatically expose the files to agents.
56
+
57
+ **Cross-framework drift detection** — `whale insights drift spacing|color|radii` scans CSS, inline styles, Vue/Svelte style blocks and JSX props for values that don't match your foundations. Run `--review` to accept exceptions as refinements or flag them for refactor.
58
+
59
+ **Human-language wizard** — `whale ignite --interactive` now asks plain-English questions (what are you building, which framework, team size) and maps answers to a sensible config automatically.
60
+
61
+ **Template extraction** — wiki templates moved to `src/templates/` as composable render functions. CLAUDE.md is now structured as three clear sections: what the project is, what to read, and how to work here.
62
+
63
+ **`whale changes --since <ref>`** — shows what changed in intelligence stores (decisions, components, refinements) since any git ref, ISO date, or `HEAD~1`.
64
+
53
65
  ## Core Commands
54
66
 
55
67
  | Command | Use it when you want to... |
56
68
  | --- | --- |
57
69
  | `whale ignite my-app` | Create a new Whale workspace |
70
+ | `whale ignite my-app --interactive` | Wizard: project type, stack, team size, packs |
58
71
  | `whale adopt` | Scan an existing React/Tailwind project |
59
72
  | `whale remember` | Regenerate agent context and wiki |
60
73
  | `whale check` | Run validation and project insights |
61
74
  | `whale improve` | Ask Selene for improvement suggestions |
62
75
  | `whale explain` | Generate docs and AI-readable context |
76
+ | `whale references add --all` | Install UI component reference library |
77
+ | `whale insights drift spacing` | Find off-grid spacing across CSS and JSX |
78
+ | `whale insights drift spacing --review` | Interactively accept or queue each drift issue |
79
+ | `whale changes --since HEAD~5` | Show what changed in intelligence stores |
63
80
  | `whale team` | See active operating roles/packs |
64
81
  | `whale create component Hero` | Generate a typed React component |
65
82
  | `whale mcp config --client cursor` | Configure an MCP client |
@@ -126,6 +143,6 @@ Brand assets ship in `brand/` and are included in the npm package.
126
143
  - License: MIT
127
144
 
128
145
  <p align="center">
129
- <img src="https://cdn.jsdelivr.net/npm/whale-igniter@1.2.1/brand/logo.svg" alt="" width="44"><br>
146
+ <img src="https://cdn.jsdelivr.net/npm/whale-igniter@1.3.0/brand/logo.svg" alt="" width="44"><br>
130
147
  <sub><b>Whale Igniter</b> · map the project before the agent moves</sub>
131
148
  </p>
@@ -253,6 +253,145 @@ function analyzeGridDrift(config, observations) {
253
253
  return [];
254
254
  }
255
255
  // ---------------------------------------------------------------------------
256
+ // Cross-framework drift analyzers (Block D)
257
+ // ---------------------------------------------------------------------------
258
+ const SPACING_PROPERTIES = new Set([
259
+ "padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
260
+ "margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
261
+ "gap", "column-gap", "row-gap"
262
+ ]);
263
+ const COLOR_PROPERTIES = new Set(["color", "background-color", "border-color"]);
264
+ const RADIUS_PROPERTIES = new Set([
265
+ "border-radius",
266
+ "border-top-left-radius", "border-top-right-radius",
267
+ "border-bottom-left-radius", "border-bottom-right-radius"
268
+ ]);
269
+ function isRefinedSpacing(refinements) {
270
+ return refinements.some((r) => r.scope?.issueType === "spacing");
271
+ }
272
+ export function analyzeSpacingDrift(observations, config, refinements = []) {
273
+ if (isRefinedSpacing(refinements))
274
+ return [];
275
+ const grid = config.foundations?.grid ?? 8;
276
+ const drifting = observations.filter((o) => SPACING_PROPERTIES.has(o.property) &&
277
+ o.pxValue != null &&
278
+ o.pxValue !== 0 &&
279
+ o.pxValue % grid !== 0);
280
+ if (drifting.length === 0)
281
+ return [];
282
+ // Group by file
283
+ const byFile = new Map();
284
+ for (const obs of drifting) {
285
+ const key = obs.file;
286
+ if (!byFile.has(key))
287
+ byFile.set(key, []);
288
+ byFile.get(key).push(obs);
289
+ }
290
+ const insights = [];
291
+ for (const [file, items] of byFile) {
292
+ const severity = items.length >= 3 ? "warning" : "info";
293
+ const fileShort = file.split("/").slice(-2).join("/");
294
+ insights.push({
295
+ id: `drift.spacing.${Buffer.from(file).toString("base64").slice(0, 12)}`,
296
+ category: "tokens",
297
+ severity,
298
+ title: `${items.length} off-grid spacing value(s) in ${fileShort}`,
299
+ detail: `Grid is ${grid}px. Found values that are not multiples: ${[...new Set(items.map((o) => o.value))].slice(0, 5).join(", ")}.`,
300
+ evidence: items.slice(0, 5).map((o) => `${fileShort}:${o.line} — ${o.property}: ${o.value}`),
301
+ action: `Run \`whale insights drift spacing --review\` to accept as refinement or flag for refactor.`
302
+ });
303
+ }
304
+ return insights;
305
+ }
306
+ export function analyzeColorDrift(observations, _config, refinements = []) {
307
+ const colorObs = observations.filter((o) => COLOR_PROPERTIES.has(o.property) &&
308
+ !o.value.startsWith("var(") &&
309
+ /^#[0-9a-fA-F]{3,8}$/.test(o.value));
310
+ if (colorObs.length === 0)
311
+ return [];
312
+ // Derive reference palette: top-10 most frequent hex values
313
+ const freq = new Map();
314
+ for (const obs of colorObs) {
315
+ const hex = obs.value.toLowerCase();
316
+ freq.set(hex, (freq.get(hex) ?? 0) + 1);
317
+ }
318
+ const palette = new Set([...freq.entries()]
319
+ .sort((a, b) => b[1] - a[1])
320
+ .slice(0, 10)
321
+ .map(([hex]) => hex));
322
+ // Check refinements for color exceptions
323
+ const hasColorRefinement = refinements.some((r) => r.scope?.issueType === "hex");
324
+ if (hasColorRefinement)
325
+ return [];
326
+ // Drifting = hex that is NOT in the top-10 palette
327
+ const drifting = colorObs.filter((o) => !palette.has(o.value.toLowerCase()));
328
+ if (drifting.length === 0)
329
+ return [];
330
+ // Group by hue family (first 2 hex digits after #)
331
+ const byHue = new Map();
332
+ for (const obs of drifting) {
333
+ const hue = obs.value.slice(1, 3).toLowerCase();
334
+ if (!byHue.has(hue))
335
+ byHue.set(hue, []);
336
+ byHue.get(hue).push(obs);
337
+ }
338
+ const insights = [];
339
+ for (const [hue, items] of byHue) {
340
+ const severity = items.length >= 3 ? "warning" : "info";
341
+ const uniqueValues = [...new Set(items.map((o) => o.value))];
342
+ insights.push({
343
+ id: `drift.color.${hue}`,
344
+ category: "tokens",
345
+ severity,
346
+ title: `${items.length} color value(s) outside the project palette (hue ~#${hue})`,
347
+ detail: `Colors not in the top-10 palette: ${uniqueValues.slice(0, 4).join(", ")}.`,
348
+ evidence: items.slice(0, 5).map((o) => {
349
+ const short = o.file.split("/").slice(-2).join("/");
350
+ return `${short}:${o.line} — ${o.property}: ${o.value}`;
351
+ }),
352
+ action: `Run \`whale insights drift color --review\` to accept as refinement or flag for refactor.`
353
+ });
354
+ }
355
+ return insights;
356
+ }
357
+ export function analyzeRadiiDrift(observations, config, refinements = []) {
358
+ const hasRadiusRefinement = refinements.some((r) => r.scope?.issueType === "radius");
359
+ if (hasRadiusRefinement)
360
+ return [];
361
+ const control = config.foundations?.radius?.control ?? 2;
362
+ const container = config.foundations?.radius?.container ?? 4;
363
+ const drifting = observations.filter((o) => RADIUS_PROPERTIES.has(o.property) &&
364
+ o.pxValue != null &&
365
+ o.pxValue !== 0 &&
366
+ o.pxValue !== 9999 &&
367
+ o.pxValue !== control &&
368
+ o.pxValue !== container);
369
+ if (drifting.length === 0)
370
+ return [];
371
+ // Group by px value to show the spread
372
+ const byValue = new Map();
373
+ for (const obs of drifting) {
374
+ const key = obs.pxValue;
375
+ if (!byValue.has(key))
376
+ byValue.set(key, []);
377
+ byValue.get(key).push(obs);
378
+ }
379
+ const spread = [...byValue.keys()].sort((a, b) => a - b);
380
+ const severity = drifting.length >= 5 ? "warning" : "info";
381
+ return [{
382
+ id: "drift.radii",
383
+ category: "tokens",
384
+ severity,
385
+ title: `${drifting.length} border-radius value(s) outside foundations (${control}px / ${container}px)`,
386
+ detail: `Values found: ${spread.map((v) => `${v}px`).join(", ")}. Expected: ${control}px (controls) or ${container}px (containers).`,
387
+ evidence: drifting.slice(0, 5).map((o) => {
388
+ const short = o.file.split("/").slice(-2).join("/");
389
+ return `${short}:${o.line} — ${o.property}: ${o.value}`;
390
+ }),
391
+ action: `Run \`whale insights drift radii --review\` to accept as refinement or flag for refactor.`
392
+ }];
393
+ }
394
+ // ---------------------------------------------------------------------------
256
395
  // Main entry
257
396
  // ---------------------------------------------------------------------------
258
397
  export function analyze(input) {
@@ -264,6 +403,11 @@ export function analyze(input) {
264
403
  all.push(...analyzeTokenDrift(input.observations));
265
404
  all.push(...analyzeDecisionTension(input.decisions));
266
405
  all.push(...analyzeGridDrift(input.config, input.observations));
406
+ if (input.styleObservations) {
407
+ all.push(...analyzeSpacingDrift(input.styleObservations, input.config, input.refinements));
408
+ all.push(...analyzeColorDrift(input.styleObservations, input.config, input.refinements));
409
+ all.push(...analyzeRadiiDrift(input.styleObservations, input.config, input.refinements));
410
+ }
267
411
  // Sort by severity (critical > warning > info), then by category for stable output.
268
412
  const sevRank = { critical: 0, warning: 1, info: 2 };
269
413
  all.sort((a, b) => {
@@ -1,12 +1,14 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import ora from "ora";
4
+ import prompts from "prompts";
4
5
  import { resolveTarget } from "../utils/paths.js";
5
6
  import { scanComponents } from "../scanner/componentScanner.js";
6
7
  import { aggregateTailwind, detectTailwindConfig } from "../scanner/tailwindScanner.js";
7
8
  import { inferFoundations } from "../scanner/foundationInferrer.js";
8
9
  import { loadProposals, saveProposals, upsertProposal, fingerprintComponent, fingerprintFoundations, pendingProposals } from "../utils/proposals.js";
9
10
  import { loadConfig, saveConfig, DEFAULT_CONFIG } from "../utils/config.js";
11
+ import { getAiAvailability } from "../utils/aiAvailability.js";
10
12
  import { ui } from "../ui/index.js";
11
13
  const SCANNER_VERSION = "1.1.0";
12
14
  export async function adoptCommand(targetArg, options = {}) {
@@ -140,6 +142,24 @@ export async function adoptCommand(targetArg, options = {}) {
140
142
  if (!(await fs.pathExists(p)))
141
143
  await fs.writeJson(p, [], { spaces: 2 });
142
144
  }
145
+ // ---- Step 7: optional Selene enrichment -----------------------------------
146
+ const ai = await getAiAvailability(target);
147
+ if (ai.available) {
148
+ console.log();
149
+ const { doEnrich } = await prompts({
150
+ type: "confirm",
151
+ name: "doEnrich",
152
+ message: "Use Selene to add prose descriptions to the inferred foundations? (requires API key)",
153
+ initial: false
154
+ });
155
+ if (doEnrich) {
156
+ console.log(ui.note("Selene enrichment: run `whale selene describe` after review to add descriptions."));
157
+ }
158
+ }
159
+ else {
160
+ console.log();
161
+ console.log(ui.muted("AI features available with Claude Code, Codex, or an API key."));
162
+ }
143
163
  // ---- Summary --------------------------------------------------------------
144
164
  console.log();
145
165
  console.log(ui.section("Proposals"));
@@ -0,0 +1,127 @@
1
+ import { execSync } from "node:child_process";
2
+ import { resolveTarget } from "../utils/paths.js";
3
+ import { loadDecisions } from "../utils/decisions.js";
4
+ import { loadComponents } from "../utils/components.js";
5
+ import { loadRefinements } from "../utils/refinements.js";
6
+ import { ui } from "../ui/index.js";
7
+ const INTELLIGENCE_FILES = [
8
+ "intelligence/decisions.json",
9
+ "intelligence/components.json",
10
+ "intelligence/refinements.json",
11
+ "whale.config.json"
12
+ ];
13
+ function gitDiffFiles(target, since) {
14
+ try {
15
+ const result = execSync(`git diff --name-only ${since} HEAD -- ${INTELLIGENCE_FILES.join(" ")}`, {
16
+ cwd: target,
17
+ encoding: "utf8"
18
+ });
19
+ return result.trim().split("\n").filter(Boolean);
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ }
25
+ function gitFileAtRef(target, ref, file) {
26
+ try {
27
+ const raw = execSync(`git show ${ref}:${file}`, { cwd: target, encoding: "utf8" });
28
+ return JSON.parse(raw);
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ export async function changesCommand(opts) {
35
+ const target = resolveTarget(opts.target);
36
+ const since = opts.since ?? "HEAD~1";
37
+ const changedFiles = gitDiffFiles(target, since);
38
+ if (changedFiles.length === 0) {
39
+ console.log(ui.note(`No intelligence store changes since ${since}`));
40
+ return;
41
+ }
42
+ console.log(ui.section(`Changes since ${since}`));
43
+ const summary = {
44
+ decisions: { added: [], removed: [], modified: [] },
45
+ components: { added: [], removed: [] },
46
+ refinements: { added: [], removed: [] },
47
+ configChanged: false
48
+ };
49
+ if (changedFiles.includes("whale.config.json")) {
50
+ summary.configChanged = true;
51
+ }
52
+ if (changedFiles.includes("intelligence/decisions.json")) {
53
+ const before = gitFileAtRef(target, since, "intelligence/decisions.json");
54
+ const after = await loadDecisions(target);
55
+ const beforeIds = new Map(before.map((d) => [d.id, d.title]));
56
+ const afterIds = new Map(after.map((d) => [d.id, d.title]));
57
+ for (const [id, title] of afterIds) {
58
+ if (!beforeIds.has(id))
59
+ summary.decisions.added.push(title);
60
+ }
61
+ for (const [id, title] of beforeIds) {
62
+ if (!afterIds.has(id))
63
+ summary.decisions.removed.push(title);
64
+ else if (afterIds.get(id) !== title)
65
+ summary.decisions.modified.push(title);
66
+ }
67
+ }
68
+ if (changedFiles.includes("intelligence/components.json")) {
69
+ const before = gitFileAtRef(target, since, "intelligence/components.json");
70
+ const after = await loadComponents(target);
71
+ const beforeNames = new Set(before.map((c) => c.name));
72
+ const afterNames = new Set(after.map((c) => c.name));
73
+ for (const name of afterNames) {
74
+ if (!beforeNames.has(name))
75
+ summary.components.added.push(name);
76
+ }
77
+ for (const name of beforeNames) {
78
+ if (!afterNames.has(name))
79
+ summary.components.removed.push(name);
80
+ }
81
+ }
82
+ if (changedFiles.includes("intelligence/refinements.json")) {
83
+ const before = gitFileAtRef(target, since, "intelligence/refinements.json");
84
+ const after = await loadRefinements(target);
85
+ const beforeIds = new Set(before.map((r) => r.id));
86
+ const afterIds = new Set(after.map((r) => r.id));
87
+ const beforeNotes = new Map(before.map((r) => [r.id, r.note]));
88
+ for (const r of after) {
89
+ if (!beforeIds.has(r.id))
90
+ summary.refinements.added.push(r.note);
91
+ }
92
+ for (const [id, note] of beforeNotes) {
93
+ if (!afterIds.has(id))
94
+ summary.refinements.removed.push(note);
95
+ }
96
+ }
97
+ // ---- Render ----------------------------------------------------------------
98
+ if (summary.configChanged) {
99
+ console.log(ui.note("whale.config.json changed"));
100
+ }
101
+ const { decisions: d } = summary;
102
+ if (d.added.length || d.removed.length || d.modified.length) {
103
+ console.log(ui.section("Decisions"));
104
+ for (const t of d.added)
105
+ console.log(ui.ok(`+ ${t}`));
106
+ for (const t of d.removed)
107
+ console.log(ui.fail(`- ${t}`));
108
+ for (const t of d.modified)
109
+ console.log(ui.warn(`~ ${t}`));
110
+ }
111
+ const { components: c } = summary;
112
+ if (c.added.length || c.removed.length) {
113
+ console.log(ui.section("Components"));
114
+ for (const n of c.added)
115
+ console.log(ui.ok(`+ ${n}`));
116
+ for (const n of c.removed)
117
+ console.log(ui.fail(`- ${n}`));
118
+ }
119
+ const { refinements: r } = summary;
120
+ if (r.added.length || r.removed.length) {
121
+ console.log(ui.section("Refinements"));
122
+ for (const n of r.added)
123
+ console.log(ui.ok(`+ ${n}`));
124
+ for (const n of r.removed)
125
+ console.log(ui.fail(`- ${n}`));
126
+ }
127
+ }
@@ -5,6 +5,9 @@ import { loadConfig, saveConfig, DEFAULT_CONFIG } from "../utils/config.js";
5
5
  import { getPack, listPacks } from "../utils/registry.js";
6
6
  import { validateCss } from "../validators/cssValidator.js";
7
7
  import { generateWiki } from "../generators/wikiGenerator.js";
8
+ import { appendDecision } from "../utils/decisions.js";
9
+ import { getAiAvailability } from "../utils/aiAvailability.js";
10
+ import { mapWizardAnswers, suggestUiCategories } from "../utils/wizardMapping.js";
8
11
  import { ui } from "../ui/index.js";
9
12
  import { PACKAGE_VERSION } from "../version.js";
10
13
  const OPINIONATED_PACKS = ["lighthouse", "forge", "scribe"];
@@ -20,8 +23,11 @@ export async function igniteCommand(projectName = "whale-project", options = {})
20
23
  console.log();
21
24
  // ---- Resolve config based on mode -----------------------------------------
22
25
  let config;
26
+ let projectIntentNote;
23
27
  if (mode === "interactive") {
24
- config = await runWizard(projectName);
28
+ const result = await runWizard(projectName);
29
+ config = result.config;
30
+ projectIntentNote = result.projectIntentNote;
25
31
  }
26
32
  else if (mode === "minimal") {
27
33
  config = {
@@ -43,6 +49,16 @@ export async function igniteCommand(projectName = "whale-project", options = {})
43
49
  const target = await initCommand(projectName, { config, silent: true });
44
50
  const targetRel = path.relative(process.cwd(), target) || ".";
45
51
  console.log(ui.ok(`Workspace created at ${ui.path(targetRel)}`));
52
+ // ---- Step 1b: record free-text project intent as first decision ------------
53
+ if (projectIntentNote) {
54
+ await appendDecision(target, {
55
+ title: "Project intent",
56
+ category: "product",
57
+ context: "Captured at project creation via `whale ignite --interactive`.",
58
+ decision: projectIntentNote
59
+ });
60
+ console.log(ui.ok("Project intent recorded as first decision."));
61
+ }
46
62
  // ---- Step 2: confirm packs against registry -------------------------------
47
63
  console.log();
48
64
  console.log(ui.section("Packs"));
@@ -58,6 +74,9 @@ export async function igniteCommand(projectName = "whale-project", options = {})
58
74
  validPacks.push(name);
59
75
  packLines.push(`${ui.glyph.check} ${ui.code(name)} ${ui.muted(`(${pack.kind})`)} — ${pack.description}`);
60
76
  }
77
+ if (packLines.length === 0) {
78
+ packLines.push(ui.muted("(no packs selected)"));
79
+ }
61
80
  console.log(ui.indent(packLines.join("\n")));
62
81
  finalConfig.packs = validPacks;
63
82
  finalConfig.ignited = {
@@ -127,86 +146,133 @@ export async function igniteCommand(projectName = "whale-project", options = {})
127
146
  console.log();
128
147
  }
129
148
  // ---------------------------------------------------------------------------
130
- // Interactive wizard — unchanged logic, just uses ui for the header.
149
+ // Interactive wizard — human-language questions, technical config is derived.
131
150
  // ---------------------------------------------------------------------------
132
151
  async function runWizard(defaultName) {
133
152
  console.log(ui.muted("Answer a few questions to shape your workspace. Ctrl+C to cancel."));
134
153
  console.log();
135
- const allPacks = listPacks();
136
- const answers = await prompts([
137
- { type: "text", name: "projectName", message: "Project name:", initial: defaultName },
138
- {
139
- type: "select",
140
- name: "projectType",
141
- message: "Project type:",
142
- choices: [
143
- { title: "Landing page", value: "landing-page" },
144
- { title: "Web app", value: "web-app" },
145
- { title: "Component library", value: "component-library" },
146
- { title: "Documentation site", value: "docs-site" },
147
- { title: "Other", value: "other" }
148
- ]
149
- },
150
- {
151
- type: "select",
152
- name: "stack",
153
- message: "Styling stack:",
154
- choices: [
155
- { title: "Plain CSS", value: "css" },
156
- { title: "Tailwind", value: "tailwind" },
157
- { title: "SCSS / Sass", value: "scss" },
158
- { title: "CSS-in-JS (styled-components, emotion, ...)", value: "css-in-js" }
159
- ]
160
- },
161
- { type: "number", name: "grid", message: "Grid unit (px):", initial: 8, min: 1, max: 32 },
162
- { type: "number", name: "radiusControl", message: "Radius for controls (buttons, inputs):", initial: 2, min: 0, max: 64 },
163
- { type: "number", name: "radiusContainer", message: "Radius for containers (cards, modals):", initial: 4, min: 0, max: 64 },
164
- {
165
- type: "multiselect",
166
- name: "packs",
167
- message: "Packs to install:",
168
- instructions: false,
169
- hint: "space to toggle, enter to confirm",
170
- choices: allPacks.map((p) => ({
171
- title: `${p.name} (${p.kind})`,
172
- description: p.description,
173
- value: p.name,
174
- selected: OPINIONATED_PACKS.includes(p.name)
175
- }))
176
- },
177
- {
178
- type: "multiselect",
179
- name: "aiTargets",
180
- message: "AI agents to generate context for:",
181
- instructions: false,
182
- hint: "space to toggle, enter to confirm",
183
- choices: [
184
- { title: "Claude (CLAUDE.md)", value: "claude", selected: true },
185
- { title: "Codex / Generic agent (AGENTS.md)", value: "codex" },
186
- { title: "Cursor (.cursorrules)", value: "cursor" },
187
- { title: "GitHub Copilot (.github/copilot-instructions.md)", value: "copilot" }
188
- ]
189
- }
190
- ], {
191
- onCancel: () => {
192
- console.log();
193
- console.log(ui.warn("Ignite cancelled."));
194
- process.exit(130);
154
+ const onCancel = () => {
155
+ console.log();
156
+ console.log(ui.warn("Ignite cancelled."));
157
+ process.exit(130);
158
+ };
159
+ // Q0: project name
160
+ const { projectName } = await prompts({ type: "text", name: "projectName", message: "Project name:", initial: defaultName }, { onCancel });
161
+ // Q1: what are you building?
162
+ const { projectType } = await prompts({
163
+ type: "select",
164
+ name: "projectType",
165
+ message: "What are you building?",
166
+ choices: [
167
+ { title: "Web app", value: "web-app" },
168
+ { title: "Marketing site", value: "marketing-site" },
169
+ { title: "Portfolio", value: "portfolio" },
170
+ { title: "Internal tool", value: "internal-tool" },
171
+ { title: "Prototype", value: "prototype" },
172
+ { title: "Something else", value: "other" }
173
+ ]
174
+ }, { onCancel });
175
+ let projectTypeOther;
176
+ if (projectType === "other") {
177
+ const { freeText } = await prompts({ type: "text", name: "freeText", message: "Describe it briefly:" }, { onCancel });
178
+ projectTypeOther = freeText;
179
+ }
180
+ // Q2: stack choices reordered by what's most likely for this project type
181
+ const appFirstChoices = [
182
+ { title: "TypeScript + React", value: "react-ts" },
183
+ { title: "TypeScript + Vue", value: "vue-ts" },
184
+ { title: "TypeScript + Svelte", value: "svelte-ts" },
185
+ { title: "Plain HTML + CSS + JS", value: "html-css-js" },
186
+ { title: "Other", value: "other" }
187
+ ];
188
+ const staticFirstChoices = [
189
+ { title: "Plain HTML + CSS + JS", value: "html-css-js" },
190
+ { title: "TypeScript + React", value: "react-ts" },
191
+ { title: "TypeScript + Vue", value: "vue-ts" },
192
+ { title: "TypeScript + Svelte", value: "svelte-ts" },
193
+ { title: "Other", value: "other" }
194
+ ];
195
+ const stackChoices = projectType === "web-app" || projectType === "internal-tool"
196
+ ? appFirstChoices
197
+ : staticFirstChoices;
198
+ const { stack } = await prompts({
199
+ type: "select",
200
+ name: "stack",
201
+ message: "What language or framework?",
202
+ choices: stackChoices
203
+ }, { onCancel });
204
+ let stackOther;
205
+ if (stack === "other") {
206
+ const { freeText } = await prompts({ type: "text", name: "freeText", message: "Briefly describe it (we'll use plain CSS defaults):" }, { onCancel });
207
+ stackOther = freeText;
208
+ }
209
+ // Q3: team size
210
+ const { teamSize } = await prompts({
211
+ type: "select",
212
+ name: "teamSize",
213
+ message: "Who will work on this?",
214
+ choices: [
215
+ { title: "Just me", value: "solo" },
216
+ { title: "A small team (2–5 people)", value: "small" },
217
+ { title: "A larger team (6+)", value: "large" }
218
+ ]
219
+ }, { onCancel });
220
+ // Q4: UI references — only if AI is available
221
+ let uiCategories;
222
+ const ai = await getAiAvailability();
223
+ if (ai.available) {
224
+ const suggested = suggestUiCategories(projectType);
225
+ const { addRefs } = await prompts({
226
+ type: "confirm",
227
+ name: "addRefs",
228
+ message: "Add UI component references to this project?",
229
+ initial: true
230
+ }, { onCancel });
231
+ if (addRefs) {
232
+ const allCategories = ["forms", "navigation", "feedback", "surface", "layout", "data-display"];
233
+ const { selected } = await prompts({
234
+ type: "multiselect",
235
+ name: "selected",
236
+ message: "Which categories? (space to toggle, enter to confirm)",
237
+ instructions: false,
238
+ hint: "suggested for this project type are pre-selected",
239
+ choices: allCategories.map((cat) => ({
240
+ title: cat,
241
+ value: cat,
242
+ selected: suggested.includes(cat)
243
+ }))
244
+ }, { onCancel });
245
+ uiCategories = selected ?? suggested;
195
246
  }
196
- });
197
- return {
198
- ...DEFAULT_CONFIG,
199
- projectName: answers.projectName,
200
- projectType: answers.projectType,
201
- stack: answers.stack,
202
- packs: answers.packs && answers.packs.length > 0 ? answers.packs : OPINIONATED_PACKS,
203
- aiTargets: answers.aiTargets && answers.aiTargets.length > 0 ? answers.aiTargets : ["claude"],
204
- foundations: {
205
- grid: answers.grid ?? 8,
206
- radius: {
207
- control: answers.radiusControl ?? 2,
208
- container: answers.radiusContainer ?? 4
209
- }
247
+ else {
248
+ uiCategories = [];
210
249
  }
250
+ }
251
+ // Q5: agent packs
252
+ const allPacks = listPacks();
253
+ const { agentPacks } = await prompts({
254
+ type: "multiselect",
255
+ name: "agentPacks",
256
+ message: "Which agent packs to install?",
257
+ instructions: false,
258
+ hint: "space to toggle, enter to confirm",
259
+ choices: allPacks.map((p) => ({
260
+ title: `${p.name} — ${p.description}`,
261
+ value: p.name,
262
+ selected: p.name === "selene"
263
+ }))
264
+ }, { onCancel });
265
+ // Map human answers to technical config
266
+ const wizardAnswers = {
267
+ projectType,
268
+ projectTypeOther,
269
+ stack,
270
+ stackOther,
271
+ teamSize,
272
+ uiCategories,
273
+ agentPacks
211
274
  };
275
+ const result = mapWizardAnswers(wizardAnswers);
276
+ result.config.projectName = projectName;
277
+ return result;
212
278
  }