@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 +1 -0
- package/dist/cron.d.ts +4 -0
- package/dist/index.d.ts +0 -67
- package/dist/index.js +314 -228
- 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.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 {
|
|
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
|
+
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 =
|
|
12581
|
+
var LAUNCH_AGENTS_DIR = join2(homedir(), "Library", "LaunchAgents");
|
|
12354
12582
|
var LAUNCHD_PREFIX = "com.opencode.job";
|
|
12355
|
-
var SYSTEMD_USER_DIR =
|
|
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 (!
|
|
12361
|
-
|
|
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
|
|
12621
|
+
return join2(SCOPES_DIR, scopeId);
|
|
12394
12622
|
}
|
|
12395
12623
|
function scopeJobsDir(scopeId) {
|
|
12396
|
-
return
|
|
12624
|
+
return join2(scopeDir(scopeId), "jobs");
|
|
12397
12625
|
}
|
|
12398
12626
|
function scopeLocksDir(scopeId) {
|
|
12399
|
-
return
|
|
12627
|
+
return join2(scopeDir(scopeId), "locks");
|
|
12400
12628
|
}
|
|
12401
12629
|
function scopeRunsDir(scopeId) {
|
|
12402
|
-
return
|
|
12630
|
+
return join2(scopeDir(scopeId), "runs");
|
|
12403
12631
|
}
|
|
12404
12632
|
function scopeLogsDir(scopeId) {
|
|
12405
|
-
return
|
|
12633
|
+
return join2(LOGS_DIR, "scheduler", scopeId);
|
|
12406
12634
|
}
|
|
12407
12635
|
function jobFilePath(scopeId, slug) {
|
|
12408
|
-
return
|
|
12636
|
+
return join2(scopeJobsDir(scopeId), `${slug}.json`);
|
|
12409
12637
|
}
|
|
12410
12638
|
function scopedLogPath(scopeId, slug) {
|
|
12411
|
-
return
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
12807
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
12820
|
-
const raw =
|
|
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
|
-
|
|
13076
|
+
join2(homedir(), ".opencode", "bin", "opencode")
|
|
12849
13077
|
];
|
|
12850
13078
|
for (const p of paths) {
|
|
12851
|
-
if (
|
|
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 =
|
|
13332
|
+
const legacyPlistPath = join2(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
|
|
13197
13333
|
const label = `${LAUNCHD_PREFIX}.${scopeId}.${job.slug}`;
|
|
13198
|
-
const plistPath =
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
13350
|
+
const scopedPlistPath = join2(LAUNCH_AGENTS_DIR, `${scopedLabel}.plist`);
|
|
13215
13351
|
const legacyLabel = `${LAUNCHD_PREFIX}.${job.slug}`;
|
|
13216
|
-
const legacyPlistPath =
|
|
13352
|
+
const legacyPlistPath = join2(LAUNCH_AGENTS_DIR, `${legacyLabel}.plist`);
|
|
13217
13353
|
for (const plistPath of [scopedPlistPath, legacyPlistPath]) {
|
|
13218
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
}
|
|
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 =
|
|
13342
|
-
const scopedTimerPath =
|
|
13343
|
-
const legacyServicePath =
|
|
13344
|
-
const legacyTimerPath =
|
|
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 (
|
|
13449
|
+
if (existsSync2(p)) {
|
|
13347
13450
|
try {
|
|
13348
|
-
|
|
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 (!
|
|
13683
|
+
if (!existsSync2(path))
|
|
13581
13684
|
return null;
|
|
13582
13685
|
try {
|
|
13583
|
-
return normalizeJob(JSON.parse(
|
|
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(
|
|
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
|
|
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 =
|
|
13624
|
-
if (!
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
13654
|
-
|
|
13756
|
+
if (existsSync2(path)) {
|
|
13757
|
+
unlinkSync2(path);
|
|
13655
13758
|
}
|
|
13656
13759
|
}
|
|
13657
13760
|
function listDirectoryFiles(dir, options) {
|
|
13658
|
-
if (!
|
|
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) =>
|
|
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 (!
|
|
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 =
|
|
13787
|
+
const schedulerLogsRoot = join2(LOGS_DIR, "scheduler");
|
|
13685
13788
|
const logScopeIds = listDirectoryNames(schedulerLogsRoot);
|
|
13686
|
-
const logPaths = includeHistory ? logScopeIds.flatMap((scopeId) => listDirectoryFiles(
|
|
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 (!
|
|
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) =>
|
|
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 (!
|
|
14284
|
+
if (!existsSync2(SCHEDULER_CONFIG))
|
|
14182
14285
|
return {};
|
|
14183
14286
|
try {
|
|
14184
|
-
const raw =
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 (
|
|
14936
|
+
if (existsSync2(oldPath)) {
|
|
14834
14937
|
try {
|
|
14835
|
-
|
|
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 =
|
|
14865
|
-
if (
|
|
14967
|
+
const legacyPath = join2(LEGACY_JOBS_DIR, `${job.slug}.json`);
|
|
14968
|
+
if (existsSync2(legacyPath)) {
|
|
14866
14969
|
try {
|
|
14867
|
-
|
|
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.
|
|
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 --
|
|
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": [
|