run-spaceapp 0.1.15-hostroot.0 → 0.1.15-hostroot.2

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.
package/README.md CHANGED
@@ -24,8 +24,9 @@ Windows, macOS, Ubuntu, Debian, Fedora, RHEL, and CentOS, or with native
24
24
  waits for Engine and Compose readiness before pulling images. Windows prefers
25
25
  Windows Package Manager's hash-pinned Docker Desktop
26
26
  manifest and retains a signed direct-download fallback. Docker Desktop license
27
- acceptance and Linux `docker` group membership require confirmation inside the
28
- same command. On Windows, approve any UAC prompt required by WSL2 or Docker
27
+ acceptance still requires confirmation. On Linux, the same command explains
28
+ the root-equivalent `docker` group and adds the current user automatically.
29
+ On Windows, approve any UAC prompt required by WSL2 or Docker
29
30
  Desktop. Use the **Command Prompt** profile because a restricted PowerShell
30
31
  policy can block npm's `npx.ps1` before SpaceApp starts. On Docker Desktop's
31
32
  first launch, select **Skip** in the top-right of the **Welcome to Docker**
@@ -51,23 +52,16 @@ current user's platform config directory:
51
52
 
52
53
  Set `SPACEAPP_HOME` to an absolute path to use a dedicated installation root.
53
54
 
54
- Linux installations are isolated by default. A trusted single owner can opt
55
- into the personal host-root prerelease with:
55
+ The `0.1.15-hostroot.2` personal candidate tests the complete x64 Linux
56
+ host-root path:
56
57
 
57
58
  ```bash
58
59
  npx --yes run-spaceapp@personal install --access host-root
59
60
  ```
60
61
 
61
- This mounts the Linux `/` at `/host` read/write in the CLI service and runs CLI
62
- sessions as container root. The core service receives `/host` read-only. It can
63
- expose every host credential or make the operating system unbootable. It does
64
- not mount the Docker socket or enable a privileged container. Profiles control
65
- resources and managed Chromium, not access. Return to isolation without
66
- deleting persistent data with:
67
-
68
- ```bash
69
- npx --yes run-spaceapp@personal install --access isolated
70
- ```
62
+ This candidate publishes matching `core` and `cli` images for `linux/amd64`
63
+ and reuses the existing signed browser manifest without rebuilding it. npm
64
+ rejects the package on arm64. Host-root is supported only on Linux.
71
65
 
72
66
  Use the same `npx --yes run-spaceapp@personal` prefix for follow-up commands
73
67
  while this prerelease is installed.
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "run-spaceapp",
3
- "version": "0.1.15-hostroot.0",
3
+ "version": "0.1.15-hostroot.2",
4
+ "spaceappRuntimeVersion": "0.1.15-hostroot.2",
5
+ "spaceappHostRootRuntimeCompatible": true,
4
6
  "description": "Cross-platform Docker launcher for the SpaceApp self-hosted agent workspace",
5
7
  "license": "Apache-2.0",
6
8
  "type": "module",
@@ -28,6 +30,9 @@
28
30
  "linux",
29
31
  "win32"
30
32
  ],
33
+ "cpu": [
34
+ "x64"
35
+ ],
31
36
  "scripts": {
32
37
  "test": "node --test tests/*.test.mjs",
33
38
  "check": "node --check bin/spaceapp.mjs && node --check src/package-info.mjs && node --check src/index.mjs && node --check src/cli.mjs && node --check src/prerequisites.mjs",
@@ -46,5 +51,5 @@
46
51
  "access": "public",
47
52
  "provenance": true
48
53
  },
49
- "gitHead": "5a8c1b92709bbd9f88b5ed869adb7ddfaea9092f"
54
+ "gitHead": "61c72544f7cb348803e2f1f71e467bf5aa86ded3"
50
55
  }
package/src/cli.mjs CHANGED
@@ -31,13 +31,18 @@ import {
31
31
  windowsPowerShellArgs
32
32
  } from "./prerequisites.mjs";
33
33
  import {
34
+ HOST_ROOT_RUNTIME_COMPATIBLE,
34
35
  PACKAGE_VERSION,
36
+ RUNTIME_VERSION,
35
37
  UNIVERSAL_COMMAND
36
38
  } from "./package-info.mjs";
37
39
 
38
- const APPLICATION_READY_WAIT_MS = 3 * 60 * 1_000;
40
+ const APPLICATION_READY_WAIT_MINUTES = 10;
41
+ const APPLICATION_READY_WAIT_MS = APPLICATION_READY_WAIT_MINUTES * 60 * 1_000;
39
42
  const APPLICATION_READY_POLL_MS = 2_000;
40
43
  const APPLICATION_READY_MAX_ATTEMPTS = APPLICATION_READY_WAIT_MS / APPLICATION_READY_POLL_MS;
44
+ const APPLICATION_READY_PROGRESS_ATTEMPTS = 30_000 / APPLICATION_READY_POLL_MS;
45
+ const APPLICATION_READY_LOG_INTERVAL_SECONDS = 120;
41
46
  const SETUP_STATUS_TIMEOUT_MS = 10_000;
42
47
  const trustedCommands = new Set([
43
48
  "codesign",
@@ -72,11 +77,13 @@ export async function run(argv, {
72
77
  prepareDockerPath = prepareDockerCliPath,
73
78
  request = globalThis.fetch,
74
79
  sleep = wait,
75
- persistSetupToken = writeSetupToken
80
+ persistSetupToken = writeSetupToken,
81
+ launcherVersion = PACKAGE_VERSION,
82
+ runtimeVersion = RUNTIME_VERSION,
83
+ hostRootRuntimeCompatible = HOST_ROOT_RUNTIME_COMPATIBLE
76
84
  } = {}) {
77
85
  const [command = "help", ...args] = argv;
78
86
  const root = resolveSpaceAppHome({ env, platform });
79
- const version = PACKAGE_VERSION;
80
87
 
81
88
  if (command !== "install") {
82
89
  await prepareDockerPath({ platform, env });
@@ -86,13 +93,15 @@ export async function run(argv, {
86
93
  return 0;
87
94
  }
88
95
  if (command === "--version" || command === "-v") {
89
- stdout.write(`${version}\n`);
96
+ stdout.write(`${launcherVersion}\n`);
90
97
  return 0;
91
98
  }
92
99
  if (command === "install") {
93
100
  return installCommand(args, {
94
101
  root,
95
- version,
102
+ launcherVersion,
103
+ runtimeVersion,
104
+ hostRootRuntimeCompatible,
96
105
  platform,
97
106
  arch,
98
107
  env,
@@ -110,7 +119,7 @@ export async function run(argv, {
110
119
  }
111
120
  if (command === "init") {
112
121
  assertNoArgs(args, "init");
113
- const result = await initializeInstallation(root, { version });
122
+ const result = await initializeInstallation(root, { version: runtimeVersion });
114
123
  stdout.write(`SpaceApp initialized at ${root}\n`);
115
124
  if (result.setupToken) {
116
125
  stdout.write(`One-time setup token: ${result.setupToken}\n`);
@@ -163,7 +172,15 @@ export async function run(argv, {
163
172
  return ownerCommand(args, { root, config, stdin, stdout, stderr, execute: runtimeExecute });
164
173
  }
165
174
  if (command === "update") {
166
- return updateCommand(args, { root, config, version, stdin, stdout, stderr, execute: runtimeExecute });
175
+ return updateCommand(args, {
176
+ root,
177
+ config,
178
+ version: runtimeVersion,
179
+ stdin,
180
+ stdout,
181
+ stderr,
182
+ execute: runtimeExecute
183
+ });
167
184
  }
168
185
  if (command === "rollback") {
169
186
  assertNoArgs(args, "rollback");
@@ -265,7 +282,9 @@ export async function run(argv, {
265
282
 
266
283
  async function installCommand(args, {
267
284
  root,
268
- version,
285
+ launcherVersion,
286
+ runtimeVersion,
287
+ hostRootRuntimeCompatible,
269
288
  platform,
270
289
  arch,
271
290
  env,
@@ -289,6 +308,11 @@ async function installCommand(args, {
289
308
  if (accessMode === "host-root" && platform !== "linux") {
290
309
  throw new Error("Host-root access is supported only on Linux.");
291
310
  }
311
+ if (accessMode === "host-root" && !hostRootRuntimeCompatible) {
312
+ throw new Error(
313
+ "Host-root access is disabled for this launcher-only candidate because its runtime images were not rebuilt. Use --access isolated."
314
+ );
315
+ }
292
316
  await prepareDockerPath({ platform, env });
293
317
  const resources = await inspectResources(root);
294
318
  const profile = resolveInstallProfile(requestedProfile, resources.totalMemoryBytes);
@@ -297,11 +321,15 @@ async function installCommand(args, {
297
321
  "WARNING: host-root access lets SpaceApp CLI sessions read and modify the entire Linux host through /host, including credentials and system files.\n"
298
322
  );
299
323
  }
300
- const result = await prepareInstallation(root, { version, profile, accessMode });
324
+ const result = await prepareInstallation(root, {
325
+ version: runtimeVersion,
326
+ profile,
327
+ accessMode
328
+ });
301
329
 
302
- stdout.write(`Launcher version: ${version}\n`);
330
+ stdout.write(`Launcher version: ${launcherVersion}\n`);
303
331
  stdout.write(
304
- `SpaceApp version: ${existingConfig?.version ?? "not installed"} -> ${result.config.version}\n`
332
+ `Runtime image version: ${existingConfig?.version ?? "not installed"} -> ${result.config.version}\n`
305
333
  );
306
334
  stdout.write(
307
335
  `Profile: ${existingConfig?.profile ?? "not configured"} -> ${result.config.profile}\n`
@@ -313,7 +341,7 @@ async function installCommand(args, {
313
341
  ? "Preserved existing data, workspaces, credentials, secrets, and persistent Docker volumes.\n"
314
342
  : "Future refreshes preserve data, workspaces, credentials, secrets, and persistent Docker volumes.\n");
315
343
  stdout.write(
316
- `Selected profile: ${profile} (${formatGigabytes(resources.totalMemoryBytes)} GB system memory detected).\n`
344
+ `Selected profile: ${profile} (${formatGibibytes(resources.totalMemoryBytes)} GiB system memory detected).\n`
317
345
  );
318
346
  stdout.write(`SpaceApp installation root: ${root}\n`);
319
347
  if (installResourceChecks(resources).some((check) => !check.ok)) {
@@ -343,12 +371,13 @@ async function installCommand(args, {
343
371
  execute,
344
372
  installArgs: { root, requestedProfile, requestedAccessMode, noOpen }
345
373
  });
346
- if (prerequisiteResult.reexecuted || prerequisiteResult.code !== 0) {
347
- if (prerequisiteResult.code !== 0) {
348
- stderr.write(
349
- `Installation stopped before downloading images. Fix the failed checks and run "${UNIVERSAL_COMMAND} install" again.\n`
350
- );
351
- }
374
+ if (prerequisiteResult.reexecuted) {
375
+ return prerequisiteResult.code;
376
+ }
377
+ if (prerequisiteResult.code !== 0) {
378
+ stderr.write(
379
+ `Installation stopped before downloading images. Fix the failed checks and run "${UNIVERSAL_COMMAND} install" again.\n`
380
+ );
352
381
  return prerequisiteResult.code;
353
382
  }
354
383
 
@@ -373,6 +402,16 @@ async function installCommand(args, {
373
402
  let runtimeMutationAttempted = false;
374
403
  const failAfterRuntimeMutation = async (code) => {
375
404
  if (runtimeMutationAttempted) {
405
+ await reportInstallDiagnostics({
406
+ root,
407
+ stateRoot: stagedStateRoot,
408
+ profile,
409
+ platform,
410
+ stdin,
411
+ stdout,
412
+ stderr,
413
+ execute
414
+ });
376
415
  await restoreRuntimeAfterFailedInstall({
377
416
  root,
378
417
  stagedStateRoot,
@@ -390,8 +429,12 @@ async function installCommand(args, {
390
429
  };
391
430
  try {
392
431
  await commitInstallation(stagedStateRoot, result.config);
393
- const stagedComposeCommand = (action) =>
394
- composeCommand(action, root, { profile, stateRoot: stagedStateRoot });
432
+ const stagedComposeCommand = (action, options = {}) =>
433
+ composeCommand(action, root, {
434
+ profile,
435
+ stateRoot: stagedStateRoot,
436
+ ...options
437
+ });
395
438
  const pullCode = await executeWithDockerDiagnostics(
396
439
  execute,
397
440
  stagedComposeCommand("pull"),
@@ -409,16 +452,60 @@ async function installCommand(args, {
409
452
  if (upCode !== 0) return await failAfterRuntimeMutation(upCode);
410
453
 
411
454
  const url = `http://${result.config.bindHost}:${result.config.port}`;
412
- stdout.write("Waiting for SpaceApp services to become ready...\n");
455
+ stdout.write(
456
+ `Waiting up to ${APPLICATION_READY_WAIT_MINUTES} minutes for SpaceApp services to become ready...\n`
457
+ );
413
458
  const ready = await waitForApplicationReady({
414
459
  url,
415
460
  request,
416
- sleep
461
+ sleep,
462
+ onProgress: async ({ elapsedSeconds }) => {
463
+ stdout.write(
464
+ `Still waiting for SpaceApp services (${elapsedSeconds} seconds elapsed; ` +
465
+ `up to ${APPLICATION_READY_WAIT_MINUTES} minutes)...\n`
466
+ );
467
+ try {
468
+ const statusCode = await executeWithDockerDiagnostics(
469
+ execute,
470
+ stagedComposeCommand("status"),
471
+ { stdin, stdout, stderr },
472
+ { platform, stderr }
473
+ );
474
+ if (statusCode !== 0) {
475
+ stderr.write(`SpaceApp service status check failed with Docker exit ${statusCode}.\n`);
476
+ }
477
+ } catch (error) {
478
+ stderr.write(
479
+ `SpaceApp could not collect status diagnostics: ${error?.message || String(error)}.\n`
480
+ );
481
+ }
482
+ if (
483
+ elapsedSeconds % APPLICATION_READY_LOG_INTERVAL_SECONDS === 0 &&
484
+ elapsedSeconds < APPLICATION_READY_WAIT_MINUTES * 60
485
+ ) {
486
+ stdout.write(`Showing recent startup logs after ${elapsedSeconds} seconds...\n`);
487
+ try {
488
+ const logsCode = await executeWithDockerDiagnostics(
489
+ execute,
490
+ stagedComposeCommand("logs", { lines: 50 }),
491
+ { stdin, stdout, stderr },
492
+ { platform, stderr }
493
+ );
494
+ if (logsCode !== 0) {
495
+ stderr.write(`SpaceApp startup log check failed with Docker exit ${logsCode}.\n`);
496
+ }
497
+ } catch (error) {
498
+ stderr.write(
499
+ `SpaceApp could not collect logs diagnostics: ${error?.message || String(error)}.\n`
500
+ );
501
+ }
502
+ }
503
+ }
417
504
  });
418
505
  if (!ready) {
419
506
  stderr.write(
420
- "SpaceApp containers started, but the application did not become ready within 3 minutes.\n" +
421
- `Run "${UNIVERSAL_COMMAND} status" and "${UNIVERSAL_COMMAND} logs", then run "${UNIVERSAL_COMMAND} install" again.\n`
507
+ `SpaceApp containers started, but the application did not become ready within ` +
508
+ `${APPLICATION_READY_WAIT_MINUTES} minutes.\n`
422
509
  );
423
510
  return await failAfterRuntimeMutation(1);
424
511
  }
@@ -512,6 +599,42 @@ async function installCommand(args, {
512
599
  }
513
600
  }
514
601
 
602
+ async function reportInstallDiagnostics({
603
+ root,
604
+ stateRoot,
605
+ profile,
606
+ platform,
607
+ stdin,
608
+ stdout,
609
+ stderr,
610
+ execute
611
+ }) {
612
+ stderr.write("Collecting SpaceApp service status and recent logs before rollback...\n");
613
+ for (const action of ["status", "logs"]) {
614
+ try {
615
+ const code = await executeWithDockerDiagnostics(
616
+ execute,
617
+ composeCommand(action, root, {
618
+ profile,
619
+ stateRoot,
620
+ lines: 200
621
+ }),
622
+ { stdin, stdout, stderr },
623
+ { platform, stderr }
624
+ );
625
+ if (code !== 0) {
626
+ stderr.write(
627
+ `SpaceApp could not collect ${action} diagnostics (Docker exit ${code}).\n`
628
+ );
629
+ }
630
+ } catch (error) {
631
+ stderr.write(
632
+ `SpaceApp could not collect ${action} diagnostics: ${error?.message || String(error)}.\n`
633
+ );
634
+ }
635
+ }
636
+ }
637
+
515
638
  async function restoreRuntimeAfterFailedInstall({
516
639
  root,
517
640
  stagedStateRoot,
@@ -739,8 +862,8 @@ async function ownerCommand(args, { root, config, stdin, stdout, stderr, execute
739
862
  );
740
863
  }
741
864
  const password = await readSecret(stdin, stdout, "New owner password: ");
742
- if (password.length < 12) {
743
- throw new Error("Owner password must be at least 12 characters.");
865
+ if (password.length < 6) {
866
+ throw new Error("Owner password must be at least 6 characters.");
744
867
  }
745
868
  return execute(composeCommand("resetOwnerPassword", root, { profile: config.profile }), {
746
869
  stdin,
@@ -755,11 +878,13 @@ async function updateCommand(args, { root, config, version, stdin, stdout, stder
755
878
  throw new Error(`Usage: ${UNIVERSAL_COMMAND} update [version]`);
756
879
  }
757
880
  const targetVersion = args[0] || version;
758
- const updated = {
759
- ...config,
760
- version: targetVersion,
761
- previousVersion: config.version
762
- };
881
+ const updated = targetVersion === config.version
882
+ ? config
883
+ : {
884
+ ...config,
885
+ version: targetVersion,
886
+ previousVersion: config.version
887
+ };
763
888
  await writeRuntimeFiles(root, updated);
764
889
  const pullCode = await execute(composeCommand("pull", root, { profile: updated.profile }), { stdin, stdout, stderr });
765
890
  if (pullCode !== 0) {
@@ -847,6 +972,7 @@ async function waitForApplicationReady({
847
972
  url,
848
973
  request,
849
974
  sleep,
975
+ onProgress,
850
976
  maxAttempts = APPLICATION_READY_MAX_ATTEMPTS
851
977
  }) {
852
978
  if (typeof request !== "function") {
@@ -876,6 +1002,15 @@ async function waitForApplicationReady({
876
1002
  }
877
1003
  if (attempt < maxAttempts && !controller.signal.aborted) {
878
1004
  await sleep(APPLICATION_READY_POLL_MS);
1005
+ const completedAttempts = attempt + 1;
1006
+ if (
1007
+ typeof onProgress === "function" &&
1008
+ completedAttempts % APPLICATION_READY_PROGRESS_ATTEMPTS === 0
1009
+ ) {
1010
+ await onProgress({
1011
+ elapsedSeconds: completedAttempts * APPLICATION_READY_POLL_MS / 1_000
1012
+ });
1013
+ }
879
1014
  }
880
1015
  }
881
1016
  return false;
@@ -1108,7 +1243,7 @@ function dockerEngineHelp(platform) {
1108
1243
  return `Start Docker Engine, verify the current user can access it, then run "${UNIVERSAL_COMMAND} doctor" again.`;
1109
1244
  }
1110
1245
 
1111
- function formatGigabytes(bytes) {
1246
+ function formatGibibytes(bytes) {
1112
1247
  return Math.floor((bytes / 1024 ** 3) * 10) / 10;
1113
1248
  }
1114
1249
 
package/src/index.mjs CHANGED
@@ -21,8 +21,8 @@ import { UNIVERSAL_COMMAND } from "./package-info.mjs";
21
21
 
22
22
  const CONFIG_SCHEMA_VERSION = 3;
23
23
  const MIN_INSTALL_CPU_COUNT = 4;
24
- const MIN_INSTALL_MEMORY_BYTES = 8_000_000_000;
25
- const MIN_INSTALL_MEMORY_LABEL = "8 GB";
24
+ const MIN_INSTALL_MEMORY_BYTES = 7 * 1024 ** 3;
25
+ const MIN_INSTALL_MEMORY_LABEL = "7 GiB usable (8 GB-class system)";
26
26
  const MIN_INSTALL_FREE_DISK_BYTES = 15 * 1024 ** 3;
27
27
  const PROFILE_RUNTIME_SETTINGS = Object.freeze({
28
28
  light: Object.freeze({
@@ -140,12 +140,12 @@ export function installResourceChecks(resources) {
140
140
  {
141
141
  name: "Memory",
142
142
  ok: totalMemoryBytes >= MIN_INSTALL_MEMORY_BYTES,
143
- detail: `${formatGibibytes(totalMemoryBytes)} GB available; ${MIN_INSTALL_MEMORY_LABEL} required`
143
+ detail: `${formatGibibytes(totalMemoryBytes)} GiB available; ${MIN_INSTALL_MEMORY_LABEL} required`
144
144
  },
145
145
  {
146
146
  name: "Free disk",
147
147
  ok: freeDiskBytes >= MIN_INSTALL_FREE_DISK_BYTES,
148
- detail: `${formatGibibytes(freeDiskBytes)} GB available; ${formatGibibytes(MIN_INSTALL_FREE_DISK_BYTES)} GB required`
148
+ detail: `${formatGibibytes(freeDiskBytes)} GiB available; ${formatGibibytes(MIN_INSTALL_FREE_DISK_BYTES)} GiB required`
149
149
  }
150
150
  ];
151
151
  }
@@ -4,7 +4,27 @@ const manifest = JSON.parse(
4
4
  readFileSync(new URL("../package.json", import.meta.url), "utf8")
5
5
  );
6
6
 
7
- export const PACKAGE_VERSION = manifest.version;
7
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
8
+
9
+ function requireVersion(value, field) {
10
+ if (typeof value !== "string" || !VERSION_PATTERN.test(value)) {
11
+ throw new Error(`run-spaceapp package.json must declare a valid ${field}.`);
12
+ }
13
+ return value;
14
+ }
15
+
16
+ export const PACKAGE_VERSION = requireVersion(manifest.version, "version");
17
+ export const RUNTIME_VERSION = requireVersion(
18
+ manifest.spaceappRuntimeVersion,
19
+ "spaceappRuntimeVersion"
20
+ );
21
+ if (typeof manifest.spaceappHostRootRuntimeCompatible !== "boolean") {
22
+ throw new Error(
23
+ "run-spaceapp package.json must declare spaceappHostRootRuntimeCompatible as a boolean."
24
+ );
25
+ }
26
+ export const HOST_ROOT_RUNTIME_COMPATIBLE =
27
+ manifest.spaceappHostRootRuntimeCompatible;
8
28
  export const LAUNCHER_DIST_TAG = PACKAGE_VERSION.includes("-hostroot.")
9
29
  ? "personal"
10
30
  : "latest";
@@ -20,6 +20,8 @@ const MAC_DOCKER_DOWNLOADS = Object.freeze({
20
20
  x64: "https://desktop.docker.com/mac/main/amd64/Docker.dmg",
21
21
  arm64: "https://desktop.docker.com/mac/main/arm64/Docker.dmg"
22
22
  });
23
+ const MAC_ADMIN_PASSWORD_PROMPT =
24
+ "SpaceApp Mac administrator password (input hidden): ";
23
25
  // Sources:
24
26
  // https://learn.microsoft.com/windows/package-manager/winget/install
25
27
  // https://github.com/microsoft/winget-pkgs/tree/master/manifests/d/Docker/DockerDesktop
@@ -444,8 +446,8 @@ async function ensureMacDocker({
444
446
  mounted = true;
445
447
  const signatureCode = await execute({
446
448
  command: "codesign",
447
- args: ["--verify", "--deep", "--strict", "--verbose=2", mountedApplication]
448
- }, { stdin, stdout: null, stderr });
449
+ args: ["--verify", "--deep", "--strict", mountedApplication]
450
+ }, { stdin, stdout: null, stderr: null });
449
451
  if (signatureCode !== 0) {
450
452
  stderr.write("The Docker Desktop application signature is not valid. Installation was stopped.\n");
451
453
  return { code: 1, reexecuted: false };
@@ -453,17 +455,33 @@ async function ensureMacDocker({
453
455
  const gatekeeperCode = await execute({
454
456
  command: "spctl",
455
457
  args: ["--assess", "--type", "execute", "--verbose=4", mountedApplication]
456
- }, { stdin, stdout: null, stderr });
458
+ }, { stdin, stdout: null, stderr: null });
457
459
  if (gatekeeperCode !== 0) {
458
460
  stderr.write("macOS Gatekeeper did not accept Docker Desktop. Installation was stopped.\n");
459
461
  return { code: 1, reexecuted: false };
460
462
  }
463
+ stdout.write("Docker Desktop signature and macOS trust verification passed.\n");
464
+ stdout.write(
465
+ "macOS administrator authorization is required to install Docker Desktop.\n" +
466
+ "Enter the password for your Mac administrator account—not a Docker or SpaceApp password.\n" +
467
+ "Characters are not displayed while you type. SpaceApp does not read, store, or log this password.\n"
468
+ );
461
469
  const installCode = await execute({
462
470
  command: "sudo",
463
- args: [installer, "--accept-license", `--user=${username}`]
471
+ args: [
472
+ "-p",
473
+ MAC_ADMIN_PASSWORD_PROMPT,
474
+ installer,
475
+ "--accept-license",
476
+ `--user=${username}`
477
+ ]
464
478
  }, { stdin, stdout, stderr });
465
479
  if (installCode !== 0) {
466
- stderr.write("Docker Desktop installation failed before SpaceApp downloaded any images.\n");
480
+ stderr.write(
481
+ "macOS authorization failed. Docker Desktop must be installed by a Mac administrator account.\n" +
482
+ "Enter that account's password—not a Docker or SpaceApp password—and rerun the same SpaceApp install command.\n" +
483
+ "No SpaceApp images were downloaded.\n"
484
+ );
467
485
  return { code: installCode, reexecuted: false };
468
486
  }
469
487
  } finally {
@@ -553,24 +571,19 @@ async function ensureLinuxDocker({
553
571
  stderr.write("A valid non-root Linux username is required for Docker Engine access.\n");
554
572
  return { code: 1, reexecuted: false };
555
573
  }
556
- const accepted = await confirmQuestion({
557
- stdin,
558
- stdout,
559
- question: [
574
+ stdout.write(
575
+ [
560
576
  "Docker's official post-install flow uses the docker group.",
561
577
  "Membership grants root-level privileges on this host.",
562
- `Add ${username} to the docker group and continue? [y/N] `
563
- ].join("\n")
564
- });
565
- if (!accepted) {
566
- stderr.write("Docker group membership was not changed.\n");
567
- return { code: 1, reexecuted: false };
568
- }
578
+ `Adding ${username} to the docker group automatically and continuing.`
579
+ ].join("\n") + "\n"
580
+ );
569
581
  const groupCode = await execute(
570
582
  { command: "sudo", args: ["usermod", "-aG", "docker", username] },
571
583
  { stdin, stdout, stderr }
572
584
  );
573
585
  if (groupCode !== 0) {
586
+ stderr.write(`SpaceApp could not add ${username} to the docker group.\n`);
574
587
  return { code: groupCode, reexecuted: false };
575
588
  }
576
589