agent-gauntlet 0.10.0 → 0.11.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 (71) hide show
  1. package/README.md +25 -23
  2. package/dist/index.js +9226 -0
  3. package/dist/index.js.map +65 -0
  4. package/dist/scripts/status.js +280 -0
  5. package/dist/scripts/status.js.map +10 -0
  6. package/package.json +22 -8
  7. package/src/built-in-reviews/code-quality.md +0 -25
  8. package/src/built-in-reviews/index.ts +0 -28
  9. package/src/bun-plugins.d.ts +0 -4
  10. package/src/cli-adapters/claude.ts +0 -327
  11. package/src/cli-adapters/codex.ts +0 -290
  12. package/src/cli-adapters/cursor.ts +0 -128
  13. package/src/cli-adapters/gemini.ts +0 -510
  14. package/src/cli-adapters/github-copilot.ts +0 -141
  15. package/src/cli-adapters/index.ts +0 -250
  16. package/src/cli-adapters/thinking-budget.ts +0 -23
  17. package/src/commands/check.ts +0 -311
  18. package/src/commands/ci/index.ts +0 -15
  19. package/src/commands/ci/init.ts +0 -96
  20. package/src/commands/ci/list-jobs.ts +0 -90
  21. package/src/commands/clean.ts +0 -54
  22. package/src/commands/detect.ts +0 -173
  23. package/src/commands/health.ts +0 -169
  24. package/src/commands/help.ts +0 -34
  25. package/src/commands/index.ts +0 -13
  26. package/src/commands/init.ts +0 -1878
  27. package/src/commands/list.ts +0 -33
  28. package/src/commands/review.ts +0 -311
  29. package/src/commands/run.ts +0 -29
  30. package/src/commands/shared.ts +0 -267
  31. package/src/commands/stop-hook.ts +0 -567
  32. package/src/commands/validate.ts +0 -20
  33. package/src/commands/wait-ci.ts +0 -518
  34. package/src/config/ci-loader.ts +0 -33
  35. package/src/config/ci-schema.ts +0 -28
  36. package/src/config/global.ts +0 -87
  37. package/src/config/loader.ts +0 -301
  38. package/src/config/schema.ts +0 -165
  39. package/src/config/stop-hook-config.ts +0 -130
  40. package/src/config/types.ts +0 -65
  41. package/src/config/validator.ts +0 -592
  42. package/src/core/change-detector.ts +0 -137
  43. package/src/core/diff-stats.ts +0 -442
  44. package/src/core/entry-point.ts +0 -190
  45. package/src/core/job.ts +0 -96
  46. package/src/core/run-executor.ts +0 -621
  47. package/src/core/runner.ts +0 -290
  48. package/src/gates/check.ts +0 -118
  49. package/src/gates/resolve-check-command.ts +0 -21
  50. package/src/gates/result.ts +0 -54
  51. package/src/gates/review.ts +0 -1333
  52. package/src/hooks/adapters/claude-stop-hook.ts +0 -99
  53. package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
  54. package/src/hooks/adapters/types.ts +0 -94
  55. package/src/hooks/stop-hook-handler.ts +0 -748
  56. package/src/index.ts +0 -47
  57. package/src/output/app-logger.ts +0 -214
  58. package/src/output/console-log.ts +0 -168
  59. package/src/output/console.ts +0 -359
  60. package/src/output/logger.ts +0 -126
  61. package/src/output/sinks/console-sink.ts +0 -59
  62. package/src/output/sinks/file-sink.ts +0 -110
  63. package/src/scripts/status.ts +0 -433
  64. package/src/templates/workflow.yml +0 -79
  65. package/src/types/gauntlet-status.ts +0 -79
  66. package/src/utils/debug-log.ts +0 -392
  67. package/src/utils/diff-parser.ts +0 -103
  68. package/src/utils/execution-state.ts +0 -472
  69. package/src/utils/log-parser.ts +0 -696
  70. package/src/utils/sanitizer.ts +0 -3
  71. package/src/utils/session-ref.ts +0 -91
@@ -1,359 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import chalk from "chalk";
3
- import type { Job } from "../core/job.js";
4
- import type { GateResult } from "../gates/result.js";
5
- import { reconstructHistory } from "../utils/log-parser.js";
6
-
7
- export class ConsoleReporter {
8
- onJobStart(job: Job) {
9
- console.error(chalk.blue(`[START] ${job.id}`));
10
- }
11
-
12
- onJobComplete(job: Job, result: GateResult) {
13
- const duration = `${(result.duration / 1000).toFixed(2)}s`;
14
-
15
- const message = result.message ?? "";
16
-
17
- if (result.subResults && result.subResults.length > 0) {
18
- // Print split results
19
-
20
- for (const sub of result.subResults) {
21
- const statusColor =
22
- sub.status === "pass"
23
- ? chalk.green
24
- : sub.status === "fail"
25
- ? chalk.red
26
- : chalk.magenta;
27
-
28
- const label =
29
- sub.status === "pass"
30
- ? "PASS"
31
- : sub.status === "fail"
32
- ? "FAIL"
33
- : "ERROR";
34
-
35
- let logInfo = "";
36
-
37
- if (sub.status !== "pass" && sub.logPath) {
38
- // Prefer JSON if it exists for reviews
39
-
40
- const displayLog = sub.logPath;
41
-
42
- const logPrefix = displayLog.endsWith(".json") ? "Review:" : "Log:";
43
-
44
- logInfo = `\n ${logPrefix} ${displayLog}`;
45
- }
46
-
47
- console.error(
48
- statusColor(
49
- `[${label}] ${job.id} ${chalk.dim(sub.nameSuffix)} (${duration}) - ${sub.message}${logInfo}`,
50
- ),
51
- );
52
- }
53
- } else {
54
- // Standard single result
55
- let logInfo = "";
56
- if (result.status !== "pass") {
57
- // Try to find a relevant log path
58
- const logPath = result.logPath || result.logPaths?.[0];
59
- if (logPath) {
60
- logInfo = `\n Log: ${logPath}`;
61
- }
62
- }
63
-
64
- if (result.status === "pass") {
65
- console.error(chalk.green(`[PASS] ${job.id} (${duration})`));
66
- } else if (result.status === "fail") {
67
- console.error(
68
- chalk.red(`[FAIL] ${job.id} (${duration}) - ${message}${logInfo}`),
69
- );
70
- } else {
71
- console.error(
72
- chalk.magenta(
73
- `[ERROR] ${job.id} (${duration}) - ${message}${logInfo}`,
74
- ),
75
- );
76
- }
77
- }
78
- }
79
-
80
- async printSummary(
81
- results: GateResult[],
82
- logDir?: string,
83
- statusOverride?: string,
84
- ) {
85
- console.error(
86
- `\n${chalk.bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`,
87
- );
88
- console.error(chalk.bold("RESULTS SUMMARY"));
89
- console.error(chalk.bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
90
-
91
- if (logDir) {
92
- try {
93
- const history = await reconstructHistory(logDir);
94
- for (const iter of history) {
95
- if (iter.fixed.length === 0 && iter.skipped.length === 0) continue;
96
-
97
- console.error(`\nIteration ${iter.iteration}:`);
98
- for (const f of iter.fixed) {
99
- const label = f.adapter ? `${f.jobId} (${f.adapter})` : f.jobId;
100
- console.error(chalk.green(` ✓ Fixed: ${label} - ${f.details}`));
101
- }
102
- for (const s of iter.skipped) {
103
- const label = s.adapter ? `${s.jobId} (${s.adapter})` : s.jobId;
104
- console.error(
105
- chalk.yellow(
106
- ` ⊘ Skipped: ${label} - ${s.file}:${s.line} ${s.issue}`,
107
- ),
108
- );
109
- if (s.result) {
110
- console.error(chalk.dim(` Reason: ${s.result}`));
111
- }
112
- }
113
- }
114
-
115
- const totalFixed = history.reduce(
116
- (sum, iter) => sum + iter.fixed.length,
117
- 0,
118
- );
119
- const totalSkipped = history.reduce(
120
- (sum, iter) => sum + iter.skipped.length,
121
- 0,
122
- );
123
-
124
- let totalFailed = 0;
125
- for (const res of results) {
126
- if (res.subResults && res.subResults.length > 0) {
127
- for (const sub of res.subResults) {
128
- if (sub.status === "fail" || sub.status === "error") {
129
- totalFailed += sub.errorCount ?? 1;
130
- }
131
- }
132
- } else if (res.status === "fail" || res.status === "error") {
133
- totalFailed += res.errorCount ?? 1;
134
- }
135
- }
136
-
137
- console.error(
138
- `\n${chalk.bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`,
139
- );
140
- const iterationsText =
141
- history.length > 1 ? ` after ${history.length} iterations` : "";
142
- console.error(
143
- `Total: ${totalFixed} fixed, ${totalSkipped} skipped, ${totalFailed} failed${iterationsText}`,
144
- );
145
- } catch (err) {
146
- console.warn(
147
- chalk.yellow(`Warning: Failed to reconstruct history: ${err}`),
148
- );
149
- }
150
- }
151
-
152
- const failed = results.filter((r) => r.status === "fail");
153
- const errored = results.filter((r) => r.status === "error");
154
- const anySkipped = results.some((r) => r.skipped && r.skipped.length > 0);
155
-
156
- let overallStatus = "Passed";
157
- let statusColor = chalk.green;
158
-
159
- if (statusOverride) {
160
- overallStatus = statusOverride;
161
- statusColor = chalk.red;
162
- } else if (errored.length > 0) {
163
- overallStatus = "Error";
164
- statusColor = chalk.magenta;
165
- } else if (failed.length > 0) {
166
- overallStatus = "Failed";
167
- statusColor = chalk.red;
168
- } else if (anySkipped) {
169
- overallStatus = "Passed with warnings";
170
- statusColor = chalk.yellow;
171
- }
172
-
173
- console.error(statusColor(`Status: ${overallStatus}`));
174
- console.error(
175
- chalk.bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"),
176
- );
177
- }
178
-
179
- /** @internal Public for testing */
180
- async extractFailureDetails(result: GateResult): Promise<string[]> {
181
- const logPaths =
182
- result.logPaths || (result.logPath ? [result.logPath] : []);
183
-
184
- if (logPaths.length === 0) {
185
- return [result.message ?? "Unknown error"];
186
- }
187
-
188
- const allDetails: string[] = [];
189
- for (const logPath of logPaths) {
190
- try {
191
- const logContent = await fs.readFile(logPath, "utf-8");
192
- const details = this.parseLogContent(logContent, result.jobId);
193
- allDetails.push(...details);
194
- } catch (_error: unknown) {
195
- allDetails.push(`(Could not read log file: ${logPath})`);
196
- }
197
- }
198
-
199
- return allDetails.length > 0
200
- ? allDetails
201
- : [result.message ?? "Unknown error"];
202
- }
203
-
204
- private parseLogContent(logContent: string, jobId: string): string[] {
205
- const _lines = logContent.split("\n");
206
- const details: string[] = [];
207
-
208
- // Check if this is a review log
209
- if (jobId.startsWith("review:")) {
210
- // Look for parsed violations section (formatted output)
211
- // Use regex to be flexible about adapter name in parentheses
212
- // Matches: "--- Parsed Result ---" or "--- Parsed Result (adapter) ---"
213
- const parsedResultRegex = /---\s*Parsed Result(?:\s+\(([^)]+)\))?\s*---/;
214
- const match = logContent.match(parsedResultRegex);
215
-
216
- if (match && match.index !== undefined) {
217
- const violationsStart = match.index;
218
- const violationsSection = logContent.substring(violationsStart);
219
- const sectionLines = violationsSection.split("\n");
220
-
221
- for (let i = 0; i < sectionLines.length; i++) {
222
- const line = sectionLines[i]!;
223
- // Match numbered violation lines: "1. file:line - issue" (line can be a number or '?')
224
- const violationMatch = line.match(
225
- /^\d+\.\s+(.+?):(\d+|\?)\s+-\s+(.+)$/,
226
- );
227
- if (violationMatch) {
228
- const file = violationMatch[1];
229
- const lineNum = violationMatch[2];
230
- const issue = violationMatch[3];
231
- details.push(
232
- ` ${chalk.cyan(file)}:${chalk.yellow(lineNum)} - ${issue}`,
233
- );
234
-
235
- // Check next line for "Fix:" suggestion
236
- if (i + 1 < sectionLines.length) {
237
- const nextLine = sectionLines[i + 1]!.trim();
238
- if (nextLine.startsWith("Fix:")) {
239
- const fix = nextLine.substring(4).trim();
240
- details.push(` ${chalk.dim("Fix:")} ${fix}`);
241
- i++; // Skip the fix line
242
- }
243
- }
244
- }
245
- }
246
- }
247
-
248
- // If no parsed violations, look for JSON violations (handles both minified and pretty-printed)
249
- if (details.length === 0) {
250
- // Find the first '{' and last '}' to extract JSON object
251
- const jsonStart = logContent.indexOf("{");
252
- const jsonEnd = logContent.lastIndexOf("}");
253
- if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
254
- try {
255
- const jsonStr = logContent.substring(jsonStart, jsonEnd + 1);
256
- const json = JSON.parse(jsonStr);
257
- if (
258
- json.status === "fail" &&
259
- json.violations &&
260
- Array.isArray(json.violations)
261
- ) {
262
- json.violations.forEach(
263
- (v: {
264
- file?: string;
265
- line?: number | string;
266
- issue?: string;
267
- fix?: string;
268
- }) => {
269
- const file = v.file || "unknown";
270
- const line = v.line || "?";
271
- const issue = v.issue || "Unknown issue";
272
- details.push(
273
- ` ${chalk.cyan(file)}:${chalk.yellow(line)} - ${issue}`,
274
- );
275
- if (v.fix) {
276
- details.push(` ${chalk.dim("Fix:")} ${v.fix}`);
277
- }
278
- },
279
- );
280
- }
281
- } catch {
282
- // JSON parse failed, fall through to other parsing
283
- }
284
- }
285
- }
286
-
287
- // If still no details, look for error messages
288
- if (details.length === 0) {
289
- // Try to find the actual error message (first non-empty line after "Error:")
290
- const errorIndex = logContent.indexOf("Error:");
291
- if (errorIndex !== -1) {
292
- const afterError = logContent.substring(errorIndex + 6).trim();
293
- const firstErrorLine = afterError.split("\n")[0]!.trim();
294
- if (
295
- firstErrorLine &&
296
- !firstErrorLine.startsWith("Usage:") &&
297
- !firstErrorLine.startsWith("Commands:")
298
- ) {
299
- details.push(` ${firstErrorLine}`);
300
- }
301
- }
302
-
303
- // Also check for "Result: error" lines
304
- if (details.length === 0) {
305
- const resultMatch = logContent.match(
306
- /Result:\s*error(?:\s*-\s*(.+?))?(?:\n|$)/,
307
- );
308
- if (resultMatch?.[1]) {
309
- details.push(` ${resultMatch[1]}`);
310
- }
311
- }
312
- }
313
- } else {
314
- // This is a check log
315
- // Look for STDERR section
316
- const stderrStart = logContent.indexOf("STDERR:");
317
- if (stderrStart !== -1) {
318
- const stderrSection = logContent.substring(stderrStart + 7).trim();
319
- const stderrLines = stderrSection.split("\n").filter((line) => {
320
- // Skip empty lines and command output markers
321
- return (
322
- line.trim() &&
323
- !line.includes("STDOUT:") &&
324
- !line.includes("Command failed:") &&
325
- !line.includes("Result:")
326
- );
327
- });
328
- if (stderrLines.length > 0) {
329
- details.push(
330
- ...stderrLines.slice(0, 10).map((line) => ` ${line.trim()}`),
331
- );
332
- }
333
- }
334
-
335
- // If no STDERR, look for error messages
336
- if (details.length === 0) {
337
- const errorMatch = logContent.match(/Command failed:\s*(.+?)(?:\n|$)/);
338
- if (errorMatch) {
339
- details.push(` ${errorMatch[1]}`);
340
- } else {
341
- // Look for any line with "Result: fail" or "Result: error"
342
- const resultMatch = logContent.match(
343
- /Result:\s*(fail|error)\s*-\s*(.+?)(?:\n|$)/,
344
- );
345
- if (resultMatch) {
346
- details.push(` ${resultMatch[2]}`);
347
- }
348
- }
349
- }
350
- }
351
-
352
- // If we still have no details, use the message from the result
353
- if (details.length === 0) {
354
- details.push(" (See log file for details)");
355
- }
356
-
357
- return details;
358
- }
359
- }
@@ -1,126 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { sanitizeJobId } from "../utils/sanitizer.js";
4
-
5
- function formatTimestamp(): string {
6
- return new Date().toISOString();
7
- }
8
-
9
- /**
10
- * Compute the global run number for the log directory.
11
- * Finds the highest run-number suffix across ALL log files and returns max+1.
12
- */
13
- async function computeGlobalRunNumber(logDir: string): Promise<number> {
14
- try {
15
- const files = await fs.readdir(logDir);
16
- let max = 0;
17
- for (const file of files) {
18
- if (!file.endsWith(".log") && !file.endsWith(".json")) continue;
19
- // Pattern: <anything>.<number>.(log|json)
20
- const m = file.match(/\.(\d+)\.(log|json)$/);
21
- if (m?.[1]) {
22
- const n = parseInt(m[1], 10);
23
- if (n > max) max = n;
24
- }
25
- }
26
- return max + 1;
27
- } catch {
28
- return 1;
29
- }
30
- }
31
-
32
- export class Logger {
33
- private initializedFiles: Set<string> = new Set();
34
- private globalRunNumber: number | null = null;
35
-
36
- constructor(private logDir: string) {}
37
-
38
- async init() {
39
- await fs.mkdir(this.logDir, { recursive: true });
40
- this.globalRunNumber = await computeGlobalRunNumber(this.logDir);
41
- }
42
-
43
- async close() {
44
- // No-op - using append mode
45
- }
46
-
47
- getRunNumber(): number {
48
- return this.globalRunNumber ?? 1;
49
- }
50
-
51
- async getLogPath(
52
- jobId: string,
53
- adapterName?: string,
54
- reviewIndex?: number,
55
- ): Promise<string> {
56
- const safeName = sanitizeJobId(jobId);
57
- const runNum = this.globalRunNumber ?? 1;
58
-
59
- let filename: string;
60
- if (adapterName && reviewIndex !== undefined) {
61
- // Review gate with index: <jobId>_<adapter>@<index>.<runNum>.log
62
- filename = `${safeName}_${adapterName}@${reviewIndex}.${runNum}.log`;
63
- } else if (adapterName) {
64
- // Review gate without explicit index (backwards compat for single review)
65
- filename = `${safeName}_${adapterName}@1.${runNum}.log`;
66
- } else {
67
- // Check gate: <jobId>.<runNum>.log
68
- filename = `${safeName}.${runNum}.log`;
69
- }
70
-
71
- return path.join(this.logDir, filename);
72
- }
73
-
74
- private async initFile(logPath: string): Promise<void> {
75
- if (this.initializedFiles.has(logPath)) {
76
- return;
77
- }
78
- this.initializedFiles.add(logPath);
79
- await fs.writeFile(logPath, "");
80
- }
81
-
82
- async createJobLogger(
83
- jobId: string,
84
- ): Promise<(text: string) => Promise<void>> {
85
- const logPath = await this.getLogPath(jobId);
86
- await this.initFile(logPath);
87
-
88
- return async (text: string) => {
89
- const timestamp = formatTimestamp();
90
- const lines = text.split("\n");
91
- if (lines.length > 0) {
92
- lines[0] = `[${timestamp}] ${lines[0]}`;
93
- }
94
- await fs.appendFile(
95
- logPath,
96
- lines.join("\n") + (text.endsWith("\n") ? "" : "\n"),
97
- );
98
- };
99
- }
100
-
101
- createLoggerFactory(
102
- jobId: string,
103
- ): (
104
- adapterName?: string,
105
- reviewIndex?: number,
106
- ) => Promise<{ logger: (text: string) => Promise<void>; logPath: string }> {
107
- return async (adapterName?: string, reviewIndex?: number) => {
108
- const logPath = await this.getLogPath(jobId, adapterName, reviewIndex);
109
- await this.initFile(logPath);
110
-
111
- const logger = async (text: string) => {
112
- const timestamp = formatTimestamp();
113
- const lines = text.split("\n");
114
- if (lines.length > 0) {
115
- lines[0] = `[${timestamp}] ${lines[0]}`;
116
- }
117
- await fs.appendFile(
118
- logPath,
119
- lines.join("\n") + (text.endsWith("\n") ? "" : "\n"),
120
- );
121
- };
122
-
123
- return { logger, logPath };
124
- };
125
- }
126
- }
@@ -1,59 +0,0 @@
1
- import type { LogRecord, Sink } from "@logtape/logtape";
2
- import chalk from "chalk";
3
-
4
- /**
5
- * Safely serialize a value, handling circular references.
6
- */
7
- function safeStringify(value: unknown): string {
8
- try {
9
- return JSON.stringify(value);
10
- } catch {
11
- return "[Unserializable]";
12
- }
13
- }
14
-
15
- /**
16
- * Format a log record with chalk colors for console output.
17
- * Level prefixes: [DEBUG] dim, [INFO] blue, [WARN] yellow, [ERROR] red
18
- */
19
- function formatLogRecord(record: LogRecord): string {
20
- const level = record.level.toUpperCase();
21
- const category = record.category.join(".");
22
- const message = record.message
23
- .map((part) => (typeof part === "string" ? part : safeStringify(part)))
24
- .join("");
25
-
26
- let levelStr: string;
27
- switch (record.level) {
28
- case "debug":
29
- levelStr = chalk.dim(`[${level}]`);
30
- break;
31
- case "info":
32
- levelStr = chalk.blue(`[${level}]`);
33
- break;
34
- case "warning":
35
- levelStr = chalk.yellow(`[${level}]`);
36
- break;
37
- case "error":
38
- case "fatal":
39
- levelStr = chalk.red(`[${level}]`);
40
- break;
41
- default:
42
- levelStr = `[${level}]`;
43
- }
44
-
45
- const categoryStr = category ? chalk.dim(`[${category}]`) : "";
46
-
47
- return `${levelStr}${categoryStr} ${message}`;
48
- }
49
-
50
- /**
51
- * Create a console sink that outputs to stderr with chalk formatting.
52
- * Uses stderr to keep stdout clean for JSON protocol responses (stop-hook).
53
- */
54
- export function createConsoleSink(): Sink {
55
- return (record: LogRecord) => {
56
- const formatted = formatLogRecord(record);
57
- console.error(formatted);
58
- };
59
- }
@@ -1,110 +0,0 @@
1
- import fs from "node:fs";
2
- import type { LogRecord, Sink } from "@logtape/logtape";
3
-
4
- // biome-ignore lint/suspicious/noControlCharactersInRegex: Required for ANSI escape code stripping
5
- const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
6
-
7
- /**
8
- * Strip ANSI escape codes from text.
9
- */
10
- function stripAnsi(text: string): string {
11
- return text.replace(ANSI_REGEX, "");
12
- }
13
-
14
- /**
15
- * Format an ISO timestamp for log output.
16
- */
17
- function formatTimestamp(timestamp: number): string {
18
- return new Date(timestamp).toISOString();
19
- }
20
-
21
- /**
22
- * Safely serialize a value, handling circular references.
23
- */
24
- function safeStringify(value: unknown): string {
25
- try {
26
- return JSON.stringify(value);
27
- } catch {
28
- return "[Unserializable]";
29
- }
30
- }
31
-
32
- /**
33
- * Format a log record for file output with plain text (no ANSI).
34
- * Format: [ISO_TIMESTAMP] LEVEL [category] message
35
- */
36
- function formatLogRecord(record: LogRecord): string {
37
- const timestamp = formatTimestamp(record.timestamp);
38
- const level = record.level.toUpperCase().padEnd(7);
39
- const category = record.category.join(".");
40
- const message = record.message
41
- .map((part) => (typeof part === "string" ? part : safeStringify(part)))
42
- .join("");
43
-
44
- // Strip any ANSI codes that might be in the message
45
- const cleanMessage = stripAnsi(message);
46
-
47
- const categoryStr = category ? `[${category}] ` : "";
48
- return `[${timestamp}] ${level} ${categoryStr}${cleanMessage}\n`;
49
- }
50
-
51
- /**
52
- * Create a file sink that appends to the specified log file.
53
- * Uses synchronous writes to ensure log ordering.
54
- *
55
- * @param logPath - Path to the log file (e.g., console.N.log)
56
- */
57
- export function createFileSink(logPath: string): Sink {
58
- // Ensure the file exists (create if not)
59
- const fd = fs.openSync(
60
- logPath,
61
- fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND,
62
- );
63
-
64
- return (record: LogRecord) => {
65
- const formatted = formatLogRecord(record);
66
- try {
67
- fs.writeSync(fd, formatted);
68
- } catch {
69
- // Suppress write errors to avoid crashing the application
70
- }
71
- };
72
- }
73
-
74
- /**
75
- * Create a closeable file sink that can be cleaned up.
76
- * Returns both the sink and a close function.
77
- */
78
- export function createCloseableFileSink(logPath: string): {
79
- sink: Sink;
80
- close: () => void;
81
- } {
82
- const fd = fs.openSync(
83
- logPath,
84
- fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND,
85
- );
86
- let isClosed = false;
87
-
88
- const sink: Sink = (record: LogRecord) => {
89
- if (isClosed) return;
90
- const formatted = formatLogRecord(record);
91
- try {
92
- fs.writeSync(fd, formatted);
93
- } catch {
94
- // Suppress write errors
95
- }
96
- };
97
-
98
- const close = () => {
99
- if (!isClosed) {
100
- isClosed = true;
101
- try {
102
- fs.closeSync(fd);
103
- } catch {
104
- // Ignore close errors
105
- }
106
- }
107
- };
108
-
109
- return { sink, close };
110
- }