@stigmer/cli 3.17.0 → 3.18.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 (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
@@ -0,0 +1,91 @@
1
+ // `stigmer bootstrap` — make the configured backend a ready Stigmer: the
2
+ // system organization exists and the official marketplace's default plugins
3
+ // are installed into it, public. `stigmer up` does this for the local stack
4
+ // on its own; this verb is for every other shape (a raw self-hosted server
5
+ // behind docker-compose or Helm, the hosted platform's scheduled lane), and
6
+ // runs the very same two phases (local/bootstrap.ts), so "ready" has one
7
+ // meaning everywhere.
8
+ //
9
+ // Flagless on purpose. The org is the system org in every edition, and the
10
+ // defaults are idempotent by the server's digest, so there is nothing to
11
+ // force: a second run over a converged backend changes nothing and says so.
12
+ // The verb never deletes anything; a scheduled lane must be able to run it
13
+ // without that power.
14
+ //
15
+ // Exit is non-zero when any default failed to land, after the outcomes are
16
+ // rendered, so a lane's run is red for the right reason and a human reads
17
+ // what did and did not install. Heavy modules load lazily inside the action
18
+ // so `--help` stays fast.
19
+
20
+ import type { Command } from "commander";
21
+ import { ensureAuthenticated } from "../config/index.js";
22
+ import { CliExitError } from "../errors/cli-exit-error.js";
23
+ import { ExitCode } from "../errors/exit-codes.js";
24
+ import { CommandResult, type OutputFlags, renderResult } from "../output/index.js";
25
+ import { addResultFlags, resultFormat } from "./shared.js";
26
+
27
+ export function registerBootstrap(program: Command): void {
28
+ const bootstrap = program
29
+ .command("bootstrap")
30
+ .description(
31
+ "make the configured backend ready: ensure the system organization and install the default plugins",
32
+ )
33
+ .action((options: OutputFlags) => runBootstrapCommand(options));
34
+ addResultFlags(bootstrap);
35
+ }
36
+
37
+ async function runBootstrapCommand(flags: OutputFlags): Promise<void> {
38
+ const [{ connectBackend }, { bootstrapBackend }, { SYSTEM_ORG }] =
39
+ await Promise.all([
40
+ import("../backend.js"),
41
+ import("../local/bootstrap.js"),
42
+ import("../local/system-org.js"),
43
+ ]);
44
+
45
+ const client = connectBackend();
46
+ ensureAuthenticated(client.config);
47
+
48
+ const say = (line: string): void => {
49
+ process.stderr.write(`${line}\n`);
50
+ };
51
+ const result = await bootstrapBackend(client.stigmer, say);
52
+
53
+ const failed = result.plugins.failed;
54
+ const out =
55
+ failed.length === 0
56
+ ? CommandResult.success("Backend is ready")
57
+ : CommandResult.warning(
58
+ `Backend bootstrapped with ${failed.length} default plugin${failed.length === 1 ? "" : "s"} not installed`,
59
+ );
60
+ out
61
+ .addSection("System organization")
62
+ .field("Slug", SYSTEM_ORG)
63
+ .field("State", result.org);
64
+ const plugins = out.addSection("Default plugins");
65
+ for (const outcome of result.plugins.outcomes) {
66
+ switch (outcome.action) {
67
+ case "installed":
68
+ case "up-to-date":
69
+ plugins.field(outcome.name, outcome.action);
70
+ break;
71
+ case "failed":
72
+ plugins.field(outcome.name, `failed: ${outcome.error}`);
73
+ break;
74
+ default: {
75
+ const exhaustive: never = outcome;
76
+ throw new Error(`unhandled outcome ${String(exhaustive)}`);
77
+ }
78
+ }
79
+ }
80
+ if (failed.length > 0) {
81
+ out.hint("Fix the cause named above and run 'stigmer bootstrap' again.");
82
+ }
83
+ renderResult(out, resultFormat(flags));
84
+
85
+ if (failed.length > 0) {
86
+ throw new CliExitError(
87
+ `default plugin${failed.length === 1 ? "" : "s"} not installed: ${failed.join(", ")}`,
88
+ ExitCode.General,
89
+ );
90
+ }
91
+ }
@@ -4,7 +4,10 @@
4
4
  // the folder prepares to); --dry-run pushes nothing; a version pin, a path,
5
5
  // an unknown name and an ambiguous name refuse with their sentences. A real
6
6
  // Connect backend over h2c serves the plugin controllers; the config file
7
- // (HOME redirected) points a selfhost backend at it.
7
+ // (HOME redirected) points a selfhost backend at it. The global fetch is
8
+ // stubbed: the built-in vendor sources are asked through their marketplace
9
+ // files (answered 404, so they declare nothing) and any download would fail
10
+ // loudly, which pins that a bare install never fetches a vendor's zipball.
8
11
 
9
12
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
10
13
  import {
@@ -101,8 +104,20 @@ afterAll(async () => {
101
104
  await new Promise<void>((resolve) => backend.close(() => resolve()));
102
105
  });
103
106
 
107
+ /** Every URL the stubbed fetch was asked; raw files answer 404, anything else refuses. */
108
+ let fetched: string[] = [];
109
+
104
110
  beforeEach(() => {
105
111
  pushes = [];
112
+ fetched = [];
113
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
114
+ const url = String(input);
115
+ fetched.push(url);
116
+ if (url.startsWith("https://raw.githubusercontent.com/")) {
117
+ return new Response("Not Found", { status: 404 });
118
+ }
119
+ throw new Error(`unexpected network request in test: ${url}`);
120
+ });
106
121
  originalHome = process.env.HOME;
107
122
  home = mkdtempSync(join(tmpdir(), "stigmer-install-cmd-"));
108
123
  fixture = mkdtempSync(join(tmpdir(), "stigmer-install-cmd-fixture-"));
@@ -122,7 +137,7 @@ beforeEach(() => {
122
137
  "context:",
123
138
  " organization: acme",
124
139
  "marketplaces:",
125
- " cursor-plugins:",
140
+ " acme-plugins:",
126
141
  " type: local",
127
142
  ` path: ${fixture}`,
128
143
  "",
@@ -131,6 +146,7 @@ beforeEach(() => {
131
146
  });
132
147
 
133
148
  afterEach(() => {
149
+ vi.unstubAllGlobals();
134
150
  if (originalHome === undefined) delete process.env.HOME;
135
151
  else process.env.HOME = originalHome;
136
152
  rmSync(home, { recursive: true, force: true });
@@ -184,7 +200,7 @@ async function run(
184
200
 
185
201
  describe("install", () => {
186
202
  it("pushes the entry's folder as push plugin would, into the context org, and renders the install", async () => {
187
- const outcome = await run("cursor-plugins/thermos", "--json");
203
+ const outcome = await run("acme-plugins/thermos", "--json");
188
204
  expect(outcome.exitCode).toBe(ExitCode.Success);
189
205
  expect(pushes).toHaveLength(1);
190
206
  const expected = await preparePluginPush(join(fixture, "thermos"));
@@ -195,7 +211,7 @@ describe("install", () => {
195
211
  ),
196
212
  ).toBe(true);
197
213
  expect(pushes[0]?.message).toBe(
198
- "installed from marketplace 'cursor-plugins'",
214
+ "installed from marketplace 'acme-plugins'",
199
215
  );
200
216
 
201
217
  const payload = JSON.parse(outcome.stdout);
@@ -207,7 +223,7 @@ describe("install", () => {
207
223
  );
208
224
  expect(about.fields).toContainEqual({
209
225
  key: "Marketplace",
210
- value: `cursor-plugins (${fixture})`,
226
+ value: `acme-plugins (${fixture})`,
211
227
  });
212
228
  expect(payload.data.members).toHaveLength(2);
213
229
  });
@@ -229,7 +245,7 @@ describe("install", () => {
229
245
 
230
246
  it("--dry-run reads the marketplace and describes the plugin without pushing", async () => {
231
247
  const outcome = await run(
232
- "cursor-plugins/thermos@1.0.0",
248
+ "acme-plugins/thermos@1.0.0",
233
249
  "--dry-run",
234
250
  "--json",
235
251
  );
@@ -237,13 +253,13 @@ describe("install", () => {
237
253
  expect(pushes).toHaveLength(0);
238
254
  const payload = JSON.parse(outcome.stdout);
239
255
  expect(payload.message).toBe(
240
- "Dry run: 'cursor-plugins/thermos' would install plugin 'thermos'",
256
+ "Dry run: 'acme-plugins/thermos' would install plugin 'thermos'",
241
257
  );
242
258
  expect(payload.data.plugin.name).toBe("thermos");
243
259
  });
244
260
 
245
261
  it("refuses a version the marketplace does not offer, naming the one it does", async () => {
246
- const outcome = await run("cursor-plugins/thermos@9.9.9");
262
+ const outcome = await run("acme-plugins/thermos@9.9.9");
247
263
  expect(outcome.exitCode).toBe(ExitCode.Usage);
248
264
  expect(outcome.message).toMatch(
249
265
  /pins version 9\.9\.9, but the marketplace offers 'thermos' at 1\.0\.0/,
@@ -280,8 +296,11 @@ describe("install", () => {
280
296
  const outcome = await run("thermos");
281
297
  expect(outcome.exitCode).toBe(ExitCode.Usage);
282
298
  expect(outcome.message).toMatch(
283
- /offered by more than one marketplace: cursor-plugins, second\n\nName the one you mean: cursor-plugins\/thermos or second\/thermos/,
299
+ /offered by more than one marketplace: acme-plugins, second\n\nName the one you mean: acme-plugins\/thermos or second\/thermos/,
284
300
  );
301
+ // The three built-in vendor sources were asked through their files and nothing was downloaded.
302
+ expect(fetched.filter((url) => url.startsWith("https://raw.githubusercontent.com/cursor/plugins/")).length).toBeGreaterThan(0);
303
+ expect(fetched.some((url) => url.startsWith("https://codeload.github.com/"))).toBe(false);
285
304
  } finally {
286
305
  rmSync(second, { recursive: true, force: true });
287
306
  }
@@ -78,14 +78,14 @@ describe("marketplace add", () => {
78
78
  expect(outcome.exitCode).toBe(ExitCode.Success);
79
79
  const payload = JSON.parse(outcome.stdout);
80
80
  expect(payload.message).toBe(
81
- `Added marketplace 'cursor-plugins' (${fixture}), offering 2 plugins`,
81
+ `Added source 'acme-plugins' (${fixture}), offering 2 plugins`,
82
82
  );
83
83
  expect(payload.data).toEqual({
84
- name: "cursor-plugins",
84
+ name: "acme-plugins",
85
85
  source: { type: "local", path: fixture },
86
86
  plugins: 2,
87
87
  });
88
- expect(load().marketplaces?.["cursor-plugins"]).toEqual({
88
+ expect(load().marketplaces?.["acme-plugins"]).toEqual({
89
89
  type: "local",
90
90
  path: fixture,
91
91
  });
@@ -108,9 +108,14 @@ describe("marketplace add", () => {
108
108
  expect(load().marketplaces).toBeUndefined();
109
109
  });
110
110
 
111
- it("refuses the reserved name and a duplicate", async () => {
111
+ it("refuses every built-in name toward --name, and a duplicate", async () => {
112
112
  expect((await run("add", fixture, "--name", "stigmer")).message).toMatch(
113
- /built-in marketplace/,
113
+ /'stigmer' is a built-in source and cannot be added or replaced[\s\S]*--name/,
114
+ );
115
+ // A local clone of a vendor's catalogue names itself after the vendor; the
116
+ // built-in wins the name and the user is told how to add the clone anyway.
117
+ expect((await run("add", fixture, "--name", "cursor-plugins")).message).toMatch(
118
+ /'cursor-plugins' is a built-in source/,
114
119
  );
115
120
  await run("add", fixture);
116
121
  expect((await run("add", fixture)).message).toMatch(/already configured/);
@@ -118,7 +123,7 @@ describe("marketplace add", () => {
118
123
  });
119
124
 
120
125
  describe("marketplace list", () => {
121
- it("is offline, official first, and names an entry it cannot read", async () => {
126
+ it("is offline, the built-ins first, and names an entry it cannot read", async () => {
122
127
  await run("add", fixture);
123
128
  const { save } = await import("../config/index.js");
124
129
  const config = load();
@@ -133,7 +138,13 @@ describe("marketplace list", () => {
133
138
  expect(payload.status).toBe("warning");
134
139
  expect(payload.data.marketplaces).toEqual([
135
140
  { name: "stigmer", source: { type: "official" } },
136
- { name: "cursor-plugins", source: { type: "local", path: fixture } },
141
+ { name: "cursor-plugins", source: { type: "github", repo: "cursor/plugins" } },
142
+ {
143
+ name: "claude-code-plugins",
144
+ source: { type: "github", repo: "anthropics/claude-code" },
145
+ },
146
+ { name: "codex-plugins", source: { type: "github", repo: "openai/plugins" } },
147
+ { name: "acme-plugins", source: { type: "local", path: fixture } },
137
148
  {
138
149
  name: "broken",
139
150
  unreadable: "unknown type 'gitlab' (expected 'github' or 'local')",
@@ -145,11 +156,11 @@ describe("marketplace list", () => {
145
156
  describe("marketplace show", () => {
146
157
  it("lists the offered plugins with their manifest versions and the entries it could not offer", async () => {
147
158
  await run("add", fixture);
148
- const outcome = await run("show", "cursor-plugins", "--json");
159
+ const outcome = await run("show", "acme-plugins", "--json");
149
160
  expect(outcome.exitCode).toBe(ExitCode.Success);
150
161
  const payload = JSON.parse(outcome.stdout);
151
162
  expect(payload.message).toBe(
152
- "Marketplace 'cursor-plugins' offers 2 plugins, with 1 warning",
163
+ "Marketplace 'acme-plugins' offers 2 plugins, with 1 warning",
153
164
  );
154
165
  expect(payload.data.plugins).toEqual([
155
166
  {
@@ -184,17 +195,20 @@ describe("marketplace show", () => {
184
195
  });
185
196
 
186
197
  describe("marketplace remove", () => {
187
- it("removes a configured marketplace and refuses the built-in and an unknown one", async () => {
198
+ it("removes an added source and refuses a built-in and an unknown one", async () => {
188
199
  await run("add", fixture);
189
- expect((await run("remove", "cursor-plugins")).exitCode).toBe(
200
+ expect((await run("remove", "acme-plugins")).exitCode).toBe(
190
201
  ExitCode.Success,
191
202
  );
192
203
  expect(load().marketplaces).toBeUndefined();
193
204
  expect((await run("remove", "stigmer")).message).toMatch(
194
- /cannot be removed/,
205
+ /'stigmer' is a built-in source and cannot be removed/,
206
+ );
207
+ expect((await run("remove", "codex-plugins")).message).toMatch(
208
+ /'codex-plugins' is a built-in source and cannot be removed/,
195
209
  );
196
- expect((await run("remove", "cursor-plugins")).message).toMatch(
197
- /no marketplace named 'cursor-plugins'/,
210
+ expect((await run("remove", "acme-plugins")).message).toMatch(
211
+ /no marketplace named 'acme-plugins'/,
198
212
  );
199
213
  });
200
214
  });
@@ -22,12 +22,14 @@ interface AddFlags extends OutputFlags {
22
22
  export function registerMarketplace(program: Command): void {
23
23
  const marketplace = program
24
24
  .command("marketplace")
25
- .description("manage the plugin marketplaces you install from");
25
+ .description(
26
+ "manage the sources the Marketplace installs from: the built-in catalogues and the ones you add",
27
+ );
26
28
 
27
29
  const add = marketplace
28
30
  .command("add <source>")
29
31
  .description(
30
- "add a marketplace: a directory, a GitHub 'owner/repo[@ref]', or that repository's URL",
32
+ "add a source: a directory, a GitHub 'owner/repo[@ref]', or that repository's URL",
31
33
  )
32
34
  .option(
33
35
  "--name <name>",
@@ -38,19 +40,19 @@ export function registerMarketplace(program: Command): void {
38
40
 
39
41
  const list = marketplace
40
42
  .command("list")
41
- .description("list the configured marketplaces (offline)")
43
+ .description("list the known sources, built in and added (offline)")
42
44
  .action((options: OutputFlags) => runList(options));
43
45
  addResultFlags(list);
44
46
 
45
47
  const show = marketplace
46
48
  .command("show <name>")
47
- .description("show the plugins a marketplace offers (reads its source)")
49
+ .description("show the plugins a source offers (reads it)")
48
50
  .action((name: string, options: OutputFlags) => runShow(name, options));
49
51
  addResultFlags(show);
50
52
 
51
53
  const remove = marketplace
52
54
  .command("remove <name>")
53
- .description("remove a configured marketplace (installed plugins stay)")
55
+ .description("remove an added source (installed plugins stay; a built-in cannot be removed)")
54
56
  .action((name: string, options: OutputFlags) => runRemove(name, options));
55
57
  addResultFlags(remove);
56
58
  }
@@ -81,7 +83,7 @@ async function runAdd(source: string, flags: AddFlags): Promise<void> {
81
83
  m.addMarketplace(name, parsed);
82
84
 
83
85
  const result = CommandResult.success(
84
- `Added marketplace '${name}' (${m.describeSource(parsed)}), offering ${offered} plugin${offered === 1 ? "" : "s"}`,
86
+ `Added source '${name}' (${m.describeSource(parsed)}), offering ${offered} plugin${offered === 1 ? "" : "s"}`,
85
87
  );
86
88
  result.hint(`See what it offers with: stigmer marketplace show ${name}`);
87
89
  result.hint(`Install from it with: stigmer install ${name}/<plugin>`);
@@ -123,7 +125,7 @@ async function runRemove(name: string, flags: OutputFlags): Promise<void> {
123
125
  const m = await import("../marketplace/index.js");
124
126
  const { CommandResult } = await import("../output/index.js");
125
127
  m.removeMarketplace(name);
126
- const result = CommandResult.success(`Removed marketplace '${name}'`);
128
+ const result = CommandResult.success(`Removed source '${name}'`);
127
129
  result.hint(
128
130
  "Plugins installed from it stay installed; remove one with: stigmer delete plugin <name>",
129
131
  );
@@ -150,11 +150,11 @@ describe("resolveOrganization", () => {
150
150
  );
151
151
  });
152
152
 
153
- it("defaults to the seedpack org in local mode when nothing is configured", () => {
153
+ it("defaults to the system org in local mode when nothing is configured", () => {
154
154
  expect(resolveOrganization(localConfig())).toBe("stigmer");
155
155
  });
156
156
 
157
- it("defaults to the seedpack org on a selfhost backend (it IS the OSS server)", () => {
157
+ it("defaults to the system org on a selfhost backend (it IS the OSS server)", () => {
158
158
  expect(resolveOrganization(selfhostConfig())).toBe("stigmer");
159
159
  });
160
160
 
@@ -8,6 +8,7 @@
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";
11
12
  import { type Config, activeBackend, isCloudMode } from "./config.js";
12
13
 
13
14
  const DEFAULT_CLOUD_ENDPOINT = "api.stigmer.ai:443";
@@ -16,13 +17,13 @@ const DEFAULT_LOCAL_ENDPOINT = "localhost:7234";
16
17
  /** Well-known URL for the Stigmer Cloud web console. */
17
18
  export const DEFAULT_CLOUD_CONSOLE_URL = "https://app.stigmer.ai";
18
19
 
19
- // Local mode is single-tenant: the seedpack bootstraps resources into the
20
- // "stigmer" org (see client-apps/cli/src/local/seedpack/apply.ts DEFAULT_ORG).
21
- // Org-scoped getByReference now requires an org, so bare-slug `get`/`run`/`apply`
22
- // in local mode fall back to this default when nothing else is configured. The
23
- // same holds for selfhost backends — they ARE the OSS server. Cloud mode has no
24
- // implicit org — the caller must select one (flag/env/context/login).
25
- const DEFAULT_LOCAL_ORG = "stigmer";
20
+ // Local mode is single-tenant: the bootstrap `stigmer up` runs creates the
21
+ // system org and installs the default plugins into it, so bare-slug
22
+ // `get`/`run`/`apply` in local mode fall back to that org when nothing else
23
+ // is configured. The same holds for selfhost backends — they ARE the OSS
24
+ // server. Cloud mode has no implicit org — the caller must select one
25
+ // (flag/env/context/login).
26
+ const DEFAULT_LOCAL_ORG = SYSTEM_ORG;
26
27
 
27
28
  /**
28
29
  * Resolve the server endpoint.
@@ -1,71 +1,143 @@
1
- // Pins the bootstrap's shape: the seedpack step then the default-plugins step,
2
- // over one client; a seedpack failure is contained by its own step and the
3
- // plugins step still runs; a plugins failure is a warning that names the
4
- // retry command, never a throw past the bootstrap.
1
+ // Pins the local bootstrap's shape: prepare, then run, then retire, over one
2
+ // client; a preparation failure skips the run and the retire together, and a
3
+ // run that cannot start skips the retire (nothing is deleted before its
4
+ // replacement is in place); a default that failed to land is one warning
5
+ // naming `stigmer bootstrap` and holds the retire back with a line saying so;
6
+ // a retire failure is contained by its own warning; nothing throws past the
7
+ // bootstrap.
5
8
 
6
9
  import { describe, expect, it } from "vitest";
10
+ import type { Stigmer } from "@stigmer/sdk";
7
11
  import type { BackendClient } from "../client/index.js";
8
12
  import {
13
+ type BootstrapResult,
14
+ type PreparedBootstrap,
9
15
  bootstrapLocalBackend,
10
- installDefaultPluginsBestEffort,
11
16
  } from "./bootstrap.js";
12
17
 
13
- const client = { stigmer: {} } as unknown as BackendClient;
18
+ const stigmer = {} as unknown as Stigmer;
19
+ const client = { stigmer } as unknown as BackendClient;
20
+ const prepared: PreparedBootstrap = { defaults: [] };
21
+
22
+ function converged(): BootstrapResult {
23
+ return { org: "present", plugins: { outcomes: [], failed: [] } };
24
+ }
14
25
 
15
26
  describe("bootstrapLocalBackend", () => {
16
- it("runs the seedpack step then the plugins step, over the same client and home", async () => {
27
+ it("prepares, runs, then retires, over the same client and home", async () => {
17
28
  const order: string[] = [];
18
29
  await bootstrapLocalBackend("/home/x", {
19
30
  client,
20
31
  say: () => {},
21
- seedpack: async (c, home) => {
22
- expect(c).toBe(client);
23
- expect(home).toBe("/home/x");
24
- order.push("seedpack");
32
+ prepare: async () => {
33
+ order.push("prepare");
34
+ return prepared;
25
35
  },
26
- plugins: async (c, home) => {
27
- expect(c).toBe(client);
36
+ run: async (s, p) => {
37
+ expect(s).toBe(stigmer);
38
+ expect(p).toBe(prepared);
39
+ order.push("run");
40
+ return converged();
41
+ },
42
+ retire: async (s, home) => {
43
+ expect(s).toBe(stigmer);
28
44
  expect(home).toBe("/home/x");
29
- order.push("plugins");
45
+ order.push("retire");
30
46
  },
31
47
  });
32
- expect(order).toEqual(["seedpack", "plugins"]);
48
+ expect(order).toEqual(["prepare", "run", "retire"]);
33
49
  });
34
50
 
35
- it("still runs the plugins step when the seedpack step has warned", async () => {
51
+ it("skips the run and the retire when preparation fails, with one warning", async () => {
36
52
  const said: string[] = [];
37
- let plugins = 0;
53
+ let retired = 0;
54
+ let ran = 0;
38
55
  await bootstrapLocalBackend("/home/x", {
39
56
  client,
40
57
  say: (line) => said.push(line),
41
- seedpack: async (_c, _h, say) => say("Warning: seedpack failed"),
42
- plugins: async () => {
43
- plugins += 1;
58
+ prepare: async () => {
59
+ throw new Error("offline");
60
+ },
61
+ run: async () => {
62
+ ran += 1;
63
+ return converged();
64
+ },
65
+ retire: async () => {
66
+ retired += 1;
44
67
  },
45
68
  });
46
- expect(plugins).toBe(1);
47
- expect(said).toEqual(["Warning: seedpack failed"]);
69
+ expect(ran).toBe(0);
70
+ expect(retired).toBe(0);
71
+ expect(said).toEqual([
72
+ "Warning: could not prepare the default plugins, so the local backend was not bootstrapped and nothing was retired. Run 'stigmer up' again to retry.",
73
+ ]);
48
74
  });
49
- });
50
75
 
51
- describe("installDefaultPluginsBestEffort", () => {
52
- it("turns a step that cannot even start into one warning naming the retry commands", async () => {
76
+ it("skips the retire when the run cannot even start, with one warning naming the retry", async () => {
53
77
  const said: string[] = [];
54
- // A client whose plugin lookups throw: the step's own failure path, not a default's.
55
- const broken = {
56
- stigmer: {
57
- plugin: {
58
- getByReference: async () => {
59
- throw new Error("backend unreachable");
60
- },
78
+ let retired = 0;
79
+ await bootstrapLocalBackend("/home/x", {
80
+ client,
81
+ say: (line) => said.push(line),
82
+ prepare: async () => prepared,
83
+ run: async () => {
84
+ throw new Error("backend unreachable");
85
+ },
86
+ retire: async () => {
87
+ retired += 1;
88
+ },
89
+ });
90
+ expect(retired).toBe(0);
91
+ expect(said).toEqual([
92
+ "Warning: failed to bootstrap the local backend, so nothing was retired. Run 'stigmer up' again to retry.",
93
+ ]);
94
+ });
95
+
96
+ it("holds the retire back when a default did not land, naming the default and saying why", async () => {
97
+ const said: string[] = [];
98
+ let retired = 0;
99
+ await bootstrapLocalBackend("/home/x", {
100
+ client,
101
+ say: (line) => said.push(line),
102
+ prepare: async () => prepared,
103
+ run: async () => ({
104
+ org: "created",
105
+ plugins: {
106
+ outcomes: [
107
+ { name: "assistant", action: "failed", error: "refused" },
108
+ ],
109
+ failed: ["assistant"],
61
110
  },
111
+ }),
112
+ retire: async () => {
113
+ retired += 1;
62
114
  },
63
- } as unknown as BackendClient;
64
- await installDefaultPluginsBestEffort(broken, "/home/x", (line) =>
65
- said.push(line),
66
- );
67
- expect(said.filter((line) => line.startsWith("Warning:"))).toEqual([
68
- "Warning: failed to install default plugin 'assistant'. Run 'stigmer install assistant' to retry.",
115
+ });
116
+ expect(retired).toBe(0);
117
+ expect(said).toEqual([
118
+ "Warning: failed to install default plugin 'assistant'. Run 'stigmer bootstrap' to retry.",
119
+ "Nothing was retired: the resources an older release installed stay until every default plugin is in place.",
120
+ ]);
121
+ });
122
+
123
+ it("contains a retire that threw in one warning, after the run succeeded", async () => {
124
+ const said: string[] = [];
125
+ let ran = 0;
126
+ await bootstrapLocalBackend("/home/x", {
127
+ client,
128
+ say: (line) => said.push(line),
129
+ prepare: async () => prepared,
130
+ run: async () => {
131
+ ran += 1;
132
+ return converged();
133
+ },
134
+ retire: async () => {
135
+ throw new Error("project read failed");
136
+ },
137
+ });
138
+ expect(ran).toBe(1);
139
+ expect(said).toEqual([
140
+ "Warning: failed to retire the resources an older release installed. Run 'stigmer up' again to retry.",
69
141
  ]);
70
142
  });
71
143
  });