@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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { GithubBackend } from "./github.js";
|
|
2
|
+
/**
|
|
3
|
+
* RoutingBackend — a transparent decorator over any StorageBackend.
|
|
4
|
+
*
|
|
5
|
+
* Collection-metadata operations (readCollection, writeCollection, registry ops, etc.)
|
|
6
|
+
* pass straight through to the inner backend — the YAML always lives where the
|
|
7
|
+
* collection was declared.
|
|
8
|
+
*
|
|
9
|
+
* Skill-file operations (downloadSkill, uploadSkill, deleteSkill) inspect col.type
|
|
10
|
+
* and dispatch to the appropriate handler:
|
|
11
|
+
* - col.type === "github" → GithubBackend helpers (clone/pull the skills repo)
|
|
12
|
+
* - col.type absent/same → inner backend (normal behaviour, no change)
|
|
13
|
+
*
|
|
14
|
+
* This means every backend gets cross-backend routing for free without any
|
|
15
|
+
* per-backend col.type checks.
|
|
16
|
+
*/
|
|
17
|
+
export class RoutingBackend {
|
|
18
|
+
inner;
|
|
19
|
+
constructor(inner) {
|
|
20
|
+
this.inner = inner;
|
|
21
|
+
}
|
|
22
|
+
// ── Pass-through: identity + collection metadata ───────────────────────────
|
|
23
|
+
getOwner() {
|
|
24
|
+
return this.inner.getOwner();
|
|
25
|
+
}
|
|
26
|
+
getStatus() {
|
|
27
|
+
return this.inner.getStatus();
|
|
28
|
+
}
|
|
29
|
+
discoverCollections() {
|
|
30
|
+
return this.inner.discoverCollections();
|
|
31
|
+
}
|
|
32
|
+
readCollection(collection) {
|
|
33
|
+
return this.inner.readCollection(collection);
|
|
34
|
+
}
|
|
35
|
+
writeCollection(collection, data) {
|
|
36
|
+
return this.inner.writeCollection(collection, data);
|
|
37
|
+
}
|
|
38
|
+
deleteCollection(collection) {
|
|
39
|
+
return this.inner.deleteCollection(collection);
|
|
40
|
+
}
|
|
41
|
+
// ── Pass-through: registry operations ─────────────────────────────────────
|
|
42
|
+
discoverRegistries() {
|
|
43
|
+
return this.inner.discoverRegistries();
|
|
44
|
+
}
|
|
45
|
+
readRegistry(registry) {
|
|
46
|
+
return this.inner.readRegistry(registry);
|
|
47
|
+
}
|
|
48
|
+
writeRegistry(registry, data) {
|
|
49
|
+
return this.inner.writeRegistry(registry, data);
|
|
50
|
+
}
|
|
51
|
+
resolveCollectionRef(ref) {
|
|
52
|
+
return this.inner.resolveCollectionRef(ref);
|
|
53
|
+
}
|
|
54
|
+
createRegistry(options) {
|
|
55
|
+
return this.inner.createRegistry(options);
|
|
56
|
+
}
|
|
57
|
+
createCollection(options) {
|
|
58
|
+
return this.inner.createCollection(options);
|
|
59
|
+
}
|
|
60
|
+
// ── Routed: dispatch on col.type for skill-file operations ─────────────────
|
|
61
|
+
async downloadSkill(collection, skillName, destDir) {
|
|
62
|
+
const col = await this.inner.readCollection(collection);
|
|
63
|
+
const skillType = col.type ?? collection.backend;
|
|
64
|
+
// Only cross-dispatch when the skill source differs from the collection's own backend.
|
|
65
|
+
// Same-backend collections (e.g. GitHub-native) handle routing internally.
|
|
66
|
+
if (skillType === "github" && collection.backend !== "github") {
|
|
67
|
+
const repo = this.requireRepo(col, collection.name);
|
|
68
|
+
const entry = this.requireEntry(col, skillName, collection.name);
|
|
69
|
+
await new GithubBackend().downloadSkillFromRepo(repo, entry.path, destDir);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
return this.inner.downloadSkill(collection, skillName, destDir);
|
|
73
|
+
}
|
|
74
|
+
async uploadSkill(collection, localPath, skillName) {
|
|
75
|
+
const col = await this.inner.readCollection(collection);
|
|
76
|
+
const skillType = col.type ?? collection.backend;
|
|
77
|
+
// Case 1: collection YAML in one backend, skills declared in another (col.type set)
|
|
78
|
+
if (skillType !== collection.backend) {
|
|
79
|
+
throw new Error(`Cannot upload skill to collection "${collection.name}": ` +
|
|
80
|
+
`its skills source type is "${skillType}". ` +
|
|
81
|
+
`Use --remote-path to register a skill path instead.`);
|
|
82
|
+
}
|
|
83
|
+
// Case 2: GitHub-native collection whose metadata.repo points to a foreign repo
|
|
84
|
+
if (skillType === "github" && col.metadata?.repo) {
|
|
85
|
+
const hostRepo = collection.folderId.split(":")[0];
|
|
86
|
+
const foreign = col.metadata.repo;
|
|
87
|
+
if (foreign !== hostRepo) {
|
|
88
|
+
throw new Error(`Cannot upload skill to collection "${collection.name}": ` +
|
|
89
|
+
`its skills source is "${foreign}" (a repo you may not own). ` +
|
|
90
|
+
`Use --remote-path to register a skill path instead.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return this.inner.uploadSkill(collection, localPath, skillName);
|
|
94
|
+
}
|
|
95
|
+
async deleteSkill(collection, skillName) {
|
|
96
|
+
const col = await this.inner.readCollection(collection);
|
|
97
|
+
const skillType = col.type ?? collection.backend;
|
|
98
|
+
// Only cross-dispatch when the skill source differs from the collection's own backend.
|
|
99
|
+
if (skillType === "github" && collection.backend !== "github") {
|
|
100
|
+
const repo = this.requireRepo(col, collection.name);
|
|
101
|
+
const entry = col.skills.find((s) => s.name === skillName);
|
|
102
|
+
if (!entry)
|
|
103
|
+
return;
|
|
104
|
+
await new GithubBackend().deleteSkillFromRepo(repo, entry.path);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
return this.inner.deleteSkill(collection, skillName);
|
|
108
|
+
}
|
|
109
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
110
|
+
requireRepo(col, collectionName) {
|
|
111
|
+
const repo = col.metadata?.repo;
|
|
112
|
+
if (!repo) {
|
|
113
|
+
throw new Error(`Collection "${collectionName}" has type "github" but is missing metadata.repo`);
|
|
114
|
+
}
|
|
115
|
+
return repo;
|
|
116
|
+
}
|
|
117
|
+
requireEntry(col, skillName, collectionName) {
|
|
118
|
+
const entry = col.skills.find((s) => s.name === skillName);
|
|
119
|
+
if (!entry) {
|
|
120
|
+
throw new Error(`Skill "${skillName}" not found in collection "${collectionName}"`);
|
|
121
|
+
}
|
|
122
|
+
return entry;
|
|
123
|
+
}
|
|
124
|
+
}
|
package/dist/commands/add.js
CHANGED
|
@@ -4,7 +4,7 @@ import chalk from "chalk";
|
|
|
4
4
|
import ora from "ora";
|
|
5
5
|
import YAML from "yaml";
|
|
6
6
|
import { readConfig, trackSkill } from "../config.js";
|
|
7
|
-
import {
|
|
7
|
+
import { detectRepoContext } from "../utils/git.js";
|
|
8
8
|
import { resolveBackend } from "../backends/resolve.js";
|
|
9
9
|
export async function addCommand(skillPath, options) {
|
|
10
10
|
// ── Remote-path mode: register a skill from a foreign repo without local files ─
|
|
@@ -39,7 +39,7 @@ export async function addCommand(skillPath, options) {
|
|
|
39
39
|
// If no --collection specified and a matching GitHub collection exists in config,
|
|
40
40
|
// use it automatically (no prompt — agent-friendly).
|
|
41
41
|
if (!options.collection) {
|
|
42
|
-
const ctx =
|
|
42
|
+
const ctx = detectRepoContext(absPath);
|
|
43
43
|
if (ctx) {
|
|
44
44
|
let config;
|
|
45
45
|
try {
|
|
@@ -80,15 +80,17 @@ export async function addCommand(skillPath, options) {
|
|
|
80
80
|
console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
|
-
// If the collection
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
83
|
+
// If the collection's skill type differs from its hosting backend, handle specially.
|
|
84
|
+
// col.type declares who handles skill files; col.metadata.repo gives the target repo.
|
|
85
|
+
{
|
|
86
|
+
const hostBackend = await resolveBackend(collection.backend);
|
|
87
|
+
const col = await hostBackend.readCollection(collection);
|
|
88
|
+
const skillType = col.type ?? collection.backend;
|
|
87
89
|
const foreignRepo = col.metadata?.repo;
|
|
88
|
-
if (foreignRepo) {
|
|
89
|
-
const ctx =
|
|
90
|
+
if (skillType !== collection.backend && skillType === "github" && foreignRepo) {
|
|
91
|
+
const ctx = detectRepoContext(absPath);
|
|
90
92
|
if (!ctx || ctx.repo !== foreignRepo) {
|
|
91
|
-
console.log(chalk.red(`This collection's skills source is "${foreignRepo}". ` +
|
|
93
|
+
console.log(chalk.red(`This collection's skills source is "${foreignRepo}" (type: ${skillType}). ` +
|
|
92
94
|
`The provided path does not belong to that repo.\n` +
|
|
93
95
|
chalk.dim(` To register a skill by path without a local clone, use:\n`) +
|
|
94
96
|
chalk.dim(` skillsmanager add --collection ${collection.name} --remote-path <rel/path> --name <name> --description <desc>`)));
|
|
@@ -105,7 +107,7 @@ export async function addCommand(skillPath, options) {
|
|
|
105
107
|
else {
|
|
106
108
|
col.skills.push(entry);
|
|
107
109
|
}
|
|
108
|
-
await
|
|
110
|
+
await hostBackend.writeCollection(collection, col);
|
|
109
111
|
trackSkill(skillName, collection.id, absPath);
|
|
110
112
|
spinner.succeed(`${chalk.bold(skillName)} registered in ${collection.name} at ${chalk.dim(ctx.relPath)}`);
|
|
111
113
|
}
|
|
@@ -151,14 +153,16 @@ async function addRemotePath(options) {
|
|
|
151
153
|
console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
|
|
152
154
|
return;
|
|
153
155
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
const hostBackend = await resolveBackend(collection.backend);
|
|
157
|
+
const colForType = await hostBackend.readCollection(collection);
|
|
158
|
+
const skillType = colForType.type ?? collection.backend;
|
|
159
|
+
if (skillType !== "github" && collection.backend !== "github") {
|
|
160
|
+
console.log(chalk.red(`--remote-path is only supported for collections with type "github".`));
|
|
156
161
|
return;
|
|
157
162
|
}
|
|
158
|
-
const github = new GithubBackend();
|
|
159
163
|
const spinner = ora(`Registering ${chalk.bold(skillName)} in ${collection.name} at ${chalk.dim(remotePath)}...`).start();
|
|
160
164
|
try {
|
|
161
|
-
const col =
|
|
165
|
+
const col = colForType;
|
|
162
166
|
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
163
167
|
const entry = { name: skillName, path: remotePath, description };
|
|
164
168
|
if (existing >= 0) {
|
|
@@ -167,7 +171,7 @@ async function addRemotePath(options) {
|
|
|
167
171
|
else {
|
|
168
172
|
col.skills.push(entry);
|
|
169
173
|
}
|
|
170
|
-
await
|
|
174
|
+
await hostBackend.writeCollection(collection, col);
|
|
171
175
|
spinner.succeed(`${chalk.bold(skillName)} registered in ${collection.name} at ${chalk.dim(remotePath)}`);
|
|
172
176
|
}
|
|
173
177
|
catch (err) {
|
|
@@ -176,7 +180,7 @@ async function addRemotePath(options) {
|
|
|
176
180
|
}
|
|
177
181
|
// ── GitHub path: register in-repo skill or copy external skill ────────────────
|
|
178
182
|
async function addToGithub(absPath, ctx, skillName, description, collection) {
|
|
179
|
-
const github =
|
|
183
|
+
const github = await resolveBackend("github");
|
|
180
184
|
const spinner = ora(`Adding ${chalk.bold(skillName)} to github:${collection.folderId}...`).start();
|
|
181
185
|
try {
|
|
182
186
|
const col = await github.readCollection(collection);
|
|
@@ -204,12 +208,8 @@ async function addToGithub(absPath, ctx, skillName, description, collection) {
|
|
|
204
208
|
return;
|
|
205
209
|
}
|
|
206
210
|
// Standard case: skill is in (or being added to) the collection's host repo
|
|
207
|
-
// uploadSkill
|
|
208
|
-
await github.uploadSkill(collection, absPath, skillName);
|
|
209
|
-
// Determine effective skill path in the repo
|
|
210
|
-
const skillEntry = absPath.startsWith(ctx.repoRoot)
|
|
211
|
-
? ctx.relPath // in-repo: use relative path
|
|
212
|
-
: `.agentskills/${skillName}`; // external: was copied here by uploadSkill
|
|
211
|
+
// uploadSkill returns the canonical in-repo path
|
|
212
|
+
const skillEntry = await github.uploadSkill(collection, absPath, skillName);
|
|
213
213
|
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
214
214
|
if (existing >= 0) {
|
|
215
215
|
col.skills[existing] = { name: skillName, path: skillEntry, description };
|
|
@@ -229,23 +229,7 @@ async function addToGithub(absPath, ctx, skillName, description, collection) {
|
|
|
229
229
|
async function uploadToCollection(backend, collection, absPath, skillName, description) {
|
|
230
230
|
const spinner = ora(`Adding ${chalk.bold(skillName)} to ${collection.name}...`).start();
|
|
231
231
|
try {
|
|
232
|
-
await backend.uploadSkill(collection, absPath, skillName);
|
|
233
|
-
// For GitHub collections, determine the effective in-repo path
|
|
234
|
-
let skillPath;
|
|
235
|
-
if (collection.backend === "github") {
|
|
236
|
-
// If the skill is already inside the repo workdir, use its relative path
|
|
237
|
-
const ctx = GithubBackend.detectRepoContext(absPath);
|
|
238
|
-
const repoFromCollection = collection.folderId.split(":")[0];
|
|
239
|
-
if (ctx && ctx.repo === repoFromCollection) {
|
|
240
|
-
skillPath = ctx.relPath; // e.g. "src/my-inrepo-skill"
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
skillPath = `.agentskills/${skillName}`; // external → copied here by uploadSkill
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
skillPath = `${skillName}/`;
|
|
248
|
-
}
|
|
232
|
+
const skillPath = await backend.uploadSkill(collection, absPath, skillName);
|
|
249
233
|
const col = await backend.readCollection(collection);
|
|
250
234
|
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
251
235
|
if (existing >= 0) {
|
|
@@ -2,63 +2,29 @@ import chalk from "chalk";
|
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { writeConfig, CONFIG_PATH, readConfig } from "../config.js";
|
|
5
|
-
import { ensureAuth } from "../auth.js";
|
|
6
|
-
import { GDriveBackend } from "../backends/gdrive.js";
|
|
7
|
-
import { GithubBackend } from "../backends/github.js";
|
|
8
|
-
import { LocalBackend } from "../backends/local.js";
|
|
9
5
|
import { resolveBackend } from "../backends/resolve.js";
|
|
10
6
|
export async function collectionCreateCommand(name, options = {}) {
|
|
11
7
|
const backendName = options.backend ?? "gdrive";
|
|
12
|
-
if (backendName === "github") {
|
|
13
|
-
await createGithubCollection(name, options.repo, options.skillsRepo);
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
await createGdriveCollection(name);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
async function createGithubCollection(name, repo, skillsRepo) {
|
|
20
|
-
if (!repo) {
|
|
8
|
+
if (backendName === "github" && !options.repo) {
|
|
21
9
|
console.log(chalk.red("GitHub backend requires --repo <owner/repo>"));
|
|
22
10
|
console.log(chalk.dim(" Example: skillsmanager collection create my-skills --backend github --repo owner/my-repo"));
|
|
23
11
|
return;
|
|
24
12
|
}
|
|
25
|
-
const collectionName = name ?? "default";
|
|
26
|
-
const
|
|
27
|
-
if (skillsRepo && skillsRepo !== repo) {
|
|
28
|
-
console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo} (skills source: ${skillsRepo})...\n`));
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo}...\n`));
|
|
32
|
-
}
|
|
13
|
+
const collectionName = name ?? (backendName === "gdrive" ? "MY_SKILLS" : "default");
|
|
14
|
+
const spinner = ora(`Creating collection "${collectionName}" in ${backendName}...`).start();
|
|
33
15
|
try {
|
|
34
|
-
const
|
|
35
|
-
|
|
16
|
+
const backend = await resolveBackend(backendName);
|
|
17
|
+
const collection = await backend.createCollection({
|
|
18
|
+
name: collectionName,
|
|
19
|
+
repo: options.repo,
|
|
20
|
+
skillsRepo: options.skillsRepo,
|
|
21
|
+
});
|
|
22
|
+
spinner.succeed(`Collection "${collection.name}" created (${backendName}:${collection.folderId})`);
|
|
36
23
|
const config = loadOrDefaultConfig();
|
|
37
|
-
upsertCollection(config, collection);
|
|
38
24
|
const registry = await ensureRegistry(config);
|
|
39
25
|
await registerCollectionInRegistry(registry, collection, config);
|
|
40
|
-
|
|
41
|
-
console.log(`\nRun ${chalk.bold("skillsmanager add <path>")} to add skills to it.\n`);
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
console.log(chalk.red(`Failed: ${err.message}`));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async function createGdriveCollection(name) {
|
|
48
|
-
const auth = await ensureAuth();
|
|
49
|
-
const backend = new GDriveBackend(auth);
|
|
50
|
-
const PREFIX = "SKILLS_";
|
|
51
|
-
const folderName = !name
|
|
52
|
-
? `${PREFIX}MY_SKILLS`
|
|
53
|
-
: name.startsWith(PREFIX) ? name : `${PREFIX}${name}`;
|
|
54
|
-
const spinner = ora(`Creating collection "${folderName}" in Google Drive...`).start();
|
|
55
|
-
try {
|
|
56
|
-
const collection = await backend.createCollection(folderName);
|
|
57
|
-
spinner.succeed(`Collection "${folderName}" created in Google Drive`);
|
|
58
|
-
const config = loadOrDefaultConfig();
|
|
26
|
+
collection.sourceRegistryId = registry.id;
|
|
59
27
|
upsertCollection(config, collection);
|
|
60
|
-
const registry = await ensureRegistry(config);
|
|
61
|
-
await registerCollectionInRegistry(registry, collection, config);
|
|
62
28
|
writeConfig(config);
|
|
63
29
|
console.log(`\nRun ${chalk.bold("skillsmanager add <path>")} to add skills to it.\n`);
|
|
64
30
|
}
|
|
@@ -90,7 +56,7 @@ async function ensureRegistry(config) {
|
|
|
90
56
|
if (config.registries.length > 0)
|
|
91
57
|
return config.registries[0];
|
|
92
58
|
console.log(chalk.dim(" No registry found — creating a local registry..."));
|
|
93
|
-
const local =
|
|
59
|
+
const local = await resolveBackend("local");
|
|
94
60
|
const registry = await local.createRegistry();
|
|
95
61
|
config.registries.push(registry);
|
|
96
62
|
console.log(chalk.green(" ✓ Local registry created"));
|
package/dist/commands/init.js
CHANGED
|
@@ -2,13 +2,13 @@ import chalk from "chalk";
|
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import { writeConfig, mergeCollections, readConfig } from "../config.js";
|
|
4
4
|
import { ensureAuth } from "../auth.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveBackend } from "../backends/resolve.js";
|
|
6
6
|
export async function initCommand() {
|
|
7
7
|
console.log(chalk.bold("\nSkills Manager Init\n"));
|
|
8
8
|
const auth = await ensureAuth();
|
|
9
9
|
console.log(chalk.green(" ✓ Authenticated"));
|
|
10
10
|
const spinner = ora(" Discovering collections...").start();
|
|
11
|
-
const backend =
|
|
11
|
+
const backend = await resolveBackend("gdrive");
|
|
12
12
|
const fresh = await backend.discoverCollections();
|
|
13
13
|
let existing = [];
|
|
14
14
|
try {
|
|
@@ -25,7 +25,7 @@ export async function initCommand() {
|
|
|
25
25
|
console.log(chalk.green(` ✓ Found ${collections.length} collection(s):`));
|
|
26
26
|
for (const c of collections) {
|
|
27
27
|
const col = await backend.readCollection(c);
|
|
28
|
-
console.log(`
|
|
28
|
+
console.log(` ${c.backend}:${c.name} (${col.skills.length} skills)`);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
let existingSkills = {};
|
package/dist/commands/list.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from "os";
|
|
1
2
|
import chalk from "chalk";
|
|
2
3
|
import ora from "ora";
|
|
3
4
|
import { readConfig } from "../config.js";
|
|
@@ -14,24 +15,93 @@ export async function getAllSkills() {
|
|
|
14
15
|
}
|
|
15
16
|
return allSkills;
|
|
16
17
|
}
|
|
18
|
+
function collectionTag(col) {
|
|
19
|
+
if (col.backend === "github") {
|
|
20
|
+
const repo = col.folderId.split(":")[0];
|
|
21
|
+
return `[github: ${repo}]`;
|
|
22
|
+
}
|
|
23
|
+
return `[${col.backend}]`;
|
|
24
|
+
}
|
|
25
|
+
const HOME = os.homedir();
|
|
26
|
+
function shortenPath(p) {
|
|
27
|
+
return p.startsWith(HOME) ? "~" + p.slice(HOME.length) : p;
|
|
28
|
+
}
|
|
29
|
+
function installedPaths(skillName, collectionId, skillIndex) {
|
|
30
|
+
const entries = skillIndex[skillName] ?? [];
|
|
31
|
+
const paths = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.collectionId !== collectionId)
|
|
34
|
+
continue;
|
|
35
|
+
for (const p of entry.installedAt) {
|
|
36
|
+
paths.push(shortenPath(p));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return paths;
|
|
40
|
+
}
|
|
41
|
+
function renderCollections(cols, collectionSkills, skillIndex) {
|
|
42
|
+
for (let ci = 0; ci < cols.length; ci++) {
|
|
43
|
+
const col = cols[ci];
|
|
44
|
+
const isLastCol = ci === cols.length - 1;
|
|
45
|
+
const colBranch = isLastCol ? "└──" : "├──";
|
|
46
|
+
const childPad = isLastCol ? " " : "│ ";
|
|
47
|
+
console.log(`${colBranch} ${chalk.bold.yellow(col.name)} ${chalk.dim(collectionTag(col))}`);
|
|
48
|
+
const skills = (collectionSkills.get(col.id) ?? []).sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
+
for (let si = 0; si < skills.length; si++) {
|
|
50
|
+
const skill = skills[si];
|
|
51
|
+
const isLastSkill = si === skills.length - 1;
|
|
52
|
+
const skillBranch = isLastSkill ? "└──" : "├──";
|
|
53
|
+
const paths = installedPaths(skill.name, col.id, skillIndex);
|
|
54
|
+
console.log(`${childPad}${skillBranch} ${chalk.cyan(skill.name)} ${chalk.dim(skill.description)}`);
|
|
55
|
+
if (paths.length > 0) {
|
|
56
|
+
const installPad = childPad + (isLastSkill ? " " : "│ ");
|
|
57
|
+
console.log(`${installPad}${chalk.magenta(`(${paths.join(", ")})`)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
17
62
|
export async function listCommand() {
|
|
18
63
|
const spinner = ora("Fetching skills...").start();
|
|
19
64
|
try {
|
|
20
|
-
const
|
|
65
|
+
const config = readConfig();
|
|
66
|
+
// Fetch skills per collection
|
|
67
|
+
const collectionSkills = new Map();
|
|
68
|
+
for (const col of config.collections) {
|
|
69
|
+
const backend = await resolveBackend(col.backend);
|
|
70
|
+
const colFile = await backend.readCollection(col);
|
|
71
|
+
collectionSkills.set(col.id, colFile.skills);
|
|
72
|
+
}
|
|
21
73
|
spinner.stop();
|
|
22
|
-
|
|
74
|
+
const totalSkills = [...collectionSkills.values()].reduce((n, s) => n + s.length, 0);
|
|
75
|
+
if (totalSkills === 0) {
|
|
23
76
|
console.log(chalk.yellow("No skills found across any collections."));
|
|
24
77
|
console.log(chalk.dim('Run "skillsmanager collection create <name>" to create a collection, then "skillsmanager add <path>" to add skills.'));
|
|
25
78
|
return;
|
|
26
79
|
}
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
80
|
+
// Group collections by their source registry
|
|
81
|
+
const byRegistry = new Map();
|
|
82
|
+
for (const col of config.collections) {
|
|
83
|
+
const key = col.sourceRegistryId ?? null;
|
|
84
|
+
if (!byRegistry.has(key))
|
|
85
|
+
byRegistry.set(key, []);
|
|
86
|
+
byRegistry.get(key).push(col);
|
|
33
87
|
}
|
|
34
88
|
console.log();
|
|
89
|
+
// Render each registry and its collections
|
|
90
|
+
for (const reg of config.registries) {
|
|
91
|
+
const cols = byRegistry.get(reg.id);
|
|
92
|
+
if (!cols || cols.length === 0)
|
|
93
|
+
continue;
|
|
94
|
+
console.log(`${chalk.bold.white(reg.name)} ${chalk.dim(`${reg.backend}`)}`);
|
|
95
|
+
renderCollections(cols, collectionSkills, config.skills);
|
|
96
|
+
console.log();
|
|
97
|
+
}
|
|
98
|
+
// Collections not associated with any registry
|
|
99
|
+
const orphans = byRegistry.get(null);
|
|
100
|
+
if (orphans && orphans.length > 0) {
|
|
101
|
+
console.log(`${chalk.bold.white("(unregistered)")} ${chalk.dim("run 'skillsmanager refresh' to link to a registry")}`);
|
|
102
|
+
renderCollections(orphans, collectionSkills, config.skills);
|
|
103
|
+
console.log();
|
|
104
|
+
}
|
|
35
105
|
}
|
|
36
106
|
catch (err) {
|
|
37
107
|
spinner.stop();
|
package/dist/commands/refresh.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function refreshCommand() {
|
|
|
46
46
|
const refBackend = await resolveBackend(ref.backend);
|
|
47
47
|
const colInfo = await refBackend.resolveCollectionRef(ref);
|
|
48
48
|
if (colInfo) {
|
|
49
|
-
freshCollections.push(colInfo);
|
|
49
|
+
freshCollections.push({ ...colInfo, sourceRegistryId: registry.id });
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
52
|
console.log(chalk.dim(`\n Warning: collection "${ref.name}" listed in registry "${registry.name}" could not be resolved`));
|
|
@@ -2,8 +2,6 @@ import chalk from "chalk";
|
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { readConfig, writeConfig, mergeRegistries, CONFIG_PATH, CACHE_DIR } from "../config.js";
|
|
5
|
-
import { GithubBackend } from "../backends/github.js";
|
|
6
|
-
import { LocalBackend } from "../backends/local.js";
|
|
7
5
|
import { resolveBackend } from "../backends/resolve.js";
|
|
8
6
|
export async function registryCreateCommand(options) {
|
|
9
7
|
const backend = options.backend ?? "local";
|
|
@@ -20,13 +18,7 @@ export async function registryCreateCommand(options) {
|
|
|
20
18
|
const label = backend === "local" ? "locally" : `in ${backend}`;
|
|
21
19
|
const spinner = ora(`Creating registry ${label}...`).start();
|
|
22
20
|
try {
|
|
23
|
-
|
|
24
|
-
if (backend === "github") {
|
|
25
|
-
registry = await new GithubBackend().createRegistry(undefined, options.repo);
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
registry = await (await resolveBackend(backend)).createRegistry();
|
|
29
|
-
}
|
|
21
|
+
const registry = await (await resolveBackend(backend)).createRegistry({ repo: options.repo });
|
|
30
22
|
spinner.succeed(`Registry created ${label}`);
|
|
31
23
|
let config = { registries: [], collections: [], skills: {}, discoveredAt: new Date().toISOString() };
|
|
32
24
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
@@ -40,7 +32,7 @@ export async function registryCreateCommand(options) {
|
|
|
40
32
|
if (backend !== "local") {
|
|
41
33
|
const localReg = config.registries.find((r) => r.backend === "local");
|
|
42
34
|
if (localReg) {
|
|
43
|
-
const local =
|
|
35
|
+
const local = await resolveBackend("local");
|
|
44
36
|
try {
|
|
45
37
|
const localData = await local.readRegistry(localReg);
|
|
46
38
|
const localCollections = localData.collections.filter((c) => c.backend === "local");
|
|
@@ -270,7 +262,7 @@ export async function registryPushCommand(options) {
|
|
|
270
262
|
console.log(chalk.yellow("No local registry to push."));
|
|
271
263
|
return;
|
|
272
264
|
}
|
|
273
|
-
const local =
|
|
265
|
+
const local = await resolveBackend("local");
|
|
274
266
|
const localData = await local.readRegistry(localReg);
|
|
275
267
|
const localCollectionRefs = localData.collections.filter((c) => c.backend === "local");
|
|
276
268
|
if (localCollectionRefs.length === 0) {
|
|
@@ -308,16 +300,10 @@ export async function registryPushCommand(options) {
|
|
|
308
300
|
const collInfo = await local.resolveCollectionRef(ref);
|
|
309
301
|
if (!collInfo)
|
|
310
302
|
throw new Error(`Collection "${ref.name}" not found locally`);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
remoteCol = await gdrive.createCollection(folderName);
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
const github = remote;
|
|
319
|
-
remoteCol = await github.createCollection(ref.name, options.repo);
|
|
320
|
-
}
|
|
303
|
+
const remoteCol = await remote.createCollection({
|
|
304
|
+
name: ref.name,
|
|
305
|
+
repo: options.repo,
|
|
306
|
+
});
|
|
321
307
|
const colData = await local.readCollection({ ...collInfo, id: "temp" });
|
|
322
308
|
for (const skill of colData.skills) {
|
|
323
309
|
const localSkillPath = path.join(collInfo.folderId, skill.name);
|
package/dist/commands/search.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function searchCommand(query) {
|
|
|
26
26
|
console.log(`\n ${chalk.dim("NAME".padEnd(maxName + 2))}${chalk.dim("DESCRIPTION".padEnd(maxDesc + 2))}${chalk.dim("SOURCE")}`);
|
|
27
27
|
console.log(` ${chalk.dim("-".repeat(maxName + maxDesc + 30))}`);
|
|
28
28
|
for (const s of ranked) {
|
|
29
|
-
console.log(` ${chalk.cyan(s.entry.name.padEnd(maxName + 2))}${s.entry.description.padEnd(maxDesc + 2)}${chalk.dim(
|
|
29
|
+
console.log(` ${chalk.cyan(s.entry.name.padEnd(maxName + 2))}${s.entry.description.padEnd(maxDesc + 2)}${chalk.dim(`${s.collection.backend}:${s.collection.name}`)}`);
|
|
30
30
|
}
|
|
31
31
|
console.log();
|
|
32
32
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,47 +1,17 @@
|
|
|
1
|
-
import os from "os";
|
|
2
1
|
import chalk from "chalk";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return { name: "gdrive", loggedIn: false, identity: "", hint: "run: skillsmanager setup google" };
|
|
10
|
-
}
|
|
11
|
-
if (!hasToken()) {
|
|
12
|
-
return { name: "gdrive", loggedIn: false, identity: "", hint: "run: skillsmanager setup google" };
|
|
13
|
-
}
|
|
14
|
-
try {
|
|
15
|
-
const client = getAuthClient();
|
|
16
|
-
const backend = new GDriveBackend(client);
|
|
17
|
-
const email = await backend.getOwner();
|
|
18
|
-
return { name: "gdrive", loggedIn: true, identity: email };
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return { name: "gdrive", loggedIn: false, identity: "", hint: "run: skillsmanager setup google" };
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function getGithubStatus() {
|
|
25
|
-
if (!ghInstalled()) {
|
|
26
|
-
return { name: "github", loggedIn: false, identity: "", hint: "install gh CLI first" };
|
|
27
|
-
}
|
|
28
|
-
if (!ghAuthed()) {
|
|
29
|
-
return { name: "github", loggedIn: false, identity: "", hint: "run: skillsmanager setup github" };
|
|
30
|
-
}
|
|
31
|
-
const login = ghGetLogin();
|
|
32
|
-
return { name: "github", loggedIn: true, identity: login };
|
|
33
|
-
}
|
|
2
|
+
import { tryResolveBackend } from "../backends/resolve.js";
|
|
3
|
+
const BACKENDS = [
|
|
4
|
+
{ name: "local", hint: "" },
|
|
5
|
+
{ name: "gdrive", hint: "run: skillsmanager setup google" },
|
|
6
|
+
{ name: "github", hint: "run: skillsmanager setup github" },
|
|
7
|
+
];
|
|
34
8
|
export async function statusCommand() {
|
|
35
|
-
const
|
|
36
|
-
name
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
getGdriveStatus(),
|
|
42
|
-
Promise.resolve(getGithubStatus()),
|
|
43
|
-
]);
|
|
44
|
-
const rows = [localStatus, gdriveStatus, githubStatus];
|
|
9
|
+
const rows = await Promise.all(BACKENDS.map(async ({ name, hint }) => {
|
|
10
|
+
const backend = await tryResolveBackend(name);
|
|
11
|
+
if (!backend)
|
|
12
|
+
return { name, loggedIn: false, identity: "", hint };
|
|
13
|
+
return { name, ...(await backend.getStatus()) };
|
|
14
|
+
}));
|
|
45
15
|
const col1 = 8;
|
|
46
16
|
const col2 = 24;
|
|
47
17
|
const header = chalk.bold("Backend".padEnd(col1)) + " " +
|
|
@@ -52,13 +22,13 @@ export async function statusCommand() {
|
|
|
52
22
|
console.log(header);
|
|
53
23
|
console.log(chalk.dim(divider));
|
|
54
24
|
for (const row of rows) {
|
|
25
|
+
const statusLabel = row.loggedIn ? "✓ logged in" : "✗ not logged in";
|
|
55
26
|
const status = row.loggedIn
|
|
56
|
-
? chalk.green(
|
|
57
|
-
: chalk.red(
|
|
27
|
+
? chalk.green(statusLabel)
|
|
28
|
+
: chalk.red(statusLabel);
|
|
58
29
|
const identity = row.loggedIn
|
|
59
30
|
? chalk.white(row.identity)
|
|
60
31
|
: chalk.dim(row.hint ?? "");
|
|
61
|
-
const statusLabel = row.loggedIn ? "✓ logged in" : "✗ not logged in";
|
|
62
32
|
console.log(row.name.padEnd(col1) + " " + status + " ".repeat(Math.max(0, col2 - statusLabel.length)) + " " + identity);
|
|
63
33
|
}
|
|
64
34
|
console.log();
|
package/dist/config.js
CHANGED
|
@@ -70,10 +70,15 @@ export function readConfig() {
|
|
|
70
70
|
* fresh UUID. This keeps cache paths stable across refreshes.
|
|
71
71
|
*/
|
|
72
72
|
export function mergeCollections(fresh, existing) {
|
|
73
|
-
|
|
73
|
+
const freshFolderIds = new Set(fresh.map((c) => c.folderId));
|
|
74
|
+
// Fresh collections, preserving UUIDs for already-known ones
|
|
75
|
+
const updated = fresh.map((c) => {
|
|
74
76
|
const prev = existing.find((e) => e.folderId === c.folderId);
|
|
75
77
|
return { ...c, id: prev?.id ?? randomUUID() };
|
|
76
78
|
});
|
|
79
|
+
// Keep existing collections that weren't re-discovered (temporarily unavailable, etc.)
|
|
80
|
+
const kept = existing.filter((e) => !freshFolderIds.has(e.folderId));
|
|
81
|
+
return [...updated, ...kept];
|
|
77
82
|
}
|
|
78
83
|
/**
|
|
79
84
|
* Merges freshly discovered registries with existing ones, preserving UUIDs
|