@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.
@@ -3,9 +3,15 @@ import path from "path";
3
3
  import chalk from "chalk";
4
4
  import ora from "ora";
5
5
  import YAML from "yaml";
6
- import { ensureReady } from "../ready.js";
7
- import { trackSkill } from "../config.js";
6
+ import { readConfig, trackSkill } from "../config.js";
7
+ import { GithubBackend } from "../backends/github.js";
8
+ import { resolveBackend } from "../backends/resolve.js";
8
9
  export async function addCommand(skillPath, options) {
10
+ // ── Remote-path mode: register a skill from a foreign repo without local files ─
11
+ if (options.remotePath) {
12
+ await addRemotePath(options);
13
+ return;
14
+ }
9
15
  const absPath = path.resolve(skillPath);
10
16
  if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) {
11
17
  console.log(chalk.red(`"${skillPath}" is not a valid directory.`));
@@ -29,8 +35,37 @@ export async function addCommand(skillPath, options) {
29
35
  console.log(chalk.red("SKILL.md frontmatter is missing 'name' field."));
30
36
  return;
31
37
  }
32
- const { config, backend } = await ensureReady();
33
- // Pick collection first one by default, or by name
38
+ // ── Auto-detect if skill lives inside a GitHub-tracked repo ─────────────────
39
+ // If no --collection specified and a matching GitHub collection exists in config,
40
+ // use it automatically (no prompt — agent-friendly).
41
+ if (!options.collection) {
42
+ const ctx = GithubBackend.detectRepoContext(absPath);
43
+ if (ctx) {
44
+ let config;
45
+ try {
46
+ config = readConfig();
47
+ }
48
+ catch {
49
+ config = null;
50
+ }
51
+ const githubCollection = config?.collections.find((c) => c.backend === "github" && c.folderId.startsWith(`${ctx.repo}:`));
52
+ if (githubCollection) {
53
+ await addToGithub(absPath, ctx, skillName, description, githubCollection);
54
+ return;
55
+ }
56
+ // No matching GitHub collection — fall through to standard flow
57
+ // (user can run `skillsmanager collection create --backend github --repo <repo>` first)
58
+ }
59
+ }
60
+ // ── Standard flow ─────────────────────────────────────────────────────────────
61
+ let config;
62
+ try {
63
+ config = readConfig();
64
+ }
65
+ catch {
66
+ console.log(chalk.red("No config found. Run: skillsmanager collection create"));
67
+ return;
68
+ }
34
69
  let collection = config.collections[0];
35
70
  if (options.collection) {
36
71
  const found = config.collections.find((c) => c.name === options.collection);
@@ -41,20 +76,187 @@ export async function addCommand(skillPath, options) {
41
76
  }
42
77
  collection = found;
43
78
  }
79
+ if (!collection) {
80
+ console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
81
+ return;
82
+ }
83
+ // If the collection has metadata.repo (foreign skills repo), handle specially
84
+ if (collection.backend === "github") {
85
+ const github = new GithubBackend();
86
+ const col = await github.readCollection(collection);
87
+ const foreignRepo = col.metadata?.repo;
88
+ if (foreignRepo) {
89
+ const ctx = GithubBackend.detectRepoContext(absPath);
90
+ if (!ctx || ctx.repo !== foreignRepo) {
91
+ console.log(chalk.red(`This collection's skills source is "${foreignRepo}". ` +
92
+ `The provided path does not belong to that repo.\n` +
93
+ chalk.dim(` To register a skill by path without a local clone, use:\n`) +
94
+ chalk.dim(` skillsmanager add --collection ${collection.name} --remote-path <rel/path> --name <name> --description <desc>`)));
95
+ return;
96
+ }
97
+ // Path is from the foreign repo (cloned locally) — register relative path only, no upload
98
+ const spinner = ora(`Adding ${chalk.bold(skillName)} to ${collection.name}...`).start();
99
+ try {
100
+ const existing = col.skills.findIndex((s) => s.name === skillName);
101
+ const entry = { name: skillName, path: ctx.relPath, description };
102
+ if (existing >= 0) {
103
+ col.skills[existing] = entry;
104
+ }
105
+ else {
106
+ col.skills.push(entry);
107
+ }
108
+ await github.writeCollection(collection, col);
109
+ trackSkill(skillName, collection.id, absPath);
110
+ spinner.succeed(`${chalk.bold(skillName)} registered in ${collection.name} at ${chalk.dim(ctx.relPath)}`);
111
+ }
112
+ catch (err) {
113
+ spinner.fail(`Failed: ${err.message}`);
114
+ }
115
+ return;
116
+ }
117
+ }
118
+ const backend = await resolveBackend(collection.backend);
119
+ await uploadToCollection(backend, collection, absPath, skillName, description);
120
+ }
121
+ // ── Remote-path mode: register a skill entry without local files ─────────────
122
+ async function addRemotePath(options) {
123
+ const { remotePath, name: skillName, description = "", collection: collectionName } = options;
124
+ if (!remotePath) {
125
+ console.log(chalk.red("--remote-path requires a relative path (e.g. tools/my-skill/)"));
126
+ return;
127
+ }
128
+ if (!skillName) {
129
+ console.log(chalk.red("--remote-path requires --name <skill-name>"));
130
+ return;
131
+ }
132
+ let config;
133
+ try {
134
+ config = readConfig();
135
+ }
136
+ catch {
137
+ console.log(chalk.red("No config found. Run: skillsmanager collection create"));
138
+ return;
139
+ }
140
+ let collection = config.collections[0];
141
+ if (collectionName) {
142
+ const found = config.collections.find((c) => c.name === collectionName);
143
+ if (!found) {
144
+ console.log(chalk.red(`Collection "${collectionName}" not found.`));
145
+ console.log(chalk.dim(` Available: ${config.collections.map((c) => c.name).join(", ")}`));
146
+ return;
147
+ }
148
+ collection = found;
149
+ }
150
+ if (!collection) {
151
+ console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
152
+ return;
153
+ }
154
+ if (collection.backend !== "github") {
155
+ console.log(chalk.red("--remote-path is only supported for GitHub collections."));
156
+ return;
157
+ }
158
+ const github = new GithubBackend();
159
+ const spinner = ora(`Registering ${chalk.bold(skillName)} in ${collection.name} at ${chalk.dim(remotePath)}...`).start();
160
+ try {
161
+ const col = await github.readCollection(collection);
162
+ const existing = col.skills.findIndex((s) => s.name === skillName);
163
+ const entry = { name: skillName, path: remotePath, description };
164
+ if (existing >= 0) {
165
+ col.skills[existing] = entry;
166
+ }
167
+ else {
168
+ col.skills.push(entry);
169
+ }
170
+ await github.writeCollection(collection, col);
171
+ spinner.succeed(`${chalk.bold(skillName)} registered in ${collection.name} at ${chalk.dim(remotePath)}`);
172
+ }
173
+ catch (err) {
174
+ spinner.fail(`Failed: ${err.message}`);
175
+ }
176
+ }
177
+ // ── GitHub path: register in-repo skill or copy external skill ────────────────
178
+ async function addToGithub(absPath, ctx, skillName, description, collection) {
179
+ const github = new GithubBackend();
180
+ const spinner = ora(`Adding ${chalk.bold(skillName)} to github:${collection.folderId}...`).start();
181
+ try {
182
+ const col = await github.readCollection(collection);
183
+ const foreignRepo = col.metadata?.repo;
184
+ const hostRepo = collection.folderId.split(":")[0];
185
+ // If collection has metadata.repo pointing to a foreign repo, validate that
186
+ // the local skill belongs to that foreign repo (not the collection host repo).
187
+ if (foreignRepo && foreignRepo !== hostRepo) {
188
+ if (ctx.repo !== foreignRepo) {
189
+ spinner.fail(`This collection's skills source is "${foreignRepo}" but the provided path belongs to "${ctx.repo}".`);
190
+ return;
191
+ }
192
+ // Skill is in the foreign repo (cloned locally) — register path only, no upload
193
+ const entry = { name: skillName, path: ctx.relPath, description };
194
+ const existing = col.skills.findIndex((s) => s.name === skillName);
195
+ if (existing >= 0) {
196
+ col.skills[existing] = entry;
197
+ }
198
+ else {
199
+ col.skills.push(entry);
200
+ }
201
+ await github.writeCollection(collection, col);
202
+ trackSkill(skillName, collection.id, absPath);
203
+ spinner.succeed(`${chalk.bold(skillName)} registered in github:${collection.folderId} at ${chalk.dim(ctx.relPath)}`);
204
+ return;
205
+ }
206
+ // Standard case: skill is in (or being added to) the collection's host repo
207
+ // uploadSkill is a no-op for in-repo skills; copies if external
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
213
+ const existing = col.skills.findIndex((s) => s.name === skillName);
214
+ if (existing >= 0) {
215
+ col.skills[existing] = { name: skillName, path: skillEntry, description };
216
+ }
217
+ else {
218
+ col.skills.push({ name: skillName, path: skillEntry, description });
219
+ }
220
+ await github.writeCollection(collection, col);
221
+ trackSkill(skillName, collection.id, absPath);
222
+ spinner.succeed(`${chalk.bold(skillName)} registered in github:${collection.folderId} at ${chalk.dim(skillEntry)}`);
223
+ }
224
+ catch (err) {
225
+ spinner.fail(`Failed: ${err.message}`);
226
+ }
227
+ }
228
+ // ── Shared: upload to any collection backend ──────────────────────────────────
229
+ async function uploadToCollection(backend, collection, absPath, skillName, description) {
44
230
  const spinner = ora(`Adding ${chalk.bold(skillName)} to ${collection.name}...`).start();
45
231
  try {
46
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
+ }
47
249
  const col = await backend.readCollection(collection);
48
250
  const existing = col.skills.findIndex((s) => s.name === skillName);
49
251
  if (existing >= 0) {
50
- col.skills[existing] = { name: skillName, path: `${skillName}/`, description };
252
+ col.skills[existing] = { name: skillName, path: skillPath, description };
51
253
  }
52
254
  else {
53
- col.skills.push({ name: skillName, path: `${skillName}/`, description });
255
+ col.skills.push({ name: skillName, path: skillPath, description });
54
256
  }
55
257
  await backend.writeCollection(collection, col);
56
258
  trackSkill(skillName, collection.id, absPath);
57
- spinner.succeed(`${chalk.bold(skillName)} added to gdrive:${collection.name}`);
259
+ spinner.succeed(`${chalk.bold(skillName)} added to ${collection.backend}:${collection.name}`);
58
260
  }
59
261
  catch (err) {
60
262
  spinner.fail(`Failed: ${err.message}`);
@@ -1 +1,5 @@
1
- export declare function collectionCreateCommand(name?: string): Promise<void>;
1
+ export declare function collectionCreateCommand(name?: string, options?: {
2
+ backend?: string;
3
+ repo?: string;
4
+ skillsRepo?: string;
5
+ }): Promise<void>;
@@ -1,42 +1,116 @@
1
1
  import chalk from "chalk";
2
2
  import ora from "ora";
3
- import { writeConfig, CONFIG_PATH } from "../config.js";
3
+ import fs from "fs";
4
+ import { writeConfig, CONFIG_PATH, readConfig } from "../config.js";
4
5
  import { ensureAuth } from "../auth.js";
5
6
  import { GDriveBackend } from "../backends/gdrive.js";
6
- import fs from "fs";
7
- export async function collectionCreateCommand(name) {
8
- const auth = await ensureAuth();
9
- const backend = new GDriveBackend(auth);
10
- const PREFIX = "SKILLS_";
11
- let folderName;
12
- if (!name) {
13
- folderName = `${PREFIX}MY_SKILLS`;
7
+ import { GithubBackend } from "../backends/github.js";
8
+ import { LocalBackend } from "../backends/local.js";
9
+ import { resolveBackend } from "../backends/resolve.js";
10
+ export async function collectionCreateCommand(name, options = {}) {
11
+ 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) {
21
+ console.log(chalk.red("GitHub backend requires --repo <owner/repo>"));
22
+ console.log(chalk.dim(" Example: skillsmanager collection create my-skills --backend github --repo owner/my-repo"));
23
+ return;
24
+ }
25
+ const collectionName = name ?? "default";
26
+ const backend = new GithubBackend();
27
+ if (skillsRepo && skillsRepo !== repo) {
28
+ console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo} (skills source: ${skillsRepo})...\n`));
14
29
  }
15
30
  else {
16
- folderName = name.startsWith(PREFIX) ? name : `${PREFIX}${name}`;
31
+ console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo}...\n`));
32
+ }
33
+ try {
34
+ const collection = await backend.createCollection(collectionName, repo, skillsRepo);
35
+ console.log(chalk.green(`\n ✓ Collection "${collectionName}" created in github:${collection.folderId}`));
36
+ const config = loadOrDefaultConfig();
37
+ upsertCollection(config, collection);
38
+ const registry = await ensureRegistry(config);
39
+ await registerCollectionInRegistry(registry, collection, config);
40
+ writeConfig(config);
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}`));
17
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}`;
18
54
  const spinner = ora(`Creating collection "${folderName}" in Google Drive...`).start();
19
55
  try {
20
56
  const collection = await backend.createCollection(folderName);
21
57
  spinner.succeed(`Collection "${folderName}" created in Google Drive`);
22
- let config = { registries: [], collections: [], skills: {}, discoveredAt: new Date().toISOString() };
23
- if (fs.existsSync(CONFIG_PATH)) {
24
- try {
25
- config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
26
- }
27
- catch { /* use default */ }
28
- }
29
- const already = config.collections.findIndex((c) => c.name === collection.name);
30
- if (already >= 0) {
31
- config.collections[already] = collection;
32
- }
33
- else {
34
- config.collections.push(collection);
35
- }
58
+ const config = loadOrDefaultConfig();
59
+ upsertCollection(config, collection);
60
+ const registry = await ensureRegistry(config);
61
+ await registerCollectionInRegistry(registry, collection, config);
36
62
  writeConfig(config);
37
- console.log(`\nRun ${chalk.bold(`skillsmanager add <path>`)} to add skills to it.\n`);
63
+ console.log(`\nRun ${chalk.bold("skillsmanager add <path>")} to add skills to it.\n`);
38
64
  }
39
65
  catch (err) {
40
66
  spinner.fail(`Failed: ${err.message}`);
41
67
  }
42
68
  }
69
+ // ── helpers ───────────────────────────────────────────────────────────────────
70
+ function loadOrDefaultConfig() {
71
+ if (fs.existsSync(CONFIG_PATH)) {
72
+ try {
73
+ return readConfig();
74
+ }
75
+ catch { /* fall through */ }
76
+ }
77
+ return { registries: [], collections: [], skills: {}, discoveredAt: new Date().toISOString() };
78
+ }
79
+ function upsertCollection(config, collection) {
80
+ const idx = config.collections.findIndex((c) => c.name === collection.name);
81
+ if (idx >= 0) {
82
+ config.collections[idx] = collection;
83
+ }
84
+ else {
85
+ config.collections.push(collection);
86
+ }
87
+ }
88
+ /** Returns the first registry in config, auto-creating a local one if none exists. */
89
+ async function ensureRegistry(config) {
90
+ if (config.registries.length > 0)
91
+ return config.registries[0];
92
+ console.log(chalk.dim(" No registry found — creating a local registry..."));
93
+ const local = new LocalBackend();
94
+ const registry = await local.createRegistry();
95
+ config.registries.push(registry);
96
+ console.log(chalk.green(" ✓ Local registry created"));
97
+ return registry;
98
+ }
99
+ /** Registers the collection ref in the given registry (writes directly to the registry's backend). */
100
+ async function registerCollectionInRegistry(registry, collection, config) {
101
+ const backend = await resolveBackend(registry.backend);
102
+ const registryData = await backend.readRegistry(registry);
103
+ if (registryData.collections.find((c) => c.name === collection.name))
104
+ return;
105
+ registryData.collections.push({
106
+ name: collection.name,
107
+ backend: collection.backend,
108
+ ref: collection.folderId,
109
+ });
110
+ await backend.writeRegistry(registry, registryData);
111
+ // Keep local config registry list in sync
112
+ if (!config.registries.find((r) => r.id === registry.id)) {
113
+ config.registries.push(registry);
114
+ }
115
+ console.log(chalk.dim(` Registered in registry "${registry.name}" (${registry.backend})`));
116
+ }
@@ -2,21 +2,22 @@ import chalk from "chalk";
2
2
  import ora from "ora";
3
3
  import path from "path";
4
4
  import { getCachePath, ensureCachePath, createSymlink } from "../cache.js";
5
- import { ensureReady } from "../ready.js";
6
- import { trackSkill } from "../config.js";
5
+ import { readConfig, trackSkill } from "../config.js";
6
+ import { resolveBackend } from "../backends/resolve.js";
7
7
  export async function fetchCommand(names, options) {
8
8
  if (names.length === 0) {
9
9
  console.log(chalk.red("Please specify at least one skill name."));
10
10
  console.log(chalk.dim(" Example: skillsmanager fetch pdf-skill --agent claude"));
11
11
  return;
12
12
  }
13
- const { config, backend } = await ensureReady();
14
- // Gather all skills across collections
13
+ const config = readConfig();
14
+ // Gather all skills across collections (resolve backend per collection)
15
15
  const allSkills = [];
16
16
  for (const collection of config.collections) {
17
+ const backend = await resolveBackend(collection.backend);
17
18
  const col = await backend.readCollection(collection);
18
19
  for (const entry of col.skills) {
19
- allSkills.push({ entry, collection });
20
+ allSkills.push({ entry, collection, backend });
20
21
  }
21
22
  }
22
23
  const scope = options.scope ?? "global";
@@ -31,7 +32,7 @@ export async function fetchCommand(names, options) {
31
32
  try {
32
33
  ensureCachePath(match.collection);
33
34
  const cachePath = getCachePath(match.collection, name);
34
- await backend.downloadSkill(match.collection, name, cachePath);
35
+ await match.backend.downloadSkill(match.collection, name, cachePath);
35
36
  const { skillsDir, created } = createSymlink(name, cachePath, options.agent, scope, cwd);
36
37
  trackSkill(name, match.collection.id, path.join(skillsDir, name));
37
38
  spinner.succeed(`${chalk.bold(name)} → ${scope === "project" ? "project" : "global"} ${options.agent} skills`);
@@ -1,10 +1,12 @@
1
1
  import chalk from "chalk";
2
2
  import ora from "ora";
3
- import { ensureReady } from "../ready.js";
3
+ import { readConfig } from "../config.js";
4
+ import { resolveBackend } from "../backends/resolve.js";
4
5
  export async function getAllSkills() {
5
- const { config, backend } = await ensureReady();
6
+ const config = readConfig();
6
7
  const allSkills = [];
7
8
  for (const collection of config.collections) {
9
+ const backend = await resolveBackend(collection.backend);
8
10
  const col = await backend.readCollection(collection);
9
11
  for (const entry of col.skills) {
10
12
  allSkills.push({ entry, collection });
@@ -27,7 +29,7 @@ export async function listCommand() {
27
29
  console.log(`\n ${chalk.dim("NAME".padEnd(maxName + 2))}${chalk.dim("DESCRIPTION".padEnd(maxDesc + 2))}${chalk.dim("SOURCE")}`);
28
30
  console.log(` ${chalk.dim("-".repeat(maxName + maxDesc + 30))}`);
29
31
  for (const s of skills.sort((a, b) => a.entry.name.localeCompare(b.entry.name))) {
30
- console.log(` ${chalk.cyan(s.entry.name.padEnd(maxName + 2))}${s.entry.description.padEnd(maxDesc + 2)}${chalk.dim(`gdrive:${s.collection.name}`)}`);
32
+ console.log(` ${chalk.cyan(s.entry.name.padEnd(maxName + 2))}${s.entry.description.padEnd(maxDesc + 2)}${chalk.dim(`${s.collection.backend}:${s.collection.name}`)}`);
31
33
  }
32
34
  console.log();
33
35
  }
@@ -0,0 +1,4 @@
1
+ export declare function logoutGoogleCommand(options: {
2
+ all?: boolean;
3
+ }): Promise<void>;
4
+ export declare function logoutGithubCommand(): void;
@@ -0,0 +1,35 @@
1
+ import fs from "fs";
2
+ import chalk from "chalk";
3
+ import { CREDENTIALS_PATH, TOKEN_PATH } from "../config.js";
4
+ import { spawnSync } from "child_process";
5
+ export async function logoutGoogleCommand(options) {
6
+ const removeAll = options.all;
7
+ let removed = false;
8
+ if (fs.existsSync(TOKEN_PATH)) {
9
+ fs.unlinkSync(TOKEN_PATH);
10
+ console.log(chalk.green(" ✓ Removed token.json (OAuth session cleared)"));
11
+ removed = true;
12
+ }
13
+ else {
14
+ console.log(chalk.dim(" – token.json not found (already logged out)"));
15
+ }
16
+ if (removeAll) {
17
+ if (fs.existsSync(CREDENTIALS_PATH)) {
18
+ fs.unlinkSync(CREDENTIALS_PATH);
19
+ console.log(chalk.green(" ✓ Removed credentials.json (OAuth client cleared)"));
20
+ removed = true;
21
+ }
22
+ else {
23
+ console.log(chalk.dim(" – credentials.json not found"));
24
+ }
25
+ }
26
+ if (removed) {
27
+ console.log(chalk.dim("\n Run skillsmanager setup google to set up again."));
28
+ }
29
+ }
30
+ export function logoutGithubCommand() {
31
+ const r = spawnSync("gh", ["auth", "logout"], { stdio: "inherit" });
32
+ if (r.status !== 0) {
33
+ console.log(chalk.red(" gh auth logout failed. Try manually: gh auth logout"));
34
+ }
35
+ }
@@ -1,40 +1,93 @@
1
1
  import chalk from "chalk";
2
2
  import ora from "ora";
3
- import { writeConfig, mergeCollections, readConfig } from "../config.js";
4
- import { ensureAuth } from "../auth.js";
5
- import { GDriveBackend } from "../backends/gdrive.js";
3
+ import { writeConfig, mergeCollections, mergeRegistries, readConfig } from "../config.js";
4
+ import { resolveBackend } from "../backends/resolve.js";
6
5
  export async function refreshCommand() {
7
- const spinner = ora("Discovering collections...").start();
6
+ const spinner = ora("Discovering registries...").start();
8
7
  try {
9
- const auth = await ensureAuth();
10
- const backend = new GDriveBackend(auth);
11
- const fresh = await backend.discoverCollections();
12
- let existing = [];
8
+ let existingCollections = [];
9
+ let existingSkills = {};
10
+ let existingRegistries = [];
13
11
  try {
14
- existing = readConfig().collections;
12
+ const cfg = readConfig();
13
+ existingCollections = cfg.collections;
14
+ existingSkills = cfg.skills ?? {};
15
+ existingRegistries = cfg.registries ?? [];
15
16
  }
16
17
  catch { /* no existing config */ }
17
- const collections = mergeCollections(fresh, existing);
18
- let existingSkills = {};
19
- try {
20
- existingSkills = readConfig().skills ?? {};
18
+ // Step 1: Discover registries across all backends
19
+ const backendsToScan = ["gdrive", "github", "local"];
20
+ const freshRegistries = [];
21
+ const skippedBackends = [];
22
+ for (const backendName of backendsToScan) {
23
+ try {
24
+ const backend = await resolveBackend(backendName);
25
+ const found = await backend.discoverRegistries();
26
+ freshRegistries.push(...found);
27
+ }
28
+ catch {
29
+ skippedBackends.push(backendName);
30
+ }
21
31
  }
22
- catch { /* ok */ }
23
- let existingRegistries = [];
24
- try {
25
- existingRegistries = readConfig().registries ?? [];
32
+ if (skippedBackends.length > 0) {
33
+ console.log(chalk.dim(` Skipped (not configured): ${skippedBackends.join(", ")}`));
34
+ console.log(chalk.dim(` Run: skillsmanager setup ${skippedBackends[0]} to enable`));
35
+ }
36
+ const mergedRegistries = mergeRegistries(freshRegistries, existingRegistries);
37
+ spinner.text = `Found ${mergedRegistries.length} registry(ies). Resolving collections...`;
38
+ // Step 2: Resolve collections from each registry's refs
39
+ const freshCollections = [];
40
+ for (const registry of mergedRegistries) {
41
+ try {
42
+ const backend = await resolveBackend(registry.backend);
43
+ const registryFile = await backend.readRegistry(registry);
44
+ for (const ref of registryFile.collections) {
45
+ try {
46
+ const refBackend = await resolveBackend(ref.backend);
47
+ const colInfo = await refBackend.resolveCollectionRef(ref);
48
+ if (colInfo) {
49
+ freshCollections.push(colInfo);
50
+ }
51
+ else {
52
+ console.log(chalk.dim(`\n Warning: collection "${ref.name}" listed in registry "${registry.name}" could not be resolved`));
53
+ }
54
+ }
55
+ catch {
56
+ // Skip unresolvable refs silently
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // Skip registries that can't be read
62
+ }
26
63
  }
27
- catch { /* ok */ }
28
- writeConfig({ registries: existingRegistries, collections, skills: existingSkills, discoveredAt: new Date().toISOString() });
64
+ // Deduplicate by folderId before merging (same collection may appear in multiple registries)
65
+ const seenFolderIds = new Set();
66
+ const dedupedCollections = freshCollections.filter((c) => {
67
+ if (seenFolderIds.has(c.folderId))
68
+ return false;
69
+ seenFolderIds.add(c.folderId);
70
+ return true;
71
+ });
72
+ const mergedCollections = mergeCollections(dedupedCollections, existingCollections);
73
+ writeConfig({
74
+ registries: mergedRegistries,
75
+ collections: mergedCollections,
76
+ skills: existingSkills,
77
+ discoveredAt: new Date().toISOString(),
78
+ });
29
79
  spinner.stop();
30
- if (collections.length === 0) {
31
- console.log(chalk.yellow("No collections found."));
32
- console.log(chalk.dim(" Run: skillsmanager collection create <name>"));
80
+ if (mergedRegistries.length === 0) {
81
+ console.log(chalk.yellow("No registries found."));
82
+ console.log(chalk.dim(" Run: skillsmanager registry create"));
33
83
  }
34
84
  else {
35
- console.log(chalk.green(`Found ${collections.length} collection(s):`));
36
- for (const c of collections) {
37
- console.log(` gdrive:${c.name}`);
85
+ console.log(chalk.green(`Found ${mergedRegistries.length} registry(ies), ${mergedCollections.length} collection(s):`));
86
+ for (const r of mergedRegistries) {
87
+ console.log(` registry: ${r.backend}:${r.name}`);
88
+ }
89
+ for (const c of mergedCollections) {
90
+ console.log(` collection: ${c.backend}:${c.name}`);
38
91
  }
39
92
  }
40
93
  console.log();
@@ -1,5 +1,6 @@
1
1
  export declare function registryCreateCommand(options: {
2
2
  backend?: string;
3
+ repo?: string;
3
4
  }): Promise<void>;
4
5
  export declare function registryListCommand(): Promise<void>;
5
6
  export declare function registryDiscoverCommand(options: {
@@ -9,6 +10,11 @@ export declare function registryAddCollectionCommand(collectionName: string, opt
9
10
  backend?: string;
10
11
  ref?: string;
11
12
  }): Promise<void>;
13
+ export declare function registryRemoveCollectionCommand(collectionName: string, options: {
14
+ delete?: boolean;
15
+ backend?: string;
16
+ }): Promise<void>;
12
17
  export declare function registryPushCommand(options: {
13
18
  backend?: string;
19
+ repo?: string;
14
20
  }): Promise<void>;