@akanjs/cli 3.1.0-rc.1 → 3.1.0-rc.3

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 (74) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-kyprmy06.js → CodeAgent-ctk4fh2b.js} +2 -2
  3. package/{CodeTui-21mtze3j.js → CodeTui-mrxtrdzw.js} +1 -1
  4. package/TunnelShare-rj74jd6x.js +13 -0
  5. package/{agent.command-f3gsc1gd.js → agent.command-ncre6kwz.js} +5 -5
  6. package/akanCode.js +2 -2
  7. package/{application.command-5s780vnz.js → application.command-54mawreg.js} +52 -41
  8. package/{applicationBuildRunner-1yaqsrdq.js → applicationBuildRunner-8573zr2w.js} +16 -30
  9. package/buildBatch.proc.js +22 -13
  10. package/{cloud.command-yj6xb2a7.js → cloud.command-nyemv0m3.js} +57 -65
  11. package/cloud.script-ht6yb203.js +23 -0
  12. package/{code.command-3wkn4ja5.js → code.command-spsrzg63.js} +6 -6
  13. package/{context.command-9jjn3gcw.js → context.command-768zv7z4.js} +16 -13
  14. package/context.runner-tf1b53qq.js +24 -0
  15. package/{guideline.command-dtczrqpf.js → guideline.command-4ycwc51q.js} +2 -2
  16. package/guidelines/conventions/conventions.instruction.md +28 -15
  17. package/guidelines/cssRule/cssRule.instruction.md +12 -6
  18. package/guidelines/mcpRule/mcpRule.instruction.md +27 -11
  19. package/guidelines/runtimeRule/runtimeRule.instruction.md +24 -15
  20. package/guidelines/transportRule/transportRule.instruction.md +28 -5
  21. package/incrementalBuilder.proc.js +9 -6
  22. package/{index-2etcv0sc.js → index-00seamy5.js} +1 -1
  23. package/{index-jx4bzvwg.js → index-1a7nhq56.js} +92 -52
  24. package/{index-e0cvze1s.js → index-1nyxvz8j.js} +4 -4
  25. package/index-2z3kykvd.js +248 -0
  26. package/{index-rtct7d43.js → index-3675mvjw.js} +1 -1
  27. package/{index-56d2g8ms.js → index-3ddt3qjg.js} +1 -1
  28. package/{index-6jjtnhkw.js → index-3x7yeecy.js} +51 -9
  29. package/{index-ww9bhp67.js → index-4pf8g7ff.js} +1 -1
  30. package/index-8935h3n1.js +33 -0
  31. package/{index-v29746vb.js → index-8eqkxwsx.js} +3 -3
  32. package/{index-c2yrsxdh.js → index-91rt5bye.js} +96 -320
  33. package/{index-55mynnqs.js → index-bv210r6t.js} +459 -210
  34. package/{index-c76k723e.js → index-c4h5b5nq.js} +2 -2
  35. package/{index-qt22xstj.js → index-c9zjb3tw.js} +5 -3
  36. package/{index-c1x0be5j.js → index-dmsvd2rv.js} +2 -2
  37. package/index-ec080x99.js +1124 -0
  38. package/{index-ctanya41.js → index-fjqvd5mx.js} +4 -4
  39. package/{index-aprt7pkz.js → index-fvn6f5wx.js} +229 -1327
  40. package/{index-rc5rwyps.js → index-jj7yapwq.js} +71 -60
  41. package/index-kmamh5bv.js +241 -0
  42. package/{index-fm46xrf1.js → index-q5abh0jq.js} +3 -3
  43. package/{index-1w2qaee2.js → index-qf8g88j9.js} +361 -291
  44. package/{index-gnyf1fqy.js → index-qvghfv6v.js} +2 -2
  45. package/{index-ae5pwhgs.js → index-r5d6rq19.js} +10 -8
  46. package/{index-2mx692qq.js → index-vhfjzda3.js} +2 -2
  47. package/{index-8r4d7cmt.js → index-w9b71b6y.js} +4 -4
  48. package/{index-8ggztp8k.js → index-wanhh011.js} +42 -177
  49. package/{index-05g3jayg.js → index-wdze51wz.js} +1 -1
  50. package/index-wjqbc7sf.js +668 -0
  51. package/{index-rwq3ekqc.js → index-xf40a295.js} +7 -7
  52. package/{index-krq7btbt.js → index-xm5tmpv0.js} +3 -3
  53. package/index.js +22 -22
  54. package/{library.command-gaga5bmq.js → library.command-88ahgych.js} +3 -3
  55. package/{localRegistry.command-vsreyy42.js → localRegistry.command-ykea1m7b.js} +12 -9
  56. package/{module.command-7vpf6db4.js → module.command-zky4b7m9.js} +5 -5
  57. package/{package.command-twmvvf69.js → package.command-vh8pcedh.js} +3 -3
  58. package/package.json +2 -2
  59. package/{page.command-5asq7fdf.js → page.command-fyqeqxc3.js} +3 -3
  60. package/{pipeline.command-d0v38572.js → pipeline.command-r5t30py6.js} +220 -219
  61. package/{primitive.command-dyrs2fm8.js → primitive.command-a0516zfk.js} +6 -6
  62. package/{projectTypecheck-cvtkrzp2.js → projectTypecheck-7djd4z50.js} +1 -1
  63. package/{quality.command-6emsqpbg.js → quality.command-k7jcnfn0.js} +2 -2
  64. package/{repair.command-8t6aegv5.js → repair.command-85zg2an3.js} +5 -5
  65. package/{scalar.command-bnggy1x7.js → scalar.command-g5vmqwca.js} +4 -4
  66. package/{subspace.command-md2teygq.js → subspace.command-mp4evgsj.js} +421 -362
  67. package/{tunnel.command-jx3tdm2d.js → tunnel.command-kbnr99sj.js} +7 -4
  68. package/{workflow.command-jx0wag5r.js → workflow.command-9y9p1n8y.js} +10 -10
  69. package/{workspace.command-yxd7rr7g.js → workspace.command-jmec2kjg.js} +33 -30
  70. package/TunnelShare-q4jq08qg.js +0 -9
  71. package/cloud.script-h6pvcpa2.js +0 -20
  72. package/context.runner-atcrzz5b.js +0 -21
  73. package/index-6w0rbr9d.js +0 -68
  74. package/index-pgj2rt78.js +0 -404
@@ -4,7 +4,7 @@ import {
4
4
  runner,
5
5
  script,
6
6
  command
7
- } from "./index-6jjtnhkw.js";
7
+ } from "./index-3x7yeecy.js";
8
8
  import {
9
9
  FileSys2
10
10
  } from "./index-52w0fvh8.js";
@@ -12,7 +12,7 @@ import {
12
12
  AkanAppConfig,
13
13
  AkanWorkspaceConfig,
14
14
  AppExecutor
15
- } from "./index-55mynnqs.js";
15
+ } from "./index-bv210r6t.js";
16
16
  import {
17
17
  resolveMobileTargets
18
18
  } from "./index-q958tqqg.js";
@@ -31,13 +31,16 @@ import {
31
31
  pipelineActions,
32
32
  pipelineMilestones as pipelineMilestones2,
33
33
  pipelineOtaPlatforms as pipelineOtaPlatforms2,
34
- pipelinePlanVersion
34
+ pipelinePlanLimits as pipelinePlanLimits2,
35
+ pipelinePlanVersion,
36
+ toError
35
37
  } from "akanjs/common";
36
38
 
37
39
  // pkgs/@akanjs/devkit/pipeline/PipelineInputReader.ts
38
40
  import {
39
41
  PipelineNames,
40
42
  PipelinePattern,
43
+ PipelineValueReader,
41
44
  pipelineActionNames,
42
45
  pipelineMilestones,
43
46
  pipelineOtaPlatforms,
@@ -47,15 +50,31 @@ import {
47
50
 
48
51
  class PipelineInputReader {
49
52
  static #futureKeys = ["cache", "artifacts", "input", "image", "services", "notify"];
53
+ static #stageKeys = ["id", "name", "description", "milestone", "when", "timeout", "allowFailure"];
54
+ static #shellKeys = [...PipelineInputReader.#stageKeys, "sh", "steps", "env", "secrets", "retry"];
55
+ static #postResults = ["success", "failure", "canceled", "always"];
50
56
  static #durationPattern = /^(\d+)(s|m|h)$/;
57
+ static #trailingHighSurrogate = /[\uD800-\uDBFF]$/;
51
58
  static #stepNameChars = 80;
59
+ static #shownChars = 80;
60
+ static clip(text, max) {
61
+ if (text.length <= max)
62
+ return text;
63
+ const cut = text.slice(0, max - 1);
64
+ return `${PipelineInputReader.#trailingHighSurrogate.test(cut) ? cut.slice(0, -1) : cut}\u2026`;
65
+ }
52
66
  diagnostics = [];
53
67
  #file;
68
+ #read = new PipelineValueReader("config", (path, ...problems) => {
69
+ for (const problem of problems)
70
+ this.#problem(path, problem);
71
+ return null;
72
+ });
54
73
  constructor(file) {
55
74
  this.#file = file;
56
75
  }
57
76
  workspace(value) {
58
- const input = this.#object(value, "pipeline", ["schemaVersion", "env", "timeout", "before", "after", "post"]);
77
+ const input = this.#read.object(value, "pipeline", ["schemaVersion", "env", "timeout", "before", "after", "post"]);
59
78
  if (!input)
60
79
  return null;
61
80
  this.#schemaVersion(input.schemaVersion, "pipeline.schemaVersion");
@@ -75,7 +94,7 @@ class PipelineInputReader {
75
94
  };
76
95
  }
77
96
  app(value) {
78
- const input = this.#object(value, "pipeline", ["schemaVersion", "stages", "env", "after"]);
97
+ const input = this.#read.object(value, "pipeline", ["schemaVersion", "stages", "env", "after"]);
79
98
  if (!input)
80
99
  return null;
81
100
  this.#schemaVersion(input.schemaVersion, "pipeline.schemaVersion");
@@ -95,53 +114,51 @@ class PipelineInputReader {
95
114
  };
96
115
  }
97
116
  #workspaceStage(value, path) {
98
- const raw = this.#object(value, path);
117
+ const raw = this.#read.object(value, path);
99
118
  if (!raw)
100
119
  return null;
101
- if ("use" in raw) {
120
+ if (this.#declares(raw, "use")) {
102
121
  this.#report("pipeline.schema.invalid", "built-in actions are declared per app, in apps/<app>/akan.config.ts", `${path}.use`);
103
122
  return null;
104
123
  }
105
- if ("parallel" in raw)
124
+ if (this.#declares(raw, "parallel"))
106
125
  return this.#parallelStage(raw, path);
107
- if ("matrix" in raw)
126
+ if (this.#declares(raw, "matrix"))
108
127
  return this.#matrixStage(raw, path);
109
- return this.#shellStage(raw, path, "workspace");
128
+ return this.#shellStage(raw, path);
110
129
  }
111
130
  #appStage(value, path) {
112
- const raw = this.#object(value, path);
131
+ const raw = this.#read.object(value, path);
113
132
  if (!raw)
114
133
  return null;
115
- if ("parallel" in raw || "matrix" in raw) {
116
- this.#report("pipeline.schema.invalid", `an app's own stage takes no \`${"parallel" in raw ? "parallel" : "matrix"}\`: it is already that app's, and parallel or per-app copies belong to the workspace root akan.config.ts`, path);
134
+ const workspaceKey = this.#declares(raw, "parallel") ? "parallel" : this.#declares(raw, "matrix") ? "matrix" : null;
135
+ if (workspaceKey) {
136
+ this.#report("pipeline.schema.invalid", `an app's own stage takes no \`${workspaceKey}\`: it is already that app's, and parallel or per-app copies belong to the workspace root akan.config.ts`, path);
117
137
  return null;
118
138
  }
119
- if ("use" in raw)
139
+ if (this.#declares(raw, "use"))
120
140
  return this.#actionStage(raw, path);
121
- return this.#shellStage(raw, path, "app");
141
+ return this.#shellStage(raw, path);
142
+ }
143
+ #declares(raw, key) {
144
+ return raw[key] !== undefined || key in raw && raw.sh === undefined && raw.steps === undefined;
122
145
  }
123
146
  #actionStage(raw, path) {
124
- if (!this.#keys(raw, path, [
147
+ if (!this.#read.keys(raw, path, [
125
148
  "use",
126
- "id",
127
- "name",
128
- "description",
129
- "milestone",
130
- "when",
131
- "timeout",
132
- "allowFailure",
149
+ ...PipelineInputReader.#stageKeys,
133
150
  "retry",
134
151
  ...raw.use === "ota" ? ["targets", "platforms"] : []
135
152
  ]))
136
153
  return null;
137
- const action = this.#oneOf(raw.use, `${path}.use`, pipelineActionNames);
154
+ const action = this.#read.oneOf(raw.use, `${path}.use`, pipelineActionNames);
138
155
  if (!action)
139
156
  return null;
140
- const name = raw.name === undefined ? null : this.#text(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
157
+ const name = raw.name === undefined ? null : this.#read.string(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
141
158
  const id = raw.id === undefined ? action : this.#id(raw.id, `${path}.id`);
142
159
  if (!id)
143
160
  return null;
144
- const attempts = raw.retry === undefined ? null : this.#int(raw.retry, `${path}.retry`, 1, pipelinePlanLimits.retryAttempts);
161
+ const attempts = raw.retry === undefined ? null : this.#read.int(raw.retry, `${path}.retry`, 1, pipelinePlanLimits.retryAttempts);
145
162
  return {
146
163
  kind: "action",
147
164
  action,
@@ -150,13 +167,13 @@ class PipelineInputReader {
150
167
  named: name !== null,
151
168
  description: this.#description(raw.description, `${path}.description`),
152
169
  milestone: this.#milestone(raw.milestone, `${path}.milestone`),
153
- when: this.#when(raw.when, `${path}.when`, false),
170
+ when: this.#when(raw.when, `${path}.when`),
154
171
  timeoutSeconds: this.#duration(raw.timeout, `${path}.timeout`),
155
172
  allowFailure: this.#boolean(raw.allowFailure, `${path}.allowFailure`),
156
173
  retry: attempts === null ? null : { attempts, delaySeconds: 0 },
157
174
  targets: raw.targets === undefined ? null : this.#nonEmptySet(this.#strings(raw.targets, `${path}.targets`), `${path}.targets`),
158
175
  platforms: raw.platforms === undefined ? null : this.#nonEmptySet(this.#list(raw.platforms, `${path}.platforms`).flatMap((platform, idx) => {
159
- const parsed = this.#oneOf(platform, `${path}.platforms[${idx}]`, pipelineOtaPlatforms);
176
+ const parsed = this.#read.oneOf(platform, `${path}.platforms[${idx}]`, pipelineOtaPlatforms);
160
177
  return parsed ? [parsed] : [];
161
178
  }), `${path}.platforms`),
162
179
  path
@@ -169,53 +186,23 @@ class PipelineInputReader {
169
186
  }
170
187
  return [...new Set(items)];
171
188
  }
172
- #shellStage(raw, path, scope) {
173
- const keys = [
174
- "id",
175
- "name",
176
- "description",
177
- "milestone",
178
- "when",
179
- "timeout",
180
- "allowFailure",
181
- "sh",
182
- "steps",
183
- "env",
184
- "secrets",
185
- "retry"
186
- ];
187
- if (!this.#keys(raw, path, keys))
189
+ #shellStage(raw, path) {
190
+ if (!this.#read.keys(raw, path, PipelineInputReader.#shellKeys))
188
191
  return null;
189
- const head = this.#stageHead(raw, path, scope === "workspace" ? "workspace" : "app");
192
+ const head = this.#stageHead(raw, path);
190
193
  if (!head)
191
194
  return null;
192
195
  const steps = this.#stageSteps(raw, path);
193
196
  return steps ? { kind: "sh", ...head, steps } : null;
194
197
  }
195
198
  #matrixStage(raw, path) {
196
- const keys = [
197
- "id",
198
- "name",
199
- "description",
200
- "milestone",
201
- "when",
202
- "timeout",
203
- "allowFailure",
204
- "sh",
205
- "steps",
206
- "env",
207
- "secrets",
208
- "retry",
209
- "matrix",
210
- "maxParallel"
211
- ];
212
- if (!this.#keys(raw, path, keys))
199
+ if (!this.#read.keys(raw, path, [...PipelineInputReader.#shellKeys, "matrix", "maxParallel"]))
213
200
  return null;
214
201
  if (raw.matrix !== "apps") {
215
- this.#report("pipeline.schema.invalid", `${JSON.stringify(raw.matrix)} is not a matrix axis: only "apps"`, `${path}.matrix`);
202
+ this.#report("pipeline.schema.invalid", `${PipelineInputReader.#show(raw.matrix)} is not a matrix axis: only "apps"`, `${path}.matrix`);
216
203
  return null;
217
204
  }
218
- const head = this.#stageHead(raw, path, "matrix");
205
+ const head = this.#stageHead(raw, path, { allowApp: true });
219
206
  if (!head)
220
207
  return null;
221
208
  const steps = this.#stageSteps(raw, path);
@@ -224,23 +211,10 @@ class PipelineInputReader {
224
211
  return { kind: "matrix", ...head, maxParallel: this.#maxParallel(raw.maxParallel, `${path}.maxParallel`), steps };
225
212
  }
226
213
  #parallelStage(raw, path) {
227
- const keys = [
228
- "id",
229
- "name",
230
- "description",
231
- "milestone",
232
- "when",
233
- "timeout",
234
- "allowFailure",
235
- "parallel",
236
- "maxParallel",
237
- "failFast",
238
- "env",
239
- "secrets"
240
- ];
241
- if (!this.#keys(raw, path, keys))
214
+ const keys = [...PipelineInputReader.#stageKeys, "parallel", "maxParallel", "failFast", "env", "secrets"];
215
+ if (!this.#read.keys(raw, path, keys))
242
216
  return null;
243
- const head = this.#stageHead(raw, path, "workspace");
217
+ const head = this.#stageHead(raw, path);
244
218
  if (!head)
245
219
  return null;
246
220
  const env = this.#env(raw.env, `${path}.env`);
@@ -248,20 +222,22 @@ class PipelineInputReader {
248
222
  const ids = new Set;
249
223
  const branches = this.#list(raw.parallel, `${path}.parallel`).flatMap((branch, idx) => {
250
224
  const branchPath = `${path}.parallel[${idx}]`;
251
- const branchRaw = this.#object(branch, branchPath);
225
+ const branchRaw = this.#read.object(branch, branchPath);
252
226
  if (!branchRaw)
253
227
  return [];
254
- if ("use" in branchRaw || "parallel" in branchRaw || "matrix" in branchRaw) {
228
+ if (["use", "parallel", "matrix"].some((key) => this.#declares(branchRaw, key))) {
255
229
  this.#report("pipeline.schema.invalid", "a parallel branch is a shell stage", branchPath);
256
230
  return [];
257
231
  }
258
- const rule = this.#shellStage(branchRaw, branchPath, "workspace");
232
+ const rule = this.#shellStage(branchRaw, branchPath);
259
233
  if (!rule)
260
234
  return [];
261
235
  if (rule.when.changed && head.when.changed) {
262
236
  this.#report("pipeline.schema.invalid", "`when.changed` on both a parallel stage and its branch cannot be combined; keep one", `${branchPath}.when.changed`);
263
237
  return [];
264
238
  }
239
+ if (head.id === "post" && PipelineInputReader.#postResults.includes(rule.id))
240
+ this.#report("pipeline.id.reserved", `"post.${rule.id}" is reserved for the steps of post.${rule.id}; give this branch or its parallel stage another \`id\``, `${branchPath}.${branchRaw.id === undefined ? "name" : "id"}`);
265
241
  this.#claimId(ids, rule.id, rule.path);
266
242
  const steps = rule.steps.map((step) => ({
267
243
  ...step,
@@ -270,7 +246,7 @@ class PipelineInputReader {
270
246
  }));
271
247
  return [{ ...rule, steps }];
272
248
  });
273
- if (!branches.length && Array.isArray(raw.parallel))
249
+ if (!branches.length && (raw.parallel === undefined || Array.isArray(raw.parallel)))
274
250
  this.#report("pipeline.schema.invalid", "a parallel stage holds at least one branch", `${path}.parallel`);
275
251
  return {
276
252
  kind: "parallel",
@@ -280,8 +256,8 @@ class PipelineInputReader {
280
256
  branches
281
257
  };
282
258
  }
283
- #stageHead(raw, path, whenScope) {
284
- const name = this.#text(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
259
+ #stageHead(raw, path, { allowApp = false } = {}) {
260
+ const name = this.#read.string(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
285
261
  if (name === null)
286
262
  return null;
287
263
  const id = raw.id === undefined ? this.#slugId(name, `${path}.name`) : this.#id(raw.id, `${path}.id`);
@@ -292,7 +268,7 @@ class PipelineInputReader {
292
268
  name,
293
269
  description: this.#description(raw.description, `${path}.description`),
294
270
  milestone: this.#milestone(raw.milestone, `${path}.milestone`),
295
- when: this.#when(raw.when, `${path}.when`, whenScope === "matrix"),
271
+ when: this.#when(raw.when, `${path}.when`, { allowApp }),
296
272
  timeoutSeconds: this.#duration(raw.timeout, `${path}.timeout`),
297
273
  allowFailure: this.#boolean(raw.allowFailure, `${path}.allowFailure`),
298
274
  path
@@ -330,20 +306,20 @@ class PipelineInputReader {
330
306
  });
331
307
  }
332
308
  #step(value, path, idx) {
333
- const raw = typeof value === "string" ? { sh: value } : this.#object(value, path);
309
+ const raw = typeof value === "string" ? { sh: value } : this.#read.object(value, path);
334
310
  if (!raw)
335
311
  return null;
336
312
  const keys = ["sh", "id", "name", "cwd", "env", "secrets", "timeout", "retry", "allowFailure", "skipExitCodes"];
337
- if (!this.#keys(raw, path, keys))
313
+ if (!this.#read.keys(raw, path, keys))
338
314
  return null;
339
- const run = this.#text(raw.sh, typeof value === "string" ? path : `${path}.sh`, Number.POSITIVE_INFINITY);
315
+ const run = this.#read.string(raw.sh, typeof value === "string" ? path : `${path}.sh`, Number.POSITIVE_INFINITY);
340
316
  if (run === null)
341
317
  return null;
342
318
  if (new TextEncoder().encode(run).length > pipelinePlanLimits.shBytes) {
343
319
  this.#report("pipeline.limit.exceeded", `a script is at most ${pipelinePlanLimits.shBytes} bytes`, path);
344
320
  return null;
345
321
  }
346
- const name = raw.name === undefined ? PipelineInputReader.#nameOf(run) : this.#text(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
322
+ const name = raw.name === undefined ? PipelineInputReader.#nameOf(run) : this.#read.string(raw.name, `${path}.name`, pipelinePlanLimits.nameChars);
347
323
  if (name === null)
348
324
  return null;
349
325
  const localId = raw.id !== undefined ? this.#id(raw.id, `${path}.id`) : raw.name !== undefined ? this.#slugId(name, `${path}.name`) : String(idx + 1);
@@ -366,11 +342,11 @@ class PipelineInputReader {
366
342
  const empty = { success: [], failure: [], canceled: [], always: [] };
367
343
  if (value === undefined)
368
344
  return empty;
369
- const raw = this.#object(value, path);
345
+ const raw = this.#read.object(value, path);
370
346
  if (!raw)
371
347
  return empty;
372
348
  for (const key of Object.keys(raw))
373
- if (!["success", "failure", "canceled", "always"].includes(key))
349
+ if (!PipelineInputReader.#postResults.includes(key))
374
350
  this.#report("pipeline.schema.unknownKey", key === "unstable" ? "`unstable` is not a result the framework knows: the executor decides which list an allowed failure runs" : `"${key}" is not a post result: success, failure, canceled or always`, `${path}.${key}`);
375
351
  const steps = (key) => {
376
352
  const ids = new Set;
@@ -388,11 +364,11 @@ class PipelineInputReader {
388
364
  always: steps("always")
389
365
  };
390
366
  }
391
- #when(value, path, allowApp) {
367
+ #when(value, path, { allowApp = false } = {}) {
392
368
  const none = { branch: null, trigger: null, changed: null, app: null };
393
369
  if (value === undefined)
394
370
  return none;
395
- const raw = this.#object(value, path);
371
+ const raw = this.#read.object(value, path);
396
372
  if (!raw)
397
373
  return none;
398
374
  for (const key of Object.keys(raw)) {
@@ -401,9 +377,11 @@ class PipelineInputReader {
401
377
  this.#report("pipeline.schema.unknownKey", key === "app" ? "`when.app` narrows a workspace matrix stage only; an app's own stages are already that app's" : `"${key}" is not a condition: branch, trigger, changed${allowApp ? " or app" : ""}`, `${path}.${key}`);
402
378
  }
403
379
  const triggers = raw.trigger === undefined ? null : (typeof raw.trigger === "string" ? [raw.trigger] : this.#list(raw.trigger, `${path}.trigger`)).flatMap((trigger, idx) => {
404
- const parsed = this.#oneOf(trigger, `${path}.trigger[${idx}]`, pipelineTriggers);
380
+ const parsed = this.#read.oneOf(trigger, `${path}.trigger[${idx}]`, pipelineTriggers);
405
381
  return parsed ? [parsed] : [];
406
382
  });
383
+ if (Array.isArray(raw.trigger) && !raw.trigger.length)
384
+ this.#report("pipeline.schema.invalid", "a trigger list may not be empty", `${path}.trigger`);
407
385
  return {
408
386
  branch: this.#patterns(raw.branch, `${path}.branch`),
409
387
  trigger: triggers,
@@ -424,12 +402,12 @@ class PipelineInputReader {
424
402
  }
425
403
  #schemaVersion(value, path) {
426
404
  if (value !== undefined && value !== 1)
427
- this.#report("pipeline.schema.unsupported", `${JSON.stringify(value)} is not a schemaVersion this CLI reads (1)`, path);
405
+ this.#report("pipeline.schema.unsupported", `${PipelineInputReader.#show(value)} is not a schemaVersion this CLI reads (1)`, path);
428
406
  }
429
407
  #env(value, path) {
430
408
  if (value === undefined)
431
409
  return {};
432
- const raw = this.#object(value, path);
410
+ const raw = this.#read.object(value, path);
433
411
  if (!raw)
434
412
  return {};
435
413
  const env = {};
@@ -439,6 +417,8 @@ class PipelineInputReader {
439
417
  this.#report("pipeline.env.name", problem, `${path}.${name}`);
440
418
  else if (typeof entry !== "string")
441
419
  this.#report("pipeline.schema.invalid", "must be a string", `${path}.${name}`);
420
+ else if (entry.length > pipelinePlanLimits.shBytes)
421
+ this.#report("pipeline.limit.exceeded", `an env value is at most ${pipelinePlanLimits.shBytes} characters`, `${path}.${name}`);
442
422
  else
443
423
  env[name] = entry;
444
424
  }
@@ -446,10 +426,10 @@ class PipelineInputReader {
446
426
  }
447
427
  #secrets(value, path) {
448
428
  const names = value === undefined ? [] : this.#strings(value, path);
449
- const valid = names.filter((name) => {
429
+ const valid = names.filter((name, idx) => {
450
430
  const problem = PipelineNames.secretNameError(name);
451
431
  if (problem)
452
- this.#report("pipeline.secret.name", problem, path);
432
+ this.#report("pipeline.secret.name", problem, `${path}[${idx}]`);
453
433
  return !problem;
454
434
  });
455
435
  return [...new Set(valid)];
@@ -457,7 +437,7 @@ class PipelineInputReader {
457
437
  #cwd(value, path) {
458
438
  if (value === undefined)
459
439
  return ".";
460
- const cwd = this.#text(value, path, 1024);
440
+ const cwd = this.#read.string(value, path, 1024);
461
441
  if (cwd === null)
462
442
  return ".";
463
443
  if (cwd.startsWith("/") || cwd.startsWith("~") || cwd.includes("\\") || cwd.split("/").includes("..")) {
@@ -470,7 +450,7 @@ class PipelineInputReader {
470
450
  if (value === undefined)
471
451
  return [];
472
452
  const codes = this.#list(value, path).flatMap((code, idx) => {
473
- const parsed = this.#int(code, `${path}[${idx}]`, 1, 255);
453
+ const parsed = this.#read.int(code, `${path}[${idx}]`, 1, 255);
474
454
  return parsed === null ? [] : [parsed];
475
455
  });
476
456
  return [...new Set(codes)];
@@ -479,13 +459,13 @@ class PipelineInputReader {
479
459
  if (value === undefined)
480
460
  return null;
481
461
  if (typeof value === "number") {
482
- const attempts = this.#int(value, path, 1, pipelinePlanLimits.retryAttempts);
462
+ const attempts = this.#read.int(value, path, 1, pipelinePlanLimits.retryAttempts);
483
463
  return attempts === null ? null : { attempts, delaySeconds: 0 };
484
464
  }
485
- const raw = this.#object(value, path);
486
- if (!raw || !this.#keys(raw, path, ["attempts", "delay"]))
465
+ const raw = this.#read.object(value, path);
466
+ if (!raw || !this.#read.keys(raw, path, ["attempts", "delay"]))
487
467
  return null;
488
- const attempts = this.#int(raw.attempts, `${path}.attempts`, 1, pipelinePlanLimits.retryAttempts);
468
+ const attempts = this.#read.int(raw.attempts, `${path}.attempts`, 1, pipelinePlanLimits.retryAttempts);
489
469
  const delaySeconds = this.#duration(raw.delay, `${path}.delay`, {
490
470
  min: 0,
491
471
  max: pipelinePlanLimits.retryDelaySeconds
@@ -498,22 +478,22 @@ class PipelineInputReader {
498
478
  const match = typeof value === "string" ? PipelineInputReader.#durationPattern.exec(value) : null;
499
479
  const seconds = typeof value === "number" && Number.isInteger(value) ? value : match ? Number(match[1]) * { s: 1, m: 60, h: 3600 }[match[2]] : Number.NaN;
500
480
  if (!Number.isInteger(seconds) || seconds < min || seconds > max) {
501
- this.#report("pipeline.duration.invalid", `${JSON.stringify(value)} is not a duration between ${min} and ${max} seconds: whole seconds, or "30s", "20m", "1h"`, path);
481
+ this.#report("pipeline.duration.invalid", `${PipelineInputReader.#show(value)} is not a duration between ${min} and ${max} seconds: whole seconds, or "30s", "20m", "1h"`, path);
502
482
  return null;
503
483
  }
504
484
  return seconds;
505
485
  }
506
486
  #maxParallel(value, path) {
507
- return value === undefined ? null : this.#int(value, path, 1, pipelinePlanLimits.stages);
487
+ return value === undefined ? null : this.#read.int(value, path, 1, pipelinePlanLimits.stages);
508
488
  }
509
489
  #milestone(value, path) {
510
- return value === undefined ? null : this.#oneOf(value, path, pipelineMilestones);
490
+ return value === undefined ? null : this.#read.oneOf(value, path, pipelineMilestones);
511
491
  }
512
492
  #description(value, path) {
513
- return value === undefined ? null : this.#text(value, path, pipelinePlanLimits.descriptionChars);
493
+ return value === undefined ? null : this.#read.string(value, path, pipelinePlanLimits.descriptionChars);
514
494
  }
515
495
  #id(value, path) {
516
- const id = this.#text(value, path, 64);
496
+ const id = this.#read.string(value, path, 64);
517
497
  if (id === null)
518
498
  return null;
519
499
  const problem = PipelineNames.idError(id);
@@ -539,77 +519,58 @@ class PipelineInputReader {
539
519
  static #nameOf(run) {
540
520
  const line = run.split(`
541
521
  `).map((text) => text.trim()).find(Boolean);
542
- const name = line ?? run.trim();
543
- return name.length > PipelineInputReader.#stepNameChars ? `${name.slice(0, PipelineInputReader.#stepNameChars - 1)}\u2026` : name;
522
+ return PipelineInputReader.clip(line ?? run.trim(), PipelineInputReader.#stepNameChars);
544
523
  }
545
- #keys(raw, path, keys) {
546
- let ok = true;
547
- for (const key of Object.keys(raw)) {
548
- if (keys.includes(key))
549
- continue;
550
- ok = false;
551
- if (PipelineInputReader.#futureKeys.includes(key))
552
- this.#report("pipeline.schema.unsupported", `\`${key}\` is not supported in schemaVersion 1`, `${path}.${key}`);
553
- else
554
- this.#report("pipeline.schema.unknownKey", `"${key}" is not a key here: ${keys.join(", ")}`, `${path}.${key}`);
555
- }
556
- return ok;
557
- }
558
- #object(value, path, keys) {
559
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
560
- this.#report("pipeline.schema.invalid", "must be an object", path);
561
- return null;
524
+ static #show(value) {
525
+ let text;
526
+ try {
527
+ text = JSON.stringify(value) ?? String(value);
528
+ } catch {
529
+ text = typeof value === "bigint" ? `${value}n` : Object.prototype.toString.call(value);
562
530
  }
563
- const raw = value;
564
- if (keys && !this.#keys(raw, path, keys))
565
- return null;
566
- return raw;
531
+ return PipelineInputReader.clip(text, PipelineInputReader.#shownChars);
567
532
  }
568
533
  #list(value, path) {
569
- if (value === undefined)
570
- return [];
571
- if (!Array.isArray(value)) {
572
- this.#report("pipeline.schema.invalid", "must be a list", path);
573
- return [];
574
- }
575
- return value;
534
+ return value === undefined ? [] : this.#read.list(value, path) ?? [];
576
535
  }
577
536
  #strings(value, path) {
578
537
  return this.#list(value, path).flatMap((item, idx) => {
579
- const text = this.#text(item, `${path}[${idx}]`, 1024);
538
+ const text = this.#read.string(item, `${path}[${idx}]`);
580
539
  return text === null ? [] : [text];
581
540
  });
582
541
  }
583
- #text(value, path, max) {
584
- if (typeof value !== "string" || !value.trim()) {
585
- this.#report("pipeline.schema.invalid", "must be a non-empty string", path);
586
- return null;
587
- }
588
- if (value.length > max) {
589
- this.#report("pipeline.schema.invalid", `is longer than ${max} characters`, path);
590
- return null;
591
- }
592
- return value;
593
- }
594
542
  #boolean(value, path) {
595
- if (value === undefined)
596
- return false;
597
- if (typeof value === "boolean")
598
- return value;
599
- this.#report("pipeline.schema.invalid", "must be true or false", path);
600
- return false;
601
- }
602
- #int(value, path, min, max) {
603
- if (typeof value === "number" && Number.isInteger(value) && value >= min && value <= max)
604
- return value;
605
- this.#report("pipeline.schema.invalid", `must be a whole number between ${min} and ${max}`, path);
606
- return null;
543
+ return value === undefined ? false : this.#read.boolean(value, path) ?? false;
607
544
  }
608
- #oneOf(value, path, options) {
609
- if (typeof value === "string" && options.includes(value))
610
- return value;
611
- this.#report("pipeline.schema.invalid", `${JSON.stringify(value)} is not one of ${options.join(", ")}`, path);
612
- return null;
545
+ #problem(path, problem) {
546
+ if (problem.kind !== "unknownKey")
547
+ this.#report("pipeline.schema.invalid", PipelineInputReader.#reason(problem), path);
548
+ else if (PipelineInputReader.#futureKeys.includes(problem.key))
549
+ this.#report("pipeline.schema.unsupported", `\`${problem.key}\` is not supported in schemaVersion 1`, `${path}.${problem.key}`);
550
+ else
551
+ this.#report("pipeline.schema.unknownKey", `"${problem.key}" is not a key here: ${problem.keys.join(", ")}`, `${path}.${problem.key}`);
552
+ }
553
+ static #reason(problem) {
554
+ switch (problem.kind) {
555
+ case "object":
556
+ return "must be an object";
557
+ case "missingKey":
558
+ return `"${problem.key}" is missing`;
559
+ case "list":
560
+ return "must be a list";
561
+ case "string":
562
+ case "empty":
563
+ return "must be a non-empty string";
564
+ case "length":
565
+ return `is longer than ${problem.max} characters`;
566
+ case "boolean":
567
+ return "must be true or false";
568
+ case "int":
569
+ case "range":
570
+ return `must be a whole number between ${problem.min} and ${problem.max}`;
571
+ case "oneOf":
572
+ return `${PipelineInputReader.#show(problem.value)} is not one of ${problem.options.join(", ")}`;
573
+ }
613
574
  }
614
575
  #report(code, message, path) {
615
576
  this.diagnostics.push({ severity: "error", code, message, file: this.#file, path });
@@ -640,6 +601,7 @@ class PipelineCompiler {
640
601
  };
641
602
  static #reservedPhaseId = /^(?:build|docker|deploy|ota|(?:before-build|after-build|after-docker|after-deploy|after-ota)(?:-\d+)?)$/;
642
603
  static #actionStepNames = { build: "akan build", docker: "image", deploy: "deploy", ota: "ota" };
604
+ static #reasonChars = 1024;
643
605
  static compile(input) {
644
606
  return new PipelineCompiler(input).#compile();
645
607
  }
@@ -654,13 +616,9 @@ class PipelineCompiler {
654
616
  const { context, workspaceApps } = this.#input;
655
617
  this.#checkContext();
656
618
  const pipelineContext = {
657
- branch: context.branch,
658
- env: context.env,
619
+ ...context,
659
620
  apps: [...context.apps],
660
- workspaceApps: [...workspaceApps],
661
- trigger: context.trigger,
662
- buildNum: context.buildNum,
663
- commit: context.commit
621
+ workspaceApps: [...workspaceApps]
664
622
  };
665
623
  const workspace = this.#readWorkspace(pipelineContext);
666
624
  const lanes = new Map;
@@ -691,11 +649,18 @@ class PipelineCompiler {
691
649
  try {
692
650
  PipelinePlanSchema.parse(plan);
693
651
  } catch (error) {
694
- const message = error instanceof Error ? error.message : String(error);
652
+ const message = toError(error).message;
695
653
  this.#error(message.includes("at most") ? "pipeline.limit.exceeded" : "pipeline.internal", message, null, null);
696
654
  return this.#failed();
697
655
  }
698
- return { ok: true, plan, diagnostics: this.#diagnostics };
656
+ const result = { ok: true, plan, diagnostics: this.#diagnostics };
657
+ const bytes = new TextEncoder().encode(`${JSON.stringify(result, null, 2)}
658
+ `).length;
659
+ if (bytes > pipelinePlanLimits2.planBytes) {
660
+ this.#error("pipeline.limit.exceeded", `the plan is ${bytes} bytes as written, and an executor reads at most ${pipelinePlanLimits2.planBytes}`, null, null);
661
+ return this.#failed();
662
+ }
663
+ return result;
699
664
  }
700
665
  #checkContext() {
701
666
  const { context, workspaceApps } = this.#input;
@@ -710,13 +675,13 @@ class PipelineCompiler {
710
675
  }
711
676
  #readWorkspace(context) {
712
677
  const source = this.#input.workspace;
713
- if (!source?.pipeline)
678
+ if (source?.pipeline == null)
714
679
  return null;
715
- const value = this.#evaluate(source.pipeline, context, source.file);
716
- if (value === undefined)
680
+ const evaluated = this.#evaluate(source.pipeline, context, source.file);
681
+ if (!evaluated)
717
682
  return null;
718
683
  const reader = new PipelineInputReader(source.file);
719
- const rule = reader.workspace(value);
684
+ const rule = reader.workspace(evaluated.value);
720
685
  this.#diagnostics.push(...reader.diagnostics);
721
686
  return rule;
722
687
  }
@@ -725,25 +690,38 @@ class PipelineCompiler {
725
690
  if (!source)
726
691
  return null;
727
692
  const appContext = { ...context, app };
728
- const value = source.pipeline ? this.#evaluate(source.pipeline, appContext, source.file) : defaultAppPipeline;
729
- if (value === undefined)
693
+ const evaluated = source.pipeline == null ? { value: defaultAppPipeline } : this.#evaluate(source.pipeline, appContext, source.file);
694
+ if (!evaluated)
730
695
  return null;
731
696
  const reader = new PipelineInputReader(source.file);
732
- const rule = reader.app(value);
697
+ const rule = reader.app(evaluated.value);
733
698
  this.#diagnostics.push(...reader.diagnostics);
734
699
  return rule ? this.#lane(app, rule, source.file) : null;
735
700
  }
736
701
  #evaluate(config, context, file) {
737
- if (typeof config !== "function")
738
- return config;
739
- if (this.#input.sampled)
740
- this.#warning("pipeline.function.sampled", "a function form was checked against one sample context; other branches, triggers or apps may differ", file, "pipeline");
741
- try {
742
- return config(context);
743
- } catch (error) {
744
- this.#error("pipeline.load.failed", `the pipeline function threw: ${error instanceof Error ? error.message : String(error)}`, file, "pipeline");
745
- return;
702
+ let value = config;
703
+ if (typeof config === "function") {
704
+ if (this.#input.sampled)
705
+ this.#warning("pipeline.function.sampled", "a function form was checked against one sample context; other branches, triggers or apps may differ", file, "pipeline");
706
+ try {
707
+ value = config(context);
708
+ } catch (error) {
709
+ this.#error("pipeline.load.failed", `the pipeline function threw: ${toError(error).message}`, file, "pipeline");
710
+ return null;
711
+ }
712
+ if (value === undefined) {
713
+ this.#error("pipeline.schema.invalid", "the pipeline function returned nothing for this run: return a pipeline on every path", file, "pipeline");
714
+ return null;
715
+ }
716
+ }
717
+ if (typeof value?.then === "function") {
718
+ Promise.resolve(value).catch(() => {
719
+ return;
720
+ });
721
+ this.#error("pipeline.schema.invalid", "is a Promise, which the planner cannot wait for: return the pipeline itself, not from an async function", file, "pipeline");
722
+ return null;
746
723
  }
724
+ return { value };
747
725
  }
748
726
  #lane(app, rule, file) {
749
727
  const lane = {
@@ -987,7 +965,7 @@ class PipelineCompiler {
987
965
  maxParallel: stage.maxParallel,
988
966
  failFast: false,
989
967
  timeoutSeconds: stage.timeoutSeconds,
990
- stages: apps.map((app) => this.#shellStage(copy, { id: `${stage.id}:${app}`, name: app, app, milestone, env }))
968
+ stages: apps.map((app) => this.#shellStage(copy, { id: `${stage.id}.apps.${app}`, name: app, app, milestone, env }))
991
969
  }
992
970
  ];
993
971
  }
@@ -1034,10 +1012,11 @@ class PipelineCompiler {
1034
1012
  }
1035
1013
  #skipOf(when) {
1036
1014
  const { branch, trigger } = this.#input.context;
1015
+ const skip = (reason) => ({ reason: PipelineInputReader.clip(reason, PipelineCompiler.#reasonChars) });
1037
1016
  if (when.branch && !PipelinePattern2.matches(when.branch, branch))
1038
- return { reason: `branch "${branch}" does not match ${when.branch.map((glob) => `"${glob}"`).join(", ")}` };
1017
+ return skip(`branch "${branch}" does not match ${when.branch.map((glob) => `"${glob}"`).join(", ")}`);
1039
1018
  if (when.trigger && !when.trigger.includes(trigger))
1040
- return { reason: `trigger "${trigger}" is not ${when.trigger.map((name) => `"${name}"`).join(" or ")}` };
1019
+ return skip(`trigger "${trigger}" is not ${when.trigger.map((name) => `"${name}"`).join(" or ")}`);
1041
1020
  return null;
1042
1021
  }
1043
1022
  #sources() {
@@ -1045,7 +1024,7 @@ class PipelineCompiler {
1045
1024
  const sourceOf = (source) => ({
1046
1025
  file: source.file,
1047
1026
  sha256: source.sha256,
1048
- declared: source.pipeline !== null
1027
+ declared: source.pipeline != null
1049
1028
  });
1050
1029
  return {
1051
1030
  workspace: workspace ? sourceOf(workspace) : null,
@@ -1071,6 +1050,7 @@ class PipelineCompiler {
1071
1050
  // pkgs/@akanjs/devkit/pipeline/PipelineDefinition.ts
1072
1051
  import { readFile } from "fs/promises";
1073
1052
  import path from "path";
1053
+ import { toError as toError2 } from "akanjs/common";
1074
1054
  class PipelineDefinition {
1075
1055
  static async load(workspace, apps) {
1076
1056
  const definition = new PipelineDefinition(workspace);
@@ -1099,7 +1079,7 @@ class PipelineDefinition {
1099
1079
  });
1100
1080
  }
1101
1081
  async#load(apps) {
1102
- this.workspaceApps = (await this.#workspace.getApps()).sort((a, b) => a.localeCompare(b));
1082
+ this.workspaceApps = (await this.#workspace.getApps()).sort();
1103
1083
  this.workspaceSource = await this.#loadWorkspace();
1104
1084
  for (const app of new Set(apps)) {
1105
1085
  if (!this.workspaceApps.includes(app))
@@ -1121,7 +1101,7 @@ class PipelineDefinition {
1121
1101
  const targets = await resolveMobileTargets(AppExecutor.from(this.#workspace, app), "all");
1122
1102
  return { targets: targets.map(({ name, config }) => ({ name, updates: !!config.updates })) };
1123
1103
  } catch (error) {
1124
- return { error: error instanceof Error ? error.message : String(error) };
1104
+ return { error: toError2(error).message };
1125
1105
  }
1126
1106
  }
1127
1107
  async#loadWorkspace() {
@@ -1155,7 +1135,7 @@ class PipelineDefinition {
1155
1135
  this.diagnostics.push({
1156
1136
  severity: "error",
1157
1137
  code: "pipeline.load.failed",
1158
- message: `could not load: ${error instanceof Error ? error.message : String(error)}`,
1138
+ message: `could not load: ${toError2(error).message}`,
1159
1139
  file,
1160
1140
  path: null
1161
1141
  });
@@ -1211,7 +1191,7 @@ class PipelineDryRun {
1211
1191
  if (phase.stages.length === 1) {
1212
1192
  const [stage] = phase.stages;
1213
1193
  const command = stage?.steps.map((step) => step.kind === "sh" ? step.name : `action ${step.action}`).join(" \xB7 ");
1214
- return [labels[0], command].filter((part) => part && part !== stage?.name).join(" ");
1194
+ return [labels[0], command].filter((part) => part && part !== phase.name).join(" ");
1215
1195
  }
1216
1196
  return `\u2016 ${labels.join(" \xB7 ")}`;
1217
1197
  }
@@ -1234,20 +1214,24 @@ class PipelineDryRun {
1234
1214
  }
1235
1215
  }
1236
1216
  // pkgs/@akanjs/cli/pipeline/pipeline.script.ts
1237
- import { Logger } from "akanjs/common";
1217
+ import { Logger, toError as toError3 } from "akanjs/common";
1238
1218
 
1239
1219
  // pkgs/@akanjs/cli/pipeline/pipeline.runner.ts
1240
- import { mkdir, open, rename } from "fs/promises";
1241
1220
  import path2 from "path";
1242
1221
  class PipelineRunner extends runner("pipeline") {
1222
+ static #flagChars = 1024;
1243
1223
  async context(workspace, flags) {
1244
- if (!Number.isInteger(flags.buildNum) || flags.buildNum < 0)
1224
+ if (!Number.isSafeInteger(flags.buildNum) || flags.buildNum < 0)
1245
1225
  throw new Error(`--build-num ${flags.buildNum} is not a whole number of 0 or more`);
1246
- const branch = flags.branch ?? await PipelineRunner.#git(workspace, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? "main";
1226
+ for (const flag of ["branch", "env", "commit"])
1227
+ PipelineRunner.#assertFlag(flag, flags[flag]);
1228
+ const branch = flags.branch ?? await PipelineRunner.#git(workspace, ["rev-parse", "--abbrev-ref", "HEAD"]);
1229
+ if (!branch)
1230
+ throw new Error("git names no branch here (a detached HEAD, or no checkout): name it with --branch");
1247
1231
  return {
1248
1232
  branch,
1249
1233
  env: flags.env ?? branch,
1250
- apps: flags.apps === null ? (await workspace.getApps()).sort((a, b) => a.localeCompare(b)) : flags.apps.split(","),
1234
+ apps: flags.apps === null ? (await workspace.getApps()).sort() : flags.apps.split(",").map((app) => app.trim()).filter(Boolean),
1251
1235
  trigger: flags.trigger,
1252
1236
  buildNum: flags.buildNum,
1253
1237
  commit: flags.commit ?? await PipelineRunner.#git(workspace, ["rev-parse", "HEAD"])
@@ -1257,17 +1241,16 @@ class PipelineRunner extends runner("pipeline") {
1257
1241
  return (await PipelineDefinition.load(workspace, context.apps)).compile(context, { sampled });
1258
1242
  }
1259
1243
  async writeResult(out, result) {
1260
- const target = path2.resolve(out);
1261
- await mkdir(path2.dirname(target), { recursive: true });
1262
- const temp = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
1263
- const handle = await open(temp, "wx");
1264
- try {
1265
- await handle.writeFile(`${JSON.stringify(result, null, 2)}
1244
+ await FileSys2.writeTextAtomic(path2.resolve(out), `${JSON.stringify(result, null, 2)}
1266
1245
  `);
1267
- } finally {
1268
- await handle.close();
1269
- }
1270
- await rename(temp, target);
1246
+ }
1247
+ static #assertFlag(flag, value) {
1248
+ if (value === null)
1249
+ return;
1250
+ if (!value.trim())
1251
+ throw new Error(`--${flag} is blank`);
1252
+ if (value.length > PipelineRunner.#flagChars)
1253
+ throw new Error(`--${flag} is longer than ${PipelineRunner.#flagChars} characters`);
1271
1254
  }
1272
1255
  static async#git(workspace, args) {
1273
1256
  try {
@@ -1282,6 +1265,15 @@ class PipelineRunner extends runner("pipeline") {
1282
1265
  // pkgs/@akanjs/cli/pipeline/pipeline.script.ts
1283
1266
  class PipelineScript extends script("pipeline", [PipelineRunner]) {
1284
1267
  async run(workspace, action, { flags, out, format }) {
1268
+ const json = format === "json" || action === "plan" && !out;
1269
+ const restoreLogs = json ? PipelineScript.#logToStderr() : null;
1270
+ try {
1271
+ await this.#run(workspace, action, { flags, out, json });
1272
+ } finally {
1273
+ restoreLogs?.();
1274
+ }
1275
+ }
1276
+ async#run(workspace, action, { flags, out, json }) {
1285
1277
  let result;
1286
1278
  try {
1287
1279
  const context = await this.pipelineRunner.context(workspace, flags);
@@ -1289,11 +1281,11 @@ class PipelineScript extends script("pipeline", [PipelineRunner]) {
1289
1281
  if (out)
1290
1282
  await this.pipelineRunner.writeResult(out, result);
1291
1283
  } catch (error) {
1292
- Logger.rawLog(`akan pipeline ${action}: ${error instanceof Error ? error.message : String(error)}`, undefined, "error");
1284
+ Logger.rawLog(`akan pipeline ${action}: ${toError3(error).message}`, undefined, "error");
1293
1285
  process.exitCode = 2;
1294
1286
  return;
1295
1287
  }
1296
- if (format === "json" || action === "plan" && !out)
1288
+ if (json)
1297
1289
  process.stdout.write(`${JSON.stringify(result, null, 2)}
1298
1290
  `);
1299
1291
  else
@@ -1301,6 +1293,15 @@ class PipelineScript extends script("pipeline", [PipelineRunner]) {
1301
1293
  if (!result.ok)
1302
1294
  process.exitCode = 1;
1303
1295
  }
1296
+ static #logToStderr() {
1297
+ const consoleOutput = Logger.consoleOutput;
1298
+ Logger.consoleOutput = false;
1299
+ const removeSink = Logger.addSink((entry) => void process.stderr.write(entry.message), { minLevel: Logger.level });
1300
+ return () => {
1301
+ removeSink();
1302
+ Logger.consoleOutput = consoleOutput;
1303
+ };
1304
+ }
1304
1305
  static #summary(action, result, out) {
1305
1306
  const head = result.ok ? `${action === "plan" ? "planned" : "valid"}: ${result.plan.phases.length} phases, ${result.plan.phases.reduce((sum, phase) => sum + phase.stages.length, 0)} stages` : `${action === "plan" ? "no plan" : "invalid"}: ${result.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length} error(s)`;
1306
1307
  return [`${head}${out ? ` \u2192 ${out}` : ""}`, ...PipelineDryRun.diagnostics(result.diagnostics)].join(`