run-spaceapp 0.1.12 → 0.1.14
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 +27 -15
- package/package.json +2 -2
- package/src/cli.mjs +176 -96
- package/src/index.mjs +31 -11
- package/src/prerequisites.mjs +5 -4
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Cross-platform Docker launcher for SpaceApp.
|
|
|
5
5
|
Run the same command in Linux, macOS, or Windows 11 Command Prompt:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx --yes run-spaceapp install
|
|
8
|
+
npx --yes run-spaceapp@latest install
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
To keep a global `spaceapp` command for later operations, install it
|
|
@@ -16,10 +16,12 @@ npm install -g run-spaceapp
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
The launcher requires Node.js 20.11 or newer, 4 CPUs, 8 GB RAM, and 15 GiB free
|
|
19
|
-
disk.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
disk. Keep `@latest` in the command so npm resolves the current release instead
|
|
20
|
+
of matching an existing local launcher. If Docker is missing, the universal
|
|
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
|
|
24
|
+
Windows Package Manager's hash-pinned Docker Desktop
|
|
23
25
|
manifest and retains a signed direct-download fallback. Docker Desktop license
|
|
24
26
|
acceptance and Linux `docker` group membership require confirmation inside the
|
|
25
27
|
same command. On Windows, approve any UAC prompt required by WSL2 or Docker
|
|
@@ -33,9 +35,9 @@ resume and asks before scheduling it. Sign back in after Windows restarts; the
|
|
|
33
35
|
same install continues automatically in Command Prompt without entering a
|
|
34
36
|
second command.
|
|
35
37
|
|
|
36
|
-
The launcher defaults to the `light` profile on every system.
|
|
37
|
-
|
|
38
|
-
has the recommended resources.
|
|
38
|
+
The launcher defaults to the `light` profile on every system. Run
|
|
39
|
+
`npx --yes run-spaceapp@latest install --profile standard` explicitly when
|
|
40
|
+
managed Chromium is required and the host has the recommended resources.
|
|
39
41
|
|
|
40
42
|
Light mode retains every bundled CLI and the core data/workflow services while
|
|
41
43
|
omitting managed Chromium. Resource limits are maximums, not immediate RAM or
|
|
@@ -57,24 +59,34 @@ Claude Code is not redistributed in SpaceApp images. Install the reviewed
|
|
|
57
59
|
package as an explicit owner action with:
|
|
58
60
|
|
|
59
61
|
```bash
|
|
60
|
-
spaceapp provider install claude
|
|
62
|
+
npx --yes run-spaceapp@latest provider install claude
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
Run `spaceapp help` for the complete command list. Full
|
|
65
|
+
Run `npx --yes run-spaceapp@latest help` for the complete command list. Full
|
|
66
|
+
documentation:
|
|
64
67
|
|
|
65
68
|
- [Getting started](https://github.com/oll4com/spaceapp/blob/main/docs/getting-started.md)
|
|
66
69
|
- [CLI providers](https://github.com/oll4com/spaceapp/blob/main/docs/cli-providers.md)
|
|
67
70
|
- [Operations](https://github.com/oll4com/spaceapp/blob/main/docs/operations.md)
|
|
68
71
|
- [Security model](https://github.com/oll4com/spaceapp/blob/main/docs/security-model.md)
|
|
69
72
|
|
|
70
|
-
After the stack passes readiness checks,
|
|
73
|
+
After the stack passes readiness checks, the install command prints the fresh
|
|
71
74
|
15-minute token to paste into the browser's **One-time setup token** field. If
|
|
72
75
|
it expires before the owner is created, run:
|
|
73
76
|
|
|
74
77
|
```bash
|
|
75
|
-
spaceapp owner rotate-setup-token
|
|
78
|
+
npx --yes run-spaceapp@latest owner rotate-setup-token
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
Common follow-up commands use the same reliable launcher prefix:
|
|
82
|
+
|
|
83
|
+
```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
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Uninstall retains data by default and prints the separate
|
|
91
|
+
`npm uninstall -g run-spaceapp` command for removing an optional global
|
|
92
|
+
launcher.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run-spaceapp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Cross-platform Docker launcher for the SpaceApp self-hosted agent workspace",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"access": "public",
|
|
47
47
|
"provenance": true
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "f6956f38649503f6a9dd5c232d3d51d7bf2fa258"
|
|
50
50
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
4
6
|
import process from "node:process";
|
|
5
7
|
import {
|
|
6
8
|
addWorkspace,
|
|
9
|
+
commitInstallation,
|
|
7
10
|
composeCommand,
|
|
8
11
|
credentialProviders,
|
|
9
12
|
initializeInstallation,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
loadConfig,
|
|
13
16
|
removeCredential,
|
|
14
17
|
removeWorkspace,
|
|
18
|
+
prepareInstallation,
|
|
15
19
|
resolveInstallProfile,
|
|
16
20
|
resolveSpaceAppHome,
|
|
17
21
|
saveConfig,
|
|
@@ -30,6 +34,7 @@ const APPLICATION_READY_WAIT_MS = 3 * 60 * 1_000;
|
|
|
30
34
|
const APPLICATION_READY_POLL_MS = 2_000;
|
|
31
35
|
const APPLICATION_READY_MAX_ATTEMPTS = APPLICATION_READY_WAIT_MS / APPLICATION_READY_POLL_MS;
|
|
32
36
|
const SETUP_STATUS_TIMEOUT_MS = 10_000;
|
|
37
|
+
const UNIVERSAL_COMMAND = "npx --yes run-spaceapp@latest";
|
|
33
38
|
const trustedCommands = new Set([
|
|
34
39
|
"codesign",
|
|
35
40
|
"docker",
|
|
@@ -104,7 +109,9 @@ export async function run(argv, {
|
|
|
104
109
|
stdout.write(`One-time setup token: ${result.setupToken}\n`);
|
|
105
110
|
stdout.write("Store it temporarily; it expires after first owner setup.\n");
|
|
106
111
|
}
|
|
107
|
-
stdout.write(
|
|
112
|
+
stdout.write(
|
|
113
|
+
`Next: ${UNIVERSAL_COMMAND} doctor && ${UNIVERSAL_COMMAND} up && ${UNIVERSAL_COMMAND} open\n`
|
|
114
|
+
);
|
|
108
115
|
return 0;
|
|
109
116
|
}
|
|
110
117
|
|
|
@@ -243,10 +250,10 @@ export async function run(argv, {
|
|
|
243
250
|
}
|
|
244
251
|
return code;
|
|
245
252
|
}
|
|
246
|
-
throw new Error(
|
|
253
|
+
throw new Error(`Usage: ${UNIVERSAL_COMMAND} uninstall [--purge-data]`);
|
|
247
254
|
}
|
|
248
255
|
|
|
249
|
-
throw new Error(`Unknown command "${command}". Run "
|
|
256
|
+
throw new Error(`Unknown command "${command}". Run "${UNIVERSAL_COMMAND} help".`);
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
async function installCommand(args, {
|
|
@@ -268,8 +275,19 @@ async function installCommand(args, {
|
|
|
268
275
|
const { requestedProfile, noOpen } = parseInstallArgs(args);
|
|
269
276
|
const resources = await inspectResources(root);
|
|
270
277
|
const profile = resolveInstallProfile(requestedProfile, resources.totalMemoryBytes);
|
|
271
|
-
const
|
|
278
|
+
const existingConfig = await loadExistingInstallation(root);
|
|
279
|
+
const result = await prepareInstallation(root, { version, profile });
|
|
272
280
|
|
|
281
|
+
stdout.write(`Launcher version: ${version}\n`);
|
|
282
|
+
stdout.write(
|
|
283
|
+
`SpaceApp version: ${existingConfig?.version ?? "not installed"} -> ${result.config.version}\n`
|
|
284
|
+
);
|
|
285
|
+
stdout.write(
|
|
286
|
+
`Profile: ${existingConfig?.profile ?? "not configured"} -> ${result.config.profile}\n`
|
|
287
|
+
);
|
|
288
|
+
stdout.write(existingConfig
|
|
289
|
+
? "Preserved existing data, workspaces, credentials, secrets, and persistent Docker volumes.\n"
|
|
290
|
+
: "Future refreshes preserve data, workspaces, credentials, secrets, and persistent Docker volumes.\n");
|
|
273
291
|
stdout.write(
|
|
274
292
|
`Selected profile: ${profile} (${formatGigabytes(resources.totalMemoryBytes)} GB system memory detected).\n`
|
|
275
293
|
);
|
|
@@ -285,7 +303,9 @@ async function installCommand(args, {
|
|
|
285
303
|
inspectResources,
|
|
286
304
|
resources
|
|
287
305
|
});
|
|
288
|
-
stderr.write(
|
|
306
|
+
stderr.write(
|
|
307
|
+
`Installation stopped before downloading images. Fix the failed checks and run "${UNIVERSAL_COMMAND} install" again.\n`
|
|
308
|
+
);
|
|
289
309
|
return doctorCode;
|
|
290
310
|
}
|
|
291
311
|
|
|
@@ -301,7 +321,9 @@ async function installCommand(args, {
|
|
|
301
321
|
});
|
|
302
322
|
if (prerequisiteResult.reexecuted || prerequisiteResult.code !== 0) {
|
|
303
323
|
if (prerequisiteResult.code !== 0) {
|
|
304
|
-
stderr.write(
|
|
324
|
+
stderr.write(
|
|
325
|
+
`Installation stopped before downloading images. Fix the failed checks and run "${UNIVERSAL_COMMAND} install" again.\n`
|
|
326
|
+
);
|
|
305
327
|
}
|
|
306
328
|
return prerequisiteResult.code;
|
|
307
329
|
}
|
|
@@ -318,95 +340,129 @@ async function installCommand(args, {
|
|
|
318
340
|
dockerReady: true
|
|
319
341
|
});
|
|
320
342
|
if (doctorCode !== 0) {
|
|
321
|
-
stderr.write("Installation stopped before downloading images. Fix the failed checks and run the same command again.\n");
|
|
322
|
-
return doctorCode;
|
|
323
|
-
}
|
|
324
|
-
const pullCode = await executeWithDockerDiagnostics(
|
|
325
|
-
execute,
|
|
326
|
-
composeCommand("pull", root, { profile }),
|
|
327
|
-
{ stdin, stdout, stderr },
|
|
328
|
-
{ platform, stderr }
|
|
329
|
-
);
|
|
330
|
-
if (pullCode !== 0) return pullCode;
|
|
331
|
-
const upCode = await executeWithDockerDiagnostics(
|
|
332
|
-
execute,
|
|
333
|
-
composeCommand("up", root, { profile }),
|
|
334
|
-
{ stdin, stdout, stderr },
|
|
335
|
-
{ platform, stderr }
|
|
336
|
-
);
|
|
337
|
-
if (upCode !== 0) return upCode;
|
|
338
|
-
|
|
339
|
-
const url = `http://${result.config.bindHost}:${result.config.port}`;
|
|
340
|
-
stdout.write("Waiting for SpaceApp services to become ready...\n");
|
|
341
|
-
const ready = await waitForApplicationReady({
|
|
342
|
-
url,
|
|
343
|
-
request,
|
|
344
|
-
sleep
|
|
345
|
-
});
|
|
346
|
-
if (!ready) {
|
|
347
343
|
stderr.write(
|
|
348
|
-
|
|
349
|
-
'Run "spaceapp status" and "spaceapp logs", then run "spaceapp install" again.\n'
|
|
344
|
+
`Installation stopped before downloading images. Fix the failed checks and run "${UNIVERSAL_COMMAND} install" again.\n`
|
|
350
345
|
);
|
|
351
|
-
return
|
|
346
|
+
return doctorCode;
|
|
352
347
|
}
|
|
353
|
-
|
|
354
|
-
let setupStatus;
|
|
348
|
+
const stagedStateRoot = await mkdtemp(join(tmpdir(), "run-spaceapp-install-"));
|
|
355
349
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
350
|
+
await commitInstallation(stagedStateRoot, result.config);
|
|
351
|
+
const stagedComposeCommand = (action) =>
|
|
352
|
+
composeCommand(action, root, { profile, stateRoot: stagedStateRoot });
|
|
353
|
+
const pullCode = await executeWithDockerDiagnostics(
|
|
354
|
+
execute,
|
|
355
|
+
stagedComposeCommand("pull"),
|
|
356
|
+
{ stdin, stdout, stderr },
|
|
357
|
+
{ platform, stderr }
|
|
361
358
|
);
|
|
362
|
-
return
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
let setupToken = null;
|
|
366
|
-
if (setupStatus.setupRequired) {
|
|
367
|
-
setupToken = randomBytes(32).toString("base64url");
|
|
368
|
-
const rotateCode = await executeWithDockerDiagnostics(
|
|
359
|
+
if (pullCode !== 0) return pullCode;
|
|
360
|
+
const upCode = await executeWithDockerDiagnostics(
|
|
369
361
|
execute,
|
|
370
|
-
|
|
371
|
-
{
|
|
372
|
-
stdin,
|
|
373
|
-
stdout,
|
|
374
|
-
stderr,
|
|
375
|
-
input: `${setupToken}\n`
|
|
376
|
-
},
|
|
362
|
+
stagedComposeCommand("up"),
|
|
363
|
+
{ stdin, stdout, stderr },
|
|
377
364
|
{ platform, stderr }
|
|
378
365
|
);
|
|
379
|
-
if (
|
|
366
|
+
if (upCode !== 0) return upCode;
|
|
367
|
+
|
|
368
|
+
const url = `http://${result.config.bindHost}:${result.config.port}`;
|
|
369
|
+
stdout.write("Waiting for SpaceApp services to become ready...\n");
|
|
370
|
+
const ready = await waitForApplicationReady({
|
|
371
|
+
url,
|
|
372
|
+
request,
|
|
373
|
+
sleep
|
|
374
|
+
});
|
|
375
|
+
if (!ready) {
|
|
380
376
|
stderr.write(
|
|
381
|
-
|
|
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`
|
|
382
379
|
);
|
|
383
|
-
return
|
|
380
|
+
return 1;
|
|
384
381
|
}
|
|
382
|
+
|
|
383
|
+
let setupStatus;
|
|
385
384
|
try {
|
|
386
|
-
await
|
|
387
|
-
} catch {
|
|
385
|
+
setupStatus = await requestSetupStatus({ url, request });
|
|
386
|
+
} catch (error) {
|
|
388
387
|
stderr.write(
|
|
389
|
-
|
|
390
|
-
|
|
388
|
+
`SpaceApp is ready, but owner setup status could not be verified: ${error?.message || String(error)}\n` +
|
|
389
|
+
`Run "${UNIVERSAL_COMMAND} status" and "${UNIVERSAL_COMMAND} logs", then run "${UNIVERSAL_COMMAND} install" again.\n`
|
|
391
390
|
);
|
|
392
391
|
return 1;
|
|
393
392
|
}
|
|
394
|
-
}
|
|
395
393
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
394
|
+
let setupToken = null;
|
|
395
|
+
if (setupStatus.setupRequired) {
|
|
396
|
+
setupToken = randomBytes(32).toString("base64url");
|
|
397
|
+
const rotateCode = await executeWithDockerDiagnostics(
|
|
398
|
+
execute,
|
|
399
|
+
stagedComposeCommand("rotateOwnerSetupToken"),
|
|
400
|
+
{
|
|
401
|
+
stdin,
|
|
402
|
+
stdout,
|
|
403
|
+
stderr,
|
|
404
|
+
input: `${setupToken}\n`
|
|
405
|
+
},
|
|
406
|
+
{ platform, stderr }
|
|
407
|
+
);
|
|
408
|
+
if (rotateCode !== 0) {
|
|
409
|
+
stderr.write(
|
|
410
|
+
`SpaceApp is ready, but a fresh owner setup token could not be created. Run "${UNIVERSAL_COMMAND} owner rotate-setup-token".\n`
|
|
411
|
+
);
|
|
412
|
+
return rotateCode;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (profile === "light") {
|
|
417
|
+
const removeBrowserCode = await executeWithDockerDiagnostics(
|
|
418
|
+
execute,
|
|
419
|
+
composeCommand("removeBrowser", root, {
|
|
420
|
+
profile: "standard",
|
|
421
|
+
stateRoot: stagedStateRoot
|
|
422
|
+
}),
|
|
423
|
+
{ stdin, stdout, stderr },
|
|
424
|
+
{ platform, stderr }
|
|
425
|
+
);
|
|
426
|
+
if (removeBrowserCode !== 0) {
|
|
427
|
+
stderr.write(
|
|
428
|
+
"SpaceApp is ready, but the managed browser container could not be removed for the light profile.\n"
|
|
429
|
+
);
|
|
430
|
+
return removeBrowserCode;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (setupToken) {
|
|
435
|
+
try {
|
|
436
|
+
await persistSetupToken(root, setupToken);
|
|
437
|
+
} catch {
|
|
438
|
+
stderr.write(
|
|
439
|
+
"SpaceApp accepted a new setup token, but it could not be saved locally.\n" +
|
|
440
|
+
`Run "${UNIVERSAL_COMMAND} owner rotate-setup-token" to obtain a usable token.\n`
|
|
441
|
+
);
|
|
442
|
+
return 1;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
await commitInstallation(root, result.config);
|
|
447
|
+
stdout.write(`SpaceApp is ready at ${url}\n`);
|
|
448
|
+
if (setupToken) {
|
|
449
|
+
stdout.write(`One-time setup token: ${setupToken}\n`);
|
|
450
|
+
stdout.write('Paste it into the "One-time setup token" field in the page that opens.\n');
|
|
451
|
+
stdout.write("It expires in 15 minutes and stops working after the first owner is created.\n");
|
|
452
|
+
stdout.write(`If it expires, run: ${UNIVERSAL_COMMAND} owner rotate-setup-token\n`);
|
|
453
|
+
}
|
|
454
|
+
stdout.write(
|
|
455
|
+
`Next: add CLI credentials with "${UNIVERSAL_COMMAND} credentials set <provider>".\n`
|
|
456
|
+
);
|
|
457
|
+
if (noOpen) return 0;
|
|
458
|
+
const openCode = await openBrowser(url, platform, execute, { stdin, stdout, stderr });
|
|
459
|
+
if (openCode !== 0) {
|
|
460
|
+
stderr.write(`Could not open SpaceApp automatically. Open ${url} manually.\n`);
|
|
461
|
+
}
|
|
462
|
+
return 0;
|
|
463
|
+
} finally {
|
|
464
|
+
await rm(stagedStateRoot, { recursive: true, force: true });
|
|
408
465
|
}
|
|
409
|
-
return 0;
|
|
410
466
|
}
|
|
411
467
|
|
|
412
468
|
function parseInstallArgs(args) {
|
|
@@ -427,19 +483,36 @@ function parseInstallArgs(args) {
|
|
|
427
483
|
requestedProfile = argument.slice("--profile=".length);
|
|
428
484
|
continue;
|
|
429
485
|
}
|
|
430
|
-
throw new Error(
|
|
486
|
+
throw new Error(
|
|
487
|
+
`Usage: ${UNIVERSAL_COMMAND} install [--profile auto|light|standard] [--no-open]`
|
|
488
|
+
);
|
|
431
489
|
}
|
|
432
490
|
if (!["auto", "light", "standard"].includes(requestedProfile)) {
|
|
433
|
-
throw new Error(
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Usage: ${UNIVERSAL_COMMAND} install [--profile auto|light|standard] [--no-open]`
|
|
493
|
+
);
|
|
434
494
|
}
|
|
435
495
|
return { requestedProfile, noOpen };
|
|
436
496
|
}
|
|
437
497
|
|
|
498
|
+
async function loadExistingInstallation(root) {
|
|
499
|
+
try {
|
|
500
|
+
return await loadConfig(root);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if (error?.code === "ENOENT") {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
438
509
|
async function workspaceCommand(args, { root, config, stdout }) {
|
|
439
510
|
const [action, identity, ...rest] = args;
|
|
440
511
|
if (action === "add") {
|
|
441
512
|
if (!identity || rest.some((arg) => arg !== "--read-only")) {
|
|
442
|
-
throw new Error(
|
|
513
|
+
throw new Error(
|
|
514
|
+
`Usage: ${UNIVERSAL_COMMAND} workspace add <absolute-path> [--read-only]`
|
|
515
|
+
);
|
|
443
516
|
}
|
|
444
517
|
const updated = await addWorkspace(config, identity, { readOnly: rest.includes("--read-only") });
|
|
445
518
|
await saveConfig(root, updated);
|
|
@@ -449,7 +522,9 @@ async function workspaceCommand(args, { root, config, stdout }) {
|
|
|
449
522
|
}
|
|
450
523
|
if (action === "remove") {
|
|
451
524
|
if (!identity || rest.length > 0) {
|
|
452
|
-
throw new Error(
|
|
525
|
+
throw new Error(
|
|
526
|
+
`Usage: ${UNIVERSAL_COMMAND} workspace remove <id-or-absolute-path>`
|
|
527
|
+
);
|
|
453
528
|
}
|
|
454
529
|
const updated = removeWorkspace(config, identity);
|
|
455
530
|
await saveConfig(root, updated);
|
|
@@ -461,7 +536,7 @@ async function workspaceCommand(args, { root, config, stdout }) {
|
|
|
461
536
|
stdout.write(`${JSON.stringify(config.workspaces, null, 2)}\n`);
|
|
462
537
|
return 0;
|
|
463
538
|
}
|
|
464
|
-
throw new Error(
|
|
539
|
+
throw new Error(`Usage: ${UNIVERSAL_COMMAND} workspace <add|remove|list>`);
|
|
465
540
|
}
|
|
466
541
|
|
|
467
542
|
async function credentialsCommand(args, { root, config, stdin, stdout, stderr, execute }) {
|
|
@@ -472,7 +547,9 @@ async function credentialsCommand(args, { root, config, stdin, stdout, stderr, e
|
|
|
472
547
|
}
|
|
473
548
|
if (action === "set") {
|
|
474
549
|
if (!provider || rest.length > 0) {
|
|
475
|
-
throw new Error(
|
|
550
|
+
throw new Error(
|
|
551
|
+
`Usage: ${UNIVERSAL_COMMAND} credentials set <provider> (the value is read from stdin)`
|
|
552
|
+
);
|
|
476
553
|
}
|
|
477
554
|
const value = await readSecret(stdin, stdout, `Enter ${provider} credential: `);
|
|
478
555
|
await writeCredential(root, provider, value);
|
|
@@ -489,7 +566,7 @@ async function credentialsCommand(args, { root, config, stdin, stdout, stderr, e
|
|
|
489
566
|
}
|
|
490
567
|
if (action === "remove") {
|
|
491
568
|
if (!provider || rest.length > 0) {
|
|
492
|
-
throw new Error(
|
|
569
|
+
throw new Error(`Usage: ${UNIVERSAL_COMMAND} credentials remove <provider>`);
|
|
493
570
|
}
|
|
494
571
|
const removed = await removeCredential(root, provider);
|
|
495
572
|
const syncCode = await execute(
|
|
@@ -503,12 +580,12 @@ async function credentialsCommand(args, { root, config, stdin, stdout, stderr, e
|
|
|
503
580
|
stdout.write(removed ? `Credential removed and applied for ${provider}.\n` : `No credential stored for ${provider}.\n`);
|
|
504
581
|
return syncCode;
|
|
505
582
|
}
|
|
506
|
-
throw new Error(
|
|
583
|
+
throw new Error(`Usage: ${UNIVERSAL_COMMAND} credentials <set|remove|list>`);
|
|
507
584
|
}
|
|
508
585
|
|
|
509
586
|
async function providerCommand(args, { root, config, stdin, stdout, stderr, execute }) {
|
|
510
587
|
if (args.length !== 2 || args[0] !== "install" || args[1] !== "claude") {
|
|
511
|
-
throw new Error(
|
|
588
|
+
throw new Error(`Usage: ${UNIVERSAL_COMMAND} provider install claude`);
|
|
512
589
|
}
|
|
513
590
|
stdout.write("Installing Claude Code from Anthropic into this installation's private provider volume.\n");
|
|
514
591
|
return execute(composeCommand("installClaude", root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
@@ -530,7 +607,9 @@ async function ownerCommand(args, { root, config, stdin, stdout, stderr, execute
|
|
|
530
607
|
return 0;
|
|
531
608
|
}
|
|
532
609
|
if (args.length !== 1 || args[0] !== "reset-password") {
|
|
533
|
-
throw new Error(
|
|
610
|
+
throw new Error(
|
|
611
|
+
`Usage: ${UNIVERSAL_COMMAND} owner <reset-password|rotate-setup-token>`
|
|
612
|
+
);
|
|
534
613
|
}
|
|
535
614
|
const password = await readSecret(stdin, stdout, "New owner password: ");
|
|
536
615
|
if (password.length < 12) {
|
|
@@ -546,7 +625,7 @@ async function ownerCommand(args, { root, config, stdin, stdout, stderr, execute
|
|
|
546
625
|
|
|
547
626
|
async function updateCommand(args, { root, config, version, stdin, stdout, stderr, execute }) {
|
|
548
627
|
if (args.length > 1) {
|
|
549
|
-
throw new Error(
|
|
628
|
+
throw new Error(`Usage: ${UNIVERSAL_COMMAND} update [version]`);
|
|
550
629
|
}
|
|
551
630
|
const targetVersion = args[0] || version;
|
|
552
631
|
const updated = {
|
|
@@ -857,7 +936,7 @@ function openBrowser(url, platform, execute, io) {
|
|
|
857
936
|
|
|
858
937
|
function assertNoArgs(args, command) {
|
|
859
938
|
if (args.length > 0) {
|
|
860
|
-
throw new Error(`Usage:
|
|
939
|
+
throw new Error(`Usage: ${UNIVERSAL_COMMAND} ${command}`);
|
|
861
940
|
}
|
|
862
941
|
}
|
|
863
942
|
|
|
@@ -887,19 +966,19 @@ function commandNeedsRuntimeFiles(command, args) {
|
|
|
887
966
|
|
|
888
967
|
function dockerInstallHelp(platform) {
|
|
889
968
|
if (platform === "win32") {
|
|
890
|
-
return
|
|
969
|
+
return `Run "${UNIVERSAL_COMMAND} install" to install and start signed Docker Desktop with WSL2 automatically.`;
|
|
891
970
|
}
|
|
892
971
|
if (platform === "darwin") {
|
|
893
|
-
return
|
|
972
|
+
return `Run "${UNIVERSAL_COMMAND} install" to install and start signed Docker Desktop automatically.`;
|
|
894
973
|
}
|
|
895
|
-
return
|
|
974
|
+
return `Run "${UNIVERSAL_COMMAND} install" to install and start Docker Engine and Compose automatically on supported Linux distributions.`;
|
|
896
975
|
}
|
|
897
976
|
|
|
898
977
|
function dockerEngineHelp(platform) {
|
|
899
978
|
if (platform === "win32" || platform === "darwin") {
|
|
900
|
-
return
|
|
979
|
+
return `Open Docker Desktop, complete any first-run prompt, then run "${UNIVERSAL_COMMAND} doctor" again.`;
|
|
901
980
|
}
|
|
902
|
-
return
|
|
981
|
+
return `Start Docker Engine, verify the current user can access it, then run "${UNIVERSAL_COMMAND} doctor" again.`;
|
|
903
982
|
}
|
|
904
983
|
|
|
905
984
|
function formatGigabytes(bytes) {
|
|
@@ -918,7 +997,8 @@ async function packageVersion() {
|
|
|
918
997
|
function helpText() {
|
|
919
998
|
return `SpaceApp self-hosted launcher
|
|
920
999
|
|
|
921
|
-
Usage:
|
|
1000
|
+
Usage: ${UNIVERSAL_COMMAND} <command>
|
|
1001
|
+
spaceapp <command> Optional global launcher
|
|
922
1002
|
|
|
923
1003
|
init Create a local SpaceApp installation
|
|
924
1004
|
install [--profile auto|light|standard] [--no-open]
|
package/src/index.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
chmod,
|
|
4
|
-
copyFile,
|
|
5
4
|
mkdir,
|
|
6
5
|
readFile,
|
|
7
6
|
readdir,
|
|
@@ -354,6 +353,8 @@ export function renderWorkspaceCompose(config) {
|
|
|
354
353
|
|
|
355
354
|
export function composeCommand(action, root, options = {}) {
|
|
356
355
|
validateHome(root);
|
|
356
|
+
const stateRoot = options.stateRoot ?? root;
|
|
357
|
+
validateHome(stateRoot);
|
|
357
358
|
const profile = options.profile ?? "standard";
|
|
358
359
|
if (profile !== "light" && profile !== "standard") {
|
|
359
360
|
throw new Error("Compose profile must be light or standard.");
|
|
@@ -362,9 +363,9 @@ export function composeCommand(action, root, options = {}) {
|
|
|
362
363
|
"compose",
|
|
363
364
|
"--project-name", composeProjectName(root),
|
|
364
365
|
"--project-directory", root,
|
|
365
|
-
"--env-file", join(
|
|
366
|
-
"-f", join(
|
|
367
|
-
"-f", join(
|
|
366
|
+
"--env-file", join(stateRoot, "runtime.env"),
|
|
367
|
+
"-f", join(stateRoot, "compose.yml"),
|
|
368
|
+
"-f", join(stateRoot, "compose.workspaces.yml"),
|
|
368
369
|
...(profile === "standard" ? ["--profile", "standard"] : [])
|
|
369
370
|
];
|
|
370
371
|
const actions = {
|
|
@@ -412,6 +413,7 @@ export function composeCommand(action, root, options = {}) {
|
|
|
412
413
|
"scripts/rotate-owner-setup-token.mjs",
|
|
413
414
|
"--stdin"
|
|
414
415
|
],
|
|
416
|
+
removeBrowser: ["rm", "--stop", "--force", "spaceapp-browser"],
|
|
415
417
|
purge: ["down", "--volumes", "--remove-orphans"]
|
|
416
418
|
};
|
|
417
419
|
let selected = actions[action];
|
|
@@ -459,7 +461,9 @@ export async function selectLatestBackupId(root) {
|
|
|
459
461
|
.sort()
|
|
460
462
|
.at(-1);
|
|
461
463
|
if (!backupId) {
|
|
462
|
-
throw new Error(
|
|
464
|
+
throw new Error(
|
|
465
|
+
'No portable backup exists. Run "npx --yes run-spaceapp@latest backup" before restore.'
|
|
466
|
+
);
|
|
463
467
|
}
|
|
464
468
|
return backupId;
|
|
465
469
|
}
|
|
@@ -472,9 +476,8 @@ export async function writeRuntimeFiles(root, config) {
|
|
|
472
476
|
await atomicWrite(join(root, "compose.workspaces.yml"), renderWorkspaceCompose(config));
|
|
473
477
|
}
|
|
474
478
|
|
|
475
|
-
export async function
|
|
479
|
+
export async function prepareInstallation(root, {
|
|
476
480
|
version,
|
|
477
|
-
templateDir = defaultTemplateDir(),
|
|
478
481
|
profile
|
|
479
482
|
}) {
|
|
480
483
|
validateHome(root);
|
|
@@ -505,10 +508,7 @@ export async function initializeInstallation(root, {
|
|
|
505
508
|
if (resolvedProfile !== undefined) {
|
|
506
509
|
config = { ...config, profile: resolvedProfile };
|
|
507
510
|
}
|
|
508
|
-
|
|
509
|
-
await copyFile(join(templateDir, "compose.yml"), join(root, "compose.yml"));
|
|
510
|
-
await chmod(join(root, "compose.yml"), 0o600);
|
|
511
|
-
await writeRuntimeFiles(root, config);
|
|
511
|
+
validateConfig(config);
|
|
512
512
|
|
|
513
513
|
const postgresPasswordPath = join(root, "secrets", "postgres-password");
|
|
514
514
|
let postgresPassword;
|
|
@@ -559,6 +559,26 @@ export async function initializeInstallation(root, {
|
|
|
559
559
|
return { config, setupToken };
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
export async function commitInstallation(root, config, {
|
|
563
|
+
templateDir = defaultTemplateDir()
|
|
564
|
+
} = {}) {
|
|
565
|
+
validateHome(root);
|
|
566
|
+
validateConfig(config);
|
|
567
|
+
const composeTemplate = await readFile(join(templateDir, "compose.yml"), "utf8");
|
|
568
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
569
|
+
await atomicWrite(join(root, "compose.yml"), composeTemplate);
|
|
570
|
+
await writeRuntimeFiles(root, config);
|
|
571
|
+
await saveConfig(root, config);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export async function initializeInstallation(root, options) {
|
|
575
|
+
const result = await prepareInstallation(root, options);
|
|
576
|
+
await commitInstallation(root, result.config, {
|
|
577
|
+
templateDir: options.templateDir
|
|
578
|
+
});
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
|
|
562
582
|
function validateWorkspace(workspace) {
|
|
563
583
|
if (!workspace || typeof workspace !== "object") {
|
|
564
584
|
throw new Error("Invalid workspace entry.");
|
package/src/prerequisites.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
|
|
12
12
|
const DOCKER_TERMS_URL = "https://www.docker.com/legal/docker-subscription-service-agreement/";
|
|
13
13
|
const DOWNLOAD_TIMEOUT_MS = 30 * 60 * 1_000;
|
|
14
|
+
const UNIVERSAL_INSTALL_COMMAND = "npx --yes run-spaceapp@latest install";
|
|
14
15
|
const WINDOWS_DOCKER_DOWNLOADS = Object.freeze({
|
|
15
16
|
x64: "https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe",
|
|
16
17
|
arm64: "https://desktop.docker.com/win/main/arm64/Docker%20Desktop%20Installer.exe"
|
|
@@ -221,7 +222,7 @@ async function ensureWindowsDocker({
|
|
|
221
222
|
);
|
|
222
223
|
if (installWslCode !== 0) {
|
|
223
224
|
stderr.write(
|
|
224
|
-
|
|
225
|
+
`Windows could not enable WSL2 automatically. Run "${UNIVERSAL_INSTALL_COMMAND}" from an Administrator Command Prompt, restart Windows if requested, and run it again.\n`
|
|
225
226
|
);
|
|
226
227
|
return { code: installWslCode, reexecuted: false };
|
|
227
228
|
}
|
|
@@ -243,7 +244,7 @@ async function ensureWindowsDocker({
|
|
|
243
244
|
if (scheduleCode !== 0) {
|
|
244
245
|
stderr.write(
|
|
245
246
|
"Windows must restart to finish WSL2, but SpaceApp could not schedule automatic continuation.\n" +
|
|
246
|
-
|
|
247
|
+
`Restart Windows, then run "${UNIVERSAL_INSTALL_COMMAND}" again.\n`
|
|
247
248
|
);
|
|
248
249
|
return { code: scheduleCode, reexecuted: false };
|
|
249
250
|
}
|
|
@@ -386,7 +387,7 @@ async function ensureWindowsDocker({
|
|
|
386
387
|
return { code: 0, reexecuted: false };
|
|
387
388
|
}
|
|
388
389
|
stderr.write(
|
|
389
|
-
|
|
390
|
+
`Docker Desktop did not become ready after 10 minutes. Open Docker Desktop and complete its first-run setup (select "Skip" on "Welcome to Docker" or sign in), restart Windows if requested, then run "${UNIVERSAL_INSTALL_COMMAND}" again.\n`
|
|
390
391
|
);
|
|
391
392
|
return { code: 1, reexecuted: false };
|
|
392
393
|
}
|
|
@@ -494,7 +495,7 @@ async function ensureMacDocker({
|
|
|
494
495
|
return { code: 0, reexecuted: false };
|
|
495
496
|
}
|
|
496
497
|
stderr.write(
|
|
497
|
-
|
|
498
|
+
`Docker Desktop was installed but did not become ready. Complete any Docker Desktop prompt, then run "${UNIVERSAL_INSTALL_COMMAND}" again.\n`
|
|
498
499
|
);
|
|
499
500
|
return { code: 1, reexecuted: false };
|
|
500
501
|
}
|