paratix 0.12.8 → 0.14.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 +18 -10
- package/dist/{chunk-M3ZQJNKM.js → chunk-475OT32R.js} +260 -29
- package/dist/cli.js +2297 -1529
- package/dist/{index-udpAybq3.d.ts → index-DlZISXJx.d.ts} +27 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.js +46 -14
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +1 -2
- package/llm-guide.md +55 -45
- package/package.json +4 -2
- package/dist/chunk-M3ZQJNKM.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/modules/index.js.map +0 -1
|
@@ -157,6 +157,17 @@ type Module = {
|
|
|
157
157
|
* @returns `"ok"` if the desired state is already present, `"needs-apply"` otherwise.
|
|
158
158
|
*/
|
|
159
159
|
check: (ssh: null | SshConnection, environment: Environment) => Promise<"needs-apply" | "ok">;
|
|
160
|
+
/**
|
|
161
|
+
* Internal discriminator that distinguishes leaf modules from recipes.
|
|
162
|
+
*
|
|
163
|
+
* Leaf modules leave this `undefined` (treated as `"module"`); recipes set
|
|
164
|
+
* `"recipe"` so the runner can narrow to the recipe contract via the
|
|
165
|
+
* `isRecipe` type guard instead of an unsafe structural cast. Intentionally
|
|
166
|
+
* optional so existing custom modules and playbooks stay source-compatible —
|
|
167
|
+
* they never need to set it.
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
kind?: "module" | "recipe";
|
|
160
171
|
/**
|
|
161
172
|
* When true the module runs locally instead of over SSH.
|
|
162
173
|
* The `ssh` parameter will be `null` in check/apply.
|
|
@@ -240,8 +251,16 @@ type SshConnection = {
|
|
|
240
251
|
probeSudo: () => Promise<void>;
|
|
241
252
|
/** Read the full contents of a remote file as a string. */
|
|
242
253
|
readFile: (remotePath: string) => Promise<string>;
|
|
243
|
-
/**
|
|
244
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Reconnect the SSH session using the current host and registered port
|
|
256
|
+
* candidates. An explicit `reconnectTimeout` in the SSH config always takes
|
|
257
|
+
* precedence; when it is unset, `options.defaultTimeout` (in milliseconds)
|
|
258
|
+
* overrides the generic reconnect default for this call — used by the reboot
|
|
259
|
+
* path to grant a longer window.
|
|
260
|
+
*/
|
|
261
|
+
reconnect: (options?: {
|
|
262
|
+
defaultTimeout?: number;
|
|
263
|
+
}) => Promise<void>;
|
|
245
264
|
/** Remove a previously registered port from the reconnect candidate list. */
|
|
246
265
|
removePort: (port: number) => void;
|
|
247
266
|
/** Return the SHA-256 hex digest of a remote file, or `null` if not found. */
|
|
@@ -250,13 +269,13 @@ type SshConnection = {
|
|
|
250
269
|
test: (command: string) => Promise<boolean>;
|
|
251
270
|
/** Update the target host address (e.g. after a reboot with new IP). */
|
|
252
271
|
updateHost: (host: string) => void;
|
|
253
|
-
/** Upload a local file to the remote host via SFTP. */
|
|
272
|
+
/** Upload a local file to the remote host via SFTP. `options.mode` is optional and defaults to `"0600"`. */
|
|
254
273
|
uploadFile: (localPath: string, remotePath: string, options?: {
|
|
255
274
|
mode?: string;
|
|
256
275
|
}) => Promise<void>;
|
|
257
|
-
/** Write a string to a remote file, creating or overwriting it. */
|
|
258
|
-
writeFile: (remotePath: string, content: string, options
|
|
259
|
-
mode
|
|
276
|
+
/** Write a string to a remote file, creating or overwriting it. `options.mode` is optional and defaults to `"0600"`. */
|
|
277
|
+
writeFile: (remotePath: string, content: string, options?: {
|
|
278
|
+
mode?: string;
|
|
260
279
|
}) => Promise<void>;
|
|
261
280
|
};
|
|
262
281
|
/** SSH connection parameters for a server. */
|
|
@@ -564,6 +583,7 @@ declare const command: {
|
|
|
564
583
|
* If it exits `0`, the command is considered already done.
|
|
565
584
|
* @param options.name - An optional display name shown in the run output.
|
|
566
585
|
* @param options.secrets - Secret values that must be redacted from command output.
|
|
586
|
+
* @param options.timeout - Maximum runtime for the command in milliseconds.
|
|
567
587
|
* @returns A Module that executes the shell command.
|
|
568
588
|
*
|
|
569
589
|
* @example
|
|
@@ -576,6 +596,7 @@ declare const command: {
|
|
|
576
596
|
check?: string;
|
|
577
597
|
name?: string;
|
|
578
598
|
secrets?: string[];
|
|
599
|
+
timeout?: number;
|
|
579
600
|
}): Module;
|
|
580
601
|
};
|
|
581
602
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as Environment, M as Module, a as ModuleMetaEntry, b as MetaEnvironmentValue, c as EnvironmentMetaEntry, S as SshdPortMetaEntry, d as SystemHostMetaEntry, e as SystemRebootMetaEntry, f as ModuleResult, g as ExecResult, h as ModuleStatus, i as SshConnection, O as OrchestrationStep, j as ShutdownSignal, k as ServerDefinition } from './index-
|
|
2
|
-
export { l as EnvironmentValue, m as ExecOptions, N as NEEDS_APPLY, n as SshConfig, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from './index-
|
|
1
|
+
import { E as Environment, M as Module, a as ModuleMetaEntry, b as MetaEnvironmentValue, c as EnvironmentMetaEntry, S as SshdPortMetaEntry, d as SystemHostMetaEntry, e as SystemRebootMetaEntry, f as ModuleResult, g as ExecResult, h as ModuleStatus, i as SshConnection, O as OrchestrationStep, j as ShutdownSignal, k as ServerDefinition } from './index-DlZISXJx.js';
|
|
2
|
+
export { l as EnvironmentValue, m as ExecOptions, N as NEEDS_APPLY, n as SshConfig, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from './index-DlZISXJx.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Fail the run if a condition on the current env is not satisfied.
|
|
@@ -185,11 +185,16 @@ type SignalHooks = {
|
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
187
|
* Internal representation of a recipe module.
|
|
188
|
-
*
|
|
188
|
+
*
|
|
189
|
+
* The `kind: "recipe"` discriminator lets the runner distinguish recipes from
|
|
190
|
+
* leaf modules through the {@link isRecipe} type guard, forming a discriminated
|
|
191
|
+
* union with plain {@link Module} values (which leave `kind` unset). The child
|
|
192
|
+
* `_modules` and optional `_signals` are recipe-specific data that TypeScript
|
|
193
|
+
* narrows to once `isRecipe` confirmed the discriminator — no structural cast
|
|
194
|
+
* is required anywhere.
|
|
189
195
|
* @internal
|
|
190
196
|
*/
|
|
191
197
|
type RecipeModule = {
|
|
192
|
-
_isRecipe: true;
|
|
193
198
|
_modules: Module[];
|
|
194
199
|
_signals?: Module[];
|
|
195
200
|
apply: (ssh: null | SshConnection, environment: Environment, options?: {
|
|
@@ -199,6 +204,7 @@ type RecipeModule = {
|
|
|
199
204
|
signalHooks?: SignalHooks;
|
|
200
205
|
verbose?: boolean;
|
|
201
206
|
}) => Promise<ModuleResult>;
|
|
207
|
+
kind: "recipe";
|
|
202
208
|
} & Module;
|
|
203
209
|
/**
|
|
204
210
|
* Group a list of modules into a named, self-contained recipe.
|
|
@@ -215,8 +221,11 @@ type RecipeModule = {
|
|
|
215
221
|
* @returns A RecipeModule that groups the child modules.
|
|
216
222
|
*
|
|
217
223
|
* @example
|
|
224
|
+
* import { recipe } from "paratix"
|
|
225
|
+
* import { package as pkg, file, service } from "paratix/modules"
|
|
226
|
+
*
|
|
218
227
|
* export const nginxRecipe = recipe("nginx", [
|
|
219
|
-
*
|
|
228
|
+
* pkg.installed("nginx"),
|
|
220
229
|
* file.template("/etc/nginx/nginx.conf", "./files/nginx.conf.tmpl"),
|
|
221
230
|
* service.enabled("nginx"),
|
|
222
231
|
* ], {
|
|
@@ -239,11 +248,14 @@ declare function recipe(name: string, modules: Module[], options?: {
|
|
|
239
248
|
* @throws {Error} When any required field is missing or empty.
|
|
240
249
|
*
|
|
241
250
|
* @example
|
|
251
|
+
* import { server } from "paratix"
|
|
252
|
+
* import { package as pkg } from "paratix/modules"
|
|
253
|
+
*
|
|
242
254
|
* export default server({
|
|
243
255
|
* name: "web-01",
|
|
244
256
|
* host: "10.0.0.1",
|
|
245
257
|
* ssh: { user: "root", ports: [22], privateKey: "~/.ssh/id_ed25519" }, // "~" is expanded
|
|
246
|
-
* run: [
|
|
258
|
+
* run: [pkg.installed("nginx")],
|
|
247
259
|
* });
|
|
248
260
|
*/
|
|
249
261
|
declare function server(config: ServerDefinition): ServerDefinition;
|
package/dist/index.js
CHANGED
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
user,
|
|
62
62
|
validateHostLabel,
|
|
63
63
|
validateSshConfig
|
|
64
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-475OT32R.js";
|
|
65
65
|
|
|
66
66
|
// src/conditionalModules.ts
|
|
67
67
|
function createConditionalApplyState(environment) {
|
|
@@ -536,6 +536,10 @@ var DEFAULT_TERMINAL_COLUMNS = 100;
|
|
|
536
536
|
var MIN_PACKAGE_COLUMNS_WIDTH = 24;
|
|
537
537
|
var MIN_PACKAGE_COLUMN_WIDTH = 18;
|
|
538
538
|
var MIN_ANIMATED_LINE_COLUMNS = 8;
|
|
539
|
+
var ELAPSED_DISPLAY_THRESHOLD_MS = 1e3;
|
|
540
|
+
var MILLISECONDS_PER_SECOND = 1e3;
|
|
541
|
+
var MILLISECONDS_PER_MINUTE = 6e4;
|
|
542
|
+
var SECONDS_PER_MINUTE = 60;
|
|
539
543
|
function splitPackageModuleName(name) {
|
|
540
544
|
for (const prefix of ["package.installed: ", "package.absent: "]) {
|
|
541
545
|
if (!name.startsWith(prefix)) continue;
|
|
@@ -573,6 +577,16 @@ function getPackageSummaryDetail(packageCount, detail) {
|
|
|
573
577
|
const packageLabel = packageCount === 1 ? "1 package" : `${packageCount} packages`;
|
|
574
578
|
return detail == null ? packageLabel : `${packageLabel} \xB7 ${detail}`;
|
|
575
579
|
}
|
|
580
|
+
function formatModuleElapsed(elapsedMs) {
|
|
581
|
+
if (!Number.isFinite(elapsedMs) || elapsedMs < ELAPSED_DISPLAY_THRESHOLD_MS) return void 0;
|
|
582
|
+
if (elapsedMs < MILLISECONDS_PER_MINUTE) {
|
|
583
|
+
return `${(elapsedMs / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
|
|
584
|
+
}
|
|
585
|
+
const totalSeconds = Math.floor(elapsedMs / MILLISECONDS_PER_SECOND);
|
|
586
|
+
const minutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE);
|
|
587
|
+
const seconds = totalSeconds % SECONDS_PER_MINUTE;
|
|
588
|
+
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
589
|
+
}
|
|
576
590
|
function fitAnimatedModuleLine(line, columns) {
|
|
577
591
|
if (columns === void 0 || columns < MIN_ANIMATED_LINE_COLUMNS) return line;
|
|
578
592
|
const plainLine = stripVTControlCharacters(line);
|
|
@@ -624,6 +638,7 @@ function getSharedLiveOutputState() {
|
|
|
624
638
|
const existing = registry[LIVE_OUTPUT_STATE_KEY];
|
|
625
639
|
if (existing != null) return existing;
|
|
626
640
|
const created = {
|
|
641
|
+
activeModuleStartedAt: null,
|
|
627
642
|
activeRecipeGuideDepths: [],
|
|
628
643
|
activeSpinner: null,
|
|
629
644
|
cursorHidden: false,
|
|
@@ -666,7 +681,7 @@ function getGuideDot(depth) {
|
|
|
666
681
|
return depth % 2 === 0 ? pc.gray("\xB7") : pc.cyan("\xB7");
|
|
667
682
|
}
|
|
668
683
|
function buildGuideIndent(baseIndent, options) {
|
|
669
|
-
const indentCharacters =
|
|
684
|
+
const indentCharacters = [...baseIndent];
|
|
670
685
|
const guideDepths = [
|
|
671
686
|
...options?.activeGuideDepths ?? liveOutputState.activeRecipeGuideDepths,
|
|
672
687
|
...options?.extraGuideDepths ?? []
|
|
@@ -702,20 +717,27 @@ async function withRecipeOutputScope(scopedOperation) {
|
|
|
702
717
|
try {
|
|
703
718
|
return await scopedOperation();
|
|
704
719
|
} finally {
|
|
705
|
-
liveOutputState.activeRecipeGuideDepths = liveOutputState.activeRecipeGuideDepths.filter(
|
|
720
|
+
liveOutputState.activeRecipeGuideDepths = liveOutputState.activeRecipeGuideDepths.filter(
|
|
721
|
+
(depth) => depth !== liveOutputState.recipeOutputDepth
|
|
722
|
+
);
|
|
706
723
|
liveOutputState.pendingRecipeClosureGuideDepths = [liveOutputState.recipeOutputDepth];
|
|
707
724
|
liveOutputState.recipeOutputDepth -= 1;
|
|
708
725
|
}
|
|
709
726
|
}
|
|
727
|
+
function getActiveModuleElapsed() {
|
|
728
|
+
if (liveOutputState.activeModuleStartedAt == null) return void 0;
|
|
729
|
+
return formatModuleElapsed(Date.now() - liveOutputState.activeModuleStartedAt);
|
|
730
|
+
}
|
|
710
731
|
function renderModuleLine(parameters) {
|
|
711
|
-
const { detail, extraGuideDepths = [], name, status, waitingFrame } = parameters;
|
|
732
|
+
const { detail, elapsed, extraGuideDepths = [], name, status, waitingFrame } = parameters;
|
|
712
733
|
const baseIndent = getModuleIndent();
|
|
713
734
|
const indent = buildGuideIndent(baseIndent, { extraGuideDepths });
|
|
714
735
|
const icon = getModuleIcon(status, waitingFrame);
|
|
715
736
|
const statusText = getModuleStatusText(status);
|
|
716
737
|
const detailSuffix = detail == null ? "" : ` ${pc.dim(detail)}`;
|
|
738
|
+
const elapsedSuffix = elapsed == null ? "" : ` ${pc.dim(elapsed)}`;
|
|
717
739
|
const alignedNameWidth = Math.max(MODULE_NAME_WIDTH - baseIndent.length, MIN_MODULE_NAME_WIDTH);
|
|
718
|
-
return `${indent}${icon} ${name.padEnd(alignedNameWidth)} ${statusText}${detailSuffix}`;
|
|
740
|
+
return `${indent}${icon} ${name.padEnd(alignedNameWidth)} ${statusText}${detailSuffix}${elapsedSuffix}`;
|
|
719
741
|
}
|
|
720
742
|
function hideCursor() {
|
|
721
743
|
if (liveOutputState.cursorHidden) return;
|
|
@@ -745,6 +767,7 @@ function stopAnimatedModuleLine(clearCurrentLine = false) {
|
|
|
745
767
|
}
|
|
746
768
|
}
|
|
747
769
|
function startModuleSpinner(name, detail) {
|
|
770
|
+
liveOutputState.activeModuleStartedAt = Date.now();
|
|
748
771
|
if (!supportsAnimatedModuleOutput()) return;
|
|
749
772
|
clearPendingRecipeClosureGuides();
|
|
750
773
|
stopAnimatedModuleLine();
|
|
@@ -765,6 +788,7 @@ function startModuleSpinner(name, detail) {
|
|
|
765
788
|
writeAnimatedModuleLine(
|
|
766
789
|
renderModuleLine({
|
|
767
790
|
detail: spinner.detail,
|
|
791
|
+
elapsed: getActiveModuleElapsed(),
|
|
768
792
|
name: displayModule.name,
|
|
769
793
|
status: "waiting",
|
|
770
794
|
waitingFrame: SPINNER_FRAMES[spinner.frameIndex]
|
|
@@ -777,6 +801,7 @@ function startModuleSpinner(name, detail) {
|
|
|
777
801
|
writeAnimatedModuleLine(
|
|
778
802
|
renderModuleLine({
|
|
779
803
|
detail: displayModule.detail,
|
|
804
|
+
elapsed: getActiveModuleElapsed(),
|
|
780
805
|
name: displayModule.name,
|
|
781
806
|
status: "waiting",
|
|
782
807
|
waitingFrame: SPINNER_FRAMES[0]
|
|
@@ -789,7 +814,10 @@ function printRecipeHeader(name) {
|
|
|
789
814
|
const header = pc.bold(pc.blue(`[${name}]`));
|
|
790
815
|
console.log(`${buildGuideIndent(getRecipeHeaderIndent())}${header}`);
|
|
791
816
|
if (liveOutputState.recipeOutputDepth >= 0) {
|
|
792
|
-
liveOutputState.activeRecipeGuideDepths = [
|
|
817
|
+
liveOutputState.activeRecipeGuideDepths = [
|
|
818
|
+
...liveOutputState.activeRecipeGuideDepths,
|
|
819
|
+
liveOutputState.recipeOutputDepth
|
|
820
|
+
];
|
|
793
821
|
}
|
|
794
822
|
}
|
|
795
823
|
function colorizeDiffLine(line) {
|
|
@@ -836,8 +864,11 @@ function printRenderedModuleResult(parameters) {
|
|
|
836
864
|
status: parameters.status,
|
|
837
865
|
terminalColumns: process.stdout.columns
|
|
838
866
|
});
|
|
867
|
+
const elapsed = getActiveModuleElapsed();
|
|
868
|
+
liveOutputState.activeModuleStartedAt = null;
|
|
839
869
|
const line = renderModuleLine({
|
|
840
870
|
detail: displayModule.detail,
|
|
871
|
+
elapsed,
|
|
841
872
|
extraGuideDepths,
|
|
842
873
|
name: displayModule.name,
|
|
843
874
|
status: parameters.status
|
|
@@ -964,7 +995,9 @@ function printCauseChain(error) {
|
|
|
964
995
|
if (visited.has(cause)) return;
|
|
965
996
|
visited.add(cause);
|
|
966
997
|
}
|
|
967
|
-
console.error(
|
|
998
|
+
console.error(
|
|
999
|
+
pc.red(`${getErrorIndent()}Cause: ${maskRegisteredSecrets(formatCauseValue(cause))}`)
|
|
1000
|
+
);
|
|
968
1001
|
if (cause instanceof CommandError) {
|
|
969
1002
|
printVerboseCommandError(
|
|
970
1003
|
maskRegisteredSecrets(cause.fullStdout),
|
|
@@ -1191,11 +1224,11 @@ async function runSignalModules(parameters) {
|
|
|
1191
1224
|
|
|
1192
1225
|
// src/recipe.ts
|
|
1193
1226
|
var INTERRUPTED_BEFORE_APPLY = /* @__PURE__ */ Symbol("recipe-interrupted-before-apply");
|
|
1194
|
-
function
|
|
1195
|
-
return module.
|
|
1227
|
+
function isRecipe(module) {
|
|
1228
|
+
return module.kind === "recipe";
|
|
1196
1229
|
}
|
|
1197
1230
|
function printRecipeChildResult(module, result) {
|
|
1198
|
-
if (
|
|
1231
|
+
if (isRecipe(module)) {
|
|
1199
1232
|
printRecipeModuleResult(module.name, result.status, result.detail);
|
|
1200
1233
|
return;
|
|
1201
1234
|
}
|
|
@@ -1262,7 +1295,7 @@ async function checkRecipeChild(targetModule, connection, currentEnvironment) {
|
|
|
1262
1295
|
return targetModule.check(connection, currentEnvironment);
|
|
1263
1296
|
}
|
|
1264
1297
|
async function applyRecipeChild(parameters) {
|
|
1265
|
-
if (
|
|
1298
|
+
if (isRecipe(parameters.targetModule)) {
|
|
1266
1299
|
return parameters.targetModule.apply(parameters.connection, parameters.currentEnvironment, {
|
|
1267
1300
|
onChildStep: parameters.onChildStep,
|
|
1268
1301
|
onSignalStep: parameters.onSignalStep,
|
|
@@ -1469,7 +1502,6 @@ function createRecipeDryRunApply(name, modules, needsDryRunApply) {
|
|
|
1469
1502
|
const result = await dryRunRecipeModule({
|
|
1470
1503
|
environment,
|
|
1471
1504
|
recipeModule: {
|
|
1472
|
-
_isRecipe: true,
|
|
1473
1505
|
_modules: modules,
|
|
1474
1506
|
async apply() {
|
|
1475
1507
|
await Promise.resolve();
|
|
@@ -1479,6 +1511,7 @@ function createRecipeDryRunApply(name, modules, needsDryRunApply) {
|
|
|
1479
1511
|
await Promise.resolve();
|
|
1480
1512
|
return "ok";
|
|
1481
1513
|
},
|
|
1514
|
+
kind: "recipe",
|
|
1482
1515
|
name
|
|
1483
1516
|
},
|
|
1484
1517
|
shutdownSignal: parameters?.shutdownSignal,
|
|
@@ -1498,7 +1531,6 @@ function recipe(name, modules, options) {
|
|
|
1498
1531
|
...modules.some((module) => module._dryRunBlocker === true) ? { _dryRunBlocker: true } : {},
|
|
1499
1532
|
...modules.some((module) => module._dryRunMetaProducer === true) ? { _dryRunMetaProducer: true } : {},
|
|
1500
1533
|
...applyDryRun == null ? {} : { _applyDryRun: applyDryRun },
|
|
1501
|
-
_isRecipe: true,
|
|
1502
1534
|
_modules: modules,
|
|
1503
1535
|
_signals: options?.signals,
|
|
1504
1536
|
_supportsChildStepHook: true,
|
|
@@ -1526,6 +1558,7 @@ function recipe(name, modules, options) {
|
|
|
1526
1558
|
}
|
|
1527
1559
|
return "ok";
|
|
1528
1560
|
},
|
|
1561
|
+
kind: "recipe",
|
|
1529
1562
|
name
|
|
1530
1563
|
};
|
|
1531
1564
|
}
|
|
@@ -1634,4 +1667,3 @@ export {
|
|
|
1634
1667
|
user,
|
|
1635
1668
|
when
|
|
1636
1669
|
};
|
|
1637
|
-
//# sourceMappingURL=index.js.map
|
package/dist/modules/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { V as UpgradeOptions, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from '../index-
|
|
1
|
+
export { V as UpgradeOptions, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from '../index-DlZISXJx.js';
|
package/dist/modules/index.js
CHANGED
package/llm-guide.md
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
# Paratix
|
|
1
|
+
# Paratix TypeScript API and Module Reference
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is the complete reference for writing Paratix playbooks and custom modules. It covers the
|
|
4
|
+
TypeScript API, built-in modules, authoring patterns, and common mistakes.
|
|
4
5
|
|
|
5
6
|
## Overview
|
|
6
7
|
|
|
7
8
|
Paratix is a CLI tool for idempotent VPS configuration via SSH using TypeScript playbooks. Each playbook exports a `server()` definition containing an ordered list of modules that are checked and applied over SSH. Modules follow a check/apply pattern: `check` determines if work is needed, `apply` enforces the desired state.
|
|
8
9
|
|
|
10
|
+
## Agent authoring guidance
|
|
11
|
+
|
|
12
|
+
When generating or changing Paratix playbooks or custom modules, automated coding agents should use
|
|
13
|
+
this document as the complete API reference. Read the relevant API and module sections before making
|
|
14
|
+
changes, and follow the [Do's and Don'ts](#dos-and-donts) and [testing patterns](#testing-patterns).
|
|
15
|
+
|
|
9
16
|
## Imports
|
|
10
17
|
|
|
11
18
|
Paratix has exactly two import paths:
|
|
@@ -150,9 +157,12 @@ export default server({
|
|
|
150
157
|
|
|
151
158
|
### `command`
|
|
152
159
|
|
|
153
|
-
| Method | Signature
|
|
154
|
-
| --------------- |
|
|
155
|
-
| `command.shell` | `(cmd: string, options?: { check?: string; name?: string; secrets?: string[] }): Module` | Only with `check` |
|
|
160
|
+
| Method | Signature | Idempotent |
|
|
161
|
+
| --------------- | ---------------------------------------------------------------------------------------------------------- | ----------------- |
|
|
162
|
+
| `command.shell` | `(cmd: string, options?: { check?: string; name?: string; secrets?: string[]; timeout?: number }): Module` | Only with `check` |
|
|
163
|
+
|
|
164
|
+
`command.shell` accepts `timeout` in milliseconds to limit the runtime of the applied command. The
|
|
165
|
+
idempotency guard configured through `check` is not affected by this timeout.
|
|
156
166
|
|
|
157
167
|
### `cron`
|
|
158
168
|
|
|
@@ -212,9 +222,9 @@ very slow artifact hosts or large downloads.
|
|
|
212
222
|
| `file.stat` | `(remotePath: string): Module` | No (always-applies) |
|
|
213
223
|
| `file.template` | `(remotePath: string, templatePath: string, options?: { mode?: string; owner?: string; strict?: boolean }): Module` | Yes |
|
|
214
224
|
|
|
215
|
-
**
|
|
225
|
+
**Note on `file.copy` and the default mode:** When `options.mode` is omitted, `file.copy` sets the mode to the documented default of `0644`. The mode is applied during upload (`uploadFile` with `{ mode }`) and compared with the desired value in `check`, so subsequent runs detect mode drift (for example, a manual `chmod 0600`) as `needs-apply`. Pass `{ mode: "0600" }` explicitly when more restrictive permissions are required, such as for secrets.
|
|
216
226
|
|
|
217
|
-
**
|
|
227
|
+
**Note on `file.line`:** The `line` value must be a single line without CR/LF characters. Use `file.block` for multiline content.
|
|
218
228
|
|
|
219
229
|
### `git`
|
|
220
230
|
|
|
@@ -342,7 +352,7 @@ rsync SSH process and does not depend on a local `known_hosts` entry.
|
|
|
342
352
|
| `ssh.authorizedKeys` | `(user: string, key: string, options?: { state?: "absent" \| "present" }): Module` | Yes |
|
|
343
353
|
| `ssh.knownHosts` | `(host: string, options?: { expectedFingerprint?: string; port?: number; publicKey?: string; state?: "absent" \| "present" }): Module` | Yes |
|
|
344
354
|
|
|
345
|
-
**Trust
|
|
355
|
+
**Trust anchor requirement for `ssh.knownHosts` (`state: "present"`):** In the default `present` state, either `expectedFingerprint` or `publicKey` must be set. Without a trust anchor, the module constructor throws immediately because `check` would otherwise treat any existing `known_hosts` entry, including older and potentially compromised TOFU acceptances, as `ok` and skip the anchor comparison. No anchor is required for `state: "absent"`, because that state removes the entry.
|
|
346
356
|
|
|
347
357
|
### `sshd`
|
|
348
358
|
|
|
@@ -481,23 +491,23 @@ function myCustomModule(configPath: string, content: string): Module {
|
|
|
481
491
|
|
|
482
492
|
Methods available on the `ssh` parameter:
|
|
483
493
|
|
|
484
|
-
| Method
|
|
485
|
-
|
|
|
486
|
-
| `ssh.exec(cmd, options?)`
|
|
487
|
-
| `ssh.test(cmd)`
|
|
488
|
-
| `ssh.output(cmd)`
|
|
489
|
-
| `ssh.lines(cmd)`
|
|
490
|
-
| `ssh.exists(path)`
|
|
491
|
-
| `ssh.readFile(path)`
|
|
492
|
-
| `ssh.writeFile(path, content, { mode: "0644" })`
|
|
493
|
-
| `ssh.uploadFile(local, remote)`
|
|
494
|
-
| `ssh.downloadFile(remote, local)`
|
|
495
|
-
| `ssh.sha256(path)`
|
|
496
|
-
| `ssh.addPort(port)`
|
|
497
|
-
| `ssh.disconnect()`
|
|
498
|
-
| `ssh.getConnectionInfo()`
|
|
499
|
-
| `ssh.probeSudo()`
|
|
500
|
-
| `ssh.updateHost(host)`
|
|
494
|
+
| Method | Return type | Description |
|
|
495
|
+
| ------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
496
|
+
| `ssh.exec(cmd, options?)` | `Promise<ExecResult>` | Run command, get `{ code, stdout, stderr }`. Throws on non-zero unless `ignoreExitCode: true`. |
|
|
497
|
+
| `ssh.test(cmd)` | `Promise<boolean>` | Run command, return `true` if exit code is 0. |
|
|
498
|
+
| `ssh.output(cmd)` | `Promise<string>` | Run command, return trimmed stdout. |
|
|
499
|
+
| `ssh.lines(cmd)` | `Promise<string[]>` | Run command, return stdout split into lines. |
|
|
500
|
+
| `ssh.exists(path)` | `Promise<boolean>` | Check if remote path exists. |
|
|
501
|
+
| `ssh.readFile(path)` | `Promise<string>` | Read remote file content. |
|
|
502
|
+
| `ssh.writeFile(path, content, { mode: "0644" })` | `Promise<void>` | Write content to remote file. `mode` is optional and defaults to `"0600"`. |
|
|
503
|
+
| `ssh.uploadFile(local, remote, { mode: "0644" })` | `Promise<void>` | Upload local file via SFTP. `mode` is optional and defaults to `"0600"`. |
|
|
504
|
+
| `ssh.downloadFile(remote, local)` | `Promise<void>` | Download remote file. |
|
|
505
|
+
| `ssh.sha256(path)` | `Promise<string \| null>` | Get SHA-256 hex digest, or null if not found. |
|
|
506
|
+
| `ssh.addPort(port)` | `void` | Register an additional port opened on the remote host (advanced). |
|
|
507
|
+
| `ssh.disconnect()` | `void` | Close the SSH connection. |
|
|
508
|
+
| `ssh.getConnectionInfo()` | `{ host, port, privateKeyPath?, user }` | Return current connection parameters. |
|
|
509
|
+
| `ssh.probeSudo()` | `Promise<void>` | Probe/cache sudo access; prompts interactively if needed. |
|
|
510
|
+
| `ssh.updateHost(host)` | `void` | Update the target host address (e.g., after IP change). |
|
|
501
511
|
|
|
502
512
|
### ExecOptions
|
|
503
513
|
|
|
@@ -666,7 +676,7 @@ fail("This branch should be unreachable")
|
|
|
666
676
|
|
|
667
677
|
### `firstRun.stop(message?)`
|
|
668
678
|
|
|
669
|
-
|
|
679
|
+
Stops the current run cleanly when Paratix was started with `--first-run`. Useful as an explicit staging boundary in bootstrap playbooks.
|
|
670
680
|
|
|
671
681
|
```typescript
|
|
672
682
|
firstRun.stop("Bootstrap foundation complete; rerun without --first-run to continue.")
|
|
@@ -674,7 +684,7 @@ firstRun.stop("Bootstrap foundation complete; rerun without --first-run to conti
|
|
|
674
684
|
|
|
675
685
|
### `signals.flush(message?)`
|
|
676
686
|
|
|
677
|
-
|
|
687
|
+
Immediately runs all currently pending signals in the active scope and clears their pending state.
|
|
678
688
|
|
|
679
689
|
```typescript
|
|
680
690
|
signals.flush("Reload services before the next bootstrap stage")
|
|
@@ -742,20 +752,20 @@ Rules:
|
|
|
742
752
|
|
|
743
753
|
Diff-producing built-in modules:
|
|
744
754
|
|
|
745
|
-
|
|
|
746
|
-
| ------------------------------------------- |
|
|
747
|
-
| `file.copy` | Unified diff
|
|
748
|
-
| `file.template` | Unified diff
|
|
749
|
-
| `sysctl.set` | `key = old → new`
|
|
750
|
-
| `hostname.set` | `hostname = old → new`.
|
|
751
|
-
| `swap.file` | Diff
|
|
752
|
-
| `swap.swappiness` | `vm.swappiness = old → new` (via `sysctl.set`).
|
|
753
|
-
| `swap.vfsCachePressure` | `vm.vfs_cache_pressure = old → new` (via `sysctl.set`).
|
|
754
|
-
| `cron.job` / `cron.absent` | Unified diff
|
|
755
|
-
| `timer.scheduled` (present) |
|
|
756
|
-
| `timer.scheduled` (absent) / `timer.absent` |
|
|
757
|
-
| `net.hosts` | Unified diff
|
|
758
|
-
| `quadlet.container` | Unified diff
|
|
755
|
+
| Module | Diff content |
|
|
756
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
757
|
+
| `file.copy` | Unified diff between the remote file and the local source. |
|
|
758
|
+
| `file.template` | Unified diff between the remote file and the rendered template. |
|
|
759
|
+
| `sysctl.set` | `key = old → new` for the desired configuration. |
|
|
760
|
+
| `hostname.set` | `hostname = old → new`. |
|
|
761
|
+
| `swap.file` | Diff of the `/etc/fstab` entry (or removal of the line for `state: "absent"`). |
|
|
762
|
+
| `swap.swappiness` | `vm.swappiness = old → new` (via `sysctl.set`). |
|
|
763
|
+
| `swap.vfsCachePressure` | `vm.vfs_cache_pressure = old → new` (via `sysctl.set`). |
|
|
764
|
+
| `cron.job` / `cron.absent` | Unified diff of the target user's crontab. |
|
|
765
|
+
| `timer.scheduled` (present) | Concatenated diffs of the `.service` and `.timer` unit files. |
|
|
766
|
+
| `timer.scheduled` (absent) / `timer.absent` | List of unit files to remove. |
|
|
767
|
+
| `net.hosts` | Unified diff of `/etc/hosts`. |
|
|
768
|
+
| `quadlet.container` | Unified diff of the `.container` unit file. If the file already matches but the `daemon-reload` flag is missing, `(dry-run, daemon-reload pending)` also appears as a detail suffix. |
|
|
759
769
|
|
|
760
770
|
### Implementing a Diff for a Custom Module
|
|
761
771
|
|
|
@@ -810,7 +820,7 @@ The diff string is plain text — no ANSI codes. The output layer applies colors
|
|
|
810
820
|
4. For idempotency with `command.shell()`, always provide a `check` command.
|
|
811
821
|
5. Use `{{KEY|shell}}` or `{{KEY|raw}}` placeholders in `.tmpl` files — strict mode is on by default and bare `{{KEY}}` will throw. Provide values via `env` in `server()`.
|
|
812
822
|
6. Use `service.restart()` and `service.reload()` as `signals` in recipes, not directly in `run`.
|
|
813
|
-
7. Use `signals.flush()` only
|
|
823
|
+
7. Use `signals.flush()` only as an explicit checkpoint when staged flows require an early signal flush.
|
|
814
824
|
8. Always pass a date string to `package.upgrade()` and `package.update()` -- it is the idempotency key.
|
|
815
825
|
9. Specify `ssh.ports` as an array -- the runner tries each port in order.
|
|
816
826
|
10. Custom modules must implement both `check` and `apply`, both async.
|
|
@@ -831,9 +841,9 @@ The diff string is plain text — no ANSI codes. The output layer applies colors
|
|
|
831
841
|
9. Do NOT use template syntax `{{key}}` in TypeScript code -- templates are only for files rendered via `file.template()`.
|
|
832
842
|
10. Do NOT use `signals` on the top-level `server()` when you mean a recipe signal -- `server.signals` fire when ANY module in `run` changed.
|
|
833
843
|
11. Do NOT treat `signals.flush()` as a global queue flush -- it only affects the current scope.
|
|
834
|
-
12. `signals.flush()`
|
|
835
|
-
-
|
|
836
|
-
-
|
|
844
|
+
12. `signals.flush()` always flushes only the current scope:
|
|
845
|
+
- inside a recipe, that recipe's signals
|
|
846
|
+
- at the top level, `server(...).signals`
|
|
837
847
|
13. Do NOT call `server()` without all required fields (`name`, `host`, `ssh`, `run`) -- it throws at construction time. `name` and `host` must not be empty strings.
|
|
838
848
|
14. Do NOT use empty arrays for `ssh.ports` or empty strings for `ssh.user`/`ssh.privateKey` -- validation rejects these. `ssh.privateKey` may be omitted entirely to use the SSH agent instead.
|
|
839
849
|
15. Do NOT return loose `meta: { ... }` maps from custom modules -- always use typed meta entries.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paratix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Idempotent VPS setup tool in TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "24.13.2",
|
|
25
25
|
"@types/ssh2": "^1.15.4",
|
|
26
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
26
27
|
"tsup": "^8.4.0",
|
|
27
28
|
"typescript": "^6.0.3",
|
|
28
29
|
"vitest": "^4.1.7"
|
|
@@ -56,9 +57,10 @@
|
|
|
56
57
|
"scripts": {
|
|
57
58
|
"build": "tsup",
|
|
58
59
|
"test": "pnpm test:unit && pnpm test:dist",
|
|
60
|
+
"test:coverage": "vitest run --coverage",
|
|
59
61
|
"test:dist": "pnpm build && vitest run --config vitest.distribution.config.ts",
|
|
60
62
|
"test:integration": "pnpm build && vitest run --config vitest.integration.config.ts",
|
|
61
|
-
"test:unit": "vitest run",
|
|
63
|
+
"test:unit": "vitest run --coverage",
|
|
62
64
|
"test:watch": "vitest"
|
|
63
65
|
}
|
|
64
66
|
}
|