paratix 0.13.0 → 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 +14 -10
- package/dist/{chunk-XIRORNO3.js → chunk-475OT32R.js} +11 -5
- package/dist/cli.js +57 -10
- package/dist/{index-DwKdTyHo.d.ts → index-DlZISXJx.d.ts} +2 -0
- 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 +38 -28
- 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,
|
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.14.0-5a61f29");
|
|
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.14.0-5a61f29");
|
|
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,
|
|
@@ -583,6 +583,7 @@ declare const command: {
|
|
|
583
583
|
* If it exits `0`, the command is considered already done.
|
|
584
584
|
* @param options.name - An optional display name shown in the run output.
|
|
585
585
|
* @param options.secrets - Secret values that must be redacted from command output.
|
|
586
|
+
* @param options.timeout - Maximum runtime for the command in milliseconds.
|
|
586
587
|
* @returns A Module that executes the shell command.
|
|
587
588
|
*
|
|
588
589
|
* @example
|
|
@@ -595,6 +596,7 @@ declare const command: {
|
|
|
595
596
|
check?: string;
|
|
596
597
|
name?: string;
|
|
597
598
|
secrets?: string[];
|
|
599
|
+
timeout?: number;
|
|
598
600
|
}): Module;
|
|
599
601
|
};
|
|
600
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.
|
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),
|
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
|
|
|
@@ -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.
|