watchfix 0.1.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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/agents/base.d.ts +19 -0
  4. package/dist/agents/base.js +140 -0
  5. package/dist/agents/claude.d.ts +11 -0
  6. package/dist/agents/claude.js +6 -0
  7. package/dist/agents/codex.d.ts +11 -0
  8. package/dist/agents/codex.js +6 -0
  9. package/dist/agents/defaults.d.ts +21 -0
  10. package/dist/agents/defaults.js +21 -0
  11. package/dist/agents/gemini.d.ts +11 -0
  12. package/dist/agents/gemini.js +6 -0
  13. package/dist/agents/index.d.ts +19 -0
  14. package/dist/agents/index.js +63 -0
  15. package/dist/agents/types.d.ts +22 -0
  16. package/dist/agents/types.js +1 -0
  17. package/dist/cli/commands/clean.d.ts +9 -0
  18. package/dist/cli/commands/clean.js +173 -0
  19. package/dist/cli/commands/config.d.ts +7 -0
  20. package/dist/cli/commands/config.js +134 -0
  21. package/dist/cli/commands/fix.d.ts +12 -0
  22. package/dist/cli/commands/fix.js +391 -0
  23. package/dist/cli/commands/ignore.d.ts +7 -0
  24. package/dist/cli/commands/ignore.js +74 -0
  25. package/dist/cli/commands/init.d.ts +5 -0
  26. package/dist/cli/commands/init.js +115 -0
  27. package/dist/cli/commands/logs.d.ts +9 -0
  28. package/dist/cli/commands/logs.js +106 -0
  29. package/dist/cli/commands/show.d.ts +8 -0
  30. package/dist/cli/commands/show.js +165 -0
  31. package/dist/cli/commands/status.d.ts +7 -0
  32. package/dist/cli/commands/status.js +110 -0
  33. package/dist/cli/commands/stop.d.ts +7 -0
  34. package/dist/cli/commands/stop.js +106 -0
  35. package/dist/cli/commands/version.d.ts +7 -0
  36. package/dist/cli/commands/version.js +36 -0
  37. package/dist/cli/commands/watch.d.ts +10 -0
  38. package/dist/cli/commands/watch.js +204 -0
  39. package/dist/cli/index.d.ts +2 -0
  40. package/dist/cli/index.js +152 -0
  41. package/dist/config/loader.d.ts +4 -0
  42. package/dist/config/loader.js +96 -0
  43. package/dist/config/schema.d.ts +375 -0
  44. package/dist/config/schema.js +99 -0
  45. package/dist/db/index.d.ts +15 -0
  46. package/dist/db/index.js +71 -0
  47. package/dist/db/queries.d.ts +45 -0
  48. package/dist/db/queries.js +111 -0
  49. package/dist/db/schema.d.ts +4 -0
  50. package/dist/db/schema.js +84 -0
  51. package/dist/fixer/context.d.ts +9 -0
  52. package/dist/fixer/context.js +361 -0
  53. package/dist/fixer/index.d.ts +37 -0
  54. package/dist/fixer/index.js +398 -0
  55. package/dist/fixer/lock.d.ts +7 -0
  56. package/dist/fixer/lock.js +49 -0
  57. package/dist/fixer/output.d.ts +21 -0
  58. package/dist/fixer/output.js +108 -0
  59. package/dist/fixer/queue.d.ts +15 -0
  60. package/dist/fixer/queue.js +53 -0
  61. package/dist/fixer/verifier.d.ts +44 -0
  62. package/dist/fixer/verifier.js +133 -0
  63. package/dist/utils/daemon.d.ts +22 -0
  64. package/dist/utils/daemon.js +143 -0
  65. package/dist/utils/duration.d.ts +2 -0
  66. package/dist/utils/duration.js +31 -0
  67. package/dist/utils/errors.d.ts +17 -0
  68. package/dist/utils/errors.js +20 -0
  69. package/dist/utils/hash.d.ts +2 -0
  70. package/dist/utils/hash.js +18 -0
  71. package/dist/utils/http.d.ts +6 -0
  72. package/dist/utils/http.js +61 -0
  73. package/dist/utils/logger.d.ts +25 -0
  74. package/dist/utils/logger.js +85 -0
  75. package/dist/utils/process.d.ts +16 -0
  76. package/dist/utils/process.js +146 -0
  77. package/dist/watcher/index.d.ts +55 -0
  78. package/dist/watcher/index.js +234 -0
  79. package/dist/watcher/parser.d.ts +42 -0
  80. package/dist/watcher/parser.js +162 -0
  81. package/dist/watcher/patterns.d.ts +5 -0
  82. package/dist/watcher/patterns.js +92 -0
  83. package/dist/watcher/sources/command.d.ts +27 -0
  84. package/dist/watcher/sources/command.js +143 -0
  85. package/dist/watcher/sources/docker.d.ts +28 -0
  86. package/dist/watcher/sources/docker.js +183 -0
  87. package/dist/watcher/sources/file.d.ts +30 -0
  88. package/dist/watcher/sources/file.js +177 -0
  89. package/dist/watcher/sources/types.d.ts +27 -0
  90. package/dist/watcher/sources/types.js +1 -0
  91. package/package.json +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 watchfix contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # watchfix
2
+
3
+ CLI tool that watches logs, detects errors, and dispatches AI agents to fix them.
4
+
5
+ ## Features
6
+
7
+ - **Log watching**: Monitor file logs, command output, or Docker container logs
8
+ - **Error detection**: Configurable patterns to identify errors in your logs
9
+ - **AI-powered fixes**: Automatically dispatch Claude, Gemini, or Codex to analyze and fix errors
10
+ - **Context awareness**: Generates relevant context files for AI agents
11
+ - **Deduplication**: Groups similar errors to avoid redundant fixes
12
+ - **Daemon mode**: Run in the background on Linux/macOS
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install -g watchfix
18
+ ```
19
+
20
+ ## Requirements
21
+
22
+ - Node.js 18+
23
+ - One of the following AI CLI tools:
24
+ - [Claude CLI](https://github.com/anthropics/claude-code)
25
+ - [Gemini CLI](https://github.com/google-gemini/gemini-cli)
26
+ - [Codex CLI](https://github.com/openai/codex)
27
+
28
+ ## Quick Start
29
+
30
+ 1. Initialize a configuration file in your project:
31
+
32
+ ```bash
33
+ watchfix init
34
+ ```
35
+
36
+ 2. Edit `watchfix.yaml` to configure your log sources and error patterns:
37
+
38
+ ```yaml
39
+ version: "1"
40
+
41
+ sources:
42
+ - name: app
43
+ type: file
44
+ path: ./logs/app.log
45
+
46
+ patterns:
47
+ - name: node-error
48
+ regex: "Error: .+"
49
+ severity: error
50
+
51
+ agent:
52
+ provider: claude
53
+ ```
54
+
55
+ 3. Start watching logs:
56
+
57
+ ```bash
58
+ watchfix watch
59
+ ```
60
+
61
+ 4. When an error is detected, fix it:
62
+
63
+ ```bash
64
+ watchfix fix <error-id>
65
+ ```
66
+
67
+ ## CLI Commands
68
+
69
+ | Command | Description |
70
+ |---------|-------------|
71
+ | `watchfix init` | Create `watchfix.yaml` in current directory |
72
+ | `watchfix watch` | Watch logs in foreground (use `--daemon` for background) |
73
+ | `watchfix fix [id]` | Analyze and fix a specific error (or `--all` for all pending) |
74
+ | `watchfix show <id>` | Show full error details and analysis |
75
+ | `watchfix status` | Show watcher state and pending errors |
76
+ | `watchfix stop` | Stop background watcher |
77
+ | `watchfix ignore <id>` | Mark error as ignored |
78
+ | `watchfix logs` | Show activity log |
79
+ | `watchfix clean` | Remove old context files |
80
+ | `watchfix config validate` | Validate configuration file |
81
+
82
+ ### Global Options
83
+
84
+ - `-c, --config <path>`: Use alternate config file
85
+ - `--verbose`: Increase output verbosity
86
+ - `-q, --quiet`: Suppress non-essential output
87
+
88
+ ## Platform Notes
89
+
90
+ - **Daemon mode** (`watchfix watch --daemon`) is only available on Linux and macOS
91
+ - Windows users should run `watchfix watch` in a terminal window or use a process manager
92
+
93
+ ## Documentation
94
+
95
+ For detailed configuration options and advanced usage, see the [specification document](./spec/watchfix-spec-v8.md).
96
+
97
+ ## License
98
+
99
+ [MIT](./LICENSE)
@@ -0,0 +1,19 @@
1
+ import type { Agent, AgentConfig, AgentResult } from './types.js';
2
+ import { Logger } from '../utils/logger.js';
3
+ type BaseAgentOptions = {
4
+ projectRoot: string;
5
+ logger?: Logger;
6
+ terminalEnabled?: boolean;
7
+ };
8
+ export declare class BaseAgent implements Agent {
9
+ config: AgentConfig;
10
+ private readonly projectRoot;
11
+ private readonly logger;
12
+ private readonly terminalEnabled;
13
+ constructor(config: AgentConfig, options: BaseAgentOptions);
14
+ analyze(contextPath: string): Promise<AgentResult>;
15
+ fix(contextPath: string): Promise<AgentResult>;
16
+ private run;
17
+ private executeOnce;
18
+ }
19
+ export {};
@@ -0,0 +1,140 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { Logger } from '../utils/logger.js';
5
+ const KILL_GRACE_PERIOD_MS = 5000;
6
+ const PROMPTS = {
7
+ analyze: (contextPath) => `Read ${contextPath} and follow the instructions.`,
8
+ fix: (contextPath) => `Read ${contextPath} and follow the instructions.`,
9
+ };
10
+ const outputPathForContext = (contextPath) => {
11
+ if (contextPath.endsWith('-analyze.md')) {
12
+ return contextPath.replace(/-analyze\.md$/, '-analysis.yaml');
13
+ }
14
+ if (contextPath.endsWith('-fix.md')) {
15
+ return contextPath.replace(/-fix\.md$/, '-result.yaml');
16
+ }
17
+ return null;
18
+ };
19
+ export class BaseAgent {
20
+ config;
21
+ projectRoot;
22
+ logger;
23
+ terminalEnabled;
24
+ constructor(config, options) {
25
+ this.config = config;
26
+ this.projectRoot = options.projectRoot;
27
+ this.terminalEnabled = options.terminalEnabled ?? true;
28
+ this.logger =
29
+ options.logger ??
30
+ new Logger({ rootDir: this.projectRoot, terminalEnabled: this.terminalEnabled });
31
+ }
32
+ async analyze(contextPath) {
33
+ return await this.run('analyze', contextPath);
34
+ }
35
+ async fix(contextPath) {
36
+ return await this.run('fix', contextPath);
37
+ }
38
+ async run(mode, contextPath) {
39
+ const attempts = Math.max(0, this.config.retries) + 1;
40
+ let lastResult = null;
41
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
42
+ lastResult = await this.executeOnce(mode, contextPath);
43
+ if (lastResult.success) {
44
+ return lastResult;
45
+ }
46
+ if (attempt < attempts) {
47
+ this.logger.warn(`Agent ${this.config.provider} ${mode} attempt ${attempt} failed; retrying`);
48
+ }
49
+ }
50
+ return (lastResult ?? {
51
+ success: false,
52
+ stdout: '',
53
+ stderr: 'Agent execution failed',
54
+ exitCode: -1,
55
+ timedOut: false,
56
+ outputFileExists: false,
57
+ });
58
+ }
59
+ async executeOnce(mode, contextPath) {
60
+ const prompt = PROMPTS[mode](contextPath);
61
+ const command = this.config.command;
62
+ const args = [...this.config.args, prompt];
63
+ const resolvedContextPath = path.isAbsolute(contextPath)
64
+ ? contextPath
65
+ : path.resolve(this.projectRoot, contextPath);
66
+ const outputPathRaw = outputPathForContext(resolvedContextPath);
67
+ const outputPath = outputPathRaw && !path.isAbsolute(outputPathRaw)
68
+ ? path.resolve(this.projectRoot, outputPathRaw)
69
+ : outputPathRaw;
70
+ return await new Promise((resolve) => {
71
+ const child = spawn(command, args, {
72
+ cwd: this.projectRoot,
73
+ stdio: ['ignore', 'pipe', 'pipe'],
74
+ });
75
+ let stdout = '';
76
+ let stderr = '';
77
+ let timedOut = false;
78
+ let resolved = false;
79
+ const finalize = (result) => {
80
+ if (resolved) {
81
+ return;
82
+ }
83
+ resolved = true;
84
+ resolve(result);
85
+ };
86
+ const timeoutId = setTimeout(() => {
87
+ timedOut = true;
88
+ child.kill('SIGTERM');
89
+ const killTimer = setTimeout(() => {
90
+ if (child.exitCode === null && child.signalCode === null) {
91
+ child.kill('SIGKILL');
92
+ }
93
+ }, KILL_GRACE_PERIOD_MS);
94
+ child.once('close', () => clearTimeout(killTimer));
95
+ }, this.config.timeout);
96
+ if (child.stdout) {
97
+ child.stdout.on('data', (data) => {
98
+ stdout += String(data);
99
+ });
100
+ }
101
+ if (child.stderr) {
102
+ child.stderr.on('data', (data) => {
103
+ const chunk = String(data);
104
+ stderr += chunk;
105
+ if (this.config.stderrIsProgress && this.terminalEnabled) {
106
+ process.stderr.write(chunk);
107
+ }
108
+ });
109
+ }
110
+ child.on('error', (error) => {
111
+ clearTimeout(timeoutId);
112
+ const outputFileExists = outputPath ? fs.existsSync(outputPath) : false;
113
+ finalize({
114
+ success: false,
115
+ stdout,
116
+ stderr: error.message,
117
+ exitCode: -1,
118
+ timedOut: false,
119
+ outputFileExists,
120
+ });
121
+ });
122
+ child.on('close', (code) => {
123
+ clearTimeout(timeoutId);
124
+ const outputFileExists = outputPath ? fs.existsSync(outputPath) : false;
125
+ const trimmedStderr = stderr.trim();
126
+ if (trimmedStderr && !this.config.stderrIsProgress) {
127
+ this.logger.warn(`Agent stderr: ${trimmedStderr}`);
128
+ }
129
+ finalize({
130
+ success: code === 0 && !timedOut,
131
+ stdout,
132
+ stderr,
133
+ exitCode: code ?? -1,
134
+ timedOut,
135
+ outputFileExists,
136
+ });
137
+ });
138
+ });
139
+ }
140
+ }
@@ -0,0 +1,11 @@
1
+ import type { AgentConfig } from './types.js';
2
+ import { BaseAgent } from './base.js';
3
+ import type { Logger } from '../utils/logger.js';
4
+ export type ClaudeAgentOptions = {
5
+ projectRoot: string;
6
+ logger?: Logger;
7
+ terminalEnabled?: boolean;
8
+ };
9
+ export declare class ClaudeAgent extends BaseAgent {
10
+ constructor(config: AgentConfig, options: ClaudeAgentOptions);
11
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseAgent } from './base.js';
2
+ export class ClaudeAgent extends BaseAgent {
3
+ constructor(config, options) {
4
+ super(config, options);
5
+ }
6
+ }
@@ -0,0 +1,11 @@
1
+ import type { AgentConfig } from './types.js';
2
+ import { BaseAgent } from './base.js';
3
+ import type { Logger } from '../utils/logger.js';
4
+ export type CodexAgentOptions = {
5
+ projectRoot: string;
6
+ logger?: Logger;
7
+ terminalEnabled?: boolean;
8
+ };
9
+ export declare class CodexAgent extends BaseAgent {
10
+ constructor(config: AgentConfig, options: CodexAgentOptions);
11
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseAgent } from './base.js';
2
+ export class CodexAgent extends BaseAgent {
3
+ constructor(config, options) {
4
+ super(config, options);
5
+ }
6
+ }
@@ -0,0 +1,21 @@
1
+ export declare const AGENT_DEFAULTS: {
2
+ readonly claude: {
3
+ readonly command: "claude";
4
+ readonly args: readonly ["--model", "sonnet", "--dangerously-skip-permissions", "-p"];
5
+ readonly stderrIsProgress: false;
6
+ };
7
+ readonly gemini: {
8
+ readonly command: "gemini";
9
+ readonly args: readonly ["--yolo", "-p"];
10
+ readonly stderrIsProgress: true;
11
+ };
12
+ readonly codex: {
13
+ readonly command: "codex";
14
+ readonly args: readonly ["exec", "--yolo"];
15
+ readonly stderrIsProgress: true;
16
+ };
17
+ };
18
+ export declare const AGENT_CONFIG_DEFAULTS: {
19
+ readonly timeout: number;
20
+ readonly retries: 2;
21
+ };
@@ -0,0 +1,21 @@
1
+ export const AGENT_DEFAULTS = {
2
+ claude: {
3
+ command: "claude",
4
+ args: ["--model", "sonnet", "--dangerously-skip-permissions", "-p"],
5
+ stderrIsProgress: false,
6
+ },
7
+ gemini: {
8
+ command: "gemini",
9
+ args: ["--yolo", "-p"],
10
+ stderrIsProgress: true,
11
+ },
12
+ codex: {
13
+ command: "codex",
14
+ args: ["exec", "--yolo"],
15
+ stderrIsProgress: true,
16
+ },
17
+ };
18
+ export const AGENT_CONFIG_DEFAULTS = {
19
+ timeout: 5 * 60 * 1000,
20
+ retries: 2,
21
+ };
@@ -0,0 +1,11 @@
1
+ import type { AgentConfig } from './types.js';
2
+ import { BaseAgent } from './base.js';
3
+ import type { Logger } from '../utils/logger.js';
4
+ export type GeminiAgentOptions = {
5
+ projectRoot: string;
6
+ logger?: Logger;
7
+ terminalEnabled?: boolean;
8
+ };
9
+ export declare class GeminiAgent extends BaseAgent {
10
+ constructor(config: AgentConfig, options: GeminiAgentOptions);
11
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseAgent } from './base.js';
2
+ export class GeminiAgent extends BaseAgent {
3
+ constructor(config, options) {
4
+ super(config, options);
5
+ }
6
+ }
@@ -0,0 +1,19 @@
1
+ import type { Agent, AgentProvider } from './types.js';
2
+ import type { Logger } from '../utils/logger.js';
3
+ export type AgentConfigInput = {
4
+ provider: AgentProvider;
5
+ command?: string;
6
+ args?: string[];
7
+ stderrIsProgress?: boolean;
8
+ timeout?: number;
9
+ retries?: number;
10
+ };
11
+ export type CreateAgentOptions = {
12
+ projectRoot: string;
13
+ logger?: Logger;
14
+ terminalEnabled?: boolean;
15
+ };
16
+ export type AgentConfigOverrides = Omit<AgentConfigInput, 'provider'>;
17
+ export declare function createAgent(provider: AgentProvider, config: AgentConfigOverrides, options: CreateAgentOptions): Agent;
18
+ export declare function createAgent(provider: AgentProvider, options: CreateAgentOptions): Agent;
19
+ export declare function createAgent(config: AgentConfigInput, options: CreateAgentOptions): Agent;
@@ -0,0 +1,63 @@
1
+ import { AGENT_CONFIG_DEFAULTS, AGENT_DEFAULTS } from './defaults.js';
2
+ import { checkCliExists } from '../utils/process.js';
3
+ import { ClaudeAgent } from './claude.js';
4
+ import { GeminiAgent } from './gemini.js';
5
+ import { CodexAgent } from './codex.js';
6
+ function resolveAgentConfig(input) {
7
+ const defaults = AGENT_DEFAULTS[input.provider];
8
+ return {
9
+ provider: input.provider,
10
+ command: input.command ?? defaults.command,
11
+ args: input.args ? [...input.args] : [...defaults.args],
12
+ stderrIsProgress: input.stderrIsProgress ?? defaults.stderrIsProgress,
13
+ timeout: input.timeout ?? AGENT_CONFIG_DEFAULTS.timeout,
14
+ retries: input.retries ?? AGENT_CONFIG_DEFAULTS.retries,
15
+ };
16
+ }
17
+ function assertCliExists(command) {
18
+ const result = checkCliExists(command);
19
+ if (!result.exists) {
20
+ const detail = result.error ? ` (${result.error})` : '';
21
+ throw new Error(`Agent CLI '${command}' not found${detail}`);
22
+ }
23
+ }
24
+ const isCreateAgentOptions = (value) => typeof value.projectRoot === 'string';
25
+ export function createAgent(providerOrConfig, configOrOptions, maybeOptions) {
26
+ let config;
27
+ let options;
28
+ if (typeof providerOrConfig === 'string') {
29
+ const provider = providerOrConfig;
30
+ if (isCreateAgentOptions(configOrOptions)) {
31
+ config = { provider };
32
+ options = configOrOptions;
33
+ }
34
+ else {
35
+ if (!maybeOptions) {
36
+ throw new Error('createAgent requires options with projectRoot');
37
+ }
38
+ config = { provider, ...configOrOptions };
39
+ options = maybeOptions;
40
+ }
41
+ }
42
+ else {
43
+ config = providerOrConfig;
44
+ if (!isCreateAgentOptions(configOrOptions)) {
45
+ throw new Error('createAgent requires options with projectRoot');
46
+ }
47
+ options = configOrOptions;
48
+ }
49
+ const resolvedConfig = resolveAgentConfig(config);
50
+ assertCliExists(resolvedConfig.command);
51
+ switch (resolvedConfig.provider) {
52
+ case 'claude':
53
+ return new ClaudeAgent(resolvedConfig, options);
54
+ case 'gemini':
55
+ return new GeminiAgent(resolvedConfig, options);
56
+ case 'codex':
57
+ return new CodexAgent(resolvedConfig, options);
58
+ default: {
59
+ const exhaustive = resolvedConfig.provider;
60
+ throw new Error(`Unsupported agent provider: ${exhaustive}`);
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,22 @@
1
+ export type AgentProvider = 'claude' | 'gemini' | 'codex';
2
+ export interface AgentConfig {
3
+ provider: AgentProvider;
4
+ command: string;
5
+ args: string[];
6
+ stderrIsProgress: boolean;
7
+ timeout: number;
8
+ retries: number;
9
+ }
10
+ export interface AgentResult {
11
+ success: boolean;
12
+ stdout: string;
13
+ stderr: string;
14
+ exitCode: number | null;
15
+ timedOut: boolean;
16
+ outputFileExists: boolean;
17
+ }
18
+ export interface Agent {
19
+ config: AgentConfig;
20
+ analyze(contextPath: string): Promise<AgentResult>;
21
+ fix(contextPath: string): Promise<AgentResult>;
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ type CleanOptions = {
2
+ config?: string;
3
+ dryRun?: boolean;
4
+ force?: boolean;
5
+ verbose?: boolean;
6
+ quiet?: boolean;
7
+ };
8
+ export declare const cleanCommand: (options: CleanOptions) => Promise<void>;
9
+ export {};
@@ -0,0 +1,173 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline/promises';
4
+ import { loadConfig } from '../../config/loader.js';
5
+ import { Database } from '../../db/index.js';
6
+ import { getErrorsByStatus } from '../../db/queries.js';
7
+ import { checkSchemaVersion, initializeSchema } from '../../db/schema.js';
8
+ import { UserError } from '../../utils/errors.js';
9
+ const DAY_MS = 24 * 60 * 60 * 1000;
10
+ const buildDatabasePath = (rootDir) => path.join(rootDir, '.watchfix', 'errors.db');
11
+ const buildContextDir = (rootDir) => path.join(rootDir, '.watchfix', 'context');
12
+ const parseContextFilename = (filename) => {
13
+ const match = filename.match(/^(\d{4}-\d{2}-\d{2})-error-(\d+)-attempt-\d+-/);
14
+ if (!match) {
15
+ return null;
16
+ }
17
+ const errorId = Number.parseInt(match[2] ?? '', 10);
18
+ if (!Number.isFinite(errorId)) {
19
+ return null;
20
+ }
21
+ return { date: match[1], errorId };
22
+ };
23
+ const parseDatePrefix = (dateValue) => {
24
+ const parsed = Date.parse(`${dateValue}T00:00:00Z`);
25
+ if (Number.isNaN(parsed)) {
26
+ return null;
27
+ }
28
+ return parsed;
29
+ };
30
+ const formatSize = (bytes) => {
31
+ if (bytes <= 0) {
32
+ return '0 B';
33
+ }
34
+ const mb = bytes / (1024 * 1024);
35
+ if (mb >= 1) {
36
+ return `${mb.toFixed(1)} MB`;
37
+ }
38
+ const kb = bytes / 1024;
39
+ return `${Math.max(1, Math.round(kb))} KB`;
40
+ };
41
+ const promptForConfirmation = async (label) => {
42
+ if (!process.stdin.isTTY) {
43
+ throw new UserError('Cannot prompt for confirmation in non-interactive mode. Use --force to proceed.');
44
+ }
45
+ const rl = readline.createInterface({
46
+ input: process.stdin,
47
+ output: process.stdout,
48
+ });
49
+ try {
50
+ const answer = await rl.question(`${label} [y/N] `);
51
+ const normalized = answer.trim().toLowerCase();
52
+ return normalized === 'y' || normalized === 'yes';
53
+ }
54
+ finally {
55
+ rl.close();
56
+ }
57
+ };
58
+ const findInProgressErrorIds = (db) => {
59
+ const inProgress = getErrorsByStatus(db, ['analyzing', 'fixing']);
60
+ return new Set(inProgress.map((error) => error.id));
61
+ };
62
+ const loadContextFiles = (contextDir, rootDir) => {
63
+ let entries;
64
+ try {
65
+ entries = fs.readdirSync(contextDir, { withFileTypes: true });
66
+ }
67
+ catch (error) {
68
+ const err = error;
69
+ if (err.code === 'ENOENT') {
70
+ return [];
71
+ }
72
+ throw new UserError(`Failed to read context directory ${contextDir}: ${err.message ?? String(err)}`);
73
+ }
74
+ const files = [];
75
+ for (const entry of entries) {
76
+ if (!entry.isFile()) {
77
+ continue;
78
+ }
79
+ const parsed = parseContextFilename(entry.name);
80
+ if (!parsed) {
81
+ continue;
82
+ }
83
+ const fullPath = path.join(contextDir, entry.name);
84
+ let stats;
85
+ try {
86
+ stats = fs.statSync(fullPath);
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ files.push({
92
+ filename: entry.name,
93
+ fullPath,
94
+ relativePath: path.relative(rootDir, fullPath),
95
+ errorId: parsed.errorId,
96
+ date: parsed.date,
97
+ sizeBytes: stats.size,
98
+ });
99
+ }
100
+ return files;
101
+ };
102
+ const selectFilesToRemove = (files, inProgress, maxAgeDays) => {
103
+ const cutoffMs = Date.now() - maxAgeDays * DAY_MS;
104
+ return files.filter((file) => {
105
+ if (inProgress.has(file.errorId)) {
106
+ return false;
107
+ }
108
+ const fileDate = parseDatePrefix(file.date);
109
+ if (!fileDate) {
110
+ return false;
111
+ }
112
+ return fileDate < cutoffMs;
113
+ });
114
+ };
115
+ const summarizeRemoval = (files) => {
116
+ const totalBytes = files.reduce((sum, file) => sum + file.sizeBytes, 0);
117
+ return `${files.length} file${files.length === 1 ? '' : 's'} (${formatSize(totalBytes)})`;
118
+ };
119
+ const listFiles = (files) => {
120
+ for (const file of files) {
121
+ process.stdout.write(`- ${file.relativePath}\n`);
122
+ }
123
+ };
124
+ export const cleanCommand = async (options) => {
125
+ const config = loadConfig(options.config);
126
+ const contextDir = buildContextDir(config.project.root);
127
+ const dbPath = buildDatabasePath(config.project.root);
128
+ const db = new Database(dbPath);
129
+ let inProgress = new Set();
130
+ try {
131
+ initializeSchema(db);
132
+ checkSchemaVersion(db);
133
+ inProgress = findInProgressErrorIds(db);
134
+ }
135
+ finally {
136
+ db.close();
137
+ }
138
+ const allFiles = loadContextFiles(contextDir, config.project.root);
139
+ const maxAgeDays = config.cleanup.context_max_age_days;
140
+ const candidates = selectFilesToRemove(allFiles, inProgress, maxAgeDays);
141
+ if (candidates.length === 0) {
142
+ process.stdout.write(`No context files older than ${maxAgeDays} day${maxAgeDays === 1 ? '' : 's'} found.\n`);
143
+ return;
144
+ }
145
+ process.stdout.write(`Context files older than ${maxAgeDays} day${maxAgeDays === 1 ? '' : 's'}:\n`);
146
+ listFiles(candidates);
147
+ if (options.dryRun) {
148
+ process.stdout.write(`Would remove ${summarizeRemoval(candidates)}.\n`);
149
+ return;
150
+ }
151
+ if (!options.force) {
152
+ const confirmed = await promptForConfirmation(`Remove ${summarizeRemoval(candidates)}?`);
153
+ if (!confirmed) {
154
+ process.stdout.write('Cleanup aborted.\n');
155
+ return;
156
+ }
157
+ }
158
+ let removedCount = 0;
159
+ let removedBytes = 0;
160
+ for (const file of candidates) {
161
+ try {
162
+ fs.unlinkSync(file.fullPath);
163
+ removedCount += 1;
164
+ removedBytes += file.sizeBytes;
165
+ }
166
+ catch (error) {
167
+ const err = error;
168
+ throw new UserError(`Failed to remove ${file.relativePath}: ${err.message ?? String(err)}`);
169
+ }
170
+ }
171
+ const removedSummary = `${removedCount} file${removedCount === 1 ? '' : 's'} (${formatSize(removedBytes)})`;
172
+ process.stdout.write(`Removed ${removedSummary}.\n`);
173
+ };