@staticduo/opencode-scheduler 1.3.0 → 1.3.2

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/README.md CHANGED
@@ -96,6 +96,7 @@ Jobs run from the working directory where you created them, picking up your `ope
96
96
  - **No overlap**: if the previous run is still active, the next scheduled tick is skipped.
97
97
  - **Non-interactive by default**: scheduled runs force `OPENCODE_PERMISSION` to deny "question" prompts, so jobs don't hang waiting for approvals.
98
98
  - **Optional timeout**: set `timeoutSeconds` to hard-stop long runs (SIGTERM, then SIGKILL).
99
+ - **Transactional systemd updates**: Linux unit installation restores the prior unit files, permissions, enabled state, and active state if any install step fails.
99
100
 
100
101
  ### Platform Support
101
102
 
package/dist/cron.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare function splitCronExpression(cron: string): [string, string, string, string, string];
2
+ export declare function parseCronField(field: string, min: number, max: number, label: string, allowSundaySeven?: boolean): number[] | null;
3
+ export declare function validateCronExpression(cron: string): void;
4
+ export declare function cronToSystemdCalendars(cron: string): string[];
package/dist/index.d.ts CHANGED
@@ -12,72 +12,5 @@
12
12
  * - Environment variable injection (PATH for node/npx)
13
13
  */
14
14
  import type { Plugin } from "@opencode-ai/plugin";
15
- import { execSync } from "child_process";
16
- declare function jobFilePath(scopeId: string, slug: string): string;
17
- type OpencodeRunFormat = "default" | "json";
18
- interface JobRunSpec {
19
- prompt?: string;
20
- command?: string;
21
- arguments?: string;
22
- files?: string[];
23
- agent?: string;
24
- model?: string;
25
- variant?: string;
26
- title?: string;
27
- share?: boolean;
28
- continue?: boolean;
29
- session?: string;
30
- runFormat?: OpencodeRunFormat;
31
- attachUrl?: string;
32
- port?: number;
33
- }
34
- type JobInvocation = {
35
- command: string;
36
- args: string[];
37
- };
38
- interface Job {
39
- scopeId?: string;
40
- slug: string;
41
- name: string;
42
- schedule: string;
43
- prompt?: string;
44
- attachUrl?: string;
45
- run?: JobRunSpec;
46
- invocation?: JobInvocation;
47
- timeoutSeconds?: number;
48
- source?: string;
49
- workdir?: string;
50
- createdAt: string;
51
- updatedAt?: string;
52
- lastRunAt?: string;
53
- lastRunExitCode?: number;
54
- lastRunError?: string;
55
- lastRunSource?: "manual" | "scheduled";
56
- lastRunStatus?: "running" | "success" | "failed";
57
- }
58
- declare function cronToSystemdCalendars(cron: string): string[];
59
- declare function withSystemdRuntimeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
60
- declare function systemdRunEnv(): NodeJS.ProcessEnv;
61
- type SystemdCommandRunner = (command: string, options?: Parameters<typeof execSync>[1]) => ReturnType<typeof execSync>;
62
- declare function createSystemdTimer(job: Job): string;
63
- declare function installSystemdJob(job: Job): void;
64
- declare function uninstallSystemdJob(job: Job): void;
65
- declare function saveJob(job: Job): void;
66
- declare function deleteJobFile(job: Job): void;
67
15
  export declare const SchedulerPlugin: Plugin;
68
16
  export default SchedulerPlugin;
69
- export type { SystemdCommandRunner };
70
- export declare const __test__: {
71
- cronToSystemdCalendars: typeof cronToSystemdCalendars;
72
- createSystemdTimer: typeof createSystemdTimer;
73
- withSystemdRuntimeEnv: typeof withSystemdRuntimeEnv;
74
- systemdRunEnv: typeof systemdRunEnv;
75
- installSystemdJob: typeof installSystemdJob;
76
- uninstallSystemdJob: typeof uninstallSystemdJob;
77
- saveJob: typeof saveJob;
78
- deleteJobFile: typeof deleteJobFile;
79
- jobFilePath: typeof jobFilePath;
80
- SYSTEMD_USER_DIR: string;
81
- SCOPES_DIR: string;
82
- setSystemdCommandRunner(runner: SystemdCommandRunner | null): void;
83
- };
package/dist/index.js CHANGED
@@ -12335,30 +12335,258 @@ function tool(input) {
12335
12335
  }
12336
12336
  tool.schema = exports_external;
12337
12337
  // src/index.ts
12338
- import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, unlinkSync } from "fs";
12339
- import { basename, dirname, join, resolve as resolvePath } from "path";
12338
+ import { createWriteStream, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
12339
+ import { basename, dirname, join as join2, resolve as resolvePath } from "path";
12340
12340
  import { homedir, platform } from "os";
12341
12341
  import { execFileSync, execSync, spawn } from "child_process";
12342
12342
  import { fileURLToPath } from "url";
12343
- var OPENCODE_CONFIG = join(homedir(), ".config", "opencode");
12344
- var LEGACY_JOBS_DIR = join(OPENCODE_CONFIG, "jobs");
12345
- var LOGS_DIR = join(OPENCODE_CONFIG, "logs");
12346
- var SCHEDULER_DIR = join(OPENCODE_CONFIG, "scheduler");
12347
- var SCOPES_DIR = join(SCHEDULER_DIR, "scopes");
12348
- var SUPERVISOR_PATH = join(SCHEDULER_DIR, "supervisor.pl");
12349
- var SCHEDULER_CONFIG = join(OPENCODE_CONFIG, "opencode-scheduler.json");
12343
+
12344
+ // src/cron.ts
12345
+ var SYSTEMD_WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
12346
+ function splitCronExpression(cron) {
12347
+ const parts = cron.trim().split(/\s+/);
12348
+ if (parts.length !== 5) {
12349
+ throw new Error(`Invalid cron: ${cron}`);
12350
+ }
12351
+ return parts;
12352
+ }
12353
+ function uniqueSorted(values) {
12354
+ return Array.from(new Set(values)).sort((a, b) => a - b);
12355
+ }
12356
+ function parseCronField(field, min, max, label, allowSundaySeven = false) {
12357
+ if (field === "*")
12358
+ return null;
12359
+ if (field.startsWith("*/")) {
12360
+ const step = parseInt(field.slice(2), 10);
12361
+ if (!Number.isFinite(step) || step <= 0) {
12362
+ throw new Error(`Invalid cron ${label} step: ${field}`);
12363
+ }
12364
+ const values = [];
12365
+ for (let value = min;value <= max; value += step)
12366
+ values.push(value);
12367
+ return values;
12368
+ }
12369
+ const parts = field.split(",");
12370
+ if (parts.length > 1) {
12371
+ return uniqueSorted(parts.map((part) => parseCronNumber(part, min, max, label, allowSundaySeven)));
12372
+ }
12373
+ if (/^\d+$/.test(field)) {
12374
+ return [parseCronNumber(field, min, max, label, allowSundaySeven)];
12375
+ }
12376
+ throw new Error(`Invalid cron ${label} field: ${field}`);
12377
+ }
12378
+ function parseCronNumber(value, min, max, label, allowSundaySeven) {
12379
+ const parsed = parseInt(value, 10);
12380
+ if (!Number.isFinite(parsed))
12381
+ throw new Error(`Invalid cron ${label} value: ${value}`);
12382
+ const normalized = allowSundaySeven && parsed === 7 ? 0 : parsed;
12383
+ if (normalized < min || normalized > max)
12384
+ throw new Error(`Invalid cron ${label} value: ${value}`);
12385
+ return normalized;
12386
+ }
12387
+ function validateCronExpression(cron) {
12388
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = splitCronExpression(cron);
12389
+ parseCronField(minute, 0, 59, "minute");
12390
+ parseCronField(hour, 0, 23, "hour");
12391
+ parseCronField(dayOfMonth, 1, 31, "day of month");
12392
+ parseCronField(month, 1, 12, "month");
12393
+ parseCronField(dayOfWeek, 0, 7, "day of week", true);
12394
+ }
12395
+ function formatSystemdValue(value, size) {
12396
+ return value.toString().padStart(size, "0");
12397
+ }
12398
+ function cronToSystemdCalendars(cron) {
12399
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = splitCronExpression(cron);
12400
+ const minuteValues = parseCronField(minute, 0, 59, "minute");
12401
+ const hourValues = parseCronField(hour, 0, 23, "hour");
12402
+ const dayValues = parseCronField(dayOfMonth, 1, 31, "day of month");
12403
+ const monthValues = parseCronField(month, 1, 12, "month");
12404
+ const weekdayValues = parseCronField(dayOfWeek, 0, 7, "day of week", true);
12405
+ const minutes = minuteValues ? minuteValues.map((value) => formatSystemdValue(value, 2)) : ["*"];
12406
+ const hours = hourValues ? hourValues.map((value) => formatSystemdValue(value, 2)) : ["*"];
12407
+ const days = dayValues ? dayValues.map((value) => formatSystemdValue(value, 2)) : ["*"];
12408
+ const months = monthValues ? monthValues.map((value) => formatSystemdValue(value, 2)) : ["*"];
12409
+ const weekdays = weekdayValues ? weekdayValues.map((value) => SYSTEMD_WEEKDAYS[value] ?? "*") : ["*"];
12410
+ const calendars = [];
12411
+ const buildCalendars = (domValues, dowValues) => {
12412
+ for (const minuteValue of minutes) {
12413
+ for (const hourValue of hours) {
12414
+ for (const domValue of domValues) {
12415
+ for (const monthValue of months) {
12416
+ for (const dowValue of dowValues) {
12417
+ const weekdayPrefix = dowValue === "*" ? "" : `${dowValue} `;
12418
+ calendars.push(`${weekdayPrefix}*-${monthValue}-${domValue} ${hourValue}:${minuteValue}:00`);
12419
+ }
12420
+ }
12421
+ }
12422
+ }
12423
+ }
12424
+ };
12425
+ if (dayValues && weekdayValues) {
12426
+ buildCalendars(days, ["*"]);
12427
+ buildCalendars(["*"], weekdays);
12428
+ } else {
12429
+ buildCalendars(days, weekdays);
12430
+ }
12431
+ return calendars;
12432
+ }
12433
+
12434
+ // src/systemd.ts
12435
+ import {
12436
+ chmodSync,
12437
+ existsSync,
12438
+ mkdirSync,
12439
+ readFileSync,
12440
+ renameSync,
12441
+ statSync,
12442
+ unlinkSync,
12443
+ writeFileSync
12444
+ } from "fs";
12445
+ import { join } from "path";
12446
+ var defaultRuntimeEnvDependencies = {
12447
+ exists: existsSync,
12448
+ uid: () => process.getuid?.()
12449
+ };
12450
+ function withSystemdRuntimeEnv(env, dependencies = defaultRuntimeEnvDependencies) {
12451
+ const next = { ...env };
12452
+ if (!next.XDG_RUNTIME_DIR) {
12453
+ const uid = dependencies.uid();
12454
+ if (typeof uid === "number") {
12455
+ const runtimeDir = `/run/user/${uid}`;
12456
+ if (dependencies.exists(runtimeDir))
12457
+ next.XDG_RUNTIME_DIR = runtimeDir;
12458
+ }
12459
+ }
12460
+ if (!next.DBUS_SESSION_BUS_ADDRESS && next.XDG_RUNTIME_DIR) {
12461
+ const busPath = join(next.XDG_RUNTIME_DIR, "bus");
12462
+ if (dependencies.exists(busPath))
12463
+ next.DBUS_SESSION_BUS_ADDRESS = `unix:path=${busPath}`;
12464
+ }
12465
+ return next;
12466
+ }
12467
+ var defaultFileSystem = {
12468
+ chmod: chmodSync,
12469
+ exists: existsSync,
12470
+ mkdir: mkdirSync,
12471
+ readFile: readFileSync,
12472
+ rename: renameSync,
12473
+ stat: statSync,
12474
+ unlink: unlinkSync,
12475
+ writeFile: writeFileSync
12476
+ };
12477
+ function snapshotFile(path, fileSystem) {
12478
+ if (!fileSystem.exists(path))
12479
+ return { path, existed: false };
12480
+ return {
12481
+ path,
12482
+ existed: true,
12483
+ content: fileSystem.readFile(path),
12484
+ mode: fileSystem.stat(path).mode & 511
12485
+ };
12486
+ }
12487
+ var temporaryFileSequence = 0;
12488
+ function atomicReplace(path, content, mode, fileSystem) {
12489
+ temporaryFileSequence += 1;
12490
+ const temporaryPath = `${path}.tmp-${process.pid}-${temporaryFileSequence}`;
12491
+ try {
12492
+ fileSystem.writeFile(temporaryPath, content, { mode });
12493
+ fileSystem.chmod(temporaryPath, mode);
12494
+ fileSystem.rename(temporaryPath, path);
12495
+ fileSystem.chmod(path, mode);
12496
+ } finally {
12497
+ try {
12498
+ fileSystem.unlink(temporaryPath);
12499
+ } catch {}
12500
+ }
12501
+ }
12502
+ function commandOutput(value) {
12503
+ if (Buffer.isBuffer(value))
12504
+ return value.toString().trim();
12505
+ if (typeof value === "string")
12506
+ return value.trim();
12507
+ if (typeof value === "object" && value !== null && "stdout" in value) {
12508
+ const stdout = value.stdout;
12509
+ if (Buffer.isBuffer(stdout))
12510
+ return stdout.toString().trim();
12511
+ if (typeof stdout === "string")
12512
+ return stdout.trim();
12513
+ }
12514
+ return "";
12515
+ }
12516
+ function queryTimerState(run, timerUnit, query) {
12517
+ let output = "";
12518
+ try {
12519
+ output = commandOutput(run(`systemctl --user ${query} ${timerUnit}`, { stdio: ["ignore", "pipe", "ignore"] }));
12520
+ } catch (error45) {
12521
+ output = commandOutput(error45);
12522
+ }
12523
+ return query === "is-enabled" ? ["enabled", "enabled-runtime", "linked", "linked-runtime", "alias"].includes(output) : ["active", "activating", "reloading"].includes(output);
12524
+ }
12525
+ function restoreFile(snapshot, fileSystem) {
12526
+ if (!snapshot.existed) {
12527
+ try {
12528
+ fileSystem.unlink(snapshot.path);
12529
+ } catch {}
12530
+ return;
12531
+ }
12532
+ atomicReplace(snapshot.path, snapshot.content ?? Buffer.alloc(0), snapshot.mode ?? 420, fileSystem);
12533
+ }
12534
+ function bestEffort(action) {
12535
+ try {
12536
+ action();
12537
+ } catch {}
12538
+ }
12539
+ function installSystemdUnits(request) {
12540
+ const fileSystem = request.fileSystem ?? defaultFileSystem;
12541
+ fileSystem.mkdir(request.unitDir, { recursive: true });
12542
+ const servicePath = join(request.unitDir, request.serviceUnit);
12543
+ const timerPath = join(request.unitDir, request.timerUnit);
12544
+ const serviceSnapshot = snapshotFile(servicePath, fileSystem);
12545
+ const timerSnapshot = snapshotFile(timerPath, fileSystem);
12546
+ const wasEnabled = queryTimerState(request.run, request.timerUnit, "is-enabled");
12547
+ const wasActive = queryTimerState(request.run, request.timerUnit, "is-active");
12548
+ try {
12549
+ atomicReplace(servicePath, request.serviceContent, 420, fileSystem);
12550
+ atomicReplace(timerPath, request.timerContent, 420, fileSystem);
12551
+ request.run("systemctl --user daemon-reload");
12552
+ request.run(`systemctl --user enable ${request.timerUnit}`);
12553
+ request.run(`systemctl --user start ${request.timerUnit}`);
12554
+ } catch (error45) {
12555
+ if (!wasActive)
12556
+ bestEffort(() => request.run(`systemctl --user stop ${request.timerUnit}`, { stdio: "ignore" }));
12557
+ if (!wasEnabled)
12558
+ bestEffort(() => request.run(`systemctl --user disable ${request.timerUnit}`, { stdio: "ignore" }));
12559
+ bestEffort(() => restoreFile(serviceSnapshot, fileSystem));
12560
+ bestEffort(() => restoreFile(timerSnapshot, fileSystem));
12561
+ bestEffort(() => request.run("systemctl --user daemon-reload", { stdio: "ignore" }));
12562
+ if (wasEnabled)
12563
+ bestEffort(() => request.run(`systemctl --user enable ${request.timerUnit}`, { stdio: "ignore" }));
12564
+ if (wasActive)
12565
+ bestEffort(() => request.run(`systemctl --user start ${request.timerUnit}`, { stdio: "ignore" }));
12566
+ throw error45;
12567
+ }
12568
+ }
12569
+
12570
+ // src/index.ts
12571
+ var OPENCODE_CONFIG = join2(homedir(), ".config", "opencode");
12572
+ var LEGACY_JOBS_DIR = join2(OPENCODE_CONFIG, "jobs");
12573
+ var LOGS_DIR = join2(OPENCODE_CONFIG, "logs");
12574
+ var SCHEDULER_DIR = join2(OPENCODE_CONFIG, "scheduler");
12575
+ var SCOPES_DIR = join2(SCHEDULER_DIR, "scopes");
12576
+ var SUPERVISOR_PATH = join2(SCHEDULER_DIR, "supervisor.pl");
12577
+ var SCHEDULER_CONFIG = join2(OPENCODE_CONFIG, "opencode-scheduler.json");
12350
12578
  var IS_MAC = platform() === "darwin";
12351
12579
  var IS_LINUX = platform() === "linux";
12352
12580
  var IS_WINDOWS = platform() === "win32";
12353
- var LAUNCH_AGENTS_DIR = join(homedir(), "Library", "LaunchAgents");
12581
+ var LAUNCH_AGENTS_DIR = join2(homedir(), "Library", "LaunchAgents");
12354
12582
  var LAUNCHD_PREFIX = "com.opencode.job";
12355
- var SYSTEMD_USER_DIR = join(homedir(), ".config", "systemd", "user");
12583
+ var SYSTEMD_USER_DIR = join2(homedir(), ".config", "systemd", "user");
12356
12584
  var WINDOWS_TASK_ROOT = "\\OpenCode";
12357
12585
  var WINDOWS_TASK_PREFIX = "opencode-job";
12358
12586
  var CRON_MANAGED_PREFIX = "opencode-scheduler";
12359
12587
  function ensureDir(dir) {
12360
- if (!existsSync(dir)) {
12361
- mkdirSync(dir, { recursive: true });
12588
+ if (!existsSync2(dir)) {
12589
+ mkdirSync2(dir, { recursive: true });
12362
12590
  }
12363
12591
  }
12364
12592
  function slugify(name) {
@@ -12390,25 +12618,25 @@ function deriveScopeId(workdir) {
12390
12618
  return `${base}-${suffix}`;
12391
12619
  }
12392
12620
  function scopeDir(scopeId) {
12393
- return join(SCOPES_DIR, scopeId);
12621
+ return join2(SCOPES_DIR, scopeId);
12394
12622
  }
12395
12623
  function scopeJobsDir(scopeId) {
12396
- return join(scopeDir(scopeId), "jobs");
12624
+ return join2(scopeDir(scopeId), "jobs");
12397
12625
  }
12398
12626
  function scopeLocksDir(scopeId) {
12399
- return join(scopeDir(scopeId), "locks");
12627
+ return join2(scopeDir(scopeId), "locks");
12400
12628
  }
12401
12629
  function scopeRunsDir(scopeId) {
12402
- return join(scopeDir(scopeId), "runs");
12630
+ return join2(scopeDir(scopeId), "runs");
12403
12631
  }
12404
12632
  function scopeLogsDir(scopeId) {
12405
- return join(LOGS_DIR, "scheduler", scopeId);
12633
+ return join2(LOGS_DIR, "scheduler", scopeId);
12406
12634
  }
12407
12635
  function jobFilePath(scopeId, slug) {
12408
- return join(scopeJobsDir(scopeId), `${slug}.json`);
12636
+ return join2(scopeJobsDir(scopeId), `${slug}.json`);
12409
12637
  }
12410
12638
  function scopedLogPath(scopeId, slug) {
12411
- return join(scopeLogsDir(scopeId), `${slug}.log`);
12639
+ return join2(scopeLogsDir(scopeId), `${slug}.log`);
12412
12640
  }
12413
12641
  function currentScopeId() {
12414
12642
  return deriveScopeId(process.cwd());
@@ -12667,7 +12895,7 @@ exit($exit_code);
12667
12895
  `;
12668
12896
  function ensureSupervisorScript() {
12669
12897
  ensureDir(SCHEDULER_DIR);
12670
- writeFileSync(SUPERVISOR_PATH, SUPERVISOR_SCRIPT);
12898
+ writeFileSync2(SUPERVISOR_PATH, SUPERVISOR_SCRIPT);
12671
12899
  }
12672
12900
  function normalizeFormat(format) {
12673
12901
  return format === "json" ? "json" : "text";
@@ -12795,19 +13023,19 @@ function installBuiltinSkill(skill, rootDir, overwrite = false) {
12795
13023
  if (!installRoot) {
12796
13024
  throw new Error("Install directory cannot be empty.");
12797
13025
  }
12798
- if (!existsSync(installRoot)) {
13026
+ if (!existsSync2(installRoot)) {
12799
13027
  throw new Error(`Directory not found: ${installRoot}`);
12800
13028
  }
12801
13029
  const relativeDir = dirname(skill.suggestedPath);
12802
- const installDir = join(installRoot, relativeDir);
13030
+ const installDir = join2(installRoot, relativeDir);
12803
13031
  ensureDir(installDir);
12804
13032
  const files = [];
12805
13033
  for (const [filename, content] of Object.entries(skill.files)) {
12806
- const targetPath = join(installDir, filename);
12807
- if (existsSync(targetPath) && !overwrite) {
13034
+ const targetPath = join2(installDir, filename);
13035
+ if (existsSync2(targetPath) && !overwrite) {
12808
13036
  throw new Error(`File already exists: ${targetPath} (pass overwrite=true to replace)`);
12809
13037
  }
12810
- writeFileSync(targetPath, `${content.trimEnd()}
13038
+ writeFileSync2(targetPath, `${content.trimEnd()}
12811
13039
  `);
12812
13040
  files.push(targetPath);
12813
13041
  }
@@ -12816,8 +13044,8 @@ function installBuiltinSkill(skill, rootDir, overwrite = false) {
12816
13044
  function loadPackageInfo() {
12817
13045
  const fallback = { name: "opencode-scheduler", version: "unknown" };
12818
13046
  try {
12819
- const packagePath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
12820
- const raw = readFileSync(packagePath, "utf-8");
13047
+ const packagePath = join2(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
13048
+ const raw = readFileSync2(packagePath, "utf-8");
12821
13049
  const parsed = JSON.parse(raw);
12822
13050
  return {
12823
13051
  name: typeof parsed.name === "string" ? parsed.name : fallback.name,
@@ -12845,10 +13073,10 @@ function findOpencode() {
12845
13073
  const paths = [
12846
13074
  "/opt/homebrew/bin/opencode",
12847
13075
  "/usr/local/bin/opencode",
12848
- join(homedir(), ".opencode", "bin", "opencode")
13076
+ join2(homedir(), ".opencode", "bin", "opencode")
12849
13077
  ];
12850
13078
  for (const p of paths) {
12851
- if (existsSync(p)) {
13079
+ if (existsSync2(p)) {
12852
13080
  return p;
12853
13081
  }
12854
13082
  }
@@ -12865,59 +13093,6 @@ function getEnhancedPath() {
12865
13093
  ];
12866
13094
  return paths.join(":");
12867
13095
  }
12868
- function splitCronExpression(cron) {
12869
- const parts = cron.trim().split(/\s+/);
12870
- if (parts.length !== 5) {
12871
- throw new Error(`Invalid cron: ${cron}`);
12872
- }
12873
- return parts;
12874
- }
12875
- function uniqueSorted(values) {
12876
- return Array.from(new Set(values)).sort((a, b) => a - b);
12877
- }
12878
- function parseCronField(field, min, max, label, allowSundaySeven = false) {
12879
- if (field === "*")
12880
- return null;
12881
- if (field.startsWith("*/")) {
12882
- const step = parseInt(field.slice(2), 10);
12883
- if (!Number.isFinite(step) || step <= 0) {
12884
- throw new Error(`Invalid cron ${label} step: ${field}`);
12885
- }
12886
- const values = [];
12887
- for (let value = min;value <= max; value += step) {
12888
- values.push(value);
12889
- }
12890
- return values;
12891
- }
12892
- const parts = field.split(",");
12893
- if (parts.length > 1) {
12894
- const values = parts.map((part) => parseCronNumber(part, min, max, label, allowSundaySeven));
12895
- return uniqueSorted(values);
12896
- }
12897
- if (/^\d+$/.test(field)) {
12898
- return [parseCronNumber(field, min, max, label, allowSundaySeven)];
12899
- }
12900
- throw new Error(`Invalid cron ${label} field: ${field}`);
12901
- }
12902
- function parseCronNumber(value, min, max, label, allowSundaySeven) {
12903
- const parsed = parseInt(value, 10);
12904
- if (!Number.isFinite(parsed)) {
12905
- throw new Error(`Invalid cron ${label} value: ${value}`);
12906
- }
12907
- const normalized = allowSundaySeven && parsed === 7 ? 0 : parsed;
12908
- if (normalized < min || normalized > max) {
12909
- throw new Error(`Invalid cron ${label} value: ${value}`);
12910
- }
12911
- return normalized;
12912
- }
12913
- function validateCronExpression(cron) {
12914
- const [minute, hour, dayOfMonth, month, dayOfWeek] = splitCronExpression(cron);
12915
- parseCronField(minute, 0, 59, "minute");
12916
- parseCronField(hour, 0, 23, "hour");
12917
- parseCronField(dayOfMonth, 1, 31, "day of month");
12918
- parseCronField(month, 1, 12, "month");
12919
- parseCronField(dayOfWeek, 0, 7, "day of week", true);
12920
- }
12921
13096
  function expandLaunchdEntries(entries, key, values) {
12922
13097
  if (!values)
12923
13098
  return entries;
@@ -12964,45 +13139,6 @@ function renderLaunchdCalendar(calendar) {
12964
13139
  <integer>${value}</integer>`).join(`
12965
13140
  `);
12966
13141
  }
12967
- var SYSTEMD_WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
12968
- function formatSystemdValue(value, size) {
12969
- return value.toString().padStart(size, "0");
12970
- }
12971
- function cronToSystemdCalendars(cron) {
12972
- const [minute, hour, dayOfMonth, month, dayOfWeek] = splitCronExpression(cron);
12973
- const minuteValues = parseCronField(minute, 0, 59, "minute");
12974
- const hourValues = parseCronField(hour, 0, 23, "hour");
12975
- const dayValues = parseCronField(dayOfMonth, 1, 31, "day of month");
12976
- const monthValues = parseCronField(month, 1, 12, "month");
12977
- const weekdayValues = parseCronField(dayOfWeek, 0, 7, "day of week", true);
12978
- const minutes = minuteValues ? minuteValues.map((value) => formatSystemdValue(value, 2)) : ["*"];
12979
- const hours = hourValues ? hourValues.map((value) => formatSystemdValue(value, 2)) : ["*"];
12980
- const days = dayValues ? dayValues.map((value) => formatSystemdValue(value, 2)) : ["*"];
12981
- const months = monthValues ? monthValues.map((value) => formatSystemdValue(value, 2)) : ["*"];
12982
- const weekdays = weekdayValues ? weekdayValues.map((value) => SYSTEMD_WEEKDAYS[value] ?? "*") : ["*"];
12983
- const calendars = [];
12984
- const buildCalendars = (domValues, dowValues) => {
12985
- for (const minuteValue of minutes) {
12986
- for (const hourValue of hours) {
12987
- for (const domValue of domValues) {
12988
- for (const monthValue of months) {
12989
- for (const dowValue of dowValues) {
12990
- const weekdayPrefix = dowValue === "*" ? "" : `${dowValue} `;
12991
- calendars.push(`${weekdayPrefix}*-${monthValue}-${domValue} ${hourValue}:${minuteValue}:00`);
12992
- }
12993
- }
12994
- }
12995
- }
12996
- }
12997
- };
12998
- if (dayValues && weekdayValues) {
12999
- buildCalendars(days, ["*"]);
13000
- buildCalendars(["*"], weekdays);
13001
- } else {
13002
- buildCalendars(days, weekdays);
13003
- }
13004
- return calendars;
13005
- }
13006
13142
  var WINDOWS_WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
13007
13143
  var WINDOWS_MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
13008
13144
  function pad2(value) {
@@ -13193,57 +13329,38 @@ function installLaunchdJob(job) {
13193
13329
  ensureDir(scopeLogsDir(scopeId));
13194
13330
  ensureSupervisorScript();
13195
13331
  const legacyLabel = `${LAUNCHD_PREFIX}.${job.slug}`;
13196
- const legacyPlistPath = join(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
13332
+ const legacyPlistPath = join2(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
13197
13333
  const label = `${LAUNCHD_PREFIX}.${scopeId}.${job.slug}`;
13198
- const plistPath = join(LAUNCH_AGENTS_DIR, `${label}.plist`);
13334
+ const plistPath = join2(LAUNCH_AGENTS_DIR, `${label}.plist`);
13199
13335
  try {
13200
13336
  execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore" });
13201
13337
  } catch {}
13202
- if (existsSync(legacyPlistPath)) {
13338
+ if (existsSync2(legacyPlistPath)) {
13203
13339
  try {
13204
13340
  execSync(`launchctl unload "${legacyPlistPath}" 2>/dev/null`, { stdio: "ignore" });
13205
13341
  } catch {}
13206
13342
  }
13207
13343
  const plist = createLaunchdPlist(job);
13208
- writeFileSync(plistPath, plist);
13344
+ writeFileSync2(plistPath, plist);
13209
13345
  execSync(`launchctl load "${plistPath}"`);
13210
13346
  }
13211
13347
  function uninstallLaunchdJob(job) {
13212
13348
  const scopeId = job.scopeId || deriveScopeId(job.workdir || homedir());
13213
13349
  const scopedLabel = `${LAUNCHD_PREFIX}.${scopeId}.${job.slug}`;
13214
- const scopedPlistPath = join(LAUNCH_AGENTS_DIR, `${scopedLabel}.plist`);
13350
+ const scopedPlistPath = join2(LAUNCH_AGENTS_DIR, `${scopedLabel}.plist`);
13215
13351
  const legacyLabel = `${LAUNCHD_PREFIX}.${job.slug}`;
13216
- const legacyPlistPath = join(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
13352
+ const legacyPlistPath = join2(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
13217
13353
  for (const plistPath of [scopedPlistPath, legacyPlistPath]) {
13218
- if (!existsSync(plistPath))
13354
+ if (!existsSync2(plistPath))
13219
13355
  continue;
13220
13356
  try {
13221
13357
  execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
13222
13358
  } catch {}
13223
13359
  try {
13224
- unlinkSync(plistPath);
13360
+ unlinkSync2(plistPath);
13225
13361
  } catch {}
13226
13362
  }
13227
13363
  }
13228
- function withSystemdRuntimeEnv(env) {
13229
- const next = { ...env };
13230
- if (!next.XDG_RUNTIME_DIR) {
13231
- const uid = process.getuid?.();
13232
- if (typeof uid === "number") {
13233
- const runtimeDir = `/run/user/${uid}`;
13234
- if (existsSync(runtimeDir)) {
13235
- next.XDG_RUNTIME_DIR = runtimeDir;
13236
- }
13237
- }
13238
- }
13239
- if (!next.DBUS_SESSION_BUS_ADDRESS && next.XDG_RUNTIME_DIR) {
13240
- const busPath = join(next.XDG_RUNTIME_DIR, "bus");
13241
- if (existsSync(busPath)) {
13242
- next.DBUS_SESSION_BUS_ADDRESS = `unix:path=${busPath}`;
13243
- }
13244
- }
13245
- return next;
13246
- }
13247
13364
  function systemdRunEnv() {
13248
13365
  const enhancedPath = getEnhancedPath();
13249
13366
  const existingPath = process.env.PATH;
@@ -13255,9 +13372,8 @@ function systemdRunEnv() {
13255
13372
  function defaultSystemdCommandRunner(command, options) {
13256
13373
  return execSync(command, { ...options, env: systemdRunEnv() });
13257
13374
  }
13258
- var systemdCommandRunner = defaultSystemdCommandRunner;
13259
13375
  function systemdExecSync(command, options) {
13260
- return systemdCommandRunner(command, options);
13376
+ return defaultSystemdCommandRunner(command, options);
13261
13377
  }
13262
13378
  function createSystemdService(job) {
13263
13379
  const scopeId = job.scopeId || deriveScopeId(job.workdir || homedir());
@@ -13296,37 +13412,24 @@ Persistent=true
13296
13412
  WantedBy=timers.target
13297
13413
  `;
13298
13414
  }
13299
- function installSystemdJob(job) {
13415
+ function installSystemdJob(job, run = defaultSystemdCommandRunner) {
13300
13416
  ensureDir(SYSTEMD_USER_DIR);
13301
13417
  ensureDir(LOGS_DIR);
13302
13418
  const scopeId = job.scopeId || deriveScopeId(job.workdir || homedir());
13303
13419
  ensureDir(scopeLogsDir(scopeId));
13304
13420
  ensureSupervisorScript();
13305
- const servicePath = join(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.service`);
13306
- const timerPath = join(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.timer`);
13307
- try {
13308
- systemdExecSync(`systemctl --user stop opencode-job-${job.slug}.timer`, { stdio: "ignore" });
13309
- systemdExecSync(`systemctl --user disable opencode-job-${job.slug}.timer`, { stdio: "ignore" });
13310
- } catch {}
13311
- writeFileSync(servicePath, createSystemdService(job), { mode: 420 });
13312
- writeFileSync(timerPath, createSystemdTimer(job), { mode: 420 });
13313
- chmodSync(servicePath, 420);
13314
- chmodSync(timerPath, 420);
13315
- try {
13316
- systemdExecSync("systemctl --user daemon-reload");
13317
- systemdExecSync(`systemctl --user enable opencode-job-${scopeId}-${job.slug}.timer`);
13318
- systemdExecSync(`systemctl --user start opencode-job-${scopeId}-${job.slug}.timer`);
13319
- } catch (error45) {
13320
- for (const unitPath of [timerPath, servicePath]) {
13321
- try {
13322
- unlinkSync(unitPath);
13323
- } catch {}
13324
- }
13325
- try {
13326
- systemdExecSync("systemctl --user daemon-reload", { stdio: "ignore" });
13327
- } catch {}
13328
- throw error45;
13329
- }
13421
+ const servicePath = join2(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.service`);
13422
+ const timerPath = join2(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.timer`);
13423
+ const serviceUnit = servicePath.slice(SYSTEMD_USER_DIR.length + 1);
13424
+ const timerUnit = timerPath.slice(SYSTEMD_USER_DIR.length + 1);
13425
+ installSystemdUnits({
13426
+ unitDir: SYSTEMD_USER_DIR,
13427
+ serviceUnit,
13428
+ timerUnit,
13429
+ serviceContent: createSystemdService(job),
13430
+ timerContent: createSystemdTimer(job),
13431
+ run
13432
+ });
13330
13433
  }
13331
13434
  function uninstallSystemdJob(job) {
13332
13435
  const scopeId = job.scopeId || deriveScopeId(job.workdir || homedir());
@@ -13338,14 +13441,14 @@ function uninstallSystemdJob(job) {
13338
13441
  systemdExecSync(`systemctl --user disable ${timerUnit}`, { stdio: "ignore" });
13339
13442
  } catch {}
13340
13443
  }
13341
- const scopedServicePath = join(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.service`);
13342
- const scopedTimerPath = join(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.timer`);
13343
- const legacyServicePath = join(SYSTEMD_USER_DIR, `opencode-job-${job.slug}.service`);
13344
- const legacyTimerPath = join(SYSTEMD_USER_DIR, `opencode-job-${job.slug}.timer`);
13444
+ const scopedServicePath = join2(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.service`);
13445
+ const scopedTimerPath = join2(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.timer`);
13446
+ const legacyServicePath = join2(SYSTEMD_USER_DIR, `opencode-job-${job.slug}.service`);
13447
+ const legacyTimerPath = join2(SYSTEMD_USER_DIR, `opencode-job-${job.slug}.timer`);
13345
13448
  for (const p of [scopedServicePath, scopedTimerPath, legacyServicePath, legacyTimerPath]) {
13346
- if (existsSync(p)) {
13449
+ if (existsSync2(p)) {
13347
13450
  try {
13348
- unlinkSync(p);
13451
+ unlinkSync2(p);
13349
13452
  } catch {}
13350
13453
  }
13351
13454
  }
@@ -13577,10 +13680,10 @@ function ensureScopeStorage(scopeId) {
13577
13680
  function loadScopedJob(scopeId, slug) {
13578
13681
  ensureScopeStorage(scopeId);
13579
13682
  const path = jobFilePath(scopeId, slug);
13580
- if (!existsSync(path))
13683
+ if (!existsSync2(path))
13581
13684
  return null;
13582
13685
  try {
13583
- return normalizeJob(JSON.parse(readFileSync(path, "utf-8")));
13686
+ return normalizeJob(JSON.parse(readFileSync2(path, "utf-8")));
13584
13687
  } catch {
13585
13688
  return null;
13586
13689
  }
@@ -13590,7 +13693,7 @@ function loadAllScopedJobs(scopeId) {
13590
13693
  const files = readdirSync(scopeJobsDir(scopeId)).filter((f) => f.endsWith(".json"));
13591
13694
  return files.map((f) => {
13592
13695
  try {
13593
- return normalizeJob(JSON.parse(readFileSync(join(scopeJobsDir(scopeId), f), "utf-8")));
13696
+ return normalizeJob(JSON.parse(readFileSync2(join2(scopeJobsDir(scopeId), f), "utf-8")));
13594
13697
  } catch {
13595
13698
  return null;
13596
13699
  }
@@ -13601,7 +13704,7 @@ function listScopeIds() {
13601
13704
  try {
13602
13705
  return readdirSync(SCOPES_DIR).filter((name) => {
13603
13706
  try {
13604
- return existsSync(scopeDir(name));
13707
+ return existsSync2(scopeDir(name));
13605
13708
  } catch {
13606
13709
  return false;
13607
13710
  }
@@ -13620,11 +13723,11 @@ function loadAllJobsAcrossScopes() {
13620
13723
  }
13621
13724
  function loadLegacyJob(slug) {
13622
13725
  ensureDir(LEGACY_JOBS_DIR);
13623
- const path = join(LEGACY_JOBS_DIR, `${slug}.json`);
13624
- if (!existsSync(path))
13726
+ const path = join2(LEGACY_JOBS_DIR, `${slug}.json`);
13727
+ if (!existsSync2(path))
13625
13728
  return null;
13626
13729
  try {
13627
- return normalizeJob(JSON.parse(readFileSync(path, "utf-8")));
13730
+ return normalizeJob(JSON.parse(readFileSync2(path, "utf-8")));
13628
13731
  } catch {
13629
13732
  return null;
13630
13733
  }
@@ -13634,7 +13737,7 @@ function loadAllLegacyJobs() {
13634
13737
  const files = readdirSync(LEGACY_JOBS_DIR).filter((f) => f.endsWith(".json"));
13635
13738
  return files.map((f) => {
13636
13739
  try {
13637
- return normalizeJob(JSON.parse(readFileSync(join(LEGACY_JOBS_DIR, f), "utf-8")));
13740
+ return normalizeJob(JSON.parse(readFileSync2(join2(LEGACY_JOBS_DIR, f), "utf-8")));
13638
13741
  } catch {
13639
13742
  return null;
13640
13743
  }
@@ -13645,27 +13748,27 @@ function saveJob(job) {
13645
13748
  const normalizedJob = { ...job, scopeId };
13646
13749
  ensureScopeStorage(scopeId);
13647
13750
  const path = jobFilePath(scopeId, normalizedJob.slug);
13648
- writeFileSync(path, JSON.stringify(sanitizeJob(normalizedJob), null, 2));
13751
+ writeFileSync2(path, JSON.stringify(sanitizeJob(normalizedJob), null, 2));
13649
13752
  }
13650
13753
  function deleteJobFile(job) {
13651
13754
  const scopeId = job.scopeId || deriveScopeId(job.workdir || homedir());
13652
13755
  const path = jobFilePath(scopeId, job.slug);
13653
- if (existsSync(path)) {
13654
- unlinkSync(path);
13756
+ if (existsSync2(path)) {
13757
+ unlinkSync2(path);
13655
13758
  }
13656
13759
  }
13657
13760
  function listDirectoryFiles(dir, options) {
13658
- if (!existsSync(dir))
13761
+ if (!existsSync2(dir))
13659
13762
  return [];
13660
13763
  try {
13661
13764
  const entries = readdirSync(dir, { withFileTypes: true });
13662
- return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).filter((name) => options?.prefix ? name.startsWith(options.prefix) : true).filter((name) => options?.suffix ? name.endsWith(options.suffix) : true).map((name) => join(dir, name)).sort();
13765
+ return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).filter((name) => options?.prefix ? name.startsWith(options.prefix) : true).filter((name) => options?.suffix ? name.endsWith(options.suffix) : true).map((name) => join2(dir, name)).sort();
13663
13766
  } catch {
13664
13767
  return [];
13665
13768
  }
13666
13769
  }
13667
13770
  function listDirectoryNames(dir) {
13668
- if (!existsSync(dir))
13771
+ if (!existsSync2(dir))
13669
13772
  return [];
13670
13773
  try {
13671
13774
  return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
@@ -13681,9 +13784,9 @@ function buildGlobalCleanupPlan(includeHistory) {
13681
13784
  const scopedJobDefinitionPaths = scopeIds.flatMap((scopeId) => listDirectoryFiles(scopeJobsDir(scopeId), { suffix: ".json" }));
13682
13785
  const lockPaths = scopeIds.flatMap((scopeId) => listDirectoryFiles(scopeLocksDir(scopeId), { suffix: ".json" }));
13683
13786
  const runHistoryPaths = includeHistory ? scopeIds.flatMap((scopeId) => listDirectoryFiles(scopeRunsDir(scopeId), { suffix: ".jsonl" })) : [];
13684
- const schedulerLogsRoot = join(LOGS_DIR, "scheduler");
13787
+ const schedulerLogsRoot = join2(LOGS_DIR, "scheduler");
13685
13788
  const logScopeIds = listDirectoryNames(schedulerLogsRoot);
13686
- const logPaths = includeHistory ? logScopeIds.flatMap((scopeId) => listDirectoryFiles(join(schedulerLogsRoot, scopeId), { suffix: ".log" })) : [];
13789
+ const logPaths = includeHistory ? logScopeIds.flatMap((scopeId) => listDirectoryFiles(join2(schedulerLogsRoot, scopeId), { suffix: ".log" })) : [];
13687
13790
  const launchdPaths = IS_MAC ? listDirectoryFiles(LAUNCH_AGENTS_DIR, { prefix: `${LAUNCHD_PREFIX}.`, suffix: ".plist" }) : [];
13688
13791
  const systemdPaths = IS_LINUX ? [
13689
13792
  ...listDirectoryFiles(SYSTEMD_USER_DIR, { prefix: "opencode-job-", suffix: ".service" }),
@@ -13705,7 +13808,7 @@ function buildGlobalCleanupPlan(includeHistory) {
13705
13808
  function removePaths(paths, errors3) {
13706
13809
  const removed = [];
13707
13810
  for (const path of uniquePaths(paths)) {
13708
- if (!existsSync(path))
13811
+ if (!existsSync2(path))
13709
13812
  continue;
13710
13813
  try {
13711
13814
  rmSync(path, { recursive: true, force: true });
@@ -13732,7 +13835,7 @@ function executeGlobalCleanup(plan, options) {
13732
13835
  }
13733
13836
  const removeOrPreview = (paths) => {
13734
13837
  if (dryRun)
13735
- return uniquePaths(paths).filter((path) => existsSync(path));
13838
+ return uniquePaths(paths).filter((path) => existsSync2(path));
13736
13839
  return removePaths(paths, errors3);
13737
13840
  };
13738
13841
  const removed = {
@@ -14178,10 +14281,10 @@ function buildRunEnvironment() {
14178
14281
  };
14179
14282
  }
14180
14283
  function loadSchedulerConfig() {
14181
- if (!existsSync(SCHEDULER_CONFIG))
14284
+ if (!existsSync2(SCHEDULER_CONFIG))
14182
14285
  return {};
14183
14286
  try {
14184
- const raw = readFileSync(SCHEDULER_CONFIG, "utf-8");
14287
+ const raw = readFileSync2(SCHEDULER_CONFIG, "utf-8");
14185
14288
  const parsed = JSON.parse(raw);
14186
14289
  if (!isRecord(parsed))
14187
14290
  return {};
@@ -14382,7 +14485,7 @@ function formatJobDetails(job) {
14382
14485
  }
14383
14486
  function getJobLogs(job, options) {
14384
14487
  const logPath = getLogPath(job);
14385
- if (!existsSync(logPath))
14488
+ if (!existsSync2(logPath))
14386
14489
  return null;
14387
14490
  const maxChars = options?.maxChars ?? 5000;
14388
14491
  const tailLines = options?.tailLines;
@@ -14395,14 +14498,14 @@ function getJobLogs(job, options) {
14395
14498
  }).toString();
14396
14499
  return output.length > maxChars ? output.slice(-maxChars) : output;
14397
14500
  } catch {
14398
- const content2 = readFileSync(logPath, "utf-8");
14501
+ const content2 = readFileSync2(logPath, "utf-8");
14399
14502
  const lines = content2.split(/\r?\n/);
14400
14503
  const output = lines.slice(-clampedLines).join(`
14401
14504
  `);
14402
14505
  return output.length > maxChars ? output.slice(-maxChars) : output;
14403
14506
  }
14404
14507
  }
14405
- const content = readFileSync(logPath, "utf-8");
14508
+ const content = readFileSync2(logPath, "utf-8");
14406
14509
  return content.length > maxChars ? content.slice(-maxChars) : content;
14407
14510
  } catch {
14408
14511
  return null;
@@ -14830,9 +14933,9 @@ ${content.trim()}
14830
14933
  installJob(updatedJob);
14831
14934
  if (scopeChanged) {
14832
14935
  const oldPath = jobFilePath(oldScopeId, job.slug);
14833
- if (existsSync(oldPath)) {
14936
+ if (existsSync2(oldPath)) {
14834
14937
  try {
14835
- unlinkSync(oldPath);
14938
+ unlinkSync2(oldPath);
14836
14939
  } catch {}
14837
14940
  }
14838
14941
  }
@@ -14861,10 +14964,10 @@ ${content.trim()}
14861
14964
  }
14862
14965
  uninstallJob(job);
14863
14966
  deleteJobFile(job);
14864
- const legacyPath = join(LEGACY_JOBS_DIR, `${job.slug}.json`);
14865
- if (existsSync(legacyPath)) {
14967
+ const legacyPath = join2(LEGACY_JOBS_DIR, `${job.slug}.json`);
14968
+ if (existsSync2(legacyPath)) {
14866
14969
  try {
14867
- unlinkSync(legacyPath);
14970
+ unlinkSync2(legacyPath);
14868
14971
  } catch {}
14869
14972
  }
14870
14973
  return okResult(format, `Deleted job "${job.name}"`, { job });
@@ -15019,24 +15122,7 @@ ${logs}`, { job, logPath, logs });
15019
15122
  };
15020
15123
  };
15021
15124
  var src_default = SchedulerPlugin;
15022
- var __test__ = {
15023
- cronToSystemdCalendars,
15024
- createSystemdTimer,
15025
- withSystemdRuntimeEnv,
15026
- systemdRunEnv,
15027
- installSystemdJob,
15028
- uninstallSystemdJob,
15029
- saveJob,
15030
- deleteJobFile,
15031
- jobFilePath,
15032
- SYSTEMD_USER_DIR,
15033
- SCOPES_DIR,
15034
- setSystemdCommandRunner(runner) {
15035
- systemdCommandRunner = runner ?? defaultSystemdCommandRunner;
15036
- }
15037
- };
15038
15125
  export {
15039
15126
  src_default as default,
15040
- __test__,
15041
15127
  SchedulerPlugin
15042
15128
  };
@@ -0,0 +1,28 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
2
+ import type { ExecSyncOptions } from "child_process";
3
+ export type SystemdCommandRunner = (command: string, options?: ExecSyncOptions) => Buffer | string;
4
+ export interface RuntimeEnvDependencies {
5
+ exists: (path: string) => boolean;
6
+ uid: () => number | undefined;
7
+ }
8
+ export declare function withSystemdRuntimeEnv(env: NodeJS.ProcessEnv, dependencies?: RuntimeEnvDependencies): NodeJS.ProcessEnv;
9
+ export interface SystemdInstallRequest {
10
+ unitDir: string;
11
+ serviceUnit: string;
12
+ timerUnit: string;
13
+ serviceContent: string;
14
+ timerContent: string;
15
+ run: SystemdCommandRunner;
16
+ fileSystem?: SystemdFileSystem;
17
+ }
18
+ export interface SystemdFileSystem {
19
+ chmod: typeof chmodSync;
20
+ exists: typeof existsSync;
21
+ mkdir: typeof mkdirSync;
22
+ readFile: typeof readFileSync;
23
+ rename: typeof renameSync;
24
+ stat: typeof statSync;
25
+ unlink: typeof unlinkSync;
26
+ writeFile: typeof writeFileSync;
27
+ }
28
+ export declare function installSystemdUnits(request: SystemdInstallRequest): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staticduo/opencode-scheduler",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "OpenCode plugin for scheduling recurring jobs using launchd, systemd, Task Scheduler, or cron fallback",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,13 +18,13 @@
18
18
  }
19
19
  },
20
20
  "scripts": {
21
- "build": "bun build src/index.ts --outdir dist --target bun --format esm && tsc --emitDeclarationOnly",
21
+ "build": "bun build src/index.ts --outdir dist --target bun --format esm && tsc --project tsconfig.build.json",
22
22
  "clean": "rm -rf dist",
23
23
  "prepublishOnly": "bun run clean && bun run build",
24
24
  "release:patch": "npm version patch && npm publish",
25
25
  "release:minor": "npm version minor && npm publish",
26
26
  "release:major": "npm version major && npm publish",
27
- "typecheck": "tsc --noEmit",
27
+ "typecheck": "tsc --project tsconfig.json --noEmit && tsc --project tsconfig.test.json --noEmit",
28
28
  "test": "bun test"
29
29
  },
30
30
  "keywords": [