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,204 @@
1
+ import path from 'node:path';
2
+ import { createAgent } from '../../agents/index.js';
3
+ import { loadConfig } from '../../config/loader.js';
4
+ import { Database } from '../../db/index.js';
5
+ import { logActivity } from '../../db/queries.js';
6
+ import { initializeSchema, checkSchemaVersion } from '../../db/schema.js';
7
+ import { FixOrchestrator } from '../../fixer/index.js';
8
+ import { FixQueue } from '../../fixer/queue.js';
9
+ import { WatcherOrchestrator } from '../../watcher/index.js';
10
+ import { daemonize, recoverStaleErrors, setupSignalHandlers, } from '../../utils/daemon.js';
11
+ import { parseDuration } from '../../utils/duration.js';
12
+ import { EXIT_CODES, UserError } from '../../utils/errors.js';
13
+ import { Logger } from '../../utils/logger.js';
14
+ import { isOurProcess } from '../../utils/process.js';
15
+ const resolveVerbosity = (options) => {
16
+ if (options.quiet) {
17
+ return 'quiet';
18
+ }
19
+ if (options.verbose) {
20
+ return 'verbose';
21
+ }
22
+ return 'normal';
23
+ };
24
+ const buildDatabasePath = (rootDir) => path.join(rootDir, '.watchfix', 'errors.db');
25
+ const getWatcherState = (db) => db.get('SELECT pid, started_at, autonomous, project_root, command_line FROM watcher_state WHERE id = 1');
26
+ const clearWatcherState = (db) => {
27
+ db.run('DELETE FROM watcher_state WHERE id = 1');
28
+ };
29
+ const ensureWatcherAvailable = (db, projectRoot, logger) => {
30
+ const existing = getWatcherState(db);
31
+ if (!existing) {
32
+ return true;
33
+ }
34
+ if (isOurProcess(existing.pid, projectRoot)) {
35
+ const mode = existing.autonomous ? 'autonomous' : 'manual';
36
+ const message = `Watcher already running (pid ${existing.pid}, ${mode} mode). Use 'watchfix stop'.`;
37
+ if (logger) {
38
+ logger.error(message);
39
+ }
40
+ else {
41
+ console.error(message);
42
+ }
43
+ process.exitCode = EXIT_CODES.WATCHER_CONFLICT;
44
+ return false;
45
+ }
46
+ clearWatcherState(db);
47
+ if (logger) {
48
+ logger.warn('Cleared stale watcher state for non-running process.');
49
+ }
50
+ return true;
51
+ };
52
+ const insertWatcherState = (db, options) => {
53
+ db.run(`INSERT OR REPLACE INTO watcher_state
54
+ (id, pid, started_at, autonomous, project_root, command_line)
55
+ VALUES (1, ?, ?, ?, ?, ?)`, [
56
+ process.pid,
57
+ new Date().toISOString(),
58
+ options.autonomous ? 1 : 0,
59
+ options.projectRoot,
60
+ process.argv.join(' '),
61
+ ]);
62
+ };
63
+ const validateDaemonChild = (options) => {
64
+ const daemonChild = Boolean(options.daemonChild);
65
+ const envDaemon = process.env.WATCHFIX_DAEMON === '1';
66
+ if (daemonChild && !envDaemon) {
67
+ throw new UserError('The --daemon-child flag is internal. Use "watchfix watch --daemon" instead.');
68
+ }
69
+ return daemonChild || envDaemon;
70
+ };
71
+ export const watchCommand = async (options) => {
72
+ const daemonChild = validateDaemonChild(options);
73
+ if ((options.daemon || daemonChild) && process.platform === 'win32') {
74
+ throw new UserError('Daemon mode is not supported on Windows.\n' +
75
+ 'Use foreground mode: watchfix watch --autonomous\n' +
76
+ 'Or use a process manager like PM2: pm2 start watchfix -- watch --autonomous');
77
+ }
78
+ const config = loadConfig(options.config);
79
+ const verbosity = resolveVerbosity(options);
80
+ if (options.daemon && !daemonChild) {
81
+ const preflightLogger = new Logger({
82
+ rootDir: config.project.root,
83
+ terminalEnabled: true,
84
+ verbosity,
85
+ });
86
+ createAgent({
87
+ provider: config.agent.provider,
88
+ command: config.agent.command,
89
+ args: config.agent.args,
90
+ stderrIsProgress: config.agent.stderr_is_progress,
91
+ timeout: parseDuration(config.agent.timeout),
92
+ retries: config.agent.retries,
93
+ }, {
94
+ projectRoot: config.project.root,
95
+ logger: preflightLogger,
96
+ terminalEnabled: true,
97
+ });
98
+ const db = new Database(buildDatabasePath(config.project.root));
99
+ initializeSchema(db);
100
+ checkSchemaVersion(db);
101
+ const available = ensureWatcherAvailable(db, config.project.root, preflightLogger);
102
+ db.close();
103
+ if (!available) {
104
+ return;
105
+ }
106
+ const pid = daemonize();
107
+ process.stdout.write(`Started watchfix daemon (pid ${pid}).\n`);
108
+ return;
109
+ }
110
+ const terminalEnabled = !daemonChild;
111
+ const logger = new Logger({
112
+ rootDir: config.project.root,
113
+ terminalEnabled,
114
+ verbosity,
115
+ });
116
+ const agentConfig = {
117
+ provider: config.agent.provider,
118
+ command: config.agent.command,
119
+ args: config.agent.args,
120
+ stderrIsProgress: config.agent.stderr_is_progress,
121
+ timeout: parseDuration(config.agent.timeout),
122
+ retries: config.agent.retries,
123
+ };
124
+ const agentOptions = {
125
+ projectRoot: config.project.root,
126
+ logger,
127
+ terminalEnabled,
128
+ };
129
+ let agent;
130
+ if (options.autonomous) {
131
+ agent = createAgent(agentConfig, agentOptions);
132
+ }
133
+ else {
134
+ createAgent(agentConfig, agentOptions);
135
+ }
136
+ const db = new Database(buildDatabasePath(config.project.root));
137
+ initializeSchema(db);
138
+ checkSchemaVersion(db);
139
+ const available = ensureWatcherAvailable(db, config.project.root, logger);
140
+ if (!available) {
141
+ db.close();
142
+ return;
143
+ }
144
+ recoverStaleErrors(db, logger);
145
+ insertWatcherState(db, {
146
+ autonomous: Boolean(options.autonomous),
147
+ projectRoot: config.project.root,
148
+ });
149
+ logActivity(db, 'watcher_start', undefined, JSON.stringify({
150
+ pid: process.pid,
151
+ autonomous: Boolean(options.autonomous),
152
+ daemon: daemonChild,
153
+ }));
154
+ const watcher = new WatcherOrchestrator(config, db, { logger });
155
+ let fixQueue = null;
156
+ let currentFix = null;
157
+ if (options.autonomous) {
158
+ const fixOrchestrator = new FixOrchestrator(db, config, {
159
+ agent,
160
+ logger,
161
+ terminalEnabled,
162
+ });
163
+ fixQueue = new FixQueue(db, {
164
+ onProcess: async (error) => {
165
+ const errorId = error.id;
166
+ const fixPromise = fixOrchestrator.fixError(errorId);
167
+ currentFix = {
168
+ promise: fixPromise,
169
+ abort: () => {
170
+ logger.warn('Fix abort requested but not supported for current agent.');
171
+ },
172
+ };
173
+ try {
174
+ await fixPromise;
175
+ }
176
+ catch (cause) {
177
+ logger.error(`Fix pipeline failed for error ${errorId}: ${cause instanceof Error ? cause.message : String(cause)}`);
178
+ }
179
+ finally {
180
+ currentFix = null;
181
+ }
182
+ },
183
+ });
184
+ watcher.on('error_detected', () => {
185
+ void fixQueue?.processQueueIfReady();
186
+ });
187
+ }
188
+ process.removeAllListeners('SIGINT');
189
+ setupSignalHandlers({
190
+ stopWatchers: async () => {
191
+ await watcher.stop();
192
+ },
193
+ getCurrentFix: () => currentFix,
194
+ }, {
195
+ logger,
196
+ db,
197
+ clearWatcherState: () => clearWatcherState(db),
198
+ });
199
+ await watcher.start();
200
+ if (options.autonomous) {
201
+ void fixQueue?.processQueueIfReady();
202
+ }
203
+ await new Promise(() => { });
204
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from 'commander';
3
+ import { createRequire } from 'node:module';
4
+ import { initCommand } from './commands/init.js';
5
+ import { showCommand } from './commands/show.js';
6
+ import { statusCommand } from './commands/status.js';
7
+ import { stopCommand } from './commands/stop.js';
8
+ import { watchCommand } from './commands/watch.js';
9
+ import { fixCommand } from './commands/fix.js';
10
+ import { ignoreCommand } from './commands/ignore.js';
11
+ import { logsCommand } from './commands/logs.js';
12
+ import { configValidateCommand } from './commands/config.js';
13
+ import { cleanCommand } from './commands/clean.js';
14
+ import { versionCommand } from './commands/version.js';
15
+ import { EXIT_CODES, InternalError, UserError } from '../utils/errors.js';
16
+ const require = createRequire(import.meta.url);
17
+ const pkg = require('../../package.json');
18
+ const addGlobalOptions = (command) => command
19
+ .option('-c, --config <path>', 'Use alternate config file')
20
+ .option('--verbose', 'Increase output verbosity')
21
+ .option('-q, --quiet', 'Suppress non-essential output');
22
+ const registerCommands = (program) => {
23
+ addGlobalOptions(program
24
+ .command('init')
25
+ .description('Create watchfix.yaml in current directory')
26
+ .option('--agent <provider>', 'Set initial agent provider (claude, gemini, codex)')
27
+ .option('--force', 'Overwrite existing watchfix.yaml')
28
+ .action(async (options) => {
29
+ await initCommand(options);
30
+ }));
31
+ addGlobalOptions(program
32
+ .command('watch')
33
+ .description('Watch logs in foreground or background')
34
+ .option('--daemon', 'Watch logs in background (Linux/macOS only)')
35
+ .option('--autonomous', 'Auto-fix errors without approval')
36
+ .addOption(new Option('--daemon-child', 'Internal daemon flag').hideHelp())
37
+ .action(async (options) => {
38
+ await watchCommand(options);
39
+ }));
40
+ addGlobalOptions(program
41
+ .command('stop')
42
+ .description('Stop background watcher')
43
+ .action(async (options) => {
44
+ await stopCommand(options);
45
+ }));
46
+ addGlobalOptions(program
47
+ .command('status')
48
+ .description('Show watcher state and pending errors')
49
+ .action(async (options) => {
50
+ await statusCommand(options);
51
+ }));
52
+ addGlobalOptions(program
53
+ .command('show')
54
+ .description('Show full error details and analysis')
55
+ .argument('<id>', 'Error identifier')
56
+ .option('--json', 'Output machine-readable JSON')
57
+ .action(async (id, options) => {
58
+ await showCommand(id, options);
59
+ }));
60
+ addGlobalOptions(program
61
+ .command('fix')
62
+ .description('Analyze and fix specific error')
63
+ .argument('[id]', 'Error identifier')
64
+ .option('--all', 'Fix all pending/suggested errors sequentially')
65
+ .option('--confirm-each', 'Prompt for confirmation before each fix')
66
+ .option('-y, --yes', 'Skip confirmation prompt')
67
+ .option('--analyze-only', "Stop after analysis, don't apply fix")
68
+ .option('--reanalyze', 'Force re-run analysis even if already suggested')
69
+ .action(async (id, options) => {
70
+ await fixCommand(id, options);
71
+ }));
72
+ addGlobalOptions(program
73
+ .command('ignore')
74
+ .description('Mark error as ignored')
75
+ .argument('<id>', 'Error identifier')
76
+ .action(async (id, options) => {
77
+ await ignoreCommand(id, options);
78
+ }));
79
+ addGlobalOptions(program
80
+ .command('logs')
81
+ .description('Show activity log')
82
+ .option('--tail', 'Follow activity log')
83
+ .option('-n, --lines <count>', 'Number of lines to show', `${50}`)
84
+ .action(async (options) => {
85
+ await logsCommand(options);
86
+ }));
87
+ const configCommand = addGlobalOptions(program.command('config').description('Configuration utilities'));
88
+ addGlobalOptions(configCommand
89
+ .command('validate')
90
+ .description('Validate configuration file')
91
+ .action(async (options) => {
92
+ await configValidateCommand(options);
93
+ }));
94
+ addGlobalOptions(program
95
+ .command('clean')
96
+ .description('Remove old context files')
97
+ .option('--dry-run', 'Show what would be removed without deleting')
98
+ .option('--force', 'Skip confirmation prompt')
99
+ .action(async (options) => {
100
+ await cleanCommand(options);
101
+ }));
102
+ addGlobalOptions(program
103
+ .command('version')
104
+ .description('Show version information')
105
+ .action(async (options) => {
106
+ await versionCommand(options);
107
+ }));
108
+ };
109
+ const program = new Command();
110
+ program
111
+ .name('watchfix')
112
+ .description('CLI tool that watches logs, detects errors, and dispatches AI agents to fix them');
113
+ addGlobalOptions(program);
114
+ program
115
+ .helpOption('-h, --help', 'Show help for command')
116
+ .version(pkg.version ?? '0.0.0', '-v, --version', 'Show version and exit');
117
+ registerCommands(program);
118
+ const handleError = (error) => {
119
+ if (error instanceof UserError) {
120
+ console.error(error.message);
121
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
122
+ return;
123
+ }
124
+ if (error instanceof InternalError) {
125
+ console.error(error.stack ?? error.message);
126
+ console.error('An internal error occurred. Please check logs.');
127
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
128
+ return;
129
+ }
130
+ if (error instanceof Error) {
131
+ console.error(error.message);
132
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
133
+ return;
134
+ }
135
+ console.error('Unknown error occurred.');
136
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
137
+ };
138
+ const main = async () => {
139
+ process.on('SIGINT', () => {
140
+ process.exit(EXIT_CODES.INTERRUPTED);
141
+ });
142
+ try {
143
+ await program.parseAsync(process.argv);
144
+ if (!process.exitCode) {
145
+ process.exitCode = EXIT_CODES.SUCCESS;
146
+ }
147
+ }
148
+ catch (error) {
149
+ handleError(error);
150
+ }
151
+ };
152
+ void main();
@@ -0,0 +1,4 @@
1
+ import { type Config } from './schema.js';
2
+ declare const DEFAULT_CONFIG_PATH = "watchfix.yaml";
3
+ export declare const loadConfig: (configPath?: string) => Config;
4
+ export { DEFAULT_CONFIG_PATH };
@@ -0,0 +1,96 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ZodError } from 'zod';
4
+ import yaml from 'yaml';
5
+ import { configSchema } from './schema.js';
6
+ import { UserError } from '../utils/errors.js';
7
+ const DEFAULT_CONFIG_PATH = 'watchfix.yaml';
8
+ const resolveConfigPath = (configPath) => path.resolve(process.cwd(), configPath ?? DEFAULT_CONFIG_PATH);
9
+ const resolveIfRelative = (baseDir, value) => path.isAbsolute(value) ? value : path.resolve(baseDir, value);
10
+ const resolveConfigPaths = (config, baseDir) => ({
11
+ ...config,
12
+ project: {
13
+ ...config.project,
14
+ root: resolveIfRelative(baseDir, config.project.root),
15
+ },
16
+ logs: {
17
+ ...config.logs,
18
+ sources: config.logs.sources.map((source) => {
19
+ if (source.type !== 'file') {
20
+ return source;
21
+ }
22
+ return {
23
+ ...source,
24
+ path: resolveIfRelative(baseDir, source.path),
25
+ };
26
+ }),
27
+ },
28
+ });
29
+ const formatZodError = (error) => {
30
+ const lines = error.issues.map((issue) => {
31
+ const pathLabel = issue.path.length > 0 ? issue.path.join('.') : 'root';
32
+ return `- ${pathLabel}: ${issue.message}`;
33
+ });
34
+ return `Invalid configuration:\n${lines.join('\n')}`;
35
+ };
36
+ const assertProjectRoot = (rootPath) => {
37
+ try {
38
+ const stats = fs.statSync(rootPath);
39
+ if (!stats.isDirectory()) {
40
+ throw new UserError(`Project root is not a directory: ${rootPath}`);
41
+ }
42
+ }
43
+ catch (error) {
44
+ if (error instanceof UserError) {
45
+ throw error;
46
+ }
47
+ const err = error;
48
+ if (err.code === 'ENOENT') {
49
+ throw new UserError(`Project root does not exist: ${rootPath}`);
50
+ }
51
+ throw new UserError(`Unable to access project root ${rootPath}: ${err.message ?? String(err)}`);
52
+ }
53
+ };
54
+ const readConfigFile = (filePath) => {
55
+ try {
56
+ return fs.readFileSync(filePath, 'utf8');
57
+ }
58
+ catch (error) {
59
+ const err = error;
60
+ if (err.code === 'ENOENT') {
61
+ throw new UserError(`Config file not found at ${filePath}. Create watchfix.yaml or use --config to specify a path.`);
62
+ }
63
+ throw new UserError(`Failed to read config file at ${filePath}: ${err.message ?? String(err)}`);
64
+ }
65
+ };
66
+ const parseConfigYaml = (contents, filePath) => {
67
+ try {
68
+ return yaml.parse(contents);
69
+ }
70
+ catch (error) {
71
+ const err = error;
72
+ throw new UserError(`Failed to parse YAML in ${filePath}: ${err.message ?? String(err)}`);
73
+ }
74
+ };
75
+ const validateConfig = (rawConfig) => {
76
+ try {
77
+ return configSchema.parse(rawConfig);
78
+ }
79
+ catch (error) {
80
+ if (error instanceof ZodError) {
81
+ throw new UserError(formatZodError(error));
82
+ }
83
+ throw error;
84
+ }
85
+ };
86
+ export const loadConfig = (configPath) => {
87
+ const resolvedPath = resolveConfigPath(configPath);
88
+ const fileContents = readConfigFile(resolvedPath);
89
+ const rawConfig = parseConfigYaml(fileContents, resolvedPath);
90
+ const parsedConfig = validateConfig(rawConfig);
91
+ const configDir = path.dirname(resolvedPath);
92
+ const resolvedConfig = resolveConfigPaths(parsedConfig, configDir);
93
+ assertProjectRoot(resolvedConfig.project.root);
94
+ return resolvedConfig;
95
+ };
96
+ export { DEFAULT_CONFIG_PATH };