myskill 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/LICENSE +21 -0
- package/README.md +141 -0
- package/bin/myskill.js +137 -0
- package/package.json +56 -0
- package/src/commands/config.js +38 -0
- package/src/commands/convert.js +111 -0
- package/src/commands/create.js +204 -0
- package/src/commands/doctor.js +60 -0
- package/src/commands/find.js +108 -0
- package/src/commands/install.js +93 -0
- package/src/commands/list.js +60 -0
- package/src/commands/pull.js +93 -0
- package/src/commands/run.js +83 -0
- package/src/commands/uninstall.js +131 -0
- package/src/commands/validate.js +87 -0
- package/src/platforms/claude.js +70 -0
- package/src/platforms/codex.js +25 -0
- package/src/platforms/gemini.js +19 -0
- package/src/platforms/index.js +27 -0
- package/src/platforms/opencode.js +36 -0
- package/src/templates/generateSkill.js +20 -0
- package/src/utils/config.js +41 -0
- package/src/utils/prompt.js +66 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { format } from "prettier";
|
|
5
|
+
import { platforms, getPlatform, getPlatformPath } from "../platforms/index.js";
|
|
6
|
+
import { generateSkill } from "../templates/generateSkill.js";
|
|
7
|
+
import { promptWithCancellation } from "../utils/prompt.js";
|
|
8
|
+
|
|
9
|
+
export async function create(options = {}) {
|
|
10
|
+
let answers = { ...options };
|
|
11
|
+
|
|
12
|
+
if (!options.nonInteractive) {
|
|
13
|
+
console.log(chalk.blue("Interactive Skill Creator"));
|
|
14
|
+
|
|
15
|
+
// Step 1: Platform Selection
|
|
16
|
+
if (!answers.platform) {
|
|
17
|
+
const platformAnswer = await promptWithCancellation([
|
|
18
|
+
{
|
|
19
|
+
type: "list",
|
|
20
|
+
name: "platform",
|
|
21
|
+
message: "Select target platform:",
|
|
22
|
+
choices: Object.values(platforms).map((p) => ({
|
|
23
|
+
name: p.name,
|
|
24
|
+
value: p.id,
|
|
25
|
+
})),
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
answers.platform = platformAnswer.platform;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const platformConfig = getPlatform(answers.platform);
|
|
32
|
+
if (!platformConfig) {
|
|
33
|
+
console.error(chalk.red(`Error: Unknown platform '${answers.platform}'`));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Helper validation function using Zod schema
|
|
38
|
+
const validateField = (field, value) => {
|
|
39
|
+
const schema = platformConfig.schema.shape[field];
|
|
40
|
+
if (!schema) return true; // No validation defined
|
|
41
|
+
const result = schema.safeParse(value);
|
|
42
|
+
if (result.success) return true;
|
|
43
|
+
return result.error.issues[0].message;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Step 2: Name & Description & Scope
|
|
47
|
+
const remainingPrompts = [];
|
|
48
|
+
if (!answers.name) {
|
|
49
|
+
remainingPrompts.push({
|
|
50
|
+
type: "input",
|
|
51
|
+
name: "name",
|
|
52
|
+
message: "Skill Name:",
|
|
53
|
+
validate: (input) => validateField("name", input),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!answers.description) {
|
|
58
|
+
remainingPrompts.push({
|
|
59
|
+
type: "input",
|
|
60
|
+
name: "description",
|
|
61
|
+
message: "Description:",
|
|
62
|
+
validate: (input) => validateField("description", input),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!answers.scope) {
|
|
67
|
+
remainingPrompts.push({
|
|
68
|
+
type: "list",
|
|
69
|
+
name: "scope",
|
|
70
|
+
message: "Where to create the skill?",
|
|
71
|
+
choices: [
|
|
72
|
+
{ name: "Current Directory (Project)", value: "project" },
|
|
73
|
+
{ name: "Global Skills Directory", value: "global" },
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Platform specific prompts
|
|
79
|
+
if (platformConfig.prompts && platformConfig.prompts.length > 0) {
|
|
80
|
+
remainingPrompts.push(...platformConfig.prompts);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const interactiveAnswers = await promptWithCancellation(remainingPrompts);
|
|
84
|
+
answers = { ...answers, ...interactiveAnswers };
|
|
85
|
+
} else {
|
|
86
|
+
// Non-interactive validation
|
|
87
|
+
if (!answers.platform || !answers.name || !answers.description) {
|
|
88
|
+
console.error(
|
|
89
|
+
chalk.red(
|
|
90
|
+
"Error: --platform, --name, and --description are required in non-interactive mode.",
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
if (process.env.NODE_ENV !== "test") process.exit(1);
|
|
94
|
+
else {
|
|
95
|
+
process.exit(1);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const platformConfig = getPlatform(answers.platform);
|
|
101
|
+
if (!platformConfig) {
|
|
102
|
+
console.error(chalk.red(`Error: Unknown platform '${answers.platform}'`));
|
|
103
|
+
if (process.env.NODE_ENV !== "test") process.exit(1);
|
|
104
|
+
else {
|
|
105
|
+
process.exit(1);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Validate using schema
|
|
111
|
+
const result = platformConfig.schema.safeParse({
|
|
112
|
+
name: answers.name,
|
|
113
|
+
description: answers.description,
|
|
114
|
+
...answers, // includes platform specific flags if any passed? Command options don't map 1:1 to flags yet except name/desc
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!result.success) {
|
|
118
|
+
console.error(chalk.red("Validation Error:"));
|
|
119
|
+
result.error.issues.forEach((issue) => {
|
|
120
|
+
console.error(chalk.red(`- ${issue.path.join(".")}: ${issue.message}`));
|
|
121
|
+
});
|
|
122
|
+
if (process.env.NODE_ENV !== "test") process.exit(1);
|
|
123
|
+
else {
|
|
124
|
+
process.exit(1);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Construct Front Matter
|
|
131
|
+
// Filter out internal keys
|
|
132
|
+
const internalKeys = ["scope", "platform", "nonInteractive"];
|
|
133
|
+
const frontMatter = {};
|
|
134
|
+
|
|
135
|
+
// We need to merge answers into frontMatter, but ONLY fields that belong to the schema
|
|
136
|
+
// or platform specific prompts.
|
|
137
|
+
// Easiest way: take everything from answers that isn't internal.
|
|
138
|
+
|
|
139
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
140
|
+
if (!internalKeys.includes(key)) {
|
|
141
|
+
frontMatter[key] = value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Specific cleanup
|
|
146
|
+
if (frontMatter.restrictTools !== undefined) delete frontMatter.restrictTools;
|
|
147
|
+
if (frontMatter.allowedTools && frontMatter.allowedTools.length === 0)
|
|
148
|
+
delete frontMatter.allowedTools;
|
|
149
|
+
// Also delete confirm if present from previous implementation logic, though here we moved it
|
|
150
|
+
if (frontMatter.confirm !== undefined) delete frontMatter.confirm;
|
|
151
|
+
|
|
152
|
+
const platformConfig = getPlatform(answers.platform); // get again to be safe
|
|
153
|
+
|
|
154
|
+
let fileContent = generateSkill(frontMatter);
|
|
155
|
+
|
|
156
|
+
// Format with Prettier
|
|
157
|
+
try {
|
|
158
|
+
fileContent = await format(fileContent, { parser: "markdown" });
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// Ignore formatting errors, fallback to raw content
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let targetDir;
|
|
164
|
+
if (answers.scope === "global") {
|
|
165
|
+
const globalPath = await getPlatformPath(answers.platform);
|
|
166
|
+
targetDir = path.join(globalPath, answers.name);
|
|
167
|
+
} else {
|
|
168
|
+
const localBase =
|
|
169
|
+
answers.platform === "opencode"
|
|
170
|
+
? ".opencode/skill"
|
|
171
|
+
: `.${answers.platform}/skills`;
|
|
172
|
+
targetDir = path.join(process.cwd(), localBase, answers.name);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!options.nonInteractive) {
|
|
176
|
+
console.log(chalk.yellow(`\nAbout to create skill at: ${targetDir}`));
|
|
177
|
+
console.log(chalk.gray(fileContent));
|
|
178
|
+
|
|
179
|
+
const { confirm } = await promptWithCancellation([
|
|
180
|
+
{
|
|
181
|
+
type: "confirm",
|
|
182
|
+
name: "confirm",
|
|
183
|
+
message: "Proceed?",
|
|
184
|
+
default: true,
|
|
185
|
+
},
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
if (!confirm) {
|
|
189
|
+
console.log(chalk.red("Aborted."));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
await fs.ensureDir(targetDir);
|
|
196
|
+
await fs.writeFile(path.join(targetDir, "SKILL.md"), fileContent);
|
|
197
|
+
await fs.ensureDir(path.join(targetDir, "scripts"));
|
|
198
|
+
console.log(chalk.green(`Skill created successfully at ${targetDir}`));
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(chalk.red(`Error creating skill: ${error.message}`));
|
|
201
|
+
if (process.env.NODE_ENV !== "test") process.exit(1);
|
|
202
|
+
else process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import simpleGit from "simple-git";
|
|
5
|
+
import { platforms, getPlatformPath } from "../platforms/index.js";
|
|
6
|
+
import { getConfig } from "../utils/config.js";
|
|
7
|
+
|
|
8
|
+
export async function doctor() {
|
|
9
|
+
console.log(chalk.bold("Running diagnostics..."));
|
|
10
|
+
console.log("");
|
|
11
|
+
|
|
12
|
+
let issues = 0;
|
|
13
|
+
|
|
14
|
+
// 1. Check Git
|
|
15
|
+
try {
|
|
16
|
+
const git = simpleGit();
|
|
17
|
+
const version = await git.raw(["--version"]);
|
|
18
|
+
console.log(`${chalk.green("✓")} Git installed: ${version.trim()}`);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.log(`${chalk.red("✗")} Git not found or error: ${e.message}`);
|
|
21
|
+
issues++;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Check Platform Directories
|
|
25
|
+
for (const platform of Object.values(platforms)) {
|
|
26
|
+
const pPath = await getPlatformPath(platform.id);
|
|
27
|
+
try {
|
|
28
|
+
await fs.ensureDir(pPath);
|
|
29
|
+
// Check write access
|
|
30
|
+
const testFile = path.join(pPath, ".write-test");
|
|
31
|
+
await fs.writeFile(testFile, "test");
|
|
32
|
+
await fs.remove(testFile);
|
|
33
|
+
console.log(
|
|
34
|
+
`${chalk.green("✓")} ${platform.name} directory writable: ${pPath}`,
|
|
35
|
+
);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log(
|
|
38
|
+
`${chalk.red("✗")} ${platform.name} directory issue: ${e.message}`,
|
|
39
|
+
);
|
|
40
|
+
issues++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Check Config
|
|
45
|
+
try {
|
|
46
|
+
const config = await getConfig();
|
|
47
|
+
console.log(`${chalk.green("✓")} Config loaded: ${JSON.stringify(config)}`);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.log(`${chalk.red("✗")} Config load error: ${e.message}`);
|
|
50
|
+
issues++;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log("");
|
|
54
|
+
if (issues === 0) {
|
|
55
|
+
console.log(chalk.green("All checks passed!"));
|
|
56
|
+
} else {
|
|
57
|
+
console.log(chalk.yellow(`Found ${issues} issues.`));
|
|
58
|
+
// Don't exit with error code, just report
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import Fuse from "fuse.js";
|
|
6
|
+
import { platforms, getPlatformPath } from "../platforms/index.js";
|
|
7
|
+
|
|
8
|
+
export async function find(query, options = {}) {
|
|
9
|
+
let targetPlatforms = [];
|
|
10
|
+
if (options.platform) {
|
|
11
|
+
if (platforms[options.platform]) {
|
|
12
|
+
targetPlatforms.push(platforms[options.platform]);
|
|
13
|
+
} else {
|
|
14
|
+
console.error(chalk.red(`Error: Unknown platform '${options.platform}'`));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
targetPlatforms = Object.values(platforms);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const allSkills = [];
|
|
22
|
+
|
|
23
|
+
// Gather skills
|
|
24
|
+
for (const platform of targetPlatforms) {
|
|
25
|
+
const globalPath = await getPlatformPath(platform.id);
|
|
26
|
+
const locations = [{ name: "Global", path: globalPath, type: "global" }];
|
|
27
|
+
|
|
28
|
+
const localBase =
|
|
29
|
+
platform.id === "opencode" ? ".opencode/skill" : `.${platform.id}/skills`;
|
|
30
|
+
locations.push({
|
|
31
|
+
name: "Project",
|
|
32
|
+
path: path.join(process.cwd(), localBase),
|
|
33
|
+
type: "project",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
for (const loc of locations) {
|
|
37
|
+
if (await fs.pathExists(loc.path)) {
|
|
38
|
+
try {
|
|
39
|
+
const items = await fs.readdir(loc.path, { withFileTypes: true });
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
if (item.isDirectory()) {
|
|
42
|
+
const skillPath = path.join(loc.path, item.name, "SKILL.md");
|
|
43
|
+
if (await fs.pathExists(skillPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const content = await fs.readFile(skillPath, "utf8");
|
|
46
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
47
|
+
if (match) {
|
|
48
|
+
const fm = yaml.load(match[1]);
|
|
49
|
+
allSkills.push({
|
|
50
|
+
name: fm.name || item.name,
|
|
51
|
+
description: fm.description || "",
|
|
52
|
+
platform: platform.name,
|
|
53
|
+
platformId: platform.id,
|
|
54
|
+
location: loc.name,
|
|
55
|
+
path: skillPath,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Ignore read errors
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
// Ignore readdir errors
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (allSkills.length === 0) {
|
|
72
|
+
console.log(chalk.yellow("No skills found."));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let results = allSkills;
|
|
77
|
+
|
|
78
|
+
if (query) {
|
|
79
|
+
const fuse = new Fuse(allSkills, {
|
|
80
|
+
keys: ["name", "description"],
|
|
81
|
+
threshold: 0.4, // Fuzzy threshold (0.0 = exact match, 1.0 = match anything)
|
|
82
|
+
includeScore: true,
|
|
83
|
+
});
|
|
84
|
+
results = fuse.search(query).map((r) => r.item);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Display results
|
|
88
|
+
if (results.length === 0) {
|
|
89
|
+
console.log(chalk.yellow(`No skills matched query '${query}'`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(chalk.bold(`Found ${results.length} skills:`));
|
|
94
|
+
console.log("------------------------------------------------");
|
|
95
|
+
|
|
96
|
+
// Group by platform for cleaner output? Or just list.
|
|
97
|
+
// List seems better for search results.
|
|
98
|
+
|
|
99
|
+
results.forEach((skill) => {
|
|
100
|
+
console.log(
|
|
101
|
+
`${chalk.green(skill.name)} ${chalk.gray(`[${skill.platform} | ${skill.location}]`)}`,
|
|
102
|
+
);
|
|
103
|
+
if (skill.description) {
|
|
104
|
+
console.log(` ${skill.description}`);
|
|
105
|
+
}
|
|
106
|
+
console.log("");
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { platforms, getPlatformPath } from "../platforms/index.js";
|
|
5
|
+
import { promptWithCancellation } from "../utils/prompt.js";
|
|
6
|
+
|
|
7
|
+
export async function install(sourcePath, options = {}) {
|
|
8
|
+
const resolvedSource = path.resolve(sourcePath);
|
|
9
|
+
if (!(await fs.pathExists(resolvedSource))) {
|
|
10
|
+
console.error(
|
|
11
|
+
chalk.red(`Error: Source path ${resolvedSource} does not exist`),
|
|
12
|
+
);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let platform;
|
|
17
|
+
if (options.platform) {
|
|
18
|
+
platform = platforms[options.platform];
|
|
19
|
+
if (!platform) {
|
|
20
|
+
console.error(chalk.red(`Error: Unknown platform '${options.platform}'`));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
console.log(
|
|
25
|
+
chalk.yellow("Platform not specified. Attempting to detect..."),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const skillMdPath = path.join(resolvedSource, "SKILL.md");
|
|
29
|
+
if (await fs.pathExists(skillMdPath)) {
|
|
30
|
+
// Exists
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const answers = await promptWithCancellation([
|
|
34
|
+
{
|
|
35
|
+
type: "list",
|
|
36
|
+
name: "platform",
|
|
37
|
+
message: "Select target platform to install to:",
|
|
38
|
+
choices: Object.values(platforms).map((p) => ({
|
|
39
|
+
name: p.name,
|
|
40
|
+
value: p.id,
|
|
41
|
+
})),
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
platform = platforms[answers.platform];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const skillName = path.basename(resolvedSource);
|
|
48
|
+
const globalPath = await getPlatformPath(platform.id);
|
|
49
|
+
const targetDir = path.join(globalPath, skillName);
|
|
50
|
+
|
|
51
|
+
if (await fs.pathExists(targetDir)) {
|
|
52
|
+
if (options.force) {
|
|
53
|
+
// Continue
|
|
54
|
+
} else if (options.nonInteractive) {
|
|
55
|
+
console.error(
|
|
56
|
+
chalk.red(
|
|
57
|
+
`Error: Skill already exists at ${targetDir}. Use --force to overwrite.`,
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
} else {
|
|
62
|
+
const { overwrite } = await promptWithCancellation([
|
|
63
|
+
{
|
|
64
|
+
type: "confirm",
|
|
65
|
+
name: "overwrite",
|
|
66
|
+
message: `Skill '${skillName}' already exists in ${platform.name} global directory. Overwrite?`,
|
|
67
|
+
default: false,
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
if (!overwrite) {
|
|
72
|
+
console.log(chalk.red("Installation aborted."));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await fs.ensureDir(globalPath);
|
|
80
|
+
await fs.copy(resolvedSource, targetDir, {
|
|
81
|
+
filter: (src) => {
|
|
82
|
+
const basename = path.basename(src);
|
|
83
|
+
return basename !== "node_modules" && basename !== ".git";
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
console.log(
|
|
87
|
+
chalk.green(`Successfully installed '${skillName}' to ${targetDir}`),
|
|
88
|
+
);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error(chalk.red(`Installation failed: ${e.message}`));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { platforms, getPlatformPath } from "../platforms/index.js";
|
|
6
|
+
|
|
7
|
+
export async function list(options = {}) {
|
|
8
|
+
let targetPlatforms = [];
|
|
9
|
+
if (options.platform) {
|
|
10
|
+
if (platforms[options.platform]) {
|
|
11
|
+
targetPlatforms.push(platforms[options.platform]);
|
|
12
|
+
} else {
|
|
13
|
+
console.error(chalk.red(`Error: Unknown platform '${options.platform}'`));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
} else {
|
|
17
|
+
targetPlatforms = Object.values(platforms);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const platform of targetPlatforms) {
|
|
21
|
+
console.log(chalk.blue(`\n=== ${platform.name} Skills ===`));
|
|
22
|
+
|
|
23
|
+
const globalPath = await getPlatformPath(platform.id);
|
|
24
|
+
const locations = [{ name: "Global", path: globalPath }];
|
|
25
|
+
|
|
26
|
+
const localBase =
|
|
27
|
+
platform.id === "opencode" ? ".opencode/skill" : `.${platform.id}/skills`;
|
|
28
|
+
locations.push({
|
|
29
|
+
name: "Project",
|
|
30
|
+
path: path.join(process.cwd(), localBase),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
for (const loc of locations) {
|
|
34
|
+
if (await fs.pathExists(loc.path)) {
|
|
35
|
+
const items = await fs.readdir(loc.path, { withFileTypes: true });
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
if (item.isDirectory()) {
|
|
38
|
+
const skillPath = path.join(loc.path, item.name, "SKILL.md");
|
|
39
|
+
if (await fs.pathExists(skillPath)) {
|
|
40
|
+
try {
|
|
41
|
+
const content = await fs.readFile(skillPath, "utf8");
|
|
42
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
43
|
+
if (match) {
|
|
44
|
+
const fm = yaml.load(match[1]);
|
|
45
|
+
console.log(
|
|
46
|
+
`- ${chalk.bold(fm.name)} (${loc.name}): ${fm.description || "No description"}`,
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
console.log(`- ${item.name} (${loc.name}): [Invalid Format]`);
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.log(`- ${item.name} (${loc.name}): [Read Error]`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import simpleGit from "simple-git";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { platforms, getPlatform, getPlatformPath } from "../platforms/index.js";
|
|
6
|
+
import { promptWithCancellation } from "../utils/prompt.js";
|
|
7
|
+
|
|
8
|
+
export async function pull(repoUrl, options = {}) {
|
|
9
|
+
// Logic:
|
|
10
|
+
// 1. Identify target platform.
|
|
11
|
+
// 2. Identify target name (from repo name or option).
|
|
12
|
+
// 3. Clone or Pull.
|
|
13
|
+
|
|
14
|
+
if (!repoUrl) {
|
|
15
|
+
console.error(chalk.red("Error: Repository URL is required."));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let platformName = options.platform;
|
|
20
|
+
if (!platformName) {
|
|
21
|
+
// Try interactive
|
|
22
|
+
if (!options.nonInteractive) {
|
|
23
|
+
const answer = await promptWithCancellation([
|
|
24
|
+
{
|
|
25
|
+
type: "list",
|
|
26
|
+
name: "platform",
|
|
27
|
+
message: "Target platform for this skill:",
|
|
28
|
+
choices: Object.values(platforms).map((p) => ({
|
|
29
|
+
name: p.name,
|
|
30
|
+
value: p.id,
|
|
31
|
+
})),
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
platformName = answer.platform;
|
|
35
|
+
} else {
|
|
36
|
+
console.error(chalk.red("Error: Platform is required (--platform)."));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const platform = getPlatform(platformName);
|
|
42
|
+
if (!platform) {
|
|
43
|
+
console.error(chalk.red(`Error: Unknown platform '${platformName}'`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Determine skill name from repo URL if not provided
|
|
48
|
+
let skillName = options.name;
|
|
49
|
+
if (!skillName) {
|
|
50
|
+
const basename = path.basename(repoUrl, ".git");
|
|
51
|
+
skillName = basename;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Target directory
|
|
55
|
+
const platformPath = await getPlatformPath(platformName);
|
|
56
|
+
const targetDir = path.join(platformPath, skillName);
|
|
57
|
+
|
|
58
|
+
const git = simpleGit();
|
|
59
|
+
|
|
60
|
+
// Check if git is installed
|
|
61
|
+
try {
|
|
62
|
+
await git.raw(["--version"]);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Git is not installed or not in PATH. Please install git to use this command.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (await fs.pathExists(targetDir)) {
|
|
70
|
+
// Exists, try to pull
|
|
71
|
+
console.log(
|
|
72
|
+
chalk.blue(`Skill '${skillName}' exists. Pulling latest changes...`),
|
|
73
|
+
);
|
|
74
|
+
try {
|
|
75
|
+
await git.cwd(targetDir).pull();
|
|
76
|
+
console.log(chalk.green("Successfully updated skill."));
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error(chalk.red(`Error pulling changes: ${e.message}`));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// Clone
|
|
83
|
+
console.log(chalk.blue(`Cloning '${skillName}' to ${targetDir}...`));
|
|
84
|
+
try {
|
|
85
|
+
await fs.ensureDir(platformPath);
|
|
86
|
+
await git.clone(repoUrl, targetDir);
|
|
87
|
+
console.log(chalk.green("Successfully cloned skill."));
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.error(chalk.red(`Error cloning repo: ${e.message}`));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
|
|
6
|
+
export async function run(skillName, args = [], options = {}) {
|
|
7
|
+
// Goal: Run the skill script.
|
|
8
|
+
// 1. Locate the skill (reuse find logic logic or simple path check).
|
|
9
|
+
// 2. Identify the entry point (e.g. scripts/run.py, scripts/index.js).
|
|
10
|
+
// 3. Execute it.
|
|
11
|
+
|
|
12
|
+
// For simplicity, let's look in current directory first, then use `uninstall`'s detection logic or similar.
|
|
13
|
+
// We'll reuse `uninstall`'s detection logic essentially? Or just rely on current dir for now as experimental.
|
|
14
|
+
// Let's implement a simple path resolver reusing what we learned.
|
|
15
|
+
|
|
16
|
+
let targetPath = path.resolve(skillName);
|
|
17
|
+
|
|
18
|
+
// If absolute path provided and exists
|
|
19
|
+
if (!(await fs.pathExists(targetPath))) {
|
|
20
|
+
// Try resolving as name in current dir
|
|
21
|
+
targetPath = path.resolve(process.cwd(), skillName);
|
|
22
|
+
if (!(await fs.pathExists(targetPath))) {
|
|
23
|
+
// TODO: Look in global paths if we want to run installed skills.
|
|
24
|
+
// For experimental runner, let's stick to local development context for now or global.
|
|
25
|
+
// Let's assume user provides path or name in current dir.
|
|
26
|
+
console.error(
|
|
27
|
+
chalk.red(`Error: Skill directory '${skillName}' not found.`),
|
|
28
|
+
);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
return; // Ensure return for test mocks
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Look for entry point
|
|
35
|
+
const scriptsDir = path.join(targetPath, "scripts");
|
|
36
|
+
if (!(await fs.pathExists(scriptsDir))) {
|
|
37
|
+
console.error(
|
|
38
|
+
chalk.red(`Error: 'scripts' directory not found in ${targetPath}`),
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const potentialEntryPoints = [
|
|
45
|
+
{ file: "index.js", cmd: "node" },
|
|
46
|
+
{ file: "main.py", cmd: "python" },
|
|
47
|
+
{ file: "run.py", cmd: "python" },
|
|
48
|
+
{ file: "run.sh", cmd: "bash" },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
let entryPoint;
|
|
52
|
+
for (const ep of potentialEntryPoints) {
|
|
53
|
+
if (await fs.pathExists(path.join(scriptsDir, ep.file))) {
|
|
54
|
+
entryPoint = ep;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!entryPoint) {
|
|
60
|
+
console.error(
|
|
61
|
+
chalk.red(
|
|
62
|
+
"Error: No supported entry point found (index.js, main.py, run.py, run.sh).",
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(chalk.blue(`Running ${entryPoint.file}...`));
|
|
70
|
+
|
|
71
|
+
const scriptPath = path.join(scriptsDir, entryPoint.file);
|
|
72
|
+
const child = spawn(entryPoint.cmd, [scriptPath, ...args], {
|
|
73
|
+
stdio: "inherit",
|
|
74
|
+
cwd: targetPath, // Run in skill root context? Or scripts? Usually root.
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
child.on("close", (code) => {
|
|
78
|
+
if (code !== 0) {
|
|
79
|
+
console.error(chalk.red(`Process exited with code ${code}`));
|
|
80
|
+
process.exit(code);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|