@hasna/loops 0.3.3 → 0.3.5
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/cli/index.js +365 -35
- package/dist/daemon/index.js +316 -23
- package/dist/index.d.ts +1 -0
- package/dist/index.js +339 -26
- package/dist/lib/executor.d.ts +10 -1
- package/dist/lib/machines.d.ts +16 -0
- package/dist/lib/store.js +10 -2
- package/dist/sdk/index.js +313 -20
- package/dist/types.d.ts +15 -0
- package/package.json +2 -1
package/dist/daemon/index.js
CHANGED
|
@@ -347,6 +347,7 @@ function rowToLoop(row) {
|
|
|
347
347
|
status: row.status,
|
|
348
348
|
schedule: JSON.parse(row.schedule_json),
|
|
349
349
|
target: JSON.parse(row.target_json),
|
|
350
|
+
machine: row.machine_json ? JSON.parse(row.machine_json) : undefined,
|
|
350
351
|
nextRunAt: row.next_run_at ?? undefined,
|
|
351
352
|
retryScheduledFor: row.retry_scheduled_for ?? undefined,
|
|
352
353
|
catchUp: row.catch_up,
|
|
@@ -489,6 +490,7 @@ class Store {
|
|
|
489
490
|
status TEXT NOT NULL,
|
|
490
491
|
schedule_json TEXT NOT NULL,
|
|
491
492
|
target_json TEXT NOT NULL,
|
|
493
|
+
machine_json TEXT,
|
|
492
494
|
next_run_at TEXT,
|
|
493
495
|
retry_scheduled_for TEXT,
|
|
494
496
|
catch_up TEXT NOT NULL,
|
|
@@ -610,10 +612,14 @@ class Store {
|
|
|
610
612
|
);
|
|
611
613
|
CREATE INDEX IF NOT EXISTS idx_workflow_events_run_sequence ON workflow_events(workflow_run_id, sequence);
|
|
612
614
|
`);
|
|
615
|
+
try {
|
|
616
|
+
this.db.query("ALTER TABLE loops ADD COLUMN machine_json TEXT").run();
|
|
617
|
+
} catch {}
|
|
613
618
|
try {
|
|
614
619
|
this.db.query("ALTER TABLE workflow_step_runs ADD COLUMN pid INTEGER").run();
|
|
615
620
|
} catch {}
|
|
616
621
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
|
|
622
|
+
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
|
|
617
623
|
}
|
|
618
624
|
assertDaemonLeaseFence(opts = {}, now = nowIso()) {
|
|
619
625
|
if (!opts.daemonLeaseId)
|
|
@@ -631,6 +637,7 @@ class Store {
|
|
|
631
637
|
status: "active",
|
|
632
638
|
schedule: input.schedule,
|
|
633
639
|
target: input.target,
|
|
640
|
+
machine: input.machine,
|
|
634
641
|
nextRunAt: initialNextRun(input.schedule, from),
|
|
635
642
|
catchUp: input.catchUp ?? "latest",
|
|
636
643
|
catchUpLimit: input.catchUpLimit ?? 50,
|
|
@@ -642,9 +649,9 @@ class Store {
|
|
|
642
649
|
createdAt: now,
|
|
643
650
|
updatedAt: now
|
|
644
651
|
};
|
|
645
|
-
this.db.query(`INSERT INTO loops (id, name, description, status, schedule_json, target_json, next_run_at, retry_scheduled_for,
|
|
652
|
+
this.db.query(`INSERT INTO loops (id, name, description, status, schedule_json, target_json, machine_json, next_run_at, retry_scheduled_for,
|
|
646
653
|
catch_up, catch_up_limit, overlap, max_attempts, retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
647
|
-
VALUES ($id, $name, $description, $status, $schedule, $target, $nextRun, NULL, $catchUp, $catchUpLimit,
|
|
654
|
+
VALUES ($id, $name, $description, $status, $schedule, $target, $machine, $nextRun, NULL, $catchUp, $catchUpLimit,
|
|
648
655
|
$overlap, $maxAttempts, $retryDelay, $leaseMs, $expiresAt, $created, $updated)`).run({
|
|
649
656
|
$id: loop.id,
|
|
650
657
|
$name: loop.name,
|
|
@@ -652,6 +659,7 @@ class Store {
|
|
|
652
659
|
$status: loop.status,
|
|
653
660
|
$schedule: JSON.stringify(loop.schedule),
|
|
654
661
|
$target: JSON.stringify(loop.target),
|
|
662
|
+
$machine: loop.machine ? JSON.stringify(loop.machine) : null,
|
|
655
663
|
$nextRun: loop.nextRunAt ?? null,
|
|
656
664
|
$catchUp: loop.catchUp,
|
|
657
665
|
$catchUpLimit: loop.catchUpLimit,
|
|
@@ -1510,8 +1518,9 @@ import { hostname as hostname2 } from "os";
|
|
|
1510
1518
|
import { spawn as spawn2 } from "child_process";
|
|
1511
1519
|
|
|
1512
1520
|
// src/lib/executor.ts
|
|
1513
|
-
import { spawn } from "child_process";
|
|
1521
|
+
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
1514
1522
|
import { once } from "events";
|
|
1523
|
+
import { resolveMachineCommand } from "@hasna/machines/consumer";
|
|
1515
1524
|
|
|
1516
1525
|
// src/lib/accounts.ts
|
|
1517
1526
|
import { spawnSync } from "child_process";
|
|
@@ -1668,6 +1677,59 @@ function commandNotFoundMessage(command, env = process.env) {
|
|
|
1668
1677
|
return `Executable not found in PATH: ${command}. Effective PATH=${env.PATH || "(empty)"}`;
|
|
1669
1678
|
}
|
|
1670
1679
|
|
|
1680
|
+
// src/lib/machines.ts
|
|
1681
|
+
import {
|
|
1682
|
+
discoverMachineTopology,
|
|
1683
|
+
resolveMachineRoute
|
|
1684
|
+
} from "@hasna/machines/consumer";
|
|
1685
|
+
function compact(value) {
|
|
1686
|
+
const text = value?.trim();
|
|
1687
|
+
return text ? text : undefined;
|
|
1688
|
+
}
|
|
1689
|
+
function entryToSummary(entry, topology) {
|
|
1690
|
+
return {
|
|
1691
|
+
id: entry.machine_id,
|
|
1692
|
+
hostname: compact(entry.hostname),
|
|
1693
|
+
platform: compact(entry.platform),
|
|
1694
|
+
user: compact(entry.user),
|
|
1695
|
+
workspacePath: compact(entry.workspace_path),
|
|
1696
|
+
route: entry.ssh.route,
|
|
1697
|
+
local: entry.machine_id === topology.local_machine_id || entry.ssh.route === "local",
|
|
1698
|
+
heartbeatStatus: entry.heartbeat_status,
|
|
1699
|
+
tailscaleOnline: entry.tailscale.online,
|
|
1700
|
+
tags: entry.tags
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
function machineFromRoute(route, topology) {
|
|
1704
|
+
if (!route.ok || !route.machine_id) {
|
|
1705
|
+
throw new Error(`OpenMachines route not found for machine: ${route.requested_machine_id}`);
|
|
1706
|
+
}
|
|
1707
|
+
const entry = topology.machines.find((machine) => machine.machine_id === route.machine_id);
|
|
1708
|
+
return {
|
|
1709
|
+
id: route.machine_id,
|
|
1710
|
+
requestedId: route.requested_machine_id !== route.machine_id ? route.requested_machine_id : undefined,
|
|
1711
|
+
route: route.route,
|
|
1712
|
+
local: route.local,
|
|
1713
|
+
confidence: route.confidence,
|
|
1714
|
+
workspacePath: compact(entry?.workspace_path),
|
|
1715
|
+
resolvedAt: route.generated_at,
|
|
1716
|
+
packageVersion: route.package.version,
|
|
1717
|
+
warnings: route.warnings.length ? route.warnings : undefined
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
function listOpenMachines() {
|
|
1721
|
+
const topology = discoverMachineTopology();
|
|
1722
|
+
return topology.machines.map((entry) => entryToSummary(entry, topology));
|
|
1723
|
+
}
|
|
1724
|
+
function resolveLoopMachine(machineId) {
|
|
1725
|
+
const topology = discoverMachineTopology();
|
|
1726
|
+
const route = resolveMachineRoute(machineId, { topology });
|
|
1727
|
+
return machineFromRoute(route, topology);
|
|
1728
|
+
}
|
|
1729
|
+
function refreshLoopMachine(machine) {
|
|
1730
|
+
return resolveLoopMachine(machine.id);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1671
1733
|
// src/lib/executor.ts
|
|
1672
1734
|
var DEFAULT_TIMEOUT_MS = 30 * 60000;
|
|
1673
1735
|
var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
|
|
@@ -1688,6 +1750,23 @@ var AUTH_ENV_KEYS = [
|
|
|
1688
1750
|
"XDG_STATE_HOME",
|
|
1689
1751
|
"XDG_CACHE_HOME"
|
|
1690
1752
|
];
|
|
1753
|
+
var TRANSPORT_ENV_KEYS = new Set([
|
|
1754
|
+
"BUN_INSTALL",
|
|
1755
|
+
"HOME",
|
|
1756
|
+
"LANG",
|
|
1757
|
+
"LANGUAGE",
|
|
1758
|
+
"LOGNAME",
|
|
1759
|
+
"PATH",
|
|
1760
|
+
"SHELL",
|
|
1761
|
+
"SSH_AGENT_PID",
|
|
1762
|
+
"SSH_AUTH_SOCK",
|
|
1763
|
+
"TERM",
|
|
1764
|
+
"TMP",
|
|
1765
|
+
"TMPDIR",
|
|
1766
|
+
"TEMP",
|
|
1767
|
+
"USER",
|
|
1768
|
+
"XDG_RUNTIME_DIR"
|
|
1769
|
+
]);
|
|
1691
1770
|
function appendBounded(current, chunk, maxBytes) {
|
|
1692
1771
|
const next = current + chunk.toString("utf8");
|
|
1693
1772
|
if (Buffer.byteLength(next, "utf8") <= maxBytes)
|
|
@@ -1714,6 +1793,29 @@ function killProcessGroup(pid) {
|
|
|
1714
1793
|
}
|
|
1715
1794
|
}, 2000).unref();
|
|
1716
1795
|
}
|
|
1796
|
+
function shellQuote(value) {
|
|
1797
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1798
|
+
}
|
|
1799
|
+
function metadataEnv(metadata) {
|
|
1800
|
+
const env = {};
|
|
1801
|
+
if (metadata.loopId)
|
|
1802
|
+
env.LOOPS_LOOP_ID = metadata.loopId;
|
|
1803
|
+
if (metadata.loopName)
|
|
1804
|
+
env.LOOPS_LOOP_NAME = metadata.loopName;
|
|
1805
|
+
if (metadata.runId)
|
|
1806
|
+
env.LOOPS_RUN_ID = metadata.runId;
|
|
1807
|
+
if (metadata.scheduledFor)
|
|
1808
|
+
env.LOOPS_SCHEDULED_FOR = metadata.scheduledFor;
|
|
1809
|
+
if (metadata.workflowId)
|
|
1810
|
+
env.LOOPS_WORKFLOW_ID = metadata.workflowId;
|
|
1811
|
+
if (metadata.workflowName)
|
|
1812
|
+
env.LOOPS_WORKFLOW_NAME = metadata.workflowName;
|
|
1813
|
+
if (metadata.workflowRunId)
|
|
1814
|
+
env.LOOPS_WORKFLOW_RUN_ID = metadata.workflowRunId;
|
|
1815
|
+
if (metadata.workflowStepId)
|
|
1816
|
+
env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
|
|
1817
|
+
return env;
|
|
1818
|
+
}
|
|
1717
1819
|
function providerCommand(provider) {
|
|
1718
1820
|
switch (provider) {
|
|
1719
1821
|
case "claude":
|
|
@@ -1835,26 +1937,213 @@ function executionEnv(spec, metadata, opts) {
|
|
|
1835
1937
|
}
|
|
1836
1938
|
Object.assign(env, spec.env ?? {});
|
|
1837
1939
|
env.PATH = normalizeExecutionPath(env);
|
|
1838
|
-
|
|
1839
|
-
env.LOOPS_LOOP_ID = metadata.loopId;
|
|
1840
|
-
if (metadata.loopName)
|
|
1841
|
-
env.LOOPS_LOOP_NAME = metadata.loopName;
|
|
1842
|
-
if (metadata.runId)
|
|
1843
|
-
env.LOOPS_RUN_ID = metadata.runId;
|
|
1844
|
-
if (metadata.scheduledFor)
|
|
1845
|
-
env.LOOPS_SCHEDULED_FOR = metadata.scheduledFor;
|
|
1846
|
-
if (metadata.workflowId)
|
|
1847
|
-
env.LOOPS_WORKFLOW_ID = metadata.workflowId;
|
|
1848
|
-
if (metadata.workflowName)
|
|
1849
|
-
env.LOOPS_WORKFLOW_NAME = metadata.workflowName;
|
|
1850
|
-
if (metadata.workflowRunId)
|
|
1851
|
-
env.LOOPS_WORKFLOW_RUN_ID = metadata.workflowRunId;
|
|
1852
|
-
if (metadata.workflowStepId)
|
|
1853
|
-
env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
|
|
1940
|
+
Object.assign(env, metadataEnv(metadata));
|
|
1854
1941
|
return env;
|
|
1855
1942
|
}
|
|
1943
|
+
function resolvedMachine(opts) {
|
|
1944
|
+
if (!opts.machine)
|
|
1945
|
+
return;
|
|
1946
|
+
return (opts.machineResolver ?? refreshLoopMachine)(opts.machine);
|
|
1947
|
+
}
|
|
1948
|
+
function commandForShell(spec) {
|
|
1949
|
+
if (!spec.args.length)
|
|
1950
|
+
return spec.command;
|
|
1951
|
+
return [spec.command, ...spec.args.map(shellQuote)].join(" ");
|
|
1952
|
+
}
|
|
1953
|
+
function hereDoc(value) {
|
|
1954
|
+
let delimiter2 = `__OPENLOOPS_STDIN_${Math.random().toString(36).slice(2).toUpperCase()}__`;
|
|
1955
|
+
while (value.split(/\r?\n/).includes(delimiter2)) {
|
|
1956
|
+
delimiter2 = `__OPENLOOPS_STDIN_${Math.random().toString(36).slice(2).toUpperCase()}__`;
|
|
1957
|
+
}
|
|
1958
|
+
return [`cat > "$__OPENLOOPS_STDIN" <<'${delimiter2}'`, value, delimiter2];
|
|
1959
|
+
}
|
|
1960
|
+
function remoteBootstrapLines(spec, metadata) {
|
|
1961
|
+
const lines = [
|
|
1962
|
+
"set -e",
|
|
1963
|
+
'export PATH="$HOME/.local/bin:$HOME/.bun/bin:$HOME/.cargo/bin:$HOME/.npm-global/bin:$HOME/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin${PATH:+:$PATH}"'
|
|
1964
|
+
];
|
|
1965
|
+
if (spec.cwd)
|
|
1966
|
+
lines.push(`cd ${shellQuote(spec.cwd)}`);
|
|
1967
|
+
if (spec.account) {
|
|
1968
|
+
if (!spec.accountTool)
|
|
1969
|
+
throw new Error("account.tool is required when no provider tool can be inferred");
|
|
1970
|
+
lines.push("if ! command -v accounts >/dev/null 2>&1; then echo 'accounts CLI is not available on remote machine' >&2; exit 127; fi", `unset ${AUTH_ENV_KEYS.join(" ")}`, `eval "$(accounts env ${shellQuote(spec.account.profile)} --tool ${shellQuote(spec.accountTool)})"`, `export LOOPS_ACCOUNT_PROFILE=${shellQuote(spec.account.profile)}`, `export LOOPS_ACCOUNT_TOOL=${shellQuote(spec.accountTool)}`);
|
|
1971
|
+
}
|
|
1972
|
+
for (const [key, value] of Object.entries({ ...metadataEnv(metadata), ...spec.env ?? {} })) {
|
|
1973
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
1974
|
+
continue;
|
|
1975
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
1976
|
+
}
|
|
1977
|
+
return lines;
|
|
1978
|
+
}
|
|
1979
|
+
function remoteScript(spec, metadata) {
|
|
1980
|
+
const lines = remoteBootstrapLines(spec, metadata);
|
|
1981
|
+
let stdinRedirect = "";
|
|
1982
|
+
if (spec.stdin !== undefined) {
|
|
1983
|
+
lines.push('__OPENLOOPS_STDIN="$(mktemp -t openloops-stdin.XXXXXX)"', `trap 'rm -f "$__OPENLOOPS_STDIN"' EXIT`);
|
|
1984
|
+
lines.push(...hereDoc(spec.stdin));
|
|
1985
|
+
stdinRedirect = ' < "$__OPENLOOPS_STDIN"';
|
|
1986
|
+
}
|
|
1987
|
+
const invocation = spec.shell ? `sh -c ${shellQuote(commandForShell(spec))}${stdinRedirect}` : `${[spec.command, ...spec.args].map(shellQuote).join(" ")}${stdinRedirect}`;
|
|
1988
|
+
lines.push(invocation);
|
|
1989
|
+
return `${lines.join(`
|
|
1990
|
+
`)}
|
|
1991
|
+
`;
|
|
1992
|
+
}
|
|
1993
|
+
function remotePreflightScript(spec, metadata) {
|
|
1994
|
+
return [
|
|
1995
|
+
...remoteBootstrapLines(spec, metadata),
|
|
1996
|
+
"command -v bash >/dev/null 2>&1",
|
|
1997
|
+
`command -v ${shellQuote(spec.shell ? "sh" : spec.command)} >/dev/null 2>&1`
|
|
1998
|
+
].join(`
|
|
1999
|
+
`);
|
|
2000
|
+
}
|
|
2001
|
+
function transportEnv(opts) {
|
|
2002
|
+
const source = opts.env ?? process.env;
|
|
2003
|
+
const env = {};
|
|
2004
|
+
for (const [key, value] of Object.entries(source)) {
|
|
2005
|
+
if (value === undefined)
|
|
2006
|
+
continue;
|
|
2007
|
+
if (TRANSPORT_ENV_KEYS.has(key) || key.startsWith("LC_"))
|
|
2008
|
+
env[key] = value;
|
|
2009
|
+
}
|
|
2010
|
+
env.PATH = normalizeExecutionPath(env);
|
|
2011
|
+
return env;
|
|
2012
|
+
}
|
|
2013
|
+
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
2014
|
+
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
2015
|
+
const result = spawnSync2(plan.command, plan.args, {
|
|
2016
|
+
encoding: "utf8",
|
|
2017
|
+
env: transportEnv(opts),
|
|
2018
|
+
input: remotePreflightScript(spec, metadata),
|
|
2019
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2020
|
+
timeout: 15000
|
|
2021
|
+
});
|
|
2022
|
+
if (result.error)
|
|
2023
|
+
throw new Error(`remote preflight failed on ${machine.id}: ${result.error.message}`);
|
|
2024
|
+
if ((result.status ?? 1) !== 0) {
|
|
2025
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
2026
|
+
throw new Error(`remote preflight failed on ${machine.id}${detail ? `: ${detail}` : ""}`);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
2030
|
+
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
2031
|
+
const startedAt = nowIso();
|
|
2032
|
+
let stdout = "";
|
|
2033
|
+
let stderr = "";
|
|
2034
|
+
let timedOut = false;
|
|
2035
|
+
let exitCode;
|
|
2036
|
+
let error;
|
|
2037
|
+
let plan;
|
|
2038
|
+
let script;
|
|
2039
|
+
try {
|
|
2040
|
+
plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
2041
|
+
script = remoteScript(spec, metadata);
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
return {
|
|
2044
|
+
status: "failed",
|
|
2045
|
+
stdout: "",
|
|
2046
|
+
stderr: "",
|
|
2047
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2048
|
+
startedAt,
|
|
2049
|
+
finishedAt: nowIso(),
|
|
2050
|
+
durationMs: 0
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
const child = spawn(plan.command, plan.args, {
|
|
2054
|
+
env: transportEnv(opts),
|
|
2055
|
+
detached: true,
|
|
2056
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2057
|
+
});
|
|
2058
|
+
if (child.pid)
|
|
2059
|
+
opts.onSpawn?.(child.pid);
|
|
2060
|
+
child.stdin?.on("error", (err) => {
|
|
2061
|
+
if (err.code !== "EPIPE")
|
|
2062
|
+
error = err.message;
|
|
2063
|
+
});
|
|
2064
|
+
child.stdin?.end(script);
|
|
2065
|
+
const abortHandler = () => {
|
|
2066
|
+
error = "cancelled";
|
|
2067
|
+
if (child.pid)
|
|
2068
|
+
killProcessGroup(child.pid);
|
|
2069
|
+
};
|
|
2070
|
+
if (opts.signal?.aborted)
|
|
2071
|
+
abortHandler();
|
|
2072
|
+
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
2073
|
+
child.stdout?.on("data", (chunk) => {
|
|
2074
|
+
stdout = appendBounded(stdout, chunk, maxOutputBytes);
|
|
2075
|
+
});
|
|
2076
|
+
child.stderr?.on("data", (chunk) => {
|
|
2077
|
+
stderr = appendBounded(stderr, chunk, maxOutputBytes);
|
|
2078
|
+
});
|
|
2079
|
+
const timer = setTimeout(() => {
|
|
2080
|
+
timedOut = true;
|
|
2081
|
+
if (child.pid)
|
|
2082
|
+
killProcessGroup(child.pid);
|
|
2083
|
+
}, spec.timeoutMs);
|
|
2084
|
+
timer.unref();
|
|
2085
|
+
try {
|
|
2086
|
+
const [code, signal] = await once(child, "exit");
|
|
2087
|
+
if (typeof code === "number")
|
|
2088
|
+
exitCode = code;
|
|
2089
|
+
if (signal)
|
|
2090
|
+
error = `terminated by ${signal}`;
|
|
2091
|
+
} catch (err) {
|
|
2092
|
+
error = err instanceof Error ? err.message : String(err);
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(timer);
|
|
2095
|
+
opts.signal?.removeEventListener("abort", abortHandler);
|
|
2096
|
+
}
|
|
2097
|
+
const finishedAt = nowIso();
|
|
2098
|
+
const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
|
|
2099
|
+
if (timedOut) {
|
|
2100
|
+
return {
|
|
2101
|
+
status: "timed_out",
|
|
2102
|
+
exitCode,
|
|
2103
|
+
stdout,
|
|
2104
|
+
stderr,
|
|
2105
|
+
error: `timed out after ${spec.timeoutMs}ms`,
|
|
2106
|
+
pid: child.pid,
|
|
2107
|
+
startedAt,
|
|
2108
|
+
finishedAt,
|
|
2109
|
+
durationMs
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
if (error || exitCode !== 0) {
|
|
2113
|
+
return {
|
|
2114
|
+
status: "failed",
|
|
2115
|
+
exitCode,
|
|
2116
|
+
stdout,
|
|
2117
|
+
stderr,
|
|
2118
|
+
error: error ?? `remote process on ${machine.id} exited with code ${exitCode ?? "unknown"}`,
|
|
2119
|
+
pid: child.pid,
|
|
2120
|
+
startedAt,
|
|
2121
|
+
finishedAt,
|
|
2122
|
+
durationMs
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
return {
|
|
2126
|
+
status: "succeeded",
|
|
2127
|
+
exitCode,
|
|
2128
|
+
stdout,
|
|
2129
|
+
stderr,
|
|
2130
|
+
pid: child.pid,
|
|
2131
|
+
startedAt,
|
|
2132
|
+
finishedAt,
|
|
2133
|
+
durationMs
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
1856
2136
|
function preflightTarget(target, metadata = {}, opts = {}) {
|
|
1857
2137
|
const spec = commandSpec(target);
|
|
2138
|
+
const machine = resolvedMachine(opts);
|
|
2139
|
+
if (machine && !machine.local) {
|
|
2140
|
+
preflightRemoteSpec(spec, machine, metadata, opts);
|
|
2141
|
+
return {
|
|
2142
|
+
command: spec.command,
|
|
2143
|
+
accountProfile: spec.account?.profile,
|
|
2144
|
+
accountTool: spec.accountTool
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
1858
2147
|
const env = executionEnv(spec, metadata, opts);
|
|
1859
2148
|
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
1860
2149
|
throw new Error(commandNotFoundMessage(spec.command, env));
|
|
@@ -1867,6 +2156,9 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
1867
2156
|
}
|
|
1868
2157
|
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
1869
2158
|
const spec = commandSpec(target);
|
|
2159
|
+
const machine = resolvedMachine(opts);
|
|
2160
|
+
if (machine && !machine.local)
|
|
2161
|
+
return executeRemoteSpec(spec, machine, metadata, opts);
|
|
1870
2162
|
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
1871
2163
|
const startedAt = nowIso();
|
|
1872
2164
|
let stdout = "";
|
|
@@ -1982,7 +2274,7 @@ async function executeLoop(loop, run, opts = {}) {
|
|
|
1982
2274
|
loopName: loop.name,
|
|
1983
2275
|
runId: run.id,
|
|
1984
2276
|
scheduledFor: run.scheduledFor
|
|
1985
|
-
}, opts);
|
|
2277
|
+
}, { ...opts, machine: opts.machine ?? loop.machine });
|
|
1986
2278
|
}
|
|
1987
2279
|
|
|
1988
2280
|
// src/lib/workflow-runner.ts
|
|
@@ -2084,6 +2376,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
2084
2376
|
try {
|
|
2085
2377
|
result = await executeTarget(targetWithStepAccount(step), metadata, {
|
|
2086
2378
|
...opts,
|
|
2379
|
+
machine: opts.machine ?? opts.loop?.machine,
|
|
2087
2380
|
signal: controller.signal,
|
|
2088
2381
|
onSpawn: (pid) => {
|
|
2089
2382
|
opts.beforePersist?.();
|
|
@@ -2683,7 +2976,7 @@ async function startDaemon(opts) {
|
|
|
2683
2976
|
|
|
2684
2977
|
// src/daemon/install.ts
|
|
2685
2978
|
import { chmodSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
2686
|
-
import { spawnSync as
|
|
2979
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2687
2980
|
import { dirname as dirname3 } from "path";
|
|
2688
2981
|
function installStartup(cliEntry, execPath = process.execPath, args = ["daemon", "run"]) {
|
|
2689
2982
|
const command = [execPath, cliEntry, ...args].join(" ");
|
|
@@ -2753,7 +3046,7 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
|
|
|
2753
3046
|
function enableStartup(result) {
|
|
2754
3047
|
const commands = result.platform === "linux" ? ["systemctl --user daemon-reload", "systemctl --user enable --now loops-daemon.service"] : result.platform === "darwin" ? [`launchctl load -w ${result.path}`] : [];
|
|
2755
3048
|
return commands.map((command) => {
|
|
2756
|
-
const run =
|
|
3049
|
+
const run = spawnSync3("sh", ["-c", command], {
|
|
2757
3050
|
encoding: "utf8",
|
|
2758
3051
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2759
3052
|
});
|
|
@@ -2768,7 +3061,7 @@ function enableStartup(result) {
|
|
|
2768
3061
|
|
|
2769
3062
|
// src/daemon/index.ts
|
|
2770
3063
|
var program = new Command;
|
|
2771
|
-
program.name("loops-daemon").description("OpenLoops daemon helper").version("0.3.
|
|
3064
|
+
program.name("loops-daemon").description("OpenLoops daemon helper").version("0.3.5");
|
|
2772
3065
|
program.command("run").option("--interval-ms <ms>", "tick interval", (value) => Number(value)).action(async (opts) => runDaemon({ intervalMs: opts.intervalMs }));
|
|
2773
3066
|
program.command("start").action(async () => {
|
|
2774
3067
|
const result = await startDaemon({ cliEntry: process.argv[1] ?? "loops-daemon", args: ["run"] });
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type { LoopsClientOptions } from "./sdk/index.js";
|
|
|
4
4
|
export { Store } from "./lib/store.js";
|
|
5
5
|
export { parseDuration, parseCron, nextCronRun, initialNextRun, computeNextAfter } from "./lib/schedule.js";
|
|
6
6
|
export { executeLoop, executeTarget, preflightTarget } from "./lib/executor.js";
|
|
7
|
+
export { listOpenMachines, refreshLoopMachine, resolveLoopMachine } from "./lib/machines.js";
|
|
7
8
|
export { tick } from "./lib/scheduler.js";
|
|
8
9
|
export { executeWorkflow, executeLoopTarget, preflightWorkflow } from "./lib/workflow-runner.js";
|
|
9
10
|
export { workflowExecutionOrder, workflowBodyFromJson } from "./lib/workflow-spec.js";
|