@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.
- package/auth/login.d.ts.map +1 -1
- package/auth/login.js +30 -5
- package/auth/login.js.map +1 -1
- package/auth/token.d.ts +10 -5
- package/auth/token.d.ts.map +1 -1
- package/auth/token.js +30 -21
- package/auth/token.js.map +1 -1
- package/commands/apikey/index.d.ts.map +1 -1
- package/commands/apikey/index.js +11 -5
- package/commands/apikey/index.js.map +1 -1
- package/commands/auth/index.d.ts.map +1 -1
- package/commands/auth/index.js +19 -8
- package/commands/auth/index.js.map +1 -1
- package/commands/config/backend.d.ts.map +1 -1
- package/commands/config/backend.js +134 -12
- package/commands/config/backend.js.map +1 -1
- package/commands/connect.d.ts.map +1 -1
- package/commands/connect.js +6 -3
- package/commands/connect.js.map +1 -1
- package/commands/share.d.ts.map +1 -1
- package/commands/share.js +8 -6
- package/commands/share.js.map +1 -1
- package/commands/up.d.ts.map +1 -1
- package/commands/up.js +22 -6
- package/commands/up.js.map +1 -1
- package/config/config.d.ts +56 -7
- package/config/config.d.ts.map +1 -1
- package/config/config.js +115 -16
- package/config/config.js.map +1 -1
- package/config/index.d.ts +1 -1
- package/config/index.d.ts.map +1 -1
- package/config/index.js +1 -1
- package/config/index.js.map +1 -1
- package/config/keys.d.ts +1 -1
- package/config/keys.d.ts.map +1 -1
- package/config/keys.js +29 -6
- package/config/keys.js.map +1 -1
- package/config/resolve.d.ts +32 -21
- package/config/resolve.d.ts.map +1 -1
- package/config/resolve.js +80 -39
- package/config/resolve.js.map +1 -1
- package/local/constants.d.ts +8 -3
- package/local/constants.d.ts.map +1 -1
- package/local/constants.js +8 -3
- package/local/constants.js.map +1 -1
- package/local/daemon/components.d.ts.map +1 -1
- package/local/daemon/components.js +4 -2
- package/local/daemon/components.js.map +1 -1
- package/local/daemon/env.d.ts +16 -3
- package/local/daemon/env.d.ts.map +1 -1
- package/local/daemon/env.js +16 -3
- package/local/daemon/env.js.map +1 -1
- package/local/daemon/launch.d.ts.map +1 -1
- package/local/daemon/launch.js +5 -4
- package/local/daemon/launch.js.map +1 -1
- package/local/daemon/process.d.ts.map +1 -1
- package/local/daemon/process.js +8 -5
- package/local/daemon/process.js.map +1 -1
- package/local/runtime/index.d.ts +2 -2
- package/local/runtime/index.d.ts.map +1 -1
- package/local/runtime/index.js +4 -2
- package/local/runtime/index.js.map +1 -1
- package/local/runtime/node.d.ts +8 -0
- package/local/runtime/node.d.ts.map +1 -1
- package/local/runtime/node.js +74 -23
- package/local/runtime/node.js.map +1 -1
- package/local/runtime/runner.d.ts +2 -1
- package/local/runtime/runner.d.ts.map +1 -1
- package/local/runtime/runner.js +4 -31
- package/local/runtime/runner.js.map +1 -1
- package/local/runtime/runtimes-install.d.ts +13 -0
- package/local/runtime/runtimes-install.d.ts.map +1 -0
- package/local/runtime/runtimes-install.js +60 -0
- package/local/runtime/runtimes-install.js.map +1 -0
- package/local/runtime/server.d.ts +26 -30
- package/local/runtime/server.d.ts.map +1 -1
- package/local/runtime/server.js +106 -108
- package/local/runtime/server.js.map +1 -1
- package/local/seedpack/content.d.ts +3 -3
- package/local/seedpack/content.d.ts.map +1 -1
- package/local/seedpack/content.js +19 -13
- package/local/seedpack/content.js.map +1 -1
- package/local/status.d.ts.map +1 -1
- package/local/status.js +4 -2
- package/local/status.js.map +1 -1
- package/local/webconsole/index.d.ts +2 -3
- package/local/webconsole/index.d.ts.map +1 -1
- package/local/webconsole/index.js +24 -10
- package/local/webconsole/index.js.map +1 -1
- package/package.json +5 -5
- package/resources/connect/connect.d.ts +4 -3
- package/resources/connect/connect.d.ts.map +1 -1
- package/resources/connect/connect.js +17 -5
- package/resources/connect/connect.js.map +1 -1
- package/resources/connect/oauth.d.ts +5 -2
- package/resources/connect/oauth.d.ts.map +1 -1
- package/resources/connect/oauth.js +5 -4
- package/resources/connect/oauth.js.map +1 -1
- package/src/auth/login.ts +54 -9
- package/src/auth/token.test.ts +69 -13
- package/src/auth/token.ts +46 -22
- package/src/client/client.test.ts +10 -2
- package/src/commands/apikey/index.ts +42 -11
- package/src/commands/auth/index.ts +52 -14
- package/src/commands/config/backend.test.ts +122 -0
- package/src/commands/config/backend.ts +195 -14
- package/src/commands/connect.test.ts +22 -4
- package/src/commands/connect.ts +37 -13
- package/src/commands/share.ts +36 -16
- package/src/commands/up.ts +24 -6
- package/src/config/config.test.ts +62 -6
- package/src/config/config.ts +165 -24
- package/src/config/index.ts +6 -0
- package/src/config/keys.test.ts +27 -4
- package/src/config/keys.ts +46 -9
- package/src/config/resolve.test.ts +135 -34
- package/src/config/resolve.ts +88 -38
- package/src/local/constants.ts +8 -4
- package/src/local/daemon/components.test.ts +9 -1
- package/src/local/daemon/components.ts +4 -2
- package/src/local/daemon/daemon.integration.test.ts +1 -1
- package/src/local/daemon/env.test.ts +15 -4
- package/src/local/daemon/env.ts +33 -5
- package/src/local/daemon/launch.ts +5 -4
- package/src/local/daemon/process.ts +7 -4
- package/src/local/runtime/index.ts +6 -5
- package/src/local/runtime/node.ts +99 -23
- package/src/local/runtime/runner.ts +5 -36
- package/src/local/runtime/runtime.test.ts +58 -14
- package/src/local/runtime/runtimes-install.test.ts +56 -0
- package/src/local/runtime/runtimes-install.ts +72 -0
- package/src/local/runtime/server.test.ts +178 -114
- package/src/local/runtime/server.ts +149 -129
- package/src/local/seedpack/content.ts +52 -21
- package/src/local/status.ts +4 -2
- package/src/local/webconsole/index.ts +27 -10
- package/src/resources/connect/connect.integration.test.ts +79 -26
- package/src/resources/connect/connect.ts +72 -19
- package/src/resources/connect/oauth.test.ts +38 -12
- package/src/resources/connect/oauth.ts +27 -11
- 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 {
|
|
8
|
-
|
|
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
|
|
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
|
|
48
|
-
if (
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
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 !== "")
|
|
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(
|
|
104
|
+
section.field(
|
|
105
|
+
"Full Name",
|
|
106
|
+
`${account.spec.firstName} ${account.spec.lastName}`.trim(),
|
|
107
|
+
);
|
|
75
108
|
}
|
|
76
|
-
section.field(
|
|
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(
|
|
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` —
|
|
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 {
|
|
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 {
|
|
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("
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
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
|
-
|
|
48
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
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([
|
|
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 {
|
|
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();
|
package/src/commands/connect.ts
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
// fast (DD-001).
|
|
8
8
|
|
|
9
9
|
import type { Command } from "commander";
|
|
10
|
-
import {
|
|
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
|
|
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(
|
|
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) =>
|
|
52
|
+
.action((reference: string, options: ConnectFlags, command: Command) =>
|
|
53
|
+
runConnect(reference, options, command),
|
|
54
|
+
);
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
async function runConnect(
|
|
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 }] =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
}
|