@skillsmanager/cli 0.0.1 → 0.0.4

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,8 +3,9 @@ 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) {
9
10
  const absPath = path.resolve(skillPath);
10
11
  if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) {
@@ -29,8 +30,37 @@ export async function addCommand(skillPath, options) {
29
30
  console.log(chalk.red("SKILL.md frontmatter is missing 'name' field."));
30
31
  return;
31
32
  }
32
- const { config, backend } = await ensureReady();
33
- // Pick collection first one by default, or by name
33
+ // ── Auto-detect if skill lives inside a GitHub-tracked repo ─────────────────
34
+ // If no --collection specified and a matching GitHub collection exists in config,
35
+ // use it automatically (no prompt — agent-friendly).
36
+ if (!options.collection) {
37
+ const ctx = GithubBackend.detectRepoContext(absPath);
38
+ if (ctx) {
39
+ let config;
40
+ try {
41
+ config = readConfig();
42
+ }
43
+ catch {
44
+ config = null;
45
+ }
46
+ const githubCollection = config?.collections.find((c) => c.backend === "github" && c.folderId.startsWith(`${ctx.repo}:`));
47
+ if (githubCollection) {
48
+ await addToGithub(absPath, ctx, skillName, description, githubCollection);
49
+ return;
50
+ }
51
+ // No matching GitHub collection — fall through to standard flow
52
+ // (user can run `skillsmanager collection create --backend github --repo <repo>` first)
53
+ }
54
+ }
55
+ // ── Standard flow ─────────────────────────────────────────────────────────────
56
+ let config;
57
+ try {
58
+ config = readConfig();
59
+ }
60
+ catch {
61
+ console.log(chalk.red("No config found. Run: skillsmanager collection create"));
62
+ return;
63
+ }
34
64
  let collection = config.collections[0];
35
65
  if (options.collection) {
36
66
  const found = config.collections.find((c) => c.name === options.collection);
@@ -41,20 +71,72 @@ export async function addCommand(skillPath, options) {
41
71
  }
42
72
  collection = found;
43
73
  }
74
+ if (!collection) {
75
+ console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
76
+ return;
77
+ }
78
+ const backend = await resolveBackend(collection.backend);
79
+ await uploadToCollection(backend, collection, absPath, skillName, description);
80
+ }
81
+ // ── GitHub path: register in-repo skill or copy external skill ────────────────
82
+ async function addToGithub(absPath, ctx, skillName, description, collection) {
83
+ const github = new GithubBackend();
84
+ const spinner = ora(`Adding ${chalk.bold(skillName)} to github:${collection.folderId}...`).start();
85
+ try {
86
+ // uploadSkill is a no-op for in-repo skills; copies if external
87
+ await github.uploadSkill(collection, absPath, skillName);
88
+ // Determine effective skill path in the repo
89
+ const skillEntry = absPath.startsWith(ctx.repoRoot)
90
+ ? ctx.relPath // in-repo: use relative path
91
+ : `.agentskills/${skillName}`; // external: was copied here by uploadSkill
92
+ const col = await github.readCollection(collection);
93
+ const existing = col.skills.findIndex((s) => s.name === skillName);
94
+ if (existing >= 0) {
95
+ col.skills[existing] = { name: skillName, path: skillEntry, description };
96
+ }
97
+ else {
98
+ col.skills.push({ name: skillName, path: skillEntry, description });
99
+ }
100
+ await github.writeCollection(collection, col);
101
+ trackSkill(skillName, collection.id, absPath);
102
+ spinner.succeed(`${chalk.bold(skillName)} registered in github:${collection.folderId} at ${chalk.dim(skillEntry)}`);
103
+ }
104
+ catch (err) {
105
+ spinner.fail(`Failed: ${err.message}`);
106
+ }
107
+ }
108
+ // ── Shared: upload to any collection backend ──────────────────────────────────
109
+ async function uploadToCollection(backend, collection, absPath, skillName, description) {
44
110
  const spinner = ora(`Adding ${chalk.bold(skillName)} to ${collection.name}...`).start();
45
111
  try {
46
112
  await backend.uploadSkill(collection, absPath, skillName);
113
+ // For GitHub collections, determine the effective in-repo path
114
+ let skillPath;
115
+ if (collection.backend === "github") {
116
+ // If the skill is already inside the repo workdir, use its relative path
117
+ const ctx = GithubBackend.detectRepoContext(absPath);
118
+ const repoFromCollection = collection.folderId.split(":")[0];
119
+ if (ctx && ctx.repo === repoFromCollection) {
120
+ skillPath = ctx.relPath; // e.g. "src/my-inrepo-skill"
121
+ }
122
+ else {
123
+ skillPath = `.agentskills/${skillName}`; // external → copied here by uploadSkill
124
+ }
125
+ }
126
+ else {
127
+ skillPath = `${skillName}/`;
128
+ }
47
129
  const col = await backend.readCollection(collection);
48
130
  const existing = col.skills.findIndex((s) => s.name === skillName);
49
131
  if (existing >= 0) {
50
- col.skills[existing] = { name: skillName, path: `${skillName}/`, description };
132
+ col.skills[existing] = { name: skillName, path: skillPath, description };
51
133
  }
52
134
  else {
53
- col.skills.push({ name: skillName, path: `${skillName}/`, description });
135
+ col.skills.push({ name: skillName, path: skillPath, description });
54
136
  }
55
137
  await backend.writeCollection(collection, col);
56
138
  trackSkill(skillName, collection.id, absPath);
57
- spinner.succeed(`${chalk.bold(skillName)} added to gdrive:${collection.name}`);
139
+ spinner.succeed(`${chalk.bold(skillName)} added to ${collection.backend}:${collection.name}`);
58
140
  }
59
141
  catch (err) {
60
142
  spinner.fail(`Failed: ${err.message}`);
@@ -1 +1,4 @@
1
- export declare function collectionCreateCommand(name?: string): Promise<void>;
1
+ export declare function collectionCreateCommand(name?: string, options?: {
2
+ backend?: string;
3
+ repo?: string;
4
+ }): Promise<void>;
@@ -1,42 +1,76 @@
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
+ export async function collectionCreateCommand(name, options = {}) {
9
+ const backendName = options.backend ?? "gdrive";
10
+ if (backendName === "github") {
11
+ await createGithubCollection(name, options.repo);
14
12
  }
15
13
  else {
16
- folderName = name.startsWith(PREFIX) ? name : `${PREFIX}${name}`;
14
+ await createGdriveCollection(name);
15
+ }
16
+ }
17
+ async function createGithubCollection(name, repo) {
18
+ if (!repo) {
19
+ console.log(chalk.red("GitHub backend requires --repo <owner/repo>"));
20
+ console.log(chalk.dim(" Example: skillsmanager collection create my-skills --backend github --repo owner/my-repo"));
21
+ return;
17
22
  }
23
+ const collectionName = name ?? "default";
24
+ const backend = new GithubBackend();
25
+ console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo}...\n`));
26
+ try {
27
+ const collection = await backend.createCollection(collectionName, repo);
28
+ console.log(chalk.green(`\n ✓ Collection "${collectionName}" created in github:${collection.folderId}`));
29
+ const config = loadOrDefaultConfig();
30
+ upsertCollection(config, collection);
31
+ writeConfig(config);
32
+ console.log(`\nRun ${chalk.bold("skillsmanager add <path>")} to add skills to it.\n`);
33
+ }
34
+ catch (err) {
35
+ console.log(chalk.red(`Failed: ${err.message}`));
36
+ }
37
+ }
38
+ async function createGdriveCollection(name) {
39
+ const auth = await ensureAuth();
40
+ const backend = new GDriveBackend(auth);
41
+ const PREFIX = "SKILLS_";
42
+ const folderName = !name
43
+ ? `${PREFIX}MY_SKILLS`
44
+ : name.startsWith(PREFIX) ? name : `${PREFIX}${name}`;
18
45
  const spinner = ora(`Creating collection "${folderName}" in Google Drive...`).start();
19
46
  try {
20
47
  const collection = await backend.createCollection(folderName);
21
48
  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
- }
49
+ const config = loadOrDefaultConfig();
50
+ upsertCollection(config, collection);
36
51
  writeConfig(config);
37
- console.log(`\nRun ${chalk.bold(`skillsmanager add <path>`)} to add skills to it.\n`);
52
+ console.log(`\nRun ${chalk.bold("skillsmanager add <path>")} to add skills to it.\n`);
38
53
  }
39
54
  catch (err) {
40
55
  spinner.fail(`Failed: ${err.message}`);
41
56
  }
42
57
  }
58
+ // ── helpers ───────────────────────────────────────────────────────────────────
59
+ function loadOrDefaultConfig() {
60
+ if (fs.existsSync(CONFIG_PATH)) {
61
+ try {
62
+ return readConfig();
63
+ }
64
+ catch { /* fall through */ }
65
+ }
66
+ return { registries: [], collections: [], skills: {}, discoveredAt: new Date().toISOString() };
67
+ }
68
+ function upsertCollection(config, collection) {
69
+ const idx = config.collections.findIndex((c) => c.name === collection.name);
70
+ if (idx >= 0) {
71
+ config.collections[idx] = collection;
72
+ }
73
+ else {
74
+ config.collections.push(collection);
75
+ }
76
+ }
@@ -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
  }
@@ -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,10 @@ 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
+ }): Promise<void>;
12
16
  export declare function registryPushCommand(options: {
13
17
  backend?: string;
18
+ repo?: string;
14
19
  }): Promise<void>;