@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
@@ -1,3 +1,5 @@
1
+ /** Deadline for the WSL interop probe. A healthy probe answers in single-digit ms. */
2
+ export declare const WSL_INTEROP_PROBE_TIMEOUT_MS = 2000;
1
3
  /**
2
4
  * Detect if the current environment is headless (no browser available).
3
5
  *
@@ -5,10 +7,17 @@
5
7
  * (desktop) or the RFC 8628 device authorization flow (headless/SSH).
6
8
  *
7
9
  * Checks in order:
8
- * 1. SSH session (SSH_CLIENT, SSH_TTY)
9
- * 2. CI environments (CI, GITHUB_ACTIONS, GITLAB_CI)
10
- * 3. Container environments (container, DOCKER_CONTAINER)
11
- * 4. Linux without display server (no DISPLAY and no WAYLAND_DISPLAY)
10
+ * 1. SSH session (SSH_CONNECTION, SSH_CLIENT, SSH_TTY)
11
+ * 2. CI environments see `isCiEnvironment` in `@kici-dev/core/ci-env` for the
12
+ * opt-out convention and why a vendor marker beats `CI=false`.
13
+ * 3. Container environments (container, DOCKER_CONTAINER, and the
14
+ * /run/.containerenv + /.dockerenv sentinel files)
15
+ * 4. WSL with reachable Windows interop — an interactive desktop, so not
16
+ * headless. WSL without reachable interop is headless, and so is WSL whose
17
+ * interop probe does not answer within the deadline (a hung Windows mount).
18
+ * 5. Linux without display server (no DISPLAY and no WAYLAND_DISPLAY)
12
19
  */
13
- export declare function isHeadless(): boolean;
20
+ export declare function isHeadless(opts?: {
21
+ probeTimeoutMs?: number;
22
+ }): Promise<boolean>;
14
23
  //# sourceMappingURL=headless-detect.d.ts.map
@@ -1,5 +1,33 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
+ import { existsSync } from "node:fs";
3
+ import pc from "picocolors";
4
+ import { isCiEnvironment } from "@kici-dev/core/ci-env";
5
+ import { canAccessPowerShell, isWsl } from "wsl-utils";
2
6
  //#region src/auth/headless-detect.ts
7
+ /** Deadline for the WSL interop probe. A healthy probe answers in single-digit ms. */
8
+ const WSL_INTEROP_PROBE_TIMEOUT_MS = 2e3;
9
+ /**
10
+ * Run the Windows-interop probe under a deadline.
11
+ *
12
+ * `canAccessPowerShell()` does unbounded filesystem I/O against the mounted
13
+ * Windows drive: on a wedged 9p mount it never settles, which would strand the
14
+ * caller. The tri-state result lets the caller tell "the probe said
15
+ * unreachable" from "the probe never answered" — only the latter is announced.
16
+ */
17
+ async function probeWithDeadline(timeoutMs) {
18
+ const probe = canAccessPowerShell();
19
+ probe.catch(() => {});
20
+ let timer;
21
+ const deadline = new Promise((resolve) => {
22
+ timer = setTimeout(() => resolve("timeout"), timeoutMs);
23
+ timer.unref?.();
24
+ });
25
+ try {
26
+ return await Promise.race([probe, deadline]);
27
+ } finally {
28
+ clearTimeout(timer);
29
+ }
30
+ }
3
31
  /**
4
32
  * Detect if the current environment is headless (no browser available).
5
33
  *
@@ -7,19 +35,32 @@ import "../rolldown-runtime-ClRpJifh.js";
7
35
  * (desktop) or the RFC 8628 device authorization flow (headless/SSH).
8
36
  *
9
37
  * Checks in order:
10
- * 1. SSH session (SSH_CLIENT, SSH_TTY)
11
- * 2. CI environments (CI, GITHUB_ACTIONS, GITLAB_CI)
12
- * 3. Container environments (container, DOCKER_CONTAINER)
13
- * 4. Linux without display server (no DISPLAY and no WAYLAND_DISPLAY)
38
+ * 1. SSH session (SSH_CONNECTION, SSH_CLIENT, SSH_TTY)
39
+ * 2. CI environments see `isCiEnvironment` in `@kici-dev/core/ci-env` for the
40
+ * opt-out convention and why a vendor marker beats `CI=false`.
41
+ * 3. Container environments (container, DOCKER_CONTAINER, and the
42
+ * /run/.containerenv + /.dockerenv sentinel files)
43
+ * 4. WSL with reachable Windows interop — an interactive desktop, so not
44
+ * headless. WSL without reachable interop is headless, and so is WSL whose
45
+ * interop probe does not answer within the deadline (a hung Windows mount).
46
+ * 5. Linux without display server (no DISPLAY and no WAYLAND_DISPLAY)
14
47
  */
15
- function isHeadless() {
16
- if (process.env.SSH_CLIENT || process.env.SSH_TTY) return true;
17
- if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI) return true;
18
- if (process.env.container || process.env.DOCKER_CONTAINER) return true;
48
+ async function isHeadless(opts = {}) {
49
+ if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) return true;
50
+ if (isCiEnvironment()) return true;
51
+ if (process.env.container || process.env.DOCKER_CONTAINER || existsSync("/run/.containerenv") || existsSync("/.dockerenv")) return true;
52
+ if (isWsl) {
53
+ const reachable = await probeWithDeadline(opts.probeTimeoutMs ?? 2e3);
54
+ if (reachable === "timeout") {
55
+ console.log(pc.dim(" Windows interop probe timed out — using the device flow."));
56
+ return true;
57
+ }
58
+ return !reachable;
59
+ }
19
60
  if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return true;
20
61
  return false;
21
62
  }
22
63
  //#endregion
23
- export { isHeadless };
64
+ export { WSL_INTEROP_PROBE_TIMEOUT_MS, isHeadless };
24
65
 
25
66
  //# sourceMappingURL=headless-detect.js.map
@@ -3,9 +3,12 @@ import type { Command } from 'commander';
3
3
  * Whether the version banner should be suppressed for the command about to run.
4
4
  *
5
5
  * Structured-output (`--json`) and quiet (`--quiet`) invocations must keep stdout
6
- * free of human-facing chrome so callers can parse stdout directly. Uses
7
- * `optsWithGlobals()` so the check stays correct if either flag is ever promoted
8
- * to a global option.
6
+ * free of human-facing chrome so callers can parse stdout directly. Walks the
7
+ * command chain checking each command's OWN opts (not `optsWithGlobals()`): a
8
+ * flag set anywhere in the chain suppresses the banner, and reading own opts
9
+ * avoids a parent's default value shadowing a subcommand's explicit flag —
10
+ * `kici run` defines `--quiet` for its routed action, which under
11
+ * `optsWithGlobals()` would mask `kici run remote --quiet`.
9
12
  */
10
13
  export declare function shouldSuppressBanner(actionCommand: Command): boolean;
11
14
  //# sourceMappingURL=cli-banner.d.ts.map
@@ -4,13 +4,21 @@ import "./rolldown-runtime-ClRpJifh.js";
4
4
  * Whether the version banner should be suppressed for the command about to run.
5
5
  *
6
6
  * Structured-output (`--json`) and quiet (`--quiet`) invocations must keep stdout
7
- * free of human-facing chrome so callers can parse stdout directly. Uses
8
- * `optsWithGlobals()` so the check stays correct if either flag is ever promoted
9
- * to a global option.
7
+ * free of human-facing chrome so callers can parse stdout directly. Walks the
8
+ * command chain checking each command's OWN opts (not `optsWithGlobals()`): a
9
+ * flag set anywhere in the chain suppresses the banner, and reading own opts
10
+ * avoids a parent's default value shadowing a subcommand's explicit flag —
11
+ * `kici run` defines `--quiet` for its routed action, which under
12
+ * `optsWithGlobals()` would mask `kici run remote --quiet`.
10
13
  */
11
14
  function shouldSuppressBanner(actionCommand) {
12
- const opts = actionCommand.optsWithGlobals();
13
- return Boolean(opts.json) || Boolean(opts.quiet);
15
+ let cmd = actionCommand;
16
+ while (cmd) {
17
+ const opts = cmd.opts();
18
+ if (opts.json === true || opts.quiet === true) return true;
19
+ cmd = cmd.parent;
20
+ }
21
+ return false;
14
22
  }
15
23
  //#endregion
16
24
  export { shouldSuppressBanner };
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { realpathSync } from "node:fs";
7
7
  import { Argument, Command, Option } from "commander";
8
8
  import pc from "picocolors";
9
9
  //#region src/cli.ts
10
- const version = "0.1.26";
10
+ const version = "0.2.0";
11
11
  /**
12
12
  * Top-level commands that were removed, mapped to their current equivalent.
13
13
  * Consulted when the CLI hits an unknown command so the user gets a precise
@@ -16,7 +16,8 @@ const version = "0.1.26";
16
16
  */
17
17
  const RETIRED_COMMANDS = {
18
18
  status: "`kici status` is no longer a command. Use `kici runs list` / `kici runs show <run-id>` to inspect runs, or `kici diagnostics` for orchestrator, scaler, and agent health.",
19
- cancel: "`kici cancel` is no longer a command. Use `kici runs cancel <run-id>`."
19
+ cancel: "`kici cancel` is no longer a command. Use `kici runs cancel <run-id>`.",
20
+ "run local": "`kici run local` is retired. Every run is now a real routed dispatch — use `kici run <event> --local` (e.g. `kici run push --local`), which runs this machine as an ephemeral agent through the local dev plane."
20
21
  };
21
22
  /**
22
23
  * Redirect message for a retired top-level command, or undefined if the name is
@@ -43,6 +44,7 @@ function attemptedCommand(program, argv) {
43
44
  function buildProgram() {
44
45
  const program = new Command();
45
46
  program.name("kici").description("KiCI workflow compiler").version(version);
47
+ program.enablePositionalOptions();
46
48
  program.hook("preAction", (_thisCommand, actionCommand) => {
47
49
  if (shouldSuppressBanner(actionCommand)) return;
48
50
  console.log(pc.gray(`kici v${version}`));
@@ -52,7 +54,7 @@ function buildProgram() {
52
54
  } });
53
55
  program.showSuggestionAfterError(true);
54
56
  program.showHelpAfterError("(run `kici --help` to see all commands)");
55
- program.command("compile").description("Compile workflows from .kici/workflows/ to kici.lock.json").option("--check", "Validate workflows without writing lock file", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--verbose", "Detailed output", false).option("--watch", "Watch for changes and recompile", false).action(async (options) => {
57
+ program.command("compile").description("Compile workflows from .kici/workflows/ to kici.lock.json").option("--check", "Validate workflows and type-check sources (tsc --noEmit) without writing lock file", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--verbose", "Detailed output", false).option("--watch", "Watch for changes and recompile", false).action(async (options) => {
56
58
  const { compileCommand, watchCommand } = await import("./commands/index.js");
57
59
  if (options.watch) await watchCommand({
58
60
  kiciDir: options.kiciDir,
@@ -73,50 +75,26 @@ function buildProgram() {
73
75
  await fixtureCommand(event, options);
74
76
  });
75
77
  const runCommand = program.command("run").description("Execute workflows locally or remotely");
76
- runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open, schedule) — optional with --pick").description("Execute workflows locally without orchestrator infrastructure").option("-p, --pick", "Interactively pick a workflow and trigger to simulate", false).option("--workflow <name>", "Run only the specified workflow").option("--job <name>", "Run only the specified job (and its dependencies)").option("--branch <name>", "Override detected git branch").option("--sha <hash>", "Override detected git SHA").option("--payload <path>", "Path to explicit event payload JSON file").option("--concurrency <n>", "Max parallel jobs (default: CPU cores)", parseInt).option("--keep-going", "Continue after job failure", false).option("--container", "Use Podman container isolation", false).option("--env <KEY=VALUE>", "Environment variable override (repeatable)", (val, prev) => [...prev, val], []).option("--input <KEY=VALUE>", "Typed workflow-dispatch input (repeatable)", (val, prev) => [...prev, val], []).option("--quiet", "Suppress streaming output", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--files <path>", "Override changed file paths (repeatable, default: git diff)", (val, prev) => [...prev, val], []).option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--in-place", "Run against the real working directory instead of an isolated tmp checkout", false).option("--keep", "Always retain the isolated tmp checkout (default: keep only on failure)", false).option("--check", "Run in check mode: report drift, change nothing", false).option("--fail-on-drift", "In check mode, exit non-zero if any step reports drift", false).action(async (event, options) => {
77
- if (options.pick && options.workflow) {
78
- console.error("Error: --pick is mutually exclusive with --workflow.");
79
- process.exit(2);
80
- }
81
- if (!options.pick && !event) {
82
- const { printRunLocalUsage } = await import("./commands/index.js");
83
- printRunLocalUsage();
84
- process.exit(2);
85
- }
86
- const { runLocalCommand } = await import("./commands/index.js");
87
- const { resolveCheckMode } = await import("./commands/check-mode.js");
88
- let checkMode;
89
- try {
90
- checkMode = resolveCheckMode({
91
- check: options.check,
92
- failOnDrift: options.failOnDrift
93
- });
94
- } catch (err) {
95
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
78
+ runCommand.enablePositionalOptions();
79
+ runCommand.argument("[event]", "Event type for a routed local run (e.g. push, pr:open)").option("--local", "Route the run with this machine as the ephemeral agent", false).option("--offline", "Force the throwaway/independent plane (offline)", false).option("--connected", "Force the connected/hybrid plane (requires attachment)", false).option("--in-place", "Reuse the working tree directly instead of an isolated clone", false).option("--trusted", "Route to the trusted fleet agent profile: steps see the ambient host env (minus the agent identity). Alias: --no-sandbox", false).option("--no-sandbox", "Alias for --trusted (the bwrap sandbox is already off by default)").option("--env <KEY=VALUE>", "Per-run secret (repeatable)", (val, prev) => [...prev, val], []).option("--payload <path>", "Dispatch payload JSON { action?, client_payload? } for a routed dispatch run").option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--quiet", "Suppress the banner + streaming output", false).option("--debug", "Verbose internals", false).action(async (event, options) => {
80
+ const retired = retiredCommandHint(event ? `run ${event}` : void 0);
81
+ if (retired) {
82
+ process.stderr.write(pc.red(`${retired}\n`));
96
83
  process.exit(2);
97
84
  }
98
- const success = await runLocalCommand({
85
+ const { runRoutedCommand } = await import("./commands/index.js");
86
+ const success = await runRoutedCommand({
99
87
  event,
100
- checkMode,
101
- pick: options.pick,
102
- workflow: options.workflow,
103
- job: options.job,
104
- branch: options.branch,
105
- sha: options.sha,
106
- payload: options.payload,
107
- concurrency: options.concurrency,
108
- keepGoing: options.keepGoing,
109
- container: options.container,
88
+ local: options.local,
89
+ offline: options.offline,
90
+ connected: options.connected,
91
+ inPlace: options.inPlace,
92
+ trusted: Boolean(options.trusted) || options.sandbox === false,
110
93
  env: options.env,
111
- inputs: options.input,
112
- quiet: options.quiet,
113
- json: options.json,
114
- junit: options.junit,
115
- files: options.files,
116
- debug: options.debug,
94
+ payload: options.payload,
117
95
  kiciDir: options.kiciDir,
118
- inPlace: options.inPlace,
119
- keep: options.keep
96
+ quiet: options.quiet,
97
+ debug: options.debug
120
98
  });
121
99
  process.exit(success ? 0 : 1);
122
100
  });
@@ -161,7 +139,7 @@ function buildProgram() {
161
139
  const success = await previewCommand(event, options);
162
140
  process.exit(success ? 0 : 1);
163
141
  });
164
- program.command("init").description("Initialize .kici/ directory with default workflows").option("--force", "Overwrite existing .kici/ directory", false).option("--skip-install", "Create files without installing dependencies", false).option("--package-manager <npm|pnpm|yarn>", "Force a package manager for the install step (default: auto-detect)").option("--mjs", "JavaScript-only mode (no TypeScript, no dependencies)", false).option("--no-agents-md", "Skip writing .kici/AGENTS.md (LLM authoring context)").option("--private-registry <url>", "Scaffold a workflow registries: entry pointing at <url>").option("--private-registry-scope <scope>", "Optional npm package scope (e.g. @my-org) for the private registry").option("--private-registry-secret <ref>", "Qualified secret reference (env:NAME) the private registry token comes from", "production:NPM_TOKEN").addOption(new Option("--use-verdaccio-local").default(false).hideHelp()).action(async (options) => {
142
+ program.command("init").description("Initialize .kici/ directory with default workflows").option("--force", "Overwrite existing .kici/ directory", false).option("--skip-install", "Create files without installing dependencies", false).option("--package-manager <npm|pnpm|yarn>", "Force a package manager for the install step (default: auto-detect)").option("--mjs", "JavaScript-only mode (no TypeScript, no dependencies)", false).option("--workspace", "Integrate .kici/ into the detected pnpm/npm/yarn workspace so workflows can import sibling packages", false).option("--standalone", "Force a self-contained .kici/ even inside a workspace", false).option("--no-agents-md", "Skip writing .kici/AGENTS.md (LLM authoring context)").option("--private-registry <url>", "Scaffold a workflow registries: entry pointing at <url>").option("--private-registry-scope <scope>", "Optional npm package scope (e.g. @my-org) for the private registry").option("--private-registry-secret <ref>", "Qualified secret reference (env:NAME) the private registry token comes from", "production:NPM_TOKEN").addOption(new Option("--use-verdaccio-local").default(false).hideHelp()).action(async (options) => {
165
143
  const { initCommand } = await import("./commands/index.js");
166
144
  const success = await initCommand({
167
145
  ...options,
@@ -174,7 +152,7 @@ function buildProgram() {
174
152
  const success = await hookInstallCommand({ git: options.git });
175
153
  process.exit(success ? 0 : 1);
176
154
  });
177
- program.command("login").description("Authenticate with KiCI via browser OAuth (default) or API key (--token)").option("--token <key>", "API key for direct authentication (legacy)").option("--device", "Force device authorization flow (for headless/SSH environments)").option("--platform-endpoint <url>", "Platform relay URL").option("--oidc-issuer <url>", "OIDC issuer URL (defaults to the hosted KiCI IdP unless a flag/env selects another)").option("--routing-key <key>", "Routing key for webhook source identification").addHelpText("after", `
155
+ program.command("login").description("Authenticate with KiCI via browser OAuth (default) or API key (--token)").option("--token <key>", "API key for direct authentication (legacy)").option("--device", "Force device authorization flow (for headless/SSH environments)").option("--platform-endpoint <url>", "Platform relay URL").option("--oidc-issuer <url>", "OIDC issuer URL (defaults to the hosted KiCI IdP unless a flag/env selects another)").option("--routing-key <key>", "Routing key for webhook source identification").option("--no-attach", "Skip the post-login prompt to attach the local dev plane").addHelpText("after", `
178
156
  Environment variables:
179
157
  KICI_BROWSER_CMD Custom browser command (use {url} placeholder, or 'none' to suppress)
180
158
  KICI_CALLBACK_PORT Fixed port for OAuth PKCE callback server (default: random)
@@ -188,7 +166,8 @@ Environment variables:
188
166
  device: options.device,
189
167
  platformEndpoint: options.platformEndpoint,
190
168
  oidcIssuer: options.oidcIssuer,
191
- routingKey: options.routingKey
169
+ routingKey: options.routingKey,
170
+ noAttachPrompt: options.attach === false
192
171
  });
193
172
  process.exit(success ? 0 : 1);
194
173
  });
@@ -224,6 +203,137 @@ Environment variables:
224
203
  const success = await orchestratorsUseCommand(name, { org: options.org });
225
204
  process.exit(success ? 0 : 1);
226
205
  });
206
+ const notificationsCommand = program.command("notifications").description("Manage the org's notification channels, subscriptions, and Slack roster");
207
+ const notificationsChannels = notificationsCommand.command("channels").description("Manage notification channels (Slack / email)");
208
+ notificationsChannels.command("list").description("List notification channels").option("--org <id>", "Target organization (overrides the active org)").option("--json", "Output as JSON").action(async (options) => {
209
+ const { notificationsChannelsListCommand } = await import("./commands/index.js");
210
+ const ok = await notificationsChannelsListCommand({
211
+ org: options.org,
212
+ json: options.json
213
+ });
214
+ process.exit(ok ? 0 : 1);
215
+ });
216
+ notificationsChannels.command("add").description("Add a notification channel").requiredOption("--type <slack|email>", "Channel transport type").requiredOption("--name <name>", "Channel display name").option("--connection <id>", "Slack connection id (slack channels)").option("--slack-channel <id>", "Slack channel id (slack channels)").option("--from-name <name>", "Sender name (email channels)").option("--reply-to <email>", "Reply-to address (email channels)").option("--org <id>", "Target organization (overrides the active org)").action(async (options) => {
217
+ const { notificationsChannelsAddCommand } = await import("./commands/index.js");
218
+ const ok = await notificationsChannelsAddCommand({
219
+ org: options.org,
220
+ type: options.type,
221
+ name: options.name,
222
+ connection: options.connection,
223
+ slackChannel: options.slackChannel,
224
+ fromName: options.fromName,
225
+ replyTo: options.replyTo
226
+ });
227
+ process.exit(ok ? 0 : 1);
228
+ });
229
+ notificationsChannels.command("remove").argument("<id>", "Channel id").description("Remove a notification channel").option("--org <id>", "Target organization (overrides the active org)").action(async (id, options) => {
230
+ const { notificationsChannelsRemoveCommand } = await import("./commands/index.js");
231
+ const ok = await notificationsChannelsRemoveCommand(id, { org: options.org });
232
+ process.exit(ok ? 0 : 1);
233
+ });
234
+ const notificationsSubscriptions = notificationsCommand.command("subscriptions").description("Manage notification subscriptions");
235
+ notificationsSubscriptions.command("list").description("List notification subscriptions").option("--org <id>", "Target organization (overrides the active org)").option("--json", "Output as JSON").action(async (options) => {
236
+ const { notificationsSubscriptionsListCommand } = await import("./commands/index.js");
237
+ const ok = await notificationsSubscriptionsListCommand({
238
+ org: options.org,
239
+ json: options.json
240
+ });
241
+ process.exit(ok ? 0 : 1);
242
+ });
243
+ notificationsSubscriptions.command("add").description("Add a notification subscription").requiredOption("--channel <id>", "Target channel id").requiredOption("--on-status <csv>", "Statuses to notify on (e.g. failed,success)").option("--level <run|job>", "Subscription granularity", "run").option("--scope <org|team|user|actor>", "Subscription scope", "org").option("--scope-id <id>", "Scope id (required for team/user scope)").option("--repo-glob <glob>", "Match runs whose repo matches this glob").option("--workflow-glob <glob>", "Match runs whose workflow matches this glob").option("--job-glob <glob>", "Match jobs matching this glob (job level)").option("--mentions <csv>", "Literal Slack member/group ids or emails to mention").option("--recipient-override <csv>", "Override the email recipient set").option("--on-failure-class <csv>", "Only match these failure classes").option("--accumulate-for <ms>", "Digest accumulation window in milliseconds").option("--org <id>", "Target organization (overrides the active org)").action(async (options) => {
244
+ const { notificationsSubscriptionsAddCommand } = await import("./commands/index.js");
245
+ const ok = await notificationsSubscriptionsAddCommand({
246
+ org: options.org,
247
+ channel: options.channel,
248
+ onStatus: options.onStatus,
249
+ level: options.level,
250
+ scope: options.scope,
251
+ scopeId: options.scopeId,
252
+ repoGlob: options.repoGlob,
253
+ workflowGlob: options.workflowGlob,
254
+ jobGlob: options.jobGlob,
255
+ mentions: options.mentions,
256
+ recipientOverride: options.recipientOverride,
257
+ onFailureClass: options.onFailureClass,
258
+ accumulateFor: options.accumulateFor
259
+ });
260
+ process.exit(ok ? 0 : 1);
261
+ });
262
+ notificationsSubscriptions.command("remove").argument("<id>", "Subscription id").description("Remove a notification subscription").option("--org <id>", "Target organization (overrides the active org)").action(async (id, options) => {
263
+ const { notificationsSubscriptionsRemoveCommand } = await import("./commands/index.js");
264
+ const ok = await notificationsSubscriptionsRemoveCommand(id, { org: options.org });
265
+ process.exit(ok ? 0 : 1);
266
+ });
267
+ const notificationsRoster = notificationsCommand.command("roster").description("Manage the Layer 1 Slack-identity roster (actor tagging)");
268
+ notificationsRoster.command("list").description("List Slack-identity roster entries").option("--org <id>", "Target organization (overrides the active org)").option("--json", "Output as JSON").action(async (options) => {
269
+ const { notificationsRosterListCommand } = await import("./commands/index.js");
270
+ const ok = await notificationsRosterListCommand({
271
+ org: options.org,
272
+ json: options.json
273
+ });
274
+ process.exit(ok ? 0 : 1);
275
+ });
276
+ notificationsRoster.command("add").description("Add a Slack-identity roster entry").requiredOption("--connection <id>", "Slack connection id").requiredOption("--subject-kind <kici_user|git_login|email>", "What the subject keys on").requiredOption("--subject <value>", "The KiCI user sub, git login, or email").requiredOption("--value <slackIdEmailOrHandle>", "Slack member id, email, or @handle").option("--input-form <id|username|email>", "How --value should be resolved", "id").option("--org <id>", "Target organization (overrides the active org)").action(async (options) => {
277
+ const { notificationsRosterAddCommand } = await import("./commands/index.js");
278
+ const ok = await notificationsRosterAddCommand({
279
+ org: options.org,
280
+ connection: options.connection,
281
+ subjectKind: options.subjectKind,
282
+ subject: options.subject,
283
+ inputForm: options.inputForm,
284
+ value: options.value
285
+ });
286
+ process.exit(ok ? 0 : 1);
287
+ });
288
+ notificationsRoster.command("remove").argument("<id>", "Roster entry id").description("Remove a Slack-identity roster entry").option("--org <id>", "Target organization (overrides the active org)").action(async (id, options) => {
289
+ const { notificationsRosterRemoveCommand } = await import("./commands/index.js");
290
+ const ok = await notificationsRosterRemoveCommand(id, { org: options.org });
291
+ process.exit(ok ? 0 : 1);
292
+ });
293
+ const localCommand = program.command("local").description("Manage the local dev orchestrator plane");
294
+ localCommand.command("up").description("Start (or reuse) the local dev plane").option("--offline", "Force the independent (offline) plane (does not clear the attachment record)", false).option("--connected", "Force the connected/hybrid plane (requires an attached, reachable Platform)", false).action(async (options) => {
295
+ const { localUpCommand } = await import("./commands/index.js");
296
+ const ok = await localUpCommand({
297
+ offline: options.offline,
298
+ connected: options.connected
299
+ });
300
+ process.exit(ok ? 0 : 1);
301
+ });
302
+ localCommand.command("status").description("Show local dev plane status and control commands").option("--json", "Emit machine-readable JSON (exits 0 for every state)", false).action(async (options) => {
303
+ const { localStatusCommand } = await import("./commands/index.js");
304
+ const ok = await localStatusCommand({ json: options.json });
305
+ process.exit(ok ? 0 : 1);
306
+ });
307
+ for (const [name, desc, fn] of [
308
+ [
309
+ "down",
310
+ "Stop the local dev plane",
311
+ "localDownCommand"
312
+ ],
313
+ [
314
+ "logs",
315
+ "Print the local dev plane orchestrator log path",
316
+ "localLogsCommand"
317
+ ],
318
+ [
319
+ "attach",
320
+ "Attach the local dev plane to the Platform (hybrid)",
321
+ "localAttachCommand"
322
+ ],
323
+ [
324
+ "detach",
325
+ "Detach the local dev plane from the Platform (offline)",
326
+ "localDetachCommand"
327
+ ]
328
+ ]) localCommand.command(name).description(desc).action(async () => {
329
+ const ok = await (await import("./commands/index.js"))[fn]();
330
+ process.exit(ok ? 0 : 1);
331
+ });
332
+ localCommand.command("trust-root").description("Export the offline dev-signed identity trust root ({ issuer, jwks }) to a file").argument("<file>", "Output path for the { issuer, jwks } trust-root JSON").action(async (file) => {
333
+ const { localTrustRootCommand } = await import("./commands/index.js");
334
+ const ok = await localTrustRootCommand(file);
335
+ process.exit(ok ? 0 : 1);
336
+ });
227
337
  program.command("secrets").description("Manage secrets").command("list").description("List test-available secret contexts").action(async () => {
228
338
  const { secretsListCommand } = await import("./commands/index.js");
229
339
  const success = await secretsListCommand();
@@ -240,7 +350,7 @@ Environment variables:
240
350
  process.exit(success ? 0 : 1);
241
351
  });
242
352
  const runsCommand = program.command("runs").description("Inspect and manage execution runs");
243
- runsCommand.command("list").description("List execution runs (mirrors the dashboard Runs page)").option("--status <s>", "Filter by status").option("--workflow <w>", "Filter by workflow name").option("--branch <b>", "Filter by branch/ref").option("--repo <r>", "Filter by repository").option("--trigger <t>", "Filter by trigger type").option("--source <routingKey>", "Filter by source routing key").option("--since <ts>", "Only runs since (ISO-8601 or epoch ms)").option("--page <n>", "Page number", (v) => parseInt(v, 10)).option("--json", "Output raw JSON", false).action(async (options) => {
353
+ runsCommand.command("list").description("List execution runs (mirrors the dashboard Runs page)").option("--status <s>", "Filter by status").option("--workflow <w>", "Filter by workflow name").option("--branch <b>", "Filter by branch/ref").option("--repo <r>", "Filter by repository").option("--trigger <t>", "Filter by trigger type").option("--source <routingKey>", "Filter by source routing key").option("--since <ts>", "Only runs since (ISO-8601 or epoch ms)").option("--cursor <cursor>", "Keyset cursor for the next page (from a prior nextCursor)").option("--json", "Output raw JSON", false).action(async (options) => {
244
354
  const { runsListCommand } = await import("./commands/index.js");
245
355
  const success = await runsListCommand(options);
246
356
  process.exit(success ? 0 : 1);
@@ -272,6 +382,20 @@ Environment variables:
272
382
  });
273
383
  process.exit(success ? 0 : 1);
274
384
  });
385
+ const runsArtifacts = runsCommand.command("artifacts").description("List and download a run's artifacts");
386
+ runsArtifacts.command("list").argument("<run-id>", "Run ID whose artifacts to list").description("List the artifacts a run uploaded").option("--json", "Output raw JSON", false).action(async (runId, options) => {
387
+ const { runsArtifactsListCommand } = await import("./commands/index.js");
388
+ const success = await runsArtifactsListCommand(runId, { json: options.json });
389
+ process.exit(success ? 0 : 1);
390
+ });
391
+ runsArtifacts.command("download").argument("<run-id>", "Run ID whose artifacts to download").argument("[name]", "Artifact name (omit to download every artifact of the run)").description("Download one artifact, or all of them — extracts by default").option("--archive", "Save the raw .tar.gz instead of extracting", false).option("-o, --output <dir>", "Output directory (default: current directory)").action(async (runId, name, options) => {
392
+ const { runsArtifactsDownloadCommand } = await import("./commands/index.js");
393
+ const success = await runsArtifactsDownloadCommand(runId, name, {
394
+ archive: options.archive,
395
+ output: options.output
396
+ });
397
+ process.exit(success ? 0 : 1);
398
+ });
275
399
  program.command("approve").argument("<run-id>", "Run ID whose approval gate to approve").description("Approve a held approval gate for a run").option("--job <name>", "Approve the hold for a specific job").option("--step <index>", "Approve a step-scoped hold (requires --job)").action(async (runId, options) => {
276
400
  const { approveCommand } = await import("./commands/index.js");
277
401
  const success = await approveCommand(runId, {
@@ -299,7 +423,15 @@ Environment variables:
299
423
  const success = await endpointsCommand({ kiciDir: options.kiciDir });
300
424
  process.exit(success ? 0 : 1);
301
425
  });
302
- program.command("diagnostics").description("Show orchestrators, scalers, and agents (mirrors the dashboard Diagnostics page)").option("--json", "Output raw JSON", false).option("--verbose", "Show extended per-agent fields", false).option("--orchestrator <id>", "Scope the tree to one connection id").action(async (options) => {
426
+ program.command("doctor").description("Diagnose your KiCI setup and print the exact next command for each problem").option("--json", "Output raw JSON instead of a table", false).option("--kici-dir <path>", "Path to the .kici directory", ".kici").action(async (options) => {
427
+ const { doctorCommand } = await import("./commands/index.js");
428
+ const exitCode = await doctorCommand({
429
+ json: options.json,
430
+ kiciDir: options.kiciDir
431
+ });
432
+ process.exit(exitCode);
433
+ });
434
+ program.command("diagnostics").description("Show orchestrators, scalers, and agents (mirrors the dashboard Infrastructure page)").option("--json", "Output raw JSON", false).option("--verbose", "Show extended per-agent fields", false).option("--orchestrator <id>", "Scope the tree to one connection id").action(async (options) => {
303
435
  const { diagnosticsCommand } = await import("./commands/index.js");
304
436
  const success = await diagnosticsCommand({
305
437
  json: options.json,
@@ -335,7 +467,7 @@ Environment variables:
335
467
  const success = await drainWorkerCommand({ url: options.url });
336
468
  process.exit(success ? 0 : 1);
337
469
  });
338
- program.command("verify-attestation").argument("[artifact]", "Artifact path to digest-check against the attestation subject (optional)").description("Verify a KiCI provenance attestation bundle offline").option("--bundle <path>", "Path or URL to the attestation bundle JSON").option("--trust-root <url-or-file>", "Trusted issuer URL, or a self-contained { issuer, jwks } file (default: hosted KiCI platform)").option("--audience <aud>", "Expected token audience").option("--json", "Output structured JSON result", false).action(async (artifact, options) => {
470
+ program.command("verify-attestation").argument("[artifact]", "Artifact path to digest-check against the attestation subject (optional)").description("Verify a KiCI provenance attestation bundle offline").option("--bundle <path>", "Path or URL to the attestation bundle JSON").option("--trust-root <url-or-file>", "Trusted issuer URL, or a self-contained { issuer, jwks } file (default: your configured orchestrator, else the hosted KiCI platform)").option("--audience <aud>", "Expected token audience").option("--json", "Output structured JSON result", false).action(async (artifact, options) => {
339
471
  const { verifyAttestationCommand } = await import("./commands/index.js");
340
472
  const success = await verifyAttestationCommand(artifact, options);
341
473
  process.exit(success ? 0 : 1);
@@ -1,5 +1,5 @@
1
1
  import { CheckMode } from '@kici-dev/engine';
2
- /** The two run-mode flags shared by `kici run local` and `kici run remote`. */
2
+ /** The two check-mode flags accepted by `kici run remote`. */
3
3
  export interface CheckModeFlags {
4
4
  /** --check: report drift, change nothing. */
5
5
  check?: boolean;
@@ -1,26 +1,39 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
- import { formatError, isCompilerError } from "../errors/formatter.js";
2
+ import { PURITY_FALLBACK_CODE, compilerError, formatError, isCompilerError } from "../errors/formatter.js";
3
3
  import "../errors/index.js";
4
4
  import { discoverWorkflows, resolveKiciDir } from "../execution/executor.js";
5
5
  import "../execution/index.js";
6
6
  import { validateConfig } from "../validation/validator.js";
7
+ import { runTypecheck } from "../validation/typecheck.js";
7
8
  import "../validation/index.js";
8
- import { computeLockfileHash, detectGitRoot, generateLockFile, serializeLockFile } from "../lockfile/generator.js";
9
+ import { BREAKING_FLOOR, SCHEMA_VERSION } from "../types.js";
10
+ import { computeLockfileHash, detectGitRoot, generateLockFile, schemaWindowWarning, serializeLockFile } from "../lockfile/generator.js";
11
+ import { collectWorkflowPurityWarnings } from "../lockfile/purity-diagnostics.js";
9
12
  import "../lockfile/index.js";
10
13
  import path from "node:path";
11
14
  import { existsSync } from "node:fs";
12
15
  import pc from "picocolors";
13
- import fs from "node:fs/promises";
16
+ import fs$1 from "node:fs/promises";
14
17
  import { logger, toErrorMessage } from "@kici-dev/core";
15
18
  import { PackageManager, detectPackageManagerSync } from "@kici-dev/core/package-manager";
16
19
  import { execSync } from "node:child_process";
17
20
  //#region src/commands/compile.ts
18
21
  /**
22
+ * Render an impure dynamic-value function as a `W101` compile warning naming the
23
+ * job, the field, the impurity reason, and the ~5-10s agent-side init-job cost.
24
+ */
25
+ function formatPurityWarning(w) {
26
+ return formatError(compilerError(PURITY_FALLBACK_CODE, `job "${w.jobName}": ${w.field} function is not pure (${w.reason}). An init job will be required, adding ~5-10s delay.`, {
27
+ severity: "warning",
28
+ suggestion: `Make the ${w.field} function pure (synchronous, referencing only its parameters) to inline it and skip the init job. See https://docs.kici.dev — dynamic values.`
29
+ }));
30
+ }
31
+ /**
19
32
  * Read the existing lock file and return its lockfileHash, if present.
20
33
  */
21
34
  async function readExistingLockfileHash(lockPath) {
22
35
  try {
23
- const content = await fs.readFile(lockPath, "utf-8");
36
+ const content = await fs$1.readFile(lockPath, "utf-8");
24
37
  return JSON.parse(content).lockfileHash ?? void 0;
25
38
  } catch {
26
39
  return;
@@ -67,15 +80,18 @@ async function compileCommand(options) {
67
80
  }
68
81
  const { workflows: workflowsWithSource, workflowDir } = await discoverWorkflows(absoluteKiciDir);
69
82
  if (options.verbose) logger.debug(pc.dim(`Found ${workflowsWithSource.length} workflow(s) in ${workflowDir}`));
70
- const validation = validateConfig(workflowsWithSource.map((w) => w.workflow), workflowDir);
83
+ const validation = validateConfig(workflowsWithSource);
71
84
  if (!validation.valid) {
72
85
  for (const error of validation.errors) logger.error(formatError(error));
73
86
  return false;
74
87
  }
75
88
  if (options.verbose) logger.debug(pc.dim("Validation passed"));
76
89
  const lockJson = serializeLockFile(generateLockFile(workflowsWithSource));
90
+ for (const warning of collectWorkflowPurityWarnings(workflowsWithSource)) logger.warn(formatPurityWarning(warning));
91
+ const windowWarning = schemaWindowWarning(BREAKING_FLOOR, SCHEMA_VERSION);
92
+ if (windowWarning) logger.warn(pc.yellow(windowWarning));
77
93
  if (!options.check) {
78
- await fs.writeFile(lockPath, lockJson, "utf-8");
94
+ await fs$1.writeFile(lockPath, lockJson, "utf-8");
79
95
  if (!options.quiet) logger.info(pc.green("✓") + ` Compiled workflows → .kici/kici.lock.json` + pc.dim(` (${workflowsWithSource.length} workflow${workflowsWithSource.length !== 1 ? "s" : ""})`));
80
96
  try {
81
97
  const { loadGlobalConfig } = await import("../remote/config.js");
@@ -92,7 +108,15 @@ async function compileCommand(options) {
92
108
  } catch {
93
109
  logger.warn(pc.yellow("Could not refresh types (Platform unreachable). Compilation succeeded."));
94
110
  }
95
- } else if (!options.quiet) logger.info(pc.green("✓") + ` Workflows are valid` + pc.dim(` (${workflowsWithSource.length} workflow${workflowsWithSource.length !== 1 ? "s" : ""})`));
111
+ } else {
112
+ const tc = await runTypecheck(absoluteKiciDir);
113
+ if (tc.errors.length > 0) {
114
+ for (const error of tc.errors) logger.error(formatError(error));
115
+ return false;
116
+ }
117
+ if (!tc.ran && !options.quiet) logger.info(pc.dim("Type-check skipped (no tsconfig.json — JavaScript mode)"));
118
+ if (!options.quiet) logger.info(pc.green("✓") + ` Workflows are valid` + pc.dim(` (${workflowsWithSource.length} workflow${workflowsWithSource.length !== 1 ? "s" : ""})`));
119
+ }
96
120
  return true;
97
121
  } catch (error) {
98
122
  if (isCompilerError(error)) logger.error(formatError(error));
@@ -3,8 +3,9 @@ import { DashboardClient, DashboardClientError } from "../remote/dashboard-clien
3
3
  import { relativeTime } from "../remote/render.js";
4
4
  import pc from "picocolors";
5
5
  import { logger, toErrorMessage } from "@kici-dev/core";
6
+ import { InfraAlertSeverity, normalizeInfraAlertSeverity } from "@kici-dev/engine";
6
7
  //#region src/commands/diagnostics.ts
7
- /** kici diagnostics — mirrors the dashboard Diagnostics page (infra tree). */
8
+ /** kici diagnostics — mirrors the dashboard Infrastructure page (infra tree). */
8
9
  async function diagnosticsCommand(options = {}) {
9
10
  try {
10
11
  const client = await DashboardClient.load();
@@ -36,9 +37,20 @@ function printSummary(s) {
36
37
  const m = s.executionMetrics;
37
38
  console.log(pc.bold("\nDiagnostics") + pc.gray(` runs(24h)=${m.totalRuns} success=${m.successRate}% avg=${m.avgDurationSeconds}s queued=${m.queuedJobs} running=${m.runningJobs}` + (s.orphanedConnections > 0 ? ` orphaned=${s.orphanedConnections}` : "")));
38
39
  }
40
+ /**
41
+ * Alert colour per known severity.
42
+ *
43
+ * Typed `Record<InfraAlertSeverity, …>`: adding a severity to the engine enum
44
+ * without colouring it fails `pnpm typecheck` instead of silently inheriting
45
+ * the warning colour.
46
+ */
47
+ const ALERT_SEVERITY_COLOR = {
48
+ [InfraAlertSeverity.enum.warning]: pc.yellow,
49
+ [InfraAlertSeverity.enum.critical]: pc.red
50
+ };
39
51
  function printAlerts(alerts) {
40
52
  for (const a of alerts) {
41
- const color = a.severity === "critical" ? pc.red : pc.yellow;
53
+ const color = ALERT_SEVERITY_COLOR[normalizeInfraAlertSeverity(a.severity)];
42
54
  console.log(color(` ! [${a.type}] ${a.message}`));
43
55
  }
44
56
  }