@kici-dev/compiler 0.1.27 → 0.3.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 (153) 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.js +120 -11
  4. package/dist/commands/compile.js +28 -4
  5. package/dist/commands/diagnostics.js +14 -2
  6. package/dist/commands/doctor.d.ts +64 -0
  7. package/dist/commands/doctor.js +413 -0
  8. package/dist/commands/endpoints.js +6 -0
  9. package/dist/commands/fixture.js +13 -0
  10. package/dist/commands/hook.js +3 -2
  11. package/dist/commands/index.d.ts +8 -0
  12. package/dist/commands/index.js +5 -1
  13. package/dist/commands/init-host-os.d.ts +34 -0
  14. package/dist/commands/init-host-os.js +44 -0
  15. package/dist/commands/init.d.ts +4 -0
  16. package/dist/commands/init.js +159 -36
  17. package/dist/commands/local.d.ts +28 -2
  18. package/dist/commands/local.js +74 -5
  19. package/dist/commands/login.js +1 -1
  20. package/dist/commands/notifications.d.ts +61 -0
  21. package/dist/commands/notifications.js +274 -0
  22. package/dist/commands/preview.js +8 -1
  23. package/dist/commands/run-routed.js +10 -1
  24. package/dist/commands/run.js +41 -12
  25. package/dist/commands/runs/artifacts/download.d.ts +24 -0
  26. package/dist/commands/runs/artifacts/download.js +154 -0
  27. package/dist/commands/runs/artifacts/list.d.ts +5 -0
  28. package/dist/commands/runs/artifacts/list.js +47 -0
  29. package/dist/commands/runs/cancel.js +2 -2
  30. package/dist/commands/runs/list.js +8 -2
  31. package/dist/commands/verify-attestation.d.ts +3 -1
  32. package/dist/commands/verify-attestation.js +18 -7
  33. package/dist/errors/formatter.d.ts +14 -1
  34. package/dist/errors/formatter.js +13 -3
  35. package/dist/errors/index.d.ts +2 -1
  36. package/dist/errors/index.js +3 -2
  37. package/dist/errors/source-location.d.ts +20 -0
  38. package/dist/errors/source-location.js +45 -0
  39. package/dist/execution/executor.js +25 -5
  40. package/dist/fixtures/defaults/index.js +2 -1
  41. package/dist/llm-context/llms-architecture.txt +51 -22
  42. package/dist/llm-context/llms-cli.txt +2461 -1582
  43. package/dist/llm-context/llms-features.txt +178 -106
  44. package/dist/llm-context/llms-full.txt +6127 -4426
  45. package/dist/llm-context/llms-getting-started.txt +306 -42
  46. package/dist/llm-context/llms-patterns.txt +13 -7
  47. package/dist/llm-context/llms-sdk-runtime.txt +2013 -0
  48. package/dist/llm-context/llms-sdk.txt +591 -2152
  49. package/dist/llm-context/llms.txt +30 -16
  50. package/dist/local-plane/orchestrator-process.d.ts +12 -7
  51. package/dist/local-plane/orchestrator-process.js +28 -14
  52. package/dist/local-plane/os-mismatch-hint.d.ts +16 -0
  53. package/dist/local-plane/os-mismatch-hint.js +34 -0
  54. package/dist/local-plane/plane-liveness.d.ts +68 -0
  55. package/dist/local-plane/plane-liveness.js +145 -0
  56. package/dist/local-plane/plane-manager.d.ts +60 -18
  57. package/dist/local-plane/plane-manager.js +210 -70
  58. package/dist/local-plane/plane-trigger.d.ts +13 -4
  59. package/dist/local-plane/plane-trigger.js +27 -14
  60. package/dist/local-plane/platform-attach.js +1 -5
  61. package/dist/local-plane/port-holder.d.ts +78 -0
  62. package/dist/local-plane/port-holder.js +227 -0
  63. package/dist/local-plane/postgres.d.ts +15 -3
  64. package/dist/local-plane/postgres.js +34 -12
  65. package/dist/local-plane/run-follow.d.ts +20 -0
  66. package/dist/local-plane/run-follow.js +51 -4
  67. package/dist/local-plane/scaler-config.d.ts +28 -14
  68. package/dist/local-plane/scaler-config.js +59 -41
  69. package/dist/local-plane/source-provider.js +4 -10
  70. package/dist/lockfile/generator.d.ts +14 -1
  71. package/dist/lockfile/generator.js +99 -33
  72. package/dist/lockfile/index.d.ts +3 -1
  73. package/dist/lockfile/index.js +3 -2
  74. package/dist/lockfile/purity-diagnostics.d.ts +31 -0
  75. package/dist/lockfile/purity-diagnostics.js +52 -0
  76. package/dist/postinstall.js +2 -1
  77. package/dist/remote/artifact-extract.d.ts +8 -0
  78. package/dist/remote/artifact-extract.js +58 -0
  79. package/dist/remote/config.d.ts +21 -3
  80. package/dist/remote/config.js +24 -5
  81. package/dist/remote/dashboard-client.d.ts +39 -2
  82. package/dist/remote/dashboard-client.js +47 -4
  83. package/dist/remote/fs-case.d.ts +21 -0
  84. package/dist/remote/fs-case.js +45 -0
  85. package/dist/remote/history.d.ts +9 -3
  86. package/dist/remote/history.js +79 -10
  87. package/dist/remote/local-repo-identity.d.ts +1 -1
  88. package/dist/remote/local-repo-identity.js +2 -1
  89. package/dist/remote/notifications-client.d.ts +149 -0
  90. package/dist/remote/notifications-client.js +103 -0
  91. package/dist/remote/oauth.d.ts +13 -0
  92. package/dist/remote/oauth.js +31 -5
  93. package/dist/remote/output/summary.js +76 -14
  94. package/dist/remote/render.js +30 -9
  95. package/dist/remote/uploader.js +5 -4
  96. package/dist/templates/agents-md.d.ts +1 -1
  97. package/dist/templates/agents-md.js +6 -6
  98. package/dist/templates/index.d.ts +1 -1
  99. package/dist/templates/index.js +2 -2
  100. package/dist/templates/package-json.d.ts +15 -0
  101. package/dist/templates/package-json.js +26 -5
  102. package/dist/test-runner/dry-run.d.ts +2 -1
  103. package/dist/test-runner/dry-run.js +18 -1
  104. package/dist/test-runner/event-types.d.ts +2 -0
  105. package/dist/test-runner/event-types.js +3 -0
  106. package/dist/test-runner/job-executor.d.ts +1 -0
  107. package/dist/test-runner/job-executor.js +81 -43
  108. package/dist/test-runner/output-formatter.d.ts +5 -2
  109. package/dist/test-runner/output-formatter.js +6 -4
  110. package/dist/test-runner/payload-builder.d.ts +7 -1
  111. package/dist/test-runner/payload-builder.js +11 -2
  112. package/dist/test-runner/rule-evaluator.d.ts +7 -3
  113. package/dist/test-runner/rule-evaluator.js +13 -12
  114. package/dist/test-runner/step-context.d.ts +23 -5
  115. package/dist/test-runner/step-context.js +28 -118
  116. package/dist/types.d.ts +21 -2
  117. package/dist/types.js +4 -2
  118. package/dist/validation/index.d.ts +2 -0
  119. package/dist/validation/index.js +2 -1
  120. package/dist/validation/typecheck.d.ts +28 -0
  121. package/dist/validation/typecheck.js +110 -0
  122. package/dist/validation/validator.d.ts +9 -4
  123. package/dist/validation/validator.js +38 -44
  124. package/package.json +12 -15
  125. package/sbom.spdx.json +1322 -1964
  126. package/dist/assets/api-TJJVHrjC.json +0 -118
  127. package/dist/assets/descriptor-BTtjzN9L.json +0 -1382
  128. package/dist/assets/package-BpQF9kR8.json +0 -74
  129. package/dist/assets/package-Ceo2h27X.json +0 -89
  130. package/dist/assets/source_context-D0atuL28.json +0 -20
  131. package/dist/assets/type-BFqO8SCZ.json +0 -202
  132. package/dist/commands/cancel.d.ts +0 -22
  133. package/dist/commands/detect-package-manager.d.ts +0 -42
  134. package/dist/commands/held-run-resolve.d.ts +0 -50
  135. package/dist/commands/status.d.ts +0 -34
  136. package/dist/commands/test.d.ts +0 -88
  137. package/dist/errors/codes.d.ts +0 -25
  138. package/dist/local-executor/dag-scheduler.d.ts +0 -44
  139. package/dist/local-executor/index.d.ts +0 -23
  140. package/dist/local-executor/job-runner.d.ts +0 -47
  141. package/dist/local-executor/materializer.d.ts +0 -44
  142. package/dist/local-executor/output-streamer.d.ts +0 -31
  143. package/dist/local-executor/payload-generator.d.ts +0 -16
  144. package/dist/local-executor/picker.d.ts +0 -33
  145. package/dist/local-executor/runs-on-display.d.ts +0 -9
  146. package/dist/local-executor/secret-loader.d.ts +0 -18
  147. package/dist/local-executor/to-event-payload.d.ts +0 -16
  148. package/dist/local-executor/types.d.ts +0 -93
  149. package/dist/local-executor/workflow-lock.d.ts +0 -82
  150. package/dist/package-F7UXSDHW.json +0 -74
  151. package/dist/remote/client.d.ts +0 -210
  152. package/dist/remote/observer.d.ts +0 -81
  153. package/dist/test-runner/summary.d.ts +0 -7
@@ -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
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.27";
10
+ const version = "0.3.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
@@ -54,7 +54,7 @@ function buildProgram() {
54
54
  } });
55
55
  program.showSuggestionAfterError(true);
56
56
  program.showHelpAfterError("(run `kici --help` to see all commands)");
57
- 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) => {
58
58
  const { compileCommand, watchCommand } = await import("./commands/index.js");
59
59
  if (options.watch) await watchCommand({
60
60
  kiciDir: options.kiciDir,
@@ -139,7 +139,7 @@ function buildProgram() {
139
139
  const success = await previewCommand(event, options);
140
140
  process.exit(success ? 0 : 1);
141
141
  });
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("--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) => {
143
143
  const { initCommand } = await import("./commands/index.js");
144
144
  const success = await initCommand({
145
145
  ...options,
@@ -203,6 +203,93 @@ Environment variables:
203
203
  const success = await orchestratorsUseCommand(name, { org: options.org });
204
204
  process.exit(success ? 0 : 1);
205
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
+ });
206
293
  const localCommand = program.command("local").description("Manage the local dev orchestrator plane");
207
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) => {
208
295
  const { localUpCommand } = await import("./commands/index.js");
@@ -212,12 +299,12 @@ Environment variables:
212
299
  });
213
300
  process.exit(ok ? 0 : 1);
214
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
+ });
215
307
  for (const [name, desc, fn] of [
216
- [
217
- "status",
218
- "Show local dev plane status and control commands",
219
- "localStatusCommand"
220
- ],
221
308
  [
222
309
  "down",
223
310
  "Stop the local dev plane",
@@ -263,7 +350,7 @@ Environment variables:
263
350
  process.exit(success ? 0 : 1);
264
351
  });
265
352
  const runsCommand = program.command("runs").description("Inspect and manage execution runs");
266
- 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) => {
267
354
  const { runsListCommand } = await import("./commands/index.js");
268
355
  const success = await runsListCommand(options);
269
356
  process.exit(success ? 0 : 1);
@@ -295,6 +382,20 @@ Environment variables:
295
382
  });
296
383
  process.exit(success ? 0 : 1);
297
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
+ });
298
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) => {
299
400
  const { approveCommand } = await import("./commands/index.js");
300
401
  const success = await approveCommand(runId, {
@@ -322,7 +423,15 @@ Environment variables:
322
423
  const success = await endpointsCommand({ kiciDir: options.kiciDir });
323
424
  process.exit(success ? 0 : 1);
324
425
  });
325
- 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) => {
326
435
  const { diagnosticsCommand } = await import("./commands/index.js");
327
436
  const success = await diagnosticsCommand({
328
437
  json: options.json,
@@ -358,7 +467,7 @@ Environment variables:
358
467
  const success = await drainWorkerCommand({ url: options.url });
359
468
  process.exit(success ? 0 : 1);
360
469
  });
361
- 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) => {
362
471
  const { verifyAttestationCommand } = await import("./commands/index.js");
363
472
  const success = await verifyAttestationCommand(artifact, options);
364
473
  process.exit(success ? 0 : 1);
@@ -1,11 +1,14 @@
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";
@@ -16,6 +19,16 @@ import { PackageManager, detectPackageManagerSync } from "@kici-dev/core/package
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) {
@@ -67,13 +80,16 @@ 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
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" : ""})`));
@@ -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
  }
@@ -0,0 +1,64 @@
1
+ import { type DiagnoseResponse, type DiagnoseResult } from '@kici-dev/core';
2
+ import { type DiagnosticsInfrastructureResponse } from '@kici-dev/engine';
3
+ import { type GlobalConfig } from '../remote/config.js';
4
+ import { type DashboardErrorKind } from '../remote/dashboard-client.js';
5
+ import { type LockFile } from '../types.js';
6
+ /** A doctor check result: a shared DiagnoseResult plus the exact fix command. */
7
+ export interface DoctorCheckResult extends DiagnoseResult {
8
+ nextCommand?: string;
9
+ }
10
+ /** A check body before the runner stamps its measured duration. */
11
+ export type DoctorCheckBody = Omit<DoctorCheckResult, 'durationMs'>;
12
+ /** The outcome of the single authenticated infrastructure probe. */
13
+ export type ProbeOutcome = {
14
+ ok: true;
15
+ infra: DiagnosticsInfrastructureResponse;
16
+ } | {
17
+ ok: false;
18
+ kind: DashboardErrorKind;
19
+ message: string;
20
+ };
21
+ /** Is a stored token + endpoint present (regardless of expiry)? */
22
+ export declare function hasCredentials(config: GlobalConfig): boolean;
23
+ /** Check 1: logged in with an unexpired token. */
24
+ export declare function checkLogin(config: GlobalConfig, now: number): DoctorCheckBody;
25
+ /** Check 2: an active organization is selected. */
26
+ export declare function checkActiveOrg(config: GlobalConfig): DoctorCheckBody;
27
+ /** Check 3: the Platform accepted the stored token (live-token probe). */
28
+ export declare function checkLiveToken(probe: ProbeOutcome | null): DoctorCheckBody;
29
+ /** Local + git state of the compiled lock file. */
30
+ export interface LockState {
31
+ exists: boolean;
32
+ fresh: boolean;
33
+ committed: boolean;
34
+ gitAvailable: boolean;
35
+ lock: LockFile | null;
36
+ }
37
+ /** Check 4: an orchestrator is connected for this org. */
38
+ export declare function checkOrchestrator(probe: ProbeOutcome | null): DoctorCheckBody;
39
+ /** Check 5: the lock file is present, fresh, and committed. */
40
+ export declare function checkLockFile(state: LockState): DoctorCheckBody;
41
+ /** Check 6: every workflow's runsOn labels are satisfiable by a connected agent or scaler. */
42
+ export declare function checkLabels(lock: LockFile | null, probe: ProbeOutcome | null): DoctorCheckBody;
43
+ export interface DoctorOptions {
44
+ json?: boolean;
45
+ kiciDir?: string;
46
+ }
47
+ /** Injectable seams so every path is unit-testable without real IO. */
48
+ export interface DoctorDeps {
49
+ loadConfig: () => Promise<GlobalConfig>;
50
+ probe: (config: GlobalConfig) => Promise<ProbeOutcome | null>;
51
+ gatherLockState: (kiciDir: string) => Promise<LockState>;
52
+ now: () => number;
53
+ }
54
+ /** Render the funnel as a status table with a trailing next-command hint per row. */
55
+ export declare function renderDoctorTable(response: DiagnoseResponse & {
56
+ checks: DoctorCheckResult[];
57
+ }): string;
58
+ /**
59
+ * Run the onboarding-funnel diagnostic and return the process exit code
60
+ * (0 all pass · 1 any warn · 2 any fail). The single infrastructure probe is
61
+ * timed and attributed to the token-live check; the local checks are instant.
62
+ */
63
+ export declare function doctorCommand(options?: DoctorOptions, deps?: DoctorDeps): Promise<number>;
64
+ //# sourceMappingURL=doctor.d.ts.map