@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
package/src/commands/share.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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(
|
|
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) =>
|
|
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(
|
|
59
|
+
throw new UsageError(
|
|
60
|
+
`invalid --audience '${value}'\n\nExpected 'public' or 'org'.`,
|
|
61
|
+
);
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
async function runShareAgent(
|
|
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 }] =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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:
|
|
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"))
|
|
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(
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
"Could not open the browser automatically. Please open the link above.\n",
|
|
115
|
+
);
|
|
96
116
|
}
|
|
97
117
|
}
|
|
98
118
|
}
|
package/src/commands/up.ts
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
// outcome. The launcher (and the heavy resolvers it pulls in) load lazily so
|
|
7
7
|
// `--help` stays fast (DD-001).
|
|
8
8
|
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
9
11
|
import type { Command } from "commander";
|
|
10
12
|
import { CommandResult, type OutputFlags, renderResult } from "../output/index.js";
|
|
11
|
-
import { SERVER_PORT } from "../local/constants.js";
|
|
13
|
+
import { HEALTH_STATE_FILE, SERVER_PORT } from "../local/constants.js";
|
|
12
14
|
import { addResultFlags, resultFormat } from "./shared.js";
|
|
13
15
|
|
|
14
16
|
interface UpFlags extends OutputFlags {
|
|
@@ -16,15 +18,17 @@ interface UpFlags extends OutputFlags {
|
|
|
16
18
|
web?: boolean; // false when --no-web is passed
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
// The server serves the web console from its unified port (DD-012); the
|
|
22
|
+
// flag suppresses probing/reporting it, not the serving itself (one
|
|
23
|
+
// process, one origin — there is no separate console to not-start).
|
|
24
|
+
const NO_WEB_HELP = "don't report the web console URL";
|
|
25
|
+
|
|
19
26
|
export function registerUp(program: Command): void {
|
|
20
27
|
const up = program
|
|
21
28
|
.command("up")
|
|
22
29
|
.description("start the local Stigmer stack (server, runner, Temporal)")
|
|
23
30
|
.option("--server-only", "start only the control plane (no runners)")
|
|
24
|
-
|
|
25
|
-
// local web console, so the stack is headless either way (use the cloud
|
|
26
|
-
// console at app.stigmer.ai for a UI).
|
|
27
|
-
.option("--no-web", "no-op: this CLI does not serve a local web console")
|
|
31
|
+
.option("--no-web", NO_WEB_HELP)
|
|
28
32
|
.action((options: UpFlags) =>
|
|
29
33
|
runUp({ serverOnly: options.serverOnly === true, noWeb: options.web === false }, options),
|
|
30
34
|
);
|
|
@@ -33,7 +37,7 @@ export function registerUp(program: Command): void {
|
|
|
33
37
|
const server = up
|
|
34
38
|
.command("server")
|
|
35
39
|
.description("start only the control plane (no runners)")
|
|
36
|
-
.option("--no-web",
|
|
40
|
+
.option("--no-web", NO_WEB_HELP)
|
|
37
41
|
.action((options: UpFlags) => runUp({ serverOnly: true, noWeb: options.web === false }, options));
|
|
38
42
|
addResultFlags(server);
|
|
39
43
|
}
|
|
@@ -46,6 +50,20 @@ async function runUp(opts: { serverOnly: boolean; noWeb: boolean }, flags: Outpu
|
|
|
46
50
|
const result = CommandResult.success(opts.serverOnly ? "Stigmer control plane is up" : "Stigmer local stack is up");
|
|
47
51
|
const section = result.addSection("Endpoints");
|
|
48
52
|
section.field("server", `http://localhost:${SERVER_PORT}`);
|
|
53
|
+
if (await consoleReported()) {
|
|
54
|
+
// Same origin as the API: the server serves the console (DD-012). Only
|
|
55
|
+
// printed when the daemon's probe found a bundled export — a dev-tree
|
|
56
|
+
// server without one must not advertise a dead URL.
|
|
57
|
+
section.field("console", `http://localhost:${SERVER_PORT}`);
|
|
58
|
+
}
|
|
49
59
|
result.hint("Check status with: stigmer status").hint("Stop it with: stigmer down");
|
|
50
60
|
renderResult(result, resultFormat(flags));
|
|
51
61
|
}
|
|
62
|
+
|
|
63
|
+
/** Whether the daemon recorded the web console as running (its own probe). */
|
|
64
|
+
async function consoleReported(): Promise<boolean> {
|
|
65
|
+
const { dataDir } = await import("../local/paths.js");
|
|
66
|
+
const { loadHealthState } = await import("../local/state/health-state.js");
|
|
67
|
+
const health = loadHealthState(join(dataDir(homedir()), HEALTH_STATE_FILE));
|
|
68
|
+
return health?.components["web-console"]?.state === "running";
|
|
69
|
+
}
|
|
@@ -28,16 +28,37 @@ describe("load", () => {
|
|
|
28
28
|
expect(load(path)).toEqual(getDefault());
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
it("
|
|
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
|
-
|
|
39
|
-
|
|
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.
|
|
49
|
-
|
|
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
|
-
|
|
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(
|
package/src/config/config.ts
CHANGED
|
@@ -1,41 +1,81 @@
|
|
|
1
1
|
// CLI configuration model and persistence (~/.stigmer/config.yaml).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
/**
|
|
19
|
-
|
|
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
|
|
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 {
|
|
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
|
-
/**
|
|
85
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
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
|
|
179
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
99
180
|
return getDefault();
|
|
100
181
|
}
|
|
101
|
-
|
|
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: {
|
|
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.
|
package/src/config/index.ts
CHANGED
package/src/config/keys.test.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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(
|
|
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(
|
|
57
|
+
expect(() => setConfigValue(getDefault(), "nope.nope", "x")).toThrow(
|
|
58
|
+
UsageError,
|
|
59
|
+
);
|
|
37
60
|
});
|
|
38
61
|
});
|