@exaudeus/workrail 3.70.6 → 3.70.7
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/dist/cli/commands/worktrain-daemon.d.ts +3 -0
- package/dist/cli/commands/worktrain-daemon.js +122 -47
- package/dist/cli-worktrain.js +6 -2
- package/dist/console-ui/assets/index-iqWCy6dR.js +28 -0
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/workflow-runner.d.ts +76 -1
- package/dist/daemon/workflow-runner.js +328 -228
- package/dist/manifest.json +14 -14
- package/docs/ideas/backlog.md +604 -7512
- package/docs/reference/worktrain-daemon-invariants.md +225 -0
- package/package.json +1 -1
- package/dist/console-ui/assets/index-RNEvfTvk.js +0 -28
|
@@ -27,5 +27,8 @@ export interface WorktrainDaemonCommandOpts {
|
|
|
27
27
|
readonly install?: boolean;
|
|
28
28
|
readonly uninstall?: boolean;
|
|
29
29
|
readonly status?: boolean;
|
|
30
|
+
readonly start?: boolean;
|
|
31
|
+
readonly stop?: boolean;
|
|
30
32
|
}
|
|
33
|
+
export declare function parseDotEnv(content: string): Record<string, string>;
|
|
31
34
|
export declare function executeWorktrainDaemonCommand(deps: WorktrainDaemonCommandDeps, opts: WorktrainDaemonCommandOpts): Promise<CliResult>;
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseDotEnv = parseDotEnv;
|
|
3
4
|
exports.executeWorktrainDaemonCommand = executeWorktrainDaemonCommand;
|
|
4
5
|
const cli_result_js_1 = require("../types/cli-result.js");
|
|
5
6
|
const LAUNCHD_LABEL = 'io.worktrain.daemon';
|
|
6
7
|
const PLIST_FILENAME = `${LAUNCHD_LABEL}.plist`;
|
|
7
8
|
const CAPTURED_ENV_VARS = [
|
|
8
9
|
'AWS_PROFILE',
|
|
9
|
-
'AWS_ACCESS_KEY_ID',
|
|
10
|
-
'AWS_SECRET_ACCESS_KEY',
|
|
11
|
-
'AWS_SESSION_TOKEN',
|
|
12
|
-
'ANTHROPIC_API_KEY',
|
|
13
10
|
'WORKRAIL_TRIGGERS_ENABLED',
|
|
14
11
|
'WORKRAIL_DEFAULT_WORKSPACE',
|
|
15
|
-
'GITHUB_TOKEN',
|
|
16
|
-
'GITLAB_TOKEN',
|
|
17
12
|
'HOME',
|
|
18
13
|
'USER',
|
|
19
14
|
'PATH',
|
|
@@ -56,20 +51,16 @@ ${envEntries}
|
|
|
56
51
|
<key>StandardErrorPath</key>
|
|
57
52
|
<string>${escapeXml(stderrLog)}</string>
|
|
58
53
|
|
|
59
|
-
<key>RunAtLoad</key>
|
|
60
|
-
<true/>
|
|
61
|
-
|
|
62
|
-
<key>KeepAlive</key>
|
|
63
|
-
<true/>
|
|
64
|
-
|
|
65
54
|
<!--
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
No RunAtLoad or KeepAlive: the daemon must be started explicitly with
|
|
56
|
+
'worktrain daemon --start'. Auto-starting at login and auto-restarting
|
|
57
|
+
on crash is unsafe -- WorkTrain acts autonomously in your repos and the
|
|
58
|
+
operator must decide when it runs.
|
|
59
|
+
|
|
60
|
+
To start: worktrain daemon --start
|
|
61
|
+
To stop: worktrain daemon --stop
|
|
62
|
+
To status: worktrain daemon --status
|
|
70
63
|
-->
|
|
71
|
-
<key>ThrottleInterval</key>
|
|
72
|
-
<integer>30</integer>
|
|
73
64
|
</dict>
|
|
74
65
|
</plist>
|
|
75
66
|
`;
|
|
@@ -121,19 +112,7 @@ async function runInstall(deps) {
|
|
|
121
112
|
const plistPath = deps.joinPath(plistDir, PLIST_FILENAME);
|
|
122
113
|
const logDir = deps.joinPath(home, '.workrail', 'logs');
|
|
123
114
|
const env = deps.env;
|
|
124
|
-
|
|
125
|
-
const hasAnthropic = !!env['ANTHROPIC_API_KEY'];
|
|
126
|
-
if (!hasBedrock && !hasAnthropic) {
|
|
127
|
-
return (0, cli_result_js_1.failure)('No LLM credentials found in the current environment. ' +
|
|
128
|
-
'Set AWS_PROFILE (for Bedrock) or ANTHROPIC_API_KEY (for Anthropic) ' +
|
|
129
|
-
'before running --install so the daemon can authenticate.', {
|
|
130
|
-
suggestions: [
|
|
131
|
-
'export AWS_PROFILE=your-sso-profile',
|
|
132
|
-
'export ANTHROPIC_API_KEY=sk-ant-...',
|
|
133
|
-
],
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
deps.print('Installing WorkTrain daemon as a launchd service...');
|
|
115
|
+
deps.print('Registering WorkTrain daemon with launchd...');
|
|
137
116
|
await deps.mkdir(plistDir, { recursive: true });
|
|
138
117
|
await deps.mkdir(logDir, { recursive: true });
|
|
139
118
|
const alreadyInstalled = await deps.exists(plistPath);
|
|
@@ -167,21 +146,30 @@ async function runInstall(deps) {
|
|
|
167
146
|
});
|
|
168
147
|
}
|
|
169
148
|
deps.print('');
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
149
|
+
deps.print('WorkTrain daemon registered with launchd.');
|
|
150
|
+
deps.print('');
|
|
151
|
+
deps.print('Before starting, put your secrets in ~/.workrail/.env');
|
|
152
|
+
deps.print('(see docs/configuration.md for the full list):');
|
|
153
|
+
deps.print('');
|
|
154
|
+
deps.print(' ANTHROPIC_API_KEY=sk-ant-...');
|
|
155
|
+
deps.print(' # or for AWS Bedrock: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY');
|
|
156
|
+
deps.print(' GITHUB_TOKEN=ghp_... # for GitHub polling triggers');
|
|
157
|
+
deps.print(' WORKTRAIN_BOT_TOKEN=ghp_... # for self-improvement queue');
|
|
158
|
+
deps.print('');
|
|
159
|
+
deps.print('Then start the daemon:');
|
|
160
|
+
deps.print('');
|
|
161
|
+
deps.print(' worktrain daemon --start Start the daemon now');
|
|
162
|
+
deps.print(' worktrain daemon --stop Stop the daemon');
|
|
163
|
+
deps.print(' worktrain daemon --status Check if running');
|
|
164
|
+
deps.print(' worktrain daemon --uninstall Remove the registration');
|
|
165
|
+
deps.print('');
|
|
177
166
|
deps.print(`Logs: ${logDir}/daemon.stdout.log`);
|
|
178
167
|
deps.print(` ${logDir}/daemon.stderr.log`);
|
|
179
168
|
return (0, cli_result_js_1.success)({
|
|
180
|
-
message:
|
|
181
|
-
? `WorkTrain daemon installed and running (PID ${status.pid})`
|
|
182
|
-
: 'WorkTrain daemon installed (service loaded, not yet running)',
|
|
169
|
+
message: 'WorkTrain daemon registered. Run: worktrain daemon --start',
|
|
183
170
|
details: [
|
|
184
171
|
`Plist: ${plistPath}`,
|
|
172
|
+
`Start: worktrain daemon --start`,
|
|
185
173
|
`Logs: ${logDir}/daemon.stdout.log`,
|
|
186
174
|
` ${logDir}/daemon.stderr.log`,
|
|
187
175
|
],
|
|
@@ -239,24 +227,107 @@ async function runStatus(deps) {
|
|
|
239
227
|
: 'WorkTrain daemon is not installed',
|
|
240
228
|
});
|
|
241
229
|
}
|
|
230
|
+
function parseDotEnv(content) {
|
|
231
|
+
const result = {};
|
|
232
|
+
for (const line of content.split('\n')) {
|
|
233
|
+
const trimmed = line.trim();
|
|
234
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
235
|
+
continue;
|
|
236
|
+
const eqIdx = trimmed.indexOf('=');
|
|
237
|
+
if (eqIdx === -1)
|
|
238
|
+
continue;
|
|
239
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
240
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
241
|
+
if (key)
|
|
242
|
+
result[key] = value;
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
async function checkCredentials(deps) {
|
|
247
|
+
const home = deps.homedir();
|
|
248
|
+
const envFilePath = deps.joinPath(home, '.workrail', '.env');
|
|
249
|
+
const merged = { ...deps.env };
|
|
250
|
+
try {
|
|
251
|
+
const envContent = await deps.readFile(envFilePath);
|
|
252
|
+
const parsed = parseDotEnv(envContent);
|
|
253
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
254
|
+
if (!(k in merged))
|
|
255
|
+
merged[k] = v;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
}
|
|
260
|
+
const hasAnthropic = !!(merged['ANTHROPIC_API_KEY']);
|
|
261
|
+
const hasBedrock = !!(merged['AWS_PROFILE'] || merged['AWS_ACCESS_KEY_ID']);
|
|
262
|
+
if (!hasAnthropic && !hasBedrock) {
|
|
263
|
+
return ('No LLM credentials found in process env or ~/.workrail/.env.\n' +
|
|
264
|
+
'The daemon will fail when it tries to call the LLM.\n' +
|
|
265
|
+
'Add one of the following to ~/.workrail/.env:\n' +
|
|
266
|
+
' ANTHROPIC_API_KEY=sk-ant-...\n' +
|
|
267
|
+
' AWS_PROFILE=your-sso-profile');
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
async function runStart(deps) {
|
|
272
|
+
const home = deps.homedir();
|
|
273
|
+
const plistPath = deps.joinPath(home, 'Library', 'LaunchAgents', PLIST_FILENAME);
|
|
274
|
+
const logDir = deps.joinPath(home, '.workrail', 'logs');
|
|
275
|
+
if (!(await deps.exists(plistPath))) {
|
|
276
|
+
return (0, cli_result_js_1.failure)('WorkTrain daemon is not installed. Run: worktrain daemon --install', { suggestions: ['worktrain daemon --install'] });
|
|
277
|
+
}
|
|
278
|
+
const credWarning = await checkCredentials(deps);
|
|
279
|
+
if (credWarning) {
|
|
280
|
+
deps.print('');
|
|
281
|
+
deps.print('WARNING: ' + credWarning);
|
|
282
|
+
deps.print('');
|
|
283
|
+
}
|
|
284
|
+
const result = await deps.exec('launchctl', ['start', LAUNCHD_LABEL]);
|
|
285
|
+
if (result.exitCode !== 0) {
|
|
286
|
+
return (0, cli_result_js_1.failure)(`launchctl start failed (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}`, { suggestions: [`View logs: tail -f ${logDir}/daemon.stderr.log`] });
|
|
287
|
+
}
|
|
288
|
+
await deps.sleep(1000);
|
|
289
|
+
const listResult = await deps.exec('launchctl', ['list', LAUNCHD_LABEL]);
|
|
290
|
+
const status = parseLaunchctlList(listResult.stdout, listResult.exitCode);
|
|
291
|
+
if (status.running) {
|
|
292
|
+
deps.print(`WorkTrain daemon started (PID ${status.pid}).`);
|
|
293
|
+
deps.print(`Logs: ${logDir}/daemon.stdout.log`);
|
|
294
|
+
return (0, cli_result_js_1.success)({ message: `WorkTrain daemon started (PID ${status.pid})` });
|
|
295
|
+
}
|
|
296
|
+
return (0, cli_result_js_1.failure)('launchctl start returned 0 but daemon does not appear to be running.', { suggestions: [`View logs: tail -f ${logDir}/daemon.stderr.log`] });
|
|
297
|
+
}
|
|
298
|
+
async function runStop(deps) {
|
|
299
|
+
const result = await deps.exec('launchctl', ['stop', LAUNCHD_LABEL]);
|
|
300
|
+
if (result.exitCode !== 0) {
|
|
301
|
+
const msg = result.stderr.trim() || result.stdout.trim();
|
|
302
|
+
if (msg.toLowerCase().includes('not running') || msg.toLowerCase().includes('no such process')) {
|
|
303
|
+
deps.print('WorkTrain daemon is not running.');
|
|
304
|
+
return (0, cli_result_js_1.success)({ message: 'WorkTrain daemon is not running.' });
|
|
305
|
+
}
|
|
306
|
+
return (0, cli_result_js_1.failure)(`launchctl stop failed (exit ${result.exitCode}): ${msg}`);
|
|
307
|
+
}
|
|
308
|
+
deps.print('WorkTrain daemon stopped.');
|
|
309
|
+
return (0, cli_result_js_1.success)({ message: 'WorkTrain daemon stopped.' });
|
|
310
|
+
}
|
|
242
311
|
async function executeWorktrainDaemonCommand(deps, opts) {
|
|
243
|
-
const flagCount = [opts.install, opts.uninstall, opts.status].filter(Boolean).length;
|
|
312
|
+
const flagCount = [opts.install, opts.uninstall, opts.status, opts.start, opts.stop].filter(Boolean).length;
|
|
244
313
|
if (flagCount === 0) {
|
|
245
314
|
if (deps.startDaemon) {
|
|
246
315
|
await deps.startDaemon();
|
|
247
316
|
return (0, cli_result_js_1.success)({ message: 'WorkTrain daemon stopped.' });
|
|
248
317
|
}
|
|
249
|
-
return (0, cli_result_js_1.misuse)('Specify one of: --install, --uninstall, or --status', [
|
|
250
|
-
'worktrain daemon --install
|
|
251
|
-
'worktrain daemon --
|
|
318
|
+
return (0, cli_result_js_1.misuse)('Specify one of: --install, --uninstall, --start, --stop, or --status', [
|
|
319
|
+
'worktrain daemon --install Register as a launchd service (does not auto-start)',
|
|
320
|
+
'worktrain daemon --start Start the daemon',
|
|
321
|
+
'worktrain daemon --stop Stop the daemon',
|
|
252
322
|
'worktrain daemon --status Show service status',
|
|
323
|
+
'worktrain daemon --uninstall Remove the launchd service registration',
|
|
253
324
|
]);
|
|
254
325
|
}
|
|
255
326
|
if (flagCount > 1) {
|
|
256
|
-
return (0, cli_result_js_1.misuse)('--install, --uninstall, and --status are mutually exclusive. Specify only one.');
|
|
327
|
+
return (0, cli_result_js_1.misuse)('--install, --uninstall, --start, --stop, and --status are mutually exclusive. Specify only one.');
|
|
257
328
|
}
|
|
258
329
|
if (deps.platform !== 'darwin') {
|
|
259
|
-
return (0, cli_result_js_1.failure)(`worktrain daemon
|
|
330
|
+
return (0, cli_result_js_1.failure)(`worktrain daemon management flags require macOS (launchd). ` +
|
|
260
331
|
`Current platform: ${deps.platform}.`, {
|
|
261
332
|
suggestions: [
|
|
262
333
|
'On Linux, use systemd: create a user service with systemctl --user.',
|
|
@@ -268,5 +339,9 @@ async function executeWorktrainDaemonCommand(deps, opts) {
|
|
|
268
339
|
return runInstall(deps);
|
|
269
340
|
if (opts.uninstall)
|
|
270
341
|
return runUninstall(deps);
|
|
342
|
+
if (opts.start)
|
|
343
|
+
return runStart(deps);
|
|
344
|
+
if (opts.stop)
|
|
345
|
+
return runStop(deps);
|
|
271
346
|
return runStatus(deps);
|
|
272
347
|
}
|
package/dist/cli-worktrain.js
CHANGED
|
@@ -241,8 +241,10 @@ program
|
|
|
241
241
|
program
|
|
242
242
|
.command('daemon')
|
|
243
243
|
.description('Start the WorkTrain daemon, or manage it as a macOS launchd service')
|
|
244
|
-
.option('--install', '
|
|
245
|
-
.option('--uninstall', '
|
|
244
|
+
.option('--install', 'Register the daemon as a launchd service (does not auto-start)')
|
|
245
|
+
.option('--uninstall', 'Unregister the daemon from launchd and remove the plist')
|
|
246
|
+
.option('--start', 'Start the daemon via launchctl (must be installed first)')
|
|
247
|
+
.option('--stop', 'Stop the running daemon via launchctl')
|
|
246
248
|
.option('--status', 'Show the current status of the daemon service')
|
|
247
249
|
.action(async (options) => {
|
|
248
250
|
await (0, daemon_env_js_1.loadDaemonEnv)();
|
|
@@ -385,6 +387,8 @@ program
|
|
|
385
387
|
}, {
|
|
386
388
|
install: options.install,
|
|
387
389
|
uninstall: options.uninstall,
|
|
390
|
+
start: options.start,
|
|
391
|
+
stop: options.stop,
|
|
388
392
|
status: options.status,
|
|
389
393
|
});
|
|
390
394
|
(0, interpret_result_js_1.interpretCliResultWithoutDI)(result);
|