paratix 0.13.0 → 0.15.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 +14 -10
- package/dist/{chunk-XIRORNO3.js → chunk-GB5QZWUM.js} +201 -51
- package/dist/cli.js +57 -10
- package/dist/{index-DwKdTyHo.d.ts → index-Da9ccU5K.d.ts} +38 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +40 -7
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +1 -1
- package/llm-guide.md +99 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,19 +119,21 @@ Paratix can also manage file-backed swap directly. Use `swap.file(...)` to provi
|
|
|
119
119
|
|
|
120
120
|
## CLI
|
|
121
121
|
|
|
122
|
+
`paratix --help` lists these options directly, so they are discoverable without drilling into `paratix apply --help` first.
|
|
123
|
+
|
|
122
124
|
```text
|
|
123
125
|
paratix apply <file> [options]
|
|
124
126
|
|
|
125
127
|
Options:
|
|
126
|
-
--diff
|
|
127
|
-
--dry-run
|
|
128
|
-
--env <key=value
|
|
129
|
-
--env-file <path>
|
|
130
|
-
--filter <names>
|
|
131
|
-
--first-run
|
|
132
|
-
--reconnect-timeout <seconds>
|
|
133
|
-
--verbose
|
|
134
|
-
--help
|
|
128
|
+
--diff Combined with --dry-run: unified diff per changed module
|
|
129
|
+
--dry-run Only check, do not apply (some runtime restarts stay unverified)
|
|
130
|
+
--env <key=value...> Set environment values for the playbook (repeatable)
|
|
131
|
+
--env-file <path> Load environment values from a dotenv file
|
|
132
|
+
--filter <names> Run only the named recipes/modules (comma-separated, repeatable)
|
|
133
|
+
--first-run Set PARATIX_FIRST_RUN=true before loading the playbook
|
|
134
|
+
--reconnect-timeout <seconds> SSH reconnect timeout for reboots/port changes (max 86400)
|
|
135
|
+
--verbose Show full stack traces on error
|
|
136
|
+
--help Show help
|
|
135
137
|
```
|
|
136
138
|
|
|
137
139
|
`--filter <names>` restricts the run to the named recipes and modules. Names are matched anywhere in the tree, and every node that is not selected is shown as `skipped` instead of being executed. The option is comma-separated and repeatable, so `--filter rybbit,palamedes-examples` and `--filter rybbit --filter palamedes-examples` are equivalent. Selecting a recipe runs its whole subtree; a recipe that is not selected but contains a selected descendant is still descended into, so only the matching children run while its siblings are skipped. If several nodes share the same name, every one of them is selected. An unknown filter name aborts the run before it connects (exit code 2). The final run summary counts only top-level nodes, so nested skipped nodes are still shown but are not added to the `skipped` tally. `--filter` composes with `--dry-run` and the other flags.
|
|
@@ -145,7 +147,9 @@ Options:
|
|
|
145
147
|
## Documentation
|
|
146
148
|
|
|
147
149
|
- For project scaffolding, see [`create-paratix`](https://www.npmjs.com/package/create-paratix).
|
|
148
|
-
-
|
|
150
|
+
- See the complete [TypeScript API and Module Reference](./llm-guide.md).
|
|
151
|
+
- For common runtime and connection failures, see [Troubleshooting](../../docs/user-guide/troubleshooting.md).
|
|
152
|
+
- Before upgrading an existing project, see the [migration notes](../../docs/user-guide/migration.md).
|
|
149
153
|
|
|
150
154
|
## License
|
|
151
155
|
|
|
@@ -3245,6 +3245,15 @@ async function commandCheckPassed(ssh2, check, secrets) {
|
|
|
3245
3245
|
});
|
|
3246
3246
|
return result.code === 0;
|
|
3247
3247
|
}
|
|
3248
|
+
function commandExecOptions(secrets, options) {
|
|
3249
|
+
const execOptions2 = {
|
|
3250
|
+
ignoreExitCode: true,
|
|
3251
|
+
secrets,
|
|
3252
|
+
silent: true
|
|
3253
|
+
};
|
|
3254
|
+
if (options?.timeout !== void 0) execOptions2.timeout = options.timeout;
|
|
3255
|
+
return execOptions2;
|
|
3256
|
+
}
|
|
3248
3257
|
var command = {
|
|
3249
3258
|
/**
|
|
3250
3259
|
* Run an arbitrary shell command on the remote host.
|
|
@@ -3264,6 +3273,7 @@ var command = {
|
|
|
3264
3273
|
* If it exits `0`, the command is considered already done.
|
|
3265
3274
|
* @param options.name - An optional display name shown in the run output.
|
|
3266
3275
|
* @param options.secrets - Secret values that must be redacted from command output.
|
|
3276
|
+
* @param options.timeout - Maximum runtime for the command in milliseconds.
|
|
3267
3277
|
* @returns A Module that executes the shell command.
|
|
3268
3278
|
*
|
|
3269
3279
|
* @example
|
|
@@ -3285,11 +3295,7 @@ var command = {
|
|
|
3285
3295
|
if (options?.check != null && await commandCheckPassed(ssh2, options.check, secrets)) {
|
|
3286
3296
|
return { status: "ok" };
|
|
3287
3297
|
}
|
|
3288
|
-
const result = await ssh2.exec(cmd,
|
|
3289
|
-
ignoreExitCode: true,
|
|
3290
|
-
secrets,
|
|
3291
|
-
silent: true
|
|
3292
|
-
});
|
|
3298
|
+
const result = await ssh2.exec(cmd, commandExecOptions(secrets, options));
|
|
3293
3299
|
if (result.code !== 0) {
|
|
3294
3300
|
return failedCommand(`[${moduleName}] command failed`, {
|
|
3295
3301
|
...result,
|
|
@@ -10535,18 +10541,19 @@ var op = {
|
|
|
10535
10541
|
}
|
|
10536
10542
|
};
|
|
10537
10543
|
|
|
10538
|
-
// src/modules/
|
|
10539
|
-
var
|
|
10540
|
-
function
|
|
10541
|
-
|
|
10542
|
-
return { ...EXEC_OPTS12, timeout: options.timeout };
|
|
10544
|
+
// src/modules/packageVersion.ts
|
|
10545
|
+
var VERSION_EXEC_OPTS = { ignoreExitCode: true, silent: true };
|
|
10546
|
+
function isPackageSpec(value) {
|
|
10547
|
+
return "name" in value && typeof value.name === "string";
|
|
10543
10548
|
}
|
|
10544
10549
|
function splitPackagesAndOptions(values) {
|
|
10545
10550
|
const packages = [];
|
|
10546
10551
|
let options;
|
|
10547
10552
|
for (const [index, value] of values.entries()) {
|
|
10548
10553
|
if (typeof value === "string") {
|
|
10549
|
-
packages.push(value);
|
|
10554
|
+
packages.push({ name: value, version: void 0 });
|
|
10555
|
+
} else if (isPackageSpec(value)) {
|
|
10556
|
+
packages.push({ name: value.name, version: value.version });
|
|
10550
10557
|
} else if (index === values.length - 1) {
|
|
10551
10558
|
options = value;
|
|
10552
10559
|
}
|
|
@@ -10554,16 +10561,135 @@ function splitPackagesAndOptions(values) {
|
|
|
10554
10561
|
return { options, packages };
|
|
10555
10562
|
}
|
|
10556
10563
|
var PACKAGE_NAME_PATTERN = new RegExp("^[a-z0-9][a-z0-9+._\\-]*[a-z0-9.]$", "v");
|
|
10557
|
-
|
|
10564
|
+
var PACKAGE_VERSION_PATTERN = new RegExp("^[A-Za-z0-9][\\w.+~:\\-]*$", "v");
|
|
10565
|
+
function isValidPackageName(packageName) {
|
|
10566
|
+
const isSingleAlnum = packageName.length === 1 && new RegExp("^[a-z0-9]$", "v").test(packageName);
|
|
10567
|
+
return isSingleAlnum || PACKAGE_NAME_PATTERN.test(packageName);
|
|
10568
|
+
}
|
|
10569
|
+
function validatePackages(moduleName, packages) {
|
|
10558
10570
|
if (packages.length === 0) {
|
|
10559
10571
|
throw new Error(`${moduleName}: at least one package name is required`);
|
|
10560
10572
|
}
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
if (!
|
|
10564
|
-
throw new Error(`${moduleName}: invalid package name ${JSON.stringify(
|
|
10573
|
+
const seenVersions = /* @__PURE__ */ new Map();
|
|
10574
|
+
for (const { name, version } of packages) {
|
|
10575
|
+
if (!isValidPackageName(name)) {
|
|
10576
|
+
throw new Error(`${moduleName}: invalid package name ${JSON.stringify(name)}`);
|
|
10577
|
+
}
|
|
10578
|
+
if (version !== void 0 && !PACKAGE_VERSION_PATTERN.test(version)) {
|
|
10579
|
+
throw new Error(
|
|
10580
|
+
`${moduleName}: invalid package version ${JSON.stringify(version)} for ${JSON.stringify(name)}`
|
|
10581
|
+
);
|
|
10582
|
+
}
|
|
10583
|
+
if (seenVersions.has(name) && seenVersions.get(name) !== version) {
|
|
10584
|
+
throw new Error(
|
|
10585
|
+
`${moduleName}: conflicting versions requested for package ${JSON.stringify(name)}`
|
|
10586
|
+
);
|
|
10565
10587
|
}
|
|
10588
|
+
seenVersions.set(name, version);
|
|
10589
|
+
}
|
|
10590
|
+
}
|
|
10591
|
+
function installToken(pm, packageName, version) {
|
|
10592
|
+
if (version === void 0) return shellQuote(packageName);
|
|
10593
|
+
const separator = pm === "dnf" || pm === "yum" ? "-" : "=";
|
|
10594
|
+
return shellQuote(`${packageName}${separator}${version}`);
|
|
10595
|
+
}
|
|
10596
|
+
function parseApkVersion(out, packageName) {
|
|
10597
|
+
if (out.length === 0) return null;
|
|
10598
|
+
const firstToken = out.split(new RegExp("\\s+", "v"))[0] ?? "";
|
|
10599
|
+
const prefix = `${packageName}-`;
|
|
10600
|
+
if (!firstToken.startsWith(prefix)) return null;
|
|
10601
|
+
const version = firstToken.slice(prefix.length);
|
|
10602
|
+
return version.length > 0 ? version : null;
|
|
10603
|
+
}
|
|
10604
|
+
async function getInstalledVersion(ssh2, pm, packageName) {
|
|
10605
|
+
const quoted = shellQuote(packageName);
|
|
10606
|
+
if (pm === "apk") {
|
|
10607
|
+
const raw = await ssh2.output(`apk version -v ${quoted}`);
|
|
10608
|
+
return parseApkVersion(raw.trim(), packageName);
|
|
10609
|
+
}
|
|
10610
|
+
if (pm === "apt") {
|
|
10611
|
+
const raw = await ssh2.output(`dpkg-query -W -f='\${Version}' ${quoted} 2>/dev/null`);
|
|
10612
|
+
const aptVersion = raw.trim();
|
|
10613
|
+
return aptVersion.length > 0 ? aptVersion : null;
|
|
10614
|
+
}
|
|
10615
|
+
const result = await ssh2.exec(
|
|
10616
|
+
`rpm -q --qf '%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n' ${quoted}`,
|
|
10617
|
+
VERSION_EXEC_OPTS
|
|
10618
|
+
);
|
|
10619
|
+
if (result.code !== 0) return null;
|
|
10620
|
+
const lines = result.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
10621
|
+
return lines.at(-1) ?? null;
|
|
10622
|
+
}
|
|
10623
|
+
async function versionsEqual(parameters) {
|
|
10624
|
+
const { installed, pinned, pm, ssh: ssh2 } = parameters;
|
|
10625
|
+
if (pm === "apt") {
|
|
10626
|
+
return ssh2.test(`dpkg --compare-versions ${shellQuote(installed)} eq ${shellQuote(pinned)}`);
|
|
10566
10627
|
}
|
|
10628
|
+
return installed === pinned;
|
|
10629
|
+
}
|
|
10630
|
+
function describePackages(packages) {
|
|
10631
|
+
return packages.map((p) => p.version === void 0 ? p.name : `${p.name}=${p.version}`).join(", ");
|
|
10632
|
+
}
|
|
10633
|
+
function aptInstallCommand(tokens, hasPin) {
|
|
10634
|
+
return hasPin ? `DEBIAN_FRONTEND=noninteractive apt-get install -y --allow-downgrades -- ${tokens}` : `DEBIAN_FRONTEND=noninteractive apt-get install -y -- ${tokens}`;
|
|
10635
|
+
}
|
|
10636
|
+
var DOWNGRADE_COMMANDS = {
|
|
10637
|
+
dnf: (tokens) => `dnf downgrade -y -- ${tokens}`,
|
|
10638
|
+
yum: (tokens) => `yum downgrade -y -- ${tokens}`
|
|
10639
|
+
};
|
|
10640
|
+
function buildInstallCommand(baseInstall, pm, packages) {
|
|
10641
|
+
const tokens = packages.map((p) => installToken(pm, p.name, p.version)).join(" ");
|
|
10642
|
+
const hasPin = packages.some((p) => p.version !== void 0);
|
|
10643
|
+
return pm === "apt" ? aptInstallCommand(tokens, hasPin) : baseInstall(tokens);
|
|
10644
|
+
}
|
|
10645
|
+
async function installedIsHigher(ssh2, installed, pinned) {
|
|
10646
|
+
if (installed === pinned) return false;
|
|
10647
|
+
const result = await ssh2.exec(
|
|
10648
|
+
`printf '%s\\n%s\\n' ${shellQuote(pinned)} ${shellQuote(installed)} | sort -V | tail -n1`,
|
|
10649
|
+
VERSION_EXEC_OPTS
|
|
10650
|
+
);
|
|
10651
|
+
if (result.code !== 0) return false;
|
|
10652
|
+
return result.stdout.trim() === installed;
|
|
10653
|
+
}
|
|
10654
|
+
async function collectDowngradeTokens(ssh2, pm, packages) {
|
|
10655
|
+
const downgrade = [];
|
|
10656
|
+
const install = [];
|
|
10657
|
+
for (const package_ of packages) {
|
|
10658
|
+
if (package_.version === void 0) {
|
|
10659
|
+
install.push(package_);
|
|
10660
|
+
continue;
|
|
10661
|
+
}
|
|
10662
|
+
const installed = await getInstalledVersion(ssh2, pm, package_.name);
|
|
10663
|
+
if (installed !== null && await installedIsHigher(ssh2, installed, package_.version)) {
|
|
10664
|
+
downgrade.push(installToken(pm, package_.name, package_.version));
|
|
10665
|
+
} else {
|
|
10666
|
+
install.push(package_);
|
|
10667
|
+
}
|
|
10668
|
+
}
|
|
10669
|
+
return { downgrade, install };
|
|
10670
|
+
}
|
|
10671
|
+
async function runVersionedInstall(parameters) {
|
|
10672
|
+
const { baseInstall, execOpts, label, packages, pm, ssh: ssh2 } = parameters;
|
|
10673
|
+
const failure = (result) => failedCommand(`[package.installed: ${label}] package installation failed`, result);
|
|
10674
|
+
let installTargets = packages;
|
|
10675
|
+
if (pm === "dnf" || pm === "yum") {
|
|
10676
|
+
const split = await collectDowngradeTokens(ssh2, pm, packages);
|
|
10677
|
+
installTargets = split.install;
|
|
10678
|
+
if (split.downgrade.length > 0) {
|
|
10679
|
+
const down = await ssh2.exec(DOWNGRADE_COMMANDS[pm](split.downgrade.join(" ")), execOpts);
|
|
10680
|
+
if (down.code !== 0) return failure(down);
|
|
10681
|
+
}
|
|
10682
|
+
}
|
|
10683
|
+
if (installTargets.length === 0) return null;
|
|
10684
|
+
const install = await ssh2.exec(buildInstallCommand(baseInstall, pm, installTargets), execOpts);
|
|
10685
|
+
return install.code === 0 ? null : failure(install);
|
|
10686
|
+
}
|
|
10687
|
+
|
|
10688
|
+
// src/modules/package.ts
|
|
10689
|
+
var EXEC_OPTS12 = { ignoreExitCode: true, silent: true };
|
|
10690
|
+
function execOptions(options) {
|
|
10691
|
+
if (options?.timeout === void 0) return EXEC_OPTS12;
|
|
10692
|
+
return { ...EXEC_OPTS12, timeout: options.timeout };
|
|
10567
10693
|
}
|
|
10568
10694
|
var pmCache = /* @__PURE__ */ new WeakMap();
|
|
10569
10695
|
var INSTALL_COMMANDS = {
|
|
@@ -10629,35 +10755,45 @@ async function isPackageInstalled(ssh2, pm, packageName) {
|
|
|
10629
10755
|
}
|
|
10630
10756
|
}
|
|
10631
10757
|
}
|
|
10758
|
+
async function isPackageSatisfied(ssh2, pm, target) {
|
|
10759
|
+
if (target.version === void 0) {
|
|
10760
|
+
return isPackageInstalled(ssh2, pm, target.name);
|
|
10761
|
+
}
|
|
10762
|
+
const installed = await getInstalledVersion(ssh2, pm, target.name);
|
|
10763
|
+
if (installed === null) return false;
|
|
10764
|
+
return versionsEqual({ installed, pinned: target.version, pm, ssh: ssh2 });
|
|
10765
|
+
}
|
|
10632
10766
|
async function hasAnyMissingPackage(ssh2, pm, packages) {
|
|
10633
|
-
for (const
|
|
10634
|
-
if (!await
|
|
10767
|
+
for (const package_ of packages) {
|
|
10768
|
+
if (!await isPackageSatisfied(ssh2, pm, package_)) return true;
|
|
10635
10769
|
}
|
|
10636
10770
|
return false;
|
|
10637
10771
|
}
|
|
10638
10772
|
async function collectStillMissingPackages(ssh2, pm, packages) {
|
|
10639
10773
|
const stillMissing = [];
|
|
10640
|
-
for (const
|
|
10641
|
-
if (!await
|
|
10642
|
-
stillMissing.push(
|
|
10774
|
+
for (const package_ of packages) {
|
|
10775
|
+
if (!await isPackageSatisfied(ssh2, pm, package_)) {
|
|
10776
|
+
stillMissing.push(package_.name);
|
|
10643
10777
|
}
|
|
10644
10778
|
}
|
|
10645
10779
|
return stillMissing;
|
|
10646
10780
|
}
|
|
10647
10781
|
async function runInstallAndVerify(parameters) {
|
|
10648
10782
|
const { options, packages, pm, ssh: ssh2 } = parameters;
|
|
10649
|
-
const
|
|
10650
|
-
const
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10783
|
+
const label = describePackages(packages);
|
|
10784
|
+
const failure = await runVersionedInstall({
|
|
10785
|
+
baseInstall: INSTALL_COMMANDS[pm],
|
|
10786
|
+
execOpts: execOptions(options),
|
|
10787
|
+
label,
|
|
10788
|
+
packages,
|
|
10789
|
+
pm,
|
|
10790
|
+
ssh: ssh2
|
|
10791
|
+
});
|
|
10792
|
+
if (failure) return failure;
|
|
10657
10793
|
const stillMissing = await collectStillMissingPackages(ssh2, pm, packages);
|
|
10658
10794
|
if (stillMissing.length > 0) {
|
|
10659
10795
|
return failed(
|
|
10660
|
-
`[package.installed: ${
|
|
10796
|
+
`[package.installed: ${label}] packages still missing after install: ${stillMissing.join(", ")}`
|
|
10661
10797
|
);
|
|
10662
10798
|
}
|
|
10663
10799
|
return { status: "changed" };
|
|
@@ -10672,8 +10808,9 @@ var pkg = {
|
|
|
10672
10808
|
* Pass an `UpgradeOptions` object as the last argument to override the SSH
|
|
10673
10809
|
* timeout for slow remove operations.
|
|
10674
10810
|
*
|
|
10675
|
-
* @param packagesAndOptions - One or more package names
|
|
10676
|
-
*
|
|
10811
|
+
* @param packagesAndOptions - One or more package names (bare strings or
|
|
10812
|
+
* `PackageSpec` objects without a `version`), optionally followed by an
|
|
10813
|
+
* `UpgradeOptions` object as the last argument.
|
|
10677
10814
|
* @returns A Module that removes the packages if any are present.
|
|
10678
10815
|
*
|
|
10679
10816
|
* @example
|
|
@@ -10682,28 +10819,33 @@ var pkg = {
|
|
|
10682
10819
|
*/
|
|
10683
10820
|
absent(...packagesAndOptions) {
|
|
10684
10821
|
const { options, packages } = splitPackagesAndOptions(packagesAndOptions);
|
|
10685
|
-
|
|
10822
|
+
validatePackages("package.absent", packages);
|
|
10823
|
+
for (const p of packages) {
|
|
10824
|
+
if (p.version !== void 0) {
|
|
10825
|
+
throw new Error(
|
|
10826
|
+
`package.absent: version pinning is not supported (package ${JSON.stringify(p.name)})`
|
|
10827
|
+
);
|
|
10828
|
+
}
|
|
10829
|
+
}
|
|
10830
|
+
const names = packages.map((p) => p.name);
|
|
10831
|
+
const label = names.join(", ");
|
|
10686
10832
|
return {
|
|
10687
10833
|
async apply(ssh2) {
|
|
10688
|
-
if (!ssh2)
|
|
10689
|
-
return failed(`[package.absent: ${packages.join(", ")}] SSH connection is required`);
|
|
10834
|
+
if (!ssh2) return failed(`[package.absent: ${label}] SSH connection is required`);
|
|
10690
10835
|
const pm = await detectPackageManager(ssh2);
|
|
10691
|
-
if (!pm) return missingPackageManager(`package.absent: ${
|
|
10836
|
+
if (!pm) return missingPackageManager(`package.absent: ${label}`);
|
|
10692
10837
|
let anyInstalled = false;
|
|
10693
|
-
for (const packageName of
|
|
10838
|
+
for (const packageName of names) {
|
|
10694
10839
|
if (await isPackageInstalled(ssh2, pm, packageName)) {
|
|
10695
10840
|
anyInstalled = true;
|
|
10696
10841
|
break;
|
|
10697
10842
|
}
|
|
10698
10843
|
}
|
|
10699
10844
|
if (!anyInstalled) return { status: "ok" };
|
|
10700
|
-
const quoted =
|
|
10845
|
+
const quoted = names.map((p) => shellQuote(p)).join(" ");
|
|
10701
10846
|
const result = await ssh2.exec(REMOVE_COMMANDS[pm](quoted), execOptions(options));
|
|
10702
10847
|
if (result.code !== 0) {
|
|
10703
|
-
return failedCommand(
|
|
10704
|
-
`[package.absent: ${packages.join(", ")}] package removal failed`,
|
|
10705
|
-
result
|
|
10706
|
-
);
|
|
10848
|
+
return failedCommand(`[package.absent: ${label}] package removal failed`, result);
|
|
10707
10849
|
}
|
|
10708
10850
|
return { status: "changed" };
|
|
10709
10851
|
},
|
|
@@ -10711,12 +10853,12 @@ var pkg = {
|
|
|
10711
10853
|
if (!ssh2) return NEEDS_APPLY;
|
|
10712
10854
|
const pm = await detectPackageManager(ssh2);
|
|
10713
10855
|
if (!pm) return NEEDS_APPLY;
|
|
10714
|
-
for (const p of
|
|
10856
|
+
for (const p of names) {
|
|
10715
10857
|
if (await isPackageInstalled(ssh2, pm, p)) return NEEDS_APPLY;
|
|
10716
10858
|
}
|
|
10717
10859
|
return "ok";
|
|
10718
10860
|
},
|
|
10719
|
-
name: `package.absent: ${
|
|
10861
|
+
name: `package.absent: ${label}`
|
|
10720
10862
|
};
|
|
10721
10863
|
},
|
|
10722
10864
|
/**
|
|
@@ -10728,24 +10870,32 @@ var pkg = {
|
|
|
10728
10870
|
* Pass an `UpgradeOptions` object as the last argument to override the SSH
|
|
10729
10871
|
* timeout for slow install operations.
|
|
10730
10872
|
*
|
|
10731
|
-
*
|
|
10732
|
-
*
|
|
10873
|
+
* Pass a `PackageSpec` (`{ name, version }`) to pin a package to an exact
|
|
10874
|
+
* version; `check` then reports drift against the installed version and
|
|
10875
|
+
* `apply` converges on the pin, including a downgrade. No package holds are
|
|
10876
|
+
* created — only the requested version is installed.
|
|
10877
|
+
*
|
|
10878
|
+
* @param packagesAndOptions - One or more packages (bare name strings or
|
|
10879
|
+
* `PackageSpec` objects), optionally followed by an `UpgradeOptions` object
|
|
10880
|
+
* as the last argument.
|
|
10733
10881
|
* @returns A Module that installs missing packages.
|
|
10734
10882
|
*
|
|
10735
10883
|
* @example
|
|
10736
10884
|
* pkg.installed("git", "curl", "unzip")
|
|
10737
10885
|
* pkg.installed("texlive-full", { timeout: 900_000 })
|
|
10886
|
+
* pkg.installed({ name: "grafana", version: "13.1.0" })
|
|
10738
10887
|
*/
|
|
10739
10888
|
installed(...packagesAndOptions) {
|
|
10740
10889
|
const { options, packages } = splitPackagesAndOptions(packagesAndOptions);
|
|
10741
|
-
|
|
10890
|
+
validatePackages("package.installed", packages);
|
|
10891
|
+
const label = describePackages(packages);
|
|
10742
10892
|
return {
|
|
10743
10893
|
async apply(ssh2) {
|
|
10744
10894
|
if (!ssh2) {
|
|
10745
|
-
return failed(`[package.installed: ${
|
|
10895
|
+
return failed(`[package.installed: ${label}] SSH connection is required`);
|
|
10746
10896
|
}
|
|
10747
10897
|
const pm = await detectPackageManager(ssh2);
|
|
10748
|
-
if (!pm) return missingPackageManager(`package.installed: ${
|
|
10898
|
+
if (!pm) return missingPackageManager(`package.installed: ${label}`);
|
|
10749
10899
|
const anyMissing = await hasAnyMissingPackage(ssh2, pm, packages);
|
|
10750
10900
|
if (!anyMissing) return { status: "ok" };
|
|
10751
10901
|
return runInstallAndVerify({ options, packages, pm, ssh: ssh2 });
|
|
@@ -10755,11 +10905,11 @@ var pkg = {
|
|
|
10755
10905
|
const pm = await detectPackageManager(ssh2);
|
|
10756
10906
|
if (!pm) return NEEDS_APPLY;
|
|
10757
10907
|
for (const p of packages) {
|
|
10758
|
-
if (!await
|
|
10908
|
+
if (!await isPackageSatisfied(ssh2, pm, p)) return NEEDS_APPLY;
|
|
10759
10909
|
}
|
|
10760
10910
|
return "ok";
|
|
10761
10911
|
},
|
|
10762
|
-
name: `package.installed: ${
|
|
10912
|
+
name: `package.installed: ${label}`
|
|
10763
10913
|
};
|
|
10764
10914
|
},
|
|
10765
10915
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -1547,6 +1547,10 @@ var DEFAULT_TERMINAL_COLUMNS = 100;
|
|
|
1547
1547
|
var MIN_PACKAGE_COLUMNS_WIDTH = 24;
|
|
1548
1548
|
var MIN_PACKAGE_COLUMN_WIDTH = 18;
|
|
1549
1549
|
var MIN_ANIMATED_LINE_COLUMNS = 8;
|
|
1550
|
+
var ELAPSED_DISPLAY_THRESHOLD_MS = 1e3;
|
|
1551
|
+
var MILLISECONDS_PER_SECOND = 1e3;
|
|
1552
|
+
var MILLISECONDS_PER_MINUTE = 6e4;
|
|
1553
|
+
var SECONDS_PER_MINUTE = 60;
|
|
1550
1554
|
function splitPackageModuleName(name) {
|
|
1551
1555
|
for (const prefix of ["package.installed: ", "package.absent: "]) {
|
|
1552
1556
|
if (!name.startsWith(prefix)) continue;
|
|
@@ -1584,6 +1588,16 @@ function getPackageSummaryDetail(packageCount, detail) {
|
|
|
1584
1588
|
const packageLabel = packageCount === 1 ? "1 package" : `${packageCount} packages`;
|
|
1585
1589
|
return detail == null ? packageLabel : `${packageLabel} \xB7 ${detail}`;
|
|
1586
1590
|
}
|
|
1591
|
+
function formatModuleElapsed(elapsedMs) {
|
|
1592
|
+
if (!Number.isFinite(elapsedMs) || elapsedMs < ELAPSED_DISPLAY_THRESHOLD_MS) return void 0;
|
|
1593
|
+
if (elapsedMs < MILLISECONDS_PER_MINUTE) {
|
|
1594
|
+
return `${(elapsedMs / MILLISECONDS_PER_SECOND).toFixed(1)}s`;
|
|
1595
|
+
}
|
|
1596
|
+
const totalSeconds = Math.floor(elapsedMs / MILLISECONDS_PER_SECOND);
|
|
1597
|
+
const minutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE);
|
|
1598
|
+
const seconds = totalSeconds % SECONDS_PER_MINUTE;
|
|
1599
|
+
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
1600
|
+
}
|
|
1587
1601
|
function fitAnimatedModuleLine(line, columns) {
|
|
1588
1602
|
if (columns === void 0 || columns < MIN_ANIMATED_LINE_COLUMNS) return line;
|
|
1589
1603
|
const plainLine = stripVTControlCharacters(line);
|
|
@@ -1835,6 +1849,7 @@ function getSharedLiveOutputState() {
|
|
|
1835
1849
|
const existing = registry[LIVE_OUTPUT_STATE_KEY];
|
|
1836
1850
|
if (existing != null) return existing;
|
|
1837
1851
|
const created = {
|
|
1852
|
+
activeModuleStartedAt: null,
|
|
1838
1853
|
activeRecipeGuideDepths: [],
|
|
1839
1854
|
activeSpinner: null,
|
|
1840
1855
|
cursorHidden: false,
|
|
@@ -1885,7 +1900,7 @@ function getGuideDot(depth) {
|
|
|
1885
1900
|
return depth % 2 === 0 ? pc.gray("\xB7") : pc.cyan("\xB7");
|
|
1886
1901
|
}
|
|
1887
1902
|
function buildGuideIndent(baseIndent, options) {
|
|
1888
|
-
const indentCharacters =
|
|
1903
|
+
const indentCharacters = [...baseIndent];
|
|
1889
1904
|
const guideDepths = [
|
|
1890
1905
|
...options?.activeGuideDepths ?? liveOutputState.activeRecipeGuideDepths,
|
|
1891
1906
|
...options?.extraGuideDepths ?? []
|
|
@@ -1921,20 +1936,27 @@ async function withRecipeOutputScope(scopedOperation) {
|
|
|
1921
1936
|
try {
|
|
1922
1937
|
return await scopedOperation();
|
|
1923
1938
|
} finally {
|
|
1924
|
-
liveOutputState.activeRecipeGuideDepths = liveOutputState.activeRecipeGuideDepths.filter(
|
|
1939
|
+
liveOutputState.activeRecipeGuideDepths = liveOutputState.activeRecipeGuideDepths.filter(
|
|
1940
|
+
(depth) => depth !== liveOutputState.recipeOutputDepth
|
|
1941
|
+
);
|
|
1925
1942
|
liveOutputState.pendingRecipeClosureGuideDepths = [liveOutputState.recipeOutputDepth];
|
|
1926
1943
|
liveOutputState.recipeOutputDepth -= 1;
|
|
1927
1944
|
}
|
|
1928
1945
|
}
|
|
1946
|
+
function getActiveModuleElapsed() {
|
|
1947
|
+
if (liveOutputState.activeModuleStartedAt == null) return void 0;
|
|
1948
|
+
return formatModuleElapsed(Date.now() - liveOutputState.activeModuleStartedAt);
|
|
1949
|
+
}
|
|
1929
1950
|
function renderModuleLine(parameters) {
|
|
1930
|
-
const { detail, extraGuideDepths = [], name, status, waitingFrame } = parameters;
|
|
1951
|
+
const { detail, elapsed, extraGuideDepths = [], name, status, waitingFrame } = parameters;
|
|
1931
1952
|
const baseIndent = getModuleIndent();
|
|
1932
1953
|
const indent = buildGuideIndent(baseIndent, { extraGuideDepths });
|
|
1933
1954
|
const icon = getModuleIcon(status, waitingFrame);
|
|
1934
1955
|
const statusText = getModuleStatusText(status);
|
|
1935
1956
|
const detailSuffix = detail == null ? "" : ` ${pc.dim(detail)}`;
|
|
1957
|
+
const elapsedSuffix = elapsed == null ? "" : ` ${pc.dim(elapsed)}`;
|
|
1936
1958
|
const alignedNameWidth = Math.max(MODULE_NAME_WIDTH - baseIndent.length, MIN_MODULE_NAME_WIDTH);
|
|
1937
|
-
return `${indent}${icon} ${name.padEnd(alignedNameWidth)} ${statusText}${detailSuffix}`;
|
|
1959
|
+
return `${indent}${icon} ${name.padEnd(alignedNameWidth)} ${statusText}${detailSuffix}${elapsedSuffix}`;
|
|
1938
1960
|
}
|
|
1939
1961
|
function hideCursor() {
|
|
1940
1962
|
if (liveOutputState.cursorHidden) return;
|
|
@@ -1967,6 +1989,7 @@ function stopLiveModuleOutput(clearCurrentLine = false) {
|
|
|
1967
1989
|
stopAnimatedModuleLine(clearCurrentLine);
|
|
1968
1990
|
}
|
|
1969
1991
|
function startModuleSpinner(name, detail) {
|
|
1992
|
+
liveOutputState.activeModuleStartedAt = Date.now();
|
|
1970
1993
|
if (!supportsAnimatedModuleOutput()) return;
|
|
1971
1994
|
clearPendingRecipeClosureGuides();
|
|
1972
1995
|
stopAnimatedModuleLine();
|
|
@@ -1987,6 +2010,7 @@ function startModuleSpinner(name, detail) {
|
|
|
1987
2010
|
writeAnimatedModuleLine(
|
|
1988
2011
|
renderModuleLine({
|
|
1989
2012
|
detail: spinner.detail,
|
|
2013
|
+
elapsed: getActiveModuleElapsed(),
|
|
1990
2014
|
name: displayModule.name,
|
|
1991
2015
|
status: "waiting",
|
|
1992
2016
|
waitingFrame: SPINNER_FRAMES[spinner.frameIndex]
|
|
@@ -1999,6 +2023,7 @@ function startModuleSpinner(name, detail) {
|
|
|
1999
2023
|
writeAnimatedModuleLine(
|
|
2000
2024
|
renderModuleLine({
|
|
2001
2025
|
detail: displayModule.detail,
|
|
2026
|
+
elapsed: getActiveModuleElapsed(),
|
|
2002
2027
|
name: displayModule.name,
|
|
2003
2028
|
status: "waiting",
|
|
2004
2029
|
waitingFrame: SPINNER_FRAMES[0]
|
|
@@ -2011,7 +2036,10 @@ function printRecipeHeader(name) {
|
|
|
2011
2036
|
const header = pc.bold(pc.blue(`[${name}]`));
|
|
2012
2037
|
console.log(`${buildGuideIndent(getRecipeHeaderIndent())}${header}`);
|
|
2013
2038
|
if (liveOutputState.recipeOutputDepth >= 0) {
|
|
2014
|
-
liveOutputState.activeRecipeGuideDepths = [
|
|
2039
|
+
liveOutputState.activeRecipeGuideDepths = [
|
|
2040
|
+
...liveOutputState.activeRecipeGuideDepths,
|
|
2041
|
+
liveOutputState.recipeOutputDepth
|
|
2042
|
+
];
|
|
2015
2043
|
}
|
|
2016
2044
|
}
|
|
2017
2045
|
function printRunContext(parameters) {
|
|
@@ -2065,8 +2093,11 @@ function printRenderedModuleResult(parameters) {
|
|
|
2065
2093
|
status: parameters.status,
|
|
2066
2094
|
terminalColumns: process.stdout.columns
|
|
2067
2095
|
});
|
|
2096
|
+
const elapsed = getActiveModuleElapsed();
|
|
2097
|
+
liveOutputState.activeModuleStartedAt = null;
|
|
2068
2098
|
const line = renderModuleLine({
|
|
2069
2099
|
detail: displayModule.detail,
|
|
2100
|
+
elapsed,
|
|
2070
2101
|
extraGuideDepths,
|
|
2071
2102
|
name: displayModule.name,
|
|
2072
2103
|
status: parameters.status
|
|
@@ -2193,7 +2224,9 @@ function printCauseChain(error) {
|
|
|
2193
2224
|
if (visited.has(cause)) return;
|
|
2194
2225
|
visited.add(cause);
|
|
2195
2226
|
}
|
|
2196
|
-
console.error(
|
|
2227
|
+
console.error(
|
|
2228
|
+
pc.red(`${getErrorIndent()}Cause: ${maskRegisteredSecrets(formatCauseValue(cause))}`)
|
|
2229
|
+
);
|
|
2197
2230
|
if (cause instanceof CommandError) {
|
|
2198
2231
|
printVerboseCommandError(
|
|
2199
2232
|
maskRegisteredSecrets(cause.fullStdout),
|
|
@@ -6229,7 +6262,7 @@ async function runApplyCommand(file, options, run = runPlaybook) {
|
|
|
6229
6262
|
if (options.diff && !options.dryRun) {
|
|
6230
6263
|
throw new CliUsageError("--diff requires --dry-run");
|
|
6231
6264
|
}
|
|
6232
|
-
printCliHeader("0.
|
|
6265
|
+
printCliHeader("0.15.0-645d2fd");
|
|
6233
6266
|
const environmentOverrides = applyCliEnvironmentOverrides(options.env, {
|
|
6234
6267
|
firstRun: options.firstRun
|
|
6235
6268
|
});
|
|
@@ -6259,9 +6292,16 @@ function exitAfterApplyError(error, verbose) {
|
|
|
6259
6292
|
const exitCode = process.exitCode === void 0 || process.exitCode === 0 ? 2 : process.exitCode;
|
|
6260
6293
|
process.exit(exitCode);
|
|
6261
6294
|
}
|
|
6295
|
+
function renderSubcommandOptionsHelp(command) {
|
|
6296
|
+
const flagsWidth = Math.max(...command.options.map((option) => option.flags.length));
|
|
6297
|
+
const rows = command.options.map(
|
|
6298
|
+
(option) => ` ${option.flags.padEnd(flagsWidth)} ${option.description}`
|
|
6299
|
+
);
|
|
6300
|
+
return ["", `Options for "paratix ${command.name()} <file>":`, ...rows].join("\n");
|
|
6301
|
+
}
|
|
6262
6302
|
var program = new Command();
|
|
6263
|
-
program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.
|
|
6264
|
-
program.command("apply <file>").description("Apply a server definition").option(
|
|
6303
|
+
program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.15.0-645d2fd");
|
|
6304
|
+
var applyCommand = program.command("apply <file>").description("Apply a server definition").option(
|
|
6265
6305
|
"--diff",
|
|
6266
6306
|
"When combined with --dry-run, show a unified diff per module that would change.",
|
|
6267
6307
|
false
|
|
@@ -6269,7 +6309,12 @@ program.command("apply <file>").description("Apply a server definition").option(
|
|
|
6269
6309
|
"--dry-run",
|
|
6270
6310
|
"Only check, do not apply. Some modules validate prospective config but cannot verify runtime restarts.",
|
|
6271
6311
|
false
|
|
6272
|
-
).option(
|
|
6312
|
+
).option(
|
|
6313
|
+
"--env <key=value...>",
|
|
6314
|
+
"Set environment values passed to the playbook (repeatable; key=value).",
|
|
6315
|
+
collectEnvironment,
|
|
6316
|
+
{}
|
|
6317
|
+
).option("--env-file <path>", "Load environment values from a dotenv file.").option(
|
|
6273
6318
|
"--filter <names>",
|
|
6274
6319
|
"Run only the named recipes/modules (comma-separated, repeatable). Every other node is shown as skipped.",
|
|
6275
6320
|
collectFilter,
|
|
@@ -6302,6 +6347,7 @@ program.command("apply <file>").description("Apply a server definition").option(
|
|
|
6302
6347
|
exitAfterApplyError(error, options.verbose);
|
|
6303
6348
|
}
|
|
6304
6349
|
});
|
|
6350
|
+
program.addHelpText("after", () => renderSubcommandOptionsHelp(applyCommand));
|
|
6305
6351
|
function parsePositiveNumber(value, options = {}) {
|
|
6306
6352
|
const parsed = Number(value);
|
|
6307
6353
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -6396,6 +6442,7 @@ export {
|
|
|
6396
6442
|
parsePositiveNumber,
|
|
6397
6443
|
parseReconnectTimeoutSeconds,
|
|
6398
6444
|
printExceptionError,
|
|
6445
|
+
renderSubcommandOptionsHelp,
|
|
6399
6446
|
resetLastResortHandlerForTests,
|
|
6400
6447
|
resetTsxRegistrationForTests,
|
|
6401
6448
|
resolveFilteredRun,
|
|
@@ -339,6 +339,27 @@ type UpgradeOptions = {
|
|
|
339
339
|
/** Override the SSH layer's command timeout (milliseconds). */
|
|
340
340
|
timeout?: number;
|
|
341
341
|
};
|
|
342
|
+
/**
|
|
343
|
+
* A package to install, optionally pinned to an exact version.
|
|
344
|
+
*
|
|
345
|
+
* A bare string package name is equivalent to `{ name }` (no version pin). The
|
|
346
|
+
* package-manager-specific version syntax (`name=version`, `name-version`) is
|
|
347
|
+
* never accepted as a pass-through string — it is built from the structured
|
|
348
|
+
* `name`/`version` fields so the raw PM syntax can never leak into a playbook.
|
|
349
|
+
*/
|
|
350
|
+
type PackageSpec = {
|
|
351
|
+
/** Package name (validated against {@link PACKAGE_NAME_PATTERN}). */
|
|
352
|
+
name: string;
|
|
353
|
+
/**
|
|
354
|
+
* Exact version to pin to. When set, `check` reports drift against the
|
|
355
|
+
* installed version and `apply` converges on this version (including a
|
|
356
|
+
* downgrade). When omitted, only presence is enforced.
|
|
357
|
+
*/
|
|
358
|
+
version?: string;
|
|
359
|
+
};
|
|
360
|
+
/** Argument accepted by the variadic `installed`/`absent` package methods. */
|
|
361
|
+
type PackageArgument = PackageSpec | string | UpgradeOptions;
|
|
362
|
+
|
|
342
363
|
/**
|
|
343
364
|
* Distro-agnostic package management module.
|
|
344
365
|
*
|
|
@@ -367,15 +388,16 @@ declare const pkg: {
|
|
|
367
388
|
* Pass an `UpgradeOptions` object as the last argument to override the SSH
|
|
368
389
|
* timeout for slow remove operations.
|
|
369
390
|
*
|
|
370
|
-
* @param packagesAndOptions - One or more package names
|
|
371
|
-
*
|
|
391
|
+
* @param packagesAndOptions - One or more package names (bare strings or
|
|
392
|
+
* `PackageSpec` objects without a `version`), optionally followed by an
|
|
393
|
+
* `UpgradeOptions` object as the last argument.
|
|
372
394
|
* @returns A Module that removes the packages if any are present.
|
|
373
395
|
*
|
|
374
396
|
* @example
|
|
375
397
|
* pkg.absent("vim", "nano")
|
|
376
398
|
* pkg.absent("vim", "nano", { timeout: 600_000 })
|
|
377
399
|
*/
|
|
378
|
-
absent(...packagesAndOptions:
|
|
400
|
+
absent(...packagesAndOptions: PackageArgument[]): Module;
|
|
379
401
|
/**
|
|
380
402
|
* Ensure the given packages are installed.
|
|
381
403
|
*
|
|
@@ -385,15 +407,22 @@ declare const pkg: {
|
|
|
385
407
|
* Pass an `UpgradeOptions` object as the last argument to override the SSH
|
|
386
408
|
* timeout for slow install operations.
|
|
387
409
|
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
410
|
+
* Pass a `PackageSpec` (`{ name, version }`) to pin a package to an exact
|
|
411
|
+
* version; `check` then reports drift against the installed version and
|
|
412
|
+
* `apply` converges on the pin, including a downgrade. No package holds are
|
|
413
|
+
* created — only the requested version is installed.
|
|
414
|
+
*
|
|
415
|
+
* @param packagesAndOptions - One or more packages (bare name strings or
|
|
416
|
+
* `PackageSpec` objects), optionally followed by an `UpgradeOptions` object
|
|
417
|
+
* as the last argument.
|
|
390
418
|
* @returns A Module that installs missing packages.
|
|
391
419
|
*
|
|
392
420
|
* @example
|
|
393
421
|
* pkg.installed("git", "curl", "unzip")
|
|
394
422
|
* pkg.installed("texlive-full", { timeout: 900_000 })
|
|
423
|
+
* pkg.installed({ name: "grafana", version: "13.1.0" })
|
|
395
424
|
*/
|
|
396
|
-
installed(...packagesAndOptions:
|
|
425
|
+
installed(...packagesAndOptions: PackageArgument[]): Module;
|
|
397
426
|
/**
|
|
398
427
|
* Refresh the package manager's package lists once per dated flag.
|
|
399
428
|
*
|
|
@@ -583,6 +612,7 @@ declare const command: {
|
|
|
583
612
|
* If it exits `0`, the command is considered already done.
|
|
584
613
|
* @param options.name - An optional display name shown in the run output.
|
|
585
614
|
* @param options.secrets - Secret values that must be redacted from command output.
|
|
615
|
+
* @param options.timeout - Maximum runtime for the command in milliseconds.
|
|
586
616
|
* @returns A Module that executes the shell command.
|
|
587
617
|
*
|
|
588
618
|
* @example
|
|
@@ -595,6 +625,7 @@ declare const command: {
|
|
|
595
625
|
check?: string;
|
|
596
626
|
name?: string;
|
|
597
627
|
secrets?: string[];
|
|
628
|
+
timeout?: number;
|
|
598
629
|
}): Module;
|
|
599
630
|
};
|
|
600
631
|
|
|
@@ -2106,4 +2137,4 @@ declare const user: {
|
|
|
2106
2137
|
present(name: string, options?: UserOptions): Module;
|
|
2107
2138
|
};
|
|
2108
2139
|
|
|
2109
|
-
export { op as A, pkg as B, quadlet as C, releaseUpgrade as D, type Environment as E, rsync as F, script as G, service as H, ssh as I, sshd as J, swap as K, sysctl as L, type Module as M, NEEDS_APPLY as N, type OrchestrationStep as O, system as P, systemd as Q, timer as R, type SshdPortMetaEntry as S, ufw as T, user as U, type
|
|
2140
|
+
export { op as A, pkg as B, quadlet as C, releaseUpgrade as D, type Environment as E, rsync as F, script as G, service as H, ssh as I, sshd as J, swap as K, sysctl as L, type Module as M, NEEDS_APPLY as N, type OrchestrationStep as O, system as P, systemd as Q, timer as R, type SshdPortMetaEntry as S, ufw as T, user as U, type PackageSpec as V, type UpgradeOptions as W, type ModuleMetaEntry as a, type MetaEnvironmentValue as b, type EnvironmentMetaEntry as c, type SystemHostMetaEntry as d, type SystemRebootMetaEntry as e, type ModuleResult as f, type ExecResult as g, type ModuleStatus as h, type SshConnection as i, type ShutdownSignal as j, type ServerDefinition as k, type EnvironmentValue as l, type ExecOptions as m, type SshConfig as n, apt as o, archive as p, command as q, compose as r, cron as s, download as t, file as u, git as v, group as w, hostname as x, mount as y, net as z };
|
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-Da9ccU5K.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-Da9ccU5K.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Fail the run if a condition on the current env is not satisfied.
|
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-GB5QZWUM.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),
|
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 PackageSpec, W 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-Da9ccU5K.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
|
|
|
@@ -267,12 +277,12 @@ that are expected to respond more slowly.
|
|
|
267
277
|
|
|
268
278
|
Import with renaming: `import { package as pkg } from "paratix/modules"`. The word `package` is reserved in JavaScript, so you must alias it.
|
|
269
279
|
|
|
270
|
-
| Method | Signature
|
|
271
|
-
| ------------------- |
|
|
272
|
-
| `package.installed` | `(...packagesAndOptions: Array<string \| UpgradeOptions>): Module` | Yes |
|
|
273
|
-
| `package.absent` | `(...packagesAndOptions: Array<string \| UpgradeOptions>): Module` | Yes |
|
|
274
|
-
| `package.update` | `(date: string, options?: UpgradeOptions): Module`
|
|
275
|
-
| `package.upgrade` | `(date: string, options?: UpgradeOptions): Module`
|
|
280
|
+
| Method | Signature | Idempotent |
|
|
281
|
+
| ------------------- | --------------------------------------------------------------------------------- | -------------------- |
|
|
282
|
+
| `package.installed` | `(...packagesAndOptions: Array<string \| PackageSpec \| UpgradeOptions>): Module` | Yes |
|
|
283
|
+
| `package.absent` | `(...packagesAndOptions: Array<string \| PackageSpec \| UpgradeOptions>): Module` | Yes |
|
|
284
|
+
| `package.update` | `(date: string, options?: UpgradeOptions): Module` | Yes (versioned flag) |
|
|
285
|
+
| `package.upgrade` | `(date: string, options?: UpgradeOptions): Module` | Yes (versioned flag) |
|
|
276
286
|
|
|
277
287
|
#### `UpgradeOptions`
|
|
278
288
|
|
|
@@ -283,10 +293,10 @@ type UpgradeOptions = {
|
|
|
283
293
|
}
|
|
284
294
|
```
|
|
285
295
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
296
|
+
Für `package.installed` / `package.absent` wird das Options-Objekt als **letztes** Argument
|
|
297
|
+
nach den Paketnamen übergeben; bestehende variadische Aufrufe wie `pkg.installed("git", "curl")`
|
|
298
|
+
funktionieren unverändert weiter. Verwende einen höheren `timeout` für langsame Operationen auf
|
|
299
|
+
Produktionsservern mit großem Update-Rückstand:
|
|
290
300
|
|
|
291
301
|
```typescript
|
|
292
302
|
pkg.upgrade("2026-05-01", { timeout: 900_000 })
|
|
@@ -294,6 +304,57 @@ pkg.installed("texlive-full", { timeout: 900_000 })
|
|
|
294
304
|
apt.distUpgrade("2026-05-01", { timeout: 1_200_000 })
|
|
295
305
|
```
|
|
296
306
|
|
|
307
|
+
#### `PackageSpec` — Versions-Pinning
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
type PackageSpec = {
|
|
311
|
+
name: string
|
|
312
|
+
version?: string // Exakte Zielversion. Ohne `version` wird nur die Anwesenheit erzwungen.
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Ein Paket kann auf eine exakte Version festgenagelt werden, indem statt eines Strings ein
|
|
317
|
+
`PackageSpec`-Objekt übergeben wird. Ein bloßer String `"grafana"` ist äquivalent zu
|
|
318
|
+
`{ name: "grafana" }` (kein Pin). Die paketmanager-spezifische Syntax (`name=version`,
|
|
319
|
+
`name-version`) wird **niemals** als Pass-through-String akzeptiert — ein String mit `=` bleibt
|
|
320
|
+
ein ungültiger Paketname. Die Argumente werden über ihre **Form** unterschieden, nicht über die
|
|
321
|
+
Position: ein Objekt **mit** `name`-Feld ist ein `PackageSpec` (darf auch an letzter Stelle
|
|
322
|
+
stehen), ein Objekt **ohne** `name`-Feld an letzter Position ist das `UpgradeOptions`-Objekt.
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
pkg.installed({ name: "grafana", version: "13.1.0" })
|
|
326
|
+
pkg.installed("curl", { name: "grafana", version: "13.1.0" }, { timeout: 600_000 })
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Übersetzung des Install-Tokens je Paketmanager:
|
|
330
|
+
|
|
331
|
+
| Paketmanager | Install-Token | Beispiel |
|
|
332
|
+
| ------------ | -------------- | ---------------------- |
|
|
333
|
+
| apt | `name=version` | `grafana=13.1.0` |
|
|
334
|
+
| apk | `name=version` | `grafana=13.1.0-r0` |
|
|
335
|
+
| dnf / yum | `name-version` | `grafana-13.1.0-1.el9` |
|
|
336
|
+
|
|
337
|
+
Semantik:
|
|
338
|
+
|
|
339
|
+
- **`check`** vergleicht die installierte gegen die gepinnte Version; jede Abweichung ergibt
|
|
340
|
+
`needs-apply`. Ohne gepinnte Version bleibt es eine reine Anwesenheitsprüfung.
|
|
341
|
+
- **`apply`** installiert exakt die gepinnte Version — auch als **Downgrade**. Für apt wird bei
|
|
342
|
+
gepinnter Version automatisch `--allow-downgrades` ergänzt; für dnf/yum wird bei einer bereits
|
|
343
|
+
installierten höheren Version `dnf downgrade` / `yum downgrade` statt `install` verwendet
|
|
344
|
+
(apk installiert die exakte Version auch abwärts ohne Extra-Flag). Die Richtungsbestimmung für
|
|
345
|
+
dnf/yum nutzt einen versionsbewussten Vergleich auf dem Zielhost (`sort -V`), damit mehrstellige
|
|
346
|
+
Komponenten wie `13.9.0` vs. `13.10.0` korrekt geordnet werden.
|
|
347
|
+
- Nach der Installation wird zusätzlich die Version verifiziert: eine Teil- oder Falschversions-
|
|
348
|
+
Installation meldet `failed`, nicht `changed`.
|
|
349
|
+
- **Kein Hold.** Es wird ausschließlich die Version installiert — es werden **keine**
|
|
350
|
+
`apt-mark hold`-Marker und **keine** `preferences.d`-Dateien angelegt. Ohne einen erneuten
|
|
351
|
+
Lauf mit gepinnter Version kann ein späterer `apt upgrade` das Paket also wieder anheben.
|
|
352
|
+
|
|
353
|
+
Gültige Versionen beginnen alphanumerisch und dürfen `[A-Za-z0-9]` sowie `. + ~ : _ -` enthalten
|
|
354
|
+
(deckt apt-Epoch/Revision wie `1:2.3-1ubuntu0.2` und rpm-`version-release` wie `13.1.0-1.el9` ab).
|
|
355
|
+
Whitespace und Shell-Metazeichen werden abgelehnt. `package.absent` ist rein namensbasiert; ein
|
|
356
|
+
`PackageSpec` mit gesetzter `version` führt dort zu einem klaren Fehler.
|
|
357
|
+
|
|
297
358
|
### `quadlet`
|
|
298
359
|
|
|
299
360
|
| Method | Signature | Idempotent |
|
|
@@ -342,7 +403,7 @@ rsync SSH process and does not depend on a local `known_hosts` entry.
|
|
|
342
403
|
| `ssh.authorizedKeys` | `(user: string, key: string, options?: { state?: "absent" \| "present" }): Module` | Yes |
|
|
343
404
|
| `ssh.knownHosts` | `(host: string, options?: { expectedFingerprint?: string; port?: number; publicKey?: string; state?: "absent" \| "present" }): Module` | Yes |
|
|
344
405
|
|
|
345
|
-
**Trust
|
|
406
|
+
**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
407
|
|
|
347
408
|
### `sshd`
|
|
348
409
|
|
|
@@ -666,7 +727,7 @@ fail("This branch should be unreachable")
|
|
|
666
727
|
|
|
667
728
|
### `firstRun.stop(message?)`
|
|
668
729
|
|
|
669
|
-
|
|
730
|
+
Stops the current run cleanly when Paratix was started with `--first-run`. Useful as an explicit staging boundary in bootstrap playbooks.
|
|
670
731
|
|
|
671
732
|
```typescript
|
|
672
733
|
firstRun.stop("Bootstrap foundation complete; rerun without --first-run to continue.")
|
|
@@ -674,7 +735,7 @@ firstRun.stop("Bootstrap foundation complete; rerun without --first-run to conti
|
|
|
674
735
|
|
|
675
736
|
### `signals.flush(message?)`
|
|
676
737
|
|
|
677
|
-
|
|
738
|
+
Immediately runs all currently pending signals in the active scope and clears their pending state.
|
|
678
739
|
|
|
679
740
|
```typescript
|
|
680
741
|
signals.flush("Reload services before the next bootstrap stage")
|
|
@@ -742,20 +803,20 @@ Rules:
|
|
|
742
803
|
|
|
743
804
|
Diff-producing built-in modules:
|
|
744
805
|
|
|
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
|
|
806
|
+
| Module | Diff content |
|
|
807
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
808
|
+
| `file.copy` | Unified diff between the remote file and the local source. |
|
|
809
|
+
| `file.template` | Unified diff between the remote file and the rendered template. |
|
|
810
|
+
| `sysctl.set` | `key = old → new` for the desired configuration. |
|
|
811
|
+
| `hostname.set` | `hostname = old → new`. |
|
|
812
|
+
| `swap.file` | Diff of the `/etc/fstab` entry (or removal of the line for `state: "absent"`). |
|
|
813
|
+
| `swap.swappiness` | `vm.swappiness = old → new` (via `sysctl.set`). |
|
|
814
|
+
| `swap.vfsCachePressure` | `vm.vfs_cache_pressure = old → new` (via `sysctl.set`). |
|
|
815
|
+
| `cron.job` / `cron.absent` | Unified diff of the target user's crontab. |
|
|
816
|
+
| `timer.scheduled` (present) | Concatenated diffs of the `.service` and `.timer` unit files. |
|
|
817
|
+
| `timer.scheduled` (absent) / `timer.absent` | List of unit files to remove. |
|
|
818
|
+
| `net.hosts` | Unified diff of `/etc/hosts`. |
|
|
819
|
+
| `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
820
|
|
|
760
821
|
### Implementing a Diff for a Custom Module
|
|
761
822
|
|
|
@@ -810,7 +871,7 @@ The diff string is plain text — no ANSI codes. The output layer applies colors
|
|
|
810
871
|
4. For idempotency with `command.shell()`, always provide a `check` command.
|
|
811
872
|
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
873
|
6. Use `service.restart()` and `service.reload()` as `signals` in recipes, not directly in `run`.
|
|
813
|
-
7. Use `signals.flush()` only
|
|
874
|
+
7. Use `signals.flush()` only as an explicit checkpoint when staged flows require an early signal flush.
|
|
814
875
|
8. Always pass a date string to `package.upgrade()` and `package.update()` -- it is the idempotency key.
|
|
815
876
|
9. Specify `ssh.ports` as an array -- the runner tries each port in order.
|
|
816
877
|
10. Custom modules must implement both `check` and `apply`, both async.
|
|
@@ -831,9 +892,9 @@ The diff string is plain text — no ANSI codes. The output layer applies colors
|
|
|
831
892
|
9. Do NOT use template syntax `{{key}}` in TypeScript code -- templates are only for files rendered via `file.template()`.
|
|
832
893
|
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
894
|
11. Do NOT treat `signals.flush()` as a global queue flush -- it only affects the current scope.
|
|
834
|
-
12. `signals.flush()`
|
|
835
|
-
-
|
|
836
|
-
-
|
|
895
|
+
12. `signals.flush()` always flushes only the current scope:
|
|
896
|
+
- inside a recipe, that recipe's signals
|
|
897
|
+
- at the top level, `server(...).signals`
|
|
837
898
|
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
899
|
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
900
|
15. Do NOT return loose `meta: { ... }` maps from custom modules -- always use typed meta entries.
|