@massu/core 1.10.8 → 1.12.1

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 (178) hide show
  1. package/CHANGELOG.md +1334 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +581 -448
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +67 -19
  16. package/dist/hooks/pre-compact.js +28 -3
  17. package/dist/hooks/pre-delete-check.js +53 -27
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +31 -6
  23. package/dist/hooks/session-start.js +8709 -8459
  24. package/dist/hooks/user-prompt.js +25 -1
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -1,430 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- /**
5
- * `massu watch` CLI runner.
6
- *
7
- * Default mode: spawn `claude-bg start ... -- npx @massu/core watch --foreground`
8
- * so the daemon registers in ~/.claude/bg-registry.jsonl and inherits the
9
- * SessionEnd reaper. (Phase 0 decision: claude-bg is a peer dependency, not
10
- * embedded.)
11
- *
12
- * --foreground: run the daemon in the current terminal. Used internally by
13
- * claude-bg-spawn AND by users who prefer their own process supervisor.
14
- *
15
- * --status / --stop: thin wrappers over `claude-bg list --mine` and
16
- * `claude-bg kill <name>`.
17
- *
18
- * --apply-now: bypass quiescence and apply immediately. Useful in CI.
19
- *
20
- * Library discipline (CR / VR-LIBRARY-NO-PROCESS-EXIT): runWatch() returns
21
- * a result object; only cli.ts calls process.exit on the returned code.
22
- */
23
-
24
- import { spawnSync } from 'child_process';
25
- import { basename, dirname, resolve } from 'path';
26
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'fs';
27
- import { runDetection } from '../detect/index.ts';
28
- import { computeFingerprint } from '../detect/drift.ts';
29
- import { runConfigRefresh } from './config-refresh.ts';
30
- import { installAll } from './install-commands.ts';
31
- import { withInstallLock, InstallLockBusyError } from '../lib/installLock.ts';
32
- import { isPidAlive } from '../lib/pidLiveness.ts';
33
- import { gitToplevel } from '../lib/gitToplevel.ts';
34
- import { startDaemon } from '../watch/daemon.ts';
35
- import { readState, updateState, watchStatePath } from '../watch/state.ts';
36
-
37
- export interface WatchResult {
38
- exitCode: 0 | 1 | 2;
39
- message?: string;
40
- }
41
-
42
- interface ParsedFlags {
43
- foreground: boolean;
44
- status: boolean;
45
- stop: boolean;
46
- applyNow: boolean;
47
- root?: string;
48
- help: boolean;
49
- }
50
-
51
- function parseFlags(args: string[]): ParsedFlags {
52
- const out: ParsedFlags = {
53
- foreground: false,
54
- status: false,
55
- stop: false,
56
- applyNow: false,
57
- help: false,
58
- };
59
- for (let i = 0; i < args.length; i++) {
60
- const a = args[i];
61
- if (a === '--foreground') out.foreground = true;
62
- else if (a === '--status') out.status = true;
63
- else if (a === '--stop') out.stop = true;
64
- else if (a === '--apply-now') out.applyNow = true;
65
- else if (a === '--help' || a === '-h') out.help = true;
66
- else if (a === '--root') {
67
- out.root = args[i + 1];
68
- i++;
69
- }
70
- }
71
- return out;
72
- }
73
-
74
- function findClaudeBg(): string | null {
75
- // Prefer the conventional ~/.claude/bin install path; fall back to PATH.
76
- const home = process.env.HOME ?? '';
77
- const fixed = home ? resolve(home, '.claude', 'bin', 'claude-bg') : null;
78
- if (fixed && existsSync(fixed)) return fixed;
79
- const which = spawnSync('which', ['claude-bg'], { encoding: 'utf-8' });
80
- if (which.status === 0 && which.stdout) {
81
- const p = which.stdout.trim();
82
- if (p) return p;
83
- }
84
- return null;
85
- }
86
-
87
- function watchName(root: string): string {
88
- return `massu-watch-${basename(root)}`;
89
- }
90
-
91
- function printHelp(out: (s: string) => void): void {
92
- out(`
93
- Usage: massu watch [flags]
94
-
95
- Default Start the watcher under claude-bg.
96
- --foreground Run the daemon in the current terminal (used by claude-bg).
97
- --status Show running watcher info.
98
- --stop Stop the watcher (kills via claude-bg).
99
- --apply-now Bypass quiescence and apply immediately (CI use).
100
- --root <dir> Daemon root (default: git rev-parse --show-toplevel).
101
- -h, --help Show this help.
102
- `);
103
- }
104
-
105
- export async function runWatch(args: string[]): Promise<WatchResult> {
106
- const flags = parseFlags(args);
107
- if (flags.help) {
108
- printHelp((s) => process.stdout.write(s));
109
- return { exitCode: 0 };
110
- }
111
-
112
- const cwd = process.cwd();
113
- const root = flags.root ?? gitToplevel(cwd);
114
-
115
- if (flags.status) return runStatus(root);
116
- if (flags.stop) return runStop(root);
117
- if (flags.applyNow) return runApplyNow(root);
118
- if (flags.foreground) return runForeground(root);
119
-
120
- // Default: spawn under claude-bg.
121
- const claudeBg = findClaudeBg();
122
- if (!claudeBg) {
123
- const msg =
124
- 'massu watch needs `claude-bg` on your PATH (or installed at ~/.claude/bin/claude-bg).\n' +
125
- 'Install: see https://massu.ai/docs/watch (or run `massu watch --foreground` to skip the bg supervisor).\n';
126
- process.stderr.write(msg);
127
- return { exitCode: 1, message: msg };
128
- }
129
-
130
- const name = watchName(root);
131
- const res = spawnSync(claudeBg, [
132
- 'start',
133
- '--name', name,
134
- '--port', '0',
135
- '--',
136
- 'npx', '@massu/core', 'watch', '--foreground', '--root', root,
137
- ], { stdio: 'inherit' });
138
-
139
- return { exitCode: (res.status ?? 1) === 0 ? 0 : 1 };
140
- }
141
-
142
- function runStatus(root: string): WatchResult {
143
- const path = watchStatePath(root);
144
- if (!existsSync(path)) {
145
- process.stdout.write('massu watch: not running (no state file)\n');
146
- return { exitCode: 0 };
147
- }
148
- let state;
149
- try {
150
- state = readState(root);
151
- } catch (err) {
152
- process.stderr.write(`massu watch: ${err instanceof Error ? err.message : String(err)}\n`);
153
- return { exitCode: 2 };
154
- }
155
- const alive = state.daemonPid ? isPidAlive(state.daemonPid) : false;
156
- process.stdout.write(
157
- `massu watch: ${alive ? 'running' : 'stale'}\n` +
158
- ` pid: ${state.daemonPid ?? 'n/a'}\n` +
159
- ` started: ${state.startedAt ?? 'n/a'}\n` +
160
- ` last refresh: ${state.lastRefreshAt ?? 'never'}\n` +
161
- ` last tick: ${state.tickedAt ?? 'never'}\n` +
162
- ` last error: ${formatLastError(state.lastError)}\n`
163
- );
164
- return { exitCode: 0 };
165
- }
166
-
167
- /**
168
- * Iter-7 fix: `state.lastError` may be multi-line (e.g. a chokidar stack
169
- * trace persisted via `updateState({lastError: msg})`). Rendering it raw on
170
- * the single-line `last error: ...` row produces messy column-broken output.
171
- * Collapse to the first non-empty line + " (...)" indicator when there were
172
- * additional lines, capped at 200 chars so a runaway message can't push
173
- * `runStatus` into multi-screen output.
174
- */
175
- function formatLastError(raw: string | null): string {
176
- if (raw === null || raw === undefined || raw === '') return 'none';
177
- const lines = raw.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
178
- if (lines.length === 0) return 'none';
179
- const head = lines[0];
180
- const truncated = head.length > 200 ? head.slice(0, 197) + '...' : head;
181
- return lines.length > 1 ? `${truncated} (+${lines.length - 1} more line(s))` : truncated;
182
- }
183
-
184
- function runStop(root: string): WatchResult {
185
- const claudeBg = findClaudeBg();
186
- if (!claudeBg) {
187
- process.stderr.write('massu watch --stop: claude-bg not found; cannot kill registered daemon\n');
188
- return { exitCode: 1 };
189
- }
190
- const res = spawnSync(claudeBg, ['kill', watchName(root)], { stdio: 'inherit' });
191
- return { exitCode: (res.status ?? 1) === 0 ? 0 : 1 };
192
- }
193
-
194
- async function runApplyNow(root: string): Promise<WatchResult> {
195
- // One-shot manual refresh — same code path as the daemon's onQuiescent.
196
- await runOnQuiescent(root);
197
- return { exitCode: 0 };
198
- }
199
-
200
- /**
201
- * Iter-8 fix (Plan 3a §256 risk #6): "second watcher with same toplevel
202
- * refuses to start". Returns a non-null message when another live daemon
203
- * already owns this root, else null. Exposed for unit testing — the
204
- * runForeground caller writes the message to stderr and returns exit 1.
205
- *
206
- * Without this guard, two `massu watch --foreground` calls racing on the
207
- * same repo result in two chokidar watchers, two debounce timers, two
208
- * fireRefresh paths writing watch-state.json + refresh-log concurrently —
209
- * the install-lock prevents data corruption but the user gets bursty
210
- * `installAll already running` stderr spew on every quiescence cycle.
211
- *
212
- * The pre-flight peek at watch-state.json + isPidAlive is cheap and
213
- * catches the common case (user re-running the command in another shell)
214
- * before any side effects.
215
- */
216
- export function checkConflictingDaemon(
217
- root: string,
218
- selfPid: number = process.pid,
219
- pidAlive: (pid: number) => boolean = isPidAlive,
220
- ): string | null {
221
- let existingState;
222
- try {
223
- existingState = readState(root);
224
- } catch {
225
- return null;
226
- }
227
- if (
228
- !existingState ||
229
- typeof existingState.daemonPid !== 'number' ||
230
- existingState.daemonPid <= 0 ||
231
- existingState.daemonPid === selfPid ||
232
- !pidAlive(existingState.daemonPid)
233
- ) {
234
- return null;
235
- }
236
- return (
237
- `massu watch: another daemon is already running for this root (PID=${existingState.daemonPid}).\n` +
238
- ` root: ${root}\n` +
239
- ` state: ${watchStatePath(root)}\n` +
240
- ` to stop it: massu watch --stop\n`
241
- );
242
- }
243
-
244
- /**
245
- * Foreground daemon entry. Resolves when SIGINT / SIGTERM is received.
246
- * cli.ts (the only allowed process.exit caller per VR-LIBRARY-NO-PROCESS-EXIT)
247
- * exits the process with the returned code.
248
- */
249
- async function runForeground(root: string): Promise<WatchResult> {
250
- const conflict = checkConflictingDaemon(root);
251
- if (conflict !== null) {
252
- process.stderr.write(conflict);
253
- return { exitCode: 1, message: conflict };
254
- }
255
-
256
- // Ensure the watcher writes startup state at the registered root, not cwd.
257
- // Save the prior cwd so shutdown can restore it (defense-in-depth — when
258
- // SIGINT/SIGTERM fires the process is exiting, but tests and any embedder
259
- // calling runForeground multiple times benefit from a clean restore).
260
- const priorCwd = process.cwd();
261
- process.chdir(root);
262
-
263
- // Iter-2 fix: if startDaemon throws (e.g., invalid config, chokidar
264
- // bootstrap fails), we must restore cwd before propagating — otherwise
265
- // tests and embedders are left with a permanently-changed cwd.
266
- let stopped = false;
267
- let handle;
268
- try {
269
- handle = await startDaemon(root, {
270
- onQuiescent: () => runOnQuiescent(root),
271
- });
272
- } catch (err) {
273
- try { process.chdir(priorCwd); } catch { /* best-effort */ }
274
- throw err;
275
- }
276
-
277
- return new Promise<WatchResult>((resolve) => {
278
- const shutdown = async (): Promise<void> => {
279
- if (stopped) return;
280
- stopped = true;
281
- process.stderr.write('[massu] shutting down watcher\n');
282
- await handle.stop();
283
- try {
284
- process.chdir(priorCwd);
285
- } catch {
286
- // priorCwd may have been removed — best-effort restore.
287
- }
288
- resolve({ exitCode: 0 });
289
- };
290
- process.on('SIGINT', () => { void shutdown(); });
291
- process.on('SIGTERM', () => { void shutdown(); });
292
- });
293
- }
294
-
295
- /**
296
- * Plan 3a Phase 6 quiescence callback:
297
- * 1. runDetection() — fresh stack inventory
298
- * 2. computeFingerprint() — compare to lastFingerprint
299
- * 3. on change: runConfigRefresh({silent, autoYes:true, skipCommands:true})
300
- * (per iter-3 G3-A9 option A: skip the recursive installAll inside
301
- * runConfigRefresh so we own the single `installAll` call ourselves)
302
- * 4. installAll(projectRoot) under withInstallLock
303
- * 5. updateState({lastFingerprint, lastRefreshAt})
304
- * 6. Append refresh-log event
305
- * 7. Stderr: `[massu] Stack changed, commands updated (N files). Diff: massu refresh-log latest`
306
- */
307
- export async function runOnQuiescent(projectRoot: string): Promise<void> {
308
- const detection = await runDetection(projectRoot);
309
- const newFingerprint = computeFingerprint(detection);
310
- const state = readState(projectRoot);
311
-
312
- if (state.lastFingerprint === newFingerprint && state.lastFingerprint !== null) {
313
- // Nothing actually changed in the stack; only file events fired.
314
- return;
315
- }
316
-
317
- // Refresh the YAML (auto-apply, but DO NOT recursively installAll).
318
- const refresh = await runConfigRefresh({
319
- cwd: projectRoot,
320
- silent: true,
321
- autoYes: true,
322
- skipCommands: true,
323
- });
324
-
325
- if (refresh.exitCode !== 0) {
326
- updateState(projectRoot, { lastError: refresh.message ?? 'refresh failed' });
327
- process.stderr.write(`[massu] config refresh failed: ${refresh.message ?? 'unknown'}\n`);
328
- return;
329
- }
330
-
331
- let installResult;
332
- try {
333
- installResult = withInstallLock(projectRoot, () => installAll(projectRoot));
334
- } catch (err) {
335
- if (err instanceof InstallLockBusyError) {
336
- // Another caller already installing — let them finish; next event re-fires.
337
- process.stderr.write(`[massu] ${err.message}\n`);
338
- updateState(projectRoot, { lastError: err.message });
339
- return;
340
- }
341
- throw err;
342
- }
343
-
344
- const filesTouched =
345
- installResult.totalInstalled +
346
- installResult.totalUpdated;
347
-
348
- appendRefreshLog(projectRoot, {
349
- at: new Date().toISOString(),
350
- fromFingerprint: state.lastFingerprint,
351
- toFingerprint: newFingerprint,
352
- filesInstalled: installResult.totalInstalled,
353
- filesUpdated: installResult.totalUpdated,
354
- filesKept: installResult.totalKept,
355
- });
356
-
357
- updateState(projectRoot, {
358
- lastFingerprint: newFingerprint,
359
- lastRefreshAt: new Date().toISOString(),
360
- lastError: null,
361
- });
362
-
363
- process.stderr.write(
364
- `[massu] Stack changed, commands updated (${filesTouched} files). Diff: massu refresh-log latest\n`
365
- );
366
- }
367
-
368
- // ============================================================
369
- // refresh-log support — append-only JSONL at .massu/refresh-log.jsonl
370
- // ============================================================
371
-
372
- export interface RefreshLogEvent {
373
- at: string;
374
- fromFingerprint: string | null;
375
- toFingerprint: string;
376
- filesInstalled: number;
377
- filesUpdated: number;
378
- filesKept: number;
379
- }
380
-
381
- function refreshLogPath(projectRoot: string): string {
382
- return resolve(projectRoot, '.massu', 'refresh-log.jsonl');
383
- }
384
-
385
- function appendRefreshLog(projectRoot: string, event: RefreshLogEvent): void {
386
- const path = refreshLogPath(projectRoot);
387
- try {
388
- // Append-only — at-most-one-line-per-event ensures partial writes corrupt
389
- // only the trailing line, which JSONL readers naturally tolerate.
390
- mkdirSync(dirname(path), { recursive: true });
391
- appendFileSync(path, JSON.stringify(event) + '\n', 'utf-8');
392
- } catch {
393
- // best-effort; never let log-write crash the daemon.
394
- }
395
- }
396
-
397
- export interface ReadRefreshLogOpts {
398
- /** Stderr writer for corrupt-line warnings (test seam; defaults to process.stderr). */
399
- warn?: (s: string) => void;
400
- }
401
-
402
- export function readRefreshLog(
403
- projectRoot: string,
404
- limit = 10,
405
- opts: ReadRefreshLogOpts = {},
406
- ): RefreshLogEvent[] {
407
- const path = refreshLogPath(projectRoot);
408
- if (!existsSync(path)) return [];
409
- const warn = opts.warn ?? ((s: string) => { process.stderr.write(s); });
410
- const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
411
- const tail = lines.slice(-limit);
412
- const out: RefreshLogEvent[] = [];
413
- let corrupt = 0;
414
- for (const line of tail) {
415
- try {
416
- const obj = JSON.parse(line);
417
- if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
418
- out.push(obj as RefreshLogEvent);
419
- } else {
420
- corrupt++;
421
- }
422
- } catch {
423
- corrupt++;
424
- }
425
- }
426
- if (corrupt > 0) {
427
- warn(`[massu] refresh-log: skipped ${corrupt} corrupt line(s) in ${path}\n`);
428
- }
429
- return out;
430
- }