@dhruvwill/skills-cli 1.0.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/src/cli.ts ADDED
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import chalk from "chalk";
4
+ import { sourceAdd, sourceRemove, sourceList } from "./commands/source.ts";
5
+ import { targetAdd, targetRemove, targetList } from "./commands/target.ts";
6
+ import { sync } from "./commands/sync.ts";
7
+ import { update } from "./commands/update.ts";
8
+ import { doctor } from "./commands/doctor.ts";
9
+ import { status } from "./commands/status.ts";
10
+
11
+ const VERSION = "1.0.0";
12
+
13
+ function printHelp() {
14
+ console.log(`
15
+ ${chalk.bold("skills")} - Sync AI skills across all your agent tools
16
+
17
+ ${chalk.bold("USAGE")}
18
+ ${chalk.cyan("skills")} <command> [options]
19
+
20
+ ${chalk.bold("COMMANDS")}
21
+ ${chalk.cyan("status")} Show overview of sources, targets & sync state
22
+ ${chalk.cyan("doctor")} Diagnose configuration issues
23
+
24
+ ${chalk.bold("Source Management")}
25
+ ${chalk.cyan("source list")} List all registered sources
26
+ ${chalk.cyan("source add")} <url> ${chalk.dim("--remote")} Add a remote Git repository
27
+ ${chalk.cyan("source add")} <path> ${chalk.dim("--local")} Add a local folder
28
+ ${chalk.cyan("source remove")} <namespace> Remove a source
29
+
30
+ ${chalk.bold("Target Management")}
31
+ ${chalk.cyan("target list")} List all targets with sync status
32
+ ${chalk.cyan("target add")} <name> <path> Add a target directory
33
+ ${chalk.cyan("target remove")} <name> Remove a target
34
+
35
+ ${chalk.bold("Synchronization")}
36
+ ${chalk.cyan("sync")} Push skills from store to all targets
37
+ ${chalk.cyan("update")} Refresh all sources from origin
38
+
39
+ ${chalk.bold("OPTIONS")}
40
+ ${chalk.cyan("--help, -h")} Show this help message
41
+ ${chalk.cyan("--version, -v")} Show version
42
+
43
+ ${chalk.bold("EXAMPLES")}
44
+ ${chalk.dim("# Add skills from GitHub (supports subdirectories)")}
45
+ ${chalk.cyan("skills source add")} https://github.com/vercel/ai-skills --remote
46
+ ${chalk.cyan("skills source add")} https://github.com/user/repo/tree/main/skills/my-skill --remote
47
+
48
+ ${chalk.dim("# Add skills from GitLab or Bitbucket")}
49
+ ${chalk.cyan("skills source add")} https://gitlab.com/user/repo --remote
50
+ ${chalk.cyan("skills source add")} https://bitbucket.org/user/repo --remote
51
+
52
+ ${chalk.dim("# Add local skills folder")}
53
+ ${chalk.cyan("skills source add")} ./my-local-skills --local
54
+
55
+ ${chalk.dim("# Add targets and sync")}
56
+ ${chalk.cyan("skills target add")} cursor ~/.cursor/skills
57
+ ${chalk.cyan("skills target add")} claude ~/.claude/settings/skills
58
+ ${chalk.cyan("skills sync")}
59
+
60
+ ${chalk.bold("DOCUMENTATION")}
61
+ ${chalk.dim("https://github.com/yourusername/skills")}
62
+ `);
63
+ }
64
+
65
+ async function main() {
66
+ const args = process.argv.slice(2);
67
+
68
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
69
+ printHelp();
70
+ process.exit(0);
71
+ }
72
+
73
+ if (args.includes("--version") || args.includes("-v")) {
74
+ console.log(`skills v${VERSION}`);
75
+ process.exit(0);
76
+ }
77
+
78
+ const command = args[0];
79
+ const subcommand = args[1];
80
+
81
+ try {
82
+ switch (command) {
83
+ case "source":
84
+ await handleSourceCommand(subcommand, args.slice(2));
85
+ break;
86
+
87
+ case "target":
88
+ await handleTargetCommand(subcommand, args.slice(2));
89
+ break;
90
+
91
+ case "sync":
92
+ await sync();
93
+ break;
94
+
95
+ case "update":
96
+ await update();
97
+ break;
98
+
99
+ case "doctor":
100
+ await doctor();
101
+ break;
102
+
103
+ case "status":
104
+ await status();
105
+ break;
106
+
107
+ case "help":
108
+ printHelp();
109
+ break;
110
+
111
+ default:
112
+ console.error(chalk.red(`Unknown command: ${command}`));
113
+ console.log(`Run ${chalk.cyan("skills --help")} for usage.`);
114
+ process.exit(1);
115
+ }
116
+ } catch (error) {
117
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : error}`));
118
+ process.exit(1);
119
+ }
120
+ }
121
+
122
+ async function handleSourceCommand(subcommand: string, args: string[]) {
123
+ switch (subcommand) {
124
+ case "add": {
125
+ const pathOrUrl = args[0];
126
+ const isRemote = args.includes("--remote");
127
+ const isLocal = args.includes("--local");
128
+
129
+ if (!pathOrUrl) {
130
+ console.error("Missing path or URL for source add");
131
+ process.exit(1);
132
+ }
133
+
134
+ if (!isRemote && !isLocal) {
135
+ console.error("Please specify --remote or --local");
136
+ process.exit(1);
137
+ }
138
+
139
+ if (isRemote && isLocal) {
140
+ console.error("Cannot specify both --remote and --local");
141
+ process.exit(1);
142
+ }
143
+
144
+ await sourceAdd(pathOrUrl, isRemote ? "remote" : "local");
145
+ break;
146
+ }
147
+
148
+ case "remove": {
149
+ const namespace = args[0];
150
+ if (!namespace) {
151
+ console.error("Missing namespace for source remove");
152
+ process.exit(1);
153
+ }
154
+ await sourceRemove(namespace);
155
+ break;
156
+ }
157
+
158
+ case "list":
159
+ await sourceList();
160
+ break;
161
+
162
+ default:
163
+ console.error(`Unknown source subcommand: ${subcommand}`);
164
+ console.log("Available: add, remove, list");
165
+ process.exit(1);
166
+ }
167
+ }
168
+
169
+ async function handleTargetCommand(subcommand: string, args: string[]) {
170
+ switch (subcommand) {
171
+ case "add": {
172
+ const name = args[0];
173
+ const path = args[1];
174
+
175
+ if (!name || !path) {
176
+ console.error("Usage: skills target add <name> <path>");
177
+ process.exit(1);
178
+ }
179
+
180
+ await targetAdd(name, path);
181
+ break;
182
+ }
183
+
184
+ case "remove": {
185
+ const name = args[0];
186
+ if (!name) {
187
+ console.error("Missing name for target remove");
188
+ process.exit(1);
189
+ }
190
+ await targetRemove(name);
191
+ break;
192
+ }
193
+
194
+ case "list":
195
+ await targetList();
196
+ break;
197
+
198
+ default:
199
+ console.error(`Unknown target subcommand: ${subcommand}`);
200
+ console.log("Available: add, remove, list");
201
+ process.exit(1);
202
+ }
203
+ }
204
+
205
+ main();
@@ -0,0 +1,248 @@
1
+ import chalk from "chalk";
2
+ import { $ } from "bun";
3
+ import { getSources, getTargets } from "../lib/config.ts";
4
+ import { getNamespacePath, SKILLS_ROOT, SKILLS_STORE, CONFIG_PATH } from "../lib/paths.ts";
5
+ import { directoryExists } from "../lib/hash.ts";
6
+
7
+ interface DiagnosticResult {
8
+ name: string;
9
+ status: "ok" | "warn" | "error";
10
+ message: string;
11
+ }
12
+
13
+ /**
14
+ * Run diagnostics on the skills CLI configuration
15
+ */
16
+ export async function doctor(): Promise<void> {
17
+ console.log();
18
+ console.log(chalk.bold("Skills Doctor"));
19
+ console.log(chalk.dim("─".repeat(50)));
20
+ console.log();
21
+
22
+ const results: DiagnosticResult[] = [];
23
+
24
+ // Check 1: Git installation
25
+ results.push(await checkGit());
26
+
27
+ // Check 2: Skills directory
28
+ results.push(await checkSkillsDirectory());
29
+
30
+ // Check 3: Config file
31
+ results.push(await checkConfigFile());
32
+
33
+ // Check 4: Store directory
34
+ results.push(await checkStoreDirectory());
35
+
36
+ // Check 5: Sources
37
+ results.push(...await checkSources());
38
+
39
+ // Check 6: Targets
40
+ results.push(...await checkTargets());
41
+
42
+ // Print results
43
+ for (const result of results) {
44
+ const icon = result.status === "ok"
45
+ ? chalk.green("✓")
46
+ : result.status === "warn"
47
+ ? chalk.yellow("!")
48
+ : chalk.red("✗");
49
+
50
+ const statusColor = result.status === "ok"
51
+ ? chalk.green
52
+ : result.status === "warn"
53
+ ? chalk.yellow
54
+ : chalk.red;
55
+
56
+ console.log(` ${icon} ${chalk.bold(result.name)}`);
57
+ console.log(` ${statusColor(result.message)}`);
58
+ console.log();
59
+ }
60
+
61
+ // Summary
62
+ const errors = results.filter(r => r.status === "error").length;
63
+ const warnings = results.filter(r => r.status === "warn").length;
64
+ const ok = results.filter(r => r.status === "ok").length;
65
+
66
+ console.log(chalk.dim("─".repeat(50)));
67
+
68
+ if (errors > 0) {
69
+ console.log(chalk.red(`${errors} error(s), ${warnings} warning(s), ${ok} passed`));
70
+ } else if (warnings > 0) {
71
+ console.log(chalk.yellow(`${warnings} warning(s), ${ok} passed`));
72
+ } else {
73
+ console.log(chalk.green(`All ${ok} checks passed!`));
74
+ }
75
+
76
+ console.log();
77
+ }
78
+
79
+ async function checkGit(): Promise<DiagnosticResult> {
80
+ try {
81
+ const result = await $`git --version`.quiet();
82
+ const version = result.text().trim();
83
+ return {
84
+ name: "Git",
85
+ status: "ok",
86
+ message: version,
87
+ };
88
+ } catch {
89
+ return {
90
+ name: "Git",
91
+ status: "error",
92
+ message: "Git is not installed. Required for remote sources.",
93
+ };
94
+ }
95
+ }
96
+
97
+ async function checkSkillsDirectory(): Promise<DiagnosticResult> {
98
+ if (await directoryExists(SKILLS_ROOT)) {
99
+ return {
100
+ name: "Skills Directory",
101
+ status: "ok",
102
+ message: SKILLS_ROOT,
103
+ };
104
+ }
105
+ return {
106
+ name: "Skills Directory",
107
+ status: "warn",
108
+ message: `Not found: ${SKILLS_ROOT}. Will be created on first use.`,
109
+ };
110
+ }
111
+
112
+ async function checkConfigFile(): Promise<DiagnosticResult> {
113
+ const file = Bun.file(CONFIG_PATH);
114
+ if (await file.exists()) {
115
+ try {
116
+ await file.json();
117
+ return {
118
+ name: "Config File",
119
+ status: "ok",
120
+ message: CONFIG_PATH,
121
+ };
122
+ } catch {
123
+ return {
124
+ name: "Config File",
125
+ status: "error",
126
+ message: `Invalid JSON: ${CONFIG_PATH}`,
127
+ };
128
+ }
129
+ }
130
+ return {
131
+ name: "Config File",
132
+ status: "warn",
133
+ message: `Not found: ${CONFIG_PATH}. Will be created on first use.`,
134
+ };
135
+ }
136
+
137
+ async function checkStoreDirectory(): Promise<DiagnosticResult> {
138
+ if (await directoryExists(SKILLS_STORE)) {
139
+ const { readdir } = await import("fs/promises");
140
+ try {
141
+ const entries = await readdir(SKILLS_STORE);
142
+ const count = entries.length;
143
+ return {
144
+ name: "Store Directory",
145
+ status: "ok",
146
+ message: `${SKILLS_STORE} (${count} item${count !== 1 ? "s" : ""})`,
147
+ };
148
+ } catch {
149
+ return {
150
+ name: "Store Directory",
151
+ status: "ok",
152
+ message: SKILLS_STORE,
153
+ };
154
+ }
155
+ }
156
+ return {
157
+ name: "Store Directory",
158
+ status: "warn",
159
+ message: `Not found: ${SKILLS_STORE}. Will be created on first use.`,
160
+ };
161
+ }
162
+
163
+ async function checkSources(): Promise<DiagnosticResult[]> {
164
+ const results: DiagnosticResult[] = [];
165
+
166
+ try {
167
+ const sources = await getSources();
168
+
169
+ if (sources.length === 0) {
170
+ results.push({
171
+ name: "Sources",
172
+ status: "warn",
173
+ message: "No sources registered. Add one with: skills source add <url> --remote",
174
+ });
175
+ return results;
176
+ }
177
+
178
+ for (const source of sources) {
179
+ const path = getNamespacePath(source.namespace);
180
+ const exists = await directoryExists(path);
181
+
182
+ if (exists) {
183
+ results.push({
184
+ name: `Source: ${source.namespace}`,
185
+ status: "ok",
186
+ message: `${source.type} → ${path}`,
187
+ });
188
+ } else {
189
+ results.push({
190
+ name: `Source: ${source.namespace}`,
191
+ status: "error",
192
+ message: `Missing directory: ${path}. Run: skills update`,
193
+ });
194
+ }
195
+ }
196
+ } catch (error) {
197
+ results.push({
198
+ name: "Sources",
199
+ status: "error",
200
+ message: `Failed to read sources: ${error}`,
201
+ });
202
+ }
203
+
204
+ return results;
205
+ }
206
+
207
+ async function checkTargets(): Promise<DiagnosticResult[]> {
208
+ const results: DiagnosticResult[] = [];
209
+
210
+ try {
211
+ const targets = await getTargets();
212
+
213
+ if (targets.length === 0) {
214
+ results.push({
215
+ name: "Targets",
216
+ status: "warn",
217
+ message: "No targets registered. Add one with: skills target add <name> <path>",
218
+ });
219
+ return results;
220
+ }
221
+
222
+ for (const target of targets) {
223
+ const exists = await directoryExists(target.path);
224
+
225
+ if (exists) {
226
+ results.push({
227
+ name: `Target: ${target.name}`,
228
+ status: "ok",
229
+ message: target.path,
230
+ });
231
+ } else {
232
+ results.push({
233
+ name: `Target: ${target.name}`,
234
+ status: "warn",
235
+ message: `Directory missing: ${target.path}. Will be created on sync.`,
236
+ });
237
+ }
238
+ }
239
+ } catch (error) {
240
+ results.push({
241
+ name: "Targets",
242
+ status: "error",
243
+ message: `Failed to read targets: ${error}`,
244
+ });
245
+ }
246
+
247
+ return results;
248
+ }
@@ -0,0 +1,191 @@
1
+ import { $ } from "bun";
2
+ import { resolve } from "path";
3
+ import { rm, cp, mkdir } from "fs/promises";
4
+ import chalk from "chalk";
5
+ import Table from "cli-table3";
6
+ import { addSource, removeSource, getSources } from "../lib/config.ts";
7
+ import { parseGitUrl, getRemoteNamespace, getLocalNamespace, getNamespacePath, SKILLS_STORE } from "../lib/paths.ts";
8
+ import { directoryExists } from "../lib/hash.ts";
9
+
10
+ /**
11
+ * Add a source (remote or local)
12
+ */
13
+ export async function sourceAdd(pathOrUrl: string, type: "remote" | "local"): Promise<void> {
14
+ if (type === "remote") {
15
+ await addRemoteSource(pathOrUrl);
16
+ } else {
17
+ await addLocalSource(pathOrUrl);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Add a remote Git repository as a source
23
+ */
24
+ async function addRemoteSource(url: string): Promise<void> {
25
+ const parsed = parseGitUrl(url);
26
+
27
+ if (!parsed) {
28
+ 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
+ }
30
+
31
+ const namespace = getRemoteNamespace(parsed);
32
+ const targetPath = getNamespacePath(namespace);
33
+ const cloneUrl = parsed.cloneUrl;
34
+ const branch = parsed.branch || "main";
35
+
36
+ console.log(`Adding remote source: ${url}`);
37
+ console.log(`Namespace: ${namespace}`);
38
+ if (parsed.subdir) {
39
+ console.log(`Subdirectory: ${parsed.subdir}`);
40
+ }
41
+
42
+ // Check if directory already exists
43
+ if (await directoryExists(targetPath)) {
44
+ throw new Error(`Source already exists at ${namespace}. Remove it first with 'skills source remove ${namespace}'`);
45
+ }
46
+
47
+ // Create parent directory if needed
48
+ await mkdir(getNamespacePath(parsed.owner), { recursive: true });
49
+
50
+ // Clone the repository
51
+ console.log("Cloning repository...");
52
+
53
+ try {
54
+ if (parsed.subdir) {
55
+ // Use sparse checkout to only get the specific subdirectory
56
+ const tempPath = `${targetPath}_temp`;
57
+
58
+ // Initialize sparse checkout
59
+ await $`git clone --filter=blob:none --no-checkout --depth 1 --branch ${branch} ${cloneUrl} ${tempPath}`.quiet();
60
+
61
+ // Configure sparse checkout
62
+ await $`git -C ${tempPath} sparse-checkout init --cone`.quiet();
63
+ await $`git -C ${tempPath} sparse-checkout set ${parsed.subdir}`.quiet();
64
+ await $`git -C ${tempPath} checkout`.quiet();
65
+
66
+ // Move the subdirectory to the target path
67
+ const subdirFullPath = `${tempPath}/${parsed.subdir}`;
68
+ await cp(subdirFullPath, targetPath, { recursive: true });
69
+
70
+ // Clean up temp directory
71
+ await rm(tempPath, { recursive: true, force: true });
72
+ } else {
73
+ // Clone the entire repository
74
+ await $`git clone --depth 1 --branch ${branch} ${cloneUrl} ${targetPath}`.quiet();
75
+
76
+ // Remove .git directory to avoid nested git repos
77
+ await rm(`${targetPath}/.git`, { recursive: true, force: true });
78
+ }
79
+ } catch (error) {
80
+ // Clean up on failure
81
+ await rm(targetPath, { recursive: true, force: true });
82
+ await rm(`${targetPath}_temp`, { recursive: true, force: true });
83
+ throw new Error(`Failed to clone repository: ${error}`);
84
+ }
85
+
86
+ // Add to config
87
+ await addSource({
88
+ type: "remote",
89
+ url,
90
+ namespace,
91
+ });
92
+
93
+ console.log(`Successfully added remote source: ${namespace}`);
94
+ }
95
+
96
+ /**
97
+ * Add a local folder as a source
98
+ */
99
+ async function addLocalSource(folderPath: string): Promise<void> {
100
+ const absolutePath = resolve(folderPath);
101
+
102
+ if (!(await directoryExists(absolutePath))) {
103
+ throw new Error(`Local folder does not exist: ${absolutePath}`);
104
+ }
105
+
106
+ const namespace = getLocalNamespace(absolutePath);
107
+ const targetPath = getNamespacePath(namespace);
108
+
109
+ console.log(`Adding local source: ${absolutePath}`);
110
+ console.log(`Namespace: ${namespace}`);
111
+
112
+ // Check if namespace already exists
113
+ if (await directoryExists(targetPath)) {
114
+ throw new Error(`Source already exists at ${namespace}. Remove it first with 'skills source remove ${namespace}'`);
115
+ }
116
+
117
+ // Create local directory
118
+ await mkdir(getNamespacePath("local"), { recursive: true });
119
+
120
+ // Copy files
121
+ console.log("Copying files...");
122
+ await cp(absolutePath, targetPath, { recursive: true });
123
+
124
+ // Add to config
125
+ await addSource({
126
+ type: "local",
127
+ path: absolutePath,
128
+ namespace,
129
+ });
130
+
131
+ console.log(`Successfully added local source: ${namespace}`);
132
+ }
133
+
134
+ /**
135
+ * Remove a source by namespace
136
+ */
137
+ export async function sourceRemove(namespace: string): Promise<void> {
138
+ const removed = await removeSource(namespace);
139
+
140
+ if (!removed) {
141
+ throw new Error(`Source not found: ${namespace}`);
142
+ }
143
+
144
+ // Remove the files from store
145
+ const targetPath = getNamespacePath(namespace);
146
+ await rm(targetPath, { recursive: true, force: true });
147
+
148
+ console.log(`Removed source: ${namespace}`);
149
+ }
150
+
151
+ /**
152
+ * List all sources in a table format
153
+ */
154
+ export async function sourceList(): Promise<void> {
155
+ const sources = await getSources();
156
+
157
+ if (sources.length === 0) {
158
+ console.log("No sources registered.");
159
+ console.log(`Add a source with: ${chalk.cyan("skills source add <url> --remote")}`);
160
+ return;
161
+ }
162
+
163
+ const table = new Table({
164
+ head: [
165
+ chalk.bold("Namespace"),
166
+ chalk.bold("Type"),
167
+ chalk.bold("Location"),
168
+ chalk.bold("Status"),
169
+ ],
170
+ style: {
171
+ head: [],
172
+ border: [],
173
+ },
174
+ });
175
+
176
+ for (const source of sources) {
177
+ const exists = await directoryExists(getNamespacePath(source.namespace));
178
+ const status = exists ? chalk.green("OK") : chalk.red("MISSING");
179
+
180
+ table.push([
181
+ source.namespace,
182
+ source.type,
183
+ source.url || source.path || "",
184
+ status,
185
+ ]);
186
+ }
187
+
188
+ console.log();
189
+ console.log(table.toString());
190
+ console.log();
191
+ }