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
@@ -0,0 +1,7 @@
1
+ type ConfigValidateOptions = {
2
+ config?: string;
3
+ verbose?: boolean;
4
+ quiet?: boolean;
5
+ };
6
+ export declare const configValidateCommand: (options: ConfigValidateOptions) => Promise<void>;
7
+ export {};
@@ -0,0 +1,134 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import yaml from 'yaml';
4
+ import { AGENT_DEFAULTS } from '../../agents/defaults.js';
5
+ import { checkCliExists } from '../../utils/process.js';
6
+ import { configSchema } from '../../config/schema.js';
7
+ import { DEFAULT_CONFIG_PATH } from '../../config/loader.js';
8
+ import { UserError } from '../../utils/errors.js';
9
+ const resolveConfigPath = (configPath) => path.resolve(process.cwd(), configPath ?? DEFAULT_CONFIG_PATH);
10
+ const resolveIfRelative = (baseDir, value) => path.isAbsolute(value) ? value : path.resolve(baseDir, value);
11
+ const resolveConfigPaths = (config, baseDir) => ({
12
+ ...config,
13
+ project: {
14
+ ...config.project,
15
+ root: resolveIfRelative(baseDir, config.project.root),
16
+ },
17
+ logs: {
18
+ ...config.logs,
19
+ sources: config.logs.sources.map((source) => {
20
+ if (source.type !== 'file') {
21
+ return source;
22
+ }
23
+ return {
24
+ ...source,
25
+ path: resolveIfRelative(baseDir, source.path),
26
+ };
27
+ }),
28
+ },
29
+ });
30
+ const formatZodErrors = (error) => error.issues.map((issue) => {
31
+ const pathLabel = issue.path.length > 0 ? issue.path.join('.') : 'root';
32
+ return `${pathLabel}: ${issue.message}`;
33
+ });
34
+ const readConfigFile = (filePath) => {
35
+ try {
36
+ return fs.readFileSync(filePath, 'utf8');
37
+ }
38
+ catch (error) {
39
+ const err = error;
40
+ if (err.code === 'ENOENT') {
41
+ throw new UserError(`Config file not found at ${filePath}. Create watchfix.yaml or use --config to specify a path.`);
42
+ }
43
+ throw new UserError(`Failed to read config file at ${filePath}: ${err.message ?? String(err)}`);
44
+ }
45
+ };
46
+ const parseConfigYaml = (contents, filePath) => {
47
+ try {
48
+ return yaml.parse(contents);
49
+ }
50
+ catch (error) {
51
+ const err = error;
52
+ throw new UserError(`Failed to parse YAML in ${filePath}: ${err.message ?? String(err)}`);
53
+ }
54
+ };
55
+ const formatCheckSection = (label, entries) => {
56
+ if (entries.length === 0) {
57
+ return [];
58
+ }
59
+ return [label, ...entries.map((entry) => ` - ${entry}`)];
60
+ };
61
+ const checkAgentCli = (config) => {
62
+ const defaults = AGENT_DEFAULTS[config.agent.provider];
63
+ const command = config.agent.command ?? defaults.command;
64
+ const result = checkCliExists(command);
65
+ if (!result.exists) {
66
+ return {
67
+ info: `Agent CLI: ${command} (not found)`,
68
+ error: result.error ?? `Agent CLI '${command}' not found`,
69
+ };
70
+ }
71
+ const versionLabel = result.version ? `(${result.version})` : '(version unknown)';
72
+ return { info: `Agent CLI: ${command} ${versionLabel}` };
73
+ };
74
+ const describeAccessError = (error) => {
75
+ if (error.code === 'ENOENT') {
76
+ return 'not found';
77
+ }
78
+ if (error.code === 'EACCES') {
79
+ return 'permission denied';
80
+ }
81
+ return error.message ?? String(error);
82
+ };
83
+ const checkLogSourcePaths = (config) => {
84
+ const warnings = [];
85
+ for (const source of config.logs.sources) {
86
+ if (source.type !== 'file') {
87
+ continue;
88
+ }
89
+ try {
90
+ fs.accessSync(source.path, fs.constants.R_OK);
91
+ }
92
+ catch (error) {
93
+ const detail = describeAccessError(error);
94
+ warnings.push(`Log source '${source.name}' path not accessible: ${source.path} (${detail})`);
95
+ }
96
+ }
97
+ return warnings;
98
+ };
99
+ export const configValidateCommand = async (options) => {
100
+ const configPath = resolveConfigPath(options.config);
101
+ const contents = readConfigFile(configPath);
102
+ const rawConfig = parseConfigYaml(contents, configPath);
103
+ const validation = configSchema.safeParse(rawConfig);
104
+ const errors = [];
105
+ const warnings = [];
106
+ const info = [];
107
+ if (!validation.success) {
108
+ errors.push(...formatZodErrors(validation.error));
109
+ }
110
+ if (validation.success) {
111
+ const configDir = path.dirname(configPath);
112
+ const resolvedConfig = resolveConfigPaths(validation.data, configDir);
113
+ const agentCheck = checkAgentCli(resolvedConfig);
114
+ info.push(agentCheck.info);
115
+ if (agentCheck.error) {
116
+ errors.push(agentCheck.error);
117
+ }
118
+ warnings.push(...checkLogSourcePaths(resolvedConfig));
119
+ }
120
+ if (errors.length > 0) {
121
+ const lines = [
122
+ 'Configuration validation failed.',
123
+ ...formatCheckSection('Errors:', errors),
124
+ ...formatCheckSection('Warnings:', warnings),
125
+ ];
126
+ throw new UserError(lines.join('\n'));
127
+ }
128
+ const lines = [
129
+ 'Configuration is valid.',
130
+ ...info,
131
+ ...formatCheckSection('Warnings:', warnings),
132
+ ].filter(Boolean);
133
+ process.stdout.write(`${lines.join('\n')}\n`);
134
+ };
@@ -0,0 +1,12 @@
1
+ type FixOptions = {
2
+ config?: string;
3
+ verbose?: boolean;
4
+ quiet?: boolean;
5
+ all?: boolean;
6
+ yes?: boolean;
7
+ analyzeOnly?: boolean;
8
+ reanalyze?: boolean;
9
+ confirmEach?: boolean;
10
+ };
11
+ export declare const fixCommand: (id: string | undefined, options: FixOptions) => Promise<void>;
12
+ export {};
@@ -0,0 +1,391 @@
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 { getError, getErrorsByStatus } from '../../db/queries.js';
7
+ import { checkSchemaVersion, initializeSchema } from '../../db/schema.js';
8
+ import { FixOrchestrator } from '../../fixer/index.js';
9
+ import { EXIT_CODES, UserError } from '../../utils/errors.js';
10
+ import { Logger } from '../../utils/logger.js';
11
+ import { isOurProcess } from '../../utils/process.js';
12
+ const FIXABLE_STATUSES = ['pending', 'suggested'];
13
+ const buildDatabasePath = (rootDir) => path.join(rootDir, '.watchfix', 'errors.db');
14
+ const resolveVerbosity = (options) => {
15
+ if (options.quiet) {
16
+ return 'quiet';
17
+ }
18
+ if (options.verbose) {
19
+ return 'verbose';
20
+ }
21
+ return 'normal';
22
+ };
23
+ const getWatcherState = (db) => db.get('SELECT pid, started_at, autonomous, project_root, command_line FROM watcher_state WHERE id = 1');
24
+ const clearWatcherState = (db) => {
25
+ db.run('DELETE FROM watcher_state WHERE id = 1');
26
+ };
27
+ const parsePositiveInt = (value) => {
28
+ const parsed = Number.parseInt(value, 10);
29
+ if (!Number.isFinite(parsed) || parsed <= 0) {
30
+ throw new UserError('Error id must be a positive integer.');
31
+ }
32
+ return parsed;
33
+ };
34
+ const parseStoredAnalysis = (value) => {
35
+ if (!value) {
36
+ return null;
37
+ }
38
+ try {
39
+ const parsed = JSON.parse(value);
40
+ if (!parsed ||
41
+ typeof parsed.summary !== 'string' ||
42
+ typeof parsed.root_cause !== 'string' ||
43
+ typeof parsed.suggested_fix !== 'string' ||
44
+ !Array.isArray(parsed.files_to_modify) ||
45
+ typeof parsed.confidence !== 'string') {
46
+ return null;
47
+ }
48
+ return parsed;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ };
54
+ const formatAnalysisSummary = (analysis) => {
55
+ const lines = ['Analysis summary:'];
56
+ lines.push(` Summary: ${analysis.summary}`);
57
+ lines.push(' Root cause:');
58
+ lines.push(...analysis.root_cause.split('\n').map((line) => ` ${line}`));
59
+ lines.push(' Suggested fix:');
60
+ lines.push(...analysis.suggested_fix.split('\n').map((line) => ` ${line}`));
61
+ lines.push(' Files to modify:');
62
+ if (analysis.files_to_modify.length === 0) {
63
+ lines.push(' (none)');
64
+ }
65
+ else {
66
+ for (const file of analysis.files_to_modify) {
67
+ lines.push(` - ${file}`);
68
+ }
69
+ }
70
+ lines.push(` Confidence: ${analysis.confidence}`);
71
+ return lines;
72
+ };
73
+ const promptForConfirmation = async (label) => {
74
+ if (!process.stdin.isTTY) {
75
+ throw new UserError('Cannot prompt for confirmation in non-interactive mode. Use --yes to proceed.');
76
+ }
77
+ const rl = readline.createInterface({
78
+ input: process.stdin,
79
+ output: process.stdout,
80
+ });
81
+ try {
82
+ const answer = await rl.question(`${label} [y/N] `);
83
+ const normalized = answer.trim().toLowerCase();
84
+ return normalized === 'y' || normalized === 'yes';
85
+ }
86
+ finally {
87
+ rl.close();
88
+ }
89
+ };
90
+ const formatFixFailure = (result) => {
91
+ if (result.verification?.failure) {
92
+ return result.verification.failure.message;
93
+ }
94
+ return result.message ?? 'Fix did not complete successfully.';
95
+ };
96
+ const checkDaemonConflict = (db, logger) => {
97
+ const state = getWatcherState(db);
98
+ if (!state) {
99
+ return true;
100
+ }
101
+ if (!isOurProcess(state.pid, state.project_root)) {
102
+ clearWatcherState(db);
103
+ return true;
104
+ }
105
+ if (state.autonomous) {
106
+ const message = 'Cannot run manual fix while daemon is in autonomous mode.\n' +
107
+ 'The daemon will automatically fix errors.\n' +
108
+ "Run 'watchfix stop' first if you want manual control.";
109
+ console.error(message);
110
+ process.exitCode = EXIT_CODES.WATCHER_CONFLICT;
111
+ return false;
112
+ }
113
+ logger.debug('Daemon running in manual mode, proceeding with fix');
114
+ return true;
115
+ };
116
+ const ensureFixable = (error) => {
117
+ if (FIXABLE_STATUSES.includes(error.status)) {
118
+ return null;
119
+ }
120
+ if (error.status === 'analyzing' || error.status === 'fixing') {
121
+ return `Error #${error.id} is currently ${error.status}.`;
122
+ }
123
+ return `Error #${error.id} is not in a fixable state (status=${error.status}).`;
124
+ };
125
+ const reportAnalysis = (errorId, analysis, note) => {
126
+ if (note) {
127
+ process.stdout.write(`${note}\n`);
128
+ }
129
+ if (!analysis) {
130
+ process.stdout.write(`No analysis available for error #${errorId}.\n`);
131
+ return;
132
+ }
133
+ const lines = [`Error #${errorId}`, ...formatAnalysisSummary(analysis)];
134
+ process.stdout.write(`${lines.join('\n')}\n`);
135
+ };
136
+ const reportAnalysisFromResult = (db, errorId, result, note) => {
137
+ const analysis = result.analysis ??
138
+ parseStoredAnalysis(getError(db, errorId)?.suggestion ?? null);
139
+ reportAnalysis(errorId, analysis, note);
140
+ };
141
+ const reportFixResult = (result) => {
142
+ if (!result.lockAcquired) {
143
+ process.stdout.write(`Skipped error #${result.errorId}: already locked by another process.\n`);
144
+ return 'skipped';
145
+ }
146
+ if (result.status === 'fixed') {
147
+ process.stdout.write(`✓ Error #${result.errorId} fixed successfully.\n`);
148
+ return 'fixed';
149
+ }
150
+ const message = formatFixFailure(result);
151
+ process.stdout.write(`✗ Error #${result.errorId} not fixed (status=${result.status}).\n${message}\n`);
152
+ return 'failed';
153
+ };
154
+ const runSingleFix = async (db, error, options, orchestrator) => {
155
+ const reanalyze = Boolean(options.reanalyze);
156
+ const analyzeOnly = Boolean(options.analyzeOnly);
157
+ const shouldPrompt = !options.yes && !analyzeOnly;
158
+ let reanalyzeForFix = reanalyze;
159
+ if (analyzeOnly) {
160
+ const result = await orchestrator.fixError(error.id, {
161
+ analyzeOnly: true,
162
+ reanalyze,
163
+ });
164
+ if (!result.lockAcquired) {
165
+ process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
166
+ return reportFixResult(result);
167
+ }
168
+ if (result.status !== 'suggested') {
169
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
170
+ reportFixResult(result);
171
+ return 'failed';
172
+ }
173
+ reportAnalysis(error.id, result.analysis ??
174
+ parseStoredAnalysis(getError(db, error.id)?.suggestion ?? null), result.message);
175
+ return 'analysis';
176
+ }
177
+ if (shouldPrompt) {
178
+ let analysis = parseStoredAnalysis(error.suggestion);
179
+ let analysisNote;
180
+ if (error.status === 'pending' || reanalyze) {
181
+ const analysisResult = await orchestrator.fixError(error.id, {
182
+ analyzeOnly: true,
183
+ reanalyze,
184
+ });
185
+ if (!analysisResult.lockAcquired) {
186
+ process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
187
+ return reportFixResult(analysisResult);
188
+ }
189
+ analysisNote = analysisResult.message;
190
+ analysis =
191
+ analysisResult.analysis ??
192
+ parseStoredAnalysis(getError(db, error.id)?.suggestion ?? null);
193
+ if (analysisResult.status !== 'suggested') {
194
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
195
+ return reportFixResult(analysisResult);
196
+ }
197
+ reanalyzeForFix = false;
198
+ }
199
+ if (analysis) {
200
+ reportAnalysis(error.id, analysis, analysisNote);
201
+ }
202
+ else if (analysisNote) {
203
+ process.stdout.write(`${analysisNote}\n`);
204
+ }
205
+ const confirmed = await promptForConfirmation(`Apply fix for error #${error.id}?`);
206
+ if (!confirmed) {
207
+ process.stdout.write(`Skipped error #${error.id}.\n`);
208
+ return 'skipped';
209
+ }
210
+ }
211
+ const result = await orchestrator.fixError(error.id, {
212
+ reanalyze: reanalyzeForFix,
213
+ });
214
+ if (!result.lockAcquired) {
215
+ process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
216
+ return reportFixResult(result);
217
+ }
218
+ reportAnalysisFromResult(db, error.id, result);
219
+ if (result.status !== 'fixed') {
220
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
221
+ }
222
+ return reportFixResult(result);
223
+ };
224
+ const runAllFixes = async (db, options, orchestrator) => {
225
+ const errors = getErrorsByStatus(db, FIXABLE_STATUSES);
226
+ if (errors.length === 0) {
227
+ process.stdout.write('No pending or suggested errors to fix.\n');
228
+ return;
229
+ }
230
+ const analyzeOnly = Boolean(options.analyzeOnly);
231
+ const reanalyze = Boolean(options.reanalyze);
232
+ const confirmEach = Boolean(options.confirmEach) && !analyzeOnly;
233
+ let fixedCount = 0;
234
+ let failedCount = 0;
235
+ let skippedCount = 0;
236
+ let analyzedCount = 0;
237
+ for (const error of errors) {
238
+ const fixabilityIssue = ensureFixable(error);
239
+ if (fixabilityIssue) {
240
+ process.stdout.write(`${fixabilityIssue} Skipping.\n`);
241
+ skippedCount += 1;
242
+ continue;
243
+ }
244
+ if (analyzeOnly) {
245
+ const result = await orchestrator.fixError(error.id, {
246
+ analyzeOnly: true,
247
+ reanalyze,
248
+ });
249
+ if (!result.lockAcquired) {
250
+ skippedCount += 1;
251
+ reportFixResult(result);
252
+ continue;
253
+ }
254
+ if (result.status !== 'suggested') {
255
+ failedCount += 1;
256
+ reportFixResult(result);
257
+ continue;
258
+ }
259
+ reportAnalysis(error.id, result.analysis ?? parseStoredAnalysis(getError(db, error.id)?.suggestion ?? null), result.message);
260
+ analyzedCount += 1;
261
+ continue;
262
+ }
263
+ if (confirmEach) {
264
+ let analysis = parseStoredAnalysis(error.suggestion);
265
+ let note;
266
+ let reanalyzeForFix = reanalyze;
267
+ if (error.status === 'pending' || reanalyze) {
268
+ const analysisResult = await orchestrator.fixError(error.id, {
269
+ analyzeOnly: true,
270
+ reanalyze,
271
+ });
272
+ if (!analysisResult.lockAcquired) {
273
+ skippedCount += 1;
274
+ reportFixResult(analysisResult);
275
+ continue;
276
+ }
277
+ note = analysisResult.message;
278
+ analysis =
279
+ analysisResult.analysis ??
280
+ parseStoredAnalysis(getError(db, error.id)?.suggestion ?? null);
281
+ if (analysisResult.status !== 'suggested') {
282
+ failedCount += 1;
283
+ reportFixResult(analysisResult);
284
+ continue;
285
+ }
286
+ reanalyzeForFix = false;
287
+ }
288
+ if (analysis) {
289
+ reportAnalysis(error.id, analysis, note);
290
+ }
291
+ else if (note) {
292
+ process.stdout.write(`${note}\n`);
293
+ }
294
+ const confirmed = await promptForConfirmation(`Apply fix for error #${error.id}?`);
295
+ if (!confirmed) {
296
+ process.stdout.write(`Skipped error #${error.id}.\n`);
297
+ skippedCount += 1;
298
+ continue;
299
+ }
300
+ const result = await orchestrator.fixError(error.id, {
301
+ reanalyze: reanalyzeForFix,
302
+ });
303
+ if (!result.lockAcquired) {
304
+ skippedCount += 1;
305
+ reportFixResult(result);
306
+ continue;
307
+ }
308
+ if (result.status === 'fixed') {
309
+ fixedCount += 1;
310
+ }
311
+ else {
312
+ failedCount += 1;
313
+ }
314
+ reportFixResult(result);
315
+ continue;
316
+ }
317
+ const result = await orchestrator.fixError(error.id, { reanalyze });
318
+ if (!result.lockAcquired) {
319
+ skippedCount += 1;
320
+ reportFixResult(result);
321
+ continue;
322
+ }
323
+ reportAnalysisFromResult(db, error.id, result);
324
+ if (result.status === 'fixed') {
325
+ fixedCount += 1;
326
+ }
327
+ else {
328
+ failedCount += 1;
329
+ }
330
+ reportFixResult(result);
331
+ }
332
+ const summary = analyzeOnly
333
+ ? `Analyzed ${analyzedCount} errors, failed ${failedCount}, skipped ${skippedCount}.`
334
+ : `Summary: fixed ${fixedCount}, failed ${failedCount}, skipped ${skippedCount}.`;
335
+ process.stdout.write(`${summary}\n`);
336
+ if (!analyzeOnly && failedCount > 0) {
337
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
338
+ }
339
+ };
340
+ export const fixCommand = async (id, options) => {
341
+ if (options.all && id) {
342
+ throw new UserError('Specify either an error id or --all, not both.');
343
+ }
344
+ if (!options.all && !id) {
345
+ throw new UserError('Error id is required unless --all is specified.');
346
+ }
347
+ const config = loadConfig(options.config);
348
+ const verbosity = resolveVerbosity(options);
349
+ const logger = new Logger({
350
+ rootDir: config.project.root,
351
+ terminalEnabled: true,
352
+ verbosity,
353
+ });
354
+ const dbPath = buildDatabasePath(config.project.root);
355
+ if (!fs.existsSync(dbPath)) {
356
+ throw new UserError(`No database found at ${dbPath}. Run watchfix watch to create it.`);
357
+ }
358
+ const db = new Database(dbPath);
359
+ try {
360
+ initializeSchema(db);
361
+ checkSchemaVersion(db);
362
+ if (!checkDaemonConflict(db, logger)) {
363
+ return;
364
+ }
365
+ const orchestrator = new FixOrchestrator(db, config, {
366
+ logger,
367
+ terminalEnabled: true,
368
+ });
369
+ if (options.all) {
370
+ await runAllFixes(db, options, orchestrator);
371
+ return;
372
+ }
373
+ const errorId = parsePositiveInt(id);
374
+ const error = getError(db, errorId);
375
+ if (!error) {
376
+ console.error(`Error #${errorId} not found.`);
377
+ process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
378
+ return;
379
+ }
380
+ const fixabilityIssue = ensureFixable(error);
381
+ if (fixabilityIssue) {
382
+ console.error(fixabilityIssue);
383
+ process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
384
+ return;
385
+ }
386
+ await runSingleFix(db, error, options, orchestrator);
387
+ }
388
+ finally {
389
+ db.close();
390
+ }
391
+ };
@@ -0,0 +1,7 @@
1
+ type IgnoreOptions = {
2
+ config?: string;
3
+ verbose?: boolean;
4
+ quiet?: boolean;
5
+ };
6
+ export declare const ignoreCommand: (id: string, options: IgnoreOptions) => Promise<void>;
7
+ export {};
@@ -0,0 +1,74 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadConfig } from '../../config/loader.js';
4
+ import { Database } from '../../db/index.js';
5
+ import { getError, logActivity } from '../../db/queries.js';
6
+ import { checkSchemaVersion, initializeSchema } from '../../db/schema.js';
7
+ import { acquireLock, generateLockId, releaseLock, transitionStatus } from '../../fixer/lock.js';
8
+ import { EXIT_CODES, UserError } from '../../utils/errors.js';
9
+ const IGNORABLE_STATUSES = ['pending', 'suggested', 'failed'];
10
+ const buildDatabasePath = (rootDir) => path.join(rootDir, '.watchfix', 'errors.db');
11
+ const parsePositiveInt = (value) => {
12
+ const parsed = Number.parseInt(value, 10);
13
+ if (!Number.isFinite(parsed) || parsed <= 0) {
14
+ throw new UserError('Error id must be a positive integer.');
15
+ }
16
+ return parsed;
17
+ };
18
+ const ensureIgnorable = (error) => {
19
+ if (IGNORABLE_STATUSES.includes(error.status)) {
20
+ return null;
21
+ }
22
+ if (error.status === 'analyzing' || error.status === 'fixing') {
23
+ return `Error #${error.id} is currently ${error.status}.`;
24
+ }
25
+ return `Error #${error.id} is not in an ignorable state (status=${error.status}).`;
26
+ };
27
+ export const ignoreCommand = async (id, options) => {
28
+ const errorId = parsePositiveInt(id);
29
+ const config = loadConfig(options.config);
30
+ const dbPath = buildDatabasePath(config.project.root);
31
+ if (!fs.existsSync(dbPath)) {
32
+ throw new UserError(`No database found at ${dbPath}. Run watchfix watch to create it.`);
33
+ }
34
+ const db = new Database(dbPath);
35
+ try {
36
+ initializeSchema(db);
37
+ checkSchemaVersion(db);
38
+ const error = getError(db, errorId);
39
+ if (!error) {
40
+ console.error(`Error #${errorId} not found.`);
41
+ process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
42
+ return;
43
+ }
44
+ const ignorableIssue = ensureIgnorable(error);
45
+ if (ignorableIssue) {
46
+ console.error(ignorableIssue);
47
+ process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
48
+ return;
49
+ }
50
+ const lockId = generateLockId();
51
+ const lockAcquired = await acquireLock(db, errorId, lockId);
52
+ if (!lockAcquired) {
53
+ console.error(`Error #${errorId} is locked by another process.`);
54
+ process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
55
+ return;
56
+ }
57
+ try {
58
+ const updated = transitionStatus(db, errorId, IGNORABLE_STATUSES, 'ignored', lockId);
59
+ if (!updated) {
60
+ console.error(`Error #${errorId} could not be ignored because it changed state.`);
61
+ process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
62
+ return;
63
+ }
64
+ logActivity(db, 'error_ignored', errorId);
65
+ process.stdout.write(`Ignored error #${errorId}.\n`);
66
+ }
67
+ finally {
68
+ await releaseLock(db, errorId, lockId);
69
+ }
70
+ }
71
+ finally {
72
+ db.close();
73
+ }
74
+ };
@@ -0,0 +1,5 @@
1
+ export type InitOptions = {
2
+ agent?: string;
3
+ force?: boolean;
4
+ };
5
+ export declare const initCommand: (options: InitOptions) => Promise<void>;