run-spaceapp 0.1.15-hostroot.2 → 0.1.18
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 +9 -0
- package/package.json +6 -5
- package/src/cli.mjs +56 -24
- package/src/index.mjs +23 -8
- package/src/telemetry.mjs +92 -0
- package/templates/compose.yml +31 -0
package/README.md
CHANGED
|
@@ -41,6 +41,15 @@ The launcher defaults to the `light` profile on every system. Run
|
|
|
41
41
|
`npx --yes run-spaceapp@latest install --profile standard` explicitly when
|
|
42
42
|
managed Chromium is required and the host has the recommended resources.
|
|
43
43
|
|
|
44
|
+
### First-install telemetry
|
|
45
|
+
|
|
46
|
+
On a successful first install (not on upgrades or re-runs), the launcher sends
|
|
47
|
+
one anonymous ping to `spaceapp.dev` with only: a random install ID, operating
|
|
48
|
+
system and CPU architecture, launcher and runtime versions, and the install
|
|
49
|
+
date. No personal data, files, or credentials are transmitted. Disable it with
|
|
50
|
+
the environment variable `SPACEAPP_TELEMETRY=0` (or `false`); telemetry is
|
|
51
|
+
otherwise on by default so the project can measure real adoption.
|
|
52
|
+
|
|
44
53
|
Light mode retains every bundled CLI and the core data/workflow services while
|
|
45
54
|
omitting managed Chromium. Resource limits are maximums, not immediate RAM or
|
|
46
55
|
disk reservations. The launcher stores only non-secret configuration in the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run-spaceapp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"spaceappRuntimeVersion": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
|
+
"spaceappRuntimeVersion": "0.1.18",
|
|
5
5
|
"spaceappHostRootRuntimeCompatible": true,
|
|
6
6
|
"description": "Cross-platform Docker launcher for the SpaceApp self-hosted agent workspace",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -31,11 +31,12 @@
|
|
|
31
31
|
"win32"
|
|
32
32
|
],
|
|
33
33
|
"cpu": [
|
|
34
|
-
"x64"
|
|
34
|
+
"x64",
|
|
35
|
+
"arm64"
|
|
35
36
|
],
|
|
36
37
|
"scripts": {
|
|
37
38
|
"test": "node --test tests/*.test.mjs",
|
|
38
|
-
"check": "node --check bin/spaceapp.mjs && node --check src/package-info.mjs && node --check src/index.mjs && node --check src/cli.mjs && node --check src/prerequisites.mjs",
|
|
39
|
+
"check": "node --check bin/spaceapp.mjs && node --check src/package-info.mjs && node --check src/index.mjs && node --check src/cli.mjs && node --check src/telemetry.mjs && node --check src/prerequisites.mjs",
|
|
39
40
|
"pack:dry-run": "npm pack --dry-run"
|
|
40
41
|
},
|
|
41
42
|
"repository": {
|
|
@@ -51,5 +52,5 @@
|
|
|
51
52
|
"access": "public",
|
|
52
53
|
"provenance": true
|
|
53
54
|
},
|
|
54
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "cf4692ac9bbf433e035042caba2c29a64c3d5827"
|
|
55
56
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -36,6 +36,10 @@ import {
|
|
|
36
36
|
RUNTIME_VERSION,
|
|
37
37
|
UNIVERSAL_COMMAND
|
|
38
38
|
} from "./package-info.mjs";
|
|
39
|
+
import {
|
|
40
|
+
INSTALL_PING_TIMEOUT_MS,
|
|
41
|
+
reportFirstInstallPing
|
|
42
|
+
} from "./telemetry.mjs";
|
|
39
43
|
|
|
40
44
|
const APPLICATION_READY_WAIT_MINUTES = 10;
|
|
41
45
|
const APPLICATION_READY_WAIT_MS = APPLICATION_READY_WAIT_MINUTES * 60 * 1_000;
|
|
@@ -144,7 +148,7 @@ export async function run(argv, {
|
|
|
144
148
|
|
|
145
149
|
if (["up", "down", "status", "logs"].includes(command)) {
|
|
146
150
|
assertNoArgs(args, command);
|
|
147
|
-
return runtimeExecute(composeCommand(command, root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
151
|
+
return runtimeExecute(composeCommand(command, root, { profile: config.profile, companionsEnabled: config.companionsEnabled }), { stdin, stdout, stderr });
|
|
148
152
|
}
|
|
149
153
|
if (command === "open") {
|
|
150
154
|
assertNoArgs(args, "open");
|
|
@@ -193,12 +197,12 @@ export async function run(argv, {
|
|
|
193
197
|
previousVersion: config.version
|
|
194
198
|
};
|
|
195
199
|
await writeRuntimeFiles(root, rollback);
|
|
196
|
-
const pullCode = await runtimeExecute(composeCommand("pull", root, { profile: rollback.profile }), { stdin, stdout, stderr });
|
|
200
|
+
const pullCode = await runtimeExecute(composeCommand("pull", root, { profile: rollback.profile, companionsEnabled: rollback.companionsEnabled }), { stdin, stdout, stderr });
|
|
197
201
|
if (pullCode !== 0) {
|
|
198
202
|
await writeRuntimeFiles(root, config);
|
|
199
203
|
return pullCode;
|
|
200
204
|
}
|
|
201
|
-
const upCode = await runtimeExecute(composeCommand("up", root, { profile: rollback.profile }), { stdin, stdout, stderr });
|
|
205
|
+
const upCode = await runtimeExecute(composeCommand("up", root, { profile: rollback.profile, companionsEnabled: rollback.companionsEnabled }), { stdin, stdout, stderr });
|
|
202
206
|
if (upCode !== 0) {
|
|
203
207
|
await writeRuntimeFiles(root, config);
|
|
204
208
|
return upCode;
|
|
@@ -209,7 +213,7 @@ export async function run(argv, {
|
|
|
209
213
|
}
|
|
210
214
|
if (command === "backup") {
|
|
211
215
|
assertNoArgs(args, command);
|
|
212
|
-
return runtimeExecute(composeCommand("backup", root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
216
|
+
return runtimeExecute(composeCommand("backup", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }), { stdin, stdout, stderr });
|
|
213
217
|
}
|
|
214
218
|
if (command === "restore") {
|
|
215
219
|
assertNoArgs(args, command);
|
|
@@ -223,22 +227,22 @@ export async function run(argv, {
|
|
|
223
227
|
throw new Error("Restore cancelled.");
|
|
224
228
|
}
|
|
225
229
|
const backupId = await selectLatestBackupId(root);
|
|
226
|
-
const backupCode = await runtimeExecute(composeCommand("backup", root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
230
|
+
const backupCode = await runtimeExecute(composeCommand("backup", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }), { stdin, stdout, stderr });
|
|
227
231
|
if (backupCode !== 0) return backupCode;
|
|
228
|
-
const stopCode = await runtimeExecute(composeCommand("stopForRestore", root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
232
|
+
const stopCode = await runtimeExecute(composeCommand("stopForRestore", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }), { stdin, stdout, stderr });
|
|
229
233
|
if (stopCode !== 0) return stopCode;
|
|
230
234
|
const restoreCode = await runtimeExecute(
|
|
231
235
|
composeCommand("restore", root, { backupId, profile: config.profile }),
|
|
232
236
|
{ stdin, stdout, stderr }
|
|
233
237
|
);
|
|
234
238
|
if (restoreCode !== 0) return restoreCode;
|
|
235
|
-
return runtimeExecute(composeCommand("up", root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
239
|
+
return runtimeExecute(composeCommand("up", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }), { stdin, stdout, stderr });
|
|
236
240
|
}
|
|
237
241
|
if (command === "uninstall") {
|
|
238
242
|
if (args.length === 0) {
|
|
239
243
|
stdout.write("Stopping and removing SpaceApp containers and network...\n");
|
|
240
244
|
const code = await runtimeExecute(
|
|
241
|
-
composeCommand("down", root, { profile: config.profile }),
|
|
245
|
+
composeCommand("down", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }),
|
|
242
246
|
{ stdin, stdout, stderr }
|
|
243
247
|
);
|
|
244
248
|
if (code === 0) {
|
|
@@ -260,7 +264,7 @@ export async function run(argv, {
|
|
|
260
264
|
}
|
|
261
265
|
stdout.write("Removing SpaceApp containers, network, and Docker volumes...\n");
|
|
262
266
|
const code = await runtimeExecute(
|
|
263
|
-
composeCommand("purge", root, { profile: config.profile }),
|
|
267
|
+
composeCommand("purge", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }),
|
|
264
268
|
{ stdin, stdout, stderr }
|
|
265
269
|
);
|
|
266
270
|
if (code === 0) {
|
|
@@ -299,7 +303,7 @@ async function installCommand(args, {
|
|
|
299
303
|
sleep,
|
|
300
304
|
persistSetupToken
|
|
301
305
|
}) {
|
|
302
|
-
const { requestedProfile, requestedAccessMode, noOpen } = parseInstallArgs(args);
|
|
306
|
+
const { requestedProfile, requestedAccessMode, noOpen, companionsEnabled } = parseInstallArgs(args);
|
|
303
307
|
const existingConfig = await loadExistingInstallation(root);
|
|
304
308
|
const accessMode = resolveInstallAccessMode(
|
|
305
309
|
requestedAccessMode,
|
|
@@ -324,7 +328,8 @@ async function installCommand(args, {
|
|
|
324
328
|
const result = await prepareInstallation(root, {
|
|
325
329
|
version: runtimeVersion,
|
|
326
330
|
profile,
|
|
327
|
-
accessMode
|
|
331
|
+
accessMode,
|
|
332
|
+
...(companionsEnabled ? { companionsEnabled } : {})
|
|
328
333
|
});
|
|
329
334
|
|
|
330
335
|
stdout.write(`Launcher version: ${launcherVersion}\n`);
|
|
@@ -417,6 +422,7 @@ async function installCommand(args, {
|
|
|
417
422
|
stagedStateRoot,
|
|
418
423
|
existingConfig,
|
|
419
424
|
attemptedProfile: profile,
|
|
425
|
+
companionsEnabled,
|
|
420
426
|
platform,
|
|
421
427
|
stdin,
|
|
422
428
|
stdout,
|
|
@@ -433,6 +439,7 @@ async function installCommand(args, {
|
|
|
433
439
|
composeCommand(action, root, {
|
|
434
440
|
profile,
|
|
435
441
|
stateRoot: stagedStateRoot,
|
|
442
|
+
companionsEnabled,
|
|
436
443
|
...options
|
|
437
444
|
});
|
|
438
445
|
const pullCode = await executeWithDockerDiagnostics(
|
|
@@ -575,6 +582,23 @@ async function installCommand(args, {
|
|
|
575
582
|
|
|
576
583
|
await commitInstallation(root, result.config);
|
|
577
584
|
runtimeMutationAttempted = false;
|
|
585
|
+
try {
|
|
586
|
+
await reportFirstInstallPing({
|
|
587
|
+
existingConfig,
|
|
588
|
+
env,
|
|
589
|
+
platform,
|
|
590
|
+
arch,
|
|
591
|
+
launcherVersion,
|
|
592
|
+
runtimeVersion
|
|
593
|
+
}, {
|
|
594
|
+
request,
|
|
595
|
+
timeoutMs: INSTALL_PING_TIMEOUT_MS
|
|
596
|
+
});
|
|
597
|
+
} catch (error) {
|
|
598
|
+
stderr.write(
|
|
599
|
+
`SpaceApp installation report could not be sent: ${error?.message || String(error)}\n`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
578
602
|
stdout.write(`SpaceApp is ready at ${url}\n`);
|
|
579
603
|
if (setupToken) {
|
|
580
604
|
stdout.write(`One-time setup token: ${setupToken}\n`);
|
|
@@ -640,6 +664,7 @@ async function restoreRuntimeAfterFailedInstall({
|
|
|
640
664
|
stagedStateRoot,
|
|
641
665
|
existingConfig,
|
|
642
666
|
attemptedProfile,
|
|
667
|
+
companionsEnabled,
|
|
643
668
|
platform,
|
|
644
669
|
stdin,
|
|
645
670
|
stdout,
|
|
@@ -657,7 +682,8 @@ async function restoreRuntimeAfterFailedInstall({
|
|
|
657
682
|
await commitInstallation(root, existingConfig);
|
|
658
683
|
const restoreCode = await runtimeExecute(
|
|
659
684
|
composeCommand("up", root, {
|
|
660
|
-
profile: existingConfig.profile
|
|
685
|
+
profile: existingConfig.profile,
|
|
686
|
+
companionsEnabled: existingConfig.companionsEnabled
|
|
661
687
|
})
|
|
662
688
|
);
|
|
663
689
|
if (restoreCode === 0) {
|
|
@@ -684,7 +710,8 @@ async function restoreRuntimeAfterFailedInstall({
|
|
|
684
710
|
const stopCode = await runtimeExecute(
|
|
685
711
|
composeCommand("down", root, {
|
|
686
712
|
profile: existingConfig?.profile ?? attemptedProfile,
|
|
687
|
-
stateRoot: existingConfig ? root : stagedStateRoot
|
|
713
|
+
stateRoot: existingConfig ? root : stagedStateRoot,
|
|
714
|
+
companionsEnabled: existingConfig?.companionsEnabled ?? companionsEnabled
|
|
688
715
|
})
|
|
689
716
|
);
|
|
690
717
|
if (stopCode !== 0) {
|
|
@@ -703,12 +730,17 @@ function parseInstallArgs(args) {
|
|
|
703
730
|
let requestedProfile = "auto";
|
|
704
731
|
let requestedAccessMode;
|
|
705
732
|
let noOpen = false;
|
|
733
|
+
let companionsEnabled = false;
|
|
706
734
|
for (let index = 0; index < args.length; index += 1) {
|
|
707
735
|
const argument = args[index];
|
|
708
736
|
if (argument === "--no-open" && !noOpen) {
|
|
709
737
|
noOpen = true;
|
|
710
738
|
continue;
|
|
711
739
|
}
|
|
740
|
+
if (argument === "--with-companions" && !companionsEnabled) {
|
|
741
|
+
companionsEnabled = true;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
712
744
|
if (argument === "--profile" && index + 1 < args.length) {
|
|
713
745
|
requestedProfile = args[index + 1];
|
|
714
746
|
index += 1;
|
|
@@ -728,7 +760,7 @@ function parseInstallArgs(args) {
|
|
|
728
760
|
continue;
|
|
729
761
|
}
|
|
730
762
|
throw new Error(
|
|
731
|
-
`Usage: ${UNIVERSAL_COMMAND} install [--profile auto|light|standard] [--access isolated|host-root] [--no-open]`
|
|
763
|
+
`Usage: ${UNIVERSAL_COMMAND} install [--profile auto|light|standard] [--access isolated|host-root] [--with-companions] [--no-open]`
|
|
732
764
|
);
|
|
733
765
|
}
|
|
734
766
|
if (
|
|
@@ -739,10 +771,10 @@ function parseInstallArgs(args) {
|
|
|
739
771
|
)
|
|
740
772
|
) {
|
|
741
773
|
throw new Error(
|
|
742
|
-
`Usage: ${UNIVERSAL_COMMAND} install [--profile auto|light|standard] [--access isolated|host-root] [--no-open]`
|
|
774
|
+
`Usage: ${UNIVERSAL_COMMAND} install [--profile auto|light|standard] [--access isolated|host-root] [--with-companions] [--no-open]`
|
|
743
775
|
);
|
|
744
776
|
}
|
|
745
|
-
return { requestedProfile, requestedAccessMode, noOpen };
|
|
777
|
+
return { requestedProfile, requestedAccessMode, noOpen, companionsEnabled };
|
|
746
778
|
}
|
|
747
779
|
|
|
748
780
|
async function loadExistingInstallation(root) {
|
|
@@ -804,7 +836,7 @@ async function credentialsCommand(args, { root, config, stdin, stdout, stderr, e
|
|
|
804
836
|
const value = await readSecret(stdin, stdout, `Enter ${provider} credential: `);
|
|
805
837
|
await writeCredential(root, provider, value);
|
|
806
838
|
const syncCode = await execute(
|
|
807
|
-
composeCommand("syncCredentials", root, { profile: config.profile }),
|
|
839
|
+
composeCommand("syncCredentials", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }),
|
|
808
840
|
{ stdin, stdout, stderr }
|
|
809
841
|
);
|
|
810
842
|
if (syncCode !== 0) {
|
|
@@ -820,7 +852,7 @@ async function credentialsCommand(args, { root, config, stdin, stdout, stderr, e
|
|
|
820
852
|
}
|
|
821
853
|
const removed = await removeCredential(root, provider);
|
|
822
854
|
const syncCode = await execute(
|
|
823
|
-
composeCommand("syncCredentials", root, { profile: config.profile }),
|
|
855
|
+
composeCommand("syncCredentials", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }),
|
|
824
856
|
{ stdin, stdout, stderr }
|
|
825
857
|
);
|
|
826
858
|
if (syncCode !== 0) {
|
|
@@ -838,13 +870,13 @@ async function providerCommand(args, { root, config, stdin, stdout, stderr, exec
|
|
|
838
870
|
throw new Error(`Usage: ${UNIVERSAL_COMMAND} provider install claude`);
|
|
839
871
|
}
|
|
840
872
|
stdout.write("Installing Claude Code from Anthropic into this installation's private provider volume.\n");
|
|
841
|
-
return execute(composeCommand("installClaude", root, { profile: config.profile }), { stdin, stdout, stderr });
|
|
873
|
+
return execute(composeCommand("installClaude", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }), { stdin, stdout, stderr });
|
|
842
874
|
}
|
|
843
875
|
|
|
844
876
|
async function ownerCommand(args, { root, config, stdin, stdout, stderr, execute }) {
|
|
845
877
|
if (args.length === 1 && args[0] === "rotate-setup-token") {
|
|
846
878
|
const token = randomBytes(32).toString("base64url");
|
|
847
|
-
const code = await execute(composeCommand("rotateOwnerSetupToken", root, { profile: config.profile }), {
|
|
879
|
+
const code = await execute(composeCommand("rotateOwnerSetupToken", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }), {
|
|
848
880
|
stdin,
|
|
849
881
|
stdout,
|
|
850
882
|
stderr,
|
|
@@ -865,7 +897,7 @@ async function ownerCommand(args, { root, config, stdin, stdout, stderr, execute
|
|
|
865
897
|
if (password.length < 6) {
|
|
866
898
|
throw new Error("Owner password must be at least 6 characters.");
|
|
867
899
|
}
|
|
868
|
-
return execute(composeCommand("resetOwnerPassword", root, { profile: config.profile }), {
|
|
900
|
+
return execute(composeCommand("resetOwnerPassword", root, { profile: config.profile, companionsEnabled: config.companionsEnabled }), {
|
|
869
901
|
stdin,
|
|
870
902
|
stdout,
|
|
871
903
|
stderr,
|
|
@@ -886,12 +918,12 @@ async function updateCommand(args, { root, config, version, stdin, stdout, stder
|
|
|
886
918
|
previousVersion: config.version
|
|
887
919
|
};
|
|
888
920
|
await writeRuntimeFiles(root, updated);
|
|
889
|
-
const pullCode = await execute(composeCommand("pull", root, { profile: updated.profile }), { stdin, stdout, stderr });
|
|
921
|
+
const pullCode = await execute(composeCommand("pull", root, { profile: updated.profile, companionsEnabled: updated.companionsEnabled }), { stdin, stdout, stderr });
|
|
890
922
|
if (pullCode !== 0) {
|
|
891
923
|
await writeRuntimeFiles(root, config);
|
|
892
924
|
return pullCode;
|
|
893
925
|
}
|
|
894
|
-
const upCode = await execute(composeCommand("up", root, { profile: updated.profile }), { stdin, stdout, stderr });
|
|
926
|
+
const upCode = await execute(composeCommand("up", root, { profile: updated.profile, companionsEnabled: updated.companionsEnabled }), { stdin, stdout, stderr });
|
|
895
927
|
if (upCode !== 0) {
|
|
896
928
|
await writeRuntimeFiles(root, config);
|
|
897
929
|
return upCode;
|
|
@@ -1258,7 +1290,7 @@ Usage: ${UNIVERSAL_COMMAND} <command>
|
|
|
1258
1290
|
spaceapp <command> Optional global launcher
|
|
1259
1291
|
|
|
1260
1292
|
init Create a local SpaceApp installation
|
|
1261
|
-
install [--profile auto|light|standard] [--access isolated|host-root] [--no-open]
|
|
1293
|
+
install [--profile auto|light|standard] [--access isolated|host-root] [--with-companions] [--no-open]
|
|
1262
1294
|
Install prerequisites, initialize, and start
|
|
1263
1295
|
up | down | status | logs Manage the Docker application
|
|
1264
1296
|
open Open the local web application
|
package/src/index.mjs
CHANGED
|
@@ -56,7 +56,7 @@ const SECRET_FIELD = /password|secret|token|api.?key|credential/i;
|
|
|
56
56
|
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
57
57
|
const BACKUP_ID_PATTERN = /^spaceapp-backup-\d{8}T\d{9}Z$/;
|
|
58
58
|
const PROVIDERS = Object.freeze({
|
|
59
|
-
bundled: Object.freeze(["
|
|
59
|
+
bundled: Object.freeze(["opencode", "codex", "gemini", "qwen", "kimi", "grok", "autohand", "cursor", "copilot"]),
|
|
60
60
|
ownerInstalled: Object.freeze(["claude"]),
|
|
61
61
|
experimental: Object.freeze(["deepseek"])
|
|
62
62
|
});
|
|
@@ -70,7 +70,8 @@ const CONFIG_KEYS = new Set([
|
|
|
70
70
|
"telemetry",
|
|
71
71
|
"profile",
|
|
72
72
|
"accessMode",
|
|
73
|
-
"workspaces"
|
|
73
|
+
"workspaces",
|
|
74
|
+
"companionsEnabled"
|
|
74
75
|
]);
|
|
75
76
|
|
|
76
77
|
export function resolveSpaceAppHome({
|
|
@@ -153,7 +154,8 @@ export function installResourceChecks(resources) {
|
|
|
153
154
|
export function createDefaultConfig({
|
|
154
155
|
version,
|
|
155
156
|
profile = "light",
|
|
156
|
-
accessMode = "isolated"
|
|
157
|
+
accessMode = "isolated",
|
|
158
|
+
companionsEnabled = false
|
|
157
159
|
}) {
|
|
158
160
|
assertVersion(version);
|
|
159
161
|
if (profile !== "light" && profile !== "standard") {
|
|
@@ -169,6 +171,7 @@ export function createDefaultConfig({
|
|
|
169
171
|
telemetry: false,
|
|
170
172
|
profile,
|
|
171
173
|
accessMode: resolvedAccessMode,
|
|
174
|
+
companionsEnabled,
|
|
172
175
|
workspaces: []
|
|
173
176
|
};
|
|
174
177
|
}
|
|
@@ -207,6 +210,9 @@ export function validateConfig(config) {
|
|
|
207
210
|
if (config.accessMode !== "isolated" && config.accessMode !== "host-root") {
|
|
208
211
|
throw new Error("accessMode must be isolated or host-root.");
|
|
209
212
|
}
|
|
213
|
+
if (typeof config.companionsEnabled !== "boolean") {
|
|
214
|
+
throw new Error("companionsEnabled must be boolean.");
|
|
215
|
+
}
|
|
210
216
|
if (!Array.isArray(config.workspaces)) {
|
|
211
217
|
throw new Error("workspaces must be an array.");
|
|
212
218
|
}
|
|
@@ -234,7 +240,8 @@ function migrateConfig(config) {
|
|
|
234
240
|
return {
|
|
235
241
|
...config,
|
|
236
242
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
237
|
-
accessMode: "isolated"
|
|
243
|
+
accessMode: "isolated",
|
|
244
|
+
companionsEnabled: false
|
|
238
245
|
};
|
|
239
246
|
}
|
|
240
247
|
if (config?.schemaVersion !== 1) {
|
|
@@ -248,7 +255,8 @@ function migrateConfig(config) {
|
|
|
248
255
|
...config,
|
|
249
256
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
250
257
|
profile: legacyProfiles[config.profile] ?? config.profile,
|
|
251
|
-
accessMode: "isolated"
|
|
258
|
+
accessMode: "isolated",
|
|
259
|
+
companionsEnabled: false
|
|
252
260
|
};
|
|
253
261
|
}
|
|
254
262
|
|
|
@@ -355,6 +363,7 @@ export function renderRuntimeEnv(config) {
|
|
|
355
363
|
`SPACEAPP_POSTGRES_CPU_LIMIT=${settings.postgresCpuLimit}`,
|
|
356
364
|
`SPACEAPP_TEMPORAL_MEMORY_LIMIT=${settings.temporalMemoryLimit}`,
|
|
357
365
|
`SPACEAPP_TEMPORAL_CPU_LIMIT=${settings.temporalCpuLimit}`,
|
|
366
|
+
`SPACEAPP_COMPANIONS_ENABLED=${config.companionsEnabled ? "true" : "false"}`,
|
|
358
367
|
""
|
|
359
368
|
].join("\n");
|
|
360
369
|
}
|
|
@@ -425,7 +434,8 @@ export function composeCommand(action, root, options = {}) {
|
|
|
425
434
|
"-f", join(stateRoot, "compose.yml"),
|
|
426
435
|
"-f", join(stateRoot, "compose.workspaces.yml"),
|
|
427
436
|
"-f", join(stateRoot, "compose.host-access.yml"),
|
|
428
|
-
...(profile === "standard" ? ["--profile", "standard"] : [])
|
|
437
|
+
...(profile === "standard" ? ["--profile", "standard"] : []),
|
|
438
|
+
...(options.companionsEnabled ? ["--profile", "companions"] : [])
|
|
429
439
|
];
|
|
430
440
|
const actions = {
|
|
431
441
|
up: ["up", "-d", "--remove-orphans"],
|
|
@@ -539,7 +549,8 @@ export async function writeRuntimeFiles(root, config) {
|
|
|
539
549
|
export async function prepareInstallation(root, {
|
|
540
550
|
version,
|
|
541
551
|
profile,
|
|
542
|
-
accessMode
|
|
552
|
+
accessMode,
|
|
553
|
+
companionsEnabled
|
|
543
554
|
}) {
|
|
544
555
|
validateHome(root);
|
|
545
556
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
@@ -560,7 +571,8 @@ export async function prepareInstallation(root, {
|
|
|
560
571
|
config = createDefaultConfig({
|
|
561
572
|
version,
|
|
562
573
|
profile: resolvedProfile ?? "light",
|
|
563
|
-
accessMode: resolveInstallAccessMode(accessMode)
|
|
574
|
+
accessMode: resolveInstallAccessMode(accessMode),
|
|
575
|
+
companionsEnabled
|
|
564
576
|
});
|
|
565
577
|
}
|
|
566
578
|
if (config.version !== version) {
|
|
@@ -577,6 +589,9 @@ export async function prepareInstallation(root, {
|
|
|
577
589
|
if (config.accessMode !== resolvedAccessMode) {
|
|
578
590
|
config = { ...config, accessMode: resolvedAccessMode };
|
|
579
591
|
}
|
|
592
|
+
if (companionsEnabled !== undefined && config.companionsEnabled !== companionsEnabled) {
|
|
593
|
+
config = { ...config, companionsEnabled };
|
|
594
|
+
}
|
|
580
595
|
validateConfig(config);
|
|
581
596
|
|
|
582
597
|
const postgresPasswordPath = join(root, "secrets", "postgres-password");
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const INSTALL_PING_ENDPOINT = "https://spaceapp.dev/install-ping.php";
|
|
4
|
+
export const INSTALL_PING_TIMEOUT_MS = 5_000;
|
|
5
|
+
export const TELEMETRY_DISABLED_VALUES = new Set(["0", "false", "no", "off"]);
|
|
6
|
+
|
|
7
|
+
export function isTelemetryEnabled(env = process.env) {
|
|
8
|
+
const value = String(env.SPACEAPP_TELEMETRY ?? "").trim().toLowerCase();
|
|
9
|
+
if (value === "") {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return !TELEMETRY_DISABLED_VALUES.has(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeOs(platform) {
|
|
16
|
+
if (platform === "win32") return "windows";
|
|
17
|
+
if (platform === "darwin") return "macos";
|
|
18
|
+
if (platform === "linux") return "linux";
|
|
19
|
+
return String(platform || "other");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildInstallPing({
|
|
23
|
+
platform,
|
|
24
|
+
arch,
|
|
25
|
+
launcherVersion,
|
|
26
|
+
runtimeVersion,
|
|
27
|
+
now = new Date()
|
|
28
|
+
}) {
|
|
29
|
+
return {
|
|
30
|
+
app: "spaceapp",
|
|
31
|
+
uid: randomUUID(),
|
|
32
|
+
os: normalizeOs(platform),
|
|
33
|
+
arch: String(arch || "unknown"),
|
|
34
|
+
version: String(launcherVersion || "unknown"),
|
|
35
|
+
runtime: String(runtimeVersion || "unknown"),
|
|
36
|
+
install_date: now.toISOString().slice(0, 10)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function sendInstallPing(
|
|
41
|
+
payload,
|
|
42
|
+
{
|
|
43
|
+
request = globalThis.fetch,
|
|
44
|
+
endpoint = INSTALL_PING_ENDPOINT,
|
|
45
|
+
timeoutMs = INSTALL_PING_TIMEOUT_MS
|
|
46
|
+
} = {}
|
|
47
|
+
) {
|
|
48
|
+
if (typeof request !== "function") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
53
|
+
try {
|
|
54
|
+
const response = await request(endpoint, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
"content-type": "application/json",
|
|
58
|
+
"user-agent": `spaceapp-launcher/${payload.version}`
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify(payload),
|
|
61
|
+
signal: controller.signal
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`install ping rejected with HTTP ${response.status}`);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function reportFirstInstallPing(
|
|
73
|
+
{
|
|
74
|
+
existingConfig,
|
|
75
|
+
env = process.env,
|
|
76
|
+
platform,
|
|
77
|
+
arch,
|
|
78
|
+
launcherVersion,
|
|
79
|
+
runtimeVersion
|
|
80
|
+
},
|
|
81
|
+
options = {}
|
|
82
|
+
) {
|
|
83
|
+
if (existingConfig) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (!isTelemetryEnabled(env)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const payload = buildInstallPing({ platform, arch, launcherVersion, runtimeVersion });
|
|
90
|
+
const sent = await sendInstallPing(payload, options);
|
|
91
|
+
return sent ? payload : null;
|
|
92
|
+
}
|
package/templates/compose.yml
CHANGED
|
@@ -117,6 +117,36 @@ services:
|
|
|
117
117
|
networks:
|
|
118
118
|
- spaceapp
|
|
119
119
|
|
|
120
|
+
codex-lb:
|
|
121
|
+
image: ghcr.io/soju06/codex-lb:1.22.0
|
|
122
|
+
profiles: ["companions"]
|
|
123
|
+
restart: unless-stopped
|
|
124
|
+
init: true
|
|
125
|
+
mem_limit: "768m"
|
|
126
|
+
cpus: "1.0"
|
|
127
|
+
ports:
|
|
128
|
+
- "${SPACEAPP_BIND_HOST}:2455:2455"
|
|
129
|
+
- "${SPACEAPP_BIND_HOST}:1455:1455"
|
|
130
|
+
volumes:
|
|
131
|
+
- spaceapp-codex-lb:/var/lib/codex-lb
|
|
132
|
+
networks:
|
|
133
|
+
- spaceapp
|
|
134
|
+
|
|
135
|
+
headroom:
|
|
136
|
+
image: ghcr.io/oll4com/headroom-codex-lb:code
|
|
137
|
+
profiles: ["companions"]
|
|
138
|
+
restart: unless-stopped
|
|
139
|
+
init: true
|
|
140
|
+
mem_limit: "768m"
|
|
141
|
+
cpus: "1.0"
|
|
142
|
+
environment:
|
|
143
|
+
HEADROOM_HOST: "0.0.0.0"
|
|
144
|
+
OPENAI_TARGET_API_URL: "${SPACEAPP_COMPANIONS_OPENAI_TARGET_API_URL:-}"
|
|
145
|
+
ports:
|
|
146
|
+
- "${SPACEAPP_BIND_HOST}:8787:8787"
|
|
147
|
+
networks:
|
|
148
|
+
- spaceapp
|
|
149
|
+
|
|
120
150
|
postgres:
|
|
121
151
|
image: pgvector/pgvector:0.8.3-pg17-trixie
|
|
122
152
|
restart: unless-stopped
|
|
@@ -187,3 +217,4 @@ volumes:
|
|
|
187
217
|
spaceapp-memory:
|
|
188
218
|
spaceapp-postgres:
|
|
189
219
|
spaceapp-workspaces:
|
|
220
|
+
spaceapp-codex-lb:
|