whale-igniter 1.5.2 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/commands/adopt.js +4 -0
- package/dist/commands/friendly.js +11 -5
- package/dist/commands/ignite.js +6 -1
- package/dist/commands/init.js +4 -0
- package/dist/commands/insights.js +7 -0
- package/dist/commands/sync.js +5 -1
- package/dist/commands/validate.js +33 -2
- package/dist/commands/wiki.js +5 -1
- package/dist/generators/markdownGenerator.js +16 -8
- package/dist/generators/reportGenerator.js +2 -28
- package/dist/generators/wikiGenerator.js +11 -3
- package/dist/index.js +19 -7
- package/dist/templates/claude.js +10 -2
- package/dist/templates/project.js +6 -4
- package/dist/utils/targetGuard.js +77 -0
- package/dist/utils/validationSummary.js +103 -0
- package/dist/validators/cssValidator.js +8 -5
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Whale Igniter are documented here.
|
|
4
4
|
|
|
5
|
+
## 1.5.3 - 2026-07-06
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added shared target-directory guardrails for commands that write files or scan projects, blocking home/root-level targets and requiring `--force` when no `.git` directory or `package.json` is present.
|
|
10
|
+
- Added grouped validation summaries for CLI and generated docs, plus `--full` for exhaustive validation output and generated context health warnings when validation scope looks misconfigured.
|
|
11
|
+
|
|
5
12
|
## 1.5.2 - 2026-06-10
|
|
6
13
|
|
|
7
14
|
### Changed
|
package/dist/commands/adopt.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "fs-extra";
|
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import prompts from "prompts";
|
|
5
5
|
import { resolveTarget } from "../utils/paths.js";
|
|
6
|
+
import { guardTargetDirectory } from "../utils/targetGuard.js";
|
|
6
7
|
import { scanComponents } from "../scanner/componentScanner.js";
|
|
7
8
|
import { aggregateTailwind, detectTailwindConfig } from "../scanner/tailwindScanner.js";
|
|
8
9
|
import { inferFoundations } from "../scanner/foundationInferrer.js";
|
|
@@ -13,6 +14,9 @@ import { ui } from "../ui/index.js";
|
|
|
13
14
|
const SCANNER_VERSION = "1.1.0";
|
|
14
15
|
export async function adoptCommand(targetArg, options = {}) {
|
|
15
16
|
const target = resolveTarget(targetArg);
|
|
17
|
+
if (!(await guardTargetDirectory(target, { force: options.force, command: "adopt" }))) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
16
20
|
console.log();
|
|
17
21
|
console.log(ui.header("Whale Igniter", `adopt • ${path.relative(process.cwd(), target) || "."}`));
|
|
18
22
|
console.log();
|
|
@@ -5,20 +5,26 @@ import { syncCommand } from "./sync.js";
|
|
|
5
5
|
import { validateCommand } from "./validate.js";
|
|
6
6
|
import { wikiCommand } from "./wiki.js";
|
|
7
7
|
import { ui } from "../ui/index.js";
|
|
8
|
-
|
|
8
|
+
import { resolveTarget } from "../utils/paths.js";
|
|
9
|
+
import { guardTargetDirectory } from "../utils/targetGuard.js";
|
|
10
|
+
export async function rememberCommand(target, options = {}) {
|
|
9
11
|
console.log();
|
|
10
12
|
console.log(ui.header("Whale Igniter", "remember"));
|
|
11
13
|
console.log(ui.muted("Updating project memory for AI assistants."));
|
|
12
|
-
await syncCommand(target);
|
|
14
|
+
await syncCommand(target, options);
|
|
13
15
|
}
|
|
14
|
-
export async function checkCommand(target) {
|
|
16
|
+
export async function checkCommand(target, options = {}) {
|
|
15
17
|
console.log();
|
|
16
18
|
console.log(ui.header("Whale Igniter", "check"));
|
|
17
19
|
console.log(ui.muted("Checking project health, then looking for useful recommendations."));
|
|
18
20
|
console.log();
|
|
19
|
-
|
|
21
|
+
const resolvedTarget = resolveTarget(target);
|
|
22
|
+
if (!(await guardTargetDirectory(resolvedTarget, { force: options.force, command: "check" }))) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await validateCommand(target, { ...options, skipTargetGuard: true });
|
|
20
26
|
const validationExit = process.exitCode;
|
|
21
|
-
await insightsCommand(target, {});
|
|
27
|
+
await insightsCommand(target, { ...options, skipTargetGuard: true });
|
|
22
28
|
if (validationExit !== undefined) {
|
|
23
29
|
process.exitCode = validationExit;
|
|
24
30
|
}
|
package/dist/commands/ignite.js
CHANGED
|
@@ -7,6 +7,7 @@ import { validateCss } from "../validators/cssValidator.js";
|
|
|
7
7
|
import { generateWiki } from "../generators/wikiGenerator.js";
|
|
8
8
|
import { appendDecision } from "../utils/decisions.js";
|
|
9
9
|
import { getAiAvailability } from "../utils/aiAvailability.js";
|
|
10
|
+
import { guardTargetDirectory } from "../utils/targetGuard.js";
|
|
10
11
|
import { mapWizardAnswers, suggestUiCategories } from "../utils/wizardMapping.js";
|
|
11
12
|
import { ui } from "../ui/index.js";
|
|
12
13
|
import { PACKAGE_VERSION } from "../version.js";
|
|
@@ -49,7 +50,11 @@ export async function igniteCommand(projectName = "whale-project", options = {})
|
|
|
49
50
|
}
|
|
50
51
|
// ---- Step 1: scaffold workspace -------------------------------------------
|
|
51
52
|
const resolvedProjectName = config.projectName ?? projectName;
|
|
52
|
-
const
|
|
53
|
+
const plannedTarget = path.resolve(process.cwd(), resolvedProjectName);
|
|
54
|
+
if (!(await guardTargetDirectory(plannedTarget, { force: options.force, command: "ignite" }))) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const target = await initCommand(resolvedProjectName, { config, silent: true, skipTargetGuard: true });
|
|
53
58
|
const targetRel = path.relative(process.cwd(), target) || ".";
|
|
54
59
|
console.log(ui.ok(`Workspace created at ${ui.path(targetRel)}`));
|
|
55
60
|
// ---- Step 1b: record free-text project intent as first decision ------------
|
package/dist/commands/init.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "fs-extra";
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import { DEFAULT_CONFIG, saveConfig } from "../utils/config.js";
|
|
5
|
+
import { guardTargetDirectory } from "../utils/targetGuard.js";
|
|
5
6
|
import { ui } from "../ui/index.js";
|
|
6
7
|
/**
|
|
7
8
|
* Folders Whale always expects to exist. Created empty so subsequent
|
|
@@ -41,6 +42,9 @@ async function ensureIntelligenceFiles(target) {
|
|
|
41
42
|
}
|
|
42
43
|
export async function initCommand(projectName = "whale-project", options = {}) {
|
|
43
44
|
const target = path.resolve(process.cwd(), projectName);
|
|
45
|
+
if (!options.skipTargetGuard && !(await guardTargetDirectory(target, { force: options.force, command: "init" }))) {
|
|
46
|
+
return target;
|
|
47
|
+
}
|
|
44
48
|
const spinner = options.silent
|
|
45
49
|
? null
|
|
46
50
|
: ora(`Scaffolding ${projectName}`).start();
|
|
@@ -3,6 +3,7 @@ import fs from "fs-extra";
|
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import prompts from "prompts";
|
|
5
5
|
import { resolveTarget } from "../utils/paths.js";
|
|
6
|
+
import { guardTargetDirectory } from "../utils/targetGuard.js";
|
|
6
7
|
import { loadConfig } from "../utils/config.js";
|
|
7
8
|
import { loadRefinements, appendRefinement } from "../utils/refinements.js";
|
|
8
9
|
import { loadDecisions } from "../utils/decisions.js";
|
|
@@ -21,6 +22,9 @@ import { randomUUID } from "node:crypto";
|
|
|
21
22
|
const SEVERITY_RANK = { info: 0, warning: 1, critical: 2 };
|
|
22
23
|
export async function insightsCommand(targetArg, options = {}) {
|
|
23
24
|
const target = resolveTarget(targetArg);
|
|
25
|
+
if (!options.skipTargetGuard && !(await guardTargetDirectory(target, { force: options.force, command: "insights" }))) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
24
28
|
if (!(await fs.pathExists(path.join(target, "whale.config.json")))) {
|
|
25
29
|
console.log();
|
|
26
30
|
console.log(ui.warn("No whale.config.json found at this target. Run `whale ignite` or `whale adopt` first."));
|
|
@@ -99,6 +103,9 @@ export async function insightsDriftReviewCommand(category, opts) {
|
|
|
99
103
|
}
|
|
100
104
|
const cat = category;
|
|
101
105
|
const target = resolveTarget(opts.target);
|
|
106
|
+
if (!(await guardTargetDirectory(target, { force: opts.force, command: "insights drift" }))) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
102
109
|
console.log();
|
|
103
110
|
console.log(ui.header("Whale Igniter", `insights drift ${cat}`));
|
|
104
111
|
console.log();
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import { resolveTarget } from "../utils/paths.js";
|
|
4
|
+
import { guardTargetDirectory } from "../utils/targetGuard.js";
|
|
4
5
|
import { generateWiki } from "../generators/wikiGenerator.js";
|
|
5
6
|
import { loadConfig } from "../utils/config.js";
|
|
6
7
|
import { loadDecisions } from "../utils/decisions.js";
|
|
@@ -17,8 +18,11 @@ import { ui } from "../ui/index.js";
|
|
|
17
18
|
* - before handing the project to an AI agent
|
|
18
19
|
* - in CI to ensure CLAUDE.md stays in sync
|
|
19
20
|
*/
|
|
20
|
-
export async function syncCommand(targetArg) {
|
|
21
|
+
export async function syncCommand(targetArg, options = {}) {
|
|
21
22
|
const target = resolveTarget(targetArg);
|
|
23
|
+
if (!(await guardTargetDirectory(target, { force: options.force, command: "sync" }))) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
22
26
|
const spinner = ora("Reading intelligence stores").start();
|
|
23
27
|
const config = await loadConfig(target);
|
|
24
28
|
const decisions = await loadDecisions(target);
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import ora from "ora";
|
|
2
2
|
import { resolveTarget } from "../utils/paths.js";
|
|
3
|
+
import { guardTargetDirectory } from "../utils/targetGuard.js";
|
|
3
4
|
import { validateCss, hasErrors } from "../validators/cssValidator.js";
|
|
5
|
+
import { formatIssueLocation, summarizeValidationIssues } from "../utils/validationSummary.js";
|
|
4
6
|
import { ui } from "../ui/index.js";
|
|
5
|
-
export async function validateCommand(targetArg) {
|
|
7
|
+
export async function validateCommand(targetArg, options = {}) {
|
|
6
8
|
const target = resolveTarget(targetArg);
|
|
9
|
+
if (!options.skipTargetGuard && !(await guardTargetDirectory(target, { force: options.force, command: "validate" }))) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
7
12
|
const spinner = ora("Scanning workspace").start();
|
|
8
13
|
let issues;
|
|
9
14
|
try {
|
|
@@ -23,7 +28,33 @@ export async function validateCommand(targetArg) {
|
|
|
23
28
|
process.exitCode = 0;
|
|
24
29
|
return;
|
|
25
30
|
}
|
|
26
|
-
|
|
31
|
+
if (!options.full) {
|
|
32
|
+
const summary = summarizeValidationIssues(issues);
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(ui.section("Top issue groups"));
|
|
35
|
+
console.log();
|
|
36
|
+
for (const group of summary.shownGroups) {
|
|
37
|
+
const head = group.severity === "error"
|
|
38
|
+
? `${ui.glyph.cross} ${ui.danger(group.type)}`
|
|
39
|
+
: `${ui.glyph.warn} ${ui.warning(group.type)}`;
|
|
40
|
+
console.log(`${head} ${ui.muted(`- ${group.count} issue(s)`)}`);
|
|
41
|
+
console.log(` example: ${ui.path(formatIssueLocation(group.example))}`);
|
|
42
|
+
console.log(` ${group.message}`);
|
|
43
|
+
console.log(` ${ui.next(group.example.suggestion)}`);
|
|
44
|
+
if (group.example.selector) {
|
|
45
|
+
console.log(` ${ui.muted("selector: " + group.example.selector)}`);
|
|
46
|
+
}
|
|
47
|
+
console.log();
|
|
48
|
+
}
|
|
49
|
+
if (summary.hiddenIssueCount > 0) {
|
|
50
|
+
console.log(ui.muted(`+ ${summary.hiddenIssueCount} more across ${summary.hiddenFileCount} files - run \`whale check --full\` for everything`));
|
|
51
|
+
console.log();
|
|
52
|
+
}
|
|
53
|
+
process.exitCode = hasErrors(issues) ? 1 : 0;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Header with the counts as badges. Kept for --full, which preserves the
|
|
57
|
+
// older exhaustive output for users who explicitly ask for everything.
|
|
27
58
|
console.log();
|
|
28
59
|
console.log(ui.summary([
|
|
29
60
|
{ label: "issues", value: issues.length },
|
package/dist/commands/wiki.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import { resolveTarget } from "../utils/paths.js";
|
|
4
|
+
import { guardTargetDirectory } from "../utils/targetGuard.js";
|
|
4
5
|
import { generateWiki } from "../generators/wikiGenerator.js";
|
|
5
6
|
import { ui } from "../ui/index.js";
|
|
6
|
-
export async function wikiCommand(targetArg) {
|
|
7
|
+
export async function wikiCommand(targetArg, options = {}) {
|
|
7
8
|
const target = resolveTarget(targetArg);
|
|
9
|
+
if (!(await guardTargetDirectory(target, { force: options.force, command: "wiki" }))) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
const spinner = ora("Generating AI context").start();
|
|
9
13
|
const { rootFiles, wikiFiles } = await generateWiki(target);
|
|
10
14
|
spinner.succeed("AI context generated");
|
|
@@ -3,6 +3,7 @@ import fs from "fs-extra";
|
|
|
3
3
|
import { loadConfig } from "../utils/config.js";
|
|
4
4
|
import { loadRefinements } from "../utils/refinements.js";
|
|
5
5
|
import { validateCss } from "../validators/cssValidator.js";
|
|
6
|
+
import { renderValidationSummaryMarkdown, summarizeValidationIssues } from "../utils/validationSummary.js";
|
|
6
7
|
async function buildContext(target) {
|
|
7
8
|
const config = await loadConfig(target);
|
|
8
9
|
const refinements = await loadRefinements(target);
|
|
@@ -27,11 +28,20 @@ function renderForge(ctx) {
|
|
|
27
28
|
"- All interactive elements MUST have a `:focus-visible` style.",
|
|
28
29
|
"- Color values MUST come from semantic tokens (`var(--color-*)`).",
|
|
29
30
|
"",
|
|
30
|
-
|
|
31
|
+
"## Open Issues",
|
|
31
32
|
""
|
|
32
33
|
];
|
|
33
|
-
|
|
34
|
-
lines.push(
|
|
34
|
+
if (issues.length === 0) {
|
|
35
|
+
lines.push("No validation issues found.");
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const summary = summarizeValidationIssues(issues);
|
|
39
|
+
for (const group of summary.shownGroups) {
|
|
40
|
+
lines.push(`- **${group.type}** (${group.severity.toUpperCase()}) - ${group.count} issue(s); example \`${group.example.file}:${group.example.line}\` - ${group.message}`);
|
|
41
|
+
}
|
|
42
|
+
if (summary.hiddenIssueCount > 0) {
|
|
43
|
+
lines.push(`- + ${summary.hiddenIssueCount} more across ${summary.hiddenFileCount} files - run \`whale check --full\` for everything.`);
|
|
44
|
+
}
|
|
35
45
|
}
|
|
36
46
|
return lines.join("\n") + "\n";
|
|
37
47
|
}
|
|
@@ -59,13 +69,11 @@ function renderScribe(ctx) {
|
|
|
59
69
|
lines.push("");
|
|
60
70
|
lines.push(`## Current Validation Status`);
|
|
61
71
|
lines.push("");
|
|
62
|
-
lines.push(
|
|
72
|
+
lines.push(renderValidationSummaryMarkdown(issues));
|
|
63
73
|
return lines.join("\n") + "\n";
|
|
64
74
|
}
|
|
65
75
|
function renderAtlas(ctx) {
|
|
66
76
|
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
77
|
const lines = [
|
|
70
78
|
"# Atlas — Product Strategy",
|
|
71
79
|
"",
|
|
@@ -77,10 +85,10 @@ function renderAtlas(ctx) {
|
|
|
77
85
|
`- Accent: ${config.branding?.accent ?? "unspecified"}`,
|
|
78
86
|
"",
|
|
79
87
|
"## Operational Health",
|
|
80
|
-
`- Errors: ${errors}`,
|
|
81
|
-
`- Warnings: ${warnings}`,
|
|
82
88
|
`- Decisions logged: ${refinements.length}`,
|
|
83
89
|
"",
|
|
90
|
+
renderValidationSummaryMarkdown(issues),
|
|
91
|
+
"",
|
|
84
92
|
"## Decision Log",
|
|
85
93
|
""
|
|
86
94
|
];
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
3
|
import { validateCss } from "../validators/cssValidator.js";
|
|
4
|
+
import { renderValidationSummaryMarkdown } from "../utils/validationSummary.js";
|
|
4
5
|
export async function generateValidationReport(target) {
|
|
5
6
|
const issues = await validateCss(target);
|
|
6
7
|
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
8
|
const lines = [
|
|
10
9
|
"# Whale Validation Report",
|
|
11
10
|
"",
|
|
@@ -13,37 +12,12 @@ export async function generateValidationReport(target) {
|
|
|
13
12
|
"",
|
|
14
13
|
"## Summary",
|
|
15
14
|
"",
|
|
16
|
-
|
|
17
|
-
`- Errors: **${errors.length}**`,
|
|
18
|
-
`- Warnings: **${warnings.length}**`,
|
|
15
|
+
renderValidationSummaryMarkdown(issues),
|
|
19
16
|
""
|
|
20
17
|
];
|
|
21
18
|
if (issues.length === 0) {
|
|
22
19
|
lines.push("✓ Clean run.");
|
|
23
20
|
}
|
|
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
21
|
await fs.ensureDir(path.dirname(reportPath));
|
|
48
22
|
await fs.writeFile(reportPath, lines.join("\n"));
|
|
49
23
|
return reportPath;
|
|
@@ -4,7 +4,8 @@ import { loadConfig } from "../utils/config.js";
|
|
|
4
4
|
import { loadRefinements } from "../utils/refinements.js";
|
|
5
5
|
import { loadDecisions } from "../utils/decisions.js";
|
|
6
6
|
import { loadComponents } from "../utils/components.js";
|
|
7
|
-
import { validateCss } from "../validators/cssValidator.js";
|
|
7
|
+
import { findCssValidationFiles, validateCss } from "../validators/cssValidator.js";
|
|
8
|
+
import { evaluateValidationHealth, renderValidationHealthWarningMarkdown, renderValidationSummaryMarkdown } from "../utils/validationSummary.js";
|
|
8
9
|
import { renderFoundations } from "../templates/foundations.js";
|
|
9
10
|
import { renderConventions } from "../templates/conventions.js";
|
|
10
11
|
import { renderDecisions } from "../templates/decisions.js";
|
|
@@ -16,14 +17,19 @@ export async function generateWiki(target) {
|
|
|
16
17
|
const refinements = await loadRefinements(target);
|
|
17
18
|
const decisions = await loadDecisions(target);
|
|
18
19
|
const components = await loadComponents(target);
|
|
19
|
-
const issues = await
|
|
20
|
+
const [issues, scannedFiles] = await Promise.all([
|
|
21
|
+
validateCss(target),
|
|
22
|
+
findCssValidationFiles(target)
|
|
23
|
+
]);
|
|
20
24
|
const errors = issues.filter((i) => i.severity === "error").length;
|
|
21
25
|
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
26
|
+
const validationSummary = renderValidationSummaryMarkdown(issues);
|
|
27
|
+
const healthWarning = renderValidationHealthWarningMarkdown(evaluateValidationHealth(issues, scannedFiles.length, config));
|
|
22
28
|
const wikiDir = path.join(target, "llm-wiki");
|
|
23
29
|
await fs.ensureDir(wikiDir);
|
|
24
30
|
const themedFiles = {
|
|
25
31
|
"README.md": renderWikiReadme(config),
|
|
26
|
-
"PROJECT.md": renderProject(config,
|
|
32
|
+
"PROJECT.md": renderProject(config, validationSummary, healthWarning, refinements.length, decisions.length),
|
|
27
33
|
"FOUNDATIONS.md": renderFoundations(config, decisions),
|
|
28
34
|
"CONVENTIONS.md": renderConventions(config, refinements),
|
|
29
35
|
"DECISIONS.md": renderDecisions(decisions),
|
|
@@ -39,6 +45,8 @@ export async function generateWiki(target) {
|
|
|
39
45
|
const rootIndex = renderRootIndex(config, {
|
|
40
46
|
errors,
|
|
41
47
|
warnings,
|
|
48
|
+
validationSummary,
|
|
49
|
+
healthWarning,
|
|
42
50
|
refinementCount: refinements.length,
|
|
43
51
|
decisionCount: decisions.length,
|
|
44
52
|
componentCount: components.length
|
package/dist/index.js
CHANGED
|
@@ -39,25 +39,31 @@ program
|
|
|
39
39
|
.description("Bootstrap a Whale workspace with AI-readable context (opinionated by default).")
|
|
40
40
|
.option("-i, --interactive", "Run an interactive wizard to configure stack, packs, AI targets")
|
|
41
41
|
.option("-m, --minimal", "Minimal scaffold: folders + config + one validator pack")
|
|
42
|
+
.option("--force", "Proceed even when the target does not look like a project directory")
|
|
42
43
|
.action((projectName, opts) => igniteCommand(projectName, opts));
|
|
43
44
|
program
|
|
44
45
|
.command("init [projectName]")
|
|
45
46
|
.description("Create an empty Whale workspace (no packs, no AI context).")
|
|
46
|
-
.
|
|
47
|
-
|
|
47
|
+
.option("--force", "Proceed even when the target does not look like a project directory")
|
|
48
|
+
.action(async (projectName, opts) => {
|
|
49
|
+
await initCommand(projectName, opts);
|
|
48
50
|
});
|
|
49
51
|
program
|
|
50
52
|
.command("sync [target]")
|
|
51
53
|
.description("Regenerate CLAUDE.md and the LLM Wiki from project intelligence.")
|
|
52
|
-
.
|
|
54
|
+
.option("--force", "Proceed even when the target does not look like a project directory")
|
|
55
|
+
.action((target, opts) => syncCommand(target, opts));
|
|
53
56
|
program
|
|
54
57
|
.command("remember [target]")
|
|
55
58
|
.description("Update project memory for AI assistants (friendly alias for sync).")
|
|
56
|
-
.
|
|
59
|
+
.option("--force", "Proceed even when the target does not look like a project directory")
|
|
60
|
+
.action((target, opts) => rememberCommand(target, opts));
|
|
57
61
|
program
|
|
58
62
|
.command("check [target]")
|
|
59
63
|
.description("Check project health and recommendations (validate + insights).")
|
|
60
|
-
.
|
|
64
|
+
.option("--force", "Proceed even when the target does not look like a project directory")
|
|
65
|
+
.option("--full", "Show every validation issue instead of grouped summaries")
|
|
66
|
+
.action((target, opts) => checkCommand(target, opts));
|
|
61
67
|
program
|
|
62
68
|
.command("improve")
|
|
63
69
|
.description("Ask Selene for project-wide improvement suggestions.")
|
|
@@ -73,7 +79,9 @@ program
|
|
|
73
79
|
program
|
|
74
80
|
.command("validate [target]")
|
|
75
81
|
.description("Run operational validations (exits non-zero on errors).")
|
|
76
|
-
.
|
|
82
|
+
.option("--force", "Proceed even when the target does not look like a project directory")
|
|
83
|
+
.option("--full", "Show every validation issue instead of grouped summaries")
|
|
84
|
+
.action((target, opts) => validateCommand(target, opts));
|
|
77
85
|
program
|
|
78
86
|
.command("docs [target]")
|
|
79
87
|
.description("Generate validation report and run installed generator packs.")
|
|
@@ -81,7 +89,8 @@ program
|
|
|
81
89
|
program
|
|
82
90
|
.command("wiki [target]")
|
|
83
91
|
.description("Regenerate AI context (alias of `sync`).")
|
|
84
|
-
.
|
|
92
|
+
.option("--force", "Proceed even when the target does not look like a project directory")
|
|
93
|
+
.action((target, opts) => wikiCommand(target, opts));
|
|
85
94
|
program
|
|
86
95
|
.command("refine <note>")
|
|
87
96
|
.description("Record an operational refinement. Mention an issue type to suppress matching issues.")
|
|
@@ -157,12 +166,14 @@ const insightsCmd = program
|
|
|
157
166
|
.option("--category <name>", "Filter by category: refinements | decisions | components | foundations | tokens | coverage")
|
|
158
167
|
.option("--min-severity <level>", "Show only insights at this severity or higher (info | warning | critical)")
|
|
159
168
|
.option("--skip-scan", "Skip the source scan (faster, but disables orphan/drift insights)")
|
|
169
|
+
.option("--force", "Proceed even when the target does not look like a project directory")
|
|
160
170
|
.action((target, opts) => insightsCommand(target, opts));
|
|
161
171
|
insightsCmd
|
|
162
172
|
.command("drift <category>")
|
|
163
173
|
.description("Show or review drift in spacing | color | radii.")
|
|
164
174
|
.option("--review", "Enter interactive review loop: accept as refinement, flag for refactor, or skip")
|
|
165
175
|
.option("--target <path>", "Workspace path (default: cwd)")
|
|
176
|
+
.option("--force", "Proceed even when the target does not look like a project directory")
|
|
166
177
|
.action((category, opts) => insightsDriftReviewCommand(category, opts));
|
|
167
178
|
const create = program
|
|
168
179
|
.command("create")
|
|
@@ -240,6 +251,7 @@ const adopt = program
|
|
|
240
251
|
.description("Scan an existing project and propose components, foundations and decisions.")
|
|
241
252
|
.option("--pattern <glob>", "Glob pattern for source files (default: src/**/*.{tsx,jsx})")
|
|
242
253
|
.option("--dry-run", "Run the scanner but don't write proposals to disk")
|
|
254
|
+
.option("--force", "Proceed even when the target does not look like a project directory")
|
|
243
255
|
.action((target, opts) => adoptCommand(target, opts));
|
|
244
256
|
adopt
|
|
245
257
|
.command("review [target]")
|
package/dist/templates/claude.js
CHANGED
|
@@ -33,7 +33,7 @@ export function renderRootIndex(config, stats) {
|
|
|
33
33
|
const stack = config.stack ?? "css";
|
|
34
34
|
const packs = (config.packs ?? []).join(", ") || "(none)";
|
|
35
35
|
const refsSection = renderReferencesSection(config.uiReferences ?? []);
|
|
36
|
-
return
|
|
36
|
+
return `${stats.healthWarning ?? ""}# AI Agent Context — ${name}
|
|
37
37
|
|
|
38
38
|
> This file is auto-generated by [Whale Igniter](https://github.com/whale-igniter).
|
|
39
39
|
> Do not edit by hand. Run \`whale sync\` after changes to regenerate.
|
|
@@ -65,7 +65,15 @@ Whale stores structured context in two locations:
|
|
|
65
65
|
- \`llm-wiki/COMPONENTS.md\` — component catalog
|
|
66
66
|
- \`llm-wiki/WORKFLOWS.md\` — how the team uses Whale day to day
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
## Current status
|
|
69
|
+
|
|
70
|
+
- Decisions: ${stats.decisionCount}
|
|
71
|
+
- Active refinements: ${stats.refinementCount}
|
|
72
|
+
- Components: ${stats.componentCount}
|
|
73
|
+
|
|
74
|
+
### Validation
|
|
75
|
+
|
|
76
|
+
${stats.validationSummary}
|
|
69
77
|
|
|
70
78
|
${refsSection}## How to work here
|
|
71
79
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export function renderProject(config,
|
|
2
|
-
return
|
|
1
|
+
export function renderProject(config, validationSummary, healthWarning, refinementCount, decisionCount) {
|
|
2
|
+
return `${healthWarning ?? ""}# Project Context
|
|
3
3
|
|
|
4
4
|
- **Project name:** ${config.projectName ?? "(unset)"}
|
|
5
5
|
- **Project type:** ${config.projectType ?? "unspecified"}
|
|
@@ -10,11 +10,13 @@ export function renderProject(config, errors, warnings, refinementCount, decisio
|
|
|
10
10
|
|
|
11
11
|
## Current status
|
|
12
12
|
|
|
13
|
-
- Validation errors: ${errors}
|
|
14
|
-
- Validation warnings: ${warnings}
|
|
15
13
|
- Decisions logged: ${decisionCount}
|
|
16
14
|
- Active refinements: ${refinementCount}
|
|
17
15
|
|
|
16
|
+
## Validation
|
|
17
|
+
|
|
18
|
+
${validationSummary}
|
|
19
|
+
|
|
18
20
|
_Generated by Whale Igniter._
|
|
19
21
|
`;
|
|
20
22
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { ui } from "../ui/index.js";
|
|
5
|
+
function isSamePath(a, b) {
|
|
6
|
+
return path.resolve(a) === path.resolve(b);
|
|
7
|
+
}
|
|
8
|
+
function isAncestorOf(ancestor, child) {
|
|
9
|
+
const relative = path.relative(ancestor, child);
|
|
10
|
+
return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
11
|
+
}
|
|
12
|
+
function projectMarkerMessage(target, command) {
|
|
13
|
+
const commandHint = command ? `whale ${command}` : "this command";
|
|
14
|
+
return [
|
|
15
|
+
`This does not look like a project directory: ${target}`,
|
|
16
|
+
"I could not find a .git directory or package.json there.",
|
|
17
|
+
`Run ${ui.code(commandHint)} from inside your project, pass an explicit project path, or add ${ui.code("--force")} if this really is the target you want.`
|
|
18
|
+
].join("\n");
|
|
19
|
+
}
|
|
20
|
+
function protectedDirectoryMessage(target, command) {
|
|
21
|
+
const commandHint = command ? `whale ${command} path/to/project` : "pass an explicit project path";
|
|
22
|
+
return [
|
|
23
|
+
`I will not run in ${target}.`,
|
|
24
|
+
"That directory is your home folder, the filesystem root, or a parent of your home folder.",
|
|
25
|
+
`Please cd into a project directory first, or run ${ui.code(commandHint)}.`
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
28
|
+
export async function checkTargetDirectory(target, options = {}) {
|
|
29
|
+
const resolvedTarget = path.resolve(target);
|
|
30
|
+
const home = path.resolve(os.homedir());
|
|
31
|
+
const root = path.parse(resolvedTarget).root;
|
|
32
|
+
if (isSamePath(resolvedTarget, home) ||
|
|
33
|
+
isSamePath(resolvedTarget, root) ||
|
|
34
|
+
isAncestorOf(resolvedTarget, home)) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
target: resolvedTarget,
|
|
38
|
+
reason: "protected-directory",
|
|
39
|
+
message: protectedDirectoryMessage(resolvedTarget, options.command)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const hasGit = await fs.pathExists(path.join(resolvedTarget, ".git"));
|
|
43
|
+
const hasPackageJson = await fs.pathExists(path.join(resolvedTarget, "package.json"));
|
|
44
|
+
if (!hasGit && !hasPackageJson) {
|
|
45
|
+
const message = projectMarkerMessage(resolvedTarget, options.command);
|
|
46
|
+
if (!options.force) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
target: resolvedTarget,
|
|
50
|
+
reason: "missing-project-marker",
|
|
51
|
+
message
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
target: resolvedTarget,
|
|
57
|
+
warning: message
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, target: resolvedTarget };
|
|
61
|
+
}
|
|
62
|
+
export async function guardTargetDirectory(target, options = {}) {
|
|
63
|
+
const result = await checkTargetDirectory(target, options);
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(ui.fail(result.message));
|
|
67
|
+
console.log();
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (result.warning) {
|
|
72
|
+
console.log();
|
|
73
|
+
console.log(ui.warn(result.warning));
|
|
74
|
+
console.log();
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export const VALIDATION_SUMMARY_LIMIT = 10;
|
|
2
|
+
export const DEFAULT_HEALTH_ERROR_THRESHOLD = 500;
|
|
3
|
+
export const DEFAULT_HEALTH_FILE_THRESHOLD = 1000;
|
|
4
|
+
function groupKey(issue) {
|
|
5
|
+
return `${issue.severity}:${issue.type}`;
|
|
6
|
+
}
|
|
7
|
+
export function summarizeValidationIssues(issues, limit = VALIDATION_SUMMARY_LIMIT) {
|
|
8
|
+
const groupsByKey = new Map();
|
|
9
|
+
for (const issue of issues) {
|
|
10
|
+
const key = groupKey(issue);
|
|
11
|
+
const existing = groupsByKey.get(key);
|
|
12
|
+
if (existing) {
|
|
13
|
+
existing.count += 1;
|
|
14
|
+
existing.files.add(issue.file);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
groupsByKey.set(key, {
|
|
18
|
+
key,
|
|
19
|
+
type: issue.type,
|
|
20
|
+
severity: issue.severity,
|
|
21
|
+
message: issue.message,
|
|
22
|
+
count: 1,
|
|
23
|
+
files: new Set([issue.file]),
|
|
24
|
+
example: issue
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const groups = [...groupsByKey.values()].sort((a, b) => {
|
|
28
|
+
if (b.count !== a.count)
|
|
29
|
+
return b.count - a.count;
|
|
30
|
+
if (a.severity !== b.severity)
|
|
31
|
+
return a.severity === "error" ? -1 : 1;
|
|
32
|
+
return a.type.localeCompare(b.type);
|
|
33
|
+
});
|
|
34
|
+
const shownGroups = groups.slice(0, limit);
|
|
35
|
+
const hiddenGroups = groups.slice(limit);
|
|
36
|
+
const hiddenFiles = new Set();
|
|
37
|
+
let hiddenIssueCount = 0;
|
|
38
|
+
for (const group of hiddenGroups) {
|
|
39
|
+
hiddenIssueCount += group.count;
|
|
40
|
+
for (const file of group.files)
|
|
41
|
+
hiddenFiles.add(file);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
total: issues.length,
|
|
45
|
+
errors: issues.filter((issue) => issue.severity === "error").length,
|
|
46
|
+
warnings: issues.filter((issue) => issue.severity === "warning").length,
|
|
47
|
+
groups,
|
|
48
|
+
shownGroups,
|
|
49
|
+
hiddenIssueCount,
|
|
50
|
+
hiddenFileCount: hiddenFiles.size
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function evaluateValidationHealth(issues, scannedFileCount, config) {
|
|
54
|
+
const errorThreshold = config.validation?.healthErrorThreshold ?? DEFAULT_HEALTH_ERROR_THRESHOLD;
|
|
55
|
+
const fileThreshold = config.validation?.healthFileThreshold ?? DEFAULT_HEALTH_FILE_THRESHOLD;
|
|
56
|
+
const errorCount = issues.filter((issue) => issue.severity === "error").length;
|
|
57
|
+
const reasons = [];
|
|
58
|
+
if (errorCount > errorThreshold) {
|
|
59
|
+
reasons.push(`Validation errors exceed the configured threshold (${errorCount} > ${errorThreshold}).`);
|
|
60
|
+
}
|
|
61
|
+
if (scannedFileCount > fileThreshold) {
|
|
62
|
+
reasons.push(`The CSS scan touched ${scannedFileCount} files, which is above the configured threshold (${fileThreshold}).`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
ok: reasons.length === 0,
|
|
66
|
+
errorThreshold,
|
|
67
|
+
fileThreshold,
|
|
68
|
+
reasons
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function formatIssueLocation(issue) {
|
|
72
|
+
return `${issue.file}:${issue.line}`;
|
|
73
|
+
}
|
|
74
|
+
export function renderValidationSummaryMarkdown(issues, options = {}) {
|
|
75
|
+
const summary = summarizeValidationIssues(issues, options.limit);
|
|
76
|
+
const fullCommand = options.fullCommand ?? "whale check --full";
|
|
77
|
+
if (summary.total === 0)
|
|
78
|
+
return "No validation issues found.";
|
|
79
|
+
const lines = ["Validation issues grouped by rule/cause.", ""];
|
|
80
|
+
for (const group of summary.shownGroups) {
|
|
81
|
+
const tag = group.severity.toUpperCase();
|
|
82
|
+
lines.push(`- **${group.type}** (${tag}) - ${group.count} issue(s); example \`${formatIssueLocation(group.example)}\` - ${group.message}`);
|
|
83
|
+
}
|
|
84
|
+
if (summary.hiddenIssueCount > 0) {
|
|
85
|
+
lines.push(`- + ${summary.hiddenIssueCount} more across ${summary.hiddenFileCount} files - run \`${fullCommand}\` for everything.`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
export function renderValidationHealthWarningMarkdown(verdict) {
|
|
90
|
+
if (verdict.ok)
|
|
91
|
+
return "";
|
|
92
|
+
return [
|
|
93
|
+
"# Whale Context Health Warning",
|
|
94
|
+
"",
|
|
95
|
+
"> This generated context is likely misconfigured and should not be trusted yet.",
|
|
96
|
+
"> Whale probably scanned the wrong directory, too broad a scope, or a project with missing ignores.",
|
|
97
|
+
"",
|
|
98
|
+
...verdict.reasons.map((reason) => `- ${reason}`),
|
|
99
|
+
"",
|
|
100
|
+
"Run `whale check --full` from the intended project directory, or pass an explicit project path, then rerun `whale sync`.",
|
|
101
|
+
""
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
@@ -4,6 +4,13 @@ import path from "node:path";
|
|
|
4
4
|
import postcss from "postcss";
|
|
5
5
|
import { loadConfig } from "../utils/config.js";
|
|
6
6
|
import { loadRefinements, isSuppressed } from "../utils/refinements.js";
|
|
7
|
+
export async function findCssValidationFiles(target) {
|
|
8
|
+
return glob("**/*.css", {
|
|
9
|
+
cwd: target,
|
|
10
|
+
absolute: true,
|
|
11
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"]
|
|
12
|
+
});
|
|
13
|
+
}
|
|
7
14
|
// Properties that take spacing values (length-based, grid-relevant).
|
|
8
15
|
const SPACING_PROPS = new Set([
|
|
9
16
|
"margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
|
|
@@ -86,11 +93,7 @@ export async function validateCss(target, options = {}) {
|
|
|
86
93
|
config.foundations?.radius?.control ?? 2,
|
|
87
94
|
config.foundations?.radius?.container ?? 4
|
|
88
95
|
];
|
|
89
|
-
const files = await
|
|
90
|
-
cwd: target,
|
|
91
|
-
absolute: true,
|
|
92
|
-
ignore: ["node_modules/**", "dist/**", ".git/**"]
|
|
93
|
-
});
|
|
96
|
+
const files = await findCssValidationFiles(target);
|
|
94
97
|
const issues = [];
|
|
95
98
|
for (const file of files) {
|
|
96
99
|
const content = await fs.readFile(file, "utf8");
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "1.5.
|
|
1
|
+
export const PACKAGE_VERSION = "1.5.3";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whale-igniter",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "CLI-first operational intelligence. Bootstraps and adopts AI-readable project context so agents like Claude Code, Codex and Cursor understand your design system from the first commit. Includes an MCP server, a file watcher, deterministic insights, and an opt-in AI bridge (Selene).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|