@rune-kit/rune 2.10.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +65 -6
- package/commands/rune.md +168 -168
- package/compiler/__tests__/detect-invariants.test.js +136 -0
- package/compiler/__tests__/doctor-mesh.test.js +229 -0
- package/compiler/__tests__/hook-dispatch.test.js +91 -0
- package/compiler/__tests__/hooks-antigravity.test.js +118 -0
- package/compiler/__tests__/hooks-cursor.test.js +139 -0
- package/compiler/__tests__/hooks-install.test.js +305 -0
- package/compiler/__tests__/hooks-merge.test.js +204 -0
- package/compiler/__tests__/hooks-tiers.test.js +519 -0
- package/compiler/__tests__/hooks-windsurf.test.js +115 -0
- package/compiler/__tests__/inject-claude-md.test.js +152 -0
- package/compiler/__tests__/load-invariants.test.js +408 -0
- package/compiler/__tests__/onboard-invariants.test.js +240 -0
- package/compiler/adapters/hooks/antigravity.js +140 -0
- package/compiler/adapters/hooks/claude.js +166 -0
- package/compiler/adapters/hooks/cursor.js +191 -0
- package/compiler/adapters/hooks/index.js +82 -0
- package/compiler/adapters/hooks/tier-emitter.js +182 -0
- package/compiler/adapters/hooks/windsurf.js +202 -0
- package/compiler/bin/rune.js +196 -6
- package/compiler/commands/hook-dispatch.js +87 -0
- package/compiler/commands/hooks/install.js +120 -0
- package/compiler/commands/hooks/merge.js +211 -0
- package/compiler/commands/hooks/presets.js +116 -0
- package/compiler/commands/hooks/status.js +112 -0
- package/compiler/commands/hooks/tiers.js +221 -0
- package/compiler/commands/hooks/uninstall.js +94 -0
- package/compiler/doctor.js +236 -0
- package/contexts/dev.md +34 -34
- package/contexts/research.md +43 -43
- package/contexts/review.md +55 -55
- package/extensions/ai-ml/PACK.md +88 -88
- package/extensions/ai-ml/skills/ai-agents.md +172 -172
- package/extensions/ai-ml/skills/code-sandbox.md +187 -187
- package/extensions/ai-ml/skills/deep-research.md +146 -146
- package/extensions/ai-ml/skills/embedding-search.md +66 -66
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
- package/extensions/ai-ml/skills/llm-architect.md +125 -125
- package/extensions/ai-ml/skills/llm-integration.md +64 -64
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
- package/extensions/ai-ml/skills/rag-patterns.md +66 -66
- package/extensions/ai-ml/skills/web-extraction.md +114 -114
- package/extensions/analytics/PACK.md +92 -92
- package/extensions/analytics/skills/ab-testing.md +72 -72
- package/extensions/analytics/skills/dashboard-patterns.md +83 -83
- package/extensions/analytics/skills/data-validation.md +68 -68
- package/extensions/analytics/skills/funnel-analysis.md +81 -81
- package/extensions/analytics/skills/sql-patterns.md +57 -57
- package/extensions/analytics/skills/statistical-analysis.md +79 -79
- package/extensions/analytics/skills/tracking-setup.md +71 -71
- package/extensions/backend/PACK.md +104 -104
- package/extensions/backend/skills/api-patterns.md +84 -84
- package/extensions/backend/skills/async-pipeline.md +193 -193
- package/extensions/backend/skills/auth-patterns.md +97 -97
- package/extensions/backend/skills/background-jobs.md +133 -133
- package/extensions/backend/skills/caching-patterns.md +108 -108
- package/extensions/backend/skills/cli-generation.md +133 -133
- package/extensions/backend/skills/database-patterns.md +87 -87
- package/extensions/backend/skills/middleware-patterns.md +104 -104
- package/extensions/chrome-ext/PACK.md +93 -93
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
- package/extensions/chrome-ext/skills/cws-publish.md +104 -104
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
- package/extensions/chrome-ext/skills/ext-storage.md +133 -133
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
- package/extensions/content/PACK.md +96 -96
- package/extensions/content/skills/blog-patterns.md +88 -88
- package/extensions/content/skills/cms-integration.md +131 -131
- package/extensions/content/skills/content-scoring.md +107 -107
- package/extensions/content/skills/i18n.md +83 -83
- package/extensions/content/skills/mdx-authoring.md +137 -137
- package/extensions/content/skills/reference.md +1014 -1014
- package/extensions/content/skills/seo-patterns.md +67 -67
- package/extensions/content/skills/video-repurpose.md +153 -153
- package/extensions/devops/PACK.md +101 -101
- package/extensions/devops/skills/chaos-testing.md +67 -67
- package/extensions/devops/skills/ci-cd.md +75 -75
- package/extensions/devops/skills/docker.md +58 -58
- package/extensions/devops/skills/edge-serverless.md +163 -163
- package/extensions/devops/skills/infra-as-code.md +158 -158
- package/extensions/devops/skills/kubernetes.md +110 -110
- package/extensions/devops/skills/monitoring.md +57 -57
- package/extensions/devops/skills/server-setup.md +64 -64
- package/extensions/devops/skills/ssl-domain.md +42 -42
- package/extensions/ecommerce/PACK.md +116 -116
- package/extensions/ecommerce/skills/cart-system.md +79 -79
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
- package/extensions/ecommerce/skills/order-management.md +126 -126
- package/extensions/ecommerce/skills/payment-integration.md +472 -472
- package/extensions/ecommerce/skills/shopify-dev.md +69 -69
- package/extensions/ecommerce/skills/subscription-billing.md +93 -93
- package/extensions/ecommerce/skills/tax-compliance.md +117 -117
- package/extensions/gamedev/PACK.md +142 -142
- package/extensions/gamedev/skills/asset-pipeline.md +74 -74
- package/extensions/gamedev/skills/audio-system.md +129 -129
- package/extensions/gamedev/skills/camera-system.md +87 -87
- package/extensions/gamedev/skills/ecs.md +98 -98
- package/extensions/gamedev/skills/game-loops.md +72 -72
- package/extensions/gamedev/skills/input-system.md +199 -199
- package/extensions/gamedev/skills/multiplayer.md +180 -180
- package/extensions/gamedev/skills/particles.md +105 -105
- package/extensions/gamedev/skills/physics-engine.md +89 -89
- package/extensions/gamedev/skills/scene-management.md +146 -146
- package/extensions/gamedev/skills/threejs-patterns.md +90 -90
- package/extensions/gamedev/skills/webgl.md +71 -71
- package/extensions/mobile/PACK.md +106 -106
- package/extensions/mobile/skills/app-store-connect.md +152 -152
- package/extensions/mobile/skills/app-store-prep.md +66 -66
- package/extensions/mobile/skills/deep-linking.md +109 -109
- package/extensions/mobile/skills/flutter.md +60 -60
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
- package/extensions/mobile/skills/native-bridge.md +66 -66
- package/extensions/mobile/skills/ota-updates.md +97 -97
- package/extensions/mobile/skills/push-notifications.md +111 -111
- package/extensions/mobile/skills/react-native.md +82 -82
- package/extensions/saas/PACK.md +116 -116
- package/extensions/saas/skills/billing-integration.md +200 -200
- package/extensions/saas/skills/feature-flags.md +130 -130
- package/extensions/saas/skills/multi-tenant.md +103 -103
- package/extensions/saas/skills/onboarding-flow.md +139 -139
- package/extensions/saas/skills/subscription-flow.md +95 -95
- package/extensions/saas/skills/team-management.md +144 -144
- package/extensions/security/PACK.md +99 -99
- package/extensions/security/skills/api-security.md +140 -140
- package/extensions/security/skills/compliance.md +68 -68
- package/extensions/security/skills/owasp-audit.md +64 -64
- package/extensions/security/skills/pentest-patterns.md +77 -77
- package/extensions/security/skills/secret-mgmt.md +65 -65
- package/extensions/security/skills/supply-chain.md +65 -65
- package/extensions/trading/PACK.md +80 -80
- package/extensions/trading/skills/chart-components.md +55 -55
- package/extensions/trading/skills/experiment-loop.md +125 -125
- package/extensions/trading/skills/fintech-patterns.md +47 -47
- package/extensions/trading/skills/indicator-library.md +58 -58
- package/extensions/trading/skills/quant-analysis.md +111 -111
- package/extensions/trading/skills/realtime-data.md +58 -58
- package/extensions/trading/skills/trade-logic.md +104 -104
- package/extensions/ui/PACK.md +130 -130
- package/extensions/ui/skills/a11y-audit.md +91 -91
- package/extensions/ui/skills/animation-patterns.md +127 -127
- package/extensions/ui/skills/component-patterns.md +100 -100
- package/extensions/ui/skills/design-decision.md +108 -108
- package/extensions/ui/skills/design-system.md +68 -68
- package/extensions/ui/skills/landing-patterns.md +155 -155
- package/extensions/ui/skills/palette-picker.md +173 -173
- package/extensions/ui/skills/react-health.md +90 -90
- package/extensions/ui/skills/type-system.md +125 -125
- package/extensions/ui/skills/web-vitals.md +153 -153
- package/extensions/zalo/PACK.md +145 -145
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
- package/hooks/auto-format/index.cjs +48 -48
- package/hooks/hooks.json +111 -111
- package/hooks/post-session-reflect/index.cjs +189 -189
- package/hooks/pre-compact/index.cjs +95 -95
- package/hooks/run-hook.cmd +1 -1
- package/hooks/secrets-scan/index.cjs +100 -100
- package/hooks/session-start/index.cjs +71 -71
- package/hooks/typecheck/index.cjs +65 -65
- package/package.json +63 -63
- package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
- package/references/ui-pro-max-data/charts.csv +26 -26
- package/references/ui-pro-max-data/colors.csv +161 -161
- package/references/ui-pro-max-data/styles.csv +68 -68
- package/references/ui-pro-max-data/typography.csv +74 -74
- package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
- package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
- package/skills/adversary/SKILL.md +283 -283
- package/skills/asset-creator/SKILL.md +157 -157
- package/skills/audit/SKILL.md +147 -2
- package/skills/autopsy/SKILL.md +335 -335
- package/skills/ba/SKILL.md +85 -1
- package/skills/brainstorm/SKILL.md +380 -342
- package/skills/browser-pilot/SKILL.md +169 -168
- package/skills/constraint-check/SKILL.md +165 -165
- package/skills/context-engine/SKILL.md +408 -404
- package/skills/cook/SKILL.md +917 -863
- package/skills/db/SKILL.md +273 -273
- package/skills/debug/SKILL.md +465 -465
- package/skills/dependency-doctor/SKILL.md +265 -235
- package/skills/deploy/SKILL.md +274 -231
- package/skills/design/DESIGN-REFERENCE.md +365 -365
- package/skills/design/SKILL.md +590 -589
- package/skills/doc-processor/SKILL.md +254 -254
- package/skills/docs/SKILL.md +374 -374
- package/skills/docs-seeker/SKILL.md +178 -177
- package/skills/fix/SKILL.md +332 -330
- package/skills/git/SKILL.md +339 -339
- package/skills/hallucination-guard/SKILL.md +220 -219
- package/skills/incident/SKILL.md +254 -253
- package/skills/integrity-check/SKILL.md +169 -169
- package/skills/journal/SKILL.md +241 -240
- package/skills/launch/SKILL.md +344 -344
- package/skills/logic-guardian/SKILL.md +269 -251
- package/skills/marketing/SKILL.md +351 -289
- package/skills/mcp-builder/SKILL.md +425 -425
- package/skills/neural-memory/SKILL.md +359 -362
- package/skills/onboard/SKILL.md +432 -403
- package/skills/onboard/references/invariants-template.md +76 -0
- package/skills/onboard/scripts/detect-invariants.js +439 -0
- package/skills/onboard/scripts/inject-claude-md.js +150 -0
- package/skills/onboard/scripts/onboard-invariants.js +194 -0
- package/skills/perf/SKILL.md +347 -346
- package/skills/plan/SKILL.md +435 -428
- package/skills/preflight/SKILL.md +415 -415
- package/skills/problem-solver/SKILL.md +380 -284
- package/skills/rescue/SKILL.md +474 -474
- package/skills/research/SKILL.md +4 -0
- package/skills/retro/SKILL.md +3 -1
- package/skills/review/SKILL.md +614 -588
- package/skills/review-intake/SKILL.md +249 -249
- package/skills/safeguard/SKILL.md +200 -200
- package/skills/sast/SKILL.md +190 -190
- package/skills/scaffold/SKILL.md +328 -287
- package/skills/scope-guard/SKILL.md +183 -180
- package/skills/scout/SKILL.md +269 -263
- package/skills/sentinel/SKILL.md +384 -381
- package/skills/sentinel-env/SKILL.md +254 -254
- package/skills/sequential-thinking/SKILL.md +234 -234
- package/skills/session-bridge/SKILL.md +595 -543
- package/skills/session-bridge/scripts/load-invariants.js +397 -0
- package/skills/skill-forge/SKILL.md +581 -581
- package/skills/skill-router/SKILL.md +3 -0
- package/skills/slides/SKILL.md +19 -0
- package/skills/surgeon/SKILL.md +215 -215
- package/skills/team/SKILL.md +557 -537
- package/skills/test/SKILL.md +620 -614
- package/skills/trend-scout/SKILL.md +145 -145
- package/skills/verification/SKILL.md +334 -326
- package/skills/video-creator/SKILL.md +201 -201
- package/skills/watchdog/SKILL.md +168 -168
- package/skills/worktree/SKILL.md +140 -140
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Project Invariants
|
|
2
|
+
|
|
3
|
+
> Auto-seeded by `rune onboard`. Edit freely — your changes survive re-runs.
|
|
4
|
+
> Append-only: new auto-detections land under `## Auto-detected (new)` and never
|
|
5
|
+
> overwrite existing rules.
|
|
6
|
+
|
|
7
|
+
This file is the project's truth-source for rules that code changes must respect.
|
|
8
|
+
`logic-guardian` loads it as a pre-edit gate, and `session-bridge` surfaces the
|
|
9
|
+
most critical rules at session start so agents know the danger zones immediately.
|
|
10
|
+
|
|
11
|
+
## How to read an entry
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
### <short rule title>
|
|
15
|
+
- **WHAT**: one-sentence statement of the rule (what must hold)
|
|
16
|
+
- **WHERE**: glob patterns for files this rule applies to
|
|
17
|
+
- **WHY**: why this rule exists — past incident, cross-file coupling,
|
|
18
|
+
compliance, external contract, etc.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If an entry becomes stale, move it to `## Archived` with a one-line note.
|
|
22
|
+
Never delete — the archive is evidence of past decisions.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Danger Zones
|
|
27
|
+
|
|
28
|
+
Paths that disproportionately affect the rest of the codebase. Test coverage,
|
|
29
|
+
review, and change-review should be stricter here.
|
|
30
|
+
|
|
31
|
+
<!-- auto-generated entries appear below -->
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Critical Invariants
|
|
36
|
+
|
|
37
|
+
Rules that cross file boundaries — shared constants, contract values, protocol
|
|
38
|
+
fields. Breaking any one silently breaks downstream code.
|
|
39
|
+
|
|
40
|
+
<!-- auto-generated entries appear below -->
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## State Machine Rules
|
|
45
|
+
|
|
46
|
+
Legal transitions for state machines in the project. Illegal transitions are
|
|
47
|
+
often the root cause of hard-to-reproduce bugs.
|
|
48
|
+
|
|
49
|
+
<!-- auto-generated entries appear below -->
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Cross-File Consistency
|
|
54
|
+
|
|
55
|
+
Fields, enums, or schemas mirrored in multiple places. Updates must propagate
|
|
56
|
+
together or the project enters an inconsistent state.
|
|
57
|
+
|
|
58
|
+
<!-- auto-generated entries appear below -->
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Auto-detected (new)
|
|
63
|
+
|
|
64
|
+
New detections from the most recent `rune onboard` run. Review these, promote
|
|
65
|
+
the real ones into the sections above, and drop the noise.
|
|
66
|
+
|
|
67
|
+
<!-- placeholder — each run appends or replaces this block -->
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Archived
|
|
72
|
+
|
|
73
|
+
Rules that were once invariants but are no longer — kept here as history. Do
|
|
74
|
+
not delete; their absence later makes reasoning harder.
|
|
75
|
+
|
|
76
|
+
<!-- empty on first run -->
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* detect-invariants.js — Scan a project and emit candidate invariant rules
|
|
5
|
+
* for `.rune/INVARIANTS.md`.
|
|
6
|
+
*
|
|
7
|
+
* Detection buckets (ordered by specificity, not confidence):
|
|
8
|
+
* 1. Danger zones — directories with high churn signals (deep + many files)
|
|
9
|
+
* 2. Critical — shared constants exported and imported in ≥ 3 places
|
|
10
|
+
* 3. State machines — reducer shapes, switch(state), enum-driven transitions
|
|
11
|
+
* 4. Cross-file — duplicated literal tuples (suggests mirrored schemas)
|
|
12
|
+
*
|
|
13
|
+
* Heuristics favor recall over precision for danger zones (let the user prune),
|
|
14
|
+
* precision over recall for critical invariants (don't cry wolf).
|
|
15
|
+
*
|
|
16
|
+
* Usage as CLI:
|
|
17
|
+
* node detect-invariants.js --root <project-root> [--json]
|
|
18
|
+
*
|
|
19
|
+
* Usage as module:
|
|
20
|
+
* import { detectInvariants } from './detect-invariants.js';
|
|
21
|
+
* const rules = await detectInvariants({ root });
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { parseArgs } from 'node:util';
|
|
27
|
+
|
|
28
|
+
const IGNORED_DIRS = new Set([
|
|
29
|
+
'node_modules',
|
|
30
|
+
'.git',
|
|
31
|
+
'dist',
|
|
32
|
+
'build',
|
|
33
|
+
'coverage',
|
|
34
|
+
'.next',
|
|
35
|
+
'.nuxt',
|
|
36
|
+
'.svelte-kit',
|
|
37
|
+
'.turbo',
|
|
38
|
+
'.cache',
|
|
39
|
+
'__pycache__',
|
|
40
|
+
'.venv',
|
|
41
|
+
'venv',
|
|
42
|
+
'target',
|
|
43
|
+
'.rune',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs']);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Files whose presence marks a directory as a first-class artifact (AI skills,
|
|
50
|
+
* extension packs, plugin manifests). These directories become danger-zone
|
|
51
|
+
* candidates even if they contain no SOURCE_EXTS files — a SKILL.md edit can
|
|
52
|
+
* reshape runtime behavior for every consumer.
|
|
53
|
+
*/
|
|
54
|
+
const SIGNAL_FILES = new Set(['SKILL.md', 'PACK.md', 'plugin.json']);
|
|
55
|
+
|
|
56
|
+
const MAX_FILES = 2000;
|
|
57
|
+
const MAX_FILE_BYTES = 200_000;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Main entry — returns structured rule objects.
|
|
61
|
+
*
|
|
62
|
+
* @param {{root: string, maxFiles?: number}} opts
|
|
63
|
+
* @returns {Promise<{danger: Rule[], critical: Rule[], state: Rule[], cross: Rule[], stats: Object}>}
|
|
64
|
+
*/
|
|
65
|
+
export async function detectInvariants(opts) {
|
|
66
|
+
const { root } = opts;
|
|
67
|
+
const maxFiles = opts.maxFiles ?? MAX_FILES;
|
|
68
|
+
if (!root) throw new Error('detectInvariants: root is required');
|
|
69
|
+
|
|
70
|
+
const index = await buildIndex(root, maxFiles);
|
|
71
|
+
|
|
72
|
+
const danger = detectDangerZones(index);
|
|
73
|
+
const critical = await detectCriticalConstants(root, index);
|
|
74
|
+
const state = await detectStateMachines(root, index);
|
|
75
|
+
const cross = await detectCrossFileConsistency(root, index);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
danger,
|
|
79
|
+
critical,
|
|
80
|
+
state,
|
|
81
|
+
cross,
|
|
82
|
+
stats: {
|
|
83
|
+
filesScanned: index.files.length,
|
|
84
|
+
directoriesSeen: index.dirs.size,
|
|
85
|
+
truncated: index.truncated,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Walk the tree, collecting source files with size + path.
|
|
92
|
+
*/
|
|
93
|
+
async function buildIndex(root, maxFiles) {
|
|
94
|
+
const files = [];
|
|
95
|
+
const dirs = new Set();
|
|
96
|
+
const signalDirs = new Map(); // rel-dir -> count of signal files
|
|
97
|
+
let truncated = false;
|
|
98
|
+
|
|
99
|
+
async function walk(dir, depth) {
|
|
100
|
+
if (files.length >= maxFiles) {
|
|
101
|
+
truncated = true;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let entries;
|
|
105
|
+
try {
|
|
106
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
107
|
+
} catch {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
dirs.add(path.relative(root, dir) || '.');
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (files.length >= maxFiles) {
|
|
113
|
+
truncated = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
117
|
+
if (entry.name.startsWith('.') && depth === 0) continue;
|
|
118
|
+
const full = path.join(dir, entry.name);
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
await walk(full, depth + 1);
|
|
121
|
+
} else if (entry.isFile() && SIGNAL_FILES.has(entry.name)) {
|
|
122
|
+
const relDir = path.relative(root, dir).replace(/\\/g, '/') || '.';
|
|
123
|
+
signalDirs.set(relDir, (signalDirs.get(relDir) || 0) + 1);
|
|
124
|
+
}
|
|
125
|
+
if (entry.isFile() && SOURCE_EXTS.has(path.extname(entry.name))) {
|
|
126
|
+
try {
|
|
127
|
+
const s = await stat(full);
|
|
128
|
+
if (s.size <= MAX_FILE_BYTES) {
|
|
129
|
+
files.push({
|
|
130
|
+
abs: full,
|
|
131
|
+
rel: path.relative(root, full).replace(/\\/g, '/'),
|
|
132
|
+
size: s.size,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
/* skip unreadable */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await walk(root, 0);
|
|
143
|
+
return { files, dirs, signalDirs, truncated };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Danger-zone heuristic: directories with ≥ 5 source files AND depth ≥ 2
|
|
148
|
+
* OR directories whose relative path contains high-risk keywords.
|
|
149
|
+
* Sort by file count desc, take top 5.
|
|
150
|
+
*/
|
|
151
|
+
function detectDangerZones(index) {
|
|
152
|
+
const HIGH_RISK_KEYWORDS = [
|
|
153
|
+
'auth',
|
|
154
|
+
'payment',
|
|
155
|
+
'billing',
|
|
156
|
+
'router',
|
|
157
|
+
'session',
|
|
158
|
+
'security',
|
|
159
|
+
'crypto',
|
|
160
|
+
'migration',
|
|
161
|
+
'compiler',
|
|
162
|
+
'parser',
|
|
163
|
+
'state',
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const byDir = new Map();
|
|
167
|
+
for (const f of index.files) {
|
|
168
|
+
const dir = path.posix.dirname(f.rel);
|
|
169
|
+
byDir.set(dir, (byDir.get(dir) || 0) + 1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Fold signal-file directories (SKILL.md / PACK.md / plugin.json) into the
|
|
173
|
+
// scoring pool so AI-skill repos surface their orchestrators as danger zones
|
|
174
|
+
// even when the directory contains no SOURCE_EXTS files.
|
|
175
|
+
const signalDirs = index.signalDirs ?? new Map();
|
|
176
|
+
const scored = [];
|
|
177
|
+
const consideredDirs = new Set([...byDir.keys(), ...signalDirs.keys()]);
|
|
178
|
+
for (const dir of consideredDirs) {
|
|
179
|
+
if (dir === '.' || dir === '') continue;
|
|
180
|
+
const count = byDir.get(dir) ?? 0;
|
|
181
|
+
const signalCount = signalDirs.get(dir) ?? 0;
|
|
182
|
+
const depth = dir.split('/').length;
|
|
183
|
+
const keywordHit = HIGH_RISK_KEYWORDS.find((kw) => dir.toLowerCase().includes(kw));
|
|
184
|
+
let score = 0;
|
|
185
|
+
if (count >= 5 && depth >= 2) score += count;
|
|
186
|
+
if (keywordHit) score += 20;
|
|
187
|
+
if (signalCount > 0) score += 15 + signalCount;
|
|
188
|
+
if (score === 0) continue;
|
|
189
|
+
scored.push({ dir, count: count + signalCount, keyword: keywordHit, signal: signalCount > 0, score });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
scored.sort((a, b) => b.score - a.score);
|
|
193
|
+
const top = scored.slice(0, 5);
|
|
194
|
+
|
|
195
|
+
return top.map((s) => {
|
|
196
|
+
const artifactLabel = s.signal ? 'skill / pack artifacts' : 'source files';
|
|
197
|
+
const why = s.keyword
|
|
198
|
+
? `High-risk keyword "${s.keyword}" in path + ${s.count} ${artifactLabel}. Cross-cutting concerns amplify blast radius.`
|
|
199
|
+
: s.signal
|
|
200
|
+
? `${s.count} skill/pack artifact(s) under \`${s.dir}/\` — SKILL.md / PACK.md edits reshape runtime behavior for every consumer.`
|
|
201
|
+
: `${s.count} source files in a directory ≥ depth 2 — concentrated surface area.`;
|
|
202
|
+
return {
|
|
203
|
+
section: 'danger',
|
|
204
|
+
title: `${s.dir} — ${s.count} ${artifactLabel}`,
|
|
205
|
+
what: `Changes under \`${s.dir}/\` touch core logic; require tests before merge.`,
|
|
206
|
+
where: [`${s.dir}/**`],
|
|
207
|
+
why,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Shared constant detection:
|
|
214
|
+
* Find `export const FOO = "literal"` (or UPPER_CASE) in ≤ 50 files,
|
|
215
|
+
* then count imports of FOO across the index. If imported ≥ 3 times, flag it.
|
|
216
|
+
*/
|
|
217
|
+
async function detectCriticalConstants(_root, index) {
|
|
218
|
+
const CONST_REGEX = /export\s+const\s+([A-Z][A-Z0-9_]+)\s*=\s*(?:['"`]|\d|\{|\[)/g;
|
|
219
|
+
const candidates = new Map(); // name -> { file, value-snippet }
|
|
220
|
+
let examined = 0;
|
|
221
|
+
|
|
222
|
+
for (const f of index.files) {
|
|
223
|
+
if (examined >= 50) break;
|
|
224
|
+
if (!/\.(ts|tsx|js|jsx|mjs)$/.test(f.rel)) continue;
|
|
225
|
+
if (f.size > 80_000) continue;
|
|
226
|
+
examined += 1;
|
|
227
|
+
let content;
|
|
228
|
+
try {
|
|
229
|
+
content = await readFile(f.abs, 'utf-8');
|
|
230
|
+
} catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
let match;
|
|
234
|
+
CONST_REGEX.lastIndex = 0;
|
|
235
|
+
while ((match = CONST_REGEX.exec(content))) {
|
|
236
|
+
const name = match[1];
|
|
237
|
+
if (!candidates.has(name)) {
|
|
238
|
+
candidates.set(name, { file: f.rel, hits: 0 });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (candidates.size === 0) return [];
|
|
244
|
+
|
|
245
|
+
// Count usages for each candidate across all indexed source files
|
|
246
|
+
const importRegexes = new Map();
|
|
247
|
+
for (const name of candidates.keys()) {
|
|
248
|
+
importRegexes.set(name, new RegExp(`\\b${escapeRegex(name)}\\b`, 'g'));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let usageScanned = 0;
|
|
252
|
+
for (const f of index.files) {
|
|
253
|
+
if (usageScanned >= 200) break;
|
|
254
|
+
if (!/\.(ts|tsx|js|jsx|mjs)$/.test(f.rel)) continue;
|
|
255
|
+
if (f.size > 80_000) continue;
|
|
256
|
+
usageScanned += 1;
|
|
257
|
+
let content;
|
|
258
|
+
try {
|
|
259
|
+
content = await readFile(f.abs, 'utf-8');
|
|
260
|
+
} catch {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
for (const [name, re] of importRegexes) {
|
|
264
|
+
const matches = content.match(re);
|
|
265
|
+
if (matches && matches.length > 0) {
|
|
266
|
+
const c = candidates.get(name);
|
|
267
|
+
if (f.rel !== c.file) c.hits += matches.length;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const rules = [];
|
|
273
|
+
for (const [name, { file, hits }] of candidates) {
|
|
274
|
+
if (hits < 3) continue;
|
|
275
|
+
rules.push({
|
|
276
|
+
section: 'critical',
|
|
277
|
+
title: `Shared constant ${name}`,
|
|
278
|
+
what: `\`${name}\` (defined in \`${file}\`) is referenced ${hits} times across the codebase. Changing its value affects every consumer.`,
|
|
279
|
+
where: [file, `**/*.{ts,tsx,js,jsx,mjs}`],
|
|
280
|
+
why: `Widely imported constants are effectively part of the project's contract. Renames or value changes must propagate atomically.`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
rules.sort((a, b) => (b.what.match(/\d+/) || 0) - (a.what.match(/\d+/) || 0));
|
|
284
|
+
return rules.slice(0, 10);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* State-machine detection: files that contain both a state enum/union AND
|
|
289
|
+
* a switch or reducer acting on it. Surface the pair as a single rule.
|
|
290
|
+
*/
|
|
291
|
+
async function detectStateMachines(_root, index) {
|
|
292
|
+
const rules = [];
|
|
293
|
+
const STATE_HINT = /\b(state|status|phase|stage)\s*[:=]\s*(?:['"])([a-z_]+)(?:['"])/i;
|
|
294
|
+
const SWITCH_HINT = /switch\s*\(\s*(?:\w+\.)?(?:state|status|phase|stage)\s*\)/;
|
|
295
|
+
const REDUCER_HINT = /case\s+['"`][A-Z_][A-Z0-9_]+['"`]\s*:/;
|
|
296
|
+
|
|
297
|
+
let scanned = 0;
|
|
298
|
+
for (const f of index.files) {
|
|
299
|
+
if (scanned >= 150) break;
|
|
300
|
+
if (!/\.(ts|tsx|js|jsx|mjs|py)$/.test(f.rel)) continue;
|
|
301
|
+
if (f.size > 80_000) continue;
|
|
302
|
+
scanned += 1;
|
|
303
|
+
let content;
|
|
304
|
+
try {
|
|
305
|
+
content = await readFile(f.abs, 'utf-8');
|
|
306
|
+
} catch {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const hasSwitch = SWITCH_HINT.test(content);
|
|
311
|
+
const hasReducer = REDUCER_HINT.test(content);
|
|
312
|
+
const stateMatch = STATE_HINT.exec(content);
|
|
313
|
+
if (!(hasSwitch || hasReducer) || !stateMatch) continue;
|
|
314
|
+
|
|
315
|
+
rules.push({
|
|
316
|
+
section: 'state',
|
|
317
|
+
title: `State machine in ${f.rel}`,
|
|
318
|
+
what: `Transitions in \`${f.rel}\` must respect declared states. New states require updating every switch/case site.`,
|
|
319
|
+
where: [f.rel],
|
|
320
|
+
why: `Detected ${hasSwitch ? 'switch(state)' : 'reducer'} + state literal (e.g. "${stateMatch[2]}"). Missing-case bugs are a classic regression vector.`,
|
|
321
|
+
});
|
|
322
|
+
if (rules.length >= 5) break;
|
|
323
|
+
}
|
|
324
|
+
return rules;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Cross-file consistency heuristic: find string literal tuples that appear
|
|
329
|
+
* verbatim in ≥ 3 files (e.g. `["pending", "active", "closed"]`). Often these
|
|
330
|
+
* are mirrored schemas/enums that must stay in lock-step.
|
|
331
|
+
*/
|
|
332
|
+
async function detectCrossFileConsistency(_root, index) {
|
|
333
|
+
const TUPLE_REGEX = /\[\s*(['"][a-z_-]{2,}['"](?:\s*,\s*['"][a-z_-]{2,}['"]){2,})\s*\]/gi;
|
|
334
|
+
const byTuple = new Map();
|
|
335
|
+
|
|
336
|
+
let scanned = 0;
|
|
337
|
+
for (const f of index.files) {
|
|
338
|
+
if (scanned >= 150) break;
|
|
339
|
+
if (!/\.(ts|tsx|js|jsx|mjs|py)$/.test(f.rel)) continue;
|
|
340
|
+
if (f.size > 80_000) continue;
|
|
341
|
+
scanned += 1;
|
|
342
|
+
let content;
|
|
343
|
+
try {
|
|
344
|
+
content = await readFile(f.abs, 'utf-8');
|
|
345
|
+
} catch {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const seen = new Set();
|
|
349
|
+
TUPLE_REGEX.lastIndex = 0;
|
|
350
|
+
let match;
|
|
351
|
+
while ((match = TUPLE_REGEX.exec(content))) {
|
|
352
|
+
const normalized = match[1].replace(/\s+/g, '').toLowerCase();
|
|
353
|
+
if (seen.has(normalized)) continue;
|
|
354
|
+
seen.add(normalized);
|
|
355
|
+
if (!byTuple.has(normalized)) byTuple.set(normalized, new Set());
|
|
356
|
+
byTuple.get(normalized).add(f.rel);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const rules = [];
|
|
361
|
+
for (const [tuple, fileSet] of byTuple) {
|
|
362
|
+
if (fileSet.size < 3) continue;
|
|
363
|
+
const sample = tuple.split(',').slice(0, 4).join(', ');
|
|
364
|
+
rules.push({
|
|
365
|
+
section: 'cross',
|
|
366
|
+
title: `Mirrored literal tuple (${sample}${tuple.includes(',') ? '…' : ''})`,
|
|
367
|
+
what: `Identical literal list appears in ${fileSet.size} files; updates must land in every location.`,
|
|
368
|
+
where: Array.from(fileSet).sort().slice(0, 10),
|
|
369
|
+
why: `Duplicated tuples are typically mirrored schemas (DB columns, API fields, state enums). Drift between copies causes subtle bugs.`,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
rules.sort((a, b) => b.where.length - a.where.length);
|
|
373
|
+
return rules.slice(0, 5);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function escapeRegex(s) {
|
|
377
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Render a detection result into the markdown block that gets injected under
|
|
382
|
+
* `## Auto-detected (new)` in INVARIANTS.md.
|
|
383
|
+
*/
|
|
384
|
+
export function renderInvariants(result) {
|
|
385
|
+
const lines = [];
|
|
386
|
+
const sections = [
|
|
387
|
+
['Danger Zones', result.danger],
|
|
388
|
+
['Critical Invariants', result.critical],
|
|
389
|
+
['State Machine Rules', result.state],
|
|
390
|
+
['Cross-File Consistency', result.cross],
|
|
391
|
+
];
|
|
392
|
+
for (const [title, rules] of sections) {
|
|
393
|
+
if (!rules || rules.length === 0) continue;
|
|
394
|
+
lines.push(`### ${title}`);
|
|
395
|
+
lines.push('');
|
|
396
|
+
for (const rule of rules) {
|
|
397
|
+
lines.push(`#### ${rule.title}`);
|
|
398
|
+
lines.push(`- **WHAT**: ${rule.what}`);
|
|
399
|
+
lines.push(`- **WHERE**: ${rule.where.map((w) => `\`${w}\``).join(', ')}`);
|
|
400
|
+
lines.push(`- **WHY**: ${rule.why}`);
|
|
401
|
+
lines.push('');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (lines.length === 0) return '_No invariants detected in this run._\n';
|
|
405
|
+
return `${lines.join('\n')}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ─── CLI ───
|
|
409
|
+
|
|
410
|
+
async function main() {
|
|
411
|
+
const { values } = parseArgs({
|
|
412
|
+
options: {
|
|
413
|
+
root: { type: 'string', default: process.cwd() },
|
|
414
|
+
json: { type: 'boolean', default: false },
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
const result = await detectInvariants({ root: values.root });
|
|
418
|
+
if (values.json) {
|
|
419
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
process.stdout.write(renderInvariants(result));
|
|
423
|
+
process.stdout.write(`\n_Scanned ${result.stats.filesScanned} files._\n`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Only run main() when invoked directly as a CLI
|
|
427
|
+
const isMain = (() => {
|
|
428
|
+
try {
|
|
429
|
+
return import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(path.basename(process.argv[1]));
|
|
430
|
+
} catch {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
})();
|
|
434
|
+
if (isMain) {
|
|
435
|
+
main().catch((err) => {
|
|
436
|
+
process.stderr.write(`detect-invariants: ${err.message}\n`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* inject-claude-md.js — Idempotent editor for CLAUDE.md that maintains an
|
|
5
|
+
* auto-generated "Invariants (auto-detected)" pointer block.
|
|
6
|
+
*
|
|
7
|
+
* The block is delimited by HTML comment markers that are valid in Markdown:
|
|
8
|
+
*
|
|
9
|
+
* <!-- @rune-invariants-pointer:start -->
|
|
10
|
+
* ...
|
|
11
|
+
* <!-- @rune-invariants-pointer:end -->
|
|
12
|
+
*
|
|
13
|
+
* Content outside these markers is NEVER touched. Re-running replaces only the
|
|
14
|
+
* content between the markers. Users can delete the markers to opt out — a
|
|
15
|
+
* subsequent run will re-inject them once and respect a user-placed
|
|
16
|
+
* `<!-- @rune-invariants-pointer:skip -->` directive indefinitely.
|
|
17
|
+
*
|
|
18
|
+
* Usage as CLI:
|
|
19
|
+
* node inject-claude-md.js --claude-md <path> --invariants <path>
|
|
20
|
+
*
|
|
21
|
+
* Usage as module:
|
|
22
|
+
* import { injectInvariantsPointer, buildPointerBlock } from './inject-claude-md.js';
|
|
23
|
+
* const { action, content } = injectInvariantsPointer({ claudeMd, globs });
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync } from 'node:fs';
|
|
27
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
28
|
+
import { parseArgs } from 'node:util';
|
|
29
|
+
|
|
30
|
+
export const MARKER_START = '<!-- @rune-invariants-pointer:start -->';
|
|
31
|
+
export const MARKER_END = '<!-- @rune-invariants-pointer:end -->';
|
|
32
|
+
export const SKIP_DIRECTIVE = '<!-- @rune-invariants-pointer:skip -->';
|
|
33
|
+
|
|
34
|
+
const DEFAULT_INVARIANTS_PATH = '.rune/INVARIANTS.md';
|
|
35
|
+
const MAX_GLOBS_IN_POINTER = 8;
|
|
36
|
+
|
|
37
|
+
export function buildPointerBlock({ globs = [], invariantsPath = DEFAULT_INVARIANTS_PATH } = {}) {
|
|
38
|
+
const unique = Array.from(new Set(globs.filter((g) => typeof g === 'string' && g.trim())));
|
|
39
|
+
const shown = unique.slice(0, MAX_GLOBS_IN_POINTER);
|
|
40
|
+
const overflow = unique.length - shown.length;
|
|
41
|
+
|
|
42
|
+
const lines = [
|
|
43
|
+
MARKER_START,
|
|
44
|
+
'## Invariants (auto-detected)',
|
|
45
|
+
'',
|
|
46
|
+
`Before editing these paths, read [\`${invariantsPath}\`](${invariantsPath}) —`,
|
|
47
|
+
'it lists danger zones and cross-file invariants this project enforces.',
|
|
48
|
+
'',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
if (shown.length === 0) {
|
|
52
|
+
lines.push('_No danger zones detected yet. Re-run `rune onboard` after the codebase grows._');
|
|
53
|
+
} else {
|
|
54
|
+
for (const glob of shown) {
|
|
55
|
+
lines.push(`- \`${glob}\``);
|
|
56
|
+
}
|
|
57
|
+
if (overflow > 0) {
|
|
58
|
+
lines.push(`- _…and ${overflow} more — see \`${invariantsPath}\`_`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
lines.push('', MARKER_END);
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function injectInvariantsPointer({ claudeMd = '', globs = [], invariantsPath = DEFAULT_INVARIANTS_PATH } = {}) {
|
|
67
|
+
if (claudeMd.includes(SKIP_DIRECTIVE)) {
|
|
68
|
+
return { action: 'skipped', reason: 'skip-directive', content: claudeMd };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const block = buildPointerBlock({ globs, invariantsPath });
|
|
72
|
+
|
|
73
|
+
const startIdx = claudeMd.indexOf(MARKER_START);
|
|
74
|
+
const endIdx = claudeMd.indexOf(MARKER_END);
|
|
75
|
+
|
|
76
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
77
|
+
const before = claudeMd.slice(0, startIdx);
|
|
78
|
+
const after = claudeMd.slice(endIdx + MARKER_END.length);
|
|
79
|
+
const next = `${before}${block}${after}`;
|
|
80
|
+
if (next === claudeMd) {
|
|
81
|
+
return { action: 'unchanged', content: claudeMd };
|
|
82
|
+
}
|
|
83
|
+
return { action: 'updated', content: next };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (startIdx !== -1 || endIdx !== -1) {
|
|
87
|
+
return {
|
|
88
|
+
action: 'error',
|
|
89
|
+
reason: 'marker-mismatch',
|
|
90
|
+
content: claudeMd,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const separator = claudeMd.length === 0 || claudeMd.endsWith('\n\n') ? '' : claudeMd.endsWith('\n') ? '\n' : '\n\n';
|
|
95
|
+
const next = `${claudeMd}${separator}${block}\n`;
|
|
96
|
+
return { action: 'created', content: next };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function applyInvariantsPointer({
|
|
100
|
+
claudeMdPath,
|
|
101
|
+
globs = [],
|
|
102
|
+
invariantsPath = DEFAULT_INVARIANTS_PATH,
|
|
103
|
+
dryRun = false,
|
|
104
|
+
} = {}) {
|
|
105
|
+
if (!claudeMdPath) {
|
|
106
|
+
throw new Error('claudeMdPath is required');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const existing = existsSync(claudeMdPath) ? await readFile(claudeMdPath, 'utf8') : '';
|
|
110
|
+
const result = injectInvariantsPointer({ claudeMd: existing, globs, invariantsPath });
|
|
111
|
+
|
|
112
|
+
if (!dryRun && (result.action === 'created' || result.action === 'updated')) {
|
|
113
|
+
await writeFile(claudeMdPath, result.content, 'utf8');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { ...result, path: claudeMdPath, existed: existing.length > 0 };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function main() {
|
|
120
|
+
const { values } = parseArgs({
|
|
121
|
+
options: {
|
|
122
|
+
'claude-md': { type: 'string' },
|
|
123
|
+
invariants: { type: 'string', default: DEFAULT_INVARIANTS_PATH },
|
|
124
|
+
globs: { type: 'string', multiple: true, default: [] },
|
|
125
|
+
dry: { type: 'boolean', default: false },
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const claudeMdPath = values['claude-md'];
|
|
130
|
+
if (!claudeMdPath) {
|
|
131
|
+
console.error('Usage: inject-claude-md.js --claude-md <path> [--invariants <path>] [--globs glob ...] [--dry]');
|
|
132
|
+
process.exit(2);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = await applyInvariantsPointer({
|
|
136
|
+
claudeMdPath,
|
|
137
|
+
globs: values.globs,
|
|
138
|
+
invariantsPath: values.invariants,
|
|
139
|
+
dryRun: values.dry,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
console.log(JSON.stringify({ path: result.path, action: result.action, reason: result.reason ?? null }));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('inject-claude-md.js')) {
|
|
146
|
+
main().catch((err) => {
|
|
147
|
+
console.error(err.message);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
|
150
|
+
}
|