run-spaceapp 0.1.14 → 0.1.15-hostroot.1

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
@@ -19,12 +19,14 @@ The launcher requires Node.js 20.11 or newer, 4 CPUs, 8 GB RAM, and 15 GiB free
19
19
  disk. Keep `@latest` in the command so npm resolves the current release instead
20
20
  of matching an existing local launcher. If Docker is missing, the universal
21
21
  install command automatically installs it from official Docker sources on
22
- Windows, macOS, Ubuntu, Debian, Fedora, RHEL, and CentOS, starts it, and waits
23
- for Engine and Compose readiness before pulling images. Windows prefers
22
+ Windows, macOS, Ubuntu, Debian, Fedora, RHEL, and CentOS, or with native
23
+ `pacman` packages on Arch-family Linux including CachyOS. It starts Docker and
24
+ waits for Engine and Compose readiness before pulling images. Windows prefers
24
25
  Windows Package Manager's hash-pinned Docker Desktop
25
26
  manifest and retains a signed direct-download fallback. Docker Desktop license
26
- acceptance and Linux `docker` group membership require confirmation inside the
27
- 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
28
30
  Desktop. Use the **Command Prompt** profile because a restricted PowerShell
29
31
  policy can block npm's `npx.ps1` before SpaceApp starts. On Docker Desktop's
30
32
  first launch, select **Skip** in the top-right of the **Welcome to Docker**
@@ -50,6 +52,21 @@ current user's platform config directory:
50
52
 
51
53
  Set `SPACEAPP_HOME` to an absolute path to use a dedicated installation root.
52
54
 
55
+ The `0.1.15-hostroot.1` personal candidate tests installer fixes while reusing
56
+ the existing `0.1.15-hostroot.0` runtime images:
57
+
58
+ ```bash
59
+ npx --yes run-spaceapp@personal install
60
+ ```
61
+
62
+ This launcher-only candidate accepts isolated access on Linux, macOS, and
63
+ Windows. It rejects `--access host-root` because no matching runtime images
64
+ were rebuilt. The launcher reports version `.1` while `runtime.env` remains
65
+ pinned to `.0`.
66
+
67
+ Use the same `npx --yes run-spaceapp@personal` prefix for follow-up commands
68
+ while this prerelease is installed.
69
+
53
70
  Provider API keys are read from masked standard input and written to
54
71
  restrictive files outside Git. OAuth and device-code credentials remain in
55
72
  isolated Docker provider state. Neither credential source is included in
@@ -59,10 +76,10 @@ Claude Code is not redistributed in SpaceApp images. Install the reviewed
59
76
  package as an explicit owner action with:
60
77
 
61
78
  ```bash
62
- npx --yes run-spaceapp@latest provider install claude
79
+ npx --yes run-spaceapp@personal provider install claude
63
80
  ```
64
81
 
65
- Run `npx --yes run-spaceapp@latest help` for the complete command list. Full
82
+ Run `npx --yes run-spaceapp@personal help` for the complete command list. Full
66
83
  documentation:
67
84
 
68
85
  - [Getting started](https://github.com/oll4com/spaceapp/blob/main/docs/getting-started.md)
@@ -75,16 +92,16 @@ After the stack passes readiness checks, the install command prints the fresh
75
92
  it expires before the owner is created, run:
76
93
 
77
94
  ```bash
78
- npx --yes run-spaceapp@latest owner rotate-setup-token
95
+ npx --yes run-spaceapp@personal owner rotate-setup-token
79
96
  ```
80
97
 
81
98
  Common follow-up commands use the same reliable launcher prefix:
82
99
 
83
100
  ```bash
84
- npx --yes run-spaceapp@latest doctor
85
- npx --yes run-spaceapp@latest status
86
- npx --yes run-spaceapp@latest credentials list
87
- npx --yes run-spaceapp@latest uninstall
101
+ npx --yes run-spaceapp@personal doctor
102
+ npx --yes run-spaceapp@personal status
103
+ npx --yes run-spaceapp@personal credentials list
104
+ npx --yes run-spaceapp@personal uninstall
88
105
  ```
89
106
 
90
107
  Uninstall retains data by default and prints the separate
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "run-spaceapp",
3
- "version": "0.1.14",
3
+ "version": "0.1.15-hostroot.1",
4
+ "spaceappRuntimeVersion": "0.1.15-hostroot.0",
5
+ "spaceappHostRootRuntimeCompatible": false,
4
6
  "description": "Cross-platform Docker launcher for the SpaceApp self-hosted agent workspace",
5
7
  "license": "Apache-2.0",
6
8
  "type": "module",
@@ -30,7 +32,7 @@
30
32
  ],
31
33
  "scripts": {
32
34
  "test": "node --test tests/*.test.mjs",
33
- "check": "node --check bin/spaceapp.mjs && node --check src/index.mjs && node --check src/cli.mjs && node --check src/prerequisites.mjs",
35
+ "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",
34
36
  "pack:dry-run": "npm pack --dry-run"
35
37
  },
36
38
  "repository": {
@@ -46,5 +48,5 @@
46
48
  "access": "public",
47
49
  "provenance": true
48
50
  },
49
- "gitHead": "f6956f38649503f6a9dd5c232d3d51d7bf2fa258"
51
+ "gitHead": "7e0543f784e614cd588bf54cd3228de3912d03d6"
50
52
  }
package/src/cli.mjs CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  removeCredential,
17
17
  removeWorkspace,
18
18
  prepareInstallation,
19
+ resolveInstallAccessMode,
19
20
  resolveInstallProfile,
20
21
  resolveSpaceAppHome,
21
22
  saveConfig,
@@ -29,12 +30,20 @@ import {
29
30
  prepareDockerCliPath,
30
31
  windowsPowerShellArgs
31
32
  } from "./prerequisites.mjs";
33
+ import {
34
+ HOST_ROOT_RUNTIME_COMPATIBLE,
35
+ PACKAGE_VERSION,
36
+ RUNTIME_VERSION,
37
+ UNIVERSAL_COMMAND
38
+ } from "./package-info.mjs";
32
39
 
33
- 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;
34
42
  const APPLICATION_READY_POLL_MS = 2_000;
35
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;
36
46
  const SETUP_STATUS_TIMEOUT_MS = 10_000;
37
- const UNIVERSAL_COMMAND = "npx --yes run-spaceapp@latest";
38
47
  const trustedCommands = new Set([
39
48
  "codesign",
40
49
  "docker",
@@ -68,25 +77,31 @@ export async function run(argv, {
68
77
  prepareDockerPath = prepareDockerCliPath,
69
78
  request = globalThis.fetch,
70
79
  sleep = wait,
71
- persistSetupToken = writeSetupToken
80
+ persistSetupToken = writeSetupToken,
81
+ launcherVersion = PACKAGE_VERSION,
82
+ runtimeVersion = RUNTIME_VERSION,
83
+ hostRootRuntimeCompatible = HOST_ROOT_RUNTIME_COMPATIBLE
72
84
  } = {}) {
73
- await prepareDockerPath({ platform, env });
74
85
  const [command = "help", ...args] = argv;
75
86
  const root = resolveSpaceAppHome({ env, platform });
76
- const version = await packageVersion();
77
87
 
88
+ if (command !== "install") {
89
+ await prepareDockerPath({ platform, env });
90
+ }
78
91
  if (command === "help" || command === "--help" || command === "-h") {
79
92
  stdout.write(helpText());
80
93
  return 0;
81
94
  }
82
95
  if (command === "--version" || command === "-v") {
83
- stdout.write(`${version}\n`);
96
+ stdout.write(`${launcherVersion}\n`);
84
97
  return 0;
85
98
  }
86
99
  if (command === "install") {
87
100
  return installCommand(args, {
88
101
  root,
89
- version,
102
+ launcherVersion,
103
+ runtimeVersion,
104
+ hostRootRuntimeCompatible,
90
105
  platform,
91
106
  arch,
92
107
  env,
@@ -96,6 +111,7 @@ export async function run(argv, {
96
111
  execute,
97
112
  inspectResources,
98
113
  ensureDocker,
114
+ prepareDockerPath,
99
115
  request,
100
116
  sleep,
101
117
  persistSetupToken
@@ -103,7 +119,7 @@ export async function run(argv, {
103
119
  }
104
120
  if (command === "init") {
105
121
  assertNoArgs(args, "init");
106
- const result = await initializeInstallation(root, { version });
122
+ const result = await initializeInstallation(root, { version: runtimeVersion });
107
123
  stdout.write(`SpaceApp initialized at ${root}\n`);
108
124
  if (result.setupToken) {
109
125
  stdout.write(`One-time setup token: ${result.setupToken}\n`);
@@ -156,7 +172,15 @@ export async function run(argv, {
156
172
  return ownerCommand(args, { root, config, stdin, stdout, stderr, execute: runtimeExecute });
157
173
  }
158
174
  if (command === "update") {
159
- 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
+ });
160
184
  }
161
185
  if (command === "rollback") {
162
186
  assertNoArgs(args, "rollback");
@@ -258,7 +282,9 @@ export async function run(argv, {
258
282
 
259
283
  async function installCommand(args, {
260
284
  root,
261
- version,
285
+ launcherVersion,
286
+ runtimeVersion,
287
+ hostRootRuntimeCompatible,
262
288
  platform,
263
289
  arch,
264
290
  env,
@@ -268,28 +294,54 @@ async function installCommand(args, {
268
294
  execute,
269
295
  inspectResources,
270
296
  ensureDocker,
297
+ prepareDockerPath,
271
298
  request,
272
299
  sleep,
273
300
  persistSetupToken
274
301
  }) {
275
- const { requestedProfile, noOpen } = parseInstallArgs(args);
302
+ const { requestedProfile, requestedAccessMode, noOpen } = parseInstallArgs(args);
303
+ const existingConfig = await loadExistingInstallation(root);
304
+ const accessMode = resolveInstallAccessMode(
305
+ requestedAccessMode,
306
+ existingConfig?.accessMode ?? "isolated"
307
+ );
308
+ if (accessMode === "host-root" && platform !== "linux") {
309
+ throw new Error("Host-root access is supported only on Linux.");
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
+ }
316
+ await prepareDockerPath({ platform, env });
276
317
  const resources = await inspectResources(root);
277
318
  const profile = resolveInstallProfile(requestedProfile, resources.totalMemoryBytes);
278
- const existingConfig = await loadExistingInstallation(root);
279
- const result = await prepareInstallation(root, { version, profile });
319
+ if (accessMode === "host-root") {
320
+ stderr.write(
321
+ "WARNING: host-root access lets SpaceApp CLI sessions read and modify the entire Linux host through /host, including credentials and system files.\n"
322
+ );
323
+ }
324
+ const result = await prepareInstallation(root, {
325
+ version: runtimeVersion,
326
+ profile,
327
+ accessMode
328
+ });
280
329
 
281
- stdout.write(`Launcher version: ${version}\n`);
330
+ stdout.write(`Launcher version: ${launcherVersion}\n`);
282
331
  stdout.write(
283
- `SpaceApp version: ${existingConfig?.version ?? "not installed"} -> ${result.config.version}\n`
332
+ `Runtime image version: ${existingConfig?.version ?? "not installed"} -> ${result.config.version}\n`
284
333
  );
285
334
  stdout.write(
286
335
  `Profile: ${existingConfig?.profile ?? "not configured"} -> ${result.config.profile}\n`
287
336
  );
337
+ stdout.write(
338
+ `Access: ${existingConfig?.accessMode ?? "not configured"} -> ${result.config.accessMode}\n`
339
+ );
288
340
  stdout.write(existingConfig
289
341
  ? "Preserved existing data, workspaces, credentials, secrets, and persistent Docker volumes.\n"
290
342
  : "Future refreshes preserve data, workspaces, credentials, secrets, and persistent Docker volumes.\n");
291
343
  stdout.write(
292
- `Selected profile: ${profile} (${formatGigabytes(resources.totalMemoryBytes)} GB system memory detected).\n`
344
+ `Selected profile: ${profile} (${formatGibibytes(resources.totalMemoryBytes)} GiB system memory detected).\n`
293
345
  );
294
346
  stdout.write(`SpaceApp installation root: ${root}\n`);
295
347
  if (installResourceChecks(resources).some((check) => !check.ok)) {
@@ -317,14 +369,15 @@ async function installCommand(args, {
317
369
  stdout,
318
370
  stderr,
319
371
  execute,
320
- installArgs: { root, requestedProfile, noOpen }
372
+ installArgs: { root, requestedProfile, requestedAccessMode, noOpen }
321
373
  });
322
- if (prerequisiteResult.reexecuted || prerequisiteResult.code !== 0) {
323
- if (prerequisiteResult.code !== 0) {
324
- stderr.write(
325
- `Installation stopped before downloading images. Fix the failed checks and run "${UNIVERSAL_COMMAND} install" again.\n`
326
- );
327
- }
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
+ );
328
381
  return prerequisiteResult.code;
329
382
  }
330
383
 
@@ -346,10 +399,42 @@ async function installCommand(args, {
346
399
  return doctorCode;
347
400
  }
348
401
  const stagedStateRoot = await mkdtemp(join(tmpdir(), "run-spaceapp-install-"));
402
+ let runtimeMutationAttempted = false;
403
+ const failAfterRuntimeMutation = async (code) => {
404
+ if (runtimeMutationAttempted) {
405
+ await reportInstallDiagnostics({
406
+ root,
407
+ stateRoot: stagedStateRoot,
408
+ profile,
409
+ platform,
410
+ stdin,
411
+ stdout,
412
+ stderr,
413
+ execute
414
+ });
415
+ await restoreRuntimeAfterFailedInstall({
416
+ root,
417
+ stagedStateRoot,
418
+ existingConfig,
419
+ attemptedProfile: profile,
420
+ platform,
421
+ stdin,
422
+ stdout,
423
+ stderr,
424
+ execute
425
+ });
426
+ runtimeMutationAttempted = false;
427
+ }
428
+ return code;
429
+ };
349
430
  try {
350
431
  await commitInstallation(stagedStateRoot, result.config);
351
- const stagedComposeCommand = (action) =>
352
- composeCommand(action, root, { profile, stateRoot: stagedStateRoot });
432
+ const stagedComposeCommand = (action, options = {}) =>
433
+ composeCommand(action, root, {
434
+ profile,
435
+ stateRoot: stagedStateRoot,
436
+ ...options
437
+ });
353
438
  const pullCode = await executeWithDockerDiagnostics(
354
439
  execute,
355
440
  stagedComposeCommand("pull"),
@@ -357,27 +442,72 @@ async function installCommand(args, {
357
442
  { platform, stderr }
358
443
  );
359
444
  if (pullCode !== 0) return pullCode;
445
+ runtimeMutationAttempted = true;
360
446
  const upCode = await executeWithDockerDiagnostics(
361
447
  execute,
362
448
  stagedComposeCommand("up"),
363
449
  { stdin, stdout, stderr },
364
450
  { platform, stderr }
365
451
  );
366
- if (upCode !== 0) return upCode;
452
+ if (upCode !== 0) return await failAfterRuntimeMutation(upCode);
367
453
 
368
454
  const url = `http://${result.config.bindHost}:${result.config.port}`;
369
- 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
+ );
370
458
  const ready = await waitForApplicationReady({
371
459
  url,
372
460
  request,
373
- 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
+ }
374
504
  });
375
505
  if (!ready) {
376
506
  stderr.write(
377
- "SpaceApp containers started, but the application did not become ready within 3 minutes.\n" +
378
- `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`
379
509
  );
380
- return 1;
510
+ return await failAfterRuntimeMutation(1);
381
511
  }
382
512
 
383
513
  let setupStatus;
@@ -388,7 +518,7 @@ async function installCommand(args, {
388
518
  `SpaceApp is ready, but owner setup status could not be verified: ${error?.message || String(error)}\n` +
389
519
  `Run "${UNIVERSAL_COMMAND} status" and "${UNIVERSAL_COMMAND} logs", then run "${UNIVERSAL_COMMAND} install" again.\n`
390
520
  );
391
- return 1;
521
+ return await failAfterRuntimeMutation(1);
392
522
  }
393
523
 
394
524
  let setupToken = null;
@@ -409,7 +539,7 @@ async function installCommand(args, {
409
539
  stderr.write(
410
540
  `SpaceApp is ready, but a fresh owner setup token could not be created. Run "${UNIVERSAL_COMMAND} owner rotate-setup-token".\n`
411
541
  );
412
- return rotateCode;
542
+ return await failAfterRuntimeMutation(rotateCode);
413
543
  }
414
544
  }
415
545
 
@@ -427,7 +557,7 @@ async function installCommand(args, {
427
557
  stderr.write(
428
558
  "SpaceApp is ready, but the managed browser container could not be removed for the light profile.\n"
429
559
  );
430
- return removeBrowserCode;
560
+ return await failAfterRuntimeMutation(removeBrowserCode);
431
561
  }
432
562
  }
433
563
 
@@ -439,11 +569,12 @@ async function installCommand(args, {
439
569
  "SpaceApp accepted a new setup token, but it could not be saved locally.\n" +
440
570
  `Run "${UNIVERSAL_COMMAND} owner rotate-setup-token" to obtain a usable token.\n`
441
571
  );
442
- return 1;
572
+ return await failAfterRuntimeMutation(1);
443
573
  }
444
574
  }
445
575
 
446
576
  await commitInstallation(root, result.config);
577
+ runtimeMutationAttempted = false;
447
578
  stdout.write(`SpaceApp is ready at ${url}\n`);
448
579
  if (setupToken) {
449
580
  stdout.write(`One-time setup token: ${setupToken}\n`);
@@ -460,13 +591,117 @@ async function installCommand(args, {
460
591
  stderr.write(`Could not open SpaceApp automatically. Open ${url} manually.\n`);
461
592
  }
462
593
  return 0;
594
+ } catch (error) {
595
+ await failAfterRuntimeMutation(1);
596
+ throw error;
463
597
  } finally {
464
598
  await rm(stagedStateRoot, { recursive: true, force: true });
465
599
  }
466
600
  }
467
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
+
638
+ async function restoreRuntimeAfterFailedInstall({
639
+ root,
640
+ stagedStateRoot,
641
+ existingConfig,
642
+ attemptedProfile,
643
+ platform,
644
+ stdin,
645
+ stdout,
646
+ stderr,
647
+ execute
648
+ }) {
649
+ const runtimeExecute = (spec) => executeWithDockerDiagnostics(
650
+ execute,
651
+ spec,
652
+ { stdin, stdout, stderr },
653
+ { platform, stderr }
654
+ );
655
+ try {
656
+ if (existingConfig) {
657
+ await commitInstallation(root, existingConfig);
658
+ const restoreCode = await runtimeExecute(
659
+ composeCommand("up", root, {
660
+ profile: existingConfig.profile
661
+ })
662
+ );
663
+ if (restoreCode === 0) {
664
+ if (existingConfig.profile === "light") {
665
+ const cleanupCode = await runtimeExecute(
666
+ composeCommand("removeBrowser", root, {
667
+ profile: "standard"
668
+ })
669
+ );
670
+ if (cleanupCode !== 0) {
671
+ stderr.write(
672
+ "The previous SpaceApp runtime was restored, but an inactive browser container may remain.\n"
673
+ );
674
+ }
675
+ }
676
+ stderr.write("The previous SpaceApp runtime and access mode were restored.\n");
677
+ return;
678
+ }
679
+ stderr.write(
680
+ "The previous SpaceApp runtime could not be restored; stopping the partially updated runtime.\n"
681
+ );
682
+ }
683
+
684
+ const stopCode = await runtimeExecute(
685
+ composeCommand("down", root, {
686
+ profile: existingConfig?.profile ?? attemptedProfile,
687
+ stateRoot: existingConfig ? root : stagedStateRoot
688
+ })
689
+ );
690
+ if (stopCode !== 0) {
691
+ stderr.write(
692
+ "The partially updated SpaceApp runtime could not be stopped automatically. Run the install command again immediately.\n"
693
+ );
694
+ }
695
+ } catch (error) {
696
+ stderr.write(
697
+ `SpaceApp could not restore or stop the partially updated runtime: ${error?.message || String(error)}\n`
698
+ );
699
+ }
700
+ }
701
+
468
702
  function parseInstallArgs(args) {
469
703
  let requestedProfile = "auto";
704
+ let requestedAccessMode;
470
705
  let noOpen = false;
471
706
  for (let index = 0; index < args.length; index += 1) {
472
707
  const argument = args[index];
@@ -483,16 +718,31 @@ function parseInstallArgs(args) {
483
718
  requestedProfile = argument.slice("--profile=".length);
484
719
  continue;
485
720
  }
721
+ if (argument === "--access" && index + 1 < args.length) {
722
+ requestedAccessMode = args[index + 1];
723
+ index += 1;
724
+ continue;
725
+ }
726
+ if (argument.startsWith("--access=")) {
727
+ requestedAccessMode = argument.slice("--access=".length);
728
+ continue;
729
+ }
486
730
  throw new Error(
487
- `Usage: ${UNIVERSAL_COMMAND} install [--profile auto|light|standard] [--no-open]`
731
+ `Usage: ${UNIVERSAL_COMMAND} install [--profile auto|light|standard] [--access isolated|host-root] [--no-open]`
488
732
  );
489
733
  }
490
- if (!["auto", "light", "standard"].includes(requestedProfile)) {
734
+ if (
735
+ !["auto", "light", "standard"].includes(requestedProfile) ||
736
+ (
737
+ requestedAccessMode !== undefined &&
738
+ !["isolated", "host-root"].includes(requestedAccessMode)
739
+ )
740
+ ) {
491
741
  throw new Error(
492
- `Usage: ${UNIVERSAL_COMMAND} install [--profile auto|light|standard] [--no-open]`
742
+ `Usage: ${UNIVERSAL_COMMAND} install [--profile auto|light|standard] [--access isolated|host-root] [--no-open]`
493
743
  );
494
744
  }
495
- return { requestedProfile, noOpen };
745
+ return { requestedProfile, requestedAccessMode, noOpen };
496
746
  }
497
747
 
498
748
  async function loadExistingInstallation(root) {
@@ -612,8 +862,8 @@ async function ownerCommand(args, { root, config, stdin, stdout, stderr, execute
612
862
  );
613
863
  }
614
864
  const password = await readSecret(stdin, stdout, "New owner password: ");
615
- if (password.length < 12) {
616
- 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.");
617
867
  }
618
868
  return execute(composeCommand("resetOwnerPassword", root, { profile: config.profile }), {
619
869
  stdin,
@@ -628,11 +878,13 @@ async function updateCommand(args, { root, config, version, stdin, stdout, stder
628
878
  throw new Error(`Usage: ${UNIVERSAL_COMMAND} update [version]`);
629
879
  }
630
880
  const targetVersion = args[0] || version;
631
- const updated = {
632
- ...config,
633
- version: targetVersion,
634
- previousVersion: config.version
635
- };
881
+ const updated = targetVersion === config.version
882
+ ? config
883
+ : {
884
+ ...config,
885
+ version: targetVersion,
886
+ previousVersion: config.version
887
+ };
636
888
  await writeRuntimeFiles(root, updated);
637
889
  const pullCode = await execute(composeCommand("pull", root, { profile: updated.profile }), { stdin, stdout, stderr });
638
890
  if (pullCode !== 0) {
@@ -720,6 +972,7 @@ async function waitForApplicationReady({
720
972
  url,
721
973
  request,
722
974
  sleep,
975
+ onProgress,
723
976
  maxAttempts = APPLICATION_READY_MAX_ATTEMPTS
724
977
  }) {
725
978
  if (typeof request !== "function") {
@@ -749,6 +1002,15 @@ async function waitForApplicationReady({
749
1002
  }
750
1003
  if (attempt < maxAttempts && !controller.signal.aborted) {
751
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
+ }
752
1014
  }
753
1015
  }
754
1016
  return false;
@@ -981,7 +1243,7 @@ function dockerEngineHelp(platform) {
981
1243
  return `Start Docker Engine, verify the current user can access it, then run "${UNIVERSAL_COMMAND} doctor" again.`;
982
1244
  }
983
1245
 
984
- function formatGigabytes(bytes) {
1246
+ function formatGibibytes(bytes) {
985
1247
  return Math.floor((bytes / 1024 ** 3) * 10) / 10;
986
1248
  }
987
1249
 
@@ -989,11 +1251,6 @@ function wait(milliseconds) {
989
1251
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
990
1252
  }
991
1253
 
992
- async function packageVersion() {
993
- const packageJson = new URL("../package.json", import.meta.url);
994
- return JSON.parse(await readFile(packageJson, "utf8")).version;
995
- }
996
-
997
1254
  function helpText() {
998
1255
  return `SpaceApp self-hosted launcher
999
1256
 
@@ -1001,7 +1258,7 @@ Usage: ${UNIVERSAL_COMMAND} <command>
1001
1258
  spaceapp <command> Optional global launcher
1002
1259
 
1003
1260
  init Create a local SpaceApp installation
1004
- install [--profile auto|light|standard] [--no-open]
1261
+ install [--profile auto|light|standard] [--access isolated|host-root] [--no-open]
1005
1262
  Install prerequisites, initialize, and start
1006
1263
  up | down | status | logs Manage the Docker application
1007
1264
  open Open the local web application
package/src/index.mjs CHANGED
@@ -17,11 +17,12 @@ import {
17
17
  normalizeWorkspaceName,
18
18
  stripTrailingLineEndings
19
19
  } from "./string-utils.mjs";
20
+ import { UNIVERSAL_COMMAND } from "./package-info.mjs";
20
21
 
21
- const CONFIG_SCHEMA_VERSION = 2;
22
+ const CONFIG_SCHEMA_VERSION = 3;
22
23
  const MIN_INSTALL_CPU_COUNT = 4;
23
- const MIN_INSTALL_MEMORY_BYTES = 8_000_000_000;
24
- 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)";
25
26
  const MIN_INSTALL_FREE_DISK_BYTES = 15 * 1024 ** 3;
26
27
  const PROFILE_RUNTIME_SETTINGS = Object.freeze({
27
28
  light: Object.freeze({
@@ -68,6 +69,7 @@ const CONFIG_KEYS = new Set([
68
69
  "port",
69
70
  "telemetry",
70
71
  "profile",
72
+ "accessMode",
71
73
  "workspaces"
72
74
  ]);
73
75
 
@@ -101,6 +103,16 @@ export function resolveInstallProfile(requestedProfile, totalMemoryBytes) {
101
103
  return "light";
102
104
  }
103
105
 
106
+ export function resolveInstallAccessMode(requestedMode, existingMode = "isolated") {
107
+ if (requestedMode === undefined) {
108
+ return existingMode;
109
+ }
110
+ if (requestedMode === "isolated" || requestedMode === "host-root") {
111
+ return requestedMode;
112
+ }
113
+ throw new Error("Install access mode must be isolated or host-root.");
114
+ }
115
+
104
116
  export async function inspectSystemResources(root) {
105
117
  validateHome(root);
106
118
  await mkdir(root, { recursive: true, mode: 0o700 });
@@ -128,21 +140,26 @@ export function installResourceChecks(resources) {
128
140
  {
129
141
  name: "Memory",
130
142
  ok: totalMemoryBytes >= MIN_INSTALL_MEMORY_BYTES,
131
- detail: `${formatGibibytes(totalMemoryBytes)} GB available; ${MIN_INSTALL_MEMORY_LABEL} required`
143
+ detail: `${formatGibibytes(totalMemoryBytes)} GiB available; ${MIN_INSTALL_MEMORY_LABEL} required`
132
144
  },
133
145
  {
134
146
  name: "Free disk",
135
147
  ok: freeDiskBytes >= MIN_INSTALL_FREE_DISK_BYTES,
136
- 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`
137
149
  }
138
150
  ];
139
151
  }
140
152
 
141
- export function createDefaultConfig({ version, profile = "light" }) {
153
+ export function createDefaultConfig({
154
+ version,
155
+ profile = "light",
156
+ accessMode = "isolated"
157
+ }) {
142
158
  assertVersion(version);
143
159
  if (profile !== "light" && profile !== "standard") {
144
160
  throw new Error("Default config requires a resolved light or standard profile.");
145
161
  }
162
+ const resolvedAccessMode = resolveInstallAccessMode(accessMode);
146
163
  return {
147
164
  schemaVersion: CONFIG_SCHEMA_VERSION,
148
165
  version,
@@ -151,6 +168,7 @@ export function createDefaultConfig({ version, profile = "light" }) {
151
168
  port: 4911,
152
169
  telemetry: false,
153
170
  profile,
171
+ accessMode: resolvedAccessMode,
154
172
  workspaces: []
155
173
  };
156
174
  }
@@ -186,6 +204,9 @@ export function validateConfig(config) {
186
204
  if (!["light", "standard"].includes(config.profile)) {
187
205
  throw new Error("profile must be light or standard.");
188
206
  }
207
+ if (config.accessMode !== "isolated" && config.accessMode !== "host-root") {
208
+ throw new Error("accessMode must be isolated or host-root.");
209
+ }
189
210
  if (!Array.isArray(config.workspaces)) {
190
211
  throw new Error("workspaces must be an array.");
191
212
  }
@@ -209,6 +230,13 @@ export async function loadConfig(root) {
209
230
  }
210
231
 
211
232
  function migrateConfig(config) {
233
+ if (config?.schemaVersion === 2) {
234
+ return {
235
+ ...config,
236
+ schemaVersion: CONFIG_SCHEMA_VERSION,
237
+ accessMode: "isolated"
238
+ };
239
+ }
212
240
  if (config?.schemaVersion !== 1) {
213
241
  return config;
214
242
  }
@@ -219,7 +247,8 @@ function migrateConfig(config) {
219
247
  return {
220
248
  ...config,
221
249
  schemaVersion: CONFIG_SCHEMA_VERSION,
222
- profile: legacyProfiles[config.profile] ?? config.profile
250
+ profile: legacyProfiles[config.profile] ?? config.profile,
251
+ accessMode: "isolated"
223
252
  };
224
253
  }
225
254
 
@@ -351,6 +380,35 @@ export function renderWorkspaceCompose(config) {
351
380
  ].join("\n");
352
381
  }
353
382
 
383
+ export function renderHostAccessCompose(config) {
384
+ validateConfig(config);
385
+ if (config.accessMode === "isolated") {
386
+ return "services: {}\n";
387
+ }
388
+ const hostMount = (readOnly) => [
389
+ " - type: bind",
390
+ ' source: "/"',
391
+ ' target: "/host"',
392
+ ` read_only: ${readOnly}`,
393
+ " bind:",
394
+ " propagation: rslave"
395
+ ];
396
+ return [
397
+ "services:",
398
+ " spaceapp-core:",
399
+ " environment:",
400
+ ' SPACE_CLI_WORKSPACE_ROOT: "/host"',
401
+ " volumes:",
402
+ ...hostMount(true),
403
+ " spaceapp-cli:",
404
+ " environment:",
405
+ ' SPACEAPP_CLI_HOST_ROOT_ACCESS: "true"',
406
+ " volumes:",
407
+ ...hostMount(false),
408
+ ""
409
+ ].join("\n");
410
+ }
411
+
354
412
  export function composeCommand(action, root, options = {}) {
355
413
  validateHome(root);
356
414
  const stateRoot = options.stateRoot ?? root;
@@ -366,6 +424,7 @@ export function composeCommand(action, root, options = {}) {
366
424
  "--env-file", join(stateRoot, "runtime.env"),
367
425
  "-f", join(stateRoot, "compose.yml"),
368
426
  "-f", join(stateRoot, "compose.workspaces.yml"),
427
+ "-f", join(stateRoot, "compose.host-access.yml"),
369
428
  ...(profile === "standard" ? ["--profile", "standard"] : [])
370
429
  ];
371
430
  const actions = {
@@ -462,7 +521,7 @@ export async function selectLatestBackupId(root) {
462
521
  .at(-1);
463
522
  if (!backupId) {
464
523
  throw new Error(
465
- 'No portable backup exists. Run "npx --yes run-spaceapp@latest backup" before restore.'
524
+ `No portable backup exists. Run "${UNIVERSAL_COMMAND} backup" before restore.`
466
525
  );
467
526
  }
468
527
  return backupId;
@@ -474,11 +533,13 @@ export async function writeRuntimeFiles(root, config) {
474
533
  await mkdir(root, { recursive: true, mode: 0o700 });
475
534
  await atomicWrite(join(root, "runtime.env"), renderRuntimeEnv(config));
476
535
  await atomicWrite(join(root, "compose.workspaces.yml"), renderWorkspaceCompose(config));
536
+ await atomicWrite(join(root, "compose.host-access.yml"), renderHostAccessCompose(config));
477
537
  }
478
538
 
479
539
  export async function prepareInstallation(root, {
480
540
  version,
481
- profile
541
+ profile,
542
+ accessMode
482
543
  }) {
483
544
  validateHome(root);
484
545
  await mkdir(root, { recursive: true, mode: 0o700 });
@@ -496,7 +557,11 @@ export async function prepareInstallation(root, {
496
557
  if (error?.code !== "ENOENT") {
497
558
  throw error;
498
559
  }
499
- config = createDefaultConfig({ version, profile: resolvedProfile ?? "light" });
560
+ config = createDefaultConfig({
561
+ version,
562
+ profile: resolvedProfile ?? "light",
563
+ accessMode: resolveInstallAccessMode(accessMode)
564
+ });
500
565
  }
501
566
  if (config.version !== version) {
502
567
  config = {
@@ -508,6 +573,10 @@ export async function prepareInstallation(root, {
508
573
  if (resolvedProfile !== undefined) {
509
574
  config = { ...config, profile: resolvedProfile };
510
575
  }
576
+ const resolvedAccessMode = resolveInstallAccessMode(accessMode, config.accessMode);
577
+ if (config.accessMode !== resolvedAccessMode) {
578
+ config = { ...config, accessMode: resolvedAccessMode };
579
+ }
511
580
  validateConfig(config);
512
581
 
513
582
  const postgresPasswordPath = join(root, "secrets", "postgres-password");
@@ -0,0 +1,32 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const manifest = JSON.parse(
4
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
5
+ );
6
+
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;
28
+ export const LAUNCHER_DIST_TAG = PACKAGE_VERSION.includes("-hostroot.")
29
+ ? "personal"
30
+ : "latest";
31
+ export const UNIVERSAL_COMMAND = `npx --yes run-spaceapp@${LAUNCHER_DIST_TAG}`;
32
+ export const UNIVERSAL_INSTALL_COMMAND = `${UNIVERSAL_COMMAND} install`;
@@ -8,10 +8,10 @@ import { Readable } from "node:stream";
8
8
  import { pipeline } from "node:stream/promises";
9
9
  import { spawn } from "node:child_process";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { UNIVERSAL_INSTALL_COMMAND } from "./package-info.mjs";
11
12
 
12
13
  const DOCKER_TERMS_URL = "https://www.docker.com/legal/docker-subscription-service-agreement/";
13
14
  const DOWNLOAD_TIMEOUT_MS = 30 * 60 * 1_000;
14
- const UNIVERSAL_INSTALL_COMMAND = "npx --yes run-spaceapp@latest install";
15
15
  const WINDOWS_DOCKER_DOWNLOADS = Object.freeze({
16
16
  x64: "https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe",
17
17
  arm64: "https://desktop.docker.com/win/main/arm64/Docker%20Desktop%20Installer.exe"
@@ -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
 
@@ -644,6 +657,10 @@ async function installLinuxDockerEngine({
644
657
  download
645
658
  }) {
646
659
  const id = String(release?.ID || "").toLowerCase();
660
+ const idFamily = new Set([
661
+ id,
662
+ ...String(release?.ID_LIKE || "").toLowerCase().split(/\s+/).filter(Boolean)
663
+ ]);
647
664
  if (id === "ubuntu" || id === "debian") {
648
665
  return installAptDockerEngine({
649
666
  arch,
@@ -659,6 +676,13 @@ async function installLinuxDockerEngine({
659
676
  if (id === "fedora" || id === "rhel" || id === "centos") {
660
677
  return installDnfDockerEngine({ id, stdin, stdout, stderr, execute });
661
678
  }
679
+ if (
680
+ idFamily.has("arch") ||
681
+ idFamily.has("archlinux") ||
682
+ idFamily.has("cachyos")
683
+ ) {
684
+ return installPacmanDockerEngine({ stdin, stdout, stderr, execute });
685
+ }
662
686
  stderr.write(
663
687
  `Automatic Docker Engine installation is not yet supported for Linux distribution "${id || "unknown"}". ` +
664
688
  "Install Docker Engine and the Compose plugin from https://docs.docker.com/engine/install/ and rerun the same command.\n"
@@ -666,6 +690,23 @@ async function installLinuxDockerEngine({
666
690
  return 1;
667
691
  }
668
692
 
693
+ function installPacmanDockerEngine({ stdin, stdout, stderr, execute }) {
694
+ return execute(
695
+ {
696
+ command: "sudo",
697
+ args: [
698
+ "pacman",
699
+ "-S",
700
+ "--needed",
701
+ "--noconfirm",
702
+ "docker",
703
+ "docker-compose"
704
+ ]
705
+ },
706
+ { stdin, stdout, stderr }
707
+ );
708
+ }
709
+
669
710
  async function installAptDockerEngine({
670
711
  arch,
671
712
  id,
@@ -786,7 +827,11 @@ async function readLinuxOsRelease() {
786
827
  return values;
787
828
  }
788
829
 
789
- function linuxReentryCommand({ requestedProfile = "auto", noOpen = false } = {}) {
830
+ function linuxReentryCommand({
831
+ requestedProfile = "auto",
832
+ requestedAccessMode,
833
+ noOpen = false
834
+ } = {}) {
790
835
  if (!["auto", "light", "standard"].includes(requestedProfile)) {
791
836
  throw new Error("Invalid SpaceApp profile for Docker group re-entry.");
792
837
  }
@@ -799,6 +844,7 @@ function linuxReentryCommand({ requestedProfile = "auto", noOpen = false } = {})
799
844
  "install",
800
845
  "--profile",
801
846
  requestedProfile,
847
+ ...installAccessArgs(requestedAccessMode, "Docker group re-entry"),
802
848
  ...(noOpen ? ["--no-open"] : [])
803
849
  ].map(shellQuote).join(" ");
804
850
  }
@@ -807,6 +853,7 @@ export function windowsResumeScript({
807
853
  executable = process.execPath,
808
854
  entrypoint = fileURLToPath(new URL("../bin/spaceapp.mjs", import.meta.url)),
809
855
  requestedProfile = "auto",
856
+ requestedAccessMode,
810
857
  noOpen = false
811
858
  } = {}) {
812
859
  if (!["auto", "light", "standard"].includes(requestedProfile)) {
@@ -830,6 +877,7 @@ export function windowsResumeScript({
830
877
  "install",
831
878
  "--profile",
832
879
  requestedProfile,
880
+ ...installAccessArgs(requestedAccessMode, "Windows resume"),
833
881
  ...(noOpen ? ["--no-open"] : [])
834
882
  ].join(" ");
835
883
  return [
@@ -847,6 +895,7 @@ export function windowsResumeScript({
847
895
  async function scheduleWindowsInstallResume({
848
896
  root,
849
897
  requestedProfile = "auto",
898
+ requestedAccessMode,
850
899
  noOpen = false,
851
900
  execute,
852
901
  stdin,
@@ -865,7 +914,7 @@ async function scheduleWindowsInstallResume({
865
914
  await mkdir(root, { recursive: true, mode: 0o700 });
866
915
  await writeFile(
867
916
  resumePath,
868
- windowsResumeScript({ requestedProfile, noOpen }),
917
+ windowsResumeScript({ requestedProfile, requestedAccessMode, noOpen }),
869
918
  { encoding: "utf8", mode: 0o600 }
870
919
  );
871
920
  const code = await execute(
@@ -882,6 +931,16 @@ async function scheduleWindowsInstallResume({
882
931
  return code;
883
932
  }
884
933
 
934
+ function installAccessArgs(requestedAccessMode, context) {
935
+ if (requestedAccessMode === undefined) {
936
+ return [];
937
+ }
938
+ if (requestedAccessMode === "isolated" || requestedAccessMode === "host-root") {
939
+ return ["--access", requestedAccessMode];
940
+ }
941
+ throw new Error(`Invalid SpaceApp access mode for ${context}.`);
942
+ }
943
+
885
944
  function shellQuote(value) {
886
945
  return `'${String(value).replaceAll("'", "'\\''")}'`;
887
946
  }