whale-igniter 1.5.2 → 1.5.4

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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to Whale Igniter are documented here.
4
4
 
5
+ ## 1.5.4 - 2026-07-08
6
+
7
+ ### Security
8
+
9
+ - Added automatic secret redaction before Selene prompts are copied, written, cached, or sent to Anthropic/OpenAI.
10
+ - Added `npm run security:scan` to detect likely committed secrets in source and compiled output.
11
+ - Added the security scan to `prepublishOnly`, after build and before tests, so npm releases fail before publishing if high-confidence secrets are found.
12
+ - Replaced the `gray-matter`/`js-yaml` dependency path with a local frontmatter parser, restoring a zero-vulnerability production audit.
13
+
14
+ ## 1.5.3 - 2026-07-06
15
+
16
+ ### Added
17
+
18
+ - 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.
19
+ - 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.
20
+
5
21
  ## 1.5.2 - 2026-06-10
6
22
 
7
23
  ### 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();
@@ -1,7 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import matter from "gray-matter";
3
2
  import { resolveTarget } from "../utils/paths.js";
4
3
  import { loadComponents } from "../utils/components.js";
4
+ import { parseFrontmatter } from "../utils/frontmatter.js";
5
5
  import { ui } from "../ui/index.js";
6
6
  const DECISIONS_DIR = "intelligence/decisions";
7
7
  const REFINEMENTS_DIR = "intelligence/refinements";
@@ -42,10 +42,10 @@ function mdLabelAtRef(target, ref, file, label) {
42
42
  }
43
43
  }
44
44
  const decisionLabel = (raw) => {
45
- const title = matter(raw).data?.title;
45
+ const title = parseFrontmatter(raw).data.title;
46
46
  return title ? String(title) : "";
47
47
  };
48
- const refinementLabel = (raw) => matter(raw).content.trim().split("\n")[0] ?? "";
48
+ const refinementLabel = (raw) => parseFrontmatter(raw).content.trim().split("\n")[0] ?? "";
49
49
  function gitDirChanges(target, since, dir, label) {
50
50
  const result = { added: [], removed: [], modified: [] };
51
51
  let raw = "";
@@ -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
- export async function rememberCommand(target) {
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
- await validateCommand(target);
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
  }
@@ -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 target = await initCommand(resolvedProjectName, { config, silent: true });
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 ------------
@@ -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();
@@ -14,6 +14,7 @@ import { copyToClipboard } from "../selene/clipboard.js";
14
14
  import { resolveProvider, describeProviderState } from "../selene/providers.js";
15
15
  import { callProvider, estimateTokens, estimateCostUsd } from "../selene/apiClient.js";
16
16
  import { readCache, writeCache, clearCache } from "../selene/cache.js";
17
+ import { redactSecrets } from "../selene/redaction.js";
17
18
  import { ui } from "../ui/index.js";
18
19
  const PENDING_DIR = ".whale/selene";
19
20
  // ---------------------------------------------------------------------------
@@ -83,6 +84,10 @@ async function emitPrompt(target, prompt, kind, options) {
83
84
  * prompt mode, etc. Centralising keeps the policy consistent.
84
85
  */
85
86
  async function runSelene(target, kind, prompt, options) {
87
+ const safePrompt = redactSecrets(prompt);
88
+ if (safePrompt !== prompt) {
89
+ console.log(ui.warning("Potential secrets were redacted before Selene output/API use."));
90
+ }
86
91
  const config = await loadConfig(target);
87
92
  const sel = config.selene ?? {};
88
93
  // Step 1: figure out the mode.
@@ -103,7 +108,7 @@ async function runSelene(target, kind, prompt, options) {
103
108
  process.exitCode = 1;
104
109
  return { rawResponse: null };
105
110
  }
106
- await emitPrompt(target, prompt, kind, options);
111
+ await emitPrompt(target, safePrompt, kind, options);
107
112
  return { rawResponse: null };
108
113
  }
109
114
  // ---- API mode --------------------------------------------------------------
@@ -111,7 +116,7 @@ async function runSelene(target, kind, prompt, options) {
111
116
  // Cache lookup, unless the user opted out.
112
117
  const cacheEnabled = !options.noCache && !sel.noCache;
113
118
  if (cacheEnabled) {
114
- const cached = await readCache(target, resolved.model, prompt);
119
+ const cached = await readCache(target, resolved.model, safePrompt);
115
120
  if (cached) {
116
121
  console.log(ui.muted(" cache hit — no API call needed"));
117
122
  // Still stash the response so `apply` from disk works if the user wants.
@@ -122,7 +127,7 @@ async function runSelene(target, kind, prompt, options) {
122
127
  // Cost confirmation.
123
128
  const confirm = options.confirmCost || sel.confirmCost;
124
129
  if (confirm) {
125
- const inputTokens = estimateTokens(prompt);
130
+ const inputTokens = estimateTokens(safePrompt);
126
131
  const expectedOutput = sel.maxTokens ?? 1500;
127
132
  const cost = estimateCostUsd(resolved.model, inputTokens, expectedOutput);
128
133
  const costStr = cost === null ? "unknown" : `≈ $${cost.toFixed(4)}`;
@@ -135,13 +140,13 @@ async function runSelene(target, kind, prompt, options) {
135
140
  });
136
141
  if (!ok) {
137
142
  console.log(ui.warning("Aborted — falling back to prompt mode."));
138
- await emitPrompt(target, prompt, kind, options);
143
+ await emitPrompt(target, safePrompt, kind, options);
139
144
  return { rawResponse: null };
140
145
  }
141
146
  }
142
147
  const spin = ora({ text: "Calling provider", stream: process.stderr }).start();
143
148
  const result = await callProvider(resolved, {
144
- prompt,
149
+ prompt: safePrompt,
145
150
  maxTokens: sel.maxTokens ?? 1500,
146
151
  temperature: sel.temperature ?? 0.2
147
152
  });
@@ -152,7 +157,7 @@ async function runSelene(target, kind, prompt, options) {
152
157
  }
153
158
  // Graceful fallback: still emit the prompt so the user isn't stuck.
154
159
  console.log(ui.warning("\nFalling back to prompt mode so you can run this manually:"));
155
- await emitPrompt(target, prompt, kind, options);
160
+ await emitPrompt(target, safePrompt, kind, options);
156
161
  return { rawResponse: null };
157
162
  }
158
163
  const usage = result.inputTokens !== null && result.outputTokens !== null
@@ -160,7 +165,7 @@ async function runSelene(target, kind, prompt, options) {
160
165
  : "";
161
166
  spin.succeed(`Response received${usage}`);
162
167
  if (cacheEnabled) {
163
- await writeCache(target, resolved.model, prompt, result.text);
168
+ await writeCache(target, resolved.model, safePrompt, result.text);
164
169
  }
165
170
  await stashResponse(target, kind, result.text);
166
171
  return { rawResponse: result.text };
@@ -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
- // Header with the counts as badges.
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 },
@@ -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
- `## Open Issues (${issues.length})`,
31
+ "## Open Issues",
31
32
  ""
32
33
  ];
33
- for (const issue of issues) {
34
- lines.push(`- \`${issue.file}:${issue.line}\` ${issue.type}: ${issue.message}`);
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(`${issues.length} issue(s) open. Run \`whale validate\` for details.`);
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
- `- Total issues: **${issues.length}**`,
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 validateCss(target);
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, errors, warnings, refinements.length, decisions.length),
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
- .action(async (projectName) => {
47
- await initCommand(projectName);
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
- .action(syncCommand);
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
- .action((target) => rememberCommand(target));
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
- .action((target) => checkCommand(target));
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
- .action(validateCommand);
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
- .action(wikiCommand);
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]")
@@ -0,0 +1,45 @@
1
+ const REDACTED = "[REDACTED_SECRET]";
2
+ const SECRET_PATTERNS = [
3
+ {
4
+ name: "private-key",
5
+ pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g
6
+ },
7
+ { name: "openai-key", pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
8
+ { name: "github-token", pattern: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
9
+ { name: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
10
+ { name: "aws-access-key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
11
+ { name: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{30,}\b/g },
12
+ { name: "anthropic-key", pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g }
13
+ ];
14
+ const ASSIGNMENT_PATTERN = /\b(api[_-]?key|token|secret|password|passwd|client[_-]?secret|private[_-]?key)\b(\s*[:=]\s*)(["']?)([^"'\s,;)}\]]{16,})(\3)/gi;
15
+ function redactAssignment(match, key, separator, quote, value) {
16
+ if (isLikelyPlaceholder(value))
17
+ return match;
18
+ return `${key}${separator}${quote}${REDACTED}${quote}`;
19
+ }
20
+ function isLikelyPlaceholder(value) {
21
+ const normalized = value.toLowerCase();
22
+ return [
23
+ "example",
24
+ "placeholder",
25
+ "changeme",
26
+ "dummy",
27
+ "fake",
28
+ "mock",
29
+ "sample",
30
+ "test",
31
+ "xxxx",
32
+ "..."
33
+ ].some((needle) => normalized.includes(needle));
34
+ }
35
+ export function redactSecrets(input) {
36
+ let output = input;
37
+ for (const { pattern } of SECRET_PATTERNS) {
38
+ output = output.replace(pattern, REDACTED);
39
+ }
40
+ output = output.replace(ASSIGNMENT_PATTERN, redactAssignment);
41
+ return output;
42
+ }
43
+ export function containsRedactedSecret(input) {
44
+ return redactSecrets(input) !== input;
45
+ }
@@ -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 `# AI Agent Context — ${name}
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
- **Current status:** ${stats.errors} error(s) · ${stats.warnings} warning(s) · ${stats.decisionCount} decision(s) · ${stats.refinementCount} refinement(s) · ${stats.componentCount} component(s)
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, errors, warnings, refinementCount, decisionCount) {
2
- return `# Project Context
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
  }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
- import matter from "gray-matter";
3
+ import { parseFrontmatter, stringifyFrontmatter } from "./frontmatter.js";
4
4
  const DIR = "intelligence/decisions";
5
5
  const LEGACY_STORE = "intelligence/decisions.json";
6
6
  function slugify(title) {
@@ -17,7 +17,7 @@ function section(body, heading) {
17
17
  return text && text.length > 0 ? text : undefined;
18
18
  }
19
19
  function parseFile(raw) {
20
- const { data, content } = matter(raw);
20
+ const { data, content } = parseFrontmatter(raw);
21
21
  const decision = section(content, "Decision");
22
22
  if (!data.id || !data.title || !decision)
23
23
  return null;
@@ -34,7 +34,7 @@ function parseFile(raw) {
34
34
  };
35
35
  }
36
36
  function serialize(d) {
37
- const fm = matter.stringify([
37
+ return stringifyFrontmatter([
38
38
  "## Context",
39
39
  d.context ?? "",
40
40
  "",
@@ -51,7 +51,6 @@ function serialize(d) {
51
51
  createdAt: d.timestamp,
52
52
  supersedes: d.supersedes ?? null
53
53
  });
54
- return fm;
55
54
  }
56
55
  async function loadLegacy(target) {
57
56
  const file = path.join(target, LEGACY_STORE);
@@ -0,0 +1,62 @@
1
+ function parseValue(raw) {
2
+ const value = raw.trim();
3
+ if (value === "null")
4
+ return null;
5
+ if (value === "true")
6
+ return true;
7
+ if (value === "false")
8
+ return false;
9
+ if (/^-?\d+(?:\.\d+)?$/.test(value))
10
+ return Number(value);
11
+ if ((value.startsWith('"') && value.endsWith('"')) ||
12
+ (value.startsWith("'") && value.endsWith("'"))) {
13
+ try {
14
+ return JSON.parse(value);
15
+ }
16
+ catch {
17
+ return value.slice(1, -1);
18
+ }
19
+ }
20
+ return value;
21
+ }
22
+ function stringifyValue(value) {
23
+ if (value === null || value === undefined)
24
+ return "null";
25
+ if (typeof value === "boolean" || typeof value === "number")
26
+ return String(value);
27
+ const text = String(value);
28
+ if (text === "" || /[:#\n\r]|^\s|\s$/.test(text))
29
+ return JSON.stringify(text);
30
+ return text;
31
+ }
32
+ export function parseFrontmatter(raw) {
33
+ if (!raw.startsWith("---\n") && !raw.startsWith("---\r\n")) {
34
+ return { data: {}, content: raw };
35
+ }
36
+ const lineEnding = raw.startsWith("---\r\n") ? "\r\n" : "\n";
37
+ const startLength = 3 + lineEnding.length;
38
+ const endMarker = `${lineEnding}---${lineEnding}`;
39
+ const end = raw.indexOf(endMarker, startLength);
40
+ if (end === -1)
41
+ return { data: {}, content: raw };
42
+ const block = raw.slice(startLength, end);
43
+ const content = raw.slice(end + endMarker.length);
44
+ const data = {};
45
+ for (const line of block.split(/\r?\n/)) {
46
+ if (!line.trim())
47
+ continue;
48
+ const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
49
+ if (!match)
50
+ continue;
51
+ data[match[1]] = parseValue(match[2]);
52
+ }
53
+ return { data, content };
54
+ }
55
+ export function stringifyFrontmatter(content, data) {
56
+ const lines = ["---"];
57
+ for (const [key, value] of Object.entries(data)) {
58
+ lines.push(`${key}: ${stringifyValue(value)}`);
59
+ }
60
+ lines.push("---", content);
61
+ return lines.join("\n");
62
+ }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
- import matter from "gray-matter";
3
+ import { parseFrontmatter, stringifyFrontmatter } from "./frontmatter.js";
4
4
  const DIR = "intelligence/refinements";
5
5
  const LEGACY_STORE = "intelligence/refinements.json";
6
6
  function slugify(text) {
@@ -11,7 +11,7 @@ function slugify(text) {
11
11
  .slice(0, 60);
12
12
  }
13
13
  function parseFile(raw) {
14
- const { data, content } = matter(raw);
14
+ const { data, content } = parseFrontmatter(raw);
15
15
  const note = content.trim();
16
16
  if (!data.id || !note)
17
17
  return null;
@@ -40,7 +40,7 @@ function serialize(r) {
40
40
  fm.selector = r.scope.selector;
41
41
  if (r.scope?.file)
42
42
  fm.file = r.scope.file;
43
- return matter.stringify(r.note, fm);
43
+ return stringifyFrontmatter(r.note, fm);
44
44
  }
45
45
  async function loadLegacy(target) {
46
46
  const file = path.join(target, LEGACY_STORE);
@@ -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 glob("**/*.css", {
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.2";
1
+ export const PACKAGE_VERSION = "1.5.4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whale-igniter",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
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",
@@ -23,8 +23,9 @@
23
23
  "build": "tsc",
24
24
  "postbuild": "node -e \"require('node:fs').chmodSync('dist/index.js', 0o755)\"",
25
25
  "start": "node dist/index.js",
26
+ "security:scan": "tsx scripts/security-scan.ts",
26
27
  "test": "tsx tests/run.ts && tsx tests/mcp-smoke.ts && tsx tests/extractors.ts && tsx tests/driftAnalyzers.ts",
27
- "prepublishOnly": "npm run build && npm test"
28
+ "prepublishOnly": "npm run build && npm run security:scan && npm test"
28
29
  },
29
30
  "engines": {
30
31
  "node": ">=20.0.0"
@@ -66,7 +67,6 @@
66
67
  "commander": "^12.1.0",
67
68
  "fs-extra": "^11.2.0",
68
69
  "glob": "^13.0.6",
69
- "gray-matter": "^4.0.3",
70
70
  "ora": "^8.1.0",
71
71
  "postcss": "^8.4.47",
72
72
  "prompts": "^2.4.2",