@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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
2
|
import { planePaths, planePorts } from "./paths.js";
|
|
3
|
-
import {
|
|
3
|
+
import { awaitOrchestratorReady, resolveServerEntry, resolveStandaloneEntry, spawnOrchestratorProcess } from "./orchestrator-process.js";
|
|
4
|
+
import { isPortFree, processCommandLine, terminatePid } from "./port-holder.js";
|
|
5
|
+
import { classificationPid, classifyPlane, planeStateOf, reclaimPlanePort } from "./plane-liveness.js";
|
|
4
6
|
import { startPlanePostgres, stopPlanePostgres } from "./postgres.js";
|
|
5
7
|
import { writeScalerConfig } from "./scaler-config.js";
|
|
6
8
|
import { derivePlatformWsUrl, mintOrchestratorKey, revokeOrchestratorKey } from "./platform-attach.js";
|
|
@@ -8,7 +10,19 @@ import path from "node:path";
|
|
|
8
10
|
import fs from "node:fs";
|
|
9
11
|
import { generateKeyPairSync, randomBytes } from "node:crypto";
|
|
10
12
|
//#region src/local-plane/plane-manager.ts
|
|
11
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The current CLI build's identity — semver plus git build commit — read at
|
|
15
|
+
* call time from the Rolldown-injected build constants (`scripts/build-ts.mjs`).
|
|
16
|
+
* Read on each call rather than captured in a module const so unit tests can
|
|
17
|
+
* inject the constants via `globalThis`. Falls back to `0.0.0` / `unknown` when
|
|
18
|
+
* running from source (unbuilt tree / vitest), where the defines are absent.
|
|
19
|
+
*/
|
|
20
|
+
function currentBuildIdentity() {
|
|
21
|
+
return {
|
|
22
|
+
version: "0.2.0",
|
|
23
|
+
buildCommit: "15d5e4447"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
12
26
|
/**
|
|
13
27
|
* Read the plane's persisted Platform orchestrator token (mode 0600), or null
|
|
14
28
|
* when the plane is independent/offline. Kept out of the stamp so the token
|
|
@@ -139,6 +153,28 @@ function writeStamp(stamp) {
|
|
|
139
153
|
fs.writeFileSync(stampFile, JSON.stringify(stamp, null, 2));
|
|
140
154
|
fs.writeFileSync(pidfile, String(stamp.orchestratorPid));
|
|
141
155
|
}
|
|
156
|
+
/** Remove the plane's on-disk record (pidfile + stamp). */
|
|
157
|
+
function clearStamp() {
|
|
158
|
+
const { pidfile, stampFile } = planePaths();
|
|
159
|
+
fs.rmSync(pidfile, { force: true });
|
|
160
|
+
fs.rmSync(stampFile, { force: true });
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Whether a running plane described by `existing` was booted from a different
|
|
164
|
+
* CLI build than the current one — a semver bump OR a git-commit change (the
|
|
165
|
+
* latter covers intermediate staging/E2E commits that share a semver). Returns
|
|
166
|
+
* false when there is no stamp, or when the current build has no concrete
|
|
167
|
+
* identity (`buildCommit === 'unknown'`, i.e. running from source / a test),
|
|
168
|
+
* so a source-context `planeUp` never reboots a healthy plane spuriously. An
|
|
169
|
+
* old stamp with no `buildCommit` field reads as `undefined` and therefore
|
|
170
|
+
* triggers a one-time reboot on the first upgrade past this feature.
|
|
171
|
+
*/
|
|
172
|
+
function planeBuildIsStale(existing) {
|
|
173
|
+
if (!existing) return false;
|
|
174
|
+
const { version, buildCommit } = currentBuildIdentity();
|
|
175
|
+
if (buildCommit === "unknown") return false;
|
|
176
|
+
return existing.kiciVersion !== version || existing.buildCommit !== buildCommit;
|
|
177
|
+
}
|
|
142
178
|
function orchestratorUrl(port) {
|
|
143
179
|
return `http://127.0.0.1:${port}`;
|
|
144
180
|
}
|
|
@@ -146,57 +182,135 @@ function orchestratorUrl(port) {
|
|
|
146
182
|
function planeLogPath() {
|
|
147
183
|
return planePaths().logFile;
|
|
148
184
|
}
|
|
149
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Read the plane's current status from whatever holds the plane port.
|
|
187
|
+
*
|
|
188
|
+
* `running` means "serving": a live orchestrator whose `/ready` fails is
|
|
189
|
+
* reported as `unready`, with the failing checks, rather than as stopped —
|
|
190
|
+
* inferring "not running" from an unready probe hides a process that still owns
|
|
191
|
+
* the port.
|
|
192
|
+
*/
|
|
150
193
|
async function planeStatus() {
|
|
151
194
|
const stamp = readStamp();
|
|
152
195
|
const port = stamp?.port ?? planePorts().orchestrator;
|
|
153
|
-
|
|
196
|
+
const classification = await classifyPlane(port, stamp);
|
|
197
|
+
const mode = stamp?.mode ?? "independent";
|
|
198
|
+
const attachment = readAttachment() ?? void 0;
|
|
199
|
+
if (classification.kind === "free") return {
|
|
154
200
|
running: false,
|
|
155
|
-
|
|
156
|
-
|
|
201
|
+
state: "stopped",
|
|
202
|
+
mode,
|
|
203
|
+
attachment
|
|
157
204
|
};
|
|
158
|
-
return {
|
|
205
|
+
if (classification.kind === "ours-ready") return {
|
|
159
206
|
running: true,
|
|
160
|
-
|
|
207
|
+
state: "ready",
|
|
208
|
+
pid: classification.pid,
|
|
161
209
|
port,
|
|
162
210
|
pgKind: stamp?.pgKind,
|
|
163
211
|
stampVersion: stamp?.stampVersion,
|
|
164
212
|
url: orchestratorUrl(port),
|
|
165
213
|
adminToken: planeAdminToken() ?? void 0,
|
|
166
|
-
mode
|
|
167
|
-
attachment
|
|
214
|
+
mode,
|
|
215
|
+
attachment
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
running: false,
|
|
219
|
+
state: planeStateOf(classification),
|
|
220
|
+
pid: classificationPid(classification) ?? void 0,
|
|
221
|
+
port,
|
|
222
|
+
pgKind: stamp?.pgKind,
|
|
223
|
+
url: orchestratorUrl(port),
|
|
224
|
+
checks: classification.kind === "ours-unready" ? classification.checks : void 0,
|
|
225
|
+
mode,
|
|
226
|
+
attachment
|
|
168
227
|
};
|
|
169
228
|
}
|
|
170
229
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
230
|
+
* Tear the plane down ahead of a fresh boot, refusing to continue when the port
|
|
231
|
+
* was not released.
|
|
232
|
+
*
|
|
233
|
+
* Booting over a still-held port only re-fails later, as a readiness timeout
|
|
234
|
+
* that names the log instead of the survivor — so the teardown's own reason (the
|
|
235
|
+
* pid still holding the port) is raised here, while it is still the accurate
|
|
236
|
+
* one.
|
|
237
|
+
*/
|
|
238
|
+
async function tearDownForBoot() {
|
|
239
|
+
const result = await planeDown();
|
|
240
|
+
if (result.stopped) return;
|
|
241
|
+
const holder = result.holderPid === void 0 ? "" : ` (pid ${result.holderPid})`;
|
|
242
|
+
throw new Error(result.reason ?? `port ${result.port} is still held after stopping the local dev plane${holder}`);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Ensure the plane port is either reusable or vacant before a boot.
|
|
246
|
+
*
|
|
247
|
+
* Returns the running plane when it may be reused as-is, or null when the caller
|
|
248
|
+
* must boot fresh. A plane no stamp accounts for is reclaimed rather than
|
|
249
|
+
* adopted: reuse would serve `kici run --local` from an orchestrator of unknown
|
|
250
|
+
* build, with an unknown scaler configuration, which is exactly the state a
|
|
251
|
+
* stamped plane's staleness check exists to prevent.
|
|
252
|
+
*/
|
|
253
|
+
async function prepareForBoot(port, existing, requestedMode) {
|
|
254
|
+
const classification = await classifyPlane(port, existing);
|
|
255
|
+
switch (classification.kind) {
|
|
256
|
+
case "free": return null;
|
|
257
|
+
case "foreign-unknown": {
|
|
258
|
+
const reclaim = await reclaimPlanePort(port, classification);
|
|
259
|
+
throw new Error(reclaim.error ?? `port ${port} is held by an unrecognised process`);
|
|
260
|
+
}
|
|
261
|
+
case "foreign-kici": {
|
|
262
|
+
const reclaim = await reclaimPlanePort(port, classification);
|
|
263
|
+
if (!reclaim.freed) throw new Error(reclaim.error ?? `could not reclaim port ${port}`);
|
|
264
|
+
clearStamp();
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
case "ours-unready":
|
|
268
|
+
await tearDownForBoot();
|
|
269
|
+
return null;
|
|
270
|
+
case "ours-ready": {
|
|
271
|
+
const modeChanged = (existing?.mode ?? "independent") !== requestedMode;
|
|
272
|
+
if (planeBuildIsStale(existing) || modeChanged) {
|
|
273
|
+
await tearDownForBoot();
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
running: true,
|
|
278
|
+
state: "ready",
|
|
279
|
+
pid: classification.pid,
|
|
280
|
+
port,
|
|
281
|
+
pgKind: existing?.pgKind,
|
|
282
|
+
stampVersion: existing?.stampVersion,
|
|
283
|
+
url: orchestratorUrl(port),
|
|
284
|
+
adminToken: planeAdminToken() ?? void 0,
|
|
285
|
+
mode: existing?.mode ?? "independent",
|
|
286
|
+
attachment: readAttachment() ?? void 0
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Start (or reuse) the local dev plane. Idempotent: a healthy running plane this
|
|
293
|
+
* config dir stamped, **whose stamped mode matches the requested mode**, is
|
|
294
|
+
* returned as-is. Otherwise the port is reclaimed if it is another KiCI plane,
|
|
295
|
+
* and Postgres + the orchestrator are booted with a fresh stamp + pidfile. When
|
|
175
296
|
* `attach` is present the orchestrator boots hybrid against the Platform relay;
|
|
176
|
-
* otherwise it boots independent with the dev-signed identity.
|
|
297
|
+
* otherwise it boots independent with the dev-signed identity. Throws when the
|
|
298
|
+
* port is held by a process that is not a KiCI plane orchestrator.
|
|
177
299
|
*/
|
|
178
300
|
async function planeUp(opts = {}) {
|
|
179
301
|
const { orchestrator: port } = planePorts();
|
|
180
302
|
const existing = readStamp();
|
|
181
303
|
const requestedMode = opts.attach ? "hybrid" : "independent";
|
|
182
304
|
if (existing && existing.stampVersion !== 3) {
|
|
183
|
-
await
|
|
305
|
+
await tearDownForBoot();
|
|
184
306
|
fs.rmSync(planePaths().pgData, {
|
|
185
307
|
recursive: true,
|
|
186
308
|
force: true
|
|
187
309
|
});
|
|
188
|
-
} else
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
port,
|
|
193
|
-
pgKind: existing?.pgKind,
|
|
194
|
-
stampVersion: existing?.stampVersion,
|
|
195
|
-
url: orchestratorUrl(port),
|
|
196
|
-
adminToken: planeAdminToken() ?? void 0,
|
|
197
|
-
mode: existing?.mode ?? "independent",
|
|
198
|
-
attachment: readAttachment() ?? void 0
|
|
199
|
-
};
|
|
310
|
+
} else {
|
|
311
|
+
const reusable = await prepareForBoot(port, existing, requestedMode);
|
|
312
|
+
if (reusable) return reusable;
|
|
313
|
+
}
|
|
200
314
|
const adminToken = readOrCreateAdminToken();
|
|
201
315
|
const secretKey = readOrCreateSecretKey();
|
|
202
316
|
const scalerConfigFile = writeScalerConfig(port);
|
|
@@ -210,7 +324,7 @@ async function planeUp(opts = {}) {
|
|
|
210
324
|
orgId: opts.attach.orgId,
|
|
211
325
|
keyId: opts.attach.keyId
|
|
212
326
|
});
|
|
213
|
-
orch =
|
|
327
|
+
orch = spawnOrchestratorProcess(pg.url, {
|
|
214
328
|
adminToken,
|
|
215
329
|
secretKey,
|
|
216
330
|
scalerConfigFile,
|
|
@@ -221,23 +335,33 @@ async function planeUp(opts = {}) {
|
|
|
221
335
|
});
|
|
222
336
|
} else {
|
|
223
337
|
const devIdentityKey = readOrCreateDevIdentity();
|
|
224
|
-
orch =
|
|
338
|
+
orch = spawnOrchestratorProcess(pg.url, {
|
|
225
339
|
adminToken,
|
|
226
340
|
secretKey,
|
|
227
341
|
scalerConfigFile,
|
|
228
342
|
devIdentityKeyFile: devIdentityKey
|
|
229
343
|
});
|
|
230
344
|
}
|
|
345
|
+
const { version: stampVersionSemver, buildCommit: stampBuildCommit } = currentBuildIdentity();
|
|
231
346
|
writeStamp({
|
|
232
347
|
orchestratorPid: orch.pid,
|
|
233
348
|
port: orch.port,
|
|
234
349
|
pgKind: pg.kind,
|
|
235
|
-
kiciVersion,
|
|
350
|
+
kiciVersion: stampVersionSemver,
|
|
351
|
+
buildCommit: stampBuildCommit,
|
|
236
352
|
stampVersion: 3,
|
|
237
353
|
mode: requestedMode
|
|
238
354
|
});
|
|
355
|
+
try {
|
|
356
|
+
await awaitOrchestratorReady(orch.port);
|
|
357
|
+
} catch {
|
|
358
|
+
await terminatePid(orch.pid);
|
|
359
|
+
if (await isPortFree(orch.port)) clearStamp();
|
|
360
|
+
throw new Error(`local orchestrator did not become ready — see ${planeLogPath()}`);
|
|
361
|
+
}
|
|
239
362
|
return {
|
|
240
363
|
running: true,
|
|
364
|
+
state: "ready",
|
|
241
365
|
pid: orch.pid,
|
|
242
366
|
port: orch.port,
|
|
243
367
|
pgKind: pg.kind,
|
|
@@ -249,49 +373,65 @@ async function planeUp(opts = {}) {
|
|
|
249
373
|
};
|
|
250
374
|
}
|
|
251
375
|
/**
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
376
|
+
* Whether the stamped pid is still one of this build's plane orchestrators.
|
|
377
|
+
*
|
|
378
|
+
* The stamp outlives the process it names — a crash, a `kill -9`, or a reboot
|
|
379
|
+
* all leave it behind — so once the operating system recycles that pid onto an
|
|
380
|
+
* unrelated process, signalling it blind would stop a stranger's program. Every
|
|
381
|
+
* other signal path in the plane is identity-verified (`reclaimPlanePort` only
|
|
382
|
+
* ever signals a pid confirmed to hold the port or to answer `/health` as a KiCI
|
|
383
|
+
* orchestrator); this keeps the stamp-driven path to the same standard by
|
|
384
|
+
* matching the live command line against the entry this build would launch.
|
|
385
|
+
*
|
|
386
|
+
* Returns false whenever identity cannot be established — an unreadable command
|
|
387
|
+
* line, an unresolvable entry, or a plane launched from a different install.
|
|
388
|
+
* That is deliberate: not signalling leaves a process running, while signalling
|
|
389
|
+
* the wrong pid kills one.
|
|
256
390
|
*/
|
|
257
|
-
async function
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
await waitForProcessExit(stamp.orchestratorPid);
|
|
265
|
-
await stopPlanePostgres(stamp.pgKind);
|
|
266
|
-
}
|
|
267
|
-
fs.rmSync(pidfile, { force: true });
|
|
268
|
-
fs.rmSync(stampFile, { force: true });
|
|
391
|
+
async function stampedPidIsOurOrchestrator(pid) {
|
|
392
|
+
const cmdline = await processCommandLine(pid);
|
|
393
|
+
if (!cmdline) return false;
|
|
394
|
+
for (const resolveEntry of [resolveStandaloneEntry, resolveServerEntry]) try {
|
|
395
|
+
if (cmdline.includes(resolveEntry())) return true;
|
|
396
|
+
} catch {}
|
|
397
|
+
return false;
|
|
269
398
|
}
|
|
270
399
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
400
|
+
* Stop the local dev plane and confirm the port was released.
|
|
401
|
+
*
|
|
402
|
+
* Teardown is reconstructed from whatever holds the port, not only from the
|
|
403
|
+
* stamp: a plane this config dir never stamped still occupies the port and is
|
|
404
|
+
* still ours to stop. The pidfile and stamp are cleared only once the port is
|
|
405
|
+
* verified free, so a survivor never loses the pid that identifies it.
|
|
275
406
|
*/
|
|
276
|
-
async function
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
407
|
+
async function planeDown() {
|
|
408
|
+
const stamp = readStamp();
|
|
409
|
+
const port = stamp?.port ?? planePorts().orchestrator;
|
|
410
|
+
const classification = await classifyPlane(port, stamp);
|
|
411
|
+
const reclaim = await reclaimPlanePort(port, classification);
|
|
412
|
+
if (!reclaim.freed) return {
|
|
413
|
+
stopped: false,
|
|
414
|
+
port,
|
|
415
|
+
holderPid: reclaim.killedPid ?? classificationPid(classification) ?? void 0,
|
|
416
|
+
reason: reclaim.error
|
|
417
|
+
};
|
|
418
|
+
if (stamp && stamp.orchestratorPid !== reclaim.killedPid && await stampedPidIsOurOrchestrator(stamp.orchestratorPid)) {
|
|
419
|
+
await terminatePid(stamp.orchestratorPid);
|
|
420
|
+
if (!await isPortFree(port)) return {
|
|
421
|
+
stopped: false,
|
|
422
|
+
port,
|
|
423
|
+
holderPid: stamp.orchestratorPid,
|
|
424
|
+
reason: `port ${port} is still held after stopping pid ${stamp.orchestratorPid}`
|
|
425
|
+
};
|
|
294
426
|
}
|
|
427
|
+
if (stamp) await stopPlanePostgres(stamp.pgKind);
|
|
428
|
+
else if (classification.kind !== "free") await stopPlanePostgres("embedded");
|
|
429
|
+
clearStamp();
|
|
430
|
+
return {
|
|
431
|
+
stopped: true,
|
|
432
|
+
port,
|
|
433
|
+
holderPid: reclaim.killedPid
|
|
434
|
+
};
|
|
295
435
|
}
|
|
296
436
|
/**
|
|
297
437
|
* Attach the local dev plane to the hosted Platform: mint an org-scoped
|
|
@@ -335,6 +475,6 @@ async function detachPlane(opts = {}) {
|
|
|
335
475
|
return planeUp();
|
|
336
476
|
}
|
|
337
477
|
//#endregion
|
|
338
|
-
export { attachPlane, clearAttachment, clearPlatformToken, detachPlane, devIdentityKeyFile, devIdentityPublicJwkFile, planeAdminToken, planeDown, planeLogPath, planeStatus, planeUp, readAttachment, readOrCreateAdminToken, readOrCreateDevIdentity, readOrCreateSecretKey, readPlatformToken,
|
|
478
|
+
export { attachPlane, clearAttachment, clearPlatformToken, clearStamp, currentBuildIdentity, detachPlane, devIdentityKeyFile, devIdentityPublicJwkFile, planeAdminToken, planeBuildIsStale, planeDown, planeLogPath, planeStatus, planeUp, readAttachment, readOrCreateAdminToken, readOrCreateDevIdentity, readOrCreateSecretKey, readPlatformToken, writeAttachment, writePlatformToken };
|
|
339
479
|
|
|
340
480
|
//# sourceMappingURL=plane-manager.js.map
|
|
@@ -45,8 +45,14 @@ export interface LocalTriggerRequest {
|
|
|
45
45
|
* of this phase — no orchestrator export is added).
|
|
46
46
|
*/
|
|
47
47
|
export declare function buildLocalTriggerRequest(input: LocalTriggerInput): LocalTriggerRequest;
|
|
48
|
-
/**
|
|
49
|
-
export
|
|
48
|
+
/** Structured result of posting a local trigger to the plane. */
|
|
49
|
+
export interface LocalTriggerResponse {
|
|
50
|
+
status: number;
|
|
51
|
+
/** Routing-key-scoped delivery id echoed by the plane, or null if absent. */
|
|
52
|
+
deliveryId: string | null;
|
|
53
|
+
}
|
|
54
|
+
/** POST the trigger request to the plane. Returns the status + delivery id. */
|
|
55
|
+
export declare function sendLocalTrigger(planeUrl: string, req: LocalTriggerRequest): Promise<LocalTriggerResponse>;
|
|
50
56
|
export interface TriggerRunOptions {
|
|
51
57
|
client?: RunDiscoveryClient;
|
|
52
58
|
pollIntervalMs?: number;
|
|
@@ -55,8 +61,11 @@ export interface TriggerRunOptions {
|
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* Trigger the run and resolve its runId. Sends the synthetic push, then polls
|
|
58
|
-
* the admin runs list
|
|
59
|
-
* webhook past a grace window to absorb source
|
|
64
|
+
* the admin runs list filtered by this webhook's routing-key-scoped delivery id
|
|
65
|
+
* (`?deliveryId=`), resending the webhook past a grace window to absorb source
|
|
66
|
+
* hot-reload latency. Correlating on the exact delivery id — rather than the
|
|
67
|
+
* newest run in a time window — guarantees the follower attaches to the run
|
|
68
|
+
* THIS invocation created, even when another local run lands concurrently.
|
|
60
69
|
*/
|
|
61
70
|
export declare function triggerRun(planeUrl: string, adminToken: string, input: LocalTriggerInput, opts?: TriggerRunOptions): Promise<string>;
|
|
62
71
|
//# sourceMappingURL=plane-trigger.d.ts.map
|
|
@@ -45,45 +45,58 @@ function buildLocalTriggerRequest(input) {
|
|
|
45
45
|
body
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
/** POST the trigger request to the plane. Returns the
|
|
48
|
+
/** POST the trigger request to the plane. Returns the status + delivery id. */
|
|
49
49
|
async function sendLocalTrigger(planeUrl, req) {
|
|
50
|
-
|
|
50
|
+
const res = await fetch(`${planeUrl.replace(/\/$/, "")}${req.path}`, {
|
|
51
51
|
method: "POST",
|
|
52
52
|
headers: req.headers,
|
|
53
53
|
body: req.body
|
|
54
|
-
})
|
|
54
|
+
});
|
|
55
|
+
let deliveryId = null;
|
|
56
|
+
try {
|
|
57
|
+
const parsed = await res.json();
|
|
58
|
+
if (typeof parsed.deliveryId === "string") deliveryId = parsed.deliveryId;
|
|
59
|
+
} catch {}
|
|
60
|
+
return {
|
|
61
|
+
status: res.status,
|
|
62
|
+
deliveryId
|
|
63
|
+
};
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
57
66
|
* Trigger the run and resolve its runId. Sends the synthetic push, then polls
|
|
58
|
-
* the admin runs list
|
|
59
|
-
* webhook past a grace window to absorb source
|
|
67
|
+
* the admin runs list filtered by this webhook's routing-key-scoped delivery id
|
|
68
|
+
* (`?deliveryId=`), resending the webhook past a grace window to absorb source
|
|
69
|
+
* hot-reload latency. Correlating on the exact delivery id — rather than the
|
|
70
|
+
* newest run in a time window — guarantees the follower attaches to the run
|
|
71
|
+
* THIS invocation created, even when another local run lands concurrently.
|
|
60
72
|
*/
|
|
61
73
|
async function triggerRun(planeUrl, adminToken, input, opts = {}) {
|
|
62
74
|
const client = opts.client ?? new AdminApiClient(planeUrl, adminToken);
|
|
63
75
|
const pollIntervalMs = opts.pollIntervalMs ?? 750;
|
|
64
76
|
const resendAfterMs = opts.resendAfterMs ?? 8e3;
|
|
65
77
|
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
66
|
-
const since = /* @__PURE__ */ new Date(Date.now() - 3e3);
|
|
67
78
|
const req = buildLocalTriggerRequest(input);
|
|
68
|
-
await sendLocalTrigger(planeUrl, req);
|
|
79
|
+
let deliveryId = (await sendLocalTrigger(planeUrl, req)).deliveryId;
|
|
69
80
|
let lastSend = Date.now();
|
|
70
81
|
const deadline = Date.now() + timeoutMs;
|
|
71
82
|
while (Date.now() < deadline) {
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
if (deliveryId) {
|
|
84
|
+
const runId = await findRunByDelivery(client, deliveryId);
|
|
85
|
+
if (runId) return runId;
|
|
86
|
+
}
|
|
74
87
|
if (Date.now() - lastSend > resendAfterMs) {
|
|
75
|
-
await sendLocalTrigger(planeUrl, req);
|
|
88
|
+
deliveryId ??= (await sendLocalTrigger(planeUrl, req)).deliveryId;
|
|
76
89
|
lastSend = Date.now();
|
|
77
90
|
}
|
|
78
91
|
await sleep(pollIntervalMs);
|
|
79
92
|
}
|
|
80
93
|
throw new Error("offline run: no run appeared after triggering the local plane");
|
|
81
94
|
}
|
|
82
|
-
/** Return the
|
|
83
|
-
async function
|
|
95
|
+
/** Return the run created by this webhook delivery, or null when none yet. */
|
|
96
|
+
async function findRunByDelivery(client, deliveryId) {
|
|
84
97
|
const qs = new URLSearchParams({
|
|
85
|
-
|
|
86
|
-
limit: "
|
|
98
|
+
deliveryId,
|
|
99
|
+
limit: "1"
|
|
87
100
|
});
|
|
88
101
|
const { runs } = await client.get(`/api/v1/admin/runs?${qs}`);
|
|
89
102
|
return runs[0]?.runId ?? null;
|
|
@@ -99,11 +99,7 @@ async function probePlatformReachable(apiBase, timeoutMs = 4e3) {
|
|
|
99
99
|
}
|
|
100
100
|
/** A short, non-sensitive label for the minted key name (the machine host). */
|
|
101
101
|
function orgHostLabel() {
|
|
102
|
-
|
|
103
|
-
return process.env.HOSTNAME || "kici-local";
|
|
104
|
-
} catch {
|
|
105
|
-
return "kici-local";
|
|
106
|
-
}
|
|
102
|
+
return process.env.HOSTNAME || "kici-local";
|
|
107
103
|
}
|
|
108
104
|
//#endregion
|
|
109
105
|
export { derivePlatformWsUrl, mintOrchestratorKey, probePlatformReachable, revokeOrchestratorKey };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Command runner, injectable so each platform branch is unit-testable anywhere. */
|
|
2
|
+
export type ExecFn = (cmd: string, args: string[]) => Promise<{
|
|
3
|
+
stdout: string;
|
|
4
|
+
exitCode: number;
|
|
5
|
+
}>;
|
|
6
|
+
/**
|
|
7
|
+
* Command line of process `pid`, or null when it cannot be determined.
|
|
8
|
+
*
|
|
9
|
+
* Linux exposes it as a NUL-separated `/proc` file; every other platform is
|
|
10
|
+
* asked through the same injectable `ExecFn` the port discovery above uses.
|
|
11
|
+
*
|
|
12
|
+
* **A null means "cannot tell", never "not that process."** Callers must treat
|
|
13
|
+
* it as an absence of evidence — the only safe reading when the answer decides
|
|
14
|
+
* whether to signal a pid.
|
|
15
|
+
*/
|
|
16
|
+
export declare function processCommandLine(pid: number, opts?: {
|
|
17
|
+
exec?: ExecFn;
|
|
18
|
+
platform?: NodeJS.Platform;
|
|
19
|
+
readProc?: (path: string) => string;
|
|
20
|
+
}): Promise<string | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the port can be bound on loopback. Probes `127.0.0.1` because that is
|
|
23
|
+
* the address the CLI talks to; a listener bound to `0.0.0.0` still makes this
|
|
24
|
+
* bind fail with EADDRINUSE, so there is no false "free".
|
|
25
|
+
*/
|
|
26
|
+
export declare function isPortFree(port: number): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Parse `ss -lptnH` rows. The process name is deliberately NOT matched — a Node
|
|
29
|
+
* listener reports as `MainThread` — so only `pid=N` is extracted. A row for a
|
|
30
|
+
* socket this user does not own carries no `users:(…)` field at all and yields
|
|
31
|
+
* null, which is what sends the caller to the lsof fallback.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseSsOutput(stdout: string, port: number): number | null;
|
|
34
|
+
/**
|
|
35
|
+
* Parse `lsof -Fpn` field output, which is a stateful stream: each `p<pid>` line
|
|
36
|
+
* precedes the `n<address>` lines belonging to that pid.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseLsofOutput(stdout: string, port: number): number | null;
|
|
39
|
+
/** Parse `netstat -ano -p tcp`: LISTENING rows only, pid in the last column. */
|
|
40
|
+
export declare function parseNetstatOutput(stdout: string, port: number): number | null;
|
|
41
|
+
/**
|
|
42
|
+
* Pid of the process listening on `port`, or null when it cannot be determined.
|
|
43
|
+
* Each platform runs the UNFILTERED listing and matches the port here rather
|
|
44
|
+
* than passing a filter expression to the tool, whose syntax is the fragile
|
|
45
|
+
* part. On Linux a null parse falls through to lsof as well as a non-zero exit,
|
|
46
|
+
* because ss omits the owning-process field for sockets this user does not own.
|
|
47
|
+
*/
|
|
48
|
+
export declare function findPortHolderPid(port: number, opts?: {
|
|
49
|
+
exec?: ExecFn;
|
|
50
|
+
platform?: NodeJS.Platform;
|
|
51
|
+
}): Promise<number | null>;
|
|
52
|
+
/** Signal sender, injectable so the escalation windows are unit-testable. */
|
|
53
|
+
export type KillFn = (pid: number, signal: NodeJS.Signals | 0) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Poll until process `pid` has exited, sending SIGTERM first and escalating to
|
|
56
|
+
* SIGKILL past the grace window. `process.kill(pid, 0)` throws ESRCH once the
|
|
57
|
+
* process is gone. On Windows every signal terminates unconditionally, so the
|
|
58
|
+
* escalation collapses into a single hard kill there.
|
|
59
|
+
*
|
|
60
|
+
* Resolving does NOT prove the process died: a pid that survives SIGKILL (a
|
|
61
|
+
* zombie awaiting reaping, or a task wedged in uninterruptible I/O) is given
|
|
62
|
+
* `hardKillWaitMs` and then abandoned, because a teardown command that hangs
|
|
63
|
+
* forever is worse than one that reports the port is still held. Callers decide
|
|
64
|
+
* the outcome from the port itself — `waitForPortFree` — never from this
|
|
65
|
+
* resolving.
|
|
66
|
+
*/
|
|
67
|
+
export declare function terminatePid(pid: number, graceMs?: number, opts?: {
|
|
68
|
+
hardKillWaitMs?: number;
|
|
69
|
+
kill?: KillFn;
|
|
70
|
+
}): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Poll until the port is bindable. This, not `terminatePid` resolving, is what
|
|
73
|
+
* decides a teardown: `terminatePid` abandons a pid that survives SIGKILL, and
|
|
74
|
+
* even a process that did exit can leave its listening socket lingering a moment
|
|
75
|
+
* longer on some platforms.
|
|
76
|
+
*/
|
|
77
|
+
export declare function waitForPortFree(port: number, timeoutMs?: number): Promise<boolean>;
|
|
78
|
+
//# sourceMappingURL=port-holder.d.ts.map
|