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,207 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { resolveTarget } from "../utils/paths.js";
4
+ import { loadConfig } from "../utils/config.js";
5
+ import { upsertComponent } from "../utils/components.js";
6
+ import { generateWiki } from "../generators/wikiGenerator.js";
7
+ import { spacingClass, radiusClass, accentColorFamily } from "../scanner/tailwindMapper.js";
8
+ import { ui } from "../ui/index.js";
9
+ const CATEGORY_TEMPLATES = {
10
+ form: "control",
11
+ navigation: "control",
12
+ feedback: "container",
13
+ surface: "container",
14
+ layout: "container"
15
+ };
16
+ /**
17
+ * `whale create component <Name>` generates a .tsx file from a template,
18
+ * using the project's current foundations so the output respects them
19
+ * by construction. Two scaffolds:
20
+ *
21
+ * - Control template (button-like): uses control radius, small padding,
22
+ * accepts variants (primary, secondary, ghost) and states.
23
+ * - Container template (card-like): uses container radius, larger
24
+ * padding, no variants by default.
25
+ *
26
+ * The choice is driven by category. If no category is passed and the
27
+ * name looks like a control (Button, Input, Toggle...), we pick control.
28
+ *
29
+ * The generator deliberately produces *typed* React components with
30
+ * `forwardRef` for control-like primitives — that matches the patterns
31
+ * the scanner detects, so created components are scanner-friendly too.
32
+ */
33
+ export async function createComponentCommand(name, options = {}) {
34
+ const target = resolveTarget();
35
+ if (!name || !/^[A-Z][A-Za-z0-9]*$/.test(name)) {
36
+ console.log(ui.fail("Component name must be PascalCase (e.g. `Button`, `UserCard`)."));
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+ const config = await loadConfig(target);
41
+ const grid = config.foundations?.grid ?? 8;
42
+ const controlRadius = config.foundations?.radius?.control ?? 4;
43
+ const containerRadius = config.foundations?.radius?.container ?? 8;
44
+ const accent = accentColorFamily(config.branding?.accent);
45
+ const variants = options.variants?.split(",").map((s) => s.trim()).filter(Boolean);
46
+ const states = options.states?.split(",").map((s) => s.trim()).filter(Boolean);
47
+ const category = options.category ?? inferCategoryFromName(name);
48
+ const templateKind = category && CATEGORY_TEMPLATES[category] ? CATEGORY_TEMPLATES[category] : (looksLikeControl(name) ? "control" : "container");
49
+ // Output path: by default `src/components/<Name>.tsx`. Configurable.
50
+ const outDir = options.outDir ?? "src/components";
51
+ const filename = `${name}.tsx`;
52
+ const fileRel = path.join(outDir, filename).replace(/\\/g, "/");
53
+ const fileAbs = path.join(target, fileRel);
54
+ if (await fs.pathExists(fileAbs)) {
55
+ if (!options.force) {
56
+ console.log(ui.fail(`${ui.path(fileRel)} already exists. Use --force to overwrite.`));
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+ console.log(ui.warn(`Overwriting ${ui.path(fileRel)} (--force).`));
61
+ }
62
+ await fs.ensureDir(path.dirname(fileAbs));
63
+ const source = templateKind === "control"
64
+ ? renderControlTemplate({
65
+ name,
66
+ grid,
67
+ radius: controlRadius,
68
+ accent,
69
+ variants: variants && variants.length > 0 ? variants : ["primary", "secondary", "ghost"],
70
+ states: states && states.length > 0 ? states : ["hover", "focus", "disabled"]
71
+ })
72
+ : renderContainerTemplate({
73
+ name,
74
+ grid,
75
+ radius: containerRadius
76
+ });
77
+ await fs.writeFile(fileAbs, source, "utf8");
78
+ console.log(ui.ok(`Created ${ui.path(fileRel)}`));
79
+ console.log(` ${ui.kv("template", templateKind, { keyWidth: 8 })}`);
80
+ console.log(` ${ui.kv("radius", `${templateKind === "control" ? controlRadius : containerRadius}px`, { keyWidth: 8 })}`);
81
+ console.log(` ${ui.kv("grid", `${grid}px`, { keyWidth: 8 })}`);
82
+ if (variants && variants.length > 0)
83
+ console.log(` ${ui.kv("variants", variants.join(", "), { keyWidth: 8 })}`);
84
+ // Auto-register in the catalog unless explicitly skipped.
85
+ if (!options.noRegister) {
86
+ await upsertComponent(target, {
87
+ name,
88
+ category,
89
+ variants: variants && variants.length > 0 ? variants : templateKind === "control" ? ["primary", "secondary", "ghost"] : undefined,
90
+ states: states && states.length > 0 ? states : templateKind === "control" ? ["hover", "focus", "disabled"] : undefined,
91
+ files: [fileRel]
92
+ });
93
+ console.log(ui.muted(` registered in intelligence/components.json`));
94
+ }
95
+ if (!options.noSync && !options.noRegister) {
96
+ await generateWiki(target);
97
+ console.log(ui.muted(`${ui.glyph.check} AI context updated`));
98
+ }
99
+ }
100
+ // ---------------------------------------------------------------------------
101
+ // Heuristics & templates
102
+ // ---------------------------------------------------------------------------
103
+ function inferCategoryFromName(name) {
104
+ const lower = name.toLowerCase();
105
+ if (/button|input|select|checkbox|radio|toggle|switch|slider|textarea/.test(lower))
106
+ return "form";
107
+ if (/nav|menu|header|sidebar|breadcrumb|tab|tabs/.test(lower))
108
+ return "navigation";
109
+ if (/modal|dialog|toast|alert|notification|popover|tooltip/.test(lower))
110
+ return "feedback";
111
+ if (/layout|grid|container|stack|cluster/.test(lower))
112
+ return "layout";
113
+ if (/card|panel|section|hero|cta|surface/.test(lower))
114
+ return "surface";
115
+ return undefined;
116
+ }
117
+ function looksLikeControl(name) {
118
+ return /button|input|select|checkbox|radio|toggle|switch|slider|tab|link/i.test(name);
119
+ }
120
+ function renderControlTemplate(args) {
121
+ const { name, grid, radius, accent, variants, states } = args;
122
+ // Padding: 2× grid horizontal, 1× grid vertical (the classic control rhythm).
123
+ // For an 8px grid this yields px-4 py-2; for 4px it yields px-2 py-1.
124
+ const px = spacingClass("px", Math.max(grid * 2, 8));
125
+ const py = spacingClass("py", grid);
126
+ const rounded = radiusClass("rounded", radius);
127
+ const variantTypeUnion = variants.map((v) => `"${v}"`).join(" | ");
128
+ const variantStylesEntries = variants.map((v) => ` ${v}: "${variantClasses(v, accent)}"`).join(",\n");
129
+ const hasDisabled = states.includes("disabled");
130
+ const hasFocus = states.includes("focus");
131
+ return `import React, { forwardRef } from "react";
132
+
133
+ type ${name}Variant = ${variantTypeUnion};
134
+
135
+ type ${name}Props = React.ButtonHTMLAttributes<HTMLButtonElement> & {
136
+ variant?: ${name}Variant;
137
+ };
138
+
139
+ // Generated by Whale Igniter. Foundations applied:
140
+ // grid ${grid}px padding uses multiples of ${grid}
141
+ // radius ${radius}px control radius from whale.config.json
142
+ // states ${states.join(", ")}
143
+
144
+ const baseClasses =
145
+ "inline-flex items-center justify-center ${px} ${py} ${rounded} text-sm font-medium transition-colors${hasFocus ? " focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2" : ""}${hasDisabled ? " disabled:opacity-50 disabled:pointer-events-none" : ""}";
146
+
147
+ const variantClasses: Record<${name}Variant, string> = {
148
+ ${variantStylesEntries}
149
+ };
150
+
151
+ export const ${name} = forwardRef<HTMLButtonElement, ${name}Props>(
152
+ ({ variant = "${variants[0]}", className, children, ...rest }, ref) => {
153
+ const composed = \`\${baseClasses} \${variantClasses[variant]}\${className ? \` \${className}\` : ""}\`;
154
+ return (
155
+ <button ref={ref} className={composed} {...rest}>
156
+ {children}
157
+ </button>
158
+ );
159
+ }
160
+ );
161
+
162
+ ${name}.displayName = "${name}";
163
+ `;
164
+ }
165
+ function variantClasses(variant, accent) {
166
+ switch (variant) {
167
+ case "primary":
168
+ return `bg-${accent}-600 text-white hover:bg-${accent}-700 focus-visible:ring-${accent}-500`;
169
+ case "secondary":
170
+ return `bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-400`;
171
+ case "ghost":
172
+ return `bg-transparent text-gray-700 hover:bg-gray-100 focus-visible:ring-gray-400`;
173
+ case "destructive":
174
+ return `bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500`;
175
+ case "outline":
176
+ return `bg-transparent text-gray-900 border border-gray-300 hover:bg-gray-50 focus-visible:ring-gray-400`;
177
+ default:
178
+ return `bg-${accent}-100 text-${accent}-900 hover:bg-${accent}-200`;
179
+ }
180
+ }
181
+ function renderContainerTemplate(args) {
182
+ const { name, grid, radius } = args;
183
+ // Container padding: 3× grid (24px on an 8px grid is a common card padding).
184
+ const p = spacingClass("p", grid * 3);
185
+ const rounded = radiusClass("rounded", radius);
186
+ return `import React from "react";
187
+
188
+ type ${name}Props = React.HTMLAttributes<HTMLDivElement> & {
189
+ children?: React.ReactNode;
190
+ };
191
+
192
+ // Generated by Whale Igniter. Foundations applied:
193
+ // grid ${grid}px padding uses multiples of ${grid}
194
+ // radius ${radius}px container radius from whale.config.json
195
+
196
+ const baseClasses = "${p} ${rounded} bg-white border border-gray-200 shadow-sm";
197
+
198
+ export function ${name}({ className, children, ...rest }: ${name}Props) {
199
+ const composed = \`\${baseClasses}\${className ? \` \${className}\` : ""}\`;
200
+ return (
201
+ <div className={composed} {...rest}>
202
+ {children}
203
+ </div>
204
+ );
205
+ }
206
+ `;
207
+ }
@@ -0,0 +1,98 @@
1
+ import prompts from "prompts";
2
+ import { resolveTarget } from "../utils/paths.js";
3
+ import { appendDecision } from "../utils/decisions.js";
4
+ import { generateWiki } from "../generators/wikiGenerator.js";
5
+ import { ui } from "../ui/index.js";
6
+ const CATEGORIES = [
7
+ "architecture",
8
+ "design-system",
9
+ "product",
10
+ "tooling",
11
+ "convention"
12
+ ];
13
+ export async function decisionCommand(options = {}) {
14
+ const target = resolveTarget();
15
+ // If essentials are missing, fall into interactive mode.
16
+ const needsPrompt = !options.title || !options.decision;
17
+ let title = options.title;
18
+ let category = options.category ?? "architecture";
19
+ let context = options.context;
20
+ let decisionText = options.decision;
21
+ let consequences = options.consequences;
22
+ if (needsPrompt) {
23
+ console.log(ui.header("Whale Igniter", "decision"));
24
+ console.log();
25
+ console.log(ui.muted("Press Ctrl+C to cancel."));
26
+ console.log();
27
+ const answers = await prompts([
28
+ {
29
+ type: title ? null : "text",
30
+ name: "title",
31
+ message: "Title (one line):",
32
+ validate: (v) => (v.trim().length > 0 ? true : "Required")
33
+ },
34
+ {
35
+ type: options.category ? null : "select",
36
+ name: "category",
37
+ message: "Category:",
38
+ choices: CATEGORIES.map((c) => ({ title: c, value: c })),
39
+ initial: 0
40
+ },
41
+ {
42
+ type: context ? null : "text",
43
+ name: "context",
44
+ message: "Context (why this came up, optional):"
45
+ },
46
+ {
47
+ type: decisionText ? null : "text",
48
+ name: "decision",
49
+ message: "Decision (what was decided):",
50
+ validate: (v) => (v.trim().length > 0 ? true : "Required")
51
+ },
52
+ {
53
+ type: consequences ? null : "text",
54
+ name: "consequences",
55
+ message: "Consequences (tradeoffs, optional):"
56
+ }
57
+ ], {
58
+ onCancel: () => {
59
+ console.log();
60
+ console.log(ui.warn("Cancelled."));
61
+ process.exit(130);
62
+ }
63
+ });
64
+ title = title ?? answers.title;
65
+ category = (category ?? answers.category);
66
+ context = context ?? answers.context;
67
+ decisionText = decisionText ?? answers.decision;
68
+ consequences = consequences ?? answers.consequences;
69
+ }
70
+ if (!title || !decisionText) {
71
+ console.log(ui.fail("Title and decision are required."));
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+ if (!CATEGORIES.includes(category)) {
76
+ console.log(ui.fail(`Unknown category: ${category}`));
77
+ console.log(ui.muted(`Valid categories: ${CATEGORIES.join(", ")}`));
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+ const created = await appendDecision(target, {
82
+ title,
83
+ category,
84
+ context: context?.trim() || undefined,
85
+ decision: decisionText,
86
+ consequences: consequences?.trim() || undefined
87
+ });
88
+ console.log();
89
+ console.log(ui.ok(`Decision recorded ${ui.muted("— " + created.id.slice(0, 8))}`));
90
+ console.log(` ${ui.accent(created.title)}`);
91
+ console.log(` ${ui.kv("category", created.category, { keyWidth: 8 })}`);
92
+ if (options.sync !== false) {
93
+ console.log();
94
+ console.log(ui.muted("Regenerating AI context..."));
95
+ await generateWiki(target);
96
+ console.log(ui.ok("CLAUDE.md and wiki updated"));
97
+ }
98
+ }
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+ import ora from "ora";
3
+ import { resolveTarget } from "../utils/paths.js";
4
+ import { loadConfig } from "../utils/config.js";
5
+ import { getPack } from "../utils/registry.js";
6
+ import { runGenerator } from "../generators/markdownGenerator.js";
7
+ import { generateValidationReport } from "../generators/reportGenerator.js";
8
+ import { ui } from "../ui/index.js";
9
+ export async function docsCommand(targetArg) {
10
+ const target = resolveTarget(targetArg);
11
+ const spinner = ora("Generating operational docs").start();
12
+ const config = await loadConfig(target);
13
+ const written = [];
14
+ // Always include the validation report — it's the core deliverable.
15
+ const reportPath = await generateValidationReport(target);
16
+ written.push(reportPath);
17
+ // Then run any generator-kind packs the project has installed.
18
+ for (const packName of config.packs ?? []) {
19
+ const pack = getPack(packName);
20
+ if (!pack || pack.kind !== "generator")
21
+ continue;
22
+ try {
23
+ const out = await runGenerator(target, pack);
24
+ written.push(out);
25
+ }
26
+ catch (err) {
27
+ spinner.warn(`Generator "${packName}" failed: ${err.message}`);
28
+ }
29
+ }
30
+ spinner.succeed("Docs generated");
31
+ for (const f of written) {
32
+ console.log(` ${ui.ok(ui.path(path.relative(target, f)))}`);
33
+ }
34
+ }
@@ -0,0 +1,212 @@
1
+ import path from "node:path";
2
+ import prompts from "prompts";
3
+ import { initCommand } from "./init.js";
4
+ import { loadConfig, saveConfig, DEFAULT_CONFIG } from "../utils/config.js";
5
+ import { getPack, listPacks } from "../utils/registry.js";
6
+ import { validateCss } from "../validators/cssValidator.js";
7
+ import { generateWiki } from "../generators/wikiGenerator.js";
8
+ import { ui } from "../ui/index.js";
9
+ import { PACKAGE_VERSION } from "../version.js";
10
+ const OPINIONATED_PACKS = ["lighthouse", "forge", "scribe"];
11
+ const MINIMAL_PACKS = ["lighthouse"];
12
+ export async function igniteCommand(projectName = "whale-project", options = {}) {
13
+ const mode = options.interactive
14
+ ? "interactive"
15
+ : options.minimal
16
+ ? "minimal"
17
+ : "opinionated";
18
+ console.log();
19
+ console.log(ui.header("Whale Igniter", `ignite • ${mode}`));
20
+ console.log();
21
+ // ---- Resolve config based on mode -----------------------------------------
22
+ let config;
23
+ if (mode === "interactive") {
24
+ config = await runWizard(projectName);
25
+ }
26
+ else if (mode === "minimal") {
27
+ config = {
28
+ ...DEFAULT_CONFIG,
29
+ projectName,
30
+ packs: MINIMAL_PACKS,
31
+ aiTargets: ["claude"]
32
+ };
33
+ }
34
+ else {
35
+ config = {
36
+ ...DEFAULT_CONFIG,
37
+ projectName,
38
+ packs: OPINIONATED_PACKS,
39
+ aiTargets: ["claude"]
40
+ };
41
+ }
42
+ // ---- Step 1: scaffold workspace -------------------------------------------
43
+ const target = await initCommand(projectName, { config, silent: true });
44
+ const targetRel = path.relative(process.cwd(), target) || ".";
45
+ console.log(ui.ok(`Workspace created at ${ui.path(targetRel)}`));
46
+ // ---- Step 2: confirm packs against registry -------------------------------
47
+ console.log();
48
+ console.log(ui.section("Packs"));
49
+ const finalConfig = await loadConfig(target);
50
+ const validPacks = [];
51
+ const packLines = [];
52
+ for (const name of finalConfig.packs ?? []) {
53
+ const pack = getPack(name);
54
+ if (!pack) {
55
+ packLines.push(ui.warn(`${name} — not in registry, skipped`));
56
+ continue;
57
+ }
58
+ validPacks.push(name);
59
+ packLines.push(`${ui.glyph.check} ${ui.code(name)} ${ui.muted(`(${pack.kind})`)} — ${pack.description}`);
60
+ }
61
+ console.log(ui.indent(packLines.join("\n")));
62
+ finalConfig.packs = validPacks;
63
+ finalConfig.ignited = {
64
+ at: new Date().toISOString(),
65
+ mode,
66
+ version: PACKAGE_VERSION
67
+ };
68
+ await saveConfig(target, finalConfig);
69
+ // ---- Step 3: generate AI context ------------------------------------------
70
+ if (mode === "minimal") {
71
+ console.log();
72
+ console.log(ui.note("Skipping AI context generation (minimal mode)."));
73
+ console.log(ui.indent(ui.muted("Run `whale sync` when you want to generate it.")));
74
+ }
75
+ else {
76
+ console.log();
77
+ console.log(ui.section("AI context"));
78
+ const { rootFiles, wikiFiles } = await generateWiki(target);
79
+ const ctxLines = [];
80
+ for (const f of rootFiles) {
81
+ ctxLines.push(`${ui.glyph.check} ${ui.path(path.relative(target, f))}`);
82
+ }
83
+ ctxLines.push(ui.muted(`+ ${wikiFiles.length} file(s) in llm-wiki/`));
84
+ console.log(ui.indent(ctxLines.join("\n")));
85
+ }
86
+ // ---- Step 4: baseline validation ------------------------------------------
87
+ if (mode !== "minimal") {
88
+ console.log();
89
+ console.log(ui.section("Baseline validation"));
90
+ const issues = await validateCss(target);
91
+ if (issues.length === 0) {
92
+ console.log(ui.indent(ui.ok("Clean baseline") + " " + ui.muted("(no CSS to scan yet — expected on a fresh project)")));
93
+ }
94
+ else {
95
+ const errors = issues.filter((i) => i.severity === "error").length;
96
+ const warnings = issues.filter((i) => i.severity === "warning").length;
97
+ console.log(ui.indent(ui.summary([
98
+ { label: "errors", value: errors, tone: errors > 0 ? "danger" : "muted" },
99
+ { label: "warnings", value: warnings, tone: warnings > 0 ? "warn" : "muted" }
100
+ ])));
101
+ console.log(ui.indent(ui.muted("Run `whale validate` for details.")));
102
+ }
103
+ }
104
+ // ---- Done -----------------------------------------------------------------
105
+ console.log();
106
+ console.log(ui.section("Next"));
107
+ const rel = path.relative(process.cwd(), target);
108
+ const steps = [];
109
+ if (rel)
110
+ steps.push([`cd ${rel}`, "enter the workspace"]);
111
+ steps.push(["whale validate", "check current state"]);
112
+ steps.push(["whale decision", "record an architecture decision"]);
113
+ steps.push(["whale component add Button", "register a component"]);
114
+ steps.push(['whale refine "<note>"', "record a validator override"]);
115
+ steps.push(["whale sync", "refresh AI context anytime"]);
116
+ const widest = Math.max(...steps.map(([cmd]) => cmd.length));
117
+ for (const [cmd, desc] of steps) {
118
+ console.log(ui.indent(`${ui.glyph.arrow} ${ui.code(cmd.padEnd(widest))} ${ui.muted(desc)}`));
119
+ }
120
+ console.log();
121
+ if (mode === "minimal") {
122
+ console.log(ui.info("Workspace ready. Run `whale sync` to generate CLAUDE.md when needed."));
123
+ }
124
+ else {
125
+ console.log(ui.info("Your CLAUDE.md is ready. Open the project in Claude Code / Cursor and start working."));
126
+ }
127
+ console.log();
128
+ }
129
+ // ---------------------------------------------------------------------------
130
+ // Interactive wizard — unchanged logic, just uses ui for the header.
131
+ // ---------------------------------------------------------------------------
132
+ async function runWizard(defaultName) {
133
+ console.log(ui.muted("Answer a few questions to shape your workspace. Ctrl+C to cancel."));
134
+ 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);
195
+ }
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
+ }
210
+ }
211
+ };
212
+ }
@@ -0,0 +1,66 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import ora from "ora";
4
+ import { DEFAULT_CONFIG, saveConfig } from "../utils/config.js";
5
+ import { ui } from "../ui/index.js";
6
+ /**
7
+ * Folders Whale always expects to exist. Created empty so subsequent
8
+ * commands never need to defensively check.
9
+ */
10
+ const FOLDERS = [
11
+ "foundations",
12
+ "recipes",
13
+ "patterns",
14
+ "packs",
15
+ "docs",
16
+ "llm-wiki",
17
+ "intelligence",
18
+ "examples",
19
+ "templates"
20
+ ];
21
+ /**
22
+ * Empty initial files for the intelligence store. Wiring them in from
23
+ * day one means `whale validate`, `whale sync` etc. don't fail on a
24
+ * fresh project.
25
+ */
26
+ async function ensureIntelligenceFiles(target) {
27
+ const intelligenceDir = path.join(target, "intelligence");
28
+ await fs.ensureDir(intelligenceDir);
29
+ const seeds = {
30
+ "refinements.json": [],
31
+ "decisions.json": [],
32
+ "components.json": []
33
+ };
34
+ for (const [name, content] of Object.entries(seeds)) {
35
+ const file = path.join(intelligenceDir, name);
36
+ if (!(await fs.pathExists(file))) {
37
+ await fs.writeJson(file, content, { spaces: 2 });
38
+ }
39
+ }
40
+ }
41
+ export async function initCommand(projectName = "whale-project", options = {}) {
42
+ const target = path.resolve(process.cwd(), projectName);
43
+ const spinner = options.silent
44
+ ? null
45
+ : ora(`Scaffolding ${projectName}`).start();
46
+ await fs.ensureDir(target);
47
+ for (const folder of FOLDERS) {
48
+ await fs.ensureDir(path.join(target, folder));
49
+ }
50
+ // Persist config (either user-provided or defaults), but keep projectName.
51
+ const config = {
52
+ ...DEFAULT_CONFIG,
53
+ ...(options.config ?? {}),
54
+ projectName: options.config?.projectName ?? projectName
55
+ };
56
+ await saveConfig(target, config);
57
+ await ensureIntelligenceFiles(target);
58
+ if (spinner) {
59
+ spinner.succeed(`Workspace ready at ${target}`);
60
+ console.log(` ${ui.ok(ui.path("whale.config.json"))}`);
61
+ console.log(` ${ui.ok(ui.path("intelligence/") + ui.muted(" (refinements, decisions, components)"))}`);
62
+ console.log(` ${ui.ok(ui.path("llm-wiki/") + ui.muted(" (empty — run `whale sync` to populate)"))}`);
63
+ console.log(ui.muted(` next: cd ${projectName} && whale validate`));
64
+ }
65
+ return target;
66
+ }