run-spaceapp 0.1.9 → 0.1.11
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 +18 -19
- package/package.json +4 -3
- package/src/cli.mjs +5 -2
- package/src/index.mjs +14 -6
- package/src/prerequisites.mjs +168 -10
package/README.md
CHANGED
|
@@ -2,22 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Cross-platform Docker launcher for SpaceApp.
|
|
4
4
|
|
|
5
|
-
Linux:
|
|
5
|
+
Run the same command in Linux, macOS, or Windows 11 Command Prompt:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
npx --yes run-spaceapp install
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
To keep a global `spaceapp` command for later operations, install it
|
|
12
|
+
separately:
|
|
12
13
|
|
|
13
14
|
```bash
|
|
14
|
-
npm install -g run-spaceapp
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Windows 11 PowerShell:
|
|
18
|
-
|
|
19
|
-
```powershell
|
|
20
|
-
npm install -g run-spaceapp; if ($LASTEXITCODE -eq 0) { spaceapp install }
|
|
15
|
+
npm install -g run-spaceapp
|
|
21
16
|
```
|
|
22
17
|
|
|
23
18
|
The launcher requires Node.js 20.11 or newer, 4 CPUs, 8 GB RAM, and 15 GiB free
|
|
@@ -28,15 +23,19 @@ images. Windows prefers Windows Package Manager's hash-pinned Docker Desktop
|
|
|
28
23
|
manifest and retains a signed direct-download fallback. Docker Desktop license
|
|
29
24
|
acceptance and Linux `docker` group membership require confirmation inside the
|
|
30
25
|
same command. On Windows, approve any UAC prompt required by WSL2 or Docker
|
|
31
|
-
Desktop.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
Desktop. Use the **Command Prompt** profile because a restricted PowerShell
|
|
27
|
+
policy can block npm's `npx.ps1` before SpaceApp starts. On Docker Desktop's
|
|
28
|
+
first launch, select **Skip** in the top-right of the **Welcome to Docker**
|
|
29
|
+
window (or sign in), accept any remaining Docker prompt, and keep the terminal
|
|
30
|
+
open. SpaceApp waits for up to ten minutes and continues automatically when
|
|
31
|
+
Docker is ready. If WSL2 requires one restart, SpaceApp registers a one-time
|
|
32
|
+
resume and asks before scheduling it. Sign back in after Windows restarts; the
|
|
33
|
+
same install continues automatically in Command Prompt without entering a
|
|
34
|
+
second command.
|
|
35
|
+
|
|
36
|
+
The launcher defaults to the `light` profile on every system. Select
|
|
37
|
+
`--profile standard` explicitly when managed Chromium is required and the host
|
|
38
|
+
has the recommended resources.
|
|
40
39
|
|
|
41
40
|
Light mode retains every bundled CLI and the core data/workflow services while
|
|
42
41
|
omitting managed Chromium. Resource limits are maximums, not immediate RAM or
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run-spaceapp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Cross-platform Docker launcher for the SpaceApp self-hosted agent workspace",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"spaceapp": "bin/spaceapp.mjs"
|
|
8
|
+
"spaceapp": "bin/spaceapp.mjs",
|
|
9
|
+
"run-spaceapp": "bin/spaceapp.mjs"
|
|
9
10
|
},
|
|
10
11
|
"exports": {
|
|
11
12
|
".": "./src/index.mjs"
|
|
@@ -45,5 +46,5 @@
|
|
|
45
46
|
"access": "public",
|
|
46
47
|
"provenance": true
|
|
47
48
|
},
|
|
48
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "c53e6dd08441f3a517f21f7b0ca6f82ec36a1761"
|
|
49
50
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -38,6 +38,7 @@ const trustedCommands = new Set([
|
|
|
38
38
|
"open",
|
|
39
39
|
"powershell.exe",
|
|
40
40
|
"sg",
|
|
41
|
+
"shutdown.exe",
|
|
41
42
|
"spctl",
|
|
42
43
|
"sudo",
|
|
43
44
|
"winget.exe",
|
|
@@ -45,7 +46,8 @@ const trustedCommands = new Set([
|
|
|
45
46
|
"xdg-open"
|
|
46
47
|
]);
|
|
47
48
|
const trustedEnvironmentNames = new Set([
|
|
48
|
-
"SPACEAPP_DOCKER_INSTALLER_PATH"
|
|
49
|
+
"SPACEAPP_DOCKER_INSTALLER_PATH",
|
|
50
|
+
"SPACEAPP_RESUME_SCRIPT_PATH"
|
|
49
51
|
]);
|
|
50
52
|
|
|
51
53
|
export async function run(argv, {
|
|
@@ -295,7 +297,7 @@ async function installCommand(args, {
|
|
|
295
297
|
stdout,
|
|
296
298
|
stderr,
|
|
297
299
|
execute,
|
|
298
|
-
installArgs: { requestedProfile, noOpen }
|
|
300
|
+
installArgs: { root, requestedProfile, noOpen }
|
|
299
301
|
});
|
|
300
302
|
if (prerequisiteResult.reexecuted || prerequisiteResult.code !== 0) {
|
|
301
303
|
if (prerequisiteResult.code !== 0) {
|
|
@@ -816,6 +818,7 @@ function spawnTrustedCommand(spec, options) {
|
|
|
816
818
|
case "powershell.exe":
|
|
817
819
|
return spawn("powershell.exe", windowsPowerShellArgs(spec.operation), options);
|
|
818
820
|
case "sg": return spawn("sg", args, options);
|
|
821
|
+
case "shutdown.exe": return spawn("shutdown.exe", args, options);
|
|
819
822
|
case "spctl": return spawn("spctl", args, options);
|
|
820
823
|
case "sudo": return spawn("sudo", args, options);
|
|
821
824
|
case "winget.exe": return spawn("winget.exe", args, options);
|
package/src/index.mjs
CHANGED
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
} from "./string-utils.mjs";
|
|
21
21
|
|
|
22
22
|
const CONFIG_SCHEMA_VERSION = 2;
|
|
23
|
-
const AUTO_LIGHT_MEMORY_THRESHOLD_BYTES = 12 * 1024 ** 3;
|
|
24
23
|
const MIN_INSTALL_CPU_COUNT = 4;
|
|
25
24
|
const MIN_INSTALL_MEMORY_BYTES = 8_000_000_000;
|
|
26
25
|
const MIN_INSTALL_MEMORY_LABEL = "8 GB";
|
|
@@ -100,7 +99,7 @@ export function resolveInstallProfile(requestedProfile, totalMemoryBytes) {
|
|
|
100
99
|
if (!Number.isFinite(totalMemoryBytes) || totalMemoryBytes <= 0) {
|
|
101
100
|
throw new Error("Total system memory must be available for automatic profile selection.");
|
|
102
101
|
}
|
|
103
|
-
return
|
|
102
|
+
return "light";
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
export async function inspectSystemResources(root) {
|
|
@@ -140,7 +139,7 @@ export function installResourceChecks(resources) {
|
|
|
140
139
|
];
|
|
141
140
|
}
|
|
142
141
|
|
|
143
|
-
export function createDefaultConfig({ version, profile = "
|
|
142
|
+
export function createDefaultConfig({ version, profile = "light" }) {
|
|
144
143
|
assertVersion(version);
|
|
145
144
|
if (profile !== "light" && profile !== "standard") {
|
|
146
145
|
throw new Error("Default config requires a resolved light or standard profile.");
|
|
@@ -484,6 +483,9 @@ export async function initializeInstallation(root, {
|
|
|
484
483
|
mkdir(join(root, "backups"), { recursive: true, mode: 0o700 }),
|
|
485
484
|
mkdir(join(root, "secrets", "providers"), { recursive: true, mode: 0o700 })
|
|
486
485
|
]);
|
|
486
|
+
const resolvedProfile = profile === undefined
|
|
487
|
+
? undefined
|
|
488
|
+
: resolveInstallProfile(profile, totalmem());
|
|
487
489
|
let config;
|
|
488
490
|
try {
|
|
489
491
|
config = await loadConfig(root);
|
|
@@ -491,10 +493,16 @@ export async function initializeInstallation(root, {
|
|
|
491
493
|
if (error?.code !== "ENOENT") {
|
|
492
494
|
throw error;
|
|
493
495
|
}
|
|
494
|
-
config = createDefaultConfig({ version, profile:
|
|
496
|
+
config = createDefaultConfig({ version, profile: resolvedProfile ?? "light" });
|
|
495
497
|
}
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
+
if (config.version !== version) {
|
|
499
|
+
config = {
|
|
500
|
+
...config,
|
|
501
|
+
version,
|
|
502
|
+
previousVersion: config.version
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (resolvedProfile !== undefined) {
|
|
498
506
|
config = { ...config, profile: resolvedProfile };
|
|
499
507
|
}
|
|
500
508
|
await saveConfig(root, config);
|
package/src/prerequisites.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createWriteStream } from "node:fs";
|
|
2
|
-
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join, posix, win32 } from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
@@ -43,6 +43,20 @@ export function windowsPowerShellArgs(operation) {
|
|
|
43
43
|
"; exit $process.ExitCode",
|
|
44
44
|
"} catch { exit 1 }"
|
|
45
45
|
].join(" "),
|
|
46
|
+
"windows-restart-pending": [
|
|
47
|
+
"$pending = (Test-Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending') -or (Test-Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired')",
|
|
48
|
+
"if ($pending) { exit 0 }",
|
|
49
|
+
"exit 1"
|
|
50
|
+
].join("; "),
|
|
51
|
+
"register-spaceapp-resume": [
|
|
52
|
+
"$ErrorActionPreference = 'Stop'",
|
|
53
|
+
"$path = $env:SPACEAPP_RESUME_SCRIPT_PATH",
|
|
54
|
+
"if ([string]::IsNullOrWhiteSpace($path) -or -not (Test-Path -LiteralPath $path -PathType Leaf)) { exit 1 }",
|
|
55
|
+
"$runOnce = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce'",
|
|
56
|
+
"New-Item -Path $runOnce -Force | Out-Null",
|
|
57
|
+
"$command = 'cmd.exe /d /k call \"' + $path + '\"'",
|
|
58
|
+
"New-ItemProperty -Path $runOnce -Name 'SpaceAppResumeInstall' -Value $command -PropertyType String -Force | Out-Null"
|
|
59
|
+
].join("; "),
|
|
46
60
|
"verify-docker-installer": [
|
|
47
61
|
"$path = $env:SPACEAPP_DOCKER_INSTALLER_PATH",
|
|
48
62
|
"if ([string]::IsNullOrWhiteSpace($path)) { exit 1 }",
|
|
@@ -101,7 +115,9 @@ export async function ensureDockerAvailable({
|
|
|
101
115
|
pathExists = fileExists,
|
|
102
116
|
sleep = wait,
|
|
103
117
|
osRelease,
|
|
104
|
-
installArgs
|
|
118
|
+
installArgs,
|
|
119
|
+
scheduleResume = scheduleWindowsInstallResume,
|
|
120
|
+
confirmRestart = confirmWindowsRestart
|
|
105
121
|
}) {
|
|
106
122
|
if (await dockerIsReady(execute)) {
|
|
107
123
|
return { code: 0, reexecuted: false };
|
|
@@ -117,7 +133,10 @@ export async function ensureDockerAvailable({
|
|
|
117
133
|
launch,
|
|
118
134
|
download,
|
|
119
135
|
pathExists,
|
|
120
|
-
sleep
|
|
136
|
+
sleep,
|
|
137
|
+
installArgs,
|
|
138
|
+
scheduleResume,
|
|
139
|
+
confirmRestart
|
|
121
140
|
});
|
|
122
141
|
}
|
|
123
142
|
if (platform === "darwin") {
|
|
@@ -158,7 +177,10 @@ async function ensureWindowsDocker({
|
|
|
158
177
|
launch,
|
|
159
178
|
download,
|
|
160
179
|
pathExists,
|
|
161
|
-
sleep
|
|
180
|
+
sleep,
|
|
181
|
+
installArgs,
|
|
182
|
+
scheduleResume,
|
|
183
|
+
confirmRestart
|
|
162
184
|
}) {
|
|
163
185
|
const paths = windowsDockerPaths(env);
|
|
164
186
|
let application = await firstExisting(paths.applications, pathExists);
|
|
@@ -172,6 +194,7 @@ async function ensureWindowsDocker({
|
|
|
172
194
|
}
|
|
173
195
|
}
|
|
174
196
|
|
|
197
|
+
let wslInstalledNow = false;
|
|
175
198
|
const wslCode = await execute(
|
|
176
199
|
{ command: "wsl.exe", args: ["--version"] },
|
|
177
200
|
{ stdin, stdout: null, stderr: null }
|
|
@@ -191,16 +214,64 @@ async function ensureWindowsDocker({
|
|
|
191
214
|
);
|
|
192
215
|
return { code: installWslCode, reexecuted: false };
|
|
193
216
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
217
|
+
wslInstalledNow = true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const restartPendingCode = await execute(
|
|
221
|
+
{ command: "powershell.exe", operation: "windows-restart-pending" },
|
|
222
|
+
{ stdin, stdout: null, stderr: null }
|
|
223
|
+
);
|
|
224
|
+
if (wslInstalledNow || restartPendingCode === 0) {
|
|
225
|
+
const scheduleCode = await scheduleResume({
|
|
226
|
+
...installArgs,
|
|
227
|
+
execute,
|
|
228
|
+
stdin,
|
|
229
|
+
stdout,
|
|
230
|
+
stderr
|
|
231
|
+
});
|
|
232
|
+
if (scheduleCode !== 0) {
|
|
233
|
+
stderr.write(
|
|
234
|
+
"Windows must restart to finish WSL2, but SpaceApp could not schedule automatic continuation.\n" +
|
|
235
|
+
"Restart Windows, then run the same SpaceApp command again.\n"
|
|
236
|
+
);
|
|
237
|
+
return { code: scheduleCode, reexecuted: false };
|
|
238
|
+
}
|
|
239
|
+
stdout.write(
|
|
240
|
+
"Windows must restart to finish enabling WSL2.\n" +
|
|
241
|
+
"SpaceApp is scheduled to resume automatically after you sign in.\n"
|
|
197
242
|
);
|
|
198
|
-
|
|
243
|
+
const restartNow = await confirmRestart({
|
|
244
|
+
stdin,
|
|
245
|
+
stdout,
|
|
246
|
+
question: "Restart Windows in 15 seconds and continue SpaceApp after sign-in? [y/N] "
|
|
247
|
+
});
|
|
248
|
+
if (!restartNow) {
|
|
249
|
+
stdout.write(
|
|
250
|
+
"Restart Windows when ready. SpaceApp will resume automatically the next time you sign in.\n"
|
|
251
|
+
);
|
|
252
|
+
return { code: 0, reexecuted: true };
|
|
253
|
+
}
|
|
254
|
+
const restartCode = await execute(
|
|
255
|
+
{
|
|
256
|
+
command: "shutdown.exe",
|
|
257
|
+
args: [
|
|
258
|
+
"/r",
|
|
259
|
+
"/t", "15",
|
|
260
|
+
"/c", "SpaceApp will continue automatically after you sign in."
|
|
261
|
+
]
|
|
262
|
+
},
|
|
263
|
+
{ stdin, stdout, stderr }
|
|
264
|
+
);
|
|
265
|
+
if (restartCode !== 0) {
|
|
199
266
|
stderr.write(
|
|
200
|
-
"Windows
|
|
267
|
+
"Windows restart could not be scheduled. Restart Windows manually; SpaceApp will resume after sign-in.\n"
|
|
201
268
|
);
|
|
202
|
-
return { code:
|
|
269
|
+
return { code: restartCode, reexecuted: false };
|
|
203
270
|
}
|
|
271
|
+
stdout.write(
|
|
272
|
+
"Windows will restart in 15 seconds. Save your work; run \"shutdown /a\" to cancel the countdown.\n"
|
|
273
|
+
);
|
|
274
|
+
return { code: 0, reexecuted: true };
|
|
204
275
|
}
|
|
205
276
|
|
|
206
277
|
if (!application) {
|
|
@@ -510,6 +581,10 @@ async function confirmDesktopInstall({ platformName, stdin, stdout }) {
|
|
|
510
581
|
});
|
|
511
582
|
}
|
|
512
583
|
|
|
584
|
+
async function confirmWindowsRestart({ stdin, stdout, question }) {
|
|
585
|
+
return confirmQuestion({ stdin, stdout, question });
|
|
586
|
+
}
|
|
587
|
+
|
|
513
588
|
async function confirmQuestion({ stdin, stdout, question }) {
|
|
514
589
|
const readline = createInterface({ input: stdin, output: stdout, terminal: Boolean(stdin.isTTY) });
|
|
515
590
|
try {
|
|
@@ -716,10 +791,93 @@ function linuxReentryCommand({ requestedProfile = "auto", noOpen = false } = {})
|
|
|
716
791
|
].map(shellQuote).join(" ");
|
|
717
792
|
}
|
|
718
793
|
|
|
794
|
+
export function windowsResumeScript({
|
|
795
|
+
executable = process.execPath,
|
|
796
|
+
entrypoint = fileURLToPath(new URL("../bin/spaceapp.mjs", import.meta.url)),
|
|
797
|
+
requestedProfile = "auto",
|
|
798
|
+
noOpen = false
|
|
799
|
+
} = {}) {
|
|
800
|
+
if (!["auto", "light", "standard"].includes(requestedProfile)) {
|
|
801
|
+
throw new Error("Invalid SpaceApp profile for Windows resume.");
|
|
802
|
+
}
|
|
803
|
+
if (typeof noOpen !== "boolean") {
|
|
804
|
+
throw new Error("Invalid SpaceApp browser option for Windows resume.");
|
|
805
|
+
}
|
|
806
|
+
for (const [name, value] of [["executable", executable], ["entrypoint", entrypoint]]) {
|
|
807
|
+
if (
|
|
808
|
+
typeof value !== "string" ||
|
|
809
|
+
!win32.isAbsolute(value) ||
|
|
810
|
+
/["\r\n%]/.test(value)
|
|
811
|
+
) {
|
|
812
|
+
throw new Error(`Invalid SpaceApp Windows resume ${name}.`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const command = [
|
|
816
|
+
windowsBatchQuote(executable),
|
|
817
|
+
windowsBatchQuote(entrypoint),
|
|
818
|
+
"install",
|
|
819
|
+
"--profile",
|
|
820
|
+
requestedProfile,
|
|
821
|
+
...(noOpen ? ["--no-open"] : [])
|
|
822
|
+
].join(" ");
|
|
823
|
+
return [
|
|
824
|
+
"@echo off",
|
|
825
|
+
"setlocal",
|
|
826
|
+
command,
|
|
827
|
+
'set "SPACEAPP_RESUME_EXIT=%ERRORLEVEL%"',
|
|
828
|
+
'if "%SPACEAPP_RESUME_EXIT%"=="0" del /f /q "%~f0"',
|
|
829
|
+
'if not "%SPACEAPP_RESUME_EXIT%"=="0" echo SpaceApp setup needs attention. Review the error above, then run this file again.',
|
|
830
|
+
"exit /b %SPACEAPP_RESUME_EXIT%",
|
|
831
|
+
""
|
|
832
|
+
].join("\r\n");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function scheduleWindowsInstallResume({
|
|
836
|
+
root,
|
|
837
|
+
requestedProfile = "auto",
|
|
838
|
+
noOpen = false,
|
|
839
|
+
execute,
|
|
840
|
+
stdin,
|
|
841
|
+
stdout,
|
|
842
|
+
stderr
|
|
843
|
+
} = {}) {
|
|
844
|
+
if (
|
|
845
|
+
typeof root !== "string" ||
|
|
846
|
+
!win32.isAbsolute(root) ||
|
|
847
|
+
/["\r\n%]/.test(root)
|
|
848
|
+
) {
|
|
849
|
+
stderr?.write("SpaceApp could not determine a safe Windows resume path.\n");
|
|
850
|
+
return 1;
|
|
851
|
+
}
|
|
852
|
+
const resumePath = win32.join(root, "resume-install.cmd");
|
|
853
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
854
|
+
await writeFile(
|
|
855
|
+
resumePath,
|
|
856
|
+
windowsResumeScript({ requestedProfile, noOpen }),
|
|
857
|
+
{ encoding: "utf8", mode: 0o600 }
|
|
858
|
+
);
|
|
859
|
+
const code = await execute(
|
|
860
|
+
{
|
|
861
|
+
command: "powershell.exe",
|
|
862
|
+
operation: "register-spaceapp-resume",
|
|
863
|
+
env: { SPACEAPP_RESUME_SCRIPT_PATH: resumePath }
|
|
864
|
+
},
|
|
865
|
+
{ stdin, stdout: null, stderr }
|
|
866
|
+
);
|
|
867
|
+
if (code !== 0) {
|
|
868
|
+
await rm(resumePath, { force: true });
|
|
869
|
+
}
|
|
870
|
+
return code;
|
|
871
|
+
}
|
|
872
|
+
|
|
719
873
|
function shellQuote(value) {
|
|
720
874
|
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
721
875
|
}
|
|
722
876
|
|
|
877
|
+
function windowsBatchQuote(value) {
|
|
878
|
+
return `"${String(value)}"`;
|
|
879
|
+
}
|
|
880
|
+
|
|
723
881
|
async function waitForDocker({ execute, sleep, attempts = 90 }) {
|
|
724
882
|
for (let attempt = 0; attempt <= attempts; attempt += 1) {
|
|
725
883
|
if (await dockerIsReady(execute)) {
|