@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/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/PRD.md +56 -0
- package/README.md +329 -0
- package/bun.lock +45 -0
- package/index.ts +2 -0
- package/package.json +37 -0
- package/src/cli.ts +205 -0
- package/src/commands/doctor.ts +248 -0
- package/src/commands/source.ts +191 -0
- package/src/commands/status.ts +84 -0
- package/src/commands/sync.ts +72 -0
- package/src/commands/target.ts +97 -0
- package/src/commands/update.ts +108 -0
- package/src/lib/config.ts +127 -0
- package/src/lib/hash.ts +80 -0
- package/src/lib/paths.ts +236 -0
- package/src/types.ts +36 -0
- package/tests/cli.test.ts +252 -0
- package/tests/config.test.ts +135 -0
- package/tests/hash.test.ts +119 -0
- package/tests/paths.test.ts +173 -0
- package/tsconfig.json +29 -0
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
|
+
}
|