@stigmer/cli 3.23.1-dev.20260923075205 → 3.25.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/commands/bootstrap.d.ts.map +1 -1
- package/commands/bootstrap.js +16 -57
- package/commands/bootstrap.js.map +1 -1
- package/commands/config/context.d.ts.map +1 -1
- package/commands/config/context.js +41 -5
- package/commands/config/context.js.map +1 -1
- package/commands/up.js +1 -1
- package/commands/up.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/resolve.d.ts +8 -0
- package/config/resolve.d.ts.map +1 -1
- package/config/resolve.js +8 -8
- package/config/resolve.js.map +1 -1
- package/local/bootstrap.d.ts +9 -29
- package/local/bootstrap.d.ts.map +1 -1
- package/local/bootstrap.js +61 -69
- package/local/bootstrap.js.map +1 -1
- package/local/daemon/launch.d.ts +2 -2
- package/local/daemon/launch.d.ts.map +1 -1
- package/marketplace/official.d.ts.map +1 -1
- package/marketplace/official.js +6 -5
- package/marketplace/official.js.map +1 -1
- package/marketplace/render.d.ts.map +1 -1
- package/marketplace/render.js +0 -3
- package/marketplace/render.js.map +1 -1
- package/package.json +7 -7
- package/src/client/client.test.ts +8 -8
- package/src/commands/bootstrap.test.ts +19 -75
- package/src/commands/bootstrap.ts +19 -68
- package/src/commands/config/context.test.ts +171 -0
- package/src/commands/config/context.ts +50 -5
- package/src/commands/marketplace.test.ts +2 -2
- package/src/commands/up.ts +1 -1
- package/src/config/index.ts +1 -0
- package/src/config/resolve.test.ts +2 -2
- package/src/config/resolve.ts +8 -8
- package/src/local/bootstrap.test.ts +132 -65
- package/src/local/bootstrap.ts +64 -139
- package/src/local/daemon/launch.ts +2 -2
- package/src/marketplace/official.ts +6 -5
- package/src/marketplace/read.test.ts +2 -3
- package/src/marketplace/render.ts +0 -3
- package/local/plugins/defaults.d.ts +0 -55
- package/local/plugins/defaults.d.ts.map +0 -1
- package/local/plugins/defaults.js +0 -111
- package/local/plugins/defaults.js.map +0 -1
- package/local/system-org.d.ts +0 -15
- package/local/system-org.d.ts.map +0 -1
- package/local/system-org.js +0 -57
- package/local/system-org.js.map +0 -1
- package/src/local/plugins/defaults.test.ts +0 -271
- package/src/local/plugins/defaults.ts +0 -179
- package/src/local/system-org.test.ts +0 -124
- package/src/local/system-org.ts +0 -63
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
// `stigmer config context show` —
|
|
1
|
+
// `stigmer config context show|set` — the active CLI context: the
|
|
2
|
+
// organization every command targets when neither `--org` nor
|
|
3
|
+
// STIGMER_ORG_ID names one.
|
|
2
4
|
//
|
|
3
|
-
// `
|
|
4
|
-
//
|
|
5
|
+
// `set --org <slug>` checks the slug against the active backend before it
|
|
6
|
+
// persists: the organization must be one the caller belongs to there
|
|
7
|
+
// (findMyOrganizations), so a typo or a stranger's slug is refused at the
|
|
8
|
+
// moment it is typed, not on the next command. An empty slug clears the
|
|
9
|
+
// context without asking the backend, and the resolver falls back to its
|
|
10
|
+
// default again. The raw `config set context.organization` stays the
|
|
11
|
+
// unchecked escape hatch.
|
|
5
12
|
|
|
6
13
|
import type { Command } from "commander";
|
|
7
|
-
import { type Config, load, resolveContextOrganization } from "../../config/index.js";
|
|
14
|
+
import { type Config, activeBackendName, ensureAuthenticated, load, resolveContextOrganization, save } from "../../config/index.js";
|
|
15
|
+
import { CliExitError, ExitCode, UsageError } from "../../errors/index.js";
|
|
8
16
|
import { CommandResult, type OutputFlags, renderResult } from "../../output/index.js";
|
|
9
17
|
import { addResultFlags, resultFormat } from "../shared.js";
|
|
10
18
|
|
|
@@ -16,11 +24,48 @@ export function addContextCommands(context: Command): void {
|
|
|
16
24
|
renderResult(buildShow(load()), resultFormat(options));
|
|
17
25
|
});
|
|
18
26
|
addResultFlags(show);
|
|
27
|
+
|
|
28
|
+
const set = context
|
|
29
|
+
.command("set")
|
|
30
|
+
.description("set the active context's organization, checked against the backend")
|
|
31
|
+
.option("--org <slug>", "an organization you belong to on the active backend (empty clears it)")
|
|
32
|
+
.action((options: OutputFlags & { org?: string }) => runSet(options));
|
|
33
|
+
addResultFlags(set);
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
function buildShow(config: Config): CommandResult {
|
|
22
37
|
const organization = resolveContextOrganization(config) || "(not set)";
|
|
23
38
|
const result = CommandResult.success("CLI context");
|
|
24
|
-
result.addSection("").field("Organization", organization).field("Backend", config
|
|
39
|
+
result.addSection("").field("Organization", organization).field("Backend", activeBackendName(config));
|
|
25
40
|
return result;
|
|
26
41
|
}
|
|
42
|
+
|
|
43
|
+
async function runSet(flags: OutputFlags & { org?: string }): Promise<void> {
|
|
44
|
+
const org = flags.org;
|
|
45
|
+
if (org === undefined) {
|
|
46
|
+
throw new UsageError(
|
|
47
|
+
"--org is required\n\nSet it with:\n stigmer config context set --org <org>\nClear it with:\n stigmer config context set --org \"\"",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const config = load();
|
|
52
|
+
if (org !== "") await assertMembership(config, org);
|
|
53
|
+
|
|
54
|
+
(config.context ??= {}).organization = org;
|
|
55
|
+
save(config);
|
|
56
|
+
const message = org === "" ? "Context organization cleared" : `Context organization set to '${org}'`;
|
|
57
|
+
renderResult(CommandResult.success(message), resultFormat(flags));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function assertMembership(config: Config, org: string): Promise<void> {
|
|
61
|
+
const { connectBackend } = await import("../../backend.js");
|
|
62
|
+
const client = connectBackend(config);
|
|
63
|
+
ensureAuthenticated(client.config);
|
|
64
|
+
const mine = await client.stigmer.organization.findMyOrganizations();
|
|
65
|
+
if (mine.entries.some((entry) => entry.metadata?.slug === org)) return;
|
|
66
|
+
throw new CliExitError(
|
|
67
|
+
`organization '${org}' is not one you belong to on the '${activeBackendName(config)}' backend`,
|
|
68
|
+
ExitCode.NotFound,
|
|
69
|
+
["See the organizations you belong to with: stigmer list organizations"],
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -169,11 +169,11 @@ describe("marketplace show", () => {
|
|
|
169
169
|
expect(payload.data.warnings[0].subject).toBe("ghost");
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
-
it("shows the built-in marketplace
|
|
172
|
+
it("shows the built-in marketplace", async () => {
|
|
173
173
|
const outcome = await run("show", "stigmer", "--json");
|
|
174
174
|
expect(outcome.exitCode).toBe(ExitCode.Success);
|
|
175
175
|
const payload = JSON.parse(outcome.stdout);
|
|
176
|
-
expect(payload.data.marketplace.
|
|
176
|
+
expect(payload.data.marketplace.name).toBe("stigmer");
|
|
177
177
|
expect(payload.data.plugins.map((p: { name: string }) => p.name)).toContain(
|
|
178
178
|
"linear",
|
|
179
179
|
);
|
package/src/commands/up.ts
CHANGED
|
@@ -74,7 +74,7 @@ async function runUp(run: UpRun, flags: OutputFlags): Promise<void> {
|
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// The card renders when the stack is serving and
|
|
77
|
+
// The card renders when the stack is serving and bootstrapped — the moment the
|
|
78
78
|
// detached shape would have returned — and the process then stays up,
|
|
79
79
|
// mirroring the server's and runner's output, until a shutdown signal.
|
|
80
80
|
const code = await launch.upForeground(run, homedir(), {
|
package/src/config/index.ts
CHANGED
|
@@ -150,11 +150,11 @@ describe("resolveOrganization", () => {
|
|
|
150
150
|
);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
it("defaults to the
|
|
153
|
+
it("defaults to the stigmer org in local mode when nothing is configured", () => {
|
|
154
154
|
expect(resolveOrganization(localConfig())).toBe("stigmer");
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
it("defaults to the
|
|
157
|
+
it("defaults to the stigmer org on a selfhost backend (it IS the OSS server)", () => {
|
|
158
158
|
expect(resolveOrganization(selfhostConfig())).toBe("stigmer");
|
|
159
159
|
});
|
|
160
160
|
|
package/src/config/resolve.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
import { CliExitError, ExitCode } from "../errors/index.js";
|
|
10
10
|
import { SERVER_PORT } from "../local/constants.js";
|
|
11
|
-
import { SYSTEM_ORG } from "../local/system-org.js";
|
|
12
11
|
import { type Config, activeBackend, isCloudMode } from "./config.js";
|
|
13
12
|
|
|
14
13
|
const DEFAULT_CLOUD_ENDPOINT = "api.stigmer.ai:443";
|
|
@@ -17,13 +16,14 @@ const DEFAULT_LOCAL_ENDPOINT = "localhost:7234";
|
|
|
17
16
|
/** Well-known URL for the Stigmer Cloud web console. */
|
|
18
17
|
export const DEFAULT_CLOUD_CONSOLE_URL = "https://app.stigmer.ai";
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The organization a local or selfhost backend falls back to when nothing
|
|
21
|
+
* names one, so bare-slug `get`/`run`/`apply` resolve without ceremony.
|
|
22
|
+
* `stigmer up` creates it on the local stack and `stigmer bootstrap` on a raw
|
|
23
|
+
* self-hosted server (local/bootstrap.ts). Cloud mode has no implicit org —
|
|
24
|
+
* the caller must select one (flag/env/context/login).
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_LOCAL_ORG = "stigmer";
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Resolve the server endpoint.
|
|
@@ -1,99 +1,166 @@
|
|
|
1
|
-
// Pins the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Pins the bootstrap's one act over a real Connect backend: an org the
|
|
2
|
+
// identity already sees is `present` with no create; an absent one is created
|
|
3
|
+
// as the Organization as code (slug, name, self-managed) with NO label, since
|
|
4
|
+
// a reserved `stigmer.ai/` label is refused by a self-hosted server with
|
|
5
|
+
// authentication on (stigmer/stigmer#1192); a create that loses the race to
|
|
6
|
+
// another launcher (ALREADY_EXISTS) is `present`, not an error; any other
|
|
7
|
+
// refusal propagates with the server's sentence. And the local shape `up`
|
|
8
|
+
// runs: it announces a created org, stays quiet on a present one, and turns
|
|
9
|
+
// any failure into one warning naming the retry, never a throw.
|
|
6
10
|
|
|
7
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createServer as createHttp2Server,
|
|
13
|
+
type Http2Server,
|
|
14
|
+
type ServerHttp2Session,
|
|
15
|
+
} from "node:http2";
|
|
16
|
+
import type { AddressInfo } from "node:net";
|
|
17
|
+
import { create } from "@bufbuild/protobuf";
|
|
18
|
+
import { Code, ConnectError, type ConnectRouter } from "@connectrpc/connect";
|
|
19
|
+
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
20
|
+
import {
|
|
21
|
+
type Organization,
|
|
22
|
+
OrganizationSchema,
|
|
23
|
+
} from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
24
|
+
import { OrganizationCommandController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/command_pb";
|
|
25
|
+
import { ManagementMode } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/enum_pb";
|
|
26
|
+
import { OrganizationsSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/io_pb";
|
|
27
|
+
import { OrganizationQueryController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/query_pb";
|
|
28
|
+
import { createNodeClient, normalizeEndpoint } from "@stigmer/sdk/node";
|
|
8
29
|
import type { Stigmer } from "@stigmer/sdk";
|
|
30
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
9
31
|
import type { BackendClient } from "../client/index.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
type PreparedBootstrap,
|
|
13
|
-
bootstrapLocalBackend,
|
|
14
|
-
} from "./bootstrap.js";
|
|
32
|
+
import { DEFAULT_LOCAL_ORG } from "../config/resolve.js";
|
|
33
|
+
import { bootstrapBackend, bootstrapLocalBackend } from "./bootstrap.js";
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
35
|
+
let backend: Http2Server;
|
|
36
|
+
let stigmer: Stigmer;
|
|
37
|
+
const openSessions = new Set<ServerHttp2Session>();
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
/** The orgs the backend holds, by slug. */
|
|
40
|
+
let held: Map<string, Organization>;
|
|
41
|
+
let creates: Organization[];
|
|
42
|
+
/** What `create` answers with instead of creating, when set. */
|
|
43
|
+
let refuseCreate: ConnectError | undefined;
|
|
23
44
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
const routes = (router: ConnectRouter) => {
|
|
47
|
+
router.service(OrganizationQueryController, {
|
|
48
|
+
findMyOrganizations: () =>
|
|
49
|
+
create(OrganizationsSchema, { entries: [...held.values()] }),
|
|
50
|
+
});
|
|
51
|
+
router.service(OrganizationCommandController, {
|
|
52
|
+
create: (org) => {
|
|
53
|
+
creates.push(org);
|
|
54
|
+
if (refuseCreate !== undefined) throw refuseCreate;
|
|
55
|
+
const slug = org.metadata?.slug ?? "";
|
|
56
|
+
const row = create(OrganizationSchema, org);
|
|
57
|
+
if (row.metadata !== undefined) row.metadata.id = `org_${slug}`;
|
|
58
|
+
held.set(slug, row);
|
|
59
|
+
return row;
|
|
39
60
|
},
|
|
40
61
|
});
|
|
41
|
-
|
|
62
|
+
};
|
|
63
|
+
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
64
|
+
backend.on("session", (session) => {
|
|
65
|
+
openSessions.add(session);
|
|
66
|
+
session.on("close", () => openSessions.delete(session));
|
|
67
|
+
});
|
|
68
|
+
await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
|
|
69
|
+
const port = (backend.address() as AddressInfo).port;
|
|
70
|
+
stigmer = createNodeClient({
|
|
71
|
+
baseUrl: normalizeEndpoint(`127.0.0.1:${port}`),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(async () => {
|
|
76
|
+
for (const session of openSessions) session.destroy();
|
|
77
|
+
await new Promise<void>((resolve) => backend.close(() => resolve()));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
held = new Map();
|
|
82
|
+
creates = [];
|
|
83
|
+
refuseCreate = undefined;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("bootstrapBackend", () => {
|
|
87
|
+
it("is present, with no create, when the identity already sees the org", async () => {
|
|
88
|
+
held.set(
|
|
89
|
+
DEFAULT_LOCAL_ORG,
|
|
90
|
+
create(OrganizationSchema, { metadata: { slug: DEFAULT_LOCAL_ORG } }),
|
|
91
|
+
);
|
|
92
|
+
expect(await bootstrapBackend(stigmer)).toBe("present");
|
|
93
|
+
expect(creates).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("creates the Organization as code, with no label, when absent", async () => {
|
|
97
|
+
held.set("acme", create(OrganizationSchema, { metadata: { slug: "acme" } }));
|
|
98
|
+
expect(await bootstrapBackend(stigmer)).toBe("created");
|
|
99
|
+
expect(creates).toHaveLength(1);
|
|
100
|
+
const sent = creates[0]!;
|
|
101
|
+
expect(sent.metadata?.slug).toBe(DEFAULT_LOCAL_ORG);
|
|
102
|
+
expect(sent.metadata?.org).toBe(DEFAULT_LOCAL_ORG);
|
|
103
|
+
expect(sent.metadata?.name).toBe("Stigmer");
|
|
104
|
+
expect(sent.metadata?.labels).toEqual({});
|
|
105
|
+
expect(sent.spec?.managementMode).toBe(ManagementMode.self_managed);
|
|
106
|
+
expect(sent.spec?.description).not.toBe("");
|
|
42
107
|
});
|
|
43
108
|
|
|
44
|
-
it("
|
|
109
|
+
it("treats a create lost to another launcher as present", async () => {
|
|
110
|
+
refuseCreate = new ConnectError(
|
|
111
|
+
`organization '${DEFAULT_LOCAL_ORG}' already exists`,
|
|
112
|
+
Code.AlreadyExists,
|
|
113
|
+
);
|
|
114
|
+
expect(await bootstrapBackend(stigmer)).toBe("present");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("propagates any other refusal with the server's sentence", async () => {
|
|
118
|
+
refuseCreate = new ConnectError("organization creation is disabled", Code.PermissionDenied);
|
|
119
|
+
await expect(bootstrapBackend(stigmer)).rejects.toThrow(
|
|
120
|
+
/organization creation is disabled/,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("bootstrapLocalBackend", () => {
|
|
126
|
+
const client = { stigmer: {} as unknown as Stigmer } as unknown as BackendClient;
|
|
127
|
+
|
|
128
|
+
it("runs the act over the local client and announces a created org", async () => {
|
|
45
129
|
const said: string[] = [];
|
|
46
|
-
let
|
|
130
|
+
let seen: Stigmer | undefined;
|
|
47
131
|
await bootstrapLocalBackend({
|
|
48
132
|
client,
|
|
49
133
|
say: (line) => said.push(line),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
run: async () => {
|
|
54
|
-
ran += 1;
|
|
55
|
-
return converged();
|
|
134
|
+
bootstrap: async (s) => {
|
|
135
|
+
seen = s;
|
|
136
|
+
return "created";
|
|
56
137
|
},
|
|
57
138
|
});
|
|
58
|
-
expect(
|
|
59
|
-
expect(said).toEqual([
|
|
60
|
-
"Warning: could not prepare the default plugins, so the local backend was not bootstrapped. Run 'stigmer up' again to retry.",
|
|
61
|
-
]);
|
|
139
|
+
expect(seen).toBe(client.stigmer);
|
|
140
|
+
expect(said).toEqual([`Created the '${DEFAULT_LOCAL_ORG}' organization`]);
|
|
62
141
|
});
|
|
63
142
|
|
|
64
|
-
it("
|
|
143
|
+
it("says nothing when the org is already present", async () => {
|
|
65
144
|
const said: string[] = [];
|
|
66
145
|
await bootstrapLocalBackend({
|
|
67
146
|
client,
|
|
68
147
|
say: (line) => said.push(line),
|
|
69
|
-
|
|
70
|
-
run: async () => {
|
|
71
|
-
throw new Error("backend unreachable");
|
|
72
|
-
},
|
|
148
|
+
bootstrap: async () => "present",
|
|
73
149
|
});
|
|
74
|
-
expect(said).toEqual([
|
|
75
|
-
"Warning: failed to bootstrap the local backend. Run 'stigmer up' again to retry.",
|
|
76
|
-
]);
|
|
150
|
+
expect(said).toEqual([]);
|
|
77
151
|
});
|
|
78
152
|
|
|
79
|
-
it("
|
|
153
|
+
it("contains a failure in one warning naming the retry", async () => {
|
|
80
154
|
const said: string[] = [];
|
|
81
155
|
await bootstrapLocalBackend({
|
|
82
156
|
client,
|
|
83
157
|
say: (line) => said.push(line),
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
plugins: {
|
|
88
|
-
outcomes: [
|
|
89
|
-
{ name: "assistant", action: "failed", error: "refused" },
|
|
90
|
-
],
|
|
91
|
-
failed: ["assistant"],
|
|
92
|
-
},
|
|
93
|
-
}),
|
|
158
|
+
bootstrap: async () => {
|
|
159
|
+
throw new Error("backend unreachable");
|
|
160
|
+
},
|
|
94
161
|
});
|
|
95
162
|
expect(said).toEqual([
|
|
96
|
-
"Warning: failed to
|
|
163
|
+
"Warning: failed to bootstrap the local backend. Run 'stigmer up' again to retry.",
|
|
97
164
|
]);
|
|
98
165
|
});
|
|
99
166
|
});
|
package/src/local/bootstrap.ts
CHANGED
|
@@ -1,173 +1,98 @@
|
|
|
1
|
-
// What makes a backend
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// cannot drift in
|
|
1
|
+
// What makes a backend ready for the CLI: the organization it falls back to
|
|
2
|
+
// on a local or selfhost backend (DEFAULT_LOCAL_ORG, `stigmer`) exists.
|
|
3
|
+
// Nothing is installed into it: a session with no agent runs the built-in
|
|
4
|
+
// assistant, so a fresh install needs no default content. `stigmer up` runs
|
|
5
|
+
// this against the local server it just started; `stigmer bootstrap` runs it
|
|
6
|
+
// against whatever backend the CLI is pointed at, a raw self-hosted server.
|
|
7
|
+
// One definition, so a laptop, a container and a raw server cannot drift in
|
|
8
|
+
// what "ready" means.
|
|
8
9
|
//
|
|
9
|
-
//
|
|
10
|
+
// "Ensure" is find-then-create with the race closed by the server: two
|
|
11
|
+
// launchers racing the same check see one `already-exists`, and that IS the
|
|
12
|
+
// desired end state, so it is reported as `present`, never rethrown. Any
|
|
13
|
+
// other refusal propagates with the server's sentence; the caller decides
|
|
14
|
+
// how loud to be.
|
|
10
15
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// push adopts a system-content row it replaces in place (a default
|
|
17
|
-
// agent an earlier release seeded keeps its id, its instances and every
|
|
18
|
-
// conversation bound to them), so nothing here ever deletes a row.
|
|
16
|
+
// The create carries no label. Writing a label in the reserved `stigmer.ai/`
|
|
17
|
+
// namespace needs a platform permission that a self-hosted server with
|
|
18
|
+
// authentication on grants to nobody, and nothing reads one on an
|
|
19
|
+
// organization (stigmer/stigmer#1192). Organizations created by older CLIs
|
|
20
|
+
// keep the `stigmer.ai/system` label they were given; it is inert.
|
|
19
21
|
//
|
|
20
|
-
// Inside `up`
|
|
21
|
-
// retry, because the stack is already serving
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
22
|
+
// Inside `up` the bootstrap is best-effort with one warning naming the
|
|
23
|
+
// retry, because the stack is already serving and a failure here must never
|
|
24
|
+
// tear it down. Both `up` shapes (detached and foreground) call the same
|
|
25
|
+
// function, and both target the local server `up` just started, never the
|
|
26
|
+
// active backend context: a CLI pointed at a remote backend still gets its
|
|
27
|
+
// local stack bootstrapped and never creates an organization elsewhere. The
|
|
28
|
+
// client is pinned to a fresh local config (localhost:SERVER_PORT) with no
|
|
29
|
+
// auth, the trusted-local identity.
|
|
28
30
|
|
|
29
|
-
import
|
|
31
|
+
import { ManagementMode } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/enum_pb";
|
|
32
|
+
import { type Stigmer, StigmerError } from "@stigmer/sdk";
|
|
30
33
|
import type { BackendClient } from "../client/index.js";
|
|
34
|
+
import { DEFAULT_LOCAL_ORG } from "../config/resolve.js";
|
|
31
35
|
import { log } from "../logger.js";
|
|
32
|
-
import type { ResolveOfficialOptions } from "../marketplace/official.js";
|
|
33
|
-
import type {
|
|
34
|
-
DefaultPluginsResult,
|
|
35
|
-
InstallingVerb,
|
|
36
|
-
PreparedDefault,
|
|
37
|
-
} from "./plugins/defaults.js";
|
|
38
|
-
import { type EnsureSystemOrgOutcome, SYSTEM_ORG } from "./system-org.js";
|
|
39
36
|
|
|
40
37
|
export type Say = (line: string) => void;
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
export interface PreparedBootstrap {
|
|
44
|
-
readonly defaults: readonly PreparedDefault[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface BootstrapResult {
|
|
48
|
-
readonly org: EnsureSystemOrgOutcome;
|
|
49
|
-
readonly plugins: DefaultPluginsResult;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface PrepareBootstrapOptions {
|
|
53
|
-
/** How the official tree is found (injectable for tests). */
|
|
54
|
-
readonly official?: ResolveOfficialOptions;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Phase 1. Throws when the catalogue cannot be resolved or a default cannot be prepared. */
|
|
58
|
-
export async function prepareBootstrap(
|
|
59
|
-
options: PrepareBootstrapOptions = {},
|
|
60
|
-
): Promise<PreparedBootstrap> {
|
|
61
|
-
const { prepareDefaultPlugins } = await import("./plugins/defaults.js");
|
|
62
|
-
return { defaults: await prepareDefaultPlugins(options.official) };
|
|
63
|
-
}
|
|
39
|
+
export type BootstrapOutcome = "present" | "created";
|
|
64
40
|
|
|
65
41
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* installed plugin's version message.
|
|
42
|
+
* Make sure the organization the CLI falls back to exists on the backend
|
|
43
|
+
* `stigmer` is bound to: what `stigmer bootstrap` runs. Idempotent; a lost
|
|
44
|
+
* create race counts as `present`.
|
|
70
45
|
*/
|
|
71
|
-
export async function
|
|
72
|
-
stigmer
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
say: Say,
|
|
94
|
-
options: PrepareBootstrapOptions = {},
|
|
95
|
-
): Promise<BootstrapResult> {
|
|
96
|
-
return runBootstrap(
|
|
97
|
-
stigmer,
|
|
98
|
-
await prepareBootstrap(options),
|
|
99
|
-
say,
|
|
100
|
-
"stigmer bootstrap",
|
|
101
|
-
);
|
|
46
|
+
export async function bootstrapBackend(stigmer: Stigmer): Promise<BootstrapOutcome> {
|
|
47
|
+
const mine = await stigmer.organization.findMyOrganizations();
|
|
48
|
+
if (mine.entries.some((org) => org.metadata?.slug === DEFAULT_LOCAL_ORG)) {
|
|
49
|
+
return "present";
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
// Organizations are self-owning: the org field is the slug itself, the
|
|
53
|
+
// shape the console's create form and the e2e harness both use.
|
|
54
|
+
await stigmer.organization.create({
|
|
55
|
+
name: "Stigmer",
|
|
56
|
+
slug: DEFAULT_LOCAL_ORG,
|
|
57
|
+
org: DEFAULT_LOCAL_ORG,
|
|
58
|
+
description: "The organization the Stigmer CLI uses when none is named",
|
|
59
|
+
managementMode: ManagementMode.self_managed,
|
|
60
|
+
});
|
|
61
|
+
return "created";
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof StigmerError && error.code === "already-exists") {
|
|
64
|
+
return "present";
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
102
68
|
}
|
|
103
69
|
|
|
104
|
-
/** Seams of the local bootstrap, injectable for tests; the defaults are the real
|
|
70
|
+
/** Seams of the local bootstrap, injectable for tests; the defaults are the real act over the real local client. */
|
|
105
71
|
export interface BootstrapDeps {
|
|
106
72
|
readonly client?: BackendClient;
|
|
107
73
|
readonly say?: Say;
|
|
108
|
-
readonly
|
|
109
|
-
readonly run?: (
|
|
110
|
-
stigmer: Stigmer,
|
|
111
|
-
prepared: PreparedBootstrap,
|
|
112
|
-
say: Say,
|
|
113
|
-
) => Promise<BootstrapResult>;
|
|
74
|
+
readonly bootstrap?: (stigmer: Stigmer) => Promise<BootstrapOutcome>;
|
|
114
75
|
}
|
|
115
76
|
|
|
116
77
|
const sayToStderr: Say = (line) => {
|
|
117
78
|
process.stderr.write(`${line}\n`);
|
|
118
79
|
};
|
|
119
80
|
|
|
120
|
-
/** What `stigmer up` does once the local server answers. Never throws past its own
|
|
121
|
-
export async function bootstrapLocalBackend(
|
|
122
|
-
deps: BootstrapDeps = {},
|
|
123
|
-
): Promise<void> {
|
|
81
|
+
/** What `stigmer up` does once the local server answers. Never throws past its own warning. */
|
|
82
|
+
export async function bootstrapLocalBackend(deps: BootstrapDeps = {}): Promise<void> {
|
|
124
83
|
const client = deps.client ?? (await freshLocalClient());
|
|
125
84
|
const say = deps.say ?? sayToStderr;
|
|
126
85
|
|
|
127
|
-
let
|
|
128
|
-
try {
|
|
129
|
-
prepared = await (deps.prepare ?? prepareBootstrap)();
|
|
130
|
-
} catch (err) {
|
|
131
|
-
log.warn("bootstrap preparation failed", { error: String(err) });
|
|
132
|
-
say(
|
|
133
|
-
"Warning: could not prepare the default plugins, so the local backend was not bootstrapped. Run 'stigmer up' again to retry.",
|
|
134
|
-
);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
let result: BootstrapResult;
|
|
86
|
+
let outcome: BootstrapOutcome;
|
|
139
87
|
try {
|
|
140
|
-
|
|
141
|
-
client.stigmer,
|
|
142
|
-
prepared,
|
|
143
|
-
say,
|
|
144
|
-
);
|
|
88
|
+
outcome = await (deps.bootstrap ?? bootstrapBackend)(client.stigmer);
|
|
145
89
|
} catch (err) {
|
|
146
90
|
log.warn("bootstrap failed", { error: String(err) });
|
|
147
|
-
say(
|
|
148
|
-
"Warning: failed to bootstrap the local backend. Run 'stigmer up' again to retry.",
|
|
149
|
-
);
|
|
91
|
+
say("Warning: failed to bootstrap the local backend. Run 'stigmer up' again to retry.");
|
|
150
92
|
return;
|
|
151
93
|
}
|
|
152
|
-
log.debug("bootstrap complete", {
|
|
153
|
-
|
|
154
|
-
outcomes: result.plugins.outcomes,
|
|
155
|
-
});
|
|
156
|
-
for (const name of result.plugins.failed) {
|
|
157
|
-
say(
|
|
158
|
-
`Warning: failed to install default plugin '${name}'. Run 'stigmer bootstrap' to retry.`,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Phase 2 in the seam's shape, naming the verb: everything this file's
|
|
164
|
-
// local bootstrap installs was installed by `stigmer up`.
|
|
165
|
-
function runLocalBootstrap(
|
|
166
|
-
stigmer: Stigmer,
|
|
167
|
-
prepared: PreparedBootstrap,
|
|
168
|
-
say: Say,
|
|
169
|
-
): Promise<BootstrapResult> {
|
|
170
|
-
return runBootstrap(stigmer, prepared, say, "stigmer up");
|
|
94
|
+
log.debug("bootstrap complete", { org: outcome });
|
|
95
|
+
if (outcome === "created") say(`Created the '${DEFAULT_LOCAL_ORG}' organization`);
|
|
171
96
|
}
|
|
172
97
|
|
|
173
98
|
async function freshLocalClient(): Promise<BackendClient> {
|
|
@@ -83,11 +83,11 @@ export interface UpForegroundDeps {
|
|
|
83
83
|
runDaemon?: (deps: InternalDaemonDeps) => Promise<number>;
|
|
84
84
|
/** Resolves when the stack should shut down (default: the first SIGTERM/SIGINT). */
|
|
85
85
|
waitForShutdown?: () => Promise<void>;
|
|
86
|
-
/** Post-readiness bootstrap (default: `bootstrapLocalBackend
|
|
86
|
+
/** Post-readiness bootstrap (default: `bootstrapLocalBackend`, which ensures the `stigmer` org). */
|
|
87
87
|
bootstrap?: () => Promise<void>;
|
|
88
88
|
/** Receives each line the server and runner write (default: a `[component]`-prefixed stdout mirror). */
|
|
89
89
|
mirror?: OutputMirror;
|
|
90
|
-
/** Invoked once the stack is serving and
|
|
90
|
+
/** Invoked once the stack is serving and bootstrapped — the moment a detached `up` would have returned. */
|
|
91
91
|
onReady?: () => void | Promise<void>;
|
|
92
92
|
}
|
|
93
93
|
|