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 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
- sudo npm install -g run-spaceapp && spaceapp install
8
+ npx --yes run-spaceapp install
9
9
  ```
10
10
 
11
- macOS:
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 && spaceapp install
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. On Docker Desktop's first launch, select **Skip** in the top-right of
32
- the **Welcome to Docker** window (or sign in), accept any remaining Docker
33
- prompt, and keep the terminal open. SpaceApp waits for up to ten minutes and
34
- continues automatically when Docker is ready. If Windows requests one restart,
35
- rerun `spaceapp install` afterward and installation continues safely.
36
-
37
- The launcher selects the `light` profile below 12 GiB RAM and the `standard`
38
- profile otherwise; override this with `--profile light` or
39
- `--profile standard`.
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.9",
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": "bca19d7f71db3bfb097c4c00146b4f51957494d9"
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 totalMemoryBytes < AUTO_LIGHT_MEMORY_THRESHOLD_BYTES ? "light" : "standard";
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 = "standard" }) {
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: profile ?? "standard" });
496
+ config = createDefaultConfig({ version, profile: resolvedProfile ?? "light" });
495
497
  }
496
- if (profile !== undefined) {
497
- const resolvedProfile = resolveInstallProfile(profile, AUTO_LIGHT_MEMORY_THRESHOLD_BYTES);
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);
@@ -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
- const installedWslCode = await execute(
195
- { command: "wsl.exe", args: ["--version"] },
196
- { stdin, stdout: null, stderr: null }
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
- if (installedWslCode !== 0) {
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 must restart to finish enabling WSL2. Restart Windows, then run the same SpaceApp command again.\n"
267
+ "Windows restart could not be scheduled. Restart Windows manually; SpaceApp will resume after sign-in.\n"
201
268
  );
202
- return { code: 1, reexecuted: false };
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)) {