whale-igniter 1.1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/dist/analyzer/imports.js +88 -0
  4. package/dist/analyzer/insights.js +276 -0
  5. package/dist/commands/add.js +36 -0
  6. package/dist/commands/adopt.js +180 -0
  7. package/dist/commands/adoptReview.js +267 -0
  8. package/dist/commands/component.js +93 -0
  9. package/dist/commands/createComponent.js +207 -0
  10. package/dist/commands/decision.js +98 -0
  11. package/dist/commands/docs.js +34 -0
  12. package/dist/commands/ignite.js +212 -0
  13. package/dist/commands/init.js +66 -0
  14. package/dist/commands/insights.js +123 -0
  15. package/dist/commands/mcp.js +106 -0
  16. package/dist/commands/refine.js +36 -0
  17. package/dist/commands/selene.js +516 -0
  18. package/dist/commands/sync.js +43 -0
  19. package/dist/commands/validate.js +48 -0
  20. package/dist/commands/watch.js +150 -0
  21. package/dist/commands/wiki.js +21 -0
  22. package/dist/generators/markdownGenerator.js +112 -0
  23. package/dist/generators/reportGenerator.js +50 -0
  24. package/dist/generators/wikiGenerator.js +365 -0
  25. package/dist/index.js +213 -0
  26. package/dist/mcp/server.js +404 -0
  27. package/dist/scanner/componentScanner.js +522 -0
  28. package/dist/scanner/foundationInferrer.js +174 -0
  29. package/dist/scanner/tailwindMapper.js +58 -0
  30. package/dist/scanner/tailwindScanner.js +186 -0
  31. package/dist/selene/apiClient.js +168 -0
  32. package/dist/selene/cache.js +68 -0
  33. package/dist/selene/clipboard.js +56 -0
  34. package/dist/selene/promptBuilder.js +229 -0
  35. package/dist/selene/providers.js +67 -0
  36. package/dist/selene/responseParser.js +149 -0
  37. package/dist/ui/atoms.js +30 -0
  38. package/dist/ui/blocks.js +208 -0
  39. package/dist/ui/capabilities.js +64 -0
  40. package/dist/ui/index.js +13 -0
  41. package/dist/ui/symbols.js +41 -0
  42. package/dist/ui/theme.js +78 -0
  43. package/dist/utils/components.js +40 -0
  44. package/dist/utils/config.js +31 -0
  45. package/dist/utils/decisions.js +32 -0
  46. package/dist/utils/paths.js +4 -0
  47. package/dist/utils/proposals.js +61 -0
  48. package/dist/utils/refinements.js +81 -0
  49. package/dist/utils/registry.js +45 -0
  50. package/dist/utils/writeJson.js +6 -0
  51. package/dist/validators/cssValidator.js +204 -0
  52. package/dist/version.js +1 -0
  53. package/docs/ROADMAP.md +206 -0
  54. package/package.json +76 -0
@@ -0,0 +1,36 @@
1
+ import { resolveTarget } from "../utils/paths.js";
2
+ import { loadConfig, saveConfig } from "../utils/config.js";
3
+ import { getPack, listPacks } from "../utils/registry.js";
4
+ import { ui } from "../ui/index.js";
5
+ export async function addCommand(packName) {
6
+ const target = resolveTarget();
7
+ const pack = getPack(packName);
8
+ if (!pack) {
9
+ console.log(ui.fail(`Unknown pack: "${packName}"`));
10
+ console.log(ui.muted("Available packs:"));
11
+ for (const p of listPacks()) {
12
+ console.log(` ${ui.glyph.bullet} ${ui.code(p.name)} ${ui.muted(`(${p.kind})`)} — ${p.description}`);
13
+ }
14
+ process.exitCode = 1;
15
+ return;
16
+ }
17
+ const config = await loadConfig(target);
18
+ config.packs = config.packs ?? [];
19
+ if (config.packs.includes(packName)) {
20
+ console.log(ui.warn(`${packName} is already installed.`));
21
+ return;
22
+ }
23
+ config.packs.push(packName);
24
+ await saveConfig(target, config);
25
+ console.log(ui.ok(`${ui.code(packName)} added to ${ui.path("whale.config.json")}`));
26
+ console.log(` ${ui.kv("kind", pack.kind, { keyWidth: 8 })}`);
27
+ console.log(` ${ui.muted(pack.description)}`);
28
+ if (pack.kind === "generator") {
29
+ console.log();
30
+ console.log(ui.next(`whale docs ${ui.muted("generate the " + packName + " report")}`));
31
+ }
32
+ else {
33
+ console.log();
34
+ console.log(ui.next(`whale validate ${ui.muted("use " + packName)}`));
35
+ }
36
+ }
@@ -0,0 +1,180 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import ora from "ora";
4
+ import { resolveTarget } from "../utils/paths.js";
5
+ import { scanComponents } from "../scanner/componentScanner.js";
6
+ import { aggregateTailwind, detectTailwindConfig } from "../scanner/tailwindScanner.js";
7
+ import { inferFoundations } from "../scanner/foundationInferrer.js";
8
+ import { loadProposals, saveProposals, upsertProposal, fingerprintComponent, fingerprintFoundations, pendingProposals } from "../utils/proposals.js";
9
+ import { loadConfig, saveConfig, DEFAULT_CONFIG } from "../utils/config.js";
10
+ import { ui } from "../ui/index.js";
11
+ const SCANNER_VERSION = "1.1.0";
12
+ export async function adoptCommand(targetArg, options = {}) {
13
+ const target = resolveTarget(targetArg);
14
+ console.log();
15
+ console.log(ui.header("Whale Igniter", `adopt • ${path.relative(process.cwd(), target) || "."}`));
16
+ console.log();
17
+ await fs.ensureDir(path.join(target, "intelligence"));
18
+ // ---- Step 1: scan components ----------------------------------------------
19
+ const spin1 = ora({ text: "Scanning components", spinner: "dots" }).start();
20
+ const components = await scanComponents(target, options.pattern ? { pattern: options.pattern } : {});
21
+ spin1.succeed(`Found ${components.length} component candidate(s)`);
22
+ // ---- Step 2: aggregate Tailwind observations ------------------------------
23
+ const spin2 = ora({ text: "Aggregating Tailwind usage", spinner: "dots" }).start();
24
+ const allClassNameStrings = components.flatMap((c) => c.classNames);
25
+ const observations = aggregateTailwind(allClassNameStrings);
26
+ const tailwindConfig = await detectTailwindConfig(target);
27
+ const tailwindNote = tailwindConfig
28
+ ? ui.muted(` (detected ${tailwindConfig})`)
29
+ : ui.muted(" (no tailwind.config found)");
30
+ spin2.succeed(`Aggregated ${observations.length} class(es) from ${allClassNameStrings.length} attribute(s)` + tailwindNote);
31
+ // ---- Step 3: infer foundations --------------------------------------------
32
+ const spin3 = ora({ text: "Inferring foundations", spinner: "dots" }).start();
33
+ const foundations = inferFoundations(observations);
34
+ spin3.succeed("Foundations inferred");
35
+ // Foundations block — nicely structured key/value with confidence tags.
36
+ console.log();
37
+ console.log(ui.section("Foundations"));
38
+ const fLines = [];
39
+ fLines.push(ui.kv("grid", `${ui.code(foundations.grid.value + "px")} ${confidenceBadge(foundations.grid.confidence)}`, { keyWidth: 8 }));
40
+ fLines.push(ui.muted(" " + foundations.grid.evidence));
41
+ fLines.push(ui.kv("radii", `control ${ui.code((foundations.radii.control ?? "?") + "px")} / container ${ui.code((foundations.radii.container ?? "?") + "px")} ${confidenceBadge(foundations.radii.confidence)}`, { keyWidth: 8 }));
42
+ fLines.push(ui.muted(" " + foundations.radii.evidence));
43
+ fLines.push(ui.kv("colors", foundations.colors.evidence, { keyWidth: 8 }));
44
+ if (foundations.colors.palette.length > 0) {
45
+ const top = foundations.colors.palette
46
+ .slice(0, 5)
47
+ .map((p) => `${ui.code(p.token)} ${ui.muted("×" + p.count)}`)
48
+ .join(" ");
49
+ fLines.push(ui.muted(" top: ") + top);
50
+ }
51
+ console.log(ui.indent(fLines.join("\n")));
52
+ if (foundations.hints.length > 0) {
53
+ console.log();
54
+ console.log(ui.subsection("Hints"));
55
+ for (const h of foundations.hints) {
56
+ console.log(ui.indent(ui.warn(h)));
57
+ }
58
+ }
59
+ // ---- Step 4: build proposals ----------------------------------------------
60
+ const store = await loadProposals(target);
61
+ store.scannedAt = new Date().toISOString();
62
+ store.scannerVersion = SCANNER_VERSION;
63
+ let addedComponents = 0;
64
+ let updatedComponents = 0;
65
+ let preservedComponents = 0;
66
+ for (const c of components) {
67
+ const fp = fingerprintComponent(c.name, c.file);
68
+ const proposal = {
69
+ kind: "component",
70
+ fingerprint: fp,
71
+ status: "pending",
72
+ proposedAt: new Date().toISOString(),
73
+ payload: {
74
+ name: c.name,
75
+ description: undefined,
76
+ category: inferCategoryFromFile(c.file),
77
+ files: [c.file],
78
+ tokens: undefined,
79
+ variants: undefined,
80
+ states: undefined
81
+ },
82
+ evidence: {
83
+ file: c.file,
84
+ exportKind: c.exportKind,
85
+ declarationKind: c.declarationKind,
86
+ classNamesSampled: c.classNames.length,
87
+ jsxComposition: c.jsxIdentifiers.slice(0, 10)
88
+ }
89
+ };
90
+ const res = upsertProposal(store, proposal);
91
+ if (res.added)
92
+ addedComponents += 1;
93
+ else if (res.preserved)
94
+ preservedComponents += 1;
95
+ else
96
+ updatedComponents += 1;
97
+ }
98
+ const foundationsProposal = {
99
+ kind: "foundations",
100
+ fingerprint: fingerprintFoundations(),
101
+ status: "pending",
102
+ proposedAt: new Date().toISOString(),
103
+ payload: foundations
104
+ };
105
+ upsertProposal(store, foundationsProposal);
106
+ if (options.dryRun) {
107
+ console.log();
108
+ console.log(ui.warn("Dry run — no changes written."));
109
+ return;
110
+ }
111
+ await saveProposals(target, store);
112
+ // ---- Step 5: ensure whale.config.json exists with sensible defaults -------
113
+ const configPath = path.join(target, "whale.config.json");
114
+ if (!(await fs.pathExists(configPath))) {
115
+ const draftConfig = {
116
+ ...DEFAULT_CONFIG,
117
+ projectName: path.basename(target),
118
+ stack: "tailwind",
119
+ ignited: {
120
+ at: new Date().toISOString(),
121
+ mode: "minimal",
122
+ version: SCANNER_VERSION
123
+ }
124
+ };
125
+ await saveConfig(target, draftConfig);
126
+ console.log();
127
+ console.log(ui.note("Drafted whale.config.json (foundations populate on review)"));
128
+ }
129
+ else {
130
+ const existing = await loadConfig(target);
131
+ if (observations.length > 0 && existing.stack !== "tailwind") {
132
+ await saveConfig(target, { ...existing, stack: "tailwind" });
133
+ console.log();
134
+ console.log(ui.note("Updated stack to 'tailwind' in whale.config.json"));
135
+ }
136
+ }
137
+ // ---- Step 6: ensure other intelligence files exist ------------------------
138
+ for (const file of ["refinements.json", "decisions.json", "components.json"]) {
139
+ const p = path.join(target, "intelligence", file);
140
+ if (!(await fs.pathExists(p)))
141
+ await fs.writeJson(p, [], { spaces: 2 });
142
+ }
143
+ // ---- Summary --------------------------------------------------------------
144
+ console.log();
145
+ console.log(ui.section("Proposals"));
146
+ console.log(ui.indent(ui.summary([
147
+ { label: "new", value: addedComponents, tone: addedComponents > 0 ? "ok" : "muted" },
148
+ ...(updatedComponents > 0 ? [{ label: "updated", value: updatedComponents, tone: "muted" }] : []),
149
+ ...(preservedComponents > 0
150
+ ? [{ label: "preserved", value: preservedComponents, tone: "muted" }]
151
+ : [])
152
+ ])));
153
+ const pending = pendingProposals(store).length;
154
+ console.log();
155
+ console.log(ui.section("Next"));
156
+ console.log(ui.indent(`${ui.glyph.arrow} ${ui.code("whale adopt review")} ${ui.muted(`accept or reject ${pending} pending`)}`));
157
+ console.log(ui.indent(`${ui.glyph.arrow} ${ui.code("whale adopt status")} ${ui.muted("list without entering review")}`));
158
+ console.log();
159
+ }
160
+ function confidenceBadge(level) {
161
+ if (level === "high")
162
+ return ui.badges.success(level);
163
+ if (level === "medium")
164
+ return ui.badges.info(level);
165
+ return ui.badges.warning(level);
166
+ }
167
+ function inferCategoryFromFile(file) {
168
+ const lower = file.toLowerCase().replace(/\\/g, "/");
169
+ if (/\/forms?\//.test(lower) || /input|button|select|checkbox|radio|toggle/.test(lower))
170
+ return "form";
171
+ if (/\/nav|\/menu|\/header|\/sidebar|breadcrumb|tab\b/.test(lower))
172
+ return "navigation";
173
+ if (/modal|dialog|toast|alert|notification|popover|tooltip/.test(lower))
174
+ return "feedback";
175
+ if (/\/layout|\/grid|\/container|\/stack|\/cluster/.test(lower))
176
+ return "layout";
177
+ if (/card|panel|section|hero|cta/.test(lower))
178
+ return "surface";
179
+ return undefined;
180
+ }
@@ -0,0 +1,267 @@
1
+ import prompts from "prompts";
2
+ import { resolveTarget } from "../utils/paths.js";
3
+ import { loadProposals, saveProposals, pendingProposals } from "../utils/proposals.js";
4
+ import { upsertComponent } from "../utils/components.js";
5
+ import { loadConfig, saveConfig } from "../utils/config.js";
6
+ import { generateWiki } from "../generators/wikiGenerator.js";
7
+ import { ui } from "../ui/index.js";
8
+ export async function adoptStatusCommand(targetArg) {
9
+ const target = resolveTarget(targetArg);
10
+ const store = await loadProposals(target);
11
+ if (!store.scannedAt) {
12
+ console.log();
13
+ console.log(ui.note("No scan run yet. Use `whale adopt` first."));
14
+ return;
15
+ }
16
+ const pending = pendingProposals(store);
17
+ const accepted = store.proposals.filter((p) => p.status === "accepted").length;
18
+ const rejected = store.proposals.filter((p) => p.status === "rejected").length;
19
+ console.log();
20
+ console.log(ui.header("Whale Igniter", "adopt status"));
21
+ console.log();
22
+ console.log(ui.muted(`Last scan: ${store.scannedAt}`));
23
+ console.log();
24
+ console.log(ui.summary([
25
+ { label: "pending", value: pending.length, tone: pending.length > 0 ? "warn" : "muted" },
26
+ { label: "accepted", value: accepted, tone: "ok" },
27
+ { label: "rejected", value: rejected, tone: rejected > 0 ? "muted" : "muted" }
28
+ ]));
29
+ if (pending.length === 0) {
30
+ console.log();
31
+ console.log(ui.ok("All proposals reviewed. Run `whale sync` to refresh AI context."));
32
+ return;
33
+ }
34
+ console.log();
35
+ console.log(ui.section("Pending"));
36
+ for (const p of pending.slice(0, 30)) {
37
+ if (p.kind === "component") {
38
+ const name = p.payload.name.padEnd(24);
39
+ console.log(ui.indent(`${ui.code(name)} ${ui.muted(p.evidence.file)}`));
40
+ }
41
+ else if (p.kind === "foundations") {
42
+ const f = p.payload;
43
+ console.log(ui.indent(`${ui.code("foundations".padEnd(24))} ${ui.muted(`grid=${f.grid.value}px radii=${f.radii.control}/${f.radii.container}px`)}`));
44
+ }
45
+ else if (p.kind === "decision") {
46
+ console.log(ui.indent(`${ui.code("decision".padEnd(24))} ${ui.muted(p.payload.title)}`));
47
+ }
48
+ }
49
+ if (pending.length > 30) {
50
+ console.log(ui.indent(ui.muted(`… and ${pending.length - 30} more`)));
51
+ }
52
+ console.log();
53
+ console.log(ui.next(`${ui.code("whale adopt review")} ${ui.muted("walk through them")}`));
54
+ console.log();
55
+ }
56
+ export async function adoptReviewCommand(targetArg, options = {}) {
57
+ const target = resolveTarget(targetArg);
58
+ const store = await loadProposals(target);
59
+ if (!store.scannedAt) {
60
+ console.log();
61
+ console.log(ui.note("No scan run yet. Use `whale adopt` first."));
62
+ return;
63
+ }
64
+ let pending = pendingProposals(store);
65
+ if (options.category)
66
+ pending = pending.filter((p) => p.kind === options.category);
67
+ if (options.limit && options.limit > 0)
68
+ pending = pending.slice(0, options.limit);
69
+ if (pending.length === 0) {
70
+ console.log();
71
+ console.log(ui.ok("Nothing pending. All proposals have been reviewed."));
72
+ return;
73
+ }
74
+ // Review foundations first — it's the most consequential.
75
+ pending.sort((a, b) => (a.kind === "foundations" ? -1 : b.kind === "foundations" ? 1 : 0));
76
+ console.log();
77
+ console.log(ui.header("Whale Igniter", `adopt review • ${pending.length} pending`));
78
+ console.log();
79
+ console.log(ui.muted("Per item: accept, reject, edit, skip, quit."));
80
+ console.log();
81
+ let processed = 0;
82
+ let acceptedCount = 0;
83
+ let rejectedCount = 0;
84
+ let skippedCount = 0;
85
+ for (const proposal of pending) {
86
+ processed += 1;
87
+ console.log(ui.muted(`[${processed}/${pending.length}]`));
88
+ renderProposal(proposal);
89
+ const action = options.acceptAll ? "accept" : await promptAction();
90
+ if (action === "quit") {
91
+ console.log();
92
+ console.log(ui.note("Stopped. Progress saved."));
93
+ break;
94
+ }
95
+ if (action === "skip") {
96
+ skippedCount += 1;
97
+ console.log(ui.indent(ui.muted("skipped")));
98
+ console.log();
99
+ continue;
100
+ }
101
+ if (action === "reject") {
102
+ const reason = await prompts({ type: "text", name: "reason", message: "Reason (optional):" }, { onCancel: () => ({ reason: undefined }) });
103
+ proposal.status = "rejected";
104
+ proposal.reviewedAt = new Date().toISOString();
105
+ proposal.rejectionReason = reason.reason || undefined;
106
+ rejectedCount += 1;
107
+ console.log(ui.indent(ui.danger("rejected")));
108
+ console.log();
109
+ continue;
110
+ }
111
+ if (action === "edit") {
112
+ await editProposal(proposal);
113
+ }
114
+ await graduate(target, proposal);
115
+ proposal.status = "accepted";
116
+ proposal.reviewedAt = new Date().toISOString();
117
+ acceptedCount += 1;
118
+ console.log(ui.indent(ui.success("accepted")));
119
+ console.log();
120
+ }
121
+ await saveProposals(target, store);
122
+ if (acceptedCount > 0) {
123
+ console.log(ui.muted("Regenerating AI context..."));
124
+ await generateWiki(target);
125
+ console.log(ui.ok("CLAUDE.md and wiki updated"));
126
+ }
127
+ console.log();
128
+ console.log(ui.section("Summary"));
129
+ console.log(ui.indent(ui.summary([
130
+ { label: "accepted", value: acceptedCount, tone: acceptedCount > 0 ? "ok" : "muted" },
131
+ { label: "rejected", value: rejectedCount, tone: rejectedCount > 0 ? "muted" : "muted" },
132
+ ...(skippedCount > 0 ? [{ label: "skipped", value: skippedCount, tone: "warn" }] : [])
133
+ ])));
134
+ console.log();
135
+ }
136
+ function renderProposal(p) {
137
+ if (p.kind === "component") {
138
+ const c = p;
139
+ console.log(ui.section(`Component: ${ui.accent(c.payload.name)}`));
140
+ console.log(ui.indent([
141
+ ui.kv("file", ui.path(c.evidence.file), { keyWidth: 12 }),
142
+ ui.kv("export", `${c.evidence.exportKind} ${ui.muted("(" + c.evidence.declarationKind + ")")}`, {
143
+ keyWidth: 12
144
+ }),
145
+ ui.kv("category", c.payload.category ?? ui.muted("(heuristic miss)"), { keyWidth: 12 }),
146
+ ui.kv("classNames", `${c.evidence.classNamesSampled} attribute(s)`, { keyWidth: 12 }),
147
+ ...(c.evidence.jsxComposition.length > 0
148
+ ? [ui.kv("composes", c.evidence.jsxComposition.slice(0, 6).join(", "), { keyWidth: 12 })]
149
+ : [])
150
+ ].join("\n")));
151
+ }
152
+ else if (p.kind === "foundations") {
153
+ const f = p.payload;
154
+ console.log(ui.section("Foundations"));
155
+ console.log(ui.indent([
156
+ ui.kv("grid", `${ui.code(f.grid.value + "px")} ${confidenceBadge(f.grid.confidence)}`, { keyWidth: 8 }),
157
+ ui.muted(" " + f.grid.evidence),
158
+ ui.kv("radii", `control ${ui.code((f.radii.control ?? "?") + "px")} / container ${ui.code((f.radii.container ?? "?") + "px")} ${confidenceBadge(f.radii.confidence)}`, { keyWidth: 8 }),
159
+ ui.muted(" " + f.radii.evidence),
160
+ ui.kv("colors", `${f.colors.palette.length} token(s), ${f.colors.arbitraryCount} arbitrary value(s)`, {
161
+ keyWidth: 8
162
+ })
163
+ ].join("\n")));
164
+ }
165
+ else {
166
+ console.log(ui.section(`Decision: ${ui.accent(p.payload.title)}`));
167
+ console.log(ui.indent(ui.kv("category", p.payload.category, { keyWidth: 10 })));
168
+ console.log(ui.indent(ui.kv("decision", p.payload.decision, { keyWidth: 10 })));
169
+ }
170
+ console.log();
171
+ }
172
+ function confidenceBadge(level) {
173
+ if (level === "high")
174
+ return ui.badges.success(level);
175
+ if (level === "medium")
176
+ return ui.badges.info(level);
177
+ return ui.badges.warning(level);
178
+ }
179
+ async function promptAction() {
180
+ const { action } = await prompts({
181
+ type: "select",
182
+ name: "action",
183
+ message: "Action:",
184
+ choices: [
185
+ { title: "Accept", value: "accept" },
186
+ { title: "Reject", value: "reject" },
187
+ { title: "Edit then accept", value: "edit" },
188
+ { title: "Skip for now", value: "skip" },
189
+ { title: "Quit (save and exit)", value: "quit" }
190
+ ]
191
+ }, { onCancel: () => ({ action: "quit" }) });
192
+ return action ?? "quit";
193
+ }
194
+ async function editProposal(proposal) {
195
+ if (proposal.kind === "component") {
196
+ const c = proposal;
197
+ const answers = await prompts([
198
+ { type: "text", name: "name", message: "Name:", initial: c.payload.name },
199
+ { type: "text", name: "description", message: "Description:", initial: c.payload.description ?? "" },
200
+ { type: "text", name: "category", message: "Category:", initial: c.payload.category ?? "" },
201
+ {
202
+ type: "text",
203
+ name: "variants",
204
+ message: "Variants (comma-separated):",
205
+ initial: c.payload.variants?.join(",") ?? ""
206
+ },
207
+ {
208
+ type: "text",
209
+ name: "states",
210
+ message: "States (comma-separated):",
211
+ initial: c.payload.states?.join(",") ?? ""
212
+ }
213
+ ], { onCancel: () => ({}) });
214
+ const split = (s) => s ? s.split(",").map((x) => x.trim()).filter(Boolean) : undefined;
215
+ if (answers.name)
216
+ c.payload.name = answers.name;
217
+ c.payload.description = (answers.description || "").trim() || undefined;
218
+ c.payload.category = (answers.category || "").trim() || undefined;
219
+ c.payload.variants = split(answers.variants);
220
+ c.payload.states = split(answers.states);
221
+ }
222
+ else if (proposal.kind === "foundations") {
223
+ const f = proposal.payload;
224
+ const answers = await prompts([
225
+ { type: "number", name: "grid", message: "Grid (px):", initial: f.grid.value, min: 1, max: 32 },
226
+ {
227
+ type: "number",
228
+ name: "control",
229
+ message: "Control radius (px):",
230
+ initial: f.radii.control ?? 4,
231
+ min: 0,
232
+ max: 64
233
+ },
234
+ {
235
+ type: "number",
236
+ name: "container",
237
+ message: "Container radius (px):",
238
+ initial: f.radii.container ?? 8,
239
+ min: 0,
240
+ max: 64
241
+ }
242
+ ], { onCancel: () => ({}) });
243
+ if (typeof answers.grid === "number")
244
+ f.grid.value = answers.grid;
245
+ if (typeof answers.control === "number")
246
+ f.radii.control = answers.control;
247
+ if (typeof answers.container === "number")
248
+ f.radii.container = answers.container;
249
+ }
250
+ }
251
+ async function graduate(target, proposal) {
252
+ if (proposal.kind === "component") {
253
+ await upsertComponent(target, proposal.payload);
254
+ }
255
+ else if (proposal.kind === "foundations") {
256
+ const config = await loadConfig(target);
257
+ const f = proposal.payload;
258
+ config.foundations = {
259
+ grid: f.grid.value,
260
+ radius: {
261
+ control: f.radii.control ?? config.foundations?.radius?.control ?? 4,
262
+ container: f.radii.container ?? config.foundations?.radius?.container ?? 8
263
+ }
264
+ };
265
+ await saveConfig(target, config);
266
+ }
267
+ }
@@ -0,0 +1,93 @@
1
+ import prompts from "prompts";
2
+ import { resolveTarget } from "../utils/paths.js";
3
+ import { loadComponents, upsertComponent } from "../utils/components.js";
4
+ import { generateWiki } from "../generators/wikiGenerator.js";
5
+ import { ui } from "../ui/index.js";
6
+ export async function componentAddCommand(name, options = {}) {
7
+ const target = resolveTarget();
8
+ if (!name) {
9
+ console.log(ui.fail("Component name is required."));
10
+ process.exitCode = 1;
11
+ return;
12
+ }
13
+ let description = options.description;
14
+ let category = options.category;
15
+ let variants = options.variants;
16
+ let states = options.states;
17
+ let files = options.files;
18
+ const noFlags = !description && !category && !variants && !states && !files;
19
+ if (noFlags) {
20
+ console.log(ui.header("Whale Igniter", `component add ${name}`));
21
+ console.log();
22
+ const answers = await prompts([
23
+ { type: "text", name: "description", message: "Description (optional):" },
24
+ {
25
+ type: "text",
26
+ name: "category",
27
+ message: "Category (form, navigation, feedback, layout, ...):"
28
+ },
29
+ {
30
+ type: "text",
31
+ name: "variants",
32
+ message: "Variants comma-separated (optional):"
33
+ },
34
+ {
35
+ type: "text",
36
+ name: "states",
37
+ message: "States comma-separated (hover, focus, disabled, ...):"
38
+ },
39
+ {
40
+ type: "text",
41
+ name: "files",
42
+ message: "Files comma-separated (optional):"
43
+ }
44
+ ], {
45
+ onCancel: () => {
46
+ console.log();
47
+ console.log(ui.warn("Cancelled."));
48
+ process.exit(130);
49
+ }
50
+ });
51
+ description = answers.description;
52
+ category = answers.category;
53
+ variants = answers.variants;
54
+ states = answers.states;
55
+ files = answers.files;
56
+ }
57
+ const split = (s) => s ? s.split(",").map((x) => x.trim()).filter(Boolean) : undefined;
58
+ const entry = await upsertComponent(target, {
59
+ name,
60
+ description: description?.trim() || undefined,
61
+ category: category?.trim() || undefined,
62
+ variants: split(variants),
63
+ states: split(states),
64
+ files: split(files)
65
+ });
66
+ console.log();
67
+ console.log(ui.ok(`Component registered: ${ui.code(entry.name)}`));
68
+ if (entry.category)
69
+ console.log(` ${ui.kv("category", entry.category, { keyWidth: 8 })}`);
70
+ if (entry.variants?.length)
71
+ console.log(` ${ui.kv("variants", entry.variants.join(", "), { keyWidth: 8 })}`);
72
+ if (options.sync !== false) {
73
+ await generateWiki(target);
74
+ console.log(ui.muted(`${ui.glyph.check} AI context updated`));
75
+ }
76
+ }
77
+ export async function componentListCommand() {
78
+ const target = resolveTarget();
79
+ const components = await loadComponents(target);
80
+ if (components.length === 0) {
81
+ console.log(ui.muted("No components registered yet."));
82
+ console.log(ui.muted("Run `whale component add <name>` to register one."));
83
+ return;
84
+ }
85
+ console.log(ui.section(`${components.length} component(s)`));
86
+ console.log();
87
+ for (const c of components) {
88
+ const head = ui.accent(c.name) + (c.category ? " " + ui.muted("— " + c.category) : "");
89
+ console.log(` ${head}`);
90
+ if (c.description)
91
+ console.log(` ${ui.muted(c.description)}`);
92
+ }
93
+ }