paratix 0.10.0 → 0.12.2
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 +7 -1
- package/dist/chunk-YOSHYUST.js +19058 -0
- package/dist/chunk-YOSHYUST.js.map +1 -0
- package/dist/cli.js +5091 -224
- package/dist/cli.js.map +1 -1
- package/dist/{user-CJDqZC8n.d.ts → index-udpAybq3.d.ts} +637 -36
- package/dist/index.d.ts +51 -7
- package/dist/index.js +965 -73
- package/dist/index.js.map +1 -1
- package/dist/modules/index.d.ts +1 -119
- package/dist/modules/index.js +1 -2
- package/llm-guide.md +176 -35
- package/package.json +10 -8
- package/dist/chunk-47PTUZZR.js +0 -495
- package/dist/chunk-47PTUZZR.js.map +0 -1
- package/dist/chunk-M7GETOJ5.js +0 -6237
- package/dist/chunk-M7GETOJ5.js.map +0 -1
- package/dist/chunk-NRDLYHJL.js +0 -1866
- package/dist/chunk-NRDLYHJL.js.map +0 -1
- package/dist/cli.d.ts +0 -62
- package/dist/types-Cl2Muw1x.d.ts +0 -254
package/llm-guide.md
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
apt,
|
|
50
50
|
archive,
|
|
51
51
|
command,
|
|
52
|
+
compose,
|
|
52
53
|
cron,
|
|
53
54
|
download,
|
|
54
55
|
file,
|
|
@@ -56,10 +57,13 @@ import {
|
|
|
56
57
|
group,
|
|
57
58
|
hostname,
|
|
58
59
|
mount,
|
|
60
|
+
net,
|
|
59
61
|
op,
|
|
60
62
|
package as pkg,
|
|
63
|
+
quadlet,
|
|
61
64
|
releaseUpgrade,
|
|
62
65
|
rsync,
|
|
66
|
+
script,
|
|
63
67
|
service,
|
|
64
68
|
ssh,
|
|
65
69
|
sshd,
|
|
@@ -146,9 +150,9 @@ export default server({
|
|
|
146
150
|
|
|
147
151
|
### `command`
|
|
148
152
|
|
|
149
|
-
| Method | Signature
|
|
150
|
-
| --------------- |
|
|
151
|
-
| `command.shell` | `(cmd: string, options?: { check?: string; name?: string }): Module` | Only with `check` |
|
|
153
|
+
| Method | Signature | Idempotent |
|
|
154
|
+
| --------------- | ---------------------------------------------------------------------------------------- | ----------------- |
|
|
155
|
+
| `command.shell` | `(cmd: string, options?: { check?: string; name?: string; secrets?: string[] }): Module` | Only with `check` |
|
|
152
156
|
|
|
153
157
|
### `cron`
|
|
154
158
|
|
|
@@ -174,11 +178,22 @@ the idiomatic way to ensure a previously installed cron job is gone.
|
|
|
174
178
|
|
|
175
179
|
### `download`
|
|
176
180
|
|
|
177
|
-
| Method | Signature
|
|
178
|
-
| ----------------- |
|
|
179
|
-
| `download.url` | `(destination: string, url: string, options?: { force?: boolean; headers?: Record<string, string>; sha256?: string; mode?: string; owner?: string; group?: string }): Module` | Yes |
|
|
180
|
-
| `download.github` | `(destination: string, options: { repo: string; tag: string; asset: string; token?: string; sha256?: string; mode?: string; owner?: string; group?: string }): Module`
|
|
181
|
-
| `download.large` | `(destination: string, url: string, options?: { group?: string; headers?: Record<string, string>; mode?: string; owner?: string; sha256?: string }): Module` | Yes (flag) |
|
|
181
|
+
| Method | Signature | Idempotent |
|
|
182
|
+
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
183
|
+
| `download.url` | `(destination: string, url: string, options?: { allowInsecureHttp?: boolean; allowInsecureHttpHeaders?: boolean; allowUnverifiedDownload?: boolean; connectTimeout?: number; force?: boolean; headers?: Record<string, string>; sha256?: string; mode?: string; owner?: string; group?: string; timeout?: number }): Module` | Yes |
|
|
184
|
+
| `download.github` | `(destination: string, options: { repo: string; tag: string; asset: string; token?: string; allowUnverifiedDownload?: boolean; connectTimeout?: number; sha256?: string; mode?: string; owner?: string; group?: string; timeout?: number }): Module` | Yes |
|
|
185
|
+
| `download.large` | `(destination: string, url: string, options?: { allowInsecureHttp?: boolean; allowInsecureHttpHeaders?: boolean; allowUnverifiedDownload?: boolean; connectTimeout?: number; group?: string; headers?: Record<string, string>; mode?: string; owner?: string; sha256?: string; timeout?: number }): Module` | Yes (flag) |
|
|
186
|
+
|
|
187
|
+
Downloads require an explicit integrity decision at runtime: provide `sha256`
|
|
188
|
+
for verification, or set `allowUnverifiedDownload: true` when the remote
|
|
189
|
+
artifact is intentionally trusted without a pinned digest. `download.url` and
|
|
190
|
+
`download.large` allow only HTTPS by default; set `allowInsecureHttp: true`
|
|
191
|
+
only when an `http://` source or redirect is intentional. Sensitive headers
|
|
192
|
+
such as `Authorization`, `Cookie`, and `X-Api-Key` are still rejected over
|
|
193
|
+
plaintext HTTP unless `allowInsecureHttpHeaders: true` is also set.
|
|
194
|
+
Curl-based downloads use bounded transfer timing by default (`connectTimeout:
|
|
195
|
+
10000`, `timeout: 300000`, both in milliseconds). Override these options for
|
|
196
|
+
very slow artifact hosts or large downloads.
|
|
182
197
|
|
|
183
198
|
### `file`
|
|
184
199
|
|
|
@@ -197,6 +212,10 @@ the idiomatic way to ensure a previously installed cron job is gone.
|
|
|
197
212
|
| `file.stat` | `(remotePath: string): Module` | No (always-applies) |
|
|
198
213
|
| `file.template` | `(remotePath: string, templatePath: string, options?: { mode?: string; owner?: string; strict?: boolean }): Module` | Yes |
|
|
199
214
|
|
|
215
|
+
**Hinweis zu `file.copy` und Default-Mode:** Wenn `options.mode` weggelassen wird, setzt `file.copy` den Modus auf den dokumentierten Default `0644`. Der Modus wird sowohl beim Hochladen (`uploadFile` mit `{ mode }`) als auch in `check` gegen den Soll-Wert verglichen, damit nachfolgende Läufe Mode-Drift (z. B. manuelles `chmod 0600`) als `needs-apply` erkennen. Wer ein restriktiveres Recht braucht (z. B. für Secrets), übergibt explizit `{ mode: "0600" }`.
|
|
216
|
+
|
|
217
|
+
**Hinweis zu `file.line`:** Der `line`-Wert muss eine einzelne Zeile ohne CR/LF sein. Für mehrzeilige Inhalte `file.block` verwenden.
|
|
218
|
+
|
|
200
219
|
### `git`
|
|
201
220
|
|
|
202
221
|
| Method | Signature | Idempotent |
|
|
@@ -223,6 +242,21 @@ the idiomatic way to ensure a previously installed cron job is gone.
|
|
|
223
242
|
| `mount.present` | `(options: { fstype: string; opts: string; path: string; persist?: boolean; src: string }): Module` | Yes |
|
|
224
243
|
| `mount.absent` | `(options: { path: string; persist?: boolean }): Module` | Yes |
|
|
225
244
|
|
|
245
|
+
### `net`
|
|
246
|
+
|
|
247
|
+
| Method | Signature | Idempotent |
|
|
248
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
249
|
+
| `net.hosts` | `(ip: string, hostnames: string[], options?: { state?: "absent" \| "present" }): Module` | Yes |
|
|
250
|
+
| `net.interface` | `(name: string, options: InterfaceOptions): Module` | Yes |
|
|
251
|
+
| `net.request` | `(url: string, options?: { allowInsecureHttpHeaders?: boolean; body?: string; connectTimeout?: number; headers?: Record<string, string>; method?: string; status?: number; timeout?: number }): Module` | Yes |
|
|
252
|
+
| `net.resolv` | `(options: { nameservers: string[]; search?: string[] }): Module` | Yes |
|
|
253
|
+
| `net.route` | `(destination: string, gateway: string, options?: { device?: string; state?: "absent" \| "present" }): Module` | Yes |
|
|
254
|
+
| `net.waitFor` | `(options: WaitForOptions): Module` | Yes |
|
|
255
|
+
|
|
256
|
+
`net.request` uses bounded curl timing by default (`connectTimeout: 10000`,
|
|
257
|
+
`timeout: 300000`, both in milliseconds). Override these values for endpoints
|
|
258
|
+
that are expected to respond more slowly.
|
|
259
|
+
|
|
226
260
|
### `op`
|
|
227
261
|
|
|
228
262
|
| Method | Signature | Idempotent |
|
|
@@ -269,9 +303,9 @@ apt.distUpgrade("2026-05-01", { timeout: 1_200_000 })
|
|
|
269
303
|
|
|
270
304
|
### `releaseUpgrade`
|
|
271
305
|
|
|
272
|
-
| Method | Signature
|
|
273
|
-
| ------------------------ |
|
|
274
|
-
| `releaseUpgrade.upgrade` | `(options?: { dryRun?: boolean; resolveHost?: () => Promise<string
|
|
306
|
+
| Method | Signature | Idempotent |
|
|
307
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------- | ---------- |
|
|
308
|
+
| `releaseUpgrade.upgrade` | `(options?: { dryRun?: boolean; resolveHost?: () => Promise<string>; timeout?: number }): Module` | Yes |
|
|
275
309
|
|
|
276
310
|
### `rsync`
|
|
277
311
|
|
|
@@ -283,6 +317,12 @@ When the active Paratix SSH session already verified the host via `ssh.expectedH
|
|
|
283
317
|
or `ssh.expectedHostPublicKey`, `rsync.sync()` reuses that verified host key for the external
|
|
284
318
|
rsync SSH process and does not depend on a local `known_hosts` entry.
|
|
285
319
|
|
|
320
|
+
### `script`
|
|
321
|
+
|
|
322
|
+
| Method | Signature | Idempotent |
|
|
323
|
+
| ------------- | -------------------------------------------------------------------------------------------- | -------------------- |
|
|
324
|
+
| `script.once` | `(name: string, localPath: string, options?: { args?: string[]; version?: string }): Module` | Yes (versioned flag) |
|
|
325
|
+
|
|
286
326
|
### `service`
|
|
287
327
|
|
|
288
328
|
| Method | Signature | Idempotent |
|
|
@@ -302,6 +342,8 @@ rsync SSH process and does not depend on a local `known_hosts` entry.
|
|
|
302
342
|
| `ssh.authorizedKeys` | `(user: string, key: string, options?: { state?: "absent" \| "present" }): Module` | Yes |
|
|
303
343
|
| `ssh.knownHosts` | `(host: string, options?: { expectedFingerprint?: string; port?: number; publicKey?: string; state?: "absent" \| "present" }): Module` | Yes |
|
|
304
344
|
|
|
345
|
+
**Trust-Anchor-Pflicht für `ssh.knownHosts` (state `present`):** Im Default-State `present` muss entweder `expectedFingerprint` oder `publicKey` gesetzt sein. Ohne Trust Anchor wirft der Modul-Konstruktor sofort, denn `check` würde sonst jeden vorhandenen `known_hosts`-Eintrag (auch ältere, möglicherweise kompromittierte TOFU-Akzeptanzen) als „ok" werten und den Anchor-Vergleich überspringen. Für `state: "absent"` ist kein Anchor nötig — dort wird der Eintrag ohnehin entfernt.
|
|
346
|
+
|
|
305
347
|
### `sshd`
|
|
306
348
|
|
|
307
349
|
| Method | Signature | Idempotent |
|
|
@@ -319,9 +361,9 @@ rsync SSH process and does not depend on a local `known_hosts` entry.
|
|
|
319
361
|
|
|
320
362
|
### `sysctl`
|
|
321
363
|
|
|
322
|
-
| Method | Signature
|
|
323
|
-
| ------------ |
|
|
324
|
-
| `sysctl.set` | `(key: string, value: string, options?: { state?: "absent" \| "present" }): Module` | Yes
|
|
364
|
+
| Method | Signature | Idempotent |
|
|
365
|
+
| ------------ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
366
|
+
| `sysctl.set` | `(key: string, value: string, options?: { resetValue?: string; state?: "absent" \| "present" }): Module` | Yes for `present`. For `absent`: only the persistence file is removed by default; pass `resetValue` to also restore the live runtime value. |
|
|
325
367
|
|
|
326
368
|
### `system`
|
|
327
369
|
|
|
@@ -411,7 +453,7 @@ function myCustomModule(configPath: string, content: string): Module {
|
|
|
411
453
|
|
|
412
454
|
async apply(ssh: SshConnection | null, env: Environment): Promise<ModuleResult> {
|
|
413
455
|
if (!ssh) return failed(`[my-module: ${configPath}] SSH connection is required`)
|
|
414
|
-
await ssh.writeFile(configPath, content)
|
|
456
|
+
await ssh.writeFile(configPath, content, { mode: "0644" })
|
|
415
457
|
return {
|
|
416
458
|
meta: [meta.env("MY_MODULE_PATH", configPath)],
|
|
417
459
|
status: "changed",
|
|
@@ -439,23 +481,23 @@ function myCustomModule(configPath: string, content: string): Module {
|
|
|
439
481
|
|
|
440
482
|
Methods available on the `ssh` parameter:
|
|
441
483
|
|
|
442
|
-
| Method
|
|
443
|
-
|
|
|
444
|
-
| `ssh.exec(cmd, options?)`
|
|
445
|
-
| `ssh.test(cmd)`
|
|
446
|
-
| `ssh.output(cmd)`
|
|
447
|
-
| `ssh.lines(cmd)`
|
|
448
|
-
| `ssh.exists(path)`
|
|
449
|
-
| `ssh.readFile(path)`
|
|
450
|
-
| `ssh.writeFile(path, content)`
|
|
451
|
-
| `ssh.uploadFile(local, remote)`
|
|
452
|
-
| `ssh.downloadFile(remote, local)`
|
|
453
|
-
| `ssh.sha256(path)`
|
|
454
|
-
| `ssh.addPort(port)`
|
|
455
|
-
| `ssh.disconnect()`
|
|
456
|
-
| `ssh.getConnectionInfo()`
|
|
457
|
-
| `ssh.probeSudo()`
|
|
458
|
-
| `ssh.updateHost(host)`
|
|
484
|
+
| Method | Return type | Description |
|
|
485
|
+
| ------------------------------------------------ | --------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
486
|
+
| `ssh.exec(cmd, options?)` | `Promise<ExecResult>` | Run command, get `{ code, stdout, stderr }`. Throws on non-zero unless `ignoreExitCode: true`. |
|
|
487
|
+
| `ssh.test(cmd)` | `Promise<boolean>` | Run command, return `true` if exit code is 0. |
|
|
488
|
+
| `ssh.output(cmd)` | `Promise<string>` | Run command, return trimmed stdout. |
|
|
489
|
+
| `ssh.lines(cmd)` | `Promise<string[]>` | Run command, return stdout split into lines. |
|
|
490
|
+
| `ssh.exists(path)` | `Promise<boolean>` | Check if remote path exists. |
|
|
491
|
+
| `ssh.readFile(path)` | `Promise<string>` | Read remote file content. |
|
|
492
|
+
| `ssh.writeFile(path, content, { mode: "0644" })` | `Promise<void>` | Write content to remote file. `mode` is required; choose an explicit file mode. |
|
|
493
|
+
| `ssh.uploadFile(local, remote)` | `Promise<void>` | Upload local file via SFTP. |
|
|
494
|
+
| `ssh.downloadFile(remote, local)` | `Promise<void>` | Download remote file. |
|
|
495
|
+
| `ssh.sha256(path)` | `Promise<string \| null>` | Get SHA-256 hex digest, or null if not found. |
|
|
496
|
+
| `ssh.addPort(port)` | `void` | Register an additional port opened on the remote host (advanced). |
|
|
497
|
+
| `ssh.disconnect()` | `void` | Close the SSH connection. |
|
|
498
|
+
| `ssh.getConnectionInfo()` | `{ host, port, privateKeyPath?, user }` | Return current connection parameters. |
|
|
499
|
+
| `ssh.probeSudo()` | `Promise<void>` | Probe/cache sudo access; prompts interactively if needed. |
|
|
500
|
+
| `ssh.updateHost(host)` | `void` | Update the target host address (e.g., after IP change). |
|
|
459
501
|
|
|
460
502
|
### ExecOptions
|
|
461
503
|
|
|
@@ -463,12 +505,18 @@ Methods available on the `ssh` parameter:
|
|
|
463
505
|
{
|
|
464
506
|
env?: Record<string, string> // Extra environment variables
|
|
465
507
|
ignoreExitCode?: boolean // Don't throw on non-zero exit
|
|
508
|
+
input?: string // Payload written to stdin before EOF
|
|
509
|
+
maxOutputBytes?: number // Cap stored stdout/stderr bytes
|
|
466
510
|
secrets?: string[] // Strings to mask in error output
|
|
467
511
|
silent?: boolean // Suppress stdout/stderr
|
|
468
512
|
timeout?: number // Abort after N milliseconds
|
|
469
513
|
}
|
|
470
514
|
```
|
|
471
515
|
|
|
516
|
+
Prefer `input` for secret payloads instead of shell arguments, so values do not
|
|
517
|
+
leak through process lists, `/proc/<pid>/cmdline`, or SSH logs. Add the same
|
|
518
|
+
values to `secrets` only as an additional masking guard for error output.
|
|
519
|
+
|
|
472
520
|
## Template System
|
|
473
521
|
|
|
474
522
|
Files deployed via `file.template(remotePath, localTemplatePath)` can contain `{{KEY}}` placeholders.
|
|
@@ -553,7 +601,7 @@ Conditionally run modules based on package state on the remote host.
|
|
|
553
601
|
|
|
554
602
|
```typescript
|
|
555
603
|
when.packageInstalled("ufw", ufw.disabled())
|
|
556
|
-
when.packageAbsent("docker-ce",
|
|
604
|
+
when.packageAbsent("docker-ce", pkg.installed("docker-ce"))
|
|
557
605
|
```
|
|
558
606
|
|
|
559
607
|
### `when.commandExists(name, ...modules)` / `when.commandMissing(name, ...modules)`
|
|
@@ -562,7 +610,7 @@ Conditionally run modules based on whether a command exists on the remote host.
|
|
|
562
610
|
|
|
563
611
|
```typescript
|
|
564
612
|
when.commandExists("docker", service.running("docker"))
|
|
565
|
-
when.commandMissing("docker",
|
|
613
|
+
when.commandMissing("docker", pkg.installed("docker-ce"))
|
|
566
614
|
```
|
|
567
615
|
|
|
568
616
|
### Filesystem guard variants
|
|
@@ -661,6 +709,97 @@ async check(ssh) {
|
|
|
661
709
|
}
|
|
662
710
|
```
|
|
663
711
|
|
|
712
|
+
## Dry-Run Diff Output (`--diff`)
|
|
713
|
+
|
|
714
|
+
Paratix runs a playbook in dry-run mode via `paratix apply <file> --dry-run`, which
|
|
715
|
+
reports per-module `changed (dry-run)` or `ok` based on each module's `check()`.
|
|
716
|
+
|
|
717
|
+
`--diff` enables a second, opt-in layer: modules that mark themselves as diff
|
|
718
|
+
producers also render a unified-diff block under their status line so the user
|
|
719
|
+
sees _what_ would change, not only _that_ something would change.
|
|
720
|
+
|
|
721
|
+
```
|
|
722
|
+
$ paratix apply server.ts --dry-run --diff
|
|
723
|
+
↺ /etc/ssh/sshd_config changed (dry-run)
|
|
724
|
+
│ --- /etc/ssh/sshd_config
|
|
725
|
+
│ +++ /local/path/sshd_config
|
|
726
|
+
│ -Port 22
|
|
727
|
+
│ +Port 2222
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
Rules:
|
|
731
|
+
|
|
732
|
+
- `--diff` requires `--dry-run`. The CLI rejects `paratix apply <file> --diff`
|
|
733
|
+
without `--dry-run` before any SSH connection or playbook side effect.
|
|
734
|
+
- Without `--diff`, the dry-run runs are byte-identical to before: no extra
|
|
735
|
+
remote round-trips, no diff output.
|
|
736
|
+
- Modules that do not opt in keep showing only `changed (dry-run)`.
|
|
737
|
+
- Every diff line is masked through the registered-secret sink and a terminal
|
|
738
|
+
sanitizer before printing, so secret-laden file contents never leak verbatim.
|
|
739
|
+
- `_dryRunDetail` (inline suffix) and `diff` (multi-line block) are independent
|
|
740
|
+
output layers — a module may emit both at once; the runner renders detail next
|
|
741
|
+
to the status line and the diff below it.
|
|
742
|
+
|
|
743
|
+
Diff-producing built-in modules:
|
|
744
|
+
|
|
745
|
+
| Modul | Diff-Inhalt |
|
|
746
|
+
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
747
|
+
| `file.copy` | Unified diff zwischen Remote-Datei und lokaler Quelle. |
|
|
748
|
+
| `file.template` | Unified diff zwischen Remote-Datei und gerendertem Template. |
|
|
749
|
+
| `sysctl.set` | `key = old → new` für die Soll-Konfiguration. |
|
|
750
|
+
| `hostname.set` | `hostname = old → new`. |
|
|
751
|
+
| `swap.file` | Diff des `/etc/fstab`-Eintrags (oder Entfernung der Zeile bei `state: "absent"`). |
|
|
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 der Crontab des Ziel-Users. |
|
|
755
|
+
| `timer.scheduled` (present) | Konkatenierte Diffs der `.service`- und `.timer`-Unit-Dateien. |
|
|
756
|
+
| `timer.scheduled` (absent) / `timer.absent` | Liste der zu entfernenden Unit-Dateien. |
|
|
757
|
+
| `net.hosts` | Unified diff von `/etc/hosts`. |
|
|
758
|
+
| `quadlet.container` | Unified diff der `.container`-Unit-Datei. Wenn die Datei bereits passt, das `daemon-reload`-Flag aber fehlt, erscheint zusätzlich `(dry-run, daemon-reload pending)` als Detail-Suffix. |
|
|
759
|
+
|
|
760
|
+
### Implementing a Diff for a Custom Module
|
|
761
|
+
|
|
762
|
+
A module participates in `--diff` output by setting `_dryRunDiffProducer: true`
|
|
763
|
+
and implementing `_applyDryRun`. The runner calls `_applyDryRun` only when the
|
|
764
|
+
user passed `--diff` and `check()` returned `"needs-apply"`.
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
import { buildUnifiedDiff } from "paratix/modules" // not exported publicly today
|
|
768
|
+
// or, for scalar drift:
|
|
769
|
+
import { buildKeyValueDiff } from "paratix/modules"
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
return {
|
|
774
|
+
_dryRunDiffProducer: true,
|
|
775
|
+
async _applyDryRun(ssh) {
|
|
776
|
+
if (!ssh) return { status: "changed" }
|
|
777
|
+
try {
|
|
778
|
+
const current = (await ssh.exists(remotePath)) ? await ssh.readFile(remotePath) : ""
|
|
779
|
+
const diff = buildUnifiedDiff(current, desired, {
|
|
780
|
+
currentLabel: remotePath,
|
|
781
|
+
desiredLabel: localPath,
|
|
782
|
+
})
|
|
783
|
+
return diff === "" ? { status: "changed" } : { diff, status: "changed" }
|
|
784
|
+
} catch {
|
|
785
|
+
// A read failure must not abort the dry-run. Drop the diff and fall back
|
|
786
|
+
// to the generic "(dry-run)" suffix.
|
|
787
|
+
return { status: "changed" }
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
async check(ssh) {
|
|
791
|
+
/* unchanged */
|
|
792
|
+
},
|
|
793
|
+
async apply(ssh) {
|
|
794
|
+
/* unchanged */
|
|
795
|
+
},
|
|
796
|
+
name: "myModule: ...",
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
The diff string is plain text — no ANSI codes. The output layer applies colors
|
|
801
|
+
(`-` red, `+` green, headers dimmed) and indentation.
|
|
802
|
+
|
|
664
803
|
## Do's and Don'ts
|
|
665
804
|
|
|
666
805
|
### DO
|
|
@@ -698,6 +837,7 @@ async check(ssh) {
|
|
|
698
837
|
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.
|
|
699
838
|
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.
|
|
700
839
|
15. Do NOT return loose `meta: { ... }` maps from custom modules -- always use typed meta entries.
|
|
840
|
+
16. Do NOT set `ssh.sudoPassword` to a value from `op.resolve()` or `meta.env()` -- `SshConfig` is frozen at server construction time, before any module in `run` executes. Use a static source like `process.env.SUDO_PASSWORD` or a variable populated before the server definition is imported.
|
|
701
841
|
|
|
702
842
|
## Testing Patterns
|
|
703
843
|
|
|
@@ -743,7 +883,8 @@ describe("myModule", () => {
|
|
|
743
883
|
### `createMockSsh` behavior
|
|
744
884
|
|
|
745
885
|
- Accepts `Record<string, Partial<ExecResult>>` mapping command strings to responses.
|
|
746
|
-
-
|
|
886
|
+
- Unknown commands fail closed with an error. Add every expected command to the
|
|
887
|
+
response map, or the helper throws instead of returning a successful default.
|
|
747
888
|
- `ssh.test(cmd)` returns `code === 0`.
|
|
748
889
|
- `ssh.exists(path)` delegates to `ssh.test("[ -e '<path>' ]")`.
|
|
749
890
|
- `ssh.readFile(path)` delegates to `ssh.output("cat '<path>'")`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paratix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Idempotent VPS setup tool in TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -15,17 +15,17 @@
|
|
|
15
15
|
"paratix": "./dist/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"commander": "^
|
|
18
|
+
"commander": "^15.0.0",
|
|
19
19
|
"picocolors": "^1.1.1",
|
|
20
20
|
"ssh2": "^1.16.0",
|
|
21
|
-
"tsx": "^4.
|
|
21
|
+
"tsx": "^4.22.3"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@types/node": "
|
|
24
|
+
"@types/node": "24.12.4",
|
|
25
25
|
"@types/ssh2": "^1.15.4",
|
|
26
26
|
"tsup": "^8.4.0",
|
|
27
|
-
"typescript": "^
|
|
28
|
-
"vitest": "^
|
|
27
|
+
"typescript": "^6.0.3",
|
|
28
|
+
"vitest": "^4.1.7"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=24.0.0"
|
|
@@ -55,8 +55,10 @@
|
|
|
55
55
|
"license": "MIT",
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsup",
|
|
58
|
-
"test": "
|
|
59
|
-
"test:
|
|
58
|
+
"test": "pnpm test:unit && pnpm test:dist",
|
|
59
|
+
"test:dist": "pnpm build && vitest run --config vitest.distribution.config.ts",
|
|
60
|
+
"test:integration": "pnpm build && vitest run --config vitest.integration.config.ts",
|
|
61
|
+
"test:unit": "vitest run",
|
|
60
62
|
"test:watch": "vitest"
|
|
61
63
|
}
|
|
62
64
|
}
|