paratix 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,620 @@
1
+ import {
2
+ printCommandFailure,
3
+ printModuleResult,
4
+ printRecipeHeader,
5
+ runSignalModules,
6
+ startModuleSpinner,
7
+ withRecipeOutputScope
8
+ } from "./chunk-DUIGEB2J.js";
9
+ import {
10
+ NEEDS_APPLY,
11
+ apt,
12
+ archive,
13
+ command,
14
+ compose,
15
+ cron,
16
+ download,
17
+ failed,
18
+ failedCommand,
19
+ file,
20
+ git,
21
+ group,
22
+ hostname,
23
+ mount,
24
+ net,
25
+ op,
26
+ pkg,
27
+ releaseUpgrade,
28
+ rsync,
29
+ script,
30
+ service,
31
+ ssh,
32
+ sshd,
33
+ sysctl,
34
+ system,
35
+ systemd,
36
+ ufw,
37
+ user
38
+ } from "./chunk-ULJMW23T.js";
39
+ import {
40
+ CommandError,
41
+ assertValidModuleMetaEntries,
42
+ assertValidModuleMetaEntry,
43
+ diffEnvironmentToMetaEntries,
44
+ environmentMeta,
45
+ environmentToMetaEntries,
46
+ isBooleanEnvironmentMetaEntry,
47
+ isEnvironmentMetaEntry,
48
+ isLazyEnvironmentMetaEntry,
49
+ isNumberEnvironmentMetaEntry,
50
+ isSshdPortMetaEntry,
51
+ isStringEnvironmentMetaEntry,
52
+ isSystemHostMetaEntry,
53
+ isSystemRebootMetaEntry,
54
+ mergeEnvironmentFromMeta,
55
+ meta,
56
+ shellQuote,
57
+ sshdPortMeta,
58
+ systemHostMeta,
59
+ systemRebootMeta,
60
+ validateSshConfig
61
+ } from "./chunk-G3BMCQKU.js";
62
+
63
+ // src/builtins.ts
64
+ function assert(condition, message) {
65
+ return {
66
+ _dryRunBlocker: true,
67
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
68
+ async apply(_ssh, environment) {
69
+ if (condition(environment)) {
70
+ return { status: "ok" };
71
+ }
72
+ return failed(`[assert] ${message}`);
73
+ },
74
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
75
+ async check(_ssh, environment) {
76
+ return condition(environment) ? "ok" : NEEDS_APPLY;
77
+ },
78
+ name: `assert: ${message}`
79
+ };
80
+ }
81
+ function debug(message) {
82
+ return {
83
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
84
+ async apply() {
85
+ console.log(` [debug] ${message}`);
86
+ return { status: "ok" };
87
+ },
88
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
89
+ async check() {
90
+ return NEEDS_APPLY;
91
+ },
92
+ name: `debug: ${message}`
93
+ };
94
+ }
95
+ function fail(message) {
96
+ return {
97
+ _dryRunBlocker: true,
98
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
99
+ async apply() {
100
+ return failed(`[fail] ${message}`);
101
+ },
102
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
103
+ async check() {
104
+ return NEEDS_APPLY;
105
+ },
106
+ name: `fail: ${message}`
107
+ };
108
+ }
109
+ function pause(message) {
110
+ return {
111
+ async apply() {
112
+ const promptText = message ?? "Press enter to continue...";
113
+ process.stdout.write(` [pause] ${promptText} `);
114
+ await new Promise((resolve) => {
115
+ process.stdin.once("data", () => {
116
+ process.stdin.pause();
117
+ resolve();
118
+ });
119
+ });
120
+ return { status: "ok" };
121
+ },
122
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
123
+ async check() {
124
+ return NEEDS_APPLY;
125
+ },
126
+ name: message == null ? "pause" : `pause: ${message}`
127
+ };
128
+ }
129
+ function isFirstRunEnabled(environment) {
130
+ return environment.PARATIX_FIRST_RUN === "true" || environment.FIRST_RUN === true;
131
+ }
132
+ var firstRun = {
133
+ /**
134
+ * Stop the current run successfully when Paratix was invoked with `--first-run`.
135
+ * Useful as an explicit stage boundary in scaffolded playbooks.
136
+ *
137
+ * @param message - Optional note shown in the module name.
138
+ * @returns A local module that stops the run only during first-run execution.
139
+ */
140
+ stop(message) {
141
+ const moduleName = message == null ? "firstRun.stop" : `firstRun.stop: ${message}`;
142
+ return {
143
+ _dryRunBlocker: true,
144
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
145
+ async apply(_ssh, environment) {
146
+ if (!isFirstRunEnabled(environment)) {
147
+ return { status: "ok" };
148
+ }
149
+ return {
150
+ _dryRunDetail: "(first-run stop)",
151
+ _stopRun: true,
152
+ status: "ok"
153
+ };
154
+ },
155
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
156
+ async check(_ssh, environment) {
157
+ return isFirstRunEnabled(environment) ? NEEDS_APPLY : "ok";
158
+ },
159
+ local: true,
160
+ name: moduleName
161
+ };
162
+ }
163
+ };
164
+ var signals = {
165
+ /**
166
+ * Flush all currently pending signals for the active scope.
167
+ * Signals remain scope-local:
168
+ * - in a recipe, this flushes that recipe's signals
169
+ * - at top level, this flushes `server(...).signals`
170
+ *
171
+ * @param message - Optional note shown in the module name.
172
+ * @returns A local control module that requests an immediate signal flush.
173
+ */
174
+ flush(message) {
175
+ const moduleName = message == null ? "signals.flush" : `signals.flush: ${message}`;
176
+ return {
177
+ _dryRunBlocker: true,
178
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
179
+ async apply() {
180
+ return {
181
+ _dryRunDetail: "(dry-run, pending signals not executed)",
182
+ _flushSignals: true,
183
+ status: "ok"
184
+ };
185
+ },
186
+ // eslint-disable-next-line @typescript-eslint/require-await -- Interface requires async
187
+ async check() {
188
+ return NEEDS_APPLY;
189
+ },
190
+ local: true,
191
+ name: moduleName
192
+ };
193
+ }
194
+ };
195
+ async function applyConditionalModules(parameters) {
196
+ const { dryRun = false, modules, ssh: ssh2 } = parameters;
197
+ let state = createConditionalApplyState(parameters.environment);
198
+ for (const currentModule of modules) {
199
+ const checkResult = await currentModule.check(ssh2, state.environment);
200
+ if (checkResult === "ok") continue;
201
+ if (!shouldExecuteConditionalApply(currentModule, dryRun)) {
202
+ state = markConditionalApplyChanged(state);
203
+ continue;
204
+ }
205
+ const result = await executeConditionalApply({
206
+ dryRun,
207
+ environment: state.environment,
208
+ module: currentModule,
209
+ ssh: ssh2
210
+ });
211
+ if (result.status === "failed") return result;
212
+ state = await mergeConditionalApplyState(state, result);
213
+ if (state.stopRun === true) break;
214
+ }
215
+ return {
216
+ _flushSignals: state.flushSignals,
217
+ _stopRun: state.stopRun,
218
+ meta: state.meta.length === 0 ? void 0 : state.meta,
219
+ status: state.status
220
+ };
221
+ }
222
+ function shouldExecuteConditionalApply(module, dryRun) {
223
+ if (!dryRun) return true;
224
+ return module._applyDryRun != null || module._dryRunBlocker === true || module._dryRunMetaProducer === true;
225
+ }
226
+ function createConditionalApplyState(environment) {
227
+ return { environment: { ...environment }, meta: [], status: "ok" };
228
+ }
229
+ function markConditionalApplyChanged(state) {
230
+ return { ...state, status: "changed" };
231
+ }
232
+ async function executeConditionalApply(parameters) {
233
+ const { dryRun, environment, module, ssh: ssh2 } = parameters;
234
+ if (dryRun && module._applyDryRun != null) {
235
+ return module._applyDryRun(ssh2, environment);
236
+ }
237
+ return module.apply(ssh2, environment);
238
+ }
239
+ function whenNeedsDryRunApply(modules) {
240
+ return modules.some((module) => shouldExecuteConditionalDryRun(module));
241
+ }
242
+ function shouldExecuteConditionalDryRun(module) {
243
+ return module._applyDryRun != null || module._dryRunBlocker === true || module._dryRunMetaProducer === true;
244
+ }
245
+ async function mergeConditionalApplyState(state, result) {
246
+ const environment = await mergeEnvironmentFromMeta(state.environment, result.meta);
247
+ return {
248
+ environment,
249
+ flushSignals: result._flushSignals === true ? true : state.flushSignals,
250
+ meta: result.meta == null ? state.meta : [...state.meta, ...result.meta],
251
+ status: result.status === "changed" ? "changed" : state.status,
252
+ stopRun: result._stopRun === true ? true : state.stopRun
253
+ };
254
+ }
255
+ function createWhenDryRunApply(condition, modules, needsDryRunApply) {
256
+ if (!needsDryRunApply) return void 0;
257
+ return async (ssh2, environment) => {
258
+ if (!condition(environment)) {
259
+ return { status: "skipped" };
260
+ }
261
+ return applyConditionalModules({ dryRun: true, environment, modules, ssh: ssh2 });
262
+ };
263
+ }
264
+ function when(condition, ...modules) {
265
+ const needsDryRunApply = whenNeedsDryRunApply(modules);
266
+ const applyDryRun = createWhenDryRunApply(condition, modules, needsDryRunApply);
267
+ return {
268
+ ...modules.some((module) => module._dryRunBlocker === true) ? { _dryRunBlocker: true } : {},
269
+ ...modules.some((module) => module._dryRunMetaProducer === true) ? { _dryRunMetaProducer: true } : {},
270
+ ...applyDryRun == null ? {} : { _applyDryRun: applyDryRun },
271
+ async apply(ssh2, environment) {
272
+ if (!condition(environment)) {
273
+ return { status: "skipped" };
274
+ }
275
+ return applyConditionalModules({ environment, modules, ssh: ssh2 });
276
+ },
277
+ async check(ssh2, environment) {
278
+ if (!condition(environment)) {
279
+ return "ok";
280
+ }
281
+ const currentEnvironment = { ...environment };
282
+ for (const currentModule of modules) {
283
+ const result = await currentModule.check(ssh2, currentEnvironment);
284
+ if (result === NEEDS_APPLY) {
285
+ return NEEDS_APPLY;
286
+ }
287
+ }
288
+ return "ok";
289
+ },
290
+ name: `when: conditional (${modules.length} modules)`
291
+ };
292
+ }
293
+
294
+ // src/recipe.ts
295
+ var INTERRUPTED_BEFORE_APPLY = /* @__PURE__ */ Symbol("recipe-interrupted-before-apply");
296
+ function applyRecipeStepToState(state, step, preserveControlPlaneMeta) {
297
+ let stepMeta;
298
+ if (step.meta == null) {
299
+ stepMeta = void 0;
300
+ } else if (preserveControlPlaneMeta) {
301
+ stepMeta = step.meta;
302
+ } else {
303
+ stepMeta = step.meta.filter(isEnvironmentMetaEntry);
304
+ }
305
+ const nextMeta = stepMeta == null ? state.meta ?? [] : [...state.meta ?? [], ...stepMeta];
306
+ let nextStatus = state.status;
307
+ if (step.status === "failed") nextStatus = "failed";
308
+ else if (step.status === "changed") nextStatus = "changed";
309
+ return {
310
+ env: step.env,
311
+ meta: nextMeta,
312
+ signalsPending: step.status === "changed" ? true : state.signalsPending,
313
+ status: nextStatus,
314
+ stopRun: step._stopRun === true ? true : state.stopRun
315
+ };
316
+ }
317
+ async function executeOneModule(parameters) {
318
+ const { currentEnvironment, ssh: ssh2, targetModule } = parameters;
319
+ const verbose = parameters.verbose ?? false;
320
+ const connection = targetModule.local === true ? null : ssh2;
321
+ const checkResult = await checkRecipeChild(targetModule, connection, currentEnvironment);
322
+ if (checkResult === "ok") {
323
+ printModuleResult(targetModule.name, "ok");
324
+ return null;
325
+ }
326
+ if ((parameters.shutdownSignal?.() ?? null) != null) {
327
+ return INTERRUPTED_BEFORE_APPLY;
328
+ }
329
+ const result = await targetModule.apply(connection, currentEnvironment);
330
+ printModuleResult(targetModule.name, result.status);
331
+ if (result.status === "failed" && result.error != null) {
332
+ printCommandFailure(result.error, verbose);
333
+ }
334
+ const environment = await mergeEnvironmentFromMeta(currentEnvironment, result.meta);
335
+ return {
336
+ _flushSignals: result._flushSignals,
337
+ _stopRun: result._stopRun,
338
+ env: environment,
339
+ meta: result.meta,
340
+ status: result.status
341
+ };
342
+ }
343
+ async function checkRecipeChild(targetModule, connection, currentEnvironment) {
344
+ startModuleSpinner(targetModule.name);
345
+ return targetModule.check(connection, currentEnvironment);
346
+ }
347
+ async function applyExecutedRecipeStep(parameters) {
348
+ if (parameters.step == null) return parameters.state;
349
+ if (parameters.step === INTERRUPTED_BEFORE_APPLY) return null;
350
+ if (parameters.onChildStep != null) {
351
+ await parameters.onChildStep(parameters.step);
352
+ }
353
+ return applyRecipeStepToState(
354
+ parameters.state,
355
+ parameters.step,
356
+ parameters.preserveControlPlaneMeta
357
+ );
358
+ }
359
+ function failedRecipeState(environment) {
360
+ return {
361
+ env: environment,
362
+ meta: void 0,
363
+ signalsPending: false,
364
+ status: "failed"
365
+ };
366
+ }
367
+ function annotateRecipeChildError(moduleName, error) {
368
+ const prefix = `[${moduleName}] `;
369
+ if (error instanceof CommandError) {
370
+ return new CommandError(`${prefix}${error.message}`, error.fullStdout, error.fullStderr);
371
+ }
372
+ if (error instanceof Error) {
373
+ return new Error(`${prefix}${error.message}`);
374
+ }
375
+ return new Error(`${prefix}${String(error)}`);
376
+ }
377
+ async function executeRecipeChildStep(parameters) {
378
+ try {
379
+ return { kind: "step", step: await executeOneModule(parameters) };
380
+ } catch (error) {
381
+ if (parameters.shutdownSignal() != null) {
382
+ return { kind: "step", step: INTERRUPTED_BEFORE_APPLY };
383
+ }
384
+ printModuleResult(parameters.targetModule.name, "failed");
385
+ printCommandFailure(error, parameters.verbose);
386
+ return { kind: "failed", state: failedRecipeState(parameters.currentEnvironment) };
387
+ }
388
+ }
389
+ async function processRecipeStep(parameters) {
390
+ if (parameters.step.kind === "failed") {
391
+ return { kind: "break", state: parameters.step.state };
392
+ }
393
+ const nextState = await applyExecutedRecipeStep({
394
+ onChildStep: parameters.onChildStep,
395
+ preserveControlPlaneMeta: parameters.preserveControlPlaneMeta,
396
+ state: parameters.state,
397
+ step: parameters.step.step
398
+ });
399
+ if (nextState == null) {
400
+ return { kind: "break", state: parameters.state };
401
+ }
402
+ let state = nextState;
403
+ if (shouldFlushRecipeSignals(parameters.step.step, state, parameters.signals)) {
404
+ const signalStatus = await flushPendingRecipeSignals({
405
+ environment: state.env,
406
+ onSignalStep: parameters.onSignalStep,
407
+ shutdownSignal: parameters.shutdownSignal,
408
+ signalHooks: parameters.signalHooks,
409
+ signals: parameters.signals,
410
+ ssh: parameters.ssh,
411
+ verbose: parameters.verbose
412
+ });
413
+ state = applyRecipeSignalStatus(state, signalStatus);
414
+ }
415
+ if (state.status === "failed" || state.stopRun === true) {
416
+ return { kind: "break", state };
417
+ }
418
+ return { kind: "continue", state };
419
+ }
420
+ async function executeModules(modules, ssh2, parameters) {
421
+ const onChildStep = parameters.onChildStep;
422
+ const preserveControlPlaneMeta = onChildStep == null;
423
+ const shutdownSignal = parameters.shutdownSignal ?? (() => null);
424
+ const verbose = parameters.verbose ?? false;
425
+ let state = {
426
+ env: { ...parameters.environment },
427
+ meta: void 0,
428
+ signalsPending: false,
429
+ status: "ok"
430
+ };
431
+ for (const currentModule of modules) {
432
+ if (shutdownSignal() != null) break;
433
+ const step = await executeRecipeChildStep({
434
+ currentEnvironment: state.env,
435
+ shutdownSignal,
436
+ ssh: ssh2,
437
+ targetModule: currentModule,
438
+ verbose
439
+ });
440
+ const processedStep = await processRecipeStep({
441
+ onChildStep,
442
+ onSignalStep: parameters.onSignalStep,
443
+ preserveControlPlaneMeta,
444
+ shutdownSignal,
445
+ signalHooks: parameters.signalHooks,
446
+ signals: parameters.signals,
447
+ ssh: ssh2,
448
+ state,
449
+ step,
450
+ verbose
451
+ });
452
+ state = processedStep.state;
453
+ if (processedStep.kind === "break") return state;
454
+ }
455
+ return state;
456
+ }
457
+ async function triggerSignals(parameters) {
458
+ return runSignalModules({
459
+ environment: parameters.environment,
460
+ hooks: parameters.signalHooks,
461
+ onSignalStep: parameters.onSignalStep,
462
+ shutdownSignal: parameters.shutdownSignal,
463
+ signals: parameters.signals,
464
+ ssh: parameters.ssh,
465
+ verbose: parameters.verbose
466
+ });
467
+ }
468
+ function shouldFlushRecipeSignals(step, state, signals2) {
469
+ return step != null && step !== INTERRUPTED_BEFORE_APPLY && step._flushSignals === true && state.signalsPending && signals2 != null;
470
+ }
471
+ function applyRecipeSignalStatus(state, signalStatus) {
472
+ return {
473
+ ...state,
474
+ signalsPending: false,
475
+ status: signalStatus === "failed" ? "failed" : state.status
476
+ };
477
+ }
478
+ async function flushPendingRecipeSignals(parameters) {
479
+ return triggerSignals(parameters);
480
+ }
481
+ async function applyRecipe(parameters) {
482
+ return withRecipeOutputScope(async () => {
483
+ const shutdownSignal = parameters.options?.shutdownSignal;
484
+ const verbose = parameters.options?.verbose ?? false;
485
+ printRecipeHeader(parameters.name);
486
+ const state = await executeModules(parameters.modules, parameters.ssh, {
487
+ environment: parameters.environment,
488
+ onChildStep: parameters.options?.onChildStep,
489
+ onSignalStep: parameters.options?.onSignalStep,
490
+ shutdownSignal,
491
+ signalHooks: parameters.options?.signalHooks,
492
+ signals: parameters.signals,
493
+ verbose
494
+ });
495
+ if (shouldRunRecipeSignalsAtEnd(state, parameters.signals)) {
496
+ state.status = await triggerSignals({
497
+ environment: state.env,
498
+ onSignalStep: parameters.options?.onSignalStep,
499
+ shutdownSignal,
500
+ signalHooks: parameters.options?.signalHooks,
501
+ signals: parameters.signals,
502
+ ssh: parameters.ssh,
503
+ verbose
504
+ });
505
+ }
506
+ return {
507
+ _stopRun: state.stopRun,
508
+ meta: state.meta,
509
+ status: state.status
510
+ };
511
+ });
512
+ }
513
+ function shouldRunRecipeSignalsAtEnd(state, signals2) {
514
+ return state.signalsPending && state.status === "changed" && signals2 != null;
515
+ }
516
+ function recipe(name, modules, options) {
517
+ return {
518
+ _isRecipe: true,
519
+ _modules: modules,
520
+ _signals: options?.signals,
521
+ async apply(ssh2, environment, parameters) {
522
+ return applyRecipe({
523
+ environment,
524
+ modules,
525
+ name,
526
+ options: parameters,
527
+ signals: options?.signals,
528
+ ssh: ssh2
529
+ });
530
+ },
531
+ async check(ssh2, environment) {
532
+ for (const childModule of modules) {
533
+ const connection = childModule.local === true ? null : ssh2;
534
+ let result;
535
+ try {
536
+ result = await childModule.check(connection, environment);
537
+ } catch (error) {
538
+ throw annotateRecipeChildError(childModule.name, error);
539
+ }
540
+ if (result === NEEDS_APPLY) return NEEDS_APPLY;
541
+ }
542
+ return "ok";
543
+ },
544
+ name
545
+ };
546
+ }
547
+
548
+ // src/server.ts
549
+ function server(config) {
550
+ if (config.host.length === 0) {
551
+ throw new Error("ServerDefinition: host is required");
552
+ }
553
+ if (config.name.length === 0) {
554
+ throw new Error("ServerDefinition: name is required");
555
+ }
556
+ validateSshConfig(config.ssh);
557
+ if (config.run.length === 0) {
558
+ throw new Error("ServerDefinition: run must contain at least one module");
559
+ }
560
+ return config;
561
+ }
562
+ export {
563
+ NEEDS_APPLY,
564
+ apt,
565
+ archive,
566
+ assert,
567
+ assertValidModuleMetaEntries,
568
+ assertValidModuleMetaEntry,
569
+ command,
570
+ compose,
571
+ cron,
572
+ debug,
573
+ diffEnvironmentToMetaEntries,
574
+ download,
575
+ environmentMeta,
576
+ environmentToMetaEntries,
577
+ fail,
578
+ failed,
579
+ failedCommand,
580
+ file,
581
+ firstRun,
582
+ git,
583
+ group,
584
+ hostname,
585
+ isBooleanEnvironmentMetaEntry,
586
+ isEnvironmentMetaEntry,
587
+ isLazyEnvironmentMetaEntry,
588
+ isNumberEnvironmentMetaEntry,
589
+ isSshdPortMetaEntry,
590
+ isStringEnvironmentMetaEntry,
591
+ isSystemHostMetaEntry,
592
+ isSystemRebootMetaEntry,
593
+ mergeEnvironmentFromMeta,
594
+ meta,
595
+ mount,
596
+ net,
597
+ op,
598
+ pkg as package,
599
+ pause,
600
+ recipe,
601
+ releaseUpgrade,
602
+ rsync,
603
+ script,
604
+ server,
605
+ service,
606
+ shellQuote,
607
+ signals,
608
+ ssh,
609
+ sshd,
610
+ sshdPortMeta,
611
+ sysctl,
612
+ system,
613
+ systemHostMeta,
614
+ systemRebootMeta,
615
+ systemd,
616
+ ufw,
617
+ user,
618
+ when
619
+ };
620
+ //# sourceMappingURL=index.js.map