@homeflare/alchemy 0.37.7 → 0.38.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 (51) hide show
  1. package/dist/caddy/admin.d.ts +42 -8
  2. package/dist/caddy/admin.d.ts.map +1 -1
  3. package/dist/caddy/config.d.ts +9 -3
  4. package/dist/caddy/config.d.ts.map +1 -1
  5. package/dist/caddy/fake-caddy.d.ts +5 -0
  6. package/dist/caddy/fake-caddy.d.ts.map +1 -1
  7. package/dist/caddy/index.js +10 -6
  8. package/dist/caddy/index.js.map +5 -5
  9. package/dist/caddy/providers.d.ts +19 -12
  10. package/dist/caddy/providers.d.ts.map +1 -1
  11. package/dist/linux/container-form.d.ts +1 -1
  12. package/dist/linux/container-generator.d.ts +28 -0
  13. package/dist/linux/container-generator.d.ts.map +1 -1
  14. package/dist/linux/container-lifecycle.d.ts +18 -1
  15. package/dist/linux/container-lifecycle.d.ts.map +1 -1
  16. package/dist/linux/container-preflight.d.ts +34 -0
  17. package/dist/linux/container-preflight.d.ts.map +1 -1
  18. package/dist/linux/container-settle.d.ts.map +1 -1
  19. package/dist/linux/fake-sudo.d.ts +14 -2
  20. package/dist/linux/fake-sudo.d.ts.map +1 -1
  21. package/dist/linux/index.js +76 -29
  22. package/dist/linux/index.js.map +11 -10
  23. package/dist/linux/sudo-exec.d.ts +16 -0
  24. package/dist/linux/sudo-exec.d.ts.map +1 -1
  25. package/dist/linux/unit-form.d.ts.map +1 -1
  26. package/dist/linux/unit-text.d.ts +35 -0
  27. package/dist/linux/unit-text.d.ts.map +1 -0
  28. package/docs/linux-sudo.md +18 -7
  29. package/package.json +1 -1
  30. package/src/caddy/admin-transport-scope.test.ts +127 -0
  31. package/src/caddy/admin.ts +45 -9
  32. package/src/caddy/config-adopt-provider.test.ts +2 -2
  33. package/src/caddy/config-format-warning.test.ts +9 -3
  34. package/src/caddy/config.ts +20 -4
  35. package/src/caddy/fake-caddy.ts +10 -1
  36. package/src/caddy/providers.test.ts +3 -2
  37. package/src/caddy/providers.ts +18 -11
  38. package/src/linux/container-form.ts +1 -1
  39. package/src/linux/container-generator.test.ts +33 -0
  40. package/src/linux/container-generator.ts +72 -0
  41. package/src/linux/container-lifecycle.test.ts +6 -3
  42. package/src/linux/container-lifecycle.ts +23 -2
  43. package/src/linux/container-preflight.ts +47 -0
  44. package/src/linux/container-settle.ts +12 -1
  45. package/src/linux/container-shadow.test.ts +131 -0
  46. package/src/linux/fake-sudo.ts +21 -5
  47. package/src/linux/sudo-exec.ts +60 -10
  48. package/src/linux/sudo-lifecycle.test.ts +64 -3
  49. package/src/linux/unit-form.ts +4 -18
  50. package/src/linux/unit-text.test.ts +67 -0
  51. package/src/linux/unit-text.ts +58 -0
@@ -27,6 +27,40 @@ export declare const assertUsable: (props: Pick<ContainerProps, 'name'>, status:
27
27
  * this declaration's own leftover, not someone else's.
28
28
  */
29
29
  export declare const assertUnclaimed: (runner: HostRunner, props: ContainerProps) => Promise<void>;
30
+ /**
31
+ * ⛔ A PLAIN UNIT SHARING THE SERVICE NAME CAN OUTRANK QUADLET'S GENERATOR, SILENTLY — but only
32
+ * when it sits in one of the directories that actually search BEFORE `${GENERATED_UNIT_DIRECTORY}`
33
+ * in systemd's own unit load path (systemd.unit(5) "Unit File Load Path") — `/etc/systemd/system`
34
+ * chief among them (`isShadowingFragment`, container-generator.ts, has the full list and the
35
+ * measured facts). ⛔ CORRECTED ON ADVERSARIAL RE-REVIEW: an EARLIER version of this check refused
36
+ * for ANY `FragmentPath` not literally under the generator directory, including the vendor
37
+ * directories (`/usr/local/lib/systemd/system`, `/usr/lib/systemd/system`) — those are LOWER
38
+ * precedence than the generator, not higher, so a plain unit there is harmlessly shadowed BY our
39
+ * generated unit once we write and reload; refusing for it was a false positive that blocked a
40
+ * create/adopt apply would have handled fine (exactly what S49 "neither looser nor stricter"
41
+ * forbids). With no `.container` file of ours on disk yet, a `FragmentPath` under one of the
42
+ * genuinely higher-precedence directories means some OTHER unit file — hand-written, or shipped by
43
+ * a package, and admin-controlled rather than vendor-controlled — already answers to this name:
44
+ * writing and reloading would generate a unit systemd never actually loads, because the
45
+ * higher-precedence plain one still wins.
46
+ * ★ TWIN OF `unit-preflight.ts`'s `assertUnclaimed`, for the one failure mode `Systemd.Unit` can't
47
+ * have: there is no generator standing between ITS file and the unit systemd loads, so nothing
48
+ * there can be shadowed the way a Quadlet generation can.
49
+ * ★ ONLY WHEN THE `.container` FILE ITSELF IS ABSENT (called from `readContainer` in that branch):
50
+ * a present file with different content is `assertUnclaimed`'s job; a present file that IS ours
51
+ * generating a shadowed unit is `verifyGenerated`'s job (container-generator.ts, hardened to check
52
+ * this same `FragmentPath` fact), at apply time, once daemon-reload has actually run — this is the
53
+ * "nothing declared yet" half neither of those covers, and the one this bug leaves refusing only
54
+ * mid-apply instead of at plan time for the directories that genuinely shadow.
55
+ * ⚠️ NEVER FIRES WITHOUT A CONCRETE `fragmentPath` TO NAME, AND NEVER FOR A MASKED UNIT — MEASURED
56
+ * against this family's own fake (`fakeQuadletHost`, unlike `sudo-lifecycle.test.ts`'s, which is
57
+ * a different fake and reads differently): `placeUnit(path, text, {masked: true})` still reports
58
+ * a real `FragmentPath`. Refusing there anyway would work, but with THIS function's generic
59
+ * "plain unit" wording instead of `assertUsable`'s specific, more actionable "is masked, run
60
+ * `systemctl unmask` deliberately" — masking is a person's decision, not an accident to explain
61
+ * as a name collision. `assertUsable` stays the one place that message comes from.
62
+ */
63
+ export declare const assertUnshadowed: (props: Pick<ContainerProps, 'name'>, status: UnitStatus) => void;
30
64
  /** The half of a rename check that needs only the NEW NAME — mirrors `assertRenameTarget`. */
31
65
  export declare const assertRenameTarget: (runner: HostRunner, old: ContainerAttributes, name: string) => Promise<void>;
32
66
  /** A rename, checked at plan time, before the plan may promise a delete-first replace. */
@@ -1 +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"}
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;AAE3D,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,gBAAgB,UAAW,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,UAAU,UAAU,KAAG,IAW1F,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"}
@@ -1 +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"}
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,CAqC7B,CAAC"}
@@ -12,19 +12,31 @@
12
12
  * `mktemp -d` registers a fresh 0700 directory the operator owns.
13
13
  * ★ SUDO REFUSES A CALL THAT WAS NOT LOGGED FIRST, so every test holds the runner to "logged
14
14
  * before it runs", exactly as the Mac fake does.
15
+ * ★ `hostFactory`/`prefixes` (below) let sudo-lifecycle.test.ts drive `fakeQuadletHost` (not just
16
+ * the plain `fakeLinuxHost`) through this SAME elevation wiring, for the Podman.Container-through
17
+ * -sshSudoRunner regression tests — `fakeQuadletHost` returns the exact shape `fakeLinuxHost`
18
+ * does (`FakeLinuxOptions` in, the same fields out), so every handler below that reads
19
+ * `fake.dirs`/`fake.modes`/`fake.files` or calls `fake.runner.exec` works unchanged either way.
15
20
  */
16
21
  import type { FileStat, HostRunner } from '../launchd/runner.ts';
17
22
  import { SUDO_SAYS } from '../launchd/fake-sudo.ts';
23
+ import { type FakeLinuxOptions, fakeLinuxHost } from './fake-linux-host.ts';
18
24
  import { type RootState } from './fake-sudo-root.ts';
19
25
  export { SUDO_SAYS } from '../launchd/fake-sudo.ts';
20
26
  export declare const OPERATOR = 1000;
21
27
  export declare const PREFIXES: string[];
28
+ export type FakeSudoHostOptions = {
29
+ /** Swap `fakeLinuxHost` for a variant that models more than plain units — `fakeQuadletHost`. */
30
+ readonly hostFactory?: (options: FakeLinuxOptions) => ReturnType<typeof fakeLinuxHost>;
31
+ /** The runner's own declared prefixes. @default PREFIXES */
32
+ readonly prefixes?: readonly string[];
33
+ };
22
34
  /**
23
35
  * `extraDirs` adds more directories to the fake filesystem (a pre-existing tree outside every
24
- * prefix, say); it never changes the runner's OWN prefixes, which stay `PREFIXES` throughout.
36
+ * prefix, say); it never changes the runner's OWN prefixes, which default to `PREFIXES`.
25
37
  * `acl` is the set of directory paths a test wants to carry the `+` (POSIX ACL) flag.
26
38
  */
27
- export declare const fakeSudoHost: (extraDirs?: Record<string, number>, acl?: ReadonlySet<string>) => {
39
+ export declare const fakeSudoHost: (extraDirs?: Record<string, number>, acl?: ReadonlySet<string>, { hostFactory, prefixes }?: FakeSudoHostOptions) => {
28
40
  base: HostRunner;
29
41
  fake: {
30
42
  calls: string[][];
@@ -1 +1 @@
1
- {"version":3,"file":"fake-sudo.d.ts","sourceRoot":"","sources":["../../src/linux/fake-sudo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAc,QAAQ,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAAE,KAAK,SAAS,EAAU,MAAM,qBAAqB,CAAC;AAK7D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,eAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,eAAO,MAAM,QAAQ,UAA4C,CAAC;AAyBlE;;;;GAIG;AACH,eAAO,MAAM,YAAY,eACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAC5B,WAAW,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;cAUS,IAAI,GAAG,MAAM,OAAO,SAAS;;CAyE/D,CAAC"}
1
+ {"version":3,"file":"fake-sudo.d.ts","sourceRoot":"","sources":["../../src/linux/fake-sudo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAc,QAAQ,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,KAAK,gBAAgB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,KAAK,SAAS,EAAU,MAAM,qBAAqB,CAAC;AAK7D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,eAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,eAAO,MAAM,QAAQ,UAA4C,CAAC;AAyBlE,MAAM,MAAM,mBAAmB,GAAG;IAChC,gGAAgG;IAChG,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;IACvF,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,eACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAC5B,WAAW,CAAC,MAAM,CAAC,8BAC8B,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;cAaxC,IAAI,GAAG,MAAM,OAAO,SAAS;;CAyE/D,CAAC"}
@@ -635,6 +635,29 @@ var renderJob = (props) => {
635
635
  return { bytes, sha256: sha256Hex(bytes), text };
636
636
  };
637
637
 
638
+ // src/linux/unit-text.ts
639
+ var INSTALL_KEYS = new Set(["WantedBy", "RequiredBy", "UpsertedBy", "Also", "Alias"]);
640
+ var hasInstallSection = (text) => {
641
+ if (!text.includes("[Install]"))
642
+ return false;
643
+ const after = text.slice(text.indexOf("[Install]"));
644
+ return [...INSTALL_KEYS].some((key) => new RegExp(`^${key}=`, "m").test(after));
645
+ };
646
+ var firstSubstantiveLine = (text) => text.split(`
647
+ `).find((line) => !(line.trim() === "" || line.startsWith("#") || line.startsWith(";")));
648
+ var textProblems = (text) => {
649
+ const found = [];
650
+ if (text.trim() === "")
651
+ found.push("the unit file is empty");
652
+ if (text.includes("\x00"))
653
+ found.push("the unit file contains NUL");
654
+ const first = firstSubstantiveLine(text);
655
+ if (first === undefined || !/^\s*\[[A-Za-z]+]/.test(first)) {
656
+ found.push("a unit file must begin with a [Section] (blank lines and #/; comments before it are fine)");
657
+ }
658
+ return found;
659
+ };
660
+
638
661
  // src/linux/unit-form.ts
639
662
  var DEFAULT_UNIT_DIRECTORY = "/etc/systemd/system";
640
663
  var UNIT_SEARCH_DIRECTORIES = [
@@ -678,7 +701,6 @@ var TYPES = new Set([
678
701
  "target",
679
702
  "timer"
680
703
  ]);
681
- var INSTALL_KEYS = new Set(["WantedBy", "RequiredBy", "UpsertedBy", "Also", "Alias"]);
682
704
  var unitProblems = (props, expect) => {
683
705
  const found = [];
684
706
  const type = NAME.exec(props.name)?.[1];
@@ -698,7 +720,7 @@ var unitProblems = (props, expect) => {
698
720
  if (props.enabled !== false && !UNIT_SEARCH_DIRECTORIES.includes(directory)) {
699
721
  found.push(`an enabled unit must live where systemd looks for it by name (${UNIT_SEARCH_DIRECTORIES.join(", ")}); ` + `${directory} is not one of them. Move it, or declare enabled: false.`);
700
722
  }
701
- if (props.enabled !== false && !hasInstall(props)) {
723
+ if (props.enabled !== false && !hasInstallSection(unitText(props))) {
702
724
  found.push("enabled units need an [Install] section with WantedBy/RequiredBy/Also/Alias; without one " + 'systemd calls the unit "static" and `systemctl enable` has nothing to link. Add one, or ' + "declare enabled: false and let something else pull it in.");
703
725
  }
704
726
  if (type === "timer" && !unitText(props).includes("[Timer]")) {
@@ -706,23 +728,6 @@ var unitProblems = (props, expect) => {
706
728
  }
707
729
  return found;
708
730
  };
709
- var hasInstall = (props) => {
710
- const text = unitText(props);
711
- if (!text.includes("[Install]"))
712
- return false;
713
- const after = text.slice(text.indexOf("[Install]"));
714
- return [...INSTALL_KEYS].some((key) => new RegExp(`^${key}=`, "m").test(after));
715
- };
716
- var textProblems = (text) => {
717
- const found = [];
718
- if (text.trim() === "")
719
- found.push("the unit file is empty");
720
- if (text.includes("\x00"))
721
- found.push("the unit file contains NUL");
722
- if (!/^\s*\[[A-Za-z]+]/.test(text))
723
- found.push("a unit file must begin with a [Section]");
724
- return found;
725
- };
726
731
 
727
732
  // src/linux/systemctl.ts
728
733
  var SYSTEMCTL = "systemctl";
@@ -802,6 +807,19 @@ var stopUnit = (runner, name) => run(runner, ["stop", "--", name]);
802
807
  var restartUnit = (runner, name) => run(runner, ["restart", "--", name]);
803
808
 
804
809
  // src/linux/container-generator.ts
810
+ var GENERATED_UNIT_DIRECTORY = "/run/systemd/generator";
811
+ var isGeneratorFragment = (fragmentPath) => fragmentPath !== undefined && fragmentPath.startsWith(`${GENERATED_UNIT_DIRECTORY}/`);
812
+ var SHADOWING_UNIT_DIRECTORIES = [
813
+ "/etc/systemd/system.control",
814
+ "/run/systemd/system.control",
815
+ "/run/systemd/transient",
816
+ "/etc/systemd/system",
817
+ "/etc/systemd/system.attached",
818
+ "/run/systemd/system",
819
+ "/run/systemd/system.attached"
820
+ ];
821
+ var isShadowingFragment = (fragmentPath) => fragmentPath !== undefined && SHADOWING_UNIT_DIRECTORIES.some((dir) => fragmentPath.startsWith(`${dir}/`));
822
+
805
823
  class QuadletGeneratorError extends Error {
806
824
  constructor(name, detail) {
807
825
  super(`Podman.Container ${name}: ${detail}`);
@@ -814,6 +832,9 @@ var verifyGenerated = (name, containerPath, serviceName, status) => {
814
832
  if (!status.known || status.loadState !== "loaded") {
815
833
  throw new QuadletGeneratorError(name, `${serviceName} does not exist after daemon-reload (LoadState=${status.loadState}). The ` + ".container file was written and reloaded, so Quadlet’s generator refused it — a bad " + "key, a bad value, or an interaction this validation does not check. Debug on the host " + "with '/usr/lib/systemd/system-generators/podman-system-generator --dryrun' or " + `'systemd-analyze --generators=true verify ${serviceName}' (podman-systemd.unit(5), Podman 5.4).`);
816
834
  }
835
+ if (status.fragmentPath !== undefined && !isGeneratorFragment(status.fragmentPath)) {
836
+ throw new QuadletGeneratorError(name, `${serviceName} is loaded from ${status.fragmentPath}, not from Quadlet’s generator ` + `(${GENERATED_UNIT_DIRECTORY}/…) — daemon-reload ran, but a plain unit at that path still ` + "answers to this name, and systemd loaded IT, not ours. Move the plain unit aside — a " + "cutover — and redeploy.");
837
+ }
817
838
  if (status.sourcePath !== undefined && status.sourcePath !== containerPath) {
818
839
  throw new QuadletGeneratorError(name, `${serviceName}'s SourcePath is ${status.sourcePath}, not ${containerPath} — a same-named ` + ".container file earlier in Quadlet’s search path is generating this unit instead of ours " + "(podman-systemd.unit(5) “Podman Unit Search Path”, Podman 5.4).");
819
840
  }
@@ -954,6 +975,15 @@ var assertUnclaimed = async (runner, props) => {
954
975
  throw refuseContainer(props.name, `${path} is already on this host and is not this resource. Remove it, or declare it as a ` + "new resource and deploy with --adopt.");
955
976
  }
956
977
  };
978
+ var assertUnshadowed = (props, status) => {
979
+ if (!status.known || status.fragmentPath === undefined)
980
+ return;
981
+ if (status.loadState === "masked" || status.unitFileState === "masked")
982
+ return;
983
+ if (!isShadowingFragment(status.fragmentPath))
984
+ return;
985
+ throw refuseContainer(props.name, `${serviceNameFor(props)} already exists as a plain unit at ${status.fragmentPath}, not a ` + `Quadlet generation — Quadlet's own output (${GENERATED_UNIT_DIRECTORY}/…) would be ` + "shadowed by it and never actually run: systemd loads the higher-precedence file first. " + "Move the plain unit aside — a cutover — before declaring this container.");
986
+ };
957
987
  var assertRenameTarget = async (runner, old, name) => {
958
988
  assertMayWrite(runner, old.name);
959
989
  assertMayWrite(runner, name);
@@ -991,7 +1021,11 @@ var settle = async (runner, props, configSha256, step) => {
991
1021
  return;
992
1022
  });
993
1023
  }
994
- throw refuseContainer(props.name, `${detail} The container's unit is NOT running.`);
1024
+ const after2 = await showUnit(runner, step.serviceName).catch(() => {
1025
+ return;
1026
+ });
1027
+ const stateNote = after2 === undefined ? "Its running state was not re-checked." : `Its unit is now ${after2.activeState}.`;
1028
+ throw refuseContainer(props.name, `${detail} ${stateNote}`);
995
1029
  };
996
1030
  try {
997
1031
  if (!wantStarted) {
@@ -1018,8 +1052,11 @@ var readContainer = async (runner, props) => {
1018
1052
  const path = containerPathFor(props);
1019
1053
  const bytes = await runner.readFile(path);
1020
1054
  const status = await showUnit(runner, serviceNameFor(props));
1021
- if (bytes === undefined && !status.known)
1022
- return;
1055
+ if (bytes === undefined) {
1056
+ if (!status.known)
1057
+ return;
1058
+ assertUnshadowed(props, status);
1059
+ }
1023
1060
  const sha = bytes === undefined ? "" : digestOf(decoder2.decode(bytes));
1024
1061
  return attributesOf(props, configDigest(props.restartOn), sha, path, serviceNameFor(props), status);
1025
1062
  };
@@ -2870,6 +2907,11 @@ var assertInstallable2 = async (base, prefix, path, options, groups) => {
2870
2907
  };
2871
2908
 
2872
2909
  // src/linux/sudo-exec.ts
2910
+ var GENERATOR_DIRECTORIES = [
2911
+ GENERATED_UNIT_DIRECTORY,
2912
+ `${GENERATED_UNIT_DIRECTORY}.late`
2913
+ ];
2914
+ var isUnderGeneratorDirectory = (fragmentPath) => GENERATOR_DIRECTORIES.some((dir) => fragmentPath.startsWith(`${dir}/`));
2873
2915
  var DIR_EXPECT = {
2874
2916
  chmod: "directory",
2875
2917
  chown: "directory",
@@ -2883,17 +2925,18 @@ var unitStatusOf = async (base, unit) => {
2883
2925
  "show",
2884
2926
  "--no-pager",
2885
2927
  "-p",
2886
- "FragmentPath,LoadState",
2928
+ "FragmentPath,LoadState,SourcePath",
2887
2929
  "--",
2888
2930
  unit
2889
2931
  ]);
2890
2932
  if (result.exitCode !== 0) {
2891
- throw new SudoRefusedError(`sshSudoRunner ${unit}: could not read FragmentPath/LoadState (systemctl show exit ` + `${String(result.exitCode)}). Nothing ran as root.`);
2933
+ throw new SudoRefusedError(`sshSudoRunner ${unit}: could not read FragmentPath/LoadState/SourcePath (systemctl show ` + `exit ${String(result.exitCode)}). Nothing ran as root.`);
2892
2934
  }
2893
2935
  const fields = parseShow(result.stdout);
2894
2936
  return {
2895
2937
  fragmentPath: fields.get("FragmentPath") ?? "",
2896
- loadState: fields.get("LoadState") ?? ""
2938
+ loadState: fields.get("LoadState") ?? "",
2939
+ sourcePath: fields.get("SourcePath") ?? ""
2897
2940
  };
2898
2941
  };
2899
2942
  var elevatedExec = (base, elevate, prefixes, argv) => {
@@ -2914,7 +2957,7 @@ var elevatedExec = (base, elevate, prefixes, argv) => {
2914
2957
  const verb = canonical[1] ?? "";
2915
2958
  const unit = canonical[3] ?? "";
2916
2959
  return elevate(canonical, {}, async () => {
2917
- const { fragmentPath, loadState } = await unitStatusOf(base, unit);
2960
+ const { fragmentPath, loadState, sourcePath } = await unitStatusOf(base, unit);
2918
2961
  if (loadState === "masked") {
2919
2962
  throw new SudoRefusedError(`sshSudoRunner ${unit}: is masked. Masking is someone's decision, not drift; unmask it ` + "deliberately, then redeploy. Nothing ran as root.");
2920
2963
  }
@@ -2923,9 +2966,13 @@ var elevatedExec = (base, elevate, prefixes, argv) => {
2923
2966
  return;
2924
2967
  throw new SudoRefusedError(`sshSudoRunner ${unit}: has no unit file (FragmentPath empty), and ${verb} is not ` + "stop/disable — refusing rather than run it against whatever systemd currently thinks " + `${unit} is (a kernel-generated unit, say). Nothing ran as root.`);
2925
2968
  }
2926
- if (prefixOf(fragmentPath, prefixes) === undefined) {
2927
- throw new SudoRefusedError(`sshSudoRunner ${unit}: FragmentPath ${fragmentPath} is outside every declared prefix ` + `(${prefixes.join(", ")}); refusing to touch a unit this runner does not own. Nothing ran as root.`);
2969
+ if (prefixOf(fragmentPath, prefixes) !== undefined)
2970
+ return;
2971
+ if (isUnderGeneratorDirectory(fragmentPath) && sourcePath !== "" && prefixOf(sourcePath, prefixes) !== undefined) {
2972
+ return;
2928
2973
  }
2974
+ const sourceNote = sourcePath === "" ? "" : `, and its SourcePath ${sourcePath} is not under one either`;
2975
+ throw new SudoRefusedError(`sshSudoRunner ${unit}: FragmentPath ${fragmentPath} is outside every declared prefix ` + `(${prefixes.join(", ")})${sourceNote}; refusing to touch a unit this runner does not ` + "own. Nothing ran as root.");
2929
2976
  });
2930
2977
  };
2931
2978
 
@@ -3132,5 +3179,5 @@ export {
3132
3179
  sshSudoRunner
3133
3180
  };
3134
3181
 
3135
- //# debugId=F28A7095F5BC6E5964756E2164756E21
3182
+ //# debugId=47589007F27006E864756E2164756E21
3136
3183
  //# sourceMappingURL=index.js.map