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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `whale watch` — regenerate the AI-readable wiki when relevant files change.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists. Whale's value depends on CLAUDE.md being current.
|
|
5
|
+
* Every write command (refine, decision, component add, adopt review) auto-syncs,
|
|
6
|
+
* but humans also edit `intelligence/*.json` directly, change `whale.config.json`,
|
|
7
|
+
* or commit catalog files from another tool. Without a watcher the gap between
|
|
8
|
+
* "what's true" and "what the AI sees" silently widens. Watcher closes that gap.
|
|
9
|
+
*
|
|
10
|
+
* Implementation. Node's built-in fs.watch is enough — we only watch a handful
|
|
11
|
+
* of small files plus the intelligence directory. We deliberately don't pull in
|
|
12
|
+
* chokidar; it adds dependencies and we don't need its cross-platform polyfills
|
|
13
|
+
* for this narrow use. Debouncing handles editors that write through a tmpfile
|
|
14
|
+
* (which often produces 2-3 events per save).
|
|
15
|
+
*/
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import fs from "fs-extra";
|
|
18
|
+
import { watch } from "node:fs";
|
|
19
|
+
import { resolveTarget } from "../utils/paths.js";
|
|
20
|
+
import { generateWiki } from "../generators/wikiGenerator.js";
|
|
21
|
+
import { ui } from "../ui/index.js";
|
|
22
|
+
/**
|
|
23
|
+
* Files and directories whose changes should trigger a regeneration.
|
|
24
|
+
* Paths are relative to the workspace root.
|
|
25
|
+
*/
|
|
26
|
+
const WATCH_TARGETS = [
|
|
27
|
+
"whale.config.json",
|
|
28
|
+
"intelligence" // directory — watch recursively if supported
|
|
29
|
+
];
|
|
30
|
+
export async function watchCommand(targetArg, options = {}) {
|
|
31
|
+
const target = resolveTarget(targetArg);
|
|
32
|
+
// Verify this is a Whale project. We don't want the watcher to silently
|
|
33
|
+
// run against a directory that has nothing to regenerate.
|
|
34
|
+
if (!(await fs.pathExists(path.join(target, "whale.config.json")))) {
|
|
35
|
+
console.log(ui.fail(`No whale.config.json at ${ui.path(target)}.`));
|
|
36
|
+
console.log(ui.muted(" Run `whale ignite` or `whale adopt` first."));
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (options.once) {
|
|
41
|
+
await regenerate(target, options.verbose);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
console.log(ui.header("Whale Igniter", `watch • ${path.relative(process.cwd(), target) || "."}`));
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(` ${ui.kv("watching", WATCH_TARGETS.join(", "), { keyWidth: 8 })}`);
|
|
47
|
+
console.log(ui.muted(" Press Ctrl+C to stop."));
|
|
48
|
+
console.log();
|
|
49
|
+
const debounceMs = options.debounce ?? 250;
|
|
50
|
+
let timer = null;
|
|
51
|
+
let inFlight = false;
|
|
52
|
+
let pendingWhileRunning = false;
|
|
53
|
+
const trigger = (reason) => {
|
|
54
|
+
if (options.verbose)
|
|
55
|
+
console.log(ui.muted(` ◦ ${reason}`));
|
|
56
|
+
if (timer)
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
timer = setTimeout(async () => {
|
|
59
|
+
timer = null;
|
|
60
|
+
if (inFlight) {
|
|
61
|
+
// Coalesce: remember that something else changed while we were busy.
|
|
62
|
+
pendingWhileRunning = true;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
inFlight = true;
|
|
66
|
+
try {
|
|
67
|
+
await regenerate(target, options.verbose);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
71
|
+
console.log(ui.fail(`regeneration failed: ${msg}`));
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
inFlight = false;
|
|
75
|
+
if (pendingWhileRunning) {
|
|
76
|
+
pendingWhileRunning = false;
|
|
77
|
+
trigger("queued change picked up");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}, debounceMs);
|
|
81
|
+
};
|
|
82
|
+
const watchers = [];
|
|
83
|
+
for (const rel of WATCH_TARGETS) {
|
|
84
|
+
const abs = path.join(target, rel);
|
|
85
|
+
if (!(await fs.pathExists(abs))) {
|
|
86
|
+
if (options.verbose)
|
|
87
|
+
console.log(ui.muted(` (skip ${rel} — not present)`));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
// recursive is supported on macOS and Windows, but not all Linux setups.
|
|
92
|
+
// Try recursive first, fall back to non-recursive on error.
|
|
93
|
+
const w = watch(abs, { recursive: true }, (eventType, filename) => {
|
|
94
|
+
trigger(`${rel}${filename ? "/" + filename : ""} (${eventType})`);
|
|
95
|
+
});
|
|
96
|
+
watchers.push(w);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
try {
|
|
100
|
+
const w = watch(abs, (eventType, filename) => {
|
|
101
|
+
trigger(`${rel}${filename ? "/" + filename : ""} (${eventType})`);
|
|
102
|
+
});
|
|
103
|
+
watchers.push(w);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
107
|
+
console.log(ui.warn(`couldn't watch ${rel}: ${msg}`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (watchers.length === 0) {
|
|
112
|
+
console.log(ui.fail("Nothing to watch. Exiting."));
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Do an initial regeneration so the wiki is fresh from the start.
|
|
117
|
+
await regenerate(target, options.verbose);
|
|
118
|
+
// Block until interrupted.
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const stop = () => {
|
|
121
|
+
console.log();
|
|
122
|
+
console.log(ui.muted("Stopping watch."));
|
|
123
|
+
for (const w of watchers) {
|
|
124
|
+
try {
|
|
125
|
+
w.close();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// ignore
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (timer)
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
resolve();
|
|
134
|
+
};
|
|
135
|
+
process.on("SIGINT", stop);
|
|
136
|
+
process.on("SIGTERM", stop);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async function regenerate(target, verbose) {
|
|
140
|
+
const start = Date.now();
|
|
141
|
+
const { rootFiles, wikiFiles } = await generateWiki(target);
|
|
142
|
+
const ms = Date.now() - start;
|
|
143
|
+
const stamp = new Date().toLocaleTimeString();
|
|
144
|
+
console.log(ui.success(`[${stamp}] ${ui.glyph.check} regenerated`) +
|
|
145
|
+
ui.muted(` (${rootFiles.length} root, ${wikiFiles.length} wiki, ${ms}ms)`));
|
|
146
|
+
if (verbose) {
|
|
147
|
+
for (const f of rootFiles)
|
|
148
|
+
console.log(ui.muted(` ${path.relative(target, f)}`));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { resolveTarget } from "../utils/paths.js";
|
|
4
|
+
import { generateWiki } from "../generators/wikiGenerator.js";
|
|
5
|
+
import { ui } from "../ui/index.js";
|
|
6
|
+
export async function wikiCommand(targetArg) {
|
|
7
|
+
const target = resolveTarget(targetArg);
|
|
8
|
+
const spinner = ora("Generating AI context").start();
|
|
9
|
+
const { rootFiles, wikiFiles } = await generateWiki(target);
|
|
10
|
+
spinner.succeed("AI context generated");
|
|
11
|
+
console.log();
|
|
12
|
+
console.log(ui.section("Root entry points"));
|
|
13
|
+
for (const file of rootFiles) {
|
|
14
|
+
console.log(` ${ui.ok(ui.path(path.relative(target, file)))}`);
|
|
15
|
+
}
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(ui.section("Wiki"));
|
|
18
|
+
for (const file of wikiFiles) {
|
|
19
|
+
console.log(` ${ui.ok(ui.path(path.relative(target, file)))}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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 { validateCss } from "../validators/cssValidator.js";
|
|
6
|
+
async function buildContext(target) {
|
|
7
|
+
const config = await loadConfig(target);
|
|
8
|
+
const refinements = await loadRefinements(target);
|
|
9
|
+
const issues = await validateCss(target, { configOverride: config });
|
|
10
|
+
return { target, config, refinements, issues };
|
|
11
|
+
}
|
|
12
|
+
function renderForge(ctx) {
|
|
13
|
+
const { config, issues } = ctx;
|
|
14
|
+
const lines = [
|
|
15
|
+
"# Forge — Engineering Handoff",
|
|
16
|
+
"",
|
|
17
|
+
"**Audience:** engineering",
|
|
18
|
+
"",
|
|
19
|
+
"## Foundations",
|
|
20
|
+
`- Grid: ${config.foundations?.grid ?? 8}px`,
|
|
21
|
+
`- Control radius: ${config.foundations?.radius?.control ?? 2}px`,
|
|
22
|
+
`- Container radius: ${config.foundations?.radius?.container ?? 4}px`,
|
|
23
|
+
"",
|
|
24
|
+
"## Implementation Contracts",
|
|
25
|
+
"- All spacing values MUST be multiples of the grid.",
|
|
26
|
+
"- Controls use the control radius; cards/containers use the container radius.",
|
|
27
|
+
"- All interactive elements MUST have a `:focus-visible` style.",
|
|
28
|
+
"- Color values MUST come from semantic tokens (`var(--color-*)`).",
|
|
29
|
+
"",
|
|
30
|
+
`## Open Issues (${issues.length})`,
|
|
31
|
+
""
|
|
32
|
+
];
|
|
33
|
+
for (const issue of issues) {
|
|
34
|
+
lines.push(`- \`${issue.file}:${issue.line}\` — ${issue.type}: ${issue.message}`);
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n") + "\n";
|
|
37
|
+
}
|
|
38
|
+
function renderScribe(ctx) {
|
|
39
|
+
const { config, issues, refinements } = ctx;
|
|
40
|
+
const lines = [
|
|
41
|
+
"# Scribe — Team Documentation",
|
|
42
|
+
"",
|
|
43
|
+
"**Audience:** team (mixed)",
|
|
44
|
+
"",
|
|
45
|
+
"## What this project uses",
|
|
46
|
+
`- Project type: ${config.projectType}`,
|
|
47
|
+
`- Active packs: ${(config.packs ?? []).join(", ") || "(none)"}`,
|
|
48
|
+
"",
|
|
49
|
+
"## Conventions",
|
|
50
|
+
`- Spacing: ${config.foundations?.grid ?? 8}px grid`,
|
|
51
|
+
`- Radii: ${config.foundations?.radius?.control ?? 2}px (controls) / ${config.foundations?.radius?.container ?? 4}px (containers)`,
|
|
52
|
+
"",
|
|
53
|
+
`## Recorded Decisions (${refinements.length})`,
|
|
54
|
+
""
|
|
55
|
+
];
|
|
56
|
+
for (const r of refinements) {
|
|
57
|
+
lines.push(`- ${r.timestamp.slice(0, 10)} — ${r.note}`);
|
|
58
|
+
}
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push(`## Current Validation Status`);
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push(`${issues.length} issue(s) open. Run \`whale validate\` for details.`);
|
|
63
|
+
return lines.join("\n") + "\n";
|
|
64
|
+
}
|
|
65
|
+
function renderAtlas(ctx) {
|
|
66
|
+
const { config, refinements, issues } = ctx;
|
|
67
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
68
|
+
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
69
|
+
const lines = [
|
|
70
|
+
"# Atlas — Product Strategy",
|
|
71
|
+
"",
|
|
72
|
+
"**Audience:** product",
|
|
73
|
+
"",
|
|
74
|
+
"## Project Posture",
|
|
75
|
+
`- Type: ${config.projectType}`,
|
|
76
|
+
`- Tone: ${config.branding?.tone ?? "unspecified"}`,
|
|
77
|
+
`- Accent: ${config.branding?.accent ?? "unspecified"}`,
|
|
78
|
+
"",
|
|
79
|
+
"## Operational Health",
|
|
80
|
+
`- Errors: ${errors}`,
|
|
81
|
+
`- Warnings: ${warnings}`,
|
|
82
|
+
`- Decisions logged: ${refinements.length}`,
|
|
83
|
+
"",
|
|
84
|
+
"## Decision Log",
|
|
85
|
+
""
|
|
86
|
+
];
|
|
87
|
+
for (const r of refinements) {
|
|
88
|
+
const scopeInfo = r.scope
|
|
89
|
+
? ` _(scope: ${[r.scope.issueType, r.scope.selector, r.scope.file].filter(Boolean).join(", ")})_`
|
|
90
|
+
: "";
|
|
91
|
+
lines.push(`- **${r.timestamp.slice(0, 10)}** — ${r.note}${scopeInfo}`);
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n") + "\n";
|
|
94
|
+
}
|
|
95
|
+
const RENDERERS = {
|
|
96
|
+
forge: renderForge,
|
|
97
|
+
scribe: renderScribe,
|
|
98
|
+
atlas: renderAtlas
|
|
99
|
+
};
|
|
100
|
+
export async function runGenerator(target, pack) {
|
|
101
|
+
const renderer = RENDERERS[pack.name];
|
|
102
|
+
if (!renderer) {
|
|
103
|
+
throw new Error(`No renderer registered for pack "${pack.name}".`);
|
|
104
|
+
}
|
|
105
|
+
const ctx = await buildContext(target);
|
|
106
|
+
const content = renderer(ctx);
|
|
107
|
+
const outDir = path.join(target, "docs", pack.name);
|
|
108
|
+
await fs.ensureDir(outDir);
|
|
109
|
+
const outFile = path.join(outDir, `${pack.name}-report.md`);
|
|
110
|
+
await fs.writeFile(outFile, content);
|
|
111
|
+
return outFile;
|
|
112
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { validateCss } from "../validators/cssValidator.js";
|
|
4
|
+
export async function generateValidationReport(target) {
|
|
5
|
+
const issues = await validateCss(target);
|
|
6
|
+
const reportPath = path.join(target, "docs", "validation-report.md");
|
|
7
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
8
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
9
|
+
const lines = [
|
|
10
|
+
"# Whale Validation Report",
|
|
11
|
+
"",
|
|
12
|
+
`_Generated ${new Date().toISOString()}_`,
|
|
13
|
+
"",
|
|
14
|
+
"## Summary",
|
|
15
|
+
"",
|
|
16
|
+
`- Total issues: **${issues.length}**`,
|
|
17
|
+
`- Errors: **${errors.length}**`,
|
|
18
|
+
`- Warnings: **${warnings.length}**`,
|
|
19
|
+
""
|
|
20
|
+
];
|
|
21
|
+
if (issues.length === 0) {
|
|
22
|
+
lines.push("✓ Clean run.");
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// Group by file so the report is actionable.
|
|
26
|
+
const byFile = new Map();
|
|
27
|
+
for (const issue of issues) {
|
|
28
|
+
const arr = byFile.get(issue.file) ?? [];
|
|
29
|
+
arr.push(issue);
|
|
30
|
+
byFile.set(issue.file, arr);
|
|
31
|
+
}
|
|
32
|
+
for (const [file, fileIssues] of byFile) {
|
|
33
|
+
lines.push(`## ${file}`);
|
|
34
|
+
lines.push("");
|
|
35
|
+
for (const issue of fileIssues) {
|
|
36
|
+
const tag = issue.severity === "error" ? "ERROR" : "WARNING";
|
|
37
|
+
lines.push(`- **${tag}** (${issue.type}) at line ${issue.line}`);
|
|
38
|
+
lines.push(` - ${issue.message}`);
|
|
39
|
+
lines.push(` - Fix: ${issue.suggestion}`);
|
|
40
|
+
if (issue.selector) {
|
|
41
|
+
lines.push(` - Selector: \`${issue.selector}\``);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
lines.push("");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await fs.ensureDir(path.dirname(reportPath));
|
|
48
|
+
await fs.writeFile(reportPath, lines.join("\n"));
|
|
49
|
+
return reportPath;
|
|
50
|
+
}
|