@phuc1403/musketeer 0.9.0 → 0.10.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 (163) hide show
  1. package/INSTALLATION.md +52 -52
  2. package/bin/musketeer.js +168 -168
  3. package/package.json +48 -48
  4. package/src/dotnet-scaffold-copier.js +79 -79
  5. package/src/provisioner/detect.js +93 -93
  6. package/src/self-update.js +77 -77
  7. package/template/.claude/agents/git-manager.md +18 -18
  8. package/template/.claude/agents/hallmark-auditor.md +78 -78
  9. package/template/.claude/agents/researcher.md +33 -33
  10. package/template/.claude/hooks/block-unsafe-adr-title.cjs +85 -85
  11. package/template/.claude/hooks/init-adr-dir.cjs +173 -173
  12. package/template/.claude/hooks/inject-adr-flags.cjs +94 -94
  13. package/template/.claude/hooks/lib/adr/command-scan.cjs +115 -115
  14. package/template/.claude/hooks/lib/characteristics/checker.cjs +357 -357
  15. package/template/.claude/hooks/lib/git-info-cache.cjs +191 -191
  16. package/template/.claude/hooks/sync-adr-toc.cjs +146 -146
  17. package/template/.claude/hooks/usage-quota-cache-refresh.cjs +166 -166
  18. package/template/.claude/hooks/validate-characteristics-hook.cjs +66 -66
  19. package/template/.claude/hooks/validate-cml-hook.js +145 -145
  20. package/template/.claude/skills/adr-writer/SKILL.md +48 -48
  21. package/template/.claude/skills/adr-writer/references/adr-example.md +35 -35
  22. package/template/.claude/skills/architecture-characteristic-writer/SKILL.md +215 -215
  23. package/template/.claude/skills/architecture-characteristic-writer/assets/worksheet-template.md +29 -29
  24. package/template/.claude/skills/architecture-characteristic-writer/references/characteristics-catalog.md +40 -40
  25. package/template/.claude/skills/architecture-characteristic-writer/scripts/ranking-table.cjs +171 -171
  26. package/template/.claude/skills/context-map/SKILL.md +80 -80
  27. package/template/.claude/skills/context-map/example.cml +106 -106
  28. package/template/.claude/skills/context-map/reference/Bounded Context/Bounded Context.md +40 -40
  29. package/template/.claude/skills/context-map/reference/Bounded Context/businessModel.md +5 -5
  30. package/template/.claude/skills/context-map/reference/Bounded Context/domainVisionStatement.md +2 -2
  31. package/template/.claude/skills/context-map/reference/Bounded Context/evolution.md +5 -5
  32. package/template/.claude/skills/context-map/reference/Bounded Context/implementationTechnology.md +1 -1
  33. package/template/.claude/skills/context-map/reference/Bounded Context/implements.md +1 -1
  34. package/template/.claude/skills/context-map/reference/Bounded Context/knowledgeLevel.md +4 -4
  35. package/template/.claude/skills/context-map/reference/Bounded Context/realizes.md +9 -9
  36. package/template/.claude/skills/context-map/reference/Bounded Context/refines.md +10 -10
  37. package/template/.claude/skills/context-map/reference/Bounded Context/responsibilities.md +26 -26
  38. package/template/.claude/skills/context-map/reference/Bounded Context/type.md +23 -23
  39. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Anticorruption Layer.md +5 -5
  40. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Bounded Context Relationship.md +12 -12
  41. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Conformist.md +5 -5
  42. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Customer-Supplier (C-S).md +22 -22
  43. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Open Host Service.md +4 -4
  44. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Partnership (P).md +13 -13
  45. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Published Language.md +4 -4
  46. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Shared Kernel (SK).md +12 -12
  47. package/template/.claude/skills/context-map/reference/Context Map.md +62 -62
  48. package/template/.claude/skills/context-map/reference/Domain/Domain.md +30 -30
  49. package/template/.claude/skills/context-map/reference/Domain/supports.md +33 -33
  50. package/template/.claude/skills/context-map/reference/Domain/type.md +3 -3
  51. package/template/.claude/skills/context-map/reference/Semantic Rules.md +32 -32
  52. package/template/.claude/skills/hallmark/SKILL.md +552 -552
  53. package/template/.claude/skills/hallmark/references/anti-patterns.md +412 -412
  54. package/template/.claude/skills/hallmark/references/assets.md +406 -406
  55. package/template/.claude/skills/hallmark/references/color.md +95 -95
  56. package/template/.claude/skills/hallmark/references/component-cookbook.md +256 -256
  57. package/template/.claude/skills/hallmark/references/components/c1-outlined-chip.md +12 -12
  58. package/template/.claude/skills/hallmark/references/components/c2-inline-form-as-cta.md +16 -16
  59. package/template/.claude/skills/hallmark/references/components/c3-typographic-link.md +8 -8
  60. package/template/.claude/skills/hallmark/references/components/c4-sticky-bottom-bar.md +16 -16
  61. package/template/.claude/skills/hallmark/references/components/f1-bento-grid.md +20 -20
  62. package/template/.claude/skills/hallmark/references/components/f2-sticky-scroll-stack.md +20 -20
  63. package/template/.claude/skills/hallmark/references/components/f3-tabular-spec-sheet.md +11 -11
  64. package/template/.claude/skills/hallmark/references/components/f4-step-sequence.md +11 -11
  65. package/template/.claude/skills/hallmark/references/components/f5-annotated-screenshot.md +11 -11
  66. package/template/.claude/skills/hallmark/references/components/f6-product-card-grid.md +41 -41
  67. package/template/.claude/skills/hallmark/references/components/ft1-mast-headed.md +13 -13
  68. package/template/.claude/skills/hallmark/references/components/ft2-inline-rule-single-line.md +10 -10
  69. package/template/.claude/skills/hallmark/references/components/ft3-index-style-category-list.md +12 -12
  70. package/template/.claude/skills/hallmark/references/components/ft4-dense-typographic.md +10 -10
  71. package/template/.claude/skills/hallmark/references/components/ft5-statement.md +21 -21
  72. package/template/.claude/skills/hallmark/references/components/ft6-letter-close.md +19 -19
  73. package/template/.claude/skills/hallmark/references/components/ft7-newsletter-first.md +27 -27
  74. package/template/.claude/skills/hallmark/references/components/ft8-marquee-scroll.md +25 -25
  75. package/template/.claude/skills/hallmark/references/components/h1-marquee.md +15 -15
  76. package/template/.claude/skills/hallmark/references/components/h2-split-diptych.md +15 -15
  77. package/template/.claude/skills/hallmark/references/components/h3-quote-led.md +11 -11
  78. package/template/.claude/skills/hallmark/references/components/h4-stat-led.md +14 -14
  79. package/template/.claude/skills/hallmark/references/components/h5-letter-hero.md +11 -11
  80. package/template/.claude/skills/hallmark/references/components/h6-photographic-fold.md +16 -16
  81. package/template/.claude/skills/hallmark/references/components/h7-demo-video-clipped-by-viewport-edge.md +27 -27
  82. package/template/.claude/skills/hallmark/references/components/h8-mockup-split-browser-framed.md +23 -23
  83. package/template/.claude/skills/hallmark/references/components/h9-custom-illustration-centerpiece.md +27 -27
  84. package/template/.claude/skills/hallmark/references/components/n1-wordmark-2-links.md +12 -12
  85. package/template/.claude/skills/hallmark/references/components/n10-floating-on-scroll-morph.md +19 -19
  86. package/template/.claude/skills/hallmark/references/components/n2-floating-chip.md +14 -14
  87. package/template/.claude/skills/hallmark/references/components/n3-side-rail.md +14 -14
  88. package/template/.claude/skills/hallmark/references/components/n4-hidden-behind-k.md +9 -9
  89. package/template/.claude/skills/hallmark/references/components/n5-floating-pill.md +28 -28
  90. package/template/.claude/skills/hallmark/references/components/n6-newspaper-masthead.md +24 -24
  91. package/template/.claude/skills/hallmark/references/components/n7-brutal-slab.md +22 -22
  92. package/template/.claude/skills/hallmark/references/components/n8-terminal-command.md +21 -21
  93. package/template/.claude/skills/hallmark/references/components/n9-edge-aligned-minimal.md +17 -17
  94. package/template/.claude/skills/hallmark/references/components/s1-left-margin-numbered.md +15 -15
  95. package/template/.claude/skills/hallmark/references/components/s2-hanging.md +13 -13
  96. package/template/.claude/skills/hallmark/references/components/s3-sticky-pinned.md +19 -19
  97. package/template/.claude/skills/hallmark/references/components/s4-inline-no-break.md +11 -11
  98. package/template/.claude/skills/hallmark/references/components/s5-bottom-anchored.md +13 -13
  99. package/template/.claude/skills/hallmark/references/components/t1-pull-quote-with-marginalia.md +12 -12
  100. package/template/.claude/skills/hallmark/references/components/t2-logo-wall-hairline.md +19 -19
  101. package/template/.claude/skills/hallmark/references/components/t3-single-huge-quote.md +11 -11
  102. package/template/.claude/skills/hallmark/references/components/t4-numbered-stat-strip.md +14 -14
  103. package/template/.claude/skills/hallmark/references/contract.md +24 -24
  104. package/template/.claude/skills/hallmark/references/copy.md +182 -182
  105. package/template/.claude/skills/hallmark/references/custom-craft.md +626 -626
  106. package/template/.claude/skills/hallmark/references/custom-theme.md +329 -329
  107. package/template/.claude/skills/hallmark/references/design-md.md +116 -116
  108. package/template/.claude/skills/hallmark/references/export-formats.md +328 -328
  109. package/template/.claude/skills/hallmark/references/floating-nav.md +89 -89
  110. package/template/.claude/skills/hallmark/references/genres/atmospheric.md +65 -65
  111. package/template/.claude/skills/hallmark/references/genres/editorial.md +70 -70
  112. package/template/.claude/skills/hallmark/references/genres/modern-minimal.md +67 -67
  113. package/template/.claude/skills/hallmark/references/genres/playful.md +65 -65
  114. package/template/.claude/skills/hallmark/references/hero-enrichment.md +474 -474
  115. package/template/.claude/skills/hallmark/references/imagery-kit.md +170 -170
  116. package/template/.claude/skills/hallmark/references/interaction-and-states.md +207 -207
  117. package/template/.claude/skills/hallmark/references/layout-and-space.md +111 -111
  118. package/template/.claude/skills/hallmark/references/macrostructures/01-bento-grid.md +35 -35
  119. package/template/.claude/skills/hallmark/references/macrostructures/02-long-document.md +34 -34
  120. package/template/.claude/skills/hallmark/references/macrostructures/03-marquee-hero.md +31 -31
  121. package/template/.claude/skills/hallmark/references/macrostructures/04-stat-led.md +32 -32
  122. package/template/.claude/skills/hallmark/references/macrostructures/05-workbench.md +32 -32
  123. package/template/.claude/skills/hallmark/references/macrostructures/06-conversational-faq.md +33 -33
  124. package/template/.claude/skills/hallmark/references/macrostructures/07-manifesto.md +32 -32
  125. package/template/.claude/skills/hallmark/references/macrostructures/08-photographic.md +34 -34
  126. package/template/.claude/skills/hallmark/references/macrostructures/09-quote-led.md +32 -32
  127. package/template/.claude/skills/hallmark/references/macrostructures/10-specimen.md +32 -32
  128. package/template/.claude/skills/hallmark/references/macrostructures/11-catalogue.md +23 -23
  129. package/template/.claude/skills/hallmark/references/macrostructures/12-letter.md +23 -23
  130. package/template/.claude/skills/hallmark/references/macrostructures/13-index-first.md +23 -23
  131. package/template/.claude/skills/hallmark/references/macrostructures/14-narrative-workflow.md +23 -23
  132. package/template/.claude/skills/hallmark/references/macrostructures/15-split-studio.md +23 -23
  133. package/template/.claude/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -23
  134. package/template/.claude/skills/hallmark/references/macrostructures/17-type-specimen.md +23 -23
  135. package/template/.claude/skills/hallmark/references/macrostructures/18-portfolio-grid.md +23 -23
  136. package/template/.claude/skills/hallmark/references/macrostructures/19-map-diagram.md +23 -23
  137. package/template/.claude/skills/hallmark/references/macrostructures/20-ecosystem-index.md +23 -23
  138. package/template/.claude/skills/hallmark/references/macrostructures/21-component-playground.md +23 -23
  139. package/template/.claude/skills/hallmark/references/macrostructures.md +89 -89
  140. package/template/.claude/skills/hallmark/references/microinteractions.md +260 -260
  141. package/template/.claude/skills/hallmark/references/motion.md +109 -109
  142. package/template/.claude/skills/hallmark/references/preview-examples.md +49 -49
  143. package/template/.claude/skills/hallmark/references/responsive.md +138 -138
  144. package/template/.claude/skills/hallmark/references/slop-test.md +205 -205
  145. package/template/.claude/skills/hallmark/references/structure.md +164 -164
  146. package/template/.claude/skills/hallmark/references/study.md +511 -511
  147. package/template/.claude/skills/hallmark/references/typography.md +243 -243
  148. package/template/.claude/skills/hallmark/references/verbs/audit.md +25 -25
  149. package/template/.claude/skills/hallmark/references/verbs/redesign.md +269 -269
  150. package/template/.claude/skills/hallmark-loop/SKILL.md +105 -105
  151. package/template/.claude/skills/hallmark-loop/references/auditor-call.md +60 -60
  152. package/template/.claude/skills/hallmark-loop/references/capture.md +78 -78
  153. package/template/.claude/skills/hallmark-loop/references/loop-control.md +79 -79
  154. package/template/.claude/skills/handoff/SKILL.md +15 -15
  155. package/template/.claude/skills/knowledge-crunching/SKILL.md +94 -94
  156. package/template/.claude/skills/research/SKILL.md +69 -69
  157. package/template/.claude/skills/tdd/SKILL.md +142 -142
  158. package/template/.claude/skills/tdd/deep-modules.md +15 -15
  159. package/template/.claude/skills/tdd/interface-design.md +31 -31
  160. package/template/.claude/skills/tdd/mocking.md +59 -59
  161. package/template/.claude/skills/tdd/refactoring.md +10 -10
  162. package/template/.claude/skills/tdd/tests.md +61 -61
  163. package/template/.claude/statusline.cjs +100 -37
@@ -1,191 +1,191 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Git Info Cache - Cross-platform git information batching
6
- *
7
- * Problem: 5-6 git process spawns per statusline render are slow on Windows (CreateProcess overhead)
8
- * Solution: Cache git query results for 3 seconds — subsequent renders read cache (zero processes)
9
- *
10
- * Performance: 5 spawns per render → event-driven refresh + 30s TTL fallback
11
- * Cross-platform: No bash-only syntax (no 2>/dev/null), windowsHide on all exec calls
12
- */
13
-
14
- const { execSync } = require('child_process');
15
- const fs = require('fs');
16
- const path = require('path');
17
- const os = require('os');
18
-
19
- // Cache TTL — long fallback for external changes (git checkout outside Claude)
20
- // Active invalidation happens via PostToolUse hooks after Edit/Write/Bash
21
- const CACHE_TTL = 30000;
22
- const CACHE_MISS = Symbol('cache_miss');
23
- const CACHE_SKIP = Symbol('cache_skip');
24
-
25
- function isTimeoutError(error) {
26
- if (!error) return false;
27
- if (error.killed) return true;
28
- if (error.signal === 'SIGTERM') return true;
29
- return /timed out|etimedout/i.test(String(error.message || ''));
30
- }
31
-
32
- function getExecTimeoutMs() {
33
- const parsed = Number.parseInt(process.env.CK_GIT_TIMEOUT_MS || '', 10);
34
- if (Number.isFinite(parsed) && parsed > 0) return parsed;
35
- return 3000;
36
- }
37
-
38
- /**
39
- * Safe command execution wrapper with optional cwd
40
- * Timeout prevents hangs on slow/network-mounted repos
41
- */
42
- function execIn(cmd, cwd) {
43
- try {
44
- return {
45
- output: execSync(cmd, {
46
- encoding: 'utf8',
47
- stdio: ['pipe', 'pipe', 'ignore'],
48
- windowsHide: true,
49
- cwd: cwd || undefined,
50
- timeout: getExecTimeoutMs()
51
- }).trim(),
52
- timedOut: false
53
- };
54
- } catch (error) {
55
- return {
56
- output: '',
57
- timedOut: isTimeoutError(error)
58
- };
59
- }
60
- }
61
-
62
- /**
63
- * Get cache file path for current working directory
64
- */
65
- function getCachePath(cwd) {
66
- const hash = require('crypto')
67
- .createHash('md5')
68
- .update(cwd)
69
- .digest('hex')
70
- .slice(0, 8);
71
- return path.join(os.tmpdir(), `ck-git-cache-${hash}.json`);
72
- }
73
-
74
- /**
75
- * Read cache if valid (not expired). Returns CACHE_MISS on miss.
76
- * No existsSync check (TOCTOU race) — just try read and catch.
77
- */
78
- function readCache(cachePath, options = {}) {
79
- const { allowStale = false } = options;
80
- try {
81
- const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
82
- if (Date.now() - cache.timestamp < CACHE_TTL || allowStale) {
83
- return cache.data; // Can be null (non-git dir) or object (git info)
84
- }
85
- } catch {
86
- // File missing, corrupted, or expired — all treated as cache miss
87
- }
88
- return CACHE_MISS;
89
- }
90
-
91
- /**
92
- * Write cache atomically (temp file + rename to avoid partial reads on Windows)
93
- */
94
- function writeCache(cachePath, data) {
95
- const tmpPath = cachePath + '.tmp';
96
- try {
97
- fs.writeFileSync(tmpPath, JSON.stringify({ timestamp: Date.now(), data }));
98
- fs.renameSync(tmpPath, cachePath);
99
- } catch {
100
- try { fs.unlinkSync(tmpPath); } catch {}
101
- }
102
- }
103
-
104
- /**
105
- * Count non-empty lines in a newline-delimited string
106
- */
107
- function countLines(str) {
108
- if (!str) return 0;
109
- return str.split('\n').filter(l => l.trim()).length;
110
- }
111
-
112
- /**
113
- * Fetch git info directly in-process
114
- * The cache is what eliminates redundant spawns — not subprocess wrapping
115
- * @param {string} cwd - Directory to run git commands in
116
- * Returns: { branch, unstaged, staged, ahead, behind } or null if not git repo
117
- */
118
- function fetchGitInfo(cwd) {
119
- // Check if git repo (fast check) — run in target cwd, not process.cwd()
120
- const repoCheck = execIn('git rev-parse --git-dir', cwd);
121
- if (repoCheck.timedOut) return CACHE_SKIP;
122
- if (!repoCheck.output) {
123
- return null;
124
- }
125
-
126
- const branchPrimary = execIn('git branch --show-current', cwd);
127
- const branchFallback = execIn('git rev-parse --short HEAD', cwd);
128
- const unstagedResult = execIn('git diff --name-only', cwd);
129
- const stagedResult = execIn('git diff --cached --name-only', cwd);
130
- const aheadBehindResult = execIn('git rev-list --left-right --count @{u}...HEAD', cwd);
131
-
132
- if (
133
- branchPrimary.timedOut ||
134
- branchFallback.timedOut ||
135
- unstagedResult.timedOut ||
136
- stagedResult.timedOut ||
137
- aheadBehindResult.timedOut
138
- ) {
139
- return CACHE_SKIP;
140
- }
141
-
142
- const branch = branchPrimary.output || branchFallback.output;
143
- const unstaged = countLines(unstagedResult.output);
144
- const staged = countLines(stagedResult.output);
145
-
146
- // Ahead/behind — no 2>/dev/null (invalid on Windows cmd.exe)
147
- let ahead = 0;
148
- let behind = 0;
149
- if (aheadBehindResult.output) {
150
- const parts = aheadBehindResult.output.split(/\s+/);
151
- behind = parseInt(parts[0], 10) || 0;
152
- ahead = parseInt(parts[1], 10) || 0;
153
- }
154
-
155
- return { branch, unstaged, staged, ahead, behind };
156
- }
157
-
158
- /**
159
- * Get git info with caching
160
- * Main export function used by statusline
161
- */
162
- function getGitInfo(cwd = process.cwd()) {
163
- const cachePath = getCachePath(cwd);
164
-
165
- // Try cache first (includes cached null for non-git dirs)
166
- const cached = readCache(cachePath);
167
- if (cached !== CACHE_MISS) return cached;
168
-
169
- // Cache miss or expired, fetch fresh data in target cwd
170
- const data = fetchGitInfo(cwd);
171
- if (data === CACHE_SKIP) {
172
- // Timeout/transient failures should not poison cache as non-git.
173
- // Prefer last known value (even stale) if available.
174
- const stale = readCache(cachePath, { allowStale: true });
175
- return stale === CACHE_MISS ? null : stale;
176
- }
177
-
178
- // Cache both positive and null results (avoids re-spawning git in non-git dirs)
179
- writeCache(cachePath, data);
180
-
181
- return data;
182
- }
183
-
184
- /**
185
- * Invalidate cache for a directory (call after file changes to trigger fresh git query)
186
- */
187
- function invalidateCache(cwd = process.cwd()) {
188
- try { fs.unlinkSync(getCachePath(cwd)); } catch {}
189
- }
190
-
191
- module.exports = { getGitInfo, invalidateCache };
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Git Info Cache - Cross-platform git information batching
6
+ *
7
+ * Problem: 5-6 git process spawns per statusline render are slow on Windows (CreateProcess overhead)
8
+ * Solution: Cache git query results for 3 seconds — subsequent renders read cache (zero processes)
9
+ *
10
+ * Performance: 5 spawns per render → event-driven refresh + 30s TTL fallback
11
+ * Cross-platform: No bash-only syntax (no 2>/dev/null), windowsHide on all exec calls
12
+ */
13
+
14
+ const { execSync } = require('child_process');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+
19
+ // Cache TTL — long fallback for external changes (git checkout outside Claude)
20
+ // Active invalidation happens via PostToolUse hooks after Edit/Write/Bash
21
+ const CACHE_TTL = 30000;
22
+ const CACHE_MISS = Symbol('cache_miss');
23
+ const CACHE_SKIP = Symbol('cache_skip');
24
+
25
+ function isTimeoutError(error) {
26
+ if (!error) return false;
27
+ if (error.killed) return true;
28
+ if (error.signal === 'SIGTERM') return true;
29
+ return /timed out|etimedout/i.test(String(error.message || ''));
30
+ }
31
+
32
+ function getExecTimeoutMs() {
33
+ const parsed = Number.parseInt(process.env.CK_GIT_TIMEOUT_MS || '', 10);
34
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
35
+ return 3000;
36
+ }
37
+
38
+ /**
39
+ * Safe command execution wrapper with optional cwd
40
+ * Timeout prevents hangs on slow/network-mounted repos
41
+ */
42
+ function execIn(cmd, cwd) {
43
+ try {
44
+ return {
45
+ output: execSync(cmd, {
46
+ encoding: 'utf8',
47
+ stdio: ['pipe', 'pipe', 'ignore'],
48
+ windowsHide: true,
49
+ cwd: cwd || undefined,
50
+ timeout: getExecTimeoutMs()
51
+ }).trim(),
52
+ timedOut: false
53
+ };
54
+ } catch (error) {
55
+ return {
56
+ output: '',
57
+ timedOut: isTimeoutError(error)
58
+ };
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get cache file path for current working directory
64
+ */
65
+ function getCachePath(cwd) {
66
+ const hash = require('crypto')
67
+ .createHash('md5')
68
+ .update(cwd)
69
+ .digest('hex')
70
+ .slice(0, 8);
71
+ return path.join(os.tmpdir(), `ck-git-cache-${hash}.json`);
72
+ }
73
+
74
+ /**
75
+ * Read cache if valid (not expired). Returns CACHE_MISS on miss.
76
+ * No existsSync check (TOCTOU race) — just try read and catch.
77
+ */
78
+ function readCache(cachePath, options = {}) {
79
+ const { allowStale = false } = options;
80
+ try {
81
+ const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
82
+ if (Date.now() - cache.timestamp < CACHE_TTL || allowStale) {
83
+ return cache.data; // Can be null (non-git dir) or object (git info)
84
+ }
85
+ } catch {
86
+ // File missing, corrupted, or expired — all treated as cache miss
87
+ }
88
+ return CACHE_MISS;
89
+ }
90
+
91
+ /**
92
+ * Write cache atomically (temp file + rename to avoid partial reads on Windows)
93
+ */
94
+ function writeCache(cachePath, data) {
95
+ const tmpPath = cachePath + '.tmp';
96
+ try {
97
+ fs.writeFileSync(tmpPath, JSON.stringify({ timestamp: Date.now(), data }));
98
+ fs.renameSync(tmpPath, cachePath);
99
+ } catch {
100
+ try { fs.unlinkSync(tmpPath); } catch {}
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Count non-empty lines in a newline-delimited string
106
+ */
107
+ function countLines(str) {
108
+ if (!str) return 0;
109
+ return str.split('\n').filter(l => l.trim()).length;
110
+ }
111
+
112
+ /**
113
+ * Fetch git info directly in-process
114
+ * The cache is what eliminates redundant spawns — not subprocess wrapping
115
+ * @param {string} cwd - Directory to run git commands in
116
+ * Returns: { branch, unstaged, staged, ahead, behind } or null if not git repo
117
+ */
118
+ function fetchGitInfo(cwd) {
119
+ // Check if git repo (fast check) — run in target cwd, not process.cwd()
120
+ const repoCheck = execIn('git rev-parse --git-dir', cwd);
121
+ if (repoCheck.timedOut) return CACHE_SKIP;
122
+ if (!repoCheck.output) {
123
+ return null;
124
+ }
125
+
126
+ const branchPrimary = execIn('git branch --show-current', cwd);
127
+ const branchFallback = execIn('git rev-parse --short HEAD', cwd);
128
+ const unstagedResult = execIn('git diff --name-only', cwd);
129
+ const stagedResult = execIn('git diff --cached --name-only', cwd);
130
+ const aheadBehindResult = execIn('git rev-list --left-right --count @{u}...HEAD', cwd);
131
+
132
+ if (
133
+ branchPrimary.timedOut ||
134
+ branchFallback.timedOut ||
135
+ unstagedResult.timedOut ||
136
+ stagedResult.timedOut ||
137
+ aheadBehindResult.timedOut
138
+ ) {
139
+ return CACHE_SKIP;
140
+ }
141
+
142
+ const branch = branchPrimary.output || branchFallback.output;
143
+ const unstaged = countLines(unstagedResult.output);
144
+ const staged = countLines(stagedResult.output);
145
+
146
+ // Ahead/behind — no 2>/dev/null (invalid on Windows cmd.exe)
147
+ let ahead = 0;
148
+ let behind = 0;
149
+ if (aheadBehindResult.output) {
150
+ const parts = aheadBehindResult.output.split(/\s+/);
151
+ behind = parseInt(parts[0], 10) || 0;
152
+ ahead = parseInt(parts[1], 10) || 0;
153
+ }
154
+
155
+ return { branch, unstaged, staged, ahead, behind };
156
+ }
157
+
158
+ /**
159
+ * Get git info with caching
160
+ * Main export function used by statusline
161
+ */
162
+ function getGitInfo(cwd = process.cwd()) {
163
+ const cachePath = getCachePath(cwd);
164
+
165
+ // Try cache first (includes cached null for non-git dirs)
166
+ const cached = readCache(cachePath);
167
+ if (cached !== CACHE_MISS) return cached;
168
+
169
+ // Cache miss or expired, fetch fresh data in target cwd
170
+ const data = fetchGitInfo(cwd);
171
+ if (data === CACHE_SKIP) {
172
+ // Timeout/transient failures should not poison cache as non-git.
173
+ // Prefer last known value (even stale) if available.
174
+ const stale = readCache(cachePath, { allowStale: true });
175
+ return stale === CACHE_MISS ? null : stale;
176
+ }
177
+
178
+ // Cache both positive and null results (avoids re-spawning git in non-git dirs)
179
+ writeCache(cachePath, data);
180
+
181
+ return data;
182
+ }
183
+
184
+ /**
185
+ * Invalidate cache for a directory (call after file changes to trigger fresh git query)
186
+ */
187
+ function invalidateCache(cwd = process.cwd()) {
188
+ try { fs.unlinkSync(getCachePath(cwd)); } catch {}
189
+ }
190
+
191
+ module.exports = { getGitInfo, invalidateCache };
@@ -1,146 +1,146 @@
1
- #!/usr/bin/env node
2
- // PostToolUse hook: after any `adr` command, rebuild docs/adr/README.md from the
3
- // ADR files themselves — one row per ADR with its title and current status.
4
- //
5
- // The index is fully derived, so it can never drift: every run re-reads the
6
- // source. That is why there is no need to be clever about which adr subcommand
7
- // ran, or to protect the file from being rewritten.
8
- //
9
- // Also deletes the tool's own `decisions.md`. Every `adr new` regenerates that
10
- // file unconditionally (not just on `adr generate toc`), which would leave two
11
- // competing indexes side by side in the same directory. README.md is the richer
12
- // one — it carries a status column — so it is the one kept.
13
- //
14
- // Also runnable directly: node sync-adr-toc.cjs --write
15
- //
16
- // Fails open at exit 0 on any problem — a hook must never block a session.
17
-
18
- const fs = require('fs');
19
- const path = require('path');
20
-
21
- const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
22
- const ADR_FILE = /^\d+-.*\.md$/;
23
-
24
- // The tool reads its directory from a committed `.adr-dir` (default `doc/adr`).
25
- function adrDir() {
26
- try {
27
- return fs.readFileSync(path.join(root, '.adr-dir'), 'utf8').trim() || 'doc/adr';
28
- } catch {
29
- return 'doc/adr';
30
- }
31
- }
32
-
33
- // Was this Bash call an adr command? (`adr new -q …` counts; a hyphenated name
34
- // such as `adr-new` does not, hence the required whitespace.)
35
- function isAdrCommand() {
36
- try {
37
- const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
38
- return /\badr\s/.test(payload?.tool_input?.command || '');
39
- } catch {
40
- return false;
41
- }
42
- }
43
-
44
- // First line, minus the leading "# " and the tool's own number prefix — the
45
- // template renders "# 12: Use X over Y" for 0012-….md. Dropping the unpadded 12
46
- // keeps the padded
47
- // stem in the ADR column the only number in the file, so nothing here can be
48
- // mistaken for the stem `adr new -s` expects.
49
- function title(body, fallback) {
50
- const first = body.split(/\r?\n/)[0] || '';
51
- return first.replace(/^#\s*(\d+\s*[:.]\s*)?/, '').trim() || fallback;
52
- }
53
-
54
- // First non-empty line of the `## Status` section. The tool maintains this:
55
- // "Accepted", or "Superseded by [4: …](0004-….md)" after `adr new -s`.
56
- function status(body) {
57
- const lines = body.split(/\r?\n/);
58
- const start = lines.findIndex((l) => /^##\s+Status\s*$/i.test(l));
59
- if (start === -1) return 'Unknown';
60
- for (const line of lines.slice(start + 1)) {
61
- if (/^##\s/.test(line)) break;
62
- if (line.trim()) return line.trim();
63
- }
64
- return 'Unknown';
65
- }
66
-
67
- function render(dir, files) {
68
- const rows = files.map((name) => {
69
- const body = fs.readFileSync(path.join(root, dir, name), 'utf8');
70
- const stem = name.replace(/\.md$/, ''); // padded — what `adr new -s` needs
71
- const cell = (s) => s.replace(/\|/g, '\\|'); // never break the table
72
- return `| [${stem}](./${name}) | ${cell(title(body, stem))} | ${cell(status(body))} |`;
73
- });
74
-
75
- return [
76
- '# Architecture Decision Records',
77
- '',
78
- 'Generated from the ADR files by the `sync-adr-toc` hook — do not hand-edit.',
79
- '',
80
- '| ADR | Title | Status |',
81
- '|-----|-------|--------|',
82
- ...rows,
83
- '',
84
- ].join('\n');
85
- }
86
-
87
- // The tool writes its own table of contents to `<adr-dir>/decisions.md` on every
88
- // `adr new`. README.md supersedes it, so drop it rather than commit two indexes
89
- // that have to agree with each other.
90
- //
91
- // Only ever deletes a file that is recognisably that generated index: the exact
92
- // heading it writes, followed by nothing but link lines. `decisions.md` is an
93
- // ordinary name for a hand-written decision log, and deleting one of those on
94
- // the next `adr new` would destroy work no backup covers.
95
- const GENERATED_TOC = /^# Table of Contents\r?\n\r?\n(?:- \[[^\]]*\]\([^)]*\)\r?\n?)*$/;
96
-
97
- function removeRedundantToc(dir) {
98
- const stale = path.join(root, dir, 'decisions.md');
99
- try {
100
- if (!fs.existsSync(stale)) return;
101
- if (!GENERATED_TOC.test(fs.readFileSync(stale, 'utf8'))) {
102
- process.stdout.write(
103
- `Left ${dir}/decisions.md alone — it is not the generated index. ` +
104
- 'README.md is the one this hook maintains.\n'
105
- );
106
- return;
107
- }
108
- fs.unlinkSync(stale);
109
- process.stdout.write(`Removed ${dir}/decisions.md (README.md is the index).\n`);
110
- } catch {
111
- /* not worth failing the hook over */
112
- }
113
- }
114
-
115
- function main() {
116
- if (!process.argv.includes('--write') && !isAdrCommand()) return;
117
-
118
- const dir = adrDir();
119
- removeRedundantToc(dir);
120
- const files = fs
121
- .readdirSync(path.join(root, dir))
122
- .filter((f) => ADR_FILE.test(f))
123
- .sort();
124
- if (files.length === 0) return;
125
-
126
- const target = path.join(root, dir, 'README.md');
127
- const next = render(dir, files);
128
-
129
- let current = null;
130
- try {
131
- current = fs.readFileSync(target, 'utf8');
132
- } catch {
133
- /* no index yet */
134
- }
135
- if (current === next) return; // already in sync
136
-
137
- fs.writeFileSync(target, next);
138
- process.stdout.write(`Regenerated ${dir}/README.md (${files.length} ADRs).\n`);
139
- }
140
-
141
- try {
142
- main();
143
- } catch {
144
- /* fail open */
145
- }
146
- process.exit(0);
1
+ #!/usr/bin/env node
2
+ // PostToolUse hook: after any `adr` command, rebuild docs/adr/README.md from the
3
+ // ADR files themselves — one row per ADR with its title and current status.
4
+ //
5
+ // The index is fully derived, so it can never drift: every run re-reads the
6
+ // source. That is why there is no need to be clever about which adr subcommand
7
+ // ran, or to protect the file from being rewritten.
8
+ //
9
+ // Also deletes the tool's own `decisions.md`. Every `adr new` regenerates that
10
+ // file unconditionally (not just on `adr generate toc`), which would leave two
11
+ // competing indexes side by side in the same directory. README.md is the richer
12
+ // one — it carries a status column — so it is the one kept.
13
+ //
14
+ // Also runnable directly: node sync-adr-toc.cjs --write
15
+ //
16
+ // Fails open at exit 0 on any problem — a hook must never block a session.
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
22
+ const ADR_FILE = /^\d+-.*\.md$/;
23
+
24
+ // The tool reads its directory from a committed `.adr-dir` (default `doc/adr`).
25
+ function adrDir() {
26
+ try {
27
+ return fs.readFileSync(path.join(root, '.adr-dir'), 'utf8').trim() || 'doc/adr';
28
+ } catch {
29
+ return 'doc/adr';
30
+ }
31
+ }
32
+
33
+ // Was this Bash call an adr command? (`adr new -q …` counts; a hyphenated name
34
+ // such as `adr-new` does not, hence the required whitespace.)
35
+ function isAdrCommand() {
36
+ try {
37
+ const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
38
+ return /\badr\s/.test(payload?.tool_input?.command || '');
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ // First line, minus the leading "# " and the tool's own number prefix — the
45
+ // template renders "# 12: Use X over Y" for 0012-….md. Dropping the unpadded 12
46
+ // keeps the padded
47
+ // stem in the ADR column the only number in the file, so nothing here can be
48
+ // mistaken for the stem `adr new -s` expects.
49
+ function title(body, fallback) {
50
+ const first = body.split(/\r?\n/)[0] || '';
51
+ return first.replace(/^#\s*(\d+\s*[:.]\s*)?/, '').trim() || fallback;
52
+ }
53
+
54
+ // First non-empty line of the `## Status` section. The tool maintains this:
55
+ // "Accepted", or "Superseded by [4: …](0004-….md)" after `adr new -s`.
56
+ function status(body) {
57
+ const lines = body.split(/\r?\n/);
58
+ const start = lines.findIndex((l) => /^##\s+Status\s*$/i.test(l));
59
+ if (start === -1) return 'Unknown';
60
+ for (const line of lines.slice(start + 1)) {
61
+ if (/^##\s/.test(line)) break;
62
+ if (line.trim()) return line.trim();
63
+ }
64
+ return 'Unknown';
65
+ }
66
+
67
+ function render(dir, files) {
68
+ const rows = files.map((name) => {
69
+ const body = fs.readFileSync(path.join(root, dir, name), 'utf8');
70
+ const stem = name.replace(/\.md$/, ''); // padded — what `adr new -s` needs
71
+ const cell = (s) => s.replace(/\|/g, '\\|'); // never break the table
72
+ return `| [${stem}](./${name}) | ${cell(title(body, stem))} | ${cell(status(body))} |`;
73
+ });
74
+
75
+ return [
76
+ '# Architecture Decision Records',
77
+ '',
78
+ 'Generated from the ADR files by the `sync-adr-toc` hook — do not hand-edit.',
79
+ '',
80
+ '| ADR | Title | Status |',
81
+ '|-----|-------|--------|',
82
+ ...rows,
83
+ '',
84
+ ].join('\n');
85
+ }
86
+
87
+ // The tool writes its own table of contents to `<adr-dir>/decisions.md` on every
88
+ // `adr new`. README.md supersedes it, so drop it rather than commit two indexes
89
+ // that have to agree with each other.
90
+ //
91
+ // Only ever deletes a file that is recognisably that generated index: the exact
92
+ // heading it writes, followed by nothing but link lines. `decisions.md` is an
93
+ // ordinary name for a hand-written decision log, and deleting one of those on
94
+ // the next `adr new` would destroy work no backup covers.
95
+ const GENERATED_TOC = /^# Table of Contents\r?\n\r?\n(?:- \[[^\]]*\]\([^)]*\)\r?\n?)*$/;
96
+
97
+ function removeRedundantToc(dir) {
98
+ const stale = path.join(root, dir, 'decisions.md');
99
+ try {
100
+ if (!fs.existsSync(stale)) return;
101
+ if (!GENERATED_TOC.test(fs.readFileSync(stale, 'utf8'))) {
102
+ process.stdout.write(
103
+ `Left ${dir}/decisions.md alone — it is not the generated index. ` +
104
+ 'README.md is the one this hook maintains.\n'
105
+ );
106
+ return;
107
+ }
108
+ fs.unlinkSync(stale);
109
+ process.stdout.write(`Removed ${dir}/decisions.md (README.md is the index).\n`);
110
+ } catch {
111
+ /* not worth failing the hook over */
112
+ }
113
+ }
114
+
115
+ function main() {
116
+ if (!process.argv.includes('--write') && !isAdrCommand()) return;
117
+
118
+ const dir = adrDir();
119
+ removeRedundantToc(dir);
120
+ const files = fs
121
+ .readdirSync(path.join(root, dir))
122
+ .filter((f) => ADR_FILE.test(f))
123
+ .sort();
124
+ if (files.length === 0) return;
125
+
126
+ const target = path.join(root, dir, 'README.md');
127
+ const next = render(dir, files);
128
+
129
+ let current = null;
130
+ try {
131
+ current = fs.readFileSync(target, 'utf8');
132
+ } catch {
133
+ /* no index yet */
134
+ }
135
+ if (current === next) return; // already in sync
136
+
137
+ fs.writeFileSync(target, next);
138
+ process.stdout.write(`Regenerated ${dir}/README.md (${files.length} ADRs).\n`);
139
+ }
140
+
141
+ try {
142
+ main();
143
+ } catch {
144
+ /* fail open */
145
+ }
146
+ process.exit(0);