whale-igniter 1.3.7 → 1.5.1
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/CHANGELOG.md +48 -0
- package/README.md +86 -11
- package/dist/commands/atlas.js +196 -0
- package/dist/commands/createComponent.js +3 -0
- package/dist/commands/createLanding.js +581 -52
- package/dist/commands/forge.js +159 -0
- package/dist/commands/scribe.js +189 -0
- package/dist/index.js +51 -1
- package/dist/selene/cache.js +11 -0
- package/dist/ui/splash.js +13 -8
- package/dist/version.js +1 -1
- package/docs/ROADMAP.md +2 -2
- package/package.json +3 -1
|
@@ -0,0 +1,159 @@
|
|
|
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 { loadComponents } from "../utils/components.js";
|
|
6
|
+
import { ui } from "../ui/index.js";
|
|
7
|
+
export async function forgeCommand(componentName, options = {}) {
|
|
8
|
+
const target = resolveTarget();
|
|
9
|
+
const config = await loadConfig(target);
|
|
10
|
+
const components = await loadComponents(target);
|
|
11
|
+
const component = components.find((c) => c.name === componentName);
|
|
12
|
+
if (!component) {
|
|
13
|
+
console.log(ui.fail(`Component "${componentName}" not found in intelligence/components.json.`));
|
|
14
|
+
console.log(ui.muted(` Run \`whale create component ${componentName}\` first, or \`whale component add ${componentName}\`.`));
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const grid = config.foundations?.grid ?? 8;
|
|
19
|
+
const controlRadius = config.foundations?.radius?.control ?? 2;
|
|
20
|
+
const containerRadius = config.foundations?.radius?.container ?? 4;
|
|
21
|
+
const accent = config.branding?.accent ?? "blue";
|
|
22
|
+
const isControl = isControlCategory(component.category);
|
|
23
|
+
const radius = isControl ? controlRadius : containerRadius;
|
|
24
|
+
const px = grid * 2;
|
|
25
|
+
const py = grid;
|
|
26
|
+
const outDir = options.out ?? "docs/forge";
|
|
27
|
+
const fileRel = path.join(outDir, `${componentName}.md`).replace(/\\/g, "/");
|
|
28
|
+
const fileAbs = path.join(target, fileRel);
|
|
29
|
+
if (await fs.pathExists(fileAbs) && !options.force) {
|
|
30
|
+
console.log(ui.fail(`${ui.path(fileRel)} already exists. Use --force to overwrite.`));
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await fs.ensureDir(path.dirname(fileAbs));
|
|
35
|
+
const content = renderForgeContract({
|
|
36
|
+
component,
|
|
37
|
+
grid,
|
|
38
|
+
radius,
|
|
39
|
+
px,
|
|
40
|
+
py,
|
|
41
|
+
accent,
|
|
42
|
+
isControl,
|
|
43
|
+
});
|
|
44
|
+
await fs.writeFile(fileAbs, content, "utf8");
|
|
45
|
+
console.log(ui.ok(`Forge contract → ${ui.path(fileRel)}`));
|
|
46
|
+
console.log(` ${ui.kv("component", componentName, { keyWidth: 10 })}`);
|
|
47
|
+
console.log(` ${ui.kv("template", isControl ? "control" : "container", { keyWidth: 10 })}`);
|
|
48
|
+
console.log(` ${ui.kv("grid", `${grid}px`, { keyWidth: 10 })}`);
|
|
49
|
+
console.log(` ${ui.kv("radius", `${radius}px`, { keyWidth: 10 })}`);
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Renderer
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
function renderForgeContract(args) {
|
|
55
|
+
const { component, grid, radius, px, py, accent, isControl } = args;
|
|
56
|
+
const { name, category, variants = [], states = [], files = [], tokens = [] } = component;
|
|
57
|
+
const paddingNote = isControl
|
|
58
|
+
? `${px}px horizontal / ${py}px vertical (${grid}px grid × 2 / × 1)`
|
|
59
|
+
: `${py * 3}px all sides (${grid}px grid × 3)`;
|
|
60
|
+
const derivedTokens = deriveTokens(variants, accent, tokens);
|
|
61
|
+
const checklist = buildChecklist(isControl, variants, states);
|
|
62
|
+
const lines = [
|
|
63
|
+
`# Forge — ${name}`,
|
|
64
|
+
"",
|
|
65
|
+
`> Auto-generated implementation contract. Keep in sync with \`intelligence/components.json\`.`,
|
|
66
|
+
"",
|
|
67
|
+
"## Component spec",
|
|
68
|
+
"",
|
|
69
|
+
`| Property | Value |`,
|
|
70
|
+
`|-----------|-------|`,
|
|
71
|
+
`| Template | ${isControl ? "control" : "container"} |`,
|
|
72
|
+
`| Category | ${category ?? "unspecified"} |`,
|
|
73
|
+
`| Grid | ${grid}px |`,
|
|
74
|
+
`| Radius | ${radius}px |`,
|
|
75
|
+
`| Padding | ${paddingNote} |`,
|
|
76
|
+
];
|
|
77
|
+
if (variants.length > 0) {
|
|
78
|
+
lines.push(`| Variants | ${variants.join(", ")} |`);
|
|
79
|
+
}
|
|
80
|
+
if (states.length > 0) {
|
|
81
|
+
lines.push(`| States | ${states.join(", ")} |`);
|
|
82
|
+
}
|
|
83
|
+
lines.push("", "## Design tokens", "");
|
|
84
|
+
if (derivedTokens.length > 0) {
|
|
85
|
+
for (const t of derivedTokens) {
|
|
86
|
+
lines.push(`- \`${t.token}\` — ${t.usage}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
lines.push("- No tokens registered. Run `whale selene describe` to infer them.");
|
|
91
|
+
}
|
|
92
|
+
lines.push("", "## Implementation checklist", "");
|
|
93
|
+
for (const item of checklist) {
|
|
94
|
+
lines.push(`- [ ] ${item}`);
|
|
95
|
+
}
|
|
96
|
+
if (files.length > 0) {
|
|
97
|
+
lines.push("", "## Files", "");
|
|
98
|
+
for (const f of files) {
|
|
99
|
+
lines.push(`- \`${f}\``);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
lines.push("", "---", `_Generated by Whale Igniter · \`whale forge ${name}\`_`, "");
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
function deriveTokens(variants, accent, existing) {
|
|
106
|
+
if (existing.length > 0) {
|
|
107
|
+
return existing.map((t) => ({ token: t, usage: "registered token" }));
|
|
108
|
+
}
|
|
109
|
+
const result = [];
|
|
110
|
+
for (const v of variants) {
|
|
111
|
+
switch (v) {
|
|
112
|
+
case "primary":
|
|
113
|
+
result.push({ token: `--color-${accent}-600`, usage: "primary background" });
|
|
114
|
+
result.push({ token: `--color-${accent}-700`, usage: "primary hover" });
|
|
115
|
+
break;
|
|
116
|
+
case "secondary":
|
|
117
|
+
result.push({ token: `--color-gray-100`, usage: "secondary background" });
|
|
118
|
+
result.push({ token: `--color-gray-200`, usage: "secondary hover" });
|
|
119
|
+
break;
|
|
120
|
+
case "ghost":
|
|
121
|
+
result.push({ token: `--color-gray-100`, usage: "ghost hover background" });
|
|
122
|
+
break;
|
|
123
|
+
case "destructive":
|
|
124
|
+
result.push({ token: `--color-red-600`, usage: "destructive background" });
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
function buildChecklist(isControl, variants, states) {
|
|
131
|
+
const items = [];
|
|
132
|
+
if (isControl) {
|
|
133
|
+
items.push("forwardRef implemented with correct HTMLElement type");
|
|
134
|
+
items.push("baseClasses use grid-aligned padding");
|
|
135
|
+
items.push(`radius matches control radius from whale.config.json`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
items.push("Container uses grid-aligned padding (grid × 3)");
|
|
139
|
+
items.push("radius matches container radius from whale.config.json");
|
|
140
|
+
}
|
|
141
|
+
if (states.includes("focus") || states.includes("focus-visible")) {
|
|
142
|
+
items.push(":focus-visible ring is visible at 2px offset");
|
|
143
|
+
}
|
|
144
|
+
if (states.includes("disabled")) {
|
|
145
|
+
items.push("disabled state reduces opacity and removes pointer-events");
|
|
146
|
+
}
|
|
147
|
+
if (states.includes("hover")) {
|
|
148
|
+
items.push("hover state uses a darker/lighter token variant, not raw hex");
|
|
149
|
+
}
|
|
150
|
+
for (const v of variants) {
|
|
151
|
+
items.push(`Variant "${v}" visually tested at all states`);
|
|
152
|
+
}
|
|
153
|
+
items.push("Color values come from tokens (var(--color-*)), not raw hex");
|
|
154
|
+
items.push("Component registered in intelligence/components.json");
|
|
155
|
+
return items;
|
|
156
|
+
}
|
|
157
|
+
function isControlCategory(category) {
|
|
158
|
+
return ["form", "navigation"].includes(category ?? "");
|
|
159
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
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 { loadConfig } from "../utils/config.js";
|
|
6
|
+
import { loadComponents } from "../utils/components.js";
|
|
7
|
+
import { loadDecisions } from "../utils/decisions.js";
|
|
8
|
+
import { loadRefinements } from "../utils/refinements.js";
|
|
9
|
+
import { resolveProvider } from "../selene/providers.js";
|
|
10
|
+
import { callProvider } from "../selene/apiClient.js";
|
|
11
|
+
import { ui } from "../ui/index.js";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// whale scribe readme <ComponentName>
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export async function scribeReadmeCommand(componentName, options = {}) {
|
|
16
|
+
const target = resolveTarget();
|
|
17
|
+
const config = await loadConfig(target);
|
|
18
|
+
const components = await loadComponents(target);
|
|
19
|
+
const component = components.find((c) => c.name === componentName);
|
|
20
|
+
if (!component) {
|
|
21
|
+
console.log(ui.fail(`Component "${componentName}" not found in intelligence/components.json.`));
|
|
22
|
+
console.log(ui.muted(` Run \`whale create component ${componentName}\` first.`));
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const outDir = options.out ?? "docs/scribe";
|
|
27
|
+
const fileRel = path.join(outDir, `${componentName}.md`).replace(/\\/g, "/");
|
|
28
|
+
const fileAbs = path.join(target, fileRel);
|
|
29
|
+
if (await fs.pathExists(fileAbs) && !options.force) {
|
|
30
|
+
console.log(ui.fail(`${ui.path(fileRel)} already exists. Use --force to overwrite.`));
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const grid = config.foundations?.grid ?? 8;
|
|
35
|
+
const controlRadius = config.foundations?.radius?.control ?? 2;
|
|
36
|
+
const containerRadius = config.foundations?.radius?.container ?? 4;
|
|
37
|
+
const accent = config.branding?.accent ?? "blue";
|
|
38
|
+
const projectName = config.projectName ?? "this project";
|
|
39
|
+
const prompt = buildReadmePrompt({
|
|
40
|
+
componentName,
|
|
41
|
+
component,
|
|
42
|
+
grid,
|
|
43
|
+
controlRadius,
|
|
44
|
+
containerRadius,
|
|
45
|
+
accent,
|
|
46
|
+
projectName,
|
|
47
|
+
});
|
|
48
|
+
if (options.promptOnly) {
|
|
49
|
+
console.log(prompt);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const resolved = resolveProvider(config);
|
|
53
|
+
if (!resolved) {
|
|
54
|
+
console.log(ui.warn("No API key found — printing prompt instead."));
|
|
55
|
+
console.log(ui.muted(" Set ANTHROPIC_API_KEY or OPENAI_API_KEY to use AI generation."));
|
|
56
|
+
console.log("\n" + prompt);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const spinner = ora(`Scribe · generating README for ${componentName}`).start();
|
|
60
|
+
const result = await callProvider(resolved, { prompt, maxTokens: 1200, temperature: 0.4 });
|
|
61
|
+
if (!result.ok) {
|
|
62
|
+
spinner.fail(`API error: ${result.error}`);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
spinner.succeed(`README generated`);
|
|
67
|
+
const content = result.text.trim();
|
|
68
|
+
await fs.ensureDir(path.dirname(fileAbs));
|
|
69
|
+
await fs.writeFile(fileAbs, content + "\n", "utf8");
|
|
70
|
+
console.log(ui.ok(`Scribe → ${ui.path(fileRel)}`));
|
|
71
|
+
console.log(ui.muted(` provider: ${resolved.provider} · ${resolved.model}`));
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// whale scribe overview
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
export async function scribeOverviewCommand(options = {}) {
|
|
77
|
+
const target = resolveTarget();
|
|
78
|
+
const config = await loadConfig(target);
|
|
79
|
+
const components = await loadComponents(target);
|
|
80
|
+
const decisions = await loadDecisions(target);
|
|
81
|
+
const refinements = await loadRefinements(target);
|
|
82
|
+
const outDir = options.out ?? "docs/scribe";
|
|
83
|
+
const fileRel = path.join(outDir, "overview.md").replace(/\\/g, "/");
|
|
84
|
+
const fileAbs = path.join(target, fileRel);
|
|
85
|
+
if (await fs.pathExists(fileAbs) && !options.force) {
|
|
86
|
+
console.log(ui.fail(`${ui.path(fileRel)} already exists. Use --force to overwrite.`));
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const prompt = buildOverviewPrompt({ config, components, decisions, refinements });
|
|
91
|
+
if (options.promptOnly) {
|
|
92
|
+
console.log(prompt);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const resolved = resolveProvider(config);
|
|
96
|
+
if (!resolved) {
|
|
97
|
+
console.log(ui.warn("No API key found — printing prompt instead."));
|
|
98
|
+
console.log(ui.muted(" Set ANTHROPIC_API_KEY or OPENAI_API_KEY to use AI generation."));
|
|
99
|
+
console.log("\n" + prompt);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const spinner = ora("Scribe · generating project overview").start();
|
|
103
|
+
const result = await callProvider(resolved, { prompt, maxTokens: 1600, temperature: 0.4 });
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
spinner.fail(`API error: ${result.error}`);
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
spinner.succeed("Overview generated");
|
|
110
|
+
const content = result.text.trim();
|
|
111
|
+
await fs.ensureDir(path.dirname(fileAbs));
|
|
112
|
+
await fs.writeFile(fileAbs, content + "\n", "utf8");
|
|
113
|
+
console.log(ui.ok(`Scribe → ${ui.path(fileRel)}`));
|
|
114
|
+
console.log(ui.muted(` provider: ${resolved.provider} · ${resolved.model}`));
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Prompt builders
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
function buildReadmePrompt(args) {
|
|
120
|
+
const { componentName, component, grid, controlRadius, containerRadius, accent, projectName } = args;
|
|
121
|
+
const { category, description, variants = [], states = [], files = [], tokens = [] } = component;
|
|
122
|
+
return [
|
|
123
|
+
`# Scribe task — README for component: ${componentName}`,
|
|
124
|
+
"",
|
|
125
|
+
"You are a technical writer generating a component README for a design system.",
|
|
126
|
+
`The project is called "${projectName}" and uses an ${grid}px grid, ${controlRadius}px control radius, ${containerRadius}px container radius, and "${accent}" as accent color.`,
|
|
127
|
+
"",
|
|
128
|
+
"## Component metadata",
|
|
129
|
+
`- Name: ${componentName}`,
|
|
130
|
+
`- Category: ${category ?? "unspecified"}`,
|
|
131
|
+
`- Description: ${description ?? "(none)"}`,
|
|
132
|
+
`- Variants: ${variants.length > 0 ? variants.join(", ") : "none"}`,
|
|
133
|
+
`- States: ${states.length > 0 ? states.join(", ") : "none"}`,
|
|
134
|
+
`- Files: ${files.length > 0 ? files.join(", ") : "not specified"}`,
|
|
135
|
+
`- Tokens: ${tokens.length > 0 ? tokens.join(", ") : "not specified"}`,
|
|
136
|
+
"",
|
|
137
|
+
"## Task",
|
|
138
|
+
`Write a concise, developer-focused README.md for the ${componentName} component.`,
|
|
139
|
+
"Include:",
|
|
140
|
+
"1. A one-paragraph description of what it does and when to use it",
|
|
141
|
+
"2. A usage example (JSX/TSX code block)",
|
|
142
|
+
"3. A props table (name | type | default | description)",
|
|
143
|
+
"4. A section on accessibility (focus, ARIA if relevant)",
|
|
144
|
+
"5. A brief design notes section mentioning the grid and token constraints",
|
|
145
|
+
"",
|
|
146
|
+
"Write in plain Markdown. Be concise — target 150-250 lines max. No preamble or explanation.",
|
|
147
|
+
].join("\n");
|
|
148
|
+
}
|
|
149
|
+
function buildOverviewPrompt(args) {
|
|
150
|
+
const { config, components, decisions, refinements } = args;
|
|
151
|
+
const componentList = components
|
|
152
|
+
.map((c) => `- ${c.name}${c.description ? `: ${c.description}` : ""}`)
|
|
153
|
+
.join("\n") || " (none registered)";
|
|
154
|
+
const decisionList = decisions
|
|
155
|
+
.map((d) => `- ${d.title}: ${d.decision}`)
|
|
156
|
+
.join("\n") || " (none)";
|
|
157
|
+
return [
|
|
158
|
+
"# Scribe task — project overview README",
|
|
159
|
+
"",
|
|
160
|
+
"You are a technical writer generating a project overview README.",
|
|
161
|
+
"",
|
|
162
|
+
"## Project config",
|
|
163
|
+
`- Name: ${config.projectName ?? "unnamed"}`,
|
|
164
|
+
`- Type: ${config.projectType ?? "unspecified"}`,
|
|
165
|
+
`- Stack: ${config.stack ?? "unspecified"}`,
|
|
166
|
+
`- Grid: ${config.foundations?.grid ?? 8}px`,
|
|
167
|
+
`- Control radius: ${config.foundations?.radius?.control ?? 2}px`,
|
|
168
|
+
`- Container radius: ${config.foundations?.radius?.container ?? 4}px`,
|
|
169
|
+
`- Accent: ${config.branding?.accent ?? "unspecified"}`,
|
|
170
|
+
`- Active packs: ${(config.packs ?? []).join(", ") || "none"}`,
|
|
171
|
+
"",
|
|
172
|
+
"## Registered components",
|
|
173
|
+
componentList,
|
|
174
|
+
"",
|
|
175
|
+
"## Architectural decisions",
|
|
176
|
+
decisionList,
|
|
177
|
+
`- Refinements recorded: ${refinements.length}`,
|
|
178
|
+
"",
|
|
179
|
+
"## Task",
|
|
180
|
+
"Write a project overview README.md that a new developer would read to understand:",
|
|
181
|
+
"1. What this project is and who it's for",
|
|
182
|
+
"2. The design system foundations (grid, radius, tokens)",
|
|
183
|
+
"3. The component catalog (what exists, categories)",
|
|
184
|
+
"4. Key architectural decisions",
|
|
185
|
+
"5. How to get started (whale commands to run)",
|
|
186
|
+
"",
|
|
187
|
+
"Write in plain Markdown. Be concise and direct. No preamble or explanation.",
|
|
188
|
+
].join("\n");
|
|
189
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,9 @@ import { adoptReviewCommand, adoptStatusCommand } from "./commands/adoptReview.j
|
|
|
15
15
|
import { insightsCommand, insightsDriftReviewCommand } from "./commands/insights.js";
|
|
16
16
|
import { createComponentCommand } from "./commands/createComponent.js";
|
|
17
17
|
import { createLandingCommand } from "./commands/createLanding.js";
|
|
18
|
+
import { forgeCommand } from "./commands/forge.js";
|
|
19
|
+
import { scribeReadmeCommand, scribeOverviewCommand } from "./commands/scribe.js";
|
|
20
|
+
import { atlasHealthCommand, atlasDecisionsCommand } from "./commands/atlas.js";
|
|
18
21
|
import { themesListCommand } from "./commands/themes.js";
|
|
19
22
|
import { updateTokensCommand } from "./commands/updateTokens.js";
|
|
20
23
|
import { seleneDescribeCommand, seleneAuditCommand, seleneSuggestCommand, seleneApplyCommand, seleneStatusCommand, seleneCacheClearCommand } from "./commands/selene.js";
|
|
@@ -178,13 +181,60 @@ create
|
|
|
178
181
|
create
|
|
179
182
|
.command("landing")
|
|
180
183
|
.description("Generate a static HTML/CSS landing page using current foundations.")
|
|
181
|
-
.option("--sections <list>", "Comma-separated sections (hero,features,proof,contact)")
|
|
184
|
+
.option("--sections <list>", "Comma-separated sections (header,hero,features,proof,contact,footer)")
|
|
182
185
|
.option("--out-dir <path>", "Output directory relative to project root (default: src)")
|
|
183
186
|
.option("--force", "Overwrite generated files if they already exist")
|
|
184
187
|
.option("--no-register", "Don't add generated sections to intelligence/components.json")
|
|
185
188
|
.option("--no-sync", "Don't regenerate CLAUDE.md / wiki after creating")
|
|
186
189
|
.option("--theme <name>", "Visual theme for the generated UI (see `whale themes list`)")
|
|
190
|
+
.option("--content <path>", "Read landing copy from a local JSON file")
|
|
191
|
+
.option("--sample-content", "Create an editable landing.json sample and use it")
|
|
192
|
+
.option("--brief", "Ask a short local brief and generate landing.json")
|
|
193
|
+
.option("--title <text>", "Override the hero headline")
|
|
194
|
+
.option("--eyebrow <text>", "Override the hero eyebrow")
|
|
195
|
+
.option("--tagline <text>", "Override the hero supporting copy")
|
|
196
|
+
.option("--cta <text>", "Override the primary CTA label")
|
|
197
|
+
.option("--cta-href <href>", "Override the primary CTA href")
|
|
187
198
|
.action((opts) => createLandingCommand(opts));
|
|
199
|
+
program
|
|
200
|
+
.command("forge <component>")
|
|
201
|
+
.description("Generate an implementation contract (Markdown) for a registered component.")
|
|
202
|
+
.option("--out <dir>", "Output directory relative to project root (default: docs/forge)")
|
|
203
|
+
.option("--force", "Overwrite the contract if it already exists")
|
|
204
|
+
.action((component, opts) => forgeCommand(component, opts));
|
|
205
|
+
const atlas = program
|
|
206
|
+
.command("atlas")
|
|
207
|
+
.description("Project health and decision intelligence for PMs and tech leads.");
|
|
208
|
+
atlas
|
|
209
|
+
.command("health")
|
|
210
|
+
.description("Generate an AI-powered project health report.")
|
|
211
|
+
.option("--out <dir>", "Output directory (default: docs/atlas)")
|
|
212
|
+
.option("--force", "Overwrite if already exists")
|
|
213
|
+
.option("--prompt-only", "Print the prompt without calling the API")
|
|
214
|
+
.action((opts) => atlasHealthCommand(opts));
|
|
215
|
+
atlas
|
|
216
|
+
.command("decisions")
|
|
217
|
+
.description("List active architectural decisions, optionally filtered.")
|
|
218
|
+
.option("--category <name>", "Filter by category (architecture, design-system, product, tooling, convention)")
|
|
219
|
+
.option("--status <status>", "Filter by status (active, superseded, deprecated) — default: active")
|
|
220
|
+
.action((opts) => atlasDecisionsCommand(opts));
|
|
221
|
+
const scribe = program
|
|
222
|
+
.command("scribe")
|
|
223
|
+
.description("AI-generated documentation for components and projects.");
|
|
224
|
+
scribe
|
|
225
|
+
.command("readme <component>")
|
|
226
|
+
.description("Generate a developer README for a registered component using AI.")
|
|
227
|
+
.option("--out <dir>", "Output directory (default: docs/scribe)")
|
|
228
|
+
.option("--force", "Overwrite if already exists")
|
|
229
|
+
.option("--prompt-only", "Print the prompt without calling the API")
|
|
230
|
+
.action((component, opts) => scribeReadmeCommand(component, opts));
|
|
231
|
+
scribe
|
|
232
|
+
.command("overview")
|
|
233
|
+
.description("Generate a project overview README using AI.")
|
|
234
|
+
.option("--out <dir>", "Output directory (default: docs/scribe)")
|
|
235
|
+
.option("--force", "Overwrite if already exists")
|
|
236
|
+
.option("--prompt-only", "Print the prompt without calling the API")
|
|
237
|
+
.action((opts) => scribeOverviewCommand(opts));
|
|
188
238
|
const adopt = program
|
|
189
239
|
.command("adopt [target]")
|
|
190
240
|
.description("Scan an existing project and propose components, foundations and decisions.")
|
package/dist/selene/cache.js
CHANGED
|
@@ -44,6 +44,7 @@ export async function writeCache(target, model, prompt, text) {
|
|
|
44
44
|
const key = cacheKey(model, prompt);
|
|
45
45
|
const dir = path.join(target, CACHE_DIR_REL);
|
|
46
46
|
await fs.ensureDir(dir);
|
|
47
|
+
await ensureWhaleGitignore(target);
|
|
47
48
|
const entry = {
|
|
48
49
|
key,
|
|
49
50
|
model,
|
|
@@ -66,3 +67,13 @@ export async function clearCache(target) {
|
|
|
66
67
|
}
|
|
67
68
|
return removed;
|
|
68
69
|
}
|
|
70
|
+
async function ensureWhaleGitignore(target) {
|
|
71
|
+
const gitignore = path.join(target, ".gitignore");
|
|
72
|
+
const entry = ".whale/";
|
|
73
|
+
const existing = (await fs.pathExists(gitignore)) ? await fs.readFile(gitignore, "utf8") : "";
|
|
74
|
+
const lines = existing.split(/\r?\n/).map((line) => line.trim());
|
|
75
|
+
if (lines.includes(entry))
|
|
76
|
+
return;
|
|
77
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
78
|
+
await fs.appendFile(gitignore, `${prefix}${entry}\n`, "utf8");
|
|
79
|
+
}
|
package/dist/ui/splash.js
CHANGED
|
@@ -6,13 +6,20 @@
|
|
|
6
6
|
* 2. Unicode, no color — same layout, no color
|
|
7
7
|
* 3. Plain — single ASCII line, no glyphs
|
|
8
8
|
*
|
|
9
|
-
* Rules: no chalk imports, no direct env var reads, no
|
|
9
|
+
* Rules: no chalk imports, no direct env var reads, no vague status copy.
|
|
10
10
|
* Uses `capabilities()` from capabilities.js and `ui` atoms.
|
|
11
11
|
*/
|
|
12
12
|
import { capabilities } from "./capabilities.js";
|
|
13
13
|
import { accent, muted, emphasis } from "./atoms.js";
|
|
14
14
|
import { PACKAGE_VERSION } from "../version.js";
|
|
15
15
|
const TAGLINE = "map the project before the agent moves";
|
|
16
|
+
const BLOCK = [
|
|
17
|
+
` _| _| _| _| _|_| _| _|_|_|_| `,
|
|
18
|
+
` _| _| _| _| _| _| _| _| `,
|
|
19
|
+
` _| _| _| _|_|_|_| _|_|_|_| _| _|_|_| `,
|
|
20
|
+
` _| _| _| _| _| _| _| _| _| `,
|
|
21
|
+
` _| _| _| _| _| _| _|_|_|_| _|_|_|_| `,
|
|
22
|
+
].join("\n");
|
|
16
23
|
export function splash() {
|
|
17
24
|
const { color, unicode } = capabilities();
|
|
18
25
|
const isPlain = !unicode;
|
|
@@ -20,17 +27,15 @@ export function splash() {
|
|
|
20
27
|
if (isPlain) {
|
|
21
28
|
return `WHALE IGNITER v${PACKAGE_VERSION} — ${TAGLINE}`;
|
|
22
29
|
}
|
|
23
|
-
// Tier 1 & 2 — unicode layout (color applied via atom functions when available)
|
|
24
|
-
const whale = "◆ ~";
|
|
25
|
-
const wordmarkWhale = color ? accent("WHALE") : "WHALE";
|
|
26
30
|
const wordmarkIgniter = color ? emphasis("IGNITER") : "IGNITER";
|
|
27
31
|
const version = color ? muted(`v${PACKAGE_VERSION}`) : `v${PACKAGE_VERSION}`;
|
|
28
32
|
const tag = color ? muted(TAGLINE) : TAGLINE;
|
|
29
|
-
const
|
|
33
|
+
const title = color ? accent(BLOCK) : BLOCK;
|
|
34
|
+
const rule = "─".repeat(50);
|
|
30
35
|
return [
|
|
31
|
-
|
|
32
|
-
`${
|
|
36
|
+
title,
|
|
37
|
+
`${wordmarkIgniter} · ${version}`,
|
|
33
38
|
tag,
|
|
34
|
-
rule
|
|
39
|
+
rule,
|
|
35
40
|
].join("\n");
|
|
36
41
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "1.
|
|
1
|
+
export const PACKAGE_VERSION = "1.5.1";
|
package/docs/ROADMAP.md
CHANGED
|
@@ -85,7 +85,7 @@ reveals that the next plan needs revising, we revise it before shipping.
|
|
|
85
85
|
- **Test coverage.** 44 unit tests + MCP smoke test that spawns the
|
|
86
86
|
server and exercises real protocol calls end-to-end.
|
|
87
87
|
|
|
88
|
-
### v1.1 —
|
|
88
|
+
### v1.1 — Polished runtime UI
|
|
89
89
|
|
|
90
90
|
The terminal output became the product surface. Commands stopped reaching
|
|
91
91
|
for chalk directly; every command now consumes a semantic UI layer where
|
|
@@ -96,7 +96,7 @@ pair) and the active theme decides what each intent looks like.
|
|
|
96
96
|
with ASCII fallbacks), blocks (header, section, kv, panel, summary,
|
|
97
97
|
next, ok/fail/warn/note), theme dispatcher. Commands import from `ui/`
|
|
98
98
|
rather than chalk; chalk is now confined to the theme module.
|
|
99
|
-
- **Single
|
|
99
|
+
- **Single polished theme — graphite.** Cyan accent, charcoal grays, three
|
|
100
100
|
text-hierarchy levels. Structure ready for v1.2's multi-theme without
|
|
101
101
|
touching any command.
|
|
102
102
|
- **Capability detection.** Auto-detects color and Unicode support; falls
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whale-igniter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "CLI-first operational intelligence. Bootstraps and adopts AI-readable project context so agents like Claude Code, Codex and Cursor understand your design system from the first commit. Includes an MCP server, a file watcher, deterministic insights, and an opt-in AI bridge (Selene).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
"themes",
|
|
15
15
|
"ui-references",
|
|
16
16
|
"README.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
17
18
|
"LICENSE",
|
|
18
19
|
"docs/ROADMAP.md"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
22
|
"dev": "tsx src/index.ts",
|
|
22
23
|
"build": "tsc",
|
|
24
|
+
"postbuild": "node -e \"require('node:fs').chmodSync('dist/index.js', 0o755)\"",
|
|
23
25
|
"start": "node dist/index.js",
|
|
24
26
|
"test": "tsx tests/run.ts && tsx tests/mcp-smoke.ts && tsx tests/extractors.ts && tsx tests/driftAnalyzers.ts",
|
|
25
27
|
"prepublishOnly": "npm run build && npm test"
|