@danieldeusing/seedr 0.1.87 → 0.1.89
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/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/chunk-JC6HFAZU.js +1188 -0
- package/dist/cli.js +1363 -554
- package/dist/index.d.ts +18 -56
- package/dist/index.js +1 -106
- package/package.json +3 -5
- package/dist/chunk-77M7PGDO.js +0 -762
|
@@ -0,0 +1,1188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config/registry.ts
|
|
4
|
+
import { readFile as readFile3, readdir, writeFile as writeFile2, mkdir as mkdir2, mkdtemp, rm as rm2 } from "fs/promises";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { tmpdir } from "os";
|
|
7
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
// ../registry-ops/src/paths.ts
|
|
11
|
+
var ALL_TYPES = ["skill", "plugin", "hook", "agent", "mcp", "settings", "command"];
|
|
12
|
+
var KNOWN_TYPES = new Set(ALL_TYPES);
|
|
13
|
+
function typeDirName(type) {
|
|
14
|
+
return type === "settings" || type === "mcp" ? type : `${type}s`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ../registry-ops/src/agents.ts
|
|
18
|
+
var CANONICAL_AGENTS = ["claude", "copilot", "antigravity", "codex", "opencode"];
|
|
19
|
+
var AGENT_ALIASES = { gemini: "antigravity" };
|
|
20
|
+
var KNOWN_AGENTS = [...CANONICAL_AGENTS, ...Object.keys(AGENT_ALIASES)];
|
|
21
|
+
function isCanonicalAgent(value) {
|
|
22
|
+
return typeof value === "string" && CANONICAL_AGENTS.includes(value);
|
|
23
|
+
}
|
|
24
|
+
function isLegacyAgent(value) {
|
|
25
|
+
return typeof value === "string" && Object.hasOwn(AGENT_ALIASES, value);
|
|
26
|
+
}
|
|
27
|
+
function canonicalAgent(value) {
|
|
28
|
+
if (isCanonicalAgent(value)) return value;
|
|
29
|
+
if (isLegacyAgent(value)) return AGENT_ALIASES[value];
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function canonicalAgents(values) {
|
|
33
|
+
const present = new Set(values.map(canonicalAgent).filter((a) => a !== null));
|
|
34
|
+
return CANONICAL_AGENTS.filter((a) => present.has(a));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ../registry-ops/src/sourceTypes.ts
|
|
38
|
+
var CANONICAL_SOURCE_TYPES = ["official", "seedr", "community"];
|
|
39
|
+
var SOURCE_TYPE_ALIASES = { toolr: "seedr" };
|
|
40
|
+
var KNOWN_SOURCE_TYPES = [...CANONICAL_SOURCE_TYPES, ...Object.keys(SOURCE_TYPE_ALIASES)];
|
|
41
|
+
function isCanonicalSourceType(value) {
|
|
42
|
+
return typeof value === "string" && CANONICAL_SOURCE_TYPES.includes(value);
|
|
43
|
+
}
|
|
44
|
+
function isLegacySourceType(value) {
|
|
45
|
+
return typeof value === "string" && Object.hasOwn(SOURCE_TYPE_ALIASES, value);
|
|
46
|
+
}
|
|
47
|
+
function canonicalSourceType(value) {
|
|
48
|
+
if (isCanonicalSourceType(value)) return value;
|
|
49
|
+
if (isLegacySourceType(value)) return SOURCE_TYPE_ALIASES[value];
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function isFirstParty(sourceType) {
|
|
53
|
+
return canonicalSourceType(sourceType) === "seedr";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/utils/slug.ts
|
|
57
|
+
var SLUG_PATTERN2 = /^[a-z0-9][a-z0-9-]*$/;
|
|
58
|
+
var MAX_SLUG_LENGTH2 = 100;
|
|
59
|
+
function isValidSlug2(value) {
|
|
60
|
+
return typeof value === "string" && value.length > 0 && value.length <= MAX_SLUG_LENGTH2 && SLUG_PATTERN2.test(value);
|
|
61
|
+
}
|
|
62
|
+
function assertValidSlug(value, what = "item slug") {
|
|
63
|
+
if (!isValidSlug2(value)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Invalid ${what}: ${JSON.stringify(value)} (expected ${SLUG_PATTERN2.source}, at most ${MAX_SLUG_LENGTH2} characters)`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/utils/digest.ts
|
|
71
|
+
import { createHash } from "crypto";
|
|
72
|
+
import { readFile } from "fs/promises";
|
|
73
|
+
import { join } from "path";
|
|
74
|
+
function sha256Hex(bytes) {
|
|
75
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
76
|
+
}
|
|
77
|
+
function digestFromFileHashes(entries) {
|
|
78
|
+
const sorted = [...entries].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
79
|
+
let buffer = "";
|
|
80
|
+
for (const entry of sorted) {
|
|
81
|
+
buffer += `${entry.path}
|
|
82
|
+
${entry.sha256}
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
return sha256Hex(buffer);
|
|
86
|
+
}
|
|
87
|
+
async function computeContentDigest(root, relativePaths) {
|
|
88
|
+
const entries = await Promise.all(
|
|
89
|
+
relativePaths.map(async (relativePath) => ({
|
|
90
|
+
path: relativePath,
|
|
91
|
+
sha256: sha256Hex(await readFile(join(root, ...relativePath.split("/"))))
|
|
92
|
+
}))
|
|
93
|
+
);
|
|
94
|
+
return digestFromFileHashes(entries);
|
|
95
|
+
}
|
|
96
|
+
function flattenFileTree(nodes, prefix = "") {
|
|
97
|
+
const paths = [];
|
|
98
|
+
for (const node of nodes) {
|
|
99
|
+
const path = prefix ? `${prefix}/${node.name}` : node.name;
|
|
100
|
+
if (node.type === "directory") {
|
|
101
|
+
if (node.children) paths.push(...flattenFileTree(node.children, path));
|
|
102
|
+
} else {
|
|
103
|
+
paths.push(path);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return paths;
|
|
107
|
+
}
|
|
108
|
+
function canonicalFileSet(files, license) {
|
|
109
|
+
const paths = flattenFileTree(files);
|
|
110
|
+
if (license?.installAs && !paths.includes(license.installAs)) {
|
|
111
|
+
paths.push(license.installAs);
|
|
112
|
+
}
|
|
113
|
+
return paths;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/utils/fs.ts
|
|
117
|
+
import {
|
|
118
|
+
access,
|
|
119
|
+
mkdir,
|
|
120
|
+
symlink,
|
|
121
|
+
copyFile,
|
|
122
|
+
readFile as readFile2,
|
|
123
|
+
writeFile,
|
|
124
|
+
unlink,
|
|
125
|
+
readlink,
|
|
126
|
+
lstat,
|
|
127
|
+
stat,
|
|
128
|
+
realpath,
|
|
129
|
+
rename,
|
|
130
|
+
cp,
|
|
131
|
+
rm
|
|
132
|
+
} from "fs/promises";
|
|
133
|
+
import { randomBytes } from "crypto";
|
|
134
|
+
import { homedir } from "os";
|
|
135
|
+
import { basename, dirname, join as join2, relative, resolve, isAbsolute, sep } from "path";
|
|
136
|
+
async function exists(path) {
|
|
137
|
+
try {
|
|
138
|
+
await access(path);
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function ensureDir(path) {
|
|
145
|
+
await mkdir(path, { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
async function assertOverwritable(path, force) {
|
|
148
|
+
if (!force && await exists(path)) {
|
|
149
|
+
throw new Error(`${path} already exists; pass --force to overwrite`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
var SAFE_SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
153
|
+
var MAX_SEGMENT_LENGTH = 100;
|
|
154
|
+
function assertSafePathSegment(value, what) {
|
|
155
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
156
|
+
throw new Error(`${what} must be a non-empty string`);
|
|
157
|
+
}
|
|
158
|
+
if (value.length > MAX_SEGMENT_LENGTH) {
|
|
159
|
+
throw new Error(`${what} is too long (${value.length} characters, maximum ${MAX_SEGMENT_LENGTH})`);
|
|
160
|
+
}
|
|
161
|
+
if (value.includes("..") || !SAFE_SEGMENT_PATTERN.test(value)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Unsafe ${what}: ${JSON.stringify(value)} (allowed: letters, digits, ".", "_" and "-"; no leading "-", no "..")`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function isStrictDescendant(root, candidate) {
|
|
168
|
+
const rel = relative(root, candidate);
|
|
169
|
+
return rel !== "" && !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
170
|
+
}
|
|
171
|
+
function isSameOrDescendant(root, candidate) {
|
|
172
|
+
return relative(root, candidate) === "" || isStrictDescendant(root, candidate);
|
|
173
|
+
}
|
|
174
|
+
async function realpathOrNull(path) {
|
|
175
|
+
try {
|
|
176
|
+
return await realpath(path);
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function resolveContained(root, ...segments) {
|
|
182
|
+
const absoluteRoot = resolve(root);
|
|
183
|
+
const resolved = resolve(absoluteRoot, ...segments);
|
|
184
|
+
if (!isStrictDescendant(absoluteRoot, resolved)) {
|
|
185
|
+
throw new Error(`Refusing path outside ${absoluteRoot}: ${resolved}`);
|
|
186
|
+
}
|
|
187
|
+
const realRoot = await realpathOrNull(absoluteRoot);
|
|
188
|
+
if (realRoot === null) {
|
|
189
|
+
return resolved;
|
|
190
|
+
}
|
|
191
|
+
let ancestor = dirname(resolved);
|
|
192
|
+
for (; ; ) {
|
|
193
|
+
const realAncestor = await realpathOrNull(ancestor);
|
|
194
|
+
if (realAncestor !== null) {
|
|
195
|
+
if (!isSameOrDescendant(realRoot, realAncestor)) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Refusing path outside ${absoluteRoot}: ${ancestor} resolves to ${realAncestor}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
const parent = dirname(ancestor);
|
|
203
|
+
if (parent === ancestor || relative(absoluteRoot, ancestor) === "") {
|
|
204
|
+
return resolved;
|
|
205
|
+
}
|
|
206
|
+
ancestor = parent;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function assertDirectoryWithin(dir, scopeRoot, what) {
|
|
210
|
+
const realRoot = await realpathOrNull(resolve(scopeRoot));
|
|
211
|
+
const realDir = await realpathOrNull(resolve(dir));
|
|
212
|
+
if (realRoot === null || realDir === null) {
|
|
213
|
+
throw new Error(`${what} ${dir} could not be resolved`);
|
|
214
|
+
}
|
|
215
|
+
if (!isSameOrDescendant(realRoot, realDir)) {
|
|
216
|
+
throw new Error(`${what} ${dir} resolves to ${realDir}, outside ${scopeRoot}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function removePathEntry(path) {
|
|
220
|
+
let stats;
|
|
221
|
+
try {
|
|
222
|
+
stats = await lstat(path);
|
|
223
|
+
} catch (error2) {
|
|
224
|
+
if (error2.code === "ENOENT") return false;
|
|
225
|
+
throw error2;
|
|
226
|
+
}
|
|
227
|
+
if (stats.isDirectory()) {
|
|
228
|
+
await rm(path, { recursive: true, force: true });
|
|
229
|
+
} else {
|
|
230
|
+
await unlink(path);
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
async function moveDirectory(source, destination) {
|
|
235
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
236
|
+
try {
|
|
237
|
+
await rename(source, destination);
|
|
238
|
+
} catch (error2) {
|
|
239
|
+
if (error2.code !== "EXDEV") throw error2;
|
|
240
|
+
await cp(source, destination, { recursive: true });
|
|
241
|
+
await rm(source, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function writeFileAtomic(path, data, options = {}) {
|
|
245
|
+
const tempPath = join2(dirname(path), `${basename(path)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
246
|
+
try {
|
|
247
|
+
await writeFile(tempPath, data, options.mode === void 0 ? {} : { mode: options.mode });
|
|
248
|
+
await rename(tempPath, path);
|
|
249
|
+
} catch (error2) {
|
|
250
|
+
await unlink(tempPath).catch(() => void 0);
|
|
251
|
+
throw error2;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function snapshotFile(path) {
|
|
255
|
+
try {
|
|
256
|
+
const [content, stats] = await Promise.all([readFile2(path), stat(path)]);
|
|
257
|
+
return { content, mode: stats.mode & 511 };
|
|
258
|
+
} catch (error2) {
|
|
259
|
+
if (error2.code === "ENOENT") return { content: null };
|
|
260
|
+
throw error2;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function restoreFile(path, snapshot) {
|
|
264
|
+
if (snapshot.content === null) {
|
|
265
|
+
await removePathEntry(path);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
await writeFileAtomic(path, snapshot.content, { mode: snapshot.mode });
|
|
269
|
+
}
|
|
270
|
+
async function installFile(source, destination, method) {
|
|
271
|
+
await ensureDir(dirname(destination));
|
|
272
|
+
await removePathEntry(destination);
|
|
273
|
+
if (method === "symlink") {
|
|
274
|
+
const relPath = relative(dirname(destination), source);
|
|
275
|
+
await symlink(relPath, destination);
|
|
276
|
+
} else {
|
|
277
|
+
await copyFile(source, destination);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function writeTextFile(path, content) {
|
|
281
|
+
await ensureDir(dirname(path));
|
|
282
|
+
await writeFile(path, content, "utf-8");
|
|
283
|
+
}
|
|
284
|
+
function getAgentsPath(contentType, slug, scope, cwd) {
|
|
285
|
+
const base = scope === "user" ? homedir() : cwd;
|
|
286
|
+
return join2(base, ".agents", contentType + "s", slug);
|
|
287
|
+
}
|
|
288
|
+
async function copyDirectory(source, destination) {
|
|
289
|
+
await cp(source, destination, { recursive: true });
|
|
290
|
+
}
|
|
291
|
+
async function installDirectory(source, destination, method) {
|
|
292
|
+
await ensureDir(dirname(destination));
|
|
293
|
+
await removePathEntry(destination);
|
|
294
|
+
if (method === "symlink") {
|
|
295
|
+
const relPath = relative(dirname(destination), source);
|
|
296
|
+
await symlink(relPath, destination);
|
|
297
|
+
} else {
|
|
298
|
+
await copyDirectory(source, destination);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/config/source.ts
|
|
303
|
+
var GITHUB_REPO_PATTERN = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:\/(.*))?$/;
|
|
304
|
+
var COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
305
|
+
var RAW_HOST = "https://raw.githubusercontent.com";
|
|
306
|
+
function parseGitHubRepo(url) {
|
|
307
|
+
const match = GITHUB_REPO_PATTERN.exec(url);
|
|
308
|
+
if (!match) return null;
|
|
309
|
+
const owner = match[1];
|
|
310
|
+
const repo = match[2];
|
|
311
|
+
if (owner.includes("..") || repo.includes("..") || owner.startsWith(".") || repo.startsWith(".")) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
return { owner, repo, rest: match[3] ?? "" };
|
|
315
|
+
}
|
|
316
|
+
function assertSafeRepoPath(path, what) {
|
|
317
|
+
const trimmed = path.replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
|
|
318
|
+
if (trimmed === "") return "";
|
|
319
|
+
for (const segment of trimmed.split("/")) {
|
|
320
|
+
if (segment === "" || segment === "." || segment === ".." || segment.includes("\\")) {
|
|
321
|
+
throw new Error(`Unsafe ${what}: ${JSON.stringify(path)}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return trimmed;
|
|
325
|
+
}
|
|
326
|
+
function assertCommitSha(value, what) {
|
|
327
|
+
if (!value || !COMMIT_SHA_PATTERN.test(value)) {
|
|
328
|
+
throw new Error(`Invalid ${what}: ${JSON.stringify(value)} (expected 40 lowercase hex characters)`);
|
|
329
|
+
}
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
function rawUrl(owner, repo, ref, path) {
|
|
333
|
+
const base = `${RAW_HOST}/${owner}/${repo}/${ref}`;
|
|
334
|
+
return path ? `${base}/${path}` : base;
|
|
335
|
+
}
|
|
336
|
+
function parseExternalUrl(externalUrl) {
|
|
337
|
+
const parsed = parseGitHubRepo(externalUrl);
|
|
338
|
+
if (!parsed) {
|
|
339
|
+
throw new Error(`unsupported source host: ${externalUrl}`);
|
|
340
|
+
}
|
|
341
|
+
if (parsed.rest === "") {
|
|
342
|
+
return { owner: parsed.owner, repo: parsed.repo, branch: null, path: "" };
|
|
343
|
+
}
|
|
344
|
+
const treeMatch = /^tree\/([^/]+)(?:\/(.*))?$/.exec(parsed.rest);
|
|
345
|
+
if (!treeMatch) {
|
|
346
|
+
throw new Error(`unsupported source URL: ${externalUrl}`);
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
owner: parsed.owner,
|
|
350
|
+
repo: parsed.repo,
|
|
351
|
+
branch: treeMatch[1],
|
|
352
|
+
path: assertSafeRepoPath(treeMatch[2] ?? "", "externalUrl path")
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function getEffectiveSourceRevision(item) {
|
|
356
|
+
return item.pluginSource?.sha ?? item.sourceRevision;
|
|
357
|
+
}
|
|
358
|
+
function resolvePluginSource(item) {
|
|
359
|
+
const source = item.pluginSource;
|
|
360
|
+
const what = `pluginSource of "${item.slug}"`;
|
|
361
|
+
if (source.kind === "marketplace-path") {
|
|
362
|
+
const marketplace = item.marketplaceRef;
|
|
363
|
+
if (!marketplace) {
|
|
364
|
+
throw new Error(`${what} is marketplace-path but the item has no marketplaceRef`);
|
|
365
|
+
}
|
|
366
|
+
const repo2 = parseGitHubRepo(marketplace.url);
|
|
367
|
+
if (!repo2) throw new Error(`unsupported source host: ${marketplace.url}`);
|
|
368
|
+
const sha2 = assertCommitSha(marketplace.sha || source.sha, `marketplaceRef.sha of "${item.slug}"`);
|
|
369
|
+
const path2 = assertSafeRepoPath(source.path ?? "", `${what} path`);
|
|
370
|
+
return {
|
|
371
|
+
baseUrl: rawUrl(repo2.owner, repo2.repo, sha2, path2),
|
|
372
|
+
rootUrl: rawUrl(repo2.owner, repo2.repo, sha2, ""),
|
|
373
|
+
revision: sha2
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (!source.url) {
|
|
377
|
+
throw new Error(`${what} (${source.kind}) has no url`);
|
|
378
|
+
}
|
|
379
|
+
const repo = parseGitHubRepo(source.url);
|
|
380
|
+
if (!repo) throw new Error(`unsupported source host: ${source.url}`);
|
|
381
|
+
const sha = assertCommitSha(source.sha, `pluginSource.sha of "${item.slug}"`);
|
|
382
|
+
const path = source.kind === "git-subdir" ? assertSafeRepoPath(source.path ?? "", `${what} path`) : "";
|
|
383
|
+
return {
|
|
384
|
+
baseUrl: rawUrl(repo.owner, repo.repo, sha, path),
|
|
385
|
+
rootUrl: rawUrl(repo.owner, repo.repo, sha, ""),
|
|
386
|
+
revision: sha
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function resolveItemSource(item) {
|
|
390
|
+
if (item.pluginSource) {
|
|
391
|
+
return resolvePluginSource(item);
|
|
392
|
+
}
|
|
393
|
+
if (!item.externalUrl) {
|
|
394
|
+
throw new Error(`Item "${item.slug}" has no externalUrl to fetch from`);
|
|
395
|
+
}
|
|
396
|
+
const { owner, repo, branch, path } = parseExternalUrl(item.externalUrl);
|
|
397
|
+
if (item.sourceRevision !== void 0) {
|
|
398
|
+
const sha = assertCommitSha(item.sourceRevision, `sourceRevision of "${item.slug}"`);
|
|
399
|
+
return { baseUrl: rawUrl(owner, repo, sha, path), rootUrl: rawUrl(owner, repo, sha, ""), revision: sha };
|
|
400
|
+
}
|
|
401
|
+
const ref = branch ?? "main";
|
|
402
|
+
return { baseUrl: rawUrl(owner, repo, ref, path), rootUrl: rawUrl(owner, repo, ref, ""), revision: null };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/config/registry.ts
|
|
406
|
+
var __dirname2 = dirname2(fileURLToPath(import.meta.url));
|
|
407
|
+
var FETCH_TIMEOUT_MS = 15e3;
|
|
408
|
+
function findPackageRoot(startDir) {
|
|
409
|
+
let dir = startDir;
|
|
410
|
+
for (; ; ) {
|
|
411
|
+
if (existsSync(join3(dir, "package.json"))) return dir;
|
|
412
|
+
const parent = dirname2(dir);
|
|
413
|
+
if (parent === dir) return null;
|
|
414
|
+
dir = parent;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function resolveLocalRegistryPath() {
|
|
418
|
+
const packageRoot = findPackageRoot(__dirname2);
|
|
419
|
+
if (!packageRoot) return null;
|
|
420
|
+
return join3(packageRoot, "..", "..", "registry");
|
|
421
|
+
}
|
|
422
|
+
var REGISTRY_PATH = process.env.SEEDR_REGISTRY_DIR || resolveLocalRegistryPath();
|
|
423
|
+
var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/danieldeusing/seedr/main/registry";
|
|
424
|
+
var REGISTRY_URL = (process.env.SEEDR_REGISTRY_URL || DEFAULT_REGISTRY_URL).replace(/\/+$/, "");
|
|
425
|
+
var REGISTRY_ROOT_URL = REGISTRY_URL.replace(/\/registry$/, "");
|
|
426
|
+
function assertSafeNodeName(name) {
|
|
427
|
+
if (name === "" || name === "." || name.includes("/") || name.includes("\\") || name.includes("..")) {
|
|
428
|
+
throw new Error(`Unsafe file name in registry content: ${JSON.stringify(name)}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
var cache = {
|
|
432
|
+
index: null,
|
|
433
|
+
types: /* @__PURE__ */ new Map(),
|
|
434
|
+
assembled: null
|
|
435
|
+
};
|
|
436
|
+
function isValidManifestIndex(value) {
|
|
437
|
+
return typeof value === "object" && value !== null && typeof value.types === "object" && value.types !== null;
|
|
438
|
+
}
|
|
439
|
+
async function loadFile(filename) {
|
|
440
|
+
if (REGISTRY_PATH) {
|
|
441
|
+
try {
|
|
442
|
+
return await readFile3(join3(REGISTRY_PATH, filename), "utf-8");
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return fetchRemote(`${REGISTRY_URL}/${filename}`);
|
|
447
|
+
}
|
|
448
|
+
async function fetchResponse(url) {
|
|
449
|
+
let response;
|
|
450
|
+
try {
|
|
451
|
+
response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
452
|
+
} catch (error2) {
|
|
453
|
+
if (error2 instanceof Error && error2.name === "TimeoutError") {
|
|
454
|
+
throw new Error(`Registry unreachable: timed out fetching ${url}`, { cause: error2 });
|
|
455
|
+
}
|
|
456
|
+
throw new Error(`Registry unreachable: ${url}`, { cause: error2 });
|
|
457
|
+
}
|
|
458
|
+
if (!response.ok) {
|
|
459
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
460
|
+
}
|
|
461
|
+
return response;
|
|
462
|
+
}
|
|
463
|
+
async function fetchRemote(url) {
|
|
464
|
+
const response = await fetchResponse(url);
|
|
465
|
+
return response.text();
|
|
466
|
+
}
|
|
467
|
+
async function fetchRemoteBuffer(url) {
|
|
468
|
+
const response = await fetchResponse(url);
|
|
469
|
+
return Buffer.from(await response.arrayBuffer());
|
|
470
|
+
}
|
|
471
|
+
async function loadIndex() {
|
|
472
|
+
if (cache.index) return cache.index;
|
|
473
|
+
if (REGISTRY_PATH) {
|
|
474
|
+
try {
|
|
475
|
+
const localContent = await readFile3(join3(REGISTRY_PATH, "manifest.json"), "utf-8");
|
|
476
|
+
const parsed = JSON.parse(localContent);
|
|
477
|
+
if (isValidManifestIndex(parsed)) {
|
|
478
|
+
cache.index = parsed;
|
|
479
|
+
return parsed;
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const content = await fetchRemote(`${REGISTRY_URL}/manifest.json`);
|
|
485
|
+
const data = JSON.parse(content);
|
|
486
|
+
cache.index = data;
|
|
487
|
+
return data;
|
|
488
|
+
}
|
|
489
|
+
async function loadTypeItems(type) {
|
|
490
|
+
const cached = cache.types.get(type);
|
|
491
|
+
if (cached) return cached;
|
|
492
|
+
const index = await loadIndex();
|
|
493
|
+
const desc = index.types[type];
|
|
494
|
+
const content = await loadFile(desc.file);
|
|
495
|
+
const typeManifest = JSON.parse(content);
|
|
496
|
+
cache.types.set(type, typeManifest.items);
|
|
497
|
+
return typeManifest.items;
|
|
498
|
+
}
|
|
499
|
+
async function loadManifest() {
|
|
500
|
+
if (cache.assembled) return cache.assembled;
|
|
501
|
+
const index = await loadIndex();
|
|
502
|
+
const typeEntries = Object.entries(index.types);
|
|
503
|
+
const typeResults = await Promise.all(
|
|
504
|
+
typeEntries.map(async ([type]) => loadTypeItems(type))
|
|
505
|
+
);
|
|
506
|
+
const items = typeResults.flat();
|
|
507
|
+
const manifest = { version: index.version, items };
|
|
508
|
+
cache.assembled = manifest;
|
|
509
|
+
return manifest;
|
|
510
|
+
}
|
|
511
|
+
async function getItem(slug, type) {
|
|
512
|
+
const manifest = await loadManifest();
|
|
513
|
+
if (type) return manifest.items.find((item) => item.slug === slug && item.type === type);
|
|
514
|
+
return manifest.items.find((item) => item.slug === slug);
|
|
515
|
+
}
|
|
516
|
+
async function getItemsBySlug(slug) {
|
|
517
|
+
const manifest = await loadManifest();
|
|
518
|
+
return manifest.items.filter((item) => item.slug === slug);
|
|
519
|
+
}
|
|
520
|
+
async function listItems(type) {
|
|
521
|
+
if (type) {
|
|
522
|
+
return loadTypeItems(type);
|
|
523
|
+
}
|
|
524
|
+
const manifest = await loadManifest();
|
|
525
|
+
return manifest.items;
|
|
526
|
+
}
|
|
527
|
+
async function searchItems(query) {
|
|
528
|
+
const manifest = await loadManifest();
|
|
529
|
+
const lowerQuery = query.toLowerCase();
|
|
530
|
+
return manifest.items.filter(
|
|
531
|
+
(item) => item.slug.toLowerCase().includes(lowerQuery) || item.name.toLowerCase().includes(lowerQuery) || item.description.toLowerCase().includes(lowerQuery)
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
async function getItemFull(item) {
|
|
535
|
+
assertValidSlug(item.slug);
|
|
536
|
+
const typeDir = typeDirName(item.type);
|
|
537
|
+
const itemJsonPath = `${typeDir}/${item.slug}/item.json`;
|
|
538
|
+
const content = await loadFile(itemJsonPath);
|
|
539
|
+
return JSON.parse(content);
|
|
540
|
+
}
|
|
541
|
+
function getItemBaseUrl(item) {
|
|
542
|
+
if (isFirstParty(item.sourceType)) {
|
|
543
|
+
assertValidSlug(item.slug);
|
|
544
|
+
const typeDir = typeDirName(item.type);
|
|
545
|
+
return {
|
|
546
|
+
local: REGISTRY_PATH ? join3(REGISTRY_PATH, typeDir, item.slug) : null,
|
|
547
|
+
remote: `${REGISTRY_URL}/${typeDir}/${item.slug}`,
|
|
548
|
+
rootUrl: REGISTRY_ROOT_URL,
|
|
549
|
+
revision: null
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const source = resolveItemSource(item);
|
|
553
|
+
return { local: null, remote: source.baseUrl, rootUrl: source.rootUrl, revision: source.revision };
|
|
554
|
+
}
|
|
555
|
+
function mainFileName(type) {
|
|
556
|
+
return type === "skill" ? "SKILL.md" : `${type}.md`;
|
|
557
|
+
}
|
|
558
|
+
async function getItemContent(item) {
|
|
559
|
+
const { local } = getItemBaseUrl(item);
|
|
560
|
+
const mainFile = mainFileName(item.type);
|
|
561
|
+
if (local) {
|
|
562
|
+
try {
|
|
563
|
+
return await readFile3(join3(local, mainFile), "utf-8");
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const tempRoot = await mkdtemp(join3(tmpdir(), "seedr-content-"));
|
|
568
|
+
try {
|
|
569
|
+
const destination = join3(tempRoot, "item");
|
|
570
|
+
await fetchItemToDestination(item, destination);
|
|
571
|
+
return await readFile3(join3(destination, mainFile), "utf-8");
|
|
572
|
+
} finally {
|
|
573
|
+
await rm2(tempRoot, { recursive: true, force: true });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function fetchItemFile(item, relativePath) {
|
|
577
|
+
const segments = relativePath.split("/");
|
|
578
|
+
for (const segment of segments) assertSafeNodeName(segment);
|
|
579
|
+
const { local, remote } = getItemBaseUrl(item);
|
|
580
|
+
if (local) {
|
|
581
|
+
try {
|
|
582
|
+
return await readFile3(join3(local, ...segments), "utf-8");
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return fetchRemote(`${remote}/${relativePath}`);
|
|
587
|
+
}
|
|
588
|
+
function getItemSourcePath(item) {
|
|
589
|
+
if (!isFirstParty(item.sourceType)) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
if (!REGISTRY_PATH) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
assertValidSlug(item.slug);
|
|
596
|
+
const typeDir = typeDirName(item.type);
|
|
597
|
+
return join3(REGISTRY_PATH, typeDir, item.slug);
|
|
598
|
+
}
|
|
599
|
+
function integrityError(item, detail) {
|
|
600
|
+
return new Error(`Registry integrity error: ${item.type} "${item.slug}" \u2014 ${detail}`);
|
|
601
|
+
}
|
|
602
|
+
function legacyFileList(item) {
|
|
603
|
+
if (item.type === "skill") return ["SKILL.md"];
|
|
604
|
+
if (item.type === "plugin") return [".claude-plugin/plugin.json"];
|
|
605
|
+
return [mainFileName(item.type)];
|
|
606
|
+
}
|
|
607
|
+
async function resolveFileSet(item) {
|
|
608
|
+
let files = item.contents?.files;
|
|
609
|
+
let license = item.license;
|
|
610
|
+
let digest = item.contentDigest ?? null;
|
|
611
|
+
if (!files && item.type === "plugin") {
|
|
612
|
+
const full = await getItemFull(item);
|
|
613
|
+
files = full.contents?.files;
|
|
614
|
+
license ??= full.license;
|
|
615
|
+
if (full.contentDigest) {
|
|
616
|
+
if (digest !== null && full.contentDigest !== digest) {
|
|
617
|
+
throw integrityError(item, `manifest digest ${digest} disagrees with item.json digest ${full.contentDigest}`);
|
|
618
|
+
}
|
|
619
|
+
digest = full.contentDigest;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (!files) {
|
|
623
|
+
return { paths: legacyFileList(item), digest, licenseExtra: null };
|
|
624
|
+
}
|
|
625
|
+
const treePaths = flattenFileTree(files);
|
|
626
|
+
const paths = canonicalFileSet(files, license);
|
|
627
|
+
let licenseExtra = null;
|
|
628
|
+
if (license?.installAs && !treePaths.includes(license.installAs)) {
|
|
629
|
+
if (!license.file) {
|
|
630
|
+
throw integrityError(item, `license.installAs "${license.installAs}" is set without license.file`);
|
|
631
|
+
}
|
|
632
|
+
licenseExtra = { installAs: license.installAs, file: license.file };
|
|
633
|
+
}
|
|
634
|
+
return { paths, digest, licenseExtra };
|
|
635
|
+
}
|
|
636
|
+
async function downloadFileSet(location, fileSet, stagingDir) {
|
|
637
|
+
await Promise.all(fileSet.paths.map(async (relativePath) => {
|
|
638
|
+
const segments = relativePath.split("/");
|
|
639
|
+
for (const segment of segments) assertSafeNodeName(segment);
|
|
640
|
+
const url = fileSet.licenseExtra && relativePath === fileSet.licenseExtra.installAs ? `${location.rootUrl}/${fileSet.licenseExtra.file}` : `${location.remote}/${relativePath}`;
|
|
641
|
+
const content = await fetchRemoteBuffer(url);
|
|
642
|
+
const filePath = join3(stagingDir, ...segments);
|
|
643
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
644
|
+
await writeFile2(filePath, content);
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
async function fetchItemToDestination(item, destPath) {
|
|
648
|
+
const location = getItemBaseUrl(item);
|
|
649
|
+
const fileSet = await resolveFileSet(item);
|
|
650
|
+
if (fileSet.digest === null && !isFirstParty(item.sourceType)) {
|
|
651
|
+
throw integrityError(item, "the registry entry carries no contentDigest; refusing to install unverifiable content");
|
|
652
|
+
}
|
|
653
|
+
await mkdir2(dirname2(destPath), { recursive: true });
|
|
654
|
+
const stagingDir = await mkdtemp(join3(dirname2(destPath), ".seedr-staging-"));
|
|
655
|
+
try {
|
|
656
|
+
await downloadFileSet(location, fileSet, stagingDir);
|
|
657
|
+
if (fileSet.digest !== null) {
|
|
658
|
+
const actual = await computeContentDigest(stagingDir, fileSet.paths);
|
|
659
|
+
if (actual !== fileSet.digest) {
|
|
660
|
+
throw integrityError(item, `content digest mismatch (expected ${fileSet.digest}, actual ${actual})`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
await removePathEntry(destPath);
|
|
664
|
+
await moveDirectory(stagingDir, destPath);
|
|
665
|
+
} finally {
|
|
666
|
+
await rm2(stagingDir, { recursive: true, force: true });
|
|
667
|
+
}
|
|
668
|
+
return { sourceRevision: location.revision, contentDigest: fileSet.digest, files: fileSet.paths };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/config/agents.ts
|
|
672
|
+
import { homedir as homedir2 } from "os";
|
|
673
|
+
import { join as join4 } from "path";
|
|
674
|
+
var home = homedir2();
|
|
675
|
+
var SKILL_DIRECTORY = {
|
|
676
|
+
path: "skills"
|
|
677
|
+
};
|
|
678
|
+
var ANTIGRAVITY = {
|
|
679
|
+
name: "Google Antigravity",
|
|
680
|
+
shortName: "antigravity",
|
|
681
|
+
projectRoot: ".agents",
|
|
682
|
+
userRoot: join4(home, ".agents"),
|
|
683
|
+
contentTypes: { skill: SKILL_DIRECTORY }
|
|
684
|
+
};
|
|
685
|
+
var CODING_AGENTS = {
|
|
686
|
+
claude: {
|
|
687
|
+
name: "Claude Code",
|
|
688
|
+
shortName: "claude",
|
|
689
|
+
projectRoot: ".claude",
|
|
690
|
+
userRoot: join4(home, ".claude"),
|
|
691
|
+
contentTypes: {
|
|
692
|
+
skill: SKILL_DIRECTORY,
|
|
693
|
+
command: {
|
|
694
|
+
path: "commands"
|
|
695
|
+
},
|
|
696
|
+
agent: {
|
|
697
|
+
path: "agents"
|
|
698
|
+
},
|
|
699
|
+
hook: {
|
|
700
|
+
path: ""
|
|
701
|
+
},
|
|
702
|
+
plugin: {
|
|
703
|
+
path: "plugins/cache"
|
|
704
|
+
},
|
|
705
|
+
settings: {
|
|
706
|
+
path: ""
|
|
707
|
+
},
|
|
708
|
+
mcp: {
|
|
709
|
+
path: ""
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
copilot: {
|
|
714
|
+
name: "GitHub Copilot",
|
|
715
|
+
shortName: "copilot",
|
|
716
|
+
projectRoot: ".github",
|
|
717
|
+
userRoot: join4(home, ".github"),
|
|
718
|
+
contentTypes: { skill: SKILL_DIRECTORY }
|
|
719
|
+
},
|
|
720
|
+
antigravity: ANTIGRAVITY,
|
|
721
|
+
gemini: ANTIGRAVITY,
|
|
722
|
+
codex: {
|
|
723
|
+
name: "OpenAI Codex CLI",
|
|
724
|
+
shortName: "codex",
|
|
725
|
+
projectRoot: ".codex",
|
|
726
|
+
userRoot: join4(home, ".codex"),
|
|
727
|
+
contentTypes: { skill: SKILL_DIRECTORY }
|
|
728
|
+
},
|
|
729
|
+
opencode: {
|
|
730
|
+
name: "OpenCode",
|
|
731
|
+
shortName: "opencode",
|
|
732
|
+
projectRoot: ".opencode",
|
|
733
|
+
userRoot: join4(home, ".opencode"),
|
|
734
|
+
contentTypes: { skill: SKILL_DIRECTORY }
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
var ALL_AGENTS = [...CANONICAL_AGENTS];
|
|
738
|
+
function getAgentConfig(agent) {
|
|
739
|
+
return CODING_AGENTS[agent];
|
|
740
|
+
}
|
|
741
|
+
function getContentTypeConfig(agent, type) {
|
|
742
|
+
return getAgentConfig(agent).contentTypes[type];
|
|
743
|
+
}
|
|
744
|
+
function getAgentRoot(agent, scope, cwd = process.cwd()) {
|
|
745
|
+
const config = getAgentConfig(agent);
|
|
746
|
+
switch (scope) {
|
|
747
|
+
case "project":
|
|
748
|
+
case "local":
|
|
749
|
+
return join4(cwd, config.projectRoot);
|
|
750
|
+
case "user":
|
|
751
|
+
return config.userRoot;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function getContentPath(agent, type, scope, cwd = process.cwd()) {
|
|
755
|
+
const typeConfig = getContentTypeConfig(agent, type);
|
|
756
|
+
if (!typeConfig) return void 0;
|
|
757
|
+
const root = getAgentRoot(agent, scope, cwd);
|
|
758
|
+
return typeConfig.path ? join4(root, typeConfig.path) : root;
|
|
759
|
+
}
|
|
760
|
+
function getSettingsPath(scope, cwd = process.cwd()) {
|
|
761
|
+
switch (scope) {
|
|
762
|
+
case "project":
|
|
763
|
+
return join4(cwd, ".claude/settings.json");
|
|
764
|
+
case "user":
|
|
765
|
+
return join4(home, ".claude/settings.json");
|
|
766
|
+
case "local":
|
|
767
|
+
return join4(cwd, ".claude/settings.local.json");
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function getMcpConfigPath(agent, scope, cwd = process.cwd()) {
|
|
771
|
+
const isUser = scope === "user";
|
|
772
|
+
switch (canonicalAgent(agent) ?? agent) {
|
|
773
|
+
case "claude":
|
|
774
|
+
return isUser ? join4(home, ".claude.json") : join4(cwd, ".mcp.json");
|
|
775
|
+
case "codex":
|
|
776
|
+
return isUser ? join4(home, ".codex", "config.toml") : join4(cwd, ".codex", "config.toml");
|
|
777
|
+
case "opencode":
|
|
778
|
+
return isUser ? join4(home, ".config", "opencode", "opencode.json") : join4(cwd, "opencode.json");
|
|
779
|
+
default:
|
|
780
|
+
throw new Error(`${agent} has no verified MCP configuration format`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function getAgentPath(agent, scope, cwd = process.cwd()) {
|
|
784
|
+
return getContentPath(agent, "skill", scope, cwd) || "";
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/utils/detection.ts
|
|
788
|
+
async function detectInstalledAgents(cwd = process.cwd()) {
|
|
789
|
+
const checks = ALL_AGENTS.flatMap((agent) => {
|
|
790
|
+
const projectPath = getAgentPath(agent, "project", cwd);
|
|
791
|
+
const userPath = getAgentPath(agent, "user", cwd);
|
|
792
|
+
return [
|
|
793
|
+
exists(projectPath).then((found) => found ? { agent, scope: "project", path: projectPath } : null),
|
|
794
|
+
exists(userPath).then((found) => found ? { agent, scope: "user", path: userPath } : null)
|
|
795
|
+
];
|
|
796
|
+
});
|
|
797
|
+
const results = await Promise.all(checks);
|
|
798
|
+
return results.filter((r) => r !== null);
|
|
799
|
+
}
|
|
800
|
+
async function detectProjectAgents(cwd = process.cwd()) {
|
|
801
|
+
const checks = ALL_AGENTS.map(async (agent) => {
|
|
802
|
+
const projectPath = getAgentPath(agent, "project", cwd);
|
|
803
|
+
return await exists(projectPath) ? agent : null;
|
|
804
|
+
});
|
|
805
|
+
const results = await Promise.all(checks);
|
|
806
|
+
return results.filter((a) => a !== null);
|
|
807
|
+
}
|
|
808
|
+
async function isAgentInstalled(agent, scope, cwd = process.cwd()) {
|
|
809
|
+
const effectiveScope = scope === "local" ? "project" : scope;
|
|
810
|
+
const path = getAgentPath(agent, effectiveScope, cwd);
|
|
811
|
+
return exists(path);
|
|
812
|
+
}
|
|
813
|
+
function parseAgentArg(arg) {
|
|
814
|
+
const normalized = arg.toLowerCase().trim();
|
|
815
|
+
const aliases = {
|
|
816
|
+
"claude-code": "claude",
|
|
817
|
+
claudecode: "claude",
|
|
818
|
+
cc: "claude",
|
|
819
|
+
"github-copilot": "copilot",
|
|
820
|
+
gh: "copilot",
|
|
821
|
+
"google-antigravity": "antigravity",
|
|
822
|
+
agy: "antigravity",
|
|
823
|
+
"gemini-code": "antigravity",
|
|
824
|
+
gca: "antigravity",
|
|
825
|
+
"openai-codex": "codex",
|
|
826
|
+
oc: "opencode"
|
|
827
|
+
};
|
|
828
|
+
return canonicalAgent(aliases[normalized] ?? normalized);
|
|
829
|
+
}
|
|
830
|
+
function parseAgentsArgStrict(agents) {
|
|
831
|
+
const parsed = [];
|
|
832
|
+
const unknown = [];
|
|
833
|
+
for (const raw of agents.split(",")) {
|
|
834
|
+
const trimmed = raw.trim();
|
|
835
|
+
if (trimmed === "") continue;
|
|
836
|
+
const agent = parseAgentArg(trimmed);
|
|
837
|
+
if (agent === null) {
|
|
838
|
+
unknown.push(trimmed);
|
|
839
|
+
} else if (!parsed.includes(agent)) {
|
|
840
|
+
parsed.push(agent);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return { agents: parsed, unknown };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/utils/ui.ts
|
|
847
|
+
import * as p from "@clack/prompts";
|
|
848
|
+
import chalk from "chalk";
|
|
849
|
+
function isInteractive() {
|
|
850
|
+
return Boolean(process.stdin.isTTY);
|
|
851
|
+
}
|
|
852
|
+
function assertInteractive(question, flag) {
|
|
853
|
+
if (process.stdin.isTTY) return;
|
|
854
|
+
throw new Error(`${question} requires a terminal \u2014 pass ${flag} to run non-interactively`);
|
|
855
|
+
}
|
|
856
|
+
var brand = chalk.hex("#22c55e");
|
|
857
|
+
var bgBrand = chalk.bgHex("#22c55e").black;
|
|
858
|
+
var LOGO = `
|
|
859
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
860
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
861
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
|
|
862
|
+
\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
863
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
864
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D`;
|
|
865
|
+
var URLS = {
|
|
866
|
+
seedr: "https://seedr.danieldeusing.de"
|
|
867
|
+
};
|
|
868
|
+
function printLogo() {
|
|
869
|
+
console.log(brand(LOGO));
|
|
870
|
+
console.log(brand(" \u{1F331} Seed your Coding Agents with capabilities"));
|
|
871
|
+
console.log(chalk.gray(` ${URLS.seedr}`));
|
|
872
|
+
console.log();
|
|
873
|
+
}
|
|
874
|
+
async function selectSkill(items) {
|
|
875
|
+
assertInteractive("Choosing an item", "the item name as an argument");
|
|
876
|
+
const result = await p.select({
|
|
877
|
+
message: "Select a skill to install",
|
|
878
|
+
options: items.map((item) => ({
|
|
879
|
+
label: item.name,
|
|
880
|
+
value: item,
|
|
881
|
+
hint: item.description
|
|
882
|
+
}))
|
|
883
|
+
});
|
|
884
|
+
return result;
|
|
885
|
+
}
|
|
886
|
+
async function selectAgents(compatible) {
|
|
887
|
+
assertInteractive("Choosing coding agents", "--agents");
|
|
888
|
+
const allOption = await p.select({
|
|
889
|
+
message: "Which coding agents do you want to install for?",
|
|
890
|
+
options: [
|
|
891
|
+
{ label: `All (${compatible.length} agents)`, value: "all" },
|
|
892
|
+
{ label: "Select specific agents...", value: "select" }
|
|
893
|
+
]
|
|
894
|
+
});
|
|
895
|
+
if (p.isCancel(allOption)) return allOption;
|
|
896
|
+
if (allOption === "all") return compatible;
|
|
897
|
+
const result = await p.multiselect({
|
|
898
|
+
message: "Select agents",
|
|
899
|
+
options: compatible.map((agent) => ({
|
|
900
|
+
label: CODING_AGENTS[agent].name,
|
|
901
|
+
value: agent,
|
|
902
|
+
hint: CODING_AGENTS[agent].projectRoot
|
|
903
|
+
})),
|
|
904
|
+
initialValues: ["claude"],
|
|
905
|
+
required: true
|
|
906
|
+
});
|
|
907
|
+
return result;
|
|
908
|
+
}
|
|
909
|
+
async function selectScope(includeLocal = false) {
|
|
910
|
+
assertInteractive("Choosing an installation scope", "--scope");
|
|
911
|
+
const options = [
|
|
912
|
+
{ label: "Project", value: "project", hint: includeLocal ? "current directory, settings.json" : "current directory" },
|
|
913
|
+
{ label: "User", value: "user", hint: "home directory" },
|
|
914
|
+
...includeLocal ? [{ label: "Local", value: "local", hint: "current directory, settings.local.json" }] : []
|
|
915
|
+
];
|
|
916
|
+
const result = await p.select({ message: "Installation scope", options });
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
async function selectMethod(symlinkPath) {
|
|
920
|
+
assertInteractive("Choosing an installation method", "--method");
|
|
921
|
+
const result = await p.select({
|
|
922
|
+
message: "Installation method",
|
|
923
|
+
options: [
|
|
924
|
+
{ label: "Symlink", value: "symlink", hint: `shared at ${symlinkPath}` },
|
|
925
|
+
{ label: "Copy", value: "copy", hint: "standalone copy per agent" }
|
|
926
|
+
]
|
|
927
|
+
});
|
|
928
|
+
return result;
|
|
929
|
+
}
|
|
930
|
+
async function confirm2(message) {
|
|
931
|
+
assertInteractive("Confirmation", "--yes");
|
|
932
|
+
return p.confirm({ message });
|
|
933
|
+
}
|
|
934
|
+
function intro2(message) {
|
|
935
|
+
p.intro(bgBrand(` ${message} `));
|
|
936
|
+
}
|
|
937
|
+
function outro2(message) {
|
|
938
|
+
p.outro(brand(message));
|
|
939
|
+
}
|
|
940
|
+
function step(message) {
|
|
941
|
+
p.log.step(message);
|
|
942
|
+
}
|
|
943
|
+
function info(message) {
|
|
944
|
+
p.log.info(message);
|
|
945
|
+
}
|
|
946
|
+
function success(message) {
|
|
947
|
+
p.log.success(message);
|
|
948
|
+
}
|
|
949
|
+
function warn(message) {
|
|
950
|
+
p.log.warn(message);
|
|
951
|
+
}
|
|
952
|
+
function error(message) {
|
|
953
|
+
p.log.error(message);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/handlers/skill.ts
|
|
957
|
+
import { relative as relative2, dirname as dirname3 } from "path";
|
|
958
|
+
import { homedir as homedir3 } from "os";
|
|
959
|
+
import { readdir as readdir2, symlink as symlink2 } from "fs/promises";
|
|
960
|
+
import chalk2 from "chalk";
|
|
961
|
+
import ora from "ora";
|
|
962
|
+
var SLUG_LABEL = "skill slug";
|
|
963
|
+
var READS_CENTRAL_DIR = /* @__PURE__ */ new Set(["antigravity", "codex", "opencode"]);
|
|
964
|
+
function getScopeRoot(scope, cwd) {
|
|
965
|
+
return scope === "user" ? homedir3() : cwd;
|
|
966
|
+
}
|
|
967
|
+
function centralSkillsDir(scope, cwd) {
|
|
968
|
+
return dirname3(getAgentsPath("skill", "any-slug", scope, cwd));
|
|
969
|
+
}
|
|
970
|
+
async function resolveCentralPath(slug, scope, cwd) {
|
|
971
|
+
const scopeRoot = getScopeRoot(scope, cwd);
|
|
972
|
+
const centralDir = centralSkillsDir(scope, cwd);
|
|
973
|
+
return resolveContained(scopeRoot, relative2(scopeRoot, centralDir), slug);
|
|
974
|
+
}
|
|
975
|
+
async function resolveAgentSkillPath(agent, slug, scope, cwd) {
|
|
976
|
+
const destDir = getContentPath(agent, "skill", scope, cwd);
|
|
977
|
+
if (!destDir) return null;
|
|
978
|
+
const scopeRoot = getScopeRoot(scope, cwd);
|
|
979
|
+
return resolveContained(scopeRoot, relative2(scopeRoot, destDir), slug);
|
|
980
|
+
}
|
|
981
|
+
async function installToCentralLocation(item, sourcePath, scope, force, cwd) {
|
|
982
|
+
const centralPath = await resolveCentralPath(item.slug, scope, cwd);
|
|
983
|
+
await assertOverwritable(centralPath, force);
|
|
984
|
+
const existed = await exists(centralPath);
|
|
985
|
+
if (sourcePath && await exists(sourcePath)) {
|
|
986
|
+
await installDirectory(sourcePath, centralPath, "copy");
|
|
987
|
+
} else {
|
|
988
|
+
await fetchItemToDestination(item, centralPath);
|
|
989
|
+
}
|
|
990
|
+
return { centralPath, created: !existed };
|
|
991
|
+
}
|
|
992
|
+
async function createAgentSymlink(centralPath, destPath) {
|
|
993
|
+
await ensureDir(dirname3(destPath));
|
|
994
|
+
await removePathEntry(destPath);
|
|
995
|
+
const relPath = relative2(dirname3(destPath), centralPath);
|
|
996
|
+
await symlink2(relPath, destPath);
|
|
997
|
+
}
|
|
998
|
+
async function installSkillForAgent(item, agent, scope, method, force, cwd, centralPath) {
|
|
999
|
+
const spinner = ora(
|
|
1000
|
+
`Installing ${item.name} for ${CODING_AGENTS[agent].name}...`
|
|
1001
|
+
).start();
|
|
1002
|
+
try {
|
|
1003
|
+
const destPath = await resolveAgentSkillPath(agent, item.slug, scope, cwd);
|
|
1004
|
+
if (!destPath) {
|
|
1005
|
+
throw new Error(`${CODING_AGENTS[agent].name} does not support skills`);
|
|
1006
|
+
}
|
|
1007
|
+
await assertOverwritable(destPath, force);
|
|
1008
|
+
const sourcePath = getItemSourcePath(item);
|
|
1009
|
+
if (method === "symlink" && centralPath) {
|
|
1010
|
+
await createAgentSymlink(centralPath, destPath);
|
|
1011
|
+
} else if (sourcePath && await exists(sourcePath)) {
|
|
1012
|
+
await installDirectory(sourcePath, destPath, "copy");
|
|
1013
|
+
} else {
|
|
1014
|
+
await fetchItemToDestination(item, destPath);
|
|
1015
|
+
}
|
|
1016
|
+
spinner.succeed(
|
|
1017
|
+
brand(`Installed ${item.name} for ${CODING_AGENTS[agent].name}`)
|
|
1018
|
+
);
|
|
1019
|
+
return { agent, success: true, path: destPath };
|
|
1020
|
+
} catch (error2) {
|
|
1021
|
+
const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1022
|
+
spinner.fail(
|
|
1023
|
+
chalk2.red(`Failed to install for ${CODING_AGENTS[agent].name}: ${errorMsg}`)
|
|
1024
|
+
);
|
|
1025
|
+
return { agent, success: false, path: "", error: errorMsg };
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
async function installSkill(item, agents, scope, method, force, cwd = process.cwd()) {
|
|
1029
|
+
assertValidSlug(item.slug, SLUG_LABEL);
|
|
1030
|
+
const results = [];
|
|
1031
|
+
const sourcePath = getItemSourcePath(item);
|
|
1032
|
+
let central;
|
|
1033
|
+
if (method === "symlink") {
|
|
1034
|
+
central = await installToCentralLocation(item, sourcePath, scope, force, cwd);
|
|
1035
|
+
}
|
|
1036
|
+
for (const agent of agents) {
|
|
1037
|
+
if (READS_CENTRAL_DIR.has(canonicalAgent(agent) ?? agent) && central) {
|
|
1038
|
+
results.push({ agent, success: true, path: central.centralPath });
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const result = await installSkillForAgent(
|
|
1042
|
+
item,
|
|
1043
|
+
agent,
|
|
1044
|
+
scope,
|
|
1045
|
+
method,
|
|
1046
|
+
force,
|
|
1047
|
+
cwd,
|
|
1048
|
+
central?.centralPath
|
|
1049
|
+
);
|
|
1050
|
+
results.push(result);
|
|
1051
|
+
}
|
|
1052
|
+
if (central?.created && !results.some((result) => result.success)) {
|
|
1053
|
+
await removePathEntry(central.centralPath);
|
|
1054
|
+
}
|
|
1055
|
+
return results;
|
|
1056
|
+
}
|
|
1057
|
+
async function uninstallSkill(slug, agent, scope, cwd = process.cwd()) {
|
|
1058
|
+
assertValidSlug(slug, SLUG_LABEL);
|
|
1059
|
+
const destPath = await resolveAgentSkillPath(agent, slug, scope, cwd);
|
|
1060
|
+
if (destPath && await removePathEntry(destPath)) return true;
|
|
1061
|
+
if (READS_CENTRAL_DIR.has(canonicalAgent(agent) ?? agent)) {
|
|
1062
|
+
return removePathEntry(await resolveCentralPath(slug, scope, cwd));
|
|
1063
|
+
}
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
async function getInstalledSkills(agent, scope, cwd = process.cwd()) {
|
|
1067
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
1068
|
+
const own = getContentPath(agent, "skill", scope, cwd);
|
|
1069
|
+
if (own) dirs.add(own);
|
|
1070
|
+
if (READS_CENTRAL_DIR.has(canonicalAgent(agent) ?? agent)) dirs.add(centralSkillsDir(scope, cwd));
|
|
1071
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
1072
|
+
for (const dir of dirs) {
|
|
1073
|
+
if (!await exists(dir)) continue;
|
|
1074
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
1075
|
+
for (const entry of entries) {
|
|
1076
|
+
if ((entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".")) slugs.add(entry.name);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return [...slugs];
|
|
1080
|
+
}
|
|
1081
|
+
async function planSkill(item, agents, scope, method, cwd) {
|
|
1082
|
+
assertValidSlug(item.slug, SLUG_LABEL);
|
|
1083
|
+
const changes = [];
|
|
1084
|
+
const kindFor = async (path) => await exists(path) ? "modify" : "create";
|
|
1085
|
+
let centralPath;
|
|
1086
|
+
if (method === "symlink") {
|
|
1087
|
+
centralPath = await resolveCentralPath(item.slug, scope, cwd);
|
|
1088
|
+
const sharedBy = agents.filter((agent) => READS_CENTRAL_DIR.has(canonicalAgent(agent) ?? agent));
|
|
1089
|
+
changes.push({
|
|
1090
|
+
agent: "shared",
|
|
1091
|
+
kind: await kindFor(centralPath),
|
|
1092
|
+
path: centralPath,
|
|
1093
|
+
detail: sharedBy.length > 0 ? `central copy, read directly by ${sharedBy.join(", ")}` : "central copy"
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
for (const agent of agents) {
|
|
1097
|
+
if (centralPath && READS_CENTRAL_DIR.has(canonicalAgent(agent) ?? agent)) continue;
|
|
1098
|
+
const destPath = await resolveAgentSkillPath(agent, item.slug, scope, cwd);
|
|
1099
|
+
if (!destPath) throw new Error(`${CODING_AGENTS[agent].name} does not support skills`);
|
|
1100
|
+
changes.push({
|
|
1101
|
+
agent,
|
|
1102
|
+
kind: await kindFor(destPath),
|
|
1103
|
+
path: destPath,
|
|
1104
|
+
detail: centralPath ? `symlink \u2192 ${centralPath}` : "skill directory"
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return changes;
|
|
1108
|
+
}
|
|
1109
|
+
var skillHandler = {
|
|
1110
|
+
type: "skill",
|
|
1111
|
+
async install(item, agents, scope, method, force, cwd) {
|
|
1112
|
+
return installSkill(item, agents, scope, method, force, cwd);
|
|
1113
|
+
},
|
|
1114
|
+
async uninstall(slug, agent, scope, cwd) {
|
|
1115
|
+
return uninstallSkill(slug, agent, scope, cwd);
|
|
1116
|
+
},
|
|
1117
|
+
async listInstalled(agent, scope, cwd) {
|
|
1118
|
+
return getInstalledSkills(agent, scope, cwd);
|
|
1119
|
+
},
|
|
1120
|
+
plan: planSkill
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
export {
|
|
1124
|
+
isLegacyAgent,
|
|
1125
|
+
canonicalAgent,
|
|
1126
|
+
canonicalAgents,
|
|
1127
|
+
SLUG_PATTERN2 as SLUG_PATTERN,
|
|
1128
|
+
MAX_SLUG_LENGTH2 as MAX_SLUG_LENGTH,
|
|
1129
|
+
isValidSlug2 as isValidSlug,
|
|
1130
|
+
assertValidSlug,
|
|
1131
|
+
exists,
|
|
1132
|
+
ensureDir,
|
|
1133
|
+
assertOverwritable,
|
|
1134
|
+
assertSafePathSegment,
|
|
1135
|
+
resolveContained,
|
|
1136
|
+
assertDirectoryWithin,
|
|
1137
|
+
removePathEntry,
|
|
1138
|
+
moveDirectory,
|
|
1139
|
+
writeFileAtomic,
|
|
1140
|
+
snapshotFile,
|
|
1141
|
+
restoreFile,
|
|
1142
|
+
installFile,
|
|
1143
|
+
writeTextFile,
|
|
1144
|
+
getAgentsPath,
|
|
1145
|
+
copyDirectory,
|
|
1146
|
+
parseGitHubRepo,
|
|
1147
|
+
getEffectiveSourceRevision,
|
|
1148
|
+
loadManifest,
|
|
1149
|
+
getItem,
|
|
1150
|
+
getItemsBySlug,
|
|
1151
|
+
listItems,
|
|
1152
|
+
searchItems,
|
|
1153
|
+
getItemContent,
|
|
1154
|
+
fetchItemFile,
|
|
1155
|
+
getItemSourcePath,
|
|
1156
|
+
fetchItemToDestination,
|
|
1157
|
+
CODING_AGENTS,
|
|
1158
|
+
ALL_AGENTS,
|
|
1159
|
+
getAgentConfig,
|
|
1160
|
+
getContentPath,
|
|
1161
|
+
getSettingsPath,
|
|
1162
|
+
getMcpConfigPath,
|
|
1163
|
+
getAgentPath,
|
|
1164
|
+
detectInstalledAgents,
|
|
1165
|
+
detectProjectAgents,
|
|
1166
|
+
isAgentInstalled,
|
|
1167
|
+
parseAgentsArgStrict,
|
|
1168
|
+
p,
|
|
1169
|
+
isInteractive,
|
|
1170
|
+
brand,
|
|
1171
|
+
printLogo,
|
|
1172
|
+
selectSkill,
|
|
1173
|
+
selectAgents,
|
|
1174
|
+
selectScope,
|
|
1175
|
+
selectMethod,
|
|
1176
|
+
confirm2 as confirm,
|
|
1177
|
+
intro2 as intro,
|
|
1178
|
+
outro2 as outro,
|
|
1179
|
+
step,
|
|
1180
|
+
info,
|
|
1181
|
+
success,
|
|
1182
|
+
warn,
|
|
1183
|
+
error,
|
|
1184
|
+
installSkill,
|
|
1185
|
+
uninstallSkill,
|
|
1186
|
+
getInstalledSkills,
|
|
1187
|
+
skillHandler
|
|
1188
|
+
};
|