paratix 0.15.1 → 0.16.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.
@@ -3446,19 +3446,8 @@ async function resolveComposeConfigImages(parameters) {
3446
3446
  `${composeCommand(parameters.runtime, parameters.projectDirectory)} config --format json`,
3447
3447
  EXEC_OPTS4
3448
3448
  );
3449
- if (result.code !== 0) {
3450
- return failedCommand(
3451
- `[compose.pull] failed to resolve images for ${parameters.projectDirectory}`,
3452
- result
3453
- );
3454
- }
3455
- const images = parseComposeConfigImages(result.stdout);
3456
- if (images === null) {
3457
- return failed(
3458
- `[compose.pull] failed to parse compose config images for ${parameters.projectDirectory}`
3459
- );
3460
- }
3461
- return images;
3449
+ if (result.code !== 0) return null;
3450
+ return parseComposeConfigImages(result.stdout);
3462
3451
  }
3463
3452
  async function inspectComposeImageBeforePull(parameters) {
3464
3453
  const result = await parameters.ssh.exec(
@@ -4194,7 +4183,15 @@ var compose = {
4194
4183
  runtime,
4195
4184
  ssh: connection
4196
4185
  });
4197
- if (!Array.isArray(images)) return images;
4186
+ if (images === null) {
4187
+ const fallbackResult = await connection.exec(
4188
+ `${composeCommand(runtime, projectDirectory)} pull 2>&1`,
4189
+ EXEC_OPTS4
4190
+ );
4191
+ if (fallbackResult.code !== 0)
4192
+ return failedCommand(`[compose.pull] failed for ${projectDirectory}`, fallbackResult);
4193
+ return { status: "changed" };
4194
+ }
4198
4195
  const beforePull = await snapshotComposeImagesBeforePull({
4199
4196
  images,
4200
4197
  runtime,
@@ -11030,10 +11027,11 @@ async function snapshotQuadletFile(ssh2, filePath) {
11030
11027
  mode: normalizeQuadletMode(rawMode)
11031
11028
  };
11032
11029
  }
11033
- async function restoreQuadletFileSnapshot(ssh2, filePath, snapshot) {
11030
+ async function restoreQuadletFileSnapshot(parameters) {
11031
+ const { filePath, label, snapshot, ssh: ssh2 } = parameters;
11034
11032
  if (snapshot.exists) {
11035
11033
  if (await isSymlink(ssh2, filePath)) {
11036
- throw new Error(`[quadlet.container] refuses to restore through symlink at ${filePath}`);
11034
+ throw new Error(`[${label}] refuses to restore through symlink at ${filePath}`);
11037
11035
  }
11038
11036
  await ssh2.writeFile(filePath, snapshot.content, { mode: snapshot.mode });
11039
11037
  return;
@@ -11041,24 +11039,30 @@ async function restoreQuadletFileSnapshot(ssh2, filePath, snapshot) {
11041
11039
  await ssh2.exec(`rm -f ${shellQuote(filePath)}`, { ignoreExitCode: true, silent: true });
11042
11040
  }
11043
11041
  async function applyQuadletFile(parameters) {
11042
+ const label = parameters.label ?? "quadlet.container";
11044
11043
  const mkdirResult = await parameters.ssh.exec(CONTAINERS_SYSTEMD_DIRECTORY_COMMAND, {
11045
11044
  ignoreExitCode: true,
11046
11045
  silent: true
11047
11046
  });
11048
11047
  if (mkdirResult.code !== 0) {
11049
11048
  return failedCommand(
11050
- `[quadlet.container: ${parameters.name}] failed to create quadlet directory`,
11049
+ `[${label}: ${parameters.name}] failed to create quadlet directory`,
11051
11050
  mkdirResult
11052
11051
  );
11053
11052
  }
11054
11053
  if (await isSymlink(parameters.ssh, parameters.filePath)) {
11055
11054
  return failed(
11056
- `[quadlet.container: ${parameters.name}] refuses to write through symlink at ${parameters.filePath}`
11055
+ `[${label}: ${parameters.name}] refuses to write through symlink at ${parameters.filePath}`
11057
11056
  );
11058
11057
  }
11059
11058
  const snapshot = await snapshotQuadletFile(parameters.ssh, parameters.filePath);
11060
11059
  const restoreSnapshot = async () => {
11061
- await restoreQuadletFileSnapshot(parameters.ssh, parameters.filePath, snapshot);
11060
+ await restoreQuadletFileSnapshot({
11061
+ filePath: parameters.filePath,
11062
+ label,
11063
+ snapshot,
11064
+ ssh: parameters.ssh
11065
+ });
11062
11066
  };
11063
11067
  try {
11064
11068
  await parameters.ssh.writeFile(parameters.filePath, parameters.content, {
@@ -11067,7 +11071,7 @@ async function applyQuadletFile(parameters) {
11067
11071
  } catch (error) {
11068
11072
  const reason = error instanceof Error ? error.message : String(error);
11069
11073
  return withRollbackFailure(
11070
- failed(`[quadlet.container: ${parameters.name}] failed to write quadlet file: ${reason}`),
11074
+ failed(`[${label}: ${parameters.name}] failed to write quadlet file: ${reason}`),
11071
11075
  restoreSnapshot,
11072
11076
  "quadlet apply failed"
11073
11077
  );
@@ -11078,10 +11082,7 @@ async function applyQuadletFile(parameters) {
11078
11082
  });
11079
11083
  if (daemonReload.code === 0) return { status: "changed" };
11080
11084
  return withRollbackFailure(
11081
- failedCommand(
11082
- `[quadlet.container: ${parameters.name}] systemctl daemon-reload failed`,
11083
- daemonReload
11084
- ),
11085
+ failedCommand(`[${label}: ${parameters.name}] systemctl daemon-reload failed`, daemonReload),
11085
11086
  restoreSnapshot,
11086
11087
  "quadlet apply failed"
11087
11088
  );
@@ -11117,7 +11118,7 @@ var QUADLET_PULL_CHANGED_OUTPUT_PATTERNS = [
11117
11118
  ];
11118
11119
  function assertQuadletLineValue(key, value) {
11119
11120
  if (QUADLET_CONTROL_CHARACTER_PATTERN.test(value)) {
11120
- throw new Error(`quadlet.container ${key} values must not contain control characters`);
11121
+ throw new Error(`quadlet ${key} values must not contain control characters`);
11121
11122
  }
11122
11123
  }
11123
11124
  function renderQuadletLine(key, value) {
@@ -11350,6 +11351,37 @@ function parseQuadletImageInspectOutput(output) {
11350
11351
  };
11351
11352
  }
11352
11353
 
11354
+ // src/modules/quadletNetworkHelpers.ts
11355
+ function getQuadletNetworkFilePath(name) {
11356
+ return `${CONTAINERS_SYSTEMD_DIRECTORY}/${name}.network`;
11357
+ }
11358
+ function buildQuadletNetworkUnitLines(options) {
11359
+ return compactQuadletLines([
11360
+ renderQuadletLine("NetworkName", options.name),
11361
+ maybeRenderQuadletLine("Driver", options.driver),
11362
+ maybeRenderQuadletLine("IPAMDriver", options.ipamDriver),
11363
+ maybeRenderQuadletBool("Internal", options.internal),
11364
+ maybeRenderQuadletBool("IPv6", options.ipv6),
11365
+ maybeRenderQuadletBool("DisableDNS", options.disableDns),
11366
+ maybeRenderQuadletLine("Subnet", options.subnet),
11367
+ maybeRenderQuadletLine("Gateway", options.gateway),
11368
+ maybeRenderQuadletLine("IPRange", options.ipRange),
11369
+ ...renderQuadletRepeated("DNS", options.dns ?? []),
11370
+ ...renderQuadletKeyValue("Options", options.options ?? {}),
11371
+ ...renderQuadletKeyValue("Label", options.label ?? {}),
11372
+ ...renderQuadletRepeated("PodmanArgs", options.podmanArgs ?? [])
11373
+ ]);
11374
+ }
11375
+ function generateNetworkQuadlet(options) {
11376
+ const sections = [
11377
+ renderQuadletSection("Unit", [
11378
+ renderQuadletLine("Description", options.description ?? `Podman network: ${options.name}`)
11379
+ ]),
11380
+ renderQuadletSection("Network", buildQuadletNetworkUnitLines(options))
11381
+ ];
11382
+ return sections.join("\n").trimEnd();
11383
+ }
11384
+
11353
11385
  // src/modules/quadletValidationHelpers.ts
11354
11386
  var UNIT_NAME_PATTERN2 = new RegExp("^[\\w@.\\-]+$", "v");
11355
11387
  var UNIT_NAME_ALL_DOTS_PATTERN = new RegExp("^\\.+$", "v");
@@ -11424,8 +11456,8 @@ function validateQuadletAuthFilePath(field, value) {
11424
11456
  // src/modules/quadlet.ts
11425
11457
  var QUADLET_RELOAD_HASH_LENGTH = 16;
11426
11458
  var SYSTEMCTL2 = "systemctl";
11427
- function buildQuadletReloadFlag(name, content) {
11428
- const flagPrefix = `quadlet-container-${sha256String(name).slice(0, QUADLET_RELOAD_HASH_LENGTH)}-`;
11459
+ function buildQuadletReloadFlag(kind, name, content) {
11460
+ const flagPrefix = `quadlet-${kind}-${sha256String(name).slice(0, QUADLET_RELOAD_HASH_LENGTH)}-`;
11429
11461
  return {
11430
11462
  flagName: `${flagPrefix}${sha256String(content).slice(0, QUADLET_RELOAD_HASH_LENGTH)}`,
11431
11463
  flagPrefix
@@ -11507,7 +11539,7 @@ async function applyQuadletImageUpdate(parameters) {
11507
11539
  ssh: parameters.ssh
11508
11540
  });
11509
11541
  }
11510
- async function buildQuadletContainerDryRunDiff(ssh2, filePath, desired) {
11542
+ async function buildQuadletDryRunDiff(ssh2, filePath, desired) {
11511
11543
  try {
11512
11544
  const exists = await ssh2.exists(filePath);
11513
11545
  const current = exists ? await ssh2.readFile(filePath) : "";
@@ -11518,6 +11550,16 @@ async function buildQuadletContainerDryRunDiff(ssh2, filePath, desired) {
11518
11550
  return void 0;
11519
11551
  }
11520
11552
  }
11553
+ async function podmanNetworkExists(ssh2, name) {
11554
+ const result = await ssh2.exec(`podman network exists -- ${shellQuote(name)}`, {
11555
+ ignoreExitCode: true,
11556
+ silent: true
11557
+ });
11558
+ return result.code === 0;
11559
+ }
11560
+ function quadletNetworkLiveHint(name) {
11561
+ return `network '${name}' already exists; the running network is not recreated automatically \u2014 remove it to apply changed settings`;
11562
+ }
11521
11563
  var quadlet = {
11522
11564
  /**
11523
11565
  * Write a Podman Quadlet `.container` definition and reload systemd when it changes.
@@ -11533,11 +11575,11 @@ var quadlet = {
11533
11575
  validateQuadletImageValue("image", options.image);
11534
11576
  const filePath = getQuadletContainerFilePath(options.name);
11535
11577
  const content = generateContainerQuadlet(options);
11536
- const reloadFlag = buildQuadletReloadFlag(options.name, content);
11578
+ const reloadFlag = buildQuadletReloadFlag("container", options.name, content);
11537
11579
  return {
11538
11580
  async _applyDryRun(ssh2) {
11539
11581
  if (!ssh2) return { status: "changed" };
11540
- const diff = await buildQuadletContainerDryRunDiff(ssh2, filePath, content);
11582
+ const diff = await buildQuadletDryRunDiff(ssh2, filePath, content);
11541
11583
  if (diff != null) return { diff, status: "changed" };
11542
11584
  const flagPresent = await hasFlag(ssh2, reloadFlag.flagName);
11543
11585
  if (flagPresent) return { status: "changed" };
@@ -11561,6 +11603,67 @@ var quadlet = {
11561
11603
  name: `quadlet.container: ${options.name}`
11562
11604
  };
11563
11605
  },
11606
+ /**
11607
+ * Write a Podman Quadlet `.network` definition and reload systemd when it changes.
11608
+ *
11609
+ * Mirrors {@link quadlet.container}: the unit is written atomically to
11610
+ * `/etc/containers/systemd/<name>.network`, symlinks are refused, and
11611
+ * `systemctl daemon-reload` runs when the content changes. Containers
11612
+ * reference the network via their existing `networks` option
11613
+ * (e.g. `networks: ["app.network"]`).
11614
+ *
11615
+ * When the change lands and a network with the same name is already live on
11616
+ * the host, the result carries an advisory detail: Podman does not recreate
11617
+ * a running network, so changed `subnet`/`gateway`/… only take effect after
11618
+ * the network is removed.
11619
+ *
11620
+ * @param options - Configuration for the Quadlet network definition.
11621
+ * @returns A Module that ensures the Quadlet `.network` file is present and up to date.
11622
+ */
11623
+ network(options) {
11624
+ validateQuadletName(options.name);
11625
+ const filePath = getQuadletNetworkFilePath(options.name);
11626
+ const content = generateNetworkQuadlet(options);
11627
+ const reloadFlag = buildQuadletReloadFlag("network", options.name, content);
11628
+ return {
11629
+ async _applyDryRun(ssh2) {
11630
+ if (!ssh2) return { status: "changed" };
11631
+ const diff = await buildQuadletDryRunDiff(ssh2, filePath, content);
11632
+ if (diff != null) {
11633
+ const liveHint = await podmanNetworkExists(ssh2, options.name) ? quadletNetworkLiveHint(options.name) : void 0;
11634
+ return liveHint == null ? { diff, status: "changed" } : { _dryRunDetail: liveHint, diff, status: "changed" };
11635
+ }
11636
+ const flagPresent = await hasFlag(ssh2, reloadFlag.flagName);
11637
+ return flagPresent ? { status: "changed" } : { _dryRunDetail: "(dry-run, daemon-reload pending)", status: "changed" };
11638
+ },
11639
+ _dryRunDiffProducer: true,
11640
+ async apply(ssh2) {
11641
+ if (!ssh2) return failed(`[quadlet.network: ${options.name}] SSH connection is required`);
11642
+ const contentChanges = await buildQuadletDryRunDiff(ssh2, filePath, content) != null;
11643
+ const result = await applyQuadletFile({
11644
+ content,
11645
+ filePath,
11646
+ label: "quadlet.network",
11647
+ name: options.name,
11648
+ ssh: ssh2
11649
+ });
11650
+ if (result.status !== "changed") return result;
11651
+ const flagFailure = await setVersionedFlag(ssh2, reloadFlag.flagName, reloadFlag.flagPrefix);
11652
+ if (flagFailure) return flagFailure;
11653
+ if (contentChanges && await podmanNetworkExists(ssh2, options.name)) {
11654
+ return { ...result, detail: quadletNetworkLiveHint(options.name) };
11655
+ }
11656
+ return result;
11657
+ },
11658
+ async check(ssh2) {
11659
+ if (!ssh2) return NEEDS_APPLY;
11660
+ const fileResult = await checkQuadletFile({ content, filePath, ssh: ssh2 });
11661
+ if (fileResult !== "ok") return fileResult;
11662
+ return await hasFlag(ssh2, reloadFlag.flagName) ? "ok" : NEEDS_APPLY;
11663
+ },
11664
+ name: `quadlet.network: ${options.name}`
11665
+ };
11666
+ },
11564
11667
  /**
11565
11668
  * Pull the latest image for a Quadlet-managed container and restart the service
11566
11669
  * only when the image changed.
package/dist/cli.js CHANGED
@@ -6262,7 +6262,7 @@ async function runApplyCommand(file, options, run = runPlaybook) {
6262
6262
  if (options.diff && !options.dryRun) {
6263
6263
  throw new CliUsageError("--diff requires --dry-run");
6264
6264
  }
6265
- printCliHeader("0.15.1-0840e3c");
6265
+ printCliHeader("0.16.0-81f051d");
6266
6266
  const environmentOverrides = applyCliEnvironmentOverrides(options.env, {
6267
6267
  firstRun: options.firstRun
6268
6268
  });
@@ -6300,7 +6300,7 @@ function renderSubcommandOptionsHelp(command) {
6300
6300
  return ["", `Options for "paratix ${command.name()} <file>":`, ...rows].join("\n");
6301
6301
  }
6302
6302
  var program = new Command();
6303
- program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.15.1-0840e3c");
6303
+ program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.16.0-81f051d");
6304
6304
  var applyCommand = program.command("apply <file>").description("Apply a server definition").option(
6305
6305
  "--diff",
6306
6306
  "When combined with --dry-run, show a unified diff per module that would change.",
@@ -1466,6 +1466,23 @@ type QuadletImageUpdateOptions = {
1466
1466
  serviceName?: string;
1467
1467
  };
1468
1468
 
1469
+ type QuadletNetworkOptions = {
1470
+ description?: string;
1471
+ disableDns?: boolean;
1472
+ dns?: string[];
1473
+ driver?: string;
1474
+ gateway?: string;
1475
+ internal?: boolean;
1476
+ ipamDriver?: string;
1477
+ ipRange?: string;
1478
+ ipv6?: boolean;
1479
+ label?: Record<string, string>;
1480
+ name: string;
1481
+ options?: Record<string, string>;
1482
+ podmanArgs?: string[];
1483
+ subnet?: string;
1484
+ };
1485
+
1469
1486
  /**
1470
1487
  * Modules for managing Podman Quadlet definitions.
1471
1488
  *
@@ -1484,6 +1501,24 @@ declare const quadlet: {
1484
1501
  * @returns A Module that ensures the Quadlet file is present and up to date.
1485
1502
  */
1486
1503
  container(options: QuadletContainerOptions): Module;
1504
+ /**
1505
+ * Write a Podman Quadlet `.network` definition and reload systemd when it changes.
1506
+ *
1507
+ * Mirrors {@link quadlet.container}: the unit is written atomically to
1508
+ * `/etc/containers/systemd/<name>.network`, symlinks are refused, and
1509
+ * `systemctl daemon-reload` runs when the content changes. Containers
1510
+ * reference the network via their existing `networks` option
1511
+ * (e.g. `networks: ["app.network"]`).
1512
+ *
1513
+ * When the change lands and a network with the same name is already live on
1514
+ * the host, the result carries an advisory detail: Podman does not recreate
1515
+ * a running network, so changed `subnet`/`gateway`/… only take effect after
1516
+ * the network is removed.
1517
+ *
1518
+ * @param options - Configuration for the Quadlet network definition.
1519
+ * @returns A Module that ensures the Quadlet `.network` file is present and up to date.
1520
+ */
1521
+ network(options: QuadletNetworkOptions): Module;
1487
1522
  /**
1488
1523
  * Pull the latest image for a Quadlet-managed container and restart the service
1489
1524
  * only when the image changed.
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-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';
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-N3n0FmKh.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-N3n0FmKh.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-C5KXNY6R.js";
64
+ } from "./chunk-56SN6NQH.js";
65
65
 
66
66
  // src/conditionalModules.ts
67
67
  function createConditionalApplyState(environment) {
@@ -1 +1 @@
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';
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-N3n0FmKh.js';
@@ -27,7 +27,7 @@ import {
27
27
  timer,
28
28
  ufw,
29
29
  user
30
- } from "../chunk-C5KXNY6R.js";
30
+ } from "../chunk-56SN6NQH.js";
31
31
  export {
32
32
  apt,
33
33
  archive,
package/llm-guide.md CHANGED
@@ -186,6 +186,8 @@ the idiomatic way to ensure a previously installed cron job is gone.
186
186
  | `compose.restart` | `(options: { projectDirectory: string; runtime?: "docker" \| "podman" }): Module` | No |
187
187
  | `compose.systemd` | `(options: { detached?: boolean; name?: string; projectDirectory: string; runtime?: "docker" \| "podman" }): Module` | Yes |
188
188
 
189
+ > **`compose.pull` change detection:** `compose.pull` compares image digests before and after the pull to report `ok` (nothing new) vs `changed`. This needs `compose config --format json`. With `runtime: "podman"`, when the compose provider is the Python `podman-compose` (which does not support `--format json`), digest-based detection is unavailable: the pull still runs, but always reports `changed`. The Docker and Compose v2 paths are unaffected.
190
+
189
191
  ### `download`
190
192
 
191
193
  | Method | Signature | Idempotent |
@@ -360,6 +362,7 @@ Whitespace und Shell-Metazeichen werden abgelehnt. `package.absent` ist rein nam
360
362
  | Method | Signature | Idempotent |
361
363
  | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
362
364
  | `quadlet.container` | `(options: { addCapability?: string[]; addDevice?: string[]; annotation?: Record<string, string>; autoUpdate?: "local" \| "registry"; containerName?: string; description?: string; dns?: string[]; dnsOption?: string[]; dnsSearch?: string[]; dropCapability?: string[]; entrypoint?: string[]; environment?: Record<string, string>; environmentFiles?: string[]; exec?: string[]; exposeHostPort?: string[]; groupAdd?: string[]; healthCmd?: string; healthInterval?: string; healthOnFailure?: "kill" \| "none" \| "restart" \| "stop"; healthRetries?: number; healthStartPeriod?: string; healthTimeout?: string; hostName?: string; image: string; ip?: string; ip6?: string; label?: Record<string, string>; logDriver?: string; mask?: string[]; mount?: string[]; name: string; networks?: string[]; noNewPrivileges?: boolean; notify?: boolean; podmanArgs?: string[]; publishPorts?: string[]; pull?: "always" \| "missing" \| "never" \| "newer"; readOnly?: boolean; restart?: "always" \| "no" \| "on-abnormal" \| "on-abort" \| "on-failure" \| "on-success" \| "on-watchdog"; runInit?: boolean; secret?: string[]; seccompProfile?: string; securityLabelDisable?: boolean; securityLabelType?: string; stopTimeout?: number; sysctl?: Record<string, string>; timeoutStartSec?: number; timeoutStopSec?: number; timezone?: string; tmpfs?: string[]; ulimit?: string[]; unmask?: string[]; user?: string; userNs?: string; volumes?: string[]; wantedBy?: string; workingDir?: string }): Module` | Yes |
365
+ | `quadlet.network` | `(options: { description?: string; disableDns?: boolean; dns?: string[]; driver?: string; gateway?: string; internal?: boolean; ipRange?: string; ipamDriver?: string; ipv6?: boolean; label?: Record<string, string>; name: string; options?: Record<string, string>; podmanArgs?: string[]; subnet?: string }): Module` — writes `/etc/containers/systemd/<name>.network`; reference it from a container via `networks: ["<name>.network"]`. When the change lands and a network with the same name is already live, `changed` carries an advisory detail that the running network is not recreated until removed | Yes |
363
366
  | `quadlet.updateImage` | `(options: { authFile?: string; image: string; name: string; serviceName?: string }): Module` — returns `changed` with the new registry digest (or local image ID fallback) in parentheses when a newer image was pulled | Partial |
364
367
 
365
368
  ### `releaseUpgrade`
@@ -803,20 +806,21 @@ Rules:
803
806
 
804
807
  Diff-producing built-in modules:
805
808
 
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. |
809
+ | Module | Diff content |
810
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
811
+ | `file.copy` | Unified diff between the remote file and the local source. |
812
+ | `file.template` | Unified diff between the remote file and the rendered template. |
813
+ | `sysctl.set` | `key = old → new` for the desired configuration. |
814
+ | `hostname.set` | `hostname = old → new`. |
815
+ | `swap.file` | Diff of the `/etc/fstab` entry (or removal of the line for `state: "absent"`). |
816
+ | `swap.swappiness` | `vm.swappiness = old → new` (via `sysctl.set`). |
817
+ | `swap.vfsCachePressure` | `vm.vfs_cache_pressure = old → new` (via `sysctl.set`). |
818
+ | `cron.job` / `cron.absent` | Unified diff of the target user's crontab. |
819
+ | `timer.scheduled` (present) | Concatenated diffs of the `.service` and `.timer` unit files. |
820
+ | `timer.scheduled` (absent) / `timer.absent` | List of unit files to remove. |
821
+ | `net.hosts` | Unified diff of `/etc/hosts`. |
822
+ | `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. |
823
+ | `quadlet.network` | Unified diff of the `.network` 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. When a network with the same name is already live, the advisory hint is appended too. |
820
824
 
821
825
  ### Implementing a Diff for a Custom Module
822
826
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paratix",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Idempotent VPS setup tool in TypeScript",
5
5
  "type": "module",
6
6
  "files": [