@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
@@ -0,0 +1,274 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { DashboardClientError } from "../remote/dashboard-client.js";
3
+ import { NotificationChannelKind, NotificationScopeKind, NotificationSubLevel, NotificationsClient, RosterInputForm, RosterSubjectKind } from "../remote/notifications-client.js";
4
+ import pc from "picocolors";
5
+ import { toErrorMessage } from "@kici-dev/core";
6
+ //#region src/commands/notifications.ts
7
+ /**
8
+ * kici notifications commands
9
+ *
10
+ * Customer self-service management of an org's notification configuration —
11
+ * channels, subscriptions, and the Layer 1 Slack-identity roster — under the
12
+ * logged-in user's PAT against the hosted Platform's org-scoped HTTP API. This
13
+ * is a tenant-plane developer operation, so it lives in the public `kici` CLI
14
+ * (like `kici org` / `kici verify-attestation`), never the DB-direct
15
+ * `kici-platform-admin`.
16
+ */
17
+ /** Split a comma-separated flag value into a trimmed, non-empty list. */
18
+ function parseCsv(value) {
19
+ if (!value) return [];
20
+ return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
21
+ }
22
+ /** Print a simple padded table. */
23
+ function printTable(headers, rows) {
24
+ if (rows.length === 0) {
25
+ console.log(pc.gray("None."));
26
+ return;
27
+ }
28
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
29
+ console.log(pc.bold(headers.map((h, i) => h.padEnd(widths[i])).join(" ")));
30
+ for (const row of rows) console.log(row.map((c, i) => c.padEnd(widths[i])).join(" "));
31
+ }
32
+ /**
33
+ * Load the client and run `fn`, turning any {@link DashboardClientError} into a
34
+ * red console message and a `false` return (the CLI exit-code convention).
35
+ */
36
+ async function withClient(opts, fn) {
37
+ try {
38
+ return await fn(await NotificationsClient.load({ org: opts.org }));
39
+ } catch (err) {
40
+ if (err instanceof DashboardClientError) {
41
+ console.error(pc.red(err.message));
42
+ return false;
43
+ }
44
+ console.error(pc.red(`Notifications command failed: ${toErrorMessage(err)}`));
45
+ return false;
46
+ }
47
+ }
48
+ /** Validate a value against a Zod enum, printing an actionable error on miss. */
49
+ function requireEnum(label, value, options) {
50
+ if (value === void 0) {
51
+ console.error(pc.red(`--${label} is required (one of: ${options.join(", ")})`));
52
+ return null;
53
+ }
54
+ if (!options.includes(value)) {
55
+ console.error(pc.red(`Invalid --${label} "${value}" (one of: ${options.join(", ")})`));
56
+ return null;
57
+ }
58
+ return value;
59
+ }
60
+ async function notificationsChannelsListCommand(opts) {
61
+ return withClient(opts, async (client) => {
62
+ const channels = await client.listChannels();
63
+ if (opts.json) {
64
+ console.log(JSON.stringify(channels, null, 2));
65
+ return true;
66
+ }
67
+ printTable([
68
+ "ID",
69
+ "TYPE",
70
+ "NAME",
71
+ "MANAGED"
72
+ ], channels.map((c) => [
73
+ c.id,
74
+ c.type,
75
+ c.name,
76
+ c.managed ? "yes" : "no"
77
+ ]));
78
+ return true;
79
+ });
80
+ }
81
+ async function notificationsChannelsAddCommand(opts) {
82
+ const type = requireEnum("type", opts.type, NotificationChannelKind.options);
83
+ if (!type) return false;
84
+ if (!opts.name) {
85
+ console.error(pc.red("--name is required"));
86
+ return false;
87
+ }
88
+ let body;
89
+ if (type === NotificationChannelKind.enum.slack) {
90
+ if (!opts.connection || !opts.slackChannel) {
91
+ console.error(pc.red("slack channels require --connection <id> and --slack-channel <id>"));
92
+ return false;
93
+ }
94
+ body = {
95
+ type: "slack",
96
+ connection_id: opts.connection,
97
+ slack_channel_id: opts.slackChannel,
98
+ name: opts.name
99
+ };
100
+ } else body = {
101
+ type: "email",
102
+ name: opts.name,
103
+ ...opts.fromName ? { from_name: opts.fromName } : {},
104
+ ...opts.replyTo ? { reply_to: opts.replyTo } : {}
105
+ };
106
+ return withClient(opts, async (client) => {
107
+ const channel = await client.createChannel(body);
108
+ console.log(pc.green(`Channel created: ${channel.id}`));
109
+ return true;
110
+ });
111
+ }
112
+ async function notificationsChannelsRemoveCommand(id, opts) {
113
+ return withClient(opts, async (client) => {
114
+ await client.deleteChannel(id);
115
+ console.log(pc.green(`Channel removed: ${id}`));
116
+ return true;
117
+ });
118
+ }
119
+ async function notificationsSubscriptionsListCommand(opts) {
120
+ return withClient(opts, async (client) => {
121
+ const subs = await client.listSubscriptions();
122
+ if (opts.json) {
123
+ console.log(JSON.stringify(subs, null, 2));
124
+ return true;
125
+ }
126
+ printTable([
127
+ "ID",
128
+ "LEVEL",
129
+ "SCOPE",
130
+ "CHANNEL",
131
+ "ON STATUS",
132
+ "ENABLED"
133
+ ], subs.map((s) => [
134
+ s.id,
135
+ s.level,
136
+ s.scope_id ? `${s.scope_type}:${s.scope_id}` : s.scope_type,
137
+ s.channel_id,
138
+ (s.on_status ?? []).join(","),
139
+ s.enabled ? "yes" : "no"
140
+ ]));
141
+ return true;
142
+ });
143
+ }
144
+ /** Build a create-subscription body from parsed flags (null on validation error). */
145
+ function buildSubscriptionBody(opts) {
146
+ if (!opts.channel) {
147
+ console.error(pc.red("--channel <id> is required"));
148
+ return null;
149
+ }
150
+ const level = requireEnum("level", opts.level ?? "run", NotificationSubLevel.options);
151
+ if (!level) return null;
152
+ const scope = requireEnum("scope", opts.scope ?? "org", NotificationScopeKind.options);
153
+ if (!scope) return null;
154
+ const onStatus = parseCsv(opts.onStatus);
155
+ if (onStatus.length === 0) {
156
+ console.error(pc.red("--on-status <csv> is required (e.g. failed,success)"));
157
+ return null;
158
+ }
159
+ const needsScopeId = scope === NotificationScopeKind.enum.team || scope === NotificationScopeKind.enum.user;
160
+ if (needsScopeId && !opts.scopeId) {
161
+ console.error(pc.red(`--scope-id is required for ${scope} scope`));
162
+ return null;
163
+ }
164
+ let accumulateFor = null;
165
+ if (opts.accumulateFor !== void 0) {
166
+ accumulateFor = Number(opts.accumulateFor);
167
+ if (!Number.isFinite(accumulateFor)) {
168
+ console.error(pc.red("--accumulate-for must be a number of milliseconds"));
169
+ return null;
170
+ }
171
+ }
172
+ const mentions = parseCsv(opts.mentions);
173
+ const recipientOverride = parseCsv(opts.recipientOverride);
174
+ const onFailureClass = parseCsv(opts.onFailureClass);
175
+ return {
176
+ level,
177
+ channel_id: opts.channel,
178
+ scope_type: scope,
179
+ scope_id: needsScopeId ? opts.scopeId : null,
180
+ on_status: onStatus,
181
+ repo_glob: opts.repoGlob ?? null,
182
+ workflow_glob: opts.workflowGlob ?? null,
183
+ job_glob: opts.jobGlob ?? null,
184
+ mentions: mentions.length > 0 ? mentions : null,
185
+ recipient_override: recipientOverride.length > 0 ? recipientOverride : null,
186
+ on_failure_class: onFailureClass.length > 0 ? onFailureClass : null,
187
+ accumulate_for: accumulateFor
188
+ };
189
+ }
190
+ async function notificationsSubscriptionsAddCommand(opts) {
191
+ const body = buildSubscriptionBody(opts);
192
+ if (!body) return false;
193
+ return withClient(opts, async (client) => {
194
+ const sub = await client.createSubscription(body);
195
+ console.log(pc.green(`Subscription created: ${sub.id}`));
196
+ return true;
197
+ });
198
+ }
199
+ async function notificationsSubscriptionsRemoveCommand(id, opts) {
200
+ return withClient(opts, async (client) => {
201
+ await client.deleteSubscription(id);
202
+ console.log(pc.green(`Subscription removed: ${id}`));
203
+ return true;
204
+ });
205
+ }
206
+ async function notificationsRosterListCommand(opts) {
207
+ return withClient(opts, async (client) => {
208
+ const identities = await client.listIdentities();
209
+ if (opts.json) {
210
+ console.log(JSON.stringify(identities, null, 2));
211
+ return true;
212
+ }
213
+ printTable([
214
+ "ID",
215
+ "SUBJECT KIND",
216
+ "SUBJECT",
217
+ "SLACK MEMBER",
218
+ "STATUS"
219
+ ], identities.map((r) => [
220
+ r.id,
221
+ r.subject_kind,
222
+ r.subject_value,
223
+ r.slack_member_id,
224
+ r.status
225
+ ]));
226
+ return true;
227
+ });
228
+ }
229
+ /** Build an add-identity body from parsed flags (null on validation error). */
230
+ function buildIdentityBody(opts) {
231
+ if (!opts.connection) {
232
+ console.error(pc.red("--connection <id> is required"));
233
+ return null;
234
+ }
235
+ const subjectKind = requireEnum("subject-kind", opts.subjectKind, RosterSubjectKind.options);
236
+ if (!subjectKind) return null;
237
+ if (!opts.subject) {
238
+ console.error(pc.red("--subject <value> is required (the KiCI user sub, git login, or email)"));
239
+ return null;
240
+ }
241
+ const inputForm = requireEnum("input-form", opts.inputForm ?? "id", RosterInputForm.options);
242
+ if (!inputForm) return null;
243
+ if (!opts.value) {
244
+ console.error(pc.red("--value <slack id | email | @handle> is required"));
245
+ return null;
246
+ }
247
+ return {
248
+ connection_id: opts.connection,
249
+ subject_kind: subjectKind,
250
+ subject_value: opts.subject,
251
+ input_form: inputForm,
252
+ value: opts.value
253
+ };
254
+ }
255
+ async function notificationsRosterAddCommand(opts) {
256
+ const body = buildIdentityBody(opts);
257
+ if (!body) return false;
258
+ return withClient(opts, async (client) => {
259
+ const row = await client.addIdentity(body);
260
+ console.log(pc.green(`Roster identity added: ${row.id} → ${row.slack_member_id}`));
261
+ return true;
262
+ });
263
+ }
264
+ async function notificationsRosterRemoveCommand(id, opts) {
265
+ return withClient(opts, async (client) => {
266
+ await client.deleteIdentity(id);
267
+ console.log(pc.green(`Roster identity removed: ${id}`));
268
+ return true;
269
+ });
270
+ }
271
+ //#endregion
272
+ export { notificationsChannelsAddCommand, notificationsChannelsListCommand, notificationsChannelsRemoveCommand, notificationsRosterAddCommand, notificationsRosterListCommand, notificationsRosterRemoveCommand, notificationsSubscriptionsAddCommand, notificationsSubscriptionsListCommand, notificationsSubscriptionsRemoveCommand };
273
+
274
+ //# sourceMappingURL=notifications.js.map
@@ -2,6 +2,8 @@ import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { discoverWorkflows, resolveKiciDir } from "../execution/executor.js";
3
3
  import "../execution/index.js";
4
4
  import { transformTriggers } from "../lockfile/generator.js";
5
+ import { analyzeJobPurity } from "../lockfile/purity-diagnostics.js";
6
+ import "../lockfile/index.js";
5
7
  import { displayDryRun } from "../test-runner/dry-run.js";
6
8
  import { parseEventArg } from "../test-runner/event-types.js";
7
9
  import { buildEventPayload } from "../test-runner/payload-builder.js";
@@ -88,10 +90,15 @@ async function previewEvent(event, options) {
88
90
  return false;
89
91
  }
90
92
  }
93
+ const purityWarnings = [];
94
+ for (const w of workflows) for (const j of w.jobs) {
95
+ if (typeof j === "function") continue;
96
+ purityWarnings.push(...analyzeJobPurity(j, w.name));
97
+ }
91
98
  displayDryRun(lockWorkflows, decisions, {
92
99
  workflow: options.workflow,
93
100
  job: options.job
94
- });
101
+ }, purityWarnings);
95
102
  return true;
96
103
  } catch (error) {
97
104
  const message = toErrorMessage(error);
@@ -2,7 +2,9 @@ import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { resolveKiciDir } from "../execution/executor.js";
3
3
  import "../execution/index.js";
4
4
  import { formatSummary } from "../remote/output/summary.js";
5
+ import { detectOsMismatchHints } from "../local-plane/os-mismatch-hint.js";
5
6
  import { IN_PLACE_ROUTING_LABEL, TRUSTED_ROUTING_LABEL } from "../local-plane/scaler-config.js";
7
+ import { planeLogPath } from "../local-plane/plane-manager.js";
6
8
  import { ensureLocalSource } from "../local-plane/plane-seed.js";
7
9
  import { triggerRun } from "../local-plane/plane-trigger.js";
8
10
  import { resolvePlaneForRun } from "../local-plane/resolve-plane.js";
@@ -76,6 +78,12 @@ async function runRouted(options) {
76
78
  check: false,
77
79
  verbose: options.debug ?? false
78
80
  }))) return false;
81
+ if (!quiet) try {
82
+ const os = await import("node:os");
83
+ const lockRaw = readFileSync(path.join(kiciDir, "kici.lock.json"), "utf-8");
84
+ const hints = detectOsMismatchHints(JSON.parse(lockRaw), os.platform(), os.arch());
85
+ for (const hint of hints) logger.warn(pc.yellow(hint));
86
+ } catch {}
79
87
  let cleanup;
80
88
  const restoreLocks = [];
81
89
  try {
@@ -146,7 +154,8 @@ async function runRouted(options) {
146
154
  if (!quiet) logger.info(pc.green(`Run started: ${runId}`));
147
155
  const outcome = await followRun(plane.url, plane.adminToken, runId, {
148
156
  quiet,
149
- onLine: (line) => process.stdout.write(line + "\n")
157
+ onLine: (line) => process.stdout.write(line + "\n"),
158
+ hintLogPath: planeLogPath()
150
159
  });
151
160
  if (!quiet) {
152
161
  const result = {
@@ -4,7 +4,7 @@ import "../execution/index.js";
4
4
  import { loadGlobalConfig } from "../remote/config.js";
5
5
  import { RunHistory } from "../remote/history.js";
6
6
  import { buildLocalRepoIdentity } from "../remote/local-repo-identity.js";
7
- import { AccessDeniedError, AmbiguousClusterError, AuthenticationError, ConnectionError, NoClusterError, PlatformRunClient } from "../remote/platform-client.js";
7
+ import { AccessDeniedError, AmbiguousClusterError, AuthenticationError, ConnectionError, NoClusterError, NotFoundError, PlatformRunClient } from "../remote/platform-client.js";
8
8
  import { buildEncryptedSecrets } from "../remote/secret-upload.js";
9
9
  import { createOverlayTarball, getSizeWarning, uploadTarball } from "../remote/uploader.js";
10
10
  import { formatJsonResult } from "../remote/output/json.js";
@@ -20,17 +20,22 @@ import path from "node:path";
20
20
  import pc from "picocolors";
21
21
  import { readFile, writeFile } from "node:fs/promises";
22
22
  import { formatBytes, logger, toErrorMessage } from "@kici-dev/core";
23
- import { coerceDispatchInputs, parseInputPairs } from "@kici-dev/engine";
23
+ import { TERMINAL_RUN_STATES, coerceDispatchInputs, parseInputPairs, toCanonicalStatus } from "@kici-dev/engine";
24
24
  import { normalizeRunsOnToMatchers } from "@kici-dev/engine/labels/compile";
25
25
  import { confirm } from "@inquirer/prompts";
26
26
  //#region src/commands/run.ts
27
- /** Terminal run statuses returned by the Platform run-status snapshot. */
28
- const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
29
- "success",
30
- "failed",
31
- "cancelled",
32
- "error"
33
- ]);
27
+ /**
28
+ * True when the Platform run-status snapshot reports a terminal run status.
29
+ *
30
+ * Resolves through the engine's canonical vocabulary rather than matching a
31
+ * hand-written literal set: that is what makes the legacy `error` spelling
32
+ * terminal without a second copy of the alias, and what stops a newly-added
33
+ * terminal status leaving `kici run` polling a finished run forever.
34
+ */
35
+ function isTerminalRunStatus(status) {
36
+ const canonical = toCanonicalStatus(status.toLowerCase());
37
+ return canonical !== void 0 && TERMINAL_RUN_STATES.has(canonical);
38
+ }
34
39
  /**
35
40
  * Compile `--target` selector strings into a {@link HostTargetSelector}. Each
36
41
  * string becomes one AND value (its own include set); repeated values
@@ -91,6 +96,19 @@ function buildDispatchInputs(pairs, descriptor) {
91
96
  /** Interval between status/log polls while a run is active. */
92
97
  const POLL_INTERVAL_MS = 750;
93
98
  /**
99
+ * How long a freshly triggered run may still read as missing before a 404 is
100
+ * reported as a real error.
101
+ *
102
+ * The trigger call answers with the run id as soon as the orchestrator has
103
+ * created the run, while the Platform's view of that run arrives over the
104
+ * relay a moment later. The first poll therefore fires while the run is not
105
+ * yet readable and legitimately 404s. Within this budget — and only until the
106
+ * first successful read — a 404 means "not visible yet" and the poll retries;
107
+ * after it, or once the run has been read at least once, a 404 is surfaced as
108
+ * a genuinely missing run.
109
+ */
110
+ const RUN_VISIBILITY_GRACE_MS = 3e4;
111
+ /**
94
112
  * Run `fn` with everything written to `process.stdout` redirected to
95
113
  * `process.stderr`, then restore the original writer. Compiling a repo
96
114
  * evaluates every user workflow / fixture module, and a `console.log` at a
@@ -466,16 +484,27 @@ async function pollRunToCompletion(ctx, runId, fixtureId, options) {
466
484
  let lastStatus = null;
467
485
  const seenHolds = /* @__PURE__ */ new Set();
468
486
  let heldCtx;
487
+ let runSeen = false;
469
488
  while (!cancelled) {
470
- const logs = await ctx.client.runLogs(ctx.orgId, runId, cursor, ctx.target);
489
+ let logs;
490
+ try {
491
+ logs = await ctx.client.runLogs(ctx.orgId, runId, cursor, ctx.target);
492
+ lastStatus = await ctx.client.runStatus(ctx.orgId, runId, ctx.target);
493
+ } catch (err) {
494
+ if (err instanceof NotFoundError && !runSeen && Date.now() - startTime < RUN_VISIBILITY_GRACE_MS) {
495
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
496
+ continue;
497
+ }
498
+ throw err;
499
+ }
500
+ runSeen = true;
471
501
  for (const line of logs.lines) {
472
502
  if (!options.quiet) process.stdout.write(line + "\n");
473
503
  tailLines.push(line);
474
504
  while (tailLines.length > MAX_TAIL) tailLines.shift();
475
505
  }
476
506
  cursor = logs.nextCursor;
477
- lastStatus = await ctx.client.runStatus(ctx.orgId, runId, ctx.target);
478
- const terminal = lastStatus.done || TERMINAL_STATUSES.has(lastStatus.status);
507
+ const terminal = lastStatus.done || isTerminalRunStatus(lastStatus.status);
479
508
  if (terminal && logs.done) return finishRun(fixtureId, runId, lastStatus, startTime, tailLines, options);
480
509
  if (!terminal) await watchRunHolds(runId, seenHolds, () => heldCtx, (c) => heldCtx = c, options);
481
510
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
@@ -0,0 +1,24 @@
1
+ export interface RunsArtifactsDownloadOptions {
2
+ archive?: boolean;
3
+ output?: string;
4
+ }
5
+ /** Basename an artifact takes under the output directory in the given mode. */
6
+ export declare function destBasename(name: string, archive: boolean): string;
7
+ /**
8
+ * Reduce a basename to the form a case-insensitive filesystem looks it up by.
9
+ *
10
+ * The NFC pass is deliberate defence for a widened artifact-name charset:
11
+ * `ArtifactNameSchema` accepts ASCII only today, so no live name can differ by
12
+ * normalisation — but HFS+ stores NFD and APFS folds case while preserving
13
+ * normalisation, so a non-ASCII name would collide on those volumes without it.
14
+ */
15
+ export declare function foldBasename(basename: string): string;
16
+ /**
17
+ * Group basenames that a case-insensitive filesystem resolves to one path.
18
+ *
19
+ * Groups appear in first-appearance order and each group is sorted; a basename
20
+ * with no partner is not reported.
21
+ */
22
+ export declare function findCaseCollisions(basenames: string[]): string[][];
23
+ export declare function runsArtifactsDownloadCommand(runId: string, name: string | undefined, options?: RunsArtifactsDownloadOptions): Promise<boolean>;
24
+ //# sourceMappingURL=download.d.ts.map
@@ -0,0 +1,154 @@
1
+ import "../../../rolldown-runtime-ClRpJifh.js";
2
+ import { extractArtifactTarball } from "../../../remote/artifact-extract.js";
3
+ import { DashboardClient, DashboardClientError } from "../../../remote/dashboard-client.js";
4
+ import { isCaseInsensitiveDir } from "../../../remote/fs-case.js";
5
+ import { join } from "node:path";
6
+ import pc from "picocolors";
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import { logger, sha256, toErrorMessage } from "@kici-dev/core";
9
+ import { ArtifactNameSchema } from "@kici-dev/engine";
10
+ //#region src/commands/runs/artifacts/download.ts
11
+ /** kici runs artifacts download — fetch one artifact (by name) or every artifact of a run. */
12
+ /** Extension of the raw artifact archive written by `--archive`. */
13
+ const ARCHIVE_EXTENSION = ".tar.gz";
14
+ /** Basename an artifact takes under the output directory in the given mode. */
15
+ function destBasename(name, archive) {
16
+ return archive ? `${name}${ARCHIVE_EXTENSION}` : name;
17
+ }
18
+ /**
19
+ * Reduce a basename to the form a case-insensitive filesystem looks it up by.
20
+ *
21
+ * The NFC pass is deliberate defence for a widened artifact-name charset:
22
+ * `ArtifactNameSchema` accepts ASCII only today, so no live name can differ by
23
+ * normalisation — but HFS+ stores NFD and APFS folds case while preserving
24
+ * normalisation, so a non-ASCII name would collide on those volumes without it.
25
+ */
26
+ function foldBasename(basename) {
27
+ return basename.normalize("NFC").toLowerCase();
28
+ }
29
+ /**
30
+ * Group basenames that a case-insensitive filesystem resolves to one path.
31
+ *
32
+ * Groups appear in first-appearance order and each group is sorted; a basename
33
+ * with no partner is not reported.
34
+ */
35
+ function findCaseCollisions(basenames) {
36
+ const byFold = /* @__PURE__ */ new Map();
37
+ for (const basename of basenames) {
38
+ const fold = foldBasename(basename);
39
+ const group = byFold.get(fold);
40
+ if (group) group.add(basename);
41
+ else byFold.set(fold, /* @__PURE__ */ new Set([basename]));
42
+ }
43
+ return [...byFold.values()].filter((g) => g.size > 1).map((g) => [...g].sort());
44
+ }
45
+ /**
46
+ * Fetch a presigned artifact URL into a Buffer and verify its sha-256.
47
+ *
48
+ * NO `Authorization` header: `downloadUrl` is a presigned object-storage GET,
49
+ * and the PAT authenticates to the Platform, not to object storage — attaching
50
+ * it would leak the credential to the storage host. The bytes therefore flow
51
+ * CLI <-> object storage directly and never transit the Platform.
52
+ */
53
+ async function fetchArtifactBytes(entry) {
54
+ if (!entry.downloadUrl) throw new Error(`artifact "${entry.name}" has no download URL (the stored object is missing or expired)`);
55
+ const res = await fetch(entry.downloadUrl);
56
+ if (!res.ok) throw new Error(`download of "${entry.name}" failed (HTTP ${res.status})`);
57
+ const buf = Buffer.from(await res.arrayBuffer());
58
+ const actual = sha256(buf);
59
+ if (actual !== entry.sha256) throw new Error(`artifact "${entry.name}" checksum mismatch: expected ${entry.sha256}, got ${actual}`);
60
+ return buf;
61
+ }
62
+ /**
63
+ * Refuse a listed name that is not a valid artifact name.
64
+ *
65
+ * Every name is validated against {@link ArtifactNameSchema} at upload, so a
66
+ * response carrying one outside that contract comes from a broken or hostile
67
+ * peer. The name becomes a path component under the output directory, so it is
68
+ * checked before it is joined — an unchecked `../…` would let a list response
69
+ * write anywhere on the caller's filesystem.
70
+ */
71
+ function assertPathSafeName(name) {
72
+ if (!ArtifactNameSchema.safeParse(name).success) throw new Error(`listed artifact "${name}" is not a valid artifact name — refusing to write it under the output directory`);
73
+ }
74
+ /**
75
+ * Refuse the whole invocation when two targets would land on the same path.
76
+ *
77
+ * Artifact names are case-sensitive, so a run can legitimately hold both
78
+ * `bundle` and `Bundle`. On a filesystem that ignores case in path lookups the
79
+ * two resolve to one path — the second extraction merges into the first's
80
+ * directory, or overwrites its archive under `--archive`. The filesystem is
81
+ * probed only once a fold collision is known to exist, so a case-sensitive
82
+ * output directory downloads both exactly as before. A probe that cannot answer
83
+ * is treated as case-insensitive: the collision is already certain, so the
84
+ * unknown half is resolved towards refusing rather than towards losing data.
85
+ */
86
+ async function assertNoCaseCollisions(targets, outDir, archive) {
87
+ const groups = findCaseCollisions(targets.map((t) => destBasename(t.name, archive)));
88
+ if (groups.length === 0) return;
89
+ let probeError;
90
+ let caseInsensitive = true;
91
+ try {
92
+ caseInsensitive = await isCaseInsensitiveDir(outDir);
93
+ } catch (err) {
94
+ probeError = err;
95
+ }
96
+ if (!caseInsensitive) return;
97
+ const pairs = groups.map((g) => g.map((basename) => `"${basename}"`).join(" and ")).join("; ");
98
+ const consequence = archive ? "writing both would overwrite one file with the other" : "extracting both would merge them into one directory";
99
+ const reason = probeError !== void 0 ? `the output directory ${outDir} could not be checked for case sensitivity (${toErrorMessage(probeError)})` : `the output directory ${outDir} is on a case-insensitive filesystem`;
100
+ throw new Error(`this run holds artifacts whose names differ only in case: ${pairs} — ${consequence}, and ${reason}. Download them one at a time into separate directories: kici runs artifacts download <run-id> <name> -o <dir>`);
101
+ }
102
+ /** Fetch one entry and write it to `outDir`, extracted or as the raw archive. */
103
+ async function writeOne(entry, outDir, archive) {
104
+ assertPathSafeName(entry.name);
105
+ const buf = await fetchArtifactBytes(entry);
106
+ await mkdir(outDir, { recursive: true });
107
+ const dest = join(outDir, destBasename(entry.name, archive));
108
+ if (archive) {
109
+ await writeFile(dest, buf);
110
+ console.log(pc.green(`✓ ${entry.name} → ${dest}`));
111
+ return;
112
+ }
113
+ await extractArtifactTarball(buf, dest);
114
+ console.log(pc.green(`✓ ${entry.name} → ${dest}/`));
115
+ }
116
+ /** Resolve which listed artifacts the invocation targets. */
117
+ function resolveTargets(artifacts, name, runId) {
118
+ if (!name) return artifacts;
119
+ const match = artifacts.find((a) => a.name === name);
120
+ if (match) return [match];
121
+ const names = artifacts.map((a) => a.name).join(", ") || "(none)";
122
+ throw new Error(`artifact "${name}" not found in run ${runId}. Available: ${names}`);
123
+ }
124
+ async function runsArtifactsDownloadCommand(runId, name, options = {}) {
125
+ try {
126
+ const { artifacts } = await (await DashboardClient.load()).listArtifacts(runId);
127
+ const outDir = options.output ?? process.cwd();
128
+ const downloadingAll = !name;
129
+ const targets = resolveTargets(artifacts, name, runId);
130
+ if (targets.length === 0) {
131
+ console.log(pc.gray("No artifacts for this run."));
132
+ return true;
133
+ }
134
+ for (const entry of targets) assertPathSafeName(entry.name);
135
+ if (downloadingAll) await assertNoCaseCollisions(targets.filter((t) => t.downloadUrl), outDir, options.archive ?? false);
136
+ let failures = 0;
137
+ for (const entry of targets) {
138
+ if (downloadingAll && !entry.downloadUrl) {
139
+ logger.warn(pc.yellow(`! ${entry.name} skipped — no download URL (object missing or expired)`));
140
+ failures++;
141
+ continue;
142
+ }
143
+ await writeOne(entry, outDir, options.archive ?? false);
144
+ }
145
+ return failures < targets.length;
146
+ } catch (err) {
147
+ logger.error(pc.red(err instanceof DashboardClientError ? err.message : toErrorMessage(err)));
148
+ return false;
149
+ }
150
+ }
151
+ //#endregion
152
+ export { destBasename, findCaseCollisions, foldBasename, runsArtifactsDownloadCommand };
153
+
154
+ //# sourceMappingURL=download.js.map
@@ -0,0 +1,5 @@
1
+ export interface RunsArtifactsListOptions {
2
+ json?: boolean;
3
+ }
4
+ export declare function runsArtifactsListCommand(runId: string, options?: RunsArtifactsListOptions): Promise<boolean>;
5
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1,47 @@
1
+ import "../../../rolldown-runtime-ClRpJifh.js";
2
+ import { DashboardClient, DashboardClientError } from "../../../remote/dashboard-client.js";
3
+ import { relativeTime, renderTable } from "../../../remote/render.js";
4
+ import pc from "picocolors";
5
+ import { formatBytes, logger, toErrorMessage } from "@kici-dev/core";
6
+ //#region src/commands/runs/artifacts/list.ts
7
+ /** kici runs artifacts list — the artifacts a run uploaded (the rows the dashboard Artifacts tab shows). */
8
+ /** Number of leading sha-256 hex chars shown in the table (full digest via --json). */
9
+ const SHA_PREFIX_LENGTH = 12;
10
+ async function runsArtifactsListCommand(runId, options = {}) {
11
+ try {
12
+ const result = await (await DashboardClient.load()).listArtifacts(runId);
13
+ if (options.json) {
14
+ console.log(JSON.stringify(result, null, 2));
15
+ return true;
16
+ }
17
+ if (result.artifacts.length === 0) {
18
+ console.log(pc.gray("No artifacts for this run."));
19
+ return true;
20
+ }
21
+ const rows = result.artifacts.map((a) => [
22
+ a.name,
23
+ a.jobId,
24
+ formatBytes(a.sizeBytes),
25
+ a.sha256.slice(0, SHA_PREFIX_LENGTH),
26
+ relativeTime(a.createdAt),
27
+ a.downloadUrl ? "" : pc.yellow("unavailable")
28
+ ]);
29
+ console.log(renderTable([
30
+ "name",
31
+ "job",
32
+ "size",
33
+ "sha-256",
34
+ "created",
35
+ ""
36
+ ], rows));
37
+ console.log(pc.gray(`\n${result.artifacts.length} artifact(s). Download with kici runs artifacts download ${runId} <name>`));
38
+ return true;
39
+ } catch (err) {
40
+ logger.error(pc.red(err instanceof DashboardClientError ? err.message : toErrorMessage(err)));
41
+ return false;
42
+ }
43
+ }
44
+ //#endregion
45
+ export { runsArtifactsListCommand };
46
+
47
+ //# sourceMappingURL=list.js.map
@@ -18,8 +18,8 @@ async function runsCancelCommand(runId, options = {}) {
18
18
  return false;
19
19
  }
20
20
  logger.info(`${options.force ? "Force-cancelling" : "Cancelling"} run ${pc.cyan(runId)}...`);
21
- const { cancelledJobs = 0 } = await client.cancelRun(runId, options.force ?? false);
22
- console.log(pc.green(`Run ${runId} cancelled (${cancelledJobs} job${cancelledJobs !== 1 ? "s" : ""} affected).`));
21
+ const { cancelledJobs = 0, alreadyTerminal } = await client.cancelRun(runId, options.force ?? false);
22
+ console.log(alreadyTerminal ? pc.yellow(`Run ${runId} had already finished; nothing was cancelled.`) : pc.green(`Run ${runId} cancelled (${cancelledJobs} job${cancelledJobs !== 1 ? "s" : ""} affected).`));
23
23
  return true;
24
24
  } catch (err) {
25
25
  logger.error(pc.red(err instanceof DashboardClientError ? err.message : toErrorMessage(err)));