whale-igniter 1.2.2 → 1.3.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/README.md +89 -281
- package/dist/analyzer/insights.js +144 -0
- package/dist/commands/adopt.js +20 -0
- package/dist/commands/changes.js +127 -0
- package/dist/commands/ignite.js +142 -76
- package/dist/commands/insights.js +146 -9
- package/dist/commands/references.js +193 -0
- package/dist/commands/sync.js +8 -0
- package/dist/generators/wikiGenerator.js +8 -304
- package/dist/index.js +33 -2
- package/dist/mcp/server.js +29 -0
- package/dist/scanner/extractors/css.js +95 -0
- package/dist/scanner/extractors/inline.js +131 -0
- package/dist/scanner/extractors/styleBlocks.js +37 -0
- package/dist/scanner/normalizer.js +59 -0
- package/dist/scanner/tailwindScanner.js +39 -0
- package/dist/templates/claude.js +93 -0
- package/dist/templates/components.js +45 -0
- package/dist/templates/conventions.js +45 -0
- package/dist/templates/decisions.js +34 -0
- package/dist/templates/foundations.js +34 -0
- package/dist/templates/project.js +82 -0
- package/dist/utils/aiAvailability.js +25 -0
- package/dist/utils/wizardMapping.js +54 -0
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { resolveTarget } from "../utils/paths.js";
|
|
4
|
+
import { loadConfig, saveConfig } from "../utils/config.js";
|
|
5
|
+
import { generateWiki } from "../generators/wikiGenerator.js";
|
|
6
|
+
import { ui } from "../ui/index.js";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
const VALID_CATEGORIES = ["forms", "navigation", "feedback", "surface", "layout", "data-display"];
|
|
9
|
+
function shippedReferencesDir() {
|
|
10
|
+
// Resolve relative to this file's location: src/commands/ → root → ui-references/
|
|
11
|
+
const thisFile = new URL(import.meta.url).pathname;
|
|
12
|
+
return path.resolve(path.dirname(thisFile), "../../ui-references");
|
|
13
|
+
}
|
|
14
|
+
function projectReferencesDir(target) {
|
|
15
|
+
return path.join(target, "references");
|
|
16
|
+
}
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// `whale references list`
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export async function referencesListCommand(targetArg) {
|
|
21
|
+
const target = resolveTarget(targetArg);
|
|
22
|
+
const config = await loadConfig(target);
|
|
23
|
+
const refDir = projectReferencesDir(target);
|
|
24
|
+
console.log();
|
|
25
|
+
console.log(ui.header("UI References", "list"));
|
|
26
|
+
console.log();
|
|
27
|
+
const configuredCategories = config.uiReferences ?? [];
|
|
28
|
+
const rows = [];
|
|
29
|
+
for (const cat of VALID_CATEGORIES) {
|
|
30
|
+
const filePath = path.join(refDir, `${cat}.md`);
|
|
31
|
+
const present = await fs.pathExists(filePath);
|
|
32
|
+
const configured = configuredCategories.includes(cat);
|
|
33
|
+
const status = present ? ui.ok("present") : ui.muted("not copied");
|
|
34
|
+
const flag = configured ? ui.glyph.check : " ";
|
|
35
|
+
rows.push([flag, ui.code(cat), status]);
|
|
36
|
+
}
|
|
37
|
+
const indexPath = path.join(refDir, "INDEX.md");
|
|
38
|
+
const indexPresent = await fs.pathExists(indexPath);
|
|
39
|
+
console.log(ui.section("Categories"));
|
|
40
|
+
for (const [flag, cat, status] of rows) {
|
|
41
|
+
console.log(` ${flag} ${cat.padEnd(16)} ${status}`);
|
|
42
|
+
}
|
|
43
|
+
console.log();
|
|
44
|
+
console.log(ui.kv("references/INDEX.md", indexPresent ? ui.ok("present") : ui.muted("not copied")));
|
|
45
|
+
if (configuredCategories.length === 0 && !indexPresent) {
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(ui.note("No references installed yet."));
|
|
48
|
+
console.log(ui.indent(ui.muted("Run `whale references add <category>` or `whale references add --all`.")));
|
|
49
|
+
}
|
|
50
|
+
console.log();
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// `whale references add [category | --all]`
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
export async function referencesAddCommand(category, opts) {
|
|
56
|
+
const target = resolveTarget(opts.target);
|
|
57
|
+
const shipped = shippedReferencesDir();
|
|
58
|
+
const refDir = projectReferencesDir(target);
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(ui.header("UI References", opts.all ? "add --all" : `add ${category ?? ""}`));
|
|
61
|
+
console.log();
|
|
62
|
+
if (!opts.all && !category) {
|
|
63
|
+
console.log(ui.fail("Specify a category or use --all."));
|
|
64
|
+
console.log(ui.muted(` Valid categories: ${VALID_CATEGORIES.join(", ")}`));
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const categoriesToAdd = opts.all
|
|
69
|
+
? [...VALID_CATEGORIES]
|
|
70
|
+
: [category];
|
|
71
|
+
// Validate
|
|
72
|
+
for (const cat of categoriesToAdd) {
|
|
73
|
+
if (!VALID_CATEGORIES.includes(cat)) {
|
|
74
|
+
console.log(ui.fail(`Unknown category: ${cat}`));
|
|
75
|
+
console.log(ui.muted(` Valid: ${VALID_CATEGORIES.join(", ")}`));
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
await fs.ensureDir(refDir);
|
|
81
|
+
// Always copy INDEX.md
|
|
82
|
+
const shippedIndex = path.join(shipped, "INDEX.md");
|
|
83
|
+
const projectIndex = path.join(refDir, "INDEX.md");
|
|
84
|
+
if (!(await fs.pathExists(projectIndex))) {
|
|
85
|
+
await fs.copy(shippedIndex, projectIndex);
|
|
86
|
+
console.log(ui.ok(`Copied references/INDEX.md`));
|
|
87
|
+
}
|
|
88
|
+
const copied = [];
|
|
89
|
+
const skipped = [];
|
|
90
|
+
for (const cat of categoriesToAdd) {
|
|
91
|
+
const src = path.join(shipped, `${cat}.md`);
|
|
92
|
+
const dest = path.join(refDir, `${cat}.md`);
|
|
93
|
+
if (!(await fs.pathExists(src))) {
|
|
94
|
+
console.log(ui.warn(`Shipped file not found for category: ${cat}`));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (await fs.pathExists(dest)) {
|
|
98
|
+
skipped.push(cat);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
await fs.copy(src, dest);
|
|
102
|
+
copied.push(cat);
|
|
103
|
+
console.log(ui.ok(`Copied references/${cat}.md`));
|
|
104
|
+
}
|
|
105
|
+
if (skipped.length > 0) {
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(ui.note(`Skipped (already present): ${skipped.join(", ")}. Run \`whale references update\` to refresh.`));
|
|
108
|
+
}
|
|
109
|
+
// Update whale.config.json
|
|
110
|
+
const config = await loadConfig(target);
|
|
111
|
+
const existing = new Set(config.uiReferences ?? []);
|
|
112
|
+
for (const cat of copied)
|
|
113
|
+
existing.add(cat);
|
|
114
|
+
config.uiReferences = Array.from(existing).sort();
|
|
115
|
+
await saveConfig(target, config);
|
|
116
|
+
// Regenerate CLAUDE.md to include the references section
|
|
117
|
+
if (copied.length > 0) {
|
|
118
|
+
await generateWiki(target);
|
|
119
|
+
console.log();
|
|
120
|
+
console.log(ui.ok("CLAUDE.md updated with references section."));
|
|
121
|
+
}
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(ui.info(`AI tools can now read references/ — consult references/INDEX.md for navigation.`));
|
|
124
|
+
console.log();
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// `whale references update`
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
export async function referencesUpdateCommand(targetArg) {
|
|
130
|
+
const target = resolveTarget(targetArg);
|
|
131
|
+
const shipped = shippedReferencesDir();
|
|
132
|
+
const refDir = projectReferencesDir(target);
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(ui.header("UI References", "update"));
|
|
135
|
+
console.log();
|
|
136
|
+
if (!(await fs.pathExists(refDir))) {
|
|
137
|
+
console.log(ui.warn("No references/ directory found in this project."));
|
|
138
|
+
console.log(ui.muted(" Run `whale references add <category>` first."));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const config = await loadConfig(target);
|
|
142
|
+
const configuredCategories = config.uiReferences ?? [];
|
|
143
|
+
const allFilesToCheck = ["INDEX", ...configuredCategories.map((c) => c)];
|
|
144
|
+
for (const slug of allFilesToCheck) {
|
|
145
|
+
const filename = slug === "INDEX" ? "INDEX.md" : `${slug}.md`;
|
|
146
|
+
const src = path.join(shipped, filename);
|
|
147
|
+
const dest = path.join(refDir, filename);
|
|
148
|
+
if (!(await fs.pathExists(src)))
|
|
149
|
+
continue;
|
|
150
|
+
if (!(await fs.pathExists(dest))) {
|
|
151
|
+
await fs.copy(src, dest);
|
|
152
|
+
console.log(ui.ok(`Added ${filename} (was missing)`));
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// Compare content — only prompt if modified
|
|
156
|
+
const [srcContent, destContent] = await Promise.all([
|
|
157
|
+
fs.readFile(src, "utf-8"),
|
|
158
|
+
fs.readFile(dest, "utf-8")
|
|
159
|
+
]);
|
|
160
|
+
if (srcContent === destContent) {
|
|
161
|
+
console.log(` ${ui.glyph.check} ${filename} is up to date`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// File differs — prompt
|
|
165
|
+
const { action } = await prompts({
|
|
166
|
+
type: "select",
|
|
167
|
+
name: "action",
|
|
168
|
+
message: `references/${filename} has local modifications. What would you like to do?`,
|
|
169
|
+
choices: [
|
|
170
|
+
{ title: "Keep my version", value: "keep" },
|
|
171
|
+
{ title: "Overwrite with shipped version", value: "overwrite" },
|
|
172
|
+
{ title: "Show diff summary", value: "diff" }
|
|
173
|
+
]
|
|
174
|
+
});
|
|
175
|
+
if (action === "overwrite") {
|
|
176
|
+
await fs.copy(src, dest);
|
|
177
|
+
console.log(ui.ok(`Overwritten: references/${filename}`));
|
|
178
|
+
}
|
|
179
|
+
else if (action === "diff") {
|
|
180
|
+
const srcLines = srcContent.split("\n").length;
|
|
181
|
+
const destLines = destContent.split("\n").length;
|
|
182
|
+
console.log(ui.note(`Shipped: ${srcLines} lines. Your version: ${destLines} lines. ` +
|
|
183
|
+
`Use a diff tool to compare: diff references/${filename} <(whale references --shipped ${slug})`));
|
|
184
|
+
console.log(ui.muted(" Keeping your version."));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
console.log(ui.muted(` Kept: references/${filename}`));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
console.log();
|
|
191
|
+
console.log(ui.accent("References are up to date."));
|
|
192
|
+
console.log();
|
|
193
|
+
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -6,6 +6,7 @@ import { loadConfig } from "../utils/config.js";
|
|
|
6
6
|
import { loadDecisions } from "../utils/decisions.js";
|
|
7
7
|
import { loadComponents } from "../utils/components.js";
|
|
8
8
|
import { loadRefinements } from "../utils/refinements.js";
|
|
9
|
+
import { getAiAvailability } from "../utils/aiAvailability.js";
|
|
9
10
|
import { ui } from "../ui/index.js";
|
|
10
11
|
/**
|
|
11
12
|
* `whale sync` regenerates everything an AI agent reads, from the
|
|
@@ -40,4 +41,11 @@ export async function syncCommand(targetArg) {
|
|
|
40
41
|
console.log();
|
|
41
42
|
console.log(ui.accent("AI agents will now see the latest project state."));
|
|
42
43
|
console.log(ui.muted(`Targets configured: ${(config.aiTargets ?? []).join(", ") || "(none)"}`));
|
|
44
|
+
if (config.enrichWithAi) {
|
|
45
|
+
const ai = await getAiAvailability(target);
|
|
46
|
+
if (ai.available) {
|
|
47
|
+
console.log();
|
|
48
|
+
console.log(ui.note("Scribe enrichment enabled (config.enrichWithAi=true). Run `whale selene suggest` to enrich wiki prose."));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
43
51
|
}
|
|
@@ -5,6 +5,12 @@ import { loadRefinements } from "../utils/refinements.js";
|
|
|
5
5
|
import { loadDecisions } from "../utils/decisions.js";
|
|
6
6
|
import { loadComponents } from "../utils/components.js";
|
|
7
7
|
import { validateCss } from "../validators/cssValidator.js";
|
|
8
|
+
import { renderFoundations } from "../templates/foundations.js";
|
|
9
|
+
import { renderConventions } from "../templates/conventions.js";
|
|
10
|
+
import { renderDecisions } from "../templates/decisions.js";
|
|
11
|
+
import { renderComponents } from "../templates/components.js";
|
|
12
|
+
import { renderProject, renderWikiReadme, renderWorkflows } from "../templates/project.js";
|
|
13
|
+
import { renderRootIndex } from "../templates/claude.js";
|
|
8
14
|
export async function generateWiki(target) {
|
|
9
15
|
const config = await loadConfig(target);
|
|
10
16
|
const refinements = await loadRefinements(target);
|
|
@@ -15,11 +21,10 @@ export async function generateWiki(target) {
|
|
|
15
21
|
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
16
22
|
const wikiDir = path.join(target, "llm-wiki");
|
|
17
23
|
await fs.ensureDir(wikiDir);
|
|
18
|
-
// ---- llm-wiki/ themed files ------------------------------------------------
|
|
19
24
|
const themedFiles = {
|
|
20
25
|
"README.md": renderWikiReadme(config),
|
|
21
26
|
"PROJECT.md": renderProject(config, errors, warnings, refinements.length, decisions.length),
|
|
22
|
-
"FOUNDATIONS.md": renderFoundations(config),
|
|
27
|
+
"FOUNDATIONS.md": renderFoundations(config, decisions),
|
|
23
28
|
"CONVENTIONS.md": renderConventions(config, refinements),
|
|
24
29
|
"DECISIONS.md": renderDecisions(decisions),
|
|
25
30
|
"COMPONENTS.md": renderComponents(components),
|
|
@@ -31,7 +36,6 @@ export async function generateWiki(target) {
|
|
|
31
36
|
await fs.writeFile(filePath, content);
|
|
32
37
|
wikiFiles.push(filePath);
|
|
33
38
|
}
|
|
34
|
-
// ---- root-level AI entry points -------------------------------------------
|
|
35
39
|
const rootIndex = renderRootIndex(config, {
|
|
36
40
|
errors,
|
|
37
41
|
warnings,
|
|
@@ -41,14 +45,12 @@ export async function generateWiki(target) {
|
|
|
41
45
|
});
|
|
42
46
|
const rootFiles = [];
|
|
43
47
|
const targets = config.aiTargets ?? ["claude"];
|
|
44
|
-
// CLAUDE.md is always written (default reader). Other agent files mirror it.
|
|
45
48
|
const filenamesByTarget = {
|
|
46
49
|
claude: "CLAUDE.md",
|
|
47
|
-
codex: "AGENTS.md",
|
|
50
|
+
codex: "AGENTS.md",
|
|
48
51
|
cursor: ".cursorrules",
|
|
49
52
|
copilot: ".github/copilot-instructions.md"
|
|
50
53
|
};
|
|
51
|
-
// Always emit CLAUDE.md as the canonical file, even if claude isn't listed.
|
|
52
54
|
const written = new Set();
|
|
53
55
|
const emit = async (filename) => {
|
|
54
56
|
if (written.has(filename))
|
|
@@ -65,301 +67,3 @@ export async function generateWiki(target) {
|
|
|
65
67
|
}
|
|
66
68
|
return { rootFiles, wikiFiles };
|
|
67
69
|
}
|
|
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
CHANGED
|
@@ -12,13 +12,15 @@ import { decisionCommand } from "./commands/decision.js";
|
|
|
12
12
|
import { componentAddCommand, componentListCommand } from "./commands/component.js";
|
|
13
13
|
import { adoptCommand } from "./commands/adopt.js";
|
|
14
14
|
import { adoptReviewCommand, adoptStatusCommand } from "./commands/adoptReview.js";
|
|
15
|
-
import { insightsCommand } from "./commands/insights.js";
|
|
15
|
+
import { insightsCommand, insightsDriftReviewCommand } from "./commands/insights.js";
|
|
16
16
|
import { createComponentCommand } from "./commands/createComponent.js";
|
|
17
17
|
import { seleneDescribeCommand, seleneAuditCommand, seleneSuggestCommand, seleneApplyCommand, seleneStatusCommand, seleneCacheClearCommand } from "./commands/selene.js";
|
|
18
18
|
import { mcpServeCommand, mcpConfigCommand } from "./commands/mcp.js";
|
|
19
19
|
import { watchCommand } from "./commands/watch.js";
|
|
20
20
|
import { rememberCommand, checkCommand, improveCommand, explainCommand } from "./commands/friendly.js";
|
|
21
21
|
import { teamAddCommand, teamListCommand } from "./commands/team.js";
|
|
22
|
+
import { referencesListCommand, referencesAddCommand, referencesUpdateCommand } from "./commands/references.js";
|
|
23
|
+
import { changesCommand } from "./commands/changes.js";
|
|
22
24
|
import { PACKAGE_VERSION } from "./version.js";
|
|
23
25
|
const program = new Command();
|
|
24
26
|
program
|
|
@@ -142,7 +144,7 @@ cache
|
|
|
142
144
|
.command("clear")
|
|
143
145
|
.description("Delete all cached Selene responses.")
|
|
144
146
|
.action(() => seleneCacheClearCommand());
|
|
145
|
-
program
|
|
147
|
+
const insightsCmd = program
|
|
146
148
|
.command("insights [target]")
|
|
147
149
|
.description("Run local analyzers over the intelligence stores and surface accountable recommendations.")
|
|
148
150
|
.option("--json", "Emit insights as JSON (for scripting / CI)")
|
|
@@ -150,6 +152,12 @@ program
|
|
|
150
152
|
.option("--min-severity <level>", "Show only insights at this severity or higher (info | warning | critical)")
|
|
151
153
|
.option("--skip-scan", "Skip the source scan (faster, but disables orphan/drift insights)")
|
|
152
154
|
.action((target, opts) => insightsCommand(target, opts));
|
|
155
|
+
insightsCmd
|
|
156
|
+
.command("drift <category>")
|
|
157
|
+
.description("Show or review drift in spacing | color | radii.")
|
|
158
|
+
.option("--review", "Enter interactive review loop: accept as refinement, flag for refactor, or skip")
|
|
159
|
+
.option("--target <path>", "Workspace path (default: cwd)")
|
|
160
|
+
.action((category, opts) => insightsDriftReviewCommand(category, opts));
|
|
153
161
|
const create = program
|
|
154
162
|
.command("create")
|
|
155
163
|
.description("Generate new code that respects the project's foundations.");
|
|
@@ -234,6 +242,24 @@ mcp
|
|
|
234
242
|
.option("--client <name>", "claude-code | cursor | zed | raw", "claude-code")
|
|
235
243
|
.option("--project <path>", "Workspace path (default: cwd)")
|
|
236
244
|
.action((opts) => mcpConfigCommand(opts));
|
|
245
|
+
const references = program
|
|
246
|
+
.command("references")
|
|
247
|
+
.description("Manage the UI component reference library for this project.");
|
|
248
|
+
references
|
|
249
|
+
.command("list [target]")
|
|
250
|
+
.description("Show available reference categories and which are present in this project.")
|
|
251
|
+
.action((target) => referencesListCommand(target));
|
|
252
|
+
references
|
|
253
|
+
.command("add [category]")
|
|
254
|
+
.description("Copy a reference category from the Whale library into references/<category>.md.")
|
|
255
|
+
.option("--all", "Copy all categories (INDEX + all six category files)")
|
|
256
|
+
.option("--target <path>", "Workspace path (default: cwd)")
|
|
257
|
+
.action((category, opts) => referencesAddCommand(category, opts));
|
|
258
|
+
references
|
|
259
|
+
.command("update [target]")
|
|
260
|
+
.description("Re-copy references from Whale, prompting to keep/overwrite locally modified files.")
|
|
261
|
+
.action((target) => referencesUpdateCommand(target));
|
|
262
|
+
references.action((opts) => referencesListCommand(opts.target));
|
|
237
263
|
program
|
|
238
264
|
.command("watch [target]")
|
|
239
265
|
.description("Watch foundations and intelligence stores; regenerate CLAUDE.md and wiki on change.")
|
|
@@ -241,4 +267,9 @@ program
|
|
|
241
267
|
.option("--verbose", "Print every change event and regeneration detail")
|
|
242
268
|
.option("--once", "Run a single regeneration and exit (useful for CI)")
|
|
243
269
|
.action((target, opts) => watchCommand(target, opts));
|
|
270
|
+
program
|
|
271
|
+
.command("changes [target]")
|
|
272
|
+
.description("Show what changed in intelligence stores since a git ref.")
|
|
273
|
+
.option("--since <ref>", "Git ref to compare against (default: HEAD~1)", "HEAD~1")
|
|
274
|
+
.action((target, opts) => changesCommand({ since: opts.since, target }));
|
|
244
275
|
program.parse();
|
package/dist/mcp/server.js
CHANGED
|
@@ -381,6 +381,35 @@ export function buildMcpServer() {
|
|
|
381
381
|
return ok(`Regenerated ${rootFiles.length} root file(s) and ${wikiFiles.length} wiki file(s).\n` +
|
|
382
382
|
`Root: ${rootFiles.map((f) => path.relative(ws, f)).join(", ")}`);
|
|
383
383
|
});
|
|
384
|
+
server.registerTool("whale_ui_reference", {
|
|
385
|
+
title: "Read a UI references category file",
|
|
386
|
+
description: "Returns the contents of a curated UI component reference file from this " +
|
|
387
|
+
"project's references/ directory. These files document the quality bar and " +
|
|
388
|
+
"best implementations for each component category. Call this BEFORE building " +
|
|
389
|
+
"any UI component — consult the relevant category to understand what states " +
|
|
390
|
+
"and behaviors are required. Use 'INDEX' as the category to get the index file.",
|
|
391
|
+
inputSchema: {
|
|
392
|
+
category: z
|
|
393
|
+
.string()
|
|
394
|
+
.describe("Category slug: forms | navigation | feedback | surface | layout | data-display | INDEX"),
|
|
395
|
+
target: z.string().optional().describe("Workspace path. Defaults to WHALE_PROJECT or cwd.")
|
|
396
|
+
}
|
|
397
|
+
}, async ({ category, target }) => {
|
|
398
|
+
const ws = resolveWorkspace(target);
|
|
399
|
+
const filename = category.toUpperCase() === "INDEX" ? "INDEX.md" : `${category}.md`;
|
|
400
|
+
const referencePath = path.join(ws, "references", filename);
|
|
401
|
+
if (!(await fs.pathExists(referencePath))) {
|
|
402
|
+
const configPath = path.join(ws, "whale.config.json");
|
|
403
|
+
const hasProject = await fs.pathExists(configPath);
|
|
404
|
+
if (!hasProject) {
|
|
405
|
+
return err(`No whale.config.json found at ${ws}. Run \`whale ignite\` or \`whale adopt\` first.`);
|
|
406
|
+
}
|
|
407
|
+
return err(`references/${filename} not found in this project. ` +
|
|
408
|
+
`Run \`whale references add ${category}\` to copy it from the Whale library.`);
|
|
409
|
+
}
|
|
410
|
+
const content = await fs.readFile(referencePath, "utf-8");
|
|
411
|
+
return ok(content);
|
|
412
|
+
});
|
|
384
413
|
return server;
|
|
385
414
|
}
|
|
386
415
|
/**
|