@amalgm/shell 0.1.162 → 0.1.164-main.34182640231

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 (144) hide show
  1. package/PURPOSE.md +163 -17
  2. package/README.md +74 -11
  3. package/config/releases/trust/runtime/main.root.json +84 -0
  4. package/config/releases/trust/runtime/preview.root.json +84 -0
  5. package/dist/authorization/recovery-host.d.ts +1 -0
  6. package/dist/authorization/recovery-host.js +2 -3
  7. package/dist/authorization/recovery-host.js.map +1 -1
  8. package/dist/boot.js +28 -20
  9. package/dist/boot.js.map +1 -1
  10. package/dist/cli-arguments.d.ts +8 -0
  11. package/dist/cli-arguments.js +47 -0
  12. package/dist/cli-arguments.js.map +1 -0
  13. package/dist/cli.d.ts +6 -4
  14. package/dist/cli.js +91 -73
  15. package/dist/cli.js.map +1 -1
  16. package/dist/compose.js +4 -3
  17. package/dist/compose.js.map +1 -1
  18. package/dist/content-cache-index.js +2 -2
  19. package/dist/content-cache-index.js.map +1 -1
  20. package/dist/detection/runtime.js +2 -2
  21. package/dist/detection/runtime.js.map +1 -1
  22. package/dist/development/profile.d.ts +27 -0
  23. package/dist/development/profile.js +142 -0
  24. package/dist/development/profile.js.map +1 -0
  25. package/dist/entity-apply-host.js +2 -2
  26. package/dist/entity-apply-host.js.map +1 -1
  27. package/dist/entity-apply-store.d.ts +2 -1
  28. package/dist/entity-apply-store.js +52 -37
  29. package/dist/entity-apply-store.js.map +1 -1
  30. package/dist/files-register-host.d.ts +1 -0
  31. package/dist/files-register-host.js +1 -0
  32. package/dist/files-register-host.js.map +1 -1
  33. package/dist/installation/environment.d.ts +30 -0
  34. package/dist/installation/environment.js +28 -0
  35. package/dist/installation/environment.js.map +1 -0
  36. package/dist/installation/lease.d.ts +6 -0
  37. package/dist/installation/lease.js +28 -0
  38. package/dist/installation/lease.js.map +1 -0
  39. package/dist/installation/ownership.d.ts +30 -0
  40. package/dist/installation/ownership.js +156 -0
  41. package/dist/installation/ownership.js.map +1 -0
  42. package/dist/installation/selection.d.ts +8 -0
  43. package/dist/installation/selection.js +29 -0
  44. package/dist/installation/selection.js.map +1 -0
  45. package/dist/machine-host.d.ts +27 -5
  46. package/dist/machine-host.js +244 -96
  47. package/dist/machine-host.js.map +1 -1
  48. package/dist/main.js +56 -2
  49. package/dist/main.js.map +1 -1
  50. package/dist/platform-product-host.d.ts +9 -9
  51. package/dist/platform-product-host.js +17 -17
  52. package/dist/platform-product-host.js.map +1 -1
  53. package/dist/platform-tools.d.ts +4 -3
  54. package/dist/platform-tools.js +34 -37
  55. package/dist/platform-tools.js.map +1 -1
  56. package/dist/process-lifecycle.d.ts +6 -0
  57. package/dist/process-lifecycle.js +42 -0
  58. package/dist/process-lifecycle.js.map +1 -0
  59. package/dist/pty-host.d.ts +1 -1
  60. package/dist/pty-host.js +70 -21
  61. package/dist/pty-host.js.map +1 -1
  62. package/dist/runtime-command-host.d.ts +7 -0
  63. package/dist/runtime-command-host.js +49 -13
  64. package/dist/runtime-command-host.js.map +1 -1
  65. package/dist/service/adapter.d.ts +36 -0
  66. package/dist/service/adapter.js +24 -0
  67. package/dist/service/adapter.js.map +1 -0
  68. package/dist/service/cli.d.ts +8 -0
  69. package/dist/service/cli.js +237 -0
  70. package/dist/service/cli.js.map +1 -0
  71. package/dist/service/control.d.ts +38 -0
  72. package/dist/service/control.js +68 -0
  73. package/dist/service/control.js.map +1 -0
  74. package/dist/service/index.d.ts +9 -0
  75. package/dist/service/index.js +10 -0
  76. package/dist/service/index.js.map +1 -0
  77. package/dist/service/keepalive.d.ts +18 -0
  78. package/dist/service/keepalive.js +418 -0
  79. package/dist/service/keepalive.js.map +1 -0
  80. package/dist/service/launchd.d.ts +9 -0
  81. package/dist/service/launchd.js +105 -0
  82. package/dist/service/launchd.js.map +1 -0
  83. package/dist/service/owner.d.ts +4 -0
  84. package/dist/service/owner.js +14 -0
  85. package/dist/service/owner.js.map +1 -0
  86. package/dist/service/portable.d.ts +10 -0
  87. package/dist/service/portable.js +32 -0
  88. package/dist/service/portable.js.map +1 -0
  89. package/dist/service/recovery.d.ts +27 -0
  90. package/dist/service/recovery.js +58 -0
  91. package/dist/service/recovery.js.map +1 -0
  92. package/dist/service/registration.d.ts +82 -0
  93. package/dist/service/registration.js +114 -0
  94. package/dist/service/registration.js.map +1 -0
  95. package/dist/service/systemd-unit.d.ts +7 -0
  96. package/dist/service/systemd-unit.js +43 -0
  97. package/dist/service/systemd-unit.js.map +1 -0
  98. package/dist/service/systemd.d.ts +5 -0
  99. package/dist/service/systemd.js +72 -0
  100. package/dist/service/systemd.js.map +1 -0
  101. package/dist/service/windows-action.d.ts +9 -0
  102. package/dist/service/windows-action.js +55 -0
  103. package/dist/service/windows-action.js.map +1 -0
  104. package/dist/service/windows-task.d.ts +9 -0
  105. package/dist/service/windows-task.js +46 -0
  106. package/dist/service/windows-task.js.map +1 -0
  107. package/dist/service/windows.d.ts +2 -0
  108. package/dist/service/windows.js +65 -0
  109. package/dist/service/windows.js.map +1 -0
  110. package/dist/sqlite.d.ts +4 -0
  111. package/dist/sqlite.js +16 -0
  112. package/dist/sqlite.js.map +1 -0
  113. package/dist/updates/artifacts.d.ts +40 -0
  114. package/dist/updates/artifacts.js +299 -0
  115. package/dist/updates/artifacts.js.map +1 -0
  116. package/dist/updates/bootstrap.d.ts +48 -0
  117. package/dist/updates/bootstrap.js +384 -0
  118. package/dist/updates/bootstrap.js.map +1 -0
  119. package/dist/updates/controller.d.ts +66 -0
  120. package/dist/updates/controller.js +322 -0
  121. package/dist/updates/controller.js.map +1 -0
  122. package/dist/updates/isolated-command-lifetime.d.ts +1 -0
  123. package/dist/updates/isolated-command-lifetime.js +63 -0
  124. package/dist/updates/isolated-command-lifetime.js.map +1 -0
  125. package/dist/updates/store.d.ts +41 -0
  126. package/dist/updates/store.js +80 -0
  127. package/dist/updates/store.js.map +1 -0
  128. package/dist/updates/trust.d.ts +20 -0
  129. package/dist/updates/trust.js +138 -0
  130. package/dist/updates/trust.js.map +1 -0
  131. package/dist/user-ground-host.d.ts +4 -1
  132. package/dist/user-ground-host.js +16 -3
  133. package/dist/user-ground-host.js.map +1 -1
  134. package/dist/watching/host.js +4 -2
  135. package/dist/watching/host.js.map +1 -1
  136. package/dist/workspace-add-staging.js +2 -2
  137. package/dist/workspace-add-staging.js.map +1 -1
  138. package/examinations/fixtures/machine-control-plane.js +57 -0
  139. package/examinations/fixtures/private-wire-authority.js +1065 -0
  140. package/examinations/native-service.mjs +92 -0
  141. package/examinations/network-view.mjs +202 -0
  142. package/examinations/runtime-upgrade.native-test.mjs +198 -0
  143. package/npm-shrinkwrap.json +2097 -218
  144. package/package.json +95 -14
@@ -0,0 +1,27 @@
1
+ import { type ChildRestartPolicy } from '@amalgm/core/supervision';
2
+ import type { ServiceIdentity } from './registration.js';
3
+ export interface RecoveryState {
4
+ readonly attempts: number;
5
+ readonly maxAttempts: number;
6
+ readonly generation: string | null;
7
+ readonly lastFailure: string | null;
8
+ readonly lastHealthyGeneration: string | null;
9
+ readonly lastHealthyAt: string | null;
10
+ readonly parked: boolean;
11
+ }
12
+ /** Only the controller holding the installation lease opens this journal.
13
+ * Charge Core's allowance before the host effect: even death during spawn
14
+ * cannot turn controller replacement into a fresh recovery budget. */
15
+ export declare class RuntimeRecovery {
16
+ readonly policy: ChildRestartPolicy;
17
+ private readonly database;
18
+ readonly maxAttempts: number;
19
+ constructor(identity: ServiceIdentity, policy?: ChildRestartPolicy);
20
+ get state(): RecoveryState;
21
+ get delayMs(): number;
22
+ begin(generation: string): void;
23
+ healthy(generation: string): void;
24
+ exited(generation: string, failure: string | null): void;
25
+ retry(): void;
26
+ close(): void;
27
+ }
@@ -0,0 +1,58 @@
1
+ import { CHILD_RESTART_POLICY } from '@amalgm/core/supervision';
2
+ import { join } from 'node:path';
3
+ import { openFileDatabase } from '../sqlite.js';
4
+ /** Only the controller holding the installation lease opens this journal.
5
+ * Charge Core's allowance before the host effect: even death during spawn
6
+ * cannot turn controller replacement into a fresh recovery budget. */
7
+ export class RuntimeRecovery {
8
+ policy;
9
+ database;
10
+ maxAttempts;
11
+ constructor(identity, policy = CHILD_RESTART_POLICY) {
12
+ this.policy = policy;
13
+ this.maxAttempts = policy.maxRestarts + 1;
14
+ this.database = openFileDatabase(join(identity.serviceDir, 'recovery.sqlite'));
15
+ try {
16
+ this.database.pragma('synchronous = FULL');
17
+ this.database.exec(`CREATE TABLE IF NOT EXISTS recovery (
18
+ id INTEGER PRIMARY KEY CHECK (id = 1),
19
+ attempts INTEGER NOT NULL CHECK (attempts >= 0),
20
+ generation TEXT, lastFailure TEXT, lastHealthyGeneration TEXT, lastHealthyAt TEXT
21
+ );
22
+ INSERT OR IGNORE INTO recovery (id, attempts) VALUES (1, 0);
23
+ UPDATE recovery SET generation = NULL,
24
+ lastFailure = 'Controller exited before its runtime drained'
25
+ WHERE generation IS NOT NULL;`);
26
+ }
27
+ catch (error) {
28
+ this.database.close();
29
+ throw error;
30
+ }
31
+ }
32
+ get state() {
33
+ const row = this.database.prepare('SELECT attempts, generation, lastFailure, lastHealthyGeneration, lastHealthyAt FROM recovery WHERE id = 1').get();
34
+ return { ...row, maxAttempts: this.maxAttempts, parked: row.generation === null && row.attempts >= this.maxAttempts };
35
+ }
36
+ get delayMs() {
37
+ const { attempts } = this.state;
38
+ return attempts === 0 ? 0 : Math.min(this.policy.initialDelayMs * 2 ** (attempts - 1), this.policy.maxDelayMs);
39
+ }
40
+ begin(generation) {
41
+ const result = this.database.prepare('UPDATE recovery SET attempts = attempts + 1, generation = ? WHERE id = 1 AND generation IS NULL AND attempts < ?').run(generation, this.maxAttempts);
42
+ if (result.changes !== 1)
43
+ throw new Error('The runtime recovery allowance is exhausted or a generation is still owned.');
44
+ }
45
+ healthy(generation) {
46
+ this.database.prepare('UPDATE recovery SET attempts = 0, lastFailure = NULL, lastHealthyGeneration = ?, lastHealthyAt = ? WHERE id = 1 AND generation = ?')
47
+ .run(generation, new Date().toISOString(), generation);
48
+ }
49
+ exited(generation, failure) {
50
+ this.database.prepare('UPDATE recovery SET generation = NULL, lastFailure = COALESCE(?, lastFailure) WHERE id = 1 AND generation = ?')
51
+ .run(failure?.slice(0, 1_024) ?? null, generation);
52
+ }
53
+ retry() {
54
+ this.database.prepare('UPDATE recovery SET attempts = 0, lastFailure = NULL WHERE id = 1 AND generation IS NULL AND attempts >= ?').run(this.maxAttempts);
55
+ }
56
+ close() { this.database.close(); }
57
+ }
58
+ //# sourceMappingURL=recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.js","sourceRoot":"","sources":["../../src/service/recovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAA2B,MAAM,0BAA0B,CAAC;AACzF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAahD;;sEAEsE;AACtE,MAAM,OAAO,eAAe;IAIsB;IAH/B,QAAQ,CAAC;IACjB,WAAW,CAAS;IAE7B,YAAY,QAAyB,EAAW,SAA6B,oBAAoB;QAAjD,WAAM,GAAN,MAAM,CAA2C;QAC/F,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,GAAG,CAAC,CAAC;QAC1C,IAAI,CAAC,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC,CAAC;QAC/E,IAAI,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAC3C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;;;;;;;;sCAQa,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YAAC,MAAM,KAAK,CAAC;QAAC,CAAC;IACzD,CAAC;IAED,IAAI,KAAK;QACP,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,2GAA2G,CAAC,CAAC,GAAG,EAAmD,CAAC;QACtM,OAAO,EAAE,GAAG,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,UAAU,KAAK,IAAI,IAAI,GAAG,CAAC,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;IACxH,CAAC;IAED,IAAI,OAAO;QACT,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;QAChC,OAAO,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACjH,CAAC;IAED,KAAK,CAAC,UAAkB;QACtB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,kHAAkH,CAAC,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QAC3L,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;IAC3H,CAAC;IAED,OAAO,CAAC,UAAkB;QACxB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,oIAAoI,CAAC;aACxJ,GAAG,CAAC,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,UAAU,CAAC,CAAC;IAC3D,CAAC;IAED,MAAM,CAAC,UAAkB,EAAE,OAAsB;QAC/C,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,+GAA+G,CAAC;aACnI,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,UAAU,CAAC,CAAC;IACvD,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,4GAA4G,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC5J,CAAC;IAED,KAAK,KAAW,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;CACzC"}
@@ -0,0 +1,82 @@
1
+ import { type RuntimeBinding } from "@amalgm/core/identity";
2
+ import { type ServiceState } from "@amalgm/core/supervision";
3
+ /** The OS owners. `portable` is "no OS owner: you run the keepalive in the foreground". */
4
+ export type Backend = "launchd" | "systemd" | "windows" | "portable";
5
+ /** What selects one service: one user, one runtime label, one Amalgm root. */
6
+ export interface ServiceSelection {
7
+ readonly user: string;
8
+ readonly runtimeLabel?: string | undefined;
9
+ readonly branch?: string | undefined;
10
+ readonly localSession?: string | undefined;
11
+ readonly home?: string | undefined;
12
+ readonly amalgmRoot?: string | undefined;
13
+ }
14
+ export interface ServiceIdentity extends ServiceSelection {
15
+ readonly binding: RuntimeBinding;
16
+ readonly label: string;
17
+ readonly labelScope: string;
18
+ readonly home: string;
19
+ readonly amalgmRoot: string;
20
+ /** `<amalgmRoot>/.amalgm/service/<label>`: registration, keepalive state, logs. */
21
+ readonly serviceDir: string;
22
+ }
23
+ /** The Node and Shell entry the service runs; both are absolute and version-stable. */
24
+ export interface Launcher {
25
+ readonly node: string;
26
+ readonly bin: string;
27
+ readonly packageRoot: string;
28
+ readonly packageVersion: string;
29
+ }
30
+ /** The registration record, shaped as Core's `ServiceState` plus what to run. */
31
+ export interface Registration extends ServiceState {
32
+ readonly binding: RuntimeBinding;
33
+ readonly user: string;
34
+ readonly amalgm_root: string;
35
+ readonly backend: Backend;
36
+ readonly label: string;
37
+ readonly package_root: string;
38
+ readonly package_version: string;
39
+ readonly node: string;
40
+ readonly bin: string;
41
+ readonly runtime_host: "node";
42
+ readonly local_only: false;
43
+ readonly files: Readonly<Record<string, string>>;
44
+ /** Selects the service dir: passed to `amalgm service keepalive`. */
45
+ readonly keepalive_args: readonly string[];
46
+ /** Selects the runtime: passed to `amalgm run` by the keepalive. */
47
+ readonly run_args: readonly string[];
48
+ readonly installed_at: string;
49
+ }
50
+ export declare function serviceIdentity(selection: ServiceSelection): ServiceIdentity;
51
+ /**
52
+ * The launcher that is running right now. A desktop bundle may not own the
53
+ * service: registering the bundled copy is the auto-update loop (npm
54
+ * updates one tree while the OS reboots the frozen bundle forever), and an
55
+ * Electron binary posing as Node is not the Node the unit must name.
56
+ */
57
+ export declare function currentLauncher(facts?: {
58
+ readonly execPath?: string;
59
+ readonly electron?: boolean;
60
+ }): Launcher;
61
+ /**
62
+ * Which owner keeps the service alive. `auto` asks the platform: launchd on
63
+ * macOS, Task Scheduler on Windows, the user's systemd where it answers,
64
+ * and otherwise portable. An explicit mode must fit the platform.
65
+ */
66
+ export declare function detectBackend(mode: unknown, facts: {
67
+ readonly platform: string;
68
+ readonly userSystemd: () => boolean;
69
+ }): Backend;
70
+ export declare function registrationFile(identity: ServiceIdentity): string;
71
+ export declare function readRegistration(identity: ServiceIdentity): Registration | null;
72
+ export declare function writeRegistration(identity: ServiceIdentity, registration: Registration): void;
73
+ /**
74
+ * An existing registration already says everything this install would say:
75
+ * same package tree and version, same owner, same arguments, files present.
76
+ * Re-running install then leaves the running service alone.
77
+ */
78
+ export declare function registrationSatisfies(existing: Registration | null, wanted: {
79
+ readonly launcher: Launcher;
80
+ readonly backend: Backend;
81
+ readonly run_args: readonly string[];
82
+ }): boolean;
@@ -0,0 +1,114 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * The service drawer: who this service is (its label), where its files live,
4
+ * which launcher installed it, and which OS owner keeps it alive.
5
+ *
6
+ * Core states the law — the label (`serviceLabel`, `serviceLabelScope`), the
7
+ * launcher-artifact paths, the registration shape (`ServiceState`), and the
8
+ * decisions over it (`stateMatchesRequest`, `normalizeMode`,
9
+ * `pathLooksLikeDesktopBundle`). This file only reads the machine and writes
10
+ * one JSON record; it never loads the runtime.
11
+ */
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { USER_CONTROL_DIR_NAME, resolveRuntimeBinding, runtimeBindingsMatch, runtimeRootSegments } from "@amalgm/core/identity";
17
+ import { normalizeMode, pathLooksLikeDesktopBundle, serviceLabel, stateMatchesRequest, } from "@amalgm/core/supervision";
18
+ import { atomicJson, readJson } from "../filesystem.js";
19
+ export function serviceIdentity(selection) {
20
+ const user = selection.user.trim().toLowerCase();
21
+ const binding = resolveRuntimeBinding(selection);
22
+ if (!user)
23
+ throw new Error("A service is registered for one user: pass --user EMAIL");
24
+ const home = selection.home ? resolve(selection.home) : homedir();
25
+ const amalgmRoot = selection.amalgmRoot ? resolve(selection.amalgmRoot) : join(home, ...runtimeRootSegments(binding));
26
+ const labelScope = `${binding.runtimeLabel}.${createHash("sha256").update(JSON.stringify([user, binding])).digest("hex").slice(0, 32)}`;
27
+ const label = serviceLabel(labelScope);
28
+ return {
29
+ ...selection,
30
+ ...binding,
31
+ binding,
32
+ user,
33
+ home,
34
+ amalgmRoot,
35
+ labelScope,
36
+ label,
37
+ serviceDir: join(amalgmRoot, USER_CONTROL_DIR_NAME, "service", label),
38
+ };
39
+ }
40
+ const PACKAGE_ROOT = fileURLToPath(new URL("../../", import.meta.url)).replace(/[\\/]+$/, "");
41
+ const MAIN_ENTRY = fileURLToPath(new URL("../main.js", import.meta.url));
42
+ /**
43
+ * The launcher that is running right now. A desktop bundle may not own the
44
+ * service: registering the bundled copy is the auto-update loop (npm
45
+ * updates one tree while the OS reboots the frozen bundle forever), and an
46
+ * Electron binary posing as Node is not the Node the unit must name.
47
+ */
48
+ export function currentLauncher(facts = {}) {
49
+ const node = facts.execPath ?? process.execPath;
50
+ const electron = facts.electron ?? Boolean(process.versions.electron);
51
+ if (electron || pathLooksLikeDesktopBundle(node) || pathLooksLikeDesktopBundle(MAIN_ENTRY)) {
52
+ throw new Error("The always-on service must be installed from the npm package (npm install -g @amalgm/shell); "
53
+ + "a desktop bundle cannot own it");
54
+ }
55
+ const packageJson = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
56
+ return { node, bin: MAIN_ENTRY, packageRoot: PACKAGE_ROOT, packageVersion: String(packageJson.version) };
57
+ }
58
+ /**
59
+ * Which owner keeps the service alive. `auto` asks the platform: launchd on
60
+ * macOS, Task Scheduler on Windows, the user's systemd where it answers,
61
+ * and otherwise portable. An explicit mode must fit the platform.
62
+ */
63
+ export function detectBackend(mode, facts) {
64
+ const requested = normalizeMode(mode);
65
+ const native = facts.platform === "darwin" ? "launchd"
66
+ : facts.platform === "win32" ? "windows"
67
+ : facts.userSystemd() ? "systemd"
68
+ : "portable";
69
+ if (requested === "auto")
70
+ return native;
71
+ if (requested === "portable")
72
+ return "portable";
73
+ if ((requested === "launchd") !== (facts.platform === "darwin")
74
+ || (requested === "windows") !== (facts.platform === "win32")
75
+ || (requested === "systemd" && native !== "systemd")) {
76
+ throw new Error(`Service mode "${requested}" is not available on this machine (auto would choose ${native})`);
77
+ }
78
+ return requested;
79
+ }
80
+ export function registrationFile(identity) {
81
+ return join(identity.serviceDir, "registration.json");
82
+ }
83
+ export function readRegistration(identity) {
84
+ const record = readJson(registrationFile(identity));
85
+ if (!record)
86
+ return null;
87
+ if (record.label !== identity.label || typeof record.bin !== "string"
88
+ || record.user !== identity.user || record.amalgm_root !== identity.amalgmRoot
89
+ || !runtimeBindingsMatch(record.binding, identity.binding)) {
90
+ throw new Error('The service registration belongs to a different installation.');
91
+ }
92
+ return record;
93
+ }
94
+ export function writeRegistration(identity, registration) {
95
+ atomicJson(registrationFile(identity), registration);
96
+ }
97
+ /**
98
+ * An existing registration already says everything this install would say:
99
+ * same package tree and version, same owner, same arguments, files present.
100
+ * Re-running install then leaves the running service alone.
101
+ */
102
+ export function registrationSatisfies(existing, wanted) {
103
+ return existing !== null
104
+ && stateMatchesRequest(existing, { mode: wanted.backend }, {
105
+ packageRoot: wanted.launcher.packageRoot,
106
+ packageVersion: wanted.launcher.packageVersion,
107
+ autoBackend: wanted.backend,
108
+ })
109
+ && existing.node === wanted.launcher.node
110
+ && existing.bin === wanted.launcher.bin
111
+ && JSON.stringify(existing.run_args) === JSON.stringify(wanted.run_args)
112
+ && Object.values(existing.files).every((file) => existsSync(file));
113
+ }
114
+ //# sourceMappingURL=registration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registration.js","sourceRoot":"","sources":["../../src/service/registration.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /** systemd directives have distinct grammars; this module owns their encoding. */
2
+ export interface SystemdUnitInput {
3
+ readonly command: readonly string[];
4
+ readonly environment: Readonly<Record<string, string>>;
5
+ readonly workingDirectory: string;
6
+ }
7
+ export declare function renderSystemdUnit(input: SystemdUnitInput): string;
@@ -0,0 +1,43 @@
1
+ /** Quoted words use C escapes followed by systemd's percent-specifier expansion. */
2
+ function word(value) {
3
+ if (value.includes('\0'))
4
+ throw new Error('A service argument or environment value cannot contain NUL');
5
+ return `"${value.replace(/[\x01-\x1f\x7f"\\%]/g, (character) => {
6
+ if (character === '%')
7
+ return '%%';
8
+ if (character === '"' || character === '\\')
9
+ return `\\${character}`;
10
+ return `\\x${character.charCodeAt(0).toString(16).padStart(2, '0')}`;
11
+ })}"`;
12
+ }
13
+ export function renderSystemdUnit(input) {
14
+ if (!input.command.length || !input.command[0].startsWith('/'))
15
+ throw new Error('A service requires an absolute executable');
16
+ if (!input.workingDirectory.startsWith('/'))
17
+ throw new Error('A service requires an absolute working directory');
18
+ // systemd trims WorkingDirectory again when serializing its execution context.
19
+ // GNU env changes directory from literal argv, then execs the same owned PID.
20
+ const command = ['/usr/bin/env', '--chdir', input.workingDirectory, '--', ...input.command];
21
+ const env = Object.entries(input.environment).map(([key, value]) => {
22
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
23
+ throw new Error('Invalid service environment name');
24
+ return `Environment=${word(`${key}=${value}`)}`;
25
+ }).join('\n');
26
+ return `[Unit]
27
+ Description=Amalgm always-on runtime
28
+ StartLimitIntervalSec=0
29
+
30
+ [Service]
31
+ Type=exec
32
+ ${env}
33
+ ExecStart=:${command.map(word).join(' ')}
34
+ Restart=on-failure
35
+ RestartSec=5
36
+ KillMode=mixed
37
+ TimeoutStopSec=30
38
+
39
+ [Install]
40
+ WantedBy=default.target
41
+ `;
42
+ }
43
+ //# sourceMappingURL=systemd-unit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemd-unit.js","sourceRoot":"","sources":["../../src/service/systemd-unit.ts"],"names":[],"mappings":"AAOA,oFAAoF;AACpF,SAAS,IAAI,CAAC,KAAa;IACzB,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IACxG,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC,SAAS,EAAE,EAAE;QAC7D,IAAI,SAAS,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QACnC,IAAI,SAAS,KAAK,GAAG,IAAI,SAAS,KAAK,IAAI;YAAE,OAAO,KAAK,SAAS,EAAE,CAAC;QACrE,OAAO,MAAM,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IACvE,CAAC,CAAC,GAAG,CAAC;AACR,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAuB;IACvD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC7H,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACjH,+EAA+E;IAC/E,8EAA8E;IAC9E,MAAM,OAAO,GAAG,CAAC,cAAc,EAAE,SAAS,EAAE,KAAK,CAAC,gBAAgB,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAC5F,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QACjE,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QAC/F,OAAO,eAAe,IAAI,CAAC,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC,EAAE,CAAC;IAClD,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO;;;;;;EAMP,GAAG;aACQ,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;;;;;;;;CAQvC,CAAC;AACF,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { type ServiceOwner } from "./adapter.js";
2
+ export { renderSystemdUnit } from './systemd-unit.js';
3
+ /** Does this login have a user manager to talk to? Containers and bare SSH often do not. */
4
+ export declare function userSystemdAvailable(): boolean;
5
+ export declare const systemdOwner: ServiceOwner;
@@ -0,0 +1,72 @@
1
+ import { stopKeepalive } from "./control.js";
2
+ /**
3
+ * Linux: a systemd user unit. The user manager starts it at first login and
4
+ * stops it at last logout; `Restart=on-failure` with no start limit means a
5
+ * dying keepalive is always brought back, never parked as `failed`.
6
+ */
7
+ import { existsSync, rmSync } from "node:fs";
8
+ import { dirname } from "node:path";
9
+ import { systemdUnavailableOutput, systemdUnitPath } from "@amalgm/core/supervision";
10
+ import { atomicWrite, ensureUserDir } from "../filesystem.js";
11
+ import { keepaliveCommand, osCommand, serviceEnvironment } from "./adapter.js";
12
+ import { renderSystemdUnit } from './systemd-unit.js';
13
+ export { renderSystemdUnit } from './systemd-unit.js';
14
+ /** Does this login have a user manager to talk to? Containers and bare SSH often do not. */
15
+ export function userSystemdAvailable() {
16
+ const probe = osCommand("systemctl", ["--user", "show-environment"], { tolerate: true });
17
+ return probe.ok && !systemdUnavailableOutput(`${probe.stdout}\n${probe.stderr}`);
18
+ }
19
+ function unit(identity) {
20
+ return systemdUnitPath(identity.home, identity.label);
21
+ }
22
+ function systemctl(args, tolerate = false) {
23
+ return osCommand("systemctl", ["--user", ...args], { tolerate });
24
+ }
25
+ export const systemdOwner = {
26
+ backend: "systemd",
27
+ files: (identity) => ({ unit: unit(identity) }),
28
+ async install(identity, launcher, keepaliveArgs) {
29
+ const description = renderSystemdUnit({
30
+ command: keepaliveCommand(launcher, keepaliveArgs),
31
+ environment: serviceEnvironment(identity),
32
+ workingDirectory: identity.home,
33
+ });
34
+ await stopKeepalive(identity);
35
+ const file = unit(identity);
36
+ ensureUserDir(dirname(file));
37
+ atomicWrite(file, description, 0o644);
38
+ systemctl(["daemon-reload"]);
39
+ // A selected data home need not be in the OS account's unit search path.
40
+ // Enabling the absolute file also links it into that account's user manager.
41
+ systemctl(["enable", file]);
42
+ systemctl(["restart", identity.label]);
43
+ },
44
+ start(identity) {
45
+ systemctl(["start", identity.label]);
46
+ },
47
+ async stop(identity) {
48
+ await stopKeepalive(identity);
49
+ systemctl(["stop", identity.label], true);
50
+ },
51
+ status(identity) {
52
+ const registered = existsSync(unit(identity));
53
+ const show = systemctl(["show", identity.label, "-p", "LoadState", "-p", "ActiveState", "-p", "SubState", "-p", "MainPID"], true);
54
+ const property = (name) => new RegExp(`^${name}=(.*)$`, "m").exec(show.stdout)?.[1]?.trim() ?? null;
55
+ const pid = Number(property("MainPID") ?? 0);
56
+ const active = property("ActiveState");
57
+ return {
58
+ registered,
59
+ loaded: show.ok ? property("LoadState") === "loaded" : null,
60
+ state: active && property("SubState") ? `${active}/${property("SubState")}` : active,
61
+ pid: pid > 0 ? pid : null,
62
+ };
63
+ },
64
+ async uninstall(identity) {
65
+ await stopKeepalive(identity);
66
+ systemctl(["disable", "--now", identity.label], true);
67
+ rmSync(unit(identity), { force: true });
68
+ systemctl(["daemon-reload"], true);
69
+ systemctl(["reset-failed", identity.label], true);
70
+ },
71
+ };
72
+ //# sourceMappingURL=systemd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemd.js","sourceRoot":"","sources":["../../src/service/systemd.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { type ChildRestartPolicy } from '@amalgm/core/supervision';
2
+ export declare const encodePowerShell: (source: string) => string;
3
+ export declare function jsonExpression(value: unknown): string;
4
+ export declare function powershellExecutable(env?: NodeJS.ProcessEnv): string;
5
+ /** Microsoft argv quoting goes directly to ProcessStartInfo, without a shell. */
6
+ export declare function quoteWindowsArgument(value: string): string;
7
+ /** Scheduler owns this action's logged-in lifetime. Its launch-retry setting
8
+ * cannot observe controller exits, so this native port applies Core's policy. */
9
+ export declare function windowsControllerAction(command: readonly string[], workingDirectory: string, restartPolicy?: ChildRestartPolicy): string;
@@ -0,0 +1,55 @@
1
+ import { win32 } from 'node:path';
2
+ import { CHILD_RESTART_POLICY } from '@amalgm/core/supervision';
3
+ export const encodePowerShell = (source) => Buffer.from(source, 'utf16le').toString('base64');
4
+ export function jsonExpression(value) {
5
+ return `ConvertFrom-Json ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${Buffer.from(JSON.stringify(value), 'utf8').toString('base64')}')))`;
6
+ }
7
+ export function powershellExecutable(env = process.env) {
8
+ return win32.join(env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
9
+ }
10
+ /** Microsoft argv quoting goes directly to ProcessStartInfo, without a shell. */
11
+ export function quoteWindowsArgument(value) {
12
+ if (value.includes('\0'))
13
+ throw new Error('A process argument cannot contain NUL.');
14
+ return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
15
+ }
16
+ /** Scheduler owns this action's logged-in lifetime. Its launch-retry setting
17
+ * cannot observe controller exits, so this native port applies Core's policy. */
18
+ export function windowsControllerAction(command, workingDirectory, restartPolicy = CHILD_RESTART_POLICY) {
19
+ if (!command.length || !command[0])
20
+ throw new Error('The controller needs an executable.');
21
+ return encodePowerShell(`
22
+ $ErrorActionPreference = 'Stop'
23
+ $launch = ${jsonExpression({ executable: command[0], arguments: command.slice(1).map(quoteWindowsArgument).join(' '), workingDirectory })}
24
+ $policy = ${jsonExpression(restartPolicy)}
25
+ $start = New-Object System.Diagnostics.ProcessStartInfo
26
+ $start.FileName = $launch.executable
27
+ $start.Arguments = $launch.arguments
28
+ $start.WorkingDirectory = $launch.workingDirectory
29
+ $start.UseShellExecute = $false
30
+ $start.CreateNoWindow = $true
31
+ $clock = [Diagnostics.Stopwatch]::StartNew()
32
+ $history = @()
33
+ $delay = $policy.initialDelayMs
34
+ while ($true) {
35
+ $started = $clock.ElapsedMilliseconds
36
+ $runtimeProcess = $null
37
+ $result = 1
38
+ try {
39
+ $runtimeProcess = [System.Diagnostics.Process]::Start($start)
40
+ $runtimeProcess.WaitForExit()
41
+ $result = $runtimeProcess.ExitCode
42
+ } catch { [Console]::Error.WriteLine($_.Exception.Message) }
43
+ finally { if ($runtimeProcess) { $runtimeProcess.Dispose() } }
44
+ if ($result -eq 0) { exit 0 }
45
+ $now = $clock.ElapsedMilliseconds
46
+ if ($now - $started -ge $policy.stableAfterMs) { $delay = $policy.initialDelayMs; $history = @() }
47
+ $history = @($history | Where-Object { $now - $_ -le $policy.windowMs })
48
+ if ($history.Count -ge $policy.maxRestarts) { exit $result }
49
+ $history += $now
50
+ Start-Sleep -Milliseconds $delay
51
+ $delay = [Math]::Min($delay * 2, $policy.maxDelayMs)
52
+ }
53
+ `);
54
+ }
55
+ //# sourceMappingURL=windows-action.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windows-action.js","sourceRoot":"","sources":["../../src/service/windows-action.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAA2B,MAAM,0BAA0B,CAAC;AAEzF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC9G,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,kFAAkF,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;AAC/J,CAAC;AACD,MAAM,UAAU,oBAAoB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACvE,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,aAAa,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAChH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IACpF,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC;AAC9E,CAAC;AAED;iFACiF;AACjF,MAAM,UAAU,uBAAuB,CAAC,OAA0B,EAAE,gBAAwB,EAC1F,gBAAoC,oBAAoB;IACxD,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IAC3F,OAAO,gBAAgB,CAAC;;YAEd,cAAc,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,gBAAgB,EAAE,CAAC;YAC7H,cAAc,CAAC,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BxC,CAAC,CAAC;AACH,CAAC"}
@@ -0,0 +1,9 @@
1
+ export { encodePowerShell, powershellExecutable, quoteWindowsArgument, windowsControllerAction } from './windows-action.js';
2
+ export type WindowsTaskOperation = 'install' | 'start' | 'disable' | 'stop' | 'status' | 'uninstall';
3
+ /** All interpolated input is encoded JSON data. Task Scheduler runs under the
4
+ * current account's interactive token and never stores an account password. */
5
+ export declare function windowsTaskOperation(input: {
6
+ readonly operation: WindowsTaskOperation;
7
+ readonly name: string;
8
+ readonly action?: string;
9
+ }): string;
@@ -0,0 +1,46 @@
1
+ import { CHILD_RESTART_POLICY } from '@amalgm/core/supervision';
2
+ import { encodePowerShell, jsonExpression, powershellExecutable } from './windows-action.js';
3
+ export { encodePowerShell, powershellExecutable, quoteWindowsArgument, windowsControllerAction } from './windows-action.js';
4
+ /** All interpolated input is encoded JSON data. Task Scheduler runs under the
5
+ * current account's interactive token and never stores an account password. */
6
+ export function windowsTaskOperation(input) {
7
+ const request = { ...input, executable: powershellExecutable(),
8
+ restartCount: CHILD_RESTART_POLICY.maxRestarts,
9
+ // MS-TSCH 2.5.4.2: these retries cover failure to start an action, not its exit.
10
+ // https://learn.microsoft.com/openspecs/windows_protocols/ms-tsch/2ff4aa5a-7bc4-449f-bbb1-27475645867f
11
+ // Task Scheduler's supported minimum is one minute.
12
+ // https://learn.microsoft.com/windows/win32/taskschd/taskschedulerschema-restarttype-complextype
13
+ restartIntervalMs: Math.max(60_000, CHILD_RESTART_POLICY.initialDelayMs) };
14
+ return encodePowerShell(`
15
+ $ErrorActionPreference = 'Stop'
16
+ $request = ${jsonExpression(request)}
17
+ $task = Get-ScheduledTask -TaskPath '\\' | Where-Object { $_.TaskName -ceq $request.name } | Select-Object -First 1
18
+ switch ($request.operation) {
19
+ 'install' {
20
+ $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
21
+ $principal = New-ScheduledTaskPrincipal -UserId $sid -LogonType Interactive -RunLevel Limited
22
+ $trigger = New-ScheduledTaskTrigger -AtLogOn -User $sid
23
+ $action = New-ScheduledTaskAction -Execute $request.executable -Argument ('-NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ' + $request.action)
24
+ $settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount $request.restartCount -RestartInterval ([TimeSpan]::FromMilliseconds($request.restartIntervalMs)) -Hidden
25
+ Register-ScheduledTask -TaskPath '\\' -TaskName $request.name -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
26
+ Start-ScheduledTask -TaskPath '\\' -TaskName $request.name
27
+ }
28
+ 'start' {
29
+ if (-not $task) { throw 'The Amalgm runtime task is not registered.' }
30
+ Enable-ScheduledTask -InputObject $task | Out-Null
31
+ Start-ScheduledTask -InputObject $task
32
+ }
33
+ 'disable' { if ($task) { Disable-ScheduledTask -InputObject $task | Out-Null } }
34
+ 'stop' { if ($task) { Stop-ScheduledTask -InputObject $task } }
35
+ 'uninstall' { if ($task) { Unregister-ScheduledTask -InputObject $task -Confirm:$false } }
36
+ 'status' {
37
+ if ($task) {
38
+ $info = Get-ScheduledTaskInfo -InputObject $task
39
+ @{ registered = $true; state = [string]$task.State; lastResult = $info.LastTaskResult } | ConvertTo-Json -Compress
40
+ } else { @{ registered = $false } | ConvertTo-Json -Compress }
41
+ }
42
+ default { throw 'Unknown runtime task operation.' }
43
+ }
44
+ `);
45
+ }
46
+ //# sourceMappingURL=windows-task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windows-task.js","sourceRoot":"","sources":["../../src/service/windows-task.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAI5H;+EAC+E;AAC/E,MAAM,UAAU,oBAAoB,CAAC,KAIpC;IACC,MAAM,OAAO,GAAG,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,oBAAoB,EAAE;QAC5D,YAAY,EAAE,oBAAoB,CAAC,WAAW;QAC9C,iFAAiF;QACjF,uGAAuG;QACvG,oDAAoD;QACpD,iGAAiG;QACjG,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,cAAc,CAAC,EAAE,CAAC;IAC7E,OAAO,gBAAgB,CAAC;;aAEb,cAAc,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BnC,CAAC,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { type ServiceOwner } from './adapter.js';
2
+ export declare const windowsOwner: ServiceOwner;