@skillsmanager/cli 0.0.5 → 0.0.6
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/README.md +5 -2
- package/dist/backends/gdrive.d.ts +5 -4
- package/dist/backends/gdrive.js +21 -13
- package/dist/backends/github.d.ts +9 -9
- package/dist/backends/github.js +57 -65
- package/dist/backends/interface.d.ts +18 -2
- package/dist/backends/local.d.ts +5 -4
- package/dist/backends/local.js +12 -13
- package/dist/backends/resolve.d.ts +2 -0
- package/dist/backends/resolve.js +25 -4
- package/dist/backends/routing.d.ts +38 -0
- package/dist/backends/routing.js +124 -0
- package/dist/commands/add.js +23 -39
- package/dist/commands/collection.js +12 -46
- package/dist/commands/init.js +3 -3
- package/dist/commands/list.js +78 -8
- package/dist/commands/refresh.js +1 -1
- package/dist/commands/registry.js +7 -21
- package/dist/commands/search.js +1 -1
- package/dist/commands/status.js +15 -45
- package/dist/config.js +6 -1
- package/dist/index.js +1 -1
- package/dist/registry.js +9 -5
- package/dist/types.d.ts +1 -0
- package/dist/utils/git.d.ts +10 -0
- package/dist/utils/git.js +27 -0
- package/package.json +1 -1
- package/skills/skillsmanager/SKILL.md +73 -5
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/talktoajayprakash/skillsmanager/actions)
|
|
5
5
|
[](https://talktoajayprakash.github.io/skillsmanager)
|
|
6
6
|
|
|
7
|
-
# Skills Manager
|
|
7
|
+
# Skills Manager CLI
|
|
8
8
|
|
|
9
9
|
**One place to manage, sync, and share all your AI agent skills — across every agent you use.**
|
|
10
10
|
|
|
@@ -72,10 +72,13 @@ skillsmanager refresh # discovers collections in your Drive
|
|
|
72
72
|
| `skillsmanager search <query>` | Search skills by name or description |
|
|
73
73
|
| `skillsmanager fetch <name> --agent <agent>` | Download and install a skill for an agent |
|
|
74
74
|
| `skillsmanager add <path>` | Upload a local skill to a collection |
|
|
75
|
+
| `skillsmanager add --remote-path <path> --name <n> --description <d>` | Register a remote skill path (no upload) |
|
|
75
76
|
| `skillsmanager update <path>` | Push local changes back to remote storage |
|
|
76
77
|
| `skillsmanager refresh` | Re-discover collections from remote |
|
|
77
|
-
| `skillsmanager collection create
|
|
78
|
+
| `skillsmanager collection create [name] --backend github --repo <owner/repo>` | Create a collection in a GitHub repo |
|
|
79
|
+
| `skillsmanager collection create [name] --skills-repo <owner/repo>` | Create a collection with skills in a separate GitHub repo |
|
|
78
80
|
| `skillsmanager registry push --backend gdrive` | Push local registry to Google Drive |
|
|
81
|
+
| `skillsmanager registry push --backend github --repo <owner/repo>` | Push local registry to GitHub |
|
|
79
82
|
|
|
80
83
|
## Local Development
|
|
81
84
|
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import type { OAuth2Client } from "google-auth-library";
|
|
2
|
-
import type { StorageBackend } from "./interface.js";
|
|
2
|
+
import type { BackendStatus, CreateRegistryOptions, StorageBackend } from "./interface.js";
|
|
3
3
|
import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
|
|
4
4
|
export declare class GDriveBackend implements StorageBackend {
|
|
5
5
|
private drive;
|
|
6
6
|
constructor(auth: OAuth2Client);
|
|
7
7
|
getOwner(): Promise<string>;
|
|
8
8
|
getOwnerEmail(): Promise<string>;
|
|
9
|
+
getStatus(): Promise<BackendStatus>;
|
|
9
10
|
discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
|
|
10
11
|
readCollection(collection: CollectionInfo): Promise<CollectionFile>;
|
|
11
12
|
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
12
13
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
13
14
|
private downloadFolder;
|
|
14
|
-
createCollection(
|
|
15
|
+
createCollection({ name, skillsRepo }: import("./interface.js").CreateCollectionOptions): Promise<CollectionInfo>;
|
|
15
16
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
16
17
|
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
17
|
-
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<
|
|
18
|
+
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<string>;
|
|
18
19
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
19
20
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
20
21
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
21
22
|
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
22
|
-
createRegistry(
|
|
23
|
+
createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
|
|
23
24
|
private findFolder;
|
|
24
25
|
private uploadFolder;
|
|
25
26
|
}
|
package/dist/backends/gdrive.js
CHANGED
|
@@ -18,6 +18,14 @@ export class GDriveBackend {
|
|
|
18
18
|
async getOwnerEmail() {
|
|
19
19
|
return this.getOwner();
|
|
20
20
|
}
|
|
21
|
+
async getStatus() {
|
|
22
|
+
try {
|
|
23
|
+
return { loggedIn: true, identity: await this.getOwner() };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { loggedIn: false, identity: "", hint: "run: skillsmanager setup google" };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
21
29
|
// ── Collection operations ────────────────────────────────────────────────
|
|
22
30
|
async discoverCollections() {
|
|
23
31
|
const collections = [];
|
|
@@ -149,30 +157,28 @@ export class GDriveBackend {
|
|
|
149
157
|
}
|
|
150
158
|
} while (pageToken);
|
|
151
159
|
}
|
|
152
|
-
async createCollection(
|
|
160
|
+
async createCollection({ name, skillsRepo }) {
|
|
161
|
+
const folderName = `SKILLS_${name.toUpperCase()}`;
|
|
153
162
|
const folderRes = await this.drive.files.create({
|
|
154
|
-
requestBody: {
|
|
155
|
-
name: folderName,
|
|
156
|
-
mimeType: FOLDER_MIME,
|
|
157
|
-
},
|
|
163
|
+
requestBody: { name: folderName, mimeType: FOLDER_MIME },
|
|
158
164
|
fields: "id, name",
|
|
159
165
|
});
|
|
160
166
|
const folderId = folderRes.data.id;
|
|
161
167
|
const owner = await this.getOwnerEmail();
|
|
162
|
-
const
|
|
163
|
-
|
|
168
|
+
const emptyCollection = { name, owner, skills: [] };
|
|
169
|
+
if (skillsRepo) {
|
|
170
|
+
emptyCollection.type = "github";
|
|
171
|
+
emptyCollection.metadata = { repo: skillsRepo };
|
|
172
|
+
}
|
|
164
173
|
const content = serializeCollection(emptyCollection);
|
|
165
174
|
const fileRes = await this.drive.files.create({
|
|
166
|
-
requestBody: {
|
|
167
|
-
name: COLLECTION_FILENAME,
|
|
168
|
-
parents: [folderId],
|
|
169
|
-
},
|
|
175
|
+
requestBody: { name: COLLECTION_FILENAME, parents: [folderId] },
|
|
170
176
|
media: { mimeType: "text/yaml", body: Readable.from(content) },
|
|
171
177
|
fields: "id",
|
|
172
178
|
});
|
|
173
179
|
return {
|
|
174
180
|
id: randomUUID(),
|
|
175
|
-
name
|
|
181
|
+
name,
|
|
176
182
|
backend: "gdrive",
|
|
177
183
|
folderId,
|
|
178
184
|
registryFileId: fileRes.data.id ?? undefined,
|
|
@@ -205,6 +211,7 @@ export class GDriveBackend {
|
|
|
205
211
|
folderId = res.data.id;
|
|
206
212
|
}
|
|
207
213
|
await this.uploadFolder(localPath, folderId);
|
|
214
|
+
return `${skillName}/`;
|
|
208
215
|
}
|
|
209
216
|
// ── Registry operations ──────────────────────────────────────────────────
|
|
210
217
|
async discoverRegistries() {
|
|
@@ -298,7 +305,8 @@ export class GDriveBackend {
|
|
|
298
305
|
}
|
|
299
306
|
return null;
|
|
300
307
|
}
|
|
301
|
-
async createRegistry(
|
|
308
|
+
async createRegistry(options) {
|
|
309
|
+
const name = options?.name;
|
|
302
310
|
const folderName = name ? `SKILLS_REGISTRY_${name}` : "SKILLS_REGISTRY";
|
|
303
311
|
const folderRes = await this.drive.files.create({
|
|
304
312
|
requestBody: { name: folderName, mimeType: FOLDER_MIME },
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { StorageBackend } from "./interface.js";
|
|
1
|
+
import type { BackendStatus, CreateRegistryOptions, StorageBackend } from "./interface.js";
|
|
2
2
|
import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
|
|
3
3
|
export declare class GithubBackend implements StorageBackend {
|
|
4
4
|
getOwner(): Promise<string>;
|
|
5
|
+
getStatus(): Promise<BackendStatus>;
|
|
5
6
|
ensureRepo(repo: string): Promise<void>;
|
|
6
7
|
private ensureWorkdir;
|
|
7
8
|
private gitPushOrPR;
|
|
@@ -9,19 +10,18 @@ export declare class GithubBackend implements StorageBackend {
|
|
|
9
10
|
discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
|
|
10
11
|
readCollection(collection: CollectionInfo): Promise<CollectionFile>;
|
|
11
12
|
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
13
|
+
/** Clone/pull repo and copy relPath to destDir. Usable by other backends for cross-backend routing. */
|
|
14
|
+
downloadSkillFromRepo(repo: string, relPath: string, destDir: string): Promise<void>;
|
|
15
|
+
/** Clone/pull repo and delete relPath. Usable by other backends for cross-backend routing. */
|
|
16
|
+
deleteSkillFromRepo(repo: string, relPath: string): Promise<void>;
|
|
12
17
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
13
|
-
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<
|
|
18
|
+
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<string>;
|
|
14
19
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
15
20
|
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
16
21
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
17
22
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
18
23
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
19
24
|
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
20
|
-
createRegistry(
|
|
21
|
-
createCollection(
|
|
22
|
-
static detectRepoContext(absPath: string): {
|
|
23
|
-
repo: string;
|
|
24
|
-
repoRoot: string;
|
|
25
|
-
relPath: string;
|
|
26
|
-
} | null;
|
|
25
|
+
createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
|
|
26
|
+
createCollection({ name, repo, skillsRepo }: import("./interface.js").CreateCollectionOptions): Promise<CollectionInfo>;
|
|
27
27
|
}
|
package/dist/backends/github.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
4
|
import { spawnSync } from "child_process";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
+
import { ghInstalled, ghAuthed, ghGetLogin } from "../commands/setup/github.js";
|
|
6
7
|
import { parseCollection, serializeCollection, parseRegistryFile, serializeRegistryFile, COLLECTION_FILENAME, REGISTRY_FILENAME, } from "../registry.js";
|
|
7
8
|
import { CONFIG_DIR } from "../config.js";
|
|
8
9
|
const GITHUB_WORKDIR = path.join(CONFIG_DIR, "github-workdir");
|
|
@@ -20,9 +21,12 @@ function parseRef(folderId) {
|
|
|
20
21
|
function workdirFor(repo) {
|
|
21
22
|
return path.join(GITHUB_WORKDIR, repo.replace("/", "_"));
|
|
22
23
|
}
|
|
23
|
-
/** Returns the repo where skill files live — defaults to
|
|
24
|
+
/** Returns the repo where skill files live — respects col.type + metadata.repo; defaults to host repo. */
|
|
24
25
|
function skillsRepo(col, hostRepo) {
|
|
25
|
-
|
|
26
|
+
if (col.type === "github" || col.type === undefined) {
|
|
27
|
+
return col.metadata?.repo ?? hostRepo;
|
|
28
|
+
}
|
|
29
|
+
return hostRepo;
|
|
26
30
|
}
|
|
27
31
|
// ── CLI helpers ───────────────────────────────────────────────────────────────
|
|
28
32
|
function ghExec(args, opts) {
|
|
@@ -68,6 +72,13 @@ export class GithubBackend {
|
|
|
68
72
|
}
|
|
69
73
|
return r.stdout;
|
|
70
74
|
}
|
|
75
|
+
async getStatus() {
|
|
76
|
+
if (!ghInstalled())
|
|
77
|
+
return { loggedIn: false, identity: "", hint: "install gh CLI first" };
|
|
78
|
+
if (!ghAuthed())
|
|
79
|
+
return { loggedIn: false, identity: "", hint: "run: skillsmanager setup github" };
|
|
80
|
+
return { loggedIn: true, identity: ghGetLogin() };
|
|
81
|
+
}
|
|
71
82
|
// ── Ensure repo exists (create private if not) ───────────────────────────────
|
|
72
83
|
async ensureRepo(repo) {
|
|
73
84
|
const check = ghExec(["api", `repos/${repo}`]);
|
|
@@ -209,6 +220,29 @@ export class GithubBackend {
|
|
|
209
220
|
gitExec(["add", path.join(metaDir, COLLECTION_FILENAME)], workdir);
|
|
210
221
|
await this.commitAndPush(workdir, `chore: update ${COLLECTION_FILENAME} for ${collection.name}`);
|
|
211
222
|
}
|
|
223
|
+
/** Clone/pull repo and copy relPath to destDir. Usable by other backends for cross-backend routing. */
|
|
224
|
+
async downloadSkillFromRepo(repo, relPath, destDir) {
|
|
225
|
+
const workdir = this.ensureWorkdir(repo);
|
|
226
|
+
gitExec(["pull", "--ff-only"], workdir);
|
|
227
|
+
const skillPath = path.join(workdir, relPath);
|
|
228
|
+
if (!fs.existsSync(skillPath)) {
|
|
229
|
+
throw new Error(`Skill directory not found at "${relPath}" in repo "${repo}"`);
|
|
230
|
+
}
|
|
231
|
+
if (path.resolve(skillPath) !== path.resolve(destDir)) {
|
|
232
|
+
copyDirSync(skillPath, destDir);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Clone/pull repo and delete relPath. Usable by other backends for cross-backend routing. */
|
|
236
|
+
async deleteSkillFromRepo(repo, relPath) {
|
|
237
|
+
const workdir = this.ensureWorkdir(repo);
|
|
238
|
+
gitExec(["pull", "--ff-only"], workdir);
|
|
239
|
+
const skillPath = path.join(workdir, relPath);
|
|
240
|
+
if (!fs.existsSync(skillPath))
|
|
241
|
+
return;
|
|
242
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
243
|
+
gitExec(["add", "-A"], workdir);
|
|
244
|
+
await this.commitAndPush(workdir, `chore: remove skill at ${relPath}`);
|
|
245
|
+
}
|
|
212
246
|
async downloadSkill(collection, skillName, destDir) {
|
|
213
247
|
const { repo: hostRepo } = parseRef(collection.folderId);
|
|
214
248
|
const col = await this.readCollection(collection);
|
|
@@ -217,38 +251,24 @@ export class GithubBackend {
|
|
|
217
251
|
throw new Error(`Skill "${skillName}" not found in collection "${collection.name}"`);
|
|
218
252
|
}
|
|
219
253
|
const srcRepo = skillsRepo(col, hostRepo);
|
|
220
|
-
|
|
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
|
-
}
|
|
254
|
+
await this.downloadSkillFromRepo(srcRepo, entry.path, destDir);
|
|
229
255
|
}
|
|
230
256
|
async uploadSkill(collection, localPath, skillName) {
|
|
231
257
|
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
258
|
const workdir = this.ensureWorkdir(hostRepo);
|
|
240
259
|
const resolvedLocal = path.resolve(localPath);
|
|
241
260
|
const resolvedWorkdir = path.resolve(workdir);
|
|
242
261
|
if (resolvedLocal.startsWith(resolvedWorkdir + path.sep) ||
|
|
243
262
|
resolvedLocal === resolvedWorkdir) {
|
|
244
|
-
// Already in the repo — no copy needed
|
|
245
|
-
return;
|
|
263
|
+
// Already in the repo — no copy needed; return relative path from workdir
|
|
264
|
+
return path.relative(workdir, resolvedLocal).replace(/\\/g, "/");
|
|
246
265
|
}
|
|
247
266
|
// External skill: copy into .agentskills/<skillName>/ in the repo
|
|
248
267
|
const dest = path.join(workdir, ".agentskills", skillName);
|
|
249
268
|
copyDirSync(localPath, dest);
|
|
250
269
|
gitExec(["add", path.join(".agentskills", skillName)], workdir);
|
|
251
270
|
await this.commitAndPush(workdir, `chore: add skill ${skillName}`);
|
|
271
|
+
return `.agentskills/${skillName}`;
|
|
252
272
|
}
|
|
253
273
|
async deleteCollection(collection) {
|
|
254
274
|
const { repo, metaDir } = parseRef(collection.folderId);
|
|
@@ -267,13 +287,7 @@ export class GithubBackend {
|
|
|
267
287
|
if (!entry)
|
|
268
288
|
return;
|
|
269
289
|
const srcRepo = skillsRepo(col, hostRepo);
|
|
270
|
-
|
|
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}`);
|
|
290
|
+
await this.deleteSkillFromRepo(srcRepo, entry.path);
|
|
277
291
|
}
|
|
278
292
|
// ── Registry operations ───────────────────────────────────────────────────────
|
|
279
293
|
async discoverRegistries() {
|
|
@@ -331,17 +345,18 @@ export class GithubBackend {
|
|
|
331
345
|
return null;
|
|
332
346
|
return { name: ref.name, backend: "github", folderId: ref.ref };
|
|
333
347
|
}
|
|
334
|
-
async createRegistry(
|
|
335
|
-
|
|
348
|
+
async createRegistry(options) {
|
|
349
|
+
const { name, repo } = options ?? {};
|
|
350
|
+
if (!repo)
|
|
336
351
|
throw new Error("GitHub backend requires --repo <owner/repo>");
|
|
337
|
-
await this.ensureRepo(
|
|
338
|
-
const workdir = this.ensureWorkdir(
|
|
352
|
+
await this.ensureRepo(repo);
|
|
353
|
+
const workdir = this.ensureWorkdir(repo);
|
|
339
354
|
const metaDir = SKILLSMANAGER_DIR;
|
|
340
355
|
const filePath = path.join(workdir, metaDir, REGISTRY_FILENAME);
|
|
341
356
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
342
357
|
const owner = await this.getOwner();
|
|
343
358
|
const registryData = {
|
|
344
|
-
name: name ?? (
|
|
359
|
+
name: name ?? (repo.split("/")[1] ?? "default"),
|
|
345
360
|
owner,
|
|
346
361
|
source: "github",
|
|
347
362
|
collections: [],
|
|
@@ -353,55 +368,32 @@ export class GithubBackend {
|
|
|
353
368
|
id: randomUUID(),
|
|
354
369
|
name: registryData.name,
|
|
355
370
|
backend: "github",
|
|
356
|
-
folderId: `${
|
|
357
|
-
fileId: `${
|
|
371
|
+
folderId: `${repo}:${metaDir}`,
|
|
372
|
+
fileId: `${repo}:${metaDir}/${REGISTRY_FILENAME}`,
|
|
358
373
|
};
|
|
359
374
|
}
|
|
360
375
|
// ── createCollection ─────────────────────────────────────────────────────────
|
|
361
|
-
async createCollection(
|
|
362
|
-
if (!
|
|
376
|
+
async createCollection({ name, repo, skillsRepo }) {
|
|
377
|
+
if (!repo)
|
|
363
378
|
throw new Error("GitHub backend requires --repo <owner/repo>");
|
|
364
|
-
const repo = repoRef;
|
|
365
379
|
await this.ensureRepo(repo);
|
|
366
380
|
const workdir = this.ensureWorkdir(repo);
|
|
367
|
-
const metaDir = `${SKILLSMANAGER_DIR}/${
|
|
381
|
+
const metaDir = `${SKILLSMANAGER_DIR}/${name}`;
|
|
368
382
|
const filePath = path.join(workdir, metaDir, COLLECTION_FILENAME);
|
|
369
383
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
370
384
|
const owner = await this.getOwner();
|
|
371
|
-
const colData = { name
|
|
372
|
-
if (
|
|
373
|
-
colData.metadata = { repo:
|
|
385
|
+
const colData = { name, owner, skills: [] };
|
|
386
|
+
if (skillsRepo && skillsRepo !== repo) {
|
|
387
|
+
colData.metadata = { repo: skillsRepo };
|
|
374
388
|
}
|
|
375
389
|
fs.writeFileSync(filePath, serializeCollection(colData));
|
|
376
390
|
gitExec(["add", path.join(metaDir, COLLECTION_FILENAME)], workdir);
|
|
377
|
-
await this.commitAndPush(workdir, `chore: init collection ${
|
|
391
|
+
await this.commitAndPush(workdir, `chore: init collection ${name}`);
|
|
378
392
|
return {
|
|
379
393
|
id: randomUUID(),
|
|
380
|
-
name
|
|
394
|
+
name,
|
|
381
395
|
backend: "github",
|
|
382
396
|
folderId: `${repo}:${metaDir}`,
|
|
383
397
|
};
|
|
384
398
|
}
|
|
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
399
|
}
|
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
|
|
2
|
+
export interface CreateCollectionOptions {
|
|
3
|
+
name: string;
|
|
4
|
+
repo?: string;
|
|
5
|
+
skillsRepo?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CreateRegistryOptions {
|
|
8
|
+
name?: string;
|
|
9
|
+
repo?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface BackendStatus {
|
|
12
|
+
loggedIn: boolean;
|
|
13
|
+
identity: string;
|
|
14
|
+
hint?: string;
|
|
15
|
+
}
|
|
2
16
|
export interface StorageBackend {
|
|
3
17
|
getOwner(): Promise<string>;
|
|
18
|
+
getStatus(): Promise<BackendStatus>;
|
|
4
19
|
discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
|
|
5
20
|
readCollection(collection: CollectionInfo): Promise<CollectionFile>;
|
|
6
21
|
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
7
22
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
8
|
-
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<
|
|
23
|
+
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<string>;
|
|
9
24
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
10
25
|
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
11
26
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
12
27
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
13
28
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
14
29
|
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
15
|
-
createRegistry(
|
|
30
|
+
createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
|
|
31
|
+
createCollection(options: CreateCollectionOptions): Promise<CollectionInfo>;
|
|
16
32
|
}
|
package/dist/backends/local.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StorageBackend } from "./interface.js";
|
|
1
|
+
import type { BackendStatus, CreateRegistryOptions, StorageBackend } from "./interface.js";
|
|
2
2
|
import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Local filesystem backend — stores collections and the registry under ~/.skillsmanager/.
|
|
@@ -6,17 +6,18 @@ import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFil
|
|
|
6
6
|
*/
|
|
7
7
|
export declare class LocalBackend implements StorageBackend {
|
|
8
8
|
getOwner(): Promise<string>;
|
|
9
|
+
getStatus(): Promise<BackendStatus>;
|
|
9
10
|
discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
|
|
10
11
|
readCollection(collection: CollectionInfo): Promise<CollectionFile>;
|
|
11
12
|
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
12
13
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
13
|
-
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<
|
|
14
|
+
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<string>;
|
|
14
15
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
15
16
|
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
16
17
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
17
18
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
18
19
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
19
20
|
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
20
|
-
createRegistry(
|
|
21
|
-
createCollection(name:
|
|
21
|
+
createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
|
|
22
|
+
createCollection({ name }: import("./interface.js").CreateCollectionOptions): Promise<CollectionInfo>;
|
|
22
23
|
}
|
package/dist/backends/local.js
CHANGED
|
@@ -20,6 +20,9 @@ export class LocalBackend {
|
|
|
20
20
|
}
|
|
21
21
|
return process.env.USER ?? process.env.USERNAME ?? "unknown";
|
|
22
22
|
}
|
|
23
|
+
async getStatus() {
|
|
24
|
+
return { loggedIn: true, identity: await this.getOwner() };
|
|
25
|
+
}
|
|
23
26
|
// ── Collection operations ────────────────────────────────────────────────
|
|
24
27
|
async discoverCollections() {
|
|
25
28
|
if (!fs.existsSync(COLLECTIONS_DIR))
|
|
@@ -68,10 +71,10 @@ export class LocalBackend {
|
|
|
68
71
|
}
|
|
69
72
|
async uploadSkill(collection, localPath, skillName) {
|
|
70
73
|
const dest = path.join(collection.folderId, skillName);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
if (path.resolve(localPath) !== path.resolve(dest)) {
|
|
75
|
+
copyDirSync(localPath, dest);
|
|
76
|
+
}
|
|
77
|
+
return `${skillName}/`;
|
|
75
78
|
}
|
|
76
79
|
async deleteCollection(collection) {
|
|
77
80
|
if (fs.existsSync(collection.folderId)) {
|
|
@@ -121,26 +124,22 @@ export class LocalBackend {
|
|
|
121
124
|
folderId: dir,
|
|
122
125
|
};
|
|
123
126
|
}
|
|
124
|
-
async createRegistry(
|
|
127
|
+
async createRegistry(options) {
|
|
128
|
+
const name = options?.name ?? "local";
|
|
125
129
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
126
130
|
const owner = await this.getOwner();
|
|
127
|
-
const data = {
|
|
128
|
-
name: name ?? "local",
|
|
129
|
-
owner,
|
|
130
|
-
source: "local",
|
|
131
|
-
collections: [],
|
|
132
|
-
};
|
|
131
|
+
const data = { name, owner, source: "local", collections: [] };
|
|
133
132
|
fs.writeFileSync(LOCAL_REGISTRY_PATH, serializeRegistryFile(data));
|
|
134
133
|
return {
|
|
135
134
|
id: randomUUID(),
|
|
136
|
-
name
|
|
135
|
+
name,
|
|
137
136
|
backend: "local",
|
|
138
137
|
folderId: CONFIG_DIR,
|
|
139
138
|
fileId: LOCAL_REGISTRY_PATH,
|
|
140
139
|
};
|
|
141
140
|
}
|
|
142
141
|
// ── Convenience: create a local collection ───────────────────────────────
|
|
143
|
-
async createCollection(name) {
|
|
142
|
+
async createCollection({ name }) {
|
|
144
143
|
const dir = path.join(COLLECTIONS_DIR, name);
|
|
145
144
|
fs.mkdirSync(dir, { recursive: true });
|
|
146
145
|
const owner = await this.getOwner();
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import type { StorageBackend } from "./interface.js";
|
|
2
2
|
export declare function resolveBackend(backendName: string): Promise<StorageBackend>;
|
|
3
|
+
/** Like resolveBackend but never triggers auth flows — returns null for unconfigured backends. */
|
|
4
|
+
export declare function tryResolveBackend(backendName: string): Promise<StorageBackend | null>;
|
package/dist/backends/resolve.js
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
|
-
import { ensureAuth } from "../auth.js";
|
|
1
|
+
import { ensureAuth, getAuthClient, hasToken } from "../auth.js";
|
|
2
|
+
import { credentialsExist } from "../config.js";
|
|
2
3
|
import { GDriveBackend } from "./gdrive.js";
|
|
3
4
|
import { GithubBackend } from "./github.js";
|
|
4
5
|
import { LocalBackend } from "./local.js";
|
|
6
|
+
import { RoutingBackend } from "./routing.js";
|
|
5
7
|
export async function resolveBackend(backendName) {
|
|
8
|
+
let inner;
|
|
6
9
|
if (backendName === "gdrive")
|
|
7
|
-
|
|
10
|
+
inner = new GDriveBackend(await ensureAuth());
|
|
11
|
+
else if (backendName === "github")
|
|
12
|
+
inner = new GithubBackend();
|
|
13
|
+
else
|
|
14
|
+
inner = new LocalBackend();
|
|
15
|
+
return new RoutingBackend(inner);
|
|
16
|
+
}
|
|
17
|
+
/** Like resolveBackend but never triggers auth flows — returns null for unconfigured backends. */
|
|
18
|
+
export async function tryResolveBackend(backendName) {
|
|
19
|
+
if (backendName === "gdrive") {
|
|
20
|
+
if (!credentialsExist() || !hasToken())
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
return new RoutingBackend(new GDriveBackend(getAuthClient()));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
8
29
|
if (backendName === "github")
|
|
9
|
-
return new GithubBackend();
|
|
10
|
-
return new LocalBackend();
|
|
30
|
+
return new RoutingBackend(new GithubBackend());
|
|
31
|
+
return new RoutingBackend(new LocalBackend());
|
|
11
32
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BackendStatus, CreateCollectionOptions, CreateRegistryOptions, StorageBackend } from "./interface.js";
|
|
2
|
+
import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* RoutingBackend — a transparent decorator over any StorageBackend.
|
|
5
|
+
*
|
|
6
|
+
* Collection-metadata operations (readCollection, writeCollection, registry ops, etc.)
|
|
7
|
+
* pass straight through to the inner backend — the YAML always lives where the
|
|
8
|
+
* collection was declared.
|
|
9
|
+
*
|
|
10
|
+
* Skill-file operations (downloadSkill, uploadSkill, deleteSkill) inspect col.type
|
|
11
|
+
* and dispatch to the appropriate handler:
|
|
12
|
+
* - col.type === "github" → GithubBackend helpers (clone/pull the skills repo)
|
|
13
|
+
* - col.type absent/same → inner backend (normal behaviour, no change)
|
|
14
|
+
*
|
|
15
|
+
* This means every backend gets cross-backend routing for free without any
|
|
16
|
+
* per-backend col.type checks.
|
|
17
|
+
*/
|
|
18
|
+
export declare class RoutingBackend implements StorageBackend {
|
|
19
|
+
private readonly inner;
|
|
20
|
+
constructor(inner: StorageBackend);
|
|
21
|
+
getOwner(): Promise<string>;
|
|
22
|
+
getStatus(): Promise<BackendStatus>;
|
|
23
|
+
discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
|
|
24
|
+
readCollection(collection: CollectionInfo): Promise<CollectionFile>;
|
|
25
|
+
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
26
|
+
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
27
|
+
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
28
|
+
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
29
|
+
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
30
|
+
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
31
|
+
createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
|
|
32
|
+
createCollection(options: CreateCollectionOptions): Promise<CollectionInfo>;
|
|
33
|
+
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
34
|
+
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<string>;
|
|
35
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
36
|
+
private requireRepo;
|
|
37
|
+
private requireEntry;
|
|
38
|
+
}
|