run-spaceapp 1.0.2 → 1.0.35

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/src/index.mjs CHANGED
@@ -20,17 +20,41 @@ import {
20
20
  import { UNIVERSAL_COMMAND } from "./package-info.mjs";
21
21
 
22
22
  const CONFIG_SCHEMA_VERSION = 4;
23
- const MIN_INSTALL_CPU_COUNT = 4;
24
- const MIN_INSTALL_MEMORY_BYTES = 7 * 1024 ** 3;
25
- const MIN_INSTALL_MEMORY_LABEL = "7 GiB usable (8 GB-class system)";
26
23
  const CONTAINER_SECRET_MODE = 0o644;
27
- const MIN_INSTALL_FREE_DISK_BYTES = 15 * 1024 ** 3;
24
+ export const MIN_INSTALL_FREE_DISK_BYTES_STANDARD = 15 * 1024 ** 3;
25
+ export const MIN_INSTALL_FREE_DISK_BYTES_LIGHT = 7 * 1024 ** 3;
26
+ export const MIN_INSTALL_FREE_DISK_BYTES = MIN_INSTALL_FREE_DISK_BYTES_STANDARD;
28
27
  const PROFILE_RUNTIME_SETTINGS = Object.freeze({
28
+ small: Object.freeze({
29
+ browserEnabled: false, temporalEnabled: false,
30
+ coreMemoryLimit: "2g", coreCpuLimit: "2.0",
31
+ cliMemoryLimit: "2g", cliCpuLimit: "1.5",
32
+ browserMemoryLimit: "1536m", browserCpuLimit: "1.5",
33
+ postgresMemoryLimit: "768m", postgresCpuLimit: "1.0",
34
+ temporalMemoryLimit: "768m", temporalCpuLimit: "1.0"
35
+ }),
36
+ medium: Object.freeze({
37
+ browserEnabled: false, temporalEnabled: true,
38
+ coreMemoryLimit: "3g", coreCpuLimit: "3.0",
39
+ cliMemoryLimit: "3g", cliCpuLimit: "2.0",
40
+ browserMemoryLimit: "1536m", browserCpuLimit: "1.5",
41
+ postgresMemoryLimit: "1g", postgresCpuLimit: "1.0",
42
+ temporalMemoryLimit: "768m", temporalCpuLimit: "1.0"
43
+ }),
44
+ large: Object.freeze({
45
+ browserEnabled: true, temporalEnabled: true,
46
+ coreMemoryLimit: "4g", coreCpuLimit: "4.0",
47
+ cliMemoryLimit: "4g", cliCpuLimit: "3.0",
48
+ browserMemoryLimit: "2g", browserCpuLimit: "2.0",
49
+ postgresMemoryLimit: "1g", postgresCpuLimit: "2.0",
50
+ temporalMemoryLimit: "1g", temporalCpuLimit: "2.0"
51
+ }),
29
52
  light: Object.freeze({
30
53
  browserEnabled: false,
54
+ temporalEnabled: true,
31
55
  coreMemoryLimit: "2g",
32
56
  coreCpuLimit: "2.0",
33
- cliMemoryLimit: "1536m",
57
+ cliMemoryLimit: "2g",
34
58
  cliCpuLimit: "1.5",
35
59
  browserMemoryLimit: "1536m",
36
60
  browserCpuLimit: "1.5",
@@ -41,6 +65,7 @@ const PROFILE_RUNTIME_SETTINGS = Object.freeze({
41
65
  }),
42
66
  standard: Object.freeze({
43
67
  browserEnabled: true,
68
+ temporalEnabled: true,
44
69
  coreMemoryLimit: "4g",
45
70
  coreCpuLimit: "4.0",
46
71
  cliMemoryLimit: "3g",
@@ -53,12 +78,27 @@ const PROFILE_RUNTIME_SETTINGS = Object.freeze({
53
78
  temporalCpuLimit: "2.0"
54
79
  })
55
80
  });
81
+ export const INSTALL_PROFILES = Object.freeze(["small", "medium", "large", "light", "standard"]);
82
+ export const PROFILE_REQUIREMENTS = Object.freeze({
83
+ small: { cpuCount: 4, memoryBytes: 7 * 1024 ** 3, freshDiskBytes: 6.5 * 1024 ** 3 },
84
+ medium: { cpuCount: 4, memoryBytes: 11 * 1024 ** 3, freshDiskBytes: 8 * 1024 ** 3 },
85
+ large: { cpuCount: 8, memoryBytes: 15 * 1024 ** 3, freshDiskBytes: 11 * 1024 ** 3 },
86
+ // Existing installations keep their settings and background integrations.
87
+ light: { cpuCount: 4, memoryBytes: 7 * 1024 ** 3, freshDiskBytes: 8 * 1024 ** 3 },
88
+ standard: { cpuCount: 4, memoryBytes: 7 * 1024 ** 3, freshDiskBytes: 11 * 1024 ** 3 }
89
+ });
90
+
91
+ export function profileRuntimeSettings(profile) {
92
+ const settings = PROFILE_RUNTIME_SETTINGS[profile];
93
+ if (!settings) throw new Error("Install profile must be small, medium, large, light, or standard.");
94
+ return settings;
95
+ }
56
96
  const SECRET_FIELD = /password|secret|token|api.?key|credential/i;
57
97
  const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
58
98
  const BACKUP_ID_PATTERN = /^spaceapp-backup-\d{8}T\d{9}Z$/;
59
99
  const PROVIDERS = Object.freeze({
60
- bundled: Object.freeze(["opencode", "codex", "gemini", "qwen", "kimi", "grok", "autohand", "cursor", "copilot"]),
61
- ownerInstalled: Object.freeze(["claude"]),
100
+ bundled: Object.freeze(["opencode"]),
101
+ ownerInstalled: Object.freeze(["codex", "gemini", "qwen", "kimi", "grok", "autohand", "cursor", "copilot", "claude"]),
62
102
  experimental: Object.freeze(["deepseek"])
63
103
  });
64
104
  const ALL_PROVIDERS = new Set(Object.values(PROVIDERS).flat());
@@ -99,17 +139,23 @@ export function resolveSpaceAppHome({
99
139
  return resolve(env.XDG_CONFIG_HOME || join(home, ".config"), "spaceapp");
100
140
  }
101
141
 
102
- export function resolveInstallProfile(requestedProfile, totalMemoryBytes) {
103
- if (requestedProfile === "light" || requestedProfile === "standard") {
142
+ export function resolveInstallProfile(requestedProfile, resources) {
143
+ if (INSTALL_PROFILES.includes(requestedProfile)) {
104
144
  return requestedProfile;
105
145
  }
106
146
  if (requestedProfile !== "auto") {
107
- throw new Error("Install profile must be auto, light, or standard.");
147
+ throw new Error("Install profile must be auto, small, medium, large, light, or standard.");
108
148
  }
149
+ const totalMemoryBytes = typeof resources === "number" ? resources : resources?.totalMemoryBytes;
109
150
  if (!Number.isFinite(totalMemoryBytes) || totalMemoryBytes <= 0) {
110
151
  throw new Error("Total system memory must be available for automatic profile selection.");
111
152
  }
112
- return "light";
153
+ // Memory-only callers cannot safely choose a larger footprint.
154
+ if (typeof resources === "number") return "small";
155
+ for (const profile of ["large", "medium", "small"]) {
156
+ if (installResourceChecks(resources, profile).every((check) => check.ok)) return profile;
157
+ }
158
+ return "small";
113
159
  }
114
160
 
115
161
  export function resolveInstallAccessMode(requestedMode, existingMode = "isolated") {
@@ -198,29 +244,37 @@ export async function inspectSystemResources(root) {
198
244
  };
199
245
  }
200
246
 
201
- export function installResourceChecks(resources) {
247
+ export function installResourceChecks(resources, profile = "small", { requiredDiskBytes, operation = "fresh" } = {}) {
202
248
  const cpuCount = Number(resources?.cpuCount);
203
249
  const totalMemoryBytes = Number(resources?.totalMemoryBytes);
204
250
  const freeDiskBytes = Number(resources?.freeDiskBytes);
205
251
  if (![cpuCount, totalMemoryBytes, freeDiskBytes].every((value) => Number.isFinite(value) && value >= 0)) {
206
252
  throw new Error("System CPU, memory, and free-disk information is required.");
207
253
  }
254
+ const requirements = PROFILE_REQUIREMENTS[profile];
255
+ if (!requirements) throw new Error("Unknown installation profile.");
256
+ const minFreeDisk = requiredDiskBytes ?? (operation === "fresh" ? requirements.freshDiskBytes : 512 * 1024 ** 2);
257
+ const engineMemoryBytes = { small: 3, medium: 5, large: 7, light: 3, standard: 3 }[profile] * 1024 ** 3;
208
258
  return [
209
259
  {
210
260
  name: "CPU",
211
- ok: cpuCount >= MIN_INSTALL_CPU_COUNT,
212
- detail: `${cpuCount} available; ${MIN_INSTALL_CPU_COUNT} required`
261
+ ok: cpuCount >= requirements.cpuCount,
262
+ detail: `${cpuCount} available; ${requirements.cpuCount} required`
213
263
  },
214
264
  {
215
265
  name: "Memory",
216
- ok: totalMemoryBytes >= MIN_INSTALL_MEMORY_BYTES,
217
- detail: `${formatGibibytes(totalMemoryBytes)} GiB available; ${MIN_INSTALL_MEMORY_LABEL} required`
266
+ ok: totalMemoryBytes >= requirements.memoryBytes,
267
+ detail: `${formatGibibytes(totalMemoryBytes)} GiB available; ${formatGibibytes(requirements.memoryBytes)} GiB usable${requirements.memoryBytes === 7 * 1024 ** 3 ? " (8 GB-class system)" : ""} required`
218
268
  },
219
269
  {
220
270
  name: "Free disk",
221
- ok: freeDiskBytes >= MIN_INSTALL_FREE_DISK_BYTES,
222
- detail: `${formatGibibytes(freeDiskBytes)} GiB available; ${formatGibibytes(MIN_INSTALL_FREE_DISK_BYTES)} GiB required`
223
- }
271
+ ok: freeDiskBytes >= minFreeDisk,
272
+ detail: `${formatGibibytes(freeDiskBytes)} GiB available; ${formatGibibytes(minFreeDisk)} GiB required${operation === "fresh" ? "" : " additionally for " + operation}`
273
+ },
274
+ ...(resources.engineMemoryBytes > 0 ? [{ name: "Docker memory", ok: resources.engineMemoryBytes >= engineMemoryBytes,
275
+ detail: `${formatGibibytes(resources.engineMemoryBytes)} GiB allocated; ${formatGibibytes(engineMemoryBytes)} GiB required for ${profile}` }] : []),
276
+ ...(resources.engineCpuCount > 0 ? [{ name: "Docker CPU", ok: resources.engineCpuCount >= 4,
277
+ detail: `${resources.engineCpuCount} allocated; 4 required` }] : [])
224
278
  ];
225
279
  }
226
280
 
@@ -231,8 +285,8 @@ export function createDefaultConfig({
231
285
  companionsEnabled = false
232
286
  }) {
233
287
  assertVersion(version);
234
- if (profile !== "light" && profile !== "standard") {
235
- throw new Error("Default config requires a resolved light or standard profile.");
288
+ if (!INSTALL_PROFILES.includes(profile)) {
289
+ throw new Error("Default config requires a resolved installation profile.");
236
290
  }
237
291
  const resolvedAccessMode = resolveInstallAccessMode(accessMode);
238
292
  return {
@@ -277,8 +331,8 @@ export function validateConfig(config) {
277
331
  if (typeof config.telemetry !== "boolean") {
278
332
  throw new Error("telemetry must be boolean.");
279
333
  }
280
- if (!["light", "standard"].includes(config.profile)) {
281
- throw new Error("profile must be light or standard.");
334
+ if (!INSTALL_PROFILES.includes(config.profile)) {
335
+ throw new Error("profile must be small, medium, large, light, or standard.");
282
336
  }
283
337
  if (config.accessMode !== "isolated" && config.accessMode !== "host-root") {
284
338
  throw new Error("accessMode must be isolated or host-root.");
@@ -487,6 +541,7 @@ export function renderRuntimeEnv(config) {
487
541
  `SPACEAPP_TELEMETRY=${config.telemetry}`,
488
542
  `SPACEAPP_PROFILE=${safeEnv(config.profile)}`,
489
543
  `SPACEAPP_BROWSER_ENABLED=${settings.browserEnabled}`,
544
+ `SPACEAPP_WORKFLOWS_ENABLED=${settings.temporalEnabled}`,
490
545
  `SPACEAPP_CORE_MEMORY_LIMIT=${settings.coreMemoryLimit}`,
491
546
  `SPACEAPP_CORE_CPU_LIMIT=${settings.coreCpuLimit}`,
492
547
  `SPACEAPP_CLI_MEMORY_LIMIT=${settings.cliMemoryLimit}`,
@@ -557,9 +612,7 @@ export function composeCommand(action, root, options = {}) {
557
612
  const stateRoot = options.stateRoot ?? root;
558
613
  validateHome(stateRoot);
559
614
  const profile = options.profile ?? "standard";
560
- if (profile !== "light" && profile !== "standard") {
561
- throw new Error("Compose profile must be light or standard.");
562
- }
615
+ const settings = profileRuntimeSettings(profile);
563
616
  const base = [
564
617
  "compose",
565
618
  "--project-name", composeProjectName(root),
@@ -568,7 +621,8 @@ export function composeCommand(action, root, options = {}) {
568
621
  "-f", join(stateRoot, "compose.yml"),
569
622
  "-f", join(stateRoot, "compose.workspaces.yml"),
570
623
  "-f", join(stateRoot, "compose.host-access.yml"),
571
- ...(profile === "standard" ? ["--profile", "standard"] : []),
624
+ ...(settings.browserEnabled ? ["--profile", "standard"] : []),
625
+ ...(settings.temporalEnabled ? ["--profile", "workflows"] : []),
572
626
  ...(options.companionsEnabled ? ["--profile", "companions"] : [])
573
627
  ];
574
628
  const actions = {
@@ -617,11 +671,20 @@ export function composeCommand(action, root, options = {}) {
617
671
  "--stdin"
618
672
  ],
619
673
  removeBrowser: ["rm", "--stop", "--force", "spaceapp-browser"],
674
+ removeTemporal: ["rm", "--stop", "--force", "temporal"],
675
+ enableTemporal: ["up", "-d", "--force-recreate", "temporal"],
676
+ databaseSize: ["exec", "-T", "postgres", "psql", "-U", "spaceapp", "-d", "spaceapp", "-Atc", "SELECT pg_database_size(current_database())"],
677
+ storageFree: ["exec", "-T", "spaceapp-core", "node", "-e", "const fs=require('node:fs');const s=fs.statfsSync('/var/lib/spaceapp');console.log(Number(s.bavail)*Number(s.bsize))"],
620
678
  purge: ["down", "--volumes", "--remove-orphans"],
621
679
  repair: ["up", "-d", "--remove-orphans", "--force-recreate"],
680
+ ensureCheckpointDatabase: ["up", "-d", "--no-deps", "--wait", "--wait-timeout", "120", "postgres"],
622
681
  checkpointDump: ["exec", "-T", "postgres", "pg_dump", "-c", "--if-exists", "-U", "spaceapp", "-d", "spaceapp"],
623
682
  checkpointRestore: ["exec", "-T", "postgres", "psql", "-v", "ON_ERROR_STOP=1", "-U", "spaceapp", "-d", "spaceapp"]
624
683
  };
684
+ if (action === "installProvider") {
685
+ assertProvider(options.provider);
686
+ actions.installProvider = ["exec", "-T", "--user", "10001:10001", "spaceapp-cli", "node", "/app/deploy/docker/provider-install.mjs", options.provider];
687
+ }
625
688
  let selected = actions[action];
626
689
  if (action === "restore") {
627
690
  if (typeof options.backupId !== "string" || !BACKUP_ID_PATTERN.test(options.backupId)) {
@@ -697,7 +760,7 @@ export async function prepareInstallation(root, {
697
760
  ]);
698
761
  const resolvedProfile = profile === undefined
699
762
  ? undefined
700
- : resolveInstallProfile(profile, totalmem());
763
+ : resolveInstallProfile(profile, profile === "auto" ? await inspectSystemResources(root) : totalmem());
701
764
  let config;
702
765
  try {
703
766
  config = await loadConfig(root);
@@ -786,7 +849,8 @@ export async function prepareInstallation(root, {
786
849
  }
787
850
 
788
851
  export async function commitInstallation(root, config, {
789
- templateDir = defaultTemplateDir()
852
+ templateDir = defaultTemplateDir(),
853
+ persistConfig = true
790
854
  } = {}) {
791
855
  validateHome(root);
792
856
  validateConfig(config);
@@ -794,7 +858,7 @@ export async function commitInstallation(root, config, {
794
858
  await mkdir(root, { recursive: true, mode: 0o700 });
795
859
  await atomicWrite(join(root, "compose.yml"), composeTemplate);
796
860
  await writeRuntimeFiles(root, config);
797
- await saveConfig(root, config);
861
+ if (persistConfig) await saveConfig(root, config);
798
862
  }
799
863
 
800
864
  export async function initializeInstallation(root, options) {
@@ -73,6 +73,8 @@ export function windowsPowerShellArgs(operation) {
73
73
  "exit $process.ExitCode"
74
74
  ].join("; "),
75
75
  "start-docker-desktop": [
76
+ "Set-Service -Name 'com.docker.service' -StartupType Automatic -ErrorAction SilentlyContinue",
77
+ "Start-Service -Name 'com.docker.service' -ErrorAction SilentlyContinue",
76
78
  "$path = $env:SPACEAPP_DOCKER_DESKTOP_PATH",
77
79
  "if ([string]::IsNullOrWhiteSpace($path)) { exit 1 }",
78
80
  "Start-Process -FilePath $path"
@@ -164,7 +166,8 @@ export async function ensureDockerAvailable({
164
166
  launch,
165
167
  download,
166
168
  pathExists,
167
- sleep
169
+ sleep,
170
+ installArgs
168
171
  });
169
172
  }
170
173
  return ensureLinuxDocker({
@@ -201,7 +204,12 @@ async function ensureWindowsDocker({
201
204
 
202
205
  if (!application) {
203
206
  stdout.write("Docker Desktop is required and is not installed.\n");
204
- const accepted = await confirmDesktopInstall({ platformName: "Windows", stdin, stdout });
207
+ const accepted = await confirmDesktopInstall({
208
+ platformName: "Windows",
209
+ stdin,
210
+ stdout,
211
+ autoConfirm: Boolean(installArgs?.autoConfirm)
212
+ });
205
213
  if (!accepted) {
206
214
  stderr.write("Docker Desktop installation was cancelled.\n");
207
215
  return { code: 1, reexecuted: false };
@@ -404,7 +412,8 @@ async function ensureMacDocker({
404
412
  launch,
405
413
  download,
406
414
  pathExists,
407
- sleep
415
+ sleep,
416
+ installArgs
408
417
  }) {
409
418
  const application = "/Applications/Docker.app";
410
419
  const cliDirectory = posix.join(application, "Contents", "Resources", "bin");
@@ -412,7 +421,12 @@ async function ensureMacDocker({
412
421
 
413
422
  if (!installed) {
414
423
  stdout.write("Docker Desktop is required and is not installed.\n");
415
- const accepted = await confirmDesktopInstall({ platformName: "macOS", stdin, stdout });
424
+ const accepted = await confirmDesktopInstall({
425
+ platformName: "macOS",
426
+ stdin,
427
+ stdout,
428
+ autoConfirm: Boolean(installArgs?.autoConfirm)
429
+ });
416
430
  if (!accepted) {
417
431
  stderr.write("Docker Desktop installation was cancelled.\n");
418
432
  return { code: 1, reexecuted: false };
@@ -508,12 +522,12 @@ async function ensureMacDocker({
508
522
  stderr.write("Docker Desktop is installed, but SpaceApp could not start it.\n");
509
523
  return { code: launchCode, reexecuted: false };
510
524
  }
511
- if (await waitForDocker({ execute, sleep })) {
525
+ if (await waitForDocker({ execute, sleep, attempts: 300 })) {
512
526
  stdout.write("Docker Desktop is ready.\n");
513
527
  return { code: 0, reexecuted: false };
514
528
  }
515
529
  stderr.write(
516
- `Docker Desktop was installed but did not become ready. Complete any Docker Desktop prompt, then run "${UNIVERSAL_INSTALL_COMMAND}" again.\n`
530
+ `Docker Desktop did not become ready after 10 minutes. Complete any Docker Desktop prompt, then run "${UNIVERSAL_INSTALL_COMMAND}" again.\n`
517
531
  );
518
532
  return { code: 1, reexecuted: false };
519
533
  }
@@ -526,6 +540,7 @@ async function ensureLinuxDocker({
526
540
  stderr,
527
541
  execute,
528
542
  download,
543
+ sleep,
529
544
  osRelease,
530
545
  installArgs
531
546
  }) {
@@ -557,7 +572,7 @@ async function ensureLinuxDocker({
557
572
  stderr.write("Docker Engine was installed, but its system service could not be started.\n");
558
573
  return { code: startCode, reexecuted: false };
559
574
  }
560
- if (await dockerIsReady(execute)) {
575
+ if (await waitForDocker({ execute, sleep, attempts: 15 })) {
561
576
  stdout.write("Docker Engine is ready.\n");
562
577
  return { code: 0, reexecuted: false };
563
578
  }
@@ -596,7 +611,11 @@ async function ensureLinuxDocker({
596
611
  return { code: reentryCode, reexecuted: true };
597
612
  }
598
613
 
599
- async function confirmDesktopInstall({ platformName, stdin, stdout }) {
614
+ async function confirmDesktopInstall({ platformName, stdin, stdout, autoConfirm = false }) {
615
+ if (autoConfirm) {
616
+ stdout.write(`Installing Docker Desktop for ${platformName} automatically as approved in setup plan...\n`);
617
+ return true;
618
+ }
600
619
  return confirmQuestion({
601
620
  stdin,
602
621
  stdout,
@@ -832,7 +851,7 @@ function linuxReentryCommand({
832
851
  requestedAccessMode,
833
852
  noOpen = false
834
853
  } = {}) {
835
- if (!["auto", "light", "standard"].includes(requestedProfile)) {
854
+ if (!["auto", "small", "medium", "large", "light", "standard"].includes(requestedProfile)) {
836
855
  throw new Error("Invalid SpaceApp profile for Docker group re-entry.");
837
856
  }
838
857
  const entrypoint = fileURLToPath(new URL("../bin/spaceapp.mjs", import.meta.url));
@@ -856,7 +875,7 @@ export function windowsResumeScript({
856
875
  requestedAccessMode,
857
876
  noOpen = false
858
877
  } = {}) {
859
- if (!["auto", "light", "standard"].includes(requestedProfile)) {
878
+ if (!["auto", "small", "medium", "large", "light", "standard"].includes(requestedProfile)) {
860
879
  throw new Error("Invalid SpaceApp profile for Windows resume.");
861
880
  }
862
881
  if (typeof noOpen !== "boolean") {
@@ -1050,7 +1069,7 @@ function launchDetachedCommand(spec) {
1050
1069
  if (spec.operation === "start-docker-desktop") {
1051
1070
  child = spawn("powershell.exe", windowsPowerShellArgs(spec.operation), options);
1052
1071
  } else if (spec.operation === "open-docker-desktop") {
1053
- child = spawn("open", ["-a", "Docker"], options);
1072
+ child = spawn("open", ["-a", "/Applications/Docker.app"], options);
1054
1073
  } else {
1055
1074
  resolve(1);
1056
1075
  return;
@@ -1061,9 +1080,15 @@ function launchDetachedCommand(spec) {
1061
1080
  }
1062
1081
  child.once("error", () => resolve(1));
1063
1082
  child.once("spawn", () => {
1083
+ // macOS open exits after Launch Services accepts or rejects the launch.
1084
+ // Spawning the utility alone is not evidence that it opened Docker.
1085
+ if (spec.operation === "open-docker-desktop") return;
1064
1086
  child.unref();
1065
1087
  resolve(0);
1066
1088
  });
1089
+ if (spec.operation === "open-docker-desktop") {
1090
+ child.once("exit", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
1091
+ }
1067
1092
  });
1068
1093
  }
1069
1094