@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.
Files changed (56) hide show
  1. package/commands/bootstrap.d.ts.map +1 -1
  2. package/commands/bootstrap.js +16 -57
  3. package/commands/bootstrap.js.map +1 -1
  4. package/commands/config/context.d.ts.map +1 -1
  5. package/commands/config/context.js +41 -5
  6. package/commands/config/context.js.map +1 -1
  7. package/commands/up.js +1 -1
  8. package/commands/up.js.map +1 -1
  9. package/config/index.d.ts +1 -1
  10. package/config/index.d.ts.map +1 -1
  11. package/config/index.js +1 -1
  12. package/config/index.js.map +1 -1
  13. package/config/resolve.d.ts +8 -0
  14. package/config/resolve.d.ts.map +1 -1
  15. package/config/resolve.js +8 -8
  16. package/config/resolve.js.map +1 -1
  17. package/local/bootstrap.d.ts +9 -29
  18. package/local/bootstrap.d.ts.map +1 -1
  19. package/local/bootstrap.js +61 -69
  20. package/local/bootstrap.js.map +1 -1
  21. package/local/daemon/launch.d.ts +2 -2
  22. package/local/daemon/launch.d.ts.map +1 -1
  23. package/marketplace/official.d.ts.map +1 -1
  24. package/marketplace/official.js +6 -5
  25. package/marketplace/official.js.map +1 -1
  26. package/marketplace/render.d.ts.map +1 -1
  27. package/marketplace/render.js +0 -3
  28. package/marketplace/render.js.map +1 -1
  29. package/package.json +7 -7
  30. package/src/commands/bootstrap.test.ts +19 -75
  31. package/src/commands/bootstrap.ts +19 -68
  32. package/src/commands/config/context.test.ts +171 -0
  33. package/src/commands/config/context.ts +50 -5
  34. package/src/commands/marketplace.test.ts +2 -2
  35. package/src/commands/up.ts +1 -1
  36. package/src/config/index.ts +1 -0
  37. package/src/config/resolve.test.ts +2 -2
  38. package/src/config/resolve.ts +8 -8
  39. package/src/local/bootstrap.test.ts +132 -65
  40. package/src/local/bootstrap.ts +64 -139
  41. package/src/local/daemon/launch.ts +2 -2
  42. package/src/marketplace/official.ts +6 -5
  43. package/src/marketplace/read.test.ts +2 -3
  44. package/src/marketplace/render.ts +0 -3
  45. package/local/plugins/defaults.d.ts +0 -55
  46. package/local/plugins/defaults.d.ts.map +0 -1
  47. package/local/plugins/defaults.js +0 -111
  48. package/local/plugins/defaults.js.map +0 -1
  49. package/local/system-org.d.ts +0 -15
  50. package/local/system-org.d.ts.map +0 -1
  51. package/local/system-org.js +0 -57
  52. package/local/system-org.js.map +0 -1
  53. package/src/local/plugins/defaults.test.ts +0 -271
  54. package/src/local/plugins/defaults.ts +0 -179
  55. package/src/local/system-org.test.ts +0 -124
  56. package/src/local/system-org.ts +0 -63
@@ -1,179 +0,0 @@
1
- // The default plugins a backend is bootstrapped with: the `defaults` list of
2
- // the official marketplace, installed in order into the system org as
3
- // public. The official list is empty today (a session with no agent runs
4
- // the built-in assistant, so a fresh install needs no default content);
5
- // the mechanism stays until the bootstrap is re-scoped.
6
- //
7
- // Two phases, deliberately separate. Preparing (resolve the official tree,
8
- // which may acquire `@stigmer/plugins` at the CLI's version; walk, zip and
9
- // hash every default) needs no backend and is all-or-nothing: the catalogue
10
- // is ours and validated on CI, so a default that cannot be prepared is a
11
- // release defect, not a condition to install around. Installing pushes the
12
- // prepared archives and is best-effort per entry: one default failing to
13
- // land does not stop the next, and the caller decides how to surface the
14
- // failures. The split keeps every network read (the catalogue, the
15
- // archives) ahead of the first write to the backend (local/bootstrap.ts).
16
- //
17
- // Idempotent by the server's own identity, not a marker file: before each
18
- // push the org's plugin of that name is read back; when its digest equals the
19
- // prepared archive's, it is READY and it is public, there is nothing to do
20
- // and nothing is sent. A second `stigmer up` therefore pushes nothing, and a
21
- // `stigmer up` after an upgrade pushes exactly the entries whose content
22
- // changed. The record is the server, so any machine that reaches the same
23
- // backend sees the same answer.
24
-
25
- import { type Stigmer, isNotFound } from "@stigmer/sdk";
26
- import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
27
- import { PluginState } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/status_pb";
28
- import { CliExitError } from "../../errors/cli-exit-error.js";
29
- import { ExitCode } from "../../errors/exit-codes.js";
30
- import { log } from "../../logger.js";
31
- import { VERSION } from "../../version.js";
32
- import {
33
- type ResolveOfficialOptions,
34
- resolveOfficialMarketplace,
35
- } from "../../marketplace/official.js";
36
- import { readMarketplaceTree } from "../../marketplace/read.js";
37
- import { prepareEntry } from "../../marketplace/install.js";
38
- import {
39
- type PreparedPluginPush,
40
- count,
41
- pushPrepared,
42
- } from "../../resources/plugin.js";
43
-
44
- /**
45
- * The command that ran the install, as the plugin's stored version message
46
- * names it. Two verbs share the one bootstrap function, and a platform
47
- * operator reading the row a year later must see which one wrote it.
48
- */
49
- export type InstallingVerb = "stigmer up" | "stigmer bootstrap";
50
-
51
- export interface DefaultPluginsDeps {
52
- /** The client bound to the backend being bootstrapped. */
53
- readonly stigmer: Stigmer;
54
- /** Human progress lines (stderr). */
55
- readonly info: (line: string) => void;
56
- /** Stamped into each installed plugin's version message. */
57
- readonly verb: InstallingVerb;
58
- }
59
-
60
- /** One default, ready to push: its marketplace name and the prepared archive. */
61
- export interface PreparedDefault {
62
- readonly name: string;
63
- readonly push: PreparedPluginPush;
64
- }
65
-
66
- export type DefaultPluginOutcome =
67
- | {
68
- readonly name: string;
69
- readonly action: "installed";
70
- readonly digest: string;
71
- }
72
- | {
73
- readonly name: string;
74
- readonly action: "up-to-date";
75
- readonly digest: string;
76
- }
77
- | {
78
- readonly name: string;
79
- readonly action: "failed";
80
- readonly error: string;
81
- };
82
-
83
- export interface DefaultPluginsResult {
84
- readonly outcomes: readonly DefaultPluginOutcome[];
85
- /** The names that did not land, for the caller's warning. */
86
- readonly failed: readonly string[];
87
- }
88
-
89
- /**
90
- * Phase 1: every default the official marketplace names, prepared in the
91
- * marketplace's order. Throws when the tree cannot be resolved, offers less
92
- * than it lists (the library drops an entry it cannot install with a
93
- * warning, which for a user's marketplace is `show`'s business and for ours
94
- * is a broken release), or any default cannot be prepared; nothing is
95
- * returned half-ready.
96
- */
97
- export async function prepareDefaultPlugins(
98
- official?: ResolveOfficialOptions,
99
- ): Promise<readonly PreparedDefault[]> {
100
- const resolved = resolveOfficialMarketplace(official);
101
- const tree = readMarketplaceTree(resolved.dir, "the official marketplace");
102
- if (tree.warnings.length > 0) {
103
- throw new CliExitError(
104
- `the official marketplace at ${resolved.dir} lists ${count(tree.warnings.length, "plugin")} this CLI cannot install`,
105
- ExitCode.General,
106
- tree.warnings.map((finding) => finding.message),
107
- );
108
- }
109
- const prepared: PreparedDefault[] = [];
110
- for (const name of tree.marketplace.defaults) {
111
- prepared.push({ name, push: await prepareEntry(tree, name) });
112
- }
113
- return prepared;
114
- }
115
-
116
- /**
117
- * Phase 2: install the prepared defaults into `org`, skipping what the
118
- * backend already holds. Best-effort per entry.
119
- */
120
- export async function installPreparedDefaults(
121
- deps: DefaultPluginsDeps,
122
- prepared: readonly PreparedDefault[],
123
- org: string,
124
- ): Promise<DefaultPluginsResult> {
125
- const outcomes: DefaultPluginOutcome[] = [];
126
- for (const { name, push } of prepared) {
127
- try {
128
- if (await isAlreadyInstalled(deps.stigmer, org, push)) {
129
- outcomes.push({ name, action: "up-to-date", digest: push.digest });
130
- continue;
131
- }
132
- deps.info(`Installing default plugin '${name}'…`);
133
- await pushPrepared(deps.stigmer, push, {
134
- org,
135
- visibility: ApiResourceVisibility.visibility_public,
136
- message: `default plugin, installed by ${deps.verb} (${VERSION})`,
137
- });
138
- outcomes.push({ name, action: "installed", digest: push.digest });
139
- } catch (error) {
140
- const message = error instanceof Error ? error.message : String(error);
141
- log.warn("default plugin install failed", { name, error: message });
142
- outcomes.push({ name, action: "failed", error: message });
143
- }
144
- }
145
- return {
146
- outcomes,
147
- failed: outcomes
148
- .filter((outcome) => outcome.action === "failed")
149
- .map((outcome) => outcome.name),
150
- };
151
- }
152
-
153
- /**
154
- * True when the org already holds this exact archive, materialised and
155
- * public. A plugin that is absent (NOT_FOUND) installs; one that is present
156
- * with another digest, still installing, failed, or not public is pushed
157
- * again, which the server treats as an upgrade or a convergence.
158
- */
159
- async function isAlreadyInstalled(
160
- stigmer: Stigmer,
161
- org: string,
162
- prepared: PreparedPluginPush,
163
- ): Promise<boolean> {
164
- let existing;
165
- try {
166
- existing = await stigmer.plugin.getByReference({
167
- org,
168
- slug: prepared.plugin.name,
169
- });
170
- } catch (error) {
171
- if (isNotFound(error)) return false;
172
- throw error;
173
- }
174
- return (
175
- existing.status?.digest === prepared.digest &&
176
- existing.status.state === PluginState.READY &&
177
- existing.metadata?.visibility === ApiResourceVisibility.visibility_public
178
- );
179
- }
@@ -1,124 +0,0 @@
1
- // Pins the system org's "ensure" over a real Connect backend: an org the
2
- // identity already sees is `present` with no create; an absent one is
3
- // created as the Organization as code (slug, name, label, self-managed) and
4
- // reported `created`; a create that loses the race to another launcher
5
- // (ALREADY_EXISTS) is `present`, not an error; any other refusal propagates
6
- // with the server's sentence.
7
-
8
- import {
9
- createServer as createHttp2Server,
10
- type Http2Server,
11
- type ServerHttp2Session,
12
- } from "node:http2";
13
- import type { AddressInfo } from "node:net";
14
- import { create } from "@bufbuild/protobuf";
15
- import { Code, ConnectError, type ConnectRouter } from "@connectrpc/connect";
16
- import { connectNodeAdapter } from "@connectrpc/connect-node";
17
- import {
18
- type Organization,
19
- OrganizationSchema,
20
- } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
21
- import { OrganizationCommandController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/command_pb";
22
- import { ManagementMode } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/enum_pb";
23
- import { OrganizationsSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/io_pb";
24
- import { OrganizationQueryController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/query_pb";
25
- import { createNodeClient, normalizeEndpoint } from "@stigmer/sdk/node";
26
- import type { Stigmer } from "@stigmer/sdk";
27
- import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
28
- import { SYSTEM_ORG, SYSTEM_ORG_LABEL, ensureSystemOrg } from "./system-org.js";
29
-
30
- let backend: Http2Server;
31
- let stigmer: Stigmer;
32
- const openSessions = new Set<ServerHttp2Session>();
33
-
34
- /** The orgs the backend holds, by slug. */
35
- let held: Map<string, Organization>;
36
- let creates: Organization[];
37
- /** What `create` answers with instead of creating, when set. */
38
- let refuseCreate: ConnectError | undefined;
39
-
40
- beforeAll(async () => {
41
- const routes = (router: ConnectRouter) => {
42
- router.service(OrganizationQueryController, {
43
- findMyOrganizations: () =>
44
- create(OrganizationsSchema, { entries: [...held.values()] }),
45
- });
46
- router.service(OrganizationCommandController, {
47
- create: (org) => {
48
- creates.push(org);
49
- if (refuseCreate !== undefined) throw refuseCreate;
50
- const slug = org.metadata?.slug ?? "";
51
- const row = create(OrganizationSchema, org);
52
- if (row.metadata !== undefined) row.metadata.id = `org_${slug}`;
53
- held.set(slug, row);
54
- return row;
55
- },
56
- });
57
- };
58
- backend = createHttp2Server(connectNodeAdapter({ routes }));
59
- backend.on("session", (session) => {
60
- openSessions.add(session);
61
- session.on("close", () => openSessions.delete(session));
62
- });
63
- await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
64
- const port = (backend.address() as AddressInfo).port;
65
- stigmer = createNodeClient({
66
- baseUrl: normalizeEndpoint(`127.0.0.1:${port}`),
67
- });
68
- });
69
-
70
- afterAll(async () => {
71
- for (const session of openSessions) session.destroy();
72
- await new Promise<void>((resolve) => backend.close(() => resolve()));
73
- });
74
-
75
- beforeEach(() => {
76
- held = new Map();
77
- creates = [];
78
- refuseCreate = undefined;
79
- });
80
-
81
- describe("ensureSystemOrg", () => {
82
- it("is present, with no create, when the identity already sees it", async () => {
83
- held.set(
84
- SYSTEM_ORG,
85
- create(OrganizationSchema, { metadata: { slug: SYSTEM_ORG } }),
86
- );
87
- expect(await ensureSystemOrg(stigmer)).toBe("present");
88
- expect(creates).toHaveLength(0);
89
- });
90
-
91
- it("creates the Organization as code when absent", async () => {
92
- held.set(
93
- "acme",
94
- create(OrganizationSchema, { metadata: { slug: "acme" } }),
95
- );
96
- expect(await ensureSystemOrg(stigmer)).toBe("created");
97
- expect(creates).toHaveLength(1);
98
- const sent = creates[0]!;
99
- expect(sent.metadata?.slug).toBe(SYSTEM_ORG);
100
- expect(sent.metadata?.org).toBe(SYSTEM_ORG);
101
- expect(sent.metadata?.name).toBe("Stigmer");
102
- expect(sent.metadata?.labels).toEqual({ [SYSTEM_ORG_LABEL]: "true" });
103
- expect(sent.spec?.managementMode).toBe(ManagementMode.self_managed);
104
- expect(sent.spec?.description).not.toBe("");
105
- });
106
-
107
- it("treats a create lost to another launcher as present", async () => {
108
- refuseCreate = new ConnectError(
109
- `organization '${SYSTEM_ORG}' already exists`,
110
- Code.AlreadyExists,
111
- );
112
- expect(await ensureSystemOrg(stigmer)).toBe("present");
113
- });
114
-
115
- it("propagates any other refusal with the server's sentence", async () => {
116
- refuseCreate = new ConnectError(
117
- "reserved labels are platform-managed",
118
- Code.PermissionDenied,
119
- );
120
- await expect(ensureSystemOrg(stigmer)).rejects.toThrow(
121
- /reserved labels are platform-managed/,
122
- );
123
- });
124
- });
@@ -1,63 +0,0 @@
1
- // The system organization, as code: the one `stigmer` org every Stigmer
2
- // backend carries, which the official default plugins are installed into
3
- // and the local CLI falls back to when no org is configured.
4
- //
5
- // This is the only place the org is defined. The bootstrap creates it when
6
- // a backend does not have it (a fresh local data dir, a raw self-hosted
7
- // server); on the hosted platform it already exists, owned by IAM, and the
8
- // create never fires. Its slug is the same in both editions, so nothing
9
- // here is configurable: a bootstrap that could be pointed at another org
10
- // would put the platform's public defaults where no cross-org lookup finds
11
- // them.
12
- //
13
- // "Ensure" is find-then-create with the race closed by the server: two
14
- // launchers racing the same check see one `already-exists`, and that IS the
15
- // desired end state, so it is reported as `present`, never rethrown. Any
16
- // other refusal (a self-hosted authorizer that denies the reserved label,
17
- // a backend that refuses the create) propagates with the server's sentence;
18
- // the caller decides how loud to be.
19
-
20
- import { ManagementMode } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/enum_pb";
21
- import { type Stigmer, StigmerError } from "@stigmer/sdk";
22
-
23
- /** The system organization's slug, identical across editions. */
24
- export const SYSTEM_ORG = "stigmer";
25
-
26
- /**
27
- * Marks the system org so an operator can tell it from a user's own org in
28
- * any listing. Informational: no server or client code path reads it.
29
- */
30
- export const SYSTEM_ORG_LABEL = "stigmer.ai/system";
31
-
32
- export type EnsureSystemOrgOutcome = "present" | "created";
33
-
34
- /**
35
- * Make sure the system organization exists on the backend `stigmer` is bound
36
- * to. Idempotent; a lost create race counts as `present`.
37
- */
38
- export async function ensureSystemOrg(
39
- stigmer: Stigmer,
40
- ): Promise<EnsureSystemOrgOutcome> {
41
- const mine = await stigmer.organization.findMyOrganizations();
42
- if (mine.entries.some((org) => org.metadata?.slug === SYSTEM_ORG)) {
43
- return "present";
44
- }
45
- try {
46
- // Organizations are self-owning: the org field is the slug itself, the
47
- // shape the console's create form and the e2e harness both use.
48
- await stigmer.organization.create({
49
- name: "Stigmer",
50
- slug: SYSTEM_ORG,
51
- org: SYSTEM_ORG,
52
- description: "System organization for Stigmer installations",
53
- labels: { [SYSTEM_ORG_LABEL]: "true" },
54
- managementMode: ManagementMode.self_managed,
55
- });
56
- return "created";
57
- } catch (error) {
58
- if (error instanceof StigmerError && error.code === "already-exists") {
59
- return "present";
60
- }
61
- throw error;
62
- }
63
- }