@stigmer/cli 3.15.3-dev.20260916211208 → 3.16.1-dev.20260917224426
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/agent-exec-flags.d.ts +2 -5
- package/commands/agent-exec-flags.d.ts.map +1 -1
- package/commands/agent-exec-flags.js +6 -13
- package/commands/agent-exec-flags.js.map +1 -1
- package/commands/connect.js +1 -1
- package/commands/connect.js.map +1 -1
- package/commands/install.d.ts +3 -0
- package/commands/install.d.ts.map +1 -0
- package/commands/install.js +68 -0
- package/commands/install.js.map +1 -0
- package/commands/marketplace.d.ts +3 -0
- package/commands/marketplace.d.ts.map +1 -0
- package/commands/marketplace.js +94 -0
- package/commands/marketplace.js.map +1 -0
- package/commands/push.js +2 -7
- package/commands/push.js.map +1 -1
- package/commands/run.js +0 -3
- package/commands/run.js.map +1 -1
- package/config/config.d.ts +19 -0
- package/config/config.d.ts.map +1 -1
- package/config/config.js +15 -0
- package/config/config.js.map +1 -1
- package/config/index.d.ts +1 -1
- package/config/index.d.ts.map +1 -1
- package/config/index.js.map +1 -1
- package/local/bootstrap.d.ts +21 -0
- package/local/bootstrap.d.ts.map +1 -0
- package/local/bootstrap.js +88 -0
- package/local/bootstrap.js.map +1 -0
- package/local/daemon/launch.d.ts +2 -2
- package/local/daemon/launch.d.ts.map +1 -1
- package/local/daemon/launch.js +4 -34
- package/local/daemon/launch.js.map +1 -1
- package/local/daemon/process.d.ts +1 -1
- package/local/plugins/defaults.d.ts +35 -0
- package/local/plugins/defaults.d.ts.map +1 -0
- package/local/plugins/defaults.js +83 -0
- package/local/plugins/defaults.js.map +1 -0
- package/local/seedpack/content.d.ts +2 -1
- package/local/seedpack/content.d.ts.map +1 -1
- package/local/seedpack/content.js +5 -34
- package/local/seedpack/content.js.map +1 -1
- package/marketplace/config.d.ts +59 -0
- package/marketplace/config.d.ts.map +1 -0
- package/marketplace/config.js +162 -0
- package/marketplace/config.js.map +1 -0
- package/marketplace/github.d.ts +31 -0
- package/marketplace/github.d.ts.map +1 -0
- package/marketplace/github.js +203 -0
- package/marketplace/github.js.map +1 -0
- package/marketplace/index.d.ts +8 -0
- package/marketplace/index.d.ts.map +1 -0
- package/marketplace/index.js +12 -0
- package/marketplace/index.js.map +1 -0
- package/marketplace/install.d.ts +28 -0
- package/marketplace/install.d.ts.map +1 -0
- package/marketplace/install.js +109 -0
- package/marketplace/install.js.map +1 -0
- package/marketplace/materialize.d.ts +19 -0
- package/marketplace/materialize.d.ts.map +1 -0
- package/marketplace/materialize.js +53 -0
- package/marketplace/materialize.js.map +1 -0
- package/marketplace/official.d.ts +28 -0
- package/marketplace/official.d.ts.map +1 -0
- package/marketplace/official.js +86 -0
- package/marketplace/official.js.map +1 -0
- package/marketplace/read.d.ts +17 -0
- package/marketplace/read.d.ts.map +1 -0
- package/marketplace/read.js +44 -0
- package/marketplace/read.js.map +1 -0
- package/marketplace/ref.d.ts +13 -0
- package/marketplace/ref.d.ts.map +1 -0
- package/marketplace/ref.js +54 -0
- package/marketplace/ref.js.map +1 -0
- package/marketplace/render.d.ts +22 -0
- package/marketplace/render.d.ts.map +1 -0
- package/marketplace/render.js +118 -0
- package/marketplace/render.js.map +1 -0
- package/output/ndjson.js +1 -1
- package/output/ndjson.js.map +1 -1
- package/package.json +7 -6
- package/program.d.ts.map +1 -1
- package/program.js +4 -2
- package/program.js.map +1 -1
- package/resources/ignore/index.d.ts +2 -4
- package/resources/ignore/index.d.ts.map +1 -1
- package/resources/ignore/index.js +5 -5
- package/resources/ignore/index.js.map +1 -1
- package/resources/ignore/matcher.d.ts +2 -35
- package/resources/ignore/matcher.d.ts.map +1 -1
- package/resources/ignore/matcher.js +25 -109
- package/resources/ignore/matcher.js.map +1 -1
- package/resources/plugin.d.ts +50 -18
- package/resources/plugin.d.ts.map +1 -1
- package/resources/plugin.js +73 -74
- package/resources/plugin.js.map +1 -1
- package/resources/run/agent-exec.js +1 -1
- package/resources/run/agent-exec.js.map +1 -1
- package/resources/run/prepare.d.ts +8 -19
- package/resources/run/prepare.d.ts.map +1 -1
- package/resources/run/prepare.js +4 -4
- package/resources/run/prepare.js.map +1 -1
- package/resources/skill.d.ts +2 -2
- package/resources/skill.d.ts.map +1 -1
- package/resources/skill.js +6 -10
- package/resources/skill.js.map +1 -1
- package/src/commands/agent-exec-flags.ts +6 -12
- package/src/commands/connect.ts +1 -1
- package/src/commands/install.test.ts +289 -0
- package/src/commands/install.ts +108 -0
- package/src/commands/marketplace.test.ts +200 -0
- package/src/commands/marketplace.ts +131 -0
- package/src/commands/push.ts +5 -10
- package/src/commands/run.ts +0 -3
- package/src/config/config.ts +37 -0
- package/src/config/index.ts +1 -0
- package/src/local/bootstrap.test.ts +71 -0
- package/src/local/bootstrap.ts +133 -0
- package/src/local/daemon/launch.ts +5 -40
- package/src/local/daemon/process.ts +1 -1
- package/src/local/plugins/defaults.test.ts +238 -0
- package/src/local/plugins/defaults.ts +135 -0
- package/src/local/seedpack/content.ts +11 -42
- package/src/marketplace/__fixtures__/cursor-marketplace.ts +62 -0
- package/src/marketplace/config.test.ts +245 -0
- package/src/marketplace/config.ts +236 -0
- package/src/marketplace/github.test.ts +318 -0
- package/src/marketplace/github.ts +296 -0
- package/src/marketplace/index.ts +54 -0
- package/src/marketplace/install.test.ts +194 -0
- package/src/marketplace/install.ts +159 -0
- package/src/marketplace/materialize.ts +80 -0
- package/src/marketplace/official.test.ts +107 -0
- package/src/marketplace/official.ts +125 -0
- package/src/marketplace/read.test.ts +94 -0
- package/src/marketplace/read.ts +76 -0
- package/src/marketplace/ref.test.ts +179 -0
- package/src/marketplace/ref.ts +74 -0
- package/src/marketplace/render.ts +154 -0
- package/src/output/ndjson.ts +1 -1
- package/src/program.test.ts +4 -13
- package/src/program.ts +4 -2
- package/src/resources/ignore/ignore.test.ts +23 -100
- package/src/resources/ignore/index.ts +11 -8
- package/src/resources/ignore/matcher.ts +33 -136
- package/src/resources/plugin.test.ts +35 -6
- package/src/resources/plugin.ts +132 -85
- package/src/resources/run/agent-exec.ts +1 -1
- package/src/resources/run/prepare.test.ts +4 -18
- package/src/resources/run/prepare.ts +12 -24
- package/src/resources/skill.ts +6 -10
- package/commands/draft.d.ts +0 -3
- package/commands/draft.d.ts.map +0 -1
- package/commands/draft.js +0 -115
- package/commands/draft.js.map +0 -1
- package/resources/ignore/defaults.d.ts +0 -2
- package/resources/ignore/defaults.d.ts.map +0 -1
- package/resources/ignore/defaults.js +0 -140
- package/resources/ignore/defaults.js.map +0 -1
- package/resources/ignore/match.d.ts +0 -3
- package/resources/ignore/match.d.ts.map +0 -1
- package/resources/ignore/match.js +0 -166
- package/resources/ignore/match.js.map +0 -1
- package/resources/ignore/pattern.d.ts +0 -13
- package/resources/ignore/pattern.d.ts.map +0 -1
- package/resources/ignore/pattern.js +0 -118
- package/resources/ignore/pattern.js.map +0 -1
- package/src/commands/draft.ts +0 -189
- package/src/resources/ignore/defaults.ts +0 -155
- package/src/resources/ignore/match.ts +0 -158
- package/src/resources/ignore/pattern.ts +0 -131
|
@@ -0,0 +1,238 @@
|
|
|
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
|
|
6
|
+
// second run over a converged backend pushes nothing.
|
|
7
|
+
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import {
|
|
11
|
+
createServer as createHttp2Server,
|
|
12
|
+
type Http2Server,
|
|
13
|
+
type ServerHttp2Session,
|
|
14
|
+
} from "node:http2";
|
|
15
|
+
import type { AddressInfo } from "node:net";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { create } from "@bufbuild/protobuf";
|
|
19
|
+
import { Code, ConnectError, type ConnectRouter } from "@connectrpc/connect";
|
|
20
|
+
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
21
|
+
import {
|
|
22
|
+
type Plugin,
|
|
23
|
+
PluginSchema,
|
|
24
|
+
} from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/api_pb";
|
|
25
|
+
import { PluginCommandController } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/command_pb";
|
|
26
|
+
import {
|
|
27
|
+
ListPluginMembersResponseSchema,
|
|
28
|
+
type PushPluginRequest,
|
|
29
|
+
} from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/io_pb";
|
|
30
|
+
import { PluginQueryController } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/query_pb";
|
|
31
|
+
import { PluginState } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/status_pb";
|
|
32
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
33
|
+
import { createNodeClient, normalizeEndpoint } from "@stigmer/sdk/node";
|
|
34
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
35
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
36
|
+
import { writeCursorMarketplace } from "../../marketplace/__fixtures__/cursor-marketplace.js";
|
|
37
|
+
import { preparePluginPush } from "../../resources/plugin.js";
|
|
38
|
+
import { installDefaultPlugins } from "./defaults.js";
|
|
39
|
+
|
|
40
|
+
let backend: Http2Server;
|
|
41
|
+
let stigmer: Stigmer;
|
|
42
|
+
const openSessions = new Set<ServerHttp2Session>();
|
|
43
|
+
|
|
44
|
+
/** The backend's plugins by slug; tests seed it and pushes converge it. */
|
|
45
|
+
let held: Map<string, Plugin>;
|
|
46
|
+
let pushes: PushPluginRequest[];
|
|
47
|
+
/** Slugs whose push the backend refuses, to prove one failure does not stop the next. */
|
|
48
|
+
let refusing: Set<string>;
|
|
49
|
+
|
|
50
|
+
let official: string;
|
|
51
|
+
let thermosDigest: string;
|
|
52
|
+
let githubDigest: string;
|
|
53
|
+
|
|
54
|
+
function pluginRow(
|
|
55
|
+
slug: string,
|
|
56
|
+
digest: string,
|
|
57
|
+
state = PluginState.READY,
|
|
58
|
+
visibility = ApiResourceVisibility.visibility_public,
|
|
59
|
+
): Plugin {
|
|
60
|
+
return create(PluginSchema, {
|
|
61
|
+
metadata: { id: `plg_${slug}`, slug, org: "stigmer", visibility },
|
|
62
|
+
spec: { name: slug },
|
|
63
|
+
status: { digest, state },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
beforeAll(async () => {
|
|
68
|
+
official = mkdtempSync(join(tmpdir(), "stigmer-defaults-official-"));
|
|
69
|
+
writeCursorMarketplace(official);
|
|
70
|
+
// The fixture is Cursor-dialect (no `defaults`); the official tree is Stigmer's, so write that file over it.
|
|
71
|
+
rmSync(join(official, ".cursor-plugin"), { recursive: true, force: true });
|
|
72
|
+
writeFileSync(
|
|
73
|
+
join(official, "marketplace.json"),
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
name: "stigmer",
|
|
76
|
+
plugins: [
|
|
77
|
+
{ name: "thermos", source: "./thermos" },
|
|
78
|
+
{ name: "github", source: "./third_party/github" },
|
|
79
|
+
],
|
|
80
|
+
defaults: ["thermos", "github"],
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
thermosDigest = (await preparePluginPush(join(official, "thermos"))).digest;
|
|
84
|
+
githubDigest = (
|
|
85
|
+
await preparePluginPush(join(official, "third_party", "github"))
|
|
86
|
+
).digest;
|
|
87
|
+
|
|
88
|
+
const routes = (router: ConnectRouter) => {
|
|
89
|
+
router.service(PluginCommandController, {
|
|
90
|
+
push: (req) => {
|
|
91
|
+
pushes.push(req);
|
|
92
|
+
// The archive names the plugin: the backend knows the two trees by their digests.
|
|
93
|
+
const slug =
|
|
94
|
+
digestOf(req.artifact) === thermosDigest ? "thermos" : "github";
|
|
95
|
+
if (refusing.has(slug))
|
|
96
|
+
throw new ConnectError(`refused ${slug}`, Code.FailedPrecondition);
|
|
97
|
+
const row = pluginRow(
|
|
98
|
+
slug,
|
|
99
|
+
digestOf(req.artifact),
|
|
100
|
+
PluginState.READY,
|
|
101
|
+
req.visibility,
|
|
102
|
+
);
|
|
103
|
+
held.set(slug, row);
|
|
104
|
+
return row;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
router.service(PluginQueryController, {
|
|
108
|
+
getByReference: (ref) => {
|
|
109
|
+
const row = held.get(ref.slug);
|
|
110
|
+
if (row === undefined)
|
|
111
|
+
throw new ConnectError(`plugin ${ref.slug} not found`, Code.NotFound);
|
|
112
|
+
return row;
|
|
113
|
+
},
|
|
114
|
+
listMembers: () =>
|
|
115
|
+
create(ListPluginMembersResponseSchema, { members: [] }),
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
119
|
+
backend.on("session", (session) => {
|
|
120
|
+
openSessions.add(session);
|
|
121
|
+
session.on("close", () => openSessions.delete(session));
|
|
122
|
+
});
|
|
123
|
+
await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
|
|
124
|
+
const port = (backend.address() as AddressInfo).port;
|
|
125
|
+
stigmer = createNodeClient({
|
|
126
|
+
baseUrl: normalizeEndpoint(`127.0.0.1:${port}`),
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
afterAll(async () => {
|
|
131
|
+
for (const session of openSessions) session.destroy();
|
|
132
|
+
await new Promise<void>((resolve) => backend.close(() => resolve()));
|
|
133
|
+
rmSync(official, { recursive: true, force: true });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
held = new Map();
|
|
138
|
+
pushes = [];
|
|
139
|
+
refusing = new Set();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
function digestOf(bytes: Uint8Array): string {
|
|
143
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const say = (): void => {};
|
|
147
|
+
|
|
148
|
+
function run() {
|
|
149
|
+
return installDefaultPlugins(
|
|
150
|
+
{ stigmer, info: say },
|
|
151
|
+
{ org: "stigmer", official: { repoDir: () => official } },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
describe("installDefaultPlugins", () => {
|
|
156
|
+
it("installs every default in order, public, with the CLI's message, on a fresh backend", async () => {
|
|
157
|
+
const result = await run();
|
|
158
|
+
expect(result.outcomes.map((o) => [o.name, o.action])).toEqual([
|
|
159
|
+
["thermos", "installed"],
|
|
160
|
+
["github", "installed"],
|
|
161
|
+
]);
|
|
162
|
+
expect(result.failed).toEqual([]);
|
|
163
|
+
expect(pushes.map((p) => p.org)).toEqual(["stigmer", "stigmer"]);
|
|
164
|
+
expect(
|
|
165
|
+
pushes.every(
|
|
166
|
+
(p) => p.visibility === ApiResourceVisibility.visibility_public,
|
|
167
|
+
),
|
|
168
|
+
).toBe(true);
|
|
169
|
+
expect(pushes[0]?.message).toMatch(
|
|
170
|
+
/^default plugin, installed by stigmer up \(/,
|
|
171
|
+
);
|
|
172
|
+
expect(digestOf(pushes[0]!.artifact)).toBe(thermosDigest);
|
|
173
|
+
expect(digestOf(pushes[1]!.artifact)).toBe(githubDigest);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("pushes nothing when every default is already held at the same digest, READY and public", async () => {
|
|
177
|
+
held.set("thermos", pluginRow("thermos", thermosDigest));
|
|
178
|
+
held.set("github", pluginRow("github", githubDigest));
|
|
179
|
+
const result = await run();
|
|
180
|
+
expect(result.outcomes.map((o) => o.action)).toEqual([
|
|
181
|
+
"up-to-date",
|
|
182
|
+
"up-to-date",
|
|
183
|
+
]);
|
|
184
|
+
expect(pushes).toHaveLength(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("pushes again on a changed digest, a non-READY state, or a non-public visibility", async () => {
|
|
188
|
+
held.set("thermos", pluginRow("thermos", "f".repeat(64)));
|
|
189
|
+
held.set("github", pluginRow("github", githubDigest, PluginState.FAILED));
|
|
190
|
+
expect((await run()).outcomes.map((o) => o.action)).toEqual([
|
|
191
|
+
"installed",
|
|
192
|
+
"installed",
|
|
193
|
+
]);
|
|
194
|
+
expect(pushes).toHaveLength(2);
|
|
195
|
+
|
|
196
|
+
pushes = [];
|
|
197
|
+
held.set(
|
|
198
|
+
"thermos",
|
|
199
|
+
pluginRow(
|
|
200
|
+
"thermos",
|
|
201
|
+
thermosDigest,
|
|
202
|
+
PluginState.READY,
|
|
203
|
+
ApiResourceVisibility.visibility_private,
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
held.set("github", pluginRow("github", githubDigest));
|
|
207
|
+
expect((await run()).outcomes.map((o) => o.action)).toEqual([
|
|
208
|
+
"installed",
|
|
209
|
+
"up-to-date",
|
|
210
|
+
]);
|
|
211
|
+
expect(pushes).toHaveLength(1);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("a second run over a backend the first converged pushes nothing", async () => {
|
|
215
|
+
await run();
|
|
216
|
+
pushes = [];
|
|
217
|
+
const result = await run();
|
|
218
|
+
expect(result.outcomes.map((o) => o.action)).toEqual([
|
|
219
|
+
"up-to-date",
|
|
220
|
+
"up-to-date",
|
|
221
|
+
]);
|
|
222
|
+
expect(pushes).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("one default failing does not stop the next, and the failure is named", async () => {
|
|
226
|
+
refusing.add("thermos");
|
|
227
|
+
const result = await run();
|
|
228
|
+
expect(result.outcomes.map((o) => [o.name, o.action])).toEqual([
|
|
229
|
+
["thermos", "failed"],
|
|
230
|
+
["github", "installed"],
|
|
231
|
+
]);
|
|
232
|
+
expect(result.failed).toEqual(["thermos"]);
|
|
233
|
+
const failed = result.outcomes[0];
|
|
234
|
+
expect(failed?.action === "failed" && failed.error).toMatch(
|
|
235
|
+
/refused thermos/,
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
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.
|
|
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).
|
|
14
|
+
//
|
|
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>`).
|
|
18
|
+
|
|
19
|
+
import { type Stigmer, isNotFound } from "@stigmer/sdk";
|
|
20
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
21
|
+
import { PluginState } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/status_pb";
|
|
22
|
+
import { log } from "../../logger.js";
|
|
23
|
+
import { VERSION } from "../../version.js";
|
|
24
|
+
import {
|
|
25
|
+
type ResolveOfficialOptions,
|
|
26
|
+
resolveOfficialMarketplace,
|
|
27
|
+
} from "../../marketplace/official.js";
|
|
28
|
+
import { readMarketplaceTree } from "../../marketplace/read.js";
|
|
29
|
+
import { prepareEntry } from "../../marketplace/install.js";
|
|
30
|
+
import {
|
|
31
|
+
type PreparedPluginPush,
|
|
32
|
+
pushPrepared,
|
|
33
|
+
} from "../../resources/plugin.js";
|
|
34
|
+
|
|
35
|
+
export interface DefaultPluginsDeps {
|
|
36
|
+
/** The client bound to the backend being bootstrapped. */
|
|
37
|
+
readonly stigmer: Stigmer;
|
|
38
|
+
/** Human progress lines (stderr). */
|
|
39
|
+
readonly info: (line: string) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
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;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type DefaultPluginOutcome =
|
|
50
|
+
| {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly action: "installed";
|
|
53
|
+
readonly digest: string;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly action: "up-to-date";
|
|
58
|
+
readonly digest: string;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly action: "failed";
|
|
63
|
+
readonly error: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface DefaultPluginsResult {
|
|
67
|
+
readonly outcomes: readonly DefaultPluginOutcome[];
|
|
68
|
+
/** The names that did not land, for the caller's warning. */
|
|
69
|
+
readonly failed: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Install every default the official marketplace names, in order, skipping what the backend already holds. */
|
|
73
|
+
export async function installDefaultPlugins(
|
|
74
|
+
deps: DefaultPluginsDeps,
|
|
75
|
+
options: DefaultPluginsOptions,
|
|
76
|
+
): Promise<DefaultPluginsResult> {
|
|
77
|
+
const official = resolveOfficialMarketplace(options.official);
|
|
78
|
+
const tree = readMarketplaceTree(official.dir, "the official marketplace");
|
|
79
|
+
|
|
80
|
+
const outcomes: DefaultPluginOutcome[] = [];
|
|
81
|
+
for (const name of tree.marketplace.defaults) {
|
|
82
|
+
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 });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
deps.info(`Installing default plugin '${name}'…`);
|
|
89
|
+
await pushPrepared(deps.stigmer, prepared, {
|
|
90
|
+
org: options.org,
|
|
91
|
+
visibility: ApiResourceVisibility.visibility_public,
|
|
92
|
+
message: `default plugin, installed by stigmer up (${VERSION})`,
|
|
93
|
+
});
|
|
94
|
+
outcomes.push({ name, action: "installed", digest: prepared.digest });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
log.warn("default plugin install failed", { name, error: message });
|
|
98
|
+
outcomes.push({ name, action: "failed", error: message });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
outcomes,
|
|
103
|
+
failed: outcomes
|
|
104
|
+
.filter((outcome) => outcome.action === "failed")
|
|
105
|
+
.map((outcome) => outcome.name),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* True when the org already holds this exact archive, materialised and
|
|
111
|
+
* public. A plugin that is absent (NOT_FOUND) installs; one that is present
|
|
112
|
+
* with another digest, still installing, failed, or not public is pushed
|
|
113
|
+
* again, which the server treats as an upgrade or a convergence.
|
|
114
|
+
*/
|
|
115
|
+
async function isAlreadyInstalled(
|
|
116
|
+
stigmer: Stigmer,
|
|
117
|
+
org: string,
|
|
118
|
+
prepared: PreparedPluginPush,
|
|
119
|
+
): Promise<boolean> {
|
|
120
|
+
let existing;
|
|
121
|
+
try {
|
|
122
|
+
existing = await stigmer.plugin.getByReference({
|
|
123
|
+
org,
|
|
124
|
+
slug: prepared.plugin.name,
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (isNotFound(error)) return false;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
return (
|
|
131
|
+
existing.status?.digest === prepared.digest &&
|
|
132
|
+
existing.status.state === PluginState.READY &&
|
|
133
|
+
existing.metadata?.visibility === ApiResourceVisibility.visibility_public
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
// package in the lean install (where the package is not a dependency, only an
|
|
18
18
|
// on-demand artifact). `seedpack/src/index.ts` is the one definition both follow.
|
|
19
19
|
|
|
20
|
-
import { execFileSync } from "node:child_process";
|
|
21
20
|
import { createHash } from "node:crypto";
|
|
22
21
|
import {
|
|
23
22
|
cpSync,
|
|
@@ -36,7 +35,12 @@ import { ExitCode } from "../../errors/exit-codes.js";
|
|
|
36
35
|
import { log } from "../../logger.js";
|
|
37
36
|
import { VERSION } from "../../version.js";
|
|
38
37
|
import { runtimesDir } from "../paths.js";
|
|
39
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
ensureRuntimesRoot,
|
|
40
|
+
isAcquirableRelease,
|
|
41
|
+
npmInstallIntoRuntimes,
|
|
42
|
+
type NpmInstall,
|
|
43
|
+
} from "../runtime/runtimes-install.js";
|
|
40
44
|
|
|
41
45
|
const SEEDPACK_PACKAGE = "@stigmer/seedpack";
|
|
42
46
|
|
|
@@ -71,7 +75,7 @@ export interface ResolveSeedpackOptions {
|
|
|
71
75
|
/** Release version of @stigmer/seedpack to acquire (defaults to the CLI version). */
|
|
72
76
|
version?: string;
|
|
73
77
|
/** npm install implementation (injectable for tests). */
|
|
74
|
-
install?:
|
|
78
|
+
install?: NpmInstall;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
/**
|
|
@@ -117,17 +121,10 @@ export function acquireSeedpack(opts: ResolveSeedpackOptions = {}): string {
|
|
|
117
121
|
);
|
|
118
122
|
}
|
|
119
123
|
log.info(`acquiring ${SEEDPACK_PACKAGE}`, { version, dir: installDir });
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
if (!existsSync(rootPkg)) {
|
|
125
|
-
writeFileSync(
|
|
126
|
-
rootPkg,
|
|
127
|
-
`${JSON.stringify({ name: "stigmer-runtime", private: true, version: "0.0.0" }, null, 2)}\n`,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
const install = opts.install ?? installSeedpack;
|
|
124
|
+
// The same per-version root the server and runner packages install into;
|
|
125
|
+
// npm install is additive and non-pruning, so the prefix is safely shared.
|
|
126
|
+
ensureRuntimesRoot(installDir);
|
|
127
|
+
const install = opts.install ?? npmInstallIntoRuntimes;
|
|
131
128
|
install(installDir, `${SEEDPACK_PACKAGE}@${version}`);
|
|
132
129
|
}
|
|
133
130
|
|
|
@@ -141,34 +138,6 @@ export function acquireSeedpack(opts: ResolveSeedpackOptions = {}): string {
|
|
|
141
138
|
return pkgDir;
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
// Install the seedpack package into an isolated prefix. Output is inherited so
|
|
145
|
-
// the user sees the one-time download progress.
|
|
146
|
-
function installSeedpack(installDir: string, spec: string): void {
|
|
147
|
-
try {
|
|
148
|
-
execFileSync(
|
|
149
|
-
"npm",
|
|
150
|
-
[
|
|
151
|
-
"install",
|
|
152
|
-
spec,
|
|
153
|
-
"--prefix",
|
|
154
|
-
installDir,
|
|
155
|
-
"--omit=dev",
|
|
156
|
-
"--no-audit",
|
|
157
|
-
"--no-fund",
|
|
158
|
-
],
|
|
159
|
-
{
|
|
160
|
-
stdio: "inherit",
|
|
161
|
-
},
|
|
162
|
-
);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
throw new CliExitError(`failed to install ${spec}`, ExitCode.General, [
|
|
165
|
-
`Command: npm install ${spec} --prefix ${installDir}`,
|
|
166
|
-
"Ensure npm is on PATH and the network is reachable.",
|
|
167
|
-
String(err),
|
|
168
|
-
]);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
141
|
/**
|
|
173
142
|
* Deterministic SHA-256 over the canonical content, identical to
|
|
174
143
|
* `@stigmer/seedpack`'s `contentHash` (and the Go algorithm): files walked in
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// A small Cursor-dialect marketplace written to a directory for the
|
|
2
|
+
// marketplace suites: two plugins offered (one with a skill, one MCP-only,
|
|
3
|
+
// the second under a nested source directory) and one entry whose directory
|
|
4
|
+
// is missing, so every suite sees an offered list and a dropped entry.
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export const CURSOR_MARKETPLACE_NAME = "cursor-plugins";
|
|
10
|
+
|
|
11
|
+
export function writeCursorMarketplace(root: string): void {
|
|
12
|
+
const write = (rel: string, content: string): void => {
|
|
13
|
+
const full = join(root, rel);
|
|
14
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
15
|
+
writeFileSync(full, content);
|
|
16
|
+
};
|
|
17
|
+
write(
|
|
18
|
+
".cursor-plugin/marketplace.json",
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
name: CURSOR_MARKETPLACE_NAME,
|
|
21
|
+
metadata: { description: "A test catalogue" },
|
|
22
|
+
plugins: [
|
|
23
|
+
{
|
|
24
|
+
name: "thermos",
|
|
25
|
+
source: "thermos",
|
|
26
|
+
description: "Keeps things warm.",
|
|
27
|
+
},
|
|
28
|
+
{ name: "github", source: "third_party/github" },
|
|
29
|
+
{ name: "ghost", source: "ghost", description: "Listed, not present." },
|
|
30
|
+
],
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
write(
|
|
34
|
+
"thermos/.cursor-plugin/plugin.json",
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
name: "thermos",
|
|
37
|
+
version: "1.0.0",
|
|
38
|
+
description: "The thermos plugin.",
|
|
39
|
+
skills: "./skills/",
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
write(
|
|
43
|
+
"thermos/skills/warm/SKILL.md",
|
|
44
|
+
"---\nname: warm\ndescription: Warms.\n---\nKeep it warm.\n",
|
|
45
|
+
);
|
|
46
|
+
write(
|
|
47
|
+
"third_party/github/.cursor-plugin/plugin.json",
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
name: "github",
|
|
50
|
+
version: "2.1.0",
|
|
51
|
+
mcpServers: "./mcp.json",
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
write(
|
|
55
|
+
"third_party/github/mcp.json",
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
mcpServers: {
|
|
58
|
+
github: { type: "http", url: "https://api.githubcopilot.com/mcp/" },
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|