runspec-node 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +20 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +219 -0
- package/dist/cli.js.map +1 -1
- package/dist/logging_setup.d.ts +10 -1
- package/dist/logging_setup.d.ts.map +1 -1
- package/dist/logging_setup.js +86 -3
- package/dist/logging_setup.js.map +1 -1
- package/dist/logs.d.ts +85 -0
- package/dist/logs.d.ts.map +1 -0
- package/dist/logs.js +504 -0
- package/dist/logs.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +220 -0
- package/src/logging_setup.ts +88 -4
- package/src/logs.ts +507 -0
- package/tests/test_cli_bin.test.ts +126 -0
- package/tests/test_logs.test.ts +209 -0
- package/tests/test_print_capture.test.ts +82 -0
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as readline from 'readline';
|
|
4
4
|
import { findConfig } from './finder';
|
|
5
|
+
import * as logs from './logs';
|
|
5
6
|
import { loadRaw } from './loader';
|
|
6
7
|
import { inferScript } from './inference';
|
|
7
8
|
import { parse } from './parser';
|
|
@@ -32,6 +33,8 @@ export function main(): void {
|
|
|
32
33
|
const commands: Record<string, (args: string[]) => void | Promise<void>> = {
|
|
33
34
|
init: cmdInit,
|
|
34
35
|
local: cmdLocal,
|
|
36
|
+
bin: cmdBin,
|
|
37
|
+
logs: cmdLogs,
|
|
35
38
|
jump: cmdJump,
|
|
36
39
|
serve: cmdServe,
|
|
37
40
|
};
|
|
@@ -112,6 +115,186 @@ function cmdLocal(args: string[]): void {
|
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
function cmdBin(_args: string[]): void {
|
|
119
|
+
let configPath: string;
|
|
120
|
+
try {
|
|
121
|
+
configPath = findConfig(process.cwd()).configPath;
|
|
122
|
+
} catch {
|
|
123
|
+
console.log("✗ No runspec.toml found. Run 'runspec init' first.");
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const projectRoot = path.dirname(configPath);
|
|
127
|
+
const result = scaffoldBin(projectRoot);
|
|
128
|
+
|
|
129
|
+
console.log(`Wrote ${result.written.length} shim(s) to ${path.join(projectRoot, 'bin')}/:\n`);
|
|
130
|
+
for (const w of result.written) console.log(` ✓ bin/${w.name}${w.target ? ` → ${w.target}` : ''}`);
|
|
131
|
+
if (result.warnings.length) {
|
|
132
|
+
console.log('\nIssues:\n');
|
|
133
|
+
for (const msg of result.warnings) console.log(` ✗ ${msg}`);
|
|
134
|
+
}
|
|
135
|
+
console.log('\nThis folder is now venv-shaped — a controller can invoke bin/<runnable> and');
|
|
136
|
+
console.log('discover/manage it via bin/runspec, from any working directory.');
|
|
137
|
+
if (result.warnings.length) process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate a venv-shaped `bin/` for the runnables declared in `{projectRoot}/
|
|
142
|
+
* runspec.toml`: a `bin/runspec` shim plus one `bin/<runnable>` per runnable.
|
|
143
|
+
* The shims resolve their own location, so the folder stays relocatable and
|
|
144
|
+
* works from any cwd (paired with caller-relative findConfig). Returns a
|
|
145
|
+
* summary; performs the filesystem writes. Exported for testing.
|
|
146
|
+
*/
|
|
147
|
+
export function scaffoldBin(projectRoot: string): { written: Array<{ name: string; target: string }>; warnings: string[] } {
|
|
148
|
+
const raw = loadRaw(configPathOf(projectRoot));
|
|
149
|
+
const runnables = Object.keys(raw.runnables);
|
|
150
|
+
const binMap = readBinMap(projectRoot);
|
|
151
|
+
const binDir = path.join(projectRoot, 'bin');
|
|
152
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
153
|
+
// The venv shape includes a logs/ dir (created lazily on first run anyway).
|
|
154
|
+
fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
|
|
155
|
+
|
|
156
|
+
const written: Array<{ name: string; target: string }> = [];
|
|
157
|
+
const warnings: string[] = [];
|
|
158
|
+
|
|
159
|
+
// bin/runspec — the CLI a controller shells for discovery + log management.
|
|
160
|
+
const cliTarget = resolveRunspecCli(projectRoot);
|
|
161
|
+
if (cliTarget) {
|
|
162
|
+
writeShim(path.join(binDir, 'runspec'), toPosix(path.relative(projectRoot, cliTarget)));
|
|
163
|
+
written.push({ name: 'runspec', target: toPosix(path.relative(projectRoot, cliTarget)) });
|
|
164
|
+
} else {
|
|
165
|
+
warnings.push("runspec-node is not installed — run 'npm install runspec-node', then 'runspec bin' again (bin/runspec skipped)");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// bin/<runnable> — one per declared runnable.
|
|
169
|
+
for (const name of runnables) {
|
|
170
|
+
const script = resolveScript(projectRoot, name, binMap);
|
|
171
|
+
if (!script) {
|
|
172
|
+
warnings.push(`${name}: no script found — add package.json bin["${name}"] or create ./${name}.js`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const rel = toPosix(path.relative(projectRoot, script));
|
|
176
|
+
writeShim(path.join(binDir, name), rel);
|
|
177
|
+
written.push({ name, target: rel });
|
|
178
|
+
}
|
|
179
|
+
return { written, warnings };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const SCRIPT_EXTS = ['.js', '.cjs', '.mjs'];
|
|
183
|
+
|
|
184
|
+
function configPathOf(projectRoot: string): string {
|
|
185
|
+
return path.join(projectRoot, 'runspec.toml');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Resolve a runnable's script: package.json bin first, then ./<name>.{js,cjs,mjs}. */
|
|
189
|
+
export function resolveScript(projectRoot: string, name: string, binMap: Record<string, string>): string | null {
|
|
190
|
+
if (binMap[name]) {
|
|
191
|
+
const p = path.resolve(projectRoot, binMap[name]);
|
|
192
|
+
return fs.existsSync(p) ? p : null;
|
|
193
|
+
}
|
|
194
|
+
for (const ext of SCRIPT_EXTS) {
|
|
195
|
+
const p = path.join(projectRoot, name + ext);
|
|
196
|
+
if (fs.existsSync(p)) return p;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** package.json `bin` as a {name: relPath} map. A string bin maps the package name. */
|
|
202
|
+
export function readBinMap(projectRoot: string): Record<string, string> {
|
|
203
|
+
try {
|
|
204
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'));
|
|
205
|
+
if (typeof pkg.bin === 'string') return pkg.name ? { [pkg.name]: pkg.bin } : {};
|
|
206
|
+
if (pkg.bin && typeof pkg.bin === 'object') return pkg.bin as Record<string, string>;
|
|
207
|
+
} catch {
|
|
208
|
+
// no package.json / unparseable — fall back to the file-name convention
|
|
209
|
+
}
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function resolveRunspecCli(projectRoot: string): string | null {
|
|
214
|
+
const candidates = [
|
|
215
|
+
path.join(projectRoot, 'node_modules', 'runspec-node', 'bin', 'runspec.js'),
|
|
216
|
+
path.join(projectRoot, 'node_modules', '.bin', 'runspec'),
|
|
217
|
+
];
|
|
218
|
+
return candidates.find((c) => fs.existsSync(c)) ?? null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** POSIX shim that resolves the project root relative to itself, then execs node. */
|
|
222
|
+
export function shimContent(relPath: string): string {
|
|
223
|
+
return ['#!/bin/sh', '# Generated by `runspec bin` — do not edit.', 'DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)', `exec node "$DIR/${relPath}" "$@"`, ''].join('\n');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function writeShim(shimPath: string, relPath: string): void {
|
|
227
|
+
fs.writeFileSync(shimPath, shimContent(relPath), 'utf-8');
|
|
228
|
+
fs.chmodSync(shimPath, 0o755);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function toPosix(p: string): string {
|
|
232
|
+
return p.split(path.sep).join('/');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function cmdLogs(args: string[]): void {
|
|
236
|
+
const has = (name: string): boolean => args.includes(name);
|
|
237
|
+
const valueFlags = new Set(['--since', '--user', '--run', '--older-than', '--max-files', '--max-total-size']);
|
|
238
|
+
const val = (name: string): string | undefined => {
|
|
239
|
+
const i = args.indexOf(name);
|
|
240
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
241
|
+
};
|
|
242
|
+
// Positionals are tokens that aren't flags or flag-values.
|
|
243
|
+
const positionals: string[] = [];
|
|
244
|
+
for (let i = 0; i < args.length; i++) {
|
|
245
|
+
const a = args[i];
|
|
246
|
+
if (a.startsWith('-')) {
|
|
247
|
+
if (valueFlags.has(a)) i++; // skip its value
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
positionals.push(a);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// `runspec logs status|prune|compact [runnable]` vs `runspec logs <runnable>`.
|
|
254
|
+
const target = positionals[0];
|
|
255
|
+
const verb = target === 'status' || target === 'prune' || target === 'compact' ? target : 'view';
|
|
256
|
+
const runnable = verb === 'view' ? target : positionals[1] ?? null;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
if (verb === 'view') {
|
|
260
|
+
if (!runnable) {
|
|
261
|
+
console.log('✗ A runnable is required: runspec logs <runnable>');
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
logs.view(runnable, {
|
|
265
|
+
since: val('--since') ? logs.parseDuration(val('--since')!) : null,
|
|
266
|
+
run: val('--run') ?? null,
|
|
267
|
+
user: val('--user') ?? null,
|
|
268
|
+
asJson: has('--json'),
|
|
269
|
+
follow: has('--follow'),
|
|
270
|
+
});
|
|
271
|
+
} else if (verb === 'status') {
|
|
272
|
+
logs.status(runnable, { asJson: has('--json') });
|
|
273
|
+
} else if (verb === 'prune') {
|
|
274
|
+
logs.prune(runnable, {
|
|
275
|
+
olderThan: val('--older-than') ? logs.parseDuration(val('--older-than')!) : null,
|
|
276
|
+
maxFiles: val('--max-files') ? parseInt(val('--max-files')!, 10) : null,
|
|
277
|
+
maxTotalSize: val('--max-total-size') ? logs.parseSize(val('--max-total-size')!) : null,
|
|
278
|
+
dryRun: has('--dry-run'),
|
|
279
|
+
asJson: has('--json'),
|
|
280
|
+
});
|
|
281
|
+
} else if (verb === 'compact') {
|
|
282
|
+
if (!val('--older-than')) {
|
|
283
|
+
console.log('✗ compact requires --older-than (e.g. --older-than 7d)');
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
logs.compact(runnable, logs.parseDuration(val('--older-than')!), {
|
|
287
|
+
gzip: has('--gzip'),
|
|
288
|
+
dryRun: has('--dry-run'),
|
|
289
|
+
asJson: has('--json'),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.log(`✗ ${(err as Error).message}`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
115
298
|
async function cmdJump(args: string[]): Promise<void> {
|
|
116
299
|
const parsed = parse({ scriptName: 'runspec', argv: ['jump', ...args], configPath: _CLI_CONFIG });
|
|
117
300
|
const fmt = String(parsed['format'] ?? 'text');
|
|
@@ -632,6 +815,8 @@ Usage:
|
|
|
632
815
|
Commands:
|
|
633
816
|
init Create runspec.toml and a code stub
|
|
634
817
|
local List runnables and emit tool schemas
|
|
818
|
+
bin Generate a venv-shaped bin/ so a controller can run this folder
|
|
819
|
+
logs View, status, prune, or compact per-invocation audit logs
|
|
635
820
|
jump Execute a runnable on a remote host via SSH
|
|
636
821
|
serve Start the MCP stdio server for local runnables
|
|
637
822
|
|
|
@@ -642,6 +827,8 @@ Examples:
|
|
|
642
827
|
runspec init --example
|
|
643
828
|
runspec local
|
|
644
829
|
runspec local --format mcp
|
|
830
|
+
runspec bin
|
|
831
|
+
runspec logs deploy
|
|
645
832
|
runspec serve`);
|
|
646
833
|
}
|
|
647
834
|
|
|
@@ -672,6 +859,39 @@ Examples:
|
|
|
672
859
|
runspec local --format mcp --script deploy
|
|
673
860
|
runspec local --format json`,
|
|
674
861
|
|
|
862
|
+
bin: `runspec bin — Generate a venv-shaped bin/ for this folder's runnables
|
|
863
|
+
|
|
864
|
+
Writes bin/runspec plus one bin/<runnable> shim per runnable in runspec.toml,
|
|
865
|
+
so a controller (e.g. runspec-console) can invoke bin/<runnable> and discover
|
|
866
|
+
the folder via bin/runspec — from any working directory, including over SSH.
|
|
867
|
+
|
|
868
|
+
Each runnable's script is resolved from package.json "bin", falling back to
|
|
869
|
+
./<runnable>.js | .cjs | .mjs next to runspec.toml. Re-run after adding a
|
|
870
|
+
runnable or 'npm install'. (POSIX shims; Windows support is a follow-up.)
|
|
871
|
+
|
|
872
|
+
Examples:
|
|
873
|
+
npm install runspec-node
|
|
874
|
+
runspec bin`,
|
|
875
|
+
|
|
876
|
+
logs: `runspec logs — View, status, prune, or compact per-invocation audit logs
|
|
877
|
+
|
|
878
|
+
For runnables using [config.logging] store = "per-run" (one file per
|
|
879
|
+
invocation, no in-process rotation). Reads/maintains the project's logs/.
|
|
880
|
+
|
|
881
|
+
View (default):
|
|
882
|
+
runspec logs <runnable> merged, timestamp-sorted stream
|
|
883
|
+
runspec logs <runnable> --follow live tail across invocations
|
|
884
|
+
runspec logs <runnable> --since 1h --user alice --run <id>
|
|
885
|
+
runspec logs <runnable> --json raw JSON lines
|
|
886
|
+
|
|
887
|
+
Status / retention (default to all runnables):
|
|
888
|
+
runspec logs status [runnable] [--json] per-runnable file + disk inventory
|
|
889
|
+
runspec logs compact [runnable] --older-than 7d [--gzip] [--dry-run]
|
|
890
|
+
runspec logs prune [runnable] --older-than 90d | --max-files N | --max-total-size 5GB [--dry-run]
|
|
891
|
+
|
|
892
|
+
prune/compact never touch a single-mode {runnable}.log — only per-run files
|
|
893
|
+
and archives. Add --json to any verb for machine-readable output.`,
|
|
894
|
+
|
|
675
895
|
jump: `runspec jump — Execute a runnable on a remote host via SSH
|
|
676
896
|
|
|
677
897
|
Not yet implemented in the Node package.
|
package/src/logging_setup.ts
CHANGED
|
@@ -22,6 +22,16 @@ const _handlers: Handler[] = [];
|
|
|
22
22
|
let _runId: string | null = null;
|
|
23
23
|
|
|
24
24
|
const RUN_SUMMARY_LOGGER = 'runspec.runsummary';
|
|
25
|
+
// Captured-stdout records are logged under this name (mirrors Python's
|
|
26
|
+
// `runspec.print`). Used only for clarity in the audit file.
|
|
27
|
+
const PRINT_LOGGER = 'runspec.print';
|
|
28
|
+
|
|
29
|
+
// The real `process.stdout.write`, captured before we tee it. The stdout
|
|
30
|
+
// handler writes through this so its own output isn't re-captured (mirrors
|
|
31
|
+
// Python capturing the real sys.stdout reference before the tee swap).
|
|
32
|
+
let _rawStdoutWrite: typeof process.stdout.write | null = null;
|
|
33
|
+
let _stdoutBuf = '';
|
|
34
|
+
let _captureInstalled = false;
|
|
25
35
|
// Uncaught exceptions are emitted on this dedicated logger so the file handler
|
|
26
36
|
// records them while the console handlers drop them by name — console display is
|
|
27
37
|
// handled explicitly in _handleUncaught (debug-gated).
|
|
@@ -114,6 +124,10 @@ interface LogRecord {
|
|
|
114
124
|
error?: Error;
|
|
115
125
|
extra?: Record<string, unknown>;
|
|
116
126
|
excStructured?: Record<string, unknown>;
|
|
127
|
+
// Set on records synthesised from captured stdout (print-capture). Console +
|
|
128
|
+
// counter handlers skip these (the real stdout write already happened, and
|
|
129
|
+
// printed lines aren't log "events"); only file handlers persist them.
|
|
130
|
+
fromPrint?: boolean;
|
|
117
131
|
}
|
|
118
132
|
|
|
119
133
|
interface Handler {
|
|
@@ -220,8 +234,11 @@ class StdoutHandler implements Handler {
|
|
|
220
234
|
emit(record: LogRecord): void {
|
|
221
235
|
if (record.levelNum >= 30) return; // WARNING+ belongs on stderr
|
|
222
236
|
if (record.loggerName === RUN_SUMMARY_LOGGER || record.loggerName === EXCEPTION_LOGGER) return;
|
|
237
|
+
if (record.fromPrint) return; // the real stdout write already emitted this
|
|
223
238
|
try {
|
|
224
|
-
|
|
239
|
+
// Write through the *raw* stream so this output isn't re-captured by the
|
|
240
|
+
// print-capture tee (would double it in the audit file).
|
|
241
|
+
(_rawStdoutWrite ?? process.stdout.write.bind(process.stdout))(formatConsole(record, this.showTracebacks) + '\n');
|
|
225
242
|
} catch {
|
|
226
243
|
// never disrupt
|
|
227
244
|
}
|
|
@@ -262,8 +279,9 @@ class RunSummaryCounter implements Handler {
|
|
|
262
279
|
};
|
|
263
280
|
|
|
264
281
|
emit(record: LogRecord): void {
|
|
265
|
-
// Don't count runspec's own bookkeeping records (summary + uncaught-exception)
|
|
266
|
-
|
|
282
|
+
// Don't count runspec's own bookkeeping records (summary + uncaught-exception)
|
|
283
|
+
// or captured stdout lines — only genuine logger events.
|
|
284
|
+
if (record.loggerName === RUN_SUMMARY_LOGGER || record.loggerName === EXCEPTION_LOGGER || record.fromPrint) return;
|
|
267
285
|
const label = LEVEL_LABEL[record.levelNum];
|
|
268
286
|
if (label && label in this.counts) {
|
|
269
287
|
this.counts[label]++;
|
|
@@ -617,6 +635,61 @@ export function _handleUncaught(err: Error): void {
|
|
|
617
635
|
}
|
|
618
636
|
}
|
|
619
637
|
|
|
638
|
+
// ── stdout capture (print → audit log) ─────────────────────────────────────────
|
|
639
|
+
|
|
640
|
+
/** Emit one captured stdout line as a file-only record (skipped by console + counter). */
|
|
641
|
+
function emitPrintLine(line: string): void {
|
|
642
|
+
if (_handlers.length === 0) return;
|
|
643
|
+
const record: LogRecord = { ts: new Date(), levelNum: 20, loggerName: PRINT_LOGGER, message: redact(line), fromPrint: true };
|
|
644
|
+
for (const h of _handlers) {
|
|
645
|
+
try {
|
|
646
|
+
if (record.levelNum >= h.level) h.emit(record);
|
|
647
|
+
} catch {
|
|
648
|
+
// never disrupt
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Tee `process.stdout.write`: output still reaches the real stream unchanged
|
|
655
|
+
* (so pipes / `runspec serve` capture are untouched), and each complete line is
|
|
656
|
+
* also written to the audit file as a `fromPrint` record. Mirrors Python's
|
|
657
|
+
* `_StdoutTee`, so a runnable's `console.log` output is preserved in the
|
|
658
|
+
* per-invocation log — not just the run summary.
|
|
659
|
+
*/
|
|
660
|
+
function installStdoutCapture(): void {
|
|
661
|
+
if (_captureInstalled) return;
|
|
662
|
+
_captureInstalled = true;
|
|
663
|
+
_rawStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
664
|
+
const patched = function (chunk: unknown, encoding?: unknown, cb?: unknown): boolean {
|
|
665
|
+
const result = (_rawStdoutWrite as (...a: unknown[]) => boolean)(chunk, encoding, cb);
|
|
666
|
+
try {
|
|
667
|
+
const s = typeof chunk === 'string' ? chunk : Buffer.isBuffer(chunk) ? chunk.toString(typeof encoding === 'string' ? (encoding as BufferEncoding) : 'utf8') : '';
|
|
668
|
+
if (s) {
|
|
669
|
+
_stdoutBuf += s;
|
|
670
|
+
let idx: number;
|
|
671
|
+
while ((idx = _stdoutBuf.indexOf('\n')) !== -1) {
|
|
672
|
+
const line = _stdoutBuf.slice(0, idx);
|
|
673
|
+
_stdoutBuf = _stdoutBuf.slice(idx + 1);
|
|
674
|
+
if (line) emitPrintLine(line);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
} catch {
|
|
678
|
+
// never disrupt
|
|
679
|
+
}
|
|
680
|
+
return result;
|
|
681
|
+
};
|
|
682
|
+
process.stdout.write = patched as typeof process.stdout.write;
|
|
683
|
+
// Flush a trailing partial line at exit. Registered before installExitHooks so
|
|
684
|
+
// it runs before the run-summary record (FIFO exit handlers).
|
|
685
|
+
process.on('exit', () => {
|
|
686
|
+
if (_stdoutBuf) {
|
|
687
|
+
emitPrintLine(_stdoutBuf);
|
|
688
|
+
_stdoutBuf = '';
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
620
693
|
function installExitHooks(): void {
|
|
621
694
|
if (_exitHooksInstalled) return;
|
|
622
695
|
_exitHooksInstalled = true;
|
|
@@ -740,6 +813,10 @@ export function configureLogging(opts: ConfigureLoggingOptions): void {
|
|
|
740
813
|
};
|
|
741
814
|
}
|
|
742
815
|
|
|
816
|
+
// Tee stdout into the audit log so printed output (not just the run summary)
|
|
817
|
+
// is preserved. Installed before the exit hooks so its flush runs first.
|
|
818
|
+
installStdoutCapture();
|
|
819
|
+
|
|
743
820
|
// Uncaught-exception handling is always installed (independent of the summary
|
|
744
821
|
// toggle) so the structured exception record reaches the audit file even when
|
|
745
822
|
// summary is off. The exit hook only flushes a summary when _summaryState is set.
|
|
@@ -757,8 +834,15 @@ export function _resetForTest(): void {
|
|
|
757
834
|
_loggers.clear();
|
|
758
835
|
_handlers.length = 0;
|
|
759
836
|
_summaryState = null;
|
|
837
|
+
// Un-tee stdout so the patch doesn't leak across tests.
|
|
838
|
+
if (_rawStdoutWrite) {
|
|
839
|
+
process.stdout.write = _rawStdoutWrite;
|
|
840
|
+
_rawStdoutWrite = null;
|
|
841
|
+
}
|
|
842
|
+
_stdoutBuf = '';
|
|
843
|
+
_captureInstalled = false;
|
|
760
844
|
// Note: process event listeners installed by installExitHooks() stay —
|
|
761
845
|
// they no-op when _summaryState is null, which is the test-time state.
|
|
762
846
|
}
|
|
763
847
|
|
|
764
|
-
export { _periodForDate, RUN_SUMMARY_LOGGER, EXCEPTION_LOGGER, buildExcStructured, formatCompactTrace };
|
|
848
|
+
export { _periodForDate, RUN_SUMMARY_LOGGER, EXCEPTION_LOGGER, buildExcStructured, formatCompactTrace, findProjectRoot };
|