whale-igniter 1.4.0 → 1.5.2
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 +35 -0
- package/README.md +68 -15
- package/dist/commands/adopt.js +7 -5
- package/dist/commands/atlas.js +196 -0
- package/dist/commands/changes.js +53 -39
- package/dist/commands/createComponent.js +3 -0
- package/dist/commands/createLanding.js +581 -52
- package/dist/commands/decision.js +1 -1
- package/dist/commands/forge.js +159 -0
- package/dist/commands/init.js +3 -2
- package/dist/commands/insights.js +0 -2
- package/dist/commands/refine.js +1 -8
- package/dist/commands/scribe.js +189 -0
- package/dist/commands/selene.js +0 -3
- package/dist/commands/sync.js +2 -2
- package/dist/commands/watch.js +1 -1
- package/dist/index.js +52 -2
- package/dist/mcp/server.js +3 -9
- package/dist/selene/promptBuilder.js +2 -2
- package/dist/templates/claude.js +4 -4
- package/dist/templates/conventions.js +4 -2
- package/dist/templates/decisions.js +17 -16
- package/dist/ui/splash.js +12 -7
- package/dist/utils/decisions.js +93 -13
- package/dist/utils/refinements.js +86 -13
- package/dist/version.js +1 -1
- package/package.json +4 -2
|
@@ -86,7 +86,7 @@ export async function decisionCommand(options = {}) {
|
|
|
86
86
|
consequences: consequences?.trim() || undefined
|
|
87
87
|
});
|
|
88
88
|
console.log();
|
|
89
|
-
console.log(ui.ok(`Decision recorded ${ui.muted("— " + created.id
|
|
89
|
+
console.log(ui.ok(`Decision recorded ${ui.muted("— " + created.id)}`));
|
|
90
90
|
console.log(` ${ui.accent(created.title)}`);
|
|
91
91
|
console.log(` ${ui.kv("category", created.category, { keyWidth: 8 })}`);
|
|
92
92
|
if (options.sync !== false) {
|
|
@@ -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
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -27,8 +27,6 @@ async function ensureIntelligenceFiles(target) {
|
|
|
27
27
|
const intelligenceDir = path.join(target, "intelligence");
|
|
28
28
|
await fs.ensureDir(intelligenceDir);
|
|
29
29
|
const seeds = {
|
|
30
|
-
"refinements.json": [],
|
|
31
|
-
"decisions.json": [],
|
|
32
30
|
"components.json": []
|
|
33
31
|
};
|
|
34
32
|
for (const [name, content] of Object.entries(seeds)) {
|
|
@@ -37,6 +35,9 @@ async function ensureIntelligenceFiles(target) {
|
|
|
37
35
|
await fs.writeJson(file, content, { spaces: 2 });
|
|
38
36
|
}
|
|
39
37
|
}
|
|
38
|
+
// Decisions and refinements are stored as individual Markdown files.
|
|
39
|
+
await fs.ensureDir(path.join(intelligenceDir, "decisions"));
|
|
40
|
+
await fs.ensureDir(path.join(intelligenceDir, "refinements"));
|
|
40
41
|
}
|
|
41
42
|
export async function initCommand(projectName = "whale-project", options = {}) {
|
|
42
43
|
const target = path.resolve(process.cwd(), projectName);
|
|
@@ -168,8 +168,6 @@ export async function insightsDriftReviewCommand(category, opts) {
|
|
|
168
168
|
break;
|
|
169
169
|
if (action === "refine") {
|
|
170
170
|
await appendRefinement(target, {
|
|
171
|
-
id: randomUUID(),
|
|
172
|
-
timestamp: new Date().toISOString(),
|
|
173
171
|
note: ins.title,
|
|
174
172
|
scope: { issueType: cat }
|
|
175
173
|
});
|
package/dist/commands/refine.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { resolveTarget } from "../utils/paths.js";
|
|
3
2
|
import { appendRefinement, inferScope } from "../utils/refinements.js";
|
|
4
3
|
import { generateWiki } from "../generators/wikiGenerator.js";
|
|
@@ -6,13 +5,7 @@ import { ui } from "../ui/index.js";
|
|
|
6
5
|
export async function refineCommand(note, options = {}) {
|
|
7
6
|
const target = resolveTarget();
|
|
8
7
|
const scope = inferScope(note);
|
|
9
|
-
|
|
10
|
-
id: randomUUID(),
|
|
11
|
-
timestamp: new Date().toISOString(),
|
|
12
|
-
note,
|
|
13
|
-
scope
|
|
14
|
-
};
|
|
15
|
-
await appendRefinement(target, refinement);
|
|
8
|
+
await appendRefinement(target, { note, scope });
|
|
16
9
|
console.log(ui.ok("Refinement recorded"));
|
|
17
10
|
if (scope) {
|
|
18
11
|
const parts = [];
|
|
@@ -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/commands/selene.js
CHANGED
|
@@ -14,7 +14,6 @@ import { copyToClipboard } from "../selene/clipboard.js";
|
|
|
14
14
|
import { resolveProvider, describeProviderState } from "../selene/providers.js";
|
|
15
15
|
import { callProvider, estimateTokens, estimateCostUsd } from "../selene/apiClient.js";
|
|
16
16
|
import { readCache, writeCache, clearCache } from "../selene/cache.js";
|
|
17
|
-
import { randomUUID } from "node:crypto";
|
|
18
17
|
import { ui } from "../ui/index.js";
|
|
19
18
|
const PENDING_DIR = ".whale/selene";
|
|
20
19
|
// ---------------------------------------------------------------------------
|
|
@@ -421,8 +420,6 @@ async function applySuggest(target, raw, options) {
|
|
|
421
420
|
}
|
|
422
421
|
else if (s.kind === "refinement") {
|
|
423
422
|
await appendRefinement(target, {
|
|
424
|
-
id: randomUUID(),
|
|
425
|
-
timestamp: new Date().toISOString(),
|
|
426
423
|
note: s.proposed_text
|
|
427
424
|
});
|
|
428
425
|
console.log(ui.success(" ✓ Recorded as refinement."));
|
package/dist/commands/sync.js
CHANGED
|
@@ -10,10 +10,10 @@ import { getAiAvailability } from "../utils/aiAvailability.js";
|
|
|
10
10
|
import { ui } from "../ui/index.js";
|
|
11
11
|
/**
|
|
12
12
|
* `whale sync` regenerates everything an AI agent reads, from the
|
|
13
|
-
*
|
|
13
|
+
* project configuration and intelligence stores (the source of truth).
|
|
14
14
|
*
|
|
15
15
|
* Run this:
|
|
16
|
-
* - after manually editing intelligence
|
|
16
|
+
* - after manually editing whale.config.json or intelligence stores
|
|
17
17
|
* - before handing the project to an AI agent
|
|
18
18
|
* - in CI to ensure CLAUDE.md stays in sync
|
|
19
19
|
*/
|
package/dist/commands/watch.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Why this exists. Whale's value depends on CLAUDE.md being current.
|
|
5
5
|
* Every write command (refine, decision, component add, adopt review) auto-syncs,
|
|
6
|
-
* but humans also edit `intelligence
|
|
6
|
+
* but humans also edit `intelligence/` directly, change `whale.config.json`,
|
|
7
7
|
* or commit catalog files from another tool. Without a watcher the gap between
|
|
8
8
|
* "what's true" and "what the AI sees" silently widens. Watcher closes that gap.
|
|
9
9
|
*
|
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";
|
|
@@ -45,7 +48,7 @@ program
|
|
|
45
48
|
});
|
|
46
49
|
program
|
|
47
50
|
.command("sync [target]")
|
|
48
|
-
.description("Regenerate CLAUDE.md and the LLM Wiki from intelligence
|
|
51
|
+
.description("Regenerate CLAUDE.md and the LLM Wiki from project intelligence.")
|
|
49
52
|
.action(syncCommand);
|
|
50
53
|
program
|
|
51
54
|
.command("remember [target]")
|
|
@@ -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/mcp/server.js
CHANGED
|
@@ -38,7 +38,6 @@ import { collectReferencedFiles, normalizePath } from "../analyzer/imports.js";
|
|
|
38
38
|
import { scanComponents } from "../scanner/componentScanner.js";
|
|
39
39
|
import { aggregateTailwind } from "../scanner/tailwindScanner.js";
|
|
40
40
|
import { validateCss } from "../validators/cssValidator.js";
|
|
41
|
-
import { randomUUID } from "node:crypto";
|
|
42
41
|
import { PACKAGE_VERSION } from "../version.js";
|
|
43
42
|
function ok(text) {
|
|
44
43
|
return { content: [{ type: "text", text }] };
|
|
@@ -354,12 +353,7 @@ export function buildMcpServer() {
|
|
|
354
353
|
if (!check.ok)
|
|
355
354
|
return err(check.reason);
|
|
356
355
|
const scope = inferScope(note);
|
|
357
|
-
await appendRefinement(ws, {
|
|
358
|
-
id: randomUUID(),
|
|
359
|
-
timestamp: new Date().toISOString(),
|
|
360
|
-
note,
|
|
361
|
-
scope
|
|
362
|
-
});
|
|
356
|
+
await appendRefinement(ws, { note, scope });
|
|
363
357
|
await generateWiki(ws);
|
|
364
358
|
const scopeDesc = scope
|
|
365
359
|
? `Scope inferred: ${Object.entries(scope).map(([k, v]) => `${k}=${v}`).join(", ")}.`
|
|
@@ -368,8 +362,8 @@ export function buildMcpServer() {
|
|
|
368
362
|
});
|
|
369
363
|
server.registerTool("whale_sync", {
|
|
370
364
|
title: "Regenerate CLAUDE.md and the LLM wiki",
|
|
371
|
-
description: "Regenerates the AI-readable context files from intelligence
|
|
372
|
-
"this if you've manually edited the
|
|
365
|
+
description: "Regenerates the AI-readable context files from project intelligence. Call " +
|
|
366
|
+
"this if you've manually edited the source stores and want the wiki to catch " +
|
|
373
367
|
"up. Whale auto-syncs on every recorded change, so manual calls are rarely needed.",
|
|
374
368
|
inputSchema: { target: z.string().optional() }
|
|
375
369
|
}, async ({ target }) => {
|
|
@@ -42,7 +42,7 @@ function renderProjectContext(ctx) {
|
|
|
42
42
|
lines.push(`- Control radius: ${c.foundations?.radius?.control ?? 4}px (buttons, inputs)`);
|
|
43
43
|
lines.push(`- Container radius: ${c.foundations?.radius?.container ?? 8}px (cards, modals)`);
|
|
44
44
|
// Active decisions — show titles only to keep tokens down. Full text
|
|
45
|
-
// is available in intelligence/decisions
|
|
45
|
+
// is available in the linked intelligence/decisions Markdown files.
|
|
46
46
|
const active = ctx.decisions.filter((d) => d.status === "active");
|
|
47
47
|
if (active.length > 0) {
|
|
48
48
|
lines.push("");
|
|
@@ -52,7 +52,7 @@ function renderProjectContext(ctx) {
|
|
|
52
52
|
lines.push(`- [${d.category}] ${d.title}`);
|
|
53
53
|
}
|
|
54
54
|
if (active.length > recent.length) {
|
|
55
|
-
lines.push(`- … and ${active.length - recent.length} more in
|
|
55
|
+
lines.push(`- … and ${active.length - recent.length} more in llm-wiki/DECISIONS.md`);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
if (ctx.refinements.length > 0) {
|
package/dist/templates/claude.js
CHANGED
|
@@ -53,12 +53,12 @@ the design system, conventions and decisions without re-explaining them every se
|
|
|
53
53
|
|
|
54
54
|
Whale stores structured context in two locations:
|
|
55
55
|
|
|
56
|
-
1. **\`intelligence/\`** — source of truth
|
|
57
|
-
- \`intelligence/refinements
|
|
58
|
-
- \`intelligence/decisions
|
|
56
|
+
1. **\`intelligence/\`** — source of truth. Prefer these for precise data:
|
|
57
|
+
- \`intelligence/refinements/\` — approved validator overrides as Markdown
|
|
58
|
+
- \`intelligence/decisions/\` — architectural and product decisions as ADRs
|
|
59
59
|
- \`intelligence/components.json\` — component catalog
|
|
60
60
|
|
|
61
|
-
2. **\`llm-wiki/\`** — human + AI readable markdown
|
|
61
|
+
2. **\`llm-wiki/\`** — compact human + AI readable markdown indexes:
|
|
62
62
|
- \`llm-wiki/FOUNDATIONS.md\` — design tokens and grid rules
|
|
63
63
|
- \`llm-wiki/CONVENTIONS.md\` — coding and design conventions
|
|
64
64
|
- \`llm-wiki/DECISIONS.md\` — decision log in narrative form
|
|
@@ -28,7 +28,8 @@ export function renderConventions(config, refinements) {
|
|
|
28
28
|
if (r.scope?.file)
|
|
29
29
|
scopeParts.push(`file=\`${r.scope.file}\``);
|
|
30
30
|
const scope = scopeParts.length ? ` _(${scopeParts.join(", ")})_` : "";
|
|
31
|
-
|
|
31
|
+
const ref = r.sourcePath ? `[${r.id}](../${r.sourcePath})` : r.id;
|
|
32
|
+
lines.push(`- **${ref}** · ${r.timestamp.slice(0, 10)} — ${r.note}${scope}`);
|
|
32
33
|
}
|
|
33
34
|
lines.push("");
|
|
34
35
|
}
|
|
@@ -36,7 +37,8 @@ export function renderConventions(config, refinements) {
|
|
|
36
37
|
lines.push("## Open questions", "");
|
|
37
38
|
lines.push("These notes do not yet have a scope — the validator cannot act on them.", "Review and convert to scoped refinements or decisions.", "");
|
|
38
39
|
for (const r of scopeless) {
|
|
39
|
-
|
|
40
|
+
const ref = r.sourcePath ? `[${r.id}](../${r.sourcePath})` : r.id;
|
|
41
|
+
lines.push(`> ⚠️ Unresolved — **${ref}** · ${r.timestamp.slice(0, 10)}: ${r.note}`);
|
|
40
42
|
}
|
|
41
43
|
lines.push("");
|
|
42
44
|
}
|