@memnexus-ai/mx-agent-cli 0.1.207 → 0.1.209
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/start-report-integration.test.d.ts +12 -0
- package/dist/__tests__/start-report-integration.test.d.ts.map +1 -0
- package/dist/__tests__/start-report-integration.test.js +123 -0
- package/dist/__tests__/start-report-integration.test.js.map +1 -0
- package/dist/__tests__/startup-health-render.test.d.ts +11 -0
- package/dist/__tests__/startup-health-render.test.d.ts.map +1 -0
- package/dist/__tests__/startup-health-render.test.js +60 -0
- package/dist/__tests__/startup-health-render.test.js.map +1 -0
- package/dist/__tests__/startup-report.test.d.ts +13 -0
- package/dist/__tests__/startup-report.test.d.ts.map +1 -0
- package/dist/__tests__/startup-report.test.js +278 -0
- package/dist/__tests__/startup-report.test.js.map +1 -0
- package/dist/__tests__/startup-shipper.test.d.ts +13 -0
- package/dist/__tests__/startup-shipper.test.d.ts.map +1 -0
- package/dist/__tests__/startup-shipper.test.js +149 -0
- package/dist/__tests__/startup-shipper.test.js.map +1 -0
- package/dist/commands/dashboard.d.ts +21 -0
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +72 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +50 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +17 -0
- package/dist/commands/sync.js.map +1 -1
- package/dist/lib/claude.d.ts.map +1 -1
- package/dist/lib/claude.js +15 -3
- package/dist/lib/claude.js.map +1 -1
- package/dist/lib/startup-report.d.ts +143 -0
- package/dist/lib/startup-report.d.ts.map +1 -0
- package/dist/lib/startup-report.js +389 -0
- package/dist/lib/startup-report.js.map +1 -0
- package/dist/lib/startup-shipper.d.ts +55 -0
- package/dist/lib/startup-shipper.d.ts.map +1 -0
- package/dist/lib/startup-shipper.js +177 -0
- package/dist/lib/startup-shipper.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* startup-report.ts — durable capture of everything `mx-agent start` prints
|
|
3
|
+
* (platform v2.58 Phase 1, #4350).
|
|
4
|
+
*
|
|
5
|
+
* PO directive: capture EVERYTHING a start invocation prints — success, info,
|
|
6
|
+
* dim, warning, and error — into ONE structured StartupReport per invocation,
|
|
7
|
+
* and append it as a single JSON line to a durable local log before the process
|
|
8
|
+
* exits, on every exit path (clean success, warning-laden success, refusal
|
|
9
|
+
* exits, and the fail-closed hard-stop throw).
|
|
10
|
+
*
|
|
11
|
+
* Design:
|
|
12
|
+
* - `startupReport.record(level, message, code?)` buffers one entry in memory.
|
|
13
|
+
* - `startupReport.finalize(outcome)` assembles the report and appends one JSON
|
|
14
|
+
* line to `~/.mx-agent/logs/startup/<worktree>.jsonl`. It is idempotent (a
|
|
15
|
+
* second call is a no-op) and MUST NEVER THROW — all I/O is wrapped so a
|
|
16
|
+
* logging failure can never affect the session start.
|
|
17
|
+
* - Two collection surfaces feed `record`:
|
|
18
|
+
* 1. The typed helpers (reportSuccess/reportWarning/reportDim/reportInfo/
|
|
19
|
+
* reportError) record with an EXACT level and print via the ORIGINAL
|
|
20
|
+
* (un-teed) console, so they are never double-counted.
|
|
21
|
+
* 2. `installConsoleCapture()` tees console.log / console.error /
|
|
22
|
+
* process.stderr.write so that EVERY other direct print during a start
|
|
23
|
+
* (the ~70 direct console calls in start.ts, plus warnIfOutdated,
|
|
24
|
+
* checkConfigGeneration, and extractBehavioralContent) is captured with
|
|
25
|
+
* an inferred level. This guarantees no site is silently missed and that
|
|
26
|
+
* future new log lines are captured automatically.
|
|
27
|
+
*
|
|
28
|
+
* The module owns NO chalk styling and NEVER changes what the user sees — it
|
|
29
|
+
* only observes the byte stream and mirrors a plain-text copy into the report.
|
|
30
|
+
*/
|
|
31
|
+
import { appendFileSync, mkdirSync, renameSync, statSync } from 'fs';
|
|
32
|
+
import { homedir, hostname } from 'os';
|
|
33
|
+
import { dirname, join } from 'path';
|
|
34
|
+
import { spoolReport } from './startup-shipper.js';
|
|
35
|
+
/** Rotate the JSONL file once it exceeds this size (simple two-generation rotation). */
|
|
36
|
+
const MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
37
|
+
/**
|
|
38
|
+
* Full terminal control-sequence stripper (CWE-150 defense, #4350 security
|
|
39
|
+
* review). SGR color stripping alone (the old `\x1b\[...m`) left OSC (window-
|
|
40
|
+
* title set, hyperlinks) and CSI cursor/screen-clear sequences intact, so a
|
|
41
|
+
* crafted report field could drive the operator's terminal when rendered. This
|
|
42
|
+
* pattern removes:
|
|
43
|
+
* 1. CSI sequences — ESC [ params intermediates final (covers SGR, cursor
|
|
44
|
+
* moves, ESC[2J screen clear).
|
|
45
|
+
* 2. OSC sequences — ESC ] … terminated by BEL or ESC\ (covers ESC]0; title
|
|
46
|
+
* set and hyperlinks).
|
|
47
|
+
* 3. C0/C1 control chars — U+0000–0008, U+000B–001F, U+007F–009F. TAB (U+0009)
|
|
48
|
+
* and LF (U+000A) are deliberately preserved.
|
|
49
|
+
*
|
|
50
|
+
* Order matters: the multi-char CSI/OSC branches come BEFORE the single-char
|
|
51
|
+
* class so a leading ESC (itself a C0 control) is consumed as part of its
|
|
52
|
+
* sequence rather than stripped alone — which would leave the payload bytes
|
|
53
|
+
* (e.g. "[2J") behind as visible text.
|
|
54
|
+
*/
|
|
55
|
+
const CONTROL_SEQ_RE = /\u001b\[[0-?]*[ -\/]*[@-~]|\u001b\][^\u0007]*(?:\u0007|\u001b\\)|[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
|
|
56
|
+
/**
|
|
57
|
+
* Strip `userinfo` (user:password@) from any `scheme://` URL. Covers git fetch
|
|
58
|
+
* stderr that echoes a credentialed remote (e.g.
|
|
59
|
+
* `https://x-access-token:SECRET@github.com/...`) and any future capture site.
|
|
60
|
+
* SSH-style `git@github.com:` remotes have no `scheme://` and are left intact.
|
|
61
|
+
*/
|
|
62
|
+
const REDACT_URL_CRED = /([a-z][a-z0-9+.-]*:\/\/)[^/@\s]*@/gi;
|
|
63
|
+
/** Redact credentials from any URL in a message before it is persisted. */
|
|
64
|
+
export function redactUrlCredentials(s) {
|
|
65
|
+
return s.replace(REDACT_URL_CRED, '$1***@');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Strip all terminal control sequences (CSI, OSC) and C0/C1 control chars from a
|
|
69
|
+
* string so it is safe to store and to print to an operator's terminal. TAB and
|
|
70
|
+
* LF are preserved. Use this on ANY externally-influenced text before it is
|
|
71
|
+
* persisted or rendered (CWE-150).
|
|
72
|
+
*/
|
|
73
|
+
export function stripControlSequences(s) {
|
|
74
|
+
return String(s ?? '').replace(CONTROL_SEQ_RE, '');
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Back-compat alias — historically this stripped only SGR color codes; it now
|
|
78
|
+
* strips the full control-sequence set. All existing call sites (level
|
|
79
|
+
* inference, the console tee, stored messages) want the broader stripping.
|
|
80
|
+
*/
|
|
81
|
+
export function stripAnsi(s) {
|
|
82
|
+
return stripControlSequences(s);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Infer a log level from a printed line without any call-site cooperation.
|
|
86
|
+
* Glyph markers (✓ / ⚠ / ✗) are the codebase's consistent success/warning/error
|
|
87
|
+
* signal and survive with colors OFF, so they are checked first; ANSI color
|
|
88
|
+
* codes are a fallback that works when colors are ON. `stream` biases the
|
|
89
|
+
* default: stderr writes are informational diagnostics (warning) unless a glyph
|
|
90
|
+
* or red color says otherwise; console.error is error-level by convention.
|
|
91
|
+
*/
|
|
92
|
+
export function inferLevel(raw, stream) {
|
|
93
|
+
const t = stripAnsi(raw).trimStart();
|
|
94
|
+
if (t.startsWith('✓'))
|
|
95
|
+
return 'success';
|
|
96
|
+
if (t.startsWith('⚠'))
|
|
97
|
+
return 'warning';
|
|
98
|
+
if (t.startsWith('✗'))
|
|
99
|
+
return 'error';
|
|
100
|
+
// ANSI color fallback (colors on). Match the SGR foreground codes chalk uses,
|
|
101
|
+
// optionally preceded by the bold attribute (e.g. "1;31").
|
|
102
|
+
if (/\[(?:1;)?(?:31|91)m/.test(raw))
|
|
103
|
+
return 'error'; // red / bright-red
|
|
104
|
+
if (/\[(?:1;)?(?:33|93)m/.test(raw))
|
|
105
|
+
return 'warning'; // yellow / bright-yellow
|
|
106
|
+
if (/\[(?:1;)?(?:32|92)m/.test(raw))
|
|
107
|
+
return 'success'; // green / bright-green
|
|
108
|
+
if (/\[(?:2|90)m/.test(raw))
|
|
109
|
+
return 'dim'; // dim / bright-black (gray)
|
|
110
|
+
if (stream === 'error-console')
|
|
111
|
+
return 'error';
|
|
112
|
+
if (stream === 'err')
|
|
113
|
+
return 'warning';
|
|
114
|
+
return 'info';
|
|
115
|
+
}
|
|
116
|
+
class StartupReport {
|
|
117
|
+
entries = [];
|
|
118
|
+
ctx = {};
|
|
119
|
+
startedAt = new Date().toISOString();
|
|
120
|
+
finalized = false;
|
|
121
|
+
// Original (un-teed) console handles, captured on installConsoleCapture().
|
|
122
|
+
// Stored as the RAW references (not bound) so removeConsoleCapture restores the
|
|
123
|
+
// exact same function identity; calls use .apply with the proper receiver.
|
|
124
|
+
origLog = console.log;
|
|
125
|
+
origError = console.error;
|
|
126
|
+
origStderrWrite = process.stderr.write;
|
|
127
|
+
capturing = false;
|
|
128
|
+
exitNetRegistered = false;
|
|
129
|
+
// Fire-and-forget central-shipping hook (#4350 Phase 2). Registered by start.ts
|
|
130
|
+
// with the observability config; null everywhere else (e.g. tests) so finalize's
|
|
131
|
+
// local-write behavior is unchanged when no shipper is wired.
|
|
132
|
+
shipHook = null;
|
|
133
|
+
// Re-entrancy guard: Node's native console.error dispatches through the live
|
|
134
|
+
// (teed) process.stderr.write, so without this flag an error line would be
|
|
135
|
+
// recorded twice — once by the console.error tee (error) and again by the
|
|
136
|
+
// stderr tee (warning). We set it around every native origError call so the
|
|
137
|
+
// stderr tee skips the write that originates inside the console.error path.
|
|
138
|
+
inErrorTee = false;
|
|
139
|
+
/** Merge in context as it becomes known (project, worktree, etc.). */
|
|
140
|
+
setContext(partial) {
|
|
141
|
+
this.ctx = { ...this.ctx, ...partial };
|
|
142
|
+
}
|
|
143
|
+
/** Buffer one entry. Never throws. This is the single choke point for every
|
|
144
|
+
* persisted message, so it scrubs URL credentials AND strips all terminal
|
|
145
|
+
* control sequences (CWE-150) here — guaranteeing no CSI/OSC/C0/C1 byte is
|
|
146
|
+
* ever stored regardless of which capture site produced the entry. */
|
|
147
|
+
record(level, message, code) {
|
|
148
|
+
try {
|
|
149
|
+
const clean = redactUrlCredentials(stripControlSequences(message));
|
|
150
|
+
const entry = { ts: new Date().toISOString(), level, message: clean };
|
|
151
|
+
if (code)
|
|
152
|
+
entry.code = stripControlSequences(code);
|
|
153
|
+
this.entries.push(entry);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
/* recording must never affect the start */
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Assemble the report and append one JSON line to the durable log. Idempotent
|
|
161
|
+
* and never throws. Also removes the console capture so later prints are
|
|
162
|
+
* untouched.
|
|
163
|
+
*/
|
|
164
|
+
finalize(outcome) {
|
|
165
|
+
this.removeConsoleCapture();
|
|
166
|
+
if (this.finalized)
|
|
167
|
+
return;
|
|
168
|
+
this.finalized = true;
|
|
169
|
+
try {
|
|
170
|
+
let machine = this.ctx.machine;
|
|
171
|
+
if (!machine) {
|
|
172
|
+
try {
|
|
173
|
+
machine = hostname();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
machine = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const report = {
|
|
180
|
+
schema_version: 1,
|
|
181
|
+
project: this.ctx.project ?? null,
|
|
182
|
+
team: this.ctx.team ?? null,
|
|
183
|
+
worktree: this.ctx.worktree ?? null,
|
|
184
|
+
cli_version: this.ctx.cliVersion ?? null,
|
|
185
|
+
machine: machine ?? null,
|
|
186
|
+
started_at: this.startedAt,
|
|
187
|
+
finished_at: new Date().toISOString(),
|
|
188
|
+
outcome,
|
|
189
|
+
entries: this.entries,
|
|
190
|
+
};
|
|
191
|
+
const json = JSON.stringify(report);
|
|
192
|
+
this.appendLine(json);
|
|
193
|
+
// #4350 Phase 2: enqueue for central shipping. spoolReport never throws;
|
|
194
|
+
// the ship hook (if any) fires a best-effort POST and is fire-and-forget so
|
|
195
|
+
// it can never block or slow the start. A failed POST leaves the file
|
|
196
|
+
// spooled for the Stop-hook `mx-agent sync` flush.
|
|
197
|
+
spoolReport(json);
|
|
198
|
+
if (this.shipHook) {
|
|
199
|
+
try {
|
|
200
|
+
this.shipHook();
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
/* shipping must never affect the start */
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
/* a logging failure must never affect the start */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Register the fire-and-forget central-shipping hook (start.ts only). */
|
|
212
|
+
setShipHook(fn) {
|
|
213
|
+
this.shipHook = fn;
|
|
214
|
+
}
|
|
215
|
+
/** Absolute path to this invocation's JSONL log. */
|
|
216
|
+
logFilePath() {
|
|
217
|
+
const name = this.ctx.worktree || this.ctx.team || 'unknown';
|
|
218
|
+
const safe = name.replace(/[^A-Za-z0-9._-]/g, '_') || 'unknown';
|
|
219
|
+
return join(homedir(), '.mx-agent', 'logs', 'startup', `${safe}.jsonl`);
|
|
220
|
+
}
|
|
221
|
+
/** Append a line, creating dirs and performing two-generation rotation. */
|
|
222
|
+
appendLine(line) {
|
|
223
|
+
const path = this.logFilePath();
|
|
224
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
225
|
+
// Rotate BEFORE appending if the current file is over the size cap.
|
|
226
|
+
try {
|
|
227
|
+
const st = statSync(path);
|
|
228
|
+
if (st.size > MAX_LOG_BYTES)
|
|
229
|
+
renameSync(path, `${path}.1`);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
/* file does not exist yet — nothing to rotate */
|
|
233
|
+
}
|
|
234
|
+
appendFileSync(path, line + '\n');
|
|
235
|
+
}
|
|
236
|
+
// ── Console capture (tee) ────────────────────────────────────────────
|
|
237
|
+
/** True while console/stderr are teed into the collector. */
|
|
238
|
+
isCapturing() {
|
|
239
|
+
return this.capturing;
|
|
240
|
+
}
|
|
241
|
+
/** Print via the ORIGINAL console.log — used by the typed helpers so their
|
|
242
|
+
* output is not re-captured by the tee (no double-counting). */
|
|
243
|
+
printOut(formatted) {
|
|
244
|
+
(this.capturing ? this.origLog : console.log).call(console, formatted);
|
|
245
|
+
}
|
|
246
|
+
/** Print via the ORIGINAL console.error — see printOut. The native console.error
|
|
247
|
+
* routes through the teed process.stderr.write, so guard it to avoid a
|
|
248
|
+
* duplicate stderr-tee record (the caller has already recorded exactly once). */
|
|
249
|
+
printErr(formatted) {
|
|
250
|
+
if (!this.capturing) {
|
|
251
|
+
console.error(formatted);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
this.inErrorTee = true;
|
|
255
|
+
try {
|
|
256
|
+
this.origError.call(console, formatted);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
this.inErrorTee = false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Tee console.log / console.error / process.stderr.write into the collector.
|
|
264
|
+
* Idempotent. Overrides never throw and always delegate to the original
|
|
265
|
+
* stream, so capture can never suppress or corrupt user-visible output.
|
|
266
|
+
*/
|
|
267
|
+
installConsoleCapture() {
|
|
268
|
+
if (this.capturing)
|
|
269
|
+
return;
|
|
270
|
+
this.origLog = console.log;
|
|
271
|
+
this.origError = console.error;
|
|
272
|
+
this.origStderrWrite = process.stderr.write;
|
|
273
|
+
this.capturing = true;
|
|
274
|
+
const self = this;
|
|
275
|
+
console.log = ((...args) => {
|
|
276
|
+
try {
|
|
277
|
+
const text = args.map((a) => String(a)).join(' ');
|
|
278
|
+
if (stripAnsi(text).trim() !== '') {
|
|
279
|
+
self.record(inferLevel(text, 'out'), stripAnsi(text));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch { /* never let capture break printing */ }
|
|
283
|
+
self.origLog.apply(console, args);
|
|
284
|
+
});
|
|
285
|
+
console.error = ((...args) => {
|
|
286
|
+
try {
|
|
287
|
+
const text = args.map((a) => String(a)).join(' ');
|
|
288
|
+
if (stripAnsi(text).trim() !== '') {
|
|
289
|
+
self.record(inferLevel(text, 'error-console'), stripAnsi(text));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch { /* never let capture break printing */ }
|
|
293
|
+
// Native console.error writes via the teed process.stderr.write; guard it
|
|
294
|
+
// so the stderr tee does not record this line a second time.
|
|
295
|
+
self.inErrorTee = true;
|
|
296
|
+
try {
|
|
297
|
+
self.origError.apply(console, args);
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
self.inErrorTee = false;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
process.stderr.write = ((chunk, ...rest) => {
|
|
304
|
+
try {
|
|
305
|
+
// Skip recording when this write originates inside the console.error tee
|
|
306
|
+
// (or reportError/printErr) — that line was already recorded once.
|
|
307
|
+
if (!self.inErrorTee) {
|
|
308
|
+
const text = typeof chunk === 'string' ? chunk : Buffer.isBuffer(chunk) ? chunk.toString('utf-8') : '';
|
|
309
|
+
if (stripAnsi(text).trim() !== '') {
|
|
310
|
+
self.record(inferLevel(text, 'err'), stripAnsi(text).replace(/\n+$/, ''));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch { /* never let capture break printing */ }
|
|
315
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
316
|
+
return self.origStderrWrite.apply(process.stderr, [chunk, ...rest]);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
/** Restore the original console/stderr handles. Idempotent. */
|
|
320
|
+
removeConsoleCapture() {
|
|
321
|
+
if (!this.capturing)
|
|
322
|
+
return;
|
|
323
|
+
console.log = this.origLog;
|
|
324
|
+
console.error = this.origError;
|
|
325
|
+
process.stderr.write = this.origStderrWrite;
|
|
326
|
+
this.capturing = false;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Register a process-exit safety net that finalizes with 'error' if start
|
|
330
|
+
* exits on a path that did not finalize explicitly. Uses synchronous I/O
|
|
331
|
+
* (appendFileSync), which is valid in an 'exit' handler. Idempotent.
|
|
332
|
+
*/
|
|
333
|
+
registerExitSafetyNet() {
|
|
334
|
+
if (this.exitNetRegistered)
|
|
335
|
+
return;
|
|
336
|
+
// Under vitest the worker process exits long after any runStart call; letting
|
|
337
|
+
// the net fire then would write a stray report to the real home dir. Explicit
|
|
338
|
+
// finalize (refused/ok/hard_stop) covers every real exit path, so skipping
|
|
339
|
+
// only the belt-and-suspenders auto-write under test changes no prod behavior.
|
|
340
|
+
if (process.env.VITEST)
|
|
341
|
+
return;
|
|
342
|
+
this.exitNetRegistered = true;
|
|
343
|
+
process.on('exit', () => {
|
|
344
|
+
if (!this.finalized)
|
|
345
|
+
this.finalize('error');
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
// ── Test hooks ────────────────────────────────────────────────────────
|
|
349
|
+
/** @internal reset all state (tests only). */
|
|
350
|
+
_reset() {
|
|
351
|
+
this.removeConsoleCapture();
|
|
352
|
+
this.entries = [];
|
|
353
|
+
this.ctx = {};
|
|
354
|
+
this.startedAt = new Date().toISOString();
|
|
355
|
+
this.finalized = false;
|
|
356
|
+
this.shipHook = null;
|
|
357
|
+
}
|
|
358
|
+
/** @internal snapshot of buffered entries (tests only). */
|
|
359
|
+
_entries() {
|
|
360
|
+
return [...this.entries];
|
|
361
|
+
}
|
|
362
|
+
/** @internal whether finalize has run (tests only). */
|
|
363
|
+
_isFinalized() {
|
|
364
|
+
return this.finalized;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
export const startupReport = new StartupReport();
|
|
368
|
+
// ── Typed print+record helpers (exact level, bypass the tee) ────────────
|
|
369
|
+
export function reportInfo(formatted, code) {
|
|
370
|
+
startupReport.printOut(formatted);
|
|
371
|
+
startupReport.record('info', stripAnsi(formatted), code);
|
|
372
|
+
}
|
|
373
|
+
export function reportSuccess(formatted, code) {
|
|
374
|
+
startupReport.printOut(formatted);
|
|
375
|
+
startupReport.record('success', stripAnsi(formatted), code);
|
|
376
|
+
}
|
|
377
|
+
export function reportDim(formatted, code) {
|
|
378
|
+
startupReport.printOut(formatted);
|
|
379
|
+
startupReport.record('dim', stripAnsi(formatted), code);
|
|
380
|
+
}
|
|
381
|
+
export function reportWarning(formatted, code) {
|
|
382
|
+
startupReport.printOut(formatted);
|
|
383
|
+
startupReport.record('warning', stripAnsi(formatted), code);
|
|
384
|
+
}
|
|
385
|
+
export function reportError(formatted, code) {
|
|
386
|
+
startupReport.printErr(formatted);
|
|
387
|
+
startupReport.record('error', stripAnsi(formatted), code);
|
|
388
|
+
}
|
|
389
|
+
//# sourceMappingURL=startup-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startup-report.js","sourceRoot":"","sources":["../../src/lib/startup-report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* startup-shipper.ts — ships StartupReports to the central observability
|
|
3
|
+
* service (platform v2.58 Phase 2, #4350).
|
|
4
|
+
*
|
|
5
|
+
* Durability model mirrors the local JSONL from Phase 1: every report is first
|
|
6
|
+
* written to a spool dir (~/.mx-agent/logs/startup/spool/<uuid>.json), then a
|
|
7
|
+
* best-effort POST deletes it on success. A failed or token-less POST leaves the
|
|
8
|
+
* file spooled; the Stop-hook `mx-agent sync` flushes the backlog on the next
|
|
9
|
+
* session end.
|
|
10
|
+
*
|
|
11
|
+
* Discipline (identical to startup-report.ts): NOTHING here throws. Shipping is
|
|
12
|
+
* additive telemetry — it must never block, slow, or fail a session start. Every
|
|
13
|
+
* filesystem and network call is wrapped; the worst outcome of any failure is a
|
|
14
|
+
* report that stays spooled for a later retry.
|
|
15
|
+
*
|
|
16
|
+
* - spoolReport(json) — write one report to the spool. Drops files older
|
|
17
|
+
* than 14 days at write time (bounded disk/staleness).
|
|
18
|
+
* - flushSpool(cfg) — POST every spooled report, deleting each on 2xx.
|
|
19
|
+
* Enforces a 50-file cap (drops oldest beyond it) and
|
|
20
|
+
* skips entirely when no token is configured.
|
|
21
|
+
* - postSpoolFile(p, cfg) — POST one spool file with a 3 s timeout.
|
|
22
|
+
*/
|
|
23
|
+
/** Absolute path to the spool directory. Computed at call time so tests that
|
|
24
|
+
* repoint $HOME observe the change (matches startup-report.ts). */
|
|
25
|
+
export declare function spoolDir(): string;
|
|
26
|
+
export interface ShipConfig {
|
|
27
|
+
apiUrl: string;
|
|
28
|
+
apiToken?: string;
|
|
29
|
+
/** Optional dim-note logger (sync passes one; startup passes nothing). */
|
|
30
|
+
log?: (msg: string) => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Write a report JSON string to the spool. Never throws. Returns the file path,
|
|
34
|
+
* or null if the write could not be performed (e.g. a read-only spool dir).
|
|
35
|
+
* Drops files older than 14 days first.
|
|
36
|
+
*/
|
|
37
|
+
export declare function spoolReport(reportJson: string): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* POST one spool file to the observability service. Returns true and deletes the
|
|
40
|
+
* file on a 2xx response. Any non-2xx, network error, or timeout returns false
|
|
41
|
+
* and leaves the file spooled. Never throws.
|
|
42
|
+
*/
|
|
43
|
+
export declare function postSpoolFile(path: string, cfg: ShipConfig): Promise<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Flush the spool: POST every spooled report, deleting each on success.
|
|
46
|
+
*
|
|
47
|
+
* Enforces the 50-file cap first (drops the oldest beyond it, with a dim note if
|
|
48
|
+
* a logger is provided). If no token is configured the whole flush is skipped —
|
|
49
|
+
* files stay spooled so a later run with a token can retry. Never throws.
|
|
50
|
+
*/
|
|
51
|
+
export declare function flushSpool(cfg: ShipConfig): Promise<{
|
|
52
|
+
shipped: number;
|
|
53
|
+
remaining: number;
|
|
54
|
+
}>;
|
|
55
|
+
//# sourceMappingURL=startup-shipper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startup-shipper.d.ts","sourceRoot":"","sources":["../../src/lib/startup-shipper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAcH;oEACoE;AACpE,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY7D;AAwCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BnF;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCjG"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* startup-shipper.ts — ships StartupReports to the central observability
|
|
3
|
+
* service (platform v2.58 Phase 2, #4350).
|
|
4
|
+
*
|
|
5
|
+
* Durability model mirrors the local JSONL from Phase 1: every report is first
|
|
6
|
+
* written to a spool dir (~/.mx-agent/logs/startup/spool/<uuid>.json), then a
|
|
7
|
+
* best-effort POST deletes it on success. A failed or token-less POST leaves the
|
|
8
|
+
* file spooled; the Stop-hook `mx-agent sync` flushes the backlog on the next
|
|
9
|
+
* session end.
|
|
10
|
+
*
|
|
11
|
+
* Discipline (identical to startup-report.ts): NOTHING here throws. Shipping is
|
|
12
|
+
* additive telemetry — it must never block, slow, or fail a session start. Every
|
|
13
|
+
* filesystem and network call is wrapped; the worst outcome of any failure is a
|
|
14
|
+
* report that stays spooled for a later retry.
|
|
15
|
+
*
|
|
16
|
+
* - spoolReport(json) — write one report to the spool. Drops files older
|
|
17
|
+
* than 14 days at write time (bounded disk/staleness).
|
|
18
|
+
* - flushSpool(cfg) — POST every spooled report, deleting each on 2xx.
|
|
19
|
+
* Enforces a 50-file cap (drops oldest beyond it) and
|
|
20
|
+
* skips entirely when no token is configured.
|
|
21
|
+
* - postSpoolFile(p, cfg) — POST one spool file with a 3 s timeout.
|
|
22
|
+
*/
|
|
23
|
+
import { mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync, readFileSync } from 'fs';
|
|
24
|
+
import { homedir } from 'os';
|
|
25
|
+
import { join } from 'path';
|
|
26
|
+
import { randomUUID } from 'crypto';
|
|
27
|
+
/** Drop spooled files older than this at write time (bounded disk / staleness). */
|
|
28
|
+
const MAX_SPOOL_AGE_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
|
|
29
|
+
/** Flush keeps at most this many files; the oldest beyond it are dropped. */
|
|
30
|
+
const MAX_SPOOL_FILES = 50;
|
|
31
|
+
/** Per-POST timeout — a single ship attempt must never stall longer than this. */
|
|
32
|
+
const POST_TIMEOUT_MS = 3000;
|
|
33
|
+
/** Absolute path to the spool directory. Computed at call time so tests that
|
|
34
|
+
* repoint $HOME observe the change (matches startup-report.ts). */
|
|
35
|
+
export function spoolDir() {
|
|
36
|
+
return join(homedir(), '.mx-agent', 'logs', 'startup', 'spool');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Write a report JSON string to the spool. Never throws. Returns the file path,
|
|
40
|
+
* or null if the write could not be performed (e.g. a read-only spool dir).
|
|
41
|
+
* Drops files older than 14 days first.
|
|
42
|
+
*/
|
|
43
|
+
export function spoolReport(reportJson) {
|
|
44
|
+
try {
|
|
45
|
+
const dir = spoolDir();
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
dropAgedFiles(dir);
|
|
48
|
+
const path = join(dir, `${randomUUID()}.json`);
|
|
49
|
+
writeFileSync(path, reportJson);
|
|
50
|
+
return path;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Spooling is best-effort — a failure here is silently tolerated.
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Remove spool files whose mtime is older than the age cap. Never throws. */
|
|
58
|
+
function dropAgedFiles(dir) {
|
|
59
|
+
try {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
for (const f of readdirSync(dir)) {
|
|
62
|
+
if (!f.endsWith('.json'))
|
|
63
|
+
continue;
|
|
64
|
+
const p = join(dir, f);
|
|
65
|
+
try {
|
|
66
|
+
if (now - statSync(p).mtimeMs > MAX_SPOOL_AGE_MS)
|
|
67
|
+
unlinkSync(p);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* individual file error — skip it */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* dir unreadable / absent — nothing to age out */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** List spool files (basenames), oldest-first by mtime. Never throws. */
|
|
79
|
+
function listSpoolFilesOldestFirst(dir) {
|
|
80
|
+
let names;
|
|
81
|
+
try {
|
|
82
|
+
names = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return names
|
|
88
|
+
.map((f) => {
|
|
89
|
+
try {
|
|
90
|
+
return { f, m: statSync(join(dir, f)).mtimeMs };
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { f, m: 0 };
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.sort((a, b) => a.m - b.m)
|
|
97
|
+
.map((x) => x.f);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* POST one spool file to the observability service. Returns true and deletes the
|
|
101
|
+
* file on a 2xx response. Any non-2xx, network error, or timeout returns false
|
|
102
|
+
* and leaves the file spooled. Never throws.
|
|
103
|
+
*/
|
|
104
|
+
export async function postSpoolFile(path, cfg) {
|
|
105
|
+
if (!cfg.apiToken)
|
|
106
|
+
return false; // no token → cannot ship
|
|
107
|
+
let body;
|
|
108
|
+
try {
|
|
109
|
+
body = readFileSync(path, 'utf-8');
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false; // file vanished (concurrent flush) — nothing to do
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(`${cfg.apiUrl}/v1/startup-reports`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
Authorization: `Bearer ${cfg.apiToken}`,
|
|
120
|
+
},
|
|
121
|
+
body,
|
|
122
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
|
|
123
|
+
});
|
|
124
|
+
if (res.ok) {
|
|
125
|
+
try {
|
|
126
|
+
unlinkSync(path);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* already removed — fine */
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return false; // 4xx/5xx — leave spooled for a later retry
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false; // network / timeout / abort — leave spooled
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Flush the spool: POST every spooled report, deleting each on success.
|
|
141
|
+
*
|
|
142
|
+
* Enforces the 50-file cap first (drops the oldest beyond it, with a dim note if
|
|
143
|
+
* a logger is provided). If no token is configured the whole flush is skipped —
|
|
144
|
+
* files stay spooled so a later run with a token can retry. Never throws.
|
|
145
|
+
*/
|
|
146
|
+
export async function flushSpool(cfg) {
|
|
147
|
+
const dir = spoolDir();
|
|
148
|
+
let files = listSpoolFilesOldestFirst(dir);
|
|
149
|
+
// 50-file cap: drop the oldest beyond the cap so disk stays bounded.
|
|
150
|
+
if (files.length > MAX_SPOOL_FILES) {
|
|
151
|
+
const drop = files.slice(0, files.length - MAX_SPOOL_FILES);
|
|
152
|
+
for (const f of drop) {
|
|
153
|
+
try {
|
|
154
|
+
unlinkSync(join(dir, f));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* ignore */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (cfg.log) {
|
|
161
|
+
cfg.log(`startup-report spool exceeded ${MAX_SPOOL_FILES} files — dropped ${drop.length} oldest`);
|
|
162
|
+
}
|
|
163
|
+
files = files.slice(files.length - MAX_SPOOL_FILES);
|
|
164
|
+
}
|
|
165
|
+
// No token → cannot ship anything. Leave the backlog for a later sync.
|
|
166
|
+
if (!cfg.apiToken) {
|
|
167
|
+
return { shipped: 0, remaining: files.length };
|
|
168
|
+
}
|
|
169
|
+
let shipped = 0;
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
if (await postSpoolFile(join(dir, f), cfg))
|
|
172
|
+
shipped++;
|
|
173
|
+
}
|
|
174
|
+
const remaining = listSpoolFilesOldestFirst(dir).length;
|
|
175
|
+
return { shipped, remaining };
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=startup-shipper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startup-shipper.js","sourceRoot":"","sources":["../../src/lib/startup-shipper.ts"],"names":[],"mappings":"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"}
|