@stigmer/cli 3.18.1-dev.20260919070736 → 3.19.1-dev.20260920100552
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/apply.d.ts.map +1 -1
- package/commands/apply.js +45 -101
- package/commands/apply.js.map +1 -1
- package/commands/install.js +2 -1
- package/commands/install.js.map +1 -1
- package/commands/marketplace.js +3 -3
- package/commands/marketplace.js.map +1 -1
- package/commands/push.js +3 -1
- package/commands/push.js.map +1 -1
- package/commands/validate.js +3 -4
- package/commands/validate.js.map +1 -1
- package/local/bootstrap.d.ts +1 -2
- package/local/bootstrap.d.ts.map +1 -1
- package/local/bootstrap.js +13 -41
- 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/local/daemon/launch.js +2 -2
- package/local/daemon/launch.js.map +1 -1
- package/local/plugins/defaults.js +2 -2
- package/local/plugins/defaults.js.map +1 -1
- package/marketplace/config.d.ts.map +1 -1
- package/marketplace/config.js +13 -9
- package/marketplace/config.js.map +1 -1
- package/marketplace/render.d.ts.map +1 -1
- package/marketplace/render.js +1 -7
- package/marketplace/render.js.map +1 -1
- package/package.json +7 -7
- package/registry/index.d.ts +2 -2
- package/registry/index.d.ts.map +1 -1
- package/registry/index.js +2 -2
- package/registry/index.js.map +1 -1
- package/registry/metadata.d.ts +9 -0
- package/registry/metadata.d.ts.map +1 -1
- package/registry/metadata.js +14 -5
- package/registry/metadata.js.map +1 -1
- package/registry/registry.d.ts +9 -0
- package/registry/registry.d.ts.map +1 -1
- package/registry/registry.js +16 -1
- package/registry/registry.js.map +1 -1
- package/registry/verb-support.d.ts.map +1 -1
- package/registry/verb-support.js +0 -13
- package/registry/verb-support.js.map +1 -1
- package/resources/apply/apply.d.ts +11 -12
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +12 -21
- package/resources/apply/apply.js.map +1 -1
- package/resources/apply/handlers.js +2 -2
- package/resources/apply/handlers.js.map +1 -1
- package/resources/connect/oauth.d.ts.map +1 -1
- package/resources/connect/oauth.js +4 -1
- package/resources/connect/oauth.js.map +1 -1
- package/resources/delete.d.ts.map +1 -1
- package/resources/delete.js +0 -1
- package/resources/delete.js.map +1 -1
- package/resources/get-bindings.d.ts.map +1 -1
- package/resources/get-bindings.js +0 -5
- package/resources/get-bindings.js.map +1 -1
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +0 -1
- package/resources/list.js.map +1 -1
- package/resources/plugin.d.ts +27 -0
- package/resources/plugin.d.ts.map +1 -1
- package/resources/plugin.js +113 -8
- package/resources/plugin.js.map +1 -1
- package/resources/validate.d.ts.map +1 -1
- package/resources/validate.js +0 -2
- package/resources/validate.js.map +1 -1
- package/src/commands/apply.ts +40 -150
- package/src/commands/install.test.ts +20 -21
- package/src/commands/install.ts +2 -1
- package/src/commands/marketplace.test.ts +10 -19
- package/src/commands/marketplace.ts +3 -3
- package/src/commands/push.ts +6 -1
- package/src/commands/validate.ts +2 -4
- package/src/local/bootstrap.test.ts +15 -59
- package/src/local/bootstrap.ts +12 -53
- package/src/local/daemon/launch.ts +4 -4
- package/src/local/plugins/defaults.test.ts +28 -28
- package/src/local/plugins/defaults.ts +2 -2
- package/src/marketplace/__fixtures__/cursor-marketplace.ts +16 -12
- package/src/marketplace/config.test.ts +12 -28
- package/src/marketplace/config.ts +13 -9
- package/src/marketplace/install.test.ts +19 -19
- package/src/marketplace/read.test.ts +6 -6
- package/src/marketplace/render.ts +1 -10
- package/src/registry/index.ts +7 -1
- package/src/registry/metadata.ts +15 -5
- package/src/registry/registry.test.ts +1 -7
- package/src/registry/registry.ts +17 -1
- package/src/registry/verb-support.ts +0 -13
- package/src/resources/apply/apply.integration.test.ts +15 -3
- package/src/resources/apply/apply.ts +15 -26
- package/src/resources/apply/handlers.ts +2 -2
- package/src/resources/connect/oauth.test.ts +2 -2
- package/src/resources/connect/oauth.ts +4 -1
- package/src/resources/delete.ts +0 -1
- package/src/resources/get-bindings.ts +0 -5
- package/src/resources/list.ts +0 -1
- package/src/resources/plugin.test.ts +119 -2
- package/src/resources/plugin.ts +140 -8
- package/src/resources/validate.test.ts +0 -1
- package/src/resources/validate.ts +0 -2
- package/local/seedpack-retire.d.ts +0 -63
- package/local/seedpack-retire.d.ts.map +0 -1
- package/local/seedpack-retire.js +0 -361
- package/local/seedpack-retire.js.map +0 -1
- package/resources/apply/declarative.d.ts +0 -106
- package/resources/apply/declarative.d.ts.map +0 -1
- package/resources/apply/declarative.js +0 -397
- package/resources/apply/declarative.js.map +0 -1
- package/resources/apply/synth/project-track.d.ts +0 -21
- package/resources/apply/synth/project-track.d.ts.map +0 -1
- package/resources/apply/synth/project-track.js +0 -102
- package/resources/apply/synth/project-track.js.map +0 -1
- package/resources/apply/synth/reader.d.ts +0 -18
- package/resources/apply/synth/reader.d.ts.map +0 -1
- package/resources/apply/synth/reader.js +0 -55
- package/resources/apply/synth/reader.js.map +0 -1
- package/resources/apply/synth/runtime.d.ts +0 -8
- package/resources/apply/synth/runtime.d.ts.map +0 -1
- package/resources/apply/synth/runtime.js +0 -32
- package/resources/apply/synth/runtime.js.map +0 -1
- package/resources/apply/synth/synthesize.d.ts +0 -43
- package/resources/apply/synth/synthesize.d.ts.map +0 -1
- package/resources/apply/synth/synthesize.js +0 -115
- package/resources/apply/synth/synthesize.js.map +0 -1
- package/src/local/seedpack-retire.test.ts +0 -499
- package/src/local/seedpack-retire.ts +0 -474
- package/src/resources/apply/declarative.integration.test.ts +0 -204
- package/src/resources/apply/declarative.test.ts +0 -132
- package/src/resources/apply/declarative.ts +0 -491
- package/src/resources/apply/synth/capstone.integration.test.ts +0 -134
- package/src/resources/apply/synth/project-track.integration.test.ts +0 -194
- package/src/resources/apply/synth/project-track.ts +0 -126
- package/src/resources/apply/synth/reader.test.ts +0 -90
- package/src/resources/apply/synth/reader.ts +0 -68
- package/src/resources/apply/synth/runtime.test.ts +0 -29
- package/src/resources/apply/synth/runtime.ts +0 -39
- package/src/resources/apply/synth/synthesize.test.ts +0 -109
- package/src/resources/apply/synth/synthesize.ts +0 -161
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
// Pins the local bootstrap's shape: prepare, then run,
|
|
2
|
-
//
|
|
3
|
-
// run that cannot start
|
|
4
|
-
//
|
|
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
|
|
1
|
+
// Pins the local bootstrap's shape: prepare, then run, over one client; a
|
|
2
|
+
// preparation failure skips the run with one warning naming the retry; a
|
|
3
|
+
// run that cannot start is one warning; a default that failed to land is one
|
|
4
|
+
// warning per default naming `stigmer bootstrap`; nothing throws past the
|
|
7
5
|
// bootstrap.
|
|
8
6
|
|
|
9
7
|
import { describe, expect, it } from "vitest";
|
|
@@ -24,9 +22,9 @@ function converged(): BootstrapResult {
|
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
describe("bootstrapLocalBackend", () => {
|
|
27
|
-
it("prepares,
|
|
25
|
+
it("prepares, then runs, over the same client", async () => {
|
|
28
26
|
const order: string[] = [];
|
|
29
|
-
await bootstrapLocalBackend(
|
|
27
|
+
await bootstrapLocalBackend({
|
|
30
28
|
client,
|
|
31
29
|
say: () => {},
|
|
32
30
|
prepare: async () => {
|
|
@@ -39,20 +37,14 @@ describe("bootstrapLocalBackend", () => {
|
|
|
39
37
|
order.push("run");
|
|
40
38
|
return converged();
|
|
41
39
|
},
|
|
42
|
-
retire: async (s, home) => {
|
|
43
|
-
expect(s).toBe(stigmer);
|
|
44
|
-
expect(home).toBe("/home/x");
|
|
45
|
-
order.push("retire");
|
|
46
|
-
},
|
|
47
40
|
});
|
|
48
|
-
expect(order).toEqual(["prepare", "run"
|
|
41
|
+
expect(order).toEqual(["prepare", "run"]);
|
|
49
42
|
});
|
|
50
43
|
|
|
51
|
-
it("skips the run
|
|
44
|
+
it("skips the run when preparation fails, with one warning", async () => {
|
|
52
45
|
const said: string[] = [];
|
|
53
|
-
let retired = 0;
|
|
54
46
|
let ran = 0;
|
|
55
|
-
await bootstrapLocalBackend(
|
|
47
|
+
await bootstrapLocalBackend({
|
|
56
48
|
client,
|
|
57
49
|
say: (line) => said.push(line),
|
|
58
50
|
prepare: async () => {
|
|
@@ -62,41 +54,31 @@ describe("bootstrapLocalBackend", () => {
|
|
|
62
54
|
ran += 1;
|
|
63
55
|
return converged();
|
|
64
56
|
},
|
|
65
|
-
retire: async () => {
|
|
66
|
-
retired += 1;
|
|
67
|
-
},
|
|
68
57
|
});
|
|
69
58
|
expect(ran).toBe(0);
|
|
70
|
-
expect(retired).toBe(0);
|
|
71
59
|
expect(said).toEqual([
|
|
72
|
-
"Warning: could not prepare the default plugins, so the local backend was not bootstrapped
|
|
60
|
+
"Warning: could not prepare the default plugins, so the local backend was not bootstrapped. Run 'stigmer up' again to retry.",
|
|
73
61
|
]);
|
|
74
62
|
});
|
|
75
63
|
|
|
76
|
-
it("
|
|
64
|
+
it("contains a run that cannot start in one warning naming the retry", async () => {
|
|
77
65
|
const said: string[] = [];
|
|
78
|
-
|
|
79
|
-
await bootstrapLocalBackend("/home/x", {
|
|
66
|
+
await bootstrapLocalBackend({
|
|
80
67
|
client,
|
|
81
68
|
say: (line) => said.push(line),
|
|
82
69
|
prepare: async () => prepared,
|
|
83
70
|
run: async () => {
|
|
84
71
|
throw new Error("backend unreachable");
|
|
85
72
|
},
|
|
86
|
-
retire: async () => {
|
|
87
|
-
retired += 1;
|
|
88
|
-
},
|
|
89
73
|
});
|
|
90
|
-
expect(retired).toBe(0);
|
|
91
74
|
expect(said).toEqual([
|
|
92
|
-
"Warning: failed to bootstrap the local backend
|
|
75
|
+
"Warning: failed to bootstrap the local backend. Run 'stigmer up' again to retry.",
|
|
93
76
|
]);
|
|
94
77
|
});
|
|
95
78
|
|
|
96
|
-
it("
|
|
79
|
+
it("names each default that did not land and the verb that retries it", async () => {
|
|
97
80
|
const said: string[] = [];
|
|
98
|
-
|
|
99
|
-
await bootstrapLocalBackend("/home/x", {
|
|
81
|
+
await bootstrapLocalBackend({
|
|
100
82
|
client,
|
|
101
83
|
say: (line) => said.push(line),
|
|
102
84
|
prepare: async () => prepared,
|
|
@@ -109,35 +91,9 @@ describe("bootstrapLocalBackend", () => {
|
|
|
109
91
|
failed: ["assistant"],
|
|
110
92
|
},
|
|
111
93
|
}),
|
|
112
|
-
retire: async () => {
|
|
113
|
-
retired += 1;
|
|
114
|
-
},
|
|
115
94
|
});
|
|
116
|
-
expect(retired).toBe(0);
|
|
117
95
|
expect(said).toEqual([
|
|
118
96
|
"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.",
|
|
141
97
|
]);
|
|
142
98
|
});
|
|
143
99
|
});
|
package/src/local/bootstrap.ts
CHANGED
|
@@ -6,29 +6,20 @@
|
|
|
6
6
|
// lane. One definition, so a laptop, a container and the hosted platform
|
|
7
7
|
// cannot drift in what "ready" means.
|
|
8
8
|
//
|
|
9
|
-
// Two phases, and the order matters
|
|
9
|
+
// Two phases, and the order matters:
|
|
10
10
|
//
|
|
11
11
|
// 1. prepare: resolve the official marketplace (acquiring `@stigmer/plugins`
|
|
12
12
|
// at the CLI's version on a release's first run) and prepare every
|
|
13
13
|
// default. No backend is touched.
|
|
14
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.
|
|
15
|
+
// skipped when the backend already holds it at the same digest. The
|
|
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
19
|
//
|
|
17
|
-
// `
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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
|
|
20
|
+
// Inside `up` both phases are best-effort with their own warning naming the
|
|
21
|
+
// retry, because the stack is already serving by the time either runs and a
|
|
22
|
+
// seeding failure must never tear it down. Both `up` shapes (detached
|
|
32
23
|
// and foreground) call this one function, and both seed the local server
|
|
33
24
|
// `up` just started, never the active backend context: a CLI pointed at
|
|
34
25
|
// cloud still gets its local stack seeded, and never has system resources
|
|
@@ -44,7 +35,6 @@ import type {
|
|
|
44
35
|
InstallingVerb,
|
|
45
36
|
PreparedDefault,
|
|
46
37
|
} from "./plugins/defaults.js";
|
|
47
|
-
import { dataDir } from "./paths.js";
|
|
48
38
|
import { type EnsureSystemOrgOutcome, SYSTEM_ORG } from "./system-org.js";
|
|
49
39
|
|
|
50
40
|
export type Say = (line: string) => void;
|
|
@@ -116,7 +106,6 @@ export interface BootstrapDeps {
|
|
|
116
106
|
readonly client?: BackendClient;
|
|
117
107
|
readonly say?: Say;
|
|
118
108
|
readonly prepare?: () => Promise<PreparedBootstrap>;
|
|
119
|
-
readonly retire?: (stigmer: Stigmer, home: string, say: Say) => Promise<void>;
|
|
120
109
|
readonly run?: (
|
|
121
110
|
stigmer: Stigmer,
|
|
122
111
|
prepared: PreparedBootstrap,
|
|
@@ -130,7 +119,6 @@ const sayToStderr: Say = (line) => {
|
|
|
130
119
|
|
|
131
120
|
/** What `stigmer up` does once the local server answers. Never throws past its own warnings. */
|
|
132
121
|
export async function bootstrapLocalBackend(
|
|
133
|
-
home: string,
|
|
134
122
|
deps: BootstrapDeps = {},
|
|
135
123
|
): Promise<void> {
|
|
136
124
|
const client = deps.client ?? (await freshLocalClient());
|
|
@@ -142,7 +130,7 @@ export async function bootstrapLocalBackend(
|
|
|
142
130
|
} catch (err) {
|
|
143
131
|
log.warn("bootstrap preparation failed", { error: String(err) });
|
|
144
132
|
say(
|
|
145
|
-
"Warning: could not prepare the default plugins, so the local backend was not bootstrapped
|
|
133
|
+
"Warning: could not prepare the default plugins, so the local backend was not bootstrapped. Run 'stigmer up' again to retry.",
|
|
146
134
|
);
|
|
147
135
|
return;
|
|
148
136
|
}
|
|
@@ -157,7 +145,7 @@ export async function bootstrapLocalBackend(
|
|
|
157
145
|
} catch (err) {
|
|
158
146
|
log.warn("bootstrap failed", { error: String(err) });
|
|
159
147
|
say(
|
|
160
|
-
"Warning: failed to bootstrap the local backend
|
|
148
|
+
"Warning: failed to bootstrap the local backend. Run 'stigmer up' again to retry.",
|
|
161
149
|
);
|
|
162
150
|
return;
|
|
163
151
|
}
|
|
@@ -165,24 +153,9 @@ export async function bootstrapLocalBackend(
|
|
|
165
153
|
org: result.org,
|
|
166
154
|
outcomes: result.plugins.outcomes,
|
|
167
155
|
});
|
|
168
|
-
|
|
169
|
-
for (const name of result.plugins.failed) {
|
|
170
|
-
say(
|
|
171
|
-
`Warning: failed to install default plugin '${name}'. Run 'stigmer bootstrap' to retry.`,
|
|
172
|
-
);
|
|
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);
|
|
182
|
-
} catch (err) {
|
|
183
|
-
log.warn("seedpack retire failed", { error: String(err) });
|
|
156
|
+
for (const name of result.plugins.failed) {
|
|
184
157
|
say(
|
|
185
|
-
|
|
158
|
+
`Warning: failed to install default plugin '${name}'. Run 'stigmer bootstrap' to retry.`,
|
|
186
159
|
);
|
|
187
160
|
}
|
|
188
161
|
}
|
|
@@ -197,20 +170,6 @@ function runLocalBootstrap(
|
|
|
197
170
|
return runBootstrap(stigmer, prepared, say, "stigmer up");
|
|
198
171
|
}
|
|
199
172
|
|
|
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
173
|
async function freshLocalClient(): Promise<BackendClient> {
|
|
215
174
|
const [{ createBackendClient }, { getDefault }] = await Promise.all([
|
|
216
175
|
import("../client/index.js"),
|
|
@@ -72,7 +72,7 @@ export async function up(options: UpOptions = {}, home: string = homedir()): Pro
|
|
|
72
72
|
|
|
73
73
|
await waitForTcp({ port: SERVER_PORT, timeoutMs: READY_TIMEOUT_MS, label: "stigmer-server" });
|
|
74
74
|
|
|
75
|
-
await bootstrapLocalBackend(
|
|
75
|
+
await bootstrapLocalBackend();
|
|
76
76
|
|
|
77
77
|
saveStartupConfig(data, buildStartupConfig(data, logs, temporalAddress, daemonPid, options));
|
|
78
78
|
}
|
|
@@ -83,8 +83,8 @@ 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`: prepare the defaults,
|
|
87
|
-
bootstrap?: (
|
|
86
|
+
/** Post-readiness bootstrap (default: `bootstrapLocalBackend`: prepare the defaults, ensure the org, install). */
|
|
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
90
|
/** Invoked once the stack is serving and seeded — the moment a detached `up` would have returned. */
|
|
@@ -118,7 +118,7 @@ export async function upForeground(
|
|
|
118
118
|
host: new NodeProcessHost({ mirror: deps.mirror ?? mirrorToStdout }),
|
|
119
119
|
waitForShutdown: deps.waitForShutdown ?? waitForShutdownSignal,
|
|
120
120
|
onStarted: async () => {
|
|
121
|
-
await bootstrap(
|
|
121
|
+
await bootstrap();
|
|
122
122
|
saveStartupConfig(data, buildStartupConfig(data, logs, temporalAddress, process.pid, options));
|
|
123
123
|
await deps.onReady?.();
|
|
124
124
|
},
|
|
@@ -49,8 +49,8 @@ let pushes: PushPluginRequest[];
|
|
|
49
49
|
let refusing: Set<string>;
|
|
50
50
|
|
|
51
51
|
let official: string;
|
|
52
|
-
let
|
|
53
|
-
let
|
|
52
|
+
let warmerDigest: string;
|
|
53
|
+
let codeforgeDigest: string;
|
|
54
54
|
|
|
55
55
|
function pluginRow(
|
|
56
56
|
slug: string,
|
|
@@ -75,15 +75,15 @@ beforeAll(async () => {
|
|
|
75
75
|
JSON.stringify({
|
|
76
76
|
name: "stigmer",
|
|
77
77
|
plugins: [
|
|
78
|
-
{ name: "
|
|
79
|
-
{ name: "
|
|
78
|
+
{ name: "warmer", source: "./warmer" },
|
|
79
|
+
{ name: "codeforge", source: "./third_party/codeforge" },
|
|
80
80
|
],
|
|
81
|
-
defaults: ["
|
|
81
|
+
defaults: ["warmer", "codeforge"],
|
|
82
82
|
}),
|
|
83
83
|
);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
await preparePluginPush(join(official, "third_party", "
|
|
84
|
+
warmerDigest = (await preparePluginPush(join(official, "warmer"))).digest;
|
|
85
|
+
codeforgeDigest = (
|
|
86
|
+
await preparePluginPush(join(official, "third_party", "codeforge"))
|
|
87
87
|
).digest;
|
|
88
88
|
|
|
89
89
|
const routes = (router: ConnectRouter) => {
|
|
@@ -92,7 +92,7 @@ beforeAll(async () => {
|
|
|
92
92
|
pushes.push(req);
|
|
93
93
|
// The archive names the plugin: the backend knows the two trees by their digests.
|
|
94
94
|
const slug =
|
|
95
|
-
digestOf(req.artifact) ===
|
|
95
|
+
digestOf(req.artifact) === warmerDigest ? "warmer" : "codeforge";
|
|
96
96
|
if (refusing.has(slug))
|
|
97
97
|
throw new ConnectError(`refused ${slug}`, Code.FailedPrecondition);
|
|
98
98
|
const row = pluginRow(
|
|
@@ -159,8 +159,8 @@ describe("prepareDefaultPlugins", () => {
|
|
|
159
159
|
it("prepares every default in the marketplace's order without touching the backend", async () => {
|
|
160
160
|
const prepared = await prepareDefaultPlugins({ repoDir: () => official });
|
|
161
161
|
expect(prepared.map((entry) => [entry.name, entry.push.digest])).toEqual([
|
|
162
|
-
["
|
|
163
|
-
["
|
|
162
|
+
["warmer", warmerDigest],
|
|
163
|
+
["codeforge", codeforgeDigest],
|
|
164
164
|
]);
|
|
165
165
|
expect(pushes).toHaveLength(0);
|
|
166
166
|
});
|
|
@@ -189,8 +189,8 @@ describe("installPreparedDefaults", () => {
|
|
|
189
189
|
it("installs every default in order, public, with the invoking verb in the message, on a fresh backend", async () => {
|
|
190
190
|
const result = await run();
|
|
191
191
|
expect(result.outcomes.map((o) => [o.name, o.action])).toEqual([
|
|
192
|
-
["
|
|
193
|
-
["
|
|
192
|
+
["warmer", "installed"],
|
|
193
|
+
["codeforge", "installed"],
|
|
194
194
|
]);
|
|
195
195
|
expect(result.failed).toEqual([]);
|
|
196
196
|
expect(pushes.map((p) => p.org)).toEqual(["stigmer", "stigmer"]);
|
|
@@ -202,13 +202,13 @@ describe("installPreparedDefaults", () => {
|
|
|
202
202
|
expect(pushes[0]?.message).toMatch(
|
|
203
203
|
/^default plugin, installed by stigmer bootstrap \(/,
|
|
204
204
|
);
|
|
205
|
-
expect(digestOf(pushes[0]!.artifact)).toBe(
|
|
206
|
-
expect(digestOf(pushes[1]!.artifact)).toBe(
|
|
205
|
+
expect(digestOf(pushes[0]!.artifact)).toBe(warmerDigest);
|
|
206
|
+
expect(digestOf(pushes[1]!.artifact)).toBe(codeforgeDigest);
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
it("pushes nothing when every default is already held at the same digest, READY and public", async () => {
|
|
210
|
-
held.set("
|
|
211
|
-
held.set("
|
|
210
|
+
held.set("warmer", pluginRow("warmer", warmerDigest));
|
|
211
|
+
held.set("codeforge", pluginRow("codeforge", codeforgeDigest));
|
|
212
212
|
const result = await run();
|
|
213
213
|
expect(result.outcomes.map((o) => o.action)).toEqual([
|
|
214
214
|
"up-to-date",
|
|
@@ -218,8 +218,8 @@ describe("installPreparedDefaults", () => {
|
|
|
218
218
|
});
|
|
219
219
|
|
|
220
220
|
it("pushes again on a changed digest, a non-READY state, or a non-public visibility", async () => {
|
|
221
|
-
held.set("
|
|
222
|
-
held.set("
|
|
221
|
+
held.set("warmer", pluginRow("warmer", "f".repeat(64)));
|
|
222
|
+
held.set("codeforge", pluginRow("codeforge", codeforgeDigest, PluginState.FAILED));
|
|
223
223
|
expect((await run()).outcomes.map((o) => o.action)).toEqual([
|
|
224
224
|
"installed",
|
|
225
225
|
"installed",
|
|
@@ -228,15 +228,15 @@ describe("installPreparedDefaults", () => {
|
|
|
228
228
|
|
|
229
229
|
pushes = [];
|
|
230
230
|
held.set(
|
|
231
|
-
"
|
|
231
|
+
"warmer",
|
|
232
232
|
pluginRow(
|
|
233
|
-
"
|
|
234
|
-
|
|
233
|
+
"warmer",
|
|
234
|
+
warmerDigest,
|
|
235
235
|
PluginState.READY,
|
|
236
236
|
ApiResourceVisibility.visibility_private,
|
|
237
237
|
),
|
|
238
238
|
);
|
|
239
|
-
held.set("
|
|
239
|
+
held.set("codeforge", pluginRow("codeforge", codeforgeDigest));
|
|
240
240
|
expect((await run()).outcomes.map((o) => o.action)).toEqual([
|
|
241
241
|
"installed",
|
|
242
242
|
"up-to-date",
|
|
@@ -256,16 +256,16 @@ describe("installPreparedDefaults", () => {
|
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
it("one default failing does not stop the next, and the failure is named", async () => {
|
|
259
|
-
refusing.add("
|
|
259
|
+
refusing.add("warmer");
|
|
260
260
|
const result = await run();
|
|
261
261
|
expect(result.outcomes.map((o) => [o.name, o.action])).toEqual([
|
|
262
|
-
["
|
|
263
|
-
["
|
|
262
|
+
["warmer", "failed"],
|
|
263
|
+
["codeforge", "installed"],
|
|
264
264
|
]);
|
|
265
|
-
expect(result.failed).toEqual(["
|
|
265
|
+
expect(result.failed).toEqual(["warmer"]);
|
|
266
266
|
const failed = result.outcomes[0];
|
|
267
267
|
expect(failed?.action === "failed" && failed.error).toMatch(
|
|
268
|
-
/refused
|
|
268
|
+
/refused warmer/,
|
|
269
269
|
);
|
|
270
270
|
});
|
|
271
271
|
});
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// release defect, not a condition to install around. Installing pushes the
|
|
11
11
|
// prepared archives and is best-effort per entry: one default failing to
|
|
12
12
|
// land does not stop the next, and the caller decides how to surface the
|
|
13
|
-
// failures. The split
|
|
14
|
-
//
|
|
13
|
+
// failures. The split keeps every network read (the catalogue, the
|
|
14
|
+
// archives) ahead of the first write to the backend (local/bootstrap.ts).
|
|
15
15
|
//
|
|
16
16
|
// Idempotent by the server's own identity, not a marker file: before each
|
|
17
17
|
// push the org's plugin of that name is read back; when its digest equals the
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
// is missing, so every suite sees an offered list and a dropped entry. It
|
|
5
5
|
// names itself `acme-plugins`: the vendors' own names are built into every
|
|
6
6
|
// client, so a fixture under one of them would meet the reserved-name
|
|
7
|
-
// refusal instead of being added.
|
|
7
|
+
// refusal instead of being added. Its plugins carry names no real catalogue
|
|
8
|
+
// offers (`warmer`, `codeforge`): a bare-name install searches the official
|
|
9
|
+
// catalogue too, which in a checkout is the live `plugins/` tree, and a
|
|
10
|
+
// fixture named after a real plugin would collide with it as the catalogue
|
|
11
|
+
// grows.
|
|
8
12
|
|
|
9
13
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
14
|
import { dirname, join } from "node:path";
|
|
@@ -24,41 +28,41 @@ export function writeCursorMarketplace(root: string): void {
|
|
|
24
28
|
metadata: { description: "A test catalogue" },
|
|
25
29
|
plugins: [
|
|
26
30
|
{
|
|
27
|
-
name: "
|
|
28
|
-
source: "
|
|
31
|
+
name: "warmer",
|
|
32
|
+
source: "warmer",
|
|
29
33
|
description: "Keeps things warm.",
|
|
30
34
|
},
|
|
31
|
-
{ name: "
|
|
35
|
+
{ name: "codeforge", source: "third_party/codeforge" },
|
|
32
36
|
{ name: "ghost", source: "ghost", description: "Listed, not present." },
|
|
33
37
|
],
|
|
34
38
|
}),
|
|
35
39
|
);
|
|
36
40
|
write(
|
|
37
|
-
"
|
|
41
|
+
"warmer/.cursor-plugin/plugin.json",
|
|
38
42
|
JSON.stringify({
|
|
39
|
-
name: "
|
|
43
|
+
name: "warmer",
|
|
40
44
|
version: "1.0.0",
|
|
41
|
-
description: "The
|
|
45
|
+
description: "The warmer plugin.",
|
|
42
46
|
skills: "./skills/",
|
|
43
47
|
}),
|
|
44
48
|
);
|
|
45
49
|
write(
|
|
46
|
-
"
|
|
50
|
+
"warmer/skills/warm/SKILL.md",
|
|
47
51
|
"---\nname: warm\ndescription: Warms.\n---\nKeep it warm.\n",
|
|
48
52
|
);
|
|
49
53
|
write(
|
|
50
|
-
"third_party/
|
|
54
|
+
"third_party/codeforge/.cursor-plugin/plugin.json",
|
|
51
55
|
JSON.stringify({
|
|
52
|
-
name: "
|
|
56
|
+
name: "codeforge",
|
|
53
57
|
version: "2.1.0",
|
|
54
58
|
mcpServers: "./mcp.json",
|
|
55
59
|
}),
|
|
56
60
|
);
|
|
57
61
|
write(
|
|
58
|
-
"third_party/
|
|
62
|
+
"third_party/codeforge/mcp.json",
|
|
59
63
|
JSON.stringify({
|
|
60
64
|
mcpServers: {
|
|
61
|
-
|
|
65
|
+
codeforge: { type: "http", url: "https://api.githubcopilot.com/mcp/" },
|
|
62
66
|
},
|
|
63
67
|
}),
|
|
64
68
|
);
|
|
@@ -48,26 +48,18 @@ function writeConfig(yaml: string): void {
|
|
|
48
48
|
writeFileSync(configPath(), yaml);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const BUILT_IN_NAMES = [
|
|
52
|
-
OFFICIAL_MARKETPLACE_NAME,
|
|
53
|
-
"cursor-plugins",
|
|
54
|
-
"claude-code-plugins",
|
|
55
|
-
"codex-plugins",
|
|
56
|
-
];
|
|
51
|
+
const BUILT_IN_NAMES = [OFFICIAL_MARKETPLACE_NAME];
|
|
57
52
|
|
|
58
53
|
describe("listMarketplaces", () => {
|
|
59
|
-
it("is the built-in
|
|
54
|
+
it("is the built-in source alone on a fresh machine, the official catalogue", () => {
|
|
60
55
|
const listing = listMarketplaces();
|
|
61
56
|
expect(listing.known.map((m) => m.name)).toEqual(BUILT_IN_NAMES);
|
|
62
57
|
expect(listing.known[0]?.source).toEqual({ type: "official" });
|
|
63
|
-
expect(listing.known
|
|
64
|
-
type: "github",
|
|
65
|
-
repo: "cursor/plugins",
|
|
66
|
-
});
|
|
58
|
+
expect(listing.known).toHaveLength(1);
|
|
67
59
|
expect(listing.unreadable).toEqual([]);
|
|
68
60
|
});
|
|
69
61
|
|
|
70
|
-
it("lists configured entries after the built-
|
|
62
|
+
it("lists configured entries after the built-in, in file order, names the unreadable ones, and lets the built-in win over a same-named entry", () => {
|
|
71
63
|
writeConfig(
|
|
72
64
|
[
|
|
73
65
|
"backend:",
|
|
@@ -76,8 +68,8 @@ describe("listMarketplaces", () => {
|
|
|
76
68
|
" acme:",
|
|
77
69
|
" type: github",
|
|
78
70
|
" repo: acme/plugins",
|
|
79
|
-
//
|
|
80
|
-
"
|
|
71
|
+
// A hand edit under the built-in's name: the built-in wins, the entry stays in the file.
|
|
72
|
+
" stigmer:",
|
|
81
73
|
" type: github",
|
|
82
74
|
" repo: someone/fork",
|
|
83
75
|
" pinned:",
|
|
@@ -102,23 +94,20 @@ describe("listMarketplaces", () => {
|
|
|
102
94
|
"pinned",
|
|
103
95
|
"team",
|
|
104
96
|
]);
|
|
105
|
-
expect(listing.known[
|
|
97
|
+
expect(listing.known[0]?.source).toEqual({ type: "official" });
|
|
98
|
+
expect(listing.known[1]?.source).toEqual({
|
|
106
99
|
type: "github",
|
|
107
100
|
repo: "acme/plugins",
|
|
108
101
|
});
|
|
109
|
-
expect(listing.known[
|
|
102
|
+
expect(listing.known[2]?.source).toEqual({
|
|
110
103
|
type: "github",
|
|
111
104
|
repo: "anthropics/claude-code",
|
|
112
105
|
ref: "v1.2.3",
|
|
113
106
|
});
|
|
114
|
-
expect(listing.known[
|
|
107
|
+
expect(listing.known[3]?.source).toEqual({
|
|
115
108
|
type: "local",
|
|
116
109
|
path: "/srv/plugins",
|
|
117
110
|
});
|
|
118
|
-
expect(listing.known[1]?.source).toEqual({
|
|
119
|
-
type: "github",
|
|
120
|
-
repo: "cursor/plugins",
|
|
121
|
-
});
|
|
122
111
|
expect(listing.unreadable).toEqual([
|
|
123
112
|
{
|
|
124
113
|
name: "broken",
|
|
@@ -173,13 +162,11 @@ describe("addMarketplace and removeMarketplace", () => {
|
|
|
173
162
|
});
|
|
174
163
|
});
|
|
175
164
|
|
|
176
|
-
it("refuses
|
|
165
|
+
it("refuses the built-in name, a duplicate, an unknown removal, and the built-in's removal; a vendor's name is free", () => {
|
|
177
166
|
expect(() =>
|
|
178
167
|
addMarketplace("stigmer", { type: "local", path: "/x" }),
|
|
179
168
|
).toThrow(/'stigmer' is a built-in source and cannot be added or replaced/);
|
|
180
|
-
|
|
181
|
-
addMarketplace("cursor-plugins", { type: "local", path: "/x" }),
|
|
182
|
-
).toThrow(/'cursor-plugins' is a built-in source/);
|
|
169
|
+
addMarketplace("cursor-plugins", { type: "github", repo: "cursor/plugins" });
|
|
183
170
|
addMarketplace("team", { type: "local", path: "/srv/plugins" });
|
|
184
171
|
expect(() =>
|
|
185
172
|
addMarketplace("team", { type: "local", path: "/other" }),
|
|
@@ -190,9 +177,6 @@ describe("addMarketplace and removeMarketplace", () => {
|
|
|
190
177
|
expect(() => removeMarketplace("stigmer")).toThrow(
|
|
191
178
|
/'stigmer' is a built-in source and cannot be removed/,
|
|
192
179
|
);
|
|
193
|
-
expect(() => removeMarketplace("codex-plugins")).toThrow(
|
|
194
|
-
/'codex-plugins' is a built-in source and cannot be removed/,
|
|
195
|
-
);
|
|
196
180
|
});
|
|
197
181
|
|
|
198
182
|
it("refuses under --standalone instead of writing a default file over the user's", () => {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
// The sources a CLI knows: the built-in
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The sources a CLI knows: the built-in one (the official catalogue, the same
|
|
2
|
+
// list the console ships) and the ones a user added under the `marketplaces`
|
|
3
|
+
// key of ~/.stigmer/config.yaml.
|
|
4
4
|
//
|
|
5
5
|
// A source is client-side configuration, never a server resource: the
|
|
6
6
|
// server only ever sees the plugin archive a client pushes. That is why the
|
|
7
7
|
// list lives beside the named backends and the context, and why `stigmer
|
|
8
|
-
// marketplace` is a noun group like `config` and `auth`. The built-
|
|
9
|
-
// code, not config:
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// marketplace` is a noun group like `config` and `auth`. The built-in is
|
|
9
|
+
// code, not config: it cannot be added over or removed, so `stigmer install
|
|
10
|
+
// assistant` works on a fresh machine and the CLI's list never drifts from
|
|
11
|
+
// the console's. A vendor's public repository is a source the user adds by
|
|
12
|
+
// name (`stigmer marketplace add cursor/plugins`), never one on offer by
|
|
13
|
+
// default: the official catalogue already carries what Stigmer can offer of
|
|
14
|
+
// it, curated (`plugins/README.md`).
|
|
12
15
|
//
|
|
13
16
|
// The config module keeps the on-disk entry loose (`MarketplaceEntryConfig`,
|
|
14
17
|
// every field optional) so a hand-edited entry this CLI cannot read survives
|
|
@@ -82,8 +85,9 @@ export function listMarketplaces(
|
|
|
82
85
|
const known: KnownMarketplace[] = [...BUILT_IN_SOURCES];
|
|
83
86
|
const unreadable: UnreadableMarketplace[] = [];
|
|
84
87
|
for (const [name, entry] of Object.entries(config.marketplaces ?? {})) {
|
|
85
|
-
// A config
|
|
86
|
-
// built-in wins and the entry is left
|
|
88
|
+
// A config may name the built-in (a hand edit, or a file written by a
|
|
89
|
+
// CLI whose built-ins differed); the built-in wins and the entry is left
|
|
90
|
+
// in the file untouched.
|
|
87
91
|
if (isBuiltInMarketplaceName(name)) continue;
|
|
88
92
|
const narrowed = narrowEntry(entry);
|
|
89
93
|
if (narrowed.ok) known.push({ name, source: narrowed.source });
|