@todu/cli 0.1.1 → 0.2.1

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 (73) hide show
  1. package/dist/commands/config.js +3 -3
  2. package/dist/commands/config.js.map +1 -1
  3. package/dist/commands/daemon.d.ts +4 -0
  4. package/dist/commands/daemon.d.ts.map +1 -0
  5. package/dist/commands/daemon.js +675 -0
  6. package/dist/commands/daemon.js.map +1 -0
  7. package/dist/commands/habit.d.ts +2 -2
  8. package/dist/commands/habit.d.ts.map +1 -1
  9. package/dist/commands/habit.js +270 -315
  10. package/dist/commands/habit.js.map +1 -1
  11. package/dist/commands/label.d.ts +2 -2
  12. package/dist/commands/label.d.ts.map +1 -1
  13. package/dist/commands/label.js +76 -92
  14. package/dist/commands/label.js.map +1 -1
  15. package/dist/commands/note.d.ts +2 -2
  16. package/dist/commands/note.d.ts.map +1 -1
  17. package/dist/commands/note.js +97 -118
  18. package/dist/commands/note.js.map +1 -1
  19. package/dist/commands/plugin.d.ts +4 -0
  20. package/dist/commands/plugin.d.ts.map +1 -0
  21. package/dist/commands/plugin.js +527 -0
  22. package/dist/commands/plugin.js.map +1 -0
  23. package/dist/commands/project.d.ts +2 -2
  24. package/dist/commands/project.d.ts.map +1 -1
  25. package/dist/commands/project.js +99 -117
  26. package/dist/commands/project.js.map +1 -1
  27. package/dist/commands/recurring.d.ts +2 -2
  28. package/dist/commands/recurring.d.ts.map +1 -1
  29. package/dist/commands/recurring.js +250 -284
  30. package/dist/commands/recurring.js.map +1 -1
  31. package/dist/commands/sync.d.ts +2 -2
  32. package/dist/commands/sync.d.ts.map +1 -1
  33. package/dist/commands/sync.js +123 -12
  34. package/dist/commands/sync.js.map +1 -1
  35. package/dist/commands/task.d.ts +2 -2
  36. package/dist/commands/task.d.ts.map +1 -1
  37. package/dist/commands/task.js +181 -207
  38. package/dist/commands/task.js.map +1 -1
  39. package/dist/daemon-command-client.d.ts +11 -0
  40. package/dist/daemon-command-client.d.ts.map +1 -0
  41. package/dist/daemon-command-client.js +57 -0
  42. package/dist/daemon-command-client.js.map +1 -0
  43. package/dist/daemon-plugin-config.d.ts +8 -0
  44. package/dist/daemon-plugin-config.d.ts.map +1 -0
  45. package/dist/daemon-plugin-config.js +22 -0
  46. package/dist/daemon-plugin-config.js.map +1 -0
  47. package/dist/daemon-plugin-paths.d.ts +8 -0
  48. package/dist/daemon-plugin-paths.d.ts.map +1 -0
  49. package/dist/daemon-plugin-paths.js +28 -0
  50. package/dist/daemon-plugin-paths.js.map +1 -0
  51. package/dist/daemon-transport.d.ts +37 -0
  52. package/dist/daemon-transport.d.ts.map +1 -0
  53. package/dist/daemon-transport.js +422 -0
  54. package/dist/daemon-transport.js.map +1 -0
  55. package/dist/daemon-worker-assignment.d.ts +8 -0
  56. package/dist/daemon-worker-assignment.d.ts.map +1 -0
  57. package/dist/daemon-worker-assignment.js +22 -0
  58. package/dist/daemon-worker-assignment.js.map +1 -0
  59. package/dist/index.js +20 -23
  60. package/dist/index.js.map +1 -1
  61. package/dist/test-helpers/daemon-process.d.ts +5 -0
  62. package/dist/test-helpers/daemon-process.d.ts.map +1 -0
  63. package/dist/test-helpers/daemon-process.js +55 -0
  64. package/dist/test-helpers/daemon-process.js.map +1 -0
  65. package/dist/version.d.ts +2 -0
  66. package/dist/version.d.ts.map +1 -0
  67. package/dist/version.js +3 -0
  68. package/dist/version.js.map +1 -0
  69. package/dist/warnings.d.ts +7 -0
  70. package/dist/warnings.d.ts.map +1 -0
  71. package/dist/warnings.js +21 -0
  72. package/dist/warnings.js.map +1 -0
  73. package/package.json +5 -3
@@ -0,0 +1,675 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { resolveRemoteSyncConfig } from "@todu/core";
6
+ import { getConfigPath, loadConfig, resolveDataDir } from "../config.js";
7
+ import { formatDaemonCommandError, resolveDaemonSocketPath, } from "../daemon-command-client.js";
8
+ import { resolveDaemonPluginConfig, TODUAI_DAEMON_PLUGIN_CONFIG_ENV, } from "../daemon-plugin-config.js";
9
+ import { resolveDaemonPluginPaths, TODUAI_DAEMON_PLUGIN_PATHS_ENV, } from "../daemon-plugin-paths.js";
10
+ import { resolveDaemonAssignedWorkers, TODUAI_DAEMON_ASSIGNED_WORKERS_ENV, } from "../daemon-worker-assignment.js";
11
+ import { formatJSON } from "../format.js";
12
+ const DIRECT_PID_FILENAME = "daemon.pid";
13
+ const SYSTEMD_SERVICE_NAME = "toduai-daemon";
14
+ const SYSTEMD_SERVICE_PATH = ".config/systemd/user/toduai-daemon.service";
15
+ const LAUNCHD_LABEL = "com.todu.daemon";
16
+ const LAUNCHD_PLIST_PATH = "Library/LaunchAgents/com.todu.daemon.plist";
17
+ const LIFECYCLE_MODE_ENV = "TODUAI_DAEMON_LIFECYCLE_MODE";
18
+ const STARTUP_TIMEOUT_MS = 5_000;
19
+ const STOP_TIMEOUT_MS = 5_000;
20
+ export function registerDaemonCommands(program, invokeDaemon) {
21
+ const daemon = program.command("daemon").description("Manage local daemon lifecycle");
22
+ daemon
23
+ .command("run")
24
+ .description("Run local daemon in foreground mode")
25
+ .action(async () => {
26
+ const context = resolveDaemonCommandContext(program);
27
+ if (!fs.existsSync(context.daemonEntrypoint)) {
28
+ console.error(`Error: daemon entrypoint not found at ${context.daemonEntrypoint}`);
29
+ process.exitCode = 1;
30
+ return;
31
+ }
32
+ const child = spawn(process.execPath, [context.daemonEntrypoint], {
33
+ cwd: process.cwd(),
34
+ stdio: "inherit",
35
+ env: createDaemonChildEnv(context),
36
+ });
37
+ const forwardSigInt = () => {
38
+ child.kill("SIGINT");
39
+ };
40
+ const forwardSigTerm = () => {
41
+ child.kill("SIGTERM");
42
+ };
43
+ process.on("SIGINT", forwardSigInt);
44
+ process.on("SIGTERM", forwardSigTerm);
45
+ try {
46
+ const result = await new Promise((resolve, reject) => {
47
+ child.once("error", reject);
48
+ child.once("exit", (code, signal) => {
49
+ resolve({ code, signal });
50
+ });
51
+ });
52
+ if (result.signal) {
53
+ process.exitCode = result.signal === "SIGINT" || result.signal === "SIGTERM" ? 0 : 1;
54
+ return;
55
+ }
56
+ process.exitCode = result.code ?? 1;
57
+ }
58
+ finally {
59
+ process.off("SIGINT", forwardSigInt);
60
+ process.off("SIGTERM", forwardSigTerm);
61
+ }
62
+ });
63
+ daemon
64
+ .command("status")
65
+ .description("Show daemon availability and health")
66
+ .action(async () => {
67
+ const result = await invokeDaemon("daemon.status", {});
68
+ const format = program.opts().format;
69
+ if (!result.ok) {
70
+ if (result.error.code === "DAEMON_UNAVAILABLE") {
71
+ const unavailable = {
72
+ running: false,
73
+ reason: result.error.message,
74
+ };
75
+ if (format === "json") {
76
+ console.log(formatJSON(unavailable));
77
+ }
78
+ else {
79
+ console.log("Daemon: not running");
80
+ console.log(`Reason: ${result.error.message}`);
81
+ }
82
+ return;
83
+ }
84
+ console.error(formatDaemonCommandError(result.error));
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+ const running = result.value.state === "running" && result.value.healthy;
89
+ const output = {
90
+ running,
91
+ status: result.value,
92
+ };
93
+ if (format === "json") {
94
+ console.log(formatJSON(output));
95
+ return;
96
+ }
97
+ console.log(`Daemon: ${running ? "running" : "not running"}`);
98
+ console.log(`State: ${result.value.state}`);
99
+ console.log(`Role: ${result.value.role}`);
100
+ if (result.value.transport?.path) {
101
+ console.log(`Socket: ${result.value.transport.path}`);
102
+ }
103
+ console.log(`Healthy: ${result.value.healthy ? "yes" : "no"}`);
104
+ if (result.value.catalog.id) {
105
+ console.log(`Catalog: ${result.value.catalog.id}`);
106
+ }
107
+ });
108
+ daemon
109
+ .command("start")
110
+ .description("Start daemon via configured service manager or direct fallback")
111
+ .action(async () => {
112
+ await handleLifecycleAction("start", program, invokeDaemon);
113
+ });
114
+ daemon
115
+ .command("stop")
116
+ .description("Stop daemon via configured service manager or direct fallback")
117
+ .action(async () => {
118
+ await handleLifecycleAction("stop", program, invokeDaemon);
119
+ });
120
+ daemon
121
+ .command("restart")
122
+ .description("Restart daemon via configured service manager or direct fallback")
123
+ .action(async () => {
124
+ await handleLifecycleAction("restart", program, invokeDaemon);
125
+ });
126
+ }
127
+ async function handleLifecycleAction(action, program, invokeDaemon) {
128
+ const context = resolveDaemonCommandContext(program);
129
+ const format = program.opts().format;
130
+ let mode;
131
+ try {
132
+ mode = resolveLifecycleMode();
133
+ }
134
+ catch (error) {
135
+ const message = error instanceof Error ? error.message : String(error);
136
+ const invalidModeResult = {
137
+ action,
138
+ ok: false,
139
+ mode: "direct",
140
+ delegated: false,
141
+ message,
142
+ };
143
+ renderLifecycleResult(invalidModeResult, format);
144
+ process.exitCode = 1;
145
+ return;
146
+ }
147
+ const result = await executeLifecycleAction(action, mode, context, invokeDaemon);
148
+ renderLifecycleResult(result, format);
149
+ if (!result.ok) {
150
+ process.exitCode = 1;
151
+ }
152
+ }
153
+ function renderLifecycleResult(result, format) {
154
+ if (format === "json") {
155
+ console.log(formatJSON(result));
156
+ return;
157
+ }
158
+ if (!result.ok) {
159
+ console.error(`Error: ${result.message}`);
160
+ console.error(`Mode: ${result.mode}`);
161
+ if (result.details) {
162
+ console.error(`Details: ${result.details}`);
163
+ }
164
+ return;
165
+ }
166
+ console.log(`Daemon ${result.action}: ${result.message}`);
167
+ console.log(`Mode: ${result.mode}${result.delegated ? " (delegated)" : ""}`);
168
+ if (typeof result.running === "boolean") {
169
+ console.log(`Running: ${result.running ? "yes" : "no"}`);
170
+ }
171
+ if (typeof result.pid === "number") {
172
+ console.log(`PID: ${result.pid}`);
173
+ }
174
+ if (result.details) {
175
+ console.log(`Details: ${result.details}`);
176
+ }
177
+ }
178
+ async function executeLifecycleAction(action, mode, context, invokeDaemon) {
179
+ if (mode === "systemd-user") {
180
+ return executeSystemdLifecycleAction(action);
181
+ }
182
+ if (mode === "launchd") {
183
+ return executeLaunchdLifecycleAction(action);
184
+ }
185
+ if (action === "start") {
186
+ return executeDirectStart(action, context, invokeDaemon);
187
+ }
188
+ if (action === "stop") {
189
+ return executeDirectStop(action, context, invokeDaemon, {
190
+ allowUnmanagedRunning: false,
191
+ });
192
+ }
193
+ const stop = await executeDirectStop("restart", context, invokeDaemon, {
194
+ allowUnmanagedRunning: false,
195
+ });
196
+ if (!stop.ok) {
197
+ return stop;
198
+ }
199
+ return executeDirectStart("restart", context, invokeDaemon);
200
+ }
201
+ function executeSystemdLifecycleAction(action) {
202
+ const servicePath = path.join(os.homedir(), SYSTEMD_SERVICE_PATH);
203
+ if (!fs.existsSync(servicePath)) {
204
+ return {
205
+ action,
206
+ ok: false,
207
+ mode: "systemd-user",
208
+ delegated: true,
209
+ message: `systemd user service is not configured at ${servicePath}`,
210
+ details: "Create the service using docs/daemon-service-operations.md or use direct mode.",
211
+ };
212
+ }
213
+ const command = runCommand("systemctl", ["--user", action, SYSTEMD_SERVICE_NAME]);
214
+ if (!command.ok) {
215
+ return {
216
+ action,
217
+ ok: false,
218
+ mode: "systemd-user",
219
+ delegated: true,
220
+ message: `systemctl --user ${action} ${SYSTEMD_SERVICE_NAME} failed`,
221
+ details: command.message,
222
+ };
223
+ }
224
+ return {
225
+ action,
226
+ ok: true,
227
+ mode: "systemd-user",
228
+ delegated: true,
229
+ message: `${action} delegated to systemd user service`,
230
+ details: servicePath,
231
+ };
232
+ }
233
+ function executeLaunchdLifecycleAction(action) {
234
+ const plistPath = path.join(os.homedir(), LAUNCHD_PLIST_PATH);
235
+ const uid = process.getuid?.();
236
+ if (!Number.isInteger(uid)) {
237
+ return {
238
+ action,
239
+ ok: false,
240
+ mode: "launchd",
241
+ delegated: true,
242
+ message: "launchd delegation requires a macOS user session with a numeric uid",
243
+ };
244
+ }
245
+ if (!fs.existsSync(plistPath)) {
246
+ return {
247
+ action,
248
+ ok: false,
249
+ mode: "launchd",
250
+ delegated: true,
251
+ message: `launchd plist is not configured at ${plistPath}`,
252
+ details: "Create the LaunchAgent using docs/daemon-service-operations.md or use direct mode.",
253
+ };
254
+ }
255
+ const domainTarget = `gui/${uid}`;
256
+ const serviceTarget = `${domainTarget}/${LAUNCHD_LABEL}`;
257
+ if (action === "stop") {
258
+ const stop = runCommand("launchctl", ["bootout", domainTarget, plistPath]);
259
+ if (!stop.ok && !isLaunchdAlreadyStopped(stop.message)) {
260
+ return {
261
+ action,
262
+ ok: false,
263
+ mode: "launchd",
264
+ delegated: true,
265
+ message: `launchctl bootout failed for ${serviceTarget}`,
266
+ details: stop.message,
267
+ };
268
+ }
269
+ return {
270
+ action,
271
+ ok: true,
272
+ mode: "launchd",
273
+ delegated: true,
274
+ message: "stop delegated to launchd",
275
+ details: plistPath,
276
+ };
277
+ }
278
+ if (action === "restart") {
279
+ const start = runLaunchdStart(serviceTarget, domainTarget, plistPath);
280
+ if (!start.ok) {
281
+ return {
282
+ action,
283
+ ok: false,
284
+ mode: "launchd",
285
+ delegated: true,
286
+ message: `launchctl restart failed for ${serviceTarget}`,
287
+ details: start.message,
288
+ };
289
+ }
290
+ return {
291
+ action,
292
+ ok: true,
293
+ mode: "launchd",
294
+ delegated: true,
295
+ message: "restart delegated to launchd",
296
+ details: plistPath,
297
+ };
298
+ }
299
+ const start = runLaunchdStart(serviceTarget, domainTarget, plistPath);
300
+ if (!start.ok) {
301
+ return {
302
+ action,
303
+ ok: false,
304
+ mode: "launchd",
305
+ delegated: true,
306
+ message: `launchctl start failed for ${serviceTarget}`,
307
+ details: start.message,
308
+ };
309
+ }
310
+ return {
311
+ action,
312
+ ok: true,
313
+ mode: "launchd",
314
+ delegated: true,
315
+ message: "start delegated to launchd",
316
+ details: plistPath,
317
+ };
318
+ }
319
+ function runLaunchdStart(serviceTarget, domainTarget, plistPath) {
320
+ const print = runCommand("launchctl", ["print", serviceTarget]);
321
+ if (!print.ok) {
322
+ const bootstrap = runCommand("launchctl", ["bootstrap", domainTarget, plistPath]);
323
+ if (!bootstrap.ok && !isLaunchdAlreadyLoaded(bootstrap.message)) {
324
+ return bootstrap;
325
+ }
326
+ }
327
+ return runCommand("launchctl", ["kickstart", "-k", serviceTarget]);
328
+ }
329
+ function isLaunchdAlreadyLoaded(message) {
330
+ return message.includes("already loaded") || message.includes("service already bootstrapped");
331
+ }
332
+ function isLaunchdAlreadyStopped(message) {
333
+ return message.includes("No such process") || message.includes("Could not find service");
334
+ }
335
+ async function executeDirectStart(action, context, invokeDaemon) {
336
+ const pidInfo = readDaemonPid(context.daemonPidPath);
337
+ if (pidInfo.pid && isProcessAlive(pidInfo.pid)) {
338
+ const running = await waitForDaemonRunning(invokeDaemon, STARTUP_TIMEOUT_MS);
339
+ if (!running.running) {
340
+ return {
341
+ action,
342
+ ok: false,
343
+ mode: "direct",
344
+ delegated: false,
345
+ message: "managed daemon process exists but daemon socket is not responding",
346
+ pid: pidInfo.pid,
347
+ };
348
+ }
349
+ return {
350
+ action,
351
+ ok: true,
352
+ mode: "direct",
353
+ delegated: false,
354
+ message: "daemon already running",
355
+ running: true,
356
+ pid: pidInfo.pid,
357
+ };
358
+ }
359
+ if (pidInfo.pid && !isProcessAlive(pidInfo.pid)) {
360
+ safeUnlink(context.daemonPidPath);
361
+ }
362
+ const unmanagedRunning = await waitForDaemonRunning(invokeDaemon, 400);
363
+ if (unmanagedRunning.running) {
364
+ return {
365
+ action,
366
+ ok: false,
367
+ mode: "direct",
368
+ delegated: false,
369
+ message: "daemon is running but not managed by direct lifecycle wrapper",
370
+ details: "Use daemon stop via the service manager that started it.",
371
+ };
372
+ }
373
+ if (!fs.existsSync(context.daemonEntrypoint)) {
374
+ return {
375
+ action,
376
+ ok: false,
377
+ mode: "direct",
378
+ delegated: false,
379
+ message: `daemon entrypoint not found at ${context.daemonEntrypoint}`,
380
+ };
381
+ }
382
+ fs.mkdirSync(context.storagePath, { recursive: true });
383
+ const daemonProcess = spawn(process.execPath, [context.daemonEntrypoint], {
384
+ cwd: process.cwd(),
385
+ detached: true,
386
+ stdio: "ignore",
387
+ env: createDaemonChildEnv(context),
388
+ });
389
+ const daemonPid = daemonProcess.pid;
390
+ if (!daemonPid) {
391
+ return {
392
+ action,
393
+ ok: false,
394
+ mode: "direct",
395
+ delegated: false,
396
+ message: "failed to spawn daemon process",
397
+ };
398
+ }
399
+ daemonProcess.unref();
400
+ try {
401
+ fs.writeFileSync(context.daemonPidPath, `${daemonPid}\n`, "utf8");
402
+ }
403
+ catch (error) {
404
+ terminateProcess(daemonPid, "SIGTERM");
405
+ return {
406
+ action,
407
+ ok: false,
408
+ mode: "direct",
409
+ delegated: false,
410
+ message: "failed to write daemon pid file",
411
+ details: error instanceof Error ? error.message : String(error),
412
+ };
413
+ }
414
+ const running = await waitForDaemonRunning(invokeDaemon, STARTUP_TIMEOUT_MS);
415
+ if (!running.running) {
416
+ terminateProcess(daemonPid, "SIGTERM");
417
+ safeUnlink(context.daemonPidPath);
418
+ return {
419
+ action,
420
+ ok: false,
421
+ mode: "direct",
422
+ delegated: false,
423
+ message: "daemon failed to become healthy after start",
424
+ pid: daemonPid,
425
+ details: running.reason,
426
+ };
427
+ }
428
+ return {
429
+ action,
430
+ ok: true,
431
+ mode: "direct",
432
+ delegated: false,
433
+ message: "started managed daemon process",
434
+ running: true,
435
+ pid: daemonPid,
436
+ };
437
+ }
438
+ async function executeDirectStop(action, context, invokeDaemon, options = { allowUnmanagedRunning: true }) {
439
+ const pidInfo = readDaemonPid(context.daemonPidPath);
440
+ if (!pidInfo.pid) {
441
+ const running = await waitForDaemonRunning(invokeDaemon, 400);
442
+ if (running.running && !options.allowUnmanagedRunning) {
443
+ return {
444
+ action,
445
+ ok: false,
446
+ mode: "direct",
447
+ delegated: false,
448
+ message: "daemon is running but not managed by direct lifecycle wrapper",
449
+ details: "Stop it through the service manager that started it.",
450
+ };
451
+ }
452
+ return {
453
+ action,
454
+ ok: true,
455
+ mode: "direct",
456
+ delegated: false,
457
+ message: "daemon already stopped",
458
+ running: false,
459
+ };
460
+ }
461
+ if (!isProcessAlive(pidInfo.pid)) {
462
+ safeUnlink(context.daemonPidPath);
463
+ return {
464
+ action,
465
+ ok: true,
466
+ mode: "direct",
467
+ delegated: false,
468
+ message: "removed stale daemon pid file",
469
+ running: false,
470
+ };
471
+ }
472
+ terminateProcess(pidInfo.pid, "SIGTERM");
473
+ const stopped = await waitForProcessExit(pidInfo.pid, STOP_TIMEOUT_MS);
474
+ if (!stopped) {
475
+ terminateProcess(pidInfo.pid, "SIGKILL");
476
+ const killed = await waitForProcessExit(pidInfo.pid, 1_000);
477
+ if (!killed) {
478
+ return {
479
+ action,
480
+ ok: false,
481
+ mode: "direct",
482
+ delegated: false,
483
+ message: `failed to stop managed daemon process ${pidInfo.pid}`,
484
+ };
485
+ }
486
+ }
487
+ safeUnlink(context.daemonPidPath);
488
+ const running = await waitForDaemonRunning(invokeDaemon, 1_000);
489
+ if (running.running) {
490
+ return {
491
+ action,
492
+ ok: false,
493
+ mode: "direct",
494
+ delegated: false,
495
+ message: "daemon socket still responds after managed process stop",
496
+ };
497
+ }
498
+ return {
499
+ action,
500
+ ok: true,
501
+ mode: "direct",
502
+ delegated: false,
503
+ message: "stopped managed daemon process",
504
+ running: false,
505
+ };
506
+ }
507
+ async function waitForDaemonRunning(invokeDaemon, timeoutMs) {
508
+ const deadline = Date.now() + timeoutMs;
509
+ let lastReason;
510
+ while (Date.now() < deadline) {
511
+ const status = await invokeDaemon("daemon.status", {});
512
+ if (status.ok) {
513
+ if (status.value.state === "running" && status.value.healthy) {
514
+ return { running: true };
515
+ }
516
+ lastReason = `state=${status.value.state}, healthy=${status.value.healthy ? "yes" : "no"}`;
517
+ await sleep(100);
518
+ continue;
519
+ }
520
+ lastReason = status.error.message;
521
+ await sleep(100);
522
+ }
523
+ return {
524
+ running: false,
525
+ reason: lastReason,
526
+ };
527
+ }
528
+ async function waitForProcessExit(pid, timeoutMs) {
529
+ const deadline = Date.now() + timeoutMs;
530
+ while (Date.now() < deadline) {
531
+ if (!isProcessAlive(pid)) {
532
+ return true;
533
+ }
534
+ await sleep(50);
535
+ }
536
+ return !isProcessAlive(pid);
537
+ }
538
+ function terminateProcess(pid, signal) {
539
+ try {
540
+ process.kill(pid, signal);
541
+ }
542
+ catch {
543
+ // Process may have already exited.
544
+ }
545
+ }
546
+ function isProcessAlive(pid) {
547
+ try {
548
+ process.kill(pid, 0);
549
+ return true;
550
+ }
551
+ catch {
552
+ return false;
553
+ }
554
+ }
555
+ function readDaemonPid(pidPath) {
556
+ try {
557
+ const raw = fs.readFileSync(pidPath, "utf8").trim();
558
+ if (!raw) {
559
+ return { pid: null };
560
+ }
561
+ const pid = Number.parseInt(raw, 10);
562
+ if (!Number.isInteger(pid) || pid < 1) {
563
+ return { pid: null };
564
+ }
565
+ return { pid };
566
+ }
567
+ catch {
568
+ return { pid: null };
569
+ }
570
+ }
571
+ function safeUnlink(filePath) {
572
+ try {
573
+ fs.unlinkSync(filePath);
574
+ }
575
+ catch {
576
+ // File may already be removed.
577
+ }
578
+ }
579
+ function resolveDaemonCommandContext(program) {
580
+ const configOpt = program.opts().config;
581
+ const configPath = getConfigPath(configOpt);
582
+ const fileConfig = loadConfig(configPath);
583
+ const storagePath = resolveDataDir(configPath, fileConfig);
584
+ const remoteSync = resolveRemoteSyncConfig(fileConfig);
585
+ const assignedWorkers = resolveDaemonAssignedWorkers(fileConfig);
586
+ const pluginPaths = resolveDaemonPluginPaths(configPath, fileConfig);
587
+ const pluginConfig = resolveDaemonPluginConfig(fileConfig);
588
+ return {
589
+ storagePath,
590
+ socketPath: resolveDaemonSocketPath(storagePath),
591
+ daemonPidPath: path.join(storagePath, DIRECT_PID_FILENAME),
592
+ daemonEntrypoint: resolveDaemonEntrypoint(),
593
+ remoteSyncServer: remoteSync?.server ?? null,
594
+ assignedWorkersEnvValue: assignedWorkers.value,
595
+ pluginPathsEnvValue: pluginPaths.value,
596
+ pluginConfigEnvValue: pluginConfig.value,
597
+ };
598
+ }
599
+ function createDaemonChildEnv(context) {
600
+ const childEnv = {
601
+ ...process.env,
602
+ TODUAI_DATA_DIR: context.storagePath,
603
+ };
604
+ if (context.remoteSyncServer) {
605
+ childEnv.TODUAI_SYNC_SERVER = context.remoteSyncServer;
606
+ childEnv.TODUAI_SYNC_ENABLED = "1";
607
+ }
608
+ if (context.socketPath) {
609
+ childEnv.TODUAI_DAEMON_SOCKET = context.socketPath;
610
+ }
611
+ if (context.assignedWorkersEnvValue !== undefined) {
612
+ childEnv[TODUAI_DAEMON_ASSIGNED_WORKERS_ENV] = context.assignedWorkersEnvValue;
613
+ }
614
+ if (context.pluginPathsEnvValue !== undefined) {
615
+ childEnv[TODUAI_DAEMON_PLUGIN_PATHS_ENV] = context.pluginPathsEnvValue;
616
+ }
617
+ if (context.pluginConfigEnvValue !== undefined) {
618
+ childEnv[TODUAI_DAEMON_PLUGIN_CONFIG_ENV] = context.pluginConfigEnvValue;
619
+ }
620
+ return childEnv;
621
+ }
622
+ function resolveLifecycleMode() {
623
+ const override = process.env[LIFECYCLE_MODE_ENV]?.trim();
624
+ if (override && override !== "auto") {
625
+ if (override === "direct" || override === "systemd-user" || override === "launchd") {
626
+ return override;
627
+ }
628
+ throw new Error(`Invalid ${LIFECYCLE_MODE_ENV} value: ${override}. Expected auto, direct, systemd-user, or launchd.`);
629
+ }
630
+ if (process.platform === "linux" &&
631
+ fs.existsSync(path.join(os.homedir(), SYSTEMD_SERVICE_PATH))) {
632
+ return "systemd-user";
633
+ }
634
+ if (process.platform === "darwin" && fs.existsSync(path.join(os.homedir(), LAUNCHD_PLIST_PATH))) {
635
+ return "launchd";
636
+ }
637
+ return "direct";
638
+ }
639
+ function runCommand(command, args) {
640
+ const result = spawnSync(command, args, {
641
+ encoding: "utf8",
642
+ stdio: ["ignore", "pipe", "pipe"],
643
+ });
644
+ if (result.error) {
645
+ return {
646
+ ok: false,
647
+ message: result.error.message,
648
+ };
649
+ }
650
+ if (result.status !== 0) {
651
+ const stderr = result.stderr?.trim();
652
+ const stdout = result.stdout?.trim();
653
+ return {
654
+ ok: false,
655
+ message: stderr || stdout || `exit code ${result.status}`,
656
+ };
657
+ }
658
+ return {
659
+ ok: true,
660
+ message: result.stdout?.trim() ?? "",
661
+ };
662
+ }
663
+ function resolveDaemonEntrypoint() {
664
+ const explicit = process.env.TODUAI_DAEMON_ENTRYPOINT;
665
+ if (explicit && explicit.trim().length > 0) {
666
+ return path.resolve(explicit);
667
+ }
668
+ return path.resolve(import.meta.dirname, "../../../daemon/dist/entrypoint.js");
669
+ }
670
+ function sleep(ms) {
671
+ return new Promise((resolve) => {
672
+ setTimeout(resolve, ms);
673
+ });
674
+ }
675
+ //# sourceMappingURL=daemon.js.map