@damian87/omp 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/skills/schedule/SKILL.md +71 -0
- package/README.md +19 -2
- package/dist/src/cli.js +83 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/schedule/commands.d.ts +34 -0
- package/dist/src/schedule/commands.js +130 -0
- package/dist/src/schedule/commands.js.map +1 -0
- package/dist/src/schedule/installer.d.ts +20 -0
- package/dist/src/schedule/installer.js +76 -0
- package/dist/src/schedule/installer.js.map +1 -0
- package/dist/src/schedule/installers/crontab.d.ts +17 -0
- package/dist/src/schedule/installers/crontab.js +112 -0
- package/dist/src/schedule/installers/crontab.js.map +1 -0
- package/dist/src/schedule/installers/launchd.d.ts +22 -0
- package/dist/src/schedule/installers/launchd.js +125 -0
- package/dist/src/schedule/installers/launchd.js.map +1 -0
- package/dist/src/schedule/installers/systemd.d.ts +15 -0
- package/dist/src/schedule/installers/systemd.js +136 -0
- package/dist/src/schedule/installers/systemd.js.map +1 -0
- package/dist/src/schedule/job-store.d.ts +21 -0
- package/dist/src/schedule/job-store.js +102 -0
- package/dist/src/schedule/job-store.js.map +1 -0
- package/dist/src/schedule/lock.d.ts +9 -0
- package/dist/src/schedule/lock.js +83 -0
- package/dist/src/schedule/lock.js.map +1 -0
- package/dist/src/schedule/paths.d.ts +15 -0
- package/dist/src/schedule/paths.js +36 -0
- package/dist/src/schedule/paths.js.map +1 -0
- package/dist/src/schedule/runner.d.ts +8 -0
- package/dist/src/schedule/runner.js +151 -0
- package/dist/src/schedule/runner.js.map +1 -0
- package/dist/src/schedule/types.d.ts +60 -0
- package/dist/src/schedule/types.js +5 -0
- package/dist/src/schedule/types.js.map +1 -0
- package/docs/research/2026-06-01-schedule-cron-feature.md +346 -0
- package/package.json +1 -1
- package/scripts/lib/schedule-results.mjs +88 -0
- package/scripts/session-start.mjs +8 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { OsBackend, ScheduleJob } from "./types.js";
|
|
2
|
+
export interface DetectOptions {
|
|
3
|
+
platform?: NodeJS.Platform;
|
|
4
|
+
hasSystemctl?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Pick the native backend for this host. Inputs are injectable for testing. */
|
|
7
|
+
export declare function detectOsBackend(opts?: DetectOptions): OsBackend;
|
|
8
|
+
export interface InstallResult {
|
|
9
|
+
backend: OsBackend;
|
|
10
|
+
installed: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Install the OS-scheduler entry for a job. On launchd, a cron expression that
|
|
14
|
+
* StartCalendarInterval cannot express falls back to crontab — the chosen
|
|
15
|
+
* backend is returned so the caller can persist it on the job.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installJob(job: ScheduleJob, logsDir: string, stateRoot: string, opts?: DetectOptions): InstallResult;
|
|
18
|
+
/** Uninstall by the job's recorded backend. Idempotent — never throws if absent. */
|
|
19
|
+
export declare function uninstallJob(id: string, backend: OsBackend): void;
|
|
20
|
+
export declare function getInstalledStatus(id: string, backend: OsBackend): boolean;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { cronToLaunchdInterval, installLaunchd, statusLaunchd, uninstallLaunchd } from "./installers/launchd.js";
|
|
3
|
+
import { installCrontab, statusCrontab, uninstallCrontab } from "./installers/crontab.js";
|
|
4
|
+
import { installSystemd, statusSystemd, uninstallSystemd } from "./installers/systemd.js";
|
|
5
|
+
function systemctlAvailable() {
|
|
6
|
+
try {
|
|
7
|
+
execFileSync("systemctl", ["--user", "--version"], { stdio: "ignore" });
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Pick the native backend for this host. Inputs are injectable for testing. */
|
|
15
|
+
export function detectOsBackend(opts = {}) {
|
|
16
|
+
const platform = opts.platform ?? process.platform;
|
|
17
|
+
if (platform === "darwin")
|
|
18
|
+
return "launchd";
|
|
19
|
+
const hasSystemctl = opts.hasSystemctl ?? systemctlAvailable();
|
|
20
|
+
if (hasSystemctl)
|
|
21
|
+
return "systemd";
|
|
22
|
+
return "crontab";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Install the OS-scheduler entry for a job. On launchd, a cron expression that
|
|
26
|
+
* StartCalendarInterval cannot express falls back to crontab — the chosen
|
|
27
|
+
* backend is returned so the caller can persist it on the job.
|
|
28
|
+
*/
|
|
29
|
+
export function installJob(job, logsDir, stateRoot, opts = {}) {
|
|
30
|
+
const backend = detectOsBackend(opts);
|
|
31
|
+
// Replace semantics: clear any prior entry for this id first.
|
|
32
|
+
uninstallJob(job.id, backend);
|
|
33
|
+
if (backend === "launchd") {
|
|
34
|
+
const sched = cronToLaunchdInterval(job.cron);
|
|
35
|
+
if (sched === null) {
|
|
36
|
+
// launchd can't express this cron — fall back to crontab.
|
|
37
|
+
installCrontab(job, logsDir, stateRoot);
|
|
38
|
+
return { backend: "crontab", installed: true };
|
|
39
|
+
}
|
|
40
|
+
installLaunchd(job, sched, logsDir, stateRoot);
|
|
41
|
+
return { backend: "launchd", installed: true };
|
|
42
|
+
}
|
|
43
|
+
if (backend === "systemd") {
|
|
44
|
+
installSystemd(job, stateRoot);
|
|
45
|
+
return { backend: "systemd", installed: true };
|
|
46
|
+
}
|
|
47
|
+
installCrontab(job, logsDir, stateRoot);
|
|
48
|
+
return { backend: "crontab", installed: true };
|
|
49
|
+
}
|
|
50
|
+
/** Uninstall by the job's recorded backend. Idempotent — never throws if absent. */
|
|
51
|
+
export function uninstallJob(id, backend) {
|
|
52
|
+
try {
|
|
53
|
+
if (backend === "launchd")
|
|
54
|
+
uninstallLaunchd(id);
|
|
55
|
+
else if (backend === "systemd")
|
|
56
|
+
uninstallSystemd(id);
|
|
57
|
+
else
|
|
58
|
+
uninstallCrontab(id);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// best effort — entry may already be gone
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function getInstalledStatus(id, backend) {
|
|
65
|
+
try {
|
|
66
|
+
if (backend === "launchd")
|
|
67
|
+
return statusLaunchd(id);
|
|
68
|
+
if (backend === "systemd")
|
|
69
|
+
return statusSystemd(id);
|
|
70
|
+
return statusCrontab(id);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=installer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installer.js","sourceRoot":"","sources":["../../../src/schedule/installer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ScheduleJob } from "../types.js";
|
|
2
|
+
export declare const BLOCK_BEGIN = "# BEGIN omp-schedule";
|
|
3
|
+
export declare const BLOCK_END = "# END omp-schedule";
|
|
4
|
+
/** The crontab command line for a job (without the id marker). */
|
|
5
|
+
export declare function crontabEntryLine(job: ScheduleJob, logsDir: string, stateRoot: string): string;
|
|
6
|
+
/** Parse the managed block of an existing crontab into an ordered id→line map. */
|
|
7
|
+
export declare function parseManagedBlock(existing: string): Map<string, string>;
|
|
8
|
+
/** Pure: add or replace the entry for `id` in the managed block. */
|
|
9
|
+
export declare function applyCrontabBlock(existing: string, id: string, entryLine: string): string;
|
|
10
|
+
/** Pure: remove the entry for `id`; drops the block markers when it empties. */
|
|
11
|
+
export declare function removeCrontabEntry(existing: string, id: string): string;
|
|
12
|
+
export declare function hasCrontabEntry(existing: string, id: string): boolean;
|
|
13
|
+
export declare function readCrontab(): string;
|
|
14
|
+
export declare function writeCrontab(content: string): void;
|
|
15
|
+
export declare function installCrontab(job: ScheduleJob, logsDir: string, stateRoot: string): string;
|
|
16
|
+
export declare function uninstallCrontab(id: string): void;
|
|
17
|
+
export declare function statusCrontab(id: string): boolean;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const BLOCK_BEGIN = "# BEGIN omp-schedule";
|
|
4
|
+
export const BLOCK_END = "# END omp-schedule";
|
|
5
|
+
const ID_PREFIX = "# omp:";
|
|
6
|
+
/** Single-quote a value for safe shell interpolation. */
|
|
7
|
+
function shq(s) {
|
|
8
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
9
|
+
}
|
|
10
|
+
/** The crontab command line for a job (without the id marker). */
|
|
11
|
+
export function crontabEntryLine(job, logsDir, stateRoot) {
|
|
12
|
+
const logFile = join(logsDir, job.id, `${job.id}.cron.log`);
|
|
13
|
+
// `omp schedule run` resolves state from --root (independent of the agent cwd).
|
|
14
|
+
return `${job.cron} ${shq(job.ompBinPath)} schedule run --id ${job.id} --root ${shq(stateRoot)} >> ${shq(logFile)} 2>&1`;
|
|
15
|
+
}
|
|
16
|
+
/** Parse the managed block of an existing crontab into an ordered id→line map. */
|
|
17
|
+
export function parseManagedBlock(existing) {
|
|
18
|
+
const out = new Map();
|
|
19
|
+
const lines = existing.split("\n");
|
|
20
|
+
let inBlock = false;
|
|
21
|
+
let currentId;
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
if (line.trim() === BLOCK_BEGIN) {
|
|
24
|
+
inBlock = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (line.trim() === BLOCK_END) {
|
|
28
|
+
inBlock = false;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!inBlock)
|
|
32
|
+
continue;
|
|
33
|
+
if (line.startsWith(ID_PREFIX)) {
|
|
34
|
+
currentId = line.slice(ID_PREFIX.length).trim();
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (currentId && line.trim()) {
|
|
38
|
+
out.set(currentId, line);
|
|
39
|
+
currentId = undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** Strip the managed block, returning everything outside it (trimmed). */
|
|
45
|
+
function stripManagedBlock(existing) {
|
|
46
|
+
const lines = existing.split("\n");
|
|
47
|
+
const kept = [];
|
|
48
|
+
let inBlock = false;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (line.trim() === BLOCK_BEGIN) {
|
|
51
|
+
inBlock = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (line.trim() === BLOCK_END) {
|
|
55
|
+
inBlock = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!inBlock)
|
|
59
|
+
kept.push(line);
|
|
60
|
+
}
|
|
61
|
+
return kept.join("\n").trim();
|
|
62
|
+
}
|
|
63
|
+
/** Render a fresh crontab from outside-block content + the managed entries. */
|
|
64
|
+
function render(outside, entries) {
|
|
65
|
+
if (entries.size === 0) {
|
|
66
|
+
return outside ? `${outside}\n` : "";
|
|
67
|
+
}
|
|
68
|
+
const block = [BLOCK_BEGIN];
|
|
69
|
+
for (const [id, line] of entries) {
|
|
70
|
+
block.push(`${ID_PREFIX}${id}`, line);
|
|
71
|
+
}
|
|
72
|
+
block.push(BLOCK_END);
|
|
73
|
+
return `${outside ? `${outside}\n\n` : ""}${block.join("\n")}\n`;
|
|
74
|
+
}
|
|
75
|
+
/** Pure: add or replace the entry for `id` in the managed block. */
|
|
76
|
+
export function applyCrontabBlock(existing, id, entryLine) {
|
|
77
|
+
const entries = parseManagedBlock(existing);
|
|
78
|
+
entries.set(id, entryLine);
|
|
79
|
+
return render(stripManagedBlock(existing), entries);
|
|
80
|
+
}
|
|
81
|
+
/** Pure: remove the entry for `id`; drops the block markers when it empties. */
|
|
82
|
+
export function removeCrontabEntry(existing, id) {
|
|
83
|
+
const entries = parseManagedBlock(existing);
|
|
84
|
+
entries.delete(id);
|
|
85
|
+
return render(stripManagedBlock(existing), entries);
|
|
86
|
+
}
|
|
87
|
+
export function hasCrontabEntry(existing, id) {
|
|
88
|
+
return parseManagedBlock(existing).has(id);
|
|
89
|
+
}
|
|
90
|
+
export function readCrontab() {
|
|
91
|
+
try {
|
|
92
|
+
return execSync("crontab -l 2>/dev/null", { encoding: "utf8" });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return ""; // no crontab yet
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function writeCrontab(content) {
|
|
99
|
+
execSync("crontab -", { input: content });
|
|
100
|
+
}
|
|
101
|
+
export function installCrontab(job, logsDir, stateRoot) {
|
|
102
|
+
const next = applyCrontabBlock(readCrontab(), job.id, crontabEntryLine(job, logsDir, stateRoot));
|
|
103
|
+
writeCrontab(next);
|
|
104
|
+
return "crontab";
|
|
105
|
+
}
|
|
106
|
+
export function uninstallCrontab(id) {
|
|
107
|
+
writeCrontab(removeCrontabEntry(readCrontab(), id));
|
|
108
|
+
}
|
|
109
|
+
export function statusCrontab(id) {
|
|
110
|
+
return hasCrontabEntry(readCrontab(), id);
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=crontab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crontab.js","sourceRoot":"","sources":["../../../../src/schedule/installers/crontab.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ScheduleJob } from "../types.js";
|
|
2
|
+
export declare function launchdLabel(id: string): string;
|
|
3
|
+
export declare function launchdPlistPath(id: string): string;
|
|
4
|
+
export interface LaunchdSchedule {
|
|
5
|
+
startInterval?: number;
|
|
6
|
+
startCalendarInterval?: {
|
|
7
|
+
Minute?: number;
|
|
8
|
+
Hour?: number;
|
|
9
|
+
Weekday?: number;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Translate a simple 5-field cron to a launchd schedule. Returns null for
|
|
14
|
+
* patterns launchd's StartCalendarInterval cannot cleanly express (lists,
|
|
15
|
+
* ranges, multi-step) — the caller then falls back to crontab.
|
|
16
|
+
*/
|
|
17
|
+
export declare function cronToLaunchdInterval(cron: string): LaunchdSchedule | null;
|
|
18
|
+
/** Generate the LaunchAgent plist XML. Deliberately omits KeepAlive (timer jobs must not auto-restart). */
|
|
19
|
+
export declare function generatePlist(job: ScheduleJob, sched: LaunchdSchedule, logsDir: string, stateRoot: string): string;
|
|
20
|
+
export declare function installLaunchd(job: ScheduleJob, sched: LaunchdSchedule, logsDir: string, stateRoot: string): void;
|
|
21
|
+
export declare function uninstallLaunchd(id: string): void;
|
|
22
|
+
export declare function statusLaunchd(id: string): boolean;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
export function launchdLabel(id) {
|
|
6
|
+
return `com.omp.schedule.${id}`;
|
|
7
|
+
}
|
|
8
|
+
export function launchdPlistPath(id) {
|
|
9
|
+
return join(homedir(), "Library", "LaunchAgents", `${launchdLabel(id)}.plist`);
|
|
10
|
+
}
|
|
11
|
+
const STEP_EVERY_MIN = /^\*\/(\d+)$/;
|
|
12
|
+
/**
|
|
13
|
+
* Translate a simple 5-field cron to a launchd schedule. Returns null for
|
|
14
|
+
* patterns launchd's StartCalendarInterval cannot cleanly express (lists,
|
|
15
|
+
* ranges, multi-step) — the caller then falls back to crontab.
|
|
16
|
+
*/
|
|
17
|
+
export function cronToLaunchdInterval(cron) {
|
|
18
|
+
const parts = cron.trim().split(/\s+/);
|
|
19
|
+
if (parts.length !== 5)
|
|
20
|
+
return null;
|
|
21
|
+
const [min, hour, dom, mon, dow] = parts;
|
|
22
|
+
// every N minutes: */N * * * *
|
|
23
|
+
const minStep = STEP_EVERY_MIN.exec(min);
|
|
24
|
+
if (minStep && hour === "*" && dom === "*" && mon === "*" && dow === "*") {
|
|
25
|
+
return { startInterval: Number(minStep[1]) * 60 };
|
|
26
|
+
}
|
|
27
|
+
// every N hours: 0 */N * * *
|
|
28
|
+
const hourStep = STEP_EVERY_MIN.exec(hour);
|
|
29
|
+
if (min === "0" && hourStep && dom === "*" && mon === "*" && dow === "*") {
|
|
30
|
+
return { startInterval: Number(hourStep[1]) * 3600 };
|
|
31
|
+
}
|
|
32
|
+
const isNum = (s) => /^\d+$/.test(s);
|
|
33
|
+
// daily at H:M — minute & hour numeric, date/dow wild
|
|
34
|
+
if (isNum(min) && isNum(hour) && dom === "*" && mon === "*" && dow === "*") {
|
|
35
|
+
return { startCalendarInterval: { Minute: Number(min), Hour: Number(hour) } };
|
|
36
|
+
}
|
|
37
|
+
// weekly at H:M on a single weekday
|
|
38
|
+
if (isNum(min) && isNum(hour) && dom === "*" && mon === "*" && isNum(dow)) {
|
|
39
|
+
return { startCalendarInterval: { Minute: Number(min), Hour: Number(hour), Weekday: Number(dow) } };
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function xmlEscape(s) {
|
|
44
|
+
return s
|
|
45
|
+
.replace(/&/g, "&")
|
|
46
|
+
.replace(/</g, "<")
|
|
47
|
+
.replace(/>/g, ">")
|
|
48
|
+
.replace(/"/g, """)
|
|
49
|
+
.replace(/'/g, "'");
|
|
50
|
+
}
|
|
51
|
+
function calendarXml(cal) {
|
|
52
|
+
const entries = Object.entries(cal)
|
|
53
|
+
.filter(([, v]) => v !== undefined)
|
|
54
|
+
.map(([k, v]) => ` <key>${k}</key>\n <integer>${v}</integer>`)
|
|
55
|
+
.join("\n");
|
|
56
|
+
return ` <key>StartCalendarInterval</key>\n <dict>\n${entries}\n </dict>`;
|
|
57
|
+
}
|
|
58
|
+
/** Generate the LaunchAgent plist XML. Deliberately omits KeepAlive (timer jobs must not auto-restart). */
|
|
59
|
+
export function generatePlist(job, sched, logsDir, stateRoot) {
|
|
60
|
+
const label = launchdLabel(job.id);
|
|
61
|
+
const outLog = join(logsDir, job.id, `${job.id}.launchd.out.log`);
|
|
62
|
+
const errLog = join(logsDir, job.id, `${job.id}.launchd.err.log`);
|
|
63
|
+
const scheduleXml = sched.startInterval !== undefined
|
|
64
|
+
? ` <key>StartInterval</key>\n <integer>${sched.startInterval}</integer>`
|
|
65
|
+
: calendarXml(sched.startCalendarInterval ?? {});
|
|
66
|
+
// run from stateRoot and pass --root so state resolves correctly; the agent
|
|
67
|
+
// subprocess cwd (job.cwd) is set by the runner, not by WorkingDirectory.
|
|
68
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
69
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
70
|
+
<plist version="1.0">
|
|
71
|
+
<dict>
|
|
72
|
+
<key>Label</key>
|
|
73
|
+
<string>${xmlEscape(label)}</string>
|
|
74
|
+
<key>ProgramArguments</key>
|
|
75
|
+
<array>
|
|
76
|
+
<string>${xmlEscape(job.ompBinPath)}</string>
|
|
77
|
+
<string>schedule</string>
|
|
78
|
+
<string>run</string>
|
|
79
|
+
<string>--id</string>
|
|
80
|
+
<string>${xmlEscape(job.id)}</string>
|
|
81
|
+
<string>--root</string>
|
|
82
|
+
<string>${xmlEscape(stateRoot)}</string>
|
|
83
|
+
</array>
|
|
84
|
+
<key>WorkingDirectory</key>
|
|
85
|
+
<string>${xmlEscape(stateRoot)}</string>
|
|
86
|
+
${scheduleXml}
|
|
87
|
+
<key>StandardOutPath</key>
|
|
88
|
+
<string>${xmlEscape(outLog)}</string>
|
|
89
|
+
<key>StandardErrorPath</key>
|
|
90
|
+
<string>${xmlEscape(errLog)}</string>
|
|
91
|
+
</dict>
|
|
92
|
+
</plist>
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
function gui() {
|
|
96
|
+
return `gui/${process.getuid?.() ?? 0}`;
|
|
97
|
+
}
|
|
98
|
+
export function installLaunchd(job, sched, logsDir, stateRoot) {
|
|
99
|
+
const plistPath = launchdPlistPath(job.id);
|
|
100
|
+
mkdirSync(dirname(plistPath), { recursive: true });
|
|
101
|
+
mkdirSync(join(logsDir, job.id), { recursive: true });
|
|
102
|
+
writeFileSync(plistPath, generatePlist(job, sched, logsDir, stateRoot), "utf8");
|
|
103
|
+
try {
|
|
104
|
+
execFileSync("launchctl", ["bootout", `${gui()}/${launchdLabel(job.id)}`], { stdio: "ignore" });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// not loaded yet — fine
|
|
108
|
+
}
|
|
109
|
+
execFileSync("launchctl", ["bootstrap", gui(), plistPath], { stdio: "ignore" });
|
|
110
|
+
}
|
|
111
|
+
export function uninstallLaunchd(id) {
|
|
112
|
+
try {
|
|
113
|
+
execFileSync("launchctl", ["bootout", `${gui()}/${launchdLabel(id)}`], { stdio: "ignore" });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// already gone
|
|
117
|
+
}
|
|
118
|
+
const plistPath = launchdPlistPath(id);
|
|
119
|
+
if (existsSync(plistPath))
|
|
120
|
+
unlinkSync(plistPath);
|
|
121
|
+
}
|
|
122
|
+
export function statusLaunchd(id) {
|
|
123
|
+
return existsSync(launchdPlistPath(id));
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=launchd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launchd.js","sourceRoot":"","sources":["../../../../src/schedule/installers/launchd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ScheduleJob } from "../types.js";
|
|
2
|
+
export declare function unitBaseName(id: string): string;
|
|
3
|
+
export declare function servicePath(id: string): string;
|
|
4
|
+
export declare function timerPath(id: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Translate a 5-field cron to a systemd OnCalendar expression. Covers the
|
|
7
|
+
* common patterns (every-N-min, every-N-hours, daily/weekly at H:M including
|
|
8
|
+
* day-of-week); approximates the rest with a best-effort date-time mapping.
|
|
9
|
+
*/
|
|
10
|
+
export declare function cronToSystemdCalendar(cron: string): string;
|
|
11
|
+
export declare function generateService(job: ScheduleJob, stateRoot: string): string;
|
|
12
|
+
export declare function generateTimer(job: ScheduleJob): string;
|
|
13
|
+
export declare function installSystemd(job: ScheduleJob, stateRoot: string): void;
|
|
14
|
+
export declare function uninstallSystemd(id: string): void;
|
|
15
|
+
export declare function statusSystemd(id: string): boolean;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export function unitBaseName(id) {
|
|
6
|
+
return `omp-schedule-${id}`;
|
|
7
|
+
}
|
|
8
|
+
function userUnitDir() {
|
|
9
|
+
return join(homedir(), ".config", "systemd", "user");
|
|
10
|
+
}
|
|
11
|
+
export function servicePath(id) {
|
|
12
|
+
return join(userUnitDir(), `${unitBaseName(id)}.service`);
|
|
13
|
+
}
|
|
14
|
+
export function timerPath(id) {
|
|
15
|
+
return join(userUnitDir(), `${unitBaseName(id)}.timer`);
|
|
16
|
+
}
|
|
17
|
+
const STEP_EVERY = /^\*\/(\d+)$/;
|
|
18
|
+
const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
19
|
+
function dayName(n) {
|
|
20
|
+
const v = Number(n);
|
|
21
|
+
if (!Number.isInteger(v))
|
|
22
|
+
return undefined;
|
|
23
|
+
return DAY_NAMES[v % 7]; // cron allows 0 and 7 for Sunday
|
|
24
|
+
}
|
|
25
|
+
/** Translate a cron day-of-week field to a systemd day prefix (empty when wild/unparseable). */
|
|
26
|
+
function dowToSystemd(dow) {
|
|
27
|
+
if (dow === "*")
|
|
28
|
+
return "";
|
|
29
|
+
if (/^\d+$/.test(dow))
|
|
30
|
+
return dayName(dow) ?? "";
|
|
31
|
+
const range = /^(\d+)-(\d+)$/.exec(dow);
|
|
32
|
+
if (range) {
|
|
33
|
+
const a = dayName(range[1]);
|
|
34
|
+
const b = dayName(range[2]);
|
|
35
|
+
return a && b ? `${a}..${b}` : "";
|
|
36
|
+
}
|
|
37
|
+
if (dow.includes(",")) {
|
|
38
|
+
const names = dow.split(",").map((d) => dayName(d.trim()));
|
|
39
|
+
if (names.every(Boolean))
|
|
40
|
+
return names.join(",");
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Translate a 5-field cron to a systemd OnCalendar expression. Covers the
|
|
46
|
+
* common patterns (every-N-min, every-N-hours, daily/weekly at H:M including
|
|
47
|
+
* day-of-week); approximates the rest with a best-effort date-time mapping.
|
|
48
|
+
*/
|
|
49
|
+
export function cronToSystemdCalendar(cron) {
|
|
50
|
+
const parts = cron.trim().split(/\s+/);
|
|
51
|
+
if (parts.length !== 5)
|
|
52
|
+
return cron;
|
|
53
|
+
const [min, hour, dom, mon, dow] = parts;
|
|
54
|
+
const minStep = STEP_EVERY.exec(min);
|
|
55
|
+
if (minStep && hour === "*" && dom === "*" && mon === "*" && dow === "*") {
|
|
56
|
+
return `*:0/${minStep[1]}`; // every N minutes
|
|
57
|
+
}
|
|
58
|
+
const hourStep = STEP_EVERY.exec(hour);
|
|
59
|
+
if (min === "0" && hourStep && dom === "*" && mon === "*" && dow === "*") {
|
|
60
|
+
return `0/${hourStep[1]}:00`; // every N hours
|
|
61
|
+
}
|
|
62
|
+
const pad = (s) => (/^\d+$/.test(s) ? s.padStart(2, "0") : "*");
|
|
63
|
+
const time = `${pad(hour)}:${pad(min)}:00`;
|
|
64
|
+
const dayPrefix = dowToSystemd(dow);
|
|
65
|
+
const prefix = dayPrefix ? `${dayPrefix} ` : "";
|
|
66
|
+
if (dom === "*" && mon === "*") {
|
|
67
|
+
return `${prefix}*-*-* ${time}`;
|
|
68
|
+
}
|
|
69
|
+
// best effort with date components
|
|
70
|
+
return `${prefix}*-${pad(mon)}-${pad(dom)} ${time}`;
|
|
71
|
+
}
|
|
72
|
+
/** Double-quote a value for a systemd unit (handles spaces; escapes backslash and quote). */
|
|
73
|
+
function sdq(s) {
|
|
74
|
+
return `"${s.replace(/(["\\])/g, "\\$1")}"`;
|
|
75
|
+
}
|
|
76
|
+
export function generateService(job, stateRoot) {
|
|
77
|
+
// Run from stateRoot with --root so state resolves independent of agent cwd.
|
|
78
|
+
return `[Unit]
|
|
79
|
+
Description=omp scheduled job ${job.id}
|
|
80
|
+
|
|
81
|
+
[Service]
|
|
82
|
+
Type=oneshot
|
|
83
|
+
ExecStart=${sdq(job.ompBinPath)} schedule run --id ${job.id} --root ${sdq(stateRoot)}
|
|
84
|
+
WorkingDirectory=${sdq(stateRoot)}
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
export function generateTimer(job) {
|
|
88
|
+
return `[Unit]
|
|
89
|
+
Description=Timer for omp scheduled job ${job.id}
|
|
90
|
+
|
|
91
|
+
[Timer]
|
|
92
|
+
OnCalendar=${cronToSystemdCalendar(job.cron)}
|
|
93
|
+
Persistent=true
|
|
94
|
+
|
|
95
|
+
[Install]
|
|
96
|
+
WantedBy=timers.target
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
export function installSystemd(job, stateRoot) {
|
|
100
|
+
mkdirSync(userUnitDir(), { recursive: true });
|
|
101
|
+
writeFileSync(servicePath(job.id), generateService(job, stateRoot), "utf8");
|
|
102
|
+
writeFileSync(timerPath(job.id), generateTimer(job), "utf8");
|
|
103
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
104
|
+
execFileSync("systemctl", ["--user", "enable", "--now", `${unitBaseName(job.id)}.timer`], { stdio: "ignore" });
|
|
105
|
+
}
|
|
106
|
+
export function uninstallSystemd(id) {
|
|
107
|
+
try {
|
|
108
|
+
execFileSync("systemctl", ["--user", "disable", "--now", `${unitBaseName(id)}.timer`], { stdio: "ignore" });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// already disabled
|
|
112
|
+
}
|
|
113
|
+
for (const p of [servicePath(id), timerPath(id)]) {
|
|
114
|
+
if (existsSync(p))
|
|
115
|
+
unlinkSync(p);
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// best effort
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function statusSystemd(id) {
|
|
125
|
+
try {
|
|
126
|
+
const out = execFileSync("systemctl", ["--user", "is-active", `${unitBaseName(id)}.timer`], {
|
|
127
|
+
encoding: "utf8",
|
|
128
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
129
|
+
});
|
|
130
|
+
return out.trim() === "active";
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return existsSync(timerPath(id));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=systemd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemd.js","sourceRoot":"","sources":["../../../../src/schedule/installers/systemd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ScheduleJob, ScheduleRunResult } from "./types.js";
|
|
2
|
+
/** Atomic write (tmp + rename), mirroring task-store.writeTask. */
|
|
3
|
+
export declare function writeJob(jobPath: string, job: ScheduleJob): void;
|
|
4
|
+
export declare function readJob(jobPath: string): ScheduleJob | undefined;
|
|
5
|
+
export declare function listJobs(jobsDir: string): ScheduleJob[];
|
|
6
|
+
export declare function deleteJob(jobPath: string): void;
|
|
7
|
+
/** Append a result line. Append-only — never read-modify-write (race-free under concurrent runs). */
|
|
8
|
+
export declare function appendRunResult(resultsPath: string, result: ScheduleRunResult): void;
|
|
9
|
+
/** Atomic write of the byte-offset cursor (tmp + rename). */
|
|
10
|
+
export declare function advanceCursor(cursorPath: string, bytes: number): void;
|
|
11
|
+
export interface ResultsScan {
|
|
12
|
+
results: ScheduleRunResult[];
|
|
13
|
+
newCursor: number;
|
|
14
|
+
cursor: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read result lines from the stored byte offset to EOF, parsing only complete
|
|
18
|
+
* lines. Mirrors outbox.scanFromCursor. `maxBytes` bounds the read so a hook
|
|
19
|
+
* with a tight time budget never scans an unbounded file.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readResultsFrom(resultsPath: string, cursorPath: string, maxBytes?: number): ResultsScan;
|