@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.
Files changed (240) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +65 -6
  3. package/commands/rune.md +168 -168
  4. package/compiler/__tests__/detect-invariants.test.js +136 -0
  5. package/compiler/__tests__/doctor-mesh.test.js +229 -0
  6. package/compiler/__tests__/hook-dispatch.test.js +91 -0
  7. package/compiler/__tests__/hooks-antigravity.test.js +118 -0
  8. package/compiler/__tests__/hooks-cursor.test.js +139 -0
  9. package/compiler/__tests__/hooks-install.test.js +305 -0
  10. package/compiler/__tests__/hooks-merge.test.js +204 -0
  11. package/compiler/__tests__/hooks-tiers.test.js +519 -0
  12. package/compiler/__tests__/hooks-windsurf.test.js +115 -0
  13. package/compiler/__tests__/inject-claude-md.test.js +152 -0
  14. package/compiler/__tests__/load-invariants.test.js +408 -0
  15. package/compiler/__tests__/onboard-invariants.test.js +240 -0
  16. package/compiler/adapters/hooks/antigravity.js +140 -0
  17. package/compiler/adapters/hooks/claude.js +166 -0
  18. package/compiler/adapters/hooks/cursor.js +191 -0
  19. package/compiler/adapters/hooks/index.js +82 -0
  20. package/compiler/adapters/hooks/tier-emitter.js +182 -0
  21. package/compiler/adapters/hooks/windsurf.js +202 -0
  22. package/compiler/bin/rune.js +196 -6
  23. package/compiler/commands/hook-dispatch.js +87 -0
  24. package/compiler/commands/hooks/install.js +120 -0
  25. package/compiler/commands/hooks/merge.js +211 -0
  26. package/compiler/commands/hooks/presets.js +116 -0
  27. package/compiler/commands/hooks/status.js +112 -0
  28. package/compiler/commands/hooks/tiers.js +221 -0
  29. package/compiler/commands/hooks/uninstall.js +94 -0
  30. package/compiler/doctor.js +236 -0
  31. package/contexts/dev.md +34 -34
  32. package/contexts/research.md +43 -43
  33. package/contexts/review.md +55 -55
  34. package/extensions/ai-ml/PACK.md +88 -88
  35. package/extensions/ai-ml/skills/ai-agents.md +172 -172
  36. package/extensions/ai-ml/skills/code-sandbox.md +187 -187
  37. package/extensions/ai-ml/skills/deep-research.md +146 -146
  38. package/extensions/ai-ml/skills/embedding-search.md +66 -66
  39. package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
  40. package/extensions/ai-ml/skills/llm-architect.md +125 -125
  41. package/extensions/ai-ml/skills/llm-integration.md +64 -64
  42. package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
  43. package/extensions/ai-ml/skills/rag-patterns.md +66 -66
  44. package/extensions/ai-ml/skills/web-extraction.md +114 -114
  45. package/extensions/analytics/PACK.md +92 -92
  46. package/extensions/analytics/skills/ab-testing.md +72 -72
  47. package/extensions/analytics/skills/dashboard-patterns.md +83 -83
  48. package/extensions/analytics/skills/data-validation.md +68 -68
  49. package/extensions/analytics/skills/funnel-analysis.md +81 -81
  50. package/extensions/analytics/skills/sql-patterns.md +57 -57
  51. package/extensions/analytics/skills/statistical-analysis.md +79 -79
  52. package/extensions/analytics/skills/tracking-setup.md +71 -71
  53. package/extensions/backend/PACK.md +104 -104
  54. package/extensions/backend/skills/api-patterns.md +84 -84
  55. package/extensions/backend/skills/async-pipeline.md +193 -193
  56. package/extensions/backend/skills/auth-patterns.md +97 -97
  57. package/extensions/backend/skills/background-jobs.md +133 -133
  58. package/extensions/backend/skills/caching-patterns.md +108 -108
  59. package/extensions/backend/skills/cli-generation.md +133 -133
  60. package/extensions/backend/skills/database-patterns.md +87 -87
  61. package/extensions/backend/skills/middleware-patterns.md +104 -104
  62. package/extensions/chrome-ext/PACK.md +93 -93
  63. package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
  64. package/extensions/chrome-ext/skills/cws-publish.md +104 -104
  65. package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
  66. package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
  67. package/extensions/chrome-ext/skills/ext-storage.md +133 -133
  68. package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
  69. package/extensions/content/PACK.md +96 -96
  70. package/extensions/content/skills/blog-patterns.md +88 -88
  71. package/extensions/content/skills/cms-integration.md +131 -131
  72. package/extensions/content/skills/content-scoring.md +107 -107
  73. package/extensions/content/skills/i18n.md +83 -83
  74. package/extensions/content/skills/mdx-authoring.md +137 -137
  75. package/extensions/content/skills/reference.md +1014 -1014
  76. package/extensions/content/skills/seo-patterns.md +67 -67
  77. package/extensions/content/skills/video-repurpose.md +153 -153
  78. package/extensions/devops/PACK.md +101 -101
  79. package/extensions/devops/skills/chaos-testing.md +67 -67
  80. package/extensions/devops/skills/ci-cd.md +75 -75
  81. package/extensions/devops/skills/docker.md +58 -58
  82. package/extensions/devops/skills/edge-serverless.md +163 -163
  83. package/extensions/devops/skills/infra-as-code.md +158 -158
  84. package/extensions/devops/skills/kubernetes.md +110 -110
  85. package/extensions/devops/skills/monitoring.md +57 -57
  86. package/extensions/devops/skills/server-setup.md +64 -64
  87. package/extensions/devops/skills/ssl-domain.md +42 -42
  88. package/extensions/ecommerce/PACK.md +116 -116
  89. package/extensions/ecommerce/skills/cart-system.md +79 -79
  90. package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
  91. package/extensions/ecommerce/skills/order-management.md +126 -126
  92. package/extensions/ecommerce/skills/payment-integration.md +472 -472
  93. package/extensions/ecommerce/skills/shopify-dev.md +69 -69
  94. package/extensions/ecommerce/skills/subscription-billing.md +93 -93
  95. package/extensions/ecommerce/skills/tax-compliance.md +117 -117
  96. package/extensions/gamedev/PACK.md +142 -142
  97. package/extensions/gamedev/skills/asset-pipeline.md +74 -74
  98. package/extensions/gamedev/skills/audio-system.md +129 -129
  99. package/extensions/gamedev/skills/camera-system.md +87 -87
  100. package/extensions/gamedev/skills/ecs.md +98 -98
  101. package/extensions/gamedev/skills/game-loops.md +72 -72
  102. package/extensions/gamedev/skills/input-system.md +199 -199
  103. package/extensions/gamedev/skills/multiplayer.md +180 -180
  104. package/extensions/gamedev/skills/particles.md +105 -105
  105. package/extensions/gamedev/skills/physics-engine.md +89 -89
  106. package/extensions/gamedev/skills/scene-management.md +146 -146
  107. package/extensions/gamedev/skills/threejs-patterns.md +90 -90
  108. package/extensions/gamedev/skills/webgl.md +71 -71
  109. package/extensions/mobile/PACK.md +106 -106
  110. package/extensions/mobile/skills/app-store-connect.md +152 -152
  111. package/extensions/mobile/skills/app-store-prep.md +66 -66
  112. package/extensions/mobile/skills/deep-linking.md +109 -109
  113. package/extensions/mobile/skills/flutter.md +60 -60
  114. package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
  115. package/extensions/mobile/skills/native-bridge.md +66 -66
  116. package/extensions/mobile/skills/ota-updates.md +97 -97
  117. package/extensions/mobile/skills/push-notifications.md +111 -111
  118. package/extensions/mobile/skills/react-native.md +82 -82
  119. package/extensions/saas/PACK.md +116 -116
  120. package/extensions/saas/skills/billing-integration.md +200 -200
  121. package/extensions/saas/skills/feature-flags.md +130 -130
  122. package/extensions/saas/skills/multi-tenant.md +103 -103
  123. package/extensions/saas/skills/onboarding-flow.md +139 -139
  124. package/extensions/saas/skills/subscription-flow.md +95 -95
  125. package/extensions/saas/skills/team-management.md +144 -144
  126. package/extensions/security/PACK.md +99 -99
  127. package/extensions/security/skills/api-security.md +140 -140
  128. package/extensions/security/skills/compliance.md +68 -68
  129. package/extensions/security/skills/owasp-audit.md +64 -64
  130. package/extensions/security/skills/pentest-patterns.md +77 -77
  131. package/extensions/security/skills/secret-mgmt.md +65 -65
  132. package/extensions/security/skills/supply-chain.md +65 -65
  133. package/extensions/trading/PACK.md +80 -80
  134. package/extensions/trading/skills/chart-components.md +55 -55
  135. package/extensions/trading/skills/experiment-loop.md +125 -125
  136. package/extensions/trading/skills/fintech-patterns.md +47 -47
  137. package/extensions/trading/skills/indicator-library.md +58 -58
  138. package/extensions/trading/skills/quant-analysis.md +111 -111
  139. package/extensions/trading/skills/realtime-data.md +58 -58
  140. package/extensions/trading/skills/trade-logic.md +104 -104
  141. package/extensions/ui/PACK.md +130 -130
  142. package/extensions/ui/skills/a11y-audit.md +91 -91
  143. package/extensions/ui/skills/animation-patterns.md +127 -127
  144. package/extensions/ui/skills/component-patterns.md +100 -100
  145. package/extensions/ui/skills/design-decision.md +108 -108
  146. package/extensions/ui/skills/design-system.md +68 -68
  147. package/extensions/ui/skills/landing-patterns.md +155 -155
  148. package/extensions/ui/skills/palette-picker.md +173 -173
  149. package/extensions/ui/skills/react-health.md +90 -90
  150. package/extensions/ui/skills/type-system.md +125 -125
  151. package/extensions/ui/skills/web-vitals.md +153 -153
  152. package/extensions/zalo/PACK.md +145 -145
  153. package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
  154. package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
  155. package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
  156. package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
  157. package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
  158. package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
  159. package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
  160. package/hooks/auto-format/index.cjs +48 -48
  161. package/hooks/hooks.json +111 -111
  162. package/hooks/post-session-reflect/index.cjs +189 -189
  163. package/hooks/pre-compact/index.cjs +95 -95
  164. package/hooks/run-hook.cmd +1 -1
  165. package/hooks/secrets-scan/index.cjs +100 -100
  166. package/hooks/session-start/index.cjs +71 -71
  167. package/hooks/typecheck/index.cjs +65 -65
  168. package/package.json +63 -63
  169. package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
  170. package/references/ui-pro-max-data/charts.csv +26 -26
  171. package/references/ui-pro-max-data/colors.csv +161 -161
  172. package/references/ui-pro-max-data/styles.csv +68 -68
  173. package/references/ui-pro-max-data/typography.csv +74 -74
  174. package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
  175. package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
  176. package/skills/adversary/SKILL.md +283 -283
  177. package/skills/asset-creator/SKILL.md +157 -157
  178. package/skills/audit/SKILL.md +147 -2
  179. package/skills/autopsy/SKILL.md +335 -335
  180. package/skills/ba/SKILL.md +85 -1
  181. package/skills/brainstorm/SKILL.md +380 -342
  182. package/skills/browser-pilot/SKILL.md +169 -168
  183. package/skills/constraint-check/SKILL.md +165 -165
  184. package/skills/context-engine/SKILL.md +408 -404
  185. package/skills/cook/SKILL.md +917 -863
  186. package/skills/db/SKILL.md +273 -273
  187. package/skills/debug/SKILL.md +465 -465
  188. package/skills/dependency-doctor/SKILL.md +265 -235
  189. package/skills/deploy/SKILL.md +274 -231
  190. package/skills/design/DESIGN-REFERENCE.md +365 -365
  191. package/skills/design/SKILL.md +590 -589
  192. package/skills/doc-processor/SKILL.md +254 -254
  193. package/skills/docs/SKILL.md +374 -374
  194. package/skills/docs-seeker/SKILL.md +178 -177
  195. package/skills/fix/SKILL.md +332 -330
  196. package/skills/git/SKILL.md +339 -339
  197. package/skills/hallucination-guard/SKILL.md +220 -219
  198. package/skills/incident/SKILL.md +254 -253
  199. package/skills/integrity-check/SKILL.md +169 -169
  200. package/skills/journal/SKILL.md +241 -240
  201. package/skills/launch/SKILL.md +344 -344
  202. package/skills/logic-guardian/SKILL.md +269 -251
  203. package/skills/marketing/SKILL.md +351 -289
  204. package/skills/mcp-builder/SKILL.md +425 -425
  205. package/skills/neural-memory/SKILL.md +359 -362
  206. package/skills/onboard/SKILL.md +432 -403
  207. package/skills/onboard/references/invariants-template.md +76 -0
  208. package/skills/onboard/scripts/detect-invariants.js +439 -0
  209. package/skills/onboard/scripts/inject-claude-md.js +150 -0
  210. package/skills/onboard/scripts/onboard-invariants.js +194 -0
  211. package/skills/perf/SKILL.md +347 -346
  212. package/skills/plan/SKILL.md +435 -428
  213. package/skills/preflight/SKILL.md +415 -415
  214. package/skills/problem-solver/SKILL.md +380 -284
  215. package/skills/rescue/SKILL.md +474 -474
  216. package/skills/research/SKILL.md +4 -0
  217. package/skills/retro/SKILL.md +3 -1
  218. package/skills/review/SKILL.md +614 -588
  219. package/skills/review-intake/SKILL.md +249 -249
  220. package/skills/safeguard/SKILL.md +200 -200
  221. package/skills/sast/SKILL.md +190 -190
  222. package/skills/scaffold/SKILL.md +328 -287
  223. package/skills/scope-guard/SKILL.md +183 -180
  224. package/skills/scout/SKILL.md +269 -263
  225. package/skills/sentinel/SKILL.md +384 -381
  226. package/skills/sentinel-env/SKILL.md +254 -254
  227. package/skills/sequential-thinking/SKILL.md +234 -234
  228. package/skills/session-bridge/SKILL.md +595 -543
  229. package/skills/session-bridge/scripts/load-invariants.js +397 -0
  230. package/skills/skill-forge/SKILL.md +581 -581
  231. package/skills/skill-router/SKILL.md +3 -0
  232. package/skills/slides/SKILL.md +19 -0
  233. package/skills/surgeon/SKILL.md +215 -215
  234. package/skills/team/SKILL.md +557 -537
  235. package/skills/test/SKILL.md +620 -614
  236. package/skills/trend-scout/SKILL.md +145 -145
  237. package/skills/verification/SKILL.md +334 -326
  238. package/skills/video-creator/SKILL.md +201 -201
  239. package/skills/watchdog/SKILL.md +168 -168
  240. package/skills/worktree/SKILL.md +140 -140
@@ -0,0 +1,305 @@
1
+ import assert from 'node:assert';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { afterEach, beforeEach, describe, test } from 'node:test';
7
+ import { installHooks } from '../commands/hooks/install.js';
8
+ import { SETTINGS_REL_PATH } from '../commands/hooks/presets.js';
9
+ import { hookStatus } from '../commands/hooks/status.js';
10
+ import { uninstallHooks } from '../commands/hooks/uninstall.js';
11
+
12
+ const RUNE_ROOT = path.resolve(import.meta.dirname, '..', '..');
13
+
14
+ let tmpRoot;
15
+
16
+ async function seedClaude(root) {
17
+ await mkdir(path.join(root, '.claude'), { recursive: true });
18
+ }
19
+
20
+ beforeEach(async () => {
21
+ tmpRoot = await mkdtemp(path.join(tmpdir(), 'rune-hooks-'));
22
+ });
23
+
24
+ afterEach(async () => {
25
+ if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
26
+ });
27
+
28
+ describe('installHooks (claude adapter)', () => {
29
+ test('fresh install writes .claude/settings.json with gentle preset', async () => {
30
+ await seedClaude(tmpRoot);
31
+ const result = await installHooks(tmpRoot, { preset: 'gentle' });
32
+ assert.strictEqual(result.preset, 'gentle');
33
+ assert.strictEqual(result.written, true);
34
+ assert.deepStrictEqual(result.platforms, ['claude']);
35
+
36
+ const settingsPath = path.join(tmpRoot, SETTINGS_REL_PATH);
37
+ assert.ok(existsSync(settingsPath));
38
+
39
+ const settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
40
+ assert.ok(settings.hooks?.PreToolUse);
41
+ assert.ok(settings.hooks?.Stop);
42
+ });
43
+
44
+ test('defaults to gentle when preset omitted', async () => {
45
+ await seedClaude(tmpRoot);
46
+ const result = await installHooks(tmpRoot, {});
47
+ assert.strictEqual(result.preset, 'gentle');
48
+ const settings = JSON.parse(await readFile(path.join(tmpRoot, SETTINGS_REL_PATH), 'utf-8'));
49
+ const stopCmd = settings.hooks.Stop[0].hooks[0].command;
50
+ assert.ok(stopCmd.includes('--gentle'));
51
+ });
52
+
53
+ test('re-install with same preset is idempotent', async () => {
54
+ await seedClaude(tmpRoot);
55
+ await installHooks(tmpRoot, { preset: 'gentle' });
56
+ const first = await readFile(path.join(tmpRoot, SETTINGS_REL_PATH), 'utf-8');
57
+ await installHooks(tmpRoot, { preset: 'gentle' });
58
+ const second = await readFile(path.join(tmpRoot, SETTINGS_REL_PATH), 'utf-8');
59
+ assert.strictEqual(first, second);
60
+ });
61
+
62
+ test('upgrade gentle → strict replaces Rune entries', async () => {
63
+ await seedClaude(tmpRoot);
64
+ await installHooks(tmpRoot, { preset: 'gentle' });
65
+ await installHooks(tmpRoot, { preset: 'strict' });
66
+ const settings = JSON.parse(await readFile(path.join(tmpRoot, SETTINGS_REL_PATH), 'utf-8'));
67
+ const preflightCmd = settings.hooks.PreToolUse[0].hooks[0].command;
68
+ assert.ok(!preflightCmd.includes('--gentle'), 'strict should not have --gentle');
69
+ });
70
+
71
+ test('preserves user hooks when installing', async () => {
72
+ const settingsDir = path.join(tmpRoot, '.claude');
73
+ await mkdir(settingsDir, { recursive: true });
74
+ await writeFile(
75
+ path.join(settingsDir, 'settings.json'),
76
+ JSON.stringify(
77
+ {
78
+ env: { MY_VAR: 'keep-me' },
79
+ hooks: {
80
+ PreToolUse: [{ matcher: 'Read', hooks: [{ type: 'command', command: 'user-read-hook.sh' }] }],
81
+ },
82
+ },
83
+ null,
84
+ 2,
85
+ ),
86
+ );
87
+
88
+ await installHooks(tmpRoot, { preset: 'gentle' });
89
+ const settings = JSON.parse(await readFile(path.join(settingsDir, 'settings.json'), 'utf-8'));
90
+ assert.deepStrictEqual(settings.env, { MY_VAR: 'keep-me' }, 'env preserved');
91
+ const readGroup = settings.hooks.PreToolUse.find((g) => g.matcher === 'Read');
92
+ assert.ok(readGroup, 'user Read matcher preserved');
93
+ assert.strictEqual(readGroup.hooks[0].command, 'user-read-hook.sh');
94
+ });
95
+
96
+ test('rejects invalid preset', async () => {
97
+ await seedClaude(tmpRoot);
98
+ await assert.rejects(installHooks(tmpRoot, { preset: 'loose' }), /Invalid preset/);
99
+ });
100
+
101
+ test('dry-run does not write', async () => {
102
+ await seedClaude(tmpRoot);
103
+ const result = await installHooks(tmpRoot, { preset: 'gentle', dry: true });
104
+ assert.strictEqual(result.written, false);
105
+ assert.ok(!existsSync(path.join(tmpRoot, SETTINGS_REL_PATH)));
106
+ });
107
+
108
+ test('preset=off uninstalls rune hooks only', async () => {
109
+ await seedClaude(tmpRoot);
110
+ await installHooks(tmpRoot, { preset: 'gentle' });
111
+ const result = await installHooks(tmpRoot, { preset: 'off' });
112
+ assert.strictEqual(result.preset, 'off');
113
+ const settings = JSON.parse(await readFile(path.join(tmpRoot, SETTINGS_REL_PATH), 'utf-8'));
114
+ assert.strictEqual(settings.hooks, undefined);
115
+ });
116
+
117
+ test('T1: preset=off with no settings.json does not create the file', async () => {
118
+ await seedClaude(tmpRoot);
119
+ const settingsPath = path.join(tmpRoot, SETTINGS_REL_PATH);
120
+ assert.ok(!existsSync(settingsPath), 'precondition: file must not exist');
121
+ const result = await installHooks(tmpRoot, { preset: 'off' });
122
+ // H1 fix: written reflects actual byte writes, not just !dry
123
+ assert.strictEqual(result.written, false);
124
+ assert.ok(result.notes.some((n) => n.includes('no changes')));
125
+ const claudeResult = result.results.find((r) => r.platform === 'claude');
126
+ assert.strictEqual(claudeResult.files.length, 0, 'no files should be written for off+missing');
127
+ assert.strictEqual(claudeResult.writes, 0);
128
+ assert.ok(!existsSync(settingsPath), 'settings.json must NOT have been created');
129
+ });
130
+
131
+ test('T2: malformed settings.json causes actionable error, file unchanged', async () => {
132
+ const settingsDir = path.join(tmpRoot, '.claude');
133
+ await mkdir(settingsDir, { recursive: true });
134
+ const settingsPath = path.join(settingsDir, 'settings.json');
135
+ const malformed = '{ not valid json }';
136
+ await writeFile(settingsPath, malformed, 'utf-8');
137
+ await assert.rejects(installHooks(tmpRoot, { preset: 'gentle' }), (err) => {
138
+ assert.ok(err.message.includes('settings.json'), 'error must mention settings.json');
139
+ return true;
140
+ });
141
+ assert.strictEqual(await readFile(settingsPath, 'utf-8'), malformed);
142
+ });
143
+
144
+ test('auto-detects no platforms → returns empty results with note', async () => {
145
+ const result = await installHooks(tmpRoot, { preset: 'gentle' });
146
+ assert.deepStrictEqual(result.platforms, []);
147
+ assert.strictEqual(result.written, false);
148
+ assert.ok(result.notes.some((n) => n.includes('No target platform')));
149
+ });
150
+
151
+ test('--platform claude forces install even without .claude/', async () => {
152
+ const result = await installHooks(tmpRoot, { preset: 'gentle', platform: 'claude' });
153
+ assert.deepStrictEqual(result.platforms, ['claude']);
154
+ assert.strictEqual(result.written, true);
155
+ assert.ok(existsSync(path.join(tmpRoot, SETTINGS_REL_PATH)));
156
+ });
157
+
158
+ test('--platform all targets only detected platforms (no silent dir creation)', async () => {
159
+ // Bare dir → no platforms detected → all expands to nothing
160
+ const bare = await installHooks(tmpRoot, { preset: 'gentle', platform: 'all' });
161
+ assert.deepStrictEqual(bare.platforms, []);
162
+ assert.strictEqual(bare.written, false);
163
+
164
+ // Seed .claude/ + .cursor/ only → all picks up those two, NOT windsurf/antigravity
165
+ await seedClaude(tmpRoot);
166
+ await mkdir(path.join(tmpRoot, '.cursor'), { recursive: true });
167
+ const result = await installHooks(tmpRoot, { preset: 'gentle', platform: 'all' });
168
+ assert.ok(result.platforms.includes('claude'));
169
+ assert.ok(result.platforms.includes('cursor'));
170
+ assert.ok(!result.platforms.includes('windsurf'), 'must not force-create windsurf dir');
171
+ assert.ok(!result.platforms.includes('antigravity'), 'must not force-create antigravity dir');
172
+ assert.ok(!existsSync(path.join(tmpRoot, '.windsurf')));
173
+ assert.ok(!existsSync(path.join(tmpRoot, '.antigravity')));
174
+ });
175
+
176
+ test('--platform <name> still force-creates the platform dir (explicit opt-in)', async () => {
177
+ const result = await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
178
+ assert.deepStrictEqual(result.platforms, ['cursor']);
179
+ assert.ok(existsSync(path.join(tmpRoot, '.cursor', 'rules')));
180
+ });
181
+
182
+ test('unknown --platform rejected', async () => {
183
+ await assert.rejects(installHooks(tmpRoot, { preset: 'gentle', platform: 'bogus' }), /Unknown platform/);
184
+ });
185
+ });
186
+
187
+ describe('uninstallHooks (claude adapter)', () => {
188
+ test('no-op when no platforms detected', async () => {
189
+ const result = await uninstallHooks(tmpRoot);
190
+ assert.deepStrictEqual(result.platforms, []);
191
+ assert.strictEqual(result.written, false);
192
+ });
193
+
194
+ test('no-op when .claude/ exists but no settings.json', async () => {
195
+ await seedClaude(tmpRoot);
196
+ const result = await uninstallHooks(tmpRoot);
197
+ assert.deepStrictEqual(result.platforms, ['claude']);
198
+ // H1 fix: written=false when no actual writes happened
199
+ assert.strictEqual(result.written, false);
200
+ const claudeResult = result.results.find((r) => r.platform === 'claude');
201
+ assert.strictEqual(claudeResult.files.length, 0);
202
+ assert.strictEqual(claudeResult.writes, 0);
203
+ });
204
+
205
+ test('removes Rune hooks, preserves user hooks', async () => {
206
+ const settingsDir = path.join(tmpRoot, '.claude');
207
+ await mkdir(settingsDir, { recursive: true });
208
+ await writeFile(
209
+ path.join(settingsDir, 'settings.json'),
210
+ JSON.stringify(
211
+ {
212
+ hooks: {
213
+ PreToolUse: [
214
+ {
215
+ matcher: 'Edit|Write',
216
+ hooks: [
217
+ { type: 'command', command: 'npx --yes @rune-kit/rune hook-dispatch preflight' },
218
+ { type: 'command', command: 'user-lint.sh' },
219
+ ],
220
+ },
221
+ ],
222
+ },
223
+ },
224
+ null,
225
+ 2,
226
+ ),
227
+ );
228
+
229
+ const result = await uninstallHooks(tmpRoot);
230
+ assert.strictEqual(result.written, true);
231
+ const settings = JSON.parse(await readFile(path.join(settingsDir, 'settings.json'), 'utf-8'));
232
+ const editGroup = settings.hooks.PreToolUse.find((g) => g.matcher === 'Edit|Write');
233
+ assert.strictEqual(editGroup.hooks.length, 1);
234
+ assert.strictEqual(editGroup.hooks[0].command, 'user-lint.sh');
235
+ });
236
+
237
+ test('T4: user hook preserved exactly through install/uninstall round-trip', async () => {
238
+ const settingsDir = path.join(tmpRoot, '.claude');
239
+ await mkdir(settingsDir, { recursive: true });
240
+ const userCommand = 'my-rune hook-dispatch-notes.sh';
241
+ const initialSettings = {
242
+ hooks: {
243
+ PreToolUse: [{ matcher: 'Edit|Write', hooks: [{ type: 'command', command: userCommand }] }],
244
+ },
245
+ };
246
+ await writeFile(path.join(settingsDir, 'settings.json'), JSON.stringify(initialSettings, null, 2));
247
+
248
+ await installHooks(tmpRoot, { preset: 'gentle' });
249
+ await uninstallHooks(tmpRoot);
250
+
251
+ const settings = JSON.parse(await readFile(path.join(settingsDir, 'settings.json'), 'utf-8'));
252
+ const editGroup = settings.hooks?.PreToolUse?.find((g) => g.matcher === 'Edit|Write');
253
+ assert.ok(editGroup, 'user Edit|Write group must survive');
254
+ assert.ok(
255
+ editGroup.hooks.some((h) => h.command === userCommand),
256
+ `user command "${userCommand}" must be preserved exactly`,
257
+ );
258
+ });
259
+
260
+ test('malformed settings.json causes actionable error on uninstall', async () => {
261
+ const settingsDir = path.join(tmpRoot, '.claude');
262
+ await mkdir(settingsDir, { recursive: true });
263
+ await writeFile(path.join(settingsDir, 'settings.json'), '{ broken json', 'utf-8');
264
+ await assert.rejects(uninstallHooks(tmpRoot), (err) => {
265
+ assert.ok(err.message.includes('settings.json') || err.message.includes('JSON'));
266
+ return true;
267
+ });
268
+ });
269
+ });
270
+
271
+ describe('hookStatus (claude adapter)', () => {
272
+ test('reports empty platforms when nothing detected', async () => {
273
+ const result = await hookStatus(tmpRoot, RUNE_ROOT);
274
+ assert.deepStrictEqual(result.platforms, []);
275
+ });
276
+
277
+ test('reports none for .claude/ without settings', async () => {
278
+ await seedClaude(tmpRoot);
279
+ const result = await hookStatus(tmpRoot, RUNE_ROOT);
280
+ const claude = result.results.find((r) => r.platform === 'claude');
281
+ assert.strictEqual(claude.installed, false);
282
+ assert.strictEqual(claude.preset, null);
283
+ });
284
+
285
+ test('reports gentle preset after install', async () => {
286
+ await seedClaude(tmpRoot);
287
+ await installHooks(tmpRoot, { preset: 'gentle' });
288
+ const result = await hookStatus(tmpRoot, RUNE_ROOT);
289
+ const claude = result.results.find((r) => r.platform === 'claude');
290
+ assert.strictEqual(claude.installed, true);
291
+ assert.strictEqual(claude.preset, 'gentle');
292
+ assert.ok(claude.events.PreToolUse.includes('preflight'));
293
+ assert.ok(claude.events.Stop.includes('completion-gate'));
294
+ });
295
+
296
+ test('--platform all surfaces every adapter', async () => {
297
+ const result = await hookStatus(tmpRoot, RUNE_ROOT, { platform: 'all' });
298
+ assert.strictEqual(result.results.length, 4);
299
+ for (const id of ['claude', 'cursor', 'windsurf', 'antigravity']) {
300
+ const r = result.results.find((x) => x.platform === id);
301
+ assert.ok(r, `result for ${id} must be present`);
302
+ assert.ok(r.capability, `capability matrix for ${id} must be present`);
303
+ }
304
+ });
305
+ });
@@ -0,0 +1,204 @@
1
+ import assert from 'node:assert';
2
+ import { describe, test } from 'node:test';
3
+ import { detectPreset, mergePreset, stripRuneHooks, summarizeRuneHooks } from '../commands/hooks/merge.js';
4
+ import { buildPreset, isRuneManaged } from '../commands/hooks/presets.js';
5
+
6
+ describe('isRuneManaged', () => {
7
+ // T3: must reject strings that merely contain "rune" or "hook-dispatch"
8
+ test('T3: rejects commands that contain "rune hook-dispatch" as substring only', () => {
9
+ const falsePositives = [
10
+ 'my-rune hook-dispatch-notes.sh',
11
+ '/usr/local/bin/rune hook-dispatch-runner',
12
+ 'echo "rune hook-dispatch done"',
13
+ 'log-rune hook-dispatch.log',
14
+ ];
15
+ for (const cmd of falsePositives) {
16
+ assert.strictEqual(isRuneManaged({ command: cmd }), false, `should NOT match: ${cmd}`);
17
+ }
18
+ });
19
+
20
+ test('T3: matches the exact @rune-kit/rune hook-dispatch invocation', () => {
21
+ const truePositives = [
22
+ 'npx --yes @rune-kit/rune hook-dispatch preflight',
23
+ 'npx @rune-kit/rune hook-dispatch sentinel',
24
+ 'npx --yes @rune-kit/rune hook-dispatch completion-gate --gentle',
25
+ ];
26
+ for (const cmd of truePositives) {
27
+ assert.strictEqual(isRuneManaged({ command: cmd }), true, `should match: ${cmd}`);
28
+ }
29
+ });
30
+ });
31
+
32
+ describe('buildPreset', () => {
33
+ test('produces valid gentle preset with expected events', () => {
34
+ const preset = buildPreset('gentle');
35
+ assert.ok(preset.hooks.PreToolUse);
36
+ assert.ok(preset.hooks.PostToolUse);
37
+ assert.ok(preset.hooks.Stop);
38
+ const editGroup = preset.hooks.PreToolUse.find((g) => g.matcher === 'Edit|Write');
39
+ assert.ok(editGroup.hooks[0].command.includes('preflight'));
40
+ assert.ok(editGroup.hooks[0].command.includes('--gentle'));
41
+ });
42
+
43
+ test('strict preset omits --gentle flag', () => {
44
+ const preset = buildPreset('strict');
45
+ const editGroup = preset.hooks.PreToolUse.find((g) => g.matcher === 'Edit|Write');
46
+ assert.ok(!editGroup.hooks[0].command.includes('--gentle'));
47
+ });
48
+
49
+ test('rejects unknown preset', () => {
50
+ assert.throws(() => buildPreset('loose'), /Unknown preset/);
51
+ });
52
+
53
+ test('all commands carry Rune-managed signature', () => {
54
+ for (const name of ['gentle', 'strict']) {
55
+ const preset = buildPreset(name);
56
+ for (const groups of Object.values(preset.hooks)) {
57
+ for (const group of groups) {
58
+ for (const entry of group.hooks) {
59
+ assert.ok(isRuneManaged(entry), `entry missing signature: ${JSON.stringify(entry)}`);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ });
65
+ });
66
+
67
+ describe('stripRuneHooks', () => {
68
+ test('removes Rune entries, preserves user entries', () => {
69
+ const settings = {
70
+ hooks: {
71
+ PreToolUse: [
72
+ {
73
+ matcher: 'Edit|Write',
74
+ hooks: [
75
+ { type: 'command', command: 'npx --yes @rune-kit/rune hook-dispatch preflight --gentle' },
76
+ { type: 'command', command: 'my-custom-hook.sh' },
77
+ ],
78
+ },
79
+ ],
80
+ },
81
+ };
82
+ const result = stripRuneHooks(settings);
83
+ const group = result.hooks.PreToolUse.find((g) => g.matcher === 'Edit|Write');
84
+ assert.strictEqual(group.hooks.length, 1);
85
+ assert.strictEqual(group.hooks[0].command, 'my-custom-hook.sh');
86
+ });
87
+
88
+ test('drops event if all entries were Rune-managed', () => {
89
+ const settings = {
90
+ hooks: {
91
+ Stop: [
92
+ {
93
+ matcher: '.*',
94
+ hooks: [{ type: 'command', command: 'npx --yes @rune-kit/rune hook-dispatch completion-gate' }],
95
+ },
96
+ ],
97
+ },
98
+ };
99
+ const result = stripRuneHooks(settings);
100
+ assert.strictEqual(result.hooks, undefined);
101
+ });
102
+
103
+ test('handles missing hooks field', () => {
104
+ assert.deepStrictEqual(stripRuneHooks({ other: 'data' }), { other: 'data' });
105
+ });
106
+
107
+ test('returns {} for null input', () => {
108
+ assert.deepStrictEqual(stripRuneHooks(null), {});
109
+ });
110
+
111
+ test('preserves top-level non-hook fields', () => {
112
+ const settings = {
113
+ $schema: 'https://example.com/schema.json',
114
+ env: { FOO: 'bar' },
115
+ hooks: {
116
+ Stop: [{ matcher: '.*', hooks: [{ command: 'npx --yes @rune-kit/rune hook-dispatch completion-gate' }] }],
117
+ },
118
+ };
119
+ const result = stripRuneHooks(settings);
120
+ assert.strictEqual(result.$schema, 'https://example.com/schema.json');
121
+ assert.deepStrictEqual(result.env, { FOO: 'bar' });
122
+ });
123
+ });
124
+
125
+ describe('mergePreset', () => {
126
+ test('fresh install writes full preset', () => {
127
+ const merged = mergePreset({}, buildPreset('gentle'));
128
+ assert.ok(merged.hooks.PreToolUse);
129
+ assert.ok(merged.hooks.Stop);
130
+ });
131
+
132
+ test('re-install replaces Rune entries, keeps user entries', () => {
133
+ const initial = mergePreset({}, buildPreset('gentle'));
134
+ // user adds their own hook to the same matcher
135
+ initial.hooks.PreToolUse[0].hooks.push({ type: 'command', command: 'user-lint.sh' });
136
+
137
+ const reinstalled = mergePreset(initial, buildPreset('strict'));
138
+ const editGroup = reinstalled.hooks.PreToolUse.find((g) => g.matcher === 'Edit|Write');
139
+ const commands = editGroup.hooks.map((h) => h.command);
140
+ assert.ok(commands.includes('user-lint.sh'), 'user hook preserved');
141
+ assert.ok(
142
+ commands.some((c) => c.includes('preflight') && !c.includes('--gentle')),
143
+ 'strict preflight installed',
144
+ );
145
+ assert.ok(!commands.some((c) => c.includes('--gentle')), 'no gentle entries remain');
146
+ });
147
+
148
+ test('idempotent — same preset twice = same result', () => {
149
+ const once = mergePreset({}, buildPreset('gentle'));
150
+ const twice = mergePreset(once, buildPreset('gentle'));
151
+ assert.deepStrictEqual(once, twice);
152
+ });
153
+
154
+ test('merges into existing user matcher without duplicate groups', () => {
155
+ const userSettings = {
156
+ hooks: {
157
+ PreToolUse: [{ matcher: 'Edit|Write', hooks: [{ command: 'user-guard.sh' }] }],
158
+ },
159
+ };
160
+ const merged = mergePreset(userSettings, buildPreset('gentle'));
161
+ const groups = merged.hooks.PreToolUse.filter((g) => g.matcher === 'Edit|Write');
162
+ assert.strictEqual(groups.length, 1, 'no duplicate matcher group');
163
+ assert.ok(groups[0].hooks.some((h) => h.command === 'user-guard.sh'));
164
+ assert.ok(groups[0].hooks.some((h) => h.command.includes('preflight')));
165
+ });
166
+ });
167
+
168
+ describe('summarizeRuneHooks', () => {
169
+ test('extracts skill names per event', () => {
170
+ const merged = mergePreset({}, buildPreset('gentle'));
171
+ const summary = summarizeRuneHooks(merged);
172
+ assert.strictEqual(summary.total, 4);
173
+ assert.ok(summary.events.PreToolUse.includes('preflight'));
174
+ assert.ok(summary.events.PreToolUse.includes('sentinel'));
175
+ assert.ok(summary.events.PostToolUse.includes('dependency-doctor'));
176
+ assert.ok(summary.events.Stop.includes('completion-gate'));
177
+ });
178
+
179
+ test('empty settings → zero total', () => {
180
+ const summary = summarizeRuneHooks({});
181
+ assert.strictEqual(summary.total, 0);
182
+ });
183
+ });
184
+
185
+ describe('detectPreset', () => {
186
+ test('detects gentle', () => {
187
+ assert.strictEqual(detectPreset(mergePreset({}, buildPreset('gentle'))), 'gentle');
188
+ });
189
+ test('detects strict', () => {
190
+ assert.strictEqual(detectPreset(mergePreset({}, buildPreset('strict'))), 'strict');
191
+ });
192
+ test('detects mixed', () => {
193
+ const mixed = mergePreset({}, buildPreset('gentle'));
194
+ // manually inject a strict entry
195
+ mixed.hooks.Stop[0].hooks.push({
196
+ type: 'command',
197
+ command: 'npx --yes @rune-kit/rune hook-dispatch sentinel',
198
+ });
199
+ assert.strictEqual(detectPreset(mixed), 'mixed');
200
+ });
201
+ test('returns none for empty settings', () => {
202
+ assert.strictEqual(detectPreset({}), 'none');
203
+ });
204
+ });