@homeflare/alchemy 0.31.2 → 0.32.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.
Files changed (140) hide show
  1. package/README.md +39 -3
  2. package/dist/linux/container-form.d.ts +129 -0
  3. package/dist/linux/container-form.d.ts.map +1 -0
  4. package/dist/linux/container-generator.d.ts +74 -0
  5. package/dist/linux/container-generator.d.ts.map +1 -0
  6. package/dist/linux/container-handlers.d.ts +15 -0
  7. package/dist/linux/container-handlers.d.ts.map +1 -0
  8. package/dist/linux/container-lifecycle.d.ts +37 -0
  9. package/dist/linux/container-lifecycle.d.ts.map +1 -0
  10. package/dist/linux/container-preflight.d.ts +34 -0
  11. package/dist/linux/container-preflight.d.ts.map +1 -0
  12. package/dist/linux/container-secrets.d.ts +37 -0
  13. package/dist/linux/container-secrets.d.ts.map +1 -0
  14. package/dist/linux/container-settle.d.ts +34 -0
  15. package/dist/linux/container-settle.d.ts.map +1 -0
  16. package/dist/linux/container.d.ts +30 -0
  17. package/dist/linux/container.d.ts.map +1 -0
  18. package/dist/linux/fake-quadlet.d.ts +25 -0
  19. package/dist/linux/fake-quadlet.d.ts.map +1 -0
  20. package/dist/linux/index.d.ts +6 -3
  21. package/dist/linux/index.d.ts.map +1 -1
  22. package/dist/linux/index.js +641 -268
  23. package/dist/linux/index.js.map +15 -7
  24. package/dist/linux/providers.d.ts +1 -1
  25. package/dist/linux/providers.d.ts.map +1 -1
  26. package/dist/linux/systemctl.d.ts +7 -1
  27. package/dist/linux/systemctl.d.ts.map +1 -1
  28. package/dist/opnsense/alias-form.d.ts +29 -0
  29. package/dist/opnsense/alias-form.d.ts.map +1 -0
  30. package/dist/opnsense/alias.d.ts +81 -0
  31. package/dist/opnsense/alias.d.ts.map +1 -0
  32. package/dist/opnsense/category-form.d.ts +10 -0
  33. package/dist/opnsense/category-form.d.ts.map +1 -0
  34. package/dist/opnsense/category.d.ts +58 -0
  35. package/dist/opnsense/category.d.ts.map +1 -0
  36. package/dist/opnsense/credentials.d.ts +36 -0
  37. package/dist/opnsense/credentials.d.ts.map +1 -0
  38. package/dist/opnsense/fake-opnsense.d.ts +51 -0
  39. package/dist/opnsense/fake-opnsense.d.ts.map +1 -0
  40. package/dist/opnsense/group-form.d.ts +11 -0
  41. package/dist/opnsense/group-form.d.ts.map +1 -0
  42. package/dist/opnsense/group.d.ts +62 -0
  43. package/dist/opnsense/group.d.ts.map +1 -0
  44. package/dist/opnsense/index.d.ts +16 -0
  45. package/dist/opnsense/index.d.ts.map +1 -0
  46. package/dist/opnsense/index.js +211 -0
  47. package/dist/opnsense/index.js.map +20 -0
  48. package/dist/opnsense/policy.d.ts +20 -0
  49. package/dist/opnsense/policy.d.ts.map +1 -0
  50. package/dist/opnsense/providers.d.ts +13 -0
  51. package/dist/opnsense/providers.d.ts.map +1 -0
  52. package/dist/opnsense/resource.d.ts +74 -0
  53. package/dist/opnsense/resource.d.ts.map +1 -0
  54. package/dist/opnsense/wire.d.ts +39 -0
  55. package/dist/opnsense/wire.d.ts.map +1 -0
  56. package/dist/proxmox/index.js +106 -47
  57. package/dist/proxmox/index.js.map +6 -4
  58. package/dist/proxmox/storage-form.d.ts +54 -0
  59. package/dist/proxmox/storage-form.d.ts.map +1 -0
  60. package/dist/proxmox/storage-wire.d.ts +56 -0
  61. package/dist/proxmox/storage-wire.d.ts.map +1 -0
  62. package/dist/proxmox/storage.d.ts +55 -60
  63. package/dist/proxmox/storage.d.ts.map +1 -1
  64. package/dist/unifi/fake-unifi.d.ts +48 -0
  65. package/dist/unifi/fake-unifi.d.ts.map +1 -0
  66. package/dist/unifi/firewall-zone-form.d.ts +40 -0
  67. package/dist/unifi/firewall-zone-form.d.ts.map +1 -0
  68. package/dist/unifi/firewall-zone.d.ts +50 -0
  69. package/dist/unifi/firewall-zone.d.ts.map +1 -0
  70. package/dist/unifi/index.d.ts +13 -0
  71. package/dist/unifi/index.d.ts.map +1 -0
  72. package/dist/unifi/index.js +190 -0
  73. package/dist/unifi/index.js.map +16 -0
  74. package/dist/unifi/network-form.d.ts +67 -0
  75. package/dist/unifi/network-form.d.ts.map +1 -0
  76. package/dist/unifi/network.d.ts +67 -0
  77. package/dist/unifi/network.d.ts.map +1 -0
  78. package/dist/unifi/policy.d.ts +20 -0
  79. package/dist/unifi/policy.d.ts.map +1 -0
  80. package/dist/unifi/providers.d.ts +13 -0
  81. package/dist/unifi/providers.d.ts.map +1 -0
  82. package/dist/unifi/resource.d.ts +64 -0
  83. package/dist/unifi/resource.d.ts.map +1 -0
  84. package/docs/opnsense.md +146 -0
  85. package/docs/provider-standard.md +1 -1
  86. package/docs/quadlet-container.md +132 -0
  87. package/docs/unifi.md +93 -0
  88. package/package.json +12 -2
  89. package/src/linux/container-fixture.test.ts +92 -0
  90. package/src/linux/container-form.ts +243 -0
  91. package/src/linux/container-generator.test.ts +195 -0
  92. package/src/linux/container-generator.ts +113 -0
  93. package/src/linux/container-handlers.ts +73 -0
  94. package/src/linux/container-lifecycle.test.ts +209 -0
  95. package/src/linux/container-lifecycle.ts +217 -0
  96. package/src/linux/container-preflight.ts +97 -0
  97. package/src/linux/container-secrets.test.ts +171 -0
  98. package/src/linux/container-secrets.ts +112 -0
  99. package/src/linux/container-settle.ts +97 -0
  100. package/src/linux/container.ts +60 -0
  101. package/src/linux/fake-quadlet.ts +127 -0
  102. package/src/linux/index.ts +11 -3
  103. package/src/linux/providers.ts +2 -0
  104. package/src/linux/systemctl.ts +9 -0
  105. package/src/opnsense/alias-form.test.ts +89 -0
  106. package/src/opnsense/alias-form.ts +53 -0
  107. package/src/opnsense/alias.test.ts +203 -0
  108. package/src/opnsense/alias.ts +117 -0
  109. package/src/opnsense/category-form.test.ts +54 -0
  110. package/src/opnsense/category-form.ts +20 -0
  111. package/src/opnsense/category.test.ts +138 -0
  112. package/src/opnsense/category.ts +82 -0
  113. package/src/opnsense/credentials.ts +41 -0
  114. package/src/opnsense/fake-opnsense.ts +79 -0
  115. package/src/opnsense/group-form.test.ts +75 -0
  116. package/src/opnsense/group-form.ts +25 -0
  117. package/src/opnsense/group.test.ts +141 -0
  118. package/src/opnsense/group.ts +78 -0
  119. package/src/opnsense/index.ts +45 -0
  120. package/src/opnsense/policy.ts +49 -0
  121. package/src/opnsense/providers.ts +21 -0
  122. package/src/opnsense/resource.ts +146 -0
  123. package/src/opnsense/wire.ts +56 -0
  124. package/src/opnsense/write-refusal.test.ts +148 -0
  125. package/src/proxmox/storage-form.ts +144 -0
  126. package/src/proxmox/storage-read-failure.test.ts +214 -0
  127. package/src/proxmox/storage-wire.ts +124 -0
  128. package/src/proxmox/storage.ts +165 -175
  129. package/src/unifi/fake-unifi.ts +60 -0
  130. package/src/unifi/firewall-zone-form.ts +65 -0
  131. package/src/unifi/firewall-zone.test.ts +88 -0
  132. package/src/unifi/firewall-zone.ts +66 -0
  133. package/src/unifi/index.ts +26 -0
  134. package/src/unifi/network-form.ts +207 -0
  135. package/src/unifi/network.test.ts +209 -0
  136. package/src/unifi/network.ts +83 -0
  137. package/src/unifi/policy.ts +37 -0
  138. package/src/unifi/providers.ts +15 -0
  139. package/src/unifi/resource.test.ts +133 -0
  140. package/src/unifi/resource.ts +131 -0
package/README.md CHANGED
@@ -148,14 +148,16 @@ one is logged (argv only, never content) before it runs. A plan never calls sudo
148
148
 
149
149
  ## Linux hosts — `@homeflare/alchemy/linux`
150
150
 
151
- The same `HostRunner` seam, over ssh, plus the three families the launchd subpath had no Linux twin
151
+ The same `HostRunner` seam, over ssh, plus the four families the launchd subpath had no Linux twin
152
152
  for: `HostDirectory` (because no file resource creates a parent), `RemoteFile` (a whole file, or one
153
- **managed block** inside a file somebody else owns) and `SystemdUnit` / `SystemdTimer`.
154
- `linuxProviders(await sshRunner({ host }))` provides all four.
153
+ **managed block** inside a file somebody else owns), `SystemdUnit` / `SystemdTimer`, and
154
+ `PodmanContainer` (one Quadlet `.container` file and the systemd unit its generator produces).
155
+ `linuxProviders(await sshRunner({ host }))` provides all five.
155
156
 
156
157
  ```ts
157
158
  RemoteFile('block', { path: '/etc/example.conf', region: { name: 'homeflare' }, content: 'a line\n' });
158
159
  SystemdUnit('thing', { name: 'thing.service', sections: [...], restartOn: [config.sha256] });
160
+ PodmanContainer('thing', { name: 'thing', container: { image: 'example/thing:1', network: 'host' } });
159
161
  ```
160
162
 
161
163
  - ⛔ **A deploy never mass-restarts.** A unit restarts only when its own file changed, when state or
@@ -166,7 +168,12 @@ SystemdUnit('thing', { name: 'thing.service', sections: [...], restartOn: [confi
166
168
  without its framing marker is an Error — never "nothing is there".
167
169
  - ⛔ **No silent sudo:** a root-owned path needs a root ssh destination or your own privileged runner.
168
170
  - ⛔ **The directive set is systemd's:** unit files render verbatim; nothing here invents a schema.
171
+ - ⛔ **`PodmanContainer`'s `[Container]` section IS typed and doc-checked** (Podman 5.4,
172
+ `podman-systemd.unit(5)`) — Quadlet's own schema, unlike bare `systemd.unit(5)`. It has no
173
+ `enabled` prop and never calls `systemctl enable`: Quadlet applies `[Install]` itself at every
174
+ `daemon-reload`, and a generator failure is a typed `QuadletGeneratorError`, never "absent".
169
175
  - Guide, the measured `systemctl` shapes and the limits: [docs/linux-host.md](./docs/linux-host.md).
176
+ `PodmanContainer`'s own guide: [docs/quadlet-container.md](./docs/quadlet-container.md).
170
177
 
171
178
  ## Release binaries — `@homeflare/alchemy/release`
172
179
 
@@ -248,6 +255,17 @@ endpoint lives in ONE `general_settings` field (a whole-list read-modify-write),
248
255
  declared in `config.yaml` is refused rather than silently overridden, and a literal secret in a
249
256
  forwarded header is refused at plan: [docs/litellm.md](./docs/litellm.md).
250
257
 
258
+ ## OPNsense — `@homeflare/alchemy/opnsense`
259
+
260
+ `Opnsense.Firewall.Alias`, `.Category` and `.Group` — READ-ONLY BY DESIGN, generated
261
+ from `@distilled.cloud/opnsense` (an interim alias, like NetBox/Proxmox above — not a
262
+ peer, `bun add` resolves it from the tarball). ⛔ `reconcile` and `delete` always
263
+ refuse with a typed error; there is no flag that lifts it, and no write operation is
264
+ reachable from any handler (`write-refusal.test.ts` proves it against a fake that
265
+ fails on any non-GET). `read` answers `Unowned` on every match, `adopt(true)` is on
266
+ by default, and a pure declaration renderer turns one live read into the props a
267
+ noop declaration needs: [docs/opnsense.md](./docs/opnsense.md).
268
+
251
269
  ## Discord — `@homeflare/alchemy/discord`
252
270
 
253
271
  `Discord.ApplicationCommand` (global) and `Discord.GuildApplicationCommand` (guild-scoped)
@@ -270,6 +288,24 @@ export const help =
270
288
  });
271
289
  ```
272
290
 
291
+ ## UniFi Network — `@homeflare/alchemy/unifi`
292
+
293
+ `Unifi.Network` and `Unifi.FirewallZone`, generated from Ubiquiti's own UniFi Network
294
+ Integration API 10.4.57 via `@distilled.cloud/unifi-network`. ⛔ **READ-ONLY, by Tim's rule
295
+ (2026-09-24):** `reconcile`/`delete` both fail with a typed `UnifiWriteRefused` naming the
296
+ policy — this family never calls a write operation, only `getNetworkDetails`/`getFirewallZone`
297
+ and their list counterparts. ⛔ `read` answers `Unowned` on every match (never a silent adopt —
298
+ `adopt(true)` is on by default via the `network`/`firewallZone` constructors), and a UniFi
299
+ **Console ID** (part of a cloud-connector base URL) is never logged, committed or written to
300
+ state: [docs/unifi.md](./docs/unifi.md).
301
+
302
+ ```ts
303
+ import { declareNetwork, network, providers } from '@homeflare/alchemy/unifi';
304
+
305
+ // A later import script writes rows like this one from a live read:
306
+ export const lan = network('lan', declareNetwork(liveNetworkDetails, 'site-id'));
307
+ ```
308
+
273
309
  ## Google Workspace — `@homeflare/alchemy/google-workspace`
274
310
 
275
311
  `Group`, `GroupMember`, `DomainAlias` and `OrgUnit` over the Admin SDK Directory API, generated
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `Podman.Container` props, attributes and the `.container` file renderer — the pure half of the
3
+ * family, mirroring unit-form.ts's split for Systemd.Unit.
4
+ *
5
+ * ★ WHY THE `[Container]` SECTION IS TYPED, UNLIKE `Systemd.Unit`'s SECTIONS. unit-form.ts renders
6
+ * `[Unit]`/`[Service]`/`[Install]` verbatim because there is no machine-readable schema of
7
+ * `systemd.unit(5)` to check a key against. Quadlet's `[Container]` section is different: it is
8
+ * ONE vendor's documented, versioned surface — `podman-systemd.unit(5)`, Podman 5.4 (checked
9
+ * against `containers/podman` tag `v5.4.0`,
10
+ * `docs/source/markdown/podman-systemd.unit.5.md`, read 2026-09-24) — so every key below is
11
+ * checked against that page, not invented. `[Unit]`/`[Service]`/`[Install]` stay close to
12
+ * unit-form.ts's own verbatim-lines approach, because THOSE three sections are still systemd's
13
+ * own directive set, which Quadlet passes straight through (same doc, "All options and tables
14
+ * available in standard systemd unit files are supported").
15
+ * ⛔ `[Container] Environment=` IS NEVER A SECRET — enforced in container-secrets.ts, called from
16
+ * `containerProblems` below. A container that needs a secret takes `EnvironmentFile=` pointing at
17
+ * a host path an out-of-band secret renderer (openbao-agent) maintains; this family never reads
18
+ * or renders that file's contents.
19
+ * ⛔ `[Install]` SUPPORTS ONLY `Alias=`/`WantedBy=`/`RequiredBy=` FOR A `.container` FILE (same doc,
20
+ * "Currently, only the Alias, WantedBy and RequiredBy keys are supported") — NOT `Also=`, which
21
+ * plain `systemd.unit(5)` allows and unit-form.ts's `INSTALL_KEYS` therefore lists. See
22
+ * container-generator.ts's header for why there is no `enabled` prop at all.
23
+ */
24
+ import { configDigest, digestOf } from './unit-form.ts';
25
+ /** ★ Reused, not redefined: same digest and `restartOn`-join contract as Systemd.Unit. */
26
+ export { configDigest, digestOf };
27
+ export type UnitLine = readonly [key: string, value: string];
28
+ /**
29
+ * The `[Container]` keys this family supports — a deliberate SUBSET of the ~80 keys
30
+ * `podman-systemd.unit(5)` documents (task scope: `Image`, `ContainerName`, `Network`, `Volume`,
31
+ * `EnvironmentFile`, `Environment`, `PublishPort`, `Exec`, `User`, `AutoUpdate`, `PodmanArgs`).
32
+ * Adding another key means adding a field here and checking it against the same doc — never a
33
+ * free-text escape hatch, or `Environment=` secret-checking (below) could be bypassed through it.
34
+ */
35
+ export type ContainerSection = {
36
+ /** `Image=`. Required. "recommended to use a fully qualified image name" (doc). */
37
+ readonly image: string;
38
+ /** `ContainerName=`. @default quadlet's own `systemd-<name>` (doc) when omitted. */
39
+ readonly containerName?: string;
40
+ /** `Network=`. MUST support `'host'` — every container this rollout declares runs host netns. */
41
+ readonly network?: string;
42
+ /** `Volume=`, repeatable. */
43
+ readonly volume?: readonly string[];
44
+ /** `EnvironmentFile=`, repeatable. The ONLY way a secret reaches the container — see file header. */
45
+ readonly environmentFile?: readonly string[];
46
+ /** `Environment=`, repeatable `KEY=VALUE`. NEVER a secret — see container-secrets.ts. */
47
+ readonly environment?: Readonly<Record<string, string>>;
48
+ /** `PublishPort=`, repeatable. Refused together with `network: 'host'` — see `containerProblems`. */
49
+ readonly publishPort?: readonly string[];
50
+ /** `Exec=`. Appended after the image's entrypoint, "the same way as ... a Kubernetes pod" (doc). */
51
+ readonly exec?: string;
52
+ /** `User=`. The numeric UID inside the container (doc: "does not need to match the UID on the host"). */
53
+ readonly user?: string;
54
+ /** `AutoUpdate=`. Podman's own two values (doc): `'registry'` or `'local'`. */
55
+ readonly autoUpdate?: 'registry' | 'local';
56
+ /** `PodmanArgs=`, repeatable. Doc: "not recommended" — the generator does not understand it. */
57
+ readonly podmanArgs?: readonly string[];
58
+ };
59
+ export type ContainerProps = {
60
+ /** The Quadlet file's basename, WITHOUT `.container`. Written as `<name>.container`. */
61
+ name: string;
62
+ /** @default QUADLET_DEFAULT_DIRECTORY. Must be one of QUADLET_SEARCH_DIRECTORIES (below). */
63
+ directory?: string;
64
+ /** `[Unit]` — `Description=`/`Documentation=` typed, everything else verbatim (see file header). */
65
+ unit?: {
66
+ description?: string;
67
+ documentation?: string;
68
+ lines?: readonly UnitLine[];
69
+ };
70
+ /** `[Container]` — Quadlet's own, typed and doc-checked. */
71
+ container: ContainerSection;
72
+ /** `[Service]` — `Restart=`/`RestartSec=` typed, everything else verbatim. */
73
+ service?: {
74
+ restart?: string;
75
+ restartSec?: string;
76
+ lines?: readonly UnitLine[];
77
+ };
78
+ /** `[Install]` — the three keys Quadlet honours for a `.container` file (see file header). */
79
+ install?: {
80
+ wantedBy?: readonly string[];
81
+ requiredBy?: readonly string[];
82
+ alias?: readonly string[];
83
+ };
84
+ /** `systemctl start`/`stop` the GENERATED `<name>.service`. @default true. No `enabled` prop —
85
+ * see container-generator.ts's header for why boot-time enablement is not a systemctl call here. */
86
+ started?: boolean;
87
+ /** Same contract as Systemd.Unit's `restartOn`: digests of things read outside the `.container` file. */
88
+ restartOn?: readonly string[];
89
+ };
90
+ export type ContainerAttributes = {
91
+ name: string;
92
+ /** The GENERATED unit's name: `<name>.service`. Never `.container` — systemctl never sees that. */
93
+ serviceName: string;
94
+ containerPath: string;
95
+ /** SHA-256 of the `.container` file this resource wrote. */
96
+ containerSha256: string;
97
+ configSha256: string;
98
+ loadState: string;
99
+ activeState: string;
100
+ unitFileState?: string;
101
+ /** `systemctl show`'s `SourcePath=` on the generated unit — measured on CT100 2026-09-24 to be
102
+ * the `.container` file's own path; used to prove the generated unit is really ours. */
103
+ sourcePath?: string;
104
+ active: boolean;
105
+ };
106
+ /**
107
+ * Podman's OWN rootful search path, most specific first — `podman-systemd.unit(5)`, "Podman Unit
108
+ * Search Path", Podman 5.4, read 2026-09-24: "Quadlet files for the root user can be placed in the
109
+ * following directories ordered in precedence" (`/run` over `/etc` over `/usr/share`).
110
+ * ★ CT100's `caddy.container` lives in the middle one — measured live, `cat` over `ssh ct100`,
111
+ * 2026-09-24: `/etc/containers/systemd/caddy.container`.
112
+ * ⛔ A directory outside this list is invisible to Quadlet: the generator only reads these three, so
113
+ * a file elsewhere is a `.container` file with no `.service` ever generated from it — silently,
114
+ * unlike `Systemd.Unit`'s directory check, which at least writes a unit systemd could load if only
115
+ * it were enabled. See `containerProblems`.
116
+ */
117
+ export declare const QUADLET_SEARCH_DIRECTORIES: readonly ['/run/containers/systemd', '/etc/containers/systemd', '/usr/share/containers/systemd'];
118
+ export declare const QUADLET_DEFAULT_DIRECTORY = "/etc/containers/systemd";
119
+ /** ★ A conservative subset of what Quadlet itself accepts as a unit name: no template `@`, no `/`. */
120
+ export declare const NAME: RegExp;
121
+ export declare const containerPathFor: (props: Pick<ContainerProps, 'name' | 'directory'>) => string;
122
+ /** The unit Quadlet generates FROM the `.container` file — systemctl only ever sees this name. */
123
+ export declare const serviceNameFor: (props: Pick<ContainerProps, 'name'>) => string;
124
+ /** The whole `.container` file. ★ Reuses unit-form.ts's `renderUnit` — same `[Section]` INI shape. */
125
+ export declare const renderContainerFile: (props: ContainerProps) => string;
126
+ export declare const refuseContainer: (name: string, message: string) => Error;
127
+ /** Every refusal at once, so one plan shows the whole list — mirrors `unitProblems`'s shape. */
128
+ export declare const containerProblems: (props: ContainerProps) => string[];
129
+ //# sourceMappingURL=container-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-form.d.ts","sourceRoot":"","sources":["../../src/linux/container-form.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Quadlet is a systemd GENERATOR (`systemd.generator(7)`, tag `v257`, `man/systemd.generator.xml`,
3
+ * read 2026-09-24), not a unit reader — this is the one fact the whole lifecycle is built around.
4
+ *
5
+ * ★ "Generator output is supposed to last only until the next `daemon-reload`... Units written by
6
+ * generators are removed when the configuration is reloaded" (same doc). `daemon-reload` DELETES
7
+ * `/run/systemd/generator` wholesale and reruns every generator from the `.container` files
8
+ * currently on disk. So a `.container` file the generator refuses produces NO generated unit —
9
+ * even one that generated fine a moment ago, if `daemon-reload` runs again over a now-broken file.
10
+ * That is why `container-lifecycle.ts` restores the LAST-KNOWN-GOOD file and reloads again on a
11
+ * failed update, rather than leaving the bad file in place the way `Systemd.Unit` leaves a bad
12
+ * update for the next deploy to retry (unit-lifecycle.ts): here, "the next deploy" might be a
13
+ * reboot, and by then the container this resource already promised running has no systemd unit
14
+ * at all, not merely a stale one.
15
+ * ⛔ THE GENERATED UNIT CANNOT BE `systemctl enable`D — `podman-systemd.unit(5)`, Podman 5.4, "Podman
16
+ * unit files": "The services created by Podman are considered transient by systemd... it is not
17
+ * possible to 'systemctl enable' them... To compensate... the generator manually applies the
18
+ * [Install] section... during generation, in the same way `systemctl enable` does when run
19
+ * later." MEASURED on CT100 2026-09-24 (`ssh ct100`, read-only): `caddy.container` declares
20
+ * `[Install] WantedBy=multi-user.target` and nothing ever ran `systemctl enable caddy`; yet
21
+ * `systemctl is-enabled caddy.service` prints `generated`, `UnitFileState=generated`, and
22
+ * `/run/systemd/generator/multi-user.target.wants/caddy.service` is a symlink the GENERATOR put
23
+ * there. So `Podman.Container` has no `enabled` prop and never calls `enableUnit`/`disableUnit`
24
+ * (systemctl.ts) — declaring `install.wantedBy` IS the enablement, applied by the generator on
25
+ * every `daemon-reload`, exactly the way `Systemd.Unit`'s enable/disable calls are applied by
26
+ * this family's own `settle` step. That is the one place this family's "enable logic" differs.
27
+ * ★ SourcePath TIES THE GENERATED UNIT BACK TO OUR FILE. MEASURED on CT100 2026-09-24:
28
+ * `systemctl show caddy.service -p SourcePath` -> `/etc/containers/systemd/caddy.container`.
29
+ * `verifyGenerated` below uses it the way `unit-preflight.ts`'s `assertUnclaimed` uses a file
30
+ * digest: a generated unit whose SourcePath is NOT our path belongs to a same-named `.container`
31
+ * file earlier in `QUADLET_SEARCH_DIRECTORIES` (container-form.ts) — a takeover, not a success.
32
+ * ⚠️ REASONED, NOT MEASURED — THE ONE GAP THIS VERIFICATION HAS: whether an ALREADY-ACTIVE unit's
33
+ * `LoadState` stays `loaded` (systemd does not garbage-collect a referenced/active unit object
34
+ * merely because a reload failed to reproduce it) even when the UPDATED `.container` file the
35
+ * generator just refused would, on its own, generate nothing. Measuring this needs writing a
36
+ * broken file to a live host, which read-only access forbids. If that is how a live host
37
+ * behaves, `LoadState`/`SourcePath` alone would not catch a failed update to a container that
38
+ * was already running — only a CREATE, or an update to an already-INACTIVE one, is verified with
39
+ * full confidence by this check alone. Restoring the last-known-good file on ANY verification
40
+ * failure (container-lifecycle.ts) does not depend on this gap: it always leaves the host at the
41
+ * file that is known to generate, closing the risk window even where detection is uncertain.
42
+ * ⛔ `needsVerificationReload` (below) is what stops a NEW resource from getting stuck — found on
43
+ * adversarial review. `container-lifecycle.ts`'s `needsReload` used to be `wrote || stale ||
44
+ * preStatus.needDaemonReload`; for an INTERRUPTED CREATE (the write landed, the apply crashed
45
+ * before `daemon-reload` ran) the retry sees `wrote: false` (the file already holds `desired`)
46
+ * and `stale: false` (there is no PRIOR state to disagree with — this is a create), so nothing
47
+ * ever reloaded and `verifyGenerated` threw on the still-missing generated unit, forever: loud,
48
+ * but stuck, since nothing about a plain retry ever changed. The same holds for adopting a file
49
+ * whose generated unit is missing or does not match. `needsVerificationReload` shares
50
+ * `generatedMatches` with `verifyGenerated` itself, so the reload gate and the verification it
51
+ * is gating can never disagree about what "already proven" means.
52
+ */
53
+ import type { UnitStatus } from './systemctl.ts';
54
+ /** MEASURED on CT100 2026-09-24: `systemctl show caddy.service -p FragmentPath`. */
55
+ export declare const GENERATED_UNIT_DIRECTORY = "/run/systemd/generator";
56
+ export declare class QuadletGeneratorError extends Error {
57
+ constructor(name: string, detail: string);
58
+ }
59
+ /**
60
+ * Whether `container-lifecycle.ts` must `daemon-reload` before trusting `status` as proof of
61
+ * anything — true whenever `status` does not already show a matching generation. Called on the
62
+ * PRE-write status, so it costs nothing when `wrote`/`stale`/`NeedDaemonReload` already force a
63
+ * reload; it earns its keep exactly when none of them do but nothing has actually been proven yet
64
+ * (a fresh create or adopt with no prior state to compare against).
65
+ */
66
+ export declare const needsVerificationReload: (containerPath: string, status: UnitStatus) => boolean;
67
+ /**
68
+ * Throws a typed, loud `QuadletGeneratorError` — never lets a generator failure read as "the
69
+ * resource is simply absent", which is what a plain `LoadState=not-found` would otherwise look
70
+ * like to a caller that only knows the `Systemd.Unit` shape (a unit that has genuinely never been
71
+ * declared also reads `not-found`).
72
+ */
73
+ export declare const verifyGenerated: (name: string, containerPath: string, serviceName: string, status: UnitStatus) => void;
74
+ //# sourceMappingURL=container-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-generator.d.ts","sourceRoot":"","sources":["../../src/linux/container-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAGvC;CACF;AAUD;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,kBAAmB,MAAM,UAAU,UAAU,KAAG,OAC1C,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,eAAe,SACpB,MAAM,iBACG,MAAM,eACR,MAAM,UACX,UAAU,KACjB,IAmBF,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Diff } from 'alchemy/Diff';
2
+ import type { Input } from 'alchemy/Input';
3
+ import * as Effect from 'effect/Effect';
4
+ import type { HostRunner } from '../launchd/runner.ts';
5
+ import type { ContainerAttributes, ContainerProps } from './container-form.ts';
6
+ export declare const readHandler: (runner: HostRunner, olds: ContainerProps, output: ContainerAttributes | undefined) => Effect.Effect<ContainerAttributes | undefined, Error>;
7
+ export declare const diffHandler: (runner: HostRunner, instanceId: string, news: Input<ContainerProps>, output: ContainerAttributes | undefined) => Effect.Effect<Diff | undefined, Error>;
8
+ export declare const reconcileHandler: (runner: HostRunner, args: {
9
+ fqn: string;
10
+ instanceId: string;
11
+ news: ContainerProps;
12
+ output: ContainerAttributes | undefined;
13
+ }) => Effect.Effect<ContainerAttributes, Error>;
14
+ export declare const deleteHandler: (runner: HostRunner, output: ContainerAttributes) => Effect.Effect<void, Error>;
15
+ //# sourceMappingURL=container-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-handlers.d.ts","sourceRoot":"","sources":["../../src/linux/container-handlers.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAS/E,eAAO,MAAM,WAAW,WACd,UAAU,QACZ,cAAc,UACZ,mBAAmB,GAAG,SAAS,KACtC,MAAM,CAAC,MAAM,CAAC,mBAAmB,GAAG,SAAS,EAAE,KAAK,CAKnD,CAAC;AAEL,eAAO,MAAM,WAAW,WACd,UAAU,cACN,MAAM,QACZ,KAAK,CAAC,cAAc,CAAC,UACnB,mBAAmB,GAAG,SAAS,KACtC,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,EAAE,KAAK,CAgBvC,CAAC;AAEF,eAAO,MAAM,gBAAgB,WACnB,UAAU,QACZ;IACJ,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACzC,KACA,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,KAAK,CAGxC,CAAC;AAEJ,eAAO,MAAM,aAAa,WAChB,UAAU,UACV,mBAAmB,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAgD,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `Podman.Container`'s read / diff / reconcile / delete as plain async functions over a
3
+ * HostRunner — same split as `unit-lifecycle.ts`, so the whole lifecycle runs against a fake host
4
+ * (fake-quadlet.ts) with no real systemd or Podman.
5
+ *
6
+ * ⛔ WHAT A DEPLOY DOES NOT DO — same rule `Systemd.Unit` was built around (unit-lifecycle.ts
7
+ * header): it NEVER mass-restarts. A container is restarted only when THIS resource's rendered
8
+ * `.container` file changed, or a digest the declaration listed in `restartOn` changed.
9
+ * ★ THE ORDER IS WRITE, RELOAD, VERIFY THE GENERATOR, THEN START OR RESTART. `daemon-reload` is
10
+ * where Quadlet turns the `.container` file into `<name>.service` (container-generator.ts) —
11
+ * writing it changes nothing on its own, and a write that fails leaves the old file (and its
12
+ * already-generated unit) exactly as they were.
13
+ * ⛔ A GENERATOR FAILURE IS ROLLED BACK, NOT LEFT FOR THE NEXT DEPLOY. `container-generator.ts`'s
14
+ * header explains why: `daemon-reload` deletes ALL generator output before regenerating, so a
15
+ * `.container` file the generator refuses has NO generated unit at all, even if the file it
16
+ * replaced generated one fine. An UPDATE that fails verification restores the previous file and
17
+ * reloads again, so the container this resource already promised running never loses its unit; a
18
+ * CREATE that fails removes the file it wrote, same as `Systemd.Unit`'s undo.
19
+ * ⛔ NO ENABLE/DISABLE — see container-generator.ts and container-settle.ts headers.
20
+ */
21
+ import type { Diff } from 'alchemy/Diff';
22
+ import type { HostRunner } from '../launchd/runner.ts';
23
+ import { type ContainerAttributes, type ContainerProps } from './container-form.ts';
24
+ /** What is on the host now: the `.container` file's digest and the GENERATED unit's status. */
25
+ export declare const readContainer: (runner: HostRunner, props: ContainerProps) => Promise<ContainerAttributes | undefined>;
26
+ export declare const diffContainer: (runner: HostRunner, news: ContainerProps, output: ContainerAttributes) => Promise<Diff>;
27
+ /**
28
+ * `adopt` is whether adoption is on AND this apply is a create or an unfinished generation of our
29
+ * own — same meaning as `Systemd.Unit`'s (unit-lifecycle.ts reconcileUnit).
30
+ */
31
+ export declare const reconcileContainer: (runner: HostRunner, props: ContainerProps, output: ContainerAttributes | undefined, adopt?: boolean) => Promise<ContainerAttributes>;
32
+ /**
33
+ * Stop, remove the `.container` file, reload — clearing the orphaned generated unit. Idempotent.
34
+ * No `disableUnit`: never enabled through systemctl in the first place (container-settle.ts).
35
+ */
36
+ export declare const deleteContainer: (runner: HostRunner, output: ContainerAttributes) => Promise<void>;
37
+ //# sourceMappingURL=container-lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-lifecycle.d.ts","sourceRoot":"","sources":["../../src/linux/container-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIvD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAOpB,MAAM,qBAAqB,CAAC;AAY7B,+FAA+F;AAC/F,eAAO,MAAM,aAAa,WAChB,UAAU,SACX,cAAc,KACpB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAczC,CAAC;AAEF,eAAO,MAAM,aAAa,WAChB,UAAU,QACZ,cAAc,UACZ,mBAAmB,KAC1B,OAAO,CAAC,IAAI,CAwBd,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,WACrB,UAAU,SACX,cAAc,UACb,mBAAmB,GAAG,SAAS,sBAEtC,OAAO,CAAC,mBAAmB,CAyF7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,WAClB,UAAU,UACV,mBAAmB,KAC1B,OAAO,CAAC,IAAI,CAad,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Everything `Podman.Container` checks before it touches the host — split from
3
+ * container-lifecycle.ts so a reconcile and a plan-time REPLACE run the very same checks, exactly
4
+ * the reason unit-preflight.ts exists (see that file's header; this mirrors it).
5
+ *
6
+ * ★ EVERY CHECK HERE IS READ-ONLY: validation, `systemctl show`, a file read and
7
+ * `HostRunner.checkWrite`. Nothing here writes, reloads or drives Podman.
8
+ */
9
+ import { type HostRunner } from '../launchd/runner.ts';
10
+ import { type UnitStatus } from './systemctl.ts';
11
+ import { type ContainerAttributes, type ContainerProps } from './container-form.ts';
12
+ /** Throw every refusal at once, so one plan shows the whole list. */
13
+ export declare const assertValid: (props: ContainerProps) => void;
14
+ /** ⛔ Writing the file and driving systemctl/Podman are root's — refuse up front, not mid-apply. */
15
+ export declare const assertMayWrite: (runner: HostRunner, name: string) => void;
16
+ /**
17
+ * ⛔ A MASKED GENERATED UNIT IS SOMEONE'S DECISION, NOT DRIFT — same rule as `Systemd.Unit`
18
+ * (unit-preflight.ts `assertUsable`). Masking a generated unit is done the ordinary way, by
19
+ * symlinking its name to `/dev/null` in a directory Quadlet's generator output does not win
20
+ * against (`/etc/systemd/system` outranks `/run/systemd/generator`); unmasking here would
21
+ * silently overrule that.
22
+ */
23
+ export declare const assertUsable: (props: Pick<ContainerProps, 'name'>, status: UnitStatus) => void;
24
+ /**
25
+ * ⛔ A `.container` FILE THIS RESOURCE DOES NOT OWN IS NEVER OVERWRITTEN — same rule and same
26
+ * exemption as `unit-preflight.ts`'s `assertUnclaimed`: a file byte-identical to our render is
27
+ * this declaration's own leftover, not someone else's.
28
+ */
29
+ export declare const assertUnclaimed: (runner: HostRunner, props: ContainerProps) => Promise<void>;
30
+ /** The half of a rename check that needs only the NEW NAME — mirrors `assertRenameTarget`. */
31
+ export declare const assertRenameTarget: (runner: HostRunner, old: ContainerAttributes, name: string) => Promise<void>;
32
+ /** A rename, checked at plan time, before the plan may promise a delete-first replace. */
33
+ export declare const assertReplaceable: (runner: HostRunner, next: ContainerProps, old: ContainerAttributes) => Promise<void>;
34
+ //# sourceMappingURL=container-preflight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-preflight.d.ts","sourceRoot":"","sources":["../../src/linux/container-preflight.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,sBAAsB,CAAC;AAErE,OAAO,EAAE,KAAK,UAAU,EAAY,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAMpB,MAAM,qBAAqB,CAAC;AAI7B,qEAAqE;AACrE,eAAO,MAAM,WAAW,UAAW,cAAc,KAAG,IAGnD,CAAC;AAEF,mGAAmG;AACnG,eAAO,MAAM,cAAc,WAAY,UAAU,QAAQ,MAAM,KAAG,IAQjE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,UAAW,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,UAAU,UAAU,KAAG,IAQtF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,WAAkB,UAAU,SAAS,cAAc,KAAG,OAAO,CAAC,IAAI,CAW7F,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,kBAAkB,WACrB,UAAU,OACb,mBAAmB,QAClB,MAAM,KACX,OAAO,CAAC,IAAI,CAId,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,iBAAiB,WACpB,UAAU,QACZ,cAAc,OACf,mBAAmB,KACvB,OAAO,CAAC,IAAI,CAKd,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A best-effort refusal of an inline secret ANYWHERE in a rendered `.container` file — checked
3
+ * from `containerProblems` (container-form.ts), so it runs at plan AND apply time, before anything
4
+ * is written.
5
+ *
6
+ * ⛔ THIS IS NOT A SECRET SCANNER. It cannot prove a value is safe, only refuse the OBVIOUS cases: a
7
+ * key whose name says what it holds, or a value shaped like a real vendor token. The actual
8
+ * contract is `EnvironmentFile=` (container-form.ts's header): a secret belongs in a host file an
9
+ * out-of-band renderer (openbao-agent) maintains, never in a prop, because the WHOLE file is
10
+ * rendered into a 0644 file (`unit-form.ts` `UNIT_WRITE`) AND persisted unencrypted in Alchemy
11
+ * state — the same two reasons `Systemd.Unit`'s `content` is never secret. A determined caller can
12
+ * still name a variable `CFG` and put a token in it; this check is a guardrail against the common
13
+ * mistake, not a boundary a hostile caller cannot cross.
14
+ * ★ KEY-NAME FIRST, VALUE SECOND. The name check catches the overwhelming majority of real
15
+ * mistakes (copying a `docker run -e SECRET=...` line verbatim) and has no false-negative risk
16
+ * from an unfamiliar token format; the value check is defense in depth for a handful of
17
+ * widely-recognised vendor prefixes, not an attempt at general entropy detection — an entropy
18
+ * check has enough false positives on ordinary config (long image digests, base64 Caddyfile
19
+ * snippets) that it would train callers to ignore the refusal.
20
+ * ⛔ CHECKED EVERYWHERE, NOT ONLY `container.environment` — found on adversarial review.
21
+ * `container.environment` is the field most likely to carry one BY ACCIDENT, and gets the
22
+ * precise per-key `secretLikeEnvironment` check below with the best error message. But every
23
+ * other free-text field renders into the SAME 0644 file and the SAME state row: `exec`,
24
+ * `podmanArgs` (each entry rendered verbatim), and the escape-hatch `unit.lines`/`service.lines`
25
+ * — a `service.lines` entry can spell a native systemd `Environment=` directly, which never goes
26
+ * through the typed `container.environment` map at all. `secretLikeLines` below runs the same two
27
+ * heuristics — value-shape, and an embedded `KEY=VALUE` whose KEY looks like a secret — over every
28
+ * OTHER rendered line, and `container-form.ts`'s `containerProblems` calls both, so nothing
29
+ * rendered escapes either check.
30
+ */
31
+ export declare const secretLikeEnvironment: (environment: Readonly<Record<string, string>>) => string[];
32
+ /** The same two heuristics as `secretLikeEnvironment`, over any OTHER rendered `[key, value]` line
33
+ * (`Exec=`, `PodmanArgs=`, and the `unit.lines`/`service.lines` escape hatches) — see file header. */
34
+ export declare const secretLikeLines: (lines: readonly (readonly [key: string, value: string])[]) => string[];
35
+ /** `PodmanArgs=` entries that try to set a container env var outright — see `ENV_FLAG`'s comment. */
36
+ export declare const podmanArgsProblems: (args: readonly string[]) => string[];
37
+ //# sourceMappingURL=container-secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-secrets.d.ts","sourceRoot":"","sources":["../../src/linux/container-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AA6BH,eAAO,MAAM,qBAAqB,gBAAiB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAG,MAAM,EAkB3F,CAAC;AAEF;uGACuG;AACvG,eAAO,MAAM,eAAe,UACnB,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,EAAE,KACxD,MAAM,EAkBR,CAAC;AAEF,qGAAqG;AACrG,eAAO,MAAM,kBAAkB,SAAU,SAAS,MAAM,EAAE,KAAG,MAAM,EAQ9D,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The step that decides whether the GENERATED `<name>.service` actually moves: running state and —
3
+ * only for a real change — a restart. Split out for the same reason unit-settle.ts is split from
4
+ * unit-lifecycle.ts: it is the restart contract, and it should be readable on its own.
5
+ *
6
+ * ⛔ THE RESTART CONDITION IS THE WHOLE CONTRACT, same as `Systemd.Unit`'s (unit-settle.ts):
7
+ * `changed` comes from the caller (container-lifecycle.ts `reconcileContainer`), `configChanged`
8
+ * is a digest the DECLARATION itself listed. Nothing else restarts a container.
9
+ * ★ NO ENABLE/DISABLE HERE — the one place this file is SHORTER than unit-settle.ts, not longer.
10
+ * Quadlet's generator applies `[Install]` itself on every `daemon-reload` (container-generator.ts
11
+ * header); calling `systemctl enable`/`disable` on a generated unit does not work at all (same
12
+ * header, doc-cited). So there is no enablement branch to settle — only started/stopped and the
13
+ * restart-on-change rule.
14
+ */
15
+ import type { HostRunner } from '../launchd/runner.ts';
16
+ import { type UnitStatus } from './systemctl.ts';
17
+ import { type ContainerAttributes, type ContainerProps } from './container-form.ts';
18
+ export declare const attributesOf: (props: ContainerProps, configSha256: string, containerSha256: string, containerPath: string, serviceName: string, status: UnitStatus) => ContainerAttributes;
19
+ export type Settle = {
20
+ readonly before: Uint8Array | undefined;
21
+ /** The `.container` file or systemd's view of it changed in a way that earns a restart. */
22
+ readonly changed: boolean;
23
+ readonly configChanged: boolean;
24
+ readonly desired: string;
25
+ readonly path: string;
26
+ readonly serviceName: string;
27
+ readonly status: UnitStatus;
28
+ };
29
+ /**
30
+ * Bring running state to the declaration, restarting ONLY for a real change. No enable/disable —
31
+ * see file header.
32
+ */
33
+ export declare const settle: (runner: HostRunner, props: ContainerProps, configSha256: string, step: Settle) => Promise<ContainerAttributes>;
34
+ //# sourceMappingURL=container-settle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-settle.d.ts","sourceRoot":"","sources":["../../src/linux/container-settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EACL,KAAK,UAAU,EAMhB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,YAAY,UAChB,cAAc,gBACP,MAAM,mBACH,MAAM,iBACR,MAAM,eACR,MAAM,UACX,UAAU,KACjB,mBAWD,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IACxC,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,MAAM,WACT,UAAU,SACX,cAAc,gBACP,MAAM,QACd,MAAM,KACX,OAAO,CAAC,mBAAmB,CA0B7B,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `Podman.Container` — one Quadlet `.container` file, declared, plus the systemd unit Podman's own
3
+ * generator turns it into.
4
+ *
5
+ * - create / update — render, write the `.container` file atomically, `daemon-reload`, VERIFY the
6
+ * generator actually produced `<name>.service` (container-generator.ts), then start or RESTART
7
+ * the generated unit only when something changed (container-lifecycle.ts, container-settle.ts).
8
+ * - read — the `.container` file's SHA-256 plus `systemctl show <name>.service`.
9
+ * - diff — the rendered digest against stored and on-disk, running state against the declaration,
10
+ * and systemd's own `NeedDaemonReload`.
11
+ * - replace — only when the file's name or directory changes, delete-first.
12
+ * - delete — stop the generated unit, remove the `.container` file, reload.
13
+ *
14
+ * ⛔ A DEPLOY NEVER MASS-RESTARTS — see container-lifecycle.ts's header.
15
+ * ⛔ THE `.container` FILE IS NEVER A SECRET, and `[Container] Environment=` is refused if it looks
16
+ * like one — see container-form.ts and container-secrets.ts.
17
+ * ⛔ THE GENERATED UNIT IS NEVER `systemctl enable`D — see container-generator.ts's header for why,
18
+ * measured and doc-cited, and what stands in for it (`install.wantedBy`).
19
+ * ⚠️ NOTHING IS ADOPTED WITHOUT `--adopt` — same contract as every family here.
20
+ */
21
+ import { Resource } from 'alchemy';
22
+ import * as Provider from 'alchemy/Provider';
23
+ import { HostRunnerService } from '../launchd/runner.ts';
24
+ import type { ContainerAttributes, ContainerProps } from './container-form.ts';
25
+ export type { ContainerAttributes as PodmanContainerAttributes, ContainerProps as PodmanContainerProps, ContainerSection, UnitLine as PodmanUnitLine, } from './container-form.ts';
26
+ export interface PodmanContainer extends Resource<'Podman.Container', ContainerProps, ContainerAttributes> {
27
+ }
28
+ export declare const PodmanContainer: import("alchemy").ResourceClass<PodmanContainer>;
29
+ export declare const PodmanContainerProvider: () => import("effect/Layer").Layer<Provider.Provider<PodmanContainer>, never, HostRunnerService>;
30
+ //# sourceMappingURL=container.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../../src/linux/container.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG/E,YAAY,EACV,mBAAmB,IAAI,yBAAyB,EAChD,cAAc,IAAI,oBAAoB,EACtC,gBAAgB,EAChB,QAAQ,IAAI,cAAc,GAC3B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,eAAgB,SAAQ,QAAQ,CAC/C,kBAAkB,EAClB,cAAc,EACd,mBAAmB,CACpB;CAAG;AAEJ,eAAO,MAAM,eAAe,kDAAgD,CAAC;AAE7E,eAAO,MAAM,uBAAuB,kGAiBjC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type FakeLinuxOptions } from './fake-linux-host.ts';
2
+ import type { HostRunner } from '../launchd/runner.ts';
3
+ /** A line no real Quadlet key ever spells, so a test can force the (fake) generator to refuse a file. */
4
+ export declare const FAIL_GENERATOR_MARKER = "X-Test-Fail-Generator=1";
5
+ export declare const fakeQuadletHost: (options?: FakeLinuxOptions) => {
6
+ calls: string[][];
7
+ dirs: Map<string, number>;
8
+ expectUnit: (path: string) => void;
9
+ files: Map<string, {
10
+ bytes: Uint8Array;
11
+ mode: number;
12
+ uid: number;
13
+ gid: number;
14
+ kind: import("./index.ts").FileStat['kind'];
15
+ }>;
16
+ modes: Map<string, {
17
+ mode: number;
18
+ uid: number;
19
+ gid: number;
20
+ }>;
21
+ placeUnit: (path: string, text: string, state?: Partial<import("./fake-systemd.ts").UnitState>) => void;
22
+ units: Map<string, import("./fake-systemd.ts").UnitState>;
23
+ runner: HostRunner;
24
+ };
25
+ //# sourceMappingURL=fake-quadlet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-quadlet.d.ts","sourceRoot":"","sources":["../../src/linux/fake-quadlet.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,gBAAgB,EAAiB,MAAM,sBAAsB,CAAC;AAG5E,OAAO,KAAK,EAAc,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEnE,yGAAyG;AACzG,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAO/D,eAAO,MAAM,eAAe,aAAa,gBAAgB;;;;;;;;;;;;;;;;;;;CAiGxD,CAAC"}
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Linux host providers for Alchemy — a host's directories, its files (whole, or one managed block
3
- * inside somebody else's file) and its systemd units, over the SAME HostRunner seam the launchd
4
- * subpath drives a Mac through.
3
+ * inside somebody else's file), its systemd units and its Podman Quadlet containers, over the SAME
4
+ * HostRunner seam the launchd subpath drives a Mac through.
5
5
  *
6
6
  * ⛔ THIS BARREL IS THE PUBLIC API, AND IT IS DELIBERATELY SMALLER THAN THE DIRECTORY. The lifecycle
7
7
  * files, the systemctl parsers and the fake host are internals; an `export *` would publish them
8
8
  * as API and make the next refactor a breaking change.
9
9
  * ⛔ A DEPLOY NEVER MASS-RESTARTS: a unit is restarted only when its own file changed, or a digest
10
- * the declaration listed changed. See docs/linux-host.md.
10
+ * the declaration listed changed. See docs/linux-host.md and docs/quadlet-container.md.
11
11
  * ★ THE SEAM IS SHARED, NOT COPIED. `HostRunner` is re-exported here so a Linux stack never has to
12
12
  * import from the launchd subpath to type its own runner.
13
13
  */
@@ -26,4 +26,7 @@ export { HostRunnerService, canActAsRoot, hostRunnerLayer } from '../launchd/run
26
26
  export type { SystemdUnitAttributes, SystemdUnitProps, UnitSection } from './unit-form.ts';
27
27
  export { DEFAULT_UNIT_DIRECTORY, renderUnit } from './unit-form.ts';
28
28
  export { SystemdTimer, SystemdTimerProvider, SystemdUnit, SystemdUnitProvider } from './unit.ts';
29
+ export type { ContainerSection, PodmanContainerAttributes, PodmanContainerProps, PodmanUnitLine, } from './container.ts';
30
+ export { QUADLET_DEFAULT_DIRECTORY, QUADLET_SEARCH_DIRECTORIES } from './container-form.ts';
31
+ export { PodmanContainer, PodmanContainerProvider } from './container.ts';
29
32
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/linux/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,YAAY,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAClE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EACV,UAAU,EACV,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,YAAY,GACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACxF,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/linux/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,YAAY,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAClE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EACV,UAAU,EACV,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,YAAY,GACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACxF,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACjG,YAAY,EACV,gBAAgB,EAChB,yBAAyB,EACzB,oBAAoB,EACpB,cAAc,GACf,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAC5F,OAAO,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC"}