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,115 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { UserError } from '../../utils/errors.js';
4
+ const AGENT_PROVIDERS = new Set(['claude', 'gemini', 'codex']);
5
+ const buildTemplate = (projectName, provider) => `# watchfix.yaml
6
+ # Generated by watchfix init
7
+
8
+ project:
9
+ name: ${projectName}
10
+ root: .
11
+
12
+ agent:
13
+ provider: ${provider}
14
+ timeout: 5m
15
+ retries: 2
16
+ # command: ${provider}
17
+ # args:
18
+ # - --model
19
+ # - sonnet
20
+ # - --dangerously-skip-permissions
21
+ # - -p
22
+ # stderr_is_progress: false
23
+
24
+ logs:
25
+ sources:
26
+ - name: app
27
+ type: file
28
+ path: ./logs/app.log
29
+
30
+ # Example docker source:
31
+ # - name: api
32
+ # type: docker
33
+ # container: my-app-api
34
+
35
+ # Example command source:
36
+ # - name: compose
37
+ # type: command
38
+ # run: docker-compose logs --tail=100
39
+ # interval: 30s
40
+
41
+ context_lines_before: 10
42
+ context_lines_after: 5
43
+ max_line_buffer: 10000
44
+
45
+ verification:
46
+ test_commands:
47
+ - npm run lint
48
+ - npm test
49
+ test_command_timeout: 5m
50
+ health_checks:
51
+ - http://localhost:3000/health
52
+ health_check_timeout: 10s
53
+ wait_after_fix: 5s
54
+
55
+ limits:
56
+ max_attempts_per_error: 3
57
+
58
+ cleanup:
59
+ context_max_age_days: 7
60
+ context_max_size_kb: 256
61
+
62
+ patterns:
63
+ match:
64
+ - "FATAL:"
65
+ - "panic:"
66
+ ignore:
67
+ - "DeprecationWarning"
68
+ - "ExperimentalWarning"
69
+ `;
70
+ const ensureGitignore = async (cwd) => {
71
+ const gitignorePath = path.join(cwd, '.gitignore');
72
+ let contents = '';
73
+ try {
74
+ contents = await fs.readFile(gitignorePath, 'utf8');
75
+ }
76
+ catch (error) {
77
+ if (error.code !== 'ENOENT') {
78
+ throw error;
79
+ }
80
+ }
81
+ const hasEntry = contents
82
+ .split(/\r?\n/)
83
+ .some((line) => line.trim() === '.watchfix/');
84
+ if (hasEntry) {
85
+ return;
86
+ }
87
+ const trimmed = contents.replace(/\s+$/u, '');
88
+ const prefix = trimmed.length > 0 ? `${trimmed}\n` : '';
89
+ const updated = `${prefix}.watchfix/\n`;
90
+ await fs.writeFile(gitignorePath, updated, 'utf8');
91
+ };
92
+ export const initCommand = async (options) => {
93
+ const cwd = process.cwd();
94
+ const configPath = path.join(cwd, 'watchfix.yaml');
95
+ const projectName = path.basename(cwd) || 'my-project';
96
+ const provider = (options.agent ?? 'claude').toLowerCase();
97
+ if (!AGENT_PROVIDERS.has(provider)) {
98
+ throw new UserError(`Unknown agent provider: ${options.agent ?? provider}. ` +
99
+ 'Use one of: claude, gemini, codex.');
100
+ }
101
+ try {
102
+ await fs.access(configPath);
103
+ if (!options.force) {
104
+ throw new UserError('watchfix.yaml already exists. Use --force to overwrite.');
105
+ }
106
+ }
107
+ catch (error) {
108
+ if (error.code !== 'ENOENT') {
109
+ throw error;
110
+ }
111
+ }
112
+ const template = buildTemplate(projectName, provider);
113
+ await fs.writeFile(configPath, template, 'utf8');
114
+ await ensureGitignore(cwd);
115
+ };
@@ -0,0 +1,9 @@
1
+ type LogsOptions = {
2
+ config?: string;
3
+ lines?: string | number;
4
+ tail?: boolean;
5
+ verbose?: boolean;
6
+ quiet?: boolean;
7
+ };
8
+ export declare const logsCommand: (options: LogsOptions) => Promise<void>;
9
+ export {};
@@ -0,0 +1,106 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadConfig } from '../../config/loader.js';
4
+ import { UserError } from '../../utils/errors.js';
5
+ import { DEFAULT_LOG_PATH } from '../../utils/logger.js';
6
+ const DEFAULT_LINES = 50;
7
+ const parseLineCount = (value) => {
8
+ if (value === undefined) {
9
+ return DEFAULT_LINES;
10
+ }
11
+ if (typeof value === 'number') {
12
+ if (!Number.isInteger(value) || value <= 0) {
13
+ throw new UserError('Line count must be a positive integer.');
14
+ }
15
+ return value;
16
+ }
17
+ const trimmed = value.trim();
18
+ if (!/^\d+$/.test(trimmed)) {
19
+ throw new UserError('Line count must be a positive integer.');
20
+ }
21
+ const parsed = Number(trimmed);
22
+ if (!Number.isFinite(parsed) || parsed <= 0) {
23
+ throw new UserError('Line count must be a positive integer.');
24
+ }
25
+ return parsed;
26
+ };
27
+ const resolveLogPath = (configPath) => {
28
+ const config = loadConfig(configPath);
29
+ return path.join(config.project.root, DEFAULT_LOG_PATH);
30
+ };
31
+ const readLogLines = (logPath) => {
32
+ const contents = fs.readFileSync(logPath, 'utf8');
33
+ const lines = contents.split('\n');
34
+ if (lines.length > 0 && lines[lines.length - 1] === '') {
35
+ lines.pop();
36
+ }
37
+ return lines;
38
+ };
39
+ const outputLastLines = (logPath, lineCount) => {
40
+ const lines = readLogLines(logPath);
41
+ const startIndex = Math.max(0, lines.length - lineCount);
42
+ const output = lines.slice(startIndex);
43
+ if (output.length === 0) {
44
+ return;
45
+ }
46
+ process.stdout.write(`${output.join('\n')}\n`);
47
+ };
48
+ const readNewEntries = (logPath, start) => {
49
+ const stats = fs.statSync(logPath);
50
+ if (stats.size < start) {
51
+ return 0;
52
+ }
53
+ if (stats.size === start) {
54
+ return start;
55
+ }
56
+ const length = stats.size - start;
57
+ const buffer = Buffer.alloc(length);
58
+ const fd = fs.openSync(logPath, 'r');
59
+ try {
60
+ fs.readSync(fd, buffer, 0, length, start);
61
+ }
62
+ finally {
63
+ fs.closeSync(fd);
64
+ }
65
+ process.stdout.write(buffer.toString('utf8'));
66
+ return stats.size;
67
+ };
68
+ const tailLog = (logPath) => {
69
+ let position = fs.existsSync(logPath) ? fs.statSync(logPath).size : 0;
70
+ const watchDir = path.dirname(logPath);
71
+ const fileName = path.basename(logPath);
72
+ const watcher = fs.watch(watchDir, { persistent: true }, (event, changed) => {
73
+ if (changed && changed !== fileName) {
74
+ return;
75
+ }
76
+ if (event === 'rename' && !fs.existsSync(logPath)) {
77
+ position = 0;
78
+ return;
79
+ }
80
+ try {
81
+ position = readNewEntries(logPath, position);
82
+ }
83
+ catch (error) {
84
+ const err = error;
85
+ if (err.code === 'ENOENT') {
86
+ position = 0;
87
+ return;
88
+ }
89
+ throw error;
90
+ }
91
+ });
92
+ process.on('SIGINT', () => {
93
+ watcher.close();
94
+ });
95
+ };
96
+ export const logsCommand = async (options) => {
97
+ const logPath = resolveLogPath(options.config);
98
+ if (!fs.existsSync(logPath)) {
99
+ throw new UserError(`No log file found at ${logPath}. Start watchfix watch to create it.`);
100
+ }
101
+ const lineCount = parseLineCount(options.lines);
102
+ outputLastLines(logPath, lineCount);
103
+ if (options.tail) {
104
+ tailLog(logPath);
105
+ }
106
+ };
@@ -0,0 +1,8 @@
1
+ type ShowOptions = {
2
+ config?: string;
3
+ json?: boolean;
4
+ verbose?: boolean;
5
+ quiet?: boolean;
6
+ };
7
+ export declare const showCommand: (id: string, options: ShowOptions) => Promise<void>;
8
+ export {};
@@ -0,0 +1,165 @@
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 } from '../../db/queries.js';
6
+ import { checkSchemaVersion, initializeSchema } from '../../db/schema.js';
7
+ import { UserError } from '../../utils/errors.js';
8
+ const buildDatabasePath = (rootDir) => path.join(rootDir, '.watchfix', 'errors.db');
9
+ const parsePositiveInt = (value) => {
10
+ const parsed = Number.parseInt(value, 10);
11
+ if (!Number.isFinite(parsed) || parsed <= 0) {
12
+ throw new UserError('Error id must be a positive integer.');
13
+ }
14
+ return parsed;
15
+ };
16
+ const parseJsonMaybe = (value) => {
17
+ if (!value) {
18
+ return null;
19
+ }
20
+ try {
21
+ return JSON.parse(value);
22
+ }
23
+ catch {
24
+ return value;
25
+ }
26
+ };
27
+ const getActivityLog = (db, errorId) => db.all('SELECT id, timestamp, action, error_id, details FROM activity_log WHERE error_id = ? ORDER BY timestamp ASC, id ASC', [errorId]);
28
+ const formatMultiline = (label, value) => {
29
+ if (!value) {
30
+ return [`${label}: (none)`];
31
+ }
32
+ const lines = value.split('\n');
33
+ return [`${label}:`, ...lines.map((line) => ` ${line}`)];
34
+ };
35
+ const formatErrorDetails = (error) => [
36
+ `ID: ${error.id}`,
37
+ `Hash: ${error.hash}`,
38
+ `Source: ${error.source}`,
39
+ `Timestamp: ${error.timestamp}`,
40
+ `Error type: ${error.errorType}`,
41
+ `Message: ${error.message}`,
42
+ `Status: ${error.status}`,
43
+ `Fix attempts: ${error.fixAttempts}`,
44
+ `Locked by: ${error.lockedBy ?? '(none)'}`,
45
+ `Locked at: ${error.lockedAt ?? '(none)'}`,
46
+ `Created at: ${error.createdAt}`,
47
+ `Updated at: ${error.updatedAt}`,
48
+ `Suggestion (raw): ${error.suggestion ?? '(none)'}`,
49
+ `Fix result (raw): ${error.fixResult ?? '(none)'}`,
50
+ ];
51
+ const formatAnalysis = (analysis) => {
52
+ if (!analysis) {
53
+ return ['Analysis: (none)'];
54
+ }
55
+ if (typeof analysis === 'string') {
56
+ return ['Analysis (raw):', ` ${analysis}`];
57
+ }
58
+ const lines = ['Analysis:'];
59
+ if (analysis.summary) {
60
+ lines.push(` Summary: ${analysis.summary}`);
61
+ }
62
+ if (analysis.root_cause) {
63
+ lines.push(' Root cause:');
64
+ lines.push(...analysis.root_cause.split('\n').map((line) => ` ${line}`));
65
+ }
66
+ if (analysis.suggested_fix) {
67
+ lines.push(' Suggested fix:');
68
+ lines.push(...analysis.suggested_fix.split('\n').map((line) => ` ${line}`));
69
+ }
70
+ if (analysis.files_to_modify && analysis.files_to_modify.length > 0) {
71
+ lines.push(' Files to modify:');
72
+ for (const file of analysis.files_to_modify) {
73
+ lines.push(` - ${file}`);
74
+ }
75
+ }
76
+ if (analysis.confidence) {
77
+ lines.push(` Confidence: ${analysis.confidence}`);
78
+ }
79
+ if (lines.length === 1) {
80
+ lines.push(' (empty)');
81
+ }
82
+ return lines;
83
+ };
84
+ const formatFixResult = (result) => {
85
+ if (!result) {
86
+ return ['Fix result: (none)'];
87
+ }
88
+ if (typeof result === 'string') {
89
+ return ['Fix result (raw):', ` ${result}`];
90
+ }
91
+ const lines = ['Fix result:'];
92
+ if (typeof result.success === 'boolean') {
93
+ lines.push(` Success: ${result.success ? 'true' : 'false'}`);
94
+ }
95
+ if (result.summary) {
96
+ lines.push(` Summary: ${result.summary}`);
97
+ }
98
+ if (result.files_changed && result.files_changed.length > 0) {
99
+ lines.push(' Files changed:');
100
+ for (const file of result.files_changed) {
101
+ lines.push(` - ${file.path}: ${file.change}`);
102
+ }
103
+ }
104
+ if (result.notes) {
105
+ lines.push(' Notes:');
106
+ lines.push(...result.notes.split('\n').map((line) => ` ${line}`));
107
+ }
108
+ if (lines.length === 1) {
109
+ lines.push(' (empty)');
110
+ }
111
+ return lines;
112
+ };
113
+ const formatActivityLog = (entries) => {
114
+ if (entries.length === 0) {
115
+ return ['Activity log: (none)'];
116
+ }
117
+ const lines = ['Activity log:'];
118
+ for (const entry of entries) {
119
+ const details = entry.details ? ` - ${entry.details}` : '';
120
+ lines.push(` [${entry.timestamp}] ${entry.action}${details}`.trimEnd());
121
+ }
122
+ return lines;
123
+ };
124
+ export const showCommand = async (id, options) => {
125
+ const errorId = parsePositiveInt(id);
126
+ const config = loadConfig(options.config);
127
+ const dbPath = buildDatabasePath(config.project.root);
128
+ if (!fs.existsSync(dbPath)) {
129
+ throw new UserError(`No database found at ${dbPath}. Run watchfix watch to create it.`);
130
+ }
131
+ const db = new Database(dbPath);
132
+ try {
133
+ initializeSchema(db);
134
+ checkSchemaVersion(db);
135
+ const error = getError(db, errorId);
136
+ if (!error) {
137
+ throw new UserError(`Error #${errorId} not found.`);
138
+ }
139
+ const analysis = parseJsonMaybe(error.suggestion);
140
+ const fixResult = parseJsonMaybe(error.fixResult);
141
+ const activityLog = getActivityLog(db, errorId);
142
+ if (options.json) {
143
+ const payload = {
144
+ error,
145
+ analysis,
146
+ fixResult,
147
+ activityLog,
148
+ };
149
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
150
+ return;
151
+ }
152
+ const lines = [];
153
+ lines.push(`Error #${error.id}`);
154
+ lines.push(...formatErrorDetails(error));
155
+ lines.push(...formatMultiline('Stack trace', error.stackTrace));
156
+ lines.push(...formatMultiline('Raw log', error.rawLog));
157
+ lines.push(...formatAnalysis(analysis));
158
+ lines.push(...formatFixResult(fixResult));
159
+ lines.push(...formatActivityLog(activityLog));
160
+ process.stdout.write(`${lines.join('\n')}\n`);
161
+ }
162
+ finally {
163
+ db.close();
164
+ }
165
+ };
@@ -0,0 +1,7 @@
1
+ type StatusOptions = {
2
+ config?: string;
3
+ verbose?: boolean;
4
+ quiet?: boolean;
5
+ };
6
+ export declare const statusCommand: (options: StatusOptions) => Promise<void>;
7
+ export {};
@@ -0,0 +1,110 @@
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 { getErrorsByStatus } from '../../db/queries.js';
6
+ import { checkSchemaVersion, initializeSchema } from '../../db/schema.js';
7
+ import { isOurProcess } from '../../utils/process.js';
8
+ const STATUS_ORDER = [
9
+ 'pending',
10
+ 'analyzing',
11
+ 'suggested',
12
+ 'fixing',
13
+ 'fixed',
14
+ 'failed',
15
+ 'ignored',
16
+ ];
17
+ const buildDatabasePath = (rootDir) => path.join(rootDir, '.watchfix', 'errors.db');
18
+ const getWatcherState = (db) => db.get('SELECT pid, started_at, autonomous, project_root, command_line FROM watcher_state WHERE id = 1');
19
+ const clearWatcherState = (db) => {
20
+ db.run('DELETE FROM watcher_state WHERE id = 1');
21
+ };
22
+ const formatUptime = (startedAt) => {
23
+ const startedMs = Date.parse(startedAt);
24
+ if (!Number.isFinite(startedMs)) {
25
+ return 'unknown';
26
+ }
27
+ const diffMs = Date.now() - startedMs;
28
+ if (diffMs < 0) {
29
+ return 'unknown';
30
+ }
31
+ const totalSeconds = Math.floor(diffMs / 1000);
32
+ const hours = Math.floor(totalSeconds / 3600);
33
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
34
+ const seconds = totalSeconds % 60;
35
+ const parts = [];
36
+ if (hours > 0) {
37
+ parts.push(`${hours}h`);
38
+ }
39
+ if (minutes > 0 || hours > 0) {
40
+ parts.push(`${minutes}m`);
41
+ }
42
+ parts.push(`${seconds}s`);
43
+ return parts.join(' ');
44
+ };
45
+ const loadStatusCounts = (db) => {
46
+ const counts = Object.fromEntries(STATUS_ORDER.map((status) => [status, 0]));
47
+ const rows = db.all('SELECT status, COUNT(*) as count FROM errors GROUP BY status');
48
+ for (const row of rows) {
49
+ if (row.status in counts) {
50
+ counts[row.status] = row.count;
51
+ }
52
+ }
53
+ return counts;
54
+ };
55
+ const formatActionableErrors = (errors) => {
56
+ if (errors.length === 0) {
57
+ return ['Actionable errors: none'];
58
+ }
59
+ const lines = ['Actionable errors:'];
60
+ for (const error of errors) {
61
+ lines.push(` #${error.id} ${error.errorType} (${error.source}): ${error.message}`);
62
+ }
63
+ return lines;
64
+ };
65
+ export const statusCommand = async (options) => {
66
+ const config = loadConfig(options.config);
67
+ const dbPath = buildDatabasePath(config.project.root);
68
+ if (!fs.existsSync(dbPath)) {
69
+ const lines = [
70
+ 'Watcher: not running.',
71
+ 'Errors:',
72
+ ...STATUS_ORDER.map((status) => ` ${status}: 0`),
73
+ 'Actionable errors: none',
74
+ ];
75
+ process.stdout.write(`${lines.join('\n')}\n`);
76
+ return;
77
+ }
78
+ const db = new Database(dbPath);
79
+ try {
80
+ initializeSchema(db);
81
+ checkSchemaVersion(db);
82
+ const lines = [];
83
+ const state = getWatcherState(db);
84
+ if (state && isOurProcess(state.pid, state.project_root)) {
85
+ const mode = state.autonomous ? 'autonomous' : 'manual';
86
+ const uptime = formatUptime(state.started_at);
87
+ lines.push(`Watcher: running (pid ${state.pid}, ${mode} mode, uptime ${uptime}).`);
88
+ }
89
+ else {
90
+ if (state) {
91
+ clearWatcherState(db);
92
+ lines.push('Watcher: not running (cleared stale state).');
93
+ }
94
+ else {
95
+ lines.push('Watcher: not running.');
96
+ }
97
+ }
98
+ const counts = loadStatusCounts(db);
99
+ lines.push('Errors:');
100
+ for (const status of STATUS_ORDER) {
101
+ lines.push(` ${status}: ${counts[status]}`);
102
+ }
103
+ const actionable = getErrorsByStatus(db, ['pending', 'suggested']);
104
+ lines.push(...formatActionableErrors(actionable));
105
+ process.stdout.write(`${lines.join('\n')}\n`);
106
+ }
107
+ finally {
108
+ db.close();
109
+ }
110
+ };
@@ -0,0 +1,7 @@
1
+ type StopOptions = {
2
+ config?: string;
3
+ verbose?: boolean;
4
+ quiet?: boolean;
5
+ };
6
+ export declare const stopCommand: (options: StopOptions) => Promise<void>;
7
+ export {};
@@ -0,0 +1,106 @@
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 { checkSchemaVersion, initializeSchema } from '../../db/schema.js';
6
+ import { EXIT_CODES, UserError } from '../../utils/errors.js';
7
+ import { isOurProcess } from '../../utils/process.js';
8
+ const buildDatabasePath = (rootDir) => path.join(rootDir, '.watchfix', 'errors.db');
9
+ const getWatcherState = (db) => db.get('SELECT pid, started_at, autonomous, project_root, command_line FROM watcher_state WHERE id = 1');
10
+ const clearWatcherState = (db) => {
11
+ db.run('DELETE FROM watcher_state WHERE id = 1');
12
+ };
13
+ const sleep = async (ms) => await new Promise((resolve) => setTimeout(resolve, ms));
14
+ const isProcessRunning = (pid) => {
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ };
23
+ const waitForProcessExit = async (pid, timeoutMs) => {
24
+ const deadline = Date.now() + timeoutMs;
25
+ while (Date.now() < deadline) {
26
+ if (!isProcessRunning(pid)) {
27
+ return true;
28
+ }
29
+ await sleep(500);
30
+ }
31
+ return !isProcessRunning(pid);
32
+ };
33
+ const formatKillError = (pid, signal, error) => {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ return `Failed to send ${signal} to watcher (pid ${pid}): ${message}`;
36
+ };
37
+ const isNoSuchProcessError = (error) => {
38
+ if (!error || typeof error !== 'object') {
39
+ return false;
40
+ }
41
+ return error.code === 'ESRCH';
42
+ };
43
+ export const stopCommand = async (options) => {
44
+ const config = loadConfig(options.config);
45
+ const dbPath = buildDatabasePath(config.project.root);
46
+ if (!fs.existsSync(dbPath)) {
47
+ process.stdout.write('No watcher running.\n');
48
+ process.exitCode = EXIT_CODES.WATCHER_CONFLICT;
49
+ return;
50
+ }
51
+ const db = new Database(dbPath);
52
+ try {
53
+ initializeSchema(db);
54
+ checkSchemaVersion(db);
55
+ const state = getWatcherState(db);
56
+ if (!state) {
57
+ process.stdout.write('No watcher running.\n');
58
+ process.exitCode = EXIT_CODES.WATCHER_CONFLICT;
59
+ return;
60
+ }
61
+ if (!isOurProcess(state.pid, state.project_root)) {
62
+ process.stdout.write('Stale watcher state (process no longer exists).\n');
63
+ clearWatcherState(db);
64
+ process.exitCode = EXIT_CODES.WATCHER_CONFLICT;
65
+ return;
66
+ }
67
+ process.stdout.write(`Stopping watcher (PID ${state.pid})...\n`);
68
+ try {
69
+ process.kill(state.pid, 'SIGTERM');
70
+ }
71
+ catch (error) {
72
+ if (isNoSuchProcessError(error)) {
73
+ process.stdout.write('Stale watcher state (process already exited).\n');
74
+ clearWatcherState(db);
75
+ process.exitCode = EXIT_CODES.WATCHER_CONFLICT;
76
+ return;
77
+ }
78
+ throw new UserError(formatKillError(state.pid, 'SIGTERM', error));
79
+ }
80
+ const stopped = await waitForProcessExit(state.pid, 30_000);
81
+ if (!stopped) {
82
+ process.stdout.write('Watcher did not stop gracefully, forcing...\n');
83
+ try {
84
+ process.kill(state.pid, 'SIGKILL');
85
+ }
86
+ catch (error) {
87
+ if (isNoSuchProcessError(error)) {
88
+ process.stdout.write('Stale watcher state (process already exited).\n');
89
+ clearWatcherState(db);
90
+ process.exitCode = EXIT_CODES.WATCHER_CONFLICT;
91
+ return;
92
+ }
93
+ throw new UserError(formatKillError(state.pid, 'SIGKILL', error));
94
+ }
95
+ const killed = await waitForProcessExit(state.pid, 5000);
96
+ if (!killed) {
97
+ throw new UserError(`Watcher did not stop after SIGKILL (pid ${state.pid}).`);
98
+ }
99
+ }
100
+ clearWatcherState(db);
101
+ process.stdout.write('Watcher stopped.\n');
102
+ }
103
+ finally {
104
+ db.close();
105
+ }
106
+ };
@@ -0,0 +1,7 @@
1
+ type VersionOptions = {
2
+ config?: string;
3
+ verbose?: boolean;
4
+ quiet?: boolean;
5
+ };
6
+ export declare const versionCommand: (options: VersionOptions) => Promise<void>;
7
+ export {};
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { loadConfig, DEFAULT_CONFIG_PATH } from '../../config/loader.js';
5
+ const require = createRequire(import.meta.url);
6
+ const pkg = require('../../../package.json');
7
+ const resolveConfigPath = (configPath) => path.resolve(process.cwd(), configPath ?? DEFAULT_CONFIG_PATH);
8
+ const describeConfigStatus = (configPath) => {
9
+ const resolvedPath = resolveConfigPath(configPath);
10
+ if (!fs.existsSync(resolvedPath)) {
11
+ return { status: 'not found' };
12
+ }
13
+ try {
14
+ const config = loadConfig(resolvedPath);
15
+ return { status: 'valid', agent: config.agent.provider };
16
+ }
17
+ catch (error) {
18
+ const err = error;
19
+ return { status: 'invalid', error: err.message ?? String(error) };
20
+ }
21
+ };
22
+ export const versionCommand = async (options) => {
23
+ const { status, agent, error } = describeConfigStatus(options.config);
24
+ const lines = [
25
+ `${pkg.name ?? 'watchfix'} version: ${pkg.version ?? '0.0.0'}`,
26
+ `Node.js version: ${process.version}`,
27
+ `Config: ${status}`,
28
+ ];
29
+ if (status !== 'not found') {
30
+ lines.push(`Agent: ${agent ?? 'unknown (config invalid)'}`);
31
+ }
32
+ if (options.verbose && error) {
33
+ lines.push(`Config error: ${error}`);
34
+ }
35
+ process.stdout.write(`${lines.join('\n')}\n`);
36
+ };
@@ -0,0 +1,10 @@
1
+ type WatchOptions = {
2
+ config?: string;
3
+ verbose?: boolean;
4
+ quiet?: boolean;
5
+ daemon?: boolean;
6
+ autonomous?: boolean;
7
+ daemonChild?: boolean;
8
+ };
9
+ export declare const watchCommand: (options: WatchOptions) => Promise<void>;
10
+ export {};