@kici-dev/compiler 0.1.26 → 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.
Files changed (199) hide show
  1. package/dist/auth/headless-detect.d.ts +14 -5
  2. package/dist/auth/headless-detect.js +50 -9
  3. package/dist/cli-banner.d.ts +6 -3
  4. package/dist/cli-banner.js +13 -5
  5. package/dist/cli.js +180 -48
  6. package/dist/commands/check-mode.d.ts +1 -1
  7. package/dist/commands/compile.js +31 -7
  8. package/dist/commands/diagnostics.js +14 -2
  9. package/dist/commands/doctor.d.ts +64 -0
  10. package/dist/commands/doctor.js +413 -0
  11. package/dist/commands/endpoints.js +6 -0
  12. package/dist/commands/fixture.js +13 -0
  13. package/dist/commands/hook.js +3 -2
  14. package/dist/commands/index.d.ts +13 -1
  15. package/dist/commands/index.js +10 -3
  16. package/dist/commands/init-host-os.d.ts +34 -0
  17. package/dist/commands/init-host-os.js +44 -0
  18. package/dist/commands/init.d.ts +4 -0
  19. package/dist/commands/init.js +160 -37
  20. package/dist/commands/local-trust-root.d.ts +6 -0
  21. package/dist/commands/local-trust-root.js +52 -0
  22. package/dist/commands/local.d.ts +65 -0
  23. package/dist/commands/local.js +206 -0
  24. package/dist/commands/login.d.ts +2 -0
  25. package/dist/commands/login.js +41 -1
  26. package/dist/commands/logout.js +7 -0
  27. package/dist/commands/notifications.d.ts +61 -0
  28. package/dist/commands/notifications.js +274 -0
  29. package/dist/commands/preview.js +9 -2
  30. package/dist/commands/run-banner.d.ts +31 -0
  31. package/dist/commands/run-banner.js +25 -0
  32. package/dist/commands/run-routed.d.ts +53 -0
  33. package/dist/commands/run-routed.js +185 -0
  34. package/dist/commands/run.d.ts +0 -17
  35. package/dist/commands/run.js +44 -58
  36. package/dist/commands/runs/artifacts/download.d.ts +24 -0
  37. package/dist/commands/runs/artifacts/download.js +154 -0
  38. package/dist/commands/runs/artifacts/list.d.ts +5 -0
  39. package/dist/commands/runs/artifacts/list.js +47 -0
  40. package/dist/commands/runs/cancel.js +2 -2
  41. package/dist/commands/runs/list.js +8 -2
  42. package/dist/commands/secrets-list.d.ts +2 -2
  43. package/dist/commands/secrets-list.js +4 -4
  44. package/dist/commands/types.d.ts +3 -3
  45. package/dist/commands/types.js +4 -4
  46. package/dist/commands/verify-attestation.d.ts +3 -1
  47. package/dist/commands/verify-attestation.js +18 -7
  48. package/dist/errors/formatter.d.ts +14 -1
  49. package/dist/errors/formatter.js +13 -3
  50. package/dist/errors/index.d.ts +2 -1
  51. package/dist/errors/index.js +3 -2
  52. package/dist/errors/source-location.d.ts +20 -0
  53. package/dist/errors/source-location.js +45 -0
  54. package/dist/execution/executor.js +32 -12
  55. package/dist/fixtures/defaults/index.js +2 -1
  56. package/dist/generators/secrets-dts.d.ts +9 -9
  57. package/dist/generators/secrets-dts.js +12 -12
  58. package/dist/llm-context/llms-architecture.txt +50 -21
  59. package/dist/llm-context/llms-cli.txt +2667 -1454
  60. package/dist/llm-context/llms-features.txt +484 -414
  61. package/dist/llm-context/llms-full.txt +6703 -4687
  62. package/dist/llm-context/llms-getting-started.txt +311 -55
  63. package/dist/llm-context/llms-patterns.txt +17 -29
  64. package/dist/llm-context/llms-providers.txt +10 -1
  65. package/dist/llm-context/llms-sdk-runtime.txt +2013 -0
  66. package/dist/llm-context/llms-sdk.txt +596 -2157
  67. package/dist/llm-context/llms.txt +31 -17
  68. package/dist/local-plane/orchestrator-process.d.ts +70 -0
  69. package/dist/local-plane/orchestrator-process.js +129 -0
  70. package/dist/local-plane/os-mismatch-hint.d.ts +16 -0
  71. package/dist/local-plane/os-mismatch-hint.js +34 -0
  72. package/dist/local-plane/paths.d.ts +39 -0
  73. package/dist/local-plane/paths.js +54 -0
  74. package/dist/local-plane/plane-liveness.d.ts +68 -0
  75. package/dist/local-plane/plane-liveness.js +145 -0
  76. package/dist/local-plane/plane-manager.d.ts +193 -0
  77. package/dist/local-plane/plane-manager.js +480 -0
  78. package/dist/local-plane/plane-seed.d.ts +76 -0
  79. package/dist/local-plane/plane-seed.js +59 -0
  80. package/dist/local-plane/plane-trigger.d.ts +71 -0
  81. package/dist/local-plane/plane-trigger.js +110 -0
  82. package/dist/local-plane/platform-attach.d.ts +53 -0
  83. package/dist/local-plane/platform-attach.js +107 -0
  84. package/dist/local-plane/port-holder.d.ts +78 -0
  85. package/dist/local-plane/port-holder.js +227 -0
  86. package/dist/local-plane/postgres.d.ts +56 -0
  87. package/dist/local-plane/postgres.js +187 -0
  88. package/dist/local-plane/resolve-plane.d.ts +37 -0
  89. package/dist/local-plane/resolve-plane.js +76 -0
  90. package/dist/local-plane/run-follow.d.ts +61 -0
  91. package/dist/local-plane/run-follow.js +151 -0
  92. package/dist/local-plane/scaler-config.d.ts +68 -0
  93. package/dist/local-plane/scaler-config.js +132 -0
  94. package/dist/local-plane/secret-seed.d.ts +59 -0
  95. package/dist/local-plane/secret-seed.js +112 -0
  96. package/dist/local-plane/source-provider.d.ts +43 -0
  97. package/dist/local-plane/source-provider.js +159 -0
  98. package/dist/local-plane/trusted-routing.d.ts +32 -0
  99. package/dist/local-plane/trusted-routing.js +47 -0
  100. package/dist/lockfile/generator.d.ts +14 -1
  101. package/dist/lockfile/generator.js +103 -37
  102. package/dist/lockfile/index.d.ts +3 -1
  103. package/dist/lockfile/index.js +3 -2
  104. package/dist/lockfile/purity-diagnostics.d.ts +31 -0
  105. package/dist/lockfile/purity-diagnostics.js +52 -0
  106. package/dist/postinstall.js +2 -1
  107. package/dist/remote/artifact-extract.d.ts +8 -0
  108. package/dist/remote/artifact-extract.js +58 -0
  109. package/dist/remote/config.d.ts +21 -3
  110. package/dist/remote/config.js +24 -5
  111. package/dist/remote/dashboard-client.d.ts +46 -9
  112. package/dist/remote/dashboard-client.js +54 -11
  113. package/dist/remote/fs-case.d.ts +21 -0
  114. package/dist/remote/fs-case.js +45 -0
  115. package/dist/remote/history.d.ts +9 -3
  116. package/dist/remote/history.js +79 -10
  117. package/dist/remote/local-repo-identity.d.ts +1 -1
  118. package/dist/remote/local-repo-identity.js +2 -1
  119. package/dist/remote/notifications-client.d.ts +149 -0
  120. package/dist/remote/notifications-client.js +103 -0
  121. package/dist/remote/oauth.d.ts +13 -0
  122. package/dist/remote/oauth.js +31 -5
  123. package/dist/remote/output/summary.js +76 -14
  124. package/dist/remote/platform-client.d.ts +1 -1
  125. package/dist/remote/render.js +30 -9
  126. package/dist/{local-executor → remote}/secret-loader.d.ts +1 -1
  127. package/dist/{local-executor → remote}/secret-loader.js +4 -4
  128. package/dist/remote/secret-upload.d.ts +1 -1
  129. package/dist/remote/secret-upload.js +2 -2
  130. package/dist/remote/uploader.d.ts +1 -1
  131. package/dist/remote/uploader.js +7 -6
  132. package/dist/templates/agents-md.d.ts +1 -1
  133. package/dist/templates/agents-md.js +8 -8
  134. package/dist/templates/index.d.ts +1 -1
  135. package/dist/templates/index.js +2 -2
  136. package/dist/templates/package-json.d.ts +15 -0
  137. package/dist/templates/package-json.js +26 -5
  138. package/dist/test-runner/dry-run.d.ts +2 -1
  139. package/dist/test-runner/dry-run.js +18 -1
  140. package/dist/test-runner/event-types.d.ts +2 -0
  141. package/dist/test-runner/event-types.js +3 -0
  142. package/dist/test-runner/job-executor.d.ts +1 -0
  143. package/dist/test-runner/job-executor.js +82 -44
  144. package/dist/test-runner/output-formatter.d.ts +5 -2
  145. package/dist/test-runner/output-formatter.js +6 -4
  146. package/dist/test-runner/payload-builder.d.ts +7 -1
  147. package/dist/test-runner/payload-builder.js +11 -2
  148. package/dist/test-runner/rule-evaluator.d.ts +7 -3
  149. package/dist/test-runner/rule-evaluator.js +13 -12
  150. package/dist/{local-executor → test-runner}/runs-on-display.js +1 -1
  151. package/dist/test-runner/step-context.d.ts +23 -5
  152. package/dist/test-runner/step-context.js +29 -119
  153. package/dist/types.d.ts +26 -7
  154. package/dist/types.js +4 -2
  155. package/dist/validation/index.d.ts +2 -0
  156. package/dist/validation/index.js +2 -1
  157. package/dist/validation/typecheck.d.ts +28 -0
  158. package/dist/validation/typecheck.js +110 -0
  159. package/dist/validation/validator.d.ts +9 -4
  160. package/dist/validation/validator.js +38 -44
  161. package/package.json +13 -13
  162. package/sbom.spdx.json +9311 -1528
  163. package/dist/assets/api-TJJVHrjC.json +0 -118
  164. package/dist/assets/descriptor-BTtjzN9L.json +0 -1382
  165. package/dist/assets/package-BpQF9kR8.json +0 -74
  166. package/dist/assets/package-Ceo2h27X.json +0 -89
  167. package/dist/assets/source_context-D0atuL28.json +0 -20
  168. package/dist/assets/type-BFqO8SCZ.json +0 -202
  169. package/dist/commands/cancel.d.ts +0 -22
  170. package/dist/commands/detect-package-manager.d.ts +0 -42
  171. package/dist/commands/held-run-resolve.d.ts +0 -50
  172. package/dist/commands/status.d.ts +0 -34
  173. package/dist/commands/test.d.ts +0 -88
  174. package/dist/errors/codes.d.ts +0 -25
  175. package/dist/local-executor/dag-scheduler.d.ts +0 -44
  176. package/dist/local-executor/dag-scheduler.js +0 -183
  177. package/dist/local-executor/index.d.ts +0 -23
  178. package/dist/local-executor/index.js +0 -378
  179. package/dist/local-executor/job-runner.d.ts +0 -47
  180. package/dist/local-executor/job-runner.js +0 -411
  181. package/dist/local-executor/materializer.d.ts +0 -44
  182. package/dist/local-executor/materializer.js +0 -132
  183. package/dist/local-executor/output-streamer.d.ts +0 -31
  184. package/dist/local-executor/output-streamer.js +0 -168
  185. package/dist/local-executor/payload-generator.d.ts +0 -16
  186. package/dist/local-executor/payload-generator.js +0 -138
  187. package/dist/local-executor/picker.d.ts +0 -33
  188. package/dist/local-executor/picker.js +0 -109
  189. package/dist/local-executor/to-event-payload.d.ts +0 -16
  190. package/dist/local-executor/to-event-payload.js +0 -21
  191. package/dist/local-executor/types.d.ts +0 -93
  192. package/dist/local-executor/types.js +0 -2
  193. package/dist/local-executor/workflow-lock.d.ts +0 -82
  194. package/dist/local-executor/workflow-lock.js +0 -0
  195. package/dist/package-F7UXSDHW.json +0 -74
  196. package/dist/remote/client.d.ts +0 -210
  197. package/dist/remote/observer.d.ts +0 -81
  198. package/dist/test-runner/summary.d.ts +0 -7
  199. /package/dist/{local-executor → test-runner}/runs-on-display.d.ts +0 -0
@@ -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
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Podman fallback Postgres image for the local dev plane. Pinned via
3
+ * `TS_CONST_PINS` in hack/containers-lock.ts (identity
4
+ * docker.io/library/postgres+alpine), so a lock bump rewrites it in place.
5
+ */
6
+ export declare const PLANE_PG_IMAGE = "docker.io/library/postgres:18.4-alpine";
7
+ /** Name of the fallback podman Postgres container. */
8
+ export declare const PLANE_PG_CONTAINER = "kici-local-postgres";
9
+ export type PlanePgHandle = {
10
+ url: string;
11
+ kind: 'embedded' | 'podman';
12
+ stop(): Promise<void>;
13
+ };
14
+ declare let readyPoller: typeof defaultReadyPoller;
15
+ /** Test seam: override the podman readiness poller. */
16
+ export declare function __setReadyPollerForTest(fn: typeof readyPoller): void;
17
+ declare let embeddedDaemon: typeof defaultEmbeddedDaemon;
18
+ /** Test seam: override the embedded postmaster daemonizer. */
19
+ export declare function __setEmbeddedDaemonForTest(fn: typeof embeddedDaemon): void;
20
+ declare function defaultReadyPoller(_port: number): Promise<boolean>;
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>;
34
+ /**
35
+ * Start a detached embedded postmaster via `pg_ctl` so it survives the exit of
36
+ * this CLI process (embedded-postgres's in-process server is killed by its own
37
+ * exit hook, so it cannot back a warm plane). A cluster already serving this
38
+ * plane's port is reused as-is.
39
+ */
40
+ declare function defaultEmbeddedDaemon(port: number): Promise<void>;
41
+ /**
42
+ * Stop the plane's Postgres by backend kind. Handle-independent so a separate
43
+ * CLI invocation (`kici local down`) can tear down what `up` started.
44
+ */
45
+ export declare function stopPlanePostgres(kind: 'embedded' | 'podman'): Promise<void>;
46
+ /**
47
+ * Provision the local dev plane's Postgres. Prefers the zero-dependency
48
+ * `embedded-postgres` binary (daemonized via pg_ctl so it stays warm); falls
49
+ * back to a podman Postgres container when the embedded binary is unavailable
50
+ * (or when forced via `forcePodman` / `KICI_LOCAL_PG_MODE=podman`).
51
+ */
52
+ export declare function startPlanePostgres(opts?: {
53
+ forcePodman?: boolean;
54
+ }): Promise<PlanePgHandle>;
55
+ export {};
56
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1,187 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { planePaths, planePorts } from "./paths.js";
3
+ import { createRequire } from "node:module";
4
+ import path from "node:path";
5
+ import fs from "node:fs";
6
+ import { $ } from "zx";
7
+ import { spawn } from "node:child_process";
8
+ import os from "node:os";
9
+ import EmbeddedPostgres from "embedded-postgres";
10
+ //#region src/local-plane/postgres.ts
11
+ /**
12
+ * Podman fallback Postgres image for the local dev plane. Pinned via
13
+ * `TS_CONST_PINS` in hack/containers-lock.ts (identity
14
+ * docker.io/library/postgres+alpine), so a lock bump rewrites it in place.
15
+ */
16
+ const PLANE_PG_IMAGE = "docker.io/library/postgres:18.4-alpine";
17
+ /** Name of the fallback podman Postgres container. */
18
+ const PLANE_PG_CONTAINER = "kici-local-postgres";
19
+ let readyPoller = defaultReadyPoller;
20
+ /** Test seam: override the podman readiness poller. */
21
+ function __setReadyPollerForTest(fn) {
22
+ readyPoller = fn;
23
+ }
24
+ let embeddedDaemon = defaultEmbeddedDaemon;
25
+ /** Test seam: override the embedded postmaster daemonizer. */
26
+ function __setEmbeddedDaemonForTest(fn) {
27
+ embeddedDaemon = fn;
28
+ }
29
+ async function defaultReadyPoller(_port) {
30
+ for (let i = 0; i < 60; i++) try {
31
+ await $`podman exec ${PLANE_PG_CONTAINER} pg_isready -U kici`.quiet();
32
+ return true;
33
+ } catch {
34
+ await new Promise((r) => setTimeout(r, 500));
35
+ }
36
+ return false;
37
+ }
38
+ /**
39
+ * Resolve the platform-specific `pg_ctl` binary bundled with embedded-postgres.
40
+ * Mirrors the package's own platform → binary-package mapping, resolving the
41
+ * binary package through embedded-postgres's module context (pnpm isolates it
42
+ * as a transitive dependency, so it is not directly resolvable here) and
43
+ * computing the `native/bin/pg_ctl` path from the resolved package directory.
44
+ */
45
+ function resolvePgCtl() {
46
+ const platform = os.platform();
47
+ const arch = os.arch();
48
+ const pkg = {
49
+ "darwin:arm64": "@embedded-postgres/darwin-arm64",
50
+ "darwin:x64": "@embedded-postgres/darwin-x64",
51
+ "linux:arm64": "@embedded-postgres/linux-arm64",
52
+ "linux:arm": "@embedded-postgres/linux-arm",
53
+ "linux:ia32": "@embedded-postgres/linux-ia32",
54
+ "linux:ppc64": "@embedded-postgres/linux-ppc64",
55
+ "linux:x64": "@embedded-postgres/linux-x64",
56
+ "win32:x64": "@embedded-postgres/windows-x64"
57
+ }[`${platform}:${arch}`];
58
+ if (!pkg) throw new Error(`unsupported platform for embedded Postgres: ${platform}/${arch}`);
59
+ const entry = createRequire(createRequire(import.meta.url).resolve("embedded-postgres")).resolve(pkg);
60
+ const binName = platform === "win32" ? "pg_ctl.exe" : "pg_ctl";
61
+ return path.resolve(path.dirname(entry), "..", "native", "bin", binName);
62
+ }
63
+ /**
64
+ * Initialise the embedded Postgres cluster + the `kici_local` database once. A
65
+ * short-lived in-process server is used only for the one-time bootstrap; the
66
+ * persistent postmaster is started separately (daemonized) so it outlives this
67
+ * CLI invocation.
68
+ */
69
+ async function ensureEmbeddedCluster(port) {
70
+ const { pgData } = planePaths();
71
+ if (fs.existsSync(path.join(pgData, "PG_VERSION"))) return;
72
+ const pg = new EmbeddedPostgres({
73
+ databaseDir: pgData,
74
+ port,
75
+ user: "kici",
76
+ password: "kici",
77
+ persistent: true
78
+ });
79
+ await pg.initialise();
80
+ await pg.start();
81
+ await pg.createDatabase("kici_local").catch(() => {});
82
+ await pg.stop();
83
+ }
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
+ /**
114
+ * Start a detached embedded postmaster via `pg_ctl` so it survives the exit of
115
+ * this CLI process (embedded-postgres's in-process server is killed by its own
116
+ * exit hook, so it cannot back a warm plane). A cluster already serving this
117
+ * plane's port is reused as-is.
118
+ */
119
+ async function defaultEmbeddedDaemon(port) {
120
+ const { pgData, logFile } = planePaths();
121
+ if (await embeddedClusterIsServing(port)) return;
122
+ await $`${resolvePgCtl()} -D ${pgData} -o ${`-p ${port}`} -l ${`${logFile}.pg`} -w start`.quiet();
123
+ }
124
+ /** Stop the detached embedded postmaster (handle-independent, reads the data dir). */
125
+ async function stopEmbeddedDaemon() {
126
+ const { pgData } = planePaths();
127
+ if (!fs.existsSync(path.join(pgData, "postmaster.pid"))) return;
128
+ await $`${resolvePgCtl()} -D ${pgData} stop -m fast`.quiet().catch(() => {});
129
+ }
130
+ /**
131
+ * Stop the plane's Postgres by backend kind. Handle-independent so a separate
132
+ * CLI invocation (`kici local down`) can tear down what `up` started.
133
+ */
134
+ async function stopPlanePostgres(kind) {
135
+ if (kind === "podman") await $`podman rm -f ${PLANE_PG_CONTAINER}`.quiet().catch(() => {});
136
+ else await stopEmbeddedDaemon();
137
+ }
138
+ /**
139
+ * Provision the local dev plane's Postgres. Prefers the zero-dependency
140
+ * `embedded-postgres` binary (daemonized via pg_ctl so it stays warm); falls
141
+ * back to a podman Postgres container when the embedded binary is unavailable
142
+ * (or when forced via `forcePodman` / `KICI_LOCAL_PG_MODE=podman`).
143
+ */
144
+ async function startPlanePostgres(opts = {}) {
145
+ const { postgres: port } = planePorts();
146
+ const url = `postgres://kici:kici@127.0.0.1:${port}/kici_local`;
147
+ if (!(opts.forcePodman || process.env.KICI_LOCAL_PG_MODE === "podman")) try {
148
+ await ensureEmbeddedCluster(port);
149
+ await embeddedDaemon(port);
150
+ return {
151
+ url,
152
+ kind: "embedded",
153
+ stop: () => stopEmbeddedDaemon()
154
+ };
155
+ } catch {}
156
+ spawn("podman", [
157
+ "run",
158
+ "-d",
159
+ "--replace",
160
+ "--name",
161
+ PLANE_PG_CONTAINER,
162
+ "-p",
163
+ `127.0.0.1:${port}:5432`,
164
+ "-e",
165
+ "POSTGRES_USER=kici",
166
+ "-e",
167
+ "POSTGRES_PASSWORD=kici",
168
+ "-e",
169
+ "POSTGRES_DB=kici_local",
170
+ PLANE_PG_IMAGE
171
+ ], {
172
+ stdio: "ignore",
173
+ detached: true
174
+ }).unref();
175
+ if (!await readyPoller(port)) throw new Error("local Postgres (podman) did not become ready");
176
+ return {
177
+ url,
178
+ kind: "podman",
179
+ stop: async () => {
180
+ await $`podman rm -f ${PLANE_PG_CONTAINER}`.quiet().catch(() => {});
181
+ }
182
+ };
183
+ }
184
+ //#endregion
185
+ export { PLANE_PG_CONTAINER, PLANE_PG_IMAGE, __setEmbeddedDaemonForTest, __setReadyPollerForTest, embeddedClusterIsServing, startPlanePostgres, stopPlanePostgres };
186
+
187
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Plane auto-selection for `kici run --local` (design §5).
3
+ *
4
+ * Rules:
5
+ * - `--offline` → force the independent (offline) plane.
6
+ * - `--connected` → require an attached plane; hard-error if not attached, and
7
+ * hard-error if the Platform is unreachable (the user explicitly demanded it).
8
+ * - neither flag → attached + Platform reachable → hybrid; attached but the
9
+ * Platform is unreachable → fall back to the independent plane with a loud
10
+ * banner; not attached → offline.
11
+ *
12
+ * The fallback is the "mid-session Platform-unreachable" guarantee: a `--local`
13
+ * run never fails just because the Platform is unreachable (unless `--connected`).
14
+ * It re-boots/keeps the independent plane and continues on local secrets +
15
+ * dev-signed identity.
16
+ */
17
+ import { type PlaneStatus } from './plane-manager.js';
18
+ export interface ResolvePlaneFlags {
19
+ offline?: boolean;
20
+ connected?: boolean;
21
+ }
22
+ export type ResolvedPlane = {
23
+ kind: 'offline';
24
+ plane: PlaneStatus;
25
+ } | {
26
+ kind: 'attached';
27
+ plane: PlaneStatus;
28
+ orgId: string;
29
+ } | {
30
+ kind: 'fallback';
31
+ plane: PlaneStatus;
32
+ reason: string;
33
+ } | {
34
+ error: string;
35
+ };
36
+ export declare function resolvePlaneForRun(flags: ResolvePlaneFlags): Promise<ResolvedPlane>;
37
+ //# sourceMappingURL=resolve-plane.d.ts.map
@@ -0,0 +1,76 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { probePlatformReachable } from "./platform-attach.js";
3
+ import { planeUp, readAttachment, readPlatformToken } from "./plane-manager.js";
4
+ //#region src/local-plane/resolve-plane.ts
5
+ /**
6
+ * Plane auto-selection for `kici run --local` (design §5).
7
+ *
8
+ * Rules:
9
+ * - `--offline` → force the independent (offline) plane.
10
+ * - `--connected` → require an attached plane; hard-error if not attached, and
11
+ * hard-error if the Platform is unreachable (the user explicitly demanded it).
12
+ * - neither flag → attached + Platform reachable → hybrid; attached but the
13
+ * Platform is unreachable → fall back to the independent plane with a loud
14
+ * banner; not attached → offline.
15
+ *
16
+ * The fallback is the "mid-session Platform-unreachable" guarantee: a `--local`
17
+ * run never fails just because the Platform is unreachable (unless `--connected`).
18
+ * It re-boots/keeps the independent plane and continues on local secrets +
19
+ * dev-signed identity.
20
+ */
21
+ /** Boot the hybrid plane from the durable attachment record + persisted token. */
22
+ async function bootAttached(attachment) {
23
+ const token = readPlatformToken();
24
+ if (!token) return { error: "The plane is attached but its Platform token is missing — run `kici local attach` to re-attach." };
25
+ return planeUp({ attach: {
26
+ platformWsUrl: attachment.platformWsUrl,
27
+ platformToken: token,
28
+ platformApiBase: attachment.platformApiBase,
29
+ orgId: attachment.orgId,
30
+ keyId: attachment.keyId
31
+ } });
32
+ }
33
+ async function resolvePlaneForRun(flags) {
34
+ const attachment = readAttachment();
35
+ if (flags.offline) return {
36
+ kind: "offline",
37
+ plane: await planeUp()
38
+ };
39
+ if (flags.connected) {
40
+ if (!attachment) return { error: "Not attached to the Platform. Run `kici local attach` first (or drop --connected)." };
41
+ if (!await probePlatformReachable(attachment.platformApiBase)) return { error: `Platform ${attachment.platformApiBase} is unreachable — cannot honor --connected.` };
42
+ const booted = await bootAttached(attachment);
43
+ if ("error" in booted) return booted;
44
+ return {
45
+ kind: "attached",
46
+ plane: booted,
47
+ orgId: attachment.orgId
48
+ };
49
+ }
50
+ if (!attachment) return {
51
+ kind: "offline",
52
+ plane: await planeUp()
53
+ };
54
+ if (await probePlatformReachable(attachment.platformApiBase)) {
55
+ const booted = await bootAttached(attachment);
56
+ if ("error" in booted) return {
57
+ kind: "fallback",
58
+ plane: await planeUp(),
59
+ reason: booted.error
60
+ };
61
+ return {
62
+ kind: "attached",
63
+ plane: booted,
64
+ orgId: attachment.orgId
65
+ };
66
+ }
67
+ return {
68
+ kind: "fallback",
69
+ plane: await planeUp(),
70
+ reason: "Platform unreachable"
71
+ };
72
+ }
73
+ //#endregion
74
+ export { resolvePlaneForRun };
75
+
76
+ //# sourceMappingURL=resolve-plane.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Follow an offline routed run against the local plane by polling the
3
+ * orchestrator admin API (no SSE in independent mode). Streams step log lines as
4
+ * they arrive and resolves when the run reaches a terminal state.
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;
16
+ /** A minimal admin-read client (AdminApiClient.get) — injectable for tests. */
17
+ export interface RunFollowClient {
18
+ get<T>(path: string): Promise<T>;
19
+ }
20
+ /** Terminal outcome of a followed run. */
21
+ export interface RunOutcome {
22
+ runId: string;
23
+ status: string;
24
+ jobs: Array<{
25
+ name: string;
26
+ status: string;
27
+ durationMs?: number;
28
+ }>;
29
+ }
30
+ export interface FollowRunOptions {
31
+ onLine?: (line: string) => void;
32
+ quiet?: boolean;
33
+ client?: RunFollowClient;
34
+ pollIntervalMs?: number;
35
+ /**
36
+ * Idle window: the max time with NO observed progress (no new log line, no run
37
+ * status change) before the follow gives up. Resets on every progress tick, so
38
+ * a legitimately long run (e.g. a full `deploy:stg`, tens of minutes of image
39
+ * builds + host mutation) never times out while it is actively advancing —
40
+ * only a genuinely stalled run does. Default 15 min. `timeoutMs` is a
41
+ * back-compat alias.
42
+ */
43
+ idleTimeoutMs?: number;
44
+ /** @deprecated Back-compat alias for `idleTimeoutMs`. */
45
+ timeoutMs?: number;
46
+ /** Absolute cap regardless of progress (safety net against a runaway follow). Default 2 h. */
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;
58
+ }
59
+ /** Poll a run to completion, streaming step logs. */
60
+ export declare function followRun(planeUrl: string, adminToken: string, runId: string, opts?: FollowRunOptions): Promise<RunOutcome>;
61
+ //# sourceMappingURL=run-follow.d.ts.map