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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/dist/analyzer/imports.js +88 -0
  4. package/dist/analyzer/insights.js +276 -0
  5. package/dist/commands/add.js +36 -0
  6. package/dist/commands/adopt.js +180 -0
  7. package/dist/commands/adoptReview.js +267 -0
  8. package/dist/commands/component.js +93 -0
  9. package/dist/commands/createComponent.js +207 -0
  10. package/dist/commands/decision.js +98 -0
  11. package/dist/commands/docs.js +34 -0
  12. package/dist/commands/ignite.js +212 -0
  13. package/dist/commands/init.js +66 -0
  14. package/dist/commands/insights.js +123 -0
  15. package/dist/commands/mcp.js +106 -0
  16. package/dist/commands/refine.js +36 -0
  17. package/dist/commands/selene.js +516 -0
  18. package/dist/commands/sync.js +43 -0
  19. package/dist/commands/validate.js +48 -0
  20. package/dist/commands/watch.js +150 -0
  21. package/dist/commands/wiki.js +21 -0
  22. package/dist/generators/markdownGenerator.js +112 -0
  23. package/dist/generators/reportGenerator.js +50 -0
  24. package/dist/generators/wikiGenerator.js +365 -0
  25. package/dist/index.js +213 -0
  26. package/dist/mcp/server.js +404 -0
  27. package/dist/scanner/componentScanner.js +522 -0
  28. package/dist/scanner/foundationInferrer.js +174 -0
  29. package/dist/scanner/tailwindMapper.js +58 -0
  30. package/dist/scanner/tailwindScanner.js +186 -0
  31. package/dist/selene/apiClient.js +168 -0
  32. package/dist/selene/cache.js +68 -0
  33. package/dist/selene/clipboard.js +56 -0
  34. package/dist/selene/promptBuilder.js +229 -0
  35. package/dist/selene/providers.js +67 -0
  36. package/dist/selene/responseParser.js +149 -0
  37. package/dist/ui/atoms.js +30 -0
  38. package/dist/ui/blocks.js +208 -0
  39. package/dist/ui/capabilities.js +64 -0
  40. package/dist/ui/index.js +13 -0
  41. package/dist/ui/symbols.js +41 -0
  42. package/dist/ui/theme.js +78 -0
  43. package/dist/utils/components.js +40 -0
  44. package/dist/utils/config.js +31 -0
  45. package/dist/utils/decisions.js +32 -0
  46. package/dist/utils/paths.js +4 -0
  47. package/dist/utils/proposals.js +61 -0
  48. package/dist/utils/refinements.js +81 -0
  49. package/dist/utils/registry.js +45 -0
  50. package/dist/utils/writeJson.js +6 -0
  51. package/dist/validators/cssValidator.js +204 -0
  52. package/dist/version.js +1 -0
  53. package/docs/ROADMAP.md +206 -0
  54. package/package.json +76 -0
@@ -0,0 +1,123 @@
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 { loadRefinements } from "../utils/refinements.js";
7
+ import { loadDecisions } from "../utils/decisions.js";
8
+ import { loadComponents } from "../utils/components.js";
9
+ import { analyze } from "../analyzer/insights.js";
10
+ import { collectReferencedFiles, normalizePath } from "../analyzer/imports.js";
11
+ import { scanComponents } from "../scanner/componentScanner.js";
12
+ import { aggregateTailwind } from "../scanner/tailwindScanner.js";
13
+ import { ui } from "../ui/index.js";
14
+ const SEVERITY_RANK = { info: 0, warning: 1, critical: 2 };
15
+ export async function insightsCommand(targetArg, options = {}) {
16
+ const target = resolveTarget(targetArg);
17
+ if (!(await fs.pathExists(path.join(target, "whale.config.json")))) {
18
+ console.log();
19
+ console.log(ui.warn("No whale.config.json found at this target. Run `whale ignite` or `whale adopt` first."));
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ const jsonMode = !!options.json;
24
+ if (!jsonMode) {
25
+ console.log();
26
+ console.log(ui.header("Whale Igniter", "insights"));
27
+ console.log();
28
+ }
29
+ const spin = ora({ text: "Loading intelligence stores", stream: process.stderr }).start();
30
+ const config = await loadConfig(target);
31
+ const refinements = await loadRefinements(target);
32
+ const decisions = await loadDecisions(target);
33
+ const components = await loadComponents(target);
34
+ spin.succeed(`Loaded ${refinements.length} refinement(s), ${decisions.length} decision(s), ${components.length} component(s)`);
35
+ let referencedFiles;
36
+ let observations;
37
+ if (!options.skipScan) {
38
+ const spinScan = ora({ text: "Scanning source for imports and Tailwind usage", stream: process.stderr }).start();
39
+ try {
40
+ const [refs, scanned] = await Promise.all([collectReferencedFiles(target), scanComponents(target)]);
41
+ referencedFiles = refs;
42
+ const allClassStrings = scanned.flatMap((c) => c.classNames);
43
+ observations = allClassStrings.length > 0 ? aggregateTailwind(allClassStrings) : undefined;
44
+ spinScan.succeed(`Scanned ${refs.size} import reference(s)` +
45
+ (observations ? ` and ${observations.length} class observation(s)` : ""));
46
+ }
47
+ catch (err) {
48
+ spinScan.warn(`Scan failed: ${err?.message ?? err}. Continuing without orphan/drift insights.`);
49
+ }
50
+ }
51
+ const normalisedComponents = referencedFiles
52
+ ? components.map((c) => ({ ...c, files: c.files?.map(normalizePath) }))
53
+ : components;
54
+ const insights = analyze({
55
+ config,
56
+ refinements,
57
+ decisions,
58
+ components: normalisedComponents,
59
+ observations,
60
+ referencedFiles
61
+ });
62
+ return printOrEmit(insights, options);
63
+ }
64
+ function printOrEmit(insights, options) {
65
+ let filtered = insights;
66
+ if (options.category)
67
+ filtered = filtered.filter((i) => i.category === options.category);
68
+ if (options.minSeverity) {
69
+ const min = SEVERITY_RANK[options.minSeverity];
70
+ filtered = filtered.filter((i) => SEVERITY_RANK[i.severity] >= min);
71
+ }
72
+ if (options.json) {
73
+ process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
74
+ return;
75
+ }
76
+ console.log();
77
+ if (filtered.length === 0) {
78
+ console.log(ui.ok("No insights to report. Stores look healthy."));
79
+ console.log();
80
+ return;
81
+ }
82
+ const groups = { critical: [], warning: [], info: [] };
83
+ for (const i of filtered)
84
+ groups[i.severity].push(i);
85
+ for (const sev of ["critical", "warning", "info"]) {
86
+ const items = groups[sev];
87
+ if (items.length === 0)
88
+ continue;
89
+ console.log(severityHeading(sev, items.length));
90
+ console.log();
91
+ for (const ins of items) {
92
+ console.log(ui.indent(ui.emphasis(ins.title)));
93
+ console.log(ui.indent(ui.muted(ins.detail), 1));
94
+ if (ins.evidence && ins.evidence.length > 0) {
95
+ console.log(ui.indent(ui.muted("Evidence"), 1));
96
+ for (const e of ins.evidence.slice(0, 5)) {
97
+ console.log(ui.indent(ui.dot(e), 2));
98
+ }
99
+ if (ins.evidence.length > 5) {
100
+ console.log(ui.indent(ui.muted(`… and ${ins.evidence.length - 5} more`), 2));
101
+ }
102
+ }
103
+ if (ins.action) {
104
+ console.log(ui.indent(`${ui.glyph.arrow} ${ins.action}`, 1));
105
+ }
106
+ console.log();
107
+ }
108
+ }
109
+ console.log(ui.summary([
110
+ { label: "total", value: filtered.length, tone: "muted" },
111
+ { label: "critical", value: groups.critical.length, tone: groups.critical.length > 0 ? "danger" : "muted" },
112
+ { label: "warning", value: groups.warning.length, tone: groups.warning.length > 0 ? "warn" : "muted" },
113
+ { label: "info", value: groups.info.length, tone: "muted" }
114
+ ]));
115
+ console.log();
116
+ }
117
+ function severityHeading(sev, count) {
118
+ const label = sev.toUpperCase();
119
+ const styled = sev === "critical" ? ui.danger(ui.emphasis(label))
120
+ : sev === "warning" ? ui.warning(ui.emphasis(label))
121
+ : ui.info(ui.emphasis(label));
122
+ return `${styled} ${ui.muted("(" + count + ")")}`;
123
+ }
@@ -0,0 +1,106 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { resolveTarget } from "../utils/paths.js";
4
+ import { startMcpServer } from "../mcp/server.js";
5
+ import { ui } from "../ui/index.js";
6
+ /**
7
+ * `whale mcp serve` — start the MCP server on stdio.
8
+ *
9
+ * This is the long-running entry point invoked by AI clients (Claude
10
+ * Code, Cursor, Zed) via their MCP config. Never prints to stdout
11
+ * because stdout is the protocol channel.
12
+ */
13
+ export async function mcpServeCommand() {
14
+ await startMcpServer();
15
+ }
16
+ export async function mcpConfigCommand(options = {}) {
17
+ const target = resolveTarget(options.project);
18
+ const exists = await fs.pathExists(path.join(target, "whale.config.json"));
19
+ if (!exists) {
20
+ console.log(ui.warn(`No whale.config.json found at ${ui.path(target)}.`));
21
+ console.log(ui.muted(" Run `whale ignite` or `whale adopt` first, then come back."));
22
+ }
23
+ const absTarget = path.resolve(target);
24
+ const client = options.client ?? "claude-code";
25
+ console.log(ui.header("Whale Igniter", `mcp config • ${client}`));
26
+ console.log();
27
+ switch (client) {
28
+ case "claude-code":
29
+ printClaudeCodeConfig(absTarget);
30
+ break;
31
+ case "cursor":
32
+ printCursorConfig(absTarget);
33
+ break;
34
+ case "zed":
35
+ printZedConfig(absTarget);
36
+ break;
37
+ case "raw":
38
+ printRawConfig(absTarget);
39
+ break;
40
+ default:
41
+ console.log(ui.fail(`Unknown client: ${client}`));
42
+ console.log(ui.muted("Try: --client claude-code | cursor | zed | raw"));
43
+ process.exitCode = 1;
44
+ }
45
+ console.log();
46
+ console.log(ui.muted("Replace `whale` with the absolute path to the binary if it isn't on your PATH."));
47
+ console.log(ui.muted(`The WHALE_PROJECT env var pins the workspace to ${absTarget}.`));
48
+ }
49
+ function printClaudeCodeConfig(target) {
50
+ console.log("Add this to your Claude Desktop config file:");
51
+ console.log(ui.muted(" macOS: ~/Library/Application Support/Claude/claude_desktop_config.json"));
52
+ console.log(ui.muted(" Linux: ~/.config/Claude/claude_desktop_config.json"));
53
+ console.log(ui.muted(" Win: %APPDATA%\\Claude\\claude_desktop_config.json"));
54
+ console.log();
55
+ console.log("```json");
56
+ console.log(JSON.stringify({
57
+ mcpServers: {
58
+ "whale-igniter": {
59
+ command: "whale",
60
+ args: ["mcp", "serve"],
61
+ env: { WHALE_PROJECT: target }
62
+ }
63
+ }
64
+ }, null, 2));
65
+ console.log("```");
66
+ }
67
+ function printCursorConfig(target) {
68
+ console.log("Add this to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` globally):");
69
+ console.log();
70
+ console.log("```json");
71
+ console.log(JSON.stringify({
72
+ mcpServers: {
73
+ "whale-igniter": {
74
+ command: "whale",
75
+ args: ["mcp", "serve"],
76
+ env: { WHALE_PROJECT: target }
77
+ }
78
+ }
79
+ }, null, 2));
80
+ console.log("```");
81
+ }
82
+ function printZedConfig(target) {
83
+ console.log("Zed reads MCP servers from `~/.config/zed/settings.json`:");
84
+ console.log();
85
+ console.log("```json");
86
+ console.log(JSON.stringify({
87
+ context_servers: {
88
+ "whale-igniter": {
89
+ command: { path: "whale", args: ["mcp", "serve"], env: { WHALE_PROJECT: target } }
90
+ }
91
+ }
92
+ }, null, 2));
93
+ console.log("```");
94
+ }
95
+ function printRawConfig(target) {
96
+ console.log("Raw stdio invocation — for clients that follow the MCP spec generically:");
97
+ console.log();
98
+ console.log(ui.accent(" Command:"));
99
+ console.log(" whale mcp serve");
100
+ console.log();
101
+ console.log(ui.accent(" Environment:"));
102
+ console.log(` WHALE_PROJECT=${target}`);
103
+ console.log();
104
+ console.log(ui.accent(" Transport:"));
105
+ console.log(" stdio");
106
+ }
@@ -0,0 +1,36 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolveTarget } from "../utils/paths.js";
3
+ import { appendRefinement, inferScope } from "../utils/refinements.js";
4
+ import { generateWiki } from "../generators/wikiGenerator.js";
5
+ import { ui } from "../ui/index.js";
6
+ export async function refineCommand(note, options = {}) {
7
+ const target = resolveTarget();
8
+ const scope = inferScope(note);
9
+ const refinement = {
10
+ id: randomUUID(),
11
+ timestamp: new Date().toISOString(),
12
+ note,
13
+ scope
14
+ };
15
+ await appendRefinement(target, refinement);
16
+ console.log(ui.ok("Refinement recorded"));
17
+ if (scope) {
18
+ const parts = [];
19
+ if (scope.issueType)
20
+ parts.push(`type=${scope.issueType}`);
21
+ if (scope.selector)
22
+ parts.push(`selector=${scope.selector}`);
23
+ if (scope.file)
24
+ parts.push(`file=${scope.file}`);
25
+ console.log(` ${ui.accent("Active scope:")} ${parts.join(", ")}`);
26
+ console.log(` ${ui.muted("Matching issues will be suppressed in future validations.")}`);
27
+ }
28
+ else {
29
+ console.log(` ${ui.muted("Note logged. No active scope inferred — will not affect validation.")}`);
30
+ console.log(` ${ui.muted("Tip: mention an issue type (radius, spacing, hex, focus) to activate.")}`);
31
+ }
32
+ if (options.sync !== false) {
33
+ await generateWiki(target);
34
+ console.log(ui.muted(`${ui.glyph.check} AI context updated`));
35
+ }
36
+ }