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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/agents/base.d.ts +19 -0
- package/dist/agents/base.js +140 -0
- package/dist/agents/claude.d.ts +11 -0
- package/dist/agents/claude.js +6 -0
- package/dist/agents/codex.d.ts +11 -0
- package/dist/agents/codex.js +6 -0
- package/dist/agents/defaults.d.ts +21 -0
- package/dist/agents/defaults.js +21 -0
- package/dist/agents/gemini.d.ts +11 -0
- package/dist/agents/gemini.js +6 -0
- package/dist/agents/index.d.ts +19 -0
- package/dist/agents/index.js +63 -0
- package/dist/agents/types.d.ts +22 -0
- package/dist/agents/types.js +1 -0
- package/dist/cli/commands/clean.d.ts +9 -0
- package/dist/cli/commands/clean.js +173 -0
- package/dist/cli/commands/config.d.ts +7 -0
- package/dist/cli/commands/config.js +134 -0
- package/dist/cli/commands/fix.d.ts +12 -0
- package/dist/cli/commands/fix.js +391 -0
- package/dist/cli/commands/ignore.d.ts +7 -0
- package/dist/cli/commands/ignore.js +74 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.js +115 -0
- package/dist/cli/commands/logs.d.ts +9 -0
- package/dist/cli/commands/logs.js +106 -0
- package/dist/cli/commands/show.d.ts +8 -0
- package/dist/cli/commands/show.js +165 -0
- package/dist/cli/commands/status.d.ts +7 -0
- package/dist/cli/commands/status.js +110 -0
- package/dist/cli/commands/stop.d.ts +7 -0
- package/dist/cli/commands/stop.js +106 -0
- package/dist/cli/commands/version.d.ts +7 -0
- package/dist/cli/commands/version.js +36 -0
- package/dist/cli/commands/watch.d.ts +10 -0
- package/dist/cli/commands/watch.js +204 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +152 -0
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.js +96 -0
- package/dist/config/schema.d.ts +375 -0
- package/dist/config/schema.js +99 -0
- package/dist/db/index.d.ts +15 -0
- package/dist/db/index.js +71 -0
- package/dist/db/queries.d.ts +45 -0
- package/dist/db/queries.js +111 -0
- package/dist/db/schema.d.ts +4 -0
- package/dist/db/schema.js +84 -0
- package/dist/fixer/context.d.ts +9 -0
- package/dist/fixer/context.js +361 -0
- package/dist/fixer/index.d.ts +37 -0
- package/dist/fixer/index.js +398 -0
- package/dist/fixer/lock.d.ts +7 -0
- package/dist/fixer/lock.js +49 -0
- package/dist/fixer/output.d.ts +21 -0
- package/dist/fixer/output.js +108 -0
- package/dist/fixer/queue.d.ts +15 -0
- package/dist/fixer/queue.js +53 -0
- package/dist/fixer/verifier.d.ts +44 -0
- package/dist/fixer/verifier.js +133 -0
- package/dist/utils/daemon.d.ts +22 -0
- package/dist/utils/daemon.js +143 -0
- package/dist/utils/duration.d.ts +2 -0
- package/dist/utils/duration.js +31 -0
- package/dist/utils/errors.d.ts +17 -0
- package/dist/utils/errors.js +20 -0
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +18 -0
- package/dist/utils/http.d.ts +6 -0
- package/dist/utils/http.js +61 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.js +85 -0
- package/dist/utils/process.d.ts +16 -0
- package/dist/utils/process.js +146 -0
- package/dist/watcher/index.d.ts +55 -0
- package/dist/watcher/index.js +234 -0
- package/dist/watcher/parser.d.ts +42 -0
- package/dist/watcher/parser.js +162 -0
- package/dist/watcher/patterns.d.ts +5 -0
- package/dist/watcher/patterns.js +92 -0
- package/dist/watcher/sources/command.d.ts +27 -0
- package/dist/watcher/sources/command.js +143 -0
- package/dist/watcher/sources/docker.d.ts +28 -0
- package/dist/watcher/sources/docker.js +183 -0
- package/dist/watcher/sources/file.d.ts +30 -0
- package/dist/watcher/sources/file.js +177 -0
- package/dist/watcher/sources/types.d.ts +27 -0
- package/dist/watcher/sources/types.js +1 -0
- 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,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,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 };
|