run-spaceapp 0.1.7 → 0.1.8
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 +12 -0
- package/package.json +2 -2
- package/src/cli.mjs +297 -33
- package/src/prerequisites.mjs +28 -11
package/README.md
CHANGED
|
@@ -67,3 +67,15 @@ Run `spaceapp help` for the complete command list. Full documentation:
|
|
|
67
67
|
- [CLI providers](https://github.com/oll4com/spaceapp/blob/main/docs/cli-providers.md)
|
|
68
68
|
- [Operations](https://github.com/oll4com/spaceapp/blob/main/docs/operations.md)
|
|
69
69
|
- [Security model](https://github.com/oll4com/spaceapp/blob/main/docs/security-model.md)
|
|
70
|
+
|
|
71
|
+
After the stack passes readiness checks, `spaceapp install` prints the fresh
|
|
72
|
+
15-minute token to paste into the browser's **One-time setup token** field. If
|
|
73
|
+
it expires before the owner is created, run:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
spaceapp owner rotate-setup-token
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`spaceapp uninstall` removes the runtime while retaining data by default and
|
|
80
|
+
prints the separate `npm uninstall -g run-spaceapp` command for removing the
|
|
81
|
+
global launcher.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run-spaceapp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Cross-platform Docker launcher for the SpaceApp self-hosted agent workspace",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"access": "public",
|
|
46
46
|
"provenance": true
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "9415b97036843aa6186c02eb18826221138c4a23"
|
|
49
49
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -22,9 +22,14 @@ import {
|
|
|
22
22
|
} from "./index.mjs";
|
|
23
23
|
import {
|
|
24
24
|
ensureDockerAvailable,
|
|
25
|
+
prepareDockerCliPath,
|
|
25
26
|
windowsPowerShellArgs
|
|
26
27
|
} from "./prerequisites.mjs";
|
|
27
28
|
|
|
29
|
+
const APPLICATION_READY_WAIT_MS = 3 * 60 * 1_000;
|
|
30
|
+
const APPLICATION_READY_POLL_MS = 2_000;
|
|
31
|
+
const APPLICATION_READY_MAX_ATTEMPTS = APPLICATION_READY_WAIT_MS / APPLICATION_READY_POLL_MS;
|
|
32
|
+
const SETUP_STATUS_TIMEOUT_MS = 10_000;
|
|
28
33
|
const trustedCommands = new Set([
|
|
29
34
|
"codesign",
|
|
30
35
|
"docker",
|
|
@@ -52,8 +57,13 @@ export async function run(argv, {
|
|
|
52
57
|
stdin = process.stdin,
|
|
53
58
|
execute = executeCommand,
|
|
54
59
|
inspectResources = inspectSystemResources,
|
|
55
|
-
ensureDocker = ensureDockerAvailable
|
|
60
|
+
ensureDocker = ensureDockerAvailable,
|
|
61
|
+
prepareDockerPath = prepareDockerCliPath,
|
|
62
|
+
request = globalThis.fetch,
|
|
63
|
+
sleep = wait,
|
|
64
|
+
persistSetupToken = writeSetupToken
|
|
56
65
|
} = {}) {
|
|
66
|
+
await prepareDockerPath({ platform, env });
|
|
57
67
|
const [command = "help", ...args] = argv;
|
|
58
68
|
const root = resolveSpaceAppHome({ env, platform });
|
|
59
69
|
const version = await packageVersion();
|
|
@@ -78,7 +88,10 @@ export async function run(argv, {
|
|
|
78
88
|
stderr,
|
|
79
89
|
execute,
|
|
80
90
|
inspectResources,
|
|
81
|
-
ensureDocker
|
|
91
|
+
ensureDocker,
|
|
92
|
+
request,
|
|
93
|
+
sleep,
|
|
94
|
+
persistSetupToken
|
|
82
95
|
});
|
|
83
96
|
}
|
|
84
97
|
if (command === "init") {
|
|
@@ -94,11 +107,19 @@ export async function run(argv, {
|
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
const config = await loadConfig(root);
|
|
97
|
-
|
|
110
|
+
if (commandNeedsRuntimeFiles(command, args)) {
|
|
111
|
+
await writeRuntimeFiles(root, config);
|
|
112
|
+
}
|
|
113
|
+
const runtimeExecute = (spec, io) => executeWithDockerDiagnostics(
|
|
114
|
+
execute,
|
|
115
|
+
spec,
|
|
116
|
+
io,
|
|
117
|
+
{ platform, stderr }
|
|
118
|
+
);
|
|
98
119
|
|
|
99
120
|
if (["up", "down", "status", "logs"].includes(command)) {
|
|
100
121
|
assertNoArgs(args, command);
|
|
101
|
-
return
|
|
122
|
+
return runtimeExecute(composeCommand(command, root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
102
123
|
}
|
|
103
124
|
if (command === "open") {
|
|
104
125
|
assertNoArgs(args, "open");
|
|
@@ -112,16 +133,16 @@ export async function run(argv, {
|
|
|
112
133
|
return workspaceCommand(args, { root, config, stdout });
|
|
113
134
|
}
|
|
114
135
|
if (command === "credentials") {
|
|
115
|
-
return credentialsCommand(args, { root, config, stdin, stdout, stderr, execute });
|
|
136
|
+
return credentialsCommand(args, { root, config, stdin, stdout, stderr, execute: runtimeExecute });
|
|
116
137
|
}
|
|
117
138
|
if (command === "provider") {
|
|
118
|
-
return providerCommand(args, { root, config, stdin, stdout, stderr, execute });
|
|
139
|
+
return providerCommand(args, { root, config, stdin, stdout, stderr, execute: runtimeExecute });
|
|
119
140
|
}
|
|
120
141
|
if (command === "owner") {
|
|
121
|
-
return ownerCommand(args, { root, config, stdin, stdout, stderr, execute });
|
|
142
|
+
return ownerCommand(args, { root, config, stdin, stdout, stderr, execute: runtimeExecute });
|
|
122
143
|
}
|
|
123
144
|
if (command === "update") {
|
|
124
|
-
return updateCommand(args, { root, config, version, stdin, stdout, stderr, execute });
|
|
145
|
+
return updateCommand(args, { root, config, version, stdin, stdout, stderr, execute: runtimeExecute });
|
|
125
146
|
}
|
|
126
147
|
if (command === "rollback") {
|
|
127
148
|
assertNoArgs(args, "rollback");
|
|
@@ -134,12 +155,12 @@ export async function run(argv, {
|
|
|
134
155
|
previousVersion: config.version
|
|
135
156
|
};
|
|
136
157
|
await writeRuntimeFiles(root, rollback);
|
|
137
|
-
const pullCode = await
|
|
158
|
+
const pullCode = await runtimeExecute(composeCommand("pull", root, { profile: rollback.profile }), { stdin, stdout, stderr });
|
|
138
159
|
if (pullCode !== 0) {
|
|
139
160
|
await writeRuntimeFiles(root, config);
|
|
140
161
|
return pullCode;
|
|
141
162
|
}
|
|
142
|
-
const upCode = await
|
|
163
|
+
const upCode = await runtimeExecute(composeCommand("up", root, { profile: rollback.profile }), { stdin, stdout, stderr });
|
|
143
164
|
if (upCode !== 0) {
|
|
144
165
|
await writeRuntimeFiles(root, config);
|
|
145
166
|
return upCode;
|
|
@@ -150,7 +171,7 @@ export async function run(argv, {
|
|
|
150
171
|
}
|
|
151
172
|
if (command === "backup") {
|
|
152
173
|
assertNoArgs(args, command);
|
|
153
|
-
return
|
|
174
|
+
return runtimeExecute(composeCommand("backup", root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
154
175
|
}
|
|
155
176
|
if (command === "restore") {
|
|
156
177
|
assertNoArgs(args, command);
|
|
@@ -164,22 +185,33 @@ export async function run(argv, {
|
|
|
164
185
|
throw new Error("Restore cancelled.");
|
|
165
186
|
}
|
|
166
187
|
const backupId = await selectLatestBackupId(root);
|
|
167
|
-
const backupCode = await
|
|
188
|
+
const backupCode = await runtimeExecute(composeCommand("backup", root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
168
189
|
if (backupCode !== 0) return backupCode;
|
|
169
|
-
const stopCode = await
|
|
190
|
+
const stopCode = await runtimeExecute(composeCommand("stopForRestore", root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
170
191
|
if (stopCode !== 0) return stopCode;
|
|
171
|
-
const restoreCode = await
|
|
192
|
+
const restoreCode = await runtimeExecute(
|
|
172
193
|
composeCommand("restore", root, { backupId, profile: config.profile }),
|
|
173
194
|
{ stdin, stdout, stderr }
|
|
174
195
|
);
|
|
175
196
|
if (restoreCode !== 0) return restoreCode;
|
|
176
|
-
return
|
|
197
|
+
return runtimeExecute(composeCommand("up", root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
177
198
|
}
|
|
178
199
|
if (command === "uninstall") {
|
|
179
200
|
if (args.length === 0) {
|
|
180
|
-
|
|
201
|
+
stdout.write("Stopping and removing SpaceApp containers and network...\n");
|
|
202
|
+
const code = await runtimeExecute(
|
|
203
|
+
composeCommand("down", root, { profile: config.profile }),
|
|
204
|
+
{ stdin, stdout, stderr }
|
|
205
|
+
);
|
|
181
206
|
if (code === 0) {
|
|
182
|
-
stdout.write(
|
|
207
|
+
stdout.write("SpaceApp runtime removed successfully. It is safe to run this command again.\n");
|
|
208
|
+
stdout.write(`Data, configuration, secrets, and backups remain at ${root}.\n`);
|
|
209
|
+
stdout.write("Docker volumes are retained. The global SpaceApp CLI remains installed.\n");
|
|
210
|
+
stdout.write("To remove only the global CLI, run: npm uninstall -g run-spaceapp\n");
|
|
211
|
+
} else {
|
|
212
|
+
stderr.write(`Uninstall could not remove the runtime (Docker exit ${code}).\n`);
|
|
213
|
+
stdout.write(`Data, configuration, secrets, and backups remain at ${root}.\n`);
|
|
214
|
+
stdout.write("The global SpaceApp CLI remains installed.\n");
|
|
183
215
|
}
|
|
184
216
|
return code;
|
|
185
217
|
}
|
|
@@ -188,7 +220,21 @@ export async function run(argv, {
|
|
|
188
220
|
if (confirmation !== "DELETE") {
|
|
189
221
|
throw new Error("Purge cancelled.");
|
|
190
222
|
}
|
|
191
|
-
|
|
223
|
+
stdout.write("Removing SpaceApp containers, network, and Docker volumes...\n");
|
|
224
|
+
const code = await runtimeExecute(
|
|
225
|
+
composeCommand("purge", root, { profile: config.profile }),
|
|
226
|
+
{ stdin, stdout, stderr }
|
|
227
|
+
);
|
|
228
|
+
if (code === 0) {
|
|
229
|
+
stdout.write("SpaceApp runtime and Docker volumes removed successfully.\n");
|
|
230
|
+
stdout.write(`Host configuration and backups remain at ${root} for manual review.\n`);
|
|
231
|
+
stdout.write("The global SpaceApp CLI remains installed.\n");
|
|
232
|
+
stdout.write("To remove only the global CLI, run: npm uninstall -g run-spaceapp\n");
|
|
233
|
+
} else {
|
|
234
|
+
stderr.write(`SpaceApp Docker volume purge failed (Docker exit ${code}).\n`);
|
|
235
|
+
stdout.write(`Host files and the global SpaceApp CLI remain at ${root}.\n`);
|
|
236
|
+
}
|
|
237
|
+
return code;
|
|
192
238
|
}
|
|
193
239
|
throw new Error("Usage: spaceapp uninstall [--purge-data]");
|
|
194
240
|
}
|
|
@@ -207,7 +253,10 @@ async function installCommand(args, {
|
|
|
207
253
|
stderr,
|
|
208
254
|
execute,
|
|
209
255
|
inspectResources,
|
|
210
|
-
ensureDocker
|
|
256
|
+
ensureDocker,
|
|
257
|
+
request,
|
|
258
|
+
sleep,
|
|
259
|
+
persistSetupToken
|
|
211
260
|
}) {
|
|
212
261
|
const { requestedProfile, noOpen } = parseInstallArgs(args);
|
|
213
262
|
const resources = await inspectResources(root);
|
|
@@ -218,11 +267,6 @@ async function installCommand(args, {
|
|
|
218
267
|
`Selected profile: ${profile} (${formatGigabytes(resources.totalMemoryBytes)} GB system memory detected).\n`
|
|
219
268
|
);
|
|
220
269
|
stdout.write(`SpaceApp installation root: ${root}\n`);
|
|
221
|
-
if (result.setupToken) {
|
|
222
|
-
stdout.write(`One-time setup token: ${result.setupToken}\n`);
|
|
223
|
-
stdout.write("Store it temporarily; it expires after first owner setup.\n");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
270
|
if (installResourceChecks(resources).some((check) => !check.ok)) {
|
|
227
271
|
const doctorCode = await doctor({
|
|
228
272
|
root,
|
|
@@ -270,13 +314,85 @@ async function installCommand(args, {
|
|
|
270
314
|
stderr.write("Installation stopped before downloading images. Fix the failed checks and run the same command again.\n");
|
|
271
315
|
return doctorCode;
|
|
272
316
|
}
|
|
273
|
-
const pullCode = await
|
|
317
|
+
const pullCode = await executeWithDockerDiagnostics(
|
|
318
|
+
execute,
|
|
319
|
+
composeCommand("pull", root, { profile }),
|
|
320
|
+
{ stdin, stdout, stderr },
|
|
321
|
+
{ platform, stderr }
|
|
322
|
+
);
|
|
274
323
|
if (pullCode !== 0) return pullCode;
|
|
275
|
-
const upCode = await
|
|
324
|
+
const upCode = await executeWithDockerDiagnostics(
|
|
325
|
+
execute,
|
|
326
|
+
composeCommand("up", root, { profile }),
|
|
327
|
+
{ stdin, stdout, stderr },
|
|
328
|
+
{ platform, stderr }
|
|
329
|
+
);
|
|
276
330
|
if (upCode !== 0) return upCode;
|
|
277
331
|
|
|
278
332
|
const url = `http://${result.config.bindHost}:${result.config.port}`;
|
|
279
|
-
stdout.write(
|
|
333
|
+
stdout.write("Waiting for SpaceApp services to become ready...\n");
|
|
334
|
+
const ready = await waitForApplicationReady({
|
|
335
|
+
url,
|
|
336
|
+
request,
|
|
337
|
+
sleep
|
|
338
|
+
});
|
|
339
|
+
if (!ready) {
|
|
340
|
+
stderr.write(
|
|
341
|
+
"SpaceApp containers started, but the application did not become ready within 3 minutes.\n" +
|
|
342
|
+
'Run "spaceapp status" and "spaceapp logs", then run "spaceapp install" again.\n'
|
|
343
|
+
);
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let setupStatus;
|
|
348
|
+
try {
|
|
349
|
+
setupStatus = await requestSetupStatus({ url, request });
|
|
350
|
+
} catch (error) {
|
|
351
|
+
stderr.write(
|
|
352
|
+
`SpaceApp is ready, but owner setup status could not be verified: ${error?.message || String(error)}\n` +
|
|
353
|
+
'Run "spaceapp status" and "spaceapp logs", then run "spaceapp install" again.\n'
|
|
354
|
+
);
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let setupToken = null;
|
|
359
|
+
if (setupStatus.setupRequired) {
|
|
360
|
+
setupToken = randomBytes(32).toString("base64url");
|
|
361
|
+
const rotateCode = await executeWithDockerDiagnostics(
|
|
362
|
+
execute,
|
|
363
|
+
composeCommand("rotateOwnerSetupToken", root, { profile }),
|
|
364
|
+
{
|
|
365
|
+
stdin,
|
|
366
|
+
stdout,
|
|
367
|
+
stderr,
|
|
368
|
+
input: `${setupToken}\n`
|
|
369
|
+
},
|
|
370
|
+
{ platform, stderr }
|
|
371
|
+
);
|
|
372
|
+
if (rotateCode !== 0) {
|
|
373
|
+
stderr.write(
|
|
374
|
+
'SpaceApp is ready, but a fresh owner setup token could not be created. Run "spaceapp owner rotate-setup-token".\n'
|
|
375
|
+
);
|
|
376
|
+
return rotateCode;
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
await persistSetupToken(root, setupToken);
|
|
380
|
+
} catch {
|
|
381
|
+
stderr.write(
|
|
382
|
+
"SpaceApp accepted a new setup token, but it could not be saved locally.\n" +
|
|
383
|
+
'Run "spaceapp owner rotate-setup-token" to obtain a usable token.\n'
|
|
384
|
+
);
|
|
385
|
+
return 1;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
stdout.write(`SpaceApp is ready at ${url}\n`);
|
|
390
|
+
if (setupToken) {
|
|
391
|
+
stdout.write(`One-time setup token: ${setupToken}\n`);
|
|
392
|
+
stdout.write('Paste it into the "One-time setup token" field in the page that opens.\n');
|
|
393
|
+
stdout.write("It expires in 15 minutes and stops working after the first owner is created.\n");
|
|
394
|
+
stdout.write("If it expires, run: spaceapp owner rotate-setup-token\n");
|
|
395
|
+
}
|
|
280
396
|
stdout.write('Next: add CLI credentials with "spaceapp credentials set <provider>".\n');
|
|
281
397
|
if (noOpen) return 0;
|
|
282
398
|
const openCode = await openBrowser(url, platform, execute, { stdin, stdout, stderr });
|
|
@@ -464,27 +580,140 @@ async function doctor({
|
|
|
464
580
|
{ name: "Configuration", ok: true, detail: root },
|
|
465
581
|
...installResourceChecks(detectedResources)
|
|
466
582
|
];
|
|
467
|
-
|
|
583
|
+
const dockerResults = [];
|
|
468
584
|
for (const probe of [
|
|
469
|
-
{ name: "Docker", command: "docker", args: ["--version"] },
|
|
585
|
+
{ name: "Docker CLI", command: "docker", args: ["--version"] },
|
|
470
586
|
{ name: "Docker Compose", command: "docker", args: ["compose", "version"] },
|
|
471
587
|
{ name: "Docker Engine", command: "docker", args: ["info"] }
|
|
472
588
|
]) {
|
|
473
589
|
const code = dockerReady
|
|
474
590
|
? 0
|
|
475
591
|
: await execute(probe, { stdin, stdout: null, stderr: null });
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
592
|
+
dockerResults.push({ ...probe, code });
|
|
593
|
+
}
|
|
594
|
+
const [dockerCli, dockerCompose, dockerEngine] = dockerResults;
|
|
595
|
+
checks.push({
|
|
596
|
+
name: dockerCli.name,
|
|
597
|
+
ok: dockerCli.code === 0,
|
|
598
|
+
detail: dockerCli.code === 0
|
|
599
|
+
? "available"
|
|
600
|
+
: dockerCli.code === 127
|
|
601
|
+
? "not found on PATH"
|
|
602
|
+
: `unavailable (exit ${dockerCli.code})`
|
|
603
|
+
});
|
|
604
|
+
checks.push({
|
|
605
|
+
name: dockerCompose.name,
|
|
606
|
+
ok: dockerCompose.code === 0,
|
|
607
|
+
detail: dockerCompose.code === 0
|
|
608
|
+
? "available"
|
|
609
|
+
: dockerCli.code !== 0
|
|
610
|
+
? "not available because Docker CLI is missing"
|
|
611
|
+
: `plugin unavailable (exit ${dockerCompose.code})`
|
|
612
|
+
});
|
|
613
|
+
checks.push({
|
|
614
|
+
name: dockerEngine.name,
|
|
615
|
+
ok: dockerEngine.code === 0,
|
|
616
|
+
detail: dockerEngine.code === 0
|
|
617
|
+
? "available"
|
|
618
|
+
: dockerCli.code === 0
|
|
619
|
+
? "installed but not running or inaccessible"
|
|
620
|
+
: "not reachable because Docker CLI is missing"
|
|
621
|
+
});
|
|
479
622
|
for (const check of checks) {
|
|
480
623
|
(check.ok ? stdout : stderr).write(`${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}\n`);
|
|
481
624
|
}
|
|
482
|
-
if (
|
|
625
|
+
if (dockerCli.code !== 0 || dockerCompose.code !== 0) {
|
|
483
626
|
stderr.write(`${dockerInstallHelp(platform)}\n`);
|
|
627
|
+
} else if (dockerEngine.code !== 0) {
|
|
628
|
+
stderr.write(`${dockerEngineHelp(platform)}\n`);
|
|
484
629
|
}
|
|
485
630
|
return checks.every((check) => check.ok) ? 0 : 1;
|
|
486
631
|
}
|
|
487
632
|
|
|
633
|
+
async function waitForApplicationReady({
|
|
634
|
+
url,
|
|
635
|
+
request,
|
|
636
|
+
sleep,
|
|
637
|
+
maxAttempts = APPLICATION_READY_MAX_ATTEMPTS
|
|
638
|
+
}) {
|
|
639
|
+
if (typeof request !== "function") {
|
|
640
|
+
throw new Error("SpaceApp readiness requires a Fetch-compatible request function.");
|
|
641
|
+
}
|
|
642
|
+
const controller = new AbortController();
|
|
643
|
+
const timeout = setTimeout(() => controller.abort(), APPLICATION_READY_WAIT_MS);
|
|
644
|
+
try {
|
|
645
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
|
|
646
|
+
try {
|
|
647
|
+
const response = await request(`${url}/readyz`, {
|
|
648
|
+
method: "GET",
|
|
649
|
+
headers: { accept: "application/json" },
|
|
650
|
+
redirect: "error",
|
|
651
|
+
signal: controller.signal
|
|
652
|
+
});
|
|
653
|
+
if (response?.ok) {
|
|
654
|
+
const payload = await response.json();
|
|
655
|
+
if (payload?.ok === true) {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
if (controller.signal.aborted) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (attempt < maxAttempts && !controller.signal.aborted) {
|
|
665
|
+
await sleep(APPLICATION_READY_POLL_MS);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return false;
|
|
669
|
+
} finally {
|
|
670
|
+
clearTimeout(timeout);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function requestSetupStatus({ url, request }) {
|
|
675
|
+
const controller = new AbortController();
|
|
676
|
+
const timeout = setTimeout(() => controller.abort(), SETUP_STATUS_TIMEOUT_MS);
|
|
677
|
+
try {
|
|
678
|
+
const response = await request(`${url}/api/setup/status`, {
|
|
679
|
+
method: "GET",
|
|
680
|
+
headers: { accept: "application/json" },
|
|
681
|
+
redirect: "error",
|
|
682
|
+
signal: controller.signal
|
|
683
|
+
});
|
|
684
|
+
if (!response?.ok) {
|
|
685
|
+
throw new Error(`HTTP ${response?.status ?? "error"}`);
|
|
686
|
+
}
|
|
687
|
+
const payload = await response.json();
|
|
688
|
+
if (
|
|
689
|
+
!payload ||
|
|
690
|
+
typeof payload !== "object" ||
|
|
691
|
+
typeof payload.setupRequired !== "boolean" ||
|
|
692
|
+
(payload.expiresAt !== null && typeof payload.expiresAt !== "string")
|
|
693
|
+
) {
|
|
694
|
+
throw new Error("invalid setup status response");
|
|
695
|
+
}
|
|
696
|
+
return payload;
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if (controller.signal.aborted) {
|
|
699
|
+
throw new Error("setup status request timed out", { cause: error });
|
|
700
|
+
}
|
|
701
|
+
throw error;
|
|
702
|
+
} finally {
|
|
703
|
+
clearTimeout(timeout);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function executeWithDockerDiagnostics(execute, spec, io, { platform, stderr }) {
|
|
708
|
+
const code = await execute(spec, io);
|
|
709
|
+
if (spec.command === "docker" && code === 127) {
|
|
710
|
+
stderr.write(
|
|
711
|
+
`SpaceApp could not find the Docker CLI. ${dockerInstallHelp(platform)}\n`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
return code;
|
|
715
|
+
}
|
|
716
|
+
|
|
488
717
|
export async function readSecret(stdin, stdout, prompt, { mask = true } = {}) {
|
|
489
718
|
stdout.write(prompt);
|
|
490
719
|
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
|
|
@@ -621,6 +850,30 @@ function assertNoArgs(args, command) {
|
|
|
621
850
|
}
|
|
622
851
|
}
|
|
623
852
|
|
|
853
|
+
function commandNeedsRuntimeFiles(command, args) {
|
|
854
|
+
if ([
|
|
855
|
+
"up",
|
|
856
|
+
"down",
|
|
857
|
+
"status",
|
|
858
|
+
"logs",
|
|
859
|
+
"backup",
|
|
860
|
+
"restore",
|
|
861
|
+
"uninstall"
|
|
862
|
+
].includes(command)) {
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
if (command === "credentials") {
|
|
866
|
+
return args[0] === "set" || args[0] === "remove";
|
|
867
|
+
}
|
|
868
|
+
if (command === "provider") {
|
|
869
|
+
return args[0] === "install";
|
|
870
|
+
}
|
|
871
|
+
if (command === "owner") {
|
|
872
|
+
return args[0] === "reset-password" || args[0] === "rotate-setup-token";
|
|
873
|
+
}
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
|
|
624
877
|
function dockerInstallHelp(platform) {
|
|
625
878
|
if (platform === "win32") {
|
|
626
879
|
return 'Run "spaceapp install" to install and start signed Docker Desktop with WSL2 automatically.';
|
|
@@ -631,10 +884,21 @@ function dockerInstallHelp(platform) {
|
|
|
631
884
|
return 'Run "spaceapp install" to install and start Docker Engine and Compose automatically on supported Linux distributions.';
|
|
632
885
|
}
|
|
633
886
|
|
|
887
|
+
function dockerEngineHelp(platform) {
|
|
888
|
+
if (platform === "win32" || platform === "darwin") {
|
|
889
|
+
return 'Open Docker Desktop, complete any first-run prompt, then run "spaceapp doctor" again.';
|
|
890
|
+
}
|
|
891
|
+
return 'Start Docker Engine, verify the current user can access it, then run "spaceapp doctor" again.';
|
|
892
|
+
}
|
|
893
|
+
|
|
634
894
|
function formatGigabytes(bytes) {
|
|
635
895
|
return Math.floor((bytes / 1024 ** 3) * 10) / 10;
|
|
636
896
|
}
|
|
637
897
|
|
|
898
|
+
function wait(milliseconds) {
|
|
899
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
900
|
+
}
|
|
901
|
+
|
|
638
902
|
async function packageVersion() {
|
|
639
903
|
const packageJson = new URL("../package.json", import.meta.url);
|
|
640
904
|
return JSON.parse(await readFile(packageJson, "utf8")).version;
|
package/src/prerequisites.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createWriteStream } from "node:fs";
|
|
2
2
|
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { join, posix, win32 } from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
@@ -67,6 +67,27 @@ export function windowsPowerShellArgs(operation) {
|
|
|
67
67
|
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", scripts[operation]];
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export async function prepareDockerCliPath({
|
|
71
|
+
platform = process.platform,
|
|
72
|
+
env = process.env,
|
|
73
|
+
pathExists = fileExists
|
|
74
|
+
} = {}) {
|
|
75
|
+
let directories = [];
|
|
76
|
+
if (platform === "win32") {
|
|
77
|
+
directories = windowsDockerPaths(env).cliDirectories;
|
|
78
|
+
} else if (platform === "darwin") {
|
|
79
|
+
directories = ["/Applications/Docker.app/Contents/Resources/bin"];
|
|
80
|
+
} else {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const directory = await firstExisting(directories, pathExists);
|
|
84
|
+
if (!directory) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
prependPath(env, directory, platform);
|
|
88
|
+
return directory;
|
|
89
|
+
}
|
|
90
|
+
|
|
70
91
|
export async function ensureDockerAvailable({
|
|
71
92
|
platform = process.platform,
|
|
72
93
|
arch = process.arch,
|
|
@@ -258,10 +279,7 @@ async function ensureWindowsDocker({
|
|
|
258
279
|
stdout.write("Docker Desktop is installed but is not running. Starting it now...\n");
|
|
259
280
|
}
|
|
260
281
|
|
|
261
|
-
|
|
262
|
-
if (cliDirectory) {
|
|
263
|
-
prependPath(env, cliDirectory);
|
|
264
|
-
}
|
|
282
|
+
await prepareDockerCliPath({ platform: "win32", env, pathExists });
|
|
265
283
|
stdout.write(
|
|
266
284
|
"Opening Docker Desktop for its first-run setup.\n" +
|
|
267
285
|
"If Docker shows \"Welcome to Docker\", select \"Skip\" in the top-right (or sign in), and accept any remaining Docker prompt.\n" +
|
|
@@ -383,9 +401,7 @@ async function ensureMacDocker({
|
|
|
383
401
|
stdout.write("Docker Desktop is installed but is not running. Starting it now...\n");
|
|
384
402
|
}
|
|
385
403
|
|
|
386
|
-
|
|
387
|
-
prependPath(env, cliDirectory);
|
|
388
|
-
}
|
|
404
|
+
await prepareDockerCliPath({ platform: "darwin", env, pathExists });
|
|
389
405
|
const launchCode = await launch({ operation: "open-docker-desktop" });
|
|
390
406
|
if (launchCode !== 0) {
|
|
391
407
|
stderr.write("Docker Desktop is installed, but SpaceApp could not start it.\n");
|
|
@@ -740,11 +756,12 @@ async function firstExisting(paths, pathExists) {
|
|
|
740
756
|
return null;
|
|
741
757
|
}
|
|
742
758
|
|
|
743
|
-
function prependPath(env, directory) {
|
|
759
|
+
function prependPath(env, directory, platform = process.platform) {
|
|
744
760
|
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") || "PATH";
|
|
745
|
-
const
|
|
761
|
+
const pathDelimiter = platform === "win32" ? win32.delimiter : posix.delimiter;
|
|
762
|
+
const entries = String(env[pathKey] || "").split(pathDelimiter).filter(Boolean);
|
|
746
763
|
if (!entries.includes(directory)) {
|
|
747
|
-
env[pathKey] = [directory, ...entries].join(
|
|
764
|
+
env[pathKey] = [directory, ...entries].join(pathDelimiter);
|
|
748
765
|
}
|
|
749
766
|
}
|
|
750
767
|
|