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/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 | Idempotent |
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 | Idempotent |
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` | Yes |
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 | Idempotent |
273
- | ------------------------ | ------------------------------------------------------------------------------- | ---------- |
274
- | `releaseUpgrade.upgrade` | `(options?: { dryRun?: boolean; resolveHost?: () => Promise<string> }): Module` | Yes |
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 | Idempotent |
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 | Return type | Description |
443
- | --------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------- |
444
- | `ssh.exec(cmd, options?)` | `Promise<ExecResult>` | Run command, get `{ code, stdout, stderr }`. Throws on non-zero unless `ignoreExitCode: true`. |
445
- | `ssh.test(cmd)` | `Promise<boolean>` | Run command, return `true` if exit code is 0. |
446
- | `ssh.output(cmd)` | `Promise<string>` | Run command, return trimmed stdout. |
447
- | `ssh.lines(cmd)` | `Promise<string[]>` | Run command, return stdout split into lines. |
448
- | `ssh.exists(path)` | `Promise<boolean>` | Check if remote path exists. |
449
- | `ssh.readFile(path)` | `Promise<string>` | Read remote file content. |
450
- | `ssh.writeFile(path, content)` | `Promise<void>` | Write content to remote file. |
451
- | `ssh.uploadFile(local, remote)` | `Promise<void>` | Upload local file via SFTP. |
452
- | `ssh.downloadFile(remote, local)` | `Promise<void>` | Download remote file. |
453
- | `ssh.sha256(path)` | `Promise<string \| null>` | Get SHA-256 hex digest, or null if not found. |
454
- | `ssh.addPort(port)` | `void` | Register an additional port opened on the remote host (advanced). |
455
- | `ssh.disconnect()` | `void` | Close the SSH connection. |
456
- | `ssh.getConnectionInfo()` | `{ host, port, privateKeyPath?, user }` | Return current connection parameters. |
457
- | `ssh.probeSudo()` | `Promise<void>` | Probe/cache sudo access; prompts interactively if needed. |
458
- | `ssh.updateHost(host)` | `void` | Update the target host address (e.g., after IP change). |
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", package.installed("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", package.installed("docker-ce"))
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
- - Default response: `{ code: 0, stdout: "", stderr: "" }`.
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.10.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": "^13.1.0",
18
+ "commander": "^15.0.0",
19
19
  "picocolors": "^1.1.1",
20
20
  "ssh2": "^1.16.0",
21
- "tsx": "^4.19.0"
21
+ "tsx": "^4.22.3"
22
22
  },
23
23
  "devDependencies": {
24
- "@types/node": "^25.5.0",
24
+ "@types/node": "24.12.4",
25
25
  "@types/ssh2": "^1.15.4",
26
26
  "tsup": "^8.4.0",
27
- "typescript": "^5.8.0",
28
- "vitest": "^3.1.0"
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": "vitest run",
59
- "test:integration": "vitest run --config vitest.integration.config.ts",
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
  }