ai-slash-commands 2026.1.3
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/README.md +71 -0
- package/package.json +13 -0
- package/prompts/commit.md +6 -0
- package/scripts/cli.mjs +78 -0
- package/scripts/gen.mjs +84 -0
- package/scripts/install.mjs +93 -0
- package/scripts/link-windsurf.mjs +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ai-slash-commands
|
|
2
|
+
|
|
3
|
+
Один набор markdown-промптов в `./prompts/*.md`, генерация в `./dist/**` и установка в домашние папки для:
|
|
4
|
+
- Claude Code
|
|
5
|
+
- Cursor
|
|
6
|
+
- Windsurf (через линк в текущий workspace)
|
|
7
|
+
- Codex (custom prompts)
|
|
8
|
+
|
|
9
|
+
## Почему так
|
|
10
|
+
- Источник истины - только prompt (markdown).
|
|
11
|
+
- `id`/имя команды вычисляется из имени файла (`prompts/<name>.md`).
|
|
12
|
+
- Всё сгенерированное лежит только в `dist/`.
|
|
13
|
+
|
|
14
|
+
## Требования
|
|
15
|
+
- Node.js 18+ (Windows / Ubuntu)
|
|
16
|
+
|
|
17
|
+
## Быстрый старт
|
|
18
|
+
1) Добавь промпты в `prompts/*.md`
|
|
19
|
+
|
|
20
|
+
2) Сгенерируй в dist:
|
|
21
|
+
```bash
|
|
22
|
+
npm run gen
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3) Установи в домашние папки:
|
|
26
|
+
```bash
|
|
27
|
+
npm run install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## NPX
|
|
31
|
+
Можно установить команды из любой папки с `*.md` файлами:
|
|
32
|
+
```bash
|
|
33
|
+
npx ai-slash-commands ./path/to/commands
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Опционально можно ограничить список целей:
|
|
37
|
+
```bash
|
|
38
|
+
npx ai-slash-commands ./path/to/commands --targets claude,cursor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Windsurf: важный момент
|
|
42
|
+
Официально workflows подхватываются из `.windsurf/workflows` внутри workspace.
|
|
43
|
+
Глобальная папка workflows в home в доках не описана, поэтому тут используется компромисс:
|
|
44
|
+
- хранение в `~/.windsurf/workflows`
|
|
45
|
+
- линк в конкретный репозиторий/папку workspace
|
|
46
|
+
|
|
47
|
+
Сделать линк для текущей папки:
|
|
48
|
+
```bash
|
|
49
|
+
npm run link:windsurf
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Скрипты
|
|
53
|
+
- `npm run gen` - копирует `prompts/*.md` в:
|
|
54
|
+
- `dist/claude/commands/*.md`
|
|
55
|
+
- `dist/cursor/commands/*.md`
|
|
56
|
+
- `dist/windsurf/workflows/*.md`
|
|
57
|
+
- `dist/codex/prompts/*.md`
|
|
58
|
+
|
|
59
|
+
- `npm run install` - копирует из `dist/**` в:
|
|
60
|
+
- `~/.claude/commands`
|
|
61
|
+
- `~/.cursor/commands`
|
|
62
|
+
- `~/.windsurf/workflows` (хранилище, дальше линк)
|
|
63
|
+
- `${CODEX_HOME:-~/.codex}/prompts`
|
|
64
|
+
|
|
65
|
+
- `npm run link:windsurf` - делает `.windsurf/workflows` -> `~/.windsurf/workflows` (symlink/junction)
|
|
66
|
+
|
|
67
|
+
## Примечания по папкам (ссылки на доки)
|
|
68
|
+
- Claude Code personal commands: `~/.claude/commands`
|
|
69
|
+
- Cursor global commands: `~/.cursor/commands`
|
|
70
|
+
- Codex custom prompts: `~/.codex/prompts` (или `$CODEX_HOME/prompts`)
|
|
71
|
+
- Windsurf workflows: `.windsurf/workflows` (workspace-level)
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-slash-commands",
|
|
3
|
+
"version": "2026.1.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ai-slash-commands": "scripts/cli.mjs"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"gen": "node scripts/gen.mjs",
|
|
10
|
+
"install": "node scripts/install.mjs",
|
|
11
|
+
"link:windsurf": "node scripts/link-windsurf.mjs"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Собери один аккуратный коммит по текущим изменениям.
|
|
2
|
+
|
|
3
|
+
Требования:
|
|
4
|
+
- Один коммит, не несколько.
|
|
5
|
+
- Сообщение коммита короткое, но информативное. Angular commit message style: feat(component), fix, test, docs, refactor, style, chore.
|
|
6
|
+
- Если есть тесты - запусти их.
|
package/scripts/cli.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { generate } from "./gen.mjs";
|
|
5
|
+
import { install } from "./install.mjs";
|
|
6
|
+
|
|
7
|
+
function parseArgs() {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
let targetsRaw = null;
|
|
10
|
+
let commandsDir = null;
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
13
|
+
const a = args[i];
|
|
14
|
+
if (a === "--targets") {
|
|
15
|
+
targetsRaw = args[i + 1];
|
|
16
|
+
i += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (a.startsWith("-")) {
|
|
20
|
+
console.error(`Unknown option: ${a}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
if (commandsDir) {
|
|
24
|
+
console.error("Only one commands directory is allowed.");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
commandsDir = a;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!commandsDir) {
|
|
31
|
+
console.error("Usage: ai-slash-commands <commands-dir> [--targets claude,cursor,windsurf,codex]");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const targets = (targetsRaw ?? "claude,cursor,windsurf,codex")
|
|
36
|
+
.split(",")
|
|
37
|
+
.map(s => s.trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
|
|
40
|
+
return { commandsDir: path.resolve(commandsDir), targets };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function ensureCommandsDir(commandsDir) {
|
|
44
|
+
let stat;
|
|
45
|
+
try {
|
|
46
|
+
stat = await fs.stat(commandsDir);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err.code === "ENOENT") {
|
|
49
|
+
console.error(`Commands directory not found: ${commandsDir}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!stat.isDirectory()) {
|
|
56
|
+
console.error(`Not a directory: ${commandsDir}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const entries = await fs.readdir(commandsDir, { withFileTypes: true });
|
|
61
|
+
const hasMd = entries.some(e => e.isFile() && e.name.toLowerCase().endsWith(".md"));
|
|
62
|
+
if (!hasMd) {
|
|
63
|
+
console.error(`No .md files found in ${commandsDir}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
const { commandsDir, targets } = parseArgs();
|
|
70
|
+
await ensureCommandsDir(commandsDir);
|
|
71
|
+
await generate({ targets, promptsDir: commandsDir });
|
|
72
|
+
await install({ targets });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main().catch((err) => {
|
|
76
|
+
console.error(err);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
package/scripts/gen.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
9
|
+
const defaultPromptsDir = path.join(repoRoot, "prompts");
|
|
10
|
+
const distDir = path.join(repoRoot, "dist");
|
|
11
|
+
|
|
12
|
+
const TARGETS = {
|
|
13
|
+
claude: { out: "claude/commands" },
|
|
14
|
+
cursor: { out: "cursor/commands" },
|
|
15
|
+
windsurf: { out: "windsurf/workflows" },
|
|
16
|
+
codex: { out: "codex/prompts" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function parseArgs() {
|
|
20
|
+
const idxTargets = process.argv.indexOf("--targets");
|
|
21
|
+
const rawTargets = idxTargets >= 0 ? process.argv[idxTargets + 1] : "claude,cursor,windsurf,codex";
|
|
22
|
+
const targets = rawTargets.split(",").map(s => s.trim()).filter(Boolean);
|
|
23
|
+
|
|
24
|
+
const idxSrc = process.argv.indexOf("--src");
|
|
25
|
+
const rawSrc = idxSrc >= 0 ? process.argv[idxSrc + 1] : null;
|
|
26
|
+
|
|
27
|
+
return { targets, promptsDir: rawSrc ? path.resolve(rawSrc) : defaultPromptsDir };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function ensureDir(p) {
|
|
31
|
+
await fs.mkdir(p, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function listPromptFiles(promptsDir) {
|
|
35
|
+
const entries = await fs.readdir(promptsDir, { withFileTypes: true });
|
|
36
|
+
return entries
|
|
37
|
+
.filter(e => e.isFile() && e.name.toLowerCase().endsWith(".md"))
|
|
38
|
+
.map(e => e.name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function generate({ targets, promptsDir }) {
|
|
42
|
+
const promptFiles = await listPromptFiles(promptsDir);
|
|
43
|
+
if (promptFiles.length === 0) {
|
|
44
|
+
console.error(`No prompts found in ${promptsDir} (expected *.md).`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const t of targets) {
|
|
49
|
+
if (!TARGETS[t]) {
|
|
50
|
+
console.error(`Unknown target: ${t}. Allowed: ${Object.keys(TARGETS).join(", ")}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
await ensureDir(path.join(distDir, TARGETS[t].out));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const name of promptFiles) {
|
|
57
|
+
const srcPath = path.join(promptsDir, name);
|
|
58
|
+
const content = await fs.readFile(srcPath, "utf8");
|
|
59
|
+
|
|
60
|
+
for (const t of targets) {
|
|
61
|
+
const outPath = path.join(distDir, TARGETS[t].out, name);
|
|
62
|
+
await fs.writeFile(outPath, content.endsWith("\n") ? content : content + "\n", "utf8");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log("Generated:");
|
|
67
|
+
for (const t of targets) {
|
|
68
|
+
console.log(`- dist/${TARGETS[t].out}/`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
const { targets, promptsDir } = parseArgs();
|
|
74
|
+
|
|
75
|
+
await generate({ targets, promptsDir });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
79
|
+
if (isMain) {
|
|
80
|
+
main().catch((err) => {
|
|
81
|
+
console.error(err);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const distDir = path.join(repoRoot, "dist");
|
|
11
|
+
|
|
12
|
+
const home = os.homedir();
|
|
13
|
+
|
|
14
|
+
// Install locations (home-based)
|
|
15
|
+
const DEST = {
|
|
16
|
+
claude: path.join(home, ".claude", "commands"),
|
|
17
|
+
cursor: path.join(home, ".cursor", "commands"),
|
|
18
|
+
// Windsurf does not (currently) document a global workflows directory.
|
|
19
|
+
// We install to ~/.windsurf/workflows and provide a separate link script
|
|
20
|
+
// to link this folder into the current workspace as .windsurf/workflows.
|
|
21
|
+
windsurf: path.join(home, ".windsurf", "workflows"),
|
|
22
|
+
// Codex supports CODEX_HOME (defaults to ~/.codex). Prompts live under $CODEX_HOME/prompts.
|
|
23
|
+
codex: path.join(process.env.CODEX_HOME ?? path.join(home, ".codex"), "prompts"),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const SRC = {
|
|
27
|
+
claude: path.join(distDir, "claude", "commands"),
|
|
28
|
+
cursor: path.join(distDir, "cursor", "commands"),
|
|
29
|
+
windsurf: path.join(distDir, "windsurf", "workflows"),
|
|
30
|
+
codex: path.join(distDir, "codex", "prompts"),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function ensureDir(p) {
|
|
34
|
+
await fs.mkdir(p, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function listMd(dir) {
|
|
38
|
+
try {
|
|
39
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
40
|
+
return entries
|
|
41
|
+
.filter(e => e.isFile() && e.name.toLowerCase().endsWith(".md"))
|
|
42
|
+
.map(e => e.name);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err.code === 'ENOENT') {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function copyAll(srcDir, dstDir) {
|
|
52
|
+
await ensureDir(dstDir);
|
|
53
|
+
|
|
54
|
+
const files = await listMd(srcDir);
|
|
55
|
+
for (const f of files) {
|
|
56
|
+
await fs.copyFile(path.join(srcDir, f), path.join(dstDir, f));
|
|
57
|
+
}
|
|
58
|
+
return files.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseArgs() {
|
|
62
|
+
const idx = process.argv.indexOf("--targets");
|
|
63
|
+
const raw = idx >= 0 ? process.argv[idx + 1] : "claude,cursor,windsurf,codex";
|
|
64
|
+
const targets = raw.split(",").map(s => s.trim()).filter(Boolean);
|
|
65
|
+
return { targets };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function install({ targets }) {
|
|
69
|
+
for (const t of targets) {
|
|
70
|
+
if (!SRC[t] || !DEST[t]) {
|
|
71
|
+
console.error(`Unknown target: ${t}. Allowed: ${Object.keys(SRC).join(", ")}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const t of targets) {
|
|
77
|
+
const count = await copyAll(SRC[t], DEST[t]);
|
|
78
|
+
console.log(`${t}: installed ${count} prompt(s) -> ${DEST[t]}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const { targets } = parseArgs();
|
|
84
|
+
await install({ targets });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
88
|
+
if (isMain) {
|
|
89
|
+
main().catch((err) => {
|
|
90
|
+
console.error(err);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
const home = os.homedir();
|
|
6
|
+
const globalWorkflows = path.join(home, ".windsurf", "workflows");
|
|
7
|
+
|
|
8
|
+
const workspaceRoot = process.cwd();
|
|
9
|
+
const workspaceWindsurfDir = path.join(workspaceRoot, ".windsurf");
|
|
10
|
+
const workspaceWorkflows = path.join(workspaceWindsurfDir, "workflows");
|
|
11
|
+
|
|
12
|
+
async function ensureDir(p) {
|
|
13
|
+
await fs.mkdir(p, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function exists(p) {
|
|
17
|
+
try { await fs.lstat(p); return true; } catch { return false; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
await ensureDir(globalWorkflows);
|
|
22
|
+
await ensureDir(workspaceWindsurfDir);
|
|
23
|
+
|
|
24
|
+
if (await exists(workspaceWorkflows)) {
|
|
25
|
+
console.error(`Already exists: ${workspaceWorkflows}`);
|
|
26
|
+
console.error("Remove it first if you want to re-link.");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// On Windows, use 'junction' to avoid admin rights requirement for symlinks.
|
|
31
|
+
const type = process.platform === "win32" ? "junction" : "dir";
|
|
32
|
+
await fs.symlink(globalWorkflows, workspaceWorkflows, type);
|
|
33
|
+
|
|
34
|
+
console.log("Linked Windsurf workflows:");
|
|
35
|
+
console.log(`- global: ${globalWorkflows}`);
|
|
36
|
+
console.log(`- workspace:${workspaceWorkflows}`);
|
|
37
|
+
console.log("Now the workspace will see workflows from your home folder.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main().catch((err) => {
|
|
41
|
+
console.error(err);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|