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.
@@ -0,0 +1,131 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import { platforms, getPlatform, getPlatformPath } from "../platforms/index.js";
5
+ import { promptWithCancellation } from "../utils/prompt.js";
6
+
7
+ export async function uninstall(name, options = {}) {
8
+ // If name is not provided, try to infer from current directory
9
+ let skillName = name;
10
+ let targetPath;
11
+ let platform;
12
+
13
+ if (!skillName) {
14
+ // Check if current directory is a skill
15
+ const skillMdPath = path.join(process.cwd(), "SKILL.md");
16
+ if (await fs.pathExists(skillMdPath)) {
17
+ skillName = path.basename(process.cwd());
18
+ targetPath = process.cwd();
19
+ console.log(
20
+ chalk.blue(`Detected skill '${skillName}' in current directory.`),
21
+ );
22
+ } else {
23
+ console.error(chalk.red("Error: Skill name is required."));
24
+ process.exit(1);
25
+ }
26
+ }
27
+
28
+ // If platform is provided, look in that platform's global path
29
+ if (options.platform) {
30
+ platform = getPlatform(options.platform);
31
+ if (!platform) {
32
+ console.error(chalk.red(`Error: Unknown platform '${options.platform}'`));
33
+ process.exit(1);
34
+ }
35
+
36
+ const globalPath = path.join(platform.defaultPath, skillName);
37
+ if (await fs.pathExists(globalPath)) {
38
+ targetPath = globalPath;
39
+ }
40
+ }
41
+
42
+ // If we still don't have a target path, search in all platforms globally or check local project structure
43
+ if (!targetPath) {
44
+ // Search in all global locations
45
+ const found = [];
46
+ for (const p of Object.values(platforms)) {
47
+ const globalPath = await getPlatformPath(p.id);
48
+ const pPath = path.join(globalPath, skillName);
49
+ if (await fs.pathExists(pPath)) {
50
+ found.push({ platform: p, path: pPath, location: "Global" });
51
+ }
52
+ }
53
+
54
+ // Check current directory if we didn't start there
55
+ if (await fs.pathExists(path.join(process.cwd(), "SKILL.md"))) {
56
+ // Check if name matches
57
+ // Logic is tricky if user provides name but we are in a folder.
58
+ // Assuming user provided name implies searching for it.
59
+ }
60
+
61
+ // Also check project local folders like .claude/skills/name
62
+ for (const p of Object.values(platforms)) {
63
+ const localBase =
64
+ p.id === "opencode" ? ".opencode/skill" : `.${p.id}/skills`;
65
+ const localPath = path.join(process.cwd(), localBase, skillName);
66
+ if (await fs.pathExists(localPath)) {
67
+ found.push({ platform: p, path: localPath, location: "Project" });
68
+ }
69
+ }
70
+
71
+ if (found.length === 0) {
72
+ console.error(chalk.red(`Error: Skill '${skillName}' not found.`));
73
+ process.exit(1);
74
+ } else if (found.length === 1) {
75
+ targetPath = found[0].path;
76
+ console.log(
77
+ chalk.blue(
78
+ `Found skill in ${found[0].platform.name} (${found[0].location})`,
79
+ ),
80
+ );
81
+ } else {
82
+ // Multiple found, ask user
83
+ if (options.nonInteractive) {
84
+ console.error(
85
+ chalk.red(
86
+ `Error: Multiple skills found with name '${skillName}'. Specify --platform or use interactive mode.`,
87
+ ),
88
+ );
89
+ process.exit(1);
90
+ }
91
+
92
+ const answer = await promptWithCancellation([
93
+ {
94
+ type: "list",
95
+ name: "target",
96
+ message: "Multiple skills found. Which one to uninstall?",
97
+ choices: found.map((f) => ({
98
+ name: `${f.platform.name} (${f.location}) - ${f.path}`,
99
+ value: f.path,
100
+ })),
101
+ },
102
+ ]);
103
+ targetPath = answer.target;
104
+ }
105
+ }
106
+
107
+ // Confirm deletion
108
+ if (!options.nonInteractive) {
109
+ const { confirm } = await promptWithCancellation([
110
+ {
111
+ type: "confirm",
112
+ name: "confirm",
113
+ message: `Are you sure you want to delete ${targetPath}? This cannot be undone.`,
114
+ default: false,
115
+ },
116
+ ]);
117
+
118
+ if (!confirm) {
119
+ console.log(chalk.yellow("Aborted."));
120
+ return;
121
+ }
122
+ }
123
+
124
+ try {
125
+ await fs.remove(targetPath);
126
+ console.log(chalk.green(`Successfully removed skill from ${targetPath}`));
127
+ } catch (e) {
128
+ console.error(chalk.red(`Error removing skill: ${e.message}`));
129
+ process.exit(1);
130
+ }
131
+ }
@@ -0,0 +1,87 @@
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 } from "../platforms/index.js";
6
+
7
+ export async function validate(targetPath, options = {}) {
8
+ const resolvedPath = path.resolve(targetPath);
9
+ const skillMdPath = path.join(resolvedPath, "SKILL.md");
10
+
11
+ if (!(await fs.pathExists(skillMdPath))) {
12
+ console.error(chalk.red(`Error: SKILL.md not found in ${resolvedPath}`));
13
+ process.exit(1);
14
+ }
15
+
16
+ const content = await fs.readFile(skillMdPath, "utf8");
17
+
18
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
19
+ if (!match) {
20
+ console.error(
21
+ chalk.red("Error: Invalid front matter format (missing --- fences)"),
22
+ );
23
+ process.exit(1);
24
+ }
25
+
26
+ let frontMatter;
27
+ try {
28
+ frontMatter = yaml.load(match[1]);
29
+ } catch (e) {
30
+ console.error(chalk.red(`Error: YAML parsing failed: ${e.message}`));
31
+ process.exit(1);
32
+ }
33
+
34
+ let targetPlatforms = [];
35
+ if (options.platform) {
36
+ if (platforms[options.platform]) {
37
+ targetPlatforms.push(platforms[options.platform]);
38
+ } else {
39
+ console.error(chalk.red(`Error: Unknown platform '${options.platform}'`));
40
+ process.exit(1);
41
+ }
42
+ } else {
43
+ targetPlatforms = Object.values(platforms);
44
+ }
45
+
46
+ let successCount = 0;
47
+ const errors = [];
48
+
49
+ for (const platform of targetPlatforms) {
50
+ try {
51
+ platform.schema.parse(frontMatter);
52
+ const dirName = path.basename(resolvedPath);
53
+ if (frontMatter.name !== dirName) {
54
+ throw new Error(
55
+ `Directory name '${dirName}' does not match skill name '${frontMatter.name}'`,
56
+ );
57
+ }
58
+
59
+ console.log(chalk.green(`✓ Valid ${platform.name} skill`));
60
+ successCount++;
61
+ } catch (e) {
62
+ if (options.platform) {
63
+ if (e.errors) {
64
+ e.errors.forEach((err) => {
65
+ errors.push(
66
+ `[${platform.name}] ${err.path.join(".")}: ${err.message}`,
67
+ );
68
+ });
69
+ } else {
70
+ errors.push(`[${platform.name}] ${e.message}`);
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ if (successCount === 0) {
77
+ console.error(chalk.red("Validation Failed:"));
78
+ if (errors.length > 0) {
79
+ errors.forEach((e) => console.error(chalk.red(e)));
80
+ } else {
81
+ console.error(
82
+ chalk.red("Skill does not match any known platform schema."),
83
+ );
84
+ }
85
+ process.exit(1);
86
+ }
87
+ }
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { getConfig } from "../utils/config.js";
5
+
6
+ // We need to make this async or a function to read config
7
+ // But index.js exports static objects.
8
+ // Let's change the platforms to be functions or getters?
9
+ // This is a breaking change for internal architecture.
10
+ // Or we can load config synchronously? No, fs-extra is async.
11
+ // Best approach: Keep the export structure but make defaultPath a getter or load it once at startup.
12
+ // Since `bin/myskill.js` is async, we can initialize platforms there?
13
+ // Or just reading config synchronously if we must?
14
+ // Node.js supports top-level await in modules.
15
+
16
+ // Let's try top-level await for config loading since we are in ESM.
17
+ let config = {};
18
+ try {
19
+ // We can't easily do top level await here without fs/promises and maybe it slows down startup.
20
+ // Instead, let's make `defaultPath` a property we resolve when needed, or check config inside commands.
21
+ // BUT the prompt definitions and validation logic depend on platform definition.
22
+ // The path is mostly used in commands (create, list, etc).
23
+ // So let's change `defaultPath` to a function `getDefaultPath()`.
24
+ } catch (e) {}
25
+
26
+ // For now, I will modify the definitions to export functions or include a resolution helper.
27
+ // Actually, simplest is to just expose the hardcoded default as fallback,
28
+ // and utility function `getPlatformPath(platformId)` in `utils/config.js` or `platforms/index.js`.
29
+
30
+ export const claude = {
31
+ id: "claude",
32
+ name: "Claude Code",
33
+ defaultPath: path.join(os.homedir(), ".claude", "skills"),
34
+ schema: z.object({
35
+ name: z
36
+ .string()
37
+ .min(1)
38
+ .max(64)
39
+ .regex(
40
+ /^[a-z0-9-]+$/,
41
+ "Name must be lowercase alphanumeric with hyphens",
42
+ ),
43
+ description: z.string().min(1).max(1024),
44
+ "allowed-tools": z.union([z.string(), z.array(z.string())]).optional(),
45
+ model: z.string().optional(),
46
+ context: z.enum(["fork"]).optional(),
47
+ agent: z.string().optional(),
48
+ hooks: z.any().optional(),
49
+ "user-invocable": z.boolean().optional(),
50
+ }),
51
+ prompts: [
52
+ {
53
+ type: "confirm",
54
+ name: "restrictTools",
55
+ message: "Do you want to restrict allowed tools?",
56
+ default: false,
57
+ },
58
+ {
59
+ type: "input",
60
+ name: "allowedTools",
61
+ message: "Enter allowed tools (comma separated):",
62
+ when: (answers) => answers.restrictTools,
63
+ filter: (input) =>
64
+ input
65
+ .split(",")
66
+ .map((s) => s.trim())
67
+ .filter(Boolean),
68
+ },
69
+ ],
70
+ };
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ export const codex = {
6
+ id: "codex",
7
+ name: "OpenAI Codex",
8
+ defaultPath: path.join(os.homedir(), ".codex", "skills"),
9
+ schema: z.object({
10
+ name: z.string().min(1),
11
+ description: z.string().min(1),
12
+ metadata: z
13
+ .object({
14
+ "short-description": z.string().optional(),
15
+ })
16
+ .optional(),
17
+ }),
18
+ prompts: [
19
+ {
20
+ type: "input",
21
+ name: "shortDescription",
22
+ message: "Short description (for UI):",
23
+ },
24
+ ],
25
+ };
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ export const gemini = {
6
+ id: "gemini",
7
+ name: "Gemini CLI",
8
+ defaultPath: path.join(os.homedir(), ".gemini", "skills"),
9
+ schema: z.object({
10
+ name: z
11
+ .string()
12
+ .regex(
13
+ /^[a-z0-9-]+$/,
14
+ "Name must be lowercase alphanumeric with hyphens",
15
+ ),
16
+ description: z.string().min(1),
17
+ }),
18
+ prompts: [],
19
+ };
@@ -0,0 +1,27 @@
1
+ import { claude } from "./claude.js";
2
+ import { opencode } from "./opencode.js";
3
+ import { codex } from "./codex.js";
4
+ import { gemini } from "./gemini.js";
5
+ import { getConfig } from "../utils/config.js";
6
+
7
+ export const platforms = {
8
+ claude,
9
+ opencode,
10
+ codex,
11
+ gemini,
12
+ };
13
+
14
+ export function getPlatform(id) {
15
+ return platforms[id];
16
+ }
17
+
18
+ export async function getPlatformPath(id) {
19
+ const platform = platforms[id];
20
+ if (!platform) return null;
21
+
22
+ const config = await getConfig();
23
+ if (config[id] && config[id].path) {
24
+ return config[id].path;
25
+ }
26
+ return platform.defaultPath;
27
+ }
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ export const opencode = {
6
+ id: "opencode",
7
+ name: "OpenCode",
8
+ defaultPath: path.join(os.homedir(), ".config", "opencode", "skill"),
9
+ schema: z.object({
10
+ name: z
11
+ .string()
12
+ .min(1)
13
+ .max(64)
14
+ .regex(
15
+ /^[a-z0-9]+(-[a-z0-9]+)*$/,
16
+ "Name must be lowercase alphanumeric, single hyphens only, no start/end hyphen",
17
+ ),
18
+ description: z.string().min(1).max(1024),
19
+ license: z.string().optional(),
20
+ compatibility: z.string().optional(),
21
+ metadata: z.record(z.string()).optional(),
22
+ }),
23
+ prompts: [
24
+ {
25
+ type: "input",
26
+ name: "license",
27
+ message: "License (e.g., MIT):",
28
+ },
29
+ {
30
+ type: "input",
31
+ name: "compatibility",
32
+ message: "Compatibility (e.g., opencode):",
33
+ default: "opencode",
34
+ },
35
+ ],
36
+ };
@@ -0,0 +1,20 @@
1
+ import yaml from "js-yaml";
2
+
3
+ export function generateSkill(frontMatter, content) {
4
+ const yamlStr = yaml.dump(frontMatter, { lineWidth: -1 });
5
+ const defaultContent =
6
+ content ||
7
+ `# ${frontMatter.name}
8
+
9
+ ## Instructions
10
+ Provide clear, step-by-step guidance for the agent here.
11
+
12
+ ## Examples
13
+ Show concrete examples of using this skill.
14
+ `;
15
+
16
+ return `---
17
+ ${yamlStr}---
18
+
19
+ ${defaultContent}`;
20
+ }
@@ -0,0 +1,41 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ const CONFIG_PATH = path.join(
6
+ os.homedir(),
7
+ ".config",
8
+ "myskill",
9
+ "config.json",
10
+ );
11
+
12
+ export async function getConfig() {
13
+ if (await fs.pathExists(CONFIG_PATH)) {
14
+ try {
15
+ return await fs.readJson(CONFIG_PATH);
16
+ } catch (e) {
17
+ return {};
18
+ }
19
+ }
20
+ return {};
21
+ }
22
+
23
+ export async function setConfig(key, value) {
24
+ const config = await getConfig();
25
+
26
+ // Handle nested keys like "claude.path"
27
+ const keys = key.split(".");
28
+ let current = config;
29
+
30
+ for (let i = 0; i < keys.length - 1; i++) {
31
+ const k = keys[i];
32
+ if (!current[k]) current[k] = {};
33
+ current = current[k];
34
+ }
35
+
36
+ current[keys[keys.length - 1]] = value;
37
+
38
+ await fs.ensureDir(path.dirname(CONFIG_PATH));
39
+ await fs.writeJson(CONFIG_PATH, config, { spaces: 2 });
40
+ return config;
41
+ }
@@ -0,0 +1,66 @@
1
+ import inquirer from "inquirer";
2
+ import readline from "readline";
3
+
4
+ /**
5
+ * Wrapper for inquirer.prompt that handles cancellation gracefully
6
+ * Allows both Ctrl+C and Escape key to cancel prompts
7
+ * @param {Array} questions - Array of question objects for inquirer
8
+ * @returns {Promise<Object>} - Resolves with answers or throws on cancellation
9
+ */
10
+ export async function promptWithCancellation(questions) {
11
+ // Create a promise that can be rejected on Escape key
12
+ let cleanup = () => {}; // Placeholder for cleanup function
13
+
14
+ const escapePromise = new Promise((_, reject) => {
15
+ // Enable keypress events on stdin
16
+ readline.emitKeypressEvents(process.stdin);
17
+
18
+ if (process.stdin.isTTY) {
19
+ process.stdin.setRawMode(true);
20
+ }
21
+
22
+ const onKeypress = (str, key) => {
23
+ if (key && key.name === "escape") {
24
+ process.stdin.off("keypress", onKeypress);
25
+ if (process.stdin.isTTY) {
26
+ process.stdin.setRawMode(false);
27
+ }
28
+ reject(new Error("ESCAPE_PRESSED"));
29
+ }
30
+ };
31
+
32
+ process.stdin.on("keypress", onKeypress);
33
+
34
+ // Set up cleanup function
35
+ cleanup = () => {
36
+ process.stdin.off("keypress", onKeypress);
37
+ if (process.stdin.isTTY) {
38
+ process.stdin.setRawMode(false);
39
+ }
40
+ };
41
+ });
42
+
43
+ // Clean up the listener when the promise is settled
44
+ escapePromise.finally(cleanup);
45
+
46
+ try {
47
+ // Race between the prompt and escape key detection
48
+ const answers = await Promise.race([
49
+ inquirer.prompt(questions),
50
+ escapePromise,
51
+ ]);
52
+ return answers;
53
+ } catch (error) {
54
+ // Handle both Inquirer cancellation and our custom Escape handling
55
+ if (
56
+ error.message === "ESCAPE_PRESSED" ||
57
+ error.isTtyError ||
58
+ error.message.includes("cancelled") ||
59
+ error.name === "ExitPromptError"
60
+ ) {
61
+ console.log("\nOperation cancelled.");
62
+ process.exit(0);
63
+ }
64
+ throw error;
65
+ }
66
+ }