@stigmer/cli 3.23.1-dev.20260923075205 → 3.24.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/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
|
@@ -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
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Resolution and on-demand acquisition of the official marketplace.
|
|
2
2
|
//
|
|
3
3
|
// The official catalogue (`plugins/` in the OSS repository, published as
|
|
4
|
-
// `@stigmer/plugins`) is
|
|
5
|
-
// so it follows the server's and the runner's rule:
|
|
6
|
-
// version, so the plugins
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// `@stigmer/plugins`) is what `stigmer install <name>` reads when no other
|
|
5
|
+
// marketplace is named, so it follows the server's and the runner's rule:
|
|
6
|
+
// acquired at the CLI's OWN version, so the plugins it installs match the
|
|
7
|
+
// control plane that serves them. A lean `npx @stigmer/cli` does not carry the
|
|
8
|
+
// content; it is fetched once, the first time an install needs it, into
|
|
9
|
+
// ~/.stigmer/runtimes/<version>/ like the server and the runner.
|
|
9
10
|
//
|
|
10
11
|
// Two sources, tried in order (mirrors runtime/server.ts and runtime/runner.ts):
|
|
11
12
|
// 1. A repo-tree checkout (dev): `<repo>/plugins`, the tree itself.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Pins the CLI's edge for the marketplace reader: a tree read through the one
|
|
2
2
|
// walker, the refusal shape, entry lookup and directory resolution, and the
|
|
3
|
-
// official tree in this repository reading clean
|
|
3
|
+
// official tree in this repository reading clean.
|
|
4
4
|
|
|
5
5
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
@@ -81,14 +81,13 @@ describe("findEntry and entryDirectory", () => {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
describe("the official tree in this repository", () => {
|
|
84
|
-
it("reads clean through the CLI walker
|
|
84
|
+
it("reads clean through the CLI walker", () => {
|
|
85
85
|
const official = resolveOfficialMarketplace();
|
|
86
86
|
expect(official.source).toBe("repo");
|
|
87
87
|
const tree = readMarketplaceTree(official.dir);
|
|
88
88
|
expect(tree.marketplace.name).toBe("stigmer");
|
|
89
89
|
expect(tree.marketplace.dialect).toBe("stigmer");
|
|
90
90
|
expect(tree.warnings).toEqual([]);
|
|
91
|
-
expect(tree.marketplace.defaults).toEqual([]);
|
|
92
91
|
expect(findEntry(tree.marketplace, "linear")?.dir).toBe("linear");
|
|
93
92
|
});
|
|
94
93
|
});
|
|
@@ -114,8 +114,6 @@ export function renderMarketplaceShow(
|
|
|
114
114
|
if (tree.marketplace.owner?.name !== undefined)
|
|
115
115
|
about.field("Owner", tree.marketplace.owner.name);
|
|
116
116
|
about.field("Format", tree.marketplace.path);
|
|
117
|
-
if (tree.marketplace.defaults.length > 0)
|
|
118
|
-
about.field("Defaults", tree.marketplace.defaults.join(", "));
|
|
119
117
|
|
|
120
118
|
const plugins = result.addSection("Plugins");
|
|
121
119
|
for (const entry of entries) {
|
|
@@ -146,7 +144,6 @@ export function renderMarketplaceShow(
|
|
|
146
144
|
...(tree.marketplace.owner !== undefined && {
|
|
147
145
|
owner: tree.marketplace.owner,
|
|
148
146
|
}),
|
|
149
|
-
defaults: tree.marketplace.defaults,
|
|
150
147
|
},
|
|
151
148
|
plugins: entries,
|
|
152
149
|
warnings: tree.warnings,
|