@skillsmanager/cli 0.0.2 → 0.0.5
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 +10 -18
- package/README.md +89 -35
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +54 -14
- package/dist/backends/gdrive.d.ts +2 -1
- package/dist/backends/gdrive.js +15 -4
- package/dist/backends/github.d.ts +27 -0
- package/dist/backends/github.js +407 -0
- package/dist/backends/interface.d.ts +2 -0
- package/dist/backends/local.d.ts +2 -0
- package/dist/backends/local.js +11 -0
- package/dist/backends/resolve.d.ts +2 -0
- package/dist/backends/resolve.js +11 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +209 -7
- package/dist/commands/collection.d.ts +5 -1
- package/dist/commands/collection.js +99 -25
- package/dist/commands/fetch.js +7 -6
- package/dist/commands/list.js +5 -3
- package/dist/commands/logout.d.ts +4 -0
- package/dist/commands/logout.js +35 -0
- package/dist/commands/refresh.js +78 -25
- package/dist/commands/registry.d.ts +6 -0
- package/dist/commands/registry.js +200 -92
- package/dist/commands/setup/github.d.ts +4 -0
- package/dist/commands/setup/github.js +91 -0
- package/dist/commands/setup/google.js +82 -42
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.js +76 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +65 -0
- package/dist/commands/update.js +6 -4
- package/dist/index.js +57 -9
- package/dist/registry.js +11 -3
- package/dist/types.d.ts +1 -0
- package/dist/types.js +2 -0
- package/package.json +2 -2
- package/skills/skillsmanager/SKILL.md +49 -4
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { parseCollection, serializeCollection, parseRegistryFile, serializeRegistryFile, COLLECTION_FILENAME, REGISTRY_FILENAME, } from "../registry.js";
|
|
7
|
+
import { CONFIG_DIR } from "../config.js";
|
|
8
|
+
const GITHUB_WORKDIR = path.join(CONFIG_DIR, "github-workdir");
|
|
9
|
+
const SKILLSMANAGER_DIR = ".skillsmanager";
|
|
10
|
+
// ── folderId format: "owner/repo:.skillsmanager/collection-name" ──────────────
|
|
11
|
+
function parseRef(folderId) {
|
|
12
|
+
const colonIdx = folderId.indexOf(":");
|
|
13
|
+
if (colonIdx === -1)
|
|
14
|
+
return { repo: folderId, metaDir: SKILLSMANAGER_DIR };
|
|
15
|
+
return {
|
|
16
|
+
repo: folderId.slice(0, colonIdx),
|
|
17
|
+
metaDir: folderId.slice(colonIdx + 1) || SKILLSMANAGER_DIR,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function workdirFor(repo) {
|
|
21
|
+
return path.join(GITHUB_WORKDIR, repo.replace("/", "_"));
|
|
22
|
+
}
|
|
23
|
+
/** Returns the repo where skill files live — defaults to the collection host repo. */
|
|
24
|
+
function skillsRepo(col, hostRepo) {
|
|
25
|
+
return col.metadata?.repo ?? hostRepo;
|
|
26
|
+
}
|
|
27
|
+
// ── CLI helpers ───────────────────────────────────────────────────────────────
|
|
28
|
+
function ghExec(args, opts) {
|
|
29
|
+
const r = spawnSync("gh", args, {
|
|
30
|
+
cwd: opts?.cwd,
|
|
31
|
+
encoding: "utf-8",
|
|
32
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
ok: r.status === 0,
|
|
36
|
+
stdout: (r.stdout ?? "").trim(),
|
|
37
|
+
stderr: (r.stderr ?? "").trim(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function gitExec(args, cwd) {
|
|
41
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8", stdio: "pipe" });
|
|
42
|
+
return {
|
|
43
|
+
ok: r.status === 0,
|
|
44
|
+
stdout: (r.stdout ?? "").trim(),
|
|
45
|
+
stderr: (r.stderr ?? "").trim(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function copyDirSync(src, dest) {
|
|
49
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
50
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
51
|
+
const srcPath = path.join(src, entry.name);
|
|
52
|
+
const destPath = path.join(dest, entry.name);
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
copyDirSync(srcPath, destPath);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
fs.copyFileSync(srcPath, destPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// ── GithubBackend ─────────────────────────────────────────────────────────────
|
|
62
|
+
export class GithubBackend {
|
|
63
|
+
// ── Identity ─────────────────────────────────────────────────────────────────
|
|
64
|
+
async getOwner() {
|
|
65
|
+
const r = ghExec(["api", "user", "--jq", ".login"]);
|
|
66
|
+
if (!r.ok || !r.stdout) {
|
|
67
|
+
throw new Error("GitHub auth failed. Run: skillsmanager setup github");
|
|
68
|
+
}
|
|
69
|
+
return r.stdout;
|
|
70
|
+
}
|
|
71
|
+
// ── Ensure repo exists (create private if not) ───────────────────────────────
|
|
72
|
+
async ensureRepo(repo) {
|
|
73
|
+
const check = ghExec(["api", `repos/${repo}`]);
|
|
74
|
+
if (!check.ok) {
|
|
75
|
+
console.log(chalk.dim(` Repo ${repo} not found — creating private repo...`));
|
|
76
|
+
const name = repo.split("/")[1];
|
|
77
|
+
const create = ghExec(["repo", "create", name, "--private", "--confirm"]);
|
|
78
|
+
if (!create.ok)
|
|
79
|
+
throw new Error(`Failed to create repo ${repo}: ${create.stderr}`);
|
|
80
|
+
console.log(chalk.green(` Created private repo: ${repo}`));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ── Private: workdir management ───────────────────────────────────────────────
|
|
84
|
+
ensureWorkdir(repo) {
|
|
85
|
+
const dir = workdirFor(repo);
|
|
86
|
+
if (!fs.existsSync(dir)) {
|
|
87
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
88
|
+
const r = spawnSync("gh", ["repo", "clone", repo, dir], { stdio: "inherit" });
|
|
89
|
+
if (r.status !== 0)
|
|
90
|
+
throw new Error(`Failed to clone ${repo}`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
gitExec(["pull", "--ff-only"], dir);
|
|
94
|
+
}
|
|
95
|
+
return dir;
|
|
96
|
+
}
|
|
97
|
+
// ── Private: push with PR fallback ───────────────────────────────────────────
|
|
98
|
+
async gitPushOrPR(workdir, title) {
|
|
99
|
+
const pushResult = gitExec(["push", "origin", "HEAD"], workdir);
|
|
100
|
+
if (pushResult.ok)
|
|
101
|
+
return;
|
|
102
|
+
// Direct push blocked — create a PR
|
|
103
|
+
console.log(chalk.yellow("\n Direct push blocked (branch protection). Creating a PR..."));
|
|
104
|
+
const branch = `skillsmanager-update-${Date.now()}`;
|
|
105
|
+
const checkout = gitExec(["checkout", "-b", branch], workdir);
|
|
106
|
+
if (!checkout.ok)
|
|
107
|
+
throw new Error(`Failed to create branch: ${checkout.stderr}`);
|
|
108
|
+
const pushBranch = gitExec(["push", "-u", "origin", branch], workdir);
|
|
109
|
+
if (!pushBranch.ok)
|
|
110
|
+
throw new Error(`Failed to push branch: ${pushBranch.stderr}`);
|
|
111
|
+
const prResult = ghExec(["pr", "create", "--title", title, "--body", "Created by skillsmanager", "--fill"], { cwd: workdir });
|
|
112
|
+
if (!prResult.ok)
|
|
113
|
+
throw new Error(`Failed to create PR: ${prResult.stderr}`);
|
|
114
|
+
const prUrl = prResult.stdout.split("\n").find((l) => l.startsWith("https://")) ?? prResult.stdout;
|
|
115
|
+
console.log(chalk.cyan(`\n PR created: ${prUrl}`));
|
|
116
|
+
console.log(chalk.dim(" Waiting for merge (up to 5 minutes)..."));
|
|
117
|
+
const timeout = Date.now() + 5 * 60 * 1000;
|
|
118
|
+
let merged = false;
|
|
119
|
+
while (Date.now() < timeout) {
|
|
120
|
+
await new Promise((r) => setTimeout(r, 10_000));
|
|
121
|
+
const stateResult = ghExec(["pr", "view", prUrl, "--json", "state", "--jq", ".state"], { cwd: workdir });
|
|
122
|
+
if (stateResult.ok && stateResult.stdout.trim() === "MERGED") {
|
|
123
|
+
merged = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!merged) {
|
|
128
|
+
console.log(chalk.yellow(`\n PR not merged within timeout. Branch "${branch}" is still open.`));
|
|
129
|
+
console.log(chalk.dim(" Merge it manually, then run: skillsmanager refresh"));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Back to default branch, pull
|
|
133
|
+
const headRef = gitExec(["rev-parse", "--abbrev-ref", "origin/HEAD"], workdir);
|
|
134
|
+
const base = headRef.stdout.replace("origin/", "") || "main";
|
|
135
|
+
gitExec(["checkout", base], workdir);
|
|
136
|
+
gitExec(["pull", "--ff-only"], workdir);
|
|
137
|
+
console.log(chalk.green(" ✓ PR merged and changes pulled."));
|
|
138
|
+
}
|
|
139
|
+
async commitAndPush(workdir, message) {
|
|
140
|
+
const commit = gitExec(["commit", "-m", message], workdir);
|
|
141
|
+
const nothingToCommit = commit.stdout.includes("nothing to commit") ||
|
|
142
|
+
commit.stderr.includes("nothing to commit");
|
|
143
|
+
if (!commit.ok && nothingToCommit)
|
|
144
|
+
return;
|
|
145
|
+
if (!commit.ok)
|
|
146
|
+
throw new Error(`Git commit failed: ${commit.stderr || commit.stdout}`);
|
|
147
|
+
await this.gitPushOrPR(workdir, message);
|
|
148
|
+
}
|
|
149
|
+
// ── Collection operations ─────────────────────────────────────────────────────
|
|
150
|
+
async discoverCollections() {
|
|
151
|
+
const r = ghExec(["repo", "list", "--json", "nameWithOwner", "--limit", "100"]);
|
|
152
|
+
if (!r.ok)
|
|
153
|
+
return [];
|
|
154
|
+
let repos = [];
|
|
155
|
+
try {
|
|
156
|
+
repos = JSON.parse(r.stdout);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const collections = [];
|
|
162
|
+
for (const { nameWithOwner } of repos) {
|
|
163
|
+
const dirCheck = ghExec([
|
|
164
|
+
"api", `repos/${nameWithOwner}/contents/${SKILLSMANAGER_DIR}`,
|
|
165
|
+
]);
|
|
166
|
+
if (!dirCheck.ok)
|
|
167
|
+
continue;
|
|
168
|
+
let entries = [];
|
|
169
|
+
try {
|
|
170
|
+
entries = JSON.parse(dirCheck.stdout);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
if (entry.type !== "dir")
|
|
177
|
+
continue;
|
|
178
|
+
const fileCheck = ghExec([
|
|
179
|
+
"api",
|
|
180
|
+
`repos/${nameWithOwner}/contents/${SKILLSMANAGER_DIR}/${entry.name}/${COLLECTION_FILENAME}`,
|
|
181
|
+
]);
|
|
182
|
+
if (!fileCheck.ok)
|
|
183
|
+
continue;
|
|
184
|
+
collections.push({
|
|
185
|
+
name: entry.name,
|
|
186
|
+
backend: "github",
|
|
187
|
+
folderId: `${nameWithOwner}:${SKILLSMANAGER_DIR}/${entry.name}`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return collections;
|
|
192
|
+
}
|
|
193
|
+
async readCollection(collection) {
|
|
194
|
+
const { repo, metaDir } = parseRef(collection.folderId);
|
|
195
|
+
const r = ghExec([
|
|
196
|
+
"api", `repos/${repo}/contents/${metaDir}/${COLLECTION_FILENAME}`, "--jq", ".content",
|
|
197
|
+
]);
|
|
198
|
+
if (!r.ok)
|
|
199
|
+
throw new Error(`Collection file not found in "${collection.name}"`);
|
|
200
|
+
const content = Buffer.from(r.stdout.replace(/\s/g, ""), "base64").toString("utf-8");
|
|
201
|
+
return parseCollection(content);
|
|
202
|
+
}
|
|
203
|
+
async writeCollection(collection, data) {
|
|
204
|
+
const { repo, metaDir } = parseRef(collection.folderId);
|
|
205
|
+
const workdir = this.ensureWorkdir(repo);
|
|
206
|
+
const filePath = path.join(workdir, metaDir, COLLECTION_FILENAME);
|
|
207
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
208
|
+
fs.writeFileSync(filePath, serializeCollection(data));
|
|
209
|
+
gitExec(["add", path.join(metaDir, COLLECTION_FILENAME)], workdir);
|
|
210
|
+
await this.commitAndPush(workdir, `chore: update ${COLLECTION_FILENAME} for ${collection.name}`);
|
|
211
|
+
}
|
|
212
|
+
async downloadSkill(collection, skillName, destDir) {
|
|
213
|
+
const { repo: hostRepo } = parseRef(collection.folderId);
|
|
214
|
+
const col = await this.readCollection(collection);
|
|
215
|
+
const entry = col.skills.find((s) => s.name === skillName);
|
|
216
|
+
if (!entry) {
|
|
217
|
+
throw new Error(`Skill "${skillName}" not found in collection "${collection.name}"`);
|
|
218
|
+
}
|
|
219
|
+
const srcRepo = skillsRepo(col, hostRepo);
|
|
220
|
+
const workdir = this.ensureWorkdir(srcRepo);
|
|
221
|
+
gitExec(["pull", "--ff-only"], workdir);
|
|
222
|
+
const skillPath = path.join(workdir, entry.path);
|
|
223
|
+
if (!fs.existsSync(skillPath)) {
|
|
224
|
+
throw new Error(`Skill directory not found at "${entry.path}" in repo "${srcRepo}"`);
|
|
225
|
+
}
|
|
226
|
+
if (path.resolve(skillPath) !== path.resolve(destDir)) {
|
|
227
|
+
copyDirSync(skillPath, destDir);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async uploadSkill(collection, localPath, skillName) {
|
|
231
|
+
const { repo: hostRepo } = parseRef(collection.folderId);
|
|
232
|
+
// If collection points to a foreign skills repo, we can't upload there
|
|
233
|
+
const col = await this.readCollection(collection);
|
|
234
|
+
const foreign = col.metadata?.repo;
|
|
235
|
+
if (foreign && foreign !== hostRepo) {
|
|
236
|
+
throw new Error(`Cannot upload skill to collection "${collection.name}": its skills source is "${foreign}" (a repo you may not own). ` +
|
|
237
|
+
`Use --remote-path to register a skill path from that repo instead.`);
|
|
238
|
+
}
|
|
239
|
+
const workdir = this.ensureWorkdir(hostRepo);
|
|
240
|
+
const resolvedLocal = path.resolve(localPath);
|
|
241
|
+
const resolvedWorkdir = path.resolve(workdir);
|
|
242
|
+
if (resolvedLocal.startsWith(resolvedWorkdir + path.sep) ||
|
|
243
|
+
resolvedLocal === resolvedWorkdir) {
|
|
244
|
+
// Already in the repo — no copy needed
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// External skill: copy into .agentskills/<skillName>/ in the repo
|
|
248
|
+
const dest = path.join(workdir, ".agentskills", skillName);
|
|
249
|
+
copyDirSync(localPath, dest);
|
|
250
|
+
gitExec(["add", path.join(".agentskills", skillName)], workdir);
|
|
251
|
+
await this.commitAndPush(workdir, `chore: add skill ${skillName}`);
|
|
252
|
+
}
|
|
253
|
+
async deleteCollection(collection) {
|
|
254
|
+
const { repo, metaDir } = parseRef(collection.folderId);
|
|
255
|
+
const workdir = this.ensureWorkdir(repo);
|
|
256
|
+
const metaDirPath = path.join(workdir, metaDir);
|
|
257
|
+
if (!fs.existsSync(metaDirPath))
|
|
258
|
+
return;
|
|
259
|
+
fs.rmSync(metaDirPath, { recursive: true, force: true });
|
|
260
|
+
gitExec(["add", "-A"], workdir);
|
|
261
|
+
await this.commitAndPush(workdir, `chore: remove collection ${collection.name}`);
|
|
262
|
+
}
|
|
263
|
+
async deleteSkill(collection, skillName) {
|
|
264
|
+
const { repo: hostRepo } = parseRef(collection.folderId);
|
|
265
|
+
const col = await this.readCollection(collection);
|
|
266
|
+
const entry = col.skills.find((s) => s.name === skillName);
|
|
267
|
+
if (!entry)
|
|
268
|
+
return;
|
|
269
|
+
const srcRepo = skillsRepo(col, hostRepo);
|
|
270
|
+
const workdir = this.ensureWorkdir(srcRepo);
|
|
271
|
+
const skillPath = path.join(workdir, entry.path);
|
|
272
|
+
if (!fs.existsSync(skillPath))
|
|
273
|
+
return;
|
|
274
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
275
|
+
gitExec(["add", "-A"], workdir);
|
|
276
|
+
await this.commitAndPush(workdir, `chore: remove skill ${skillName}`);
|
|
277
|
+
}
|
|
278
|
+
// ── Registry operations ───────────────────────────────────────────────────────
|
|
279
|
+
async discoverRegistries() {
|
|
280
|
+
const r = ghExec(["repo", "list", "--json", "nameWithOwner", "--limit", "100"]);
|
|
281
|
+
if (!r.ok)
|
|
282
|
+
return [];
|
|
283
|
+
let repos = [];
|
|
284
|
+
try {
|
|
285
|
+
repos = JSON.parse(r.stdout);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
const registries = [];
|
|
291
|
+
for (const { nameWithOwner } of repos) {
|
|
292
|
+
const check = ghExec([
|
|
293
|
+
"api", `repos/${nameWithOwner}/contents/${SKILLSMANAGER_DIR}/${REGISTRY_FILENAME}`,
|
|
294
|
+
]);
|
|
295
|
+
if (!check.ok)
|
|
296
|
+
continue;
|
|
297
|
+
registries.push({
|
|
298
|
+
name: nameWithOwner.split("/")[1] ?? nameWithOwner,
|
|
299
|
+
backend: "github",
|
|
300
|
+
folderId: `${nameWithOwner}:${SKILLSMANAGER_DIR}`,
|
|
301
|
+
fileId: `${nameWithOwner}:${SKILLSMANAGER_DIR}/${REGISTRY_FILENAME}`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return registries;
|
|
305
|
+
}
|
|
306
|
+
async readRegistry(registry) {
|
|
307
|
+
const { repo, metaDir } = parseRef(registry.folderId);
|
|
308
|
+
const r = ghExec([
|
|
309
|
+
"api", `repos/${repo}/contents/${metaDir}/${REGISTRY_FILENAME}`, "--jq", ".content",
|
|
310
|
+
]);
|
|
311
|
+
if (!r.ok)
|
|
312
|
+
throw new Error(`Registry file not found for "${registry.name}"`);
|
|
313
|
+
const content = Buffer.from(r.stdout.replace(/\s/g, ""), "base64").toString("utf-8");
|
|
314
|
+
return parseRegistryFile(content);
|
|
315
|
+
}
|
|
316
|
+
async writeRegistry(registry, data) {
|
|
317
|
+
const { repo, metaDir } = parseRef(registry.folderId);
|
|
318
|
+
const workdir = this.ensureWorkdir(repo);
|
|
319
|
+
const filePath = path.join(workdir, metaDir, REGISTRY_FILENAME);
|
|
320
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
321
|
+
fs.writeFileSync(filePath, serializeRegistryFile(data));
|
|
322
|
+
gitExec(["add", path.join(metaDir, REGISTRY_FILENAME)], workdir);
|
|
323
|
+
await this.commitAndPush(workdir, `chore: update ${REGISTRY_FILENAME}`);
|
|
324
|
+
}
|
|
325
|
+
async resolveCollectionRef(ref) {
|
|
326
|
+
if (ref.backend !== "github")
|
|
327
|
+
return null;
|
|
328
|
+
const { repo, metaDir } = parseRef(ref.ref);
|
|
329
|
+
const check = ghExec(["api", `repos/${repo}/contents/${metaDir}/${COLLECTION_FILENAME}`]);
|
|
330
|
+
if (!check.ok)
|
|
331
|
+
return null;
|
|
332
|
+
return { name: ref.name, backend: "github", folderId: ref.ref };
|
|
333
|
+
}
|
|
334
|
+
async createRegistry(name, repoRef) {
|
|
335
|
+
if (!repoRef)
|
|
336
|
+
throw new Error("GitHub backend requires --repo <owner/repo>");
|
|
337
|
+
await this.ensureRepo(repoRef);
|
|
338
|
+
const workdir = this.ensureWorkdir(repoRef);
|
|
339
|
+
const metaDir = SKILLSMANAGER_DIR;
|
|
340
|
+
const filePath = path.join(workdir, metaDir, REGISTRY_FILENAME);
|
|
341
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
342
|
+
const owner = await this.getOwner();
|
|
343
|
+
const registryData = {
|
|
344
|
+
name: name ?? (repoRef.split("/")[1] ?? "default"),
|
|
345
|
+
owner,
|
|
346
|
+
source: "github",
|
|
347
|
+
collections: [],
|
|
348
|
+
};
|
|
349
|
+
fs.writeFileSync(filePath, serializeRegistryFile(registryData));
|
|
350
|
+
gitExec(["add", path.join(metaDir, REGISTRY_FILENAME)], workdir);
|
|
351
|
+
await this.commitAndPush(workdir, "chore: init SKILLS_REGISTRY");
|
|
352
|
+
return {
|
|
353
|
+
id: randomUUID(),
|
|
354
|
+
name: registryData.name,
|
|
355
|
+
backend: "github",
|
|
356
|
+
folderId: `${repoRef}:${metaDir}`,
|
|
357
|
+
fileId: `${repoRef}:${metaDir}/${REGISTRY_FILENAME}`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
// ── createCollection ─────────────────────────────────────────────────────────
|
|
361
|
+
async createCollection(collectionName, repoRef, skillsRepoRef) {
|
|
362
|
+
if (!repoRef)
|
|
363
|
+
throw new Error("GitHub backend requires --repo <owner/repo>");
|
|
364
|
+
const repo = repoRef;
|
|
365
|
+
await this.ensureRepo(repo);
|
|
366
|
+
const workdir = this.ensureWorkdir(repo);
|
|
367
|
+
const metaDir = `${SKILLSMANAGER_DIR}/${collectionName}`;
|
|
368
|
+
const filePath = path.join(workdir, metaDir, COLLECTION_FILENAME);
|
|
369
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
370
|
+
const owner = await this.getOwner();
|
|
371
|
+
const colData = { name: collectionName, owner, skills: [] };
|
|
372
|
+
if (skillsRepoRef && skillsRepoRef !== repo) {
|
|
373
|
+
colData.metadata = { repo: skillsRepoRef };
|
|
374
|
+
}
|
|
375
|
+
fs.writeFileSync(filePath, serializeCollection(colData));
|
|
376
|
+
gitExec(["add", path.join(metaDir, COLLECTION_FILENAME)], workdir);
|
|
377
|
+
await this.commitAndPush(workdir, `chore: init collection ${collectionName}`);
|
|
378
|
+
return {
|
|
379
|
+
id: randomUUID(),
|
|
380
|
+
name: collectionName,
|
|
381
|
+
backend: "github",
|
|
382
|
+
folderId: `${repo}:${metaDir}`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// ── Static: detect if a path is inside a GitHub-tracked git repo ──────────────
|
|
386
|
+
static detectRepoContext(absPath) {
|
|
387
|
+
const rootResult = spawnSync("git", ["-C", absPath, "rev-parse", "--show-toplevel"], {
|
|
388
|
+
encoding: "utf-8", stdio: "pipe",
|
|
389
|
+
});
|
|
390
|
+
if (rootResult.status !== 0)
|
|
391
|
+
return null;
|
|
392
|
+
const repoRoot = rootResult.stdout.trim();
|
|
393
|
+
const remoteResult = spawnSync("git", ["-C", repoRoot, "remote", "get-url", "origin"], {
|
|
394
|
+
encoding: "utf-8", stdio: "pipe",
|
|
395
|
+
});
|
|
396
|
+
if (remoteResult.status !== 0)
|
|
397
|
+
return null;
|
|
398
|
+
const remoteUrl = remoteResult.stdout.trim();
|
|
399
|
+
const match = remoteUrl.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/) ??
|
|
400
|
+
remoteUrl.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
401
|
+
if (!match)
|
|
402
|
+
return null;
|
|
403
|
+
const repo = match[1].replace(/\.git$/, "");
|
|
404
|
+
const relPath = path.relative(repoRoot, absPath).replace(/\\/g, "/");
|
|
405
|
+
return { repo, repoRoot, relPath };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -6,6 +6,8 @@ export interface StorageBackend {
|
|
|
6
6
|
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
7
7
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
8
8
|
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
|
|
9
|
+
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
10
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
9
11
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
10
12
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
11
13
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
package/dist/backends/local.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare class LocalBackend implements StorageBackend {
|
|
|
11
11
|
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
12
12
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
13
13
|
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
|
|
14
|
+
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
15
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
14
16
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
15
17
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
16
18
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
package/dist/backends/local.js
CHANGED
|
@@ -73,6 +73,17 @@ export class LocalBackend {
|
|
|
73
73
|
return;
|
|
74
74
|
copyDirSync(localPath, dest);
|
|
75
75
|
}
|
|
76
|
+
async deleteCollection(collection) {
|
|
77
|
+
if (fs.existsSync(collection.folderId)) {
|
|
78
|
+
fs.rmSync(collection.folderId, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async deleteSkill(collection, skillName) {
|
|
82
|
+
const skillPath = path.join(collection.folderId, skillName);
|
|
83
|
+
if (fs.existsSync(skillPath)) {
|
|
84
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
76
87
|
// ── Registry operations ──────────────────────────────────────────────────
|
|
77
88
|
async discoverRegistries() {
|
|
78
89
|
if (!fs.existsSync(LOCAL_REGISTRY_PATH))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ensureAuth } from "../auth.js";
|
|
2
|
+
import { GDriveBackend } from "./gdrive.js";
|
|
3
|
+
import { GithubBackend } from "./github.js";
|
|
4
|
+
import { LocalBackend } from "./local.js";
|
|
5
|
+
export async function resolveBackend(backendName) {
|
|
6
|
+
if (backendName === "gdrive")
|
|
7
|
+
return new GDriveBackend(await ensureAuth());
|
|
8
|
+
if (backendName === "github")
|
|
9
|
+
return new GithubBackend();
|
|
10
|
+
return new LocalBackend();
|
|
11
|
+
}
|