paratix 0.10.0 → 0.12.2

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.
package/dist/index.js CHANGED
@@ -1,72 +1,75 @@
1
1
  import {
2
- printCommandFailure,
3
- printModuleResult,
4
- printRecipeHeader,
5
- printRecipeModuleResult,
6
- runSignalModules,
7
- startModuleSpinner,
8
- withRecipeOutputScope
9
- } from "./chunk-47PTUZZR.js";
10
- import {
2
+ CommandError,
11
3
  NEEDS_APPLY,
12
4
  apt,
13
5
  archive,
6
+ assertValidModuleMetaEntries,
7
+ assertValidModuleMetaEntry,
14
8
  command,
15
9
  compose,
10
+ createNullPrototypeEnvironment,
16
11
  cron,
12
+ describeHostValidationFailure,
17
13
  detectPackageManager,
14
+ diffEnvironmentToMetaEntries,
18
15
  download,
16
+ environmentMeta,
17
+ environmentToMetaEntries,
19
18
  failed,
20
19
  failedCommand,
21
20
  file,
21
+ getRunnerAbortSignal,
22
22
  git,
23
23
  group,
24
24
  hostname,
25
+ inspectRedactedDiagnosticValue,
26
+ isBooleanEnvironmentMetaEntry,
27
+ isEnvironmentMetaEntry,
28
+ isLazyEnvironmentMetaEntry,
29
+ isNumberEnvironmentMetaEntry,
25
30
  isPackageInstalled,
31
+ isSshdPortMetaEntry,
32
+ isStringEnvironmentMetaEntry,
33
+ isSystemHostMetaEntry,
34
+ isSystemRebootMetaEntry,
35
+ maskRegisteredSecrets,
36
+ mergeEnvironmentFromMeta,
37
+ meta,
26
38
  mount,
27
39
  net,
28
40
  op,
29
41
  pkg,
30
42
  quadlet,
31
43
  releaseUpgrade,
44
+ resolveEnvironment,
32
45
  rsync,
46
+ sanitizeTerminalText,
33
47
  script,
34
48
  service,
49
+ shellQuote,
35
50
  ssh,
36
51
  sshd,
52
+ sshdPortMeta,
53
+ swap,
37
54
  sysctl,
38
55
  system,
39
- systemd,
40
- ufw,
41
- user
42
- } from "./chunk-M7GETOJ5.js";
43
- import {
44
- CommandError,
45
- assertValidModuleMetaEntries,
46
- assertValidModuleMetaEntry,
47
- diffEnvironmentToMetaEntries,
48
- environmentMeta,
49
- environmentToMetaEntries,
50
- isBooleanEnvironmentMetaEntry,
51
- isEnvironmentMetaEntry,
52
- isLazyEnvironmentMetaEntry,
53
- isNumberEnvironmentMetaEntry,
54
- isSshdPortMetaEntry,
55
- isStringEnvironmentMetaEntry,
56
- isSystemHostMetaEntry,
57
- isSystemRebootMetaEntry,
58
- mergeEnvironmentFromMeta,
59
- meta,
60
- shellQuote,
61
- sshdPortMeta,
62
56
  systemHostMeta,
63
57
  systemRebootMeta,
58
+ systemd,
59
+ timer,
60
+ ufw,
61
+ user,
62
+ validateHostLabel,
64
63
  validateSshConfig
65
- } from "./chunk-NRDLYHJL.js";
64
+ } from "./chunk-YOSHYUST.js";
66
65
 
67
66
  // src/conditionalModules.ts
68
67
  function createConditionalApplyState(environment) {
69
- return { environment: { ...environment }, meta: [], status: "ok" };
68
+ return {
69
+ environment: Object.assign(createNullPrototypeEnvironment(), environment),
70
+ meta: [],
71
+ status: "ok"
72
+ };
70
73
  }
71
74
  function markConditionalApplyChanged(state) {
72
75
  return { ...state, status: "changed" };
@@ -74,7 +77,15 @@ function markConditionalApplyChanged(state) {
74
77
  async function executeConditionalApply(parameters) {
75
78
  const { dryRun, environment, module, ssh: ssh2 } = parameters;
76
79
  if (dryRun && module._applyDryRun != null) {
77
- return module._applyDryRun(ssh2, environment);
80
+ return module._applyDryRun(ssh2, environment, {
81
+ shutdownSignal: parameters.shutdownSignal
82
+ });
83
+ }
84
+ if (module._supportsChildStepHook === true) {
85
+ return module.apply(ssh2, environment, {
86
+ onChildStep: parameters.onChildStep,
87
+ shutdownSignal: parameters.shutdownSignal
88
+ });
78
89
  }
79
90
  return module.apply(ssh2, environment);
80
91
  }
@@ -82,35 +93,61 @@ function shouldExecuteConditionalApply(module, dryRun) {
82
93
  if (!dryRun) return true;
83
94
  return module._applyDryRun != null || module._dryRunBlocker === true || module._dryRunMetaProducer === true;
84
95
  }
85
- async function mergeConditionalApplyState(state, result) {
96
+ function getConditionalChildConnection(module, ssh2) {
97
+ return module.local === true ? null : ssh2;
98
+ }
99
+ async function mergeConditionalApplyState(preserveControlPlaneMeta, state, result) {
86
100
  const environment = await mergeEnvironmentFromMeta(state.environment, result.meta);
101
+ const resultMeta = result.meta == null || preserveControlPlaneMeta ? result.meta : result.meta.filter(isEnvironmentMetaEntry);
87
102
  return {
88
103
  environment,
89
104
  flushSignals: result._flushSignals === true ? true : state.flushSignals,
90
- meta: result.meta == null ? state.meta : [...state.meta, ...result.meta],
105
+ meta: resultMeta == null ? state.meta : [...state.meta, ...resultMeta],
91
106
  status: result.status === "changed" ? "changed" : state.status,
92
107
  stopRun: result._stopRun === true ? true : state.stopRun
93
108
  };
94
109
  }
110
+ async function notifyConditionalChildStep(parameters) {
111
+ if (parameters.onChildStep == null) return;
112
+ await parameters.onChildStep({
113
+ _flushSignals: parameters.result._flushSignals,
114
+ _stopRun: parameters.result._stopRun,
115
+ env: parameters.environment,
116
+ meta: parameters.result.meta,
117
+ status: parameters.result.status
118
+ });
119
+ }
120
+ async function processConditionalApplyResult(parameters) {
121
+ const state = await mergeConditionalApplyState(
122
+ parameters.preserveControlPlaneMeta,
123
+ parameters.state,
124
+ parameters.result
125
+ );
126
+ await notifyConditionalChildStep({
127
+ environment: state.environment,
128
+ onChildStep: parameters.onChildStep,
129
+ result: parameters.result
130
+ });
131
+ return state;
132
+ }
95
133
  async function applyConditionalModules(parameters) {
96
134
  const { dryRun = false, modules, ssh: ssh2 } = parameters;
135
+ const preserveControlPlaneMeta = parameters.onChildStep == null;
136
+ const shutdownSignal = parameters.shutdownSignal ?? (() => null);
97
137
  let state = createConditionalApplyState(parameters.environment);
98
138
  for (const currentModule of modules) {
99
- const checkResult = await currentModule.check(ssh2, state.environment);
100
- if (checkResult === "ok") continue;
101
- if (!shouldExecuteConditionalApply(currentModule, dryRun)) {
102
- state = markConditionalApplyChanged(state);
103
- continue;
104
- }
105
- const result = await executeConditionalApply({
139
+ const step = await applyConditionalModuleStep({
140
+ currentModule,
106
141
  dryRun,
107
- environment: state.environment,
108
- module: currentModule,
109
- ssh: ssh2
142
+ onChildStep: parameters.onChildStep,
143
+ preserveControlPlaneMeta,
144
+ shutdownSignal,
145
+ ssh: ssh2,
146
+ state
110
147
  });
111
- if (result.status === "failed") return result;
112
- state = await mergeConditionalApplyState(state, result);
113
- if (state.stopRun === true) break;
148
+ if (step.kind === "failed") return step.result;
149
+ state = step.state;
150
+ if (step.kind === "break") break;
114
151
  }
115
152
  return {
116
153
  _flushSignals: state.flushSignals,
@@ -119,6 +156,34 @@ async function applyConditionalModules(parameters) {
119
156
  status: state.status
120
157
  };
121
158
  }
159
+ async function applyConditionalModuleStep(parameters) {
160
+ if (parameters.shutdownSignal() != null) {
161
+ return { kind: "break", state: parameters.state };
162
+ }
163
+ const connection = getConditionalChildConnection(parameters.currentModule, parameters.ssh);
164
+ const checkResult = await parameters.currentModule.check(connection, parameters.state.environment);
165
+ if (checkResult === "ok") return { kind: "continue", state: parameters.state };
166
+ if (parameters.shutdownSignal() != null) return { kind: "break", state: parameters.state };
167
+ if (!shouldExecuteConditionalApply(parameters.currentModule, parameters.dryRun)) {
168
+ return { kind: "continue", state: markConditionalApplyChanged(parameters.state) };
169
+ }
170
+ const result = await executeConditionalApply({
171
+ dryRun: parameters.dryRun,
172
+ environment: parameters.state.environment,
173
+ module: parameters.currentModule,
174
+ onChildStep: parameters.onChildStep,
175
+ shutdownSignal: parameters.shutdownSignal,
176
+ ssh: connection
177
+ });
178
+ if (result.status === "failed") return { kind: "failed", result };
179
+ const state = await processConditionalApplyResult({
180
+ onChildStep: parameters.onChildStep,
181
+ preserveControlPlaneMeta: parameters.preserveControlPlaneMeta,
182
+ result,
183
+ state: parameters.state
184
+ });
185
+ return { kind: state.stopRun === true ? "break" : "continue", state };
186
+ }
122
187
  function shouldExecuteConditionalDryRun(module) {
123
188
  return module._applyDryRun != null || module._dryRunBlocker === true || module._dryRunMetaProducer === true;
124
189
  }
@@ -126,9 +191,10 @@ function whenNeedsDryRunApply(modules) {
126
191
  return modules.some((module) => shouldExecuteConditionalDryRun(module));
127
192
  }
128
193
  async function checkConditionalModules(modules, ssh2, environment) {
129
- const currentEnvironment = { ...environment };
194
+ const currentEnvironment = Object.assign(createNullPrototypeEnvironment(), environment);
130
195
  for (const currentModule of modules) {
131
- const result = await currentModule.check(ssh2, currentEnvironment);
196
+ const connection = getConditionalChildConnection(currentModule, ssh2);
197
+ const result = await currentModule.check(connection, currentEnvironment);
132
198
  if (result === NEEDS_APPLY) {
133
199
  return NEEDS_APPLY;
134
200
  }
@@ -137,11 +203,17 @@ async function checkConditionalModules(modules, ssh2, environment) {
137
203
  }
138
204
  function createWhenDryRunApply(condition, modules, needsDryRunApply) {
139
205
  if (!needsDryRunApply) return void 0;
140
- return async (ssh2, environment) => {
206
+ return async (ssh2, environment, options) => {
141
207
  if (!await condition(ssh2, environment)) {
142
208
  return { status: "skipped" };
143
209
  }
144
- return applyConditionalModules({ dryRun: true, environment, modules, ssh: ssh2 });
210
+ return applyConditionalModules({
211
+ dryRun: true,
212
+ environment,
213
+ modules,
214
+ shutdownSignal: options?.shutdownSignal,
215
+ ssh: ssh2
216
+ });
145
217
  };
146
218
  }
147
219
  function createConditionalModule(parameters) {
@@ -152,14 +224,21 @@ function createConditionalModule(parameters) {
152
224
  needsDryRunApply
153
225
  );
154
226
  return {
227
+ _supportsChildStepHook: true,
155
228
  ...parameters.modules.some((module) => module._dryRunBlocker === true) ? { _dryRunBlocker: true } : {},
156
229
  ...parameters.modules.some((module) => module._dryRunMetaProducer === true) ? { _dryRunMetaProducer: true } : {},
157
230
  ...applyDryRun == null ? {} : { _applyDryRun: applyDryRun },
158
- async apply(ssh2, environment) {
231
+ async apply(ssh2, environment, options) {
159
232
  if (!await parameters.condition(ssh2, environment)) {
160
233
  return { status: "skipped" };
161
234
  }
162
- return applyConditionalModules({ environment, modules: parameters.modules, ssh: ssh2 });
235
+ return applyConditionalModules({
236
+ environment,
237
+ modules: parameters.modules,
238
+ onChildStep: options?.onChildStep,
239
+ shutdownSignal: options?.shutdownSignal,
240
+ ssh: ssh2
241
+ });
163
242
  },
164
243
  async check(ssh2, environment) {
165
244
  if (!await parameters.condition(ssh2, environment)) {
@@ -170,6 +249,8 @@ function createConditionalModule(parameters) {
170
249
  name: parameters.name
171
250
  };
172
251
  }
252
+
253
+ // src/conditionalGuards.ts
173
254
  function filesystemTypeName(testFlag) {
174
255
  switch (testFlag) {
175
256
  case "-d": {
@@ -189,7 +270,7 @@ function filesystemTypeName(testFlag) {
189
270
  function createFilesystemGuard(parameters) {
190
271
  const typeName = filesystemTypeName(parameters.testFlag);
191
272
  return createConditionalModule({
192
- condition: async (ssh2) => {
273
+ async condition(ssh2) {
193
274
  if (ssh2 == null) return false;
194
275
  const exists = await ssh2.test(`test ${parameters.testFlag} ${shellQuote(parameters.path)}`);
195
276
  return parameters.invert ? !exists : exists;
@@ -200,7 +281,7 @@ function createFilesystemGuard(parameters) {
200
281
  }
201
282
  function createCommandGuard(commandName, invert, modules) {
202
283
  return createConditionalModule({
203
- condition: async (ssh2) => {
284
+ async condition(ssh2) {
204
285
  if (ssh2 == null) return false;
205
286
  const exists = await ssh2.test(`command -v ${shellQuote(commandName)} >/dev/null 2>&1`);
206
287
  return invert ? !exists : exists;
@@ -211,7 +292,7 @@ function createCommandGuard(commandName, invert, modules) {
211
292
  }
212
293
  function createPackageGuard(packageName, invert, modules) {
213
294
  return createConditionalModule({
214
- condition: async (ssh2) => {
295
+ async condition(ssh2) {
215
296
  if (ssh2 == null) return false;
216
297
  const pm = await detectPackageManager(ssh2);
217
298
  if (pm == null) return false;
@@ -269,17 +350,70 @@ function fail(message) {
269
350
  name: `fail: ${message}`
270
351
  };
271
352
  }
353
+ function normalizePauseAbortReason(reason) {
354
+ if (reason instanceof Error) return reason;
355
+ if (reason === void 0 || reason === null) return new Error("pause aborted");
356
+ if (typeof reason === "string") return new Error(reason);
357
+ return new Error("pause aborted");
358
+ }
359
+ async function waitForEnterOrAbort(abortSignal) {
360
+ return new Promise((resolve, reject) => {
361
+ let settled = false;
362
+ const onData = (chunk) => {
363
+ const input = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
364
+ if (!input.includes("\n") && !input.includes("\r")) return;
365
+ if (settled) return;
366
+ settled = true;
367
+ cleanup();
368
+ process.stdin.pause();
369
+ resolve();
370
+ };
371
+ const onAbort = () => {
372
+ if (settled) return;
373
+ settled = true;
374
+ cleanup();
375
+ process.stdin.pause();
376
+ reject(normalizePauseAbortReason(abortSignal?.reason));
377
+ };
378
+ const onClosed = () => {
379
+ if (settled) return;
380
+ settled = true;
381
+ cleanup();
382
+ process.stdin.pause();
383
+ reject(new Error("pause input closed before Enter"));
384
+ };
385
+ const onError = (error) => {
386
+ if (settled) return;
387
+ settled = true;
388
+ cleanup();
389
+ process.stdin.pause();
390
+ reject(error instanceof Error ? error : new Error("pause input error"));
391
+ };
392
+ function cleanup() {
393
+ process.stdin.removeListener("data", onData);
394
+ process.stdin.removeListener("end", onClosed);
395
+ process.stdin.removeListener("close", onClosed);
396
+ process.stdin.removeListener("error", onError);
397
+ abortSignal?.removeEventListener("abort", onAbort);
398
+ }
399
+ if (abortSignal?.aborted === true) {
400
+ onAbort();
401
+ return;
402
+ }
403
+ process.stdin.on("end", onClosed);
404
+ process.stdin.on("close", onClosed);
405
+ process.stdin.on("error", onError);
406
+ process.stdin.on("data", onData);
407
+ process.stdin.resume();
408
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
409
+ });
410
+ }
272
411
  function pause(message) {
273
412
  return {
274
413
  async apply() {
275
414
  const promptText = message ?? "Press enter to continue...";
276
415
  process.stdout.write(` [pause] ${promptText} `);
277
- await new Promise((resolve) => {
278
- process.stdin.once("data", () => {
279
- process.stdin.pause();
280
- resolve();
281
- });
282
- });
416
+ await waitForEnterOrAbort(getRunnerAbortSignal());
283
417
  return { status: "ok" };
284
418
  },
285
419
  // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
@@ -377,6 +511,656 @@ var when = Object.assign(baseWhen, {
377
511
  symlinkMissing: (path, ...modules) => createFilesystemGuard({ invert: true, modules, path, testFlag: "-L" })
378
512
  });
379
513
 
514
+ // src/firstRunContext.ts
515
+ import { AsyncLocalStorage } from "async_hooks";
516
+ var firstRunContext = new AsyncLocalStorage();
517
+ function isFirstRun() {
518
+ return firstRunContext.getStore() === true;
519
+ }
520
+
521
+ // src/dryRunDispatch.ts
522
+ function shouldExecuteApplyDuringDryRun(module, diffEnabled) {
523
+ if (module._dryRunBlocker === true || module._dryRunMetaProducer === true) return true;
524
+ if (diffEnabled && module._dryRunDiffProducer === true && module._applyDryRun != null) return true;
525
+ return module._applyDryRun != null && module._dryRunDiffProducer !== true;
526
+ }
527
+
528
+ // src/output.ts
529
+ import pc from "picocolors";
530
+
531
+ // src/outputFormatting.ts
532
+ import { stripVTControlCharacters } from "util";
533
+ var PACKAGE_MODULE_SUFFIX_LENGTH = 2;
534
+ var PACKAGE_COLUMN_GAP_WIDTH = 2;
535
+ var DEFAULT_TERMINAL_COLUMNS = 100;
536
+ var MIN_PACKAGE_COLUMNS_WIDTH = 24;
537
+ var MIN_PACKAGE_COLUMN_WIDTH = 18;
538
+ var MIN_ANIMATED_LINE_COLUMNS = 8;
539
+ function splitPackageModuleName(name) {
540
+ for (const prefix of ["package.installed: ", "package.absent: "]) {
541
+ if (!name.startsWith(prefix)) continue;
542
+ const packages = name.slice(prefix.length).split(",").map((entry) => entry.trim()).filter(Boolean);
543
+ if (packages.length === 0) return null;
544
+ return {
545
+ packages,
546
+ summaryName: prefix.slice(0, -PACKAGE_MODULE_SUFFIX_LENGTH)
547
+ };
548
+ }
549
+ return null;
550
+ }
551
+ function getPackageColumns(parameters) {
552
+ if (parameters.packages.length <= 1) return parameters.packages;
553
+ const availableWidth = Math.max(
554
+ (parameters.terminalColumns ?? DEFAULT_TERMINAL_COLUMNS) - parameters.continuationIndentWidth,
555
+ MIN_PACKAGE_COLUMNS_WIDTH
556
+ );
557
+ const widestPackage = Math.max(...parameters.packages.map((entry) => entry.length));
558
+ const columnWidth = Math.max(widestPackage + PACKAGE_COLUMN_GAP_WIDTH, MIN_PACKAGE_COLUMN_WIDTH);
559
+ const columnCount = Math.max(Math.floor(availableWidth / columnWidth), 1);
560
+ const rowCount = Math.ceil(parameters.packages.length / columnCount);
561
+ return Array.from(
562
+ { length: rowCount },
563
+ (_row, rowIndex) => Array.from({ length: columnCount }, (_column, columnIndex) => {
564
+ const packageIndex = rowIndex + columnIndex * rowCount;
565
+ const packageName = parameters.packages.at(packageIndex);
566
+ if (packageName == null) return "";
567
+ const isLastVisibleColumn = columnIndex === columnCount - 1 || packageIndex + rowCount >= parameters.packages.length;
568
+ return isLastVisibleColumn ? packageName : packageName.padEnd(columnWidth);
569
+ }).filter(Boolean).join("")
570
+ );
571
+ }
572
+ function getPackageSummaryDetail(packageCount, detail) {
573
+ const packageLabel = packageCount === 1 ? "1 package" : `${packageCount} packages`;
574
+ return detail == null ? packageLabel : `${packageLabel} \xB7 ${detail}`;
575
+ }
576
+ function fitAnimatedModuleLine(line, columns) {
577
+ if (columns === void 0 || columns < MIN_ANIMATED_LINE_COLUMNS) return line;
578
+ const plainLine = stripVTControlCharacters(line);
579
+ if (plainLine.length < columns) return line;
580
+ return `${plainLine.slice(0, columns - 1)}\u2026`;
581
+ }
582
+ function formatDisplayModule(parameters) {
583
+ const packageModule = splitPackageModuleName(parameters.name);
584
+ if (packageModule == null) {
585
+ return {
586
+ detail: parameters.detail,
587
+ detailLines: [],
588
+ name: parameters.name
589
+ };
590
+ }
591
+ return {
592
+ detail: getPackageSummaryDetail(packageModule.packages.length, parameters.detail),
593
+ detailLines: parameters.status === "waiting" ? [] : getPackageColumns({
594
+ continuationIndentWidth: parameters.continuationIndentWidth,
595
+ packages: packageModule.packages,
596
+ terminalColumns: parameters.terminalColumns
597
+ }),
598
+ name: packageModule.summaryName
599
+ };
600
+ }
601
+
602
+ // src/output.ts
603
+ var CAUSE_INSPECT_DEPTH = 2;
604
+ var CAUSE_INSPECT_MAX_STRING_LENGTH = 1024;
605
+ var CAUSE_REDACT_BINARY_MAX_DEPTH = CAUSE_INSPECT_DEPTH + 1;
606
+ var MODULE_NAME_WIDTH = 56;
607
+ var MIN_MODULE_NAME_WIDTH = 12;
608
+ var OUTPUT_INDENT_UNIT = " ";
609
+ var SPINNER_FRAME_INTERVAL_MS = 80;
610
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
611
+ var STATUS_ICONS = {
612
+ changed: pc.yellow("\u21BA"),
613
+ failed: pc.red("\u2717"),
614
+ ok: pc.green("\u2713"),
615
+ skipped: pc.dim("\u2298"),
616
+ waiting: pc.cyan("\u23F8")
617
+ };
618
+ var activeSpinner = null;
619
+ var activeRecipeGuideDepths = [];
620
+ var pendingRecipeClosureGuideDepths = [];
621
+ var recipeOutputDepth = -1;
622
+ function supportsAnimatedModuleOutput() {
623
+ return process.stdout.isTTY && typeof process.stdout.clearLine === "function" && typeof process.stdout.cursorTo === "function";
624
+ }
625
+ function getModuleIcon(status, waitingFrame) {
626
+ return status === "waiting" ? pc.cyan(waitingFrame ?? "|") : STATUS_ICONS[status];
627
+ }
628
+ function getModuleStatusText(status) {
629
+ switch (status) {
630
+ case "changed": {
631
+ return pc.yellow(status);
632
+ }
633
+ case "failed": {
634
+ return pc.red(status);
635
+ }
636
+ case "ok": {
637
+ return pc.green(status);
638
+ }
639
+ case "skipped": {
640
+ return pc.dim(status);
641
+ }
642
+ case "waiting": {
643
+ return pc.cyan("running");
644
+ }
645
+ }
646
+ }
647
+ function getCurrentOutputDepth() {
648
+ return Math.max(recipeOutputDepth, 0);
649
+ }
650
+ function getGuideDot(depth) {
651
+ return depth % 2 === 0 ? pc.gray("\xB7") : pc.cyan("\xB7");
652
+ }
653
+ function buildGuideIndent(baseIndent, options) {
654
+ const indentCharacters = Array.from(baseIndent);
655
+ const guideDepths = [
656
+ ...options?.activeGuideDepths ?? activeRecipeGuideDepths,
657
+ ...options?.extraGuideDepths ?? []
658
+ ];
659
+ for (const guideDepth of guideDepths) {
660
+ const guideCharacterIndex = OUTPUT_INDENT_UNIT.length * (guideDepth + 1);
661
+ if (guideCharacterIndex >= indentCharacters.length) continue;
662
+ indentCharacters[guideCharacterIndex] = options?.colorize === false ? "\xB7" : getGuideDot(guideDepth);
663
+ }
664
+ return indentCharacters.join("");
665
+ }
666
+ function clearPendingRecipeClosureGuides() {
667
+ pendingRecipeClosureGuideDepths = [];
668
+ }
669
+ function getModuleIndent() {
670
+ if (recipeOutputDepth < 0) {
671
+ return OUTPUT_INDENT_UNIT;
672
+ }
673
+ return OUTPUT_INDENT_UNIT.repeat(getCurrentOutputDepth() + 2);
674
+ }
675
+ function getRecipeHeaderIndent() {
676
+ if (recipeOutputDepth < 0) return "";
677
+ return OUTPUT_INDENT_UNIT.repeat(getCurrentOutputDepth() + 1);
678
+ }
679
+ function getErrorIndent() {
680
+ return `${getModuleIndent()}\u2502 `;
681
+ }
682
+ function getContinuationIndent() {
683
+ return `${getModuleIndent()} `;
684
+ }
685
+ async function withRecipeOutputScope(scopedOperation) {
686
+ recipeOutputDepth += 1;
687
+ try {
688
+ return await scopedOperation();
689
+ } finally {
690
+ activeRecipeGuideDepths = activeRecipeGuideDepths.filter((depth) => depth !== recipeOutputDepth);
691
+ pendingRecipeClosureGuideDepths = [recipeOutputDepth];
692
+ recipeOutputDepth -= 1;
693
+ }
694
+ }
695
+ function renderModuleLine(parameters) {
696
+ const { detail, extraGuideDepths = [], name, status, waitingFrame } = parameters;
697
+ const baseIndent = getModuleIndent();
698
+ const indent = buildGuideIndent(baseIndent, { extraGuideDepths });
699
+ const icon = getModuleIcon(status, waitingFrame);
700
+ const statusText = getModuleStatusText(status);
701
+ const detailSuffix = detail == null ? "" : ` ${pc.dim(detail)}`;
702
+ const alignedNameWidth = Math.max(MODULE_NAME_WIDTH - baseIndent.length, MIN_MODULE_NAME_WIDTH);
703
+ return `${indent}${icon} ${name.padEnd(alignedNameWidth)} ${statusText}${detailSuffix}`;
704
+ }
705
+ function writeAnimatedModuleLine(line) {
706
+ process.stdout.clearLine(0);
707
+ process.stdout.cursorTo(0);
708
+ process.stdout.write(fitAnimatedModuleLine(line, process.stdout.columns));
709
+ }
710
+ function stopAnimatedModuleLine(clearCurrentLine = false) {
711
+ if (activeSpinner == null) return;
712
+ clearInterval(activeSpinner.interval);
713
+ activeSpinner = null;
714
+ if (clearCurrentLine && supportsAnimatedModuleOutput()) {
715
+ process.stdout.clearLine(0);
716
+ process.stdout.cursorTo(0);
717
+ }
718
+ }
719
+ function startModuleSpinner(name, detail) {
720
+ if (!supportsAnimatedModuleOutput()) return;
721
+ clearPendingRecipeClosureGuides();
722
+ stopAnimatedModuleLine();
723
+ const maskedName = maskRegisteredSecrets(name);
724
+ const maskedDetail = detail == null ? void 0 : maskRegisteredSecrets(detail);
725
+ const displayModule = formatDisplayModule({
726
+ continuationIndentWidth: getContinuationIndent().length,
727
+ detail: maskedDetail,
728
+ name: maskedName,
729
+ status: "waiting",
730
+ terminalColumns: process.stdout.columns
731
+ });
732
+ const spinner = {
733
+ detail: displayModule.detail,
734
+ frameIndex: 0,
735
+ interval: setInterval(() => {
736
+ spinner.frameIndex = (spinner.frameIndex + 1) % SPINNER_FRAMES.length;
737
+ writeAnimatedModuleLine(
738
+ renderModuleLine({
739
+ detail: spinner.detail,
740
+ name: displayModule.name,
741
+ status: "waiting",
742
+ waitingFrame: SPINNER_FRAMES[spinner.frameIndex]
743
+ })
744
+ );
745
+ }, SPINNER_FRAME_INTERVAL_MS)
746
+ };
747
+ if (typeof spinner.interval.unref === "function") spinner.interval.unref();
748
+ activeSpinner = spinner;
749
+ writeAnimatedModuleLine(
750
+ renderModuleLine({
751
+ detail: displayModule.detail,
752
+ name: displayModule.name,
753
+ status: "waiting",
754
+ waitingFrame: SPINNER_FRAMES[0]
755
+ })
756
+ );
757
+ }
758
+ function printRecipeHeader(name) {
759
+ stopAnimatedModuleLine(true);
760
+ clearPendingRecipeClosureGuides();
761
+ const header = pc.bold(pc.blue(`[${name}]`));
762
+ console.log(`${buildGuideIndent(getRecipeHeaderIndent())}${header}`);
763
+ if (recipeOutputDepth >= 0) {
764
+ activeRecipeGuideDepths = [...activeRecipeGuideDepths, recipeOutputDepth];
765
+ }
766
+ }
767
+ function colorizeDiffLine(line) {
768
+ if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("@@")) {
769
+ return pc.dim(line);
770
+ }
771
+ if (line.startsWith("-")) return pc.red(line);
772
+ if (line.startsWith("+")) return pc.green(line);
773
+ return pc.dim(line);
774
+ }
775
+ function renderDiffLines(diff) {
776
+ if (diff.trim() === "") return [];
777
+ const sanitized = sanitizeTerminalText(maskRegisteredSecrets(diff));
778
+ return sanitized.split("\n").map((line) => `\u2502 ${colorizeDiffLine(line)}`);
779
+ }
780
+ function writeContinuationLine(line, extraGuideDepths, via) {
781
+ const composed = `${buildGuideIndent(getContinuationIndent(), { extraGuideDepths })}${line}`;
782
+ if (via === "stdout") {
783
+ process.stdout.write(`${composed}
784
+ `);
785
+ } else {
786
+ console.log(composed);
787
+ }
788
+ }
789
+ function writeContinuationBlock(parameters) {
790
+ for (const detailLine of parameters.detailLines) {
791
+ writeContinuationLine(pc.dim(detailLine), parameters.extraGuideDepths, parameters.via);
792
+ }
793
+ for (const diffLine of parameters.diffLines) {
794
+ writeContinuationLine(diffLine, parameters.extraGuideDepths, parameters.via);
795
+ }
796
+ }
797
+ function printRenderedModuleResult(parameters) {
798
+ const extraGuideDepths = parameters.extraGuideDepths ?? [];
799
+ const maskedName = maskRegisteredSecrets(parameters.name);
800
+ const maskedDetail = parameters.detail == null ? void 0 : maskRegisteredSecrets(parameters.detail);
801
+ const displayModule = formatDisplayModule({
802
+ continuationIndentWidth: `${buildGuideIndent(
803
+ OUTPUT_INDENT_UNIT.repeat(Math.max(getCurrentOutputDepth() + 2, 1)),
804
+ { extraGuideDepths }
805
+ )} `.length,
806
+ detail: maskedDetail,
807
+ name: maskedName,
808
+ status: parameters.status,
809
+ terminalColumns: process.stdout.columns
810
+ });
811
+ const line = renderModuleLine({
812
+ detail: displayModule.detail,
813
+ extraGuideDepths,
814
+ name: displayModule.name,
815
+ status: parameters.status
816
+ });
817
+ const diffLines = parameters.diff == null ? [] : renderDiffLines(parameters.diff);
818
+ const usesSpinner = supportsAnimatedModuleOutput() && activeSpinner != null;
819
+ if (usesSpinner) {
820
+ stopAnimatedModuleLine();
821
+ writeAnimatedModuleLine(line);
822
+ process.stdout.write("\n");
823
+ } else {
824
+ console.log(line);
825
+ }
826
+ writeContinuationBlock({
827
+ detailLines: displayModule.detailLines,
828
+ diffLines,
829
+ extraGuideDepths,
830
+ via: usesSpinner ? "stdout" : "console"
831
+ });
832
+ }
833
+ function printModuleResult(name, status, detail, diff) {
834
+ clearPendingRecipeClosureGuides();
835
+ printRenderedModuleResult({ detail, diff, name, status });
836
+ }
837
+ function printRecipeModuleResult(name, status, detail, diff) {
838
+ printRenderedModuleResult({
839
+ detail,
840
+ diff,
841
+ extraGuideDepths: pendingRecipeClosureGuideDepths,
842
+ name,
843
+ status
844
+ });
845
+ clearPendingRecipeClosureGuides();
846
+ }
847
+ function printCommandError(stdout, stderr) {
848
+ const maskedStdout = sanitizeTerminalText(maskRegisteredSecrets(stdout));
849
+ const maskedStderr = sanitizeTerminalText(maskRegisteredSecrets(stderr));
850
+ const lines = [];
851
+ if (maskedStderr.trim()) {
852
+ lines.push(...maskedStderr.trim().split("\n"));
853
+ }
854
+ if (maskedStdout.trim()) {
855
+ lines.push(...maskedStdout.trim().split("\n"));
856
+ }
857
+ if (lines.length > 0) {
858
+ console.error(pc.red(`${getErrorIndent()}Error output:`));
859
+ for (const line of lines) {
860
+ console.error(pc.red(`${getErrorIndent()}${line}`));
861
+ }
862
+ }
863
+ }
864
+ function printVerboseCommandError(stdout, stderr) {
865
+ const maskedStdout = sanitizeTerminalText(maskRegisteredSecrets(stdout));
866
+ const maskedStderr = sanitizeTerminalText(maskRegisteredSecrets(stderr));
867
+ if (maskedStderr.trim()) {
868
+ console.error(pc.red(`${getErrorIndent()}Full stderr:`));
869
+ for (const line of maskedStderr.trim().split("\n")) {
870
+ console.error(pc.red(`${getErrorIndent()}${line}`));
871
+ }
872
+ }
873
+ if (maskedStdout.trim()) {
874
+ console.error(pc.red(`${getErrorIndent()}Full stdout:`));
875
+ for (const line of maskedStdout.trim().split("\n")) {
876
+ console.error(pc.red(`${getErrorIndent()}${line}`));
877
+ }
878
+ }
879
+ }
880
+ function printVerboseErrorBlock(label, content) {
881
+ if (!content.trim()) {
882
+ return;
883
+ }
884
+ console.error(pc.red(`${getErrorIndent()}${label}`));
885
+ for (const line of content.trim().split("\n")) {
886
+ console.error(pc.red(`${getErrorIndent()}${line}`));
887
+ }
888
+ }
889
+ function getErrorCause(error) {
890
+ return error.cause;
891
+ }
892
+ function printVerboseErrorCause(cause, depth, visitedCauses) {
893
+ const label = `Cause ${depth}:`;
894
+ if (cause instanceof Error) {
895
+ if (visitedCauses.has(cause)) {
896
+ printVerboseErrorBlock(label, "<cycle detected>");
897
+ return;
898
+ }
899
+ visitedCauses.add(cause);
900
+ const stack = cause.stack?.trim() ?? "";
901
+ const stackOrMessage = stack.length > 0 ? stack : String(cause);
902
+ printVerboseErrorBlock(label, maskRegisteredSecrets(stackOrMessage));
903
+ const nestedCause = getErrorCause(cause);
904
+ if (nestedCause !== void 0) {
905
+ printVerboseErrorCause(nestedCause, depth + 1, visitedCauses);
906
+ }
907
+ return;
908
+ }
909
+ printVerboseErrorBlock(label, maskRegisteredSecrets(formatCauseValue(cause)));
910
+ }
911
+ function printVerboseGenericError(error) {
912
+ const stack = error.stack?.trim() ?? "";
913
+ const stackOrMessage = stack.length > 0 ? stack : String(error);
914
+ printVerboseErrorBlock("Full stack:", maskRegisteredSecrets(stackOrMessage));
915
+ const cause = getErrorCause(error);
916
+ if (cause !== void 0) {
917
+ const visitedCauses = /* @__PURE__ */ new WeakSet();
918
+ visitedCauses.add(error);
919
+ printVerboseErrorCause(cause, 1, visitedCauses);
920
+ }
921
+ }
922
+ function formatCauseValue(cause) {
923
+ if (cause instanceof Error) return cause.message;
924
+ return inspectRedactedDiagnosticValue(cause, {
925
+ depth: CAUSE_INSPECT_DEPTH,
926
+ maxStringLength: CAUSE_INSPECT_MAX_STRING_LENGTH,
927
+ redactMaxDepth: CAUSE_REDACT_BINARY_MAX_DEPTH
928
+ });
929
+ }
930
+ function printCauseChain(error) {
931
+ const visited = /* @__PURE__ */ new WeakSet();
932
+ visited.add(error);
933
+ let cause = getErrorCause(error);
934
+ while (cause !== void 0) {
935
+ if (cause instanceof Error) {
936
+ if (visited.has(cause)) return;
937
+ visited.add(cause);
938
+ }
939
+ console.error(pc.red(`${getErrorIndent()}Cause: ${maskRegisteredSecrets(formatCauseValue(cause))}`));
940
+ if (cause instanceof CommandError) {
941
+ printVerboseCommandError(
942
+ maskRegisteredSecrets(cause.fullStdout),
943
+ maskRegisteredSecrets(cause.fullStderr)
944
+ );
945
+ }
946
+ cause = cause instanceof Error ? getErrorCause(cause) : void 0;
947
+ }
948
+ }
949
+ function printCommandFailure(error, verbose) {
950
+ if (verbose && error instanceof CommandError) {
951
+ const summaryLine = error.message.split("\n")[0];
952
+ printCommandError("", maskRegisteredSecrets(summaryLine));
953
+ printVerboseCommandError(
954
+ maskRegisteredSecrets(error.fullStdout),
955
+ maskRegisteredSecrets(error.fullStderr)
956
+ );
957
+ return;
958
+ }
959
+ printCommandError("", maskRegisteredSecrets(String(error)));
960
+ if (error instanceof Error) {
961
+ if (!(error instanceof CommandError)) {
962
+ printCauseChain(error);
963
+ }
964
+ if (verbose) {
965
+ printVerboseGenericError(error);
966
+ }
967
+ }
968
+ }
969
+
970
+ // src/dryRunRecipe.ts
971
+ function interruptedDryRunResult(parameters) {
972
+ return {
973
+ env: parameters.currentEnvironment,
974
+ meta: parameters.aggregatedMeta.length === 0 ? void 0 : parameters.aggregatedMeta,
975
+ shouldBreak: true,
976
+ status: parameters.aggregatedStatus === "changed" ? "changed" : void 0
977
+ };
978
+ }
979
+ async function executeDryRunBlockingModule(parameters) {
980
+ const { childModule, connection, diff, environment, verbose } = parameters;
981
+ if (parameters.shutdownSignal() != null) return { env: environment, shouldBreak: true };
982
+ startModuleSpinner(childModule.name);
983
+ const result = childModule._applyDryRun == null ? await childModule.apply(connection, environment) : await childModule._applyDryRun(connection, environment, {
984
+ shutdownSignal: parameters.shutdownSignal
985
+ });
986
+ const nextEnvironment = result.meta == null ? environment : await mergeEnvironmentFromMeta(environment, result.meta);
987
+ const diffOutput = diff ? result.diff : void 0;
988
+ printModuleResult(
989
+ childModule.name,
990
+ result.status,
991
+ result._dryRunDetail ?? "(dry-run)",
992
+ diffOutput
993
+ );
994
+ if (result.status === "failed" && result.error != null) {
995
+ printCommandFailure(result.error, verbose);
996
+ }
997
+ return {
998
+ env: nextEnvironment,
999
+ meta: result.meta,
1000
+ shouldBreak: result.status === "failed" || result._stopRun === true,
1001
+ status: result.status,
1002
+ stopRun: result._stopRun
1003
+ };
1004
+ }
1005
+ async function executeDryRunChildModule(parameters) {
1006
+ const { childModule, diff, environment, ssh: ssh2, verbose } = parameters;
1007
+ if (parameters.shutdownSignal() != null) return { env: environment, shouldBreak: true };
1008
+ const connection = childModule.local === true ? null : ssh2;
1009
+ startModuleSpinner(childModule.name);
1010
+ const checkResult = await childModule.check(connection, environment);
1011
+ if (parameters.shutdownSignal() != null) return { env: environment, shouldBreak: true };
1012
+ if (checkResult !== "ok" && shouldExecuteApplyDuringDryRun(childModule, diff)) {
1013
+ return executeDryRunBlockingModule({
1014
+ childModule,
1015
+ connection,
1016
+ diff,
1017
+ environment,
1018
+ shutdownSignal: parameters.shutdownSignal,
1019
+ verbose
1020
+ });
1021
+ }
1022
+ const status = checkResult === "ok" ? "ok" : "changed";
1023
+ const suffix = checkResult === "ok" ? void 0 : "(dry-run)";
1024
+ printModuleResult(childModule.name, status, suffix);
1025
+ return { env: environment, shouldBreak: false, status };
1026
+ }
1027
+ function applyDryRunChildResult(accumulator, result) {
1028
+ return {
1029
+ aggregatedMeta: result.meta == null ? accumulator.aggregatedMeta : [...accumulator.aggregatedMeta, ...result.meta],
1030
+ aggregatedStatus: result.status === "changed" ? "changed" : accumulator.aggregatedStatus,
1031
+ currentEnvironment: result.env
1032
+ };
1033
+ }
1034
+ async function runDryRunChildLoop(parameters) {
1035
+ let accumulator = parameters.accumulator;
1036
+ for (const childModule of parameters.recipeModule._modules) {
1037
+ if (parameters.shutdownSignal() != null) {
1038
+ return interruptedDryRunResult({
1039
+ aggregatedMeta: accumulator.aggregatedMeta,
1040
+ aggregatedStatus: accumulator.aggregatedStatus,
1041
+ currentEnvironment: accumulator.currentEnvironment
1042
+ });
1043
+ }
1044
+ const result = await executeDryRunChildModule({
1045
+ childModule,
1046
+ diff: parameters.diff,
1047
+ environment: accumulator.currentEnvironment,
1048
+ shutdownSignal: parameters.shutdownSignal,
1049
+ ssh: parameters.ssh,
1050
+ verbose: parameters.verbose
1051
+ });
1052
+ if (result.shouldBreak) return result;
1053
+ accumulator = applyDryRunChildResult(accumulator, result);
1054
+ }
1055
+ return accumulator;
1056
+ }
1057
+ async function dryRunRecipeModule(parameters) {
1058
+ return withRecipeOutputScope(async () => {
1059
+ const { environment, recipeModule, ssh: ssh2 } = parameters;
1060
+ printRecipeHeader(recipeModule.name);
1061
+ const shutdownSignal = parameters.shutdownSignal ?? (() => null);
1062
+ const loopResult = await runDryRunChildLoop({
1063
+ accumulator: {
1064
+ aggregatedMeta: [],
1065
+ aggregatedStatus: "ok",
1066
+ currentEnvironment: environment
1067
+ },
1068
+ diff: parameters.options?.diff ?? false,
1069
+ recipeModule,
1070
+ shutdownSignal,
1071
+ ssh: ssh2,
1072
+ verbose: parameters.options?.verbose ?? false
1073
+ });
1074
+ if ("shouldBreak" in loopResult) return loopResult;
1075
+ return {
1076
+ env: loopResult.currentEnvironment,
1077
+ meta: loopResult.aggregatedMeta.length === 0 ? void 0 : loopResult.aggregatedMeta,
1078
+ shouldBreak: false,
1079
+ status: loopResult.aggregatedStatus
1080
+ };
1081
+ });
1082
+ }
1083
+
1084
+ // src/signalOrchestration.ts
1085
+ function handleSignalResult(parameters) {
1086
+ const { hooks, result, signalName, verbose } = parameters;
1087
+ printModuleResult(`signal: ${signalName}`, result.status);
1088
+ if (result.status === "failed" && result.error != null) {
1089
+ printCommandFailure(result.error, verbose);
1090
+ }
1091
+ hooks?.onSignalFinished?.(result.status);
1092
+ return result.status === "failed" ? "failed" : "changed";
1093
+ }
1094
+ function handleSignalFailure(parameters) {
1095
+ const { error, hooks, signalName, verbose } = parameters;
1096
+ printModuleResult(`signal: ${signalName}`, "failed");
1097
+ printCommandFailure(error, verbose);
1098
+ hooks?.onSignalFinished?.("failed");
1099
+ return "failed";
1100
+ }
1101
+ async function applySignalMeta(parameters) {
1102
+ assertValidModuleMetaEntries(parameters.result.meta);
1103
+ const nextEnvironment = parameters.result.status === "failed" ? parameters.currentEnvironment : await mergeEnvironmentFromMeta(parameters.currentEnvironment, parameters.result.meta);
1104
+ await parameters.onSignalStep?.({
1105
+ env: nextEnvironment,
1106
+ meta: parameters.result.meta,
1107
+ status: parameters.result.status
1108
+ });
1109
+ return nextEnvironment;
1110
+ }
1111
+ async function runOneSignal(parameters) {
1112
+ const connection = parameters.signal.local === true ? null : parameters.ssh;
1113
+ startModuleSpinner(`signal: ${parameters.signal.name}`);
1114
+ const result = parameters.shutdownSignal == null ? await parameters.signal.apply(connection, parameters.currentEnvironment) : await parameters.signal.apply(connection, parameters.currentEnvironment, {
1115
+ shutdownSignal: parameters.shutdownSignal
1116
+ });
1117
+ const nextEnvironment = await applySignalMeta({
1118
+ currentEnvironment: parameters.currentEnvironment,
1119
+ onSignalStep: parameters.onSignalStep,
1120
+ result
1121
+ });
1122
+ return {
1123
+ nextEnvironment,
1124
+ status: handleSignalResult({
1125
+ hooks: parameters.hooks,
1126
+ result,
1127
+ signalName: parameters.signal.name,
1128
+ verbose: parameters.verbose
1129
+ })
1130
+ };
1131
+ }
1132
+ async function runSignalModules(parameters) {
1133
+ const getShutdownSignal = parameters.shutdownSignal ?? (() => null);
1134
+ const verbose = parameters.verbose ?? false;
1135
+ let currentEnvironment = parameters.environment;
1136
+ let status = "changed";
1137
+ for (const signal of parameters.signals) {
1138
+ if (getShutdownSignal() != null) break;
1139
+ parameters.hooks?.onSignalStarted?.();
1140
+ try {
1141
+ const signalStep = await runOneSignal({
1142
+ currentEnvironment,
1143
+ hooks: parameters.hooks,
1144
+ onSignalStep: parameters.onSignalStep,
1145
+ shutdownSignal: parameters.shutdownSignal,
1146
+ signal,
1147
+ ssh: parameters.ssh,
1148
+ verbose
1149
+ });
1150
+ currentEnvironment = signalStep.nextEnvironment;
1151
+ if (signalStep.status === "failed") status = "failed";
1152
+ } catch (error) {
1153
+ status = handleSignalFailure({
1154
+ error,
1155
+ hooks: parameters.hooks,
1156
+ signalName: signal.name,
1157
+ verbose
1158
+ });
1159
+ }
1160
+ }
1161
+ return status;
1162
+ }
1163
+
380
1164
  // src/recipe.ts
381
1165
  var INTERRUPTED_BEFORE_APPLY = /* @__PURE__ */ Symbol("recipe-interrupted-before-apply");
382
1166
  function isRecipeModuleLike(module) {
@@ -422,12 +1206,21 @@ async function executeOneModule(parameters) {
422
1206
  if ((parameters.shutdownSignal?.() ?? null) != null) {
423
1207
  return INTERRUPTED_BEFORE_APPLY;
424
1208
  }
425
- const result = await targetModule.apply(connection, currentEnvironment);
1209
+ const result = await applyRecipeChild({
1210
+ connection,
1211
+ currentEnvironment,
1212
+ onChildStep: parameters.onChildStep,
1213
+ onSignalStep: parameters.onSignalStep,
1214
+ shutdownSignal: parameters.shutdownSignal,
1215
+ signalHooks: parameters.signalHooks,
1216
+ targetModule,
1217
+ verbose
1218
+ });
426
1219
  printRecipeChildResult(targetModule, result);
427
1220
  if (result.status === "failed" && result.error != null) {
428
1221
  printCommandFailure(result.error, verbose);
429
1222
  }
430
- const environment = await mergeEnvironmentFromMeta(currentEnvironment, result.meta);
1223
+ const environment = result.status === "failed" ? currentEnvironment : await mergeEnvironmentFromMeta(currentEnvironment, result.meta);
431
1224
  return {
432
1225
  _flushSignals: result._flushSignals,
433
1226
  _stopRun: result._stopRun,
@@ -440,6 +1233,24 @@ async function checkRecipeChild(targetModule, connection, currentEnvironment) {
440
1233
  startModuleSpinner(targetModule.name);
441
1234
  return targetModule.check(connection, currentEnvironment);
442
1235
  }
1236
+ async function applyRecipeChild(parameters) {
1237
+ if (isRecipeModuleLike(parameters.targetModule)) {
1238
+ return parameters.targetModule.apply(parameters.connection, parameters.currentEnvironment, {
1239
+ onChildStep: parameters.onChildStep,
1240
+ onSignalStep: parameters.onSignalStep,
1241
+ shutdownSignal: parameters.shutdownSignal,
1242
+ signalHooks: parameters.signalHooks,
1243
+ verbose: parameters.verbose
1244
+ });
1245
+ }
1246
+ if (parameters.targetModule._supportsChildStepHook === true && (parameters.onChildStep != null || parameters.shutdownSignal != null)) {
1247
+ return parameters.targetModule.apply(parameters.connection, parameters.currentEnvironment, {
1248
+ onChildStep: parameters.onChildStep,
1249
+ shutdownSignal: parameters.shutdownSignal
1250
+ });
1251
+ }
1252
+ return parameters.targetModule.apply(parameters.connection, parameters.currentEnvironment);
1253
+ }
443
1254
  async function applyExecutedRecipeStep(parameters) {
444
1255
  if (parameters.step == null) return parameters.state;
445
1256
  if (parameters.step === INTERRUPTED_BEFORE_APPLY) return null;
@@ -519,7 +1330,13 @@ async function executeModules(modules, ssh2, parameters) {
519
1330
  const shutdownSignal = parameters.shutdownSignal ?? (() => null);
520
1331
  const verbose = parameters.verbose ?? false;
521
1332
  let state = {
522
- env: { ...parameters.environment },
1333
+ // R-0000079: preserve the null-prototype guarantee that
1334
+ // R-0000069/R-0000070/R-0000074 establish for the runner-level
1335
+ // environment. Plain object spread (`{ ...environment }`) would create a
1336
+ // map with `Object.prototype`, exposing the first child module of every
1337
+ // recipe to a polluted-fähige environment. Mirror the meta.ts:214
1338
+ // approach.
1339
+ env: Object.assign(createNullPrototypeEnvironment(), parameters.environment),
523
1340
  meta: void 0,
524
1341
  signalsPending: false,
525
1342
  status: "ok"
@@ -528,7 +1345,10 @@ async function executeModules(modules, ssh2, parameters) {
528
1345
  if (shutdownSignal() != null) break;
529
1346
  const step = await executeRecipeChildStep({
530
1347
  currentEnvironment: state.env,
1348
+ onChildStep,
1349
+ onSignalStep: parameters.onSignalStep,
531
1350
  shutdownSignal,
1351
+ signalHooks: parameters.signalHooks,
532
1352
  ssh: ssh2,
533
1353
  targetModule: currentModule,
534
1354
  verbose
@@ -609,11 +1429,51 @@ async function applyRecipe(parameters) {
609
1429
  function shouldRunRecipeSignalsAtEnd(state, signals2) {
610
1430
  return state.signalsPending && state.status === "changed" && signals2 != null;
611
1431
  }
1432
+ function shouldExecuteRecipeDryRun(module) {
1433
+ return module._applyDryRun != null || module._dryRunBlocker === true || module._dryRunMetaProducer === true;
1434
+ }
1435
+ function recipeNeedsDryRunApply(modules) {
1436
+ return modules.some((module) => shouldExecuteRecipeDryRun(module));
1437
+ }
1438
+ function createRecipeDryRunApply(name, modules, needsDryRunApply) {
1439
+ if (!needsDryRunApply) return void 0;
1440
+ return async (ssh2, environment, parameters) => {
1441
+ const result = await dryRunRecipeModule({
1442
+ environment,
1443
+ recipeModule: {
1444
+ _isRecipe: true,
1445
+ _modules: modules,
1446
+ async apply() {
1447
+ await Promise.resolve();
1448
+ return { status: "ok" };
1449
+ },
1450
+ async check() {
1451
+ await Promise.resolve();
1452
+ return "ok";
1453
+ },
1454
+ name
1455
+ },
1456
+ shutdownSignal: parameters?.shutdownSignal,
1457
+ ssh: ssh2
1458
+ });
1459
+ return {
1460
+ _stopRun: result.stopRun,
1461
+ meta: result.meta,
1462
+ status: result.status ?? "ok"
1463
+ };
1464
+ };
1465
+ }
612
1466
  function recipe(name, modules, options) {
1467
+ const needsDryRunApply = recipeNeedsDryRunApply(modules);
1468
+ const applyDryRun = createRecipeDryRunApply(name, modules, needsDryRunApply);
613
1469
  return {
1470
+ ...modules.some((module) => module._dryRunBlocker === true) ? { _dryRunBlocker: true } : {},
1471
+ ...modules.some((module) => module._dryRunMetaProducer === true) ? { _dryRunMetaProducer: true } : {},
1472
+ ...applyDryRun == null ? {} : { _applyDryRun: applyDryRun },
614
1473
  _isRecipe: true,
615
1474
  _modules: modules,
616
1475
  _signals: options?.signals,
1476
+ _supportsChildStepHook: true,
617
1477
  async apply(ssh2, environment, parameters) {
618
1478
  return applyRecipe({
619
1479
  environment,
@@ -626,6 +1486,7 @@ function recipe(name, modules, options) {
626
1486
  },
627
1487
  async check(ssh2, environment) {
628
1488
  for (const childModule of modules) {
1489
+ if (getRunnerAbortSignal()?.aborted === true) return NEEDS_APPLY;
629
1490
  const connection = childModule.local === true ? null : ssh2;
630
1491
  let result;
631
1492
  try {
@@ -642,17 +1503,44 @@ function recipe(name, modules, options) {
642
1503
  }
643
1504
 
644
1505
  // src/server.ts
645
- function server(config) {
646
- if (config.host.length === 0) {
1506
+ function isModuleLike(value) {
1507
+ if (value === null || typeof value !== "object") return false;
1508
+ return "apply" in value && "check" in value && "name" in value && typeof value.name === "string" && value.name.length > 0 && typeof value.check === "function" && typeof value.apply === "function";
1509
+ }
1510
+ function validateModuleList(modules, property, options) {
1511
+ if (!Array.isArray(modules)) {
1512
+ throw new TypeError(`ServerDefinition: ${property} must be an array of modules`);
1513
+ }
1514
+ if (options?.requireNonEmpty === true && modules.length === 0) {
1515
+ throw new Error("ServerDefinition: run must contain at least one module");
1516
+ }
1517
+ for (const [index, module] of modules.entries()) {
1518
+ if (!isModuleLike(module)) {
1519
+ throw new Error(
1520
+ `ServerDefinition: ${property}[${index}] must be a module with name, check, and apply`
1521
+ );
1522
+ }
1523
+ }
1524
+ }
1525
+ function validateServerDefinition(config, options) {
1526
+ const hostValidationFailure = validateHostLabel(config.host);
1527
+ if (hostValidationFailure === "empty") {
647
1528
  throw new Error("ServerDefinition: host is required");
648
1529
  }
1530
+ if (hostValidationFailure != null) {
1531
+ throw new Error(
1532
+ `ServerDefinition: host ${describeHostValidationFailure(hostValidationFailure)}`
1533
+ );
1534
+ }
649
1535
  if (config.name.length === 0) {
650
1536
  throw new Error("ServerDefinition: name is required");
651
1537
  }
652
1538
  validateSshConfig(config.ssh);
653
- if (config.run.length === 0) {
654
- throw new Error("ServerDefinition: run must contain at least one module");
655
- }
1539
+ validateModuleList(config.run, "run", { requireNonEmpty: options?.allowEmptyRun !== true });
1540
+ if (config.signals !== void 0) validateModuleList(config.signals, "signals");
1541
+ }
1542
+ function server(config) {
1543
+ validateServerDefinition(config);
656
1544
  return config;
657
1545
  }
658
1546
  export {
@@ -680,6 +1568,7 @@ export {
680
1568
  hostname,
681
1569
  isBooleanEnvironmentMetaEntry,
682
1570
  isEnvironmentMetaEntry,
1571
+ isFirstRun,
683
1572
  isLazyEnvironmentMetaEntry,
684
1573
  isNumberEnvironmentMetaEntry,
685
1574
  isSshdPortMetaEntry,
@@ -696,6 +1585,7 @@ export {
696
1585
  quadlet,
697
1586
  recipe,
698
1587
  releaseUpgrade,
1588
+ resolveEnvironment,
699
1589
  rsync,
700
1590
  script,
701
1591
  server,
@@ -705,11 +1595,13 @@ export {
705
1595
  ssh,
706
1596
  sshd,
707
1597
  sshdPortMeta,
1598
+ swap,
708
1599
  sysctl,
709
1600
  system,
710
1601
  systemHostMeta,
711
1602
  systemRebootMeta,
712
1603
  systemd,
1604
+ timer,
713
1605
  ufw,
714
1606
  user,
715
1607
  when