@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
@@ -4,12 +4,24 @@
4
4
  // the standard mutating-output flags.
5
5
 
6
6
  import type { Command } from "commander";
7
- import { ensureAuthenticated, load, resolveContextOrganization, save } from "../../config/index.js";
8
- import { CommandResult, type OutputFlags, renderResult } from "../../output/index.js";
7
+ import {
8
+ activeBackend,
9
+ ensureAuthenticated,
10
+ load,
11
+ resolveContextOrganization,
12
+ save,
13
+ } from "../../config/index.js";
14
+ import {
15
+ CommandResult,
16
+ type OutputFlags,
17
+ renderResult,
18
+ } from "../../output/index.js";
9
19
  import { addResultFlags, resultFormat } from "../shared.js";
10
20
 
11
21
  export function registerAuth(program: Command): void {
12
- const auth = program.command("auth").description("manage authentication with Stigmer Cloud");
22
+ const auth = program
23
+ .command("auth")
24
+ .description("manage authentication with Stigmer Cloud");
13
25
 
14
26
  auth
15
27
  .command("login")
@@ -44,15 +56,32 @@ export function registerAuth(program: Command): void {
44
56
 
45
57
  function runLogout(): CommandResult {
46
58
  const config = load();
47
- const cloud = config.backend.cloud;
48
- if (cloud === undefined || (cloud.token === undefined && cloud.refresh_token === undefined)) {
49
- return CommandResult.warning("Not currently logged in").hint("Run 'stigmer auth login' to authenticate.");
59
+ const { name, entry } = activeBackend(config);
60
+ if (entry?.type === "selfhost") {
61
+ if (entry.api_key === undefined) {
62
+ return CommandResult.warning("No API key stored for this backend");
63
+ }
64
+ entry.api_key = undefined;
65
+ save(config);
66
+ return CommandResult.success(
67
+ `Cleared the stored API key for backend "${name}"`,
68
+ );
69
+ }
70
+ if (
71
+ entry === undefined ||
72
+ (entry.token === undefined && entry.refresh_token === undefined)
73
+ ) {
74
+ return CommandResult.warning("Not currently logged in").hint(
75
+ "Run 'stigmer auth login' to authenticate.",
76
+ );
50
77
  }
51
- cloud.token = undefined;
52
- cloud.refresh_token = undefined;
53
- cloud.token_expiry = undefined;
78
+ entry.token = undefined;
79
+ entry.refresh_token = undefined;
80
+ entry.token_expiry = undefined;
54
81
  save(config);
55
- return CommandResult.success("Logged out from Stigmer Cloud").hint("Run 'stigmer auth login' to authenticate again.");
82
+ return CommandResult.success("Logged out from Stigmer Cloud").hint(
83
+ "Run 'stigmer auth login' to authenticate again.",
84
+ );
56
85
  }
57
86
 
58
87
  async function runWhoami(): Promise<CommandResult> {
@@ -66,21 +95,30 @@ async function runWhoami(): Promise<CommandResult> {
66
95
 
67
96
  if (account.metadata !== undefined) {
68
97
  section.field("Account ID", account.metadata.id);
69
- if (account.metadata.name !== "") section.field("Name", account.metadata.name);
98
+ if (account.metadata.name !== "")
99
+ section.field("Name", account.metadata.name);
70
100
  }
71
101
  if (account.spec !== undefined) {
72
102
  if (account.spec.email !== "") section.field("Email", account.spec.email);
73
103
  if (account.spec.firstName !== "" || account.spec.lastName !== "") {
74
- section.field("Full Name", `${account.spec.firstName} ${account.spec.lastName}`.trim());
104
+ section.field(
105
+ "Full Name",
106
+ `${account.spec.firstName} ${account.spec.lastName}`.trim(),
107
+ );
75
108
  }
76
- section.field("Account Type", account.spec.isMachineAccount ? "Machine Account" : "User Account");
109
+ section.field(
110
+ "Account Type",
111
+ account.spec.isMachineAccount ? "Machine Account" : "User Account",
112
+ );
77
113
  }
78
114
 
79
115
  const org = resolveContextOrganization(client.config);
80
116
  if (org !== "") {
81
117
  section.field("Organization", org);
82
118
  } else {
83
- result.hint("No organization set. Use: stigmer config context set --org <slug>");
119
+ result.hint(
120
+ "No organization set. Use: stigmer config context set --org <slug>",
121
+ );
84
122
  }
85
123
 
86
124
  return result;
@@ -0,0 +1,122 @@
1
+ // Command-level contract for the named-backend surface (O3):
2
+ // add/use/list/remove over the real config file (HOME redirected to a
3
+ // throwaway dir), the reserved-name refusals, and the remove-current guard.
4
+ // The model-level semantics (migration, resolution, credentials) are pinned
5
+ // in config.test.ts / resolve.test.ts — this file pins the command layer's
6
+ // own rules.
7
+
8
+ import { mkdtempSync, rmSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12
+ import { load } from "../../config/index.js";
13
+ import { classify, ExitCode } from "../../errors/index.js";
14
+ import { buildProgram } from "../../program.js";
15
+
16
+ let home: string;
17
+ let originalHome: string | undefined;
18
+
19
+ beforeEach(() => {
20
+ originalHome = process.env.HOME;
21
+ home = mkdtempSync(join(tmpdir(), "stigmer-backend-cmd-"));
22
+ process.env.HOME = home;
23
+ });
24
+
25
+ afterEach(() => {
26
+ if (originalHome === undefined) delete process.env.HOME;
27
+ else process.env.HOME = originalHome;
28
+ rmSync(home, { recursive: true, force: true });
29
+ });
30
+
31
+ interface RunOutcome {
32
+ readonly exitCode: number;
33
+ readonly message: string;
34
+ }
35
+
36
+ async function run(...args: string[]): Promise<RunOutcome> {
37
+ const program = buildProgram();
38
+ program.exitOverride();
39
+ const outSpy = vi.spyOn(process.stdout, "write").mockReturnValue(true);
40
+ const errSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true);
41
+ try {
42
+ await program.parseAsync(["node", "stigmer", "config", "backend", ...args]);
43
+ return { exitCode: ExitCode.Success, message: "" };
44
+ } catch (err) {
45
+ return {
46
+ exitCode: classify(err)?.exitCode ?? -1,
47
+ message: err instanceof Error ? err.message : String(err),
48
+ };
49
+ } finally {
50
+ outSpy.mockRestore();
51
+ errSpy.mockRestore();
52
+ }
53
+ }
54
+
55
+ describe("config backend add/use/list/remove", () => {
56
+ it("add persists a selfhost entry with its endpoint and api key", async () => {
57
+ const outcome = await run(
58
+ "add",
59
+ "staging",
60
+ "--endpoint",
61
+ "stigmer.example.com:7234",
62
+ "--api-key",
63
+ "stk_x",
64
+ );
65
+ expect(outcome.exitCode).toBe(ExitCode.Success);
66
+ const config = load();
67
+ expect(config.backends?.["staging"]).toEqual({
68
+ type: "selfhost",
69
+ endpoint: "stigmer.example.com:7234",
70
+ api_key: "stk_x",
71
+ });
72
+ // Adding never switches — the user opts in with `use`.
73
+ expect(config.current_backend).toBe("local");
74
+ });
75
+
76
+ it("add updates an existing entry without dropping its credential", async () => {
77
+ await run("add", "staging", "--endpoint", "old:7234", "--api-key", "stk_x");
78
+ await run("add", "staging", "--endpoint", "new:7234");
79
+ const entry = load().backends?.["staging"];
80
+ expect(entry?.endpoint).toBe("new:7234");
81
+ expect(entry?.api_key).toBe("stk_x");
82
+ });
83
+
84
+ it("use switches the current backend; unknown names refuse", async () => {
85
+ await run("add", "staging", "--endpoint", "host:7234");
86
+ expect((await run("use", "staging")).exitCode).toBe(ExitCode.Success);
87
+ expect(load().current_backend).toBe("staging");
88
+
89
+ const unknown = await run("use", "nope");
90
+ expect(unknown.exitCode).toBe(ExitCode.Usage);
91
+ expect(unknown.message).toContain('unknown backend "nope"');
92
+ });
93
+
94
+ it("the local name is reserved: add and remove refuse it", async () => {
95
+ const add = await run("add", "local", "--endpoint", "host:7234");
96
+ expect(add.exitCode).toBe(ExitCode.Usage);
97
+ const remove = await run("remove", "local");
98
+ expect(remove.exitCode).toBe(ExitCode.Usage);
99
+ });
100
+
101
+ it("remove refuses the current backend and deletes others", async () => {
102
+ await run("add", "staging", "--endpoint", "host:7234");
103
+ await run("use", "staging");
104
+
105
+ const current = await run("remove", "staging");
106
+ expect(current.exitCode).toBe(ExitCode.Usage);
107
+ expect(current.message).toContain("current backend");
108
+
109
+ await run("use", "local");
110
+ expect((await run("remove", "staging")).exitCode).toBe(ExitCode.Success);
111
+ expect(load().backends?.["staging"]).toBeUndefined();
112
+ });
113
+
114
+ it("set cloud seeds and selects the reserved cloud entry (legacy sugar)", async () => {
115
+ expect((await run("set", "cloud")).exitCode).toBe(ExitCode.Success);
116
+ const config = load();
117
+ expect(config.current_backend).toBe("cloud");
118
+ expect(config.backends?.["cloud"]?.endpoint).toBe("api.stigmer.ai:443");
119
+ expect((await run("set", "local")).exitCode).toBe(ExitCode.Success);
120
+ expect(load().current_backend).toBe("local");
121
+ });
122
+ });
@@ -1,13 +1,36 @@
1
- // `stigmer config backend status|set` — inspect and switch the active backend.
1
+ // `stigmer config backend status|set|add|use|list|remove` — the named-backend
2
+ // surface (O3, 20260827.06; the kubectl-context model). `status` and
3
+ // `set <local|cloud>` predate named backends and stay as the simple view:
4
+ // set is sugar for `use` over the two reserved names.
2
5
 
3
6
  import type { Command } from "commander";
4
- import { type Config, isCloudMode, load, resolveEndpoint, save } from "../../config/index.js";
7
+ import {
8
+ CLOUD_BACKEND_NAME,
9
+ type Config,
10
+ LOCAL_BACKEND_NAME,
11
+ type NamedBackendType,
12
+ activeBackend,
13
+ activeBackendName,
14
+ load,
15
+ resolveEndpoint,
16
+ save,
17
+ } from "../../config/index.js";
5
18
  import { UsageError } from "../../errors/index.js";
6
- import { CommandResult, type OutputFlags, renderResult } from "../../output/index.js";
19
+ import {
20
+ CommandResult,
21
+ type OutputFlags,
22
+ renderResult,
23
+ } from "../../output/index.js";
7
24
  import { addResultFlags, resultFormat } from "../shared.js";
8
25
 
9
26
  const DEFAULT_CLOUD_ENDPOINT = "api.stigmer.ai:443";
10
27
 
28
+ interface AddOptions extends OutputFlags {
29
+ endpoint?: string;
30
+ type?: string;
31
+ apiKey?: string;
32
+ }
33
+
11
34
  export function addBackendCommands(backend: Command): void {
12
35
  const status = backend
13
36
  .command("status")
@@ -19,40 +42,198 @@ export function addBackendCommands(backend: Command): void {
19
42
 
20
43
  const set = backend
21
44
  .command("set <type>")
22
- .description("set the backend type (local or cloud)")
45
+ .description("switch between the reserved local and cloud backends")
23
46
  .action((type: string, options: OutputFlags) => {
24
47
  renderResult(applySet(type), resultFormat(options));
25
48
  });
26
49
  addResultFlags(set);
50
+
51
+ const add = backend
52
+ .command("add <name>")
53
+ .description("add or update a named backend (self-hosted by default)")
54
+ .requiredOption(
55
+ "--endpoint <host:port>",
56
+ "the server address, e.g. stigmer.example.com:443",
57
+ )
58
+ .option("--type <type>", "backend type: selfhost or cloud", "selfhost")
59
+ .option(
60
+ "--api-key <key>",
61
+ "API token for a self-hosted backend (stigmer apikey create)",
62
+ )
63
+ .action((name: string, options: AddOptions) => {
64
+ renderResult(applyAdd(name, options), resultFormat(options));
65
+ });
66
+ addResultFlags(add);
67
+
68
+ const use = backend
69
+ .command("use <name>")
70
+ .description("switch the active backend")
71
+ .action((name: string, options: OutputFlags) => {
72
+ renderResult(applyUse(name), resultFormat(options));
73
+ });
74
+ addResultFlags(use);
75
+
76
+ const list = backend
77
+ .command("list")
78
+ .description("list configured backends")
79
+ .action((options: OutputFlags) => {
80
+ renderResult(buildList(load()), resultFormat(options));
81
+ });
82
+ addResultFlags(list);
83
+
84
+ const remove = backend
85
+ .command("remove <name>")
86
+ .description("remove a named backend")
87
+ .action((name: string, options: OutputFlags) => {
88
+ renderResult(applyRemove(name), resultFormat(options));
89
+ });
90
+ addResultFlags(remove);
27
91
  }
28
92
 
29
93
  function buildStatus(config: Config): CommandResult {
94
+ const { name, entry } = activeBackend(config);
30
95
  const result = CommandResult.success("Backend configuration");
31
- const section = result.addSection("").field("Type", config.backend.type).field("Endpoint", resolveEndpoint(config));
32
- if (isCloudMode(config)) {
33
- section.field("Auth", config.backend.cloud?.token ? "Logged in" : "Not logged in");
96
+ const section = result
97
+ .addSection("")
98
+ .field("Backend", name)
99
+ .field("Type", entry?.type ?? "local")
100
+ .field("Endpoint", resolveEndpoint(config));
101
+ if (entry?.type === "cloud") {
102
+ section.field("Auth", entry.token ? "Logged in" : "Not logged in");
103
+ }
104
+ if (entry?.type === "selfhost") {
105
+ section.field(
106
+ "Auth",
107
+ entry.api_key
108
+ ? "API key stored"
109
+ : "No credential (unauthenticated server, or STIGMER_API_KEY)",
110
+ );
34
111
  }
35
112
  return result;
36
113
  }
37
114
 
38
115
  function applySet(type: string): CommandResult {
39
116
  if (type !== "local" && type !== "cloud") {
40
- throw new UsageError(`invalid backend type "${type}" (expected: local, cloud)`);
117
+ throw new UsageError(
118
+ `invalid backend type "${type}" (expected: local, cloud — named backends switch with 'use')`,
119
+ );
41
120
  }
42
121
 
43
122
  const config = load();
44
- config.backend.type = type;
45
-
46
123
  if (type === "cloud") {
47
- if (config.backend.cloud === undefined) {
48
- config.backend.cloud = { endpoint: DEFAULT_CLOUD_ENDPOINT };
49
- }
124
+ const backends = (config.backends ??= {});
125
+ backends[CLOUD_BACKEND_NAME] ??= {
126
+ type: "cloud",
127
+ endpoint: DEFAULT_CLOUD_ENDPOINT,
128
+ };
129
+ config.current_backend = CLOUD_BACKEND_NAME;
50
130
  save(config);
51
131
  return CommandResult.success("Backend set to cloud")
52
132
  .hint("Please authenticate:")
53
133
  .hint(" stigmer auth login");
54
134
  }
55
135
 
136
+ config.current_backend = LOCAL_BACKEND_NAME;
137
+ save(config);
138
+ return CommandResult.success("Backend set to local").hint(
139
+ "Make sure the Stigmer server is running.",
140
+ );
141
+ }
142
+
143
+ function applyAdd(name: string, options: AddOptions): CommandResult {
144
+ if (name === LOCAL_BACKEND_NAME) {
145
+ throw new UsageError(
146
+ `"${LOCAL_BACKEND_NAME}" is the managed local daemon — it cannot be redefined`,
147
+ );
148
+ }
149
+ const type = options.type ?? "selfhost";
150
+ if (type !== "selfhost" && type !== "cloud") {
151
+ throw new UsageError(
152
+ `invalid backend type "${type}" (expected: selfhost, cloud)`,
153
+ );
154
+ }
155
+ const endpoint = options.endpoint ?? "";
156
+ if (endpoint === "") {
157
+ throw new UsageError("--endpoint is required");
158
+ }
159
+
160
+ const config = load();
161
+ const backends = (config.backends ??= {});
162
+ const existing = backends[name];
163
+ backends[name] = {
164
+ ...existing,
165
+ type: type as NamedBackendType,
166
+ endpoint,
167
+ ...(options.apiKey !== undefined ? { api_key: options.apiKey } : {}),
168
+ };
169
+ save(config);
170
+
171
+ const result = CommandResult.success(
172
+ `Backend "${name}" ${existing !== undefined ? "updated" : "added"} (${type}, ${endpoint})`,
173
+ ).hint(`Switch to it: stigmer config backend use ${name}`);
174
+ if (type === "selfhost" && backends[name].api_key === undefined) {
175
+ result.hint(
176
+ "No API key stored. For an authenticated server, mint one on that backend:",
177
+ );
178
+ result.hint(" stigmer apikey create --name cli");
179
+ }
180
+ return result;
181
+ }
182
+
183
+ function applyUse(name: string): CommandResult {
184
+ const config = load();
185
+ if (name !== LOCAL_BACKEND_NAME && config.backends?.[name] === undefined) {
186
+ throw new UsageError(
187
+ `unknown backend "${name}" (known: ${knownBackendNames(config).join(", ")})`,
188
+ );
189
+ }
190
+ config.current_backend = name;
191
+ save(config);
192
+ return CommandResult.success(`Switched to backend "${name}"`);
193
+ }
194
+
195
+ function buildList(config: Config): CommandResult {
196
+ const current = activeBackendName(config);
197
+ const result = CommandResult.success("Configured backends");
198
+ for (const name of knownBackendNames(config)) {
199
+ const entry = config.backends?.[name];
200
+ const marker = name === current ? " (current)" : "";
201
+ const section = result.addSection(`${name}${marker}`);
202
+ section.field("Type", entry?.type ?? "local");
203
+ section.field(
204
+ "Endpoint",
205
+ entry?.endpoint ??
206
+ (entry?.type === "cloud" ? DEFAULT_CLOUD_ENDPOINT : "localhost:7234"),
207
+ );
208
+ if (entry?.type === "cloud") {
209
+ section.field("Auth", entry.token ? "Logged in" : "Not logged in");
210
+ } else if (entry?.type === "selfhost") {
211
+ section.field("Auth", entry.api_key ? "API key stored" : "None");
212
+ }
213
+ }
214
+ return result;
215
+ }
216
+
217
+ function applyRemove(name: string): CommandResult {
218
+ if (name === LOCAL_BACKEND_NAME) {
219
+ throw new UsageError(
220
+ `"${LOCAL_BACKEND_NAME}" is the managed local daemon — it cannot be removed`,
221
+ );
222
+ }
223
+ const config = load();
224
+ if (config.backends?.[name] === undefined) {
225
+ throw new UsageError(`unknown backend "${name}"`);
226
+ }
227
+ if (activeBackendName(config) === name) {
228
+ throw new UsageError(
229
+ `backend "${name}" is the current backend — switch first: stigmer config backend use ${LOCAL_BACKEND_NAME}`,
230
+ );
231
+ }
232
+ delete config.backends[name];
56
233
  save(config);
57
- return CommandResult.success("Backend set to local").hint("Make sure the Stigmer server is running.");
234
+ return CommandResult.success(`Removed backend "${name}"`);
235
+ }
236
+
237
+ function knownBackendNames(config: Config): string[] {
238
+ return [LOCAL_BACKEND_NAME, ...Object.keys(config.backends ?? {}).sort()];
58
239
  }
@@ -31,7 +31,11 @@ vi.mock("../config/index.js", async (importOriginal) => {
31
31
 
32
32
  /** An authenticated cloud config with no org selected — the guard's target shape. */
33
33
  function cloudConfigWithoutOrg(): Config {
34
- return { backend: { type: "cloud", cloud: { token: "test-token" } } };
34
+ return {
35
+ backend: { type: "cloud" },
36
+ backends: { cloud: { type: "cloud", token: "test-token" } },
37
+ current_backend: "cloud",
38
+ };
35
39
  }
36
40
 
37
41
  interface RunOutcome {
@@ -43,16 +47,30 @@ interface RunOutcome {
43
47
  // suppressed, returning the thrown error's classified exit code and message (or
44
48
  // a success sentinel). `--standalone` is a program-global flag, so it must
45
49
  // precede the subcommand (commander's enablePositionalOptions).
46
- async function runConnect(ref: string, ...flags: string[]): Promise<RunOutcome> {
50
+ async function runConnect(
51
+ ref: string,
52
+ ...flags: string[]
53
+ ): Promise<RunOutcome> {
47
54
  const program = buildProgram();
48
55
  program.exitOverride();
49
56
  const outSpy = vi.spyOn(process.stdout, "write").mockReturnValue(true);
50
57
  const errSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true);
51
58
  try {
52
- await program.parseAsync(["node", "stigmer", "--standalone", "connect", "mcp-server", ref, ...flags]);
59
+ await program.parseAsync([
60
+ "node",
61
+ "stigmer",
62
+ "--standalone",
63
+ "connect",
64
+ "mcp-server",
65
+ ref,
66
+ ...flags,
67
+ ]);
53
68
  return { exitCode: ExitCode.Success, message: "" };
54
69
  } catch (err) {
55
- return { exitCode: classify(err)?.exitCode ?? -1, message: err instanceof Error ? err.message : String(err) };
70
+ return {
71
+ exitCode: classify(err)?.exitCode ?? -1,
72
+ message: err instanceof Error ? err.message : String(err),
73
+ };
56
74
  } finally {
57
75
  outSpy.mockRestore();
58
76
  errSpy.mockRestore();
@@ -7,7 +7,12 @@
7
7
  // fast (DD-001).
8
8
 
9
9
  import type { Command } from "commander";
10
- import { ensureAuthenticated, resolveOrganization } from "../config/index.js";
10
+ import {
11
+ activeBackend,
12
+ ensureAuthenticated,
13
+ resolveConsoleURL,
14
+ resolveOrganization,
15
+ } from "../config/index.js";
11
16
  import { UsageError } from "../errors/index.js";
12
17
  import { shouldColorize } from "../output/style.js";
13
18
  import { globalOrg } from "./shared.js";
@@ -21,7 +26,9 @@ interface ConnectFlags {
21
26
  const DEFAULT_TIMEOUT_SECONDS = 30;
22
27
 
23
28
  export function registerConnect(program: Command): void {
24
- const connect = program.command("connect").description("connect to external services and discover capabilities");
29
+ const connect = program
30
+ .command("connect")
31
+ .description("connect to external services and discover capabilities");
25
32
 
26
33
  connect
27
34
  .command("mcp-server <slug-or-id>")
@@ -32,17 +39,26 @@ export function registerConnect(program: Command): void {
32
39
  "bounds the server-side connect only when set explicitly)",
33
40
  String(DEFAULT_TIMEOUT_SECONDS),
34
41
  )
35
- .option("--dry-run", "discover and display results without pushing to the backend")
42
+ .option(
43
+ "--dry-run",
44
+ "discover and display results without pushing to the backend",
45
+ )
36
46
  .option(
37
47
  "--env <KEY=VALUE>",
38
48
  "environment variable for the MCP server (repeatable)",
39
49
  (value: string, previous: string[]) => [...previous, value],
40
50
  [],
41
51
  )
42
- .action((reference: string, options: ConnectFlags, command: Command) => runConnect(reference, options, command));
52
+ .action((reference: string, options: ConnectFlags, command: Command) =>
53
+ runConnect(reference, options, command),
54
+ );
43
55
  }
44
56
 
45
- async function runConnect(reference: string, options: ConnectFlags, command: Command): Promise<void> {
57
+ async function runConnect(
58
+ reference: string,
59
+ options: ConnectFlags,
60
+ command: Command,
61
+ ): Promise<void> {
46
62
  const timeoutMs = parseTimeout(options.timeout);
47
63
  // The 30s default exists for --dry-run's local discovery. A real connect
48
64
  // legitimately takes minutes (server-side sandbox boot + discovery + tool
@@ -50,11 +66,12 @@ async function runConnect(reference: string, options: ConnectFlags, command: Com
50
66
  // connects — the wait is bounded only when the user set --timeout
51
67
  // explicitly (issue #239: the flag used to be silently ignored here).
52
68
  const timeoutIsExplicit = command.getOptionValueSource("timeout") === "cli";
53
- const [{ connectBackend }, { connectMcpServer }, { renderConnectResult }] = await Promise.all([
54
- import("../backend.js"),
55
- import("../resources/connect/connect.js"),
56
- import("../resources/connect/display.js"),
57
- ]);
69
+ const [{ connectBackend }, { connectMcpServer }, { renderConnectResult }] =
70
+ await Promise.all([
71
+ import("../backend.js"),
72
+ import("../resources/connect/connect.js"),
73
+ import("../resources/connect/display.js"),
74
+ ]);
58
75
 
59
76
  const client = connectBackend();
60
77
  ensureAuthenticated(client.config);
@@ -81,12 +98,17 @@ async function runConnect(reference: string, options: ConnectFlags, command: Com
81
98
  pushTimeoutMs: timeoutIsExplicit ? timeoutMs : undefined,
82
99
  dryRun: options.dryRun === true,
83
100
  envOverrides: options.env,
84
- backendType: client.config.backend.type,
101
+ consoleURL: resolveConsoleURL(client.config),
102
+ probeLocalConsole: activeBackend(client.config).entry === undefined,
85
103
  interactive: process.stderr.isTTY === true,
86
104
  });
87
105
 
88
106
  const colorize = shouldColorize(process.stdout);
89
- renderConnectResult(result, (line) => process.stdout.write(`${line}\n`), colorize);
107
+ renderConnectResult(
108
+ result,
109
+ (line) => process.stdout.write(`${line}\n`),
110
+ colorize,
111
+ );
90
112
  }
91
113
 
92
114
  // Parse --timeout seconds into milliseconds. Mirrors Go's DurationVar default
@@ -95,7 +117,9 @@ function parseTimeout(raw: string | undefined): number {
95
117
  if (raw === undefined || raw === "") return DEFAULT_TIMEOUT_SECONDS * 1000;
96
118
  const seconds = Number(raw);
97
119
  if (!Number.isFinite(seconds) || seconds < 0) {
98
- throw new UsageError(`invalid --timeout '${raw}': expected a number of seconds`);
120
+ throw new UsageError(
121
+ `invalid --timeout '${raw}': expected a number of seconds`,
122
+ );
99
123
  }
100
124
  return Math.round(seconds * 1000);
101
125
  }