@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.
- 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/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 +28 -20
- package/config/resolve.d.ts.map +1 -1
- package/config/resolve.js +74 -36
- package/config/resolve.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/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/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 +83 -36
- package/src/local/seedpack/content.ts +52 -21
- 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
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
// Pins the effective-value resolvers over the named-backend model (O3):
|
|
2
|
+
// env > active entry > defaults for endpoint/token/org/console-URL, the
|
|
3
|
+
// selfhost credential lane (api_key), and the auth gate's three postures
|
|
4
|
+
// (local never, selfhost never client-side, cloud requires a credential).
|
|
5
|
+
|
|
1
6
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
7
|
import { CliExitError } from "../errors/index.js";
|
|
3
|
-
import type { Config } from "./config.js";
|
|
8
|
+
import type { Config, NamedBackendConfig } from "./config.js";
|
|
4
9
|
import {
|
|
5
10
|
DEFAULT_CLOUD_CONSOLE_URL,
|
|
6
11
|
ensureAuthenticated,
|
|
@@ -11,29 +16,78 @@ import {
|
|
|
11
16
|
resolveToken,
|
|
12
17
|
} from "./resolve.js";
|
|
13
18
|
|
|
14
|
-
const ENV_KEYS = [
|
|
19
|
+
const ENV_KEYS = [
|
|
20
|
+
"STIGMER_SERVER_ADDRESS",
|
|
21
|
+
"STIGMER_API_KEY",
|
|
22
|
+
"STIGMER_ORG_ID",
|
|
23
|
+
] as const;
|
|
15
24
|
|
|
16
25
|
afterEach(() => {
|
|
17
26
|
for (const key of ENV_KEYS) delete process.env[key];
|
|
18
27
|
});
|
|
19
28
|
|
|
20
|
-
function
|
|
21
|
-
return {
|
|
29
|
+
function localConfig(context?: Config["context"]): Config {
|
|
30
|
+
return {
|
|
31
|
+
backend: { type: "local" },
|
|
32
|
+
backends: {},
|
|
33
|
+
current_backend: "local",
|
|
34
|
+
context,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cloudConfig(
|
|
39
|
+
overrides: Partial<NamedBackendConfig> = {},
|
|
40
|
+
context?: Config["context"],
|
|
41
|
+
): Config {
|
|
42
|
+
return {
|
|
43
|
+
backend: { type: "cloud" },
|
|
44
|
+
backends: {
|
|
45
|
+
cloud: { type: "cloud", endpoint: "cloud.example:443", ...overrides },
|
|
46
|
+
},
|
|
47
|
+
current_backend: "cloud",
|
|
48
|
+
context,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function selfhostConfig(overrides: Partial<NamedBackendConfig> = {}): Config {
|
|
53
|
+
return {
|
|
54
|
+
backend: { type: "cloud" },
|
|
55
|
+
backends: {
|
|
56
|
+
staging: {
|
|
57
|
+
type: "selfhost",
|
|
58
|
+
endpoint: "stigmer.example.com:7234",
|
|
59
|
+
...overrides,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
current_backend: "staging",
|
|
63
|
+
};
|
|
22
64
|
}
|
|
23
65
|
|
|
24
66
|
describe("resolveEndpoint", () => {
|
|
25
67
|
it("defaults to the local daemon address in local mode", () => {
|
|
26
|
-
expect(resolveEndpoint(
|
|
68
|
+
expect(resolveEndpoint(localConfig())).toBe("localhost:7234");
|
|
27
69
|
});
|
|
28
70
|
|
|
29
71
|
it("uses the cloud default when no endpoint is configured", () => {
|
|
30
|
-
expect(resolveEndpoint({
|
|
72
|
+
expect(resolveEndpoint(cloudConfig({ endpoint: undefined }))).toBe(
|
|
73
|
+
"api.stigmer.ai:443",
|
|
74
|
+
);
|
|
31
75
|
});
|
|
32
76
|
|
|
33
77
|
it("prefers the configured cloud endpoint", () => {
|
|
34
78
|
expect(resolveEndpoint(cloudConfig())).toBe("cloud.example:443");
|
|
35
79
|
});
|
|
36
80
|
|
|
81
|
+
it("uses the selfhost entry's endpoint", () => {
|
|
82
|
+
expect(resolveEndpoint(selfhostConfig())).toBe("stigmer.example.com:7234");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("refuses a selfhost entry without an endpoint (hand-edited config)", () => {
|
|
86
|
+
expect(() =>
|
|
87
|
+
resolveEndpoint(selfhostConfig({ endpoint: undefined })),
|
|
88
|
+
).toThrow(CliExitError);
|
|
89
|
+
});
|
|
90
|
+
|
|
37
91
|
it("lets STIGMER_SERVER_ADDRESS override everything", () => {
|
|
38
92
|
process.env.STIGMER_SERVER_ADDRESS = "override:7234";
|
|
39
93
|
expect(resolveEndpoint(cloudConfig())).toBe("override:7234");
|
|
@@ -43,15 +97,27 @@ describe("resolveEndpoint", () => {
|
|
|
43
97
|
describe("resolveToken", () => {
|
|
44
98
|
it("prefers STIGMER_API_KEY over the config token", () => {
|
|
45
99
|
process.env.STIGMER_API_KEY = "env-key";
|
|
46
|
-
expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe(
|
|
100
|
+
expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe(
|
|
101
|
+
"env-key",
|
|
102
|
+
);
|
|
47
103
|
});
|
|
48
104
|
|
|
49
|
-
it("falls back to the
|
|
50
|
-
expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe(
|
|
105
|
+
it("falls back to the cloud entry's login token", () => {
|
|
106
|
+
expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe(
|
|
107
|
+
"config-token",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("uses the selfhost entry's api_key", () => {
|
|
112
|
+
expect(resolveToken(selfhostConfig({ api_key: "stk_stored" }))).toBe(
|
|
113
|
+
"stk_stored",
|
|
114
|
+
);
|
|
51
115
|
});
|
|
52
116
|
|
|
53
117
|
it("returns empty string when unauthenticated", () => {
|
|
54
|
-
expect(resolveToken({
|
|
118
|
+
expect(resolveToken(cloudConfig({ endpoint: undefined }))).toBe("");
|
|
119
|
+
expect(resolveToken(selfhostConfig())).toBe("");
|
|
120
|
+
expect(resolveToken(localConfig())).toBe("");
|
|
55
121
|
});
|
|
56
122
|
});
|
|
57
123
|
|
|
@@ -62,64 +128,95 @@ describe("resolveOrganization", () => {
|
|
|
62
128
|
expect(resolveOrganization(config, "flag-org")).toBe("flag-org");
|
|
63
129
|
});
|
|
64
130
|
|
|
65
|
-
it("falls back to STIGMER_ORG_ID, then context, then
|
|
131
|
+
it("falls back to STIGMER_ORG_ID, then context, then the entry's org_id", () => {
|
|
66
132
|
process.env.STIGMER_ORG_ID = "env-org";
|
|
67
|
-
expect(resolveOrganization(cloudConfig({ org_id: "config-org" }))).toBe(
|
|
133
|
+
expect(resolveOrganization(cloudConfig({ org_id: "config-org" }))).toBe(
|
|
134
|
+
"env-org",
|
|
135
|
+
);
|
|
68
136
|
});
|
|
69
137
|
|
|
70
138
|
it("uses context organization when no flag/env is set", () => {
|
|
71
|
-
const config
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
139
|
+
const config = cloudConfig(
|
|
140
|
+
{ org_id: "legacy" },
|
|
141
|
+
{ organization: "ctx-org" },
|
|
142
|
+
);
|
|
75
143
|
expect(resolveOrganization(config)).toBe("ctx-org");
|
|
76
144
|
expect(resolveContextOrganization(config)).toBe("ctx-org");
|
|
77
145
|
});
|
|
78
146
|
|
|
79
|
-
it("falls back to
|
|
80
|
-
expect(resolveContextOrganization(cloudConfig({ org_id: "legacy" }))).toBe(
|
|
147
|
+
it("falls back to the entry's org_id", () => {
|
|
148
|
+
expect(resolveContextOrganization(cloudConfig({ org_id: "legacy" }))).toBe(
|
|
149
|
+
"legacy",
|
|
150
|
+
);
|
|
81
151
|
});
|
|
82
152
|
|
|
83
153
|
it("defaults to the seedpack org in local mode when nothing is configured", () => {
|
|
84
|
-
expect(resolveOrganization(
|
|
154
|
+
expect(resolveOrganization(localConfig())).toBe("stigmer");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("defaults to the seedpack org on a selfhost backend (it IS the OSS server)", () => {
|
|
158
|
+
expect(resolveOrganization(selfhostConfig())).toBe("stigmer");
|
|
85
159
|
});
|
|
86
160
|
|
|
87
161
|
it("prefers an explicit context org over the local default", () => {
|
|
88
|
-
|
|
89
|
-
|
|
162
|
+
expect(
|
|
163
|
+
resolveOrganization(localConfig({ organization: "my-local-org" })),
|
|
164
|
+
).toBe("my-local-org");
|
|
90
165
|
});
|
|
91
166
|
|
|
92
167
|
it("does not apply the local default in cloud mode (org stays empty)", () => {
|
|
93
|
-
expect(resolveOrganization({
|
|
168
|
+
expect(resolveOrganization(cloudConfig({ endpoint: undefined }))).toBe("");
|
|
94
169
|
});
|
|
95
170
|
|
|
96
171
|
it("reports an unset local org as empty via resolveContextOrganization (no implicit default)", () => {
|
|
97
172
|
// The implicit local default lives only in resolveOrganization (effective
|
|
98
173
|
// org for operations); the context query must stay faithful so config/context
|
|
99
174
|
// display can show "(not set)".
|
|
100
|
-
expect(resolveContextOrganization(
|
|
175
|
+
expect(resolveContextOrganization(localConfig())).toBe("");
|
|
101
176
|
});
|
|
102
177
|
});
|
|
103
178
|
|
|
104
179
|
describe("resolveConsoleURL", () => {
|
|
105
180
|
it("prefers the STIGMER_CONSOLE_URL override", () => {
|
|
106
|
-
expect(
|
|
107
|
-
|
|
108
|
-
|
|
181
|
+
expect(
|
|
182
|
+
resolveConsoleURL(cloudConfig(), {
|
|
183
|
+
STIGMER_CONSOLE_URL: "https://console.example",
|
|
184
|
+
} as NodeJS.ProcessEnv),
|
|
185
|
+
).toBe("https://console.example");
|
|
109
186
|
});
|
|
110
187
|
|
|
111
188
|
it("uses the local web-console port for the local backend", () => {
|
|
112
|
-
expect(resolveConsoleURL(
|
|
189
|
+
expect(resolveConsoleURL(localConfig(), {} as NodeJS.ProcessEnv)).toBe(
|
|
190
|
+
"http://localhost:7234",
|
|
191
|
+
);
|
|
113
192
|
});
|
|
114
193
|
|
|
115
194
|
it("uses the cloud console URL for the cloud backend", () => {
|
|
116
|
-
expect(resolveConsoleURL(
|
|
195
|
+
expect(resolveConsoleURL(cloudConfig(), {} as NodeJS.ProcessEnv)).toBe(
|
|
196
|
+
DEFAULT_CLOUD_CONSOLE_URL,
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("derives the selfhost console from the entry's endpoint (one-origin rule)", () => {
|
|
201
|
+
expect(resolveConsoleURL(selfhostConfig(), {} as NodeJS.ProcessEnv)).toBe(
|
|
202
|
+
"http://stigmer.example.com:7234",
|
|
203
|
+
);
|
|
204
|
+
expect(
|
|
205
|
+
resolveConsoleURL(
|
|
206
|
+
selfhostConfig({ endpoint: "stigmer.example.com:443" }),
|
|
207
|
+
{} as NodeJS.ProcessEnv,
|
|
208
|
+
),
|
|
209
|
+
).toBe("https://stigmer.example.com");
|
|
117
210
|
});
|
|
118
211
|
});
|
|
119
212
|
|
|
120
213
|
describe("ensureAuthenticated", () => {
|
|
121
214
|
it("never requires auth in local mode", () => {
|
|
122
|
-
expect(() => ensureAuthenticated(
|
|
215
|
+
expect(() => ensureAuthenticated(localConfig())).not.toThrow();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("never gates selfhost client-side (the server owns that decision)", () => {
|
|
219
|
+
expect(() => ensureAuthenticated(selfhostConfig())).not.toThrow();
|
|
123
220
|
});
|
|
124
221
|
|
|
125
222
|
it("throws in cloud mode with no credentials", () => {
|
|
@@ -127,15 +224,19 @@ describe("ensureAuthenticated", () => {
|
|
|
127
224
|
});
|
|
128
225
|
|
|
129
226
|
it("passes with a stored access token", () => {
|
|
130
|
-
expect(() =>
|
|
227
|
+
expect(() =>
|
|
228
|
+
ensureAuthenticated(cloudConfig({ token: "t" })),
|
|
229
|
+
).not.toThrow();
|
|
131
230
|
});
|
|
132
231
|
|
|
133
|
-
it("passes with only a refresh token", () => {
|
|
134
|
-
expect(() =>
|
|
232
|
+
it("passes with only a refresh token (silent refresh will mint one)", () => {
|
|
233
|
+
expect(() =>
|
|
234
|
+
ensureAuthenticated(cloudConfig({ refresh_token: "r" })),
|
|
235
|
+
).not.toThrow();
|
|
135
236
|
});
|
|
136
237
|
|
|
137
|
-
it("passes with STIGMER_API_KEY
|
|
138
|
-
process.env.STIGMER_API_KEY = "
|
|
238
|
+
it("passes with STIGMER_API_KEY in the environment", () => {
|
|
239
|
+
process.env.STIGMER_API_KEY = "stk_env";
|
|
139
240
|
expect(() => ensureAuthenticated(cloudConfig())).not.toThrow();
|
|
140
241
|
});
|
|
141
242
|
});
|
package/src/config/resolve.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// Effective-value resolution with env > config > default precedence. These are
|
|
2
2
|
// the settings the backend client and read verbs depend on; centralizing the
|
|
3
3
|
// precedence keeps every call site consistent (mirrors the Go CLI's Resolve*).
|
|
4
|
+
//
|
|
5
|
+
// All resolvers read the ACTIVE named backend (config.ts): local is the
|
|
6
|
+
// managed daemon (fixed endpoint, never requires auth), cloud entries carry
|
|
7
|
+
// the OAuth token set, selfhost entries carry an endpoint + api_key.
|
|
4
8
|
|
|
5
9
|
import { CliExitError, ExitCode } from "../errors/index.js";
|
|
6
10
|
import { SERVER_PORT } from "../local/constants.js";
|
|
7
|
-
import { type
|
|
11
|
+
import { type Config, activeBackend, isCloudMode } from "./config.js";
|
|
8
12
|
|
|
9
13
|
const DEFAULT_CLOUD_ENDPOINT = "api.stigmer.ai:443";
|
|
10
14
|
const DEFAULT_LOCAL_ENDPOINT = "localhost:7234";
|
|
@@ -15,45 +19,68 @@ export const DEFAULT_CLOUD_CONSOLE_URL = "https://app.stigmer.ai";
|
|
|
15
19
|
// Local mode is single-tenant: the seedpack bootstraps resources into the
|
|
16
20
|
// "stigmer" org (see client-apps/cli/src/local/seedpack/apply.ts DEFAULT_ORG).
|
|
17
21
|
// Org-scoped getByReference now requires an org, so bare-slug `get`/`run`/`apply`
|
|
18
|
-
// in local mode fall back to this default when nothing else is configured.
|
|
19
|
-
//
|
|
22
|
+
// in local mode fall back to this default when nothing else is configured. The
|
|
23
|
+
// same holds for selfhost backends — they ARE the OSS server. Cloud mode has no
|
|
24
|
+
// implicit org — the caller must select one (flag/env/context/login).
|
|
20
25
|
const DEFAULT_LOCAL_ORG = "stigmer";
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Resolve the server endpoint.
|
|
24
|
-
* STIGMER_SERVER_ADDRESS
|
|
25
|
-
* >
|
|
26
|
-
* >
|
|
29
|
+
* STIGMER_SERVER_ADDRESS (explicit override, any backend)
|
|
30
|
+
* > the active entry's endpoint (cloud falls back to api.stigmer.ai:443)
|
|
31
|
+
* > localhost:7234 (the local daemon)
|
|
32
|
+
*
|
|
33
|
+
* A selfhost entry without an endpoint is a loud configuration error —
|
|
34
|
+
* `config backend add` requires one, so this only happens to hand-edited
|
|
35
|
+
* files, and guessing an address for a self-hosted server helps nobody.
|
|
27
36
|
*/
|
|
28
37
|
export function resolveEndpoint(config: Config): string {
|
|
29
38
|
const override = process.env.STIGMER_SERVER_ADDRESS;
|
|
30
39
|
if (override !== undefined && override !== "") {
|
|
31
40
|
return override;
|
|
32
41
|
}
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
const { name, entry } = activeBackend(config);
|
|
43
|
+
if (entry === undefined) {
|
|
44
|
+
return DEFAULT_LOCAL_ENDPOINT;
|
|
45
|
+
}
|
|
46
|
+
if (entry.endpoint !== undefined && entry.endpoint !== "") {
|
|
47
|
+
return entry.endpoint;
|
|
48
|
+
}
|
|
49
|
+
if (entry.type === "cloud") {
|
|
50
|
+
return DEFAULT_CLOUD_ENDPOINT;
|
|
35
51
|
}
|
|
36
|
-
|
|
52
|
+
throw new CliExitError(
|
|
53
|
+
`backend "${name}" has no endpoint configured`,
|
|
54
|
+
ExitCode.Usage,
|
|
55
|
+
[
|
|
56
|
+
`Re-add it with one:`,
|
|
57
|
+
` stigmer config backend add ${name} --endpoint <host:port>`,
|
|
58
|
+
],
|
|
59
|
+
);
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
/**
|
|
40
63
|
* Resolve the auth token.
|
|
41
|
-
* STIGMER_API_KEY
|
|
42
|
-
* > cloud
|
|
43
|
-
* > ""
|
|
64
|
+
* STIGMER_API_KEY (env, also where the --api-key global is bridged)
|
|
65
|
+
* > the active entry's credential (cloud: login token; selfhost: api_key)
|
|
66
|
+
* > "" (unauthenticated)
|
|
44
67
|
*/
|
|
45
68
|
export function resolveToken(config: Config): string {
|
|
46
69
|
const env = process.env.STIGMER_API_KEY;
|
|
47
70
|
if (env !== undefined && env !== "") {
|
|
48
71
|
return env;
|
|
49
72
|
}
|
|
50
|
-
|
|
73
|
+
const { entry } = activeBackend(config);
|
|
74
|
+
if (entry === undefined) {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
return (entry.type === "cloud" ? entry.token : entry.api_key) ?? "";
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
/**
|
|
54
81
|
* Resolve the active organization.
|
|
55
82
|
* --org flag override > STIGMER_ORG_ID env > context.organization
|
|
56
|
-
* >
|
|
83
|
+
* > the active entry's org_id (legacy fallback)
|
|
57
84
|
*/
|
|
58
85
|
export function resolveOrganization(config: Config, flagOrg?: string): string {
|
|
59
86
|
if (flagOrg !== undefined && flagOrg !== "") {
|
|
@@ -67,56 +94,76 @@ export function resolveOrganization(config: Config, flagOrg?: string): string {
|
|
|
67
94
|
if (context !== "") {
|
|
68
95
|
return context;
|
|
69
96
|
}
|
|
70
|
-
// Nothing configured: local
|
|
71
|
-
// so bare-slug operations resolve without ceremony; cloud
|
|
72
|
-
// caller must select an org, e.g. apply raises a clear
|
|
97
|
+
// Nothing configured: local and selfhost fall back to the single-tenant
|
|
98
|
+
// default org so bare-slug operations resolve without ceremony; cloud
|
|
99
|
+
// stays empty (the caller must select an org, e.g. apply raises a clear
|
|
100
|
+
// "organization not set").
|
|
73
101
|
return isCloudMode(config) ? "" : DEFAULT_LOCAL_ORG;
|
|
74
102
|
}
|
|
75
103
|
|
|
76
104
|
/**
|
|
77
105
|
* Resolve the web console URL:
|
|
78
106
|
* 1. `STIGMER_CONSOLE_URL` (explicit override)
|
|
79
|
-
* 2. local
|
|
107
|
+
* 2. local daemon → `http://localhost:{SERVER_PORT}` — the server's own
|
|
80
108
|
* unified port serves the console since DD-012 (one origin for UI and
|
|
81
109
|
* API; the Go-era separate 8234 listener is retired)
|
|
82
|
-
* 3.
|
|
110
|
+
* 3. selfhost → the entry's endpoint on the same one-origin rule
|
|
111
|
+
* (https for :443, http otherwise)
|
|
112
|
+
* 4. cloud → {@link DEFAULT_CLOUD_CONSOLE_URL}
|
|
83
113
|
*
|
|
84
114
|
* The console origin is also the app origin that serves the hosted chat
|
|
85
115
|
* page (`/chat/<org>/<slug>`) and the embed loader (`/embed.js`), so
|
|
86
116
|
* share-link builders use this same resolver — both routes live in the
|
|
87
117
|
* same static export the server serves.
|
|
88
118
|
*/
|
|
89
|
-
export function resolveConsoleURL(
|
|
119
|
+
export function resolveConsoleURL(
|
|
120
|
+
config: Config,
|
|
121
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
122
|
+
): string {
|
|
90
123
|
const override = env.STIGMER_CONSOLE_URL;
|
|
91
124
|
if (override !== undefined && override !== "") return override;
|
|
92
|
-
|
|
125
|
+
const { entry } = activeBackend(config);
|
|
126
|
+
if (entry === undefined) return `http://localhost:${SERVER_PORT}`;
|
|
127
|
+
if (entry.type === "selfhost") {
|
|
128
|
+
const endpoint = entry.endpoint ?? "";
|
|
129
|
+
const scheme = endpoint.endsWith(":443") ? "https" : "http";
|
|
130
|
+
return `${scheme}://${endpoint.replace(/:443$/, "")}`;
|
|
131
|
+
}
|
|
93
132
|
return DEFAULT_CLOUD_CONSOLE_URL;
|
|
94
133
|
}
|
|
95
134
|
|
|
96
135
|
/**
|
|
97
|
-
* The configured organization, with the
|
|
98
|
-
* only what is explicitly set (no implicit local default) so
|
|
99
|
-
* display can faithfully show "(not set)".
|
|
136
|
+
* The configured organization, with the active entry's org_id fallback.
|
|
137
|
+
* Reports only what is explicitly set (no implicit local default) so
|
|
138
|
+
* config/context display can faithfully show "(not set)".
|
|
100
139
|
*/
|
|
101
140
|
export function resolveContextOrganization(config: Config): string {
|
|
102
|
-
if (
|
|
141
|
+
if (
|
|
142
|
+
config.context?.organization !== undefined &&
|
|
143
|
+
config.context.organization !== ""
|
|
144
|
+
) {
|
|
103
145
|
return config.context.organization;
|
|
104
146
|
}
|
|
105
|
-
return config.
|
|
147
|
+
return activeBackend(config).entry?.org_id ?? "";
|
|
106
148
|
}
|
|
107
149
|
|
|
108
150
|
/**
|
|
109
|
-
* Fail fast with a clear "please log in" error when a
|
|
110
|
-
* usable credential.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
151
|
+
* Fail fast with a clear "please log in" error when a CLOUD command has no
|
|
152
|
+
* usable credential. The local daemon never requires auth. Selfhost
|
|
153
|
+
* backends are deliberately not gated client-side: an unauthenticated
|
|
154
|
+
* self-host (no issuer configured) is a legitimate posture, and an
|
|
155
|
+
* authenticated one answers UNAUTHENTICATED with a clear message — the
|
|
156
|
+
* server owns that decision. A credential is any of: the STIGMER_API_KEY
|
|
157
|
+
* env (or bridged --api-key), a persisted access token, or a refresh token
|
|
158
|
+
* (which the token provider can exchange for a fresh access token).
|
|
115
159
|
*/
|
|
116
160
|
export function ensureAuthenticated(config: Config): void {
|
|
117
|
-
|
|
161
|
+
const { entry } = activeBackend(config);
|
|
162
|
+
if (entry?.type !== "cloud") return;
|
|
118
163
|
if (process.env.STIGMER_API_KEY) return;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
164
|
+
if (entry.token || entry.refresh_token) return;
|
|
165
|
+
throw new CliExitError("Not authenticated", ExitCode.Auth, [
|
|
166
|
+
"Please sign in:",
|
|
167
|
+
" stigmer auth login",
|
|
168
|
+
]);
|
|
122
169
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Resolution and on-demand acquisition of the seedpack content.
|
|
2
2
|
//
|
|
3
3
|
// The seedpack (system agents, skills, MCP servers, workflows under the
|
|
4
|
-
// "stigmer" org) is the
|
|
4
|
+
// "stigmer" org) is the system content every backend is bootstrapped with.
|
|
5
5
|
// A lean `npx @stigmer/cli` must not carry ~300 content files (DD-002), so the
|
|
6
6
|
// content ships as `@stigmer/seedpack` and is acquired on demand — the same
|
|
7
7
|
// pattern as `@stigmer/runner-slim` and the managed Temporal binary.
|
|
@@ -12,14 +12,22 @@
|
|
|
12
12
|
// ~/.stigmer/runtimes/<version>/ and read from
|
|
13
13
|
// node_modules/@stigmer/seedpack.
|
|
14
14
|
//
|
|
15
|
-
// The content-hash and extract logic mirror `@stigmer/seedpack` exactly
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// The content-hash and extract logic mirror `@stigmer/seedpack` exactly; the CLI
|
|
16
|
+
// reimplements them over a resolved directory so it stays decoupled from the
|
|
17
|
+
// package in the lean install (where the package is not a dependency, only an
|
|
18
|
+
// on-demand artifact). `seedpack/src/index.ts` is the one definition both follow.
|
|
19
19
|
|
|
20
20
|
import { execFileSync } from "node:child_process";
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
cpSync,
|
|
24
|
+
existsSync,
|
|
25
|
+
mkdirSync,
|
|
26
|
+
readdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
statSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from "node:fs";
|
|
23
31
|
import { homedir } from "node:os";
|
|
24
32
|
import { dirname, join, posix, relative, sep } from "node:path";
|
|
25
33
|
import { fileURLToPath } from "node:url";
|
|
@@ -33,9 +41,9 @@ const SEEDPACK_PACKAGE = "@stigmer/seedpack";
|
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
43
|
* The canonical seedpack entries, in apply-safe order. Kept in sync with
|
|
36
|
-
* `SEEDPACK_ENTRIES` in `@stigmer/seedpack`
|
|
37
|
-
*
|
|
38
|
-
*
|
|
44
|
+
* `SEEDPACK_ENTRIES` in `@stigmer/seedpack` (`seedpack/src/index.ts`, the one
|
|
45
|
+
* definition) — `tools/`, `icons/`, and `canary/` (CI canary metadata, not a
|
|
46
|
+
* resource) are deliberately excluded so the content set (and thus the
|
|
39
47
|
* idempotency hash) is identical across delivery paths.
|
|
40
48
|
*/
|
|
41
49
|
export const SEEDPACK_ENTRIES = [
|
|
@@ -70,7 +78,9 @@ export interface ResolveSeedpackOptions {
|
|
|
70
78
|
* on-demand-acquired `@stigmer/seedpack` package. Throws actionable guidance when
|
|
71
79
|
* acquisition is not possible (non-release build, or a failed/incomplete install).
|
|
72
80
|
*/
|
|
73
|
-
export function resolveSeedpackContent(
|
|
81
|
+
export function resolveSeedpackContent(
|
|
82
|
+
opts: ResolveSeedpackOptions = {},
|
|
83
|
+
): SeedpackContent {
|
|
74
84
|
const repo = repoSeedpackDir();
|
|
75
85
|
if (repo !== null) return { dir: repo, source: "repo" };
|
|
76
86
|
return { dir: acquireSeedpack(opts), source: "package" };
|
|
@@ -86,10 +96,14 @@ export function acquireSeedpack(opts: ResolveSeedpackOptions = {}): string {
|
|
|
86
96
|
const home = opts.home ?? homedir();
|
|
87
97
|
const version = opts.version ?? VERSION;
|
|
88
98
|
if (!isAcquirableRelease(version)) {
|
|
89
|
-
throw new CliExitError(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
throw new CliExitError(
|
|
100
|
+
`cannot acquire ${SEEDPACK_PACKAGE} for a non-release build (${version})`,
|
|
101
|
+
ExitCode.General,
|
|
102
|
+
[
|
|
103
|
+
"Run from the repo (the seedpack/ tree is used directly in dev).",
|
|
104
|
+
"On-demand acquisition is only available for published releases.",
|
|
105
|
+
],
|
|
106
|
+
);
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
const installDir = join(runtimesDir(home), version);
|
|
@@ -103,16 +117,21 @@ export function acquireSeedpack(opts: ResolveSeedpackOptions = {}): string {
|
|
|
103
117
|
// additive and non-pruning, so this prefix is safely shared with the runner.
|
|
104
118
|
const rootPkg = join(installDir, "package.json");
|
|
105
119
|
if (!existsSync(rootPkg)) {
|
|
106
|
-
writeFileSync(
|
|
120
|
+
writeFileSync(
|
|
121
|
+
rootPkg,
|
|
122
|
+
`${JSON.stringify({ name: "stigmer-runtime", private: true, version: "0.0.0" }, null, 2)}\n`,
|
|
123
|
+
);
|
|
107
124
|
}
|
|
108
125
|
const install = opts.install ?? installSeedpack;
|
|
109
126
|
install(installDir, `${SEEDPACK_PACKAGE}@${version}`);
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
if (!existsSync(marker)) {
|
|
113
|
-
throw new CliExitError(
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
throw new CliExitError(
|
|
131
|
+
`${SEEDPACK_PACKAGE} install did not produce ${marker}`,
|
|
132
|
+
ExitCode.General,
|
|
133
|
+
[`Remove ${installDir} and retry.`],
|
|
134
|
+
);
|
|
116
135
|
}
|
|
117
136
|
return pkgDir;
|
|
118
137
|
}
|
|
@@ -121,9 +140,21 @@ export function acquireSeedpack(opts: ResolveSeedpackOptions = {}): string {
|
|
|
121
140
|
// the user sees the one-time download progress.
|
|
122
141
|
function installSeedpack(installDir: string, spec: string): void {
|
|
123
142
|
try {
|
|
124
|
-
execFileSync(
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
execFileSync(
|
|
144
|
+
"npm",
|
|
145
|
+
[
|
|
146
|
+
"install",
|
|
147
|
+
spec,
|
|
148
|
+
"--prefix",
|
|
149
|
+
installDir,
|
|
150
|
+
"--omit=dev",
|
|
151
|
+
"--no-audit",
|
|
152
|
+
"--no-fund",
|
|
153
|
+
],
|
|
154
|
+
{
|
|
155
|
+
stdio: "inherit",
|
|
156
|
+
},
|
|
157
|
+
);
|
|
127
158
|
} catch (err) {
|
|
128
159
|
throw new CliExitError(`failed to install ${spec}`, ExitCode.General, [
|
|
129
160
|
`Command: npm install ${spec} --prefix ${installDir}`,
|