@kici-dev/compiler 0.1.27 → 0.2.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/dist/auth/headless-detect.d.ts +14 -5
- package/dist/auth/headless-detect.js +50 -9
- package/dist/cli.js +120 -11
- package/dist/commands/compile.js +28 -4
- package/dist/commands/diagnostics.js +14 -2
- package/dist/commands/doctor.d.ts +64 -0
- package/dist/commands/doctor.js +413 -0
- package/dist/commands/endpoints.js +6 -0
- package/dist/commands/fixture.js +13 -0
- package/dist/commands/hook.js +3 -2
- package/dist/commands/index.d.ts +8 -0
- package/dist/commands/index.js +5 -1
- package/dist/commands/init-host-os.d.ts +34 -0
- package/dist/commands/init-host-os.js +44 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +159 -36
- package/dist/commands/local.d.ts +28 -2
- package/dist/commands/local.js +74 -5
- package/dist/commands/login.js +1 -1
- package/dist/commands/notifications.d.ts +61 -0
- package/dist/commands/notifications.js +274 -0
- package/dist/commands/preview.js +8 -1
- package/dist/commands/run-routed.js +10 -1
- package/dist/commands/run.js +41 -12
- package/dist/commands/runs/artifacts/download.d.ts +24 -0
- package/dist/commands/runs/artifacts/download.js +154 -0
- package/dist/commands/runs/artifacts/list.d.ts +5 -0
- package/dist/commands/runs/artifacts/list.js +47 -0
- package/dist/commands/runs/cancel.js +2 -2
- package/dist/commands/runs/list.js +8 -2
- package/dist/commands/verify-attestation.d.ts +3 -1
- package/dist/commands/verify-attestation.js +18 -7
- package/dist/errors/formatter.d.ts +14 -1
- package/dist/errors/formatter.js +13 -3
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +3 -2
- package/dist/errors/source-location.d.ts +20 -0
- package/dist/errors/source-location.js +45 -0
- package/dist/execution/executor.js +25 -5
- package/dist/fixtures/defaults/index.js +2 -1
- package/dist/llm-context/llms-architecture.txt +48 -19
- package/dist/llm-context/llms-cli.txt +2461 -1582
- package/dist/llm-context/llms-features.txt +178 -106
- package/dist/llm-context/llms-full.txt +6124 -4423
- package/dist/llm-context/llms-getting-started.txt +306 -42
- package/dist/llm-context/llms-patterns.txt +13 -7
- package/dist/llm-context/llms-sdk-runtime.txt +2013 -0
- package/dist/llm-context/llms-sdk.txt +591 -2152
- package/dist/llm-context/llms.txt +30 -16
- package/dist/local-plane/orchestrator-process.d.ts +12 -7
- package/dist/local-plane/orchestrator-process.js +28 -14
- package/dist/local-plane/os-mismatch-hint.d.ts +16 -0
- package/dist/local-plane/os-mismatch-hint.js +34 -0
- package/dist/local-plane/plane-liveness.d.ts +68 -0
- package/dist/local-plane/plane-liveness.js +145 -0
- package/dist/local-plane/plane-manager.d.ts +60 -18
- package/dist/local-plane/plane-manager.js +210 -70
- package/dist/local-plane/plane-trigger.d.ts +13 -4
- package/dist/local-plane/plane-trigger.js +27 -14
- package/dist/local-plane/platform-attach.js +1 -5
- package/dist/local-plane/port-holder.d.ts +78 -0
- package/dist/local-plane/port-holder.js +227 -0
- package/dist/local-plane/postgres.d.ts +15 -3
- package/dist/local-plane/postgres.js +34 -12
- package/dist/local-plane/run-follow.d.ts +20 -0
- package/dist/local-plane/run-follow.js +51 -4
- package/dist/local-plane/scaler-config.d.ts +28 -14
- package/dist/local-plane/scaler-config.js +59 -41
- package/dist/local-plane/source-provider.js +4 -10
- package/dist/lockfile/generator.d.ts +14 -1
- package/dist/lockfile/generator.js +99 -33
- package/dist/lockfile/index.d.ts +3 -1
- package/dist/lockfile/index.js +3 -2
- package/dist/lockfile/purity-diagnostics.d.ts +31 -0
- package/dist/lockfile/purity-diagnostics.js +52 -0
- package/dist/postinstall.js +2 -1
- package/dist/remote/artifact-extract.d.ts +8 -0
- package/dist/remote/artifact-extract.js +58 -0
- package/dist/remote/config.d.ts +21 -3
- package/dist/remote/config.js +24 -5
- package/dist/remote/dashboard-client.d.ts +39 -2
- package/dist/remote/dashboard-client.js +47 -4
- package/dist/remote/fs-case.d.ts +21 -0
- package/dist/remote/fs-case.js +45 -0
- package/dist/remote/history.d.ts +9 -3
- package/dist/remote/history.js +79 -10
- package/dist/remote/local-repo-identity.d.ts +1 -1
- package/dist/remote/local-repo-identity.js +2 -1
- package/dist/remote/notifications-client.d.ts +149 -0
- package/dist/remote/notifications-client.js +103 -0
- package/dist/remote/oauth.d.ts +13 -0
- package/dist/remote/oauth.js +31 -5
- package/dist/remote/output/summary.js +76 -14
- package/dist/remote/render.js +30 -9
- package/dist/remote/uploader.js +5 -4
- package/dist/templates/agents-md.d.ts +1 -1
- package/dist/templates/agents-md.js +6 -6
- package/dist/templates/index.d.ts +1 -1
- package/dist/templates/index.js +2 -2
- package/dist/templates/package-json.d.ts +15 -0
- package/dist/templates/package-json.js +26 -5
- package/dist/test-runner/dry-run.d.ts +2 -1
- package/dist/test-runner/dry-run.js +18 -1
- package/dist/test-runner/event-types.d.ts +2 -0
- package/dist/test-runner/event-types.js +3 -0
- package/dist/test-runner/job-executor.d.ts +1 -0
- package/dist/test-runner/job-executor.js +81 -43
- package/dist/test-runner/output-formatter.d.ts +5 -2
- package/dist/test-runner/output-formatter.js +6 -4
- package/dist/test-runner/payload-builder.d.ts +7 -1
- package/dist/test-runner/payload-builder.js +11 -2
- package/dist/test-runner/rule-evaluator.d.ts +7 -3
- package/dist/test-runner/rule-evaluator.js +13 -12
- package/dist/test-runner/step-context.d.ts +23 -5
- package/dist/test-runner/step-context.js +28 -118
- package/dist/types.d.ts +21 -2
- package/dist/types.js +4 -2
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.js +2 -1
- package/dist/validation/typecheck.d.ts +28 -0
- package/dist/validation/typecheck.js +110 -0
- package/dist/validation/validator.d.ts +9 -4
- package/dist/validation/validator.js +38 -44
- package/package.json +12 -15
- package/sbom.spdx.json +1322 -1964
- package/dist/assets/api-TJJVHrjC.json +0 -118
- package/dist/assets/descriptor-BTtjzN9L.json +0 -1382
- package/dist/assets/package-BpQF9kR8.json +0 -74
- package/dist/assets/package-Ceo2h27X.json +0 -89
- package/dist/assets/source_context-D0atuL28.json +0 -20
- package/dist/assets/type-BFqO8SCZ.json +0 -202
- package/dist/commands/cancel.d.ts +0 -22
- package/dist/commands/detect-package-manager.d.ts +0 -42
- package/dist/commands/held-run-resolve.d.ts +0 -50
- package/dist/commands/status.d.ts +0 -34
- package/dist/commands/test.d.ts +0 -88
- package/dist/errors/codes.d.ts +0 -25
- package/dist/local-executor/dag-scheduler.d.ts +0 -44
- package/dist/local-executor/index.d.ts +0 -23
- package/dist/local-executor/job-runner.d.ts +0 -47
- package/dist/local-executor/materializer.d.ts +0 -44
- package/dist/local-executor/output-streamer.d.ts +0 -31
- package/dist/local-executor/payload-generator.d.ts +0 -16
- package/dist/local-executor/picker.d.ts +0 -33
- package/dist/local-executor/runs-on-display.d.ts +0 -9
- package/dist/local-executor/secret-loader.d.ts +0 -18
- package/dist/local-executor/to-event-payload.d.ts +0 -16
- package/dist/local-executor/types.d.ts +0 -93
- package/dist/local-executor/workflow-lock.d.ts +0 -82
- package/dist/package-F7UXSDHW.json +0 -74
- package/dist/remote/client.d.ts +0 -210
- package/dist/remote/observer.d.ts +0 -81
- package/dist/test-runner/summary.d.ts +0 -7
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
//#region src/local-plane/port-holder.ts
|
|
6
|
+
/**
|
|
7
|
+
* OS-level ownership of the local dev plane's port.
|
|
8
|
+
*
|
|
9
|
+
* Answers two questions the plane's on-disk stamp cannot: is the port occupied
|
|
10
|
+
* at all, and which process occupies it. Every external command is reached
|
|
11
|
+
* through an injectable `ExecFn` so each platform branch has a real unit test on
|
|
12
|
+
* any host, and every failure degrades to `null` rather than throwing — a
|
|
13
|
+
* discovery failure must never break the command that asked.
|
|
14
|
+
*/
|
|
15
|
+
const defaultExec = (cmd, args) => new Promise((resolve) => {
|
|
16
|
+
execFile(cmd, args, {
|
|
17
|
+
timeout: 5e3,
|
|
18
|
+
maxBuffer: 8 * 1024 * 1024
|
|
19
|
+
}, (err, stdout) => {
|
|
20
|
+
const raw = err?.code;
|
|
21
|
+
resolve({
|
|
22
|
+
stdout: stdout ?? "",
|
|
23
|
+
exitCode: typeof raw === "number" ? raw : err ? 1 : 0
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Command line of process `pid`, or null when it cannot be determined.
|
|
29
|
+
*
|
|
30
|
+
* Linux exposes it as a NUL-separated `/proc` file; every other platform is
|
|
31
|
+
* asked through the same injectable `ExecFn` the port discovery above uses.
|
|
32
|
+
*
|
|
33
|
+
* **A null means "cannot tell", never "not that process."** Callers must treat
|
|
34
|
+
* it as an absence of evidence — the only safe reading when the answer decides
|
|
35
|
+
* whether to signal a pid.
|
|
36
|
+
*/
|
|
37
|
+
async function processCommandLine(pid, opts = {}) {
|
|
38
|
+
const platform = opts.platform ?? process.platform;
|
|
39
|
+
try {
|
|
40
|
+
if (platform === "linux") {
|
|
41
|
+
const joined = (opts.readProc ?? ((p) => fs.readFileSync(p, "utf-8")))(`/proc/${pid}/cmdline`).split("\0").filter(Boolean).join(" ").trim();
|
|
42
|
+
return joined === "" ? null : joined;
|
|
43
|
+
}
|
|
44
|
+
if (platform === "win32") return null;
|
|
45
|
+
const r = await (opts.exec ?? defaultExec)("ps", [
|
|
46
|
+
"-o",
|
|
47
|
+
"args=",
|
|
48
|
+
"-p",
|
|
49
|
+
String(pid)
|
|
50
|
+
]);
|
|
51
|
+
if (r.exitCode !== 0) return null;
|
|
52
|
+
const line = r.stdout.trim();
|
|
53
|
+
return line === "" ? null : line;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether the port can be bound on loopback. Probes `127.0.0.1` because that is
|
|
60
|
+
* the address the CLI talks to; a listener bound to `0.0.0.0` still makes this
|
|
61
|
+
* bind fail with EADDRINUSE, so there is no false "free".
|
|
62
|
+
*/
|
|
63
|
+
function isPortFree(port) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const probe = net.createServer();
|
|
66
|
+
probe.once("error", () => resolve(false));
|
|
67
|
+
probe.listen({
|
|
68
|
+
port,
|
|
69
|
+
host: "127.0.0.1",
|
|
70
|
+
exclusive: true
|
|
71
|
+
}, () => {
|
|
72
|
+
probe.close(() => resolve(true));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Port of a socket address column. The port is the segment after the LAST
|
|
78
|
+
* colon, because an IPv6 address is written `[::]:5355`.
|
|
79
|
+
*/
|
|
80
|
+
function addressPort(addr) {
|
|
81
|
+
const i = addr.lastIndexOf(":");
|
|
82
|
+
if (i < 0) return null;
|
|
83
|
+
const n = Number(addr.slice(i + 1));
|
|
84
|
+
return Number.isInteger(n) ? n : null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse `ss -lptnH` rows. The process name is deliberately NOT matched — a Node
|
|
88
|
+
* listener reports as `MainThread` — so only `pid=N` is extracted. A row for a
|
|
89
|
+
* socket this user does not own carries no `users:(…)` field at all and yields
|
|
90
|
+
* null, which is what sends the caller to the lsof fallback.
|
|
91
|
+
*/
|
|
92
|
+
function parseSsOutput(stdout, port) {
|
|
93
|
+
for (const line of stdout.split("\n")) {
|
|
94
|
+
const cols = line.trim().split(/\s+/);
|
|
95
|
+
if (cols.length < 4) continue;
|
|
96
|
+
if (addressPort(cols[3]) !== port) continue;
|
|
97
|
+
const m = /pid=(\d+)/.exec(line);
|
|
98
|
+
return m ? Number(m[1]) : null;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Parse `lsof -Fpn` field output, which is a stateful stream: each `p<pid>` line
|
|
104
|
+
* precedes the `n<address>` lines belonging to that pid.
|
|
105
|
+
*/
|
|
106
|
+
function parseLsofOutput(stdout, port) {
|
|
107
|
+
let current = null;
|
|
108
|
+
for (const line of stdout.split("\n")) if (line.startsWith("p")) {
|
|
109
|
+
const n = Number(line.slice(1));
|
|
110
|
+
current = Number.isInteger(n) ? n : null;
|
|
111
|
+
} else if (line.startsWith("n") && current !== null) {
|
|
112
|
+
if (addressPort(line.slice(1)) === port) return current;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/** Parse `netstat -ano -p tcp`: LISTENING rows only, pid in the last column. */
|
|
117
|
+
function parseNetstatOutput(stdout, port) {
|
|
118
|
+
for (const line of stdout.split("\n")) {
|
|
119
|
+
const cols = line.trim().split(/\s+/);
|
|
120
|
+
if (cols.length < 5) continue;
|
|
121
|
+
if (!/^LISTENING$/i.test(cols[3])) continue;
|
|
122
|
+
if (addressPort(cols[1]) !== port) continue;
|
|
123
|
+
const pid = Number(cols[cols.length - 1]);
|
|
124
|
+
return Number.isInteger(pid) ? pid : null;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Pid of the process listening on `port`, or null when it cannot be determined.
|
|
130
|
+
* Each platform runs the UNFILTERED listing and matches the port here rather
|
|
131
|
+
* than passing a filter expression to the tool, whose syntax is the fragile
|
|
132
|
+
* part. On Linux a null parse falls through to lsof as well as a non-zero exit,
|
|
133
|
+
* because ss omits the owning-process field for sockets this user does not own.
|
|
134
|
+
*/
|
|
135
|
+
async function findPortHolderPid(port, opts = {}) {
|
|
136
|
+
const exec = opts.exec ?? defaultExec;
|
|
137
|
+
const platform = opts.platform ?? process.platform;
|
|
138
|
+
const viaLsof = async () => {
|
|
139
|
+
const r = await exec("lsof", [
|
|
140
|
+
"-nP",
|
|
141
|
+
"-iTCP",
|
|
142
|
+
"-sTCP:LISTEN",
|
|
143
|
+
"-Fpn"
|
|
144
|
+
]);
|
|
145
|
+
return r.exitCode === 0 ? parseLsofOutput(r.stdout, port) : null;
|
|
146
|
+
};
|
|
147
|
+
try {
|
|
148
|
+
if (platform === "win32") {
|
|
149
|
+
const r = await exec("netstat", [
|
|
150
|
+
"-ano",
|
|
151
|
+
"-p",
|
|
152
|
+
"tcp"
|
|
153
|
+
]);
|
|
154
|
+
return r.exitCode === 0 ? parseNetstatOutput(r.stdout, port) : null;
|
|
155
|
+
}
|
|
156
|
+
if (platform === "linux") {
|
|
157
|
+
const r = await exec("ss", ["-lptnH"]);
|
|
158
|
+
return (r.exitCode === 0 ? parseSsOutput(r.stdout, port) : null) ?? await viaLsof();
|
|
159
|
+
}
|
|
160
|
+
return await viaLsof();
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const defaultKill = (pid, signal) => process.kill(pid, signal);
|
|
166
|
+
/** How long to keep watching for the process to disappear after SIGKILL. */
|
|
167
|
+
const HARD_KILL_WAIT_MS = 5e3;
|
|
168
|
+
/**
|
|
169
|
+
* Poll until process `pid` has exited, sending SIGTERM first and escalating to
|
|
170
|
+
* SIGKILL past the grace window. `process.kill(pid, 0)` throws ESRCH once the
|
|
171
|
+
* process is gone. On Windows every signal terminates unconditionally, so the
|
|
172
|
+
* escalation collapses into a single hard kill there.
|
|
173
|
+
*
|
|
174
|
+
* Resolving does NOT prove the process died: a pid that survives SIGKILL (a
|
|
175
|
+
* zombie awaiting reaping, or a task wedged in uninterruptible I/O) is given
|
|
176
|
+
* `hardKillWaitMs` and then abandoned, because a teardown command that hangs
|
|
177
|
+
* forever is worse than one that reports the port is still held. Callers decide
|
|
178
|
+
* the outcome from the port itself — `waitForPortFree` — never from this
|
|
179
|
+
* resolving.
|
|
180
|
+
*/
|
|
181
|
+
async function terminatePid(pid, graceMs = 1e4, opts = {}) {
|
|
182
|
+
const kill = opts.kill ?? defaultKill;
|
|
183
|
+
const hardKillWaitMs = opts.hardKillWaitMs ?? HARD_KILL_WAIT_MS;
|
|
184
|
+
try {
|
|
185
|
+
kill(pid, "SIGTERM");
|
|
186
|
+
} catch {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let deadline = Date.now() + graceMs;
|
|
190
|
+
let killed = false;
|
|
191
|
+
for (;;) {
|
|
192
|
+
try {
|
|
193
|
+
kill(pid, 0);
|
|
194
|
+
} catch {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (Date.now() > deadline) {
|
|
198
|
+
if (killed) return;
|
|
199
|
+
try {
|
|
200
|
+
kill(pid, "SIGKILL");
|
|
201
|
+
} catch {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
killed = true;
|
|
205
|
+
deadline = Date.now() + hardKillWaitMs;
|
|
206
|
+
}
|
|
207
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Poll until the port is bindable. This, not `terminatePid` resolving, is what
|
|
212
|
+
* decides a teardown: `terminatePid` abandons a pid that survives SIGKILL, and
|
|
213
|
+
* even a process that did exit can leave its listening socket lingering a moment
|
|
214
|
+
* longer on some platforms.
|
|
215
|
+
*/
|
|
216
|
+
async function waitForPortFree(port, timeoutMs = 5e3) {
|
|
217
|
+
const deadline = Date.now() + timeoutMs;
|
|
218
|
+
for (;;) {
|
|
219
|
+
if (await isPortFree(port)) return true;
|
|
220
|
+
if (Date.now() >= deadline) return false;
|
|
221
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
export { findPortHolderPid, isPortFree, parseLsofOutput, parseNetstatOutput, parseSsOutput, processCommandLine, terminatePid, waitForPortFree };
|
|
226
|
+
|
|
227
|
+
//# sourceMappingURL=port-holder.js.map
|
|
@@ -18,12 +18,24 @@ declare let embeddedDaemon: typeof defaultEmbeddedDaemon;
|
|
|
18
18
|
/** Test seam: override the embedded postmaster daemonizer. */
|
|
19
19
|
export declare function __setEmbeddedDaemonForTest(fn: typeof embeddedDaemon): void;
|
|
20
20
|
declare function defaultReadyPoller(_port: number): Promise<boolean>;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Whether this plane's embedded postmaster is already serving `port`.
|
|
23
|
+
*
|
|
24
|
+
* `pg_ctl start` fails outright against a running cluster, so a boot that
|
|
25
|
+
* reclaims the plane port while leaving PostgreSQL up — reclaiming another
|
|
26
|
+
* plane's orchestrator does exactly that, since only the port holder is
|
|
27
|
+
* signalled — would otherwise fall through to the Podman path and fail there.
|
|
28
|
+
*
|
|
29
|
+
* The port comes from the cluster's own `postmaster.pid` (line 4) rather than
|
|
30
|
+
* being assumed, so a postmaster left on a different port is not mistaken for
|
|
31
|
+
* one this plane can reuse.
|
|
32
|
+
*/
|
|
33
|
+
export declare function embeddedClusterIsServing(port: number): Promise<boolean>;
|
|
23
34
|
/**
|
|
24
35
|
* Start a detached embedded postmaster via `pg_ctl` so it survives the exit of
|
|
25
36
|
* this CLI process (embedded-postgres's in-process server is killed by its own
|
|
26
|
-
* exit hook, so it cannot back a warm plane).
|
|
37
|
+
* exit hook, so it cannot back a warm plane). A cluster already serving this
|
|
38
|
+
* plane's port is reused as-is.
|
|
27
39
|
*/
|
|
28
40
|
declare function defaultEmbeddedDaemon(port: number): Promise<void>;
|
|
29
41
|
/**
|
|
@@ -3,9 +3,9 @@ import { planePaths, planePorts } from "./paths.js";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
+
import { $ } from "zx";
|
|
6
7
|
import { spawn } from "node:child_process";
|
|
7
8
|
import os from "node:os";
|
|
8
|
-
import { $ } from "zx";
|
|
9
9
|
import EmbeddedPostgres from "embedded-postgres";
|
|
10
10
|
//#region src/local-plane/postgres.ts
|
|
11
11
|
/**
|
|
@@ -35,15 +35,6 @@ async function defaultReadyPoller(_port) {
|
|
|
35
35
|
}
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
|
-
/** Whether a podman binary is available on PATH. */
|
|
39
|
-
async function isPodmanAvailable() {
|
|
40
|
-
try {
|
|
41
|
-
await $`podman --version`.quiet();
|
|
42
|
-
return true;
|
|
43
|
-
} catch {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
38
|
/**
|
|
48
39
|
* Resolve the platform-specific `pg_ctl` binary bundled with embedded-postgres.
|
|
49
40
|
* Mirrors the package's own platform → binary-package mapping, resolving the
|
|
@@ -91,12 +82,43 @@ async function ensureEmbeddedCluster(port) {
|
|
|
91
82
|
await pg.stop();
|
|
92
83
|
}
|
|
93
84
|
/**
|
|
85
|
+
* Whether this plane's embedded postmaster is already serving `port`.
|
|
86
|
+
*
|
|
87
|
+
* `pg_ctl start` fails outright against a running cluster, so a boot that
|
|
88
|
+
* reclaims the plane port while leaving PostgreSQL up — reclaiming another
|
|
89
|
+
* plane's orchestrator does exactly that, since only the port holder is
|
|
90
|
+
* signalled — would otherwise fall through to the Podman path and fail there.
|
|
91
|
+
*
|
|
92
|
+
* The port comes from the cluster's own `postmaster.pid` (line 4) rather than
|
|
93
|
+
* being assumed, so a postmaster left on a different port is not mistaken for
|
|
94
|
+
* one this plane can reuse.
|
|
95
|
+
*/
|
|
96
|
+
async function embeddedClusterIsServing(port) {
|
|
97
|
+
const { pgData } = planePaths();
|
|
98
|
+
const pidFile = path.join(pgData, "postmaster.pid");
|
|
99
|
+
let runningPort;
|
|
100
|
+
try {
|
|
101
|
+
runningPort = Number(fs.readFileSync(pidFile, "utf-8").split("\n")[3]);
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (runningPort !== port) return false;
|
|
106
|
+
try {
|
|
107
|
+
await $`${resolvePgCtl()} -D ${pgData} status`.quiet();
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
94
114
|
* Start a detached embedded postmaster via `pg_ctl` so it survives the exit of
|
|
95
115
|
* this CLI process (embedded-postgres's in-process server is killed by its own
|
|
96
|
-
* exit hook, so it cannot back a warm plane).
|
|
116
|
+
* exit hook, so it cannot back a warm plane). A cluster already serving this
|
|
117
|
+
* plane's port is reused as-is.
|
|
97
118
|
*/
|
|
98
119
|
async function defaultEmbeddedDaemon(port) {
|
|
99
120
|
const { pgData, logFile } = planePaths();
|
|
121
|
+
if (await embeddedClusterIsServing(port)) return;
|
|
100
122
|
await $`${resolvePgCtl()} -D ${pgData} -o ${`-p ${port}`} -l ${`${logFile}.pg`} -w start`.quiet();
|
|
101
123
|
}
|
|
102
124
|
/** Stop the detached embedded postmaster (handle-independent, reads the data dir). */
|
|
@@ -160,6 +182,6 @@ async function startPlanePostgres(opts = {}) {
|
|
|
160
182
|
};
|
|
161
183
|
}
|
|
162
184
|
//#endregion
|
|
163
|
-
export { PLANE_PG_CONTAINER, PLANE_PG_IMAGE, __setEmbeddedDaemonForTest, __setReadyPollerForTest,
|
|
185
|
+
export { PLANE_PG_CONTAINER, PLANE_PG_IMAGE, __setEmbeddedDaemonForTest, __setReadyPollerForTest, embeddedClusterIsServing, startPlanePostgres, stopPlanePostgres };
|
|
164
186
|
|
|
165
187
|
//# sourceMappingURL=postgres.js.map
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
* orchestrator admin API (no SSE in independent mode). Streams step log lines as
|
|
4
4
|
* they arrive and resolves when the run reaches a terminal state.
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the acceptance window, falling back to the default for any value that
|
|
8
|
+
* is not a positive number of milliseconds.
|
|
9
|
+
*
|
|
10
|
+
* A malformed override must never reach the deadline arithmetic: `Number('')`
|
|
11
|
+
* is `0` (every run fails instantly) and `Number('2 min')` is `NaN`, which makes
|
|
12
|
+
* `now > deadline` permanently false and silently restores the 900 s hang this
|
|
13
|
+
* window exists to prevent. Both failure modes are worse than ignoring a typo.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveAcceptanceTimeoutMs(explicitMs: number | undefined, raw: string | undefined): number;
|
|
6
16
|
/** A minimal admin-read client (AdminApiClient.get) — injectable for tests. */
|
|
7
17
|
export interface RunFollowClient {
|
|
8
18
|
get<T>(path: string): Promise<T>;
|
|
@@ -35,6 +45,16 @@ export interface FollowRunOptions {
|
|
|
35
45
|
timeoutMs?: number;
|
|
36
46
|
/** Absolute cap regardless of progress (safety net against a runaway follow). Default 2 h. */
|
|
37
47
|
maxTotalMs?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Max time for the FIRST job to leave `pending`/`queued` — that is, for some
|
|
50
|
+
* agent to claim the run. A plane whose scaler matches no label set, or whose
|
|
51
|
+
* agent cannot start, never gets past this, so it fails in seconds instead of
|
|
52
|
+
* consuming the whole idle window. Default 2 min; override with
|
|
53
|
+
* `KICI_LOCAL_ACCEPTANCE_TIMEOUT_MS`.
|
|
54
|
+
*/
|
|
55
|
+
acceptanceTimeoutMs?: number;
|
|
56
|
+
/** Plane log path named in the acceptance-failure message. */
|
|
57
|
+
hintLogPath?: string;
|
|
38
58
|
}
|
|
39
59
|
/** Poll a run to completion, streaming step logs. */
|
|
40
60
|
export declare function followRun(planeUrl: string, adminToken: string, runId: string, opts?: FollowRunOptions): Promise<RunOutcome>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
|
-
import { ExecutionJobStatus, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES } from "@kici-dev/engine";
|
|
2
|
+
import { ExecutionJobStatus, ExecutionRunStatus, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES } from "@kici-dev/engine";
|
|
3
3
|
import { AdminApiClient } from "@kici-dev/orchestrator";
|
|
4
4
|
//#region src/local-plane/run-follow.ts
|
|
5
5
|
/**
|
|
@@ -7,29 +7,76 @@ import { AdminApiClient } from "@kici-dev/orchestrator";
|
|
|
7
7
|
* orchestrator admin API (no SSE in independent mode). Streams step log lines as
|
|
8
8
|
* they arrive and resolves when the run reaches a terminal state.
|
|
9
9
|
*/
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Job states that mean the job did NOT succeed (failed / stale / cancelled /
|
|
12
|
+
* dropped / unroutable). Read only by the anti-false-green guard below, so a
|
|
13
|
+
* missing member lets a lagging `success` run header be reported verbatim even
|
|
14
|
+
* though a job settled badly.
|
|
15
|
+
*/
|
|
11
16
|
const FAILED_JOB_STATES = /* @__PURE__ */ new Set([
|
|
12
17
|
ExecutionJobStatus.enum.failed,
|
|
13
18
|
ExecutionJobStatus.enum.timed_out_stale,
|
|
14
19
|
ExecutionJobStatus.enum.cancelled,
|
|
15
|
-
ExecutionJobStatus.enum.drift_dropped
|
|
20
|
+
ExecutionJobStatus.enum.drift_dropped,
|
|
21
|
+
ExecutionJobStatus.enum.unroutable
|
|
16
22
|
]);
|
|
23
|
+
/** Job states in which no agent has claimed the job yet. */
|
|
24
|
+
const UNCLAIMED_JOB_STATES = /* @__PURE__ */ new Set([ExecutionJobStatus.enum.pending, ExecutionJobStatus.enum.queued]);
|
|
25
|
+
/**
|
|
26
|
+
* How often the acceptance check re-fetches jobs. Deliberately far coarser than
|
|
27
|
+
* the poll interval: at 750 ms a two-minute window would cost ~160 extra admin
|
|
28
|
+
* requests to answer a question that changes once.
|
|
29
|
+
*/
|
|
30
|
+
const ACCEPTANCE_CHECK_INTERVAL_MS = 5e3;
|
|
31
|
+
/** Default window for an agent to claim the run before the follow gives up. */
|
|
32
|
+
const DEFAULT_ACCEPTANCE_TIMEOUT_MS = 12e4;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the acceptance window, falling back to the default for any value that
|
|
35
|
+
* is not a positive number of milliseconds.
|
|
36
|
+
*
|
|
37
|
+
* A malformed override must never reach the deadline arithmetic: `Number('')`
|
|
38
|
+
* is `0` (every run fails instantly) and `Number('2 min')` is `NaN`, which makes
|
|
39
|
+
* `now > deadline` permanently false and silently restores the 900 s hang this
|
|
40
|
+
* window exists to prevent. Both failure modes are worse than ignoring a typo.
|
|
41
|
+
*/
|
|
42
|
+
function resolveAcceptanceTimeoutMs(explicitMs, raw) {
|
|
43
|
+
if (explicitMs !== void 0) return explicitMs;
|
|
44
|
+
if (raw === void 0) return DEFAULT_ACCEPTANCE_TIMEOUT_MS;
|
|
45
|
+
const parsed = Number(raw);
|
|
46
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ACCEPTANCE_TIMEOUT_MS;
|
|
47
|
+
}
|
|
17
48
|
/** Poll a run to completion, streaming step logs. */
|
|
18
49
|
async function followRun(planeUrl, adminToken, runId, opts = {}) {
|
|
19
50
|
const client = opts.client ?? new AdminApiClient(planeUrl, adminToken);
|
|
20
51
|
const pollIntervalMs = opts.pollIntervalMs ?? 750;
|
|
21
52
|
const idleTimeoutMs = opts.idleTimeoutMs ?? opts.timeoutMs ?? 9e5;
|
|
22
53
|
const maxTotalMs = opts.maxTotalMs ?? 72e5;
|
|
54
|
+
const acceptanceTimeoutMs = resolveAcceptanceTimeoutMs(opts.acceptanceTimeoutMs, process.env.KICI_LOCAL_ACCEPTANCE_TIMEOUT_MS);
|
|
23
55
|
const cursors = /* @__PURE__ */ new Map();
|
|
24
56
|
const stream = !opts.quiet && opts.onLine ? opts.onLine : void 0;
|
|
25
57
|
const startedAt = Date.now();
|
|
26
58
|
let lastProgressAt = startedAt;
|
|
27
59
|
let lastStatus = "";
|
|
60
|
+
let accepted = false;
|
|
61
|
+
let acceptanceDeadline = startedAt + acceptanceTimeoutMs;
|
|
62
|
+
let lastAcceptanceCheck = 0;
|
|
28
63
|
for (;;) {
|
|
29
64
|
const now = Date.now();
|
|
30
65
|
if (now - lastProgressAt > idleTimeoutMs) throw new Error(`offline run: follow timed out (no progress for ${idleTimeoutMs}ms) for run ${runId}`);
|
|
31
66
|
if (now - startedAt > maxTotalMs) throw new Error(`offline run: follow exceeded its max window (${maxTotalMs}ms) for run ${runId}`);
|
|
32
67
|
const { run } = await client.get(`/api/v1/admin/runs/${runId}`);
|
|
68
|
+
if (!accepted && !TERMINAL_RUN_STATES.has(run.status)) {
|
|
69
|
+
if (run.status === ExecutionRunStatus.enum.held) acceptanceDeadline = now + acceptanceTimeoutMs;
|
|
70
|
+
else if (now - lastAcceptanceCheck >= ACCEPTANCE_CHECK_INTERVAL_MS) {
|
|
71
|
+
lastAcceptanceCheck = now;
|
|
72
|
+
const claimedJobs = await fetchJobs(client, runId);
|
|
73
|
+
accepted = claimedJobs.length > 0 && claimedJobs.some((j) => !UNCLAIMED_JOB_STATES.has(j.status));
|
|
74
|
+
}
|
|
75
|
+
if (!accepted && now > acceptanceDeadline) {
|
|
76
|
+
const seconds = Math.round(acceptanceTimeoutMs / 1e3);
|
|
77
|
+
throw new Error(`offline run: no agent picked up this run within ${seconds}s — no scaler label set matched the job's runsOn, or the agent failed to start.` + (opts.hintLogPath ? ` Plane log: ${opts.hintLogPath}` : ""));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
33
80
|
let progressed = false;
|
|
34
81
|
if (run.status !== lastStatus) {
|
|
35
82
|
lastStatus = run.status;
|
|
@@ -99,6 +146,6 @@ function sleep(ms) {
|
|
|
99
146
|
return new Promise((r) => setTimeout(r, ms));
|
|
100
147
|
}
|
|
101
148
|
//#endregion
|
|
102
|
-
export { followRun };
|
|
149
|
+
export { followRun, resolveAcceptanceTimeoutMs };
|
|
103
150
|
|
|
104
151
|
//# sourceMappingURL=run-follow.js.map
|
|
@@ -34,21 +34,35 @@ export declare const TRUSTED_ROUTING_LABEL = "self-hosted";
|
|
|
34
34
|
*/
|
|
35
35
|
export declare const IN_PLACE_ROUTING_LABEL = "in-place";
|
|
36
36
|
/**
|
|
37
|
-
* Write the plane's bare-metal scaler YAML and return its path.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Write the plane's bare-metal scaler YAML and return its path. TWO coexisting
|
|
38
|
+
* bare-metal scalers spawn the agent wrapper against the plane orchestrator's
|
|
39
|
+
* localhost WS endpoint — a **sandboxed** pool and a `self-hosted`-**tainted**
|
|
40
|
+
* TRUSTED pool:
|
|
40
41
|
*
|
|
41
|
-
* - `
|
|
42
|
-
* `kici run --local` dispatch (jobs with
|
|
43
|
-
* on.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
42
|
+
* - `kici-local-bare-metal` (sandboxed) — one `default` label set, no taint. The
|
|
43
|
+
* credential-isolated profile a normal `kici run --local` dispatch (jobs with
|
|
44
|
+
* `runsOn: ['default']` or none) lands on. Its agents inherit no
|
|
45
|
+
* `mandatoryLabels`, so a bare `default` job routes here.
|
|
46
|
+
* - `kici-local-bare-metal-trusted` (TRUSTED) — carries `mandatoryLabels:
|
|
47
|
+
* ['self-hosted']`, so ONLY a job that explicitly requests `self-hosted`
|
|
48
|
+
* (i.e. a `kici run --local --trusted` run, whose lock patch appends the
|
|
49
|
+
* `self-hosted` routing label, or a `runsOn: self-hosted` workflow) is ever
|
|
50
|
+
* routed to it — at BOTH the scaler spawn matcher AND the orchestrator's
|
|
51
|
+
* agent-registry dispatch gate. Two label sets:
|
|
52
|
+
* - `default` + `self-hosted` — the trusted fleet profile
|
|
53
|
+
* (`KICI_TRUSTED_ENV=true`, bwrap off): steps run with the ambient host
|
|
54
|
+
* env passed through, minus the agent's own KiCI identity secrets.
|
|
55
|
+
* - `default` + `self-hosted` + `in-place` — the trusted **in-place**
|
|
56
|
+
* profile (adds `KICI_IN_PLACE=true`) a `kici run --local --trusted
|
|
57
|
+
* --in-place` run lands on: same trusted env, but the agent uses the
|
|
58
|
+
* operator's real working tree directly (no clone) — the profile KiCI's
|
|
59
|
+
* own routed `deploy:stg` uses.
|
|
60
|
+
*
|
|
61
|
+
* The `self-hosted` taint is the isolation guarantee: without it, an idle
|
|
62
|
+
* trusted agent (spawned for an earlier `--trusted` run and lingering for
|
|
63
|
+
* follow-up jobs) would satisfy the agent-registry's subset match for a plain
|
|
64
|
+
* `default` job and leak the ambient host env into a sandboxed run. The taint
|
|
65
|
+
* closes that at the dispatch layer, not just the spawn heuristic.
|
|
52
66
|
*/
|
|
53
67
|
export declare function writeScalerConfig(orchestratorPort: number): string;
|
|
54
68
|
//# sourceMappingURL=scaler-config.d.ts.map
|
|
@@ -49,60 +49,78 @@ const TRUSTED_ROUTING_LABEL = "self-hosted";
|
|
|
49
49
|
*/
|
|
50
50
|
const IN_PLACE_ROUTING_LABEL = "in-place";
|
|
51
51
|
/**
|
|
52
|
-
* Write the plane's bare-metal scaler YAML and return its path.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* Write the plane's bare-metal scaler YAML and return its path. TWO coexisting
|
|
53
|
+
* bare-metal scalers spawn the agent wrapper against the plane orchestrator's
|
|
54
|
+
* localhost WS endpoint — a **sandboxed** pool and a `self-hosted`-**tainted**
|
|
55
|
+
* TRUSTED pool:
|
|
55
56
|
*
|
|
56
|
-
* - `
|
|
57
|
-
* `kici run --local` dispatch (jobs with
|
|
58
|
-
* on.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
57
|
+
* - `kici-local-bare-metal` (sandboxed) — one `default` label set, no taint. The
|
|
58
|
+
* credential-isolated profile a normal `kici run --local` dispatch (jobs with
|
|
59
|
+
* `runsOn: ['default']` or none) lands on. Its agents inherit no
|
|
60
|
+
* `mandatoryLabels`, so a bare `default` job routes here.
|
|
61
|
+
* - `kici-local-bare-metal-trusted` (TRUSTED) — carries `mandatoryLabels:
|
|
62
|
+
* ['self-hosted']`, so ONLY a job that explicitly requests `self-hosted`
|
|
63
|
+
* (i.e. a `kici run --local --trusted` run, whose lock patch appends the
|
|
64
|
+
* `self-hosted` routing label, or a `runsOn: self-hosted` workflow) is ever
|
|
65
|
+
* routed to it — at BOTH the scaler spawn matcher AND the orchestrator's
|
|
66
|
+
* agent-registry dispatch gate. Two label sets:
|
|
67
|
+
* - `default` + `self-hosted` — the trusted fleet profile
|
|
68
|
+
* (`KICI_TRUSTED_ENV=true`, bwrap off): steps run with the ambient host
|
|
69
|
+
* env passed through, minus the agent's own KiCI identity secrets.
|
|
70
|
+
* - `default` + `self-hosted` + `in-place` — the trusted **in-place**
|
|
71
|
+
* profile (adds `KICI_IN_PLACE=true`) a `kici run --local --trusted
|
|
72
|
+
* --in-place` run lands on: same trusted env, but the agent uses the
|
|
73
|
+
* operator's real working tree directly (no clone) — the profile KiCI's
|
|
74
|
+
* own routed `deploy:stg` uses.
|
|
75
|
+
*
|
|
76
|
+
* The `self-hosted` taint is the isolation guarantee: without it, an idle
|
|
77
|
+
* trusted agent (spawned for an earlier `--trusted` run and lingering for
|
|
78
|
+
* follow-up jobs) would satisfy the agent-registry's subset match for a plain
|
|
79
|
+
* `default` job and leak the ambient host env into a sandboxed run. The taint
|
|
80
|
+
* closes that at the dispatch layer, not just the spawn heuristic.
|
|
67
81
|
*/
|
|
68
82
|
function writeScalerConfig(orchestratorPort) {
|
|
69
83
|
const { scalerConfigFile, root } = planePaths();
|
|
70
84
|
fs.mkdirSync(root, { recursive: true });
|
|
71
85
|
const binaryPath = writeAgentWrapper();
|
|
86
|
+
const orchestratorUrl = `ws://127.0.0.1:${orchestratorPort}/ws`;
|
|
72
87
|
const yaml = stringify({
|
|
73
88
|
version: 1,
|
|
74
89
|
scalers: [{
|
|
75
90
|
name: "kici-local-bare-metal",
|
|
76
91
|
type: "bare-metal",
|
|
77
92
|
maxAgents: 10,
|
|
78
|
-
orchestratorUrl
|
|
79
|
-
labelSets: [
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
orchestratorUrl,
|
|
94
|
+
labelSets: [{
|
|
95
|
+
labels: ["default"],
|
|
96
|
+
binaryPath
|
|
97
|
+
}]
|
|
98
|
+
}, {
|
|
99
|
+
name: "kici-local-bare-metal-trusted",
|
|
100
|
+
type: "bare-metal",
|
|
101
|
+
maxAgents: 10,
|
|
102
|
+
orchestratorUrl,
|
|
103
|
+
mandatoryLabels: [TRUSTED_ROUTING_LABEL],
|
|
104
|
+
labelSets: [{
|
|
105
|
+
labels: ["default", TRUSTED_ROUTING_LABEL],
|
|
106
|
+
binaryPath,
|
|
107
|
+
env: {
|
|
108
|
+
KICI_TRUSTED_ENV: "true",
|
|
109
|
+
KICI_SANDBOX: "false"
|
|
110
|
+
}
|
|
111
|
+
}, {
|
|
112
|
+
labels: [
|
|
113
|
+
"default",
|
|
114
|
+
TRUSTED_ROUTING_LABEL,
|
|
115
|
+
IN_PLACE_ROUTING_LABEL
|
|
116
|
+
],
|
|
117
|
+
binaryPath,
|
|
118
|
+
env: {
|
|
119
|
+
KICI_TRUSTED_ENV: "true",
|
|
120
|
+
KICI_SANDBOX: "false",
|
|
121
|
+
KICI_IN_PLACE: "true"
|
|
104
122
|
}
|
|
105
|
-
]
|
|
123
|
+
}]
|
|
106
124
|
}]
|
|
107
125
|
});
|
|
108
126
|
fs.writeFileSync(scalerConfigFile, yaml);
|