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 +8 -14
- package/package.json +7 -2
- package/src/cli.mjs +167 -32
- package/src/index.mjs +4 -4
- package/src/package-info.mjs +21 -1
- package/src/prerequisites.mjs +29 -16
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
|
|
28
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
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": "
|
|
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
|
|
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(`${
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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, {
|
|
324
|
+
const result = await prepareInstallation(root, {
|
|
325
|
+
version: runtimeVersion,
|
|
326
|
+
profile,
|
|
327
|
+
accessMode
|
|
328
|
+
});
|
|
301
329
|
|
|
302
|
-
stdout.write(`Launcher version: ${
|
|
330
|
+
stdout.write(`Launcher version: ${launcherVersion}\n`);
|
|
303
331
|
stdout.write(
|
|
304
|
-
`
|
|
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} (${
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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, {
|
|
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(
|
|
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
|
-
|
|
421
|
-
|
|
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 <
|
|
743
|
-
throw new Error("Owner password must be at least
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
|
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 =
|
|
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)}
|
|
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)}
|
|
148
|
+
detail: `${formatGibibytes(freeDiskBytes)} GiB available; ${formatGibibytes(MIN_INSTALL_FREE_DISK_BYTES)} GiB required`
|
|
149
149
|
}
|
|
150
150
|
];
|
|
151
151
|
}
|
package/src/package-info.mjs
CHANGED
|
@@ -4,7 +4,27 @@ const manifest = JSON.parse(
|
|
|
4
4
|
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
5
5
|
);
|
|
6
6
|
|
|
7
|
-
|
|
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";
|
package/src/prerequisites.mjs
CHANGED
|
@@ -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",
|
|
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: [
|
|
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(
|
|
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
|
-
|
|
557
|
-
|
|
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
|
-
`
|
|
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
|
|