@stigmer/cli 3.18.0-dev.20260918103812 → 3.18.1-dev.20260919070736

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 (109) hide show
  1. package/commands/bootstrap.d.ts +3 -0
  2. package/commands/bootstrap.d.ts.map +1 -0
  3. package/commands/bootstrap.js +75 -0
  4. package/commands/bootstrap.js.map +1 -0
  5. package/commands/marketplace.d.ts.map +1 -1
  6. package/commands/marketplace.js +7 -7
  7. package/commands/marketplace.js.map +1 -1
  8. package/config/resolve.d.ts.map +1 -1
  9. package/config/resolve.js +8 -7
  10. package/config/resolve.js.map +1 -1
  11. package/local/bootstrap.d.ts +32 -13
  12. package/local/bootstrap.d.ts.map +1 -1
  13. package/local/bootstrap.js +105 -64
  14. package/local/bootstrap.js.map +1 -1
  15. package/local/daemon/launch.d.ts +1 -1
  16. package/local/daemon/launch.d.ts.map +1 -1
  17. package/local/paths.d.ts +1 -1
  18. package/local/paths.js +1 -1
  19. package/local/plugins/defaults.d.ts +27 -7
  20. package/local/plugins/defaults.d.ts.map +1 -1
  21. package/local/plugins/defaults.js +55 -28
  22. package/local/plugins/defaults.js.map +1 -1
  23. package/local/runtime/runtimes-install.d.ts.map +1 -1
  24. package/local/runtime/runtimes-install.js +7 -4
  25. package/local/runtime/runtimes-install.js.map +1 -1
  26. package/local/seedpack-retire.d.ts +63 -0
  27. package/local/seedpack-retire.d.ts.map +1 -0
  28. package/local/seedpack-retire.js +361 -0
  29. package/local/seedpack-retire.js.map +1 -0
  30. package/local/system-org.d.ts +15 -0
  31. package/local/system-org.d.ts.map +1 -0
  32. package/local/system-org.js +57 -0
  33. package/local/system-org.js.map +1 -0
  34. package/marketplace/config.d.ts +3 -1
  35. package/marketplace/config.d.ts.map +1 -1
  36. package/marketplace/config.js +24 -11
  37. package/marketplace/config.js.map +1 -1
  38. package/marketplace/github.d.ts +37 -0
  39. package/marketplace/github.d.ts.map +1 -1
  40. package/marketplace/github.js +70 -1
  41. package/marketplace/github.js.map +1 -1
  42. package/marketplace/install.d.ts +13 -6
  43. package/marketplace/install.d.ts.map +1 -1
  44. package/marketplace/install.js +63 -16
  45. package/marketplace/install.js.map +1 -1
  46. package/marketplace/official.js +4 -4
  47. package/marketplace/official.js.map +1 -1
  48. package/marketplace/render.d.ts.map +1 -1
  49. package/marketplace/render.js +10 -4
  50. package/marketplace/render.js.map +1 -1
  51. package/package.json +7 -7
  52. package/program.js +2 -2
  53. package/program.js.map +1 -1
  54. package/resources/apply/declarative.js +2 -2
  55. package/resources/apply/declarative.js.map +1 -1
  56. package/resources/plugin.d.ts.map +1 -1
  57. package/resources/plugin.js +12 -9
  58. package/resources/plugin.js.map +1 -1
  59. package/src/commands/bootstrap.test.ts +253 -0
  60. package/src/commands/bootstrap.ts +91 -0
  61. package/src/commands/install.test.ts +28 -9
  62. package/src/commands/marketplace.test.ts +28 -14
  63. package/src/commands/marketplace.ts +9 -7
  64. package/src/config/resolve.test.ts +2 -2
  65. package/src/config/resolve.ts +8 -7
  66. package/src/local/bootstrap.test.ts +110 -38
  67. package/src/local/bootstrap.ts +179 -89
  68. package/src/local/daemon/launch.ts +1 -1
  69. package/src/local/paths.ts +1 -1
  70. package/src/local/plugins/defaults.test.ts +46 -13
  71. package/src/local/plugins/defaults.ts +77 -34
  72. package/src/local/runtime/runtimes-install.test.ts +2 -0
  73. package/src/local/runtime/runtimes-install.ts +7 -4
  74. package/src/local/seedpack-retire.test.ts +499 -0
  75. package/src/local/seedpack-retire.ts +474 -0
  76. package/src/local/system-org.test.ts +124 -0
  77. package/src/local/system-org.ts +63 -0
  78. package/src/marketplace/__fixtures__/cursor-marketplace.ts +5 -2
  79. package/src/marketplace/config.test.ts +47 -22
  80. package/src/marketplace/config.ts +27 -12
  81. package/src/marketplace/github.test.ts +77 -1
  82. package/src/marketplace/github.ts +115 -1
  83. package/src/marketplace/install.test.ts +84 -17
  84. package/src/marketplace/install.ts +73 -15
  85. package/src/marketplace/official.ts +4 -4
  86. package/src/marketplace/read.test.ts +1 -1
  87. package/src/marketplace/render.ts +13 -4
  88. package/src/program.ts +2 -2
  89. package/src/resources/apply/declarative.integration.test.ts +2 -2
  90. package/src/resources/apply/declarative.ts +2 -2
  91. package/src/resources/connect/discover.test.ts +1 -1
  92. package/src/resources/plugin.ts +11 -10
  93. package/commands/seedpack.d.ts +0 -3
  94. package/commands/seedpack.d.ts.map +0 -1
  95. package/commands/seedpack.js +0 -98
  96. package/commands/seedpack.js.map +0 -1
  97. package/local/seedpack/apply.d.ts +0 -72
  98. package/local/seedpack/apply.d.ts.map +0 -1
  99. package/local/seedpack/apply.js +0 -145
  100. package/local/seedpack/apply.js.map +0 -1
  101. package/local/seedpack/content.d.ts +0 -61
  102. package/local/seedpack/content.d.ts.map +0 -1
  103. package/local/seedpack/content.js +0 -178
  104. package/local/seedpack/content.js.map +0 -1
  105. package/src/commands/seedpack.ts +0 -116
  106. package/src/local/seedpack/apply.test.ts +0 -287
  107. package/src/local/seedpack/apply.ts +0 -199
  108. package/src/local/seedpack/content.test.ts +0 -169
  109. package/src/local/seedpack/content.ts +0 -222
@@ -1,133 +1,223 @@
1
- // What `stigmer up` does once the local server answers: seed it. Two steps,
2
- // in this order, each best-effort with its own warning, because the stack is
3
- // already serving by the time either runs and a seeding failure must never
4
- // tear it down:
1
+ // What makes a backend a ready Stigmer: the system org exists and the
2
+ // official marketplace's default plugins are installed into it, public.
3
+ // `stigmer up` runs this against the local server it just started;
4
+ // `stigmer bootstrap` runs the same two phases against whatever backend the
5
+ // CLI is pointed at, for a raw self-hosted server or the platform's daily
6
+ // lane. One definition, so a laptop, a container and the hosted platform
7
+ // cannot drift in what "ready" means.
5
8
  //
6
- // 1. the seedpack (the system org, the `stigmer` MCP server, the remaining
7
- // system agents, skills and workflows), content-hash idempotent;
8
- // 2. the default plugins the official marketplace names, installed into the
9
- // same org as public, idempotent by the server's own digest.
9
+ // Two phases, and the order matters (a third, `up`'s retire, follows them):
10
10
  //
11
- // The seedpack goes first because the plugins install INTO the org it
12
- // creates and reference the server it holds. When the seedpack retires, this
13
- // file keeps step 2 and loses step 1, and nothing else moves.
11
+ // 1. prepare: resolve the official marketplace (acquiring `@stigmer/plugins`
12
+ // at the CLI's version on a release's first run) and prepare every
13
+ // default. No backend is touched.
14
+ // 2. run: ensure the system org, then push the prepared defaults, each
15
+ // skipped when the backend already holds it at the same digest.
14
16
  //
15
- // Both `up` shapes (detached and foreground) call this one function, so what
16
- // a detached `up` seeds and what a container's `up --foreground` seeds cannot
17
- // drift. Both always seed the local server `up` just started, never the
18
- // active backend context: a user whose CLI is pointed at cloud must still get
19
- // their local stack seeded, and must never have system resources applied to
20
- // their cloud org. The client is pinned to a fresh local config
17
+ // `stigmer up` adds a third phase after them: retiring the seedpack an
18
+ // older release installed. That step deletes rows, so it runs only once
19
+ // every default has LANDED, not merely been prepared: the plugin push adopts
20
+ // the system-content rows it replaces in place (the seedpack's `assistant`
21
+ // keeps its id, its instances and every conversation bound to them), and a
22
+ // row that is now a plugin's member is never a candidate for the retire. A
23
+ // user who upgrades offline and cannot fetch the new catalogue, or whose
24
+ // backend refused a default, keeps the old rows for another day instead of
25
+ // losing them and getting nothing back. The retire lives here and not in
26
+ // `stigmer bootstrap` on purpose: a verb a scheduled lane runs against the
27
+ // platform must never be able to delete rows.
28
+ //
29
+ // Inside `up` every phase is best-effort with its own warning naming the
30
+ // retry, because the stack is already serving by the time any of them runs
31
+ // and a seeding failure must never tear it down. Both `up` shapes (detached
32
+ // and foreground) call this one function, and both seed the local server
33
+ // `up` just started, never the active backend context: a CLI pointed at
34
+ // cloud still gets its local stack seeded, and never has system resources
35
+ // applied to its cloud org. The client is pinned to a fresh local config
21
36
  // (localhost:SERVER_PORT) with no auth, the trusted-local identity.
22
37
 
38
+ import type { Stigmer } from "@stigmer/sdk";
23
39
  import type { BackendClient } from "../client/index.js";
24
40
  import { log } from "../logger.js";
41
+ import type { ResolveOfficialOptions } from "../marketplace/official.js";
42
+ import type {
43
+ DefaultPluginsResult,
44
+ InstallingVerb,
45
+ PreparedDefault,
46
+ } from "./plugins/defaults.js";
25
47
  import { dataDir } from "./paths.js";
48
+ import { type EnsureSystemOrgOutcome, SYSTEM_ORG } from "./system-org.js";
26
49
 
27
- /** One seeding step: talks to the user through `say`, never throws past its own warning. */
28
- export type BootstrapStep = (
29
- client: BackendClient,
30
- home: string,
50
+ export type Say = (line: string) => void;
51
+
52
+ /** Everything phase 2 needs, gathered without a backend. */
53
+ export interface PreparedBootstrap {
54
+ readonly defaults: readonly PreparedDefault[];
55
+ }
56
+
57
+ export interface BootstrapResult {
58
+ readonly org: EnsureSystemOrgOutcome;
59
+ readonly plugins: DefaultPluginsResult;
60
+ }
61
+
62
+ export interface PrepareBootstrapOptions {
63
+ /** How the official tree is found (injectable for tests). */
64
+ readonly official?: ResolveOfficialOptions;
65
+ }
66
+
67
+ /** Phase 1. Throws when the catalogue cannot be resolved or a default cannot be prepared. */
68
+ export async function prepareBootstrap(
69
+ options: PrepareBootstrapOptions = {},
70
+ ): Promise<PreparedBootstrap> {
71
+ const { prepareDefaultPlugins } = await import("./plugins/defaults.js");
72
+ return { defaults: await prepareDefaultPlugins(options.official) };
73
+ }
74
+
75
+ /**
76
+ * Phase 2. Throws when the system org cannot be ensured; a default that
77
+ * fails to land is reported in the result, not thrown, so the caller can
78
+ * name each one. `verb` is the command running this, stamped into each
79
+ * installed plugin's version message.
80
+ */
81
+ export async function runBootstrap(
82
+ stigmer: Stigmer,
83
+ prepared: PreparedBootstrap,
31
84
  say: Say,
32
- ) => Promise<void>;
85
+ verb: InstallingVerb,
86
+ ): Promise<BootstrapResult> {
87
+ const [{ ensureSystemOrg }, { installPreparedDefaults }] = await Promise.all(
88
+ [import("./system-org.js"), import("./plugins/defaults.js")],
89
+ );
90
+ const org = await ensureSystemOrg(stigmer);
91
+ if (org === "created") say(`Created the '${SYSTEM_ORG}' organization`);
92
+ const plugins = await installPreparedDefaults(
93
+ { stigmer, info: say, verb },
94
+ prepared.defaults,
95
+ SYSTEM_ORG,
96
+ );
97
+ return { org, plugins };
98
+ }
33
99
 
34
- export type Say = (line: string) => void;
100
+ /** Both phases, against the backend `stigmer` is bound to: what `stigmer bootstrap` runs. */
101
+ export async function bootstrapBackend(
102
+ stigmer: Stigmer,
103
+ say: Say,
104
+ options: PrepareBootstrapOptions = {},
105
+ ): Promise<BootstrapResult> {
106
+ return runBootstrap(
107
+ stigmer,
108
+ await prepareBootstrap(options),
109
+ say,
110
+ "stigmer bootstrap",
111
+ );
112
+ }
35
113
 
36
- /** Seams of the bootstrap, injectable for tests; the defaults are the real steps over the real local client. */
114
+ /** Seams of the local bootstrap, injectable for tests; the defaults are the real phases over the real local client. */
37
115
  export interface BootstrapDeps {
38
116
  readonly client?: BackendClient;
39
- readonly seedpack?: BootstrapStep;
40
- readonly plugins?: BootstrapStep;
41
117
  readonly say?: Say;
118
+ readonly prepare?: () => Promise<PreparedBootstrap>;
119
+ readonly retire?: (stigmer: Stigmer, home: string, say: Say) => Promise<void>;
120
+ readonly run?: (
121
+ stigmer: Stigmer,
122
+ prepared: PreparedBootstrap,
123
+ say: Say,
124
+ ) => Promise<BootstrapResult>;
42
125
  }
43
126
 
44
127
  const sayToStderr: Say = (line) => {
45
128
  process.stderr.write(`${line}\n`);
46
129
  };
47
130
 
131
+ /** What `stigmer up` does once the local server answers. Never throws past its own warnings. */
48
132
  export async function bootstrapLocalBackend(
49
133
  home: string,
50
134
  deps: BootstrapDeps = {},
51
135
  ): Promise<void> {
52
136
  const client = deps.client ?? (await freshLocalClient());
53
137
  const say = deps.say ?? sayToStderr;
54
- await (deps.seedpack ?? applySeedpackBestEffort)(client, home, say);
55
- await (deps.plugins ?? installDefaultPluginsBestEffort)(client, home, say);
56
- }
57
138
 
58
- async function freshLocalClient(): Promise<BackendClient> {
59
- const [{ createBackendClient }, { getDefault }] = await Promise.all([
60
- import("../client/index.js"),
61
- import("../config/config.js"),
62
- ]);
63
- return createBackendClient({
64
- config: getDefault(),
65
- getAccessToken: () => null,
66
- });
67
- }
68
-
69
- /** Step 1. Idempotent via a content-hash marker, so repeated `up`s are cheap. */
70
- export async function applySeedpackBestEffort(
71
- client: BackendClient,
72
- home: string,
73
- say: Say,
74
- ): Promise<void> {
139
+ let prepared: PreparedBootstrap;
75
140
  try {
76
- const { applySeedpack } = await import("./seedpack/apply.js");
77
- const result = await applySeedpack(
78
- {
79
- controller: client.controller,
80
- stigmer: client.stigmer,
81
- info: say,
82
- warn: say,
83
- },
84
- { markerDir: dataDir(home), home },
85
- );
86
- log.debug("seedpack bootstrap complete", {
87
- applied: result.applied,
88
- hash: result.hash,
89
- org: result.org,
90
- });
141
+ prepared = await (deps.prepare ?? prepareBootstrap)();
91
142
  } catch (err) {
92
- log.warn("seedpack bootstrap failed", { error: String(err) });
143
+ log.warn("bootstrap preparation failed", { error: String(err) });
93
144
  say(
94
- "Warning: failed to apply system resources (seedpack). Run 'stigmer seedpack apply' to retry.",
145
+ "Warning: could not prepare the default plugins, so the local backend was not bootstrapped and nothing was retired. Run 'stigmer up' again to retry.",
95
146
  );
147
+ return;
96
148
  }
97
- }
98
149
 
99
- /**
100
- * Step 2. Runs even when step 1 warned: the plugins can only fail on their
101
- * own terms (an org that does not exist fails the push, and the warning
102
- * names the command that retries it).
103
- */
104
- export async function installDefaultPluginsBestEffort(
105
- client: BackendClient,
106
- _home: string,
107
- say: Say,
108
- ): Promise<void> {
150
+ let result: BootstrapResult;
109
151
  try {
110
- const [{ installDefaultPlugins }, { resolveSeedpackOrg }] =
111
- await Promise.all([
112
- import("./plugins/defaults.js"),
113
- import("./seedpack/apply.js"),
114
- ]);
115
- const result = await installDefaultPlugins(
116
- { stigmer: client.stigmer, info: say },
117
- { org: resolveSeedpackOrg() },
152
+ result = await (deps.run ?? runLocalBootstrap)(
153
+ client.stigmer,
154
+ prepared,
155
+ say,
118
156
  );
119
- log.debug("default plugins bootstrap complete", {
120
- outcomes: result.outcomes,
121
- });
122
- for (const name of result.failed) {
157
+ } catch (err) {
158
+ log.warn("bootstrap failed", { error: String(err) });
159
+ say(
160
+ "Warning: failed to bootstrap the local backend, so nothing was retired. Run 'stigmer up' again to retry.",
161
+ );
162
+ return;
163
+ }
164
+ log.debug("bootstrap complete", {
165
+ org: result.org,
166
+ outcomes: result.plugins.outcomes,
167
+ });
168
+ if (result.plugins.failed.length > 0) {
169
+ for (const name of result.plugins.failed) {
123
170
  say(
124
- `Warning: failed to install default plugin '${name}'. Run 'stigmer install ${name}' to retry.`,
171
+ `Warning: failed to install default plugin '${name}'. Run 'stigmer bootstrap' to retry.`,
125
172
  );
126
173
  }
174
+ say(
175
+ "Nothing was retired: the resources an older release installed stay until every default plugin is in place.",
176
+ );
177
+ return;
178
+ }
179
+
180
+ try {
181
+ await (deps.retire ?? retireSeedpack)(client.stigmer, home, say);
127
182
  } catch (err) {
128
- log.warn("default plugins bootstrap failed", { error: String(err) });
183
+ log.warn("seedpack retire failed", { error: String(err) });
129
184
  say(
130
- "Warning: failed to install the default plugins. Run 'stigmer marketplace show stigmer' to see them and 'stigmer install <name>' to retry.",
185
+ "Warning: failed to retire the resources an older release installed. Run 'stigmer up' again to retry.",
131
186
  );
132
187
  }
133
188
  }
189
+
190
+ // Phase 2 in the seam's shape, naming the verb: everything this file's
191
+ // local bootstrap installs was installed by `stigmer up`.
192
+ function runLocalBootstrap(
193
+ stigmer: Stigmer,
194
+ prepared: PreparedBootstrap,
195
+ say: Say,
196
+ ): Promise<BootstrapResult> {
197
+ return runBootstrap(stigmer, prepared, say, "stigmer up");
198
+ }
199
+
200
+ // The retire step in the shape the seam expects; the module is loaded only
201
+ // inside `up`, the one place a retire may run.
202
+ async function retireSeedpack(
203
+ stigmer: Stigmer,
204
+ home: string,
205
+ say: Say,
206
+ ): Promise<void> {
207
+ const { retireSeedpack: retire, renderRetireReport } = await import(
208
+ "./seedpack-retire.js"
209
+ );
210
+ const result = await retire(stigmer, { markerDir: dataDir(home) });
211
+ renderRetireReport(result, say);
212
+ }
213
+
214
+ async function freshLocalClient(): Promise<BackendClient> {
215
+ const [{ createBackendClient }, { getDefault }] = await Promise.all([
216
+ import("../client/index.js"),
217
+ import("../config/config.js"),
218
+ ]);
219
+ return createBackendClient({
220
+ config: getDefault(),
221
+ getAccessToken: () => null,
222
+ });
223
+ }
@@ -83,7 +83,7 @@ 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`, the seedpack then the default plugins). */
86
+ /** Post-readiness bootstrap (default: `bootstrapLocalBackend`: prepare the defaults, retire an older release's seedpack, ensure the org, install). */
87
87
  bootstrap?: (home: string) => Promise<void>;
88
88
  /** Receives each line the server and runner write (default: a `[component]`-prefixed stdout mirror). */
89
89
  mirror?: OutputMirror;
@@ -48,7 +48,7 @@ export function binDir(home: string = homedir()): string {
48
48
 
49
49
  /**
50
50
  * Env var that relocates the acquired-runtimes root. The acquirers (server,
51
- * runner, seedpack) keep their `<root>/<version>/node_modules/@stigmer/<pkg>`
51
+ * runner, the official plugin catalogue) keep their `<root>/<version>/node_modules/@stigmer/<pkg>`
52
52
  * layout under it, so a root that already holds the packages is used as-is
53
53
  * and nothing is downloaded. The all-in-one image bakes exactly that layout
54
54
  * read-only off its data volume — a `stigmer up` whose first-run acquisitions
@@ -1,8 +1,9 @@
1
- // Pins the default-set step over a real Connect backend: a default absent
2
- // from the org is pushed public with the CLI's message; one present at the
3
- // same digest, READY and public is skipped without a push; a changed digest,
4
- // a non-READY state or a non-public visibility pushes again; the defaults go
5
- // in the marketplace's order; one failure does not stop the next; and a
1
+ // Pins the default-set step over a real Connect backend: preparation is
2
+ // all-or-nothing and touches no backend; a default absent from the org is
3
+ // pushed public with the CLI's message; one present at the same digest,
4
+ // READY and public is skipped without a push; a changed digest, a non-READY
5
+ // state or a non-public visibility pushes again; the defaults go in the
6
+ // marketplace's order; one push failure does not stop the next; and a
6
7
  // second run over a converged backend pushes nothing.
7
8
 
8
9
  import { createHash } from "node:crypto";
@@ -35,7 +36,7 @@ import type { Stigmer } from "@stigmer/sdk";
35
36
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
36
37
  import { writeCursorMarketplace } from "../../marketplace/__fixtures__/cursor-marketplace.js";
37
38
  import { preparePluginPush } from "../../resources/plugin.js";
38
- import { installDefaultPlugins } from "./defaults.js";
39
+ import { installPreparedDefaults, prepareDefaultPlugins } from "./defaults.js";
39
40
 
40
41
  let backend: Http2Server;
41
42
  let stigmer: Stigmer;
@@ -145,15 +146,47 @@ function digestOf(bytes: Uint8Array): string {
145
146
 
146
147
  const say = (): void => {};
147
148
 
148
- function run() {
149
- return installDefaultPlugins(
150
- { stigmer, info: say },
151
- { org: "stigmer", official: { repoDir: () => official } },
149
+ async function run() {
150
+ const prepared = await prepareDefaultPlugins({ repoDir: () => official });
151
+ return installPreparedDefaults(
152
+ { stigmer, info: say, verb: "stigmer bootstrap" },
153
+ prepared,
154
+ "stigmer",
152
155
  );
153
156
  }
154
157
 
155
- describe("installDefaultPlugins", () => {
156
- it("installs every default in order, public, with the CLI's message, on a fresh backend", async () => {
158
+ describe("prepareDefaultPlugins", () => {
159
+ it("prepares every default in the marketplace's order without touching the backend", async () => {
160
+ const prepared = await prepareDefaultPlugins({ repoDir: () => official });
161
+ expect(prepared.map((entry) => [entry.name, entry.push.digest])).toEqual([
162
+ ["thermos", thermosDigest],
163
+ ["github", githubDigest],
164
+ ]);
165
+ expect(pushes).toHaveLength(0);
166
+ });
167
+
168
+ it("refuses the whole set when one default cannot be prepared", async () => {
169
+ const broken = mkdtempSync(join(tmpdir(), "stigmer-defaults-broken-"));
170
+ try {
171
+ writeFileSync(
172
+ join(broken, "marketplace.json"),
173
+ JSON.stringify({
174
+ name: "stigmer",
175
+ plugins: [{ name: "missing", source: "./missing" }],
176
+ defaults: ["missing"],
177
+ }),
178
+ );
179
+ await expect(
180
+ prepareDefaultPlugins({ repoDir: () => broken }),
181
+ ).rejects.toThrow(/1 plugin this CLI cannot install/);
182
+ } finally {
183
+ rmSync(broken, { recursive: true, force: true });
184
+ }
185
+ });
186
+ });
187
+
188
+ describe("installPreparedDefaults", () => {
189
+ it("installs every default in order, public, with the invoking verb in the message, on a fresh backend", async () => {
157
190
  const result = await run();
158
191
  expect(result.outcomes.map((o) => [o.name, o.action])).toEqual([
159
192
  ["thermos", "installed"],
@@ -167,7 +200,7 @@ describe("installDefaultPlugins", () => {
167
200
  ),
168
201
  ).toBe(true);
169
202
  expect(pushes[0]?.message).toMatch(
170
- /^default plugin, installed by stigmer up \(/,
203
+ /^default plugin, installed by stigmer bootstrap \(/,
171
204
  );
172
205
  expect(digestOf(pushes[0]!.artifact)).toBe(thermosDigest);
173
206
  expect(digestOf(pushes[1]!.artifact)).toBe(githubDigest);
@@ -1,24 +1,31 @@
1
- // The default plugins a fresh local backend is bootstrapped with: the
2
- // `defaults` list of the official marketplace, installed in order into the
3
- // system org as public, so the global default-agent lookup and cross-org
4
- // references find them exactly where the seedpack used to put them.
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, so the global default-agent lookup and cross-org references find
4
+ // them.
5
5
  //
6
- // Idempotent by the server's own identity, not a marker file: before each
7
- // push the entry is prepared (walked, zipped, hashed) and the org's plugin
8
- // of that name is read back; when its digest equals the prepared archive's,
9
- // it is READY and it is public, there is nothing to do and nothing is sent.
10
- // A second `stigmer up` therefore pushes nothing, and a `stigmer up` after
11
- // an upgrade pushes exactly the entries whose content changed. The record is
12
- // the server, so any machine that reaches the same backend sees the same
13
- // answer (the seedpack's cloud-mode rule, made the only rule).
6
+ // Two phases, deliberately separate. Preparing (resolve the official tree,
7
+ // which may acquire `@stigmer/plugins` at the CLI's version; walk, zip and
8
+ // hash every default) needs no backend and is all-or-nothing: the catalogue
9
+ // is ours and validated on CI, so a default that cannot be prepared is a
10
+ // release defect, not a condition to install around. Installing pushes the
11
+ // prepared archives and is best-effort per entry: one default failing to
12
+ // land does not stop the next, and the caller decides how to surface the
13
+ // failures. The split lets `stigmer up` hold every replacement in hand
14
+ // before it retires what the replacement supersedes (local/bootstrap.ts).
14
15
  //
15
- // Best-effort per entry: one default failing to install does not stop the
16
- // next, and the caller decides how to surface the failures (the daemon
17
- // launcher warns and names `stigmer install <name>`).
16
+ // Idempotent by the server's own identity, not a marker file: before each
17
+ // push the org's plugin of that name is read back; when its digest equals the
18
+ // prepared archive's, it is READY and it is public, there is nothing to do
19
+ // and nothing is sent. A second `stigmer up` therefore pushes nothing, and a
20
+ // `stigmer up` after an upgrade pushes exactly the entries whose content
21
+ // changed. The record is the server, so any machine that reaches the same
22
+ // backend sees the same answer.
18
23
 
19
24
  import { type Stigmer, isNotFound } from "@stigmer/sdk";
20
25
  import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
21
26
  import { PluginState } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/status_pb";
27
+ import { CliExitError } from "../../errors/cli-exit-error.js";
28
+ import { ExitCode } from "../../errors/exit-codes.js";
22
29
  import { log } from "../../logger.js";
23
30
  import { VERSION } from "../../version.js";
24
31
  import {
@@ -29,21 +36,30 @@ import { readMarketplaceTree } from "../../marketplace/read.js";
29
36
  import { prepareEntry } from "../../marketplace/install.js";
30
37
  import {
31
38
  type PreparedPluginPush,
39
+ count,
32
40
  pushPrepared,
33
41
  } from "../../resources/plugin.js";
34
42
 
43
+ /**
44
+ * The command that ran the install, as the plugin's stored version message
45
+ * names it. Two verbs share the one bootstrap function, and a platform
46
+ * operator reading the row a year later must see which one wrote it.
47
+ */
48
+ export type InstallingVerb = "stigmer up" | "stigmer bootstrap";
49
+
35
50
  export interface DefaultPluginsDeps {
36
51
  /** The client bound to the backend being bootstrapped. */
37
52
  readonly stigmer: Stigmer;
38
53
  /** Human progress lines (stderr). */
39
54
  readonly info: (line: string) => void;
55
+ /** Stamped into each installed plugin's version message. */
56
+ readonly verb: InstallingVerb;
40
57
  }
41
58
 
42
- export interface DefaultPluginsOptions {
43
- /** The org the defaults are installed into: the seedpack's org, until the seedpack is gone. */
44
- readonly org: string;
45
- /** How the official tree is found (injectable for tests). */
46
- readonly official?: ResolveOfficialOptions;
59
+ /** One default, ready to push: its marketplace name and the prepared archive. */
60
+ export interface PreparedDefault {
61
+ readonly name: string;
62
+ readonly push: PreparedPluginPush;
47
63
  }
48
64
 
49
65
  export type DefaultPluginOutcome =
@@ -69,29 +85,56 @@ export interface DefaultPluginsResult {
69
85
  readonly failed: readonly string[];
70
86
  }
71
87
 
72
- /** Install every default the official marketplace names, in order, skipping what the backend already holds. */
73
- export async function installDefaultPlugins(
88
+ /**
89
+ * Phase 1: every default the official marketplace names, prepared in the
90
+ * marketplace's order. Throws when the tree cannot be resolved, offers less
91
+ * than it lists (the library drops an entry it cannot install with a
92
+ * warning, which for a user's marketplace is `show`'s business and for ours
93
+ * is a broken release), or any default cannot be prepared; nothing is
94
+ * returned half-ready.
95
+ */
96
+ export async function prepareDefaultPlugins(
97
+ official?: ResolveOfficialOptions,
98
+ ): Promise<readonly PreparedDefault[]> {
99
+ const resolved = resolveOfficialMarketplace(official);
100
+ const tree = readMarketplaceTree(resolved.dir, "the official marketplace");
101
+ if (tree.warnings.length > 0) {
102
+ throw new CliExitError(
103
+ `the official marketplace at ${resolved.dir} lists ${count(tree.warnings.length, "plugin")} this CLI cannot install`,
104
+ ExitCode.General,
105
+ tree.warnings.map((finding) => finding.message),
106
+ );
107
+ }
108
+ const prepared: PreparedDefault[] = [];
109
+ for (const name of tree.marketplace.defaults) {
110
+ prepared.push({ name, push: await prepareEntry(tree, name) });
111
+ }
112
+ return prepared;
113
+ }
114
+
115
+ /**
116
+ * Phase 2: install the prepared defaults into `org`, skipping what the
117
+ * backend already holds. Best-effort per entry.
118
+ */
119
+ export async function installPreparedDefaults(
74
120
  deps: DefaultPluginsDeps,
75
- options: DefaultPluginsOptions,
121
+ prepared: readonly PreparedDefault[],
122
+ org: string,
76
123
  ): Promise<DefaultPluginsResult> {
77
- const official = resolveOfficialMarketplace(options.official);
78
- const tree = readMarketplaceTree(official.dir, "the official marketplace");
79
-
80
124
  const outcomes: DefaultPluginOutcome[] = [];
81
- for (const name of tree.marketplace.defaults) {
125
+ for (const { name, push } of prepared) {
82
126
  try {
83
- const prepared = await prepareEntry(tree, name);
84
- if (await isAlreadyInstalled(deps.stigmer, options.org, prepared)) {
85
- outcomes.push({ name, action: "up-to-date", digest: prepared.digest });
127
+ if (await isAlreadyInstalled(deps.stigmer, org, push)) {
128
+ outcomes.push({ name, action: "up-to-date", digest: push.digest });
86
129
  continue;
87
130
  }
88
131
  deps.info(`Installing default plugin '${name}'…`);
89
- await pushPrepared(deps.stigmer, prepared, {
90
- org: options.org,
132
+ await pushPrepared(deps.stigmer, push, {
133
+ org,
91
134
  visibility: ApiResourceVisibility.visibility_public,
92
- message: `default plugin, installed by stigmer up (${VERSION})`,
135
+ message: `default plugin, installed by ${deps.verb} (${VERSION})`,
93
136
  });
94
- outcomes.push({ name, action: "installed", digest: prepared.digest });
137
+ outcomes.push({ name, action: "installed", digest: push.digest });
95
138
  } catch (error) {
96
139
  const message = error instanceof Error ? error.message : String(error);
97
140
  log.warn("default plugin install failed", { name, error: message });
@@ -36,6 +36,8 @@ describe("isAcquirableRelease", () => {
36
36
  expect(isAcquirableRelease("0.5.0-rc.1")).toBe(true);
37
37
  expect(isAcquirableRelease("0.0.0-dev")).toBe(false);
38
38
  expect(isAcquirableRelease("0.5.0-dev.20260825")).toBe(false);
39
+ // The bare stamp an unbundled server reports; the substring test this once was let it through.
40
+ expect(isAcquirableRelease("dev")).toBe(false);
39
41
  });
40
42
  });
41
43
 
@@ -7,6 +7,7 @@
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
+ import { isReleaseVersion } from "@stigmer/plugin-package/client";
10
11
  import { CliExitError } from "../../errors/cli-exit-error.js";
11
12
  import { ExitCode } from "../../errors/exit-codes.js";
12
13
 
@@ -64,9 +65,11 @@ export function npmInstallIntoRuntimes(installDir: string, spec: string): void {
64
65
  }
65
66
  }
66
67
 
67
- // A source build reports "0.0.0-dev" and the dev npm channel stamps
68
- // "<v>-dev.<stamp>" versions; neither publishes matching runtime packages,
69
- // so they are not acquirable. Release and rc/next versions are.
68
+ // A source build reports "0.0.0-dev", an unbundled server the bare "dev",
69
+ // and the dev npm channel stamps "<v>-dev.<stamp>"; none publishes matching
70
+ // runtime packages, so none is acquirable. Release and rc/next versions are.
71
+ // The rule is the shared client predicate, so the console's read of the
72
+ // official catalogue and this acquisition agree on every version.
70
73
  export function isAcquirableRelease(version: string): boolean {
71
- return !version.includes("-dev");
74
+ return isReleaseVersion(version);
72
75
  }