@exaudeus/workrail 3.70.5 → 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.
@@ -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
- ThrottleInterval: minimum seconds between launchd restarts.
67
- WHY 30s: prevents launchd from spinning in a tight restart loop if the daemon
68
- exits immediately (e.g., missing credentials or invalid workspace path).
69
- Without this, a misconfigured service consumes CPU and spams logs.
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
- const hasBedrock = !!(env['AWS_PROFILE'] || env['AWS_ACCESS_KEY_ID']);
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
- if (status.running) {
171
- deps.print(`WorkTrain daemon installed and running (PID ${status.pid}).`);
172
- }
173
- else {
174
- deps.print(`WorkTrain daemon installed. Service loaded but not yet running.`);
175
- deps.print(`This may be normal if WORKRAIL_TRIGGERS_ENABLED was not set.`);
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: status.running
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 Install and start as a launchd service',
251
- 'worktrain daemon --uninstall Stop and remove the launchd service',
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 --install requires macOS (launchd). ` +
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
  }
@@ -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', 'Create the launchd plist and start the daemon service')
245
- .option('--uninstall', 'Stop the daemon service and remove the launchd plist')
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);