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.
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/analyzer/imports.js +88 -0
- package/dist/analyzer/insights.js +276 -0
- package/dist/commands/add.js +36 -0
- package/dist/commands/adopt.js +180 -0
- package/dist/commands/adoptReview.js +267 -0
- package/dist/commands/component.js +93 -0
- package/dist/commands/createComponent.js +207 -0
- package/dist/commands/decision.js +98 -0
- package/dist/commands/docs.js +34 -0
- package/dist/commands/ignite.js +212 -0
- package/dist/commands/init.js +66 -0
- package/dist/commands/insights.js +123 -0
- package/dist/commands/mcp.js +106 -0
- package/dist/commands/refine.js +36 -0
- package/dist/commands/selene.js +516 -0
- package/dist/commands/sync.js +43 -0
- package/dist/commands/validate.js +48 -0
- package/dist/commands/watch.js +150 -0
- package/dist/commands/wiki.js +21 -0
- package/dist/generators/markdownGenerator.js +112 -0
- package/dist/generators/reportGenerator.js +50 -0
- package/dist/generators/wikiGenerator.js +365 -0
- package/dist/index.js +213 -0
- package/dist/mcp/server.js +404 -0
- package/dist/scanner/componentScanner.js +522 -0
- package/dist/scanner/foundationInferrer.js +174 -0
- package/dist/scanner/tailwindMapper.js +58 -0
- package/dist/scanner/tailwindScanner.js +186 -0
- package/dist/selene/apiClient.js +168 -0
- package/dist/selene/cache.js +68 -0
- package/dist/selene/clipboard.js +56 -0
- package/dist/selene/promptBuilder.js +229 -0
- package/dist/selene/providers.js +67 -0
- package/dist/selene/responseParser.js +149 -0
- package/dist/ui/atoms.js +30 -0
- package/dist/ui/blocks.js +208 -0
- package/dist/ui/capabilities.js +64 -0
- package/dist/ui/index.js +13 -0
- package/dist/ui/symbols.js +41 -0
- package/dist/ui/theme.js +78 -0
- package/dist/utils/components.js +40 -0
- package/dist/utils/config.js +31 -0
- package/dist/utils/decisions.js +32 -0
- package/dist/utils/paths.js +4 -0
- package/dist/utils/proposals.js +61 -0
- package/dist/utils/refinements.js +81 -0
- package/dist/utils/registry.js +45 -0
- package/dist/utils/writeJson.js +6 -0
- package/dist/validators/cssValidator.js +204 -0
- package/dist/version.js +1 -0
- package/docs/ROADMAP.md +206 -0
- package/package.json +76 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { loadConfig } from "../utils/config.js";
|
|
4
|
+
import { loadRefinements } from "../utils/refinements.js";
|
|
5
|
+
import { loadDecisions } from "../utils/decisions.js";
|
|
6
|
+
import { loadComponents } from "../utils/components.js";
|
|
7
|
+
import { validateCss } from "../validators/cssValidator.js";
|
|
8
|
+
export async function generateWiki(target) {
|
|
9
|
+
const config = await loadConfig(target);
|
|
10
|
+
const refinements = await loadRefinements(target);
|
|
11
|
+
const decisions = await loadDecisions(target);
|
|
12
|
+
const components = await loadComponents(target);
|
|
13
|
+
const issues = await validateCss(target);
|
|
14
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
15
|
+
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
16
|
+
const wikiDir = path.join(target, "llm-wiki");
|
|
17
|
+
await fs.ensureDir(wikiDir);
|
|
18
|
+
// ---- llm-wiki/ themed files ------------------------------------------------
|
|
19
|
+
const themedFiles = {
|
|
20
|
+
"README.md": renderWikiReadme(config),
|
|
21
|
+
"PROJECT.md": renderProject(config, errors, warnings, refinements.length, decisions.length),
|
|
22
|
+
"FOUNDATIONS.md": renderFoundations(config),
|
|
23
|
+
"CONVENTIONS.md": renderConventions(config, refinements),
|
|
24
|
+
"DECISIONS.md": renderDecisions(decisions),
|
|
25
|
+
"COMPONENTS.md": renderComponents(components),
|
|
26
|
+
"WORKFLOWS.md": renderWorkflows(config)
|
|
27
|
+
};
|
|
28
|
+
const wikiFiles = [];
|
|
29
|
+
for (const [name, content] of Object.entries(themedFiles)) {
|
|
30
|
+
const filePath = path.join(wikiDir, name);
|
|
31
|
+
await fs.writeFile(filePath, content);
|
|
32
|
+
wikiFiles.push(filePath);
|
|
33
|
+
}
|
|
34
|
+
// ---- root-level AI entry points -------------------------------------------
|
|
35
|
+
const rootIndex = renderRootIndex(config, {
|
|
36
|
+
errors,
|
|
37
|
+
warnings,
|
|
38
|
+
refinementCount: refinements.length,
|
|
39
|
+
decisionCount: decisions.length,
|
|
40
|
+
componentCount: components.length
|
|
41
|
+
});
|
|
42
|
+
const rootFiles = [];
|
|
43
|
+
const targets = config.aiTargets ?? ["claude"];
|
|
44
|
+
// CLAUDE.md is always written (default reader). Other agent files mirror it.
|
|
45
|
+
const filenamesByTarget = {
|
|
46
|
+
claude: "CLAUDE.md",
|
|
47
|
+
codex: "AGENTS.md", // OpenAI Codex / agents.md convention
|
|
48
|
+
cursor: ".cursorrules",
|
|
49
|
+
copilot: ".github/copilot-instructions.md"
|
|
50
|
+
};
|
|
51
|
+
// Always emit CLAUDE.md as the canonical file, even if claude isn't listed.
|
|
52
|
+
const written = new Set();
|
|
53
|
+
const emit = async (filename) => {
|
|
54
|
+
if (written.has(filename))
|
|
55
|
+
return;
|
|
56
|
+
written.add(filename);
|
|
57
|
+
const filePath = path.join(target, filename);
|
|
58
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
59
|
+
await fs.writeFile(filePath, rootIndex);
|
|
60
|
+
rootFiles.push(filePath);
|
|
61
|
+
};
|
|
62
|
+
await emit("CLAUDE.md");
|
|
63
|
+
for (const t of targets) {
|
|
64
|
+
await emit(filenamesByTarget[t]);
|
|
65
|
+
}
|
|
66
|
+
return { rootFiles, wikiFiles };
|
|
67
|
+
}
|
|
68
|
+
// ----------------------------------------------------------------------------
|
|
69
|
+
// Renderers
|
|
70
|
+
// ----------------------------------------------------------------------------
|
|
71
|
+
function renderRootIndex(config, stats) {
|
|
72
|
+
const name = config.projectName ?? "this project";
|
|
73
|
+
const grid = config.foundations?.grid ?? 8;
|
|
74
|
+
const ctrl = config.foundations?.radius?.control ?? 2;
|
|
75
|
+
const cont = config.foundations?.radius?.container ?? 4;
|
|
76
|
+
const stack = config.stack ?? "css";
|
|
77
|
+
const packs = (config.packs ?? []).join(", ") || "(none)";
|
|
78
|
+
return `# AI Agent Context — ${name}
|
|
79
|
+
|
|
80
|
+
> This file is auto-generated by [Whale Igniter](https://github.com/whale-igniter).
|
|
81
|
+
> Do not edit by hand. Run \`whale sync\` after changes to regenerate.
|
|
82
|
+
|
|
83
|
+
You are working in a project managed by Whale Igniter. Whale maintains
|
|
84
|
+
machine-readable operational context so AI agents understand the design
|
|
85
|
+
system, conventions and decisions without re-explaining them every session.
|
|
86
|
+
|
|
87
|
+
## Quick facts
|
|
88
|
+
|
|
89
|
+
- **Project type:** ${config.projectType ?? "unspecified"}
|
|
90
|
+
- **Stack:** ${stack}
|
|
91
|
+
- **Grid:** ${grid}px (all spacing must be a multiple)
|
|
92
|
+
- **Border radius:** ${ctrl}px (controls), ${cont}px (containers)
|
|
93
|
+
- **Active packs:** ${packs}
|
|
94
|
+
- **Validation status:** ${stats.errors} error(s), ${stats.warnings} warning(s)
|
|
95
|
+
- **Decisions logged:** ${stats.decisionCount}
|
|
96
|
+
- **Refinements active:** ${stats.refinementCount}
|
|
97
|
+
- **Components catalogued:** ${stats.componentCount}
|
|
98
|
+
|
|
99
|
+
## How to read this project
|
|
100
|
+
|
|
101
|
+
Whale stores structured context in two locations:
|
|
102
|
+
|
|
103
|
+
1. **\`intelligence/\`** — source of truth (JSON). Read these when you need precise data:
|
|
104
|
+
- \`intelligence/refinements.json\` — validator overrides the team has accepted
|
|
105
|
+
- \`intelligence/decisions.json\` — architectural and product decisions
|
|
106
|
+
- \`intelligence/components.json\` — catalog of components in the project
|
|
107
|
+
|
|
108
|
+
2. **\`llm-wiki/\`** — human + AI readable markdown rendered from the JSON:
|
|
109
|
+
- \`llm-wiki/FOUNDATIONS.md\` — design tokens and grid rules
|
|
110
|
+
- \`llm-wiki/CONVENTIONS.md\` — coding and design conventions
|
|
111
|
+
- \`llm-wiki/DECISIONS.md\` — decision log in narrative form
|
|
112
|
+
- \`llm-wiki/COMPONENTS.md\` — component catalog
|
|
113
|
+
- \`llm-wiki/WORKFLOWS.md\` — how the team uses Whale day to day
|
|
114
|
+
|
|
115
|
+
If a question can be answered from \`intelligence/*.json\`, prefer that file —
|
|
116
|
+
it's the source. The wiki is a rendered view.
|
|
117
|
+
|
|
118
|
+
## Rules you must follow
|
|
119
|
+
|
|
120
|
+
1. **Spacing.** Every padding, margin and gap must be a multiple of \`${grid}px\`.
|
|
121
|
+
2. **Radius.** Buttons, inputs, selects use \`${ctrl}px\`. Cards, modals, sheets use \`${cont}px\`.
|
|
122
|
+
3. **Color.** Prefer semantic tokens over raw hex. If you must add a hex,
|
|
123
|
+
record a refinement explaining why.
|
|
124
|
+
4. **Focus.** Every interactive element needs a visible \`:focus-visible\` state.
|
|
125
|
+
5. **Decisions.** When you make a non-obvious choice, propose recording it via
|
|
126
|
+
\`whale decision\` so future sessions inherit the reasoning.
|
|
127
|
+
6. **Refinements.** Honor entries in \`intelligence/refinements.json\` —
|
|
128
|
+
they encode intentional exceptions the team approved.
|
|
129
|
+
|
|
130
|
+
## When the user asks you to add a component
|
|
131
|
+
|
|
132
|
+
Before writing code:
|
|
133
|
+
- Check \`intelligence/components.json\` to see if it already exists.
|
|
134
|
+
- Check \`llm-wiki/FOUNDATIONS.md\` for the tokens to use.
|
|
135
|
+
- After creating it, suggest the user run \`whale component add <name>\`
|
|
136
|
+
so future agents know it exists.
|
|
137
|
+
|
|
138
|
+
## Commands available
|
|
139
|
+
|
|
140
|
+
- \`whale validate\` — runs the validators, exits non-zero on errors
|
|
141
|
+
- \`whale refine "<note>"\` — record a validator override
|
|
142
|
+
- \`whale decision\` — record an architectural decision (interactive)
|
|
143
|
+
- \`whale component add <name>\` — register a component
|
|
144
|
+
- \`whale sync\` — regenerate this file and the wiki
|
|
145
|
+
- \`whale docs\` — generate human-facing reports
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
_Last sync: ${new Date().toISOString()}_
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
function renderWikiReadme(config) {
|
|
152
|
+
return `# LLM Wiki
|
|
153
|
+
|
|
154
|
+
This directory is generated by Whale Igniter. It contains a human-and-AI
|
|
155
|
+
readable view of the project's operational context.
|
|
156
|
+
|
|
157
|
+
**Do not edit these files directly** — they are regenerated from
|
|
158
|
+
\`intelligence/*.json\` and \`whale.config.json\` whenever you run
|
|
159
|
+
\`whale sync\`.
|
|
160
|
+
|
|
161
|
+
## Files
|
|
162
|
+
|
|
163
|
+
- \`PROJECT.md\` — high-level project facts
|
|
164
|
+
- \`FOUNDATIONS.md\` — grid, radius, tokens
|
|
165
|
+
- \`CONVENTIONS.md\` — active conventions and refinements
|
|
166
|
+
- \`DECISIONS.md\` — decision log
|
|
167
|
+
- \`COMPONENTS.md\` — component catalog
|
|
168
|
+
- \`WORKFLOWS.md\` — recommended usage flow
|
|
169
|
+
|
|
170
|
+
## For AI agents
|
|
171
|
+
|
|
172
|
+
The canonical entry point for AI agents is \`/CLAUDE.md\` at the project root.
|
|
173
|
+
This wiki is the detailed view — start with the root file, drill in here.
|
|
174
|
+
|
|
175
|
+
_Generated by Whale Igniter v0.7._
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
function renderProject(config, errors, warnings, refinementCount, decisionCount) {
|
|
179
|
+
return `# Project Context
|
|
180
|
+
|
|
181
|
+
- **Project name:** ${config.projectName ?? "(unset)"}
|
|
182
|
+
- **Project type:** ${config.projectType ?? "unspecified"}
|
|
183
|
+
- **Stack:** ${config.stack ?? "css"}
|
|
184
|
+
- **AI targets:** ${(config.aiTargets ?? []).join(", ") || "(none)"}
|
|
185
|
+
- **Active packs:** ${(config.packs ?? []).join(", ") || "(none)"}
|
|
186
|
+
- **Ignited:** ${config.ignited?.at ?? "(not yet)"} (${config.ignited?.mode ?? "—"})
|
|
187
|
+
|
|
188
|
+
## Current status
|
|
189
|
+
|
|
190
|
+
- Validation errors: ${errors}
|
|
191
|
+
- Validation warnings: ${warnings}
|
|
192
|
+
- Decisions logged: ${decisionCount}
|
|
193
|
+
- Active refinements: ${refinementCount}
|
|
194
|
+
|
|
195
|
+
_Generated by Whale Igniter._
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
function renderFoundations(config) {
|
|
199
|
+
const grid = config.foundations?.grid ?? 8;
|
|
200
|
+
const ctrl = config.foundations?.radius?.control ?? 2;
|
|
201
|
+
const cont = config.foundations?.radius?.container ?? 4;
|
|
202
|
+
return `# Foundations
|
|
203
|
+
|
|
204
|
+
The invariants of this project. All UI must conform unless explicitly refined.
|
|
205
|
+
|
|
206
|
+
## Spacing
|
|
207
|
+
|
|
208
|
+
Grid unit: **${grid}px**.
|
|
209
|
+
|
|
210
|
+
Every padding, margin and gap should be a multiple of ${grid}px. The allowed
|
|
211
|
+
scale is:
|
|
212
|
+
|
|
213
|
+
\`\`\`
|
|
214
|
+
${[1, 2, 3, 4, 5, 6, 8, 10, 12].map((n) => `${n * grid}px`).join(" ")}
|
|
215
|
+
\`\`\`
|
|
216
|
+
|
|
217
|
+
## Radius
|
|
218
|
+
|
|
219
|
+
| Use | Value |
|
|
220
|
+
| -------------- | ------- |
|
|
221
|
+
| Controls | ${ctrl}px |
|
|
222
|
+
| Containers | ${cont}px |
|
|
223
|
+
|
|
224
|
+
Controls = buttons, inputs, selects, toggles. Containers = cards, modals,
|
|
225
|
+
sheets, popovers.
|
|
226
|
+
|
|
227
|
+
## Tone & accent
|
|
228
|
+
|
|
229
|
+
- Tone: ${config.branding?.tone ?? "(unset)"}
|
|
230
|
+
- Accent: ${config.branding?.accent ?? "(unset)"}
|
|
231
|
+
|
|
232
|
+
_Generated by Whale Igniter._
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
function renderConventions(config, refinements) {
|
|
236
|
+
const lines = [
|
|
237
|
+
"# Conventions",
|
|
238
|
+
"",
|
|
239
|
+
"## Rules",
|
|
240
|
+
"",
|
|
241
|
+
`- Spacing is a multiple of ${config.foundations?.grid ?? 8}px.`,
|
|
242
|
+
"- Controls and containers use distinct radius values (see FOUNDATIONS.md).",
|
|
243
|
+
"- Every interactive element has a visible \`:focus-visible\` state.",
|
|
244
|
+
"- Prefer semantic tokens over raw color values.",
|
|
245
|
+
"",
|
|
246
|
+
"## Active refinements",
|
|
247
|
+
""
|
|
248
|
+
];
|
|
249
|
+
if (refinements.length === 0) {
|
|
250
|
+
lines.push("_No refinements recorded yet._");
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
lines.push("These are exceptions the team has explicitly approved. The validator", "suppresses matching issues.", "");
|
|
254
|
+
for (const r of refinements) {
|
|
255
|
+
const scopeParts = [];
|
|
256
|
+
if (r.scope?.issueType)
|
|
257
|
+
scopeParts.push(`type=\`${r.scope.issueType}\``);
|
|
258
|
+
if (r.scope?.selector)
|
|
259
|
+
scopeParts.push(`selector=\`${r.scope.selector}\``);
|
|
260
|
+
if (r.scope?.file)
|
|
261
|
+
scopeParts.push(`file=\`${r.scope.file}\``);
|
|
262
|
+
const scope = scopeParts.length ? ` _(${scopeParts.join(", ")})_` : "";
|
|
263
|
+
lines.push(`- **${r.timestamp.slice(0, 10)}** — ${r.note}${scope}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
lines.push("", "_Generated by Whale Igniter._");
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
function renderDecisions(decisions) {
|
|
270
|
+
const lines = [
|
|
271
|
+
"# Decision Log",
|
|
272
|
+
"",
|
|
273
|
+
"Architectural, product and tooling decisions, in reverse chronological order.",
|
|
274
|
+
""
|
|
275
|
+
];
|
|
276
|
+
if (decisions.length === 0) {
|
|
277
|
+
lines.push("_No decisions logged yet. Use `whale decision` to record one._");
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
const sorted = [...decisions].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
281
|
+
for (const d of sorted) {
|
|
282
|
+
lines.push(`## ${d.title}`);
|
|
283
|
+
lines.push("");
|
|
284
|
+
lines.push(`- **Date:** ${d.timestamp.slice(0, 10)}`);
|
|
285
|
+
lines.push(`- **Category:** ${d.category}`);
|
|
286
|
+
lines.push(`- **Status:** ${d.status}`);
|
|
287
|
+
if (d.context) {
|
|
288
|
+
lines.push("", "**Context**", "", d.context);
|
|
289
|
+
}
|
|
290
|
+
lines.push("", "**Decision**", "", d.decision);
|
|
291
|
+
if (d.consequences) {
|
|
292
|
+
lines.push("", "**Consequences**", "", d.consequences);
|
|
293
|
+
}
|
|
294
|
+
lines.push("", "---", "");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
lines.push("_Generated by Whale Igniter._");
|
|
298
|
+
return lines.join("\n");
|
|
299
|
+
}
|
|
300
|
+
function renderComponents(components) {
|
|
301
|
+
const lines = ["# Component Catalog", ""];
|
|
302
|
+
if (components.length === 0) {
|
|
303
|
+
lines.push("_No components catalogued yet. Use `whale component add <name>` to register one._");
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
const sorted = [...components].sort((a, b) => a.name.localeCompare(b.name));
|
|
307
|
+
for (const c of sorted) {
|
|
308
|
+
lines.push(`## ${c.name}`);
|
|
309
|
+
lines.push("");
|
|
310
|
+
if (c.description)
|
|
311
|
+
lines.push(c.description, "");
|
|
312
|
+
const meta = [];
|
|
313
|
+
if (c.category)
|
|
314
|
+
meta.push(`Category: ${c.category}`);
|
|
315
|
+
if (c.variants?.length)
|
|
316
|
+
meta.push(`Variants: ${c.variants.join(", ")}`);
|
|
317
|
+
if (c.states?.length)
|
|
318
|
+
meta.push(`States: ${c.states.join(", ")}`);
|
|
319
|
+
if (meta.length)
|
|
320
|
+
lines.push(meta.map((m) => `- ${m}`).join("\n"), "");
|
|
321
|
+
if (c.files?.length) {
|
|
322
|
+
lines.push("**Files:**");
|
|
323
|
+
for (const f of c.files)
|
|
324
|
+
lines.push(`- \`${f}\``);
|
|
325
|
+
lines.push("");
|
|
326
|
+
}
|
|
327
|
+
lines.push("---", "");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
lines.push("_Generated by Whale Igniter._");
|
|
331
|
+
return lines.join("\n");
|
|
332
|
+
}
|
|
333
|
+
function renderWorkflows(_config) {
|
|
334
|
+
return `# Workflows
|
|
335
|
+
|
|
336
|
+
## Daily flow
|
|
337
|
+
|
|
338
|
+
\`\`\`
|
|
339
|
+
whale validate # see what's drifting
|
|
340
|
+
whale refine "<note>" # accept an intentional exception
|
|
341
|
+
whale decision # record a non-obvious choice
|
|
342
|
+
whale component add <name> # register a new component
|
|
343
|
+
whale sync # regenerate AI context
|
|
344
|
+
\`\`\`
|
|
345
|
+
|
|
346
|
+
## When to use what
|
|
347
|
+
|
|
348
|
+
| You want to... | Command |
|
|
349
|
+
| --------------------------------------------- | ----------------------------- |
|
|
350
|
+
| See what's broken | \`whale validate\` |
|
|
351
|
+
| Accept an exception ("we use radius 0 here") | \`whale refine "..."\` |
|
|
352
|
+
| Log an architecture/product choice | \`whale decision\` |
|
|
353
|
+
| Track a new component | \`whale component add\` |
|
|
354
|
+
| Refresh AI context after manual edits | \`whale sync\` |
|
|
355
|
+
| Produce human reports | \`whale docs\` |
|
|
356
|
+
|
|
357
|
+
## Recommended cadence
|
|
358
|
+
|
|
359
|
+
- **Per change:** run \`validate\` before commit.
|
|
360
|
+
- **Per significant choice:** record a \`decision\` or \`refinement\`.
|
|
361
|
+
- **Per session:** run \`sync\` before handing off to an AI agent.
|
|
362
|
+
|
|
363
|
+
_Generated by Whale Igniter._
|
|
364
|
+
`;
|
|
365
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { initCommand } from "./commands/init.js";
|
|
4
|
+
import { validateCommand } from "./commands/validate.js";
|
|
5
|
+
import { docsCommand } from "./commands/docs.js";
|
|
6
|
+
import { wikiCommand } from "./commands/wiki.js";
|
|
7
|
+
import { refineCommand } from "./commands/refine.js";
|
|
8
|
+
import { addCommand } from "./commands/add.js";
|
|
9
|
+
import { igniteCommand } from "./commands/ignite.js";
|
|
10
|
+
import { syncCommand } from "./commands/sync.js";
|
|
11
|
+
import { decisionCommand } from "./commands/decision.js";
|
|
12
|
+
import { componentAddCommand, componentListCommand } from "./commands/component.js";
|
|
13
|
+
import { adoptCommand } from "./commands/adopt.js";
|
|
14
|
+
import { adoptReviewCommand, adoptStatusCommand } from "./commands/adoptReview.js";
|
|
15
|
+
import { insightsCommand } from "./commands/insights.js";
|
|
16
|
+
import { createComponentCommand } from "./commands/createComponent.js";
|
|
17
|
+
import { seleneDescribeCommand, seleneAuditCommand, seleneSuggestCommand, seleneApplyCommand, seleneStatusCommand, seleneCacheClearCommand } from "./commands/selene.js";
|
|
18
|
+
import { mcpServeCommand, mcpConfigCommand } from "./commands/mcp.js";
|
|
19
|
+
import { watchCommand } from "./commands/watch.js";
|
|
20
|
+
import { PACKAGE_VERSION } from "./version.js";
|
|
21
|
+
const program = new Command();
|
|
22
|
+
program
|
|
23
|
+
.name("whale")
|
|
24
|
+
.description("Whale Igniter — CLI-first operational intelligence. " +
|
|
25
|
+
"Bootstraps and adopts AI-readable project context for Claude, Codex, Cursor and Copilot.")
|
|
26
|
+
.version(PACKAGE_VERSION);
|
|
27
|
+
program
|
|
28
|
+
.command("ignite [projectName]")
|
|
29
|
+
.description("Bootstrap a Whale workspace with AI-readable context (opinionated by default).")
|
|
30
|
+
.option("-i, --interactive", "Run an interactive wizard to configure stack, packs, AI targets")
|
|
31
|
+
.option("-m, --minimal", "Minimal scaffold: folders + config + one validator pack")
|
|
32
|
+
.action((projectName, opts) => igniteCommand(projectName, opts));
|
|
33
|
+
program
|
|
34
|
+
.command("init [projectName]")
|
|
35
|
+
.description("Create an empty Whale workspace (no packs, no AI context).")
|
|
36
|
+
.action(async (projectName) => {
|
|
37
|
+
await initCommand(projectName);
|
|
38
|
+
});
|
|
39
|
+
program
|
|
40
|
+
.command("sync [target]")
|
|
41
|
+
.description("Regenerate CLAUDE.md and the LLM Wiki from intelligence/*.json.")
|
|
42
|
+
.action(syncCommand);
|
|
43
|
+
program
|
|
44
|
+
.command("add <pack>")
|
|
45
|
+
.description("Install a Whale operational pack.")
|
|
46
|
+
.action(addCommand);
|
|
47
|
+
program
|
|
48
|
+
.command("validate [target]")
|
|
49
|
+
.description("Run operational validations (exits non-zero on errors).")
|
|
50
|
+
.action(validateCommand);
|
|
51
|
+
program
|
|
52
|
+
.command("docs [target]")
|
|
53
|
+
.description("Generate validation report and run installed generator packs.")
|
|
54
|
+
.action(docsCommand);
|
|
55
|
+
program
|
|
56
|
+
.command("wiki [target]")
|
|
57
|
+
.description("Regenerate AI context (alias of `sync`).")
|
|
58
|
+
.action(wikiCommand);
|
|
59
|
+
program
|
|
60
|
+
.command("refine <note>")
|
|
61
|
+
.description("Record an operational refinement. Mention an issue type to suppress matching issues.")
|
|
62
|
+
.option("--no-sync", "Skip automatic AI context regeneration")
|
|
63
|
+
.action((note, opts) => refineCommand(note, opts));
|
|
64
|
+
const selene = program
|
|
65
|
+
.command("selene")
|
|
66
|
+
.description("AI-assisted suggestions — works offline (prompt mode) or via API key (API mode).");
|
|
67
|
+
const apiFlagsHelp = "API mode flags: --prompt-only, --api-only, --provider, --model, --no-cache, --confirm-cost";
|
|
68
|
+
selene
|
|
69
|
+
.command("describe <component>")
|
|
70
|
+
.description("Generate a catalog description for a component. " + apiFlagsHelp)
|
|
71
|
+
.option("--file <path>", "Component file path (defaults to the one registered in the catalog)")
|
|
72
|
+
.option("--out <path>", "Write the prompt to this file instead of clipboard / API")
|
|
73
|
+
.option("--stdout", "Print the prompt to stdout")
|
|
74
|
+
.option("--prompt-only", "Force prompt mode even if an API key is available")
|
|
75
|
+
.option("--api-only", "Force API mode (errors if no key configured)")
|
|
76
|
+
.option("--provider <name>", "Override the provider: anthropic | openai")
|
|
77
|
+
.option("--model <name>", "Override the model id")
|
|
78
|
+
.option("--no-cache", "Skip the on-disk response cache")
|
|
79
|
+
.option("--confirm-cost", "Show estimated cost before each call and require confirmation")
|
|
80
|
+
.option("--no-auto-apply", "In API mode, don't auto-apply the response")
|
|
81
|
+
.action((name, opts) => seleneDescribeCommand(name, opts));
|
|
82
|
+
selene
|
|
83
|
+
.command("audit <file>")
|
|
84
|
+
.description("Audit a file against project foundations and conventions. " + apiFlagsHelp)
|
|
85
|
+
.option("--out <path>", "Write the prompt to this file instead of clipboard / API")
|
|
86
|
+
.option("--stdout", "Print the prompt to stdout")
|
|
87
|
+
.option("--prompt-only", "Force prompt mode even if an API key is available")
|
|
88
|
+
.option("--api-only", "Force API mode (errors if no key configured)")
|
|
89
|
+
.option("--provider <name>", "Override the provider: anthropic | openai")
|
|
90
|
+
.option("--model <name>", "Override the model id")
|
|
91
|
+
.option("--no-cache", "Skip the on-disk response cache")
|
|
92
|
+
.option("--confirm-cost", "Show estimated cost before each call and require confirmation")
|
|
93
|
+
.option("--no-auto-apply", "In API mode, don't auto-apply the response")
|
|
94
|
+
.action((file, opts) => seleneAuditCommand(file, opts));
|
|
95
|
+
selene
|
|
96
|
+
.command("suggest")
|
|
97
|
+
.description("Suggest patterns or decisions worth recording. " + apiFlagsHelp)
|
|
98
|
+
.option("--focus <area>", "decisions | refinements | components | all")
|
|
99
|
+
.option("--out <path>", "Write the prompt to this file instead of clipboard / API")
|
|
100
|
+
.option("--stdout", "Print the prompt to stdout")
|
|
101
|
+
.option("--prompt-only", "Force prompt mode even if an API key is available")
|
|
102
|
+
.option("--api-only", "Force API mode (errors if no key configured)")
|
|
103
|
+
.option("--provider <name>", "Override the provider: anthropic | openai")
|
|
104
|
+
.option("--model <name>", "Override the model id")
|
|
105
|
+
.option("--no-cache", "Skip the on-disk response cache")
|
|
106
|
+
.option("--confirm-cost", "Show estimated cost before each call and require confirmation")
|
|
107
|
+
.option("--no-auto-apply", "In API mode, don't auto-apply the response")
|
|
108
|
+
.option("--yes", "Skip per-suggestion confirmation when auto-applying")
|
|
109
|
+
.action((opts) => seleneSuggestCommand(opts));
|
|
110
|
+
selene
|
|
111
|
+
.command("apply <kind>")
|
|
112
|
+
.description("Paste the LLM response (stdin or --from) and apply it. kind: describe | audit | suggest")
|
|
113
|
+
.option("--from <path>", "Read the response from a file instead of stdin")
|
|
114
|
+
.option("--yes", "Skip per-change confirmation prompts")
|
|
115
|
+
.action((kind, opts) => seleneApplyCommand(kind, opts));
|
|
116
|
+
selene
|
|
117
|
+
.command("status")
|
|
118
|
+
.description("Show available providers, current selene config, cache state.")
|
|
119
|
+
.action(() => seleneStatusCommand());
|
|
120
|
+
const cache = selene
|
|
121
|
+
.command("cache")
|
|
122
|
+
.description("Manage the Selene response cache.");
|
|
123
|
+
cache
|
|
124
|
+
.command("clear")
|
|
125
|
+
.description("Delete all cached Selene responses.")
|
|
126
|
+
.action(() => seleneCacheClearCommand());
|
|
127
|
+
program
|
|
128
|
+
.command("insights [target]")
|
|
129
|
+
.description("Run local analyzers over the intelligence stores and surface accountable recommendations.")
|
|
130
|
+
.option("--json", "Emit insights as JSON (for scripting / CI)")
|
|
131
|
+
.option("--category <name>", "Filter by category: refinements | decisions | components | foundations | tokens | coverage")
|
|
132
|
+
.option("--min-severity <level>", "Show only insights at this severity or higher (info | warning | critical)")
|
|
133
|
+
.option("--skip-scan", "Skip the source scan (faster, but disables orphan/drift insights)")
|
|
134
|
+
.action((target, opts) => insightsCommand(target, opts));
|
|
135
|
+
const create = program
|
|
136
|
+
.command("create")
|
|
137
|
+
.description("Generate new code that respects the project's foundations.");
|
|
138
|
+
create
|
|
139
|
+
.command("component <name>")
|
|
140
|
+
.description("Generate a typed React component scaffold using current foundations.")
|
|
141
|
+
.option("--variants <list>", "Comma-separated variants (e.g. primary,secondary,ghost)")
|
|
142
|
+
.option("--states <list>", "Comma-separated states (e.g. hover,focus,disabled)")
|
|
143
|
+
.option("--category <name>", "Component category (form, navigation, feedback, surface, layout)")
|
|
144
|
+
.option("--out-dir <path>", "Output directory relative to project root (default: src/components)")
|
|
145
|
+
.option("--force", "Overwrite the file if it already exists")
|
|
146
|
+
.option("--no-register", "Don't add this component to intelligence/components.json")
|
|
147
|
+
.option("--no-sync", "Don't regenerate CLAUDE.md / wiki after creating")
|
|
148
|
+
.action((name, opts) => createComponentCommand(name, opts));
|
|
149
|
+
const adopt = program
|
|
150
|
+
.command("adopt [target]")
|
|
151
|
+
.description("Scan an existing project and propose components, foundations and decisions.")
|
|
152
|
+
.option("--pattern <glob>", "Glob pattern for source files (default: src/**/*.{tsx,jsx})")
|
|
153
|
+
.option("--dry-run", "Run the scanner but don't write proposals to disk")
|
|
154
|
+
.action((target, opts) => adoptCommand(target, opts));
|
|
155
|
+
adopt
|
|
156
|
+
.command("review [target]")
|
|
157
|
+
.description("Walk through pending proposals and accept, reject, or edit each.")
|
|
158
|
+
.option("--limit <n>", "Review at most N proposals in this pass", (v) => parseInt(v, 10))
|
|
159
|
+
.option("--accept-all", "Accept every pending proposal without prompting (use with care)")
|
|
160
|
+
.option("--category <kind>", "Only review one kind: component | foundations | decision")
|
|
161
|
+
.action((target, opts) => adoptReviewCommand(target, opts));
|
|
162
|
+
adopt
|
|
163
|
+
.command("status [target]")
|
|
164
|
+
.description("Show counts and a preview of pending proposals.")
|
|
165
|
+
.action((target) => adoptStatusCommand(target));
|
|
166
|
+
const decision = program
|
|
167
|
+
.command("decision")
|
|
168
|
+
.description("Record an architectural, product or tooling decision.")
|
|
169
|
+
.option("--title <title>", "Decision title")
|
|
170
|
+
.option("--category <category>", "architecture | design-system | product | tooling | convention")
|
|
171
|
+
.option("--context <text>", "Context: why this came up")
|
|
172
|
+
.option("--decision <text>", "What was decided")
|
|
173
|
+
.option("--consequences <text>", "Tradeoffs and implications")
|
|
174
|
+
.option("--no-sync", "Skip automatic AI context regeneration")
|
|
175
|
+
.action((opts) => decisionCommand(opts));
|
|
176
|
+
const component = program
|
|
177
|
+
.command("component")
|
|
178
|
+
.description("Manage the component catalog.");
|
|
179
|
+
component
|
|
180
|
+
.command("add <name>")
|
|
181
|
+
.description("Register a component in the catalog.")
|
|
182
|
+
.option("--description <text>", "Component description")
|
|
183
|
+
.option("--category <text>", "form | navigation | feedback | layout | ...")
|
|
184
|
+
.option("--variants <list>", "Comma-separated variants")
|
|
185
|
+
.option("--states <list>", "Comma-separated states (hover, focus, disabled, ...)")
|
|
186
|
+
.option("--files <list>", "Comma-separated file paths")
|
|
187
|
+
.option("--no-sync", "Skip automatic AI context regeneration")
|
|
188
|
+
.action((name, opts) => componentAddCommand(name, opts));
|
|
189
|
+
component
|
|
190
|
+
.command("list")
|
|
191
|
+
.description("List components registered in the catalog.")
|
|
192
|
+
.action(componentListCommand);
|
|
193
|
+
const mcp = program
|
|
194
|
+
.command("mcp")
|
|
195
|
+
.description("Run or configure the Whale MCP server (for Claude Code, Cursor, Zed, etc.).");
|
|
196
|
+
mcp
|
|
197
|
+
.command("serve")
|
|
198
|
+
.description("Start the MCP server on stdio. Invoked by AI clients via their MCP config.")
|
|
199
|
+
.action(() => mcpServeCommand());
|
|
200
|
+
mcp
|
|
201
|
+
.command("config")
|
|
202
|
+
.description("Print example MCP client configuration for the current workspace.")
|
|
203
|
+
.option("--client <name>", "claude-code | cursor | zed | raw", "claude-code")
|
|
204
|
+
.option("--project <path>", "Workspace path (default: cwd)")
|
|
205
|
+
.action((opts) => mcpConfigCommand(opts));
|
|
206
|
+
program
|
|
207
|
+
.command("watch [target]")
|
|
208
|
+
.description("Watch foundations and intelligence stores; regenerate CLAUDE.md and wiki on change.")
|
|
209
|
+
.option("--debounce <ms>", "Debounce window in milliseconds (default 250)", (v) => parseInt(v, 10))
|
|
210
|
+
.option("--verbose", "Print every change event and regeneration detail")
|
|
211
|
+
.option("--once", "Run a single regeneration and exit (useful for CI)")
|
|
212
|
+
.action((target, opts) => watchCommand(target, opts));
|
|
213
|
+
program.parse();
|