@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.
@@ -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
+ }
@@ -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 { GithubBackend } from "../backends/github.js";
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 = GithubBackend.detectRepoContext(absPath);
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 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);
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 = GithubBackend.detectRepoContext(absPath);
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 github.writeCollection(collection, col);
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
- if (collection.backend !== "github") {
155
- console.log(chalk.red("--remote-path is only supported for GitHub collections."));
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 = await github.readCollection(collection);
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 github.writeCollection(collection, col);
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 = new GithubBackend();
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 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
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 backend = new GithubBackend();
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 collection = await backend.createCollection(collectionName, repo, skillsRepo);
35
- console.log(chalk.green(`\n ✓ Collection "${collectionName}" created in github:${collection.folderId}`));
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
- 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}`));
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 = new LocalBackend();
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"));
@@ -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 { GDriveBackend } from "../backends/gdrive.js";
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 = new GDriveBackend(auth);
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(` gdrive:${c.name} (${col.skills.length} skills)`);
28
+ console.log(` ${c.backend}:${c.name} (${col.skills.length} skills)`);
29
29
  }
30
30
  }
31
31
  let existingSkills = {};
@@ -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 skills = await getAllSkills();
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
- if (skills.length === 0) {
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
- const maxName = Math.max(...skills.map((s) => s.entry.name.length), 4);
28
- const maxDesc = Math.max(...skills.map((s) => s.entry.description.length), 11);
29
- console.log(`\n ${chalk.dim("NAME".padEnd(maxName + 2))}${chalk.dim("DESCRIPTION".padEnd(maxDesc + 2))}${chalk.dim("SOURCE")}`);
30
- console.log(` ${chalk.dim("-".repeat(maxName + maxDesc + 30))}`);
31
- for (const s of skills.sort((a, b) => a.entry.name.localeCompare(b.entry.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}`)}`);
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();
@@ -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
- let registry;
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 = new LocalBackend();
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 = new LocalBackend();
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
- let remoteCol;
312
- if (targetBackend === "gdrive") {
313
- const gdrive = remote;
314
- const folderName = `SKILLS_${ref.name.toUpperCase()}`;
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);
@@ -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(`gdrive:${s.collection.name}`)}`);
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
  }
@@ -1,47 +1,17 @@
1
- import os from "os";
2
1
  import chalk from "chalk";
3
- import { credentialsExist } from "../config.js";
4
- import { hasToken, getAuthClient } from "../auth.js";
5
- import { GDriveBackend } from "../backends/gdrive.js";
6
- import { ghInstalled, ghAuthed, ghGetLogin } from "./setup/github.js";
7
- async function getGdriveStatus() {
8
- if (!credentialsExist()) {
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 localStatus = {
36
- name: "local",
37
- loggedIn: true,
38
- identity: os.userInfo().username,
39
- };
40
- const [gdriveStatus, githubStatus] = await Promise.all([
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("✓ logged in")
57
- : chalk.red("✗ not logged in");
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
- return fresh.map((c) => {
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