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,33 +0,0 @@
1
- import chalk from "chalk";
2
- import type { Command } from "commander";
3
- import { loadConfig } from "../config/loader.js";
4
-
5
- export function registerListCommand(program: Command): void {
6
- program
7
- .command("list")
8
- .description("List configured gates")
9
- .action(async () => {
10
- try {
11
- const config = await loadConfig();
12
- console.log(chalk.bold("Check Gates:"));
13
- Object.values(config.checks).forEach((c) => {
14
- console.log(` - ${c.name}`);
15
- });
16
-
17
- console.log(chalk.bold("\nReview Gates:"));
18
- Object.values(config.reviews).forEach((r) => {
19
- console.log(` - ${r.name} (Tools: ${r.cli_preference?.join(", ")})`);
20
- });
21
-
22
- console.log(chalk.bold("\nEntry Points:"));
23
- config.project.entry_points.forEach((ep) => {
24
- console.log(` - ${ep.path}`);
25
- if (ep.checks) console.log(` Checks: ${ep.checks.join(", ")}`);
26
- if (ep.reviews) console.log(` Reviews: ${ep.reviews.join(", ")}`);
27
- });
28
- } catch (error: unknown) {
29
- const err = error as { message?: string };
30
- console.error(chalk.red("Error:"), err.message);
31
- }
32
- });
33
- }
@@ -1,311 +0,0 @@
1
- import chalk from "chalk";
2
- import type { Command } from "commander";
3
- import { loadGlobalConfig } from "../config/global.js";
4
- import { loadConfig } from "../config/loader.js";
5
- import { ChangeDetector } from "../core/change-detector.js";
6
- import { EntryPointExpander } from "../core/entry-point.js";
7
- import { JobGenerator } from "../core/job.js";
8
- import { Runner } from "../core/runner.js";
9
- import { ConsoleReporter } from "../output/console.js";
10
- import {
11
- type ConsoleLogHandle,
12
- startConsoleLog,
13
- } from "../output/console-log.js";
14
- import { Logger } from "../output/logger.js";
15
- import {
16
- getDebugLogger,
17
- initDebugLogger,
18
- mergeDebugLogConfig,
19
- } from "../utils/debug-log.js";
20
- import {
21
- readExecutionState,
22
- resolveFixBase,
23
- writeExecutionState,
24
- } from "../utils/execution-state.js";
25
- import {
26
- findPreviousFailures,
27
- type PassedSlot,
28
- type PreviousViolation,
29
- } from "../utils/log-parser.js";
30
- import {
31
- acquireLock,
32
- cleanLogs,
33
- hasExistingLogs,
34
- performAutoClean,
35
- releaseLock,
36
- shouldAutoClean,
37
- } from "./shared.js";
38
-
39
- export function registerReviewCommand(program: Command): void {
40
- program
41
- .command("review")
42
- .description("Run only applicable reviews for detected changes")
43
- .option(
44
- "-b, --base-branch <branch>",
45
- "Override base branch for change detection",
46
- )
47
- .option("-g, --gate <name>", "Run specific review gate only")
48
- .option("-c, --commit <sha>", "Use diff for a specific commit")
49
- .option(
50
- "-u, --uncommitted",
51
- "Use diff for current uncommitted changes (staged and unstaged)",
52
- )
53
- .action(async (options) => {
54
- let config: Awaited<ReturnType<typeof loadConfig>> | undefined;
55
- let lockAcquired = false;
56
- let restoreConsole: ConsoleLogHandle | undefined;
57
- try {
58
- config = await loadConfig();
59
-
60
- // Initialize debug logger
61
- const globalConfig = await loadGlobalConfig();
62
- const debugLogConfig = mergeDebugLogConfig(
63
- config.project.debug_log,
64
- globalConfig.debug_log,
65
- );
66
- initDebugLogger(config.project.log_dir, debugLogConfig);
67
-
68
- // Log the command invocation
69
- const debugLogger = getDebugLogger();
70
- const args = [
71
- options.baseBranch ? `-b ${options.baseBranch}` : "",
72
- options.gate ? `-g ${options.gate}` : "",
73
- options.commit ? `-c ${options.commit}` : "",
74
- options.uncommitted ? "-u" : "",
75
- ].filter(Boolean);
76
- await debugLogger?.logCommand("review", args);
77
-
78
- // Determine effective base branch first (needed for auto-clean)
79
- const effectiveBaseBranch =
80
- options.baseBranch ||
81
- (process.env.GITHUB_BASE_REF &&
82
- (process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true")
83
- ? process.env.GITHUB_BASE_REF
84
- : null) ||
85
- config.project.base_branch;
86
-
87
- // Auto-clean on context change (branch changed, commit merged)
88
- const autoCleanResult = await shouldAutoClean(
89
- config.project.log_dir,
90
- effectiveBaseBranch,
91
- );
92
- if (autoCleanResult.clean) {
93
- console.log(
94
- chalk.dim(`Auto-cleaning logs (${autoCleanResult.reason})...`),
95
- );
96
- await debugLogger?.logClean(
97
- "auto",
98
- autoCleanResult.reason || "unknown",
99
- );
100
- await performAutoClean(config.project.log_dir, autoCleanResult);
101
- }
102
-
103
- // Detect rerun mode after auto-clean (clean may have removed logs)
104
- const logsExist = await hasExistingLogs(config.project.log_dir);
105
- const isRerun = logsExist && !options.commit;
106
-
107
- // Acquire lock BEFORE starting console log (prevents orphaned log files)
108
- await acquireLock(config.project.log_dir);
109
- lockAcquired = true;
110
-
111
- // Initialize Logger early to get unified run number for console log
112
- const logger = new Logger(config.project.log_dir);
113
- await logger.init();
114
- const runNumber = logger.getRunNumber();
115
-
116
- restoreConsole = await startConsoleLog(
117
- config.project.log_dir,
118
- runNumber,
119
- );
120
-
121
- let failuresMap:
122
- | Map<string, Map<string, PreviousViolation[]>>
123
- | undefined;
124
- let changeOptions:
125
- | { commit?: string; uncommitted?: boolean; fixBase?: string }
126
- | undefined;
127
-
128
- let passedSlotsMap: Map<string, Map<number, PassedSlot>> | undefined;
129
-
130
- if (isRerun) {
131
- console.log(
132
- chalk.dim(
133
- "Existing logs detected — running in verification mode...",
134
- ),
135
- );
136
- const { failures: previousFailures, passedSlots } =
137
- await findPreviousFailures(
138
- config.project.log_dir,
139
- options.gate,
140
- true,
141
- );
142
-
143
- failuresMap = new Map();
144
- for (const gateFailure of previousFailures) {
145
- const adapterMap = new Map<string, PreviousViolation[]>();
146
- for (const af of gateFailure.adapterFailures) {
147
- const key = af.reviewIndex
148
- ? String(af.reviewIndex)
149
- : af.adapterName;
150
- adapterMap.set(key, af.violations);
151
- }
152
- failuresMap.set(gateFailure.jobId, adapterMap);
153
- }
154
-
155
- passedSlotsMap = passedSlots;
156
-
157
- if (previousFailures.length > 0) {
158
- const totalViolations = previousFailures.reduce(
159
- (sum, gf) =>
160
- sum +
161
- gf.adapterFailures.reduce(
162
- (s, af) => s + af.violations.length,
163
- 0,
164
- ),
165
- 0,
166
- );
167
- console.log(
168
- chalk.yellow(
169
- `Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`,
170
- ),
171
- );
172
- }
173
-
174
- changeOptions = { uncommitted: true };
175
- // Use working_tree_ref from execution state for rerun diff scoping
176
- const executionState = await readExecutionState(
177
- config.project.log_dir,
178
- );
179
- if (executionState?.working_tree_ref) {
180
- changeOptions.fixBase = executionState.working_tree_ref;
181
- }
182
- } else if (!logsExist) {
183
- // Post-clean run: check if execution state has a working_tree_ref to use as fixBase
184
- const executionState = await readExecutionState(
185
- config.project.log_dir,
186
- );
187
- if (executionState) {
188
- const resolved = await resolveFixBase(
189
- executionState,
190
- effectiveBaseBranch,
191
- );
192
- if (resolved.warning) {
193
- console.log(chalk.yellow(`Warning: ${resolved.warning}`));
194
- }
195
- if (resolved.fixBase) {
196
- changeOptions = { fixBase: resolved.fixBase };
197
- }
198
- }
199
- }
200
-
201
- // Allow explicit commit or uncommitted options to override fixBase
202
- if (options.commit || options.uncommitted) {
203
- changeOptions = {
204
- commit: options.commit,
205
- uncommitted: options.uncommitted,
206
- fixBase: changeOptions?.fixBase,
207
- };
208
- }
209
-
210
- const changeDetector = new ChangeDetector(
211
- effectiveBaseBranch,
212
- changeOptions || {
213
- commit: options.commit,
214
- uncommitted: options.uncommitted,
215
- },
216
- );
217
- const expander = new EntryPointExpander();
218
- const jobGen = new JobGenerator(config);
219
-
220
- console.log(chalk.dim("Detecting changes..."));
221
- const changes = await changeDetector.getChangedFiles();
222
-
223
- if (changes.length === 0) {
224
- console.log(chalk.green("No changes detected."));
225
- await writeExecutionState(config.project.log_dir);
226
- await releaseLock(config.project.log_dir);
227
- restoreConsole?.restore();
228
- process.exit(0);
229
- }
230
-
231
- console.log(chalk.dim(`Found ${changes.length} changed files.`));
232
-
233
- const entryPoints = await expander.expand(
234
- config.project.entry_points,
235
- changes,
236
- );
237
- let jobs = jobGen.generateJobs(entryPoints);
238
-
239
- // Filter to only reviews
240
- jobs = jobs.filter((j) => j.type === "review");
241
-
242
- if (options.gate) {
243
- jobs = jobs.filter((j) => j.name === options.gate);
244
- }
245
-
246
- if (jobs.length === 0) {
247
- console.log(chalk.yellow("No applicable reviews for these changes."));
248
- await writeExecutionState(config.project.log_dir);
249
- await releaseLock(config.project.log_dir);
250
- restoreConsole?.restore();
251
- process.exit(0);
252
- }
253
-
254
- console.log(chalk.dim(`Running ${jobs.length} review(s)...`));
255
-
256
- // Log run start
257
- const runMode = isRerun ? "verification" : "full";
258
- await debugLogger?.logRunStart(runMode, changes.length, jobs.length);
259
-
260
- const reporter = new ConsoleReporter();
261
- const runner = new Runner(
262
- config,
263
- logger,
264
- reporter,
265
- failuresMap,
266
- changeOptions,
267
- effectiveBaseBranch,
268
- passedSlotsMap,
269
- debugLogger ?? undefined,
270
- isRerun,
271
- );
272
-
273
- const success = await runner.run(jobs);
274
-
275
- // Log run end
276
- await debugLogger?.logRunEnd(
277
- success ? "pass" : "fail",
278
- 0,
279
- 0,
280
- 0,
281
- logger.getRunNumber(),
282
- );
283
-
284
- // Write execution state before releasing lock (for interval checks)
285
- // This now captures working_tree_ref which is used for rerun diff scoping
286
- await writeExecutionState(config.project.log_dir);
287
-
288
- if (success) {
289
- await debugLogger?.logClean("auto", "all_passed");
290
- await cleanLogs(config.project.log_dir);
291
- }
292
- await releaseLock(config.project.log_dir);
293
- restoreConsole?.restore();
294
- process.exit(success ? 0 : 1);
295
- } catch (error: unknown) {
296
- // Write execution state even on error (if lock was acquired)
297
- if (config && lockAcquired) {
298
- try {
299
- await writeExecutionState(config.project.log_dir);
300
- } catch {
301
- // Ignore errors writing state during error handling
302
- }
303
- await releaseLock(config.project.log_dir);
304
- }
305
- const err = error as { message?: string };
306
- console.error(chalk.red("Error:"), err.message);
307
- restoreConsole?.restore();
308
- process.exit(1);
309
- }
310
- });
311
- }
@@ -1,29 +0,0 @@
1
- import type { Command } from "commander";
2
- import { executeRun } from "../core/run-executor.js";
3
- import { isSuccessStatus } from "../types/gauntlet-status.js";
4
-
5
- export function registerRunCommand(program: Command): void {
6
- program
7
- .command("run")
8
- .description("Run gates for detected changes")
9
- .option(
10
- "-b, --base-branch <branch>",
11
- "Override base branch for change detection",
12
- )
13
- .option("-g, --gate <name>", "Run specific gate only")
14
- .option("-c, --commit <sha>", "Use diff for a specific commit")
15
- .option(
16
- "-u, --uncommitted",
17
- "Use diff for current uncommitted changes (staged and unstaged)",
18
- )
19
- .action(async (options) => {
20
- const result = await executeRun({
21
- baseBranch: options.baseBranch,
22
- gate: options.gate,
23
- commit: options.commit,
24
- uncommitted: options.uncommitted,
25
- });
26
-
27
- process.exit(isSuccessStatus(result.status) ? 0 : 1);
28
- });
29
- }
@@ -1,267 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import {
4
- getDebugLogBackupFilename,
5
- getDebugLogFilename,
6
- } from "../utils/debug-log.js";
7
- import {
8
- deleteExecutionState,
9
- getCurrentBranch,
10
- getExecutionStateFilename,
11
- isCommitInBranch,
12
- readExecutionState,
13
- } from "../utils/execution-state.js";
14
-
15
- const LOCK_FILENAME = ".gauntlet-run.lock";
16
- const SESSION_REF_FILENAME = ".session_ref";
17
-
18
- export interface AutoCleanResult {
19
- clean: boolean;
20
- reason?: string;
21
- resetState?: boolean;
22
- }
23
-
24
- /**
25
- * Check if logs should be auto-cleaned based on execution context changes.
26
- * Returns { clean: true, reason, resetState } if context has changed.
27
- * Returns { clean: false } if context is unchanged or state file doesn't exist.
28
- * When resetState is true, the execution state should be deleted (not just logs).
29
- */
30
- export async function shouldAutoClean(
31
- logDir: string,
32
- baseBranch: string,
33
- ): Promise<AutoCleanResult> {
34
- const state = await readExecutionState(logDir);
35
-
36
- // No state file = no auto-clean needed
37
- if (!state) {
38
- return { clean: false };
39
- }
40
-
41
- // Check if branch changed
42
- try {
43
- const currentBranch = await getCurrentBranch();
44
- if (currentBranch !== state.branch) {
45
- return { clean: true, reason: "branch changed", resetState: true };
46
- }
47
- } catch {
48
- // If we can't get the current branch, don't auto-clean
49
- return { clean: false };
50
- }
51
-
52
- // Check if commit was merged into base branch
53
- try {
54
- const isMerged = await isCommitInBranch(state.commit, baseBranch);
55
- if (isMerged) {
56
- return { clean: true, reason: "commit merged", resetState: true };
57
- }
58
- } catch {
59
- // If we can't check merge status, don't auto-clean
60
- }
61
-
62
- return { clean: false };
63
- }
64
-
65
- /**
66
- * Perform auto-clean with state reset if needed.
67
- */
68
- export async function performAutoClean(
69
- logDir: string,
70
- result: AutoCleanResult,
71
- maxPreviousLogs = 3,
72
- ): Promise<void> {
73
- await cleanLogs(logDir, maxPreviousLogs);
74
-
75
- // Delete execution state if context changed (branch changed or commit merged)
76
- if (result.resetState) {
77
- await deleteExecutionState(logDir);
78
- }
79
- }
80
-
81
- /**
82
- * Get the lock filename constant.
83
- * Useful for checking lock status from other modules.
84
- */
85
- export function getLockFilename(): string {
86
- return LOCK_FILENAME;
87
- }
88
-
89
- export async function exists(filePath: string): Promise<boolean> {
90
- try {
91
- await fs.stat(filePath);
92
- return true;
93
- } catch {
94
- return false;
95
- }
96
- }
97
-
98
- export async function acquireLock(logDir: string): Promise<void> {
99
- await fs.mkdir(logDir, { recursive: true });
100
- const lockPath = path.resolve(logDir, LOCK_FILENAME);
101
- try {
102
- await fs.writeFile(lockPath, String(process.pid), { flag: "wx" });
103
- } catch (err: unknown) {
104
- if (
105
- typeof err === "object" &&
106
- err !== null &&
107
- "code" in err &&
108
- (err as { code: string }).code === "EEXIST"
109
- ) {
110
- console.error(
111
- `Error: A gauntlet run is already in progress (lock file: ${lockPath}).`,
112
- );
113
- console.error(
114
- "If no run is actually in progress, delete the lock file manually.",
115
- );
116
- process.exit(1);
117
- }
118
- throw err;
119
- }
120
- }
121
-
122
- export async function releaseLock(logDir: string): Promise<void> {
123
- const lockPath = path.resolve(logDir, LOCK_FILENAME);
124
- try {
125
- await fs.rm(lockPath, { force: true });
126
- } catch {
127
- // no-op if missing
128
- }
129
- }
130
-
131
- export async function hasExistingLogs(logDir: string): Promise<boolean> {
132
- try {
133
- const entries = await fs.readdir(logDir);
134
- return entries.some(
135
- (f) =>
136
- (f.endsWith(".log") || f.endsWith(".json")) &&
137
- f !== "previous" &&
138
- !f.startsWith("console.") &&
139
- !f.startsWith("."),
140
- );
141
- } catch {
142
- return false;
143
- }
144
- }
145
-
146
- /**
147
- * Get the set of persistent files that should never be moved during clean.
148
- */
149
- /**
150
- * Marker file used by stop-hook to detect nested invocations.
151
- * Must match STOP_HOOK_MARKER_FILE in stop-hook.ts.
152
- */
153
- const STOP_HOOK_MARKER_FILE = ".stop-hook-active";
154
-
155
- function getPersistentFiles(): Set<string> {
156
- return new Set([
157
- getExecutionStateFilename(),
158
- getDebugLogFilename(),
159
- getDebugLogBackupFilename(),
160
- LOCK_FILENAME,
161
- SESSION_REF_FILENAME, // Will be deleted, not moved
162
- STOP_HOOK_MARKER_FILE, // Cleaned up by stop-hook finally block, not cleanLogs
163
- ]);
164
- }
165
-
166
- /**
167
- * Check if there are current logs to archive.
168
- * Returns true if there are .log or .json files in the log directory root.
169
- * Excludes persistent files (.execution_state, .debug.log, etc.)
170
- */
171
- async function hasCurrentLogs(logDir: string): Promise<boolean> {
172
- try {
173
- const files = await fs.readdir(logDir);
174
- const persistentFiles = getPersistentFiles();
175
- return files.some(
176
- (f) =>
177
- (f.endsWith(".log") || f.endsWith(".json")) &&
178
- f !== "previous" &&
179
- !persistentFiles.has(f),
180
- );
181
- } catch {
182
- return false;
183
- }
184
- }
185
-
186
- /** Get current log files (excludes previous dirs and persistent files). */
187
- function getCurrentLogFiles(files: string[]): string[] {
188
- const persistentFiles = getPersistentFiles();
189
- return files.filter(
190
- (file) => !file.startsWith("previous") && !persistentFiles.has(file),
191
- );
192
- }
193
-
194
- /** Delete current logs without archiving (maxPreviousLogs === 0). */
195
- async function deleteCurrentLogs(logDir: string): Promise<void> {
196
- const files = await fs.readdir(logDir);
197
- await Promise.all(
198
- getCurrentLogFiles(files).map((file) =>
199
- fs.rm(path.join(logDir, file), { recursive: true, force: true }),
200
- ),
201
- );
202
- }
203
-
204
- /** Rotate existing previous/ directories to make room for a new archive. */
205
- async function rotatePreviousDirs(
206
- logDir: string,
207
- maxPreviousLogs: number,
208
- ): Promise<void> {
209
- const oldestSuffix = maxPreviousLogs - 1;
210
- const oldestDir =
211
- oldestSuffix === 0 ? "previous" : `previous.${oldestSuffix}`;
212
- const oldestPath = path.join(logDir, oldestDir);
213
- if (await exists(oldestPath)) {
214
- await fs.rm(oldestPath, { recursive: true, force: true });
215
- }
216
-
217
- for (let i = oldestSuffix - 1; i >= 0; i--) {
218
- const fromName = i === 0 ? "previous" : `previous.${i}`;
219
- const toName = `previous.${i + 1}`;
220
- const fromPath = path.join(logDir, fromName);
221
- const toPath = path.join(logDir, toName);
222
- if (await exists(fromPath)) {
223
- await fs.rename(fromPath, toPath);
224
- }
225
- }
226
- }
227
-
228
- export async function cleanLogs(
229
- logDir: string,
230
- maxPreviousLogs = 3,
231
- ): Promise<void> {
232
- try {
233
- if (!(await exists(logDir))) return;
234
- if (!(await hasCurrentLogs(logDir))) return;
235
-
236
- if (maxPreviousLogs === 0) {
237
- await deleteCurrentLogs(logDir);
238
- return;
239
- }
240
-
241
- await rotatePreviousDirs(logDir, maxPreviousLogs);
242
-
243
- const previousDir = path.join(logDir, "previous");
244
- await fs.mkdir(previousDir, { recursive: true });
245
-
246
- const files = await fs.readdir(logDir);
247
- await Promise.all(
248
- getCurrentLogFiles(files).map((file) =>
249
- fs.rename(path.join(logDir, file), path.join(previousDir, file)),
250
- ),
251
- );
252
-
253
- // Delete legacy .session_ref if it exists (migration cleanup)
254
- try {
255
- await fs.rm(path.join(logDir, SESSION_REF_FILENAME), { force: true });
256
- } catch {
257
- // Ignore errors
258
- }
259
- } catch (error) {
260
- console.warn(
261
- "Failed to clean logs in",
262
- logDir,
263
- ":",
264
- error instanceof Error ? error.message : error,
265
- );
266
- }
267
- }