paratix 0.0.1 → 0.1.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/README.md +81 -3
- package/dist/chunk-DUIGEB2J.js +439 -0
- package/dist/chunk-DUIGEB2J.js.map +1 -0
- package/dist/chunk-G3BMCQKU.js +1706 -0
- package/dist/chunk-G3BMCQKU.js.map +1 -0
- package/dist/chunk-ULJMW23T.js +4961 -0
- package/dist/chunk-ULJMW23T.js.map +1 -0
- package/dist/cli.d.ts +62 -0
- package/dist/cli.js +779 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +201 -0
- package/dist/index.js +620 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/index.d.ts +1332 -0
- package/dist/modules/index.js +56 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/types-BPzPHfax.d.ts +252 -0
- package/llm-guide.md +607 -0
- package/package.json +35 -2
package/dist/cli.js
ADDED
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
import {
|
|
2
|
+
printCliHeader,
|
|
3
|
+
printCommandFailure,
|
|
4
|
+
printModuleResult,
|
|
5
|
+
printRecipeHeader,
|
|
6
|
+
printRunContext,
|
|
7
|
+
printSummary,
|
|
8
|
+
runSignalModules,
|
|
9
|
+
startModuleSpinner,
|
|
10
|
+
withRecipeOutputScope
|
|
11
|
+
} from "./chunk-DUIGEB2J.js";
|
|
12
|
+
import {
|
|
13
|
+
SshConnectionImpl,
|
|
14
|
+
assertValidModuleMetaEntries,
|
|
15
|
+
collectSshConfigErrors,
|
|
16
|
+
isSshdPortMetaEntry,
|
|
17
|
+
isSystemHostMetaEntry,
|
|
18
|
+
isSystemRebootMetaEntry,
|
|
19
|
+
loadDotEnvironment,
|
|
20
|
+
mergeEnvironment,
|
|
21
|
+
mergeEnvironmentFromMeta
|
|
22
|
+
} from "./chunk-G3BMCQKU.js";
|
|
23
|
+
|
|
24
|
+
// src/cli.ts
|
|
25
|
+
import { Command } from "commander";
|
|
26
|
+
import { realpathSync } from "fs";
|
|
27
|
+
import { resolve } from "path";
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
29
|
+
import { inspect } from "util";
|
|
30
|
+
import pc from "picocolors";
|
|
31
|
+
|
|
32
|
+
// src/dryRunRecipe.ts
|
|
33
|
+
function shouldExecuteApplyDuringDryRun(module) {
|
|
34
|
+
return module._applyDryRun != null || module._dryRunBlocker === true || module._dryRunMetaProducer === true;
|
|
35
|
+
}
|
|
36
|
+
async function executeDryRunBlockingModule(parameters) {
|
|
37
|
+
const { childModule, connection, environment, verbose } = parameters;
|
|
38
|
+
startModuleSpinner(childModule.name);
|
|
39
|
+
const result = childModule._applyDryRun == null ? await childModule.apply(connection, environment) : await childModule._applyDryRun(connection, environment);
|
|
40
|
+
const nextEnvironment = result.meta == null ? environment : await mergeEnvironmentFromMeta(environment, result.meta);
|
|
41
|
+
printModuleResult(childModule.name, result.status, result._dryRunDetail ?? "(dry-run)");
|
|
42
|
+
if (result.status === "failed" && result.error != null) {
|
|
43
|
+
printCommandFailure(result.error, verbose);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
env: nextEnvironment,
|
|
47
|
+
shouldBreak: result.status === "failed" || result._stopRun === true,
|
|
48
|
+
status: result.status,
|
|
49
|
+
stopRun: result._stopRun
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function executeDryRunChildModule(parameters) {
|
|
53
|
+
const { childModule, environment, ssh, verbose } = parameters;
|
|
54
|
+
const connection = childModule.local === true ? null : ssh;
|
|
55
|
+
startModuleSpinner(childModule.name);
|
|
56
|
+
const checkResult = await childModule.check(connection, environment);
|
|
57
|
+
if (checkResult !== "ok" && shouldExecuteApplyDuringDryRun(childModule)) {
|
|
58
|
+
return executeDryRunBlockingModule({ childModule, connection, environment, verbose });
|
|
59
|
+
}
|
|
60
|
+
const status = checkResult === "ok" ? "ok" : "changed";
|
|
61
|
+
const suffix = checkResult === "ok" ? void 0 : "(dry-run)";
|
|
62
|
+
printModuleResult(childModule.name, status, suffix);
|
|
63
|
+
return { env: environment, shouldBreak: false, status };
|
|
64
|
+
}
|
|
65
|
+
async function dryRunRecipeModule(parameters) {
|
|
66
|
+
return withRecipeOutputScope(async () => {
|
|
67
|
+
const { environment, recipeModule, ssh } = parameters;
|
|
68
|
+
printRecipeHeader(recipeModule.name);
|
|
69
|
+
let aggregatedStatus = "ok";
|
|
70
|
+
let currentEnvironment = environment;
|
|
71
|
+
const verbose = parameters.options?.verbose ?? false;
|
|
72
|
+
for (const childModule of recipeModule._modules) {
|
|
73
|
+
const result = await executeDryRunChildModule({
|
|
74
|
+
childModule,
|
|
75
|
+
environment: currentEnvironment,
|
|
76
|
+
ssh,
|
|
77
|
+
verbose
|
|
78
|
+
});
|
|
79
|
+
if (result.shouldBreak) return result;
|
|
80
|
+
currentEnvironment = result.env;
|
|
81
|
+
if (result.status === "changed") aggregatedStatus = "changed";
|
|
82
|
+
}
|
|
83
|
+
return { env: currentEnvironment, shouldBreak: false, status: aggregatedStatus };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/runnerHelpers.ts
|
|
88
|
+
var SIGNAL_EXIT_BASE = 128;
|
|
89
|
+
var SIGTERM_NUMBER = 15;
|
|
90
|
+
var SIGINT_NUMBER = 2;
|
|
91
|
+
function signalExitCode(signal) {
|
|
92
|
+
return SIGNAL_EXIT_BASE + (signal === "SIGTERM" ? SIGTERM_NUMBER : SIGINT_NUMBER);
|
|
93
|
+
}
|
|
94
|
+
function resolveExitCode(shutdownSignal, stats) {
|
|
95
|
+
if (shutdownSignal != null) {
|
|
96
|
+
process.exitCode = signalExitCode(shutdownSignal);
|
|
97
|
+
} else if (stats.failed > 0) {
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/runner.ts
|
|
103
|
+
function setupShutdownHandlers() {
|
|
104
|
+
let receivedSignal = null;
|
|
105
|
+
let ssh = null;
|
|
106
|
+
const promptAbortController = new AbortController();
|
|
107
|
+
const handleShutdownSignal = (signal) => {
|
|
108
|
+
if (receivedSignal != null) {
|
|
109
|
+
process.exit(signalExitCode(signal));
|
|
110
|
+
}
|
|
111
|
+
receivedSignal = signal;
|
|
112
|
+
promptAbortController.abort(new Error(`Terminal prompt interrupted by ${signal}`));
|
|
113
|
+
console.error(`
|
|
114
|
+
Received ${signal}, shutting down\u2026`);
|
|
115
|
+
ssh?.disconnect();
|
|
116
|
+
};
|
|
117
|
+
process.on("SIGINT", handleShutdownSignal);
|
|
118
|
+
process.on("SIGTERM", handleShutdownSignal);
|
|
119
|
+
return {
|
|
120
|
+
handleShutdownSignal,
|
|
121
|
+
promptAbortSignal: promptAbortController.signal,
|
|
122
|
+
setSsh: (connection) => {
|
|
123
|
+
ssh = connection;
|
|
124
|
+
},
|
|
125
|
+
shutdownSignal: () => receivedSignal
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
var RunStats = class {
|
|
129
|
+
changed = 0;
|
|
130
|
+
failed = 0;
|
|
131
|
+
ok = 0;
|
|
132
|
+
signals = 0;
|
|
133
|
+
skipped = 0;
|
|
134
|
+
incrementSignals() {
|
|
135
|
+
this.signals++;
|
|
136
|
+
}
|
|
137
|
+
update(status) {
|
|
138
|
+
switch (status) {
|
|
139
|
+
case "changed": {
|
|
140
|
+
this.changed++;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "failed": {
|
|
144
|
+
this.failed++;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "ok": {
|
|
148
|
+
this.ok++;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "skipped": {
|
|
152
|
+
this.skipped++;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
function interruptedStepResult(environment) {
|
|
159
|
+
return { env: environment, shouldBreak: true };
|
|
160
|
+
}
|
|
161
|
+
function shouldBreakAfterResult(result) {
|
|
162
|
+
return result.status === "failed" || result._stopRun === true;
|
|
163
|
+
}
|
|
164
|
+
function interruptedBeforeApply(environment, shutdownSignal) {
|
|
165
|
+
if (shutdownSignal() == null) return void 0;
|
|
166
|
+
return interruptedStepResult(environment);
|
|
167
|
+
}
|
|
168
|
+
async function applyCheckedModule(parameters) {
|
|
169
|
+
const interrupted = interruptedBeforeApply(
|
|
170
|
+
parameters.currentEnvironment,
|
|
171
|
+
parameters.shutdownSignal
|
|
172
|
+
);
|
|
173
|
+
if (interrupted != null) return interrupted;
|
|
174
|
+
return applyModule({
|
|
175
|
+
currentEnvironment: parameters.currentEnvironment,
|
|
176
|
+
dryRun: parameters.dryRun,
|
|
177
|
+
ssh: parameters.ssh,
|
|
178
|
+
targetModule: parameters.targetModule,
|
|
179
|
+
verbose: parameters.verbose
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function shouldExecuteApplyDuringDryRun2(module) {
|
|
183
|
+
return module._applyDryRun != null || module._dryRunBlocker === true || module._dryRunMetaProducer === true;
|
|
184
|
+
}
|
|
185
|
+
function handleCaughtStepError(parameters) {
|
|
186
|
+
if (parameters.shutdownSignal() != null) {
|
|
187
|
+
return interruptedStepResult(parameters.environment);
|
|
188
|
+
}
|
|
189
|
+
printModuleResult(parameters.moduleName, "failed");
|
|
190
|
+
printCommandFailure(parameters.error, parameters.verbose);
|
|
191
|
+
return { env: parameters.environment, shouldBreak: true, status: "failed" };
|
|
192
|
+
}
|
|
193
|
+
function isRecipe(target) {
|
|
194
|
+
return "_isRecipe" in target && target._isRecipe;
|
|
195
|
+
}
|
|
196
|
+
async function initializeEnvironment(options, definition) {
|
|
197
|
+
const dotEnvironment = options.envFile == null ? void 0 : await loadDotEnvironment(options.envFile);
|
|
198
|
+
return mergeEnvironment({}, dotEnvironment, definition.env, options.envOverrides);
|
|
199
|
+
}
|
|
200
|
+
async function handlePortChange(ssh, metaEntries) {
|
|
201
|
+
const portEntry = metaEntries?.find((entry) => isSshdPortMetaEntry(entry));
|
|
202
|
+
if (portEntry == null) return;
|
|
203
|
+
const newPort = portEntry.port;
|
|
204
|
+
ssh.addPort(newPort);
|
|
205
|
+
if (metaEntries?.some((entry) => isSystemRebootMetaEntry(entry)) ?? false) return;
|
|
206
|
+
try {
|
|
207
|
+
await ssh.reconnect();
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error(
|
|
210
|
+
`Failed to reconnect on port ${newPort} after port change: ${String(error)}. Verify that port ${newPort} is allowed by the server's firewall rules.`
|
|
211
|
+
);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function handleReboot(ssh, metaEntries) {
|
|
216
|
+
if (!(metaEntries?.some((entry) => isSystemRebootMetaEntry(entry)) ?? false)) return;
|
|
217
|
+
const hostEntry = metaEntries?.find((entry) => isSystemHostMetaEntry(entry));
|
|
218
|
+
if (hostEntry != null) {
|
|
219
|
+
ssh.updateHost(hostEntry.host);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
await ssh.reconnect();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(`Failed to reconnect after reboot: ${String(error)}`);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function applyRunnerControlPlaneMeta(ssh, step) {
|
|
229
|
+
if (step.meta == null) return;
|
|
230
|
+
assertValidModuleMetaEntries(step.meta);
|
|
231
|
+
await handlePortChange(ssh, step.meta);
|
|
232
|
+
await handleReboot(ssh, step.meta);
|
|
233
|
+
}
|
|
234
|
+
async function handleMetaAndBuildResult(ssh, environment, result) {
|
|
235
|
+
let currentEnvironment = environment;
|
|
236
|
+
if (result.meta != null) {
|
|
237
|
+
currentEnvironment = await mergeEnvironmentFromMeta(currentEnvironment, result.meta);
|
|
238
|
+
await applyRunnerControlPlaneMeta(ssh, { meta: result.meta });
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
env: currentEnvironment,
|
|
242
|
+
flushSignals: result._flushSignals,
|
|
243
|
+
shouldBreak: shouldBreakAfterResult(result),
|
|
244
|
+
status: result.status,
|
|
245
|
+
stopRun: result._stopRun
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async function runDryRunRecipeModule(recipeModule, environment, ssh, verbose) {
|
|
249
|
+
return dryRunRecipeModule({
|
|
250
|
+
environment,
|
|
251
|
+
options: { verbose },
|
|
252
|
+
recipeModule,
|
|
253
|
+
ssh
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async function runRecipeModule(recipeModule, environment, ssh, stats, verbose, dryRun, shutdownSignal) {
|
|
257
|
+
try {
|
|
258
|
+
if (dryRun) return await runDryRunRecipeModule(recipeModule, environment, ssh, verbose);
|
|
259
|
+
startModuleSpinner(recipeModule.name);
|
|
260
|
+
const checkResult = await recipeModule.check(ssh, environment);
|
|
261
|
+
if (checkResult === "ok") {
|
|
262
|
+
printModuleResult(recipeModule.name, "ok");
|
|
263
|
+
return { env: environment, shouldBreak: false, status: "ok" };
|
|
264
|
+
}
|
|
265
|
+
const result = await recipeModule.apply(ssh, environment, {
|
|
266
|
+
onChildStep: async (step) => {
|
|
267
|
+
await applyRunnerControlPlaneMeta(ssh, step);
|
|
268
|
+
},
|
|
269
|
+
onSignalStep: async (step) => {
|
|
270
|
+
await applyRunnerControlPlaneMeta(ssh, step);
|
|
271
|
+
},
|
|
272
|
+
shutdownSignal,
|
|
273
|
+
signalHooks: {
|
|
274
|
+
onSignalFinished: (status) => {
|
|
275
|
+
stats.update(status);
|
|
276
|
+
},
|
|
277
|
+
onSignalStarted: () => {
|
|
278
|
+
stats.incrementSignals();
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
verbose
|
|
282
|
+
});
|
|
283
|
+
return await handleMetaAndBuildResult(ssh, environment, result);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return handleCaughtStepError({
|
|
286
|
+
environment,
|
|
287
|
+
error,
|
|
288
|
+
moduleName: recipeModule.name,
|
|
289
|
+
shutdownSignal,
|
|
290
|
+
verbose
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function applyModule(parameters) {
|
|
295
|
+
const { currentEnvironment, dryRun = false, ssh, targetModule, verbose } = parameters;
|
|
296
|
+
const connection = targetModule.local === true ? null : ssh;
|
|
297
|
+
const result = dryRun && targetModule._applyDryRun != null ? await targetModule._applyDryRun(connection, currentEnvironment) : await targetModule.apply(connection, currentEnvironment);
|
|
298
|
+
const stepResult = await handleMetaAndBuildResult(ssh, currentEnvironment, result);
|
|
299
|
+
const detail = dryRun ? result._dryRunDetail ?? "(dry-run)" : void 0;
|
|
300
|
+
printModuleResult(targetModule.name, result.status, detail);
|
|
301
|
+
if (result.status === "failed" && result.error != null) {
|
|
302
|
+
printCommandFailure(result.error, verbose);
|
|
303
|
+
}
|
|
304
|
+
return stepResult;
|
|
305
|
+
}
|
|
306
|
+
async function checkRegularModule(parameters) {
|
|
307
|
+
const { env, ssh, targetModule } = parameters;
|
|
308
|
+
const connection = targetModule.local === true ? null : ssh;
|
|
309
|
+
startModuleSpinner(targetModule.name);
|
|
310
|
+
return targetModule.check(connection, env);
|
|
311
|
+
}
|
|
312
|
+
function buildDryRunChangedResult(environment) {
|
|
313
|
+
return { env: environment, shouldBreak: false, status: "changed" };
|
|
314
|
+
}
|
|
315
|
+
async function runRegularModule(parameters) {
|
|
316
|
+
const { dryRun, env, ssh, targetModule, verbose } = parameters;
|
|
317
|
+
const shutdownSignal = parameters.shutdownSignal;
|
|
318
|
+
try {
|
|
319
|
+
const checkResult = await checkRegularModule({ env, ssh, targetModule });
|
|
320
|
+
if (checkResult === "ok") {
|
|
321
|
+
printModuleResult(targetModule.name, "ok");
|
|
322
|
+
return { env, shouldBreak: false, status: "ok" };
|
|
323
|
+
}
|
|
324
|
+
if (dryRun) {
|
|
325
|
+
if (shouldExecuteApplyDuringDryRun2(targetModule)) {
|
|
326
|
+
return await applyCheckedModule({
|
|
327
|
+
currentEnvironment: env,
|
|
328
|
+
dryRun: true,
|
|
329
|
+
shutdownSignal,
|
|
330
|
+
ssh,
|
|
331
|
+
targetModule,
|
|
332
|
+
verbose
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
printModuleResult(targetModule.name, "changed", "(dry-run)");
|
|
336
|
+
return buildDryRunChangedResult(env);
|
|
337
|
+
}
|
|
338
|
+
return await applyCheckedModule({
|
|
339
|
+
currentEnvironment: env,
|
|
340
|
+
dryRun: false,
|
|
341
|
+
shutdownSignal,
|
|
342
|
+
ssh,
|
|
343
|
+
targetModule,
|
|
344
|
+
verbose
|
|
345
|
+
});
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return handleCaughtStepError({
|
|
348
|
+
environment: env,
|
|
349
|
+
error,
|
|
350
|
+
moduleName: targetModule.name,
|
|
351
|
+
shutdownSignal,
|
|
352
|
+
verbose
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function updateLoopSignalState(input) {
|
|
357
|
+
if (input.result.status == null) return input.currentSignalsPending;
|
|
358
|
+
input.stats.update(input.result.status);
|
|
359
|
+
return input.result.status === "changed" ? true : input.currentSignalsPending;
|
|
360
|
+
}
|
|
361
|
+
function shouldFlushTopLevelSignals(input) {
|
|
362
|
+
return input.stepResult.flushSignals === true && !input.dryRun && input.shutdownSignal() == null && input.signalsPending && input.stats.failed === 0 && input.definitionSignals != null;
|
|
363
|
+
}
|
|
364
|
+
async function flushPendingTopLevelSignals(input) {
|
|
365
|
+
return runSignals({
|
|
366
|
+
env: input.currentEnvironment,
|
|
367
|
+
shutdownSignal: input.shutdownSignal,
|
|
368
|
+
signals: input.definitionSignals,
|
|
369
|
+
ssh: input.ssh,
|
|
370
|
+
stats: input.stats,
|
|
371
|
+
verbose: input.verbose
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
function applyLoopResultToState(state, result, stats) {
|
|
375
|
+
return {
|
|
376
|
+
currentEnvironment: result.env,
|
|
377
|
+
signalsPending: updateLoopSignalState({
|
|
378
|
+
currentSignalsPending: state.signalsPending,
|
|
379
|
+
result,
|
|
380
|
+
stats
|
|
381
|
+
}),
|
|
382
|
+
stopRun: result.stopRun === true ? true : state.stopRun
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
async function flushTopLevelSignalsIfRequested(parameters) {
|
|
386
|
+
if (!shouldFlushTopLevelSignals({
|
|
387
|
+
definitionSignals: parameters.definitionSignals,
|
|
388
|
+
dryRun: parameters.dryRun,
|
|
389
|
+
shutdownSignal: parameters.shutdownSignal,
|
|
390
|
+
signalsPending: parameters.loopState.signalsPending,
|
|
391
|
+
stats: parameters.stats,
|
|
392
|
+
stepResult: parameters.result
|
|
393
|
+
})) {
|
|
394
|
+
return { nextSignalsPending: parameters.loopState.signalsPending, outcome: "continue" };
|
|
395
|
+
}
|
|
396
|
+
const definitionSignals = parameters.definitionSignals;
|
|
397
|
+
if (definitionSignals == null) {
|
|
398
|
+
return { nextSignalsPending: parameters.loopState.signalsPending, outcome: "continue" };
|
|
399
|
+
}
|
|
400
|
+
const signalStatus = await flushPendingTopLevelSignals({
|
|
401
|
+
currentEnvironment: parameters.loopState.currentEnvironment,
|
|
402
|
+
definitionSignals,
|
|
403
|
+
shutdownSignal: parameters.shutdownSignal,
|
|
404
|
+
ssh: parameters.ssh,
|
|
405
|
+
stats: parameters.stats,
|
|
406
|
+
verbose: parameters.verbose
|
|
407
|
+
});
|
|
408
|
+
return {
|
|
409
|
+
nextSignalsPending: false,
|
|
410
|
+
outcome: signalStatus === "failed" ? "break" : "continue"
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
async function createModuleStepPromise(parameters) {
|
|
414
|
+
const { currentEnvironment, currentModule, dryRun, shutdownSignal, ssh, stats, verbose } = parameters;
|
|
415
|
+
return isRecipe(currentModule) ? runRecipeModule(
|
|
416
|
+
currentModule,
|
|
417
|
+
currentEnvironment,
|
|
418
|
+
ssh,
|
|
419
|
+
stats,
|
|
420
|
+
verbose,
|
|
421
|
+
dryRun,
|
|
422
|
+
shutdownSignal
|
|
423
|
+
) : runRegularModule({
|
|
424
|
+
dryRun,
|
|
425
|
+
env: currentEnvironment,
|
|
426
|
+
shutdownSignal,
|
|
427
|
+
ssh,
|
|
428
|
+
targetModule: currentModule,
|
|
429
|
+
verbose
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
async function runModuleLoop(parameters) {
|
|
433
|
+
const { definitionSignals, dryRun, modules, shutdownSignal, ssh, stats, verbose } = parameters;
|
|
434
|
+
const loopState = {
|
|
435
|
+
currentEnvironment: parameters.env,
|
|
436
|
+
signalsPending: false,
|
|
437
|
+
stopRun: void 0
|
|
438
|
+
};
|
|
439
|
+
for (const currentModule of modules) {
|
|
440
|
+
if (shutdownSignal() != null) break;
|
|
441
|
+
const stepPromise = createModuleStepPromise({
|
|
442
|
+
currentEnvironment: loopState.currentEnvironment,
|
|
443
|
+
currentModule,
|
|
444
|
+
dryRun,
|
|
445
|
+
shutdownSignal,
|
|
446
|
+
ssh,
|
|
447
|
+
stats,
|
|
448
|
+
verbose
|
|
449
|
+
});
|
|
450
|
+
const result = await stepPromise;
|
|
451
|
+
Object.assign(loopState, applyLoopResultToState(loopState, result, stats));
|
|
452
|
+
const flushResult = await flushTopLevelSignalsIfRequested({
|
|
453
|
+
definitionSignals,
|
|
454
|
+
dryRun,
|
|
455
|
+
loopState,
|
|
456
|
+
result,
|
|
457
|
+
shutdownSignal,
|
|
458
|
+
ssh,
|
|
459
|
+
stats,
|
|
460
|
+
verbose
|
|
461
|
+
});
|
|
462
|
+
loopState.signalsPending = flushResult.nextSignalsPending;
|
|
463
|
+
if (flushResult.outcome === "break") break;
|
|
464
|
+
if (result.shouldBreak) break;
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
env: loopState.currentEnvironment,
|
|
468
|
+
signalsPending: loopState.signalsPending,
|
|
469
|
+
stopRun: loopState.stopRun
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
async function runSignals(parameters) {
|
|
473
|
+
const { env, shutdownSignal, signals, ssh, stats, verbose } = parameters;
|
|
474
|
+
return runSignalModules({
|
|
475
|
+
environment: env,
|
|
476
|
+
hooks: {
|
|
477
|
+
onSignalFinished: (status) => {
|
|
478
|
+
stats.update(status);
|
|
479
|
+
},
|
|
480
|
+
onSignalStarted: () => {
|
|
481
|
+
stats.incrementSignals();
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
onSignalStep: async (step) => {
|
|
485
|
+
await applyRunnerControlPlaneMeta(ssh, step);
|
|
486
|
+
},
|
|
487
|
+
shutdownSignal,
|
|
488
|
+
signals,
|
|
489
|
+
ssh,
|
|
490
|
+
verbose
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
function throwIfShutdownRequested(shutdownSignal) {
|
|
494
|
+
const signal = shutdownSignal();
|
|
495
|
+
if (signal == null) return;
|
|
496
|
+
throw new Error(`Bootstrap interrupted by ${signal}`);
|
|
497
|
+
}
|
|
498
|
+
async function connectAndRegister(parameters) {
|
|
499
|
+
const { definition, options, promptAbortSignal, setSsh, shutdownSignal } = parameters;
|
|
500
|
+
const sshConfig = {
|
|
501
|
+
...definition.ssh,
|
|
502
|
+
ports: [...definition.ssh.ports],
|
|
503
|
+
...options.reconnectTimeout == null ? {} : { reconnectTimeout: options.reconnectTimeout }
|
|
504
|
+
};
|
|
505
|
+
const ssh = new SshConnectionImpl(definition.host, sshConfig);
|
|
506
|
+
setSsh(ssh);
|
|
507
|
+
throwIfShutdownRequested(shutdownSignal);
|
|
508
|
+
await ssh.connect({ abortSignal: promptAbortSignal });
|
|
509
|
+
throwIfShutdownRequested(shutdownSignal);
|
|
510
|
+
return ssh;
|
|
511
|
+
}
|
|
512
|
+
async function executeRun(parameters) {
|
|
513
|
+
const { definition, dryRun, environment, shutdownSignal, ssh, stats, verbose } = parameters;
|
|
514
|
+
printRecipeHeader(definition.name);
|
|
515
|
+
const loopResult = await runModuleLoop({
|
|
516
|
+
definitionSignals: definition.signals,
|
|
517
|
+
dryRun,
|
|
518
|
+
env: environment,
|
|
519
|
+
modules: definition.run,
|
|
520
|
+
shutdownSignal,
|
|
521
|
+
ssh,
|
|
522
|
+
stats,
|
|
523
|
+
verbose
|
|
524
|
+
});
|
|
525
|
+
const finalEnvironment = loopResult.env;
|
|
526
|
+
if (!dryRun && shutdownSignal() == null && loopResult.signalsPending && stats.failed === 0 && definition.signals != null)
|
|
527
|
+
await runSignals({
|
|
528
|
+
env: finalEnvironment,
|
|
529
|
+
shutdownSignal,
|
|
530
|
+
signals: definition.signals,
|
|
531
|
+
ssh,
|
|
532
|
+
stats,
|
|
533
|
+
verbose
|
|
534
|
+
});
|
|
535
|
+
printSummary(stats);
|
|
536
|
+
}
|
|
537
|
+
function rethrowIfNotShutdown(error, shutdownSignal) {
|
|
538
|
+
if (shutdownSignal() == null) throw error;
|
|
539
|
+
}
|
|
540
|
+
async function runPlaybook(definition, options = {}) {
|
|
541
|
+
const { dryRun = false, verbose = false } = options;
|
|
542
|
+
const environment = await initializeEnvironment(options, definition);
|
|
543
|
+
const { handleShutdownSignal, promptAbortSignal, setSsh, shutdownSignal } = setupShutdownHandlers();
|
|
544
|
+
const stats = new RunStats();
|
|
545
|
+
let ssh;
|
|
546
|
+
printRunContext({
|
|
547
|
+
dryRun,
|
|
548
|
+
host: definition.host,
|
|
549
|
+
name: definition.name,
|
|
550
|
+
ports: definition.ssh.ports
|
|
551
|
+
});
|
|
552
|
+
try {
|
|
553
|
+
ssh = await connectAndRegister({
|
|
554
|
+
definition,
|
|
555
|
+
options,
|
|
556
|
+
promptAbortSignal,
|
|
557
|
+
setSsh,
|
|
558
|
+
shutdownSignal
|
|
559
|
+
});
|
|
560
|
+
await executeRun({ definition, dryRun, environment, shutdownSignal, ssh, stats, verbose });
|
|
561
|
+
} catch (error) {
|
|
562
|
+
rethrowIfNotShutdown(error, shutdownSignal);
|
|
563
|
+
} finally {
|
|
564
|
+
for (const signal of ["SIGINT", "SIGTERM"])
|
|
565
|
+
process.removeListener(signal, handleShutdownSignal);
|
|
566
|
+
ssh?.disconnect();
|
|
567
|
+
}
|
|
568
|
+
resolveExitCode(shutdownSignal(), stats);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/cli.ts
|
|
572
|
+
var SECONDS_TO_MS = 1e3;
|
|
573
|
+
var DEFAULT_RECONNECT_TIMEOUT_SECONDS = 300;
|
|
574
|
+
var ENVIRONMENT_KEY_PATTERN = new RegExp("^[A-Za-z_]\\w*$", "v");
|
|
575
|
+
var FIRST_RUN_ENV_NAME = "PARATIX_FIRST_RUN";
|
|
576
|
+
function isServerDefinitionLike(value) {
|
|
577
|
+
return collectDefinitionErrors(value).length === 0;
|
|
578
|
+
}
|
|
579
|
+
function collectStringErrors(object, check, errors) {
|
|
580
|
+
const name = check.label ?? check.key;
|
|
581
|
+
if (!(check.key in object)) {
|
|
582
|
+
errors.push(`Missing property '${name}' (expected string)`);
|
|
583
|
+
} else if (typeof object[check.key] !== "string") {
|
|
584
|
+
errors.push(`Invalid property '${name}' (expected string, got ${typeof object[check.key]})`);
|
|
585
|
+
} else if (object[check.key].length === 0) {
|
|
586
|
+
errors.push(`Property '${name}' must not be empty`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function collectArrayErrors(object, check, errors) {
|
|
590
|
+
const name = check.label ?? check.key;
|
|
591
|
+
if (!(check.key in object)) {
|
|
592
|
+
errors.push(`Missing property '${name}' (expected array)`);
|
|
593
|
+
} else if (!Array.isArray(object[check.key])) {
|
|
594
|
+
errors.push(`Invalid property '${name}' (expected array, got ${typeof object[check.key]})`);
|
|
595
|
+
} else if (object[check.key].length === 0) {
|
|
596
|
+
errors.push(`Property '${name}' must not be empty`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function collectSshErrors(value, errors) {
|
|
600
|
+
if (!("ssh" in value)) {
|
|
601
|
+
errors.push("Missing property 'ssh' (expected object)");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
errors.push(...collectSshConfigErrors(value.ssh));
|
|
605
|
+
}
|
|
606
|
+
function collectDefinitionErrors(value) {
|
|
607
|
+
const errors = [];
|
|
608
|
+
if (typeof value !== "object" || value === null) {
|
|
609
|
+
errors.push("Export is not an object");
|
|
610
|
+
return errors;
|
|
611
|
+
}
|
|
612
|
+
const object = value;
|
|
613
|
+
collectStringErrors(object, { key: "name" }, errors);
|
|
614
|
+
collectStringErrors(object, { key: "host" }, errors);
|
|
615
|
+
collectSshErrors(object, errors);
|
|
616
|
+
collectArrayErrors(object, { key: "run" }, errors);
|
|
617
|
+
return errors;
|
|
618
|
+
}
|
|
619
|
+
function validateServerDefinition(value, file) {
|
|
620
|
+
if (isServerDefinitionLike(value)) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const errors = collectDefinitionErrors(value);
|
|
624
|
+
const details = errors.map((entry) => ` - ${entry}`).join("\n");
|
|
625
|
+
console.error(
|
|
626
|
+
`Error: ${file} does not export a valid ServerDefinition.
|
|
627
|
+
${details}
|
|
628
|
+
Use the server() helper to create a valid definition.`
|
|
629
|
+
);
|
|
630
|
+
process.exit(2);
|
|
631
|
+
}
|
|
632
|
+
function errorToString(value) {
|
|
633
|
+
if (value instanceof Error) return value.message;
|
|
634
|
+
if (typeof value === "object" && value !== null) {
|
|
635
|
+
try {
|
|
636
|
+
return JSON.stringify(value);
|
|
637
|
+
} catch {
|
|
638
|
+
return inspect(value, { breakLength: Infinity, depth: 5 });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return String(value);
|
|
642
|
+
}
|
|
643
|
+
function printCauseChain(error) {
|
|
644
|
+
let cause = error.cause;
|
|
645
|
+
while (cause != null) {
|
|
646
|
+
console.error(` Caused by: ${errorToString(cause)}`);
|
|
647
|
+
cause = cause instanceof Error ? cause.cause : void 0;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function printExceptionError(error, verbose) {
|
|
651
|
+
console.error(`Error: ${errorToString(error)}`);
|
|
652
|
+
if (error instanceof Error) {
|
|
653
|
+
printCauseChain(error);
|
|
654
|
+
if (verbose && error.stack != null) {
|
|
655
|
+
console.error(`
|
|
656
|
+
${error.stack}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function handleTsxLoadFailure(filePath) {
|
|
661
|
+
if (new RegExp("\\.[cm]?ts$", "v").test(filePath)) {
|
|
662
|
+
console.error(
|
|
663
|
+
`${pc.red("Error:")} tsx is required to run TypeScript playbooks but could not be loaded.
|
|
664
|
+
Install it with: ${pc.bold("npm install -g tsx")} or add it as a devDependency.`
|
|
665
|
+
);
|
|
666
|
+
process.exit(2);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function isDirectCliExecution(moduleUrl, candidateEntryScript) {
|
|
670
|
+
if (candidateEntryScript == null) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(candidateEntryScript);
|
|
674
|
+
}
|
|
675
|
+
function applyCliEnvironmentOverrides(environment, options) {
|
|
676
|
+
if (!options.firstRun) return environment;
|
|
677
|
+
return { ...environment, [FIRST_RUN_ENV_NAME]: "true" };
|
|
678
|
+
}
|
|
679
|
+
function applyCliProcessEnvironment(options) {
|
|
680
|
+
if (!options.firstRun) return;
|
|
681
|
+
process.env[FIRST_RUN_ENV_NAME] = "true";
|
|
682
|
+
}
|
|
683
|
+
async function loadServerDefinitionFromFile(file, options) {
|
|
684
|
+
const filePath = resolve(file);
|
|
685
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
686
|
+
applyCliProcessEnvironment(options);
|
|
687
|
+
await import("tsx/esm/api").then((tsx) => {
|
|
688
|
+
tsx.register();
|
|
689
|
+
}).catch(() => {
|
|
690
|
+
handleTsxLoadFailure(filePath);
|
|
691
|
+
});
|
|
692
|
+
const imported = await import(fileUrl);
|
|
693
|
+
const definition = imported.default ?? imported;
|
|
694
|
+
validateServerDefinition(definition, filePath);
|
|
695
|
+
return definition;
|
|
696
|
+
}
|
|
697
|
+
var program = new Command();
|
|
698
|
+
program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.1.0");
|
|
699
|
+
program.command("apply <file>").description("Apply a server definition").option(
|
|
700
|
+
"--dry-run",
|
|
701
|
+
"Only check, do not apply. Some modules validate prospective config but cannot verify runtime restarts.",
|
|
702
|
+
false
|
|
703
|
+
).option("--env <key=value...>", "Set env values", collectEnvironment, {}).option("--env-file <path>", "Load dotenv file").option("--first-run", "Set PARATIX_FIRST_RUN=true before loading the playbook", false).option(
|
|
704
|
+
"--reconnect-timeout <seconds>",
|
|
705
|
+
"SSH reconnect timeout",
|
|
706
|
+
parsePositiveNumber,
|
|
707
|
+
DEFAULT_RECONNECT_TIMEOUT_SECONDS
|
|
708
|
+
).option("--verbose", "Show full stack traces on error", false).action(async (file, options) => {
|
|
709
|
+
try {
|
|
710
|
+
printCliHeader("0.1.0");
|
|
711
|
+
const environmentOverrides = applyCliEnvironmentOverrides(
|
|
712
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
713
|
+
options.env,
|
|
714
|
+
{
|
|
715
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
716
|
+
firstRun: options.firstRun
|
|
717
|
+
}
|
|
718
|
+
);
|
|
719
|
+
const definition = await loadServerDefinitionFromFile(file, {
|
|
720
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
721
|
+
firstRun: options.firstRun
|
|
722
|
+
});
|
|
723
|
+
await runPlaybook(definition, {
|
|
724
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
725
|
+
dryRun: options.dryRun,
|
|
726
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
727
|
+
envFile: options.envFile,
|
|
728
|
+
envOverrides: environmentOverrides,
|
|
729
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
730
|
+
reconnectTimeout: options.reconnectTimeout * SECONDS_TO_MS,
|
|
731
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Commander options typed as Record<string, unknown>
|
|
732
|
+
verbose: options.verbose
|
|
733
|
+
});
|
|
734
|
+
} catch (error) {
|
|
735
|
+
printExceptionError(error, options.verbose);
|
|
736
|
+
process.exit(process.exitCode ?? 2);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
function parsePositiveNumber(value) {
|
|
740
|
+
const parsed = Number(value);
|
|
741
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
742
|
+
console.error(`Invalid --reconnect-timeout value: ${value} (expected a positive number)`);
|
|
743
|
+
process.exit(2);
|
|
744
|
+
}
|
|
745
|
+
return parsed;
|
|
746
|
+
}
|
|
747
|
+
function collectEnvironment(value, previous) {
|
|
748
|
+
const eqIndex = value.indexOf("=");
|
|
749
|
+
if (eqIndex === -1) {
|
|
750
|
+
console.error(`Invalid --env format: ${value} (expected key=value)`);
|
|
751
|
+
process.exit(2);
|
|
752
|
+
}
|
|
753
|
+
const key = value.slice(0, eqIndex);
|
|
754
|
+
const value_ = value.slice(eqIndex + 1);
|
|
755
|
+
if (!ENVIRONMENT_KEY_PATTERN.test(key)) {
|
|
756
|
+
console.error(
|
|
757
|
+
`Invalid --env name: ${key === "" ? "(empty)" : key} (expected [A-Za-z_][A-Za-z0-9_]*)`
|
|
758
|
+
);
|
|
759
|
+
process.exit(2);
|
|
760
|
+
}
|
|
761
|
+
return { ...previous, [key]: value_ };
|
|
762
|
+
}
|
|
763
|
+
var entryScript = process.argv[1];
|
|
764
|
+
if (isDirectCliExecution(import.meta.url, entryScript)) {
|
|
765
|
+
await program.parseAsync();
|
|
766
|
+
}
|
|
767
|
+
export {
|
|
768
|
+
applyCliEnvironmentOverrides,
|
|
769
|
+
applyCliProcessEnvironment,
|
|
770
|
+
collectDefinitionErrors,
|
|
771
|
+
collectEnvironment,
|
|
772
|
+
handleTsxLoadFailure,
|
|
773
|
+
isDirectCliExecution,
|
|
774
|
+
isServerDefinitionLike,
|
|
775
|
+
loadServerDefinitionFromFile,
|
|
776
|
+
parsePositiveNumber,
|
|
777
|
+
printExceptionError
|
|
778
|
+
};
|
|
779
|
+
//# sourceMappingURL=cli.js.map
|