@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
@@ -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
  }
@@ -8,7 +8,12 @@
8
8
  // lazy-imported so `--help` stays fast (DD-001).
9
9
 
10
10
  import type { Command } from "commander";
11
- import { ensureAuthenticated, resolveConsoleURL, resolveOrganization } from "../config/index.js";
11
+ import {
12
+ activeBackend,
13
+ ensureAuthenticated,
14
+ resolveConsoleURL,
15
+ resolveOrganization,
16
+ } from "../config/index.js";
12
17
  import { UsageError } from "../errors/index.js";
13
18
  import type { OutputFlags } from "../output/index.js";
14
19
  import type { ShareAudience } from "../resources/share.js";
@@ -22,11 +27,15 @@ interface ShareAgentFlags extends OutputFlags {
22
27
  }
23
28
 
24
29
  export function registerShare(program: Command): void {
25
- const share = program.command("share").description("share resources via a hosted link");
30
+ const share = program
31
+ .command("share")
32
+ .description("share resources via a hosted link");
26
33
 
27
34
  const agent = share
28
35
  .command("agent <ref>")
29
- .description("enable sharing for an agent and print its chat link and embed snippet")
36
+ .description(
37
+ "enable sharing for an agent and print its chat link and embed snippet",
38
+ )
30
39
  .option("--off", "disable sharing (the link stops working immediately)")
31
40
  .option(
32
41
  "--audience <audience>",
@@ -37,7 +46,9 @@ export function registerShare(program: Command): void {
37
46
  "generate a new share link and kill the current one immediately (public audience)",
38
47
  )
39
48
  .option("--open", "open the chat link in your browser")
40
- .action((ref: string, options: ShareAgentFlags, command: Command) => runShareAgent(ref, options, command));
49
+ .action((ref: string, options: ShareAgentFlags, command: Command) =>
50
+ runShareAgent(ref, options, command),
51
+ );
41
52
  addResultFlags(agent);
42
53
  }
43
54
 
@@ -45,24 +56,30 @@ export function registerShare(program: Command): void {
45
56
  function parseAudience(value: string | undefined): ShareAudience | undefined {
46
57
  if (value === undefined) return undefined;
47
58
  if (value === "public" || value === "org") return value;
48
- throw new UsageError(`invalid --audience '${value}'\n\nExpected 'public' or 'org'.`);
59
+ throw new UsageError(
60
+ `invalid --audience '${value}'\n\nExpected 'public' or 'org'.`,
61
+ );
49
62
  }
50
63
 
51
- async function runShareAgent(ref: string, options: ShareAgentFlags, command: Command): Promise<void> {
64
+ async function runShareAgent(
65
+ ref: string,
66
+ options: ShareAgentFlags,
67
+ command: Command,
68
+ ): Promise<void> {
52
69
  const format = resultFormat(options);
53
70
 
54
- const [{ connectBackend }, { shareAgent }, { renderResult }] = await Promise.all([
55
- import("../backend.js"),
56
- import("../resources/share.js"),
57
- import("../output/command-result.js"),
58
- ]);
71
+ const [{ connectBackend }, { shareAgent }, { renderResult }] =
72
+ await Promise.all([
73
+ import("../backend.js"),
74
+ import("../resources/share.js"),
75
+ import("../output/command-result.js"),
76
+ ]);
59
77
 
60
78
  const client = connectBackend();
61
79
  ensureAuthenticated(client.config);
62
80
  const org = resolveOrganization(client.config, globalOrg(command));
63
81
 
64
- const backendType = client.config.backend.type;
65
- const appOrigin = resolveConsoleURL(backendType);
82
+ const appOrigin = resolveConsoleURL(client.config);
66
83
  const enabled = options.off !== true;
67
84
  const audience = parseAudience(options.audience);
68
85
  const resetLink = options.resetLink === true;
@@ -79,20 +96,23 @@ async function runShareAgent(ref: string, options: ShareAgentFlags, command: Com
79
96
  ...(audience !== undefined ? { audience } : {}),
80
97
  resetLink,
81
98
  appOrigin,
82
- isLocal: backendType === "local",
99
+ isLocal: activeBackend(client.config).entry === undefined,
83
100
  });
84
101
  renderResult(result, format);
85
102
 
86
103
  if (options.open === true && enabled) {
87
104
  // Best-effort: the link is already rendered above, so a launch failure
88
105
  // only needs a nudge, never an error exit (mirrors auth login).
89
- const url = result.sections.find((s) => s.title.endsWith("chat link"))?.items[0];
106
+ const url = result.sections.find((s) => s.title.endsWith("chat link"))
107
+ ?.items[0];
90
108
  if (url !== undefined) {
91
109
  try {
92
110
  const { default: open } = await import("open");
93
111
  await open(url);
94
112
  } catch {
95
- process.stderr.write("Could not open the browser automatically. Please open the link above.\n");
113
+ process.stderr.write(
114
+ "Could not open the browser automatically. Please open the link above.\n",
115
+ );
96
116
  }
97
117
  }
98
118
  }