@stigmer/cli 3.12.9 → 3.14.0-dev.20260910084630

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 (141) hide show
  1. package/auth/login.d.ts.map +1 -1
  2. package/auth/login.js +30 -5
  3. package/auth/login.js.map +1 -1
  4. package/auth/token.d.ts +10 -5
  5. package/auth/token.d.ts.map +1 -1
  6. package/auth/token.js +30 -21
  7. package/auth/token.js.map +1 -1
  8. package/commands/apikey/index.d.ts.map +1 -1
  9. package/commands/apikey/index.js +11 -5
  10. package/commands/apikey/index.js.map +1 -1
  11. package/commands/auth/index.d.ts.map +1 -1
  12. package/commands/auth/index.js +19 -8
  13. package/commands/auth/index.js.map +1 -1
  14. package/commands/config/backend.d.ts.map +1 -1
  15. package/commands/config/backend.js +134 -12
  16. package/commands/config/backend.js.map +1 -1
  17. package/commands/connect.d.ts.map +1 -1
  18. package/commands/connect.js +6 -3
  19. package/commands/connect.js.map +1 -1
  20. package/commands/share.d.ts.map +1 -1
  21. package/commands/share.js +8 -6
  22. package/commands/share.js.map +1 -1
  23. package/commands/up.d.ts.map +1 -1
  24. package/commands/up.js +22 -6
  25. package/commands/up.js.map +1 -1
  26. package/config/config.d.ts +56 -7
  27. package/config/config.d.ts.map +1 -1
  28. package/config/config.js +115 -16
  29. package/config/config.js.map +1 -1
  30. package/config/index.d.ts +1 -1
  31. package/config/index.d.ts.map +1 -1
  32. package/config/index.js +1 -1
  33. package/config/index.js.map +1 -1
  34. package/config/keys.d.ts +1 -1
  35. package/config/keys.d.ts.map +1 -1
  36. package/config/keys.js +29 -6
  37. package/config/keys.js.map +1 -1
  38. package/config/resolve.d.ts +32 -21
  39. package/config/resolve.d.ts.map +1 -1
  40. package/config/resolve.js +80 -39
  41. package/config/resolve.js.map +1 -1
  42. package/local/constants.d.ts +8 -3
  43. package/local/constants.d.ts.map +1 -1
  44. package/local/constants.js +8 -3
  45. package/local/constants.js.map +1 -1
  46. package/local/daemon/components.d.ts.map +1 -1
  47. package/local/daemon/components.js +4 -2
  48. package/local/daemon/components.js.map +1 -1
  49. package/local/daemon/env.d.ts +16 -3
  50. package/local/daemon/env.d.ts.map +1 -1
  51. package/local/daemon/env.js +16 -3
  52. package/local/daemon/env.js.map +1 -1
  53. package/local/daemon/launch.d.ts.map +1 -1
  54. package/local/daemon/launch.js +5 -4
  55. package/local/daemon/launch.js.map +1 -1
  56. package/local/daemon/process.d.ts.map +1 -1
  57. package/local/daemon/process.js +8 -5
  58. package/local/daemon/process.js.map +1 -1
  59. package/local/runtime/index.d.ts +2 -2
  60. package/local/runtime/index.d.ts.map +1 -1
  61. package/local/runtime/index.js +4 -2
  62. package/local/runtime/index.js.map +1 -1
  63. package/local/runtime/node.d.ts +8 -0
  64. package/local/runtime/node.d.ts.map +1 -1
  65. package/local/runtime/node.js +74 -23
  66. package/local/runtime/node.js.map +1 -1
  67. package/local/runtime/runner.d.ts +2 -1
  68. package/local/runtime/runner.d.ts.map +1 -1
  69. package/local/runtime/runner.js +4 -31
  70. package/local/runtime/runner.js.map +1 -1
  71. package/local/runtime/runtimes-install.d.ts +13 -0
  72. package/local/runtime/runtimes-install.d.ts.map +1 -0
  73. package/local/runtime/runtimes-install.js +60 -0
  74. package/local/runtime/runtimes-install.js.map +1 -0
  75. package/local/runtime/server.d.ts +26 -30
  76. package/local/runtime/server.d.ts.map +1 -1
  77. package/local/runtime/server.js +106 -108
  78. package/local/runtime/server.js.map +1 -1
  79. package/local/seedpack/content.d.ts +3 -3
  80. package/local/seedpack/content.d.ts.map +1 -1
  81. package/local/seedpack/content.js +19 -13
  82. package/local/seedpack/content.js.map +1 -1
  83. package/local/status.d.ts.map +1 -1
  84. package/local/status.js +4 -2
  85. package/local/status.js.map +1 -1
  86. package/local/webconsole/index.d.ts +2 -3
  87. package/local/webconsole/index.d.ts.map +1 -1
  88. package/local/webconsole/index.js +24 -10
  89. package/local/webconsole/index.js.map +1 -1
  90. package/package.json +5 -5
  91. package/resources/connect/connect.d.ts +4 -3
  92. package/resources/connect/connect.d.ts.map +1 -1
  93. package/resources/connect/connect.js +17 -5
  94. package/resources/connect/connect.js.map +1 -1
  95. package/resources/connect/oauth.d.ts +5 -2
  96. package/resources/connect/oauth.d.ts.map +1 -1
  97. package/resources/connect/oauth.js +5 -4
  98. package/resources/connect/oauth.js.map +1 -1
  99. package/src/auth/login.ts +54 -9
  100. package/src/auth/token.test.ts +69 -13
  101. package/src/auth/token.ts +46 -22
  102. package/src/client/client.test.ts +10 -2
  103. package/src/commands/apikey/index.ts +42 -11
  104. package/src/commands/auth/index.ts +52 -14
  105. package/src/commands/config/backend.test.ts +122 -0
  106. package/src/commands/config/backend.ts +195 -14
  107. package/src/commands/connect.test.ts +22 -4
  108. package/src/commands/connect.ts +37 -13
  109. package/src/commands/share.ts +36 -16
  110. package/src/commands/up.ts +24 -6
  111. package/src/config/config.test.ts +62 -6
  112. package/src/config/config.ts +165 -24
  113. package/src/config/index.ts +6 -0
  114. package/src/config/keys.test.ts +27 -4
  115. package/src/config/keys.ts +46 -9
  116. package/src/config/resolve.test.ts +135 -34
  117. package/src/config/resolve.ts +88 -38
  118. package/src/local/constants.ts +8 -4
  119. package/src/local/daemon/components.test.ts +9 -1
  120. package/src/local/daemon/components.ts +4 -2
  121. package/src/local/daemon/daemon.integration.test.ts +1 -1
  122. package/src/local/daemon/env.test.ts +15 -4
  123. package/src/local/daemon/env.ts +33 -5
  124. package/src/local/daemon/launch.ts +5 -4
  125. package/src/local/daemon/process.ts +7 -4
  126. package/src/local/runtime/index.ts +6 -5
  127. package/src/local/runtime/node.ts +99 -23
  128. package/src/local/runtime/runner.ts +5 -36
  129. package/src/local/runtime/runtime.test.ts +58 -14
  130. package/src/local/runtime/runtimes-install.test.ts +56 -0
  131. package/src/local/runtime/runtimes-install.ts +72 -0
  132. package/src/local/runtime/server.test.ts +178 -114
  133. package/src/local/runtime/server.ts +149 -129
  134. package/src/local/seedpack/content.ts +52 -21
  135. package/src/local/status.ts +4 -2
  136. package/src/local/webconsole/index.ts +27 -10
  137. package/src/resources/connect/connect.integration.test.ts +79 -26
  138. package/src/resources/connect/connect.ts +72 -19
  139. package/src/resources/connect/oauth.test.ts +38 -12
  140. package/src/resources/connect/oauth.ts +27 -11
  141. package/src/resources/share.test.ts +2 -2
@@ -0,0 +1,72 @@
1
+ // Shared npm-install mechanics for on-demand-acquired runtime packages
2
+ // (~/.stigmer/runtimes/<version>/). Extracted when the TS server became the
3
+ // second consumer (D4 #24) — the runner and the server acquire different
4
+ // packages into the SAME per-version install root, so the root bootstrap and
5
+ // the install invocation must not drift between them.
6
+
7
+ import { execFileSync } from "node:child_process";
8
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { CliExitError } from "../../errors/cli-exit-error.js";
11
+ import { ExitCode } from "../../errors/exit-codes.js";
12
+
13
+ /** The install implementation, injectable for tests. */
14
+ export type NpmInstall = (installDir: string, spec: string) => void;
15
+
16
+ /**
17
+ * Ensure the per-version install root exists with a stable package.json.
18
+ * A stable root makes installs deterministic and records the pinned
19
+ * dependencies rather than letting npm synthesize an ad-hoc root; both the
20
+ * runner and the server packages install into this one root, so the file is
21
+ * written once and shared.
22
+ */
23
+ export function ensureRuntimesRoot(installDir: string): void {
24
+ mkdirSync(installDir, { recursive: true });
25
+ const rootManifest = join(installDir, "package.json");
26
+ if (!existsSync(rootManifest)) {
27
+ writeFileSync(
28
+ rootManifest,
29
+ `${JSON.stringify({ name: "stigmer-runtime", private: true, version: "0.0.0" }, null, 2)}\n`,
30
+ );
31
+ }
32
+ }
33
+
34
+ // Install a package (plus its platform-native optional dependency) into an
35
+ // isolated prefix. `--omit=dev` drops devDependencies while keeping the
36
+ // optional native package npm selects by os/cpu; output is inherited so the
37
+ // user sees the one-time download progress.
38
+ export function npmInstallIntoRuntimes(installDir: string, spec: string): void {
39
+ try {
40
+ execFileSync(
41
+ "npm",
42
+ [
43
+ "install",
44
+ spec,
45
+ "--prefix",
46
+ installDir,
47
+ "--omit=dev",
48
+ "--no-audit",
49
+ "--no-fund",
50
+ ],
51
+ {
52
+ stdio: "inherit",
53
+ },
54
+ );
55
+ } catch (err) {
56
+ throw new CliExitError(`failed to install ${spec}`, ExitCode.General, [
57
+ `Command: npm install ${spec} --prefix ${installDir}`,
58
+ "Ensure npm is on PATH and the network is reachable.",
59
+ "If npm reported EBADENGINE, this Node line is outside the package's",
60
+ "supported range. If a previous install was interrupted, remove",
61
+ `${installDir} and retry.`,
62
+ String(err),
63
+ ]);
64
+ }
65
+ }
66
+
67
+ // A source build reports "0.0.0-dev" and the dev npm channel stamps
68
+ // "<v>-dev.<stamp>" versions; neither publishes matching runtime packages,
69
+ // so they are not acquirable. Release and rc/next versions are.
70
+ export function isAcquirableRelease(version: string): boolean {
71
+ return !version.includes("-dev");
72
+ }
@@ -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
+ }