@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.
- package/README.md +132 -69
- package/_config.yml +9 -0
- package/package.json +1 -1
- package/src/cli.ts +57 -28
- package/src/commands/doctor.ts +4 -4
- package/src/commands/source.ts +41 -36
- package/src/commands/status.ts +5 -5
- package/src/commands/target.ts +84 -5
- package/src/commands/update.ts +12 -12
- package/src/lib/config.ts +67 -6
- package/src/lib/known-targets.ts +106 -0
- package/src/lib/paths.ts +11 -14
- package/src/types.ts +2 -2
- package/tests/cli.test.ts +40 -24
- package/tests/paths.test.ts +11 -11
package/src/commands/source.ts
CHANGED
|
@@ -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,
|
|
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
|
|
32
|
-
const
|
|
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(`
|
|
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
|
|
49
|
+
// Check if skill name already exists
|
|
43
50
|
if (await directoryExists(targetPath)) {
|
|
44
|
-
throw new Error(`
|
|
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
|
-
|
|
94
|
+
name: skillName,
|
|
91
95
|
});
|
|
92
96
|
|
|
93
|
-
console.log(`Successfully added
|
|
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
|
|
107
|
-
const
|
|
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(`
|
|
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
|
|
120
|
+
// Check if skill name already exists
|
|
113
121
|
if (await directoryExists(targetPath)) {
|
|
114
|
-
throw new Error(`
|
|
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
|
-
|
|
133
|
+
name: skillName,
|
|
129
134
|
});
|
|
130
135
|
|
|
131
|
-
console.log(`Successfully added
|
|
136
|
+
console.log(chalk.green(`Successfully added skill: ${skillName}`));
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
/**
|
|
135
|
-
* Remove a source by
|
|
140
|
+
* Remove a source by name
|
|
136
141
|
*/
|
|
137
|
-
export async function sourceRemove(
|
|
138
|
-
const removed = await removeSource(
|
|
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(`
|
|
146
|
+
throw new Error(`Skill not found: ${name}`);
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
// Remove the files from store
|
|
145
|
-
const targetPath =
|
|
150
|
+
const targetPath = getSkillPath(name);
|
|
146
151
|
await rm(targetPath, { recursive: true, force: true });
|
|
147
152
|
|
|
148
|
-
console.log(`Removed
|
|
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("
|
|
170
|
+
chalk.bold("Skill"),
|
|
166
171
|
chalk.bold("Type"),
|
|
167
|
-
chalk.bold("
|
|
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(
|
|
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.
|
|
186
|
+
source.name,
|
|
182
187
|
source.type,
|
|
183
188
|
source.url || source.path || "",
|
|
184
189
|
status,
|
package/src/commands/status.ts
CHANGED
|
@@ -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,
|
|
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(`
|
|
24
|
+
console.log(chalk.bold(`Skills (${sources.length})`));
|
|
25
25
|
|
|
26
26
|
if (sources.length === 0) {
|
|
27
|
-
console.log(chalk.dim(" No
|
|
27
|
+
console.log(chalk.dim(" No skills registered"));
|
|
28
28
|
} else {
|
|
29
29
|
for (const source of sources) {
|
|
30
|
-
const exists = await directoryExists(
|
|
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.
|
|
33
|
+
console.log(` ${icon} ${source.name} ${chalk.dim(`(${typeLabel})`)}`);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
package/src/commands/target.ts
CHANGED
|
@@ -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
|
|
14
|
-
|
|
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
|
/**
|
package/src/commands/update.ts
CHANGED
|
@@ -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 {
|
|
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.
|
|
24
|
+
await updateRemoteSource(source.name, source.url!);
|
|
25
25
|
} else {
|
|
26
|
-
await updateLocalSource(source.
|
|
26
|
+
await updateLocalSource(source.name, source.path!);
|
|
27
27
|
}
|
|
28
28
|
} catch (error) {
|
|
29
|
-
console.error(` ${source.
|
|
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(
|
|
41
|
-
const targetPath =
|
|
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(` ${
|
|
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(` ${
|
|
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(
|
|
92
|
-
const targetPath =
|
|
91
|
+
async function updateLocalSource(name: string, sourcePath: string): Promise<void> {
|
|
92
|
+
const targetPath = getSkillPath(name);
|
|
93
93
|
|
|
94
|
-
console.log(` ${
|
|
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(` ${
|
|
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
|
|
56
|
-
const existing = config.sources.find(s => s.
|
|
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(`
|
|
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(
|
|
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.
|
|
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
|
|
14
|
+
* Get the store path for a given skill name
|
|
15
15
|
*/
|
|
16
|
-
export function
|
|
17
|
-
return join(SKILLS_STORE,
|
|
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
|
|
218
|
-
*
|
|
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
|
|
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
|
-
|
|
225
|
-
return `${parsed.owner}/${subdirName}`;
|
|
223
|
+
return parsed.subdir.split("/").pop() || parsed.subdir;
|
|
226
224
|
}
|
|
227
|
-
return
|
|
225
|
+
return parsed.repo;
|
|
228
226
|
}
|
|
229
227
|
|
|
230
228
|
/**
|
|
231
|
-
* Get the
|
|
229
|
+
* Get the default skill name from a local folder path
|
|
232
230
|
*/
|
|
233
|
-
export function
|
|
234
|
-
|
|
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
|
-
/**
|
|
12
|
-
|
|
11
|
+
/** Skill name used in store and targets (e.g., "react-best-practices") */
|
|
12
|
+
name: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|