@stigmer/cli 3.13.0 → 3.14.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 (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
@@ -28,16 +28,37 @@ describe("load", () => {
28
28
  expect(load(path)).toEqual(getDefault());
29
29
  });
30
30
 
31
- it("parses a cloud config", () => {
31
+ it("migrates a legacy cloud config into the named model on load", () => {
32
32
  const path = tempConfigPath();
33
33
  writeFileSync(
34
34
  path,
35
35
  "backend:\n type: cloud\n cloud:\n endpoint: api.stigmer.ai:443\n org_id: acme\ncontext:\n organization: acme\n",
36
36
  );
37
37
  const config = load(path);
38
- expect(config.backend.type).toBe("cloud");
39
- expect(config.backend.cloud?.org_id).toBe("acme");
38
+ // The legacy slot becomes the reserved "cloud" entry; the legacy type
39
+ // selects it as current. The slot itself is not carried forward.
40
+ expect(config.current_backend).toBe("cloud");
41
+ expect(config.backends?.["cloud"]).toEqual({
42
+ type: "cloud",
43
+ endpoint: "api.stigmer.ai:443",
44
+ org_id: "acme",
45
+ });
46
+ expect(config.backend.cloud).toBeUndefined();
40
47
  expect(config.context?.organization).toBe("acme");
48
+ expect(isCloudMode(config)).toBe(true);
49
+ });
50
+
51
+ it("parses a named-model config with selfhost entries", () => {
52
+ const path = tempConfigPath();
53
+ writeFileSync(
54
+ path,
55
+ "backend:\n type: cloud\nbackends:\n staging:\n type: selfhost\n endpoint: stigmer.example.com:7234\n api_key: stk_x\ncurrent_backend: staging\n",
56
+ );
57
+ const config = load(path);
58
+ expect(config.current_backend).toBe("staging");
59
+ expect(config.backends?.["staging"]?.type).toBe("selfhost");
60
+ expect(config.backends?.["staging"]?.api_key).toBe("stk_x");
61
+ expect(isCloudMode(config)).toBe(false);
41
62
  });
42
63
  });
43
64
 
@@ -45,17 +66,52 @@ describe("save", () => {
45
66
  it("round-trips a config and writes a 0600 file with the doc header", () => {
46
67
  const path = tempConfigPath();
47
68
  const config = getDefault();
48
- config.backend.type = "cloud";
49
- config.backend.cloud = { endpoint: "api.stigmer.ai:443", token: "t", org_id: "acme" };
69
+ (config.backends ??= {})["cloud"] = {
70
+ type: "cloud",
71
+ endpoint: "api.stigmer.ai:443",
72
+ token: "t",
73
+ org_id: "acme",
74
+ };
75
+ config.current_backend = "cloud";
50
76
  save(config, path);
51
77
 
52
78
  const text = readFileSync(path, "utf8");
53
79
  expect(text).toContain("# Stigmer CLI Configuration");
54
- expect(load(path)).toEqual(config);
80
+ const reloaded = load(path);
81
+ expect(reloaded.backends).toEqual(config.backends);
82
+ expect(reloaded.current_backend).toBe("cloud");
83
+ // The legacy mirror is written for older readers.
84
+ expect(reloaded.backend.type).toBe("cloud");
55
85
 
56
86
  rmSync(path);
57
87
  });
58
88
 
89
+ it("writes a legacy-loaded cloud config in the named shape (the one-time migration)", () => {
90
+ const path = tempConfigPath();
91
+ writeFileSync(
92
+ path,
93
+ "backend:\n type: cloud\n cloud:\n endpoint: api.stigmer.ai:443\n token: t\n",
94
+ );
95
+ save(load(path), path);
96
+
97
+ const text = readFileSync(path, "utf8");
98
+ expect(text).toContain("backends:");
99
+ expect(text).toContain("current_backend: cloud");
100
+ // The legacy slot is gone; its content lives in backends.cloud.
101
+ expect(text).not.toContain(" cloud:\n endpoint");
102
+ const reloaded = load(path);
103
+ expect(reloaded.backends?.["cloud"]?.token).toBe("t");
104
+ });
105
+
106
+ it("keeps a pristine local config byte-stable (no named-model keys appear)", () => {
107
+ const path = tempConfigPath();
108
+ save(getDefault(), path);
109
+ const text = readFileSync(path, "utf8");
110
+ expect(text).not.toContain("backends:");
111
+ expect(text).not.toContain("current_backend:");
112
+ expect(text).toContain("type: local");
113
+ });
114
+
59
115
  it("preserves the opaque local backend section across a load/save round-trip", () => {
60
116
  const path = tempConfigPath();
61
117
  writeFileSync(
@@ -1,41 +1,81 @@
1
1
  // CLI configuration model and persistence (~/.stigmer/config.yaml).
2
2
  //
3
- // Parity + coexistence: the file format matches the Go CLI exactly (snake_case
4
- // keys, 0600 perms, doc-link header) so a single config works whether invoked
5
- // through the Go or TS CLI during the migration. Crucially, the local-backend
6
- // section (daemon/LLM/Temporal settings owned by later waves and the Go CLI) is
7
- // carried through opaquely on save, so the TS CLI never drops fields it does
8
- // not yet model.
3
+ // Named backends (O3, 20260827.06 — the parent program's recorded revisit:
4
+ // "a named backend without a credential story is half a feature"): the
5
+ // `backends` map + `current_backend` are the kubectl-context model — each
6
+ // entry carries its own endpoint AND its own credentials, so switching
7
+ // between Stigmer Cloud and a self-hosted server never clobbers either
8
+ // side's login state. Two names are reserved: "local" (the managed daemon,
9
+ // never stored in the map — its endpoint and no-auth posture are fixed)
10
+ // and "cloud" (where `stigmer auth login` lands by default).
11
+ //
12
+ // Legacy shape + migration: pre-O3 files carried one `backend.cloud` slot
13
+ // selected by `backend.type`. Loading migrates that shape in memory
14
+ // (cloud section → `backends.cloud`, type → `current_backend`); the first
15
+ // save writes the new shape — the ruled one-time write migration. The
16
+ // legacy `backend.type` is still WRITTEN (mirroring local vs non-local)
17
+ // so older readers keep a coherent view, and the opaque `backend.local`
18
+ // section (daemon/LLM/Temporal settings owned by other tools) is
19
+ // preserved verbatim, exactly as before.
9
20
 
10
21
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
22
  import { dirname } from "node:path";
12
23
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
24
+ import { UsageError } from "../errors/index.js";
13
25
  import { isStandalone } from "../runtime.js";
14
26
  import { configPath } from "./paths.js";
15
27
 
28
+ /** The legacy binary switch, still written as a mirror for older readers. */
16
29
  export type BackendType = "local" | "cloud";
17
30
 
18
- /** Cloud backend connection. Property names are snake_case to serialize to the
19
- * exact YAML keys the Go CLI reads/writes. */
31
+ /** The two named-backend families: Stigmer Cloud, or any OSS server. */
32
+ export type NamedBackendType = "cloud" | "selfhost";
33
+
34
+ /** The reserved name of the managed local daemon backend. */
35
+ export const LOCAL_BACKEND_NAME = "local";
36
+
37
+ /** The reserved name `stigmer auth login` targets from local mode. */
38
+ export const CLOUD_BACKEND_NAME = "cloud";
39
+
40
+ /**
41
+ * One named backend: endpoint + its own credentials. Property names are
42
+ * snake_case to keep the YAML in the file's established dialect.
43
+ *
44
+ * Credential lanes by type:
45
+ * - cloud: the OAuth token set (token / refresh_token / token_expiry),
46
+ * written by `stigmer auth login` and rotated by the token provider.
47
+ * - selfhost: `api_key` — a server-minted `stk_` token
48
+ * (`stigmer apikey create`). Browser login against self-hosted
49
+ * issuers is deliberately not modeled here; the API token is the
50
+ * self-host credential story.
51
+ */
52
+ export interface NamedBackendConfig {
53
+ type: NamedBackendType;
54
+ endpoint?: string;
55
+ token?: string;
56
+ org_id?: string;
57
+ env_id?: string;
58
+ refresh_token?: string;
59
+ token_expiry?: string;
60
+ api_key?: string;
61
+ }
62
+
63
+ /** Cloud backend connection — the LEGACY single-slot shape (pre-O3 files). */
20
64
  export interface CloudBackendConfig {
21
65
  endpoint?: string;
22
66
  token?: string;
23
67
  org_id?: string;
24
68
  env_id?: string;
25
- // Refresh-token support is new in the TS CLI (the Go CLI persists only the
26
- // access token). These ride alongside the access token so the CLI can refresh
27
- // silently instead of forcing re-login every hour. Interop note: the Go CLI
28
- // models cloud config as a fixed struct and will drop these two fields if it
29
- // re-saves the file — acceptable during the migration that retires it.
30
69
  refresh_token?: string;
31
70
  token_expiry?: string;
32
71
  }
33
72
 
34
73
  export interface BackendConfig {
35
74
  type: BackendType;
36
- /** Local daemon/LLM/Temporal settings — opaque to the TS CLI in Wave 1,
75
+ /** Local daemon/LLM/Temporal settings — opaque to this CLI,
37
76
  * preserved verbatim across save so coexisting tools keep their config. */
38
77
  local?: unknown;
78
+ /** Legacy cloud slot — read for migration, never written back. */
39
79
  cloud?: CloudBackendConfig;
40
80
  }
41
81
 
@@ -45,6 +85,8 @@ export interface ContextConfig {
45
85
 
46
86
  export interface Config {
47
87
  backend: BackendConfig;
88
+ backends?: Record<string, NamedBackendConfig>;
89
+ current_backend?: string;
48
90
  context?: ContextConfig;
49
91
  }
50
92
 
@@ -54,9 +96,13 @@ const SAVE_HEADER = `# Stigmer CLI Configuration
54
96
 
55
97
  `;
56
98
 
57
- /** The default config: local backend, no daemon settings fabricated. */
99
+ /** The default config: the local backend, no daemon settings fabricated. */
58
100
  export function getDefault(): Config {
59
- return { backend: { type: "local" } };
101
+ return {
102
+ backend: { type: "local" },
103
+ backends: {},
104
+ current_backend: LOCAL_BACKEND_NAME,
105
+ };
60
106
  }
61
107
 
62
108
  /**
@@ -81,30 +127,125 @@ export function load(path: string = configPath()): Config {
81
127
  return normalize(parsed);
82
128
  }
83
129
 
84
- /** Persist the config to disk (0600 file in a 0755 directory), with the
85
- * documentation header the Go CLI writes. */
130
+ /**
131
+ * Persist the config to disk (0600 file in a 0755 directory), with the
132
+ * documentation header the file has always carried. Writes the NAMED shape
133
+ * (the one-time migration): the legacy cloud slot is dropped — its content
134
+ * lives in `backends.cloud` — and `backend.type` is written as the
135
+ * local-vs-not mirror. A pristine local config (no named backends) writes
136
+ * exactly the pre-O3 bytes.
137
+ */
86
138
  export function save(config: Config, path: string = configPath()): void {
87
139
  mkdirSync(dirname(path), { recursive: true, mode: 0o755 });
88
- const body = stringifyYaml(stripUndefined(config));
140
+ const body = stringifyYaml(stripUndefined(serializable(config)));
89
141
  writeFileSync(path, SAVE_HEADER + body, { mode: 0o600 });
90
142
  }
91
143
 
92
- /** True when the cloud backend is selected. */
144
+ /** The active backend's name (normalize always stamps current_backend). */
145
+ export function activeBackendName(config: Config): string {
146
+ const name = config.current_backend ?? "";
147
+ return name === "" ? LOCAL_BACKEND_NAME : name;
148
+ }
149
+
150
+ /**
151
+ * The active backend: the reserved local daemon (entry undefined) or a
152
+ * named entry. A current_backend naming a missing entry is a loud
153
+ * configuration error, never a silent fallback — the config was edited by
154
+ * hand or a remove left it dangling.
155
+ */
156
+ export function activeBackend(config: Config): {
157
+ name: string;
158
+ entry: NamedBackendConfig | undefined;
159
+ } {
160
+ const name = activeBackendName(config);
161
+ if (name === LOCAL_BACKEND_NAME) {
162
+ return { name, entry: undefined };
163
+ }
164
+ const entry = config.backends?.[name];
165
+ if (entry === undefined) {
166
+ throw new UsageError(
167
+ `current backend "${name}" does not exist — run 'stigmer config backend list' and 'stigmer config backend use <name>'`,
168
+ );
169
+ }
170
+ return { name, entry };
171
+ }
172
+
173
+ /** True when the active backend is Stigmer Cloud. */
93
174
  export function isCloudMode(config: Config): boolean {
94
- return config.backend.type === "cloud";
175
+ return activeBackend(config).entry?.type === "cloud";
95
176
  }
96
177
 
97
178
  function normalize(parsed: Partial<Config> | null): Config {
98
- if (parsed === null || parsed.backend === undefined) {
179
+ if (parsed === null || typeof parsed !== "object") {
99
180
  return getDefault();
100
181
  }
101
- const type: BackendType = parsed.backend.type === "cloud" ? "cloud" : "local";
182
+
183
+ const legacy = parsed.backend;
184
+ const backends = normalizeBackends(parsed.backends);
185
+
186
+ // Migration: a pre-O3 cloud slot becomes the reserved "cloud" entry —
187
+ // unless a named shape already exists (then the named shape wins and the
188
+ // stale legacy slot is ignored).
189
+ if (Object.keys(backends).length === 0 && legacy?.cloud !== undefined) {
190
+ backends[CLOUD_BACKEND_NAME] = { type: "cloud", ...legacy.cloud };
191
+ }
192
+
193
+ let current =
194
+ typeof parsed.current_backend === "string" ? parsed.current_backend : "";
195
+ if (current === "") {
196
+ current =
197
+ legacy?.type === "cloud" && backends[CLOUD_BACKEND_NAME] !== undefined
198
+ ? CLOUD_BACKEND_NAME
199
+ : LOCAL_BACKEND_NAME;
200
+ }
201
+
102
202
  return {
103
- backend: { type, local: parsed.backend.local, cloud: parsed.backend.cloud },
203
+ backend: {
204
+ type: legacy?.type === "cloud" ? "cloud" : "local",
205
+ local: legacy?.local,
206
+ },
207
+ backends,
208
+ current_backend: current,
104
209
  context: parsed.context,
105
210
  };
106
211
  }
107
212
 
213
+ function normalizeBackends(
214
+ parsed: Record<string, NamedBackendConfig> | undefined,
215
+ ): Record<string, NamedBackendConfig> {
216
+ const backends: Record<string, NamedBackendConfig> = {};
217
+ if (parsed === undefined || typeof parsed !== "object") {
218
+ return backends;
219
+ }
220
+ for (const [name, entry] of Object.entries(parsed)) {
221
+ if (entry === null || typeof entry !== "object") continue;
222
+ backends[name] = {
223
+ ...entry,
224
+ type: entry.type === "cloud" ? "cloud" : "selfhost",
225
+ };
226
+ }
227
+ return backends;
228
+ }
229
+
230
+ /** The on-disk shape: named model + legacy mirror, minimal for defaults. */
231
+ function serializable(config: Config): Config {
232
+ const backends = config.backends ?? {};
233
+ const current = activeBackendName(config);
234
+ const pristineLocal =
235
+ Object.keys(backends).length === 0 && current === LOCAL_BACKEND_NAME;
236
+ return {
237
+ backend: {
238
+ // The legacy mirror: local stays "local"; anything named is "cloud"
239
+ // to older readers (which cannot represent selfhost anyway).
240
+ type: current === LOCAL_BACKEND_NAME ? "local" : "cloud",
241
+ local: config.backend.local,
242
+ // The legacy cloud slot is never written back — migrated.
243
+ },
244
+ ...(pristineLocal ? {} : { backends, current_backend: current }),
245
+ context: config.context,
246
+ };
247
+ }
248
+
108
249
  // Recursively drop undefined-valued keys so the serialized YAML omits empty
109
250
  // optionals (matching Go's yaml omitempty), while preserving the opaque local
110
251
  // section untouched.
@@ -6,6 +6,12 @@ export {
6
6
  type CloudBackendConfig,
7
7
  type Config,
8
8
  type ContextConfig,
9
+ type NamedBackendConfig,
10
+ type NamedBackendType,
11
+ CLOUD_BACKEND_NAME,
12
+ LOCAL_BACKEND_NAME,
13
+ activeBackend,
14
+ activeBackendName,
9
15
  getDefault,
10
16
  isCloudMode,
11
17
  load,
@@ -10,6 +10,7 @@ describe("config keys", () => {
10
10
  "backend.cloud.org_id",
11
11
  "backend.type",
12
12
  "context.organization",
13
+ "current_backend",
13
14
  ]);
14
15
  });
15
16
 
@@ -21,18 +22,40 @@ describe("config keys", () => {
21
22
  expect(getConfigValue(getDefault(), "backend.cloud.org_id")).toBe("");
22
23
  });
23
24
 
24
- it("sets a nested key, creating sub-objects", () => {
25
+ it("sets a nested key, creating the reserved cloud entry", () => {
25
26
  const config = getDefault();
26
27
  setConfigValue(config, "backend.cloud.org_id", "acme");
27
- expect(config.backend.cloud?.org_id).toBe("acme");
28
+ expect(config.backends?.["cloud"]?.org_id).toBe("acme");
29
+ expect(getConfigValue(config, "backend.cloud.org_id")).toBe("acme");
30
+ });
31
+
32
+ it("backend.type set to cloud selects the reserved cloud entry", () => {
33
+ const config = getDefault();
34
+ setConfigValue(config, "backend.type", "cloud");
35
+ expect(config.current_backend).toBe("cloud");
36
+ expect(config.backends?.["cloud"]?.type).toBe("cloud");
37
+ expect(getConfigValue(config, "backend.type")).toBe("cloud");
38
+ });
39
+
40
+ it("current_backend switches only to known names", () => {
41
+ const config = getDefault();
42
+ expect(() => setConfigValue(config, "current_backend", "nope")).toThrow(
43
+ UsageError,
44
+ );
45
+ setConfigValue(config, "current_backend", "local");
46
+ expect(getConfigValue(config, "current_backend")).toBe("local");
28
47
  });
29
48
 
30
49
  it("validates backend.type values", () => {
31
- expect(() => setConfigValue(getDefault(), "backend.type", "bogus")).toThrow(UsageError);
50
+ expect(() => setConfigValue(getDefault(), "backend.type", "bogus")).toThrow(
51
+ UsageError,
52
+ );
32
53
  });
33
54
 
34
55
  it("rejects unknown keys", () => {
35
56
  expect(() => getConfigValue(getDefault(), "nope.nope")).toThrow(UsageError);
36
- expect(() => setConfigValue(getDefault(), "nope.nope", "x")).toThrow(UsageError);
57
+ expect(() => setConfigValue(getDefault(), "nope.nope", "x")).toThrow(
58
+ UsageError,
59
+ );
37
60
  });
38
61
  });
@@ -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);