@amalgm/shell 0.1.162 → 0.1.163

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 (89) hide show
  1. package/PURPOSE.md +98 -14
  2. package/README.md +16 -0
  3. package/dist/authorization/recovery-host.d.ts +1 -0
  4. package/dist/authorization/recovery-host.js +2 -3
  5. package/dist/authorization/recovery-host.js.map +1 -1
  6. package/dist/cli-arguments.d.ts +8 -0
  7. package/dist/cli-arguments.js +47 -0
  8. package/dist/cli-arguments.js.map +1 -0
  9. package/dist/cli.d.ts +6 -4
  10. package/dist/cli.js +85 -73
  11. package/dist/cli.js.map +1 -1
  12. package/dist/compose.js +4 -3
  13. package/dist/compose.js.map +1 -1
  14. package/dist/development/profile.d.ts +27 -0
  15. package/dist/development/profile.js +142 -0
  16. package/dist/development/profile.js.map +1 -0
  17. package/dist/files-register-host.d.ts +1 -0
  18. package/dist/files-register-host.js +1 -0
  19. package/dist/files-register-host.js.map +1 -1
  20. package/dist/installation/environment.d.ts +30 -0
  21. package/dist/installation/environment.js +28 -0
  22. package/dist/installation/environment.js.map +1 -0
  23. package/dist/installation/lease.d.ts +6 -0
  24. package/dist/installation/lease.js +28 -0
  25. package/dist/installation/lease.js.map +1 -0
  26. package/dist/installation/ownership.d.ts +30 -0
  27. package/dist/installation/ownership.js +156 -0
  28. package/dist/installation/ownership.js.map +1 -0
  29. package/dist/installation/selection.d.ts +8 -0
  30. package/dist/installation/selection.js +29 -0
  31. package/dist/installation/selection.js.map +1 -0
  32. package/dist/machine-host.d.ts +26 -5
  33. package/dist/machine-host.js +192 -69
  34. package/dist/machine-host.js.map +1 -1
  35. package/dist/main.js +56 -2
  36. package/dist/main.js.map +1 -1
  37. package/dist/platform-product-host.d.ts +9 -9
  38. package/dist/platform-product-host.js +17 -17
  39. package/dist/platform-product-host.js.map +1 -1
  40. package/dist/platform-tools.d.ts +4 -3
  41. package/dist/platform-tools.js +34 -37
  42. package/dist/platform-tools.js.map +1 -1
  43. package/dist/process-lifecycle.d.ts +8 -0
  44. package/dist/process-lifecycle.js +32 -0
  45. package/dist/process-lifecycle.js.map +1 -0
  46. package/dist/runtime-command-host.d.ts +7 -0
  47. package/dist/runtime-command-host.js +49 -13
  48. package/dist/runtime-command-host.js.map +1 -1
  49. package/dist/service/adapter.d.ts +36 -0
  50. package/dist/service/adapter.js +24 -0
  51. package/dist/service/adapter.js.map +1 -0
  52. package/dist/service/cli.d.ts +8 -0
  53. package/dist/service/cli.js +196 -0
  54. package/dist/service/cli.js.map +1 -0
  55. package/dist/service/control.d.ts +29 -0
  56. package/dist/service/control.js +68 -0
  57. package/dist/service/control.js.map +1 -0
  58. package/dist/service/index.d.ts +9 -0
  59. package/dist/service/index.js +10 -0
  60. package/dist/service/index.js.map +1 -0
  61. package/dist/service/keepalive.d.ts +11 -0
  62. package/dist/service/keepalive.js +180 -0
  63. package/dist/service/keepalive.js.map +1 -0
  64. package/dist/service/launchd.d.ts +9 -0
  65. package/dist/service/launchd.js +105 -0
  66. package/dist/service/launchd.js.map +1 -0
  67. package/dist/service/owner.d.ts +4 -0
  68. package/dist/service/owner.js +14 -0
  69. package/dist/service/owner.js.map +1 -0
  70. package/dist/service/portable.d.ts +10 -0
  71. package/dist/service/portable.js +32 -0
  72. package/dist/service/portable.js.map +1 -0
  73. package/dist/service/registration.d.ts +82 -0
  74. package/dist/service/registration.js +114 -0
  75. package/dist/service/registration.js.map +1 -0
  76. package/dist/service/systemd.d.ts +9 -0
  77. package/dist/service/systemd.js +94 -0
  78. package/dist/service/systemd.js.map +1 -0
  79. package/dist/service/windows-task.d.ts +16 -0
  80. package/dist/service/windows-task.js +82 -0
  81. package/dist/service/windows-task.js.map +1 -0
  82. package/dist/service/windows.d.ts +2 -0
  83. package/dist/service/windows.js +65 -0
  84. package/dist/service/windows.js.map +1 -0
  85. package/dist/user-ground-host.d.ts +3 -0
  86. package/dist/user-ground-host.js +13 -0
  87. package/dist/user-ground-host.js.map +1 -1
  88. package/npm-shrinkwrap.json +49 -83
  89. package/package.json +9 -9
@@ -0,0 +1,94 @@
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 { systemdQuote, systemdUnavailableOutput, systemdUnitPath } from "@amalgm/core/supervision";
10
+ import { atomicWrite, ensureUserDir } from "../filesystem.js";
11
+ import { keepaliveCommand, osCommand, serviceEnvironment } from "./adapter.js";
12
+ const RESTART_SEC = 5;
13
+ /** SIGTERM to the keepalive, SIGKILL to the whole group after this. */
14
+ const TIMEOUT_STOP_SEC = 30;
15
+ export function renderSystemdUnit(input) {
16
+ const env = Object.entries(input.environment)
17
+ .map(([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}`)
18
+ .join("\n");
19
+ return `[Unit]
20
+ Description=Amalgm always-on runtime
21
+ StartLimitIntervalSec=0
22
+
23
+ [Service]
24
+ Type=exec
25
+ WorkingDirectory=${systemdQuote(input.workingDirectory)}
26
+ ${env}
27
+ ExecStart=${input.command.map(systemdQuote).join(" ")}
28
+ Restart=on-failure
29
+ RestartSec=${RESTART_SEC}
30
+ KillMode=mixed
31
+ TimeoutStopSec=${TIMEOUT_STOP_SEC}
32
+
33
+ [Install]
34
+ WantedBy=default.target
35
+ `;
36
+ }
37
+ /** Does this login have a user manager to talk to? Containers and bare SSH often do not. */
38
+ export function userSystemdAvailable() {
39
+ const probe = osCommand("systemctl", ["--user", "show-environment"], { tolerate: true });
40
+ return probe.ok && !systemdUnavailableOutput(`${probe.stdout}\n${probe.stderr}`);
41
+ }
42
+ function unit(identity) {
43
+ return systemdUnitPath(identity.home, identity.label);
44
+ }
45
+ function systemctl(args, tolerate = false) {
46
+ return osCommand("systemctl", ["--user", ...args], { tolerate });
47
+ }
48
+ export const systemdOwner = {
49
+ backend: "systemd",
50
+ files: (identity) => ({ unit: unit(identity) }),
51
+ async install(identity, launcher, keepaliveArgs) {
52
+ await stopKeepalive(identity);
53
+ const file = unit(identity);
54
+ ensureUserDir(dirname(file));
55
+ atomicWrite(file, renderSystemdUnit({
56
+ command: keepaliveCommand(launcher, keepaliveArgs),
57
+ environment: serviceEnvironment(identity),
58
+ workingDirectory: identity.home,
59
+ }), 0o644);
60
+ systemctl(["daemon-reload"]);
61
+ // A selected data home need not be in the OS account's unit search path.
62
+ // Enabling the absolute file also links it into that account's user manager.
63
+ systemctl(["enable", file]);
64
+ systemctl(["restart", identity.label]);
65
+ },
66
+ start(identity) {
67
+ systemctl(["start", identity.label]);
68
+ },
69
+ async stop(identity) {
70
+ await stopKeepalive(identity);
71
+ systemctl(["stop", identity.label], true);
72
+ },
73
+ status(identity) {
74
+ const registered = existsSync(unit(identity));
75
+ const show = systemctl(["show", identity.label, "-p", "LoadState", "-p", "ActiveState", "-p", "SubState", "-p", "MainPID"], true);
76
+ const property = (name) => new RegExp(`^${name}=(.*)$`, "m").exec(show.stdout)?.[1]?.trim() ?? null;
77
+ const pid = Number(property("MainPID") ?? 0);
78
+ const active = property("ActiveState");
79
+ return {
80
+ registered,
81
+ loaded: show.ok ? property("LoadState") === "loaded" : null,
82
+ state: active && property("SubState") ? `${active}/${property("SubState")}` : active,
83
+ pid: pid > 0 ? pid : null,
84
+ };
85
+ },
86
+ async uninstall(identity) {
87
+ await stopKeepalive(identity);
88
+ systemctl(["disable", "--now", identity.label], true);
89
+ rmSync(unit(identity), { force: true });
90
+ systemctl(["daemon-reload"], true);
91
+ systemctl(["reset-failed", identity.label], true);
92
+ },
93
+ };
94
+ //# 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,16 @@
1
+ export declare function encodePowerShell(source: string): string;
2
+ /** Microsoft command-line quoting, applied directly to ProcessStartInfo.
3
+ * No cmd.exe, environment expansion, or shell interpretation is involved. */
4
+ export declare function quoteWindowsArgument(value: string): string;
5
+ export declare function powershellExecutable(env?: NodeJS.ProcessEnv): string;
6
+ /** The scheduler must observe the controller's real lifetime and exit code.
7
+ * A hidden, waiting host wrapper does not make any recovery decisions. */
8
+ export declare function windowsControllerAction(command: readonly string[], workingDirectory: string): string;
9
+ export type WindowsTaskOperation = 'install' | 'start' | 'disable' | 'stop' | 'status' | 'uninstall';
10
+ /** All interpolated input is encoded JSON data. Task Scheduler runs under the
11
+ * current account's interactive token and never stores an account password. */
12
+ export declare function windowsTaskOperation(input: {
13
+ readonly operation: WindowsTaskOperation;
14
+ readonly name: string;
15
+ readonly action?: string;
16
+ }): string;
@@ -0,0 +1,82 @@
1
+ import { win32 } from 'node:path';
2
+ import { CHILD_RESTART_POLICY } from '@amalgm/core/supervision';
3
+ export function encodePowerShell(source) {
4
+ return Buffer.from(source, 'utf16le').toString('base64');
5
+ }
6
+ /** Microsoft command-line quoting, applied directly to ProcessStartInfo.
7
+ * No cmd.exe, environment expansion, or shell interpretation is involved. */
8
+ export function quoteWindowsArgument(value) {
9
+ if (value.includes('\0'))
10
+ throw new Error('A process argument cannot contain NUL.');
11
+ return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
12
+ }
13
+ export function powershellExecutable(env = process.env) {
14
+ return win32.join(env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
15
+ }
16
+ function jsonExpression(value) {
17
+ const encoded = Buffer.from(JSON.stringify(value), 'utf8').toString('base64');
18
+ return `ConvertFrom-Json ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')))`;
19
+ }
20
+ /** The scheduler must observe the controller's real lifetime and exit code.
21
+ * A hidden, waiting host wrapper does not make any recovery decisions. */
22
+ export function windowsControllerAction(command, workingDirectory) {
23
+ if (!command.length || !command[0])
24
+ throw new Error('The controller needs an executable.');
25
+ return encodePowerShell(`
26
+ $ErrorActionPreference = 'Stop'
27
+ $launch = ${jsonExpression({ executable: command[0], arguments: command.slice(1).map(quoteWindowsArgument).join(' '), workingDirectory })}
28
+ $start = New-Object System.Diagnostics.ProcessStartInfo
29
+ $start.FileName = $launch.executable
30
+ $start.Arguments = $launch.arguments
31
+ $start.WorkingDirectory = $launch.workingDirectory
32
+ $start.UseShellExecute = $false
33
+ $start.CreateNoWindow = $true
34
+ $runtimeProcess = [System.Diagnostics.Process]::Start($start)
35
+ try {
36
+ $runtimeProcess.WaitForExit()
37
+ $result = $runtimeProcess.ExitCode
38
+ } finally { $runtimeProcess.Dispose() }
39
+ exit $result
40
+ `);
41
+ }
42
+ /** All interpolated input is encoded JSON data. Task Scheduler runs under the
43
+ * current account's interactive token and never stores an account password. */
44
+ export function windowsTaskOperation(input) {
45
+ const request = { ...input, executable: powershellExecutable(),
46
+ restartCount: CHILD_RESTART_POLICY.maxRestarts,
47
+ // Task Scheduler's supported minimum is one minute.
48
+ // https://learn.microsoft.com/windows/win32/taskschd/taskschedulerschema-restarttype-complextype
49
+ restartIntervalMs: Math.max(60_000, CHILD_RESTART_POLICY.initialDelayMs) };
50
+ return encodePowerShell(`
51
+ $ErrorActionPreference = 'Stop'
52
+ $request = ${jsonExpression(request)}
53
+ $task = Get-ScheduledTask -TaskPath '\\' | Where-Object { $_.TaskName -ceq $request.name } | Select-Object -First 1
54
+ switch ($request.operation) {
55
+ 'install' {
56
+ $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
57
+ $principal = New-ScheduledTaskPrincipal -UserId $sid -LogonType Interactive -RunLevel Limited
58
+ $trigger = New-ScheduledTaskTrigger -AtLogOn -User $sid
59
+ $action = New-ScheduledTaskAction -Execute $request.executable -Argument ('-NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ' + $request.action)
60
+ $settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount $request.restartCount -RestartInterval ([TimeSpan]::FromMilliseconds($request.restartIntervalMs)) -Hidden
61
+ Register-ScheduledTask -TaskPath '\\' -TaskName $request.name -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
62
+ Start-ScheduledTask -TaskPath '\\' -TaskName $request.name
63
+ }
64
+ 'start' {
65
+ if (-not $task) { throw 'The Amalgm runtime task is not registered.' }
66
+ Enable-ScheduledTask -InputObject $task | Out-Null
67
+ Start-ScheduledTask -InputObject $task
68
+ }
69
+ 'disable' { if ($task) { Disable-ScheduledTask -InputObject $task | Out-Null } }
70
+ 'stop' { if ($task) { Stop-ScheduledTask -InputObject $task } }
71
+ 'uninstall' { if ($task) { Unregister-ScheduledTask -InputObject $task -Confirm:$false } }
72
+ 'status' {
73
+ if ($task) {
74
+ $info = Get-ScheduledTaskInfo -InputObject $task
75
+ @{ registered = $true; state = [string]$task.State; lastResult = $info.LastTaskResult } | ConvertTo-Json -Compress
76
+ } else { @{ registered = $false } | ConvertTo-Json -Compress }
77
+ }
78
+ default { throw 'Unknown runtime task operation.' }
79
+ }
80
+ `);
81
+ }
82
+ //# 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,KAAK,EAAE,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhE,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC3D,CAAC;AAED;6EAC6E;AAC7E,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,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,SAAS,cAAc,CAAC,KAAc;IACpC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC9E,OAAO,kFAAkF,OAAO,MAAM,CAAC;AACzG,CAAC;AAED;0EAC0E;AAC1E,MAAM,UAAU,uBAAuB,CAAC,OAA0B,EAAE,gBAAwB;IAC1F,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;;;;;;;;;;;;;CAaxI,CAAC,CAAC;AACH,CAAC;AAID;+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,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;
@@ -0,0 +1,65 @@
1
+ /** Windows Task Scheduler owns a hidden controller in the current account's
2
+ * logged-in lifetime, including Core's bounded automatic crash recovery. */
3
+ import { existsSync, rmSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { windowsStartupScriptPath } from '@amalgm/core/supervision';
6
+ import { atomicJson } from '../filesystem.js';
7
+ import { keepaliveStatus, stopKeepalive } from './control.js';
8
+ import { keepaliveCommand, osCommand } from './adapter.js';
9
+ import { powershellExecutable, windowsControllerAction, windowsTaskOperation } from './windows-task.js';
10
+ const definition = (identity) => join(identity.serviceDir, 'windows-task.json');
11
+ const task = (identity, operation, action) => osCommand(powershellExecutable(), ['-NoProfile', '-NonInteractive', '-EncodedCommand', windowsTaskOperation({
12
+ operation, name: identity.label, ...(action ? { action } : {}),
13
+ })]);
14
+ async function waitForController(identity) {
15
+ const deadline = Date.now() + 15_000;
16
+ while (Date.now() < deadline) {
17
+ if ((await keepaliveStatus(identity))?.alive)
18
+ return;
19
+ await new Promise((resolve) => setTimeout(resolve, 100));
20
+ }
21
+ throw new Error(`The Windows task for ${identity.label} did not report an authenticated controller; see ${identity.serviceDir}`);
22
+ }
23
+ export const windowsOwner = {
24
+ backend: 'windows',
25
+ files: (identity) => ({ task: definition(identity) }),
26
+ async install(identity, launcher, keepaliveArgs) {
27
+ await stopKeepalive(identity);
28
+ const action = windowsControllerAction(keepaliveCommand(launcher, keepaliveArgs), identity.home);
29
+ task(identity, 'install', action);
30
+ await waitForController(identity);
31
+ atomicJson(definition(identity), { name: identity.label, action, binding: identity.binding });
32
+ // Retire only the known previous registration after the new owner is live.
33
+ rmSync(windowsStartupScriptPath(identity.labelScope, { home: identity.home, appData: process.env.APPDATA }), { force: true });
34
+ },
35
+ async start(identity) {
36
+ if ((await keepaliveStatus(identity))?.alive)
37
+ return;
38
+ task(identity, 'start');
39
+ await waitForController(identity);
40
+ },
41
+ async stop(identity) {
42
+ task(identity, 'disable');
43
+ await stopKeepalive(identity);
44
+ task(identity, 'stop');
45
+ },
46
+ async status(identity) {
47
+ const native = JSON.parse(task(identity, 'status').stdout);
48
+ const controller = await keepaliveStatus(identity);
49
+ return {
50
+ registered: native.registered && existsSync(definition(identity)), loaded: native.registered,
51
+ state: controller?.alive ? 'running' : native.state?.toLowerCase() || 'not registered',
52
+ pid: controller?.alive ? controller.pid : null,
53
+ ...(native.lastResult ? { detail: `Task Scheduler result: ${native.lastResult}` } : {}),
54
+ };
55
+ },
56
+ async uninstall(identity) {
57
+ task(identity, 'disable');
58
+ await stopKeepalive(identity);
59
+ task(identity, 'stop');
60
+ task(identity, 'uninstall');
61
+ rmSync(definition(identity), { force: true });
62
+ rmSync(windowsStartupScriptPath(identity.labelScope, { home: identity.home, appData: process.env.APPDATA }), { force: true });
63
+ },
64
+ };
65
+ //# sourceMappingURL=windows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windows.js","sourceRoot":"","sources":["../../src/service/windows.ts"],"names":[],"mappings":"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"}
@@ -68,6 +68,8 @@ export interface UserGroundHostOptions {
68
68
  readonly tunnelUrl: string;
69
69
  readonly tunnelToken: string;
70
70
  readonly runtimeVersion: string;
71
+ /** Installation-wide host admission, before any writer touches ground. */
72
+ readonly reserveWritableRoot?: (path: string) => void;
71
73
  readonly declaration?: UserHomeDeclaration;
72
74
  readonly now?: () => Date;
73
75
  /** Product diagnostics consume this; Core's `/healthz` remains liveness-only. */
@@ -256,6 +258,7 @@ export declare class UserGroundHost {
256
258
  * stay one read each. A lone artifact is a set of one. */
257
259
  private downloadContentSet;
258
260
  private downloadContent;
261
+ private reserveGroundRoots;
259
262
  private ensureWatchers;
260
263
  private scheduleRescan;
261
264
  /** A finite cold command may suppress Detect while it owns publication, but
@@ -171,6 +171,7 @@ export class UserGroundHost {
171
171
  converge: async (identity) => {
172
172
  this.converged = false;
173
173
  this.activeIdentity = normalizedIdentity(identity);
174
+ this.reserveGroundRoots(this.activeIdentity);
174
175
  const local = this.localPort(this.activeIdentity);
175
176
  const result = await convergeUserGround(this.activeIdentity, {
176
177
  sha256Hex,
@@ -312,6 +313,7 @@ export class UserGroundHost {
312
313
  const register = createFilesRegisterPorts({
313
314
  userRoot,
314
315
  deviceId: identity.deviceId,
316
+ reserveWritableRoot: this.options.reserveWritableRoot,
315
317
  onReferenceDeclared: () => this.scheduleRescan(),
316
318
  findKnownWorkspaceId: ({ absolutePath, deviceNumber, inode }) => findKnownEntityId(this.databasePath(identity), absolutePath, deviceNumber, inode),
317
319
  });
@@ -363,6 +365,7 @@ export class UserGroundHost {
363
365
  return current;
364
366
  },
365
367
  materializeWorkspace: async (input) => {
368
+ this.options.reserveWritableRoot?.(join(input.destinationParent, input.workspace.name));
366
369
  const groundWaitStarted = performance.now();
367
370
  this.stage({
368
371
  primitive: "download",
@@ -2060,9 +2063,19 @@ export class UserGroundHost {
2060
2063
  throw new Error(`download set did not seal ${artifact.contentHash}`);
2061
2064
  return local;
2062
2065
  }
2066
+ reserveGroundRoots(identity) {
2067
+ if (!this.options.reserveWritableRoot)
2068
+ return;
2069
+ const userRoot = this.userRoot(identity);
2070
+ this.options.reserveWritableRoot(userRoot);
2071
+ for (const { directory } of materializedWorkspaceBindings(workspaceBindingDir(userRoot, identity.deviceId))) {
2072
+ this.options.reserveWritableRoot(directory);
2073
+ }
2074
+ }
2063
2075
  ensureWatchers(identity, authoritativeBaselineRootIds = []) {
2064
2076
  if (this.closing)
2065
2077
  return this.watchHost.health();
2078
+ this.reserveGroundRoots(identity);
2066
2079
  const started = performance.now();
2067
2080
  const root = this.userRoot(identity);
2068
2081
  const wanted = new Map([[root, {