open-agents-ai 0.187.314 → 0.187.316
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/dist/index.js +474 -4
- package/dist/postinstall-daemon.cjs +91 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -251457,9 +251457,33 @@ function installCronJob(task, workingDir) {
|
|
|
251457
251457
|
const storeFile = resolve24(workingDir, ".oa", "scheduled", "tasks.json");
|
|
251458
251458
|
const taskEscaped = task.task.replace(/'/g, "'\\''");
|
|
251459
251459
|
const taskId = task.id;
|
|
251460
|
+
const lockDir = resolve24(workingDir, ".oa", "run");
|
|
251461
|
+
const lockPath = join37(lockDir, `${task.id}.lock`);
|
|
251460
251462
|
const wrapperCmd = [
|
|
251461
251463
|
`cd ${JSON.stringify(workingDir)}`,
|
|
251462
251464
|
`mkdir -p ${JSON.stringify(logDir)}`,
|
|
251465
|
+
`mkdir -p ${JSON.stringify(lockDir)}`,
|
|
251466
|
+
// Single-flight begin: try to acquire lock; if exists, check staleness
|
|
251467
|
+
`if mkdir ${JSON.stringify(lockPath)} 2>/dev/null; then`,
|
|
251468
|
+
` echo $$ > ${JSON.stringify(join37(lockPath, "pid"))}`,
|
|
251469
|
+
` trap 'rm -rf ${lockPath}' EXIT`,
|
|
251470
|
+
`else`,
|
|
251471
|
+
` if [ -f ${JSON.stringify(join37(lockPath, "pid"))} ]; then`,
|
|
251472
|
+
` oldpid=$(cat ${JSON.stringify(join37(lockPath, "pid"))} 2>/dev/null || echo)`,
|
|
251473
|
+
` if [ -n "$oldpid" ] && kill -0 "$oldpid" 2>/dev/null; then`,
|
|
251474
|
+
` echo "[oa-scheduler] ${taskId} already running as PID $oldpid; skipping" >> ${JSON.stringify(logFile)}`,
|
|
251475
|
+
` exit 0`,
|
|
251476
|
+
` else`,
|
|
251477
|
+
` # stale lock — remove and proceed`,
|
|
251478
|
+
` rm -rf ${JSON.stringify(lockPath)} 2>/dev/null || true`,
|
|
251479
|
+
` mkdir -p ${JSON.stringify(lockPath)} && echo $$ > ${JSON.stringify(join37(lockPath, "pid"))} && trap 'rm -rf ${lockPath}' EXIT`,
|
|
251480
|
+
` fi`,
|
|
251481
|
+
` else`,
|
|
251482
|
+
` # lock without pid file — assume stale`,
|
|
251483
|
+
` rm -rf ${JSON.stringify(lockPath)} 2>/dev/null || true`,
|
|
251484
|
+
` mkdir -p ${JSON.stringify(lockPath)} && echo $$ > ${JSON.stringify(join37(lockPath, "pid"))} && trap 'rm -rf ${lockPath}' EXIT`,
|
|
251485
|
+
` fi`,
|
|
251486
|
+
`fi`,
|
|
251463
251487
|
// Run the task and capture exit code
|
|
251464
251488
|
`${oaBin} '${taskEscaped}' >> ${JSON.stringify(logFile)} 2>&1; _oa_exit=$?`,
|
|
251465
251489
|
// Update store: increment runCount, set lastRun, handle oneShot/maxRuns
|
|
@@ -299476,6 +299500,35 @@ The session corrections MUST become hard rules in the SKILL.md Rules section.`;
|
|
|
299476
299500
|
}
|
|
299477
299501
|
return "handled";
|
|
299478
299502
|
}
|
|
299503
|
+
if (sub === "adopt" || sub === "reconcile") {
|
|
299504
|
+
try {
|
|
299505
|
+
renderInfo2("Reconciling cron → tasks.json (adopt missing entries)...");
|
|
299506
|
+
const r2 = await doFetch("/v1/scheduled/reconcile", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ apply: true }) });
|
|
299507
|
+
const d2 = await r2.json();
|
|
299508
|
+
const a2 = Array.isArray(d2.adopted) ? d2.adopted.length : 0;
|
|
299509
|
+
const m2 = Array.isArray(d2.mismatches) ? d2.mismatches.length : 0;
|
|
299510
|
+
const e2 = Array.isArray(d2.errors) ? d2.errors.length : 0;
|
|
299511
|
+
renderInfo2(`Adopted ${a2} entries${m2 ? `, ${m2} mismatches` : ""}${e2 ? `, ${e2} errors` : ""}.`);
|
|
299512
|
+
} catch (e2) {
|
|
299513
|
+
renderError2(e2?.message || String(e2));
|
|
299514
|
+
}
|
|
299515
|
+
return "handled";
|
|
299516
|
+
}
|
|
299517
|
+
if (sub === "fixup" || sub === "migrate") {
|
|
299518
|
+
const dry = tokens.includes("--dry-run") || tokens.includes("-n");
|
|
299519
|
+
try {
|
|
299520
|
+
const mode = sub === "fixup" ? "cron" : "migrate";
|
|
299521
|
+
renderInfo2(`${sub} ${dry ? "(dry-run)" : ""}...`);
|
|
299522
|
+
const r2 = await doFetch("/v1/scheduled/fixup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode, dryRun: dry }) });
|
|
299523
|
+
const d2 = await r2.json();
|
|
299524
|
+
const changed = Array.isArray(d2.changes) ? d2.changes.length : 0;
|
|
299525
|
+
const errs = Array.isArray(d2.errors) ? d2.errors.length : 0;
|
|
299526
|
+
renderInfo2(`${sub} complete: ${changed} changes${errs ? `, ${errs} errors` : ""}.`);
|
|
299527
|
+
} catch (e2) {
|
|
299528
|
+
renderError2(e2?.message || String(e2));
|
|
299529
|
+
}
|
|
299530
|
+
return "handled";
|
|
299531
|
+
}
|
|
299479
299532
|
if (sub === "list") {
|
|
299480
299533
|
try {
|
|
299481
299534
|
const r2 = await doFetch("/v1/scheduled");
|
|
@@ -320640,6 +320693,7 @@ body {
|
|
|
320640
320693
|
<div id="dashboard-health" style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap"></div>
|
|
320641
320694
|
<div id="dashboard-daemons" style="margin-bottom:16px"></div>
|
|
320642
320695
|
<div id="dashboard-scheduled" style="margin-bottom:16px"></div>
|
|
320696
|
+
<div id="dashboard-services" style="margin-bottom:16px"></div>
|
|
320643
320697
|
<div id="dashboard-usage" style="margin-bottom:16px"></div>
|
|
320644
320698
|
<h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Job History</h3>
|
|
320645
320699
|
<div id="jobs-list" style="font-size:0.78rem"></div>
|
|
@@ -321822,7 +321876,7 @@ async function loadDaemons() {
|
|
|
321822
321876
|
// Scheduled jobs panel
|
|
321823
321877
|
async function loadScheduled() {
|
|
321824
321878
|
try {
|
|
321825
|
-
const r = await fetch('/v1/scheduled', { headers: headers() });
|
|
321879
|
+
const r = await fetch('/v1/scheduled/status', { headers: headers() });
|
|
321826
321880
|
const d = await r.json();
|
|
321827
321881
|
const el = document.getElementById('dashboard-scheduled');
|
|
321828
321882
|
if (!el) return;
|
|
@@ -321837,10 +321891,13 @@ async function loadScheduled() {
|
|
|
321837
321891
|
? '<button onclick="toggleScheduled('' + t.id + '',false)" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable</button>'
|
|
321838
321892
|
: '<button onclick="toggleScheduled('' + t.id + '',true)" style="background:#2a2a30;border:1px solid #2a3a2a;color:#4ec94e;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">enable</button>';
|
|
321839
321893
|
const color = enabled ? '#4ec94e' : '#5a2a2a';
|
|
321894
|
+
const procInfo = t.procs && t.procs.length ? (' (' + t.procs.length + ' proc)') : '';
|
|
321895
|
+
const up = t.procs && t.procs[0] && t.procs[0].uptime_s ? (' • up ' + Math.max(1, Math.round(t.procs[0].uptime_s/60)) + 'm') : '';
|
|
321896
|
+
const killBtn = '<button onclick="killScheduledTask('' + t.id + '')" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">kill</button>';
|
|
321840
321897
|
const row = '<div style="background:#1e1e22;border-left:2px solid ' + color + ';padding:6px 10px;margin:4px 0;font-size:0.72rem">'
|
|
321841
|
-
+ '<div style="color:#b0b0b0">' + (t.name || '(task)') + ' <span style="color:#555">' + (t.schedule || '') + '</span
|
|
321898
|
+
+ '<div style="color:#b0b0b0">' + (t.name || '(task)') + ' <span style="color:#555">' + (t.schedule || '') + '</span>' + procInfo + up + '</div>'
|
|
321842
321899
|
+ '<div style="color:#555;font-size:0.6rem">' + t.file + '#' + t.index + '</div>'
|
|
321843
|
-
+ '<div style="margin-top:4px">' + btn + '</div>'
|
|
321900
|
+
+ '<div style="margin-top:4px;display:flex;gap:8px">' + btn + killBtn + '</div>'
|
|
321844
321901
|
+ '</div>';
|
|
321845
321902
|
return row;
|
|
321846
321903
|
}).join('');
|
|
@@ -321849,6 +321906,9 @@ async function loadScheduled() {
|
|
|
321849
321906
|
+ '<button onclick="disableAllScheduled()" title="Disable all scheduled tasks" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable all</button>'
|
|
321850
321907
|
+ '<button onclick="enableAllScheduled()" title="Enable all scheduled tasks" style="background:#2a2a30;border:1px solid #2a3a2a;color:#4ec94e;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">enable all</button>'
|
|
321851
321908
|
+ '<button onclick="killScheduled()" title="Kill OA scheduler processes" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">kill OA schedulers</button>'
|
|
321909
|
+
+ '<button onclick="adoptScheduled()" title="Adopt legacy cron jobs into tasks.json" style="background:#2a2a30;border:1px solid #2a2a5a;color:#6e7bd9;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">adopt</button>'
|
|
321910
|
+
+ '<button onclick="fixupScheduled()" title="Rewrite cron entries to canonical form" style="background:#2a2a30;border:1px solid #5a2a2a;color:#dba15f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">fixup</button>'
|
|
321911
|
+
+ '<button onclick="migrateScheduled()" title="Migrate cron → systemd user timers" style="background:#2a2a30;border:1px solid #5a2a2a;color:#a1db5f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">migrate</button>'
|
|
321852
321912
|
+ '</div>';
|
|
321853
321913
|
} catch {}
|
|
321854
321914
|
}
|
|
@@ -321916,6 +321976,110 @@ No remaining matched processes.';
|
|
|
321916
321976
|
} catch {}
|
|
321917
321977
|
}
|
|
321918
321978
|
|
|
321979
|
+
(window as any).adoptScheduled = async function() {
|
|
321980
|
+
try {
|
|
321981
|
+
const r = await fetch('/v1/scheduled/reconcile', { method:'POST', headers: headers(), body: JSON.stringify({ apply: true }) });
|
|
321982
|
+
const j = await r.json();
|
|
321983
|
+
const a = Array.isArray(j.adopted) ? j.adopted.length : 0;
|
|
321984
|
+
const m = Array.isArray(j.mismatches) ? j.mismatches.length : 0;
|
|
321985
|
+
const e = Array.isArray(j.errors) ? j.errors.length : 0;
|
|
321986
|
+
alert('Adopted ' + a + ' entries' + (m ? (', ' + m + ' mismatches') : '') + (e ? (', ' + e + ' errors') : ''));
|
|
321987
|
+
loadScheduled();
|
|
321988
|
+
} catch (e) { alert('Adopt failed: ' + (e && e.message || String(e))); }
|
|
321989
|
+
}
|
|
321990
|
+
|
|
321991
|
+
(window as any).fixupScheduled = async function() {
|
|
321992
|
+
try {
|
|
321993
|
+
if (!confirm('Rewrite OA cron entries to canonical launcher?')) return;
|
|
321994
|
+
const r = await fetch('/v1/scheduled/fixup', { method:'POST', headers: headers(), body: JSON.stringify({ mode: 'cron', dryRun: false }) });
|
|
321995
|
+
const j = await r.json();
|
|
321996
|
+
alert('Fixup: ' + (Array.isArray(j.changes)?j.changes.length:0) + ' changes, ' + (Array.isArray(j.errors)?j.errors.length:0) + ' errors');
|
|
321997
|
+
loadScheduled();
|
|
321998
|
+
} catch (e) { alert('Fixup failed: ' + (e && e.message || String(e))); }
|
|
321999
|
+
}
|
|
322000
|
+
|
|
322001
|
+
(window as any).migrateScheduled = async function() {
|
|
322002
|
+
try {
|
|
322003
|
+
if (!confirm('Migrate OA cron entries to systemd user timers?')) return;
|
|
322004
|
+
const r = await fetch('/v1/scheduled/fixup', { method:'POST', headers: headers(), body: JSON.stringify({ mode: 'migrate', dryRun: false }) });
|
|
322005
|
+
const j = await r.json();
|
|
322006
|
+
alert('Migrate: ' + (Array.isArray(j.changes)?j.changes.length:0) + ' changes, ' + (Array.isArray(j.errors)?j.errors.length:0) + ' errors');
|
|
322007
|
+
loadScheduled();
|
|
322008
|
+
} catch (e) { alert('Migrate failed: ' + (e && e.message || String(e))); }
|
|
322009
|
+
}
|
|
322010
|
+
|
|
322011
|
+
(window as any).killScheduledTask = async function(id) {
|
|
322012
|
+
try {
|
|
322013
|
+
// Fetch task to derive a pattern (directory of tasks.json)
|
|
322014
|
+
const r = await fetch('/v1/scheduled/status', { headers: headers() });
|
|
322015
|
+
const d = await r.json();
|
|
322016
|
+
const tasks = Array.isArray(d.tasks) ? d.tasks : [];
|
|
322017
|
+
const t = tasks.find(x => x.id === id);
|
|
322018
|
+
if (!t) { alert('Task not found'); return; }
|
|
322019
|
+
const dir = (t.file || '').split('/').slice(0, -1).join('/') || t.file;
|
|
322020
|
+
// Escape for regex without using a character class that includes brace/dollar combo (parser quirk)
|
|
322021
|
+
const safe = dir
|
|
322022
|
+
.replace(/\\/g, "\\\\")
|
|
322023
|
+
.replace(/[/g, "\\[")
|
|
322024
|
+
.replace(/]/g, "\\]")
|
|
322025
|
+
.replace(/{/g, "\\{")
|
|
322026
|
+
.replace(/}/g, "\\}")
|
|
322027
|
+
.replace(/(/g, "\\(")
|
|
322028
|
+
.replace(/)/g, "\\)")
|
|
322029
|
+
.replace(/*/g, "\\*")
|
|
322030
|
+
.replace(/+/g, "\\+")
|
|
322031
|
+
.replace(/?/g, "\\?")
|
|
322032
|
+
.replace(/^/g, "\\^")
|
|
322033
|
+
.replace(/$/g, "\\$")
|
|
322034
|
+
.replace(/|/g, "\\|")
|
|
322035
|
+
.replace(/./g, "\\.");
|
|
322036
|
+
const body = { pattern: safe };
|
|
322037
|
+
const resp = await fetch('/v1/scheduled/kill', { method:'POST', headers: headers(), body: JSON.stringify(body) });
|
|
322038
|
+
const j = await resp.json();
|
|
322039
|
+
const kb = Array.isArray(j.killed) ? j.killed.length : 0;
|
|
322040
|
+
const ka = Array.isArray(j.additionally) ? j.additionally.length : 0;
|
|
322041
|
+
const before = j.gpu_before && j.gpu_before[0] ? j.gpu_before[0] : null;
|
|
322042
|
+
const after = j.gpu_after && j.gpu_after[0] ? j.gpu_after[0] : null;
|
|
322043
|
+
const rem = Array.isArray(j.procs_after) ? j.procs_after.length : 0;
|
|
322044
|
+
let msg = 'Killed ' + (kb + ka) + ' processes for task.';
|
|
322045
|
+
if (before && after) msg += '
|
|
322046
|
+
GPU util: ' + before.gpu_pct + '% → ' + after.gpu_pct + '%';
|
|
322047
|
+
msg += rem > 0 ? ('
|
|
322048
|
+
Remaining matched processes: ' + rem) : '
|
|
322049
|
+
No remaining matched processes.';
|
|
322050
|
+
alert(msg);
|
|
322051
|
+
loadScheduled();
|
|
322052
|
+
} catch (e) { alert('Kill failed: ' + (e && e.message || String(e))); }
|
|
322053
|
+
}
|
|
322054
|
+
|
|
322055
|
+
async function loadServices() {
|
|
322056
|
+
try {
|
|
322057
|
+
const r = await fetch('/v1/services/systemd', { headers: headers() });
|
|
322058
|
+
const d = await r.json();
|
|
322059
|
+
const el = document.getElementById('dashboard-services');
|
|
322060
|
+
if (!el) return;
|
|
322061
|
+
const svcs = Array.isArray(d.services) ? d.services : [];
|
|
322062
|
+
if (!svcs.length) { el.innerHTML = ''; return; }
|
|
322063
|
+
const rows = svcs.map(s => {
|
|
322064
|
+
const stopBtn = '<button onclick="svcAction('' + s.name + '','stop')" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">stop</button>';
|
|
322065
|
+
const disBtn = '<button onclick="svcAction('' + s.name + '','disable')" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable</button>';
|
|
322066
|
+
return '<div style="background:#1e1e22;border-left:2px solid #3a3a42;padding:6px 10px;margin:4px 0;font-size:0.72rem">'
|
|
322067
|
+
+ '<div style="color:#b0b0b0">' + s.name + '</div>'
|
|
322068
|
+
+ '<div style="color:#555;font-size:0.6rem">enabled: ' + s.enabled + ' • active: ' + s.active + '</div>'
|
|
322069
|
+
+ '<div style="margin-top:4px;display:flex;gap:8px">' + stopBtn + disBtn + '</div>'
|
|
322070
|
+
+ '</div>';
|
|
322071
|
+
}).join('');
|
|
322072
|
+
el.innerHTML = '<h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Services (systemd --user)</h3>' + rows;
|
|
322073
|
+
} catch {}
|
|
322074
|
+
}
|
|
322075
|
+
|
|
322076
|
+
(window as any).svcAction = async function(name, action) {
|
|
322077
|
+
try {
|
|
322078
|
+
await fetch('/v1/services/systemd/' + encodeURIComponent(name), { method:'POST', headers: headers(), body: JSON.stringify({ action }) });
|
|
322079
|
+
loadServices();
|
|
322080
|
+
} catch (e) { alert('Service action failed: ' + (e && e.message || String(e))); }
|
|
322081
|
+
}
|
|
322082
|
+
|
|
321919
322083
|
// Agent task
|
|
321920
322084
|
let currentRunId = null;
|
|
321921
322085
|
async function loadProfiles() {
|
|
@@ -323046,6 +323210,7 @@ async function doUpdate() {
|
|
|
323046
323210
|
try { checkHealth(); } catch {}
|
|
323047
323211
|
try { pollMetrics(); } catch {}
|
|
323048
323212
|
try { loadScheduled(); } catch {}
|
|
323213
|
+
try { loadServices(); } catch {}
|
|
323049
323214
|
|
|
323050
323215
|
btn.textContent = 'updated v' + newVersion;
|
|
323051
323216
|
btn.style.background = '#1a3a1a';
|
|
@@ -323680,6 +323845,7 @@ restoreChatSession(); // WO-CHAT-RESUME — rehydrate from server state
|
|
|
323680
323845
|
setInterval(checkHealth, 30000);
|
|
323681
323846
|
setInterval(pollMetrics, 10000);
|
|
323682
323847
|
setInterval(loadScheduled, 15000);
|
|
323848
|
+
setInterval(loadServices, 30000);
|
|
323683
323849
|
setInterval(pollVersionBump, 5000);
|
|
323684
323850
|
input.focus();
|
|
323685
323851
|
</script>
|
|
@@ -327682,6 +327848,37 @@ async function handleRequest(req2, res, ollamaUrl, verbose) {
|
|
|
327682
327848
|
});
|
|
327683
327849
|
return;
|
|
327684
327850
|
}
|
|
327851
|
+
if (pathname === "/v1/scheduled/reconcile" && (method === "GET" || method === "POST")) {
|
|
327852
|
+
const body = method === "POST" ? await parseJsonBody(req2) : {};
|
|
327853
|
+
const apply = Boolean(body?.apply);
|
|
327854
|
+
const result = reconcileScheduledTasks(apply);
|
|
327855
|
+
jsonResponse(res, 200, { applied: apply, ...result });
|
|
327856
|
+
return;
|
|
327857
|
+
}
|
|
327858
|
+
if (pathname === "/v1/scheduled/fixup" && method === "POST") {
|
|
327859
|
+
const body = await parseJsonBody(req2);
|
|
327860
|
+
const mode = String(body?.mode || "cron");
|
|
327861
|
+
const dryRun = Boolean(body?.dryRun);
|
|
327862
|
+
const result = fixupOrMigrateScheduled(mode, dryRun);
|
|
327863
|
+
jsonResponse(res, 200, { mode, dryRun, ...result });
|
|
327864
|
+
return;
|
|
327865
|
+
}
|
|
327866
|
+
if (pathname === "/v1/services/systemd" && method === "GET") {
|
|
327867
|
+
jsonResponse(res, 200, { services: listUserServices(/oa|open|agent|nexus|run-/i) });
|
|
327868
|
+
return;
|
|
327869
|
+
}
|
|
327870
|
+
if (pathname?.startsWith("/v1/services/systemd/") && method === "POST") {
|
|
327871
|
+
const unit = pathname.split("/")[4] || "";
|
|
327872
|
+
const body = await parseJsonBody(req2);
|
|
327873
|
+
const action = String(body?.action || "").toLowerCase();
|
|
327874
|
+
if (!unit || !action) {
|
|
327875
|
+
jsonResponse(res, 400, { error: "Missing unit or action" });
|
|
327876
|
+
return;
|
|
327877
|
+
}
|
|
327878
|
+
const ok2 = userServiceAction(unit, action);
|
|
327879
|
+
jsonResponse(res, ok2 ? 200 : 500, ok2 ? { unit, action, status: "ok" } : { error: "action failed", unit, action });
|
|
327880
|
+
return;
|
|
327881
|
+
}
|
|
327685
327882
|
if ((pathname === "/v1/chat" || pathname === "/api/chat") && method === "POST") {
|
|
327686
327883
|
if (!checkAuth(req2, res, "run")) {
|
|
327687
327884
|
status = 401;
|
|
@@ -328724,6 +328921,278 @@ function sampleGpuUtil() {
|
|
|
328724
328921
|
return null;
|
|
328725
328922
|
}
|
|
328726
328923
|
}
|
|
328924
|
+
function getCurrentCrontabLines() {
|
|
328925
|
+
try {
|
|
328926
|
+
const { execSync: es } = __require("node:child_process");
|
|
328927
|
+
return es("crontab -l 2>/dev/null", { encoding: "utf8", stdio: "pipe" }).split("\n");
|
|
328928
|
+
} catch {
|
|
328929
|
+
return [];
|
|
328930
|
+
}
|
|
328931
|
+
}
|
|
328932
|
+
function detectLegacyCron() {
|
|
328933
|
+
const lines = getCurrentCrontabLines();
|
|
328934
|
+
const out = [];
|
|
328935
|
+
for (const l2 of lines) {
|
|
328936
|
+
if (!l2.includes(CRON_MARKER2)) continue;
|
|
328937
|
+
const idMatch = l2.match(new RegExp(`${CRON_MARKER2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\S+)`));
|
|
328938
|
+
const id = idMatch?.[1] || "unknown";
|
|
328939
|
+
const parts = l2.trim().split(/\s+/);
|
|
328940
|
+
if (parts.length < 6) continue;
|
|
328941
|
+
const cron = parts.slice(0, 5).join(" ");
|
|
328942
|
+
let workingDir = null;
|
|
328943
|
+
const cdMatch = l2.match(/\bcd\s+(["'])([^"']+)\1/);
|
|
328944
|
+
if (cdMatch) workingDir = cdMatch[2];
|
|
328945
|
+
let task = null;
|
|
328946
|
+
try {
|
|
328947
|
+
const afterCd = l2.split(/\bcd\s+["'][^"']+["']/)[1] || l2;
|
|
328948
|
+
const oaIdx = afterCd.search(/\boa\b|open-agents-ai/);
|
|
328949
|
+
if (oaIdx >= 0) {
|
|
328950
|
+
const tail = afterCd.slice(oaIdx);
|
|
328951
|
+
const segMatch = tail.match(/['"][\s\S]*?>>/);
|
|
328952
|
+
if (segMatch) {
|
|
328953
|
+
let seg = segMatch[0];
|
|
328954
|
+
seg = seg.replace(/>>[\s\S]*/, "");
|
|
328955
|
+
seg = seg.replace(/'\\''/g, "\0");
|
|
328956
|
+
seg = seg.replace(/['"]/g, "");
|
|
328957
|
+
seg = seg.replace(/\u0000/g, "'");
|
|
328958
|
+
task = seg;
|
|
328959
|
+
}
|
|
328960
|
+
}
|
|
328961
|
+
} catch {
|
|
328962
|
+
}
|
|
328963
|
+
out.push({ id, cron, workingDir, task, line: l2 });
|
|
328964
|
+
}
|
|
328965
|
+
return out;
|
|
328966
|
+
}
|
|
328967
|
+
function reconcileScheduledTasks(apply) {
|
|
328968
|
+
const found = detectLegacyCron();
|
|
328969
|
+
const adopted = [];
|
|
328970
|
+
const mismatches = [];
|
|
328971
|
+
const errors = [];
|
|
328972
|
+
for (const f2 of found) {
|
|
328973
|
+
const wdir = f2.workingDir || process.cwd();
|
|
328974
|
+
const file = join99(wdir, ".oa", "scheduled", "tasks.json");
|
|
328975
|
+
try {
|
|
328976
|
+
let json = { tasks: [] };
|
|
328977
|
+
try {
|
|
328978
|
+
const raw = readFileSync65(file, "utf-8");
|
|
328979
|
+
json = JSON.parse(raw);
|
|
328980
|
+
} catch {
|
|
328981
|
+
}
|
|
328982
|
+
const arr = Array.isArray(json?.tasks) ? json.tasks : Array.isArray(json) ? json : [];
|
|
328983
|
+
const exists2 = arr.some((t2) => String(t2?.schedule || t2?.cron || "") === f2.cron && String(t2?.task || t2?.name || t2?.command || "") === (f2.task || ""));
|
|
328984
|
+
if (!exists2) {
|
|
328985
|
+
if (apply) {
|
|
328986
|
+
const entry = { task: f2.task || `legacy ${f2.id}`, schedule: f2.cron, enabled: true };
|
|
328987
|
+
arr.push(entry);
|
|
328988
|
+
const toWrite = Array.isArray(json?.tasks) ? { ...json, tasks: arr } : Array.isArray(json) ? arr : { tasks: arr };
|
|
328989
|
+
mkdirSync51(join99(wdir, ".oa", "scheduled"), { recursive: true });
|
|
328990
|
+
mkdirSync51(join99(wdir, ".oa", "scheduled", "logs"), { recursive: true });
|
|
328991
|
+
writeFileSync45(file, JSON.stringify(toWrite, null, 2));
|
|
328992
|
+
adopted.push({ file, index: arr.length - 1 });
|
|
328993
|
+
}
|
|
328994
|
+
} else {
|
|
328995
|
+
const idx = arr.findIndex((t2) => String(t2?.schedule || t2?.cron || "") === f2.cron && String(t2?.task || t2?.name || t2?.command || "") === (f2.task || ""));
|
|
328996
|
+
if (idx >= 0) {
|
|
328997
|
+
const en = typeof arr[idx].enabled === "boolean" ? arr[idx].enabled : true;
|
|
328998
|
+
if (!en) mismatches.push({ file, id: f2.id, reason: "cron present but task disabled in tasks.json" });
|
|
328999
|
+
}
|
|
329000
|
+
}
|
|
329001
|
+
} catch (e2) {
|
|
329002
|
+
errors.push({ line: f2.line, error: e2?.message || String(e2) });
|
|
329003
|
+
}
|
|
329004
|
+
}
|
|
329005
|
+
return { found: found.map(({ id, cron, workingDir, task }) => ({ id, cron, workingDir, task })), adopted, mismatches, errors };
|
|
329006
|
+
}
|
|
329007
|
+
function findOaBinary4() {
|
|
329008
|
+
try {
|
|
329009
|
+
const { execSync: es } = __require("node:child_process");
|
|
329010
|
+
for (const cmd of ["oa", "open-agents"]) {
|
|
329011
|
+
try {
|
|
329012
|
+
const p2 = es(`which ${cmd} 2>/dev/null`, { encoding: "utf8", stdio: "pipe" }).trim();
|
|
329013
|
+
if (p2) return p2;
|
|
329014
|
+
} catch {
|
|
329015
|
+
}
|
|
329016
|
+
}
|
|
329017
|
+
} catch {
|
|
329018
|
+
}
|
|
329019
|
+
return "npx open-agents-ai";
|
|
329020
|
+
}
|
|
329021
|
+
function getCrontabLines() {
|
|
329022
|
+
return getCurrentCrontabLines();
|
|
329023
|
+
}
|
|
329024
|
+
function writeCrontabLines(lines) {
|
|
329025
|
+
try {
|
|
329026
|
+
const { spawnSync: spawnSync6 } = __require("node:child_process");
|
|
329027
|
+
const result = spawnSync6("crontab", ["-"], { input: lines.join("\n") + "\n", encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
329028
|
+
if (result.status !== 0) throw new Error(`crontab exited ${result.status}: ${String(result.stderr || "").trim()}`);
|
|
329029
|
+
} catch (e2) {
|
|
329030
|
+
throw new Error(`writeCrontab failed: ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
329031
|
+
}
|
|
329032
|
+
}
|
|
329033
|
+
function canonicalCronLine(rec) {
|
|
329034
|
+
const oaBin = findOaBinary4();
|
|
329035
|
+
const logDir = join99(rec.workingDir, ".oa", "scheduled", "logs");
|
|
329036
|
+
const logFile = join99(logDir, `${rec.id}.log`);
|
|
329037
|
+
const storeFile = join99(rec.workingDir, ".oa", "scheduled", "tasks.json");
|
|
329038
|
+
const taskEsc = rec.task.replace(/'/g, "'\\''");
|
|
329039
|
+
const lockDir = join99(rec.workingDir, ".oa", "run");
|
|
329040
|
+
const lockPath = join99(lockDir, `${rec.id}.lock`);
|
|
329041
|
+
const wrapper = [
|
|
329042
|
+
`cd ${JSON.stringify(rec.workingDir)}`,
|
|
329043
|
+
`mkdir -p ${JSON.stringify(logDir)}`,
|
|
329044
|
+
`mkdir -p ${JSON.stringify(lockDir)}`,
|
|
329045
|
+
`if mkdir ${JSON.stringify(lockPath)} 2>/dev/null; then`,
|
|
329046
|
+
` echo $$ > ${JSON.stringify(join99(lockPath, "pid"))}`,
|
|
329047
|
+
` trap 'rm -rf ${lockPath}' EXIT`,
|
|
329048
|
+
`else`,
|
|
329049
|
+
` if [ -f ${JSON.stringify(join99(lockPath, "pid"))} ]; then`,
|
|
329050
|
+
` oldpid=$(cat ${JSON.stringify(join99(lockPath, "pid"))} 2>/dev/null || echo)`,
|
|
329051
|
+
` if [ -n "$oldpid" ] && kill -0 "$oldpid" 2>/dev/null; then`,
|
|
329052
|
+
` echo "[oa-scheduler] ${rec.id} already running as PID $oldpid; skipping" >> ${JSON.stringify(logFile)}`,
|
|
329053
|
+
` exit 0`,
|
|
329054
|
+
` else`,
|
|
329055
|
+
` rm -rf ${JSON.stringify(lockPath)} 2>/dev/null || true`,
|
|
329056
|
+
` mkdir -p ${JSON.stringify(lockPath)} && echo $$ > ${JSON.stringify(join99(lockPath, "pid"))} && trap 'rm -rf ${lockPath}' EXIT`,
|
|
329057
|
+
` fi`,
|
|
329058
|
+
` else`,
|
|
329059
|
+
` rm -rf ${JSON.stringify(lockPath)} 2>/dev/null || true`,
|
|
329060
|
+
` mkdir -p ${JSON.stringify(lockPath)} && echo $$ > ${JSON.stringify(join99(lockPath, "pid"))} && trap 'rm -rf ${lockPath}' EXIT`,
|
|
329061
|
+
` fi`,
|
|
329062
|
+
`fi`,
|
|
329063
|
+
`${oaBin} '${taskEsc}' >> ${JSON.stringify(logFile)} 2>&1; _oa_exit=$?`,
|
|
329064
|
+
`node -e "const fs=require('fs'),p=${JSON.stringify(storeFile)};try{const s=JSON.parse(fs.readFileSync(p,'utf8'));const t=s.tasks.find(x=>x.id==='${rec.id}');if(t){t.runCount=(t.runCount||0)+1;t.lastRun=new Date().toISOString();if(t.oneShot)t.enabled=false;if(t.maxRuns&&t.runCount>=t.maxRuns)t.enabled=false;s.updatedAt=new Date().toISOString();fs.writeFileSync(p,JSON.stringify(s,null,2));}}catch(e){}" 2>/dev/null`
|
|
329065
|
+
].join("; ");
|
|
329066
|
+
return `${rec.cron} ${wrapper} ${CRON_MARKER2}${rec.id}`;
|
|
329067
|
+
}
|
|
329068
|
+
function fixupOrMigrateScheduled(mode, dryRun) {
|
|
329069
|
+
const found = detectLegacyCron();
|
|
329070
|
+
const changes = [];
|
|
329071
|
+
const errors = [];
|
|
329072
|
+
if (mode === "cron") {
|
|
329073
|
+
try {
|
|
329074
|
+
const lines = getCrontabLines();
|
|
329075
|
+
let next = lines.filter((l2) => !l2.includes(CRON_MARKER2));
|
|
329076
|
+
for (const f2 of found) {
|
|
329077
|
+
if (!f2.workingDir || !f2.task) continue;
|
|
329078
|
+
const rec = { id: f2.id, cron: f2.cron, workingDir: f2.workingDir, task: f2.task };
|
|
329079
|
+
const newline = canonicalCronLine(rec);
|
|
329080
|
+
next.push(newline);
|
|
329081
|
+
changes.push({ id: f2.id, action: dryRun ? "would-rewrite" : "rewrite", detail: "cron line canonicalized" });
|
|
329082
|
+
}
|
|
329083
|
+
if (!dryRun) writeCrontabLines(next);
|
|
329084
|
+
} catch (e2) {
|
|
329085
|
+
errors.push({ error: e2?.message || String(e2) });
|
|
329086
|
+
}
|
|
329087
|
+
} else if (mode === "migrate" || mode === "systemd") {
|
|
329088
|
+
for (const f2 of found) {
|
|
329089
|
+
try {
|
|
329090
|
+
if (!f2.workingDir || !f2.task) continue;
|
|
329091
|
+
const unitBase = `oa-${f2.id}`;
|
|
329092
|
+
const unitDir = join99(homedir38(), ".config", "systemd", "user");
|
|
329093
|
+
const svc = join99(unitDir, `${unitBase}.service`);
|
|
329094
|
+
const tim = join99(unitDir, `${unitBase}.timer`);
|
|
329095
|
+
const oaBin = findOaBinary4();
|
|
329096
|
+
const rec = { id: f2.id, cron: f2.cron, workingDir: f2.workingDir, task: f2.task };
|
|
329097
|
+
const cmd = canonicalCronLine(rec).split(" ").slice(5).join(" ");
|
|
329098
|
+
const execCmd = `/bin/sh -lc ${JSON.stringify(cmd.replace(/\s+${CRON_MARKER}.+$/, "").trim())}`;
|
|
329099
|
+
const svcText = `[Unit]
|
|
329100
|
+
Description=Open Agents Scheduled Task ${f2.id}
|
|
329101
|
+
After=default.target
|
|
329102
|
+
|
|
329103
|
+
[Service]
|
|
329104
|
+
Type=oneshot
|
|
329105
|
+
ExecStart=${execCmd}
|
|
329106
|
+
WorkingDirectory=${f2.workingDir}
|
|
329107
|
+
Environment=OA_DAEMON=1
|
|
329108
|
+
|
|
329109
|
+
[Install]
|
|
329110
|
+
WantedBy=default.target
|
|
329111
|
+
`;
|
|
329112
|
+
const onCal = f2.cron.trim() === "* * * * *" ? "*:*:00" : f2.cron.trim().startsWith("*/") ? `*:0/${f2.cron.trim().split(/\s+/)[0].slice(2)}:00` : "*-*-* *:*:00";
|
|
329113
|
+
const timText = `[Unit]
|
|
329114
|
+
Description=Timer for ${unitBase}
|
|
329115
|
+
|
|
329116
|
+
[Timer]
|
|
329117
|
+
OnCalendar=${onCal}
|
|
329118
|
+
Persistent=true
|
|
329119
|
+
|
|
329120
|
+
[Install]
|
|
329121
|
+
WantedBy=timers.target
|
|
329122
|
+
`;
|
|
329123
|
+
if (!dryRun) {
|
|
329124
|
+
mkdirSync51(unitDir, { recursive: true });
|
|
329125
|
+
writeFileSync45(svc, svcText);
|
|
329126
|
+
writeFileSync45(tim, timText);
|
|
329127
|
+
try {
|
|
329128
|
+
const { execSync: es } = __require("node:child_process");
|
|
329129
|
+
es("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
329130
|
+
es(`systemctl --user enable --now ${unitBase}.timer`, { stdio: "pipe" });
|
|
329131
|
+
} catch {
|
|
329132
|
+
}
|
|
329133
|
+
}
|
|
329134
|
+
changes.push({ id: f2.id, action: dryRun ? "would-create-timer" : "create-timer", detail: `${unitBase}.timer` });
|
|
329135
|
+
} catch (e2) {
|
|
329136
|
+
errors.push({ id: f2.id, error: e2?.message || String(e2) });
|
|
329137
|
+
}
|
|
329138
|
+
}
|
|
329139
|
+
if (mode === "migrate") {
|
|
329140
|
+
try {
|
|
329141
|
+
const lines = getCrontabLines();
|
|
329142
|
+
const next = lines.filter((l2) => !l2.includes(CRON_MARKER2));
|
|
329143
|
+
if (!dryRun) writeCrontabLines(next);
|
|
329144
|
+
changes.push({ id: "*", action: dryRun ? "would-remove-cron" : "remove-cron", detail: "removed all OA cron entries" });
|
|
329145
|
+
} catch (e2) {
|
|
329146
|
+
errors.push({ error: e2?.message || String(e2) });
|
|
329147
|
+
}
|
|
329148
|
+
}
|
|
329149
|
+
}
|
|
329150
|
+
return { changes, errors };
|
|
329151
|
+
}
|
|
329152
|
+
function listUserServices(pattern) {
|
|
329153
|
+
const out = [];
|
|
329154
|
+
try {
|
|
329155
|
+
const { execSync: es } = __require("node:child_process");
|
|
329156
|
+
const files = es("systemctl --user list-unit-files --type=service --no-legend", { encoding: "utf8", stdio: "pipe" }).trim().split("\n");
|
|
329157
|
+
const enabledMap = /* @__PURE__ */ new Map();
|
|
329158
|
+
for (const line of files) {
|
|
329159
|
+
const m2 = line.trim().match(/^(\S+)\s+(\S+)/);
|
|
329160
|
+
if (!m2) continue;
|
|
329161
|
+
const name11 = m2[1], state = m2[2];
|
|
329162
|
+
if (pattern.test(name11)) enabledMap.set(name11, state);
|
|
329163
|
+
}
|
|
329164
|
+
const units = es("systemctl --user list-units --type=service --all --no-legend", { encoding: "utf8", stdio: "pipe" }).trim().split("\n");
|
|
329165
|
+
const activeMap = /* @__PURE__ */ new Map();
|
|
329166
|
+
for (const line of units) {
|
|
329167
|
+
const m2 = line.trim().match(/^(\S+)\s+\S+\s+\S+\s+(\S+)/);
|
|
329168
|
+
if (!m2) continue;
|
|
329169
|
+
const name11 = m2[1], state = m2[2];
|
|
329170
|
+
if (pattern.test(name11)) activeMap.set(name11, state);
|
|
329171
|
+
}
|
|
329172
|
+
for (const [name11, en] of enabledMap.entries()) {
|
|
329173
|
+
out.push({ name: name11, enabled: en, active: activeMap.get(name11) || "inactive" });
|
|
329174
|
+
}
|
|
329175
|
+
} catch {
|
|
329176
|
+
}
|
|
329177
|
+
return out;
|
|
329178
|
+
}
|
|
329179
|
+
function userServiceAction(unit, action) {
|
|
329180
|
+
try {
|
|
329181
|
+
const { execSync: es } = __require("node:child_process");
|
|
329182
|
+
if (action === "stop") {
|
|
329183
|
+
es(`systemctl --user stop ${unit}`, { stdio: "pipe" });
|
|
329184
|
+
return true;
|
|
329185
|
+
} else if (action === "disable") {
|
|
329186
|
+
es(`systemctl --user disable ${unit}`, { stdio: "pipe" });
|
|
329187
|
+
return true;
|
|
329188
|
+
} else if (action === "restart") {
|
|
329189
|
+
es(`systemctl --user restart ${unit}`, { stdio: "pipe" });
|
|
329190
|
+
return true;
|
|
329191
|
+
}
|
|
329192
|
+
} catch {
|
|
329193
|
+
}
|
|
329194
|
+
return false;
|
|
329195
|
+
}
|
|
328727
329196
|
function startApiServer(options2 = {}) {
|
|
328728
329197
|
if (options2.quiet) setQuiet(true);
|
|
328729
329198
|
const log22 = options2.quiet ? (_msg) => {
|
|
@@ -329271,7 +329740,7 @@ async function apiServeCommand(opts, config) {
|
|
|
329271
329740
|
server.on("close", resolve40);
|
|
329272
329741
|
});
|
|
329273
329742
|
}
|
|
329274
|
-
var endpointRegistry, modelRouteMap, endpointUsage, metrics, startedAt, _corsOrigins, _corsLocalOnly, runningProcesses, perKeyUsage;
|
|
329743
|
+
var endpointRegistry, modelRouteMap, endpointUsage, metrics, startedAt, _corsOrigins, _corsLocalOnly, runningProcesses, perKeyUsage, CRON_MARKER2;
|
|
329275
329744
|
var init_serve = __esm({
|
|
329276
329745
|
"packages/cli/src/api/serve.ts"() {
|
|
329277
329746
|
"use strict";
|
|
@@ -329308,6 +329777,7 @@ var init_serve = __esm({
|
|
|
329308
329777
|
_corsLocalOnly = _corsOrigins.length === 0;
|
|
329309
329778
|
runningProcesses = /* @__PURE__ */ new Map();
|
|
329310
329779
|
perKeyUsage = /* @__PURE__ */ new Map();
|
|
329780
|
+
CRON_MARKER2 = "# OPEN-AGENTS-SCHEDULED:";
|
|
329311
329781
|
}
|
|
329312
329782
|
});
|
|
329313
329783
|
|
|
@@ -1,4 +1,93 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Postinstall hook
|
|
3
|
-
|
|
2
|
+
// Postinstall hook — auto-migrate simple OA cron jobs to systemd --user timers
|
|
3
|
+
// Safe, best-effort, idempotent. Skips if systemd --user is unavailable.
|
|
4
|
+
|
|
5
|
+
const { execSync } = require('node:child_process');
|
|
6
|
+
const { mkdirSync, writeFileSync } = require('node:fs');
|
|
7
|
+
const { join } = require('node:path');
|
|
8
|
+
|
|
9
|
+
function hasSystemdUser() {
|
|
10
|
+
try { execSync('systemctl --user --version', { stdio: 'pipe' }); return true; } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readCrontab() {
|
|
14
|
+
try { return execSync('crontab -l 2>/dev/null', { encoding: 'utf8', stdio: 'pipe' }).split('\n'); } catch { return []; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeCrontab(lines) {
|
|
18
|
+
try {
|
|
19
|
+
const { spawnSync } = require('node:child_process');
|
|
20
|
+
const r = spawnSync('crontab', ['-'], { input: lines.join('\n') + '\n', encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
|
|
21
|
+
return r.status === 0;
|
|
22
|
+
} catch { return false; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseCron(line) {
|
|
26
|
+
const m = line.trim().match(/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/);
|
|
27
|
+
if (!m) return null;
|
|
28
|
+
const [ , min, hour, dom, mon, dow, rest ] = m;
|
|
29
|
+
return { min, hour, dom, mon, dow, rest };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isOaCron(line) { return /#\s*OPEN-AGENTS-SCHEDULED:/.test(line); }
|
|
4
33
|
|
|
34
|
+
function extractId(line) {
|
|
35
|
+
const m = line.match(/OPEN-AGENTS-SCHEDULED:(\S+)/); return m ? m[1] : 'unknown';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractWorkingDir(line) {
|
|
39
|
+
const m = line.match(/\bcd\s+(["'])([^"']+)\1/); return m ? m[2] : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripMarker(cmd) { return cmd.replace(/\s+#\s*OPEN-AGENTS-SCHEDULED:\S+.*/, ''); }
|
|
43
|
+
|
|
44
|
+
function timerSpecFromCron(min, hour, dom, mon, dow) {
|
|
45
|
+
// Support only */N minute and * * * * * → map to OnUnitActiveSec. Others return null (skip)
|
|
46
|
+
if (min === '*' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return { everySec: 60 };
|
|
47
|
+
const step = (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') ? parseInt(min.slice(2), 10) : NaN;
|
|
48
|
+
if (!Number.isNaN(step) && step > 0) return { everySec: step * 60 };
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function migrate() {
|
|
53
|
+
if (process.env.OA_SKIP_AUTOMIGRATE === '1') return;
|
|
54
|
+
if (!hasSystemdUser()) return;
|
|
55
|
+
const lines = readCrontab();
|
|
56
|
+
if (!lines.length) return;
|
|
57
|
+
const unitDir = join(process.env.HOME || process.env.USERPROFILE || '.', '.config', 'systemd', 'user');
|
|
58
|
+
const kept = [];
|
|
59
|
+
let changed = 0; let created = 0;
|
|
60
|
+
for (const l of lines) {
|
|
61
|
+
if (!isOaCron(l)) { kept.push(l); continue; }
|
|
62
|
+
const id = extractId(l);
|
|
63
|
+
const cron = parseCron(l);
|
|
64
|
+
if (!cron) { kept.push(l); continue; }
|
|
65
|
+
const spec = timerSpecFromCron(cron.min, cron.hour, cron.dom, cron.mon, cron.dow);
|
|
66
|
+
if (!spec) { kept.push(l); continue; }
|
|
67
|
+
const wd = extractWorkingDir(l) || process.cwd();
|
|
68
|
+
const shell = stripMarker(cron.rest);
|
|
69
|
+
const unitBase = `oa-${id}`;
|
|
70
|
+
const svc = join(unitDir, `${unitBase}.service`);
|
|
71
|
+
const tim = join(unitDir, `${unitBase}.timer`);
|
|
72
|
+
const execCmd = `/bin/sh -lc ${JSON.stringify(shell)}`;
|
|
73
|
+
const svcText = `[Unit]\nDescription=Open Agents Scheduled Task ${id}\nAfter=default.target\n\n[Service]\nType=oneshot\nExecStart=${execCmd}\nWorkingDirectory=${wd}\nEnvironment=OA_DAEMON=1\n\n[Install]\nWantedBy=default.target\n`;
|
|
74
|
+
const timText = `[Unit]\nDescription=Timer for ${unitBase}\n\n[Timer]\nOnBootSec=30\nOnUnitActiveSec=${spec.everySec}\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n`;
|
|
75
|
+
try {
|
|
76
|
+
mkdirSync(unitDir, { recursive: true });
|
|
77
|
+
writeFileSync(svc, svcText);
|
|
78
|
+
writeFileSync(tim, timText);
|
|
79
|
+
try {
|
|
80
|
+
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
|
81
|
+
execSync(`systemctl --user enable --now ${unitBase}.timer`, { stdio: 'pipe' });
|
|
82
|
+
} catch {}
|
|
83
|
+
created++;
|
|
84
|
+
// Drop this cron entry since it’s now covered by timer
|
|
85
|
+
changed++;
|
|
86
|
+
} catch { kept.push(l); }
|
|
87
|
+
}
|
|
88
|
+
// If any changes, write new crontab
|
|
89
|
+
if (changed) writeCrontab(kept);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try { migrate(); } catch {}
|
|
93
|
+
process.exit(0);
|
package/package.json
CHANGED