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 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.15-hostroot.2",
4
- "spaceappRuntimeVersion": "0.1.15-hostroot.2",
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": "61c72544f7cb348803e2f1f71e467bf5aa86ded3"
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(["codex", "gemini", "opencode", "qwen", "kimi", "grok"]),
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
+ }
@@ -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: