@stigmer/cli 3.12.8 → 3.13.0

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.
Files changed (106) hide show
  1. package/commands/agent-exec-flags.d.ts +1 -0
  2. package/commands/agent-exec-flags.d.ts.map +1 -1
  3. package/commands/agent-exec-flags.js +3 -1
  4. package/commands/agent-exec-flags.js.map +1 -1
  5. package/commands/run.js +3 -0
  6. package/commands/run.js.map +1 -1
  7. package/commands/up.d.ts.map +1 -1
  8. package/commands/up.js +22 -6
  9. package/commands/up.js.map +1 -1
  10. package/config/resolve.d.ts +5 -2
  11. package/config/resolve.d.ts.map +1 -1
  12. package/config/resolve.js +7 -4
  13. package/config/resolve.js.map +1 -1
  14. package/local/constants.d.ts +8 -3
  15. package/local/constants.d.ts.map +1 -1
  16. package/local/constants.js +8 -3
  17. package/local/constants.js.map +1 -1
  18. package/local/daemon/components.d.ts.map +1 -1
  19. package/local/daemon/components.js +4 -2
  20. package/local/daemon/components.js.map +1 -1
  21. package/local/daemon/env.d.ts +16 -3
  22. package/local/daemon/env.d.ts.map +1 -1
  23. package/local/daemon/env.js +16 -3
  24. package/local/daemon/env.js.map +1 -1
  25. package/local/daemon/launch.d.ts.map +1 -1
  26. package/local/daemon/launch.js +5 -4
  27. package/local/daemon/launch.js.map +1 -1
  28. package/local/daemon/process.d.ts.map +1 -1
  29. package/local/daemon/process.js +8 -5
  30. package/local/daemon/process.js.map +1 -1
  31. package/local/runtime/index.d.ts +2 -2
  32. package/local/runtime/index.d.ts.map +1 -1
  33. package/local/runtime/index.js +4 -2
  34. package/local/runtime/index.js.map +1 -1
  35. package/local/runtime/node.d.ts +8 -0
  36. package/local/runtime/node.d.ts.map +1 -1
  37. package/local/runtime/node.js +74 -23
  38. package/local/runtime/node.js.map +1 -1
  39. package/local/runtime/runner.d.ts +2 -1
  40. package/local/runtime/runner.d.ts.map +1 -1
  41. package/local/runtime/runner.js +4 -31
  42. package/local/runtime/runner.js.map +1 -1
  43. package/local/runtime/runtimes-install.d.ts +13 -0
  44. package/local/runtime/runtimes-install.d.ts.map +1 -0
  45. package/local/runtime/runtimes-install.js +60 -0
  46. package/local/runtime/runtimes-install.js.map +1 -0
  47. package/local/runtime/server.d.ts +26 -30
  48. package/local/runtime/server.d.ts.map +1 -1
  49. package/local/runtime/server.js +106 -108
  50. package/local/runtime/server.js.map +1 -1
  51. package/local/status.d.ts.map +1 -1
  52. package/local/status.js +4 -2
  53. package/local/status.js.map +1 -1
  54. package/local/webconsole/index.d.ts +2 -3
  55. package/local/webconsole/index.d.ts.map +1 -1
  56. package/local/webconsole/index.js +24 -10
  57. package/local/webconsole/index.js.map +1 -1
  58. package/package.json +5 -5
  59. package/resources/run/agent-exec.d.ts.map +1 -1
  60. package/resources/run/agent-exec.js +2 -0
  61. package/resources/run/agent-exec.js.map +1 -1
  62. package/resources/run/create.d.ts +2 -1
  63. package/resources/run/create.d.ts.map +1 -1
  64. package/resources/run/create.js +21 -3
  65. package/resources/run/create.js.map +1 -1
  66. package/resources/run/header.d.ts +9 -1
  67. package/resources/run/header.d.ts.map +1 -1
  68. package/resources/run/header.js +3 -0
  69. package/resources/run/header.js.map +1 -1
  70. package/resources/run/prepare.d.ts +41 -3
  71. package/resources/run/prepare.d.ts.map +1 -1
  72. package/resources/run/prepare.js +53 -16
  73. package/resources/run/prepare.js.map +1 -1
  74. package/src/commands/agent-exec-flags.ts +5 -2
  75. package/src/commands/run.ts +3 -0
  76. package/src/commands/up.ts +24 -6
  77. package/src/config/resolve.test.ts +1 -1
  78. package/src/config/resolve.ts +7 -4
  79. package/src/local/constants.ts +8 -4
  80. package/src/local/daemon/components.test.ts +9 -1
  81. package/src/local/daemon/components.ts +4 -2
  82. package/src/local/daemon/daemon.integration.test.ts +1 -1
  83. package/src/local/daemon/env.test.ts +15 -4
  84. package/src/local/daemon/env.ts +33 -5
  85. package/src/local/daemon/launch.ts +5 -4
  86. package/src/local/daemon/process.ts +7 -4
  87. package/src/local/runtime/index.ts +6 -5
  88. package/src/local/runtime/node.ts +99 -23
  89. package/src/local/runtime/runner.ts +5 -36
  90. package/src/local/runtime/runtime.test.ts +58 -14
  91. package/src/local/runtime/runtimes-install.test.ts +56 -0
  92. package/src/local/runtime/runtimes-install.ts +72 -0
  93. package/src/local/runtime/server.test.ts +178 -114
  94. package/src/local/runtime/server.ts +149 -129
  95. package/src/local/status.ts +4 -2
  96. package/src/local/webconsole/index.ts +27 -10
  97. package/src/resources/connect/oauth.test.ts +1 -1
  98. package/src/resources/run/agent-exec.test.ts +25 -0
  99. package/src/resources/run/agent-exec.ts +2 -0
  100. package/src/resources/run/create.test.ts +92 -1
  101. package/src/resources/run/create.ts +25 -6
  102. package/src/resources/run/header.test.ts +18 -0
  103. package/src/resources/run/header.ts +11 -1
  104. package/src/resources/run/prepare.test.ts +247 -5
  105. package/src/resources/run/prepare.ts +99 -21
  106. package/src/resources/share.test.ts +2 -2
@@ -1,51 +1,36 @@
1
- import { createHash } from "node:crypto";
2
- import { chmodSync, mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
1
+ // Pins the server resolution: exactly like the runner's (explicit dir →
2
+ // repo tree → acquired slim package).
3
+
4
+ import {
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ } from "node:fs";
3
10
  import { tmpdir } from "node:os";
4
11
  import { join } from "node:path";
5
- import { gzipSync } from "fflate";
6
12
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
- import { downloadServerBinary, ensureServerBinary } from "./server.js";
8
-
9
- // Build a minimal ustar archive with a single regular-file entry.
10
- function makeTar(name: string, content: Buffer): Buffer {
11
- const header = Buffer.alloc(512);
12
- header.write(name, 0, "utf8");
13
- header.write(content.length.toString(8).padStart(11, "0"), 124, "ascii");
14
- header.write("0", 156, "ascii");
15
- const data = Buffer.alloc(Math.ceil(content.length / 512) * 512);
16
- content.copy(data);
17
- const trailer = Buffer.alloc(1024);
18
- return Buffer.concat([header, data, trailer]);
19
- }
13
+ import { acquireServer, ensureServer, resolveServerTs } from "./server.js";
20
14
 
21
- function sha256(bytes: Uint8Array): string {
22
- return createHash("sha256").update(bytes).digest("hex");
15
+ function tempDir(prefix: string): string {
16
+ return mkdtempSync(join(tmpdir(), prefix));
23
17
  }
24
18
 
25
- // A fetch double that serves a gzipped tar for the archive URL and a
26
- // shasum-format checksum for the `.sha256` URL. `corruptChecksum` flips the
27
- // returned hash to exercise the verification failure path.
28
- function fakeFetch(archiveGz: Uint8Array, opts: { corruptChecksum?: boolean; capturedUrls?: string[] } = {}): typeof fetch {
29
- const checksum = opts.corruptChecksum === true ? "0".repeat(64) : sha256(archiveGz);
30
- return (async (url: string) => {
31
- opts.capturedUrls?.push(url);
32
- if (url.endsWith(".sha256")) {
33
- return { ok: true, status: 200, text: async () => `${checksum} archive.tar.gz\n` };
34
- }
35
- return {
36
- ok: true,
37
- status: 200,
38
- arrayBuffer: async () => archiveGz.buffer.slice(archiveGz.byteOffset, archiveGz.byteOffset + archiveGz.byteLength),
39
- };
40
- }) as unknown as typeof fetch;
41
- }
19
+ const fakeNode = (): string => "/usr/bin/node";
42
20
 
43
- const TOUCHED = ["STIGMER_SERVER_BIN"] as const;
21
+ // Snapshot and restore the env vars the resolution reads, so tests don't leak.
22
+ const TOUCHED = [
23
+ "STIGMER_SERVER_DIR",
24
+ "STIGMER_NODE_BIN",
25
+ ] as const;
44
26
  let saved: Record<string, string | undefined>;
45
27
 
46
28
  beforeEach(() => {
47
29
  saved = {};
48
- for (const key of TOUCHED) saved[key] = process.env[key];
30
+ for (const key of TOUCHED) {
31
+ saved[key] = process.env[key];
32
+ delete process.env[key];
33
+ }
49
34
  });
50
35
 
51
36
  afterEach(() => {
@@ -55,98 +40,177 @@ afterEach(() => {
55
40
  }
56
41
  });
57
42
 
58
- describe("downloadServerBinary", () => {
59
- it("downloads, verifies the checksum, extracts, and writes an executable", async () => {
60
- const payload = Buffer.from("the-stigmer-server-binary");
61
- const gz = gzipSync(new Uint8Array(makeTar("stigmer-server", payload)));
62
- const capturedUrls: string[] = [];
63
- const binPath = join(mkdtempSync(join(tmpdir(), "stigmer-server-")), "bin", "stigmer-server");
43
+ function builtServerDir(): string {
44
+ const dir = tempDir("stigmer-server-");
45
+ writeFileSync(join(dir, "package.json"), "{}");
46
+ mkdirSync(join(dir, "dist"));
47
+ writeFileSync(join(dir, "dist", "main.js"), "//");
48
+ return dir;
49
+ }
64
50
 
65
- await downloadServerBinary({
66
- version: "0.5.0",
67
- binPath,
68
- platform: "darwin",
69
- arch: "arm64",
70
- fetchImpl: fakeFetch(gz, { capturedUrls }),
71
- });
51
+ describe("ensureServer", () => {
52
+ it("resolves the server from STIGMER_SERVER_DIR", () => {
53
+ const dir = builtServerDir();
54
+ process.env.STIGMER_SERVER_DIR = dir;
72
55
 
73
- expect(readFileSync(binPath, "utf8")).toBe("the-stigmer-server-binary");
74
- expect(statSync(binPath).mode & 0o111).not.toBe(0); // executable
75
- expect(capturedUrls).toContain(
76
- "https://github.com/stigmer/stigmer/releases/download/v0.5.0/stigmer-server-v0.5.0-darwin-arm64.tar.gz",
77
- );
78
- expect(capturedUrls).toContain(
79
- "https://github.com/stigmer/stigmer/releases/download/v0.5.0/stigmer-server-v0.5.0-darwin-arm64.tar.gz.sha256",
80
- );
56
+ expect(ensureServer({ node: fakeNode })).toEqual({
57
+ nodeBin: "/usr/bin/node",
58
+ entryPath: join(dir, "dist", "main.js"),
59
+ appDir: dir,
60
+ });
81
61
  });
62
+ });
63
+
64
+ describe("resolveServerTs", () => {
65
+ it("resolves a built server from STIGMER_SERVER_DIR", () => {
66
+ const dir = builtServerDir();
67
+ process.env.STIGMER_SERVER_DIR = dir;
82
68
 
83
- it("maps a prerelease version to its v-prefixed release tag", async () => {
84
- const gz = gzipSync(new Uint8Array(makeTar("stigmer-server", Buffer.from("x"))));
85
- const capturedUrls: string[] = [];
86
- const binPath = join(mkdtempSync(join(tmpdir(), "stigmer-server-")), "stigmer-server");
87
-
88
- await downloadServerBinary({
89
- version: "0.5.0-rc.1",
90
- binPath,
91
- platform: "linux",
92
- arch: "x64",
93
- fetchImpl: fakeFetch(gz, { capturedUrls }),
69
+ expect(resolveServerTs(fakeNode)).toEqual({
70
+ nodeBin: "/usr/bin/node",
71
+ entryPath: join(dir, "dist", "main.js"),
72
+ appDir: dir,
94
73
  });
74
+ });
95
75
 
96
- expect(capturedUrls[0]).toBe(
97
- "https://github.com/stigmer/stigmer/releases/download/v0.5.0-rc.1/stigmer-server-v0.5.0-rc.1-linux-amd64.tar.gz",
98
- );
76
+ it("errors with build guidance when dist/main.js is missing", () => {
77
+ const dir = tempDir("stigmer-server-");
78
+ writeFileSync(join(dir, "package.json"), "{}");
79
+ process.env.STIGMER_SERVER_DIR = dir;
80
+
81
+ expect(() => resolveServerTs(fakeNode)).toThrow(/not built/);
82
+ // Remediation rides in hints, not the message.
83
+ const hints = captureHints(() => resolveServerTs(fakeNode));
84
+ expect(hints.join("\n")).toMatch(/make build-server/);
99
85
  });
100
86
 
101
- it("refuses a mismatched checksum", async () => {
102
- const gz = gzipSync(new Uint8Array(makeTar("stigmer-server", Buffer.from("x"))));
103
- const binPath = join(mkdtempSync(join(tmpdir(), "stigmer-server-")), "stigmer-server");
104
- await expect(
105
- downloadServerBinary({
106
- version: "0.5.0",
107
- binPath,
108
- platform: "darwin",
109
- arch: "arm64",
110
- fetchImpl: fakeFetch(gz, { corruptChecksum: true }),
111
- }),
112
- ).rejects.toThrow(/checksum mismatch/);
87
+ it("rejects an override that is not a server package", () => {
88
+ const dir = tempDir("stigmer-server-");
89
+ process.env.STIGMER_SERVER_DIR = join(dir, "no-package-here"); // no package.json
90
+ expect(() => resolveServerTs(fakeNode)).toThrow(/not a server package/);
113
91
  });
92
+ });
114
93
 
115
- it("fails clearly on a non-OK archive response", async () => {
116
- const fetchImpl = (async (url: string) => ({
117
- ok: false,
118
- status: 404,
119
- arrayBuffer: async () => new ArrayBuffer(0),
120
- text: async () => "",
121
- })) as unknown as typeof fetch;
122
- const binPath = join(mkdtempSync(join(tmpdir(), "stigmer-server-")), "stigmer-server");
123
- await expect(
124
- downloadServerBinary({ version: "9.9.9", binPath, platform: "darwin", arch: "arm64", fetchImpl }),
125
- ).rejects.toThrow(/HTTP 404/);
94
+ describe("acquireServer", () => {
95
+ it("installs the slim package and resolves its main.js entry", () => {
96
+ const home = tempDir("stigmer-home-");
97
+ const installed: Array<{ dir: string; spec: string }> = [];
98
+ const install = (dir: string, spec: string): void => {
99
+ installed.push({ dir, spec });
100
+ // Simulate the npm install laying down the slim package.
101
+ const pkgDir = join(dir, "node_modules", "@stigmer", "server-slim");
102
+ mkdirSync(pkgDir, { recursive: true });
103
+ writeFileSync(join(pkgDir, "main.js"), "// slim bundle");
104
+ };
105
+
106
+ const launch = acquireServer({
107
+ home,
108
+ version: "0.5.0",
109
+ node: fakeNode,
110
+ install,
111
+ });
112
+
113
+ const installDir = join(home, ".stigmer", "runtimes", "0.5.0");
114
+ expect(launch).toEqual({
115
+ nodeBin: "/usr/bin/node",
116
+ entryPath: join(
117
+ installDir,
118
+ "node_modules",
119
+ "@stigmer",
120
+ "server-slim",
121
+ "main.js",
122
+ ),
123
+ appDir: join(installDir, "node_modules", "@stigmer", "server-slim"),
124
+ });
125
+ expect(installed).toEqual([
126
+ { dir: installDir, spec: "@stigmer/server-slim@0.5.0" },
127
+ ]);
126
128
  });
127
129
 
128
- it("refuses an unsupported platform with actionable guidance", async () => {
129
- const binPath = join(mkdtempSync(join(tmpdir(), "stigmer-server-")), "stigmer-server");
130
- const fetchImpl = vi.fn() as unknown as typeof fetch;
131
- await expect(
132
- downloadServerBinary({ version: "0.5.0", binPath, platform: "win32", arch: "x64", fetchImpl }),
133
- ).rejects.toThrow(/does not support this platform/);
134
- expect(fetchImpl).not.toHaveBeenCalled();
130
+ it("reuses a prior install without reinstalling", () => {
131
+ const home = tempDir("stigmer-home-");
132
+ const pkgDir = join(
133
+ home,
134
+ ".stigmer",
135
+ "runtimes",
136
+ "0.5.0",
137
+ "node_modules",
138
+ "@stigmer",
139
+ "server-slim",
140
+ );
141
+ mkdirSync(pkgDir, { recursive: true });
142
+ writeFileSync(join(pkgDir, "main.js"), "// slim bundle");
143
+
144
+ const install = vi.fn();
145
+ const launch = acquireServer({
146
+ home,
147
+ version: "0.5.0",
148
+ node: fakeNode,
149
+ install,
150
+ });
151
+
152
+ expect(launch.nodeBin).toBe("/usr/bin/node");
153
+ expect(install).not.toHaveBeenCalled();
135
154
  });
136
- });
137
155
 
138
- describe("ensureServerBinary", () => {
139
- it("returns an existing override binary without downloading", async () => {
140
- const dir = mkdtempSync(join(tmpdir(), "stigmer-server-"));
141
- const bin = join(dir, "stigmer-server");
142
- writeFileSync(bin, "#!/bin/sh\n");
143
- chmodSync(bin, 0o755);
144
- process.env.STIGMER_SERVER_BIN = bin;
156
+ // The runner and the server share one per-version install root; acquiring
157
+ // the server after the runner must ADD to it, never clobber the root
158
+ // manifest the runner's install already wrote.
159
+ it("shares the runtimes install root with the runner without clobbering it", () => {
160
+ const home = tempDir("stigmer-home-");
161
+ const installDir = join(home, ".stigmer", "runtimes", "0.5.0");
162
+ mkdirSync(installDir, { recursive: true });
163
+ writeFileSync(
164
+ join(installDir, "package.json"),
165
+ '{"name":"stigmer-runtime","marker":"pre-existing"}\n',
166
+ );
145
167
 
146
- const fetchImpl = vi.fn() as unknown as typeof fetch;
147
- const resolved = await ensureServerBinary({ home: dir, version: "0.5.0", fetchImpl });
168
+ const install = (dir: string): void => {
169
+ const pkgDir = join(dir, "node_modules", "@stigmer", "server-slim");
170
+ mkdirSync(pkgDir, { recursive: true });
171
+ writeFileSync(join(pkgDir, "main.js"), "// slim bundle");
172
+ };
173
+ acquireServer({ home, version: "0.5.0", node: fakeNode, install });
148
174
 
149
- expect(resolved).toBe(bin);
150
- expect(fetchImpl).not.toHaveBeenCalled();
175
+ expect(readFileSync(join(installDir, "package.json"), "utf8")).toContain(
176
+ "pre-existing",
177
+ );
178
+ });
179
+
180
+ // The hunted npm failure shape: the install runs but the platform-native
181
+ // package was skipped (unsupported OS/arch), so the entry never appears.
182
+ it("errors actionably when the install produces no entry", () => {
183
+ const home = tempDir("stigmer-home-");
184
+ const install = vi.fn(); // runs, lays down nothing
185
+ const attempt = (): unknown => acquireServer({ home, version: "0.5.0", node: fakeNode, install });
186
+ expect(attempt).toThrow(/install did not produce/);
187
+ expect(install).toHaveBeenCalledOnce();
188
+ const hints = captureHints(attempt).join("\n");
189
+ expect(hints).toMatch(/platform-native/);
190
+ expect(hints).toMatch(/Remove .* and retry/);
191
+ });
192
+
193
+ it("refuses to acquire for a non-release (dev) build, naming the fallback", () => {
194
+ const home = tempDir("stigmer-home-");
195
+ const attempt = (): unknown =>
196
+ acquireServer({
197
+ home,
198
+ version: "0.0.0-dev",
199
+ node: fakeNode,
200
+ install: vi.fn(),
201
+ });
202
+ expect(attempt).toThrow(/non-release build/);
203
+ // The escape hatch rides in the hints: the explicit server dir.
204
+ const hints = captureHints(attempt);
205
+ expect(hints.join("\n")).toMatch(/STIGMER_SERVER_DIR/);
151
206
  });
152
207
  });
208
+
209
+ function captureHints(attempt: () => unknown): readonly string[] {
210
+ try {
211
+ attempt();
212
+ } catch (err) {
213
+ return (err as { hints?: readonly string[] }).hints ?? [];
214
+ }
215
+ throw new Error("expected the attempt to throw");
216
+ }
@@ -1,20 +1,20 @@
1
- // Resolution and on-demand acquisition of the `stigmer-server` control plane.
1
+ // Resolution and on-demand acquisition of the stigmer server — the
2
+ // TypeScript implementation serving since the DD-006 cutover (D4 #24; the Go
3
+ // binary ladder that backed rollback retired with #25).
2
4
  //
3
- // DD-003: a TypeScript CLI cannot `go:embed` a Go binary, so it resolves an
4
- // existing `stigmer-server` (dev build / Homebrew / a prior download) and, when
5
- // none is present, downloads the standalone release asset into ~/.stigmer/bin and
6
- // verifies its sha256 before first use. The fetch/extract mechanics are shared
7
- // with the Temporal downloader (`../artifact.ts`); the one deliberate difference
8
- // is the checksum step — this is our own executable, and DD-003 makes integrity
9
- // non-negotiable.
5
+ // The server is launched exactly like the runner: a compiled `node main.js`,
6
+ // never `tsx src/main.ts` (the workers bundle Temporal workflows on boot in
7
+ // dev shape, and the slim artifact ships pre-built bundles — either way the
8
+ // entry must be compiled; see the server package's workflow-source.ts).
10
9
  //
11
- // Resolution order (first existing hit wins; download is the last resort):
12
- // 1. STIGMER_SERVER_BIN — explicit override (tests, custom installs)
13
- // 2. repo `bin/stigmer-server` — the dev build (`make build`)
14
- // 3. ~/bin/stigmer-server — a common local install location
15
- // 4. ~/.stigmer/bin/stigmer-server — a previously-downloaded release asset
16
- // 5. PATH — `stigmer-server`
17
- // 6. download the pinned release asset into ~/.stigmer/bin (checksum-verified)
10
+ // Resolution order (the D2 §6 switch semantics):
11
+ // 1. STIGMER_SERVER_DIR — an explicit server package dir (dist/main.js
12
+ // required), the STIGMER_RUNNER_DIR mirror.
13
+ // 2. The repo-tree server package (dev; build required).
14
+ // 3. The published `@stigmer/server-slim`, installed on demand into
15
+ // ~/.stigmer/runtimes/<version>/ alongside the runner's package — one
16
+ // CJS main.js, pre-built workflow bundles, and the per-platform
17
+ // Temporal native bridge selected by npm via optionalDependencies.
18
18
 
19
19
  import { existsSync } from "node:fs";
20
20
  import { homedir } from "node:os";
@@ -22,148 +22,168 @@ import { dirname, join } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { CliExitError } from "../../errors/cli-exit-error.js";
24
24
  import { ExitCode } from "../../errors/exit-codes.js";
25
+ import { log } from "../../logger.js";
25
26
  import { VERSION } from "../../version.js";
26
- import { fetchTarballBinary, mapReleaseArch, mapReleaseOs } from "../artifact.js";
27
- import { binDir } from "../paths.js";
28
- import { which } from "./which.js";
29
-
30
- const SERVER_BINARY = "stigmer-server";
31
- const GITHUB_REPO = "stigmer/stigmer";
32
-
33
- // Platform/arch combos for which `release.cli.yaml` publishes a stigmer-server
34
- // asset. The npm CLI installs on more platforms than this (e.g. linux-arm64,
35
- // Windows), so an unsupported `stigmer up` must fail with clear guidance rather
36
- // than a confusing 404.
37
- const SUPPORTED_PLATFORMS = new Set(["darwin-arm64", "darwin-amd64", "linux-amd64"]);
38
-
39
- /**
40
- * Find an already-present `stigmer-server` binary, or null. Pure: never
41
- * downloads. `home` is injectable for tests.
42
- */
43
- export function resolveServerBinary(home: string = homedir()): string | null {
44
- for (const candidate of serverCandidates(home)) {
45
- if (existsSync(candidate)) return candidate;
46
- }
47
- return which(SERVER_BINARY);
48
- }
27
+ import { runtimesDir } from "../paths.js";
28
+ import type { ServerLaunch } from "../daemon/env.js";
29
+ import { resolveServerNode } from "./node.js";
30
+ import {
31
+ ensureRuntimesRoot,
32
+ isAcquirableRelease,
33
+ npmInstallIntoRuntimes,
34
+ type NpmInstall,
35
+ } from "./runtimes-install.js";
36
+
37
+ const SLIM_PACKAGE = "@stigmer/server-slim";
49
38
 
50
39
  export interface EnsureServerOptions {
51
40
  home?: string;
52
- /** Release version to download if needed (defaults to the CLI's own version). */
41
+ /** Release version of the slim server to acquire (defaults to the CLI version). */
53
42
  version?: string;
54
- /** Override the fetch implementation (tests). */
55
- fetchImpl?: typeof fetch;
43
+ /** Node resolver (injectable for tests). */
44
+ node?: () => string;
45
+ /** npm install implementation (injectable for tests). */
46
+ install?: NpmInstall;
56
47
  }
57
48
 
58
49
  /**
59
- * Ensure a `stigmer-server` binary is available, returning its path. Reuses an
60
- * existing binary if found; otherwise downloads the pinned release asset into
61
- * ~/.stigmer/bin (checksum-verified). Throws actionable guidance when no binary
62
- * exists and a download is not possible (a source/dev build, or an unsupported
63
- * platform).
50
+ * Resolve what the daemon launches as `stigmer-server`: the explicit
51
+ * STIGMER_SERVER_DIR override, then the repo tree, then acquiring the
52
+ * published slim package. Throws actionable guidance on a missing build, an
53
+ * invalid override, or a non-release CLI build with nothing local.
64
54
  */
65
- export async function ensureServerBinary(opts: EnsureServerOptions = {}): Promise<string> {
66
- const home = opts.home ?? homedir();
67
- const existing = resolveServerBinary(home);
68
- if (existing !== null) return existing;
69
-
70
- const version = opts.version ?? VERSION;
71
- if (!isDownloadableRelease(version)) {
72
- throw notFoundError();
73
- }
74
-
75
- const binPath = join(binDir(home), SERVER_BINARY);
76
- await downloadServerBinary({ version, binPath, fetchImpl: opts.fetchImpl });
77
- return binPath;
78
- }
79
-
80
- export interface ServerDownloadTarget {
81
- /** Release version, e.g. "0.5.0" or "0.5.0-rc.1" (with or without a leading "v"). */
82
- version: string;
83
- /** Absolute path to write the extracted `stigmer-server` binary to. */
84
- binPath: string;
85
- /** Node platform (defaults to process.platform). */
86
- platform?: NodeJS.Platform;
87
- /** Node arch (defaults to process.arch). */
88
- arch?: string;
89
- /** Override the fetch implementation (tests). */
90
- fetchImpl?: typeof fetch;
55
+ export function ensureServer(opts: EnsureServerOptions = {}): ServerLaunch {
56
+ const node = opts.node ?? resolveServerNode;
57
+ const local = resolveServerTs(node);
58
+ if (local !== null) return local;
59
+ return acquireServer({ ...opts, node });
91
60
  }
92
61
 
93
62
  /**
94
- * Download, checksum-verify, and install the standalone `stigmer-server` release
95
- * asset. Throws a {@link CliExitError} on an unsupported platform or any
96
- * network/checksum/extraction failure.
63
+ * Resolve a repo-tree TS server (dev) or an explicit STIGMER_SERVER_DIR, or
64
+ * null when neither is present (the caller then acquires the slim package).
65
+ * Throws when an override is set but invalid, or when a found server is not
66
+ * built — both are actionable user-facing conditions, not a reason to
67
+ * silently download. `node` is injectable for tests.
97
68
  */
98
- export async function downloadServerBinary(target: ServerDownloadTarget): Promise<void> {
99
- const os = mapReleaseOs(target.platform ?? process.platform);
100
- const arch = mapReleaseArch(target.arch ?? process.arch);
101
- const platformKey = `${os}-${arch}`;
102
- if (!SUPPORTED_PLATFORMS.has(platformKey)) {
103
- throw unsupportedPlatformError(platformKey);
69
+ export function resolveServerTs(
70
+ node: () => string = resolveServerNode,
71
+ ): ServerLaunch | null {
72
+ const override = process.env.STIGMER_SERVER_DIR;
73
+ if (override !== undefined && override !== "") {
74
+ if (!hasPackageJson(override)) {
75
+ throw new CliExitError(
76
+ `STIGMER_SERVER_DIR is not a server package: ${override}`,
77
+ ExitCode.General,
78
+ [
79
+ "Point STIGMER_SERVER_DIR at the @stigmer/server package directory (the one with package.json).",
80
+ ],
81
+ );
82
+ }
83
+ return resolveBuiltServer(override, node);
104
84
  }
105
85
 
106
- const tag = releaseTag(target.version);
107
- const archive = `${SERVER_BINARY}-${tag}-${platformKey}.tar.gz`;
108
- const url = `https://github.com/${GITHUB_REPO}/releases/download/${tag}/${archive}`;
109
-
110
- await fetchTarballBinary({
111
- url,
112
- checksumUrl: `${url}.sha256`,
113
- entryName: SERVER_BINARY,
114
- binPath: target.binPath,
115
- label: SERVER_BINARY,
116
- fetchImpl: target.fetchImpl,
117
- });
86
+ const root = repoRoot();
87
+ if (root !== null) {
88
+ const candidate = join(root, "backend", "services", "stigmer-server");
89
+ if (hasPackageJson(candidate)) return resolveBuiltServer(candidate, node);
90
+ }
91
+ return null;
118
92
  }
119
93
 
120
- function serverCandidates(home: string): string[] {
121
- const candidates: string[] = [];
122
- const override = process.env.STIGMER_SERVER_BIN;
123
- if (override !== undefined && override !== "") candidates.push(override);
94
+ /**
95
+ * Acquire the published `@stigmer/server-slim@<version>` into
96
+ * ~/.stigmer/runtimes/<version>/ (idempotent: reuses a prior install; shares
97
+ * the install root with the runner's package) and resolve its `main.js`
98
+ * entry. The version is pinned to the CLI's own version so the server's
99
+ * protos and behavior stay in lockstep with the CLI and the runner.
100
+ */
101
+ export function acquireServer(opts: EnsureServerOptions = {}): ServerLaunch {
102
+ const home = opts.home ?? homedir();
103
+ const version = opts.version ?? VERSION;
104
+ if (!isAcquirableRelease(version)) {
105
+ throw new CliExitError(
106
+ `cannot acquire ${SLIM_PACKAGE} for a non-release build (${version})`,
107
+ ExitCode.General,
108
+ [
109
+ "Run from the repo with a built server (make build-server), or set",
110
+ "STIGMER_SERVER_DIR to a built server package.",
111
+ "On-demand acquisition is only available for published releases.",
112
+ ],
113
+ );
114
+ }
124
115
 
125
- const root = repoRoot();
126
- if (root !== null) candidates.push(join(root, "bin", SERVER_BINARY));
127
- candidates.push(join(home, "bin", SERVER_BINARY));
128
- candidates.push(join(binDir(home), SERVER_BINARY)); // a previously-downloaded asset
129
- return candidates;
130
- }
116
+ // Probe the Node capability BEFORE the download: a user on an FTS5-less
117
+ // Node must not fetch the full slim artifact only to be rejected after.
118
+ const node = opts.node ?? resolveServerNode;
119
+ const nodeBin = node();
120
+ const installDir = join(runtimesDir(home), version);
121
+ const entryPath = join(
122
+ installDir,
123
+ "node_modules",
124
+ "@stigmer",
125
+ "server-slim",
126
+ "main.js",
127
+ );
128
+
129
+ if (!existsSync(entryPath)) {
130
+ log.info(`acquiring ${SLIM_PACKAGE}`, { version, dir: installDir });
131
+ ensureRuntimesRoot(installDir);
132
+ const install = opts.install ?? npmInstallIntoRuntimes;
133
+ install(installDir, `${SLIM_PACKAGE}@${version}`);
134
+ }
131
135
 
132
- // Only tagged releases publish a downloadable asset. A source build reports the
133
- // "0.0.0-dev" sentinel and the dev npm channel stamps "<v>-dev.<stamp>" versions
134
- // (release.dev.yaml) that never cut a GitHub release — neither is downloadable.
135
- function isDownloadableRelease(version: string): boolean {
136
- return !version.includes("-dev");
137
- }
136
+ if (!existsSync(entryPath)) {
137
+ throw new CliExitError(
138
+ `${SLIM_PACKAGE} install did not produce ${entryPath}`,
139
+ ExitCode.General,
140
+ [
141
+ "The install may have skipped the platform-native package for this OS/arch.",
142
+ `Remove ${installDir} and retry, or set STIGMER_SERVER_DIR to a built server.`,
143
+ ],
144
+ );
145
+ }
138
146
 
139
- // The npm package version maps to the GitHub release tag by prefixing "v"
140
- // (npm 0.5.0 -> tag v0.5.0; npm 0.5.0-rc.1 -> tag v0.5.0-rc.1).
141
- function releaseTag(version: string): string {
142
- return version.startsWith("v") ? version : `v${version}`;
147
+ return {
148
+ nodeBin,
149
+ entryPath,
150
+ appDir: dirname(entryPath),
151
+ };
143
152
  }
144
153
 
145
- function notFoundError(): CliExitError {
146
- return new CliExitError(`${SERVER_BINARY} binary not found`, ExitCode.General, [
147
- "Build it from the repo with `make build` (produces ./bin/stigmer-server),",
148
- "or set STIGMER_SERVER_BIN to an existing binary.",
149
- "Automatic download is only available for tagged releases.",
150
- ]);
154
+ function resolveBuiltServer(appDir: string, node: () => string): ServerLaunch {
155
+ const entryPath = join(appDir, "dist", "main.js");
156
+ if (!existsSync(entryPath)) {
157
+ throw new CliExitError(
158
+ `TS server not built: ${entryPath} is missing`,
159
+ ExitCode.General,
160
+ [
161
+ "Build it with: make build-server",
162
+ "The server must be compiled — it cannot run from source via tsx (its",
163
+ "Temporal workers bundle workflow code from the compiled dist).",
164
+ "STIGMER_SERVER_DIR expects the source package (dist/main.js); to run an",
165
+ "acquired @stigmer/server-slim install, unset it — the CLI resolves that",
166
+ "shape itself.",
167
+ ],
168
+ );
169
+ }
170
+ return { nodeBin: node(), entryPath, appDir };
151
171
  }
152
172
 
153
- function unsupportedPlatformError(platformKey: string): CliExitError {
154
- return new CliExitError(`the local stack does not support this platform (${platformKey})`, ExitCode.General, [
155
- "`stigmer up` needs a stigmer-server release asset, which is published for",
156
- "macOS (arm64/amd64) and Linux (amd64) only.",
157
- "Point STIGMER_SERVER_BIN at a server binary you built yourself to proceed.",
158
- ]);
173
+ function hasPackageJson(dir: string): boolean {
174
+ return existsSync(join(dir, "package.json"));
159
175
  }
160
176
 
161
- // Walk up from this module to the repo root (the directory containing
162
- // `bin/stigmer-server` in a dev checkout). Returns null outside a checkout.
177
+ // Walk up from this module to a repo root containing the TS server package.
163
178
  function repoRoot(): string | null {
164
179
  let dir = dirname(fileURLToPath(import.meta.url));
165
- for (let i = 0; i < 8; i += 1) {
166
- if (existsSync(join(dir, "bin", SERVER_BINARY))) return dir;
180
+ for (let i = 0; i < 10; i += 1) {
181
+ if (
182
+ existsSync(
183
+ join(dir, "backend", "services", "stigmer-server", "package.json"),
184
+ )
185
+ )
186
+ return dir;
167
187
  const parent = dirname(dir);
168
188
  if (parent === dir) break;
169
189
  dir = parent;