@deveonc/spec-to-code 1.0.0-beta.2
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 +123 -0
- package/bin/lib/agent-metadata.js +57 -0
- package/bin/lib/checklist.js +81 -0
- package/bin/lib/install-config.js +26 -0
- package/bin/lib/rules-manager.js +42 -0
- package/bin/scripts/claude/generate-skills.js +111 -0
- package/bin/scripts/opencode/generate-commands.js +97 -0
- package/bin/scripts/opencode/generate-opencode-agents.js +96 -0
- package/bin/scripts/rules/configure-rules.js +170 -0
- package/bin/spec-to-code.js +258 -0
- package/package.json +37 -0
- package/templates/.ai/agents/architect.agent.md +45 -0
- package/templates/.ai/agents/develop.agent.md +88 -0
- package/templates/.ai/agents/planning.agent.md +165 -0
- package/templates/.ai/agents/reviewer.agent.md +86 -0
- package/templates/.ai/rules/angular.rules.md +318 -0
- package/templates/.ai/rules/default.rules.md +33 -0
- package/templates/.ai/rules/nextjs.rules.md +42 -0
- package/templates/.ai/rules/quarkus.rules.md +39 -0
- package/templates/.ai/rules/react.rules.md +36 -0
- package/templates/.ai/rules/springboot.rules.md +134 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# π spec-to-code
|
|
2
|
+
|
|
3
|
+
β¨ A lightweight npm package that bootstraps a **Spec-to-Code workflow**,
|
|
4
|
+
from any project folder, whether you're starting a new app or adding features/tests,
|
|
5
|
+
using **LLM agents π€** and **rules π**.
|
|
6
|
+
|
|
7
|
+
Inspired by Mattia D'Argenioβs *Spec-to-Code* methodology and the **bmad-method**
|
|
8
|
+
approach to agent-based development.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## π§ Why spec-to-code?
|
|
13
|
+
|
|
14
|
+
Building software with LLMs works **only if itβs structured**.
|
|
15
|
+
|
|
16
|
+
This project helps you move from a vague idea π‘ to production-ready code ποΈ by
|
|
17
|
+
organizing LLM interactions into clear, reproducible steps:
|
|
18
|
+
|
|
19
|
+
β‘οΈ **Specification β Planning β Execution β Review**
|
|
20
|
+
|
|
21
|
+
No prompt spaghetti.
|
|
22
|
+
No magic.
|
|
23
|
+
Just explicit workflows and clearly defined responsibilities.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## β‘ Quick start
|
|
28
|
+
|
|
29
|
+
In a new project, run once:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
npx @deveonc/spec-to-code
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then, in your agent tool:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
/Architect
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
π§ The **Architect agent** will automatically guide you through the specification
|
|
43
|
+
process, one question at a time.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## π¦ What gets installed
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
.ai/
|
|
51
|
+
βββ agents/ π€ Architect, Planning, Develop, Reviewer agents
|
|
52
|
+
βββ rules/ π Coding & architectural rules
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Everything is local, versioned, and auditable.
|
|
56
|
+
|
|
57
|
+
If a `.opencode/` folder exists, the installer generates OpenCode
|
|
58
|
+
commands + agents from `.ai/agents` into `.opencode/commands` and
|
|
59
|
+
`.opencode/agents`.
|
|
60
|
+
|
|
61
|
+
If a `.claude/` folder exists, the installer generates Agent Skills
|
|
62
|
+
from `.ai/agents` into `.claude/skills` (compatible with Claude Code).
|
|
63
|
+
|
|
64
|
+
Installer state is stored in `.ai/config.json` (version, date,
|
|
65
|
+
selected integrations, selected rules).
|
|
66
|
+
|
|
67
|
+
You can regenerate commands, agents, and skills manually:
|
|
68
|
+
```
|
|
69
|
+
node bin/scripts/opencode/generate-commands.js --agents .ai/agents --out .opencode/commands
|
|
70
|
+
node bin/scripts/opencode/generate-opencode-agents.js --agents .ai/agents --out .opencode/agents
|
|
71
|
+
node bin/scripts/claude/generate-skills.js --agents .ai/agents --out .claude/skills
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Script layout (by tool):
|
|
75
|
+
- `bin/scripts/opencode/` - OpenCode commands + agents
|
|
76
|
+
- `bin/scripts/claude/` - Claude Code skills (Agent Skills format)
|
|
77
|
+
- `bin/scripts/rules/` - Rules selection and updates
|
|
78
|
+
- `bin/lib/` - Shared utilities
|
|
79
|
+
|
|
80
|
+
During installation, a checklist is shown for OpenCode/Claude Code
|
|
81
|
+
integration. Existing folders are pre-selected, and you can enable
|
|
82
|
+
or disable each integration before the files are copied. After that,
|
|
83
|
+
you choose which rules to install. Finish by running `/Architect`.
|
|
84
|
+
|
|
85
|
+
On rerun:
|
|
86
|
+
- If the version changed, `.ai` is refreshed (config preserved).
|
|
87
|
+
- If the version is the same, no files are copied unless you change
|
|
88
|
+
integrations or rules.
|
|
89
|
+
|
|
90
|
+
You can also choose which rules are installed. `default.rules.md` is always
|
|
91
|
+
included. To reconfigure later:
|
|
92
|
+
```
|
|
93
|
+
node bin/scripts/rules/configure-rules.js --out .ai/rules
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## π― Designed for
|
|
99
|
+
|
|
100
|
+
- π§βπ» OpenCode users
|
|
101
|
+
- π€ Agent-based LLM workflows
|
|
102
|
+
- ποΈ Developers who want structure, not vibes
|
|
103
|
+
- π°οΈ Long-lived, maintainable projects
|
|
104
|
+
- π§ Compatibility with Claude Code and OpenCode
|
|
105
|
+
|
|
106
|
+
Works for:
|
|
107
|
+
- creating a brand new application from scratch
|
|
108
|
+
- extending an existing codebase with new features or tests
|
|
109
|
+
|
|
110
|
+
When you request new features after the initial spec, route them through
|
|
111
|
+
Planning to add tasks to `docs/todo.md` before any implementation.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## π§© Philosophy
|
|
116
|
+
|
|
117
|
+
LLMs are powerful β but only when constrained.
|
|
118
|
+
|
|
119
|
+
**spec-to-code** treats prompts as **contracts**,
|
|
120
|
+
agents as **roles**,
|
|
121
|
+
and development as a **workflow**, not a conversation.
|
|
122
|
+
|
|
123
|
+
Happy building π
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const toTitle = (value) => value.charAt(0).toUpperCase() + value.slice(1);
|
|
5
|
+
|
|
6
|
+
const parseFrontmatter = (lines) => {
|
|
7
|
+
if (lines.length === 0 || lines[0].trim() !== "---") {
|
|
8
|
+
return { metadata: {}, body: lines.join("\n") };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const endIndex = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
12
|
+
if (endIndex === -1) {
|
|
13
|
+
return { metadata: {}, body: lines.join("\n") };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const metadataLines = lines.slice(1, endIndex);
|
|
17
|
+
const body = lines.slice(endIndex + 1).join("\n").replace(/^\n+/u, "");
|
|
18
|
+
const metadata = {};
|
|
19
|
+
|
|
20
|
+
for (const line of metadataLines) {
|
|
21
|
+
const separatorIndex = line.indexOf(":");
|
|
22
|
+
if (separatorIndex === -1) continue;
|
|
23
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
24
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
25
|
+
if (key) {
|
|
26
|
+
metadata[key] = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { metadata, body };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const readAgentMetadata = (agentPath) => {
|
|
34
|
+
const content = fs.readFileSync(agentPath, "utf8");
|
|
35
|
+
const { metadata, body } = parseFrontmatter(content.split(/\r?\n/u));
|
|
36
|
+
const baseName = path.basename(agentPath, ".agent.md");
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name: metadata.name ?? baseName,
|
|
40
|
+
description: metadata.description ?? `Agent instructions for ${baseName}.`,
|
|
41
|
+
command: metadata.command ?? toTitle(baseName),
|
|
42
|
+
body,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const readAllAgentMetadata = (agentsDir) => {
|
|
47
|
+
if (!fs.existsSync(agentsDir)) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return fs
|
|
52
|
+
.readdirSync(agentsDir, { withFileTypes: true })
|
|
53
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".agent.md"))
|
|
54
|
+
.map((entry) => readAgentMetadata(path.join(agentsDir, entry.name)));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export { readAgentMetadata, readAllAgentMetadata, toTitle };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import readline from "readline";
|
|
2
|
+
|
|
3
|
+
const askQuestion = (prompt) => new Promise((resolve) => {
|
|
4
|
+
const rl = readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
rl.question(prompt, (answer) => {
|
|
10
|
+
rl.close();
|
|
11
|
+
resolve(answer);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const renderChecklist = (title, items) => {
|
|
16
|
+
if (title) {
|
|
17
|
+
console.log(title);
|
|
18
|
+
}
|
|
19
|
+
items.forEach((item, index) => {
|
|
20
|
+
const marker = item.checked ? "[x]" : "[ ]";
|
|
21
|
+
console.log(` ${marker} ${index + 1}. ${item.label}`);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const parseToggleInput = (input) => new Set(
|
|
26
|
+
input
|
|
27
|
+
.split(/[\s,]+/u)
|
|
28
|
+
.map((value) => Number.parseInt(value.trim(), 10))
|
|
29
|
+
.filter((value) => Number.isInteger(value) && value > 0),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const promptChecklist = async ({ title, prompt, items }) => {
|
|
33
|
+
renderChecklist(title, items);
|
|
34
|
+
|
|
35
|
+
const answer = await askQuestion(prompt);
|
|
36
|
+
if (!answer.trim()) {
|
|
37
|
+
return items.map((item) => item.checked);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const toggles = parseToggleInput(answer);
|
|
41
|
+
|
|
42
|
+
return items.map((item, index) => {
|
|
43
|
+
if (toggles.has(index + 1)) {
|
|
44
|
+
return !item.checked;
|
|
45
|
+
}
|
|
46
|
+
return item.checked;
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ensureSelection = async ({
|
|
51
|
+
title,
|
|
52
|
+
prompt,
|
|
53
|
+
items,
|
|
54
|
+
selections,
|
|
55
|
+
fallbackSelections,
|
|
56
|
+
}) => {
|
|
57
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
58
|
+
if (selections.some(Boolean)) {
|
|
59
|
+
return selections;
|
|
60
|
+
}
|
|
61
|
+
if (fallbackSelections) {
|
|
62
|
+
console.warn("β οΈ No selection; using defaults.");
|
|
63
|
+
return fallbackSelections;
|
|
64
|
+
}
|
|
65
|
+
throw new Error("No selection provided.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let current = selections;
|
|
69
|
+
while (!current.some(Boolean)) {
|
|
70
|
+
console.warn("β οΈ Select at least one option to continue.");
|
|
71
|
+
const nextItems = items.map((item, index) => ({
|
|
72
|
+
...item,
|
|
73
|
+
checked: current[index],
|
|
74
|
+
}));
|
|
75
|
+
current = await promptChecklist({ title, prompt, items: nextItems });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return current;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export { ensureSelection, promptChecklist };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
|
|
3
|
+
const readInstallConfig = (configPath) => {
|
|
4
|
+
if (!fs.existsSync(configPath)) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
9
|
+
if (!raw.trim()) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.warn("β οΈ Skipped: .ai/config.json (invalid JSON)");
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const writeInstallConfig = (configPath, config) => {
|
|
22
|
+
const content = `${JSON.stringify(config, null, 4)}\n`;
|
|
23
|
+
fs.writeFileSync(configPath, content, "utf8");
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { readInstallConfig, writeInstallConfig };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const listRuleFiles = (sourceDir) => {
|
|
5
|
+
if (!fs.existsSync(sourceDir)) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return fs
|
|
10
|
+
.readdirSync(sourceDir, { withFileTypes: true })
|
|
11
|
+
.filter((entry) => entry.isFile())
|
|
12
|
+
.map((entry) => entry.name)
|
|
13
|
+
.sort((left, right) => left.localeCompare(right));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const applyRules = ({ sourceDir, targetDir, selected }) => {
|
|
17
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
const available = listRuleFiles(sourceDir);
|
|
20
|
+
const selectedSet = new Set(selected);
|
|
21
|
+
const results = { copied: [], removed: [] };
|
|
22
|
+
|
|
23
|
+
for (const ruleName of available) {
|
|
24
|
+
const srcFile = path.join(sourceDir, ruleName);
|
|
25
|
+
const dstFile = path.join(targetDir, ruleName);
|
|
26
|
+
|
|
27
|
+
if (selectedSet.has(ruleName)) {
|
|
28
|
+
fs.copyFileSync(srcFile, dstFile);
|
|
29
|
+
results.copied.push(ruleName);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(dstFile)) {
|
|
34
|
+
fs.unlinkSync(dstFile);
|
|
35
|
+
results.removed.push(ruleName);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return results;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { applyRules, listRuleFiles };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
|
|
6
|
+
import { readAgentMetadata, toTitle } from "../../lib/agent-metadata.js";
|
|
7
|
+
|
|
8
|
+
const buildSkillContent = (name, description, agentContent) => {
|
|
9
|
+
const title = toTitle(name);
|
|
10
|
+
const trimmed = agentContent.trimEnd();
|
|
11
|
+
|
|
12
|
+
return [
|
|
13
|
+
"---",
|
|
14
|
+
`name: ${name}`,
|
|
15
|
+
`description: ${description}`,
|
|
16
|
+
"---",
|
|
17
|
+
"",
|
|
18
|
+
`# ${title}`,
|
|
19
|
+
"",
|
|
20
|
+
trimmed,
|
|
21
|
+
"",
|
|
22
|
+
].join("\n");
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const parseArgs = (args) => {
|
|
26
|
+
const options = {};
|
|
27
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
28
|
+
const value = args[index];
|
|
29
|
+
if (value === "--agents" && args[index + 1]) {
|
|
30
|
+
options.agentsDir = args[index + 1];
|
|
31
|
+
index += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (value === "--out" && args[index + 1]) {
|
|
35
|
+
options.skillsDir = args[index + 1];
|
|
36
|
+
index += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return options;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const resolvePath = (value) => path.resolve(process.cwd(), value);
|
|
43
|
+
|
|
44
|
+
const generateSkills = ({ agentsDir, skillsDir }) => {
|
|
45
|
+
if (!fs.existsSync(agentsDir)) {
|
|
46
|
+
throw new Error(`Missing agents directory: ${agentsDir}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
const results = { created: [], skipped: [] };
|
|
52
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (!entry.isFile() || !entry.name.endsWith(".agent.md")) continue;
|
|
55
|
+
|
|
56
|
+
const agentPath = path.join(agentsDir, entry.name);
|
|
57
|
+
const metadata = readAgentMetadata(agentPath);
|
|
58
|
+
const skillPath = path.join(skillsDir, metadata.name, "SKILL.md");
|
|
59
|
+
|
|
60
|
+
if (fs.existsSync(skillPath)) {
|
|
61
|
+
results.skipped.push(metadata.name);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const skillContent = buildSkillContent(
|
|
66
|
+
metadata.name,
|
|
67
|
+
metadata.description,
|
|
68
|
+
metadata.body,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
fs.mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
72
|
+
fs.writeFileSync(skillPath, skillContent, "utf8");
|
|
73
|
+
results.created.push(metadata.name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return results;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const run = () => {
|
|
80
|
+
const { agentsDir, skillsDir } = parseArgs(process.argv.slice(2));
|
|
81
|
+
const resolvedAgentsDir = resolvePath(
|
|
82
|
+
agentsDir ?? path.join(process.cwd(), ".ai", "agents"),
|
|
83
|
+
);
|
|
84
|
+
const resolvedSkillsDir = resolvePath(
|
|
85
|
+
skillsDir ?? path.join(process.cwd(), ".claude", "skills"),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const { created, skipped } = generateSkills({
|
|
89
|
+
agentsDir: resolvedAgentsDir,
|
|
90
|
+
skillsDir: resolvedSkillsDir,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log(`β
Skills generated in ${resolvedSkillsDir}`);
|
|
94
|
+
if (created.length > 0) {
|
|
95
|
+
console.log(`β
Created: ${created.join(", ")}`);
|
|
96
|
+
}
|
|
97
|
+
if (skipped.length > 0) {
|
|
98
|
+
console.log(`β οΈ Skipped: ${skipped.join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
103
|
+
try {
|
|
104
|
+
run();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(error);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { generateSkills };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
|
|
6
|
+
import { readAgentMetadata } from "../../lib/agent-metadata.js";
|
|
7
|
+
|
|
8
|
+
const parseArgs = (args) => {
|
|
9
|
+
const options = {};
|
|
10
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
11
|
+
const value = args[index];
|
|
12
|
+
if (value === "--agents" && args[index + 1]) {
|
|
13
|
+
options.agentsDir = args[index + 1];
|
|
14
|
+
index += 1;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (value === "--out" && args[index + 1]) {
|
|
18
|
+
options.commandsDir = args[index + 1];
|
|
19
|
+
index += 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return options;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const resolvePath = (value) => path.resolve(process.cwd(), value);
|
|
26
|
+
|
|
27
|
+
const buildCommandContent = (description, agentFileName) => [
|
|
28
|
+
"---",
|
|
29
|
+
`description: ${description}`,
|
|
30
|
+
"---",
|
|
31
|
+
`@.ai/agents/${agentFileName}`,
|
|
32
|
+
"",
|
|
33
|
+
].join("\n");
|
|
34
|
+
|
|
35
|
+
const generateCommands = ({ agentsDir, commandsDir }) => {
|
|
36
|
+
if (!fs.existsSync(agentsDir)) {
|
|
37
|
+
throw new Error(`Missing agents directory: ${agentsDir}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
41
|
+
|
|
42
|
+
const results = { created: [], skipped: [] };
|
|
43
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (!entry.isFile() || !entry.name.endsWith(".agent.md")) continue;
|
|
46
|
+
|
|
47
|
+
const agentPath = path.join(agentsDir, entry.name);
|
|
48
|
+
const metadata = readAgentMetadata(agentPath);
|
|
49
|
+
const commandFileName = `${metadata.command}.md`;
|
|
50
|
+
const commandPath = path.join(commandsDir, commandFileName);
|
|
51
|
+
|
|
52
|
+
if (fs.existsSync(commandPath)) {
|
|
53
|
+
results.skipped.push(metadata.command);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const commandContent = buildCommandContent(metadata.description, entry.name);
|
|
58
|
+
fs.writeFileSync(commandPath, commandContent, "utf8");
|
|
59
|
+
results.created.push(metadata.command);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return results;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const run = () => {
|
|
66
|
+
const { agentsDir, commandsDir } = parseArgs(process.argv.slice(2));
|
|
67
|
+
const resolvedAgentsDir = resolvePath(
|
|
68
|
+
agentsDir ?? path.join(process.cwd(), ".ai", "agents"),
|
|
69
|
+
);
|
|
70
|
+
const resolvedCommandsDir = resolvePath(
|
|
71
|
+
commandsDir ?? path.join(process.cwd(), ".opencode", "commands"),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const { created, skipped } = generateCommands({
|
|
75
|
+
agentsDir: resolvedAgentsDir,
|
|
76
|
+
commandsDir: resolvedCommandsDir,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log(`β
Commands generated in ${resolvedCommandsDir}`);
|
|
80
|
+
if (created.length > 0) {
|
|
81
|
+
console.log(`β
Created: ${created.join(", ")}`);
|
|
82
|
+
}
|
|
83
|
+
if (skipped.length > 0) {
|
|
84
|
+
console.log(`β οΈ Skipped: ${skipped.join(", ")}`);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
89
|
+
try {
|
|
90
|
+
run();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(error);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { generateCommands };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
|
|
6
|
+
import { readAgentMetadata } from "../../lib/agent-metadata.js";
|
|
7
|
+
|
|
8
|
+
const parseArgs = (args) => {
|
|
9
|
+
const options = {};
|
|
10
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
11
|
+
const value = args[index];
|
|
12
|
+
if (value === "--agents" && args[index + 1]) {
|
|
13
|
+
options.agentsDir = args[index + 1];
|
|
14
|
+
index += 1;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (value === "--out" && args[index + 1]) {
|
|
18
|
+
options.outDir = args[index + 1];
|
|
19
|
+
index += 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return options;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const resolvePath = (value) => path.resolve(process.cwd(), value);
|
|
26
|
+
|
|
27
|
+
const buildAgentContent = (description, body) => [
|
|
28
|
+
"---",
|
|
29
|
+
`description: ${description}`,
|
|
30
|
+
"---",
|
|
31
|
+
body.trimEnd(),
|
|
32
|
+
"",
|
|
33
|
+
].join("\n");
|
|
34
|
+
|
|
35
|
+
const generateOpencodeAgents = ({ agentsDir, outDir }) => {
|
|
36
|
+
if (!fs.existsSync(agentsDir)) {
|
|
37
|
+
throw new Error(`Missing agents directory: ${agentsDir}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
41
|
+
|
|
42
|
+
const results = { created: [], skipped: [] };
|
|
43
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (!entry.isFile() || !entry.name.endsWith(".agent.md")) continue;
|
|
46
|
+
|
|
47
|
+
const agentPath = path.join(agentsDir, entry.name);
|
|
48
|
+
const metadata = readAgentMetadata(agentPath);
|
|
49
|
+
const outPath = path.join(outDir, `${metadata.name}.md`);
|
|
50
|
+
|
|
51
|
+
if (fs.existsSync(outPath)) {
|
|
52
|
+
results.skipped.push(metadata.name);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const content = buildAgentContent(metadata.description, metadata.body);
|
|
57
|
+
fs.writeFileSync(outPath, content, "utf8");
|
|
58
|
+
results.created.push(metadata.name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return results;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const run = () => {
|
|
65
|
+
const { agentsDir, outDir } = parseArgs(process.argv.slice(2));
|
|
66
|
+
const resolvedAgentsDir = resolvePath(
|
|
67
|
+
agentsDir ?? path.join(process.cwd(), ".ai", "agents"),
|
|
68
|
+
);
|
|
69
|
+
const resolvedOutDir = resolvePath(
|
|
70
|
+
outDir ?? path.join(process.cwd(), ".opencode", "agents"),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const { created, skipped } = generateOpencodeAgents({
|
|
74
|
+
agentsDir: resolvedAgentsDir,
|
|
75
|
+
outDir: resolvedOutDir,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(`β
OpenCode agents generated in ${resolvedOutDir}`);
|
|
79
|
+
if (created.length > 0) {
|
|
80
|
+
console.log(`β
Created: ${created.join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
if (skipped.length > 0) {
|
|
83
|
+
console.log(`β οΈ Skipped: ${skipped.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
88
|
+
try {
|
|
89
|
+
run();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(error);
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { generateOpencodeAgents };
|