@staticduo/opencode-scheduler 1.3.1 → 1.3.3

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.js CHANGED
@@ -12335,30 +12335,270 @@ 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
+ const enabledStates = ["enabled", "enabled-runtime", "linked", "linked-runtime", "alias"];
12524
+ const disabledStates = ["disabled", "static", "indirect", "masked", "masked-runtime", "not-found"];
12525
+ const activeStates = ["active", "activating", "reloading"];
12526
+ const inactiveStates = ["inactive", "failed", "deactivating", "unknown"];
12527
+ if (query === "is-enabled" && enabledStates.includes(output))
12528
+ return true;
12529
+ if (query === "is-enabled" && disabledStates.includes(output))
12530
+ return false;
12531
+ if (query === "is-active" && activeStates.includes(output))
12532
+ return true;
12533
+ if (query === "is-active" && inactiveStates.includes(output))
12534
+ return false;
12535
+ throw new Error(`Unable to determine whether ${timerUnit} ${query}: ${output || "no status returned"}`);
12536
+ }
12537
+ function restoreFile(snapshot, fileSystem) {
12538
+ if (!snapshot.existed) {
12539
+ try {
12540
+ fileSystem.unlink(snapshot.path);
12541
+ } catch {}
12542
+ return;
12543
+ }
12544
+ atomicReplace(snapshot.path, snapshot.content ?? Buffer.alloc(0), snapshot.mode ?? 420, fileSystem);
12545
+ }
12546
+ function bestEffort(action) {
12547
+ try {
12548
+ action();
12549
+ } catch {}
12550
+ }
12551
+ function installSystemdUnits(request) {
12552
+ const fileSystem = request.fileSystem ?? defaultFileSystem;
12553
+ fileSystem.mkdir(request.unitDir, { recursive: true });
12554
+ const servicePath = join(request.unitDir, request.serviceUnit);
12555
+ const timerPath = join(request.unitDir, request.timerUnit);
12556
+ const serviceSnapshot = snapshotFile(servicePath, fileSystem);
12557
+ const timerSnapshot = snapshotFile(timerPath, fileSystem);
12558
+ const wasEnabled = queryTimerState(request.run, request.timerUnit, "is-enabled");
12559
+ const wasActive = queryTimerState(request.run, request.timerUnit, "is-active");
12560
+ try {
12561
+ atomicReplace(servicePath, request.serviceContent, 420, fileSystem);
12562
+ atomicReplace(timerPath, request.timerContent, 420, fileSystem);
12563
+ request.run("systemctl --user daemon-reload");
12564
+ request.run(`systemctl --user enable ${request.timerUnit}`);
12565
+ request.run(`systemctl --user start ${request.timerUnit}`);
12566
+ } catch (error45) {
12567
+ if (!wasActive)
12568
+ bestEffort(() => request.run(`systemctl --user stop ${request.timerUnit}`, { stdio: "ignore" }));
12569
+ if (!wasEnabled)
12570
+ bestEffort(() => request.run(`systemctl --user disable ${request.timerUnit}`, { stdio: "ignore" }));
12571
+ bestEffort(() => restoreFile(serviceSnapshot, fileSystem));
12572
+ bestEffort(() => restoreFile(timerSnapshot, fileSystem));
12573
+ bestEffort(() => request.run("systemctl --user daemon-reload", { stdio: "ignore" }));
12574
+ if (wasEnabled)
12575
+ bestEffort(() => request.run(`systemctl --user enable ${request.timerUnit}`, { stdio: "ignore" }));
12576
+ if (wasActive)
12577
+ bestEffort(() => request.run(`systemctl --user start ${request.timerUnit}`, { stdio: "ignore" }));
12578
+ throw error45;
12579
+ }
12580
+ }
12581
+
12582
+ // src/index.ts
12583
+ var OPENCODE_CONFIG = join2(homedir(), ".config", "opencode");
12584
+ var LEGACY_JOBS_DIR = join2(OPENCODE_CONFIG, "jobs");
12585
+ var LOGS_DIR = join2(OPENCODE_CONFIG, "logs");
12586
+ var SCHEDULER_DIR = join2(OPENCODE_CONFIG, "scheduler");
12587
+ var SCOPES_DIR = join2(SCHEDULER_DIR, "scopes");
12588
+ var SUPERVISOR_PATH = join2(SCHEDULER_DIR, "supervisor.pl");
12589
+ var SCHEDULER_CONFIG = join2(OPENCODE_CONFIG, "opencode-scheduler.json");
12350
12590
  var IS_MAC = platform() === "darwin";
12351
12591
  var IS_LINUX = platform() === "linux";
12352
12592
  var IS_WINDOWS = platform() === "win32";
12353
- var LAUNCH_AGENTS_DIR = join(homedir(), "Library", "LaunchAgents");
12593
+ var LAUNCH_AGENTS_DIR = join2(homedir(), "Library", "LaunchAgents");
12354
12594
  var LAUNCHD_PREFIX = "com.opencode.job";
12355
- var SYSTEMD_USER_DIR = join(homedir(), ".config", "systemd", "user");
12595
+ var SYSTEMD_USER_DIR = join2(homedir(), ".config", "systemd", "user");
12356
12596
  var WINDOWS_TASK_ROOT = "\\OpenCode";
12357
12597
  var WINDOWS_TASK_PREFIX = "opencode-job";
12358
12598
  var CRON_MANAGED_PREFIX = "opencode-scheduler";
12359
12599
  function ensureDir(dir) {
12360
- if (!existsSync(dir)) {
12361
- mkdirSync(dir, { recursive: true });
12600
+ if (!existsSync2(dir)) {
12601
+ mkdirSync2(dir, { recursive: true });
12362
12602
  }
12363
12603
  }
12364
12604
  function slugify(name) {
@@ -12390,25 +12630,25 @@ function deriveScopeId(workdir) {
12390
12630
  return `${base}-${suffix}`;
12391
12631
  }
12392
12632
  function scopeDir(scopeId) {
12393
- return join(SCOPES_DIR, scopeId);
12633
+ return join2(SCOPES_DIR, scopeId);
12394
12634
  }
12395
12635
  function scopeJobsDir(scopeId) {
12396
- return join(scopeDir(scopeId), "jobs");
12636
+ return join2(scopeDir(scopeId), "jobs");
12397
12637
  }
12398
12638
  function scopeLocksDir(scopeId) {
12399
- return join(scopeDir(scopeId), "locks");
12639
+ return join2(scopeDir(scopeId), "locks");
12400
12640
  }
12401
12641
  function scopeRunsDir(scopeId) {
12402
- return join(scopeDir(scopeId), "runs");
12642
+ return join2(scopeDir(scopeId), "runs");
12403
12643
  }
12404
12644
  function scopeLogsDir(scopeId) {
12405
- return join(LOGS_DIR, "scheduler", scopeId);
12645
+ return join2(LOGS_DIR, "scheduler", scopeId);
12406
12646
  }
12407
12647
  function jobFilePath(scopeId, slug) {
12408
- return join(scopeJobsDir(scopeId), `${slug}.json`);
12648
+ return join2(scopeJobsDir(scopeId), `${slug}.json`);
12409
12649
  }
12410
12650
  function scopedLogPath(scopeId, slug) {
12411
- return join(scopeLogsDir(scopeId), `${slug}.log`);
12651
+ return join2(scopeLogsDir(scopeId), `${slug}.log`);
12412
12652
  }
12413
12653
  function currentScopeId() {
12414
12654
  return deriveScopeId(process.cwd());
@@ -12667,7 +12907,7 @@ exit($exit_code);
12667
12907
  `;
12668
12908
  function ensureSupervisorScript() {
12669
12909
  ensureDir(SCHEDULER_DIR);
12670
- writeFileSync(SUPERVISOR_PATH, SUPERVISOR_SCRIPT);
12910
+ writeFileSync2(SUPERVISOR_PATH, SUPERVISOR_SCRIPT);
12671
12911
  }
12672
12912
  function normalizeFormat(format) {
12673
12913
  return format === "json" ? "json" : "text";
@@ -12795,19 +13035,19 @@ function installBuiltinSkill(skill, rootDir, overwrite = false) {
12795
13035
  if (!installRoot) {
12796
13036
  throw new Error("Install directory cannot be empty.");
12797
13037
  }
12798
- if (!existsSync(installRoot)) {
13038
+ if (!existsSync2(installRoot)) {
12799
13039
  throw new Error(`Directory not found: ${installRoot}`);
12800
13040
  }
12801
13041
  const relativeDir = dirname(skill.suggestedPath);
12802
- const installDir = join(installRoot, relativeDir);
13042
+ const installDir = join2(installRoot, relativeDir);
12803
13043
  ensureDir(installDir);
12804
13044
  const files = [];
12805
13045
  for (const [filename, content] of Object.entries(skill.files)) {
12806
- const targetPath = join(installDir, filename);
12807
- if (existsSync(targetPath) && !overwrite) {
13046
+ const targetPath = join2(installDir, filename);
13047
+ if (existsSync2(targetPath) && !overwrite) {
12808
13048
  throw new Error(`File already exists: ${targetPath} (pass overwrite=true to replace)`);
12809
13049
  }
12810
- writeFileSync(targetPath, `${content.trimEnd()}
13050
+ writeFileSync2(targetPath, `${content.trimEnd()}
12811
13051
  `);
12812
13052
  files.push(targetPath);
12813
13053
  }
@@ -12816,8 +13056,8 @@ function installBuiltinSkill(skill, rootDir, overwrite = false) {
12816
13056
  function loadPackageInfo() {
12817
13057
  const fallback = { name: "opencode-scheduler", version: "unknown" };
12818
13058
  try {
12819
- const packagePath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
12820
- const raw = readFileSync(packagePath, "utf-8");
13059
+ const packagePath = join2(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
13060
+ const raw = readFileSync2(packagePath, "utf-8");
12821
13061
  const parsed = JSON.parse(raw);
12822
13062
  return {
12823
13063
  name: typeof parsed.name === "string" ? parsed.name : fallback.name,
@@ -12845,10 +13085,10 @@ function findOpencode() {
12845
13085
  const paths = [
12846
13086
  "/opt/homebrew/bin/opencode",
12847
13087
  "/usr/local/bin/opencode",
12848
- join(homedir(), ".opencode", "bin", "opencode")
13088
+ join2(homedir(), ".opencode", "bin", "opencode")
12849
13089
  ];
12850
13090
  for (const p of paths) {
12851
- if (existsSync(p)) {
13091
+ if (existsSync2(p)) {
12852
13092
  return p;
12853
13093
  }
12854
13094
  }
@@ -12865,59 +13105,6 @@ function getEnhancedPath() {
12865
13105
  ];
12866
13106
  return paths.join(":");
12867
13107
  }
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
13108
  function expandLaunchdEntries(entries, key, values) {
12922
13109
  if (!values)
12923
13110
  return entries;
@@ -12964,45 +13151,6 @@ function renderLaunchdCalendar(calendar) {
12964
13151
  <integer>${value}</integer>`).join(`
12965
13152
  `);
12966
13153
  }
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
13154
  var WINDOWS_WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
13007
13155
  var WINDOWS_MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
13008
13156
  function pad2(value) {
@@ -13193,57 +13341,38 @@ function installLaunchdJob(job) {
13193
13341
  ensureDir(scopeLogsDir(scopeId));
13194
13342
  ensureSupervisorScript();
13195
13343
  const legacyLabel = `${LAUNCHD_PREFIX}.${job.slug}`;
13196
- const legacyPlistPath = join(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
13344
+ const legacyPlistPath = join2(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
13197
13345
  const label = `${LAUNCHD_PREFIX}.${scopeId}.${job.slug}`;
13198
- const plistPath = join(LAUNCH_AGENTS_DIR, `${label}.plist`);
13346
+ const plistPath = join2(LAUNCH_AGENTS_DIR, `${label}.plist`);
13199
13347
  try {
13200
13348
  execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore" });
13201
13349
  } catch {}
13202
- if (existsSync(legacyPlistPath)) {
13350
+ if (existsSync2(legacyPlistPath)) {
13203
13351
  try {
13204
13352
  execSync(`launchctl unload "${legacyPlistPath}" 2>/dev/null`, { stdio: "ignore" });
13205
13353
  } catch {}
13206
13354
  }
13207
13355
  const plist = createLaunchdPlist(job);
13208
- writeFileSync(plistPath, plist);
13356
+ writeFileSync2(plistPath, plist);
13209
13357
  execSync(`launchctl load "${plistPath}"`);
13210
13358
  }
13211
13359
  function uninstallLaunchdJob(job) {
13212
13360
  const scopeId = job.scopeId || deriveScopeId(job.workdir || homedir());
13213
13361
  const scopedLabel = `${LAUNCHD_PREFIX}.${scopeId}.${job.slug}`;
13214
- const scopedPlistPath = join(LAUNCH_AGENTS_DIR, `${scopedLabel}.plist`);
13362
+ const scopedPlistPath = join2(LAUNCH_AGENTS_DIR, `${scopedLabel}.plist`);
13215
13363
  const legacyLabel = `${LAUNCHD_PREFIX}.${job.slug}`;
13216
- const legacyPlistPath = join(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
13364
+ const legacyPlistPath = join2(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
13217
13365
  for (const plistPath of [scopedPlistPath, legacyPlistPath]) {
13218
- if (!existsSync(plistPath))
13366
+ if (!existsSync2(plistPath))
13219
13367
  continue;
13220
13368
  try {
13221
13369
  execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
13222
13370
  } catch {}
13223
13371
  try {
13224
- unlinkSync(plistPath);
13372
+ unlinkSync2(plistPath);
13225
13373
  } catch {}
13226
13374
  }
13227
13375
  }
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
13376
  function systemdRunEnv() {
13248
13377
  const enhancedPath = getEnhancedPath();
13249
13378
  const existingPath = process.env.PATH;
@@ -13255,9 +13384,8 @@ function systemdRunEnv() {
13255
13384
  function defaultSystemdCommandRunner(command, options) {
13256
13385
  return execSync(command, { ...options, env: systemdRunEnv() });
13257
13386
  }
13258
- var systemdCommandRunner = defaultSystemdCommandRunner;
13259
13387
  function systemdExecSync(command, options) {
13260
- return systemdCommandRunner(command, options);
13388
+ return defaultSystemdCommandRunner(command, options);
13261
13389
  }
13262
13390
  function createSystemdService(job) {
13263
13391
  const scopeId = job.scopeId || deriveScopeId(job.workdir || homedir());
@@ -13296,37 +13424,24 @@ Persistent=true
13296
13424
  WantedBy=timers.target
13297
13425
  `;
13298
13426
  }
13299
- function installSystemdJob(job) {
13427
+ function installSystemdJob(job, run = defaultSystemdCommandRunner) {
13300
13428
  ensureDir(SYSTEMD_USER_DIR);
13301
13429
  ensureDir(LOGS_DIR);
13302
13430
  const scopeId = job.scopeId || deriveScopeId(job.workdir || homedir());
13303
13431
  ensureDir(scopeLogsDir(scopeId));
13304
13432
  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
- }
13433
+ const servicePath = join2(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.service`);
13434
+ const timerPath = join2(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.timer`);
13435
+ const serviceUnit = servicePath.slice(SYSTEMD_USER_DIR.length + 1);
13436
+ const timerUnit = timerPath.slice(SYSTEMD_USER_DIR.length + 1);
13437
+ installSystemdUnits({
13438
+ unitDir: SYSTEMD_USER_DIR,
13439
+ serviceUnit,
13440
+ timerUnit,
13441
+ serviceContent: createSystemdService(job),
13442
+ timerContent: createSystemdTimer(job),
13443
+ run
13444
+ });
13330
13445
  }
13331
13446
  function uninstallSystemdJob(job) {
13332
13447
  const scopeId = job.scopeId || deriveScopeId(job.workdir || homedir());
@@ -13338,14 +13453,14 @@ function uninstallSystemdJob(job) {
13338
13453
  systemdExecSync(`systemctl --user disable ${timerUnit}`, { stdio: "ignore" });
13339
13454
  } catch {}
13340
13455
  }
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`);
13456
+ const scopedServicePath = join2(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.service`);
13457
+ const scopedTimerPath = join2(SYSTEMD_USER_DIR, `opencode-job-${scopeId}-${job.slug}.timer`);
13458
+ const legacyServicePath = join2(SYSTEMD_USER_DIR, `opencode-job-${job.slug}.service`);
13459
+ const legacyTimerPath = join2(SYSTEMD_USER_DIR, `opencode-job-${job.slug}.timer`);
13345
13460
  for (const p of [scopedServicePath, scopedTimerPath, legacyServicePath, legacyTimerPath]) {
13346
- if (existsSync(p)) {
13461
+ if (existsSync2(p)) {
13347
13462
  try {
13348
- unlinkSync(p);
13463
+ unlinkSync2(p);
13349
13464
  } catch {}
13350
13465
  }
13351
13466
  }
@@ -13577,10 +13692,10 @@ function ensureScopeStorage(scopeId) {
13577
13692
  function loadScopedJob(scopeId, slug) {
13578
13693
  ensureScopeStorage(scopeId);
13579
13694
  const path = jobFilePath(scopeId, slug);
13580
- if (!existsSync(path))
13695
+ if (!existsSync2(path))
13581
13696
  return null;
13582
13697
  try {
13583
- return normalizeJob(JSON.parse(readFileSync(path, "utf-8")));
13698
+ return normalizeJob(JSON.parse(readFileSync2(path, "utf-8")));
13584
13699
  } catch {
13585
13700
  return null;
13586
13701
  }
@@ -13590,7 +13705,7 @@ function loadAllScopedJobs(scopeId) {
13590
13705
  const files = readdirSync(scopeJobsDir(scopeId)).filter((f) => f.endsWith(".json"));
13591
13706
  return files.map((f) => {
13592
13707
  try {
13593
- return normalizeJob(JSON.parse(readFileSync(join(scopeJobsDir(scopeId), f), "utf-8")));
13708
+ return normalizeJob(JSON.parse(readFileSync2(join2(scopeJobsDir(scopeId), f), "utf-8")));
13594
13709
  } catch {
13595
13710
  return null;
13596
13711
  }
@@ -13601,7 +13716,7 @@ function listScopeIds() {
13601
13716
  try {
13602
13717
  return readdirSync(SCOPES_DIR).filter((name) => {
13603
13718
  try {
13604
- return existsSync(scopeDir(name));
13719
+ return existsSync2(scopeDir(name));
13605
13720
  } catch {
13606
13721
  return false;
13607
13722
  }
@@ -13620,11 +13735,11 @@ function loadAllJobsAcrossScopes() {
13620
13735
  }
13621
13736
  function loadLegacyJob(slug) {
13622
13737
  ensureDir(LEGACY_JOBS_DIR);
13623
- const path = join(LEGACY_JOBS_DIR, `${slug}.json`);
13624
- if (!existsSync(path))
13738
+ const path = join2(LEGACY_JOBS_DIR, `${slug}.json`);
13739
+ if (!existsSync2(path))
13625
13740
  return null;
13626
13741
  try {
13627
- return normalizeJob(JSON.parse(readFileSync(path, "utf-8")));
13742
+ return normalizeJob(JSON.parse(readFileSync2(path, "utf-8")));
13628
13743
  } catch {
13629
13744
  return null;
13630
13745
  }
@@ -13634,7 +13749,7 @@ function loadAllLegacyJobs() {
13634
13749
  const files = readdirSync(LEGACY_JOBS_DIR).filter((f) => f.endsWith(".json"));
13635
13750
  return files.map((f) => {
13636
13751
  try {
13637
- return normalizeJob(JSON.parse(readFileSync(join(LEGACY_JOBS_DIR, f), "utf-8")));
13752
+ return normalizeJob(JSON.parse(readFileSync2(join2(LEGACY_JOBS_DIR, f), "utf-8")));
13638
13753
  } catch {
13639
13754
  return null;
13640
13755
  }
@@ -13645,27 +13760,27 @@ function saveJob(job) {
13645
13760
  const normalizedJob = { ...job, scopeId };
13646
13761
  ensureScopeStorage(scopeId);
13647
13762
  const path = jobFilePath(scopeId, normalizedJob.slug);
13648
- writeFileSync(path, JSON.stringify(sanitizeJob(normalizedJob), null, 2));
13763
+ writeFileSync2(path, JSON.stringify(sanitizeJob(normalizedJob), null, 2));
13649
13764
  }
13650
13765
  function deleteJobFile(job) {
13651
13766
  const scopeId = job.scopeId || deriveScopeId(job.workdir || homedir());
13652
13767
  const path = jobFilePath(scopeId, job.slug);
13653
- if (existsSync(path)) {
13654
- unlinkSync(path);
13768
+ if (existsSync2(path)) {
13769
+ unlinkSync2(path);
13655
13770
  }
13656
13771
  }
13657
13772
  function listDirectoryFiles(dir, options) {
13658
- if (!existsSync(dir))
13773
+ if (!existsSync2(dir))
13659
13774
  return [];
13660
13775
  try {
13661
13776
  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();
13777
+ 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
13778
  } catch {
13664
13779
  return [];
13665
13780
  }
13666
13781
  }
13667
13782
  function listDirectoryNames(dir) {
13668
- if (!existsSync(dir))
13783
+ if (!existsSync2(dir))
13669
13784
  return [];
13670
13785
  try {
13671
13786
  return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
@@ -13681,9 +13796,9 @@ function buildGlobalCleanupPlan(includeHistory) {
13681
13796
  const scopedJobDefinitionPaths = scopeIds.flatMap((scopeId) => listDirectoryFiles(scopeJobsDir(scopeId), { suffix: ".json" }));
13682
13797
  const lockPaths = scopeIds.flatMap((scopeId) => listDirectoryFiles(scopeLocksDir(scopeId), { suffix: ".json" }));
13683
13798
  const runHistoryPaths = includeHistory ? scopeIds.flatMap((scopeId) => listDirectoryFiles(scopeRunsDir(scopeId), { suffix: ".jsonl" })) : [];
13684
- const schedulerLogsRoot = join(LOGS_DIR, "scheduler");
13799
+ const schedulerLogsRoot = join2(LOGS_DIR, "scheduler");
13685
13800
  const logScopeIds = listDirectoryNames(schedulerLogsRoot);
13686
- const logPaths = includeHistory ? logScopeIds.flatMap((scopeId) => listDirectoryFiles(join(schedulerLogsRoot, scopeId), { suffix: ".log" })) : [];
13801
+ const logPaths = includeHistory ? logScopeIds.flatMap((scopeId) => listDirectoryFiles(join2(schedulerLogsRoot, scopeId), { suffix: ".log" })) : [];
13687
13802
  const launchdPaths = IS_MAC ? listDirectoryFiles(LAUNCH_AGENTS_DIR, { prefix: `${LAUNCHD_PREFIX}.`, suffix: ".plist" }) : [];
13688
13803
  const systemdPaths = IS_LINUX ? [
13689
13804
  ...listDirectoryFiles(SYSTEMD_USER_DIR, { prefix: "opencode-job-", suffix: ".service" }),
@@ -13705,7 +13820,7 @@ function buildGlobalCleanupPlan(includeHistory) {
13705
13820
  function removePaths(paths, errors3) {
13706
13821
  const removed = [];
13707
13822
  for (const path of uniquePaths(paths)) {
13708
- if (!existsSync(path))
13823
+ if (!existsSync2(path))
13709
13824
  continue;
13710
13825
  try {
13711
13826
  rmSync(path, { recursive: true, force: true });
@@ -13732,7 +13847,7 @@ function executeGlobalCleanup(plan, options) {
13732
13847
  }
13733
13848
  const removeOrPreview = (paths) => {
13734
13849
  if (dryRun)
13735
- return uniquePaths(paths).filter((path) => existsSync(path));
13850
+ return uniquePaths(paths).filter((path) => existsSync2(path));
13736
13851
  return removePaths(paths, errors3);
13737
13852
  };
13738
13853
  const removed = {
@@ -14178,10 +14293,10 @@ function buildRunEnvironment() {
14178
14293
  };
14179
14294
  }
14180
14295
  function loadSchedulerConfig() {
14181
- if (!existsSync(SCHEDULER_CONFIG))
14296
+ if (!existsSync2(SCHEDULER_CONFIG))
14182
14297
  return {};
14183
14298
  try {
14184
- const raw = readFileSync(SCHEDULER_CONFIG, "utf-8");
14299
+ const raw = readFileSync2(SCHEDULER_CONFIG, "utf-8");
14185
14300
  const parsed = JSON.parse(raw);
14186
14301
  if (!isRecord(parsed))
14187
14302
  return {};
@@ -14382,7 +14497,7 @@ function formatJobDetails(job) {
14382
14497
  }
14383
14498
  function getJobLogs(job, options) {
14384
14499
  const logPath = getLogPath(job);
14385
- if (!existsSync(logPath))
14500
+ if (!existsSync2(logPath))
14386
14501
  return null;
14387
14502
  const maxChars = options?.maxChars ?? 5000;
14388
14503
  const tailLines = options?.tailLines;
@@ -14395,14 +14510,14 @@ function getJobLogs(job, options) {
14395
14510
  }).toString();
14396
14511
  return output.length > maxChars ? output.slice(-maxChars) : output;
14397
14512
  } catch {
14398
- const content2 = readFileSync(logPath, "utf-8");
14513
+ const content2 = readFileSync2(logPath, "utf-8");
14399
14514
  const lines = content2.split(/\r?\n/);
14400
14515
  const output = lines.slice(-clampedLines).join(`
14401
14516
  `);
14402
14517
  return output.length > maxChars ? output.slice(-maxChars) : output;
14403
14518
  }
14404
14519
  }
14405
- const content = readFileSync(logPath, "utf-8");
14520
+ const content = readFileSync2(logPath, "utf-8");
14406
14521
  return content.length > maxChars ? content.slice(-maxChars) : content;
14407
14522
  } catch {
14408
14523
  return null;
@@ -14830,9 +14945,9 @@ ${content.trim()}
14830
14945
  installJob(updatedJob);
14831
14946
  if (scopeChanged) {
14832
14947
  const oldPath = jobFilePath(oldScopeId, job.slug);
14833
- if (existsSync(oldPath)) {
14948
+ if (existsSync2(oldPath)) {
14834
14949
  try {
14835
- unlinkSync(oldPath);
14950
+ unlinkSync2(oldPath);
14836
14951
  } catch {}
14837
14952
  }
14838
14953
  }
@@ -14861,10 +14976,10 @@ ${content.trim()}
14861
14976
  }
14862
14977
  uninstallJob(job);
14863
14978
  deleteJobFile(job);
14864
- const legacyPath = join(LEGACY_JOBS_DIR, `${job.slug}.json`);
14865
- if (existsSync(legacyPath)) {
14979
+ const legacyPath = join2(LEGACY_JOBS_DIR, `${job.slug}.json`);
14980
+ if (existsSync2(legacyPath)) {
14866
14981
  try {
14867
- unlinkSync(legacyPath);
14982
+ unlinkSync2(legacyPath);
14868
14983
  } catch {}
14869
14984
  }
14870
14985
  return okResult(format, `Deleted job "${job.name}"`, { job });
@@ -15019,24 +15134,6 @@ ${logs}`, { job, logPath, logs });
15019
15134
  };
15020
15135
  };
15021
15136
  var src_default = SchedulerPlugin;
15022
- Object.assign(SchedulerPlugin, {
15023
- __test__: {
15024
- cronToSystemdCalendars,
15025
- createSystemdTimer,
15026
- withSystemdRuntimeEnv,
15027
- systemdRunEnv,
15028
- installSystemdJob,
15029
- uninstallSystemdJob,
15030
- saveJob,
15031
- deleteJobFile,
15032
- jobFilePath,
15033
- SYSTEMD_USER_DIR,
15034
- SCOPES_DIR,
15035
- setSystemdCommandRunner(runner) {
15036
- systemdCommandRunner = runner ?? defaultSystemdCommandRunner;
15037
- }
15038
- }
15039
- });
15040
15137
  export {
15041
15138
  src_default as default,
15042
15139
  SchedulerPlugin
@@ -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.1",
3
+ "version": "1.3.3",
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": [