@dhruvwill/skills-cli 1.0.0 → 1.1.0

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.
@@ -4,49 +4,53 @@ import { rm, cp, mkdir } from "fs/promises";
4
4
  import chalk from "chalk";
5
5
  import Table from "cli-table3";
6
6
  import { addSource, removeSource, getSources } from "../lib/config.ts";
7
- import { parseGitUrl, getRemoteNamespace, getLocalNamespace, getNamespacePath, SKILLS_STORE } from "../lib/paths.ts";
7
+ import { parseGitUrl, getDefaultSkillName, getLocalSkillName, getSkillPath, SKILLS_STORE } from "../lib/paths.ts";
8
8
  import { directoryExists } from "../lib/hash.ts";
9
9
 
10
10
  /**
11
11
  * Add a source (remote or local)
12
+ * @param pathOrUrl - Git URL or local path
13
+ * @param type - "remote" or "local"
14
+ * @param customName - Optional custom name to override default
12
15
  */
13
- export async function sourceAdd(pathOrUrl: string, type: "remote" | "local"): Promise<void> {
16
+ export async function sourceAdd(pathOrUrl: string, type: "remote" | "local", customName?: string): Promise<void> {
14
17
  if (type === "remote") {
15
- await addRemoteSource(pathOrUrl);
18
+ await addRemoteSource(pathOrUrl, customName);
16
19
  } else {
17
- await addLocalSource(pathOrUrl);
20
+ await addLocalSource(pathOrUrl, customName);
18
21
  }
19
22
  }
20
23
 
21
24
  /**
22
25
  * Add a remote Git repository as a source
23
26
  */
24
- async function addRemoteSource(url: string): Promise<void> {
27
+ async function addRemoteSource(url: string, customName?: string): Promise<void> {
25
28
  const parsed = parseGitUrl(url);
26
29
 
27
30
  if (!parsed) {
28
31
  throw new Error(`Invalid Git URL: ${url}. Expected formats:\n - https://github.com/owner/repo\n - https://gitlab.com/owner/repo\n - https://bitbucket.org/owner/repo\n - https://any-host.com/owner/repo.git`);
29
32
  }
30
33
 
31
- const namespace = getRemoteNamespace(parsed);
32
- const targetPath = getNamespacePath(namespace);
34
+ const defaultName = getDefaultSkillName(parsed);
35
+ const skillName = customName || defaultName;
36
+ const targetPath = getSkillPath(skillName);
33
37
  const cloneUrl = parsed.cloneUrl;
34
38
  const branch = parsed.branch || "main";
35
39
 
36
40
  console.log(`Adding remote source: ${url}`);
37
- console.log(`Namespace: ${namespace}`);
41
+ console.log(`Skill name: ${chalk.cyan(skillName)}`);
42
+ if (customName && customName !== defaultName) {
43
+ console.log(` (renamed from: ${defaultName})`);
44
+ }
38
45
  if (parsed.subdir) {
39
46
  console.log(`Subdirectory: ${parsed.subdir}`);
40
47
  }
41
48
 
42
- // Check if directory already exists
49
+ // Check if skill name already exists
43
50
  if (await directoryExists(targetPath)) {
44
- throw new Error(`Source already exists at ${namespace}. Remove it first with 'skills source remove ${namespace}'`);
51
+ throw new Error(`Skill "${skillName}" already exists. Remove it first with 'skills source remove ${skillName}' or use --name to specify a different name.`);
45
52
  }
46
53
 
47
- // Create parent directory if needed
48
- await mkdir(getNamespacePath(parsed.owner), { recursive: true });
49
-
50
54
  // Clone the repository
51
55
  console.log("Cloning repository...");
52
56
 
@@ -87,36 +91,37 @@ async function addRemoteSource(url: string): Promise<void> {
87
91
  await addSource({
88
92
  type: "remote",
89
93
  url,
90
- namespace,
94
+ name: skillName,
91
95
  });
92
96
 
93
- console.log(`Successfully added remote source: ${namespace}`);
97
+ console.log(chalk.green(`Successfully added skill: ${skillName}`));
94
98
  }
95
99
 
96
100
  /**
97
101
  * Add a local folder as a source
98
102
  */
99
- async function addLocalSource(folderPath: string): Promise<void> {
103
+ async function addLocalSource(folderPath: string, customName?: string): Promise<void> {
100
104
  const absolutePath = resolve(folderPath);
101
105
 
102
106
  if (!(await directoryExists(absolutePath))) {
103
107
  throw new Error(`Local folder does not exist: ${absolutePath}`);
104
108
  }
105
109
 
106
- const namespace = getLocalNamespace(absolutePath);
107
- const targetPath = getNamespacePath(namespace);
110
+ const defaultName = getLocalSkillName(absolutePath);
111
+ const skillName = customName || defaultName;
112
+ const targetPath = getSkillPath(skillName);
108
113
 
109
114
  console.log(`Adding local source: ${absolutePath}`);
110
- console.log(`Namespace: ${namespace}`);
115
+ console.log(`Skill name: ${chalk.cyan(skillName)}`);
116
+ if (customName && customName !== defaultName) {
117
+ console.log(` (renamed from: ${defaultName})`);
118
+ }
111
119
 
112
- // Check if namespace already exists
120
+ // Check if skill name already exists
113
121
  if (await directoryExists(targetPath)) {
114
- throw new Error(`Source already exists at ${namespace}. Remove it first with 'skills source remove ${namespace}'`);
122
+ throw new Error(`Skill "${skillName}" already exists. Remove it first with 'skills source remove ${skillName}' or use --name to specify a different name.`);
115
123
  }
116
124
 
117
- // Create local directory
118
- await mkdir(getNamespacePath("local"), { recursive: true });
119
-
120
125
  // Copy files
121
126
  console.log("Copying files...");
122
127
  await cp(absolutePath, targetPath, { recursive: true });
@@ -125,27 +130,27 @@ async function addLocalSource(folderPath: string): Promise<void> {
125
130
  await addSource({
126
131
  type: "local",
127
132
  path: absolutePath,
128
- namespace,
133
+ name: skillName,
129
134
  });
130
135
 
131
- console.log(`Successfully added local source: ${namespace}`);
136
+ console.log(chalk.green(`Successfully added skill: ${skillName}`));
132
137
  }
133
138
 
134
139
  /**
135
- * Remove a source by namespace
140
+ * Remove a source by name
136
141
  */
137
- export async function sourceRemove(namespace: string): Promise<void> {
138
- const removed = await removeSource(namespace);
142
+ export async function sourceRemove(name: string): Promise<void> {
143
+ const removed = await removeSource(name);
139
144
 
140
145
  if (!removed) {
141
- throw new Error(`Source not found: ${namespace}`);
146
+ throw new Error(`Skill not found: ${name}`);
142
147
  }
143
148
 
144
149
  // Remove the files from store
145
- const targetPath = getNamespacePath(namespace);
150
+ const targetPath = getSkillPath(name);
146
151
  await rm(targetPath, { recursive: true, force: true });
147
152
 
148
- console.log(`Removed source: ${namespace}`);
153
+ console.log(chalk.green(`Removed skill: ${name}`));
149
154
  }
150
155
 
151
156
  /**
@@ -162,9 +167,9 @@ export async function sourceList(): Promise<void> {
162
167
 
163
168
  const table = new Table({
164
169
  head: [
165
- chalk.bold("Namespace"),
170
+ chalk.bold("Skill"),
166
171
  chalk.bold("Type"),
167
- chalk.bold("Location"),
172
+ chalk.bold("Source"),
168
173
  chalk.bold("Status"),
169
174
  ],
170
175
  style: {
@@ -174,11 +179,11 @@ export async function sourceList(): Promise<void> {
174
179
  });
175
180
 
176
181
  for (const source of sources) {
177
- const exists = await directoryExists(getNamespacePath(source.namespace));
182
+ const exists = await directoryExists(getSkillPath(source.name));
178
183
  const status = exists ? chalk.green("OK") : chalk.red("MISSING");
179
184
 
180
185
  table.push([
181
- source.namespace,
186
+ source.name,
182
187
  source.type,
183
188
  source.url || source.path || "",
184
189
  status,
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import Table from "cli-table3";
3
3
  import { getSources, getTargets } from "../lib/config.ts";
4
- import { SKILLS_ROOT, SKILLS_STORE, getNamespacePath } from "../lib/paths.ts";
4
+ import { SKILLS_ROOT, SKILLS_STORE, getSkillPath } from "../lib/paths.ts";
5
5
  import { directoryExists, compareDirectories } from "../lib/hash.ts";
6
6
 
7
7
  /**
@@ -21,16 +21,16 @@ export async function status(): Promise<void> {
21
21
  // Sources summary
22
22
  const sources = await getSources();
23
23
  console.log();
24
- console.log(chalk.bold(`Sources (${sources.length})`));
24
+ console.log(chalk.bold(`Skills (${sources.length})`));
25
25
 
26
26
  if (sources.length === 0) {
27
- console.log(chalk.dim(" No sources registered"));
27
+ console.log(chalk.dim(" No skills registered"));
28
28
  } else {
29
29
  for (const source of sources) {
30
- const exists = await directoryExists(getNamespacePath(source.namespace));
30
+ const exists = await directoryExists(getSkillPath(source.name));
31
31
  const icon = exists ? chalk.green("●") : chalk.red("●");
32
32
  const typeLabel = source.type === "remote" ? chalk.blue("remote") : chalk.magenta("local");
33
- console.log(` ${icon} ${source.namespace} ${chalk.dim(`(${typeLabel})`)}`);
33
+ console.log(` ${icon} ${source.name} ${chalk.dim(`(${typeLabel})`)}`);
34
34
  }
35
35
  }
36
36
 
@@ -6,14 +6,32 @@ import { addTarget, removeTarget, getTargets } from "../lib/config.ts";
6
6
  import { SKILLS_STORE } from "../lib/paths.ts";
7
7
  import { compareDirectories, directoryExists } from "../lib/hash.ts";
8
8
  import { syncToTarget } from "./sync.ts";
9
+ import { getKnownTarget, KNOWN_TARGETS } from "../lib/known-targets.ts";
9
10
 
10
11
  /**
11
12
  * Add a target directory
13
+ * If path is not provided, looks up known targets
12
14
  */
13
- export async function targetAdd(name: string, path: string): Promise<void> {
14
- const absolutePath = resolve(path.replace(/^~/, process.env.HOME || process.env.USERPROFILE || ""));
15
+ export async function targetAdd(name: string, path?: string): Promise<void> {
16
+ let absolutePath: string;
17
+
18
+ if (path) {
19
+ // User provided a custom path
20
+ absolutePath = resolve(path.replace(/^~/, process.env.HOME || process.env.USERPROFILE || ""));
21
+ } else {
22
+ // Look up known target
23
+ const known = getKnownTarget(name);
24
+ if (!known) {
25
+ console.error(chalk.red(`Unknown target: ${name}`));
26
+ console.log(`\nRun ${chalk.cyan("skills target available")} to see predefined targets.`);
27
+ console.log(`Or specify a custom path: ${chalk.cyan(`skills target add ${name} <path>`)}`);
28
+ process.exit(1);
29
+ }
30
+ absolutePath = known.path;
31
+ console.log(`Using predefined path for ${chalk.cyan(known.description)}`);
32
+ }
15
33
 
16
- console.log(`Adding target: ${name}`);
34
+ console.log(`Adding target: ${chalk.cyan(name)}`);
17
35
  console.log(`Path: ${absolutePath}`);
18
36
 
19
37
  // Create the target directory if it doesn't exist
@@ -25,13 +43,74 @@ export async function targetAdd(name: string, path: string): Promise<void> {
25
43
  path: absolutePath,
26
44
  });
27
45
 
28
- console.log(`Successfully registered target: ${name}`);
46
+ console.log(chalk.green(`Successfully registered target: ${name}`));
29
47
 
30
48
  // Perform initial sync
31
49
  console.log("Performing initial sync...");
32
50
  await syncToTarget({ name, path: absolutePath });
33
51
 
34
- console.log(`Target "${name}" is now synced.`);
52
+ console.log(chalk.green(`Target "${name}" is now synced.`));
53
+ }
54
+
55
+ /**
56
+ * Show available predefined targets
57
+ */
58
+ export async function targetAvailable(): Promise<void> {
59
+ const existingTargets = await getTargets();
60
+ const existingNames = new Set(existingTargets.map(t => t.name.toLowerCase()));
61
+
62
+ console.log();
63
+ console.log(chalk.bold("Available Predefined Targets"));
64
+ console.log(chalk.dim("─".repeat(60)));
65
+ console.log();
66
+
67
+ const table = new Table({
68
+ head: [
69
+ chalk.bold("Name"),
70
+ chalk.bold("Description"),
71
+ chalk.bold("Path"),
72
+ chalk.bold("Tool Status"),
73
+ chalk.bold("Added"),
74
+ ],
75
+ style: {
76
+ head: [],
77
+ border: [],
78
+ },
79
+ });
80
+
81
+ for (const target of KNOWN_TARGETS) {
82
+ const isAdded = existingNames.has(target.name.toLowerCase());
83
+ const addedStatus = isAdded ? chalk.green("✓") : chalk.dim("-");
84
+
85
+ // Color code the tool status
86
+ let toolStatus: string;
87
+ if (target.status === "GA") {
88
+ toolStatus = chalk.green(target.status);
89
+ } else if (target.status === "Beta") {
90
+ toolStatus = chalk.yellow(target.status);
91
+ } else {
92
+ toolStatus = chalk.dim(target.status);
93
+ }
94
+
95
+ table.push([
96
+ target.name,
97
+ target.description,
98
+ target.path,
99
+ toolStatus,
100
+ addedStatus,
101
+ ]);
102
+ }
103
+
104
+ console.log(table.toString());
105
+ console.log();
106
+ console.log(chalk.bold("Usage:"));
107
+ console.log(` ${chalk.cyan("skills target add <name>")} Add a predefined target`);
108
+ console.log(` ${chalk.cyan("skills target add <name> <path>")} Add a custom target`);
109
+ console.log();
110
+ console.log(chalk.bold("Examples:"));
111
+ console.log(` ${chalk.cyan("skills target add cursor")} Uses predefined path`);
112
+ console.log(` ${chalk.cyan("skills target add vscode ~/.vscode/skills")} Custom path`);
113
+ console.log();
35
114
  }
36
115
 
37
116
  /**
@@ -1,7 +1,7 @@
1
1
  import { $ } from "bun";
2
2
  import { rm, cp } from "fs/promises";
3
3
  import { getSources } from "../lib/config.ts";
4
- import { getNamespacePath, parseGitUrl } from "../lib/paths.ts";
4
+ import { getSkillPath, parseGitUrl } from "../lib/paths.ts";
5
5
  import { directoryExists } from "../lib/hash.ts";
6
6
 
7
7
  /**
@@ -21,12 +21,12 @@ export async function update(): Promise<void> {
21
21
  for (const source of sources) {
22
22
  try {
23
23
  if (source.type === "remote") {
24
- await updateRemoteSource(source.namespace, source.url!);
24
+ await updateRemoteSource(source.name, source.url!);
25
25
  } else {
26
- await updateLocalSource(source.namespace, source.path!);
26
+ await updateLocalSource(source.name, source.path!);
27
27
  }
28
28
  } catch (error) {
29
- console.error(` ${source.namespace}: Failed - ${error instanceof Error ? error.message : error}`);
29
+ console.error(` ${source.name}: Failed - ${error instanceof Error ? error.message : error}`);
30
30
  }
31
31
  }
32
32
 
@@ -37,8 +37,8 @@ export async function update(): Promise<void> {
37
37
  /**
38
38
  * Update a remote source by re-cloning
39
39
  */
40
- async function updateRemoteSource(namespace: string, url: string): Promise<void> {
41
- const targetPath = getNamespacePath(namespace);
40
+ async function updateRemoteSource(name: string, url: string): Promise<void> {
41
+ const targetPath = getSkillPath(name);
42
42
  const parsed = parseGitUrl(url);
43
43
 
44
44
  if (!parsed) {
@@ -48,7 +48,7 @@ async function updateRemoteSource(namespace: string, url: string): Promise<void>
48
48
  const cloneUrl = parsed.cloneUrl;
49
49
  const branch = parsed.branch || "main";
50
50
 
51
- console.log(` ${namespace}: Updating from ${url}...`);
51
+ console.log(` ${name}: Updating from ${url}...`);
52
52
 
53
53
  // Remove existing directory
54
54
  await rm(targetPath, { recursive: true, force: true });
@@ -77,7 +77,7 @@ async function updateRemoteSource(namespace: string, url: string): Promise<void>
77
77
  await rm(`${targetPath}/.git`, { recursive: true, force: true });
78
78
  }
79
79
 
80
- console.log(` ${namespace}: Updated`);
80
+ console.log(` ${name}: Updated`);
81
81
  } catch (error) {
82
82
  // Clean up on failure
83
83
  await rm(`${targetPath}_temp`, { recursive: true, force: true });
@@ -88,10 +88,10 @@ async function updateRemoteSource(namespace: string, url: string): Promise<void>
88
88
  /**
89
89
  * Update a local source by re-copying
90
90
  */
91
- async function updateLocalSource(namespace: string, sourcePath: string): Promise<void> {
92
- const targetPath = getNamespacePath(namespace);
91
+ async function updateLocalSource(name: string, sourcePath: string): Promise<void> {
92
+ const targetPath = getSkillPath(name);
93
93
 
94
- console.log(` ${namespace}: Updating from ${sourcePath}...`);
94
+ console.log(` ${name}: Updating from ${sourcePath}...`);
95
95
 
96
96
  // Check if source still exists
97
97
  if (!(await directoryExists(sourcePath))) {
@@ -104,5 +104,5 @@ async function updateLocalSource(namespace: string, sourcePath: string): Promise
104
104
  // Re-copy
105
105
  await cp(sourcePath, targetPath, { recursive: true });
106
106
 
107
- console.log(` ${namespace}: Updated`);
107
+ console.log(` ${name}: Updated`);
108
108
  }
package/src/lib/config.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { mkdir } from "fs/promises";
1
+ import { mkdir, rename, rm } from "fs/promises";
2
+ import { join } from "path";
2
3
  import { SKILLS_ROOT, SKILLS_STORE, CONFIG_PATH } from "./paths.ts";
3
4
  import type { Config, Source, Target } from "../types.ts";
4
5
 
@@ -17,6 +18,7 @@ export async function ensureDirectories(): Promise<void> {
17
18
 
18
19
  /**
19
20
  * Read the config file, creating it if it doesn't exist
21
+ * Also handles migration from old config format (namespace -> name)
20
22
  */
21
23
  export async function readConfig(): Promise<Config> {
22
24
  await ensureDirectories();
@@ -30,6 +32,30 @@ export async function readConfig(): Promise<Config> {
30
32
 
31
33
  try {
32
34
  const content = await configFile.json();
35
+
36
+ // Migrate old config format (namespace -> name)
37
+ let needsMigration = false;
38
+ if (content.sources) {
39
+ for (const source of content.sources) {
40
+ if (source.namespace && !source.name) {
41
+ // Extract skill name from old namespace (e.g., "owner/skill" -> "skill")
42
+ const oldNamespace = source.namespace;
43
+ const parts = oldNamespace.split("/");
44
+ const newName = parts[parts.length - 1];
45
+ source.name = newName;
46
+ delete source.namespace;
47
+ needsMigration = true;
48
+
49
+ // Also migrate the folder structure
50
+ await migrateSkillFolder(oldNamespace, newName);
51
+ }
52
+ }
53
+ }
54
+
55
+ if (needsMigration) {
56
+ await writeConfig(content as Config);
57
+ }
58
+
33
59
  return content as Config;
34
60
  } catch {
35
61
  // If config is corrupted, reset it
@@ -38,6 +64,41 @@ export async function readConfig(): Promise<Config> {
38
64
  }
39
65
  }
40
66
 
67
+ /**
68
+ * Migrate a skill folder from old nested structure to new flat structure
69
+ */
70
+ async function migrateSkillFolder(oldNamespace: string, newName: string): Promise<void> {
71
+ const oldPath = join(SKILLS_STORE, oldNamespace);
72
+ const newPath = join(SKILLS_STORE, newName);
73
+
74
+ try {
75
+ // Check if old path exists
76
+ const oldExists = await Bun.file(join(oldPath, "SKILL.md")).exists() ||
77
+ await Bun.file(oldPath).exists();
78
+
79
+ if (oldExists) {
80
+ // Check if new path already exists
81
+ const newExists = await Bun.file(newPath).exists();
82
+
83
+ if (!newExists) {
84
+ // Move the folder
85
+ await rename(oldPath, newPath);
86
+ console.log(`Migrated skill folder: ${oldNamespace} -> ${newName}`);
87
+
88
+ // Try to clean up empty parent directories
89
+ const parentDir = join(SKILLS_STORE, oldNamespace.split("/")[0]);
90
+ try {
91
+ await rm(parentDir, { recursive: false });
92
+ } catch {
93
+ // Parent dir not empty or doesn't exist, ignore
94
+ }
95
+ }
96
+ }
97
+ } catch {
98
+ // Migration failed, folder might not exist
99
+ }
100
+ }
101
+
41
102
  /**
42
103
  * Write the config file
43
104
  */
@@ -52,10 +113,10 @@ export async function writeConfig(config: Config): Promise<void> {
52
113
  export async function addSource(source: Source): Promise<void> {
53
114
  const config = await readConfig();
54
115
 
55
- // Check if namespace already exists
56
- const existing = config.sources.find(s => s.namespace === source.namespace);
116
+ // Check if name already exists
117
+ const existing = config.sources.find(s => s.name === source.name);
57
118
  if (existing) {
58
- throw new Error(`Source with namespace "${source.namespace}" already exists`);
119
+ throw new Error(`Skill with name "${source.name}" already exists`);
59
120
  }
60
121
 
61
122
  config.sources.push(source);
@@ -65,10 +126,10 @@ export async function addSource(source: Source): Promise<void> {
65
126
  /**
66
127
  * Remove a source from the config
67
128
  */
68
- export async function removeSource(namespace: string): Promise<Source | null> {
129
+ export async function removeSource(name: string): Promise<Source | null> {
69
130
  const config = await readConfig();
70
131
 
71
- const index = config.sources.findIndex(s => s.namespace === namespace);
132
+ const index = config.sources.findIndex(s => s.name === name);
72
133
  if (index === -1) {
73
134
  return null;
74
135
  }
@@ -0,0 +1,106 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+
4
+ export interface KnownTarget {
5
+ name: string;
6
+ description: string;
7
+ path: string;
8
+ status: "GA" | "Beta" | "Experimental";
9
+ }
10
+
11
+ const home = homedir();
12
+
13
+ /**
14
+ * Predefined target paths for common AI tools
15
+ * Based on official documentation for each tool's global skills folder
16
+ *
17
+ * Reference:
18
+ * | Tool | Global Skills Folder | Status |
19
+ * |--------------|-----------------------------------|--------|
20
+ * | Claude Code | ~/.claude/skills/ | GA |
21
+ * | Gemini CLI | ~/.gemini/skills/ | Beta |
22
+ * | Cursor | ~/.cursor/skills/ | GA |
23
+ * | VS Code | ~/.copilot/skills/ | GA |
24
+ * | OpenCode | ~/.config/opencode/skills/ | GA |
25
+ * | Windsurf | ~/.windsurf/skills/ | GA |
26
+ * | Antigravity | ~/.gemini/antigravity/ | Exp. |
27
+ * | Aider | ~/.aider/skills/ | Beta |
28
+ * | Goose | ~/.config/goose/skills/ | Beta |
29
+ * | Amp | ~/.amp/skills/ | Beta |
30
+ */
31
+ export const KNOWN_TARGETS: KnownTarget[] = [
32
+ {
33
+ name: "cursor",
34
+ description: "Cursor IDE",
35
+ path: join(home, ".cursor", "skills"),
36
+ status: "GA",
37
+ },
38
+ {
39
+ name: "claude",
40
+ description: "Claude Code / Claude Desktop",
41
+ path: join(home, ".claude", "skills"),
42
+ status: "GA",
43
+ },
44
+ {
45
+ name: "gemini",
46
+ description: "Gemini CLI",
47
+ path: join(home, ".gemini", "skills"),
48
+ status: "Beta",
49
+ },
50
+ {
51
+ name: "copilot",
52
+ description: "GitHub Copilot / VS Code",
53
+ path: join(home, ".copilot", "skills"),
54
+ status: "GA",
55
+ },
56
+ {
57
+ name: "opencode",
58
+ description: "OpenCode CLI",
59
+ path: join(home, ".config", "opencode", "skills"),
60
+ status: "GA",
61
+ },
62
+ {
63
+ name: "windsurf",
64
+ description: "Windsurf IDE",
65
+ path: join(home, ".windsurf", "skills"),
66
+ status: "GA",
67
+ },
68
+ {
69
+ name: "antigravity",
70
+ description: "Antigravity",
71
+ path: join(home, ".gemini", "antigravity"),
72
+ status: "Experimental",
73
+ },
74
+ {
75
+ name: "aider",
76
+ description: "Aider CLI",
77
+ path: join(home, ".aider", "skills"),
78
+ status: "Beta",
79
+ },
80
+ {
81
+ name: "goose",
82
+ description: "Goose AI",
83
+ path: join(home, ".config", "goose", "skills"),
84
+ status: "Beta",
85
+ },
86
+ {
87
+ name: "amp",
88
+ description: "Amp AI",
89
+ path: join(home, ".amp", "skills"),
90
+ status: "Beta",
91
+ },
92
+ ];
93
+
94
+ /**
95
+ * Get a known target by name
96
+ */
97
+ export function getKnownTarget(name: string): KnownTarget | undefined {
98
+ return KNOWN_TARGETS.find(t => t.name.toLowerCase() === name.toLowerCase());
99
+ }
100
+
101
+ /**
102
+ * Get all known target names
103
+ */
104
+ export function getKnownTargetNames(): string[] {
105
+ return KNOWN_TARGETS.map(t => t.name);
106
+ }
package/src/lib/paths.ts CHANGED
@@ -11,10 +11,10 @@ export const SKILLS_STORE = join(SKILLS_ROOT, "store");
11
11
  export const CONFIG_PATH = join(SKILLS_ROOT, "config.json");
12
12
 
13
13
  /**
14
- * Get the store path for a given namespace
14
+ * Get the store path for a given skill name
15
15
  */
16
- export function getNamespacePath(namespace: string): string {
17
- return join(SKILLS_STORE, namespace);
16
+ export function getSkillPath(name: string): string {
17
+ return join(SKILLS_STORE, name);
18
18
  }
19
19
 
20
20
  export interface ParsedGitUrl {
@@ -214,23 +214,20 @@ function parseGenericGitUrl(url: string): ParsedGitUrl | null {
214
214
  }
215
215
 
216
216
  /**
217
- * Get the namespace for a parsed Git URL
218
- * For subdirectories, uses owner/skill-name
219
- * For full repos, uses owner/repo
217
+ * Get the default skill name from a parsed Git URL
218
+ * Uses the last folder name (skill folder name)
220
219
  */
221
- export function getRemoteNamespace(parsed: ParsedGitUrl): string {
220
+ export function getDefaultSkillName(parsed: ParsedGitUrl): string {
222
221
  if (parsed.subdir) {
223
222
  // Use the last part of the subdir path as the skill name
224
- const subdirName = parsed.subdir.split("/").pop() || parsed.subdir;
225
- return `${parsed.owner}/${subdirName}`;
223
+ return parsed.subdir.split("/").pop() || parsed.subdir;
226
224
  }
227
- return `${parsed.owner}/${parsed.repo}`;
225
+ return parsed.repo;
228
226
  }
229
227
 
230
228
  /**
231
- * Get the local namespace for a folder path
229
+ * Get the default skill name from a local folder path
232
230
  */
233
- export function getLocalNamespace(folderPath: string): string {
234
- const folderName = folderPath.split(/[\/\\]/).filter(Boolean).pop() || "unnamed";
235
- return `local/${folderName}`;
231
+ export function getLocalSkillName(folderPath: string): string {
232
+ return folderPath.split(/[\/\\]/).filter(Boolean).pop() || "unnamed";
236
233
  }
package/src/types.ts CHANGED
@@ -8,8 +8,8 @@ export interface Source {
8
8
  url?: string;
9
9
  /** Absolute path for local sources */
10
10
  path?: string;
11
- /** Store subdirectory (e.g., "anthropic/skills" or "local/my-folder") */
12
- namespace: string;
11
+ /** Skill name used in store and targets (e.g., "react-best-practices") */
12
+ name: string;
13
13
  }
14
14
 
15
15
  /**