opencode-scheduled-tasks 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/dist/cli.js ADDED
@@ -0,0 +1,1225 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { fileURLToPath as fileURLToPath2 } from "url";
5
+ import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
6
+ import { dirname as dirname3, join as join3 } from "path";
7
+
8
+ // src/lib/db.ts
9
+ import { randomUUID } from "crypto";
10
+ import { mkdirSync } from "fs";
11
+ import { dirname } from "path";
12
+
13
+ // src/lib/sqlite.ts
14
+ import { createRequire } from "module";
15
+ var isBun = typeof globalThis.Bun !== "undefined";
16
+ var require2 = createRequire(import.meta.url);
17
+ function openDatabase(path) {
18
+ if (isBun) {
19
+ return openBunDatabase(path);
20
+ }
21
+ return openNodeDatabase(path);
22
+ }
23
+ function openBunDatabase(path) {
24
+ const { Database: BunDatabase } = require2("bun:sqlite");
25
+ const db = new BunDatabase(path);
26
+ return {
27
+ exec(sql) {
28
+ db.exec(sql);
29
+ },
30
+ prepare(sql) {
31
+ const stmt = db.prepare(sql);
32
+ return {
33
+ run(...params) {
34
+ const result = stmt.run(...params);
35
+ return { changes: result.changes ?? 0 };
36
+ },
37
+ get(...params) {
38
+ return stmt.get(...params);
39
+ },
40
+ all(...params) {
41
+ return stmt.all(...params);
42
+ }
43
+ };
44
+ },
45
+ pragma(pragma) {
46
+ return db.exec(`PRAGMA ${pragma}`);
47
+ },
48
+ close() {
49
+ db.close();
50
+ }
51
+ };
52
+ }
53
+ function openNodeDatabase(path) {
54
+ const BetterSqlite3 = require2("better-sqlite3");
55
+ const db = new BetterSqlite3(path);
56
+ return {
57
+ exec(sql) {
58
+ db.exec(sql);
59
+ },
60
+ prepare(sql) {
61
+ const stmt = db.prepare(sql);
62
+ return {
63
+ run(...params) {
64
+ const result = stmt.run(...params);
65
+ return { changes: result.changes ?? 0 };
66
+ },
67
+ get(...params) {
68
+ return stmt.get(...params);
69
+ },
70
+ all(...params) {
71
+ return stmt.all(...params);
72
+ }
73
+ };
74
+ },
75
+ pragma(pragma) {
76
+ return db.pragma(pragma);
77
+ },
78
+ close() {
79
+ db.close();
80
+ }
81
+ };
82
+ }
83
+
84
+ // src/lib/db.ts
85
+ var SCHEMA_VERSION = 2;
86
+ var SCHEMA_V1 = `
87
+ CREATE TABLE IF NOT EXISTS schema_version (
88
+ version INTEGER PRIMARY KEY
89
+ );
90
+
91
+ CREATE TABLE IF NOT EXISTS oneoff_tasks (
92
+ id TEXT PRIMARY KEY,
93
+ description TEXT NOT NULL,
94
+ prompt TEXT NOT NULL,
95
+ cwd TEXT NOT NULL,
96
+ scheduled_at TEXT NOT NULL,
97
+ session_mode TEXT NOT NULL DEFAULT 'new',
98
+ session_name TEXT,
99
+ model TEXT,
100
+ agent TEXT,
101
+ permission TEXT,
102
+ status TEXT NOT NULL DEFAULT 'pending',
103
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
104
+ executed_at TEXT,
105
+ session_id TEXT,
106
+ error TEXT,
107
+ created_by_session TEXT
108
+ );
109
+
110
+ CREATE TABLE IF NOT EXISTS task_runs (
111
+ id TEXT PRIMARY KEY,
112
+ task_name TEXT NOT NULL,
113
+ started_at TEXT NOT NULL,
114
+ completed_at TEXT,
115
+ status TEXT NOT NULL DEFAULT 'running',
116
+ session_id TEXT,
117
+ error TEXT
118
+ );
119
+
120
+ CREATE TABLE IF NOT EXISTS session_map (
121
+ session_name TEXT PRIMARY KEY,
122
+ session_id TEXT NOT NULL,
123
+ task_name TEXT,
124
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
125
+ );
126
+ `;
127
+ var MIGRATION_V2 = `
128
+ ALTER TABLE task_runs ADD COLUMN pid INTEGER;
129
+ ALTER TABLE oneoff_tasks ADD COLUMN pid INTEGER;
130
+ `;
131
+ var TaskDatabase = class {
132
+ db;
133
+ constructor(dbPath) {
134
+ mkdirSync(dirname(dbPath), { recursive: true });
135
+ this.db = openDatabase(dbPath);
136
+ this.db.pragma("journal_mode = WAL");
137
+ this.db.pragma("foreign_keys = ON");
138
+ this.initialize();
139
+ }
140
+ initialize() {
141
+ const versionExists = this.db.prepare(
142
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'"
143
+ ).get();
144
+ if (!versionExists) {
145
+ this.db.exec(SCHEMA_V1);
146
+ this.db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(1);
147
+ }
148
+ const row = this.db.prepare("SELECT MAX(version) as version FROM schema_version").get();
149
+ const currentVersion = row?.version ?? 0;
150
+ if (currentVersion < SCHEMA_VERSION) {
151
+ this.migrate(currentVersion);
152
+ }
153
+ }
154
+ migrate(fromVersion) {
155
+ if (fromVersion < 2) {
156
+ const statements = MIGRATION_V2.trim().split(";").filter(Boolean);
157
+ for (const stmt of statements) {
158
+ try {
159
+ this.db.exec(stmt.trim() + ";");
160
+ } catch {
161
+ }
162
+ }
163
+ this.db.prepare(
164
+ "INSERT OR REPLACE INTO schema_version (version) VALUES (?)"
165
+ ).run(2);
166
+ }
167
+ }
168
+ // --- One-off tasks ---
169
+ createOneoffTask(task) {
170
+ const id = randomUUID();
171
+ this.db.prepare(
172
+ `INSERT INTO oneoff_tasks (id, description, prompt, cwd, scheduled_at, session_name, model, agent, permission, created_by_session)
173
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
174
+ ).run(
175
+ id,
176
+ task.description,
177
+ task.prompt,
178
+ task.cwd,
179
+ task.scheduledAt,
180
+ task.sessionName ?? null,
181
+ task.model ?? null,
182
+ task.agent ?? null,
183
+ task.permission ? JSON.stringify(task.permission) : null,
184
+ task.createdBySession ?? null
185
+ );
186
+ return this.getOneoffTask(id);
187
+ }
188
+ getOneoffTask(id) {
189
+ const row = this.db.prepare("SELECT * FROM oneoff_tasks WHERE id = ?").get(id);
190
+ return row ? this.mapOneoffRow(row) : void 0;
191
+ }
192
+ listOneoffTasks(options) {
193
+ const status = options?.status ?? "all";
194
+ let rows;
195
+ if (status === "all") {
196
+ rows = this.db.prepare("SELECT * FROM oneoff_tasks ORDER BY scheduled_at ASC").all();
197
+ } else {
198
+ rows = this.db.prepare(
199
+ "SELECT * FROM oneoff_tasks WHERE status = ? ORDER BY scheduled_at ASC"
200
+ ).all(status);
201
+ }
202
+ return rows.map((r) => this.mapOneoffRow(r));
203
+ }
204
+ getDueOneoffTasks() {
205
+ return this.db.prepare(
206
+ "SELECT * FROM oneoff_tasks WHERE status = 'pending' AND scheduled_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now') ORDER BY scheduled_at ASC"
207
+ ).all().map((r) => this.mapOneoffRow(r));
208
+ }
209
+ updateOneoffTaskStatus(id, status, extra) {
210
+ const executedAt = status === "running" ? (/* @__PURE__ */ new Date()).toISOString() : void 0;
211
+ this.db.prepare(
212
+ `UPDATE oneoff_tasks SET status = ?, executed_at = COALESCE(?, executed_at), session_id = COALESCE(?, session_id), error = ?, pid = COALESCE(?, pid) WHERE id = ?`
213
+ ).run(
214
+ status,
215
+ executedAt ?? null,
216
+ extra?.sessionId ?? null,
217
+ extra?.error ?? null,
218
+ extra?.pid ?? null,
219
+ id
220
+ );
221
+ }
222
+ setTaskRunPid(id, pid) {
223
+ this.db.prepare("UPDATE task_runs SET pid = ? WHERE id = ?").run(pid, id);
224
+ }
225
+ cancelOneoffTask(id) {
226
+ const result = this.db.prepare(
227
+ "UPDATE oneoff_tasks SET status = 'cancelled' WHERE id = ? AND status = 'pending'"
228
+ ).run(id);
229
+ return result.changes > 0;
230
+ }
231
+ // --- Task runs (recurring) ---
232
+ createTaskRun(taskName, pid) {
233
+ const id = randomUUID();
234
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
235
+ this.db.prepare(
236
+ "INSERT INTO task_runs (id, task_name, started_at, pid) VALUES (?, ?, ?, ?)"
237
+ ).run(id, taskName, startedAt, pid ?? null);
238
+ return { id, taskName, startedAt, status: "running", pid };
239
+ }
240
+ completeTaskRun(id, status, extra) {
241
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
242
+ this.db.prepare(
243
+ `UPDATE task_runs SET status = ?, completed_at = ?, session_id = COALESCE(?, session_id), error = ? WHERE id = ?`
244
+ ).run(
245
+ status,
246
+ completedAt,
247
+ extra?.sessionId ?? null,
248
+ extra?.error ?? null,
249
+ id
250
+ );
251
+ }
252
+ getLastTaskRun(taskName) {
253
+ const row = this.db.prepare(
254
+ "SELECT * FROM task_runs WHERE task_name = ? ORDER BY started_at DESC LIMIT 1"
255
+ ).get(taskName);
256
+ return row ? this.mapTaskRunRow(row) : void 0;
257
+ }
258
+ getLastSuccessfulTaskRun(taskName) {
259
+ const row = this.db.prepare(
260
+ "SELECT * FROM task_runs WHERE task_name = ? AND status = 'completed' ORDER BY started_at DESC LIMIT 1"
261
+ ).get(taskName);
262
+ return row ? this.mapTaskRunRow(row) : void 0;
263
+ }
264
+ getTaskRunHistory(taskName, limit = 10) {
265
+ return this.db.prepare(
266
+ "SELECT * FROM task_runs WHERE task_name = ? ORDER BY started_at DESC LIMIT ?"
267
+ ).all(taskName, limit).map((r) => this.mapTaskRunRow(r));
268
+ }
269
+ hasRunningTask(taskName) {
270
+ const row = this.db.prepare(
271
+ "SELECT id FROM task_runs WHERE task_name = ? AND status = 'running' LIMIT 1"
272
+ ).get(taskName);
273
+ return !!row;
274
+ }
275
+ hasRunningOneoffTask(id) {
276
+ const row = this.db.prepare(
277
+ "SELECT id FROM oneoff_tasks WHERE id = ? AND status = 'running' LIMIT 1"
278
+ ).get(id);
279
+ return !!row;
280
+ }
281
+ /**
282
+ * Get all running task runs (for PID-based reaping).
283
+ */
284
+ getRunningTaskRuns() {
285
+ return this.db.prepare("SELECT * FROM task_runs WHERE status = 'running'").all().map((r) => this.mapTaskRunRow(r));
286
+ }
287
+ /**
288
+ * Get all running one-off tasks (for PID-based reaping).
289
+ */
290
+ getRunningOneoffTasks() {
291
+ return this.db.prepare("SELECT * FROM oneoff_tasks WHERE status = 'running'").all().map((r) => this.mapOneoffRow(r));
292
+ }
293
+ /**
294
+ * Mark stale running records as failed.
295
+ * A record is stale if it has a PID set and that process is no longer alive,
296
+ * or if it has no PID and is older than maxAgeMs (fallback for records
297
+ * created before async execution was added).
298
+ */
299
+ cleanupStaleRuns(maxAgeMs = 2 * 60 * 60 * 1e3) {
300
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
301
+ const taskRunResult = this.db.prepare(
302
+ "UPDATE task_runs SET status = 'failed', completed_at = datetime('now'), error = 'Timed out (stale running record)' WHERE status = 'running' AND pid IS NULL AND started_at < ?"
303
+ ).run(cutoff);
304
+ const oneoffResult = this.db.prepare(
305
+ "UPDATE oneoff_tasks SET status = 'failed', error = 'Timed out (stale running record)' WHERE status = 'running' AND pid IS NULL AND executed_at < ?"
306
+ ).run(cutoff);
307
+ return taskRunResult.changes + oneoffResult.changes;
308
+ }
309
+ // --- Session map ---
310
+ getSessionMapping(sessionName) {
311
+ const row = this.db.prepare("SELECT * FROM session_map WHERE session_name = ?").get(sessionName);
312
+ return row ? this.mapSessionMapRow(row) : void 0;
313
+ }
314
+ upsertSessionMapping(sessionName, sessionId, taskName) {
315
+ this.db.prepare(
316
+ `INSERT INTO session_map (session_name, session_id, task_name, updated_at)
317
+ VALUES (?, ?, ?, datetime('now'))
318
+ ON CONFLICT(session_name) DO UPDATE SET
319
+ session_id = excluded.session_id,
320
+ task_name = COALESCE(excluded.task_name, session_map.task_name),
321
+ updated_at = datetime('now')`
322
+ ).run(sessionName, sessionId, taskName ?? null);
323
+ }
324
+ // --- Row mappers ---
325
+ mapOneoffRow(row) {
326
+ return {
327
+ id: row.id,
328
+ description: row.description,
329
+ prompt: row.prompt,
330
+ cwd: row.cwd,
331
+ scheduledAt: row.scheduled_at,
332
+ sessionName: row.session_name ?? void 0,
333
+ model: row.model ?? void 0,
334
+ agent: row.agent ?? void 0,
335
+ permission: row.permission ? JSON.parse(row.permission) : void 0,
336
+ status: row.status,
337
+ createdAt: row.created_at,
338
+ executedAt: row.executed_at ?? void 0,
339
+ sessionId: row.session_id ?? void 0,
340
+ error: row.error ?? void 0,
341
+ createdBySession: row.created_by_session ?? void 0,
342
+ pid: row.pid ?? void 0
343
+ };
344
+ }
345
+ mapTaskRunRow(row) {
346
+ return {
347
+ id: row.id,
348
+ taskName: row.task_name,
349
+ startedAt: row.started_at,
350
+ completedAt: row.completed_at ?? void 0,
351
+ status: row.status,
352
+ sessionId: row.session_id ?? void 0,
353
+ error: row.error ?? void 0,
354
+ pid: row.pid ?? void 0
355
+ };
356
+ }
357
+ mapSessionMapRow(row) {
358
+ return {
359
+ sessionName: row.session_name,
360
+ sessionId: row.session_id,
361
+ taskName: row.task_name ?? void 0,
362
+ updatedAt: row.updated_at
363
+ };
364
+ }
365
+ close() {
366
+ this.db.close();
367
+ }
368
+ };
369
+ function getDefaultDbPath() {
370
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
371
+ return `${home}/.config/opencode/.tasks.db`;
372
+ }
373
+
374
+ // src/lib/tasks.ts
375
+ import matter from "gray-matter";
376
+ import { readdirSync, readFileSync, writeFileSync, existsSync } from "fs";
377
+ import { join, basename } from "path";
378
+ function getTasksDir() {
379
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
380
+ return join(home, ".config", "opencode", "tasks");
381
+ }
382
+ function expandPath(p) {
383
+ if (p.startsWith("~/") || p === "~") {
384
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
385
+ return join(home, p.slice(2));
386
+ }
387
+ return p;
388
+ }
389
+ function validateFrontmatter(data, fileName) {
390
+ const errors = [];
391
+ if (data.description !== void 0 && typeof data.description !== "string") {
392
+ errors.push("Invalid 'description' field (must be a string)");
393
+ }
394
+ if (!data.schedule || typeof data.schedule !== "string") {
395
+ errors.push("Missing or invalid 'schedule' field");
396
+ }
397
+ if (!data.cwd || typeof data.cwd !== "string") {
398
+ errors.push("Missing or invalid 'cwd' field");
399
+ }
400
+ if (data.session_name !== void 0 && typeof data.session_name !== "string") {
401
+ errors.push("Invalid 'session_name' field (must be a string)");
402
+ }
403
+ if (data.model !== void 0 && typeof data.model !== "string") {
404
+ errors.push("Invalid 'model' field (must be a string)");
405
+ }
406
+ if (data.agent !== void 0 && typeof data.agent !== "string") {
407
+ errors.push("Invalid 'agent' field (must be a string)");
408
+ }
409
+ if (data.enabled !== void 0 && typeof data.enabled !== "boolean") {
410
+ errors.push("Invalid 'enabled' field (must be a boolean)");
411
+ }
412
+ return errors;
413
+ }
414
+ function parseTaskFile(filePath) {
415
+ const content = readFileSync(filePath, "utf-8");
416
+ const fileName = basename(filePath);
417
+ const { data, content: body } = matter(content);
418
+ const fm = data;
419
+ const errors = validateFrontmatter(data, fileName);
420
+ if (errors.length > 0) {
421
+ throw new Error(
422
+ `Invalid task file "${fileName}":
423
+ - ${errors.join("\n - ")}`
424
+ );
425
+ }
426
+ const name = fileName.replace(/\.md$/, "");
427
+ return {
428
+ name,
429
+ description: fm.description,
430
+ schedule: fm.schedule,
431
+ cwd: fm.cwd,
432
+ sessionName: fm.session_name,
433
+ model: fm.model,
434
+ agent: fm.agent,
435
+ permission: fm.permission,
436
+ enabled: fm.enabled ?? true,
437
+ prompt: body.trim(),
438
+ filePath
439
+ };
440
+ }
441
+ function readAllTasks(tasksDir) {
442
+ const dir = tasksDir ?? getTasksDir();
443
+ const tasks = [];
444
+ const errors = [];
445
+ if (!existsSync(dir)) {
446
+ return { tasks, errors };
447
+ }
448
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
449
+ for (const file of files) {
450
+ const filePath = join(dir, file);
451
+ try {
452
+ const task = parseTaskFile(filePath);
453
+ tasks.push(task);
454
+ } catch (err) {
455
+ errors.push({ file, error: err.message });
456
+ }
457
+ }
458
+ return { tasks, errors };
459
+ }
460
+
461
+ // src/lib/cron.ts
462
+ import cronParser from "cron-parser";
463
+ var CronExpressionParser = cronParser.CronExpressionParser ?? cronParser;
464
+ function getNextRunTime(expression, after) {
465
+ const expr = CronExpressionParser.parse(expression, {
466
+ currentDate: after ?? /* @__PURE__ */ new Date()
467
+ });
468
+ const next = expr.next().toISOString();
469
+ if (!next) throw new Error(`No next run time for expression "${expression}"`);
470
+ return next;
471
+ }
472
+ function getPreviousRunTime(expression, before) {
473
+ const expr = CronExpressionParser.parse(expression, {
474
+ currentDate: before ?? /* @__PURE__ */ new Date()
475
+ });
476
+ const prev = expr.prev().toISOString();
477
+ if (!prev) throw new Error(`No previous run time for expression "${expression}"`);
478
+ return prev;
479
+ }
480
+ function isDue(expression, lastRunTime) {
481
+ if (!lastRunTime) {
482
+ const prevTrigger2 = new Date(getPreviousRunTime(expression));
483
+ const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3);
484
+ return prevTrigger2 >= twentyFourHoursAgo;
485
+ }
486
+ const prevTrigger = new Date(getPreviousRunTime(expression));
487
+ const lastRun = new Date(lastRunTime);
488
+ return prevTrigger > lastRun;
489
+ }
490
+
491
+ // src/lib/runner.ts
492
+ import { spawn } from "child_process";
493
+ function parseSessionIdFromJsonOutput(output) {
494
+ for (const line of output.split("\n")) {
495
+ if (!line.trim()) continue;
496
+ try {
497
+ const event = JSON.parse(line);
498
+ if (event.properties?.info?.id?.startsWith("ses_")) {
499
+ return event.properties.info.id;
500
+ }
501
+ if (event.sessionID?.startsWith("ses_")) {
502
+ return event.sessionID;
503
+ }
504
+ if (event.properties?.sessionID?.startsWith("ses_")) {
505
+ return event.properties.sessionID;
506
+ }
507
+ } catch {
508
+ }
509
+ }
510
+ return void 0;
511
+ }
512
+ function isProcessAlive(pid) {
513
+ try {
514
+ process.kill(pid, 0);
515
+ return true;
516
+ } catch {
517
+ return false;
518
+ }
519
+ }
520
+ function buildTaskCommand(task, db) {
521
+ const sessionArgs = [];
522
+ if (task.sessionName) {
523
+ const mapping = db.getSessionMapping(task.sessionName);
524
+ if (mapping) {
525
+ sessionArgs.push("--session", mapping.sessionId);
526
+ } else {
527
+ sessionArgs.push("--title", task.sessionName);
528
+ }
529
+ } else {
530
+ sessionArgs.push(
531
+ "--title",
532
+ `${task.name} - ${(/* @__PURE__ */ new Date()).toISOString()}`
533
+ );
534
+ }
535
+ const args = ["run", ...sessionArgs, "--format", "json"];
536
+ if (task.model) args.push("--model", task.model);
537
+ if (task.agent) args.push("--agent", task.agent);
538
+ args.push(task.prompt);
539
+ const env = {};
540
+ for (const [k, v] of Object.entries(process.env)) {
541
+ if (v !== void 0) env[k] = v;
542
+ }
543
+ if (task.permission) {
544
+ env.OPENCODE_PERMISSION = JSON.stringify(task.permission);
545
+ }
546
+ return { args, env, cwd: expandPath(task.cwd) };
547
+ }
548
+ function spawnWorker(schedulerPath, runId, isOneoff) {
549
+ const args = ["--exec-task", runId];
550
+ if (isOneoff) args.push("--oneoff");
551
+ const child = spawn(process.execPath, [schedulerPath, ...args], {
552
+ detached: true,
553
+ stdio: "ignore"
554
+ });
555
+ child.unref();
556
+ if (!child.pid) {
557
+ throw new Error("Failed to spawn worker process");
558
+ }
559
+ return child.pid;
560
+ }
561
+ async function execTaskAndUpdateDb(task, runId, isOneoff, db) {
562
+ const { args, env, cwd } = buildTaskCommand(task, db);
563
+ const { stdout, stderr, exitCode } = await new Promise((resolve2) => {
564
+ const child = spawn("opencode", args, {
565
+ cwd,
566
+ env,
567
+ stdio: ["ignore", "pipe", "pipe"]
568
+ });
569
+ let stdout2 = "";
570
+ let stderr2 = "";
571
+ child.stdout.on("data", (data) => {
572
+ stdout2 += data.toString();
573
+ });
574
+ child.stderr.on("data", (data) => {
575
+ stderr2 += data.toString();
576
+ });
577
+ child.on("close", (code) => {
578
+ resolve2({ stdout: stdout2, stderr: stderr2, exitCode: code ?? 1 });
579
+ });
580
+ child.on("error", (err) => {
581
+ resolve2({ stdout: stdout2, stderr: err.message, exitCode: 1 });
582
+ });
583
+ });
584
+ const success = exitCode === 0;
585
+ const sessionId = parseSessionIdFromJsonOutput(stdout);
586
+ if (task.sessionName && sessionId) {
587
+ db.upsertSessionMapping(task.sessionName, sessionId, task.name);
588
+ }
589
+ if (isOneoff) {
590
+ db.updateOneoffTaskStatus(runId, success ? "completed" : "failed", {
591
+ sessionId,
592
+ error: success ? void 0 : stderr.slice(0, 4096)
593
+ });
594
+ } else {
595
+ db.completeTaskRun(runId, success ? "completed" : "failed", {
596
+ sessionId,
597
+ error: success ? void 0 : stderr.slice(0, 4096)
598
+ });
599
+ }
600
+ }
601
+
602
+ // src/lib/installer.ts
603
+ import { execFileSync } from "child_process";
604
+ import {
605
+ existsSync as existsSync2,
606
+ mkdirSync as mkdirSync2,
607
+ unlinkSync,
608
+ writeFileSync as writeFileSync2
609
+ } from "fs";
610
+ import { basename as basename2, dirname as dirname2, join as join2, resolve } from "path";
611
+ import { fileURLToPath } from "url";
612
+ var LAUNCHD_LABEL = "ai.opencode.scheduled-tasks";
613
+ var SYSTEMD_SERVICE = "opencode-scheduler.service";
614
+ var SYSTEMD_TIMER = "opencode-scheduler.timer";
615
+ function detectPlatform() {
616
+ if (process.platform === "darwin") return "macos-launchd";
617
+ if (process.platform === "linux") {
618
+ try {
619
+ execFileSync("systemctl", ["--version"], { stdio: "ignore" });
620
+ return "linux-systemd";
621
+ } catch {
622
+ }
623
+ }
624
+ return "unsupported";
625
+ }
626
+ function resolveSchedulerPath() {
627
+ const thisFile = fileURLToPath(import.meta.url);
628
+ if (basename2(thisFile) === "cli.js") {
629
+ return resolve(thisFile);
630
+ }
631
+ const candidates = [
632
+ join2(dirname2(dirname2(thisFile)), "..", "dist", "cli.js"),
633
+ // from src/lib/
634
+ join2(dirname2(thisFile), "..", "dist", "cli.js"),
635
+ // from src/
636
+ join2(dirname2(dirname2(thisFile)), "cli.js")
637
+ // from dist/lib/ (if unbundled)
638
+ ];
639
+ for (const candidate of candidates) {
640
+ if (existsSync2(candidate)) {
641
+ return resolve(candidate);
642
+ }
643
+ }
644
+ try {
645
+ const result = execFileSync("which", ["opencode-scheduler"], {
646
+ encoding: "utf-8"
647
+ }).trim();
648
+ if (result) return resolve(result);
649
+ } catch {
650
+ }
651
+ throw new Error(
652
+ "Could not find the opencode-scheduler script. Make sure the package is properly installed."
653
+ );
654
+ }
655
+ function getHome() {
656
+ return process.env.HOME ?? process.env.USERPROFILE ?? "";
657
+ }
658
+ function getLogDir() {
659
+ const dir = join2(getHome(), ".local", "share", "opencode");
660
+ mkdirSync2(dir, { recursive: true });
661
+ return dir;
662
+ }
663
+ function getLaunchdPlistPath() {
664
+ return join2(
665
+ getHome(),
666
+ "Library",
667
+ "LaunchAgents",
668
+ `${LAUNCHD_LABEL}.plist`
669
+ );
670
+ }
671
+ function generateLaunchdPlist(nodePath, schedulerPath) {
672
+ const logDir = getLogDir();
673
+ const currentPath = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
674
+ return `<?xml version="1.0" encoding="UTF-8"?>
675
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
676
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
677
+ <plist version="1.0">
678
+ <dict>
679
+ <key>Label</key>
680
+ <string>${LAUNCHD_LABEL}</string>
681
+ <key>ProgramArguments</key>
682
+ <array>
683
+ <string>${nodePath}</string>
684
+ <string>${schedulerPath}</string>
685
+ <string>--run-once</string>
686
+ </array>
687
+ <key>StartInterval</key>
688
+ <integer>60</integer>
689
+ <key>StandardOutPath</key>
690
+ <string>${join2(logDir, "scheduler.log")}</string>
691
+ <key>StandardErrorPath</key>
692
+ <string>${join2(logDir, "scheduler.err")}</string>
693
+ <key>RunAtLoad</key>
694
+ <true/>
695
+ <key>EnvironmentVariables</key>
696
+ <dict>
697
+ <key>PATH</key>
698
+ <string>${currentPath}</string>
699
+ </dict>
700
+ </dict>
701
+ </plist>`;
702
+ }
703
+ async function installLaunchd() {
704
+ const nodePath = process.execPath;
705
+ const schedulerPath = resolveSchedulerPath();
706
+ const plistPath = getLaunchdPlistPath();
707
+ mkdirSync2(dirname2(plistPath), { recursive: true });
708
+ try {
709
+ execFileSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
710
+ } catch {
711
+ }
712
+ const plist = generateLaunchdPlist(nodePath, schedulerPath);
713
+ writeFileSync2(plistPath, plist);
714
+ execFileSync("launchctl", ["load", plistPath]);
715
+ console.log("Scheduler installed (macOS launchd)");
716
+ console.log(` Plist: ${plistPath}`);
717
+ console.log(` Node: ${nodePath}`);
718
+ console.log(` Script: ${schedulerPath}`);
719
+ console.log(` Interval: every 60 seconds`);
720
+ console.log(` Logs: ${getLogDir()}/scheduler.{log,err}`);
721
+ }
722
+ async function uninstallLaunchd() {
723
+ const plistPath = getLaunchdPlistPath();
724
+ if (!existsSync2(plistPath)) {
725
+ console.log("Scheduler is not installed (no launchd plist found)");
726
+ return;
727
+ }
728
+ try {
729
+ execFileSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
730
+ } catch {
731
+ }
732
+ unlinkSync(plistPath);
733
+ console.log("Scheduler uninstalled (macOS launchd)");
734
+ console.log(` Removed: ${plistPath}`);
735
+ }
736
+ function isLaunchdInstalled() {
737
+ return existsSync2(getLaunchdPlistPath());
738
+ }
739
+ function getSystemdDir() {
740
+ return join2(getHome(), ".config", "systemd", "user");
741
+ }
742
+ function generateSystemdService(nodePath, schedulerPath) {
743
+ const currentPath = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
744
+ return `[Unit]
745
+ Description=OpenCode Scheduled Tasks Runner
746
+
747
+ [Service]
748
+ Type=oneshot
749
+ ExecStart=${nodePath} ${schedulerPath} --run-once
750
+ Environment=PATH=${currentPath}
751
+ `;
752
+ }
753
+ function generateSystemdTimer() {
754
+ return `[Unit]
755
+ Description=OpenCode Scheduled Tasks Timer
756
+
757
+ [Timer]
758
+ OnBootSec=60
759
+ OnUnitActiveSec=60
760
+ AccuracySec=1s
761
+
762
+ [Install]
763
+ WantedBy=timers.target
764
+ `;
765
+ }
766
+ async function installSystemd() {
767
+ const nodePath = process.execPath;
768
+ const schedulerPath = resolveSchedulerPath();
769
+ const systemdDir = getSystemdDir();
770
+ mkdirSync2(systemdDir, { recursive: true });
771
+ const servicePath = join2(systemdDir, SYSTEMD_SERVICE);
772
+ const timerPath = join2(systemdDir, SYSTEMD_TIMER);
773
+ try {
774
+ execFileSync("systemctl", ["--user", "stop", SYSTEMD_TIMER], {
775
+ stdio: "ignore"
776
+ });
777
+ } catch {
778
+ }
779
+ writeFileSync2(servicePath, generateSystemdService(nodePath, schedulerPath));
780
+ writeFileSync2(timerPath, generateSystemdTimer());
781
+ execFileSync("systemctl", ["--user", "daemon-reload"]);
782
+ execFileSync("systemctl", ["--user", "enable", SYSTEMD_TIMER]);
783
+ execFileSync("systemctl", ["--user", "start", SYSTEMD_TIMER]);
784
+ console.log("Scheduler installed (Linux systemd)");
785
+ console.log(` Service: ${servicePath}`);
786
+ console.log(` Timer: ${timerPath}`);
787
+ console.log(` Node: ${nodePath}`);
788
+ console.log(` Script: ${schedulerPath}`);
789
+ console.log(` Interval: every 60 seconds`);
790
+ }
791
+ async function uninstallSystemd() {
792
+ const systemdDir = getSystemdDir();
793
+ const servicePath = join2(systemdDir, SYSTEMD_SERVICE);
794
+ const timerPath = join2(systemdDir, SYSTEMD_TIMER);
795
+ if (!existsSync2(timerPath) && !existsSync2(servicePath)) {
796
+ console.log("Scheduler is not installed (no systemd units found)");
797
+ return;
798
+ }
799
+ try {
800
+ execFileSync("systemctl", ["--user", "stop", SYSTEMD_TIMER], {
801
+ stdio: "ignore"
802
+ });
803
+ execFileSync("systemctl", ["--user", "disable", SYSTEMD_TIMER], {
804
+ stdio: "ignore"
805
+ });
806
+ } catch {
807
+ }
808
+ if (existsSync2(servicePath)) unlinkSync(servicePath);
809
+ if (existsSync2(timerPath)) unlinkSync(timerPath);
810
+ try {
811
+ execFileSync("systemctl", ["--user", "daemon-reload"]);
812
+ } catch {
813
+ }
814
+ console.log("Scheduler uninstalled (Linux systemd)");
815
+ console.log(` Removed: ${servicePath}`);
816
+ console.log(` Removed: ${timerPath}`);
817
+ }
818
+ function isSystemdInstalled() {
819
+ const systemdDir = getSystemdDir();
820
+ return existsSync2(join2(systemdDir, SYSTEMD_TIMER));
821
+ }
822
+ async function install() {
823
+ const platform = detectPlatform();
824
+ switch (platform) {
825
+ case "macos-launchd":
826
+ await installLaunchd();
827
+ break;
828
+ case "linux-systemd":
829
+ await installSystemd();
830
+ break;
831
+ case "unsupported":
832
+ console.error(
833
+ "Unsupported platform. Supported: macOS (launchd), Linux (systemd)."
834
+ );
835
+ console.error("You can still run the scheduler manually:");
836
+ console.error(" npx opencode-scheduler --run-once");
837
+ process.exit(1);
838
+ }
839
+ }
840
+ async function uninstall() {
841
+ const platform = detectPlatform();
842
+ switch (platform) {
843
+ case "macos-launchd":
844
+ await uninstallLaunchd();
845
+ break;
846
+ case "linux-systemd":
847
+ await uninstallSystemd();
848
+ break;
849
+ case "unsupported":
850
+ console.error("No supported init system found.");
851
+ process.exit(1);
852
+ }
853
+ }
854
+ function isInstalled() {
855
+ const platform = detectPlatform();
856
+ switch (platform) {
857
+ case "macos-launchd":
858
+ return isLaunchdInstalled();
859
+ case "linux-systemd":
860
+ return isSystemdInstalled();
861
+ default:
862
+ return false;
863
+ }
864
+ }
865
+ function getInstallInfo() {
866
+ const platform = detectPlatform();
867
+ const installed = isInstalled();
868
+ let details;
869
+ if (installed) {
870
+ switch (platform) {
871
+ case "macos-launchd":
872
+ details = `Plist: ${getLaunchdPlistPath()}`;
873
+ break;
874
+ case "linux-systemd":
875
+ details = `Timer: ${join2(getSystemdDir(), SYSTEMD_TIMER)}`;
876
+ break;
877
+ }
878
+ }
879
+ return { installed, platform, details };
880
+ }
881
+
882
+ // src/cli.ts
883
+ var SCHEDULER_PATH = fileURLToPath2(import.meta.url);
884
+ function reapWorkers(db) {
885
+ for (const run of db.getRunningTaskRuns()) {
886
+ if (!run.pid) continue;
887
+ if (!isProcessAlive(run.pid)) {
888
+ db.completeTaskRun(run.id, "failed", {
889
+ error: `Worker process (PID ${run.pid}) exited unexpectedly`
890
+ });
891
+ log(
892
+ `Reaped crashed worker for "${run.taskName}" (PID ${run.pid})`,
893
+ "error"
894
+ );
895
+ }
896
+ }
897
+ for (const task of db.getRunningOneoffTasks()) {
898
+ if (!task.pid) continue;
899
+ if (!isProcessAlive(task.pid)) {
900
+ db.updateOneoffTaskStatus(task.id, "failed", {
901
+ error: `Worker process (PID ${task.pid}) exited unexpectedly`
902
+ });
903
+ log(
904
+ `Reaped crashed worker for one-off "${task.description}" (PID ${task.pid})`,
905
+ "error"
906
+ );
907
+ }
908
+ }
909
+ }
910
+ function runTick() {
911
+ const db = new TaskDatabase(getDefaultDbPath());
912
+ try {
913
+ const staleCount = db.cleanupStaleRuns();
914
+ if (staleCount > 0) {
915
+ log(`Cleaned up ${staleCount} stale running record(s)`);
916
+ }
917
+ reapWorkers(db);
918
+ const { tasks, errors } = readAllTasks();
919
+ for (const { file, error } of errors) {
920
+ log(`Error parsing task file "${file}": ${error}`, "error");
921
+ }
922
+ for (const task of tasks) {
923
+ if (!task.enabled) continue;
924
+ if (db.hasRunningTask(task.name)) {
925
+ continue;
926
+ }
927
+ const lastRun = db.getLastSuccessfulTaskRun(task.name);
928
+ if (!isDue(task.schedule, lastRun?.startedAt)) {
929
+ continue;
930
+ }
931
+ log(`Spawning worker for recurring task: ${task.name}`);
932
+ const run = db.createTaskRun(task.name);
933
+ try {
934
+ const pid = spawnWorker(SCHEDULER_PATH, run.id, false);
935
+ db.setTaskRunPid(run.id, pid);
936
+ log(` Worker spawned (PID ${pid}, run ${run.id})`);
937
+ } catch (err) {
938
+ db.completeTaskRun(run.id, "failed", {
939
+ error: `Failed to spawn worker: ${err.message}`
940
+ });
941
+ log(` Failed to spawn worker: ${err.message}`, "error");
942
+ }
943
+ }
944
+ const dueTasks = db.getDueOneoffTasks();
945
+ for (const task of dueTasks) {
946
+ if (db.hasRunningOneoffTask(task.id)) {
947
+ continue;
948
+ }
949
+ log(`Spawning worker for one-off task: ${task.description} (${task.id})`);
950
+ db.updateOneoffTaskStatus(task.id, "running");
951
+ try {
952
+ const pid = spawnWorker(SCHEDULER_PATH, task.id, true);
953
+ db.updateOneoffTaskStatus(task.id, "running", { pid });
954
+ log(` Worker spawned (PID ${pid})`);
955
+ } catch (err) {
956
+ db.updateOneoffTaskStatus(task.id, "failed", {
957
+ error: `Failed to spawn worker: ${err.message}`
958
+ });
959
+ log(` Failed to spawn worker: ${err.message}`, "error");
960
+ }
961
+ }
962
+ } finally {
963
+ db.close();
964
+ }
965
+ }
966
+ async function execTask(runId, isOneoff) {
967
+ const db = new TaskDatabase(getDefaultDbPath());
968
+ try {
969
+ let config;
970
+ if (isOneoff) {
971
+ const task = db.getOneoffTask(runId);
972
+ if (!task) {
973
+ throw new Error(`One-off task not found: ${runId}`);
974
+ }
975
+ config = {
976
+ name: `oneoff-${task.id.slice(0, 8)}`,
977
+ prompt: task.prompt,
978
+ cwd: task.cwd,
979
+ sessionName: task.sessionName,
980
+ model: task.model,
981
+ agent: task.agent,
982
+ permission: task.permission
983
+ };
984
+ } else {
985
+ const runs = db.getTaskRunHistory(runId, 1);
986
+ const allRuns = db.getRunningTaskRuns();
987
+ const run = allRuns.find((r) => r.id === runId);
988
+ if (!run) {
989
+ throw new Error(`Task run not found: ${runId}`);
990
+ }
991
+ const { tasks } = readAllTasks();
992
+ const task = tasks.find((t) => t.name === run.taskName);
993
+ if (!task) {
994
+ throw new Error(`Task file not found for: ${run.taskName}`);
995
+ }
996
+ config = {
997
+ name: task.name,
998
+ prompt: task.prompt,
999
+ cwd: task.cwd,
1000
+ sessionName: task.sessionName,
1001
+ model: task.model,
1002
+ agent: task.agent,
1003
+ permission: task.permission
1004
+ };
1005
+ }
1006
+ await execTaskAndUpdateDb(config, runId, isOneoff, db);
1007
+ } finally {
1008
+ db.close();
1009
+ }
1010
+ }
1011
+ function listTasks() {
1012
+ const db = new TaskDatabase(getDefaultDbPath());
1013
+ try {
1014
+ const { tasks, errors } = readAllTasks();
1015
+ if (errors.length > 0) {
1016
+ for (const { file, error } of errors) {
1017
+ console.error(`Error in "${file}": ${error}`);
1018
+ }
1019
+ }
1020
+ if (tasks.length > 0) {
1021
+ console.log("Recurring tasks:");
1022
+ console.log("");
1023
+ for (const task of tasks) {
1024
+ const lastRun = db.getLastTaskRun(task.name);
1025
+ const statusStr = task.enabled ? "enabled" : "disabled";
1026
+ let nextStr = "";
1027
+ let lastStr = "never";
1028
+ if (task.enabled) {
1029
+ try {
1030
+ nextStr = `next: ${getNextRunTime(task.schedule)}`;
1031
+ } catch {
1032
+ nextStr = "next: invalid cron";
1033
+ }
1034
+ }
1035
+ if (lastRun) {
1036
+ lastStr = `${lastRun.status} ${lastRun.startedAt}`;
1037
+ }
1038
+ console.log(
1039
+ ` ${task.name.padEnd(24)} ${statusStr.padEnd(10)} ${nextStr.padEnd(40)} last: ${lastStr}`
1040
+ );
1041
+ }
1042
+ } else {
1043
+ console.log("No recurring tasks found.");
1044
+ }
1045
+ const oneoffs = db.listOneoffTasks({ status: "pending" });
1046
+ if (oneoffs.length > 0) {
1047
+ console.log("");
1048
+ console.log("Pending one-off tasks:");
1049
+ console.log("");
1050
+ for (const task of oneoffs) {
1051
+ console.log(
1052
+ ` ${task.id.slice(0, 12)}... "${task.description}" scheduled: ${task.scheduledAt}`
1053
+ );
1054
+ }
1055
+ }
1056
+ } finally {
1057
+ db.close();
1058
+ }
1059
+ }
1060
+ function showStatus() {
1061
+ const info = getInstallInfo();
1062
+ const platform = info.platform === "unsupported" ? "unknown" : info.platform;
1063
+ if (info.installed) {
1064
+ console.log(`Scheduler: installed (${platform})`);
1065
+ if (info.details) {
1066
+ console.log(` ${info.details}`);
1067
+ }
1068
+ } else {
1069
+ console.log(`Scheduler: not installed (detected platform: ${platform})`);
1070
+ console.log(" Run: npx opencode-scheduler --install");
1071
+ }
1072
+ console.log("");
1073
+ const db = new TaskDatabase(getDefaultDbPath());
1074
+ try {
1075
+ const { tasks, errors } = readAllTasks();
1076
+ const enabled = tasks.filter((t) => t.enabled);
1077
+ const disabled = tasks.filter((t) => !t.enabled);
1078
+ console.log(
1079
+ `Recurring tasks: ${tasks.length} (${enabled.length} enabled, ${disabled.length} disabled)`
1080
+ );
1081
+ for (const task of tasks) {
1082
+ const lastRun = db.getLastTaskRun(task.name);
1083
+ if (!task.enabled) {
1084
+ console.log(` ${task.name.padEnd(24)} disabled`);
1085
+ continue;
1086
+ }
1087
+ let nextStr = "";
1088
+ try {
1089
+ nextStr = `next: ${getNextRunTime(task.schedule)}`;
1090
+ } catch {
1091
+ nextStr = "next: invalid cron";
1092
+ }
1093
+ let lastStr = "never run";
1094
+ if (lastRun) {
1095
+ lastStr = `${lastRun.status} ${lastRun.startedAt}`;
1096
+ }
1097
+ console.log(
1098
+ ` ${task.name.padEnd(24)} ${nextStr.padEnd(44)} last: ${lastStr}`
1099
+ );
1100
+ }
1101
+ if (errors.length > 0) {
1102
+ console.log("");
1103
+ console.log(`Task file errors: ${errors.length}`);
1104
+ for (const { file, error } of errors) {
1105
+ console.log(` ${file}: ${error}`);
1106
+ }
1107
+ }
1108
+ const pendingOneoffs = db.listOneoffTasks({ status: "pending" });
1109
+ if (pendingOneoffs.length > 0) {
1110
+ console.log("");
1111
+ console.log(`One-off tasks: ${pendingOneoffs.length} pending`);
1112
+ for (const task of pendingOneoffs) {
1113
+ console.log(
1114
+ ` ${task.id.slice(0, 12)}... "${task.description}" scheduled: ${task.scheduledAt}`
1115
+ );
1116
+ }
1117
+ }
1118
+ } finally {
1119
+ db.close();
1120
+ }
1121
+ }
1122
+ function log(message, level = "info") {
1123
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1124
+ const prefix = `[${timestamp}]`;
1125
+ if (level === "error") {
1126
+ console.error(`${prefix} ERROR: ${message}`);
1127
+ } else {
1128
+ console.log(`${prefix} ${message}`);
1129
+ }
1130
+ }
1131
+ function installSkill() {
1132
+ const cliPath = fileURLToPath2(import.meta.url);
1133
+ const packageRoot = dirname3(dirname3(cliPath));
1134
+ const skillSrc = join3(packageRoot, "skill", "SKILL.md");
1135
+ if (!existsSync3(skillSrc)) {
1136
+ const altSrc = join3(dirname3(dirname3(cliPath)), "skill", "SKILL.md");
1137
+ if (!existsSync3(altSrc)) {
1138
+ console.error("Could not find SKILL.md in the package.");
1139
+ console.error("Looked at:", skillSrc, "and", altSrc);
1140
+ process.exit(1);
1141
+ }
1142
+ doInstallSkill(altSrc);
1143
+ return;
1144
+ }
1145
+ doInstallSkill(skillSrc);
1146
+ }
1147
+ function doInstallSkill(srcPath) {
1148
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
1149
+ const destDir = join3(home, ".config", "opencode", "skills", "scheduled-tasks");
1150
+ const destPath = join3(destDir, "SKILL.md");
1151
+ mkdirSync3(destDir, { recursive: true });
1152
+ copyFileSync(srcPath, destPath);
1153
+ console.log("Skill installed successfully!");
1154
+ console.log(` Source: ${srcPath}`);
1155
+ console.log(` Installed to: ${destPath}`);
1156
+ console.log("");
1157
+ console.log("The 'scheduled-tasks' skill is now available to OpenCode agents.");
1158
+ console.log("Agents will automatically discover it and can load it when relevant.");
1159
+ }
1160
+ function printUsage() {
1161
+ console.log(`opencode-scheduler - CLI for OpenCode scheduled tasks
1162
+
1163
+ Usage:
1164
+ opencode-scheduler --run-once Run one scheduler tick
1165
+ opencode-scheduler --install Install the system scheduler (launchd/systemd)
1166
+ opencode-scheduler --uninstall Remove the system scheduler
1167
+ opencode-scheduler --install-skill Install the scheduled-tasks agent skill
1168
+ opencode-scheduler --status Show scheduler and task status
1169
+ opencode-scheduler --list List all tasks with next run times
1170
+ opencode-scheduler --help Show this help message
1171
+
1172
+ Internal (used by spawned workers):
1173
+ opencode-scheduler --exec-task <runId> [--oneoff]
1174
+ `);
1175
+ }
1176
+ async function main() {
1177
+ const args = process.argv.slice(2);
1178
+ const command = args[0];
1179
+ switch (command) {
1180
+ case "--install":
1181
+ await install();
1182
+ break;
1183
+ case "--uninstall":
1184
+ await uninstall();
1185
+ break;
1186
+ case "--install-skill":
1187
+ installSkill();
1188
+ break;
1189
+ case "--status":
1190
+ showStatus();
1191
+ break;
1192
+ case "--list":
1193
+ listTasks();
1194
+ break;
1195
+ case "--help":
1196
+ case "-h":
1197
+ printUsage();
1198
+ break;
1199
+ case "--exec-task": {
1200
+ const runId = args[1];
1201
+ if (!runId) {
1202
+ console.error("--exec-task requires a run ID");
1203
+ process.exit(1);
1204
+ }
1205
+ const isOneoff = args.includes("--oneoff");
1206
+ await execTask(runId, isOneoff);
1207
+ break;
1208
+ }
1209
+ case "--run-once":
1210
+ runTick();
1211
+ break;
1212
+ case void 0:
1213
+ printUsage();
1214
+ break;
1215
+ default:
1216
+ console.error(`Unknown command: ${command}`);
1217
+ printUsage();
1218
+ process.exit(1);
1219
+ }
1220
+ }
1221
+ main().catch((err) => {
1222
+ console.error("Fatal error:", err.message ?? err);
1223
+ process.exit(1);
1224
+ });
1225
+ //# sourceMappingURL=cli.js.map