@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 +1 -0
- package/dist/cron.d.ts +4 -0
- package/dist/index.js +326 -229
- package/dist/systemd.d.ts +28 -0
- package/package.json +3 -3
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 {
|
|
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
|
-
|
|
12344
|
-
|
|
12345
|
-
var
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
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 =
|
|
12593
|
+
var LAUNCH_AGENTS_DIR = join2(homedir(), "Library", "LaunchAgents");
|
|
12354
12594
|
var LAUNCHD_PREFIX = "com.opencode.job";
|
|
12355
|
-
var SYSTEMD_USER_DIR =
|
|
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 (!
|
|
12361
|
-
|
|
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
|
|
12633
|
+
return join2(SCOPES_DIR, scopeId);
|
|
12394
12634
|
}
|
|
12395
12635
|
function scopeJobsDir(scopeId) {
|
|
12396
|
-
return
|
|
12636
|
+
return join2(scopeDir(scopeId), "jobs");
|
|
12397
12637
|
}
|
|
12398
12638
|
function scopeLocksDir(scopeId) {
|
|
12399
|
-
return
|
|
12639
|
+
return join2(scopeDir(scopeId), "locks");
|
|
12400
12640
|
}
|
|
12401
12641
|
function scopeRunsDir(scopeId) {
|
|
12402
|
-
return
|
|
12642
|
+
return join2(scopeDir(scopeId), "runs");
|
|
12403
12643
|
}
|
|
12404
12644
|
function scopeLogsDir(scopeId) {
|
|
12405
|
-
return
|
|
12645
|
+
return join2(LOGS_DIR, "scheduler", scopeId);
|
|
12406
12646
|
}
|
|
12407
12647
|
function jobFilePath(scopeId, slug) {
|
|
12408
|
-
return
|
|
12648
|
+
return join2(scopeJobsDir(scopeId), `${slug}.json`);
|
|
12409
12649
|
}
|
|
12410
12650
|
function scopedLogPath(scopeId, slug) {
|
|
12411
|
-
return
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
12807
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
12820
|
-
const raw =
|
|
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
|
-
|
|
13088
|
+
join2(homedir(), ".opencode", "bin", "opencode")
|
|
12849
13089
|
];
|
|
12850
13090
|
for (const p of paths) {
|
|
12851
|
-
if (
|
|
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 =
|
|
13344
|
+
const legacyPlistPath = join2(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
|
|
13197
13345
|
const label = `${LAUNCHD_PREFIX}.${scopeId}.${job.slug}`;
|
|
13198
|
-
const plistPath =
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
13362
|
+
const scopedPlistPath = join2(LAUNCH_AGENTS_DIR, `${scopedLabel}.plist`);
|
|
13215
13363
|
const legacyLabel = `${LAUNCHD_PREFIX}.${job.slug}`;
|
|
13216
|
-
const legacyPlistPath =
|
|
13364
|
+
const legacyPlistPath = join2(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
|
|
13217
13365
|
for (const plistPath of [scopedPlistPath, legacyPlistPath]) {
|
|
13218
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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 =
|
|
13306
|
-
const timerPath =
|
|
13307
|
-
|
|
13308
|
-
|
|
13309
|
-
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
13313
|
-
|
|
13314
|
-
|
|
13315
|
-
|
|
13316
|
-
|
|
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 =
|
|
13342
|
-
const scopedTimerPath =
|
|
13343
|
-
const legacyServicePath =
|
|
13344
|
-
const legacyTimerPath =
|
|
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 (
|
|
13461
|
+
if (existsSync2(p)) {
|
|
13347
13462
|
try {
|
|
13348
|
-
|
|
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 (!
|
|
13695
|
+
if (!existsSync2(path))
|
|
13581
13696
|
return null;
|
|
13582
13697
|
try {
|
|
13583
|
-
return normalizeJob(JSON.parse(
|
|
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(
|
|
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
|
|
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 =
|
|
13624
|
-
if (!
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
13654
|
-
|
|
13768
|
+
if (existsSync2(path)) {
|
|
13769
|
+
unlinkSync2(path);
|
|
13655
13770
|
}
|
|
13656
13771
|
}
|
|
13657
13772
|
function listDirectoryFiles(dir, options) {
|
|
13658
|
-
if (!
|
|
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) =>
|
|
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 (!
|
|
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 =
|
|
13799
|
+
const schedulerLogsRoot = join2(LOGS_DIR, "scheduler");
|
|
13685
13800
|
const logScopeIds = listDirectoryNames(schedulerLogsRoot);
|
|
13686
|
-
const logPaths = includeHistory ? logScopeIds.flatMap((scopeId) => listDirectoryFiles(
|
|
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 (!
|
|
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) =>
|
|
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 (!
|
|
14296
|
+
if (!existsSync2(SCHEDULER_CONFIG))
|
|
14182
14297
|
return {};
|
|
14183
14298
|
try {
|
|
14184
|
-
const raw =
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 (
|
|
14948
|
+
if (existsSync2(oldPath)) {
|
|
14834
14949
|
try {
|
|
14835
|
-
|
|
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 =
|
|
14865
|
-
if (
|
|
14979
|
+
const legacyPath = join2(LEGACY_JOBS_DIR, `${job.slug}.json`);
|
|
14980
|
+
if (existsSync2(legacyPath)) {
|
|
14866
14981
|
try {
|
|
14867
|
-
|
|
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.
|
|
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 --
|
|
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": [
|