@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,193 @@
1
+ import { type PlaneState } from './plane-liveness.js';
2
+ /**
3
+ * The current CLI build's identity — semver plus git build commit — read at
4
+ * call time from the Rolldown-injected build constants (`scripts/build-ts.mjs`).
5
+ * Read on each call rather than captured in a module const so unit tests can
6
+ * inject the constants via `globalThis`. Falls back to `0.0.0` / `unknown` when
7
+ * running from source (unbuilt tree / vitest), where the defines are absent.
8
+ */
9
+ export declare function currentBuildIdentity(): {
10
+ version: string;
11
+ buildCommit: string;
12
+ };
13
+ /** Whether the plane runs offline (independent) or attached to the Platform (hybrid). */
14
+ export type PlaneMode = 'independent' | 'hybrid';
15
+ /**
16
+ * Attachment record for a hybrid plane: the Platform relay it dials, the org it
17
+ * is scoped to, and the Platform api_keys id of its minted orchestrator key (for
18
+ * later revoke). The orchestrator token itself is NEVER stored here — it lives
19
+ * in a sibling 0600 file so it can't land in a world-readable stamp.
20
+ */
21
+ export interface PlaneAttachment {
22
+ /** Orchestrator KICI_PLATFORM_URL — a ws(s)://…/ws relay URL. */
23
+ platformWsUrl: string;
24
+ /** HTTPS Platform API base (provenance/verify context, key revoke). */
25
+ platformApiBase: string;
26
+ orgId: string;
27
+ /** Platform api_keys id of the minted orchestrator key. */
28
+ keyId: string;
29
+ }
30
+ /** On-disk record of the running plane, written on boot, read on status/down. */
31
+ export interface PlaneStamp {
32
+ orchestratorPid: number;
33
+ port: number;
34
+ pgKind: 'embedded' | 'podman';
35
+ kiciVersion: string;
36
+ /** Git build commit of the CLI that booted this plane (see planeBuildIsStale). */
37
+ buildCommit: string;
38
+ stampVersion: number;
39
+ /** Offline (independent) vs attached (hybrid). Absent (legacy) reads as independent. */
40
+ mode?: PlaneMode;
41
+ }
42
+ export interface PlaneStatus {
43
+ /** True only when the plane is serving — a live-but-unready plane is false. */
44
+ running: boolean;
45
+ /** Liveness of the plane port: stopped, serving, live-but-not-serving, or held by someone else. */
46
+ state: PlaneState;
47
+ /** Failing `/ready` checks, when the plane is live but not serving. */
48
+ checks?: Record<string, boolean>;
49
+ pid?: number;
50
+ port?: number;
51
+ pgKind?: 'embedded' | 'podman';
52
+ stampVersion?: number;
53
+ url?: string;
54
+ /** Bootstrap admin token the CLI presents to the plane's admin API. */
55
+ adminToken?: string;
56
+ /** Offline (independent) vs attached (hybrid). */
57
+ mode: PlaneMode;
58
+ /** Present iff mode === 'hybrid'. */
59
+ attachment?: PlaneAttachment;
60
+ }
61
+ /**
62
+ * Read the plane's persisted Platform orchestrator token (mode 0600), or null
63
+ * when the plane is independent/offline. Kept out of the stamp so the token
64
+ * never lands in a world-readable file.
65
+ */
66
+ export declare function readPlatformToken(): string | null;
67
+ /** Persist the Platform orchestrator token (mode 0600). */
68
+ export declare function writePlatformToken(token: string): void;
69
+ /** Remove the persisted Platform orchestrator token (on detach). */
70
+ export declare function clearPlatformToken(): void;
71
+ /**
72
+ * Read the durable attachment record (survives `kici local down`), or null when
73
+ * the plane is not attached. This — not the running stamp — is the source of
74
+ * truth for "is this plane attached to the Platform".
75
+ */
76
+ export declare function readAttachment(): PlaneAttachment | null;
77
+ /** Persist the durable attachment record (never the token). */
78
+ export declare function writeAttachment(attachment: PlaneAttachment): void;
79
+ /** Remove the durable attachment record (on detach). */
80
+ export declare function clearAttachment(): void;
81
+ /**
82
+ * Read the plane's persisted bootstrap admin token, generating + persisting one
83
+ * (mode 0600) on first boot. A warm plane reuses the same token across CLI
84
+ * invocations so a running orchestrator keeps accepting the CLI's admin calls.
85
+ */
86
+ export declare function readOrCreateAdminToken(): string;
87
+ /** Read the plane's admin token without generating one (null when absent). */
88
+ export declare function planeAdminToken(): string | null;
89
+ /**
90
+ * Read the plane's local master secret key, generating + persisting a fresh 64
91
+ * hex-char key (mode 0600) on first boot. Stable across boots so DB-encrypted
92
+ * material stays decryptable. Freshly generated — never derived from any sops
93
+ * secret. Enables the orchestrator's secrets subsystem, which gates its admin
94
+ * API surface.
95
+ */
96
+ export declare function readOrCreateSecretKey(): string;
97
+ /** Absolute path of the dev-signed identity's private JWK key file. */
98
+ export declare function devIdentityKeyFile(): string;
99
+ /** Absolute path of the dev-signed identity's public JWK (written by the plane orchestrator). */
100
+ export declare function devIdentityPublicJwkFile(): string;
101
+ /**
102
+ * Read the plane's dev-signed identity keypair, generating + persisting a fresh
103
+ * EC P-256 private JWK (mode 0600) on first boot. Stable across boots so a
104
+ * `kici local trust-root` export stays valid for previously-minted tokens.
105
+ * Freshly generated — NEVER derived from any sops secret or real key. Only the
106
+ * private key is written here; the plane orchestrator derives + writes the
107
+ * public JWK next to it. Returns the private-key file path passed to the
108
+ * orchestrator via KICI_DEV_IDENTITY_KEY_FILE.
109
+ */
110
+ export declare function readOrCreateDevIdentity(): string;
111
+ /** Remove the plane's on-disk record (pidfile + stamp). */
112
+ export declare function clearStamp(): void;
113
+ /**
114
+ * Whether a running plane described by `existing` was booted from a different
115
+ * CLI build than the current one — a semver bump OR a git-commit change (the
116
+ * latter covers intermediate staging/E2E commits that share a semver). Returns
117
+ * false when there is no stamp, or when the current build has no concrete
118
+ * identity (`buildCommit === 'unknown'`, i.e. running from source / a test),
119
+ * so a source-context `planeUp` never reboots a healthy plane spuriously. An
120
+ * old stamp with no `buildCommit` field reads as `undefined` and therefore
121
+ * triggers a one-time reboot on the first upgrade past this feature.
122
+ */
123
+ export declare function planeBuildIsStale(existing: PlaneStamp | null): boolean;
124
+ /** Absolute path of the plane orchestrator's log file. */
125
+ export declare function planeLogPath(): string;
126
+ /**
127
+ * Read the plane's current status from whatever holds the plane port.
128
+ *
129
+ * `running` means "serving": a live orchestrator whose `/ready` fails is
130
+ * reported as `unready`, with the failing checks, rather than as stopped —
131
+ * inferring "not running" from an unready probe hides a process that still owns
132
+ * the port.
133
+ */
134
+ export declare function planeStatus(): Promise<PlaneStatus>;
135
+ /** Attach parameters for a hybrid plane boot (see attachPlane). */
136
+ export interface PlaneUpAttach {
137
+ platformWsUrl: string;
138
+ platformToken: string;
139
+ platformApiBase: string;
140
+ orgId: string;
141
+ keyId: string;
142
+ }
143
+ export interface PlaneUpOptions {
144
+ /** Boot the plane hybrid (attached to the Platform) instead of independent. */
145
+ attach?: PlaneUpAttach;
146
+ }
147
+ /**
148
+ * Start (or reuse) the local dev plane. Idempotent: a healthy running plane this
149
+ * config dir stamped, **whose stamped mode matches the requested mode**, is
150
+ * returned as-is. Otherwise the port is reclaimed if it is another KiCI plane,
151
+ * and Postgres + the orchestrator are booted with a fresh stamp + pidfile. When
152
+ * `attach` is present the orchestrator boots hybrid against the Platform relay;
153
+ * otherwise it boots independent with the dev-signed identity. Throws when the
154
+ * port is held by a process that is not a KiCI plane orchestrator.
155
+ */
156
+ export declare function planeUp(opts?: PlaneUpOptions): Promise<PlaneStatus>;
157
+ /** Outcome of a teardown attempt. `stopped` is true only when the port is verified free. */
158
+ export interface PlaneDownResult {
159
+ stopped: boolean;
160
+ port: number;
161
+ holderPid?: number;
162
+ reason?: string;
163
+ }
164
+ /**
165
+ * Stop the local dev plane and confirm the port was released.
166
+ *
167
+ * Teardown is reconstructed from whatever holds the port, not only from the
168
+ * stamp: a plane this config dir never stamped still occupies the port and is
169
+ * still ours to stop. The pidfile and stamp are cleared only once the port is
170
+ * verified free, so a survivor never loses the pid that identifies it.
171
+ */
172
+ export declare function planeDown(): Promise<PlaneDownResult>;
173
+ /**
174
+ * Attach the local dev plane to the hosted Platform: mint an org-scoped
175
+ * orchestrator key with the logged-in PAT, then (re)boot the plane hybrid
176
+ * against the Platform relay. Registration with the Platform is implicit — the
177
+ * hybrid orchestrator's PlatformClient sends `source.register` on WS auth.
178
+ */
179
+ export declare function attachPlane(args: {
180
+ apiBase: string;
181
+ pat: string;
182
+ orgId: string;
183
+ }): Promise<PlaneStatus>;
184
+ /**
185
+ * Detach the local dev plane: best-effort revoke the minted orchestrator key on
186
+ * the Platform, clear the durable attachment + token, then reboot the plane
187
+ * independent (offline). The revoke is non-fatal — a detach must succeed even
188
+ * offline or with an expired PAT.
189
+ */
190
+ export declare function detachPlane(opts?: {
191
+ pat?: string;
192
+ }): Promise<PlaneStatus>;
193
+ //# sourceMappingURL=plane-manager.d.ts.map
@@ -0,0 +1,480 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { planePaths, planePorts } from "./paths.js";
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";
6
+ import { startPlanePostgres, stopPlanePostgres } from "./postgres.js";
7
+ import { writeScalerConfig } from "./scaler-config.js";
8
+ import { derivePlatformWsUrl, mintOrchestratorKey, revokeOrchestratorKey } from "./platform-attach.js";
9
+ import path from "node:path";
10
+ import fs from "node:fs";
11
+ import { generateKeyPairSync, randomBytes } from "node:crypto";
12
+ //#region src/local-plane/plane-manager.ts
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
+ }
26
+ /**
27
+ * Read the plane's persisted Platform orchestrator token (mode 0600), or null
28
+ * when the plane is independent/offline. Kept out of the stamp so the token
29
+ * never lands in a world-readable file.
30
+ */
31
+ function readPlatformToken() {
32
+ try {
33
+ return fs.readFileSync(planePaths().platformTokenFile, "utf-8").trim() || null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ /** Persist the Platform orchestrator token (mode 0600). */
39
+ function writePlatformToken(token) {
40
+ const { platformTokenFile, root } = planePaths();
41
+ fs.mkdirSync(root, { recursive: true });
42
+ fs.writeFileSync(platformTokenFile, token, { mode: 384 });
43
+ }
44
+ /** Remove the persisted Platform orchestrator token (on detach). */
45
+ function clearPlatformToken() {
46
+ fs.rmSync(planePaths().platformTokenFile, { force: true });
47
+ }
48
+ /**
49
+ * Read the durable attachment record (survives `kici local down`), or null when
50
+ * the plane is not attached. This — not the running stamp — is the source of
51
+ * truth for "is this plane attached to the Platform".
52
+ */
53
+ function readAttachment() {
54
+ try {
55
+ return JSON.parse(fs.readFileSync(planePaths().attachmentFile, "utf-8"));
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ /** Persist the durable attachment record (never the token). */
61
+ function writeAttachment(attachment) {
62
+ const { attachmentFile, root } = planePaths();
63
+ fs.mkdirSync(root, { recursive: true });
64
+ fs.writeFileSync(attachmentFile, JSON.stringify(attachment, null, 2));
65
+ }
66
+ /** Remove the durable attachment record (on detach). */
67
+ function clearAttachment() {
68
+ fs.rmSync(planePaths().attachmentFile, { force: true });
69
+ }
70
+ /**
71
+ * Read the plane's persisted bootstrap admin token, generating + persisting one
72
+ * (mode 0600) on first boot. A warm plane reuses the same token across CLI
73
+ * invocations so a running orchestrator keeps accepting the CLI's admin calls.
74
+ */
75
+ function readOrCreateAdminToken() {
76
+ const { adminTokenFile, root } = planePaths();
77
+ try {
78
+ const existing = fs.readFileSync(adminTokenFile, "utf-8").trim();
79
+ if (existing) return existing;
80
+ } catch {}
81
+ fs.mkdirSync(root, { recursive: true });
82
+ const token = `kici-local-${randomBytes(24).toString("hex")}`;
83
+ fs.writeFileSync(adminTokenFile, token, { mode: 384 });
84
+ return token;
85
+ }
86
+ /** Read the plane's admin token without generating one (null when absent). */
87
+ function planeAdminToken() {
88
+ try {
89
+ return fs.readFileSync(planePaths().adminTokenFile, "utf-8").trim() || null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+ /**
95
+ * Read the plane's local master secret key, generating + persisting a fresh 64
96
+ * hex-char key (mode 0600) on first boot. Stable across boots so DB-encrypted
97
+ * material stays decryptable. Freshly generated — never derived from any sops
98
+ * secret. Enables the orchestrator's secrets subsystem, which gates its admin
99
+ * API surface.
100
+ */
101
+ function readOrCreateSecretKey() {
102
+ const { secretKeyFile, root } = planePaths();
103
+ try {
104
+ const existing = fs.readFileSync(secretKeyFile, "utf-8").trim();
105
+ if (existing) return existing;
106
+ } catch {}
107
+ fs.mkdirSync(root, { recursive: true });
108
+ const key = randomBytes(32).toString("hex");
109
+ fs.writeFileSync(secretKeyFile, key, { mode: 384 });
110
+ return key;
111
+ }
112
+ /** Absolute path of the dev-signed identity's private JWK key file. */
113
+ function devIdentityKeyFile() {
114
+ return path.join(planePaths().devIdentityDir, "identity.jwk");
115
+ }
116
+ /** Absolute path of the dev-signed identity's public JWK (written by the plane orchestrator). */
117
+ function devIdentityPublicJwkFile() {
118
+ return path.join(planePaths().devIdentityDir, "identity.pub.jwk");
119
+ }
120
+ /**
121
+ * Read the plane's dev-signed identity keypair, generating + persisting a fresh
122
+ * EC P-256 private JWK (mode 0600) on first boot. Stable across boots so a
123
+ * `kici local trust-root` export stays valid for previously-minted tokens.
124
+ * Freshly generated — NEVER derived from any sops secret or real key. Only the
125
+ * private key is written here; the plane orchestrator derives + writes the
126
+ * public JWK next to it. Returns the private-key file path passed to the
127
+ * orchestrator via KICI_DEV_IDENTITY_KEY_FILE.
128
+ */
129
+ function readOrCreateDevIdentity() {
130
+ const keyFile = devIdentityKeyFile();
131
+ try {
132
+ if (fs.readFileSync(keyFile, "utf-8").trim()) return keyFile;
133
+ } catch {}
134
+ fs.mkdirSync(planePaths().devIdentityDir, {
135
+ recursive: true,
136
+ mode: 448
137
+ });
138
+ const { privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
139
+ const jwk = privateKey.export({ format: "jwk" });
140
+ fs.writeFileSync(keyFile, JSON.stringify(jwk, null, 2), { mode: 384 });
141
+ return keyFile;
142
+ }
143
+ function readStamp() {
144
+ try {
145
+ return JSON.parse(fs.readFileSync(planePaths().stampFile, "utf-8"));
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ function writeStamp(stamp) {
151
+ const { root, stampFile, pidfile } = planePaths();
152
+ fs.mkdirSync(root, { recursive: true });
153
+ fs.writeFileSync(stampFile, JSON.stringify(stamp, null, 2));
154
+ fs.writeFileSync(pidfile, String(stamp.orchestratorPid));
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
+ }
178
+ function orchestratorUrl(port) {
179
+ return `http://127.0.0.1:${port}`;
180
+ }
181
+ /** Absolute path of the plane orchestrator's log file. */
182
+ function planeLogPath() {
183
+ return planePaths().logFile;
184
+ }
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
+ */
193
+ async function planeStatus() {
194
+ const stamp = readStamp();
195
+ const port = stamp?.port ?? planePorts().orchestrator;
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 {
200
+ running: false,
201
+ state: "stopped",
202
+ mode,
203
+ attachment
204
+ };
205
+ if (classification.kind === "ours-ready") return {
206
+ running: true,
207
+ state: "ready",
208
+ pid: classification.pid,
209
+ port,
210
+ pgKind: stamp?.pgKind,
211
+ stampVersion: stamp?.stampVersion,
212
+ url: orchestratorUrl(port),
213
+ adminToken: planeAdminToken() ?? void 0,
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
227
+ };
228
+ }
229
+ /**
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
296
+ * `attach` is present the orchestrator boots hybrid against the Platform relay;
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.
299
+ */
300
+ async function planeUp(opts = {}) {
301
+ const { orchestrator: port } = planePorts();
302
+ const existing = readStamp();
303
+ const requestedMode = opts.attach ? "hybrid" : "independent";
304
+ if (existing && existing.stampVersion !== 3) {
305
+ await tearDownForBoot();
306
+ fs.rmSync(planePaths().pgData, {
307
+ recursive: true,
308
+ force: true
309
+ });
310
+ } else {
311
+ const reusable = await prepareForBoot(port, existing, requestedMode);
312
+ if (reusable) return reusable;
313
+ }
314
+ const adminToken = readOrCreateAdminToken();
315
+ const secretKey = readOrCreateSecretKey();
316
+ const scalerConfigFile = writeScalerConfig(port);
317
+ const pg = await startPlanePostgres();
318
+ let orch;
319
+ if (opts.attach) {
320
+ writePlatformToken(opts.attach.platformToken);
321
+ writeAttachment({
322
+ platformWsUrl: opts.attach.platformWsUrl,
323
+ platformApiBase: opts.attach.platformApiBase,
324
+ orgId: opts.attach.orgId,
325
+ keyId: opts.attach.keyId
326
+ });
327
+ orch = spawnOrchestratorProcess(pg.url, {
328
+ adminToken,
329
+ secretKey,
330
+ scalerConfigFile,
331
+ attach: {
332
+ platformWsUrl: opts.attach.platformWsUrl,
333
+ platformToken: opts.attach.platformToken
334
+ }
335
+ });
336
+ } else {
337
+ const devIdentityKey = readOrCreateDevIdentity();
338
+ orch = spawnOrchestratorProcess(pg.url, {
339
+ adminToken,
340
+ secretKey,
341
+ scalerConfigFile,
342
+ devIdentityKeyFile: devIdentityKey
343
+ });
344
+ }
345
+ const { version: stampVersionSemver, buildCommit: stampBuildCommit } = currentBuildIdentity();
346
+ writeStamp({
347
+ orchestratorPid: orch.pid,
348
+ port: orch.port,
349
+ pgKind: pg.kind,
350
+ kiciVersion: stampVersionSemver,
351
+ buildCommit: stampBuildCommit,
352
+ stampVersion: 3,
353
+ mode: requestedMode
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
+ }
362
+ return {
363
+ running: true,
364
+ state: "ready",
365
+ pid: orch.pid,
366
+ port: orch.port,
367
+ pgKind: pg.kind,
368
+ stampVersion: 3,
369
+ url: orchestratorUrl(orch.port),
370
+ adminToken,
371
+ mode: requestedMode,
372
+ attachment: readAttachment() ?? void 0
373
+ };
374
+ }
375
+ /**
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.
390
+ */
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;
398
+ }
399
+ /**
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.
406
+ */
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
+ };
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
+ };
435
+ }
436
+ /**
437
+ * Attach the local dev plane to the hosted Platform: mint an org-scoped
438
+ * orchestrator key with the logged-in PAT, then (re)boot the plane hybrid
439
+ * against the Platform relay. Registration with the Platform is implicit — the
440
+ * hybrid orchestrator's PlatformClient sends `source.register` on WS auth.
441
+ */
442
+ async function attachPlane(args) {
443
+ const platformWsUrl = derivePlatformWsUrl(args.apiBase);
444
+ const minted = await mintOrchestratorKey({
445
+ apiBase: args.apiBase,
446
+ pat: args.pat,
447
+ orgId: args.orgId,
448
+ name: "local dev plane"
449
+ });
450
+ return planeUp({ attach: {
451
+ platformWsUrl,
452
+ platformToken: minted.key,
453
+ platformApiBase: args.apiBase,
454
+ orgId: args.orgId,
455
+ keyId: minted.keyId
456
+ } });
457
+ }
458
+ /**
459
+ * Detach the local dev plane: best-effort revoke the minted orchestrator key on
460
+ * the Platform, clear the durable attachment + token, then reboot the plane
461
+ * independent (offline). The revoke is non-fatal — a detach must succeed even
462
+ * offline or with an expired PAT.
463
+ */
464
+ async function detachPlane(opts = {}) {
465
+ const attachment = readAttachment();
466
+ if (attachment && opts.pat) await revokeOrchestratorKey({
467
+ apiBase: attachment.platformApiBase,
468
+ pat: opts.pat,
469
+ orgId: attachment.orgId,
470
+ keyId: attachment.keyId
471
+ }).catch(() => false);
472
+ clearAttachment();
473
+ clearPlatformToken();
474
+ await planeDown();
475
+ return planeUp();
476
+ }
477
+ //#endregion
478
+ export { attachPlane, clearAttachment, clearPlatformToken, clearStamp, currentBuildIdentity, detachPlane, devIdentityKeyFile, devIdentityPublicJwkFile, planeAdminToken, planeBuildIsStale, planeDown, planeLogPath, planeStatus, planeUp, readAttachment, readOrCreateAdminToken, readOrCreateDevIdentity, readOrCreateSecretKey, readPlatformToken, writeAttachment, writePlatformToken };
479
+
480
+ //# sourceMappingURL=plane-manager.js.map