run-spaceapp 0.1.10 → 0.1.12

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.10",
3
+ "version": "0.1.12",
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": "4443cc6ad69b1e6057912df4060a58c1596274b1"
49
+ "gitHead": "55dff6314db5ded932361275b85a5a2008920128"
49
50
  }
package/src/cli.mjs CHANGED
@@ -33,11 +33,11 @@ const SETUP_STATUS_TIMEOUT_MS = 10_000;
33
33
  const trustedCommands = new Set([
34
34
  "codesign",
35
35
  "docker",
36
- "explorer.exe",
37
36
  "hdiutil",
38
37
  "open",
39
38
  "powershell.exe",
40
39
  "sg",
40
+ "shutdown.exe",
41
41
  "spctl",
42
42
  "sudo",
43
43
  "winget.exe",
@@ -45,7 +45,9 @@ const trustedCommands = new Set([
45
45
  "xdg-open"
46
46
  ]);
47
47
  const trustedEnvironmentNames = new Set([
48
- "SPACEAPP_DOCKER_INSTALLER_PATH"
48
+ "SPACEAPP_DOCKER_INSTALLER_PATH",
49
+ "SPACEAPP_OPEN_URL",
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) {
@@ -810,12 +812,12 @@ function spawnTrustedCommand(spec, options) {
810
812
  switch (spec.command) {
811
813
  case "codesign": return spawn("codesign", args, options);
812
814
  case "docker": return spawn("docker", args, options);
813
- case "explorer.exe": return spawn("explorer.exe", args, options);
814
815
  case "hdiutil": return spawn("hdiutil", args, options);
815
816
  case "open": return spawn("open", args, options);
816
817
  case "powershell.exe":
817
818
  return spawn("powershell.exe", windowsPowerShellArgs(spec.operation), options);
818
819
  case "sg": return spawn("sg", args, options);
820
+ case "shutdown.exe": return spawn("shutdown.exe", args, options);
819
821
  case "spctl": return spawn("spctl", args, options);
820
822
  case "sudo": return spawn("sudo", args, options);
821
823
  case "winget.exe": return spawn("winget.exe", args, options);
@@ -844,7 +846,11 @@ function openBrowser(url, platform, execute, io) {
844
846
  return execute({ command: "open", args: [url] }, io);
845
847
  }
846
848
  if (platform === "win32") {
847
- return execute({ command: "explorer.exe", args: [url] }, io);
849
+ return execute({
850
+ command: "powershell.exe",
851
+ operation: "open-spaceapp-browser",
852
+ env: { SPACEAPP_OPEN_URL: url }
853
+ }, io);
848
854
  }
849
855
  return execute({ command: "xdg-open", args: [url] }, io);
850
856
  }
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 }",
@@ -59,6 +73,17 @@ export function windowsPowerShellArgs(operation) {
59
73
  "$path = $env:SPACEAPP_DOCKER_DESKTOP_PATH",
60
74
  "if ([string]::IsNullOrWhiteSpace($path)) { exit 1 }",
61
75
  "Start-Process -FilePath $path"
76
+ ].join("; "),
77
+ "open-spaceapp-browser": [
78
+ "$ErrorActionPreference = 'Stop'",
79
+ "$url = $env:SPACEAPP_OPEN_URL",
80
+ "$uri = $null",
81
+ "if ([string]::IsNullOrWhiteSpace($url) -or -not [Uri]::TryCreate($url, [UriKind]::Absolute, [ref]$uri)) { exit 1 }",
82
+ "if ($uri.Scheme -ne 'http' -or @('127.0.0.1','0.0.0.0') -notcontains $uri.Host -or $uri.Port -lt 1024 -or $uri.Port -gt 65535) { exit 1 }",
83
+ "$roots = @([Environment]::GetEnvironmentVariable('ProgramFiles(x86)'), [Environment]::GetEnvironmentVariable('ProgramFiles'), [Environment]::GetEnvironmentVariable('LOCALAPPDATA')) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }",
84
+ "$edge = $roots | ForEach-Object { Join-Path $_ 'Microsoft\\Edge\\Application\\msedge.exe' } | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1",
85
+ "if ($edge) { Start-Process -FilePath $edge -ArgumentList @('--no-first-run','--no-default-browser-check',$url); exit 0 }",
86
+ "Start-Process -FilePath 'explorer.exe' -ArgumentList @($url)"
62
87
  ].join("; ")
63
88
  };
64
89
  if (!Object.hasOwn(scripts, operation)) {
@@ -101,7 +126,9 @@ export async function ensureDockerAvailable({
101
126
  pathExists = fileExists,
102
127
  sleep = wait,
103
128
  osRelease,
104
- installArgs
129
+ installArgs,
130
+ scheduleResume = scheduleWindowsInstallResume,
131
+ confirmRestart = confirmWindowsRestart
105
132
  }) {
106
133
  if (await dockerIsReady(execute)) {
107
134
  return { code: 0, reexecuted: false };
@@ -117,7 +144,10 @@ export async function ensureDockerAvailable({
117
144
  launch,
118
145
  download,
119
146
  pathExists,
120
- sleep
147
+ sleep,
148
+ installArgs,
149
+ scheduleResume,
150
+ confirmRestart
121
151
  });
122
152
  }
123
153
  if (platform === "darwin") {
@@ -158,7 +188,10 @@ async function ensureWindowsDocker({
158
188
  launch,
159
189
  download,
160
190
  pathExists,
161
- sleep
191
+ sleep,
192
+ installArgs,
193
+ scheduleResume,
194
+ confirmRestart
162
195
  }) {
163
196
  const paths = windowsDockerPaths(env);
164
197
  let application = await firstExisting(paths.applications, pathExists);
@@ -172,6 +205,7 @@ async function ensureWindowsDocker({
172
205
  }
173
206
  }
174
207
 
208
+ let wslInstalledNow = false;
175
209
  const wslCode = await execute(
176
210
  { command: "wsl.exe", args: ["--version"] },
177
211
  { stdin, stdout: null, stderr: null }
@@ -191,16 +225,64 @@ async function ensureWindowsDocker({
191
225
  );
192
226
  return { code: installWslCode, reexecuted: false };
193
227
  }
194
- const installedWslCode = await execute(
195
- { command: "wsl.exe", args: ["--version"] },
196
- { stdin, stdout: null, stderr: null }
228
+ wslInstalledNow = true;
229
+ }
230
+
231
+ const restartPendingCode = await execute(
232
+ { command: "powershell.exe", operation: "windows-restart-pending" },
233
+ { stdin, stdout: null, stderr: null }
234
+ );
235
+ if (wslInstalledNow || restartPendingCode === 0) {
236
+ const scheduleCode = await scheduleResume({
237
+ ...installArgs,
238
+ execute,
239
+ stdin,
240
+ stdout,
241
+ stderr
242
+ });
243
+ if (scheduleCode !== 0) {
244
+ stderr.write(
245
+ "Windows must restart to finish WSL2, but SpaceApp could not schedule automatic continuation.\n" +
246
+ "Restart Windows, then run the same SpaceApp command again.\n"
247
+ );
248
+ return { code: scheduleCode, reexecuted: false };
249
+ }
250
+ stdout.write(
251
+ "Windows must restart to finish enabling WSL2.\n" +
252
+ "SpaceApp is scheduled to resume automatically after you sign in.\n"
253
+ );
254
+ const restartNow = await confirmRestart({
255
+ stdin,
256
+ stdout,
257
+ question: "Restart Windows in 15 seconds and continue SpaceApp after sign-in? [y/N] "
258
+ });
259
+ if (!restartNow) {
260
+ stdout.write(
261
+ "Restart Windows when ready. SpaceApp will resume automatically the next time you sign in.\n"
262
+ );
263
+ return { code: 0, reexecuted: true };
264
+ }
265
+ const restartCode = await execute(
266
+ {
267
+ command: "shutdown.exe",
268
+ args: [
269
+ "/r",
270
+ "/t", "15",
271
+ "/c", "SpaceApp will continue automatically after you sign in."
272
+ ]
273
+ },
274
+ { stdin, stdout, stderr }
197
275
  );
198
- if (installedWslCode !== 0) {
276
+ if (restartCode !== 0) {
199
277
  stderr.write(
200
- "Windows must restart to finish enabling WSL2. Restart Windows, then run the same SpaceApp command again.\n"
278
+ "Windows restart could not be scheduled. Restart Windows manually; SpaceApp will resume after sign-in.\n"
201
279
  );
202
- return { code: 1, reexecuted: false };
280
+ return { code: restartCode, reexecuted: false };
203
281
  }
282
+ stdout.write(
283
+ "Windows will restart in 15 seconds. Save your work; run \"shutdown /a\" to cancel the countdown.\n"
284
+ );
285
+ return { code: 0, reexecuted: true };
204
286
  }
205
287
 
206
288
  if (!application) {
@@ -510,6 +592,10 @@ async function confirmDesktopInstall({ platformName, stdin, stdout }) {
510
592
  });
511
593
  }
512
594
 
595
+ async function confirmWindowsRestart({ stdin, stdout, question }) {
596
+ return confirmQuestion({ stdin, stdout, question });
597
+ }
598
+
513
599
  async function confirmQuestion({ stdin, stdout, question }) {
514
600
  const readline = createInterface({ input: stdin, output: stdout, terminal: Boolean(stdin.isTTY) });
515
601
  try {
@@ -716,10 +802,93 @@ function linuxReentryCommand({ requestedProfile = "auto", noOpen = false } = {})
716
802
  ].map(shellQuote).join(" ");
717
803
  }
718
804
 
805
+ export function windowsResumeScript({
806
+ executable = process.execPath,
807
+ entrypoint = fileURLToPath(new URL("../bin/spaceapp.mjs", import.meta.url)),
808
+ requestedProfile = "auto",
809
+ noOpen = false
810
+ } = {}) {
811
+ if (!["auto", "light", "standard"].includes(requestedProfile)) {
812
+ throw new Error("Invalid SpaceApp profile for Windows resume.");
813
+ }
814
+ if (typeof noOpen !== "boolean") {
815
+ throw new Error("Invalid SpaceApp browser option for Windows resume.");
816
+ }
817
+ for (const [name, value] of [["executable", executable], ["entrypoint", entrypoint]]) {
818
+ if (
819
+ typeof value !== "string" ||
820
+ !win32.isAbsolute(value) ||
821
+ /["\r\n%]/.test(value)
822
+ ) {
823
+ throw new Error(`Invalid SpaceApp Windows resume ${name}.`);
824
+ }
825
+ }
826
+ const command = [
827
+ windowsBatchQuote(executable),
828
+ windowsBatchQuote(entrypoint),
829
+ "install",
830
+ "--profile",
831
+ requestedProfile,
832
+ ...(noOpen ? ["--no-open"] : [])
833
+ ].join(" ");
834
+ return [
835
+ "@echo off",
836
+ "setlocal",
837
+ command,
838
+ 'set "SPACEAPP_RESUME_EXIT=%ERRORLEVEL%"',
839
+ 'if "%SPACEAPP_RESUME_EXIT%"=="0" del /f /q "%~f0"',
840
+ 'if not "%SPACEAPP_RESUME_EXIT%"=="0" echo SpaceApp setup needs attention. Review the error above, then run this file again.',
841
+ "exit /b %SPACEAPP_RESUME_EXIT%",
842
+ ""
843
+ ].join("\r\n");
844
+ }
845
+
846
+ async function scheduleWindowsInstallResume({
847
+ root,
848
+ requestedProfile = "auto",
849
+ noOpen = false,
850
+ execute,
851
+ stdin,
852
+ stdout,
853
+ stderr
854
+ } = {}) {
855
+ if (
856
+ typeof root !== "string" ||
857
+ !win32.isAbsolute(root) ||
858
+ /["\r\n%]/.test(root)
859
+ ) {
860
+ stderr?.write("SpaceApp could not determine a safe Windows resume path.\n");
861
+ return 1;
862
+ }
863
+ const resumePath = win32.join(root, "resume-install.cmd");
864
+ await mkdir(root, { recursive: true, mode: 0o700 });
865
+ await writeFile(
866
+ resumePath,
867
+ windowsResumeScript({ requestedProfile, noOpen }),
868
+ { encoding: "utf8", mode: 0o600 }
869
+ );
870
+ const code = await execute(
871
+ {
872
+ command: "powershell.exe",
873
+ operation: "register-spaceapp-resume",
874
+ env: { SPACEAPP_RESUME_SCRIPT_PATH: resumePath }
875
+ },
876
+ { stdin, stdout: null, stderr }
877
+ );
878
+ if (code !== 0) {
879
+ await rm(resumePath, { force: true });
880
+ }
881
+ return code;
882
+ }
883
+
719
884
  function shellQuote(value) {
720
885
  return `'${String(value).replaceAll("'", "'\\''")}'`;
721
886
  }
722
887
 
888
+ function windowsBatchQuote(value) {
889
+ return `"${String(value)}"`;
890
+ }
891
+
723
892
  async function waitForDocker({ execute, sleep, attempts = 90 }) {
724
893
  for (let attempt = 0; attempt <= attempts; attempt += 1) {
725
894
  if (await dockerIsReady(execute)) {