@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
@@ -3,7 +3,12 @@
3
3
  // managed by their own (later) commands and preserved opaquely on save.
4
4
 
5
5
  import { UsageError } from "../errors/usage-error.js";
6
- import type { BackendType, Config } from "./config.js";
6
+ import {
7
+ CLOUD_BACKEND_NAME,
8
+ type Config,
9
+ LOCAL_BACKEND_NAME,
10
+ activeBackendName,
11
+ } from "./config.js";
7
12
 
8
13
  interface ConfigKey {
9
14
  /** Stored (non-resolved) value, "" when unset. */
@@ -12,26 +17,54 @@ interface ConfigKey {
12
17
  set(config: Config, value: string): void;
13
18
  }
14
19
 
20
+ // The backend.cloud.* keys predate named backends and remain the documented
21
+ // kv surface for the reserved "cloud" entry; `current_backend` is the named
22
+ // model's own key. `backend.type` stays the simple local/cloud switch
23
+ // (named backends switch through `config backend use`).
15
24
  const KEYS: Record<string, ConfigKey> = {
16
25
  "backend.type": {
17
- get: (config) => config.backend.type,
26
+ get: (config) =>
27
+ activeBackendName(config) === LOCAL_BACKEND_NAME ? "local" : "cloud",
18
28
  set: (config, value) => {
19
29
  if (value !== "local" && value !== "cloud") {
20
- throw new UsageError(`invalid backend.type "${value}" (expected: local, cloud)`);
30
+ throw new UsageError(
31
+ `invalid backend.type "${value}" (expected: local, cloud)`,
32
+ );
21
33
  }
22
- config.backend.type = value as BackendType;
34
+ if (value === "cloud") {
35
+ (config.backends ??= {})[CLOUD_BACKEND_NAME] ??= { type: "cloud" };
36
+ config.current_backend = CLOUD_BACKEND_NAME;
37
+ } else {
38
+ config.current_backend = LOCAL_BACKEND_NAME;
39
+ }
40
+ },
41
+ },
42
+ current_backend: {
43
+ get: (config) => activeBackendName(config),
44
+ set: (config, value) => {
45
+ if (
46
+ value !== LOCAL_BACKEND_NAME &&
47
+ config.backends?.[value] === undefined
48
+ ) {
49
+ throw new UsageError(
50
+ `unknown backend "${value}" (add it first: stigmer config backend add)`,
51
+ );
52
+ }
53
+ config.current_backend = value;
23
54
  },
24
55
  },
25
56
  "backend.cloud.endpoint": {
26
- get: (config) => config.backend.cloud?.endpoint ?? "",
57
+ get: (config) => config.backends?.[CLOUD_BACKEND_NAME]?.endpoint ?? "",
27
58
  set: (config, value) => {
28
- (config.backend.cloud ??= {}).endpoint = value;
59
+ const backends = (config.backends ??= {});
60
+ (backends[CLOUD_BACKEND_NAME] ??= { type: "cloud" }).endpoint = value;
29
61
  },
30
62
  },
31
63
  "backend.cloud.org_id": {
32
- get: (config) => config.backend.cloud?.org_id ?? "",
64
+ get: (config) => config.backends?.[CLOUD_BACKEND_NAME]?.org_id ?? "",
33
65
  set: (config, value) => {
34
- (config.backend.cloud ??= {}).org_id = value;
66
+ const backends = (config.backends ??= {});
67
+ (backends[CLOUD_BACKEND_NAME] ??= { type: "cloud" }).org_id = value;
35
68
  },
36
69
  },
37
70
  "context.organization": {
@@ -53,7 +86,11 @@ export function getConfigValue(config: Config, key: string): string {
53
86
  return entry.get(config);
54
87
  }
55
88
 
56
- export function setConfigValue(config: Config, key: string, value: string): void {
89
+ export function setConfigValue(
90
+ config: Config,
91
+ key: string,
92
+ value: string,
93
+ ): void {
57
94
  const entry = KEYS[key];
58
95
  if (entry === undefined) throw new UsageError(unknownKeyMessage(key));
59
96
  entry.set(config, value);
@@ -1,6 +1,11 @@
1
+ // Pins the effective-value resolvers over the named-backend model (O3):
2
+ // env > active entry > defaults for endpoint/token/org/console-URL, the
3
+ // selfhost credential lane (api_key), and the auth gate's three postures
4
+ // (local never, selfhost never client-side, cloud requires a credential).
5
+
1
6
  import { afterEach, describe, expect, it } from "vitest";
2
7
  import { CliExitError } from "../errors/index.js";
3
- import type { Config } from "./config.js";
8
+ import type { Config, NamedBackendConfig } from "./config.js";
4
9
  import {
5
10
  DEFAULT_CLOUD_CONSOLE_URL,
6
11
  ensureAuthenticated,
@@ -11,29 +16,78 @@ import {
11
16
  resolveToken,
12
17
  } from "./resolve.js";
13
18
 
14
- const ENV_KEYS = ["STIGMER_SERVER_ADDRESS", "STIGMER_API_KEY", "STIGMER_ORG_ID"] as const;
19
+ const ENV_KEYS = [
20
+ "STIGMER_SERVER_ADDRESS",
21
+ "STIGMER_API_KEY",
22
+ "STIGMER_ORG_ID",
23
+ ] as const;
15
24
 
16
25
  afterEach(() => {
17
26
  for (const key of ENV_KEYS) delete process.env[key];
18
27
  });
19
28
 
20
- function cloudConfig(overrides: Partial<Config["backend"]["cloud"]> = {}): Config {
21
- return { backend: { type: "cloud", cloud: { endpoint: "cloud.example:443", ...overrides } } };
29
+ function localConfig(context?: Config["context"]): Config {
30
+ return {
31
+ backend: { type: "local" },
32
+ backends: {},
33
+ current_backend: "local",
34
+ context,
35
+ };
36
+ }
37
+
38
+ function cloudConfig(
39
+ overrides: Partial<NamedBackendConfig> = {},
40
+ context?: Config["context"],
41
+ ): Config {
42
+ return {
43
+ backend: { type: "cloud" },
44
+ backends: {
45
+ cloud: { type: "cloud", endpoint: "cloud.example:443", ...overrides },
46
+ },
47
+ current_backend: "cloud",
48
+ context,
49
+ };
50
+ }
51
+
52
+ function selfhostConfig(overrides: Partial<NamedBackendConfig> = {}): Config {
53
+ return {
54
+ backend: { type: "cloud" },
55
+ backends: {
56
+ staging: {
57
+ type: "selfhost",
58
+ endpoint: "stigmer.example.com:7234",
59
+ ...overrides,
60
+ },
61
+ },
62
+ current_backend: "staging",
63
+ };
22
64
  }
23
65
 
24
66
  describe("resolveEndpoint", () => {
25
67
  it("defaults to the local daemon address in local mode", () => {
26
- expect(resolveEndpoint({ backend: { type: "local" } })).toBe("localhost:7234");
68
+ expect(resolveEndpoint(localConfig())).toBe("localhost:7234");
27
69
  });
28
70
 
29
71
  it("uses the cloud default when no endpoint is configured", () => {
30
- expect(resolveEndpoint({ backend: { type: "cloud" } })).toBe("api.stigmer.ai:443");
72
+ expect(resolveEndpoint(cloudConfig({ endpoint: undefined }))).toBe(
73
+ "api.stigmer.ai:443",
74
+ );
31
75
  });
32
76
 
33
77
  it("prefers the configured cloud endpoint", () => {
34
78
  expect(resolveEndpoint(cloudConfig())).toBe("cloud.example:443");
35
79
  });
36
80
 
81
+ it("uses the selfhost entry's endpoint", () => {
82
+ expect(resolveEndpoint(selfhostConfig())).toBe("stigmer.example.com:7234");
83
+ });
84
+
85
+ it("refuses a selfhost entry without an endpoint (hand-edited config)", () => {
86
+ expect(() =>
87
+ resolveEndpoint(selfhostConfig({ endpoint: undefined })),
88
+ ).toThrow(CliExitError);
89
+ });
90
+
37
91
  it("lets STIGMER_SERVER_ADDRESS override everything", () => {
38
92
  process.env.STIGMER_SERVER_ADDRESS = "override:7234";
39
93
  expect(resolveEndpoint(cloudConfig())).toBe("override:7234");
@@ -43,15 +97,27 @@ describe("resolveEndpoint", () => {
43
97
  describe("resolveToken", () => {
44
98
  it("prefers STIGMER_API_KEY over the config token", () => {
45
99
  process.env.STIGMER_API_KEY = "env-key";
46
- expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe("env-key");
100
+ expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe(
101
+ "env-key",
102
+ );
47
103
  });
48
104
 
49
- it("falls back to the config token", () => {
50
- expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe("config-token");
105
+ it("falls back to the cloud entry's login token", () => {
106
+ expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe(
107
+ "config-token",
108
+ );
109
+ });
110
+
111
+ it("uses the selfhost entry's api_key", () => {
112
+ expect(resolveToken(selfhostConfig({ api_key: "stk_stored" }))).toBe(
113
+ "stk_stored",
114
+ );
51
115
  });
52
116
 
53
117
  it("returns empty string when unauthenticated", () => {
54
- expect(resolveToken({ backend: { type: "cloud" } })).toBe("");
118
+ expect(resolveToken(cloudConfig({ endpoint: undefined }))).toBe("");
119
+ expect(resolveToken(selfhostConfig())).toBe("");
120
+ expect(resolveToken(localConfig())).toBe("");
55
121
  });
56
122
  });
57
123
 
@@ -62,64 +128,95 @@ describe("resolveOrganization", () => {
62
128
  expect(resolveOrganization(config, "flag-org")).toBe("flag-org");
63
129
  });
64
130
 
65
- it("falls back to STIGMER_ORG_ID, then context, then cloud org_id", () => {
131
+ it("falls back to STIGMER_ORG_ID, then context, then the entry's org_id", () => {
66
132
  process.env.STIGMER_ORG_ID = "env-org";
67
- expect(resolveOrganization(cloudConfig({ org_id: "config-org" }))).toBe("env-org");
133
+ expect(resolveOrganization(cloudConfig({ org_id: "config-org" }))).toBe(
134
+ "env-org",
135
+ );
68
136
  });
69
137
 
70
138
  it("uses context organization when no flag/env is set", () => {
71
- const config: Config = {
72
- backend: { type: "cloud", cloud: { org_id: "legacy" } },
73
- context: { organization: "ctx-org" },
74
- };
139
+ const config = cloudConfig(
140
+ { org_id: "legacy" },
141
+ { organization: "ctx-org" },
142
+ );
75
143
  expect(resolveOrganization(config)).toBe("ctx-org");
76
144
  expect(resolveContextOrganization(config)).toBe("ctx-org");
77
145
  });
78
146
 
79
- it("falls back to legacy cloud org_id", () => {
80
- expect(resolveContextOrganization(cloudConfig({ org_id: "legacy" }))).toBe("legacy");
147
+ it("falls back to the entry's org_id", () => {
148
+ expect(resolveContextOrganization(cloudConfig({ org_id: "legacy" }))).toBe(
149
+ "legacy",
150
+ );
81
151
  });
82
152
 
83
153
  it("defaults to the seedpack org in local mode when nothing is configured", () => {
84
- expect(resolveOrganization({ backend: { type: "local" } })).toBe("stigmer");
154
+ expect(resolveOrganization(localConfig())).toBe("stigmer");
155
+ });
156
+
157
+ it("defaults to the seedpack org on a selfhost backend (it IS the OSS server)", () => {
158
+ expect(resolveOrganization(selfhostConfig())).toBe("stigmer");
85
159
  });
86
160
 
87
161
  it("prefers an explicit context org over the local default", () => {
88
- const config: Config = { backend: { type: "local" }, context: { organization: "my-local-org" } };
89
- expect(resolveOrganization(config)).toBe("my-local-org");
162
+ expect(
163
+ resolveOrganization(localConfig({ organization: "my-local-org" })),
164
+ ).toBe("my-local-org");
90
165
  });
91
166
 
92
167
  it("does not apply the local default in cloud mode (org stays empty)", () => {
93
- expect(resolveOrganization({ backend: { type: "cloud" } })).toBe("");
168
+ expect(resolveOrganization(cloudConfig({ endpoint: undefined }))).toBe("");
94
169
  });
95
170
 
96
171
  it("reports an unset local org as empty via resolveContextOrganization (no implicit default)", () => {
97
172
  // The implicit local default lives only in resolveOrganization (effective
98
173
  // org for operations); the context query must stay faithful so config/context
99
174
  // display can show "(not set)".
100
- expect(resolveContextOrganization({ backend: { type: "local" } })).toBe("");
175
+ expect(resolveContextOrganization(localConfig())).toBe("");
101
176
  });
102
177
  });
103
178
 
104
179
  describe("resolveConsoleURL", () => {
105
180
  it("prefers the STIGMER_CONSOLE_URL override", () => {
106
- expect(resolveConsoleURL("cloud", { STIGMER_CONSOLE_URL: "https://console.example" } as NodeJS.ProcessEnv)).toBe(
107
- "https://console.example",
108
- );
181
+ expect(
182
+ resolveConsoleURL(cloudConfig(), {
183
+ STIGMER_CONSOLE_URL: "https://console.example",
184
+ } as NodeJS.ProcessEnv),
185
+ ).toBe("https://console.example");
109
186
  });
110
187
 
111
188
  it("uses the local web-console port for the local backend", () => {
112
- expect(resolveConsoleURL("local", {} as NodeJS.ProcessEnv)).toBe("http://localhost:8234");
189
+ expect(resolveConsoleURL(localConfig(), {} as NodeJS.ProcessEnv)).toBe(
190
+ "http://localhost:7234",
191
+ );
113
192
  });
114
193
 
115
194
  it("uses the cloud console URL for the cloud backend", () => {
116
- expect(resolveConsoleURL("cloud", {} as NodeJS.ProcessEnv)).toBe(DEFAULT_CLOUD_CONSOLE_URL);
195
+ expect(resolveConsoleURL(cloudConfig(), {} as NodeJS.ProcessEnv)).toBe(
196
+ DEFAULT_CLOUD_CONSOLE_URL,
197
+ );
198
+ });
199
+
200
+ it("derives the selfhost console from the entry's endpoint (one-origin rule)", () => {
201
+ expect(resolveConsoleURL(selfhostConfig(), {} as NodeJS.ProcessEnv)).toBe(
202
+ "http://stigmer.example.com:7234",
203
+ );
204
+ expect(
205
+ resolveConsoleURL(
206
+ selfhostConfig({ endpoint: "stigmer.example.com:443" }),
207
+ {} as NodeJS.ProcessEnv,
208
+ ),
209
+ ).toBe("https://stigmer.example.com");
117
210
  });
118
211
  });
119
212
 
120
213
  describe("ensureAuthenticated", () => {
121
214
  it("never requires auth in local mode", () => {
122
- expect(() => ensureAuthenticated({ backend: { type: "local" } })).not.toThrow();
215
+ expect(() => ensureAuthenticated(localConfig())).not.toThrow();
216
+ });
217
+
218
+ it("never gates selfhost client-side (the server owns that decision)", () => {
219
+ expect(() => ensureAuthenticated(selfhostConfig())).not.toThrow();
123
220
  });
124
221
 
125
222
  it("throws in cloud mode with no credentials", () => {
@@ -127,15 +224,19 @@ describe("ensureAuthenticated", () => {
127
224
  });
128
225
 
129
226
  it("passes with a stored access token", () => {
130
- expect(() => ensureAuthenticated(cloudConfig({ token: "t" }))).not.toThrow();
227
+ expect(() =>
228
+ ensureAuthenticated(cloudConfig({ token: "t" })),
229
+ ).not.toThrow();
131
230
  });
132
231
 
133
- it("passes with only a refresh token", () => {
134
- expect(() => ensureAuthenticated(cloudConfig({ refresh_token: "r" }))).not.toThrow();
232
+ it("passes with only a refresh token (silent refresh will mint one)", () => {
233
+ expect(() =>
234
+ ensureAuthenticated(cloudConfig({ refresh_token: "r" })),
235
+ ).not.toThrow();
135
236
  });
136
237
 
137
- it("passes with STIGMER_API_KEY set", () => {
138
- process.env.STIGMER_API_KEY = "env-key";
238
+ it("passes with STIGMER_API_KEY in the environment", () => {
239
+ process.env.STIGMER_API_KEY = "stk_env";
139
240
  expect(() => ensureAuthenticated(cloudConfig())).not.toThrow();
140
241
  });
141
242
  });
@@ -1,10 +1,14 @@
1
1
  // Effective-value resolution with env > config > default precedence. These are
2
2
  // the settings the backend client and read verbs depend on; centralizing the
3
3
  // precedence keeps every call site consistent (mirrors the Go CLI's Resolve*).
4
+ //
5
+ // All resolvers read the ACTIVE named backend (config.ts): local is the
6
+ // managed daemon (fixed endpoint, never requires auth), cloud entries carry
7
+ // the OAuth token set, selfhost entries carry an endpoint + api_key.
4
8
 
5
9
  import { CliExitError, ExitCode } from "../errors/index.js";
6
- import { WEB_CONSOLE_PORT } from "../local/constants.js";
7
- import { type BackendType, type Config, isCloudMode } from "./config.js";
10
+ import { SERVER_PORT } from "../local/constants.js";
11
+ import { type Config, activeBackend, isCloudMode } from "./config.js";
8
12
 
9
13
  const DEFAULT_CLOUD_ENDPOINT = "api.stigmer.ai:443";
10
14
  const DEFAULT_LOCAL_ENDPOINT = "localhost:7234";
@@ -15,45 +19,68 @@ export const DEFAULT_CLOUD_CONSOLE_URL = "https://app.stigmer.ai";
15
19
  // Local mode is single-tenant: the seedpack bootstraps resources into the
16
20
  // "stigmer" org (see client-apps/cli/src/local/seedpack/apply.ts DEFAULT_ORG).
17
21
  // Org-scoped getByReference now requires an org, so bare-slug `get`/`run`/`apply`
18
- // in local mode fall back to this default when nothing else is configured. Cloud
19
- // mode has no implicit org — the caller must select one (flag/env/context/login).
22
+ // in local mode fall back to this default when nothing else is configured. The
23
+ // same holds for selfhost backends — they ARE the OSS server. Cloud mode has no
24
+ // implicit org — the caller must select one (flag/env/context/login).
20
25
  const DEFAULT_LOCAL_ORG = "stigmer";
21
26
 
22
27
  /**
23
28
  * Resolve the server endpoint.
24
- * STIGMER_SERVER_ADDRESS (explicit override, either backend)
25
- * > cloud.endpoint (cloud mode)
26
- * > api.stigmer.ai:443 (cloud default) / localhost:7234 (local default)
29
+ * STIGMER_SERVER_ADDRESS (explicit override, any backend)
30
+ * > the active entry's endpoint (cloud falls back to api.stigmer.ai:443)
31
+ * > localhost:7234 (the local daemon)
32
+ *
33
+ * A selfhost entry without an endpoint is a loud configuration error —
34
+ * `config backend add` requires one, so this only happens to hand-edited
35
+ * files, and guessing an address for a self-hosted server helps nobody.
27
36
  */
28
37
  export function resolveEndpoint(config: Config): string {
29
38
  const override = process.env.STIGMER_SERVER_ADDRESS;
30
39
  if (override !== undefined && override !== "") {
31
40
  return override;
32
41
  }
33
- if (isCloudMode(config)) {
34
- return config.backend.cloud?.endpoint || DEFAULT_CLOUD_ENDPOINT;
42
+ const { name, entry } = activeBackend(config);
43
+ if (entry === undefined) {
44
+ return DEFAULT_LOCAL_ENDPOINT;
45
+ }
46
+ if (entry.endpoint !== undefined && entry.endpoint !== "") {
47
+ return entry.endpoint;
48
+ }
49
+ if (entry.type === "cloud") {
50
+ return DEFAULT_CLOUD_ENDPOINT;
35
51
  }
36
- return DEFAULT_LOCAL_ENDPOINT;
52
+ throw new CliExitError(
53
+ `backend "${name}" has no endpoint configured`,
54
+ ExitCode.Usage,
55
+ [
56
+ `Re-add it with one:`,
57
+ ` stigmer config backend add ${name} --endpoint <host:port>`,
58
+ ],
59
+ );
37
60
  }
38
61
 
39
62
  /**
40
63
  * Resolve the auth token.
41
- * STIGMER_API_KEY (env, also where the --api-key global is bridged)
42
- * > cloud.token (persisted login token)
43
- * > "" (unauthenticated)
64
+ * STIGMER_API_KEY (env, also where the --api-key global is bridged)
65
+ * > the active entry's credential (cloud: login token; selfhost: api_key)
66
+ * > "" (unauthenticated)
44
67
  */
45
68
  export function resolveToken(config: Config): string {
46
69
  const env = process.env.STIGMER_API_KEY;
47
70
  if (env !== undefined && env !== "") {
48
71
  return env;
49
72
  }
50
- return config.backend.cloud?.token ?? "";
73
+ const { entry } = activeBackend(config);
74
+ if (entry === undefined) {
75
+ return "";
76
+ }
77
+ return (entry.type === "cloud" ? entry.token : entry.api_key) ?? "";
51
78
  }
52
79
 
53
80
  /**
54
81
  * Resolve the active organization.
55
82
  * --org flag override > STIGMER_ORG_ID env > context.organization
56
- * > cloud.org_id (legacy fallback)
83
+ * > the active entry's org_id (legacy fallback)
57
84
  */
58
85
  export function resolveOrganization(config: Config, flagOrg?: string): string {
59
86
  if (flagOrg !== undefined && flagOrg !== "") {
@@ -67,53 +94,76 @@ export function resolveOrganization(config: Config, flagOrg?: string): string {
67
94
  if (context !== "") {
68
95
  return context;
69
96
  }
70
- // Nothing configured: local mode falls back to its single-tenant default org
71
- // so bare-slug operations resolve without ceremony; cloud stays empty (the
72
- // caller must select an org, e.g. apply raises a clear "organization not set").
97
+ // Nothing configured: local and selfhost fall back to the single-tenant
98
+ // default org so bare-slug operations resolve without ceremony; cloud
99
+ // stays empty (the caller must select an org, e.g. apply raises a clear
100
+ // "organization not set").
73
101
  return isCloudMode(config) ? "" : DEFAULT_LOCAL_ORG;
74
102
  }
75
103
 
76
104
  /**
77
105
  * Resolve the web console URL:
78
106
  * 1. `STIGMER_CONSOLE_URL` (explicit override)
79
- * 2. local backend → `http://localhost:{WEB_CONSOLE_PORT}`
80
- * 3. cloud backend → {@link DEFAULT_CLOUD_CONSOLE_URL}
107
+ * 2. local daemon → `http://localhost:{SERVER_PORT}` — the server's own
108
+ * unified port serves the console since DD-012 (one origin for UI and
109
+ * API; the Go-era separate 8234 listener is retired)
110
+ * 3. selfhost → the entry's endpoint on the same one-origin rule
111
+ * (https for :443, http otherwise)
112
+ * 4. cloud → {@link DEFAULT_CLOUD_CONSOLE_URL}
81
113
  *
82
114
  * The console origin is also the app origin that serves the hosted chat
83
115
  * page (`/chat/<org>/<slug>`) and the embed loader (`/embed.js`), so
84
- * share-link builders use this same resolver.
116
+ * share-link builders use this same resolver — both routes live in the
117
+ * same static export the server serves.
85
118
  */
86
- export function resolveConsoleURL(backendType: BackendType, env: NodeJS.ProcessEnv = process.env): string {
119
+ export function resolveConsoleURL(
120
+ config: Config,
121
+ env: NodeJS.ProcessEnv = process.env,
122
+ ): string {
87
123
  const override = env.STIGMER_CONSOLE_URL;
88
124
  if (override !== undefined && override !== "") return override;
89
- if (backendType === "local") return `http://localhost:${WEB_CONSOLE_PORT}`;
125
+ const { entry } = activeBackend(config);
126
+ if (entry === undefined) return `http://localhost:${SERVER_PORT}`;
127
+ if (entry.type === "selfhost") {
128
+ const endpoint = entry.endpoint ?? "";
129
+ const scheme = endpoint.endsWith(":443") ? "https" : "http";
130
+ return `${scheme}://${endpoint.replace(/:443$/, "")}`;
131
+ }
90
132
  return DEFAULT_CLOUD_CONSOLE_URL;
91
133
  }
92
134
 
93
135
  /**
94
- * The configured organization, with the legacy cloud.org_id fallback. Reports
95
- * only what is explicitly set (no implicit local default) so config/context
96
- * display can faithfully show "(not set)".
136
+ * The configured organization, with the active entry's org_id fallback.
137
+ * Reports only what is explicitly set (no implicit local default) so
138
+ * config/context display can faithfully show "(not set)".
97
139
  */
98
140
  export function resolveContextOrganization(config: Config): string {
99
- if (config.context?.organization !== undefined && config.context.organization !== "") {
141
+ if (
142
+ config.context?.organization !== undefined &&
143
+ config.context.organization !== ""
144
+ ) {
100
145
  return config.context.organization;
101
146
  }
102
- return config.backend.cloud?.org_id ?? "";
147
+ return activeBackend(config).entry?.org_id ?? "";
103
148
  }
104
149
 
105
150
  /**
106
- * Fail fast with a clear "please log in" error when a cloud command has no
107
- * usable credential. Local mode never requires auth. A credential is any of:
108
- * the STIGMER_API_KEY env (or bridged --api-key), a persisted access token, or
109
- * a refresh token (which the token provider can exchange for a fresh access
110
- * token). This lives in the config layer — not the client façade — because it
111
- * is purely a question of which credentials are configured.
151
+ * Fail fast with a clear "please log in" error when a CLOUD command has no
152
+ * usable credential. The local daemon never requires auth. Selfhost
153
+ * backends are deliberately not gated client-side: an unauthenticated
154
+ * self-host (no issuer configured) is a legitimate posture, and an
155
+ * authenticated one answers UNAUTHENTICATED with a clear message — the
156
+ * server owns that decision. A credential is any of: the STIGMER_API_KEY
157
+ * env (or bridged --api-key), a persisted access token, or a refresh token
158
+ * (which the token provider can exchange for a fresh access token).
112
159
  */
113
160
  export function ensureAuthenticated(config: Config): void {
114
- if (!isCloudMode(config)) return;
161
+ const { entry } = activeBackend(config);
162
+ if (entry?.type !== "cloud") return;
115
163
  if (process.env.STIGMER_API_KEY) return;
116
- const cloud = config.backend.cloud;
117
- if (cloud?.token || cloud?.refresh_token) return;
118
- throw new CliExitError("Not authenticated", ExitCode.Auth, ["Please sign in:", " stigmer auth login"]);
164
+ if (entry.token || entry.refresh_token) return;
165
+ throw new CliExitError("Not authenticated", ExitCode.Auth, [
166
+ "Please sign in:",
167
+ " stigmer auth login",
168
+ ]);
119
169
  }
@@ -7,12 +7,16 @@
7
7
  // runner-poll can never drift (see the T05 plan's "Critical correctness
8
8
  // finding").
9
9
 
10
- /** Port `stigmer-server` listens on (gRPC). Matches the Go CLI's `DaemonPort`. */
10
+ /**
11
+ * Port `stigmer-server` listens on (gRPC). Matches the Go CLI's `DaemonPort`.
12
+ *
13
+ * This is also the web console's origin: since DD-012 the server serves the
14
+ * console's static export on this same unified port (lane 4), so the Go-era
15
+ * WEB_CONSOLE_PORT (8234, a separate CLI-embedded listener) is retired —
16
+ * one process, one origin, no CORS hop.
17
+ */
11
18
  export const SERVER_PORT = 7234;
12
19
 
13
- /** Port the embedded web console is served on (7xxx=API, 8xxx=UI convention). */
14
- export const WEB_CONSOLE_PORT = 8234;
15
-
16
20
  /** Temporal frontend gRPC port (dev server default). */
17
21
  export const TEMPORAL_PORT = 7233;
18
22
 
@@ -11,7 +11,7 @@ const config: DaemonConfig = {
11
11
  temporalAddress: "127.0.0.1:7233",
12
12
  serverOnly: false,
13
13
  noWeb: false,
14
- serverBin: "/bin/stigmer-server",
14
+ server: { nodeBin: "/bin/node", entryPath: "/repo/server/dist/main.js", appDir: "/repo/server" },
15
15
  runner: { nodeBin: "/bin/node", entryPath: "/repo/runner/dist/main.js", appDir: "/repo/runner" },
16
16
  };
17
17
 
@@ -133,6 +133,14 @@ describe("buildComponents", () => {
133
133
  expect(runner.resolve().args).toEqual(["/repo/runner/dist/main.js"]);
134
134
  });
135
135
 
136
+ // The TS server child is `node <entry>` — the runner's launch shape.
137
+ it("spawns the node-shape server as node + entry", () => {
138
+ const [server] = buildComponents(config, {});
139
+ const spawn = server.resolve();
140
+ expect(spawn.command).toBe("/bin/node");
141
+ expect(spawn.args).toEqual(["/repo/server/dist/main.js"]);
142
+ });
143
+
136
144
  it("omits the runner in server-only mode", () => {
137
145
  const components = buildComponents({ ...config, serverOnly: true }, {});
138
146
  expect(components.map((c) => c.name)).toEqual(["stigmer-server"]);
@@ -91,9 +91,11 @@ export function buildComponents(config: DaemonConfig, base: NodeJS.ProcessEnv =
91
91
  name: "stigmer-server",
92
92
  pidFile: join(config.dataDir, SERVER_PID_FILE),
93
93
  critical: true,
94
+ // The server is `node <entry>` — the runner's launch shape, one child
95
+ // contract for both components.
94
96
  resolve: () => ({
95
- command: config.serverBin,
96
- args: [],
97
+ command: config.server.nodeBin,
98
+ args: [config.server.entryPath],
97
99
  env: buildServerEnv(config, base),
98
100
  logFile: join(config.logDir, "stigmer-server.log"),
99
101
  }),
@@ -128,7 +128,7 @@ describe("runInternalDaemon (full body with fakes)", () => {
128
128
  temporalAddress: "127.0.0.1:7233",
129
129
  serverOnly: false,
130
130
  noWeb: true,
131
- serverBin: "server-cmd",
131
+ server: { nodeBin: "node-cmd", entryPath: join(data, "server-entry.js"), appDir: data },
132
132
  runner: { nodeBin: "node-cmd", entryPath: join(data, "entry.js"), appDir: data },
133
133
  };
134
134