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,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
|
+
}
|