@stigmer/cli 3.13.0 → 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 (74) 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/config/config.d.ts +56 -7
  24. package/config/config.d.ts.map +1 -1
  25. package/config/config.js +115 -16
  26. package/config/config.js.map +1 -1
  27. package/config/index.d.ts +1 -1
  28. package/config/index.d.ts.map +1 -1
  29. package/config/index.js +1 -1
  30. package/config/index.js.map +1 -1
  31. package/config/keys.d.ts +1 -1
  32. package/config/keys.d.ts.map +1 -1
  33. package/config/keys.js +29 -6
  34. package/config/keys.js.map +1 -1
  35. package/config/resolve.d.ts +28 -20
  36. package/config/resolve.d.ts.map +1 -1
  37. package/config/resolve.js +74 -36
  38. package/config/resolve.js.map +1 -1
  39. package/local/seedpack/content.d.ts +3 -3
  40. package/local/seedpack/content.d.ts.map +1 -1
  41. package/local/seedpack/content.js +19 -13
  42. package/local/seedpack/content.js.map +1 -1
  43. package/package.json +5 -5
  44. package/resources/connect/connect.d.ts +4 -3
  45. package/resources/connect/connect.d.ts.map +1 -1
  46. package/resources/connect/connect.js +17 -5
  47. package/resources/connect/connect.js.map +1 -1
  48. package/resources/connect/oauth.d.ts +5 -2
  49. package/resources/connect/oauth.d.ts.map +1 -1
  50. package/resources/connect/oauth.js +5 -4
  51. package/resources/connect/oauth.js.map +1 -1
  52. package/src/auth/login.ts +54 -9
  53. package/src/auth/token.test.ts +69 -13
  54. package/src/auth/token.ts +46 -22
  55. package/src/client/client.test.ts +10 -2
  56. package/src/commands/apikey/index.ts +42 -11
  57. package/src/commands/auth/index.ts +52 -14
  58. package/src/commands/config/backend.test.ts +122 -0
  59. package/src/commands/config/backend.ts +195 -14
  60. package/src/commands/connect.test.ts +22 -4
  61. package/src/commands/connect.ts +37 -13
  62. package/src/commands/share.ts +36 -16
  63. package/src/config/config.test.ts +62 -6
  64. package/src/config/config.ts +165 -24
  65. package/src/config/index.ts +6 -0
  66. package/src/config/keys.test.ts +27 -4
  67. package/src/config/keys.ts +46 -9
  68. package/src/config/resolve.test.ts +135 -34
  69. package/src/config/resolve.ts +83 -36
  70. package/src/local/seedpack/content.ts +52 -21
  71. package/src/resources/connect/connect.integration.test.ts +79 -26
  72. package/src/resources/connect/connect.ts +72 -19
  73. package/src/resources/connect/oauth.test.ts +38 -12
  74. package/src/resources/connect/oauth.ts +27 -11
@@ -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:7234");
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
10
  import { SERVER_PORT } from "../local/constants.js";
7
- import { type BackendType, type Config, isCloudMode } from "./config.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,56 +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:{SERVER_PORT}` — the server's own
107
+ * 2. local daemon → `http://localhost:{SERVER_PORT}` — the server's own
80
108
  * unified port serves the console since DD-012 (one origin for UI and
81
109
  * API; the Go-era separate 8234 listener is retired)
82
- * 3. cloud backend → {@link DEFAULT_CLOUD_CONSOLE_URL}
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}
83
113
  *
84
114
  * The console origin is also the app origin that serves the hosted chat
85
115
  * page (`/chat/<org>/<slug>`) and the embed loader (`/embed.js`), so
86
116
  * share-link builders use this same resolver — both routes live in the
87
117
  * same static export the server serves.
88
118
  */
89
- 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 {
90
123
  const override = env.STIGMER_CONSOLE_URL;
91
124
  if (override !== undefined && override !== "") return override;
92
- if (backendType === "local") return `http://localhost:${SERVER_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
+ }
93
132
  return DEFAULT_CLOUD_CONSOLE_URL;
94
133
  }
95
134
 
96
135
  /**
97
- * The configured organization, with the legacy cloud.org_id fallback. Reports
98
- * only what is explicitly set (no implicit local default) so config/context
99
- * 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)".
100
139
  */
101
140
  export function resolveContextOrganization(config: Config): string {
102
- if (config.context?.organization !== undefined && config.context.organization !== "") {
141
+ if (
142
+ config.context?.organization !== undefined &&
143
+ config.context.organization !== ""
144
+ ) {
103
145
  return config.context.organization;
104
146
  }
105
- return config.backend.cloud?.org_id ?? "";
147
+ return activeBackend(config).entry?.org_id ?? "";
106
148
  }
107
149
 
108
150
  /**
109
- * Fail fast with a clear "please log in" error when a cloud command has no
110
- * usable credential. Local mode never requires auth. A credential is any of:
111
- * the STIGMER_API_KEY env (or bridged --api-key), a persisted access token, or
112
- * a refresh token (which the token provider can exchange for a fresh access
113
- * token). This lives in the config layer — not the client façade — because it
114
- * 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).
115
159
  */
116
160
  export function ensureAuthenticated(config: Config): void {
117
- if (!isCloudMode(config)) return;
161
+ const { entry } = activeBackend(config);
162
+ if (entry?.type !== "cloud") return;
118
163
  if (process.env.STIGMER_API_KEY) return;
119
- const cloud = config.backend.cloud;
120
- if (cloud?.token || cloud?.refresh_token) return;
121
- 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
+ ]);
122
169
  }
@@ -1,7 +1,7 @@
1
1
  // Resolution and on-demand acquisition of the seedpack content.
2
2
  //
3
3
  // The seedpack (system agents, skills, MCP servers, workflows under the
4
- // "stigmer" org) is the TS equivalent of the Go CLI's `go:embed`-ed bundle.
4
+ // "stigmer" org) is the system content every backend is bootstrapped with.
5
5
  // A lean `npx @stigmer/cli` must not carry ~300 content files (DD-002), so the
6
6
  // content ships as `@stigmer/seedpack` and is acquired on demand — the same
7
7
  // pattern as `@stigmer/runner-slim` and the managed Temporal binary.
@@ -12,14 +12,22 @@
12
12
  // ~/.stigmer/runtimes/<version>/ and read from
13
13
  // node_modules/@stigmer/seedpack.
14
14
  //
15
- // The content-hash and extract logic mirror `@stigmer/seedpack` exactly (which
16
- // in turn mirrors the Go `seedpack` package); the CLI reimplements them over a
17
- // resolved directory so it stays decoupled from the package in the lean install
18
- // (where the package is not a dependency, only an on-demand artifact).
15
+ // The content-hash and extract logic mirror `@stigmer/seedpack` exactly; the CLI
16
+ // reimplements them over a resolved directory so it stays decoupled from the
17
+ // package in the lean install (where the package is not a dependency, only an
18
+ // on-demand artifact). `seedpack/src/index.ts` is the one definition both follow.
19
19
 
20
20
  import { execFileSync } from "node:child_process";
21
21
  import { createHash } from "node:crypto";
22
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
22
+ import {
23
+ cpSync,
24
+ existsSync,
25
+ mkdirSync,
26
+ readdirSync,
27
+ readFileSync,
28
+ statSync,
29
+ writeFileSync,
30
+ } from "node:fs";
23
31
  import { homedir } from "node:os";
24
32
  import { dirname, join, posix, relative, sep } from "node:path";
25
33
  import { fileURLToPath } from "node:url";
@@ -33,9 +41,9 @@ const SEEDPACK_PACKAGE = "@stigmer/seedpack";
33
41
 
34
42
  /**
35
43
  * The canonical seedpack entries, in apply-safe order. Kept in sync with
36
- * `SEEDPACK_ENTRIES` in `@stigmer/seedpack` and the `//go:embed` set in
37
- * `seedpack/embed.go` — `tools/`, `icons/`, and `canary/` (CI canary metadata,
38
- * not a resource) are deliberately excluded so the content set (and thus the
44
+ * `SEEDPACK_ENTRIES` in `@stigmer/seedpack` (`seedpack/src/index.ts`, the one
45
+ * definition) — `tools/`, `icons/`, and `canary/` (CI canary metadata, not a
46
+ * resource) are deliberately excluded so the content set (and thus the
39
47
  * idempotency hash) is identical across delivery paths.
40
48
  */
41
49
  export const SEEDPACK_ENTRIES = [
@@ -70,7 +78,9 @@ export interface ResolveSeedpackOptions {
70
78
  * on-demand-acquired `@stigmer/seedpack` package. Throws actionable guidance when
71
79
  * acquisition is not possible (non-release build, or a failed/incomplete install).
72
80
  */
73
- export function resolveSeedpackContent(opts: ResolveSeedpackOptions = {}): SeedpackContent {
81
+ export function resolveSeedpackContent(
82
+ opts: ResolveSeedpackOptions = {},
83
+ ): SeedpackContent {
74
84
  const repo = repoSeedpackDir();
75
85
  if (repo !== null) return { dir: repo, source: "repo" };
76
86
  return { dir: acquireSeedpack(opts), source: "package" };
@@ -86,10 +96,14 @@ export function acquireSeedpack(opts: ResolveSeedpackOptions = {}): string {
86
96
  const home = opts.home ?? homedir();
87
97
  const version = opts.version ?? VERSION;
88
98
  if (!isAcquirableRelease(version)) {
89
- throw new CliExitError(`cannot acquire ${SEEDPACK_PACKAGE} for a non-release build (${version})`, ExitCode.General, [
90
- "Run from the repo (the seedpack/ tree is used directly in dev).",
91
- "On-demand acquisition is only available for published releases.",
92
- ]);
99
+ throw new CliExitError(
100
+ `cannot acquire ${SEEDPACK_PACKAGE} for a non-release build (${version})`,
101
+ ExitCode.General,
102
+ [
103
+ "Run from the repo (the seedpack/ tree is used directly in dev).",
104
+ "On-demand acquisition is only available for published releases.",
105
+ ],
106
+ );
93
107
  }
94
108
 
95
109
  const installDir = join(runtimesDir(home), version);
@@ -103,16 +117,21 @@ export function acquireSeedpack(opts: ResolveSeedpackOptions = {}): string {
103
117
  // additive and non-pruning, so this prefix is safely shared with the runner.
104
118
  const rootPkg = join(installDir, "package.json");
105
119
  if (!existsSync(rootPkg)) {
106
- writeFileSync(rootPkg, `${JSON.stringify({ name: "stigmer-runtime", private: true, version: "0.0.0" }, null, 2)}\n`);
120
+ writeFileSync(
121
+ rootPkg,
122
+ `${JSON.stringify({ name: "stigmer-runtime", private: true, version: "0.0.0" }, null, 2)}\n`,
123
+ );
107
124
  }
108
125
  const install = opts.install ?? installSeedpack;
109
126
  install(installDir, `${SEEDPACK_PACKAGE}@${version}`);
110
127
  }
111
128
 
112
129
  if (!existsSync(marker)) {
113
- throw new CliExitError(`${SEEDPACK_PACKAGE} install did not produce ${marker}`, ExitCode.General, [
114
- `Remove ${installDir} and retry.`,
115
- ]);
130
+ throw new CliExitError(
131
+ `${SEEDPACK_PACKAGE} install did not produce ${marker}`,
132
+ ExitCode.General,
133
+ [`Remove ${installDir} and retry.`],
134
+ );
116
135
  }
117
136
  return pkgDir;
118
137
  }
@@ -121,9 +140,21 @@ export function acquireSeedpack(opts: ResolveSeedpackOptions = {}): string {
121
140
  // the user sees the one-time download progress.
122
141
  function installSeedpack(installDir: string, spec: string): void {
123
142
  try {
124
- execFileSync("npm", ["install", spec, "--prefix", installDir, "--omit=dev", "--no-audit", "--no-fund"], {
125
- stdio: "inherit",
126
- });
143
+ execFileSync(
144
+ "npm",
145
+ [
146
+ "install",
147
+ spec,
148
+ "--prefix",
149
+ installDir,
150
+ "--omit=dev",
151
+ "--no-audit",
152
+ "--no-fund",
153
+ ],
154
+ {
155
+ stdio: "inherit",
156
+ },
157
+ );
127
158
  } catch (err) {
128
159
  throw new CliExitError(`failed to install ${spec}`, ExitCode.General, [
129
160
  `Command: npm install ${spec} --prefix ${installDir}`,