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 +19 -2
- package/dist/analyzer/insights.js +144 -0
- package/dist/commands/adopt.js +20 -0
- package/dist/commands/changes.js +127 -0
- package/dist/commands/ignite.js +142 -76
- package/dist/commands/insights.js +146 -9
- package/dist/commands/references.js +193 -0
- package/dist/commands/sync.js +8 -0
- package/dist/generators/wikiGenerator.js +8 -304
- package/dist/index.js +33 -2
- package/dist/mcp/server.js +29 -0
- package/dist/scanner/extractors/css.js +95 -0
- package/dist/scanner/extractors/inline.js +131 -0
- package/dist/scanner/extractors/styleBlocks.js +37 -0
- package/dist/scanner/normalizer.js +59 -0
- package/dist/scanner/tailwindScanner.js +39 -0
- package/dist/templates/claude.js +93 -0
- package/dist/templates/components.js +45 -0
- package/dist/templates/conventions.js +45 -0
- package/dist/templates/decisions.js +34 -0
- package/dist/templates/foundations.js +34 -0
- package/dist/templates/project.js +82 -0
- package/dist/utils/aiAvailability.js +25 -0
- package/dist/utils/wizardMapping.js +54 -0
- package/dist/version.js +1 -1
- package/package.json +2 -2
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
|
+
<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.
|
|
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) => {
|
package/dist/commands/adopt.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/ignite.js
CHANGED
|
@@ -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
|
-
|
|
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 —
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
}
|