cdspec 0.1.1 → 0.1.4
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/dist/cli.js +1 -40
- package/dist/config/default.js +1 -48
- package/dist/config/loader.js +1 -30
- package/dist/config/path.js +1 -11
- package/dist/config/types.js +1 -1
- package/dist/skill-core/adapters/claudecode-adapter.js +1 -35
- package/dist/skill-core/adapters/codex-adapter.js +1 -28
- package/dist/skill-core/adapters/iflow-adapter.js +1 -39
- package/dist/skill-core/adapters/index.js +1 -34
- package/dist/skill-core/adapters/shared.js +1 -36
- package/dist/skill-core/manifest-loader.js +1 -60
- package/dist/skill-core/scaffold.js +1 -169
- package/dist/skill-core/service.js +1 -109
- package/dist/skill-core/tool-interactions.js +1 -70
- package/dist/skill-core/types.js +1 -1
- package/dist/skill-core/validator.js +1 -25
- package/dist/utils/frontmatter.js +1 -40
- package/dist/utils/fs.js +1 -37
- package/package.json +11 -2
- package/templates/{standards-backend → backend-standard}/SKILL.md +55 -55
- package/templates/backend-standard/agents/openai.yaml +4 -0
- package/templates/{standards-backend → backend-standard}/references/DDD/346/236/266/346/236/204/347/272/246/346/235/237.md +103 -103
- package/templates/{standards-backend → backend-standard}/references/JUC/345/271/266/345/217/221/350/247/204/350/214/203.md +232 -232
- package/templates/{standards-backend → backend-standard}/references//344/274/240/347/273/237/344/270/211/345/261/202/346/236/266/346/236/204/347/272/246/346/235/237.md +35 -35
- package/templates/{standards-backend → backend-standard}/references//345/220/216/347/253/257/345/274/200/345/217/221/350/247/204/350/214/203.md +49 -49
- package/templates/{standards-backend → backend-standard}/references//346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/350/247/204/350/214/203.md +116 -116
- package/templates/{standards-backend → backend-standard}/references//350/256/276/350/256/241/346/250/241/345/274/217/350/220/275/345/234/260/346/211/213/345/206/214.md +395 -395
- package/templates/{frontend-develop-standard → frontend-standard}/SKILL.md +63 -63
- package/templates/frontend-standard/agents/openai.yaml +4 -0
- package/templates/{frontend-develop-standard/references/frontend_develop_standard.md → frontend-standard/references/frontend_standard.md} +28 -321
- package/cdspec.config.yaml +0 -34
- package/dist/skill-core/agent-config.js +0 -40
- package/dist/task-core/parser.js +0 -70
- package/dist/task-core/service.js +0 -28
- package/dist/task-core/storage.js +0 -159
- package/dist/task-core/types.js +0 -1
- package/src/cli.ts +0 -44
- package/src/config/default.ts +0 -51
- package/src/config/loader.ts +0 -37
- package/src/config/path.ts +0 -13
- package/src/config/types.ts +0 -22
- package/src/skill-core/adapters/claudecode-adapter.ts +0 -45
- package/src/skill-core/adapters/codex-adapter.ts +0 -36
- package/src/skill-core/adapters/iflow-adapter.ts +0 -49
- package/src/skill-core/adapters/index.ts +0 -39
- package/src/skill-core/adapters/shared.ts +0 -45
- package/src/skill-core/manifest-loader.ts +0 -72
- package/src/skill-core/scaffold.ts +0 -192
- package/src/skill-core/service.ts +0 -137
- package/src/skill-core/tool-interactions.ts +0 -95
- package/src/skill-core/types.ts +0 -22
- package/src/skill-core/validator.ts +0 -28
- package/src/types/yaml.d.ts +0 -4
- package/src/utils/frontmatter.ts +0 -55
- package/src/utils/fs.ts +0 -41
- package/templates/frontend-develop-standard/agents/openai.yaml +0 -4
- package/templates/standards-backend/agents/openai.yaml +0 -4
- package/tests/init.test.ts +0 -63
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -9
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
4
|
-
import { listDirs, pathExists } from "../utils/fs.js";
|
|
5
|
-
import { SkillManifest } from "./types.js";
|
|
6
|
-
|
|
7
|
-
const CODEX_DIR = ".codex";
|
|
8
|
-
|
|
9
|
-
export async function loadAllSkillManifests(cwd: string): Promise<SkillManifest[]> {
|
|
10
|
-
const sourceRoot = path.join(cwd, CODEX_DIR);
|
|
11
|
-
const skillDirs = await listDirs(sourceRoot);
|
|
12
|
-
const manifests: SkillManifest[] = [];
|
|
13
|
-
for (const dirName of skillDirs) {
|
|
14
|
-
const fullPath = path.join(sourceRoot, dirName);
|
|
15
|
-
const manifest = await loadSkillManifest(fullPath);
|
|
16
|
-
if (manifest) manifests.push(manifest);
|
|
17
|
-
}
|
|
18
|
-
return manifests;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function loadSkillManifest(skillDir: string): Promise<SkillManifest | null> {
|
|
22
|
-
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
23
|
-
if (!(await pathExists(skillMdPath))) return null;
|
|
24
|
-
|
|
25
|
-
const content = await readFile(skillMdPath, "utf8");
|
|
26
|
-
const parsed = parseFrontmatter(content);
|
|
27
|
-
const frontName = parsed.attributes.name?.trim();
|
|
28
|
-
const frontDescription = parsed.attributes.description?.trim();
|
|
29
|
-
const displayName = frontName || path.basename(skillDir);
|
|
30
|
-
|
|
31
|
-
const agents = await collectRelativeFiles(path.join(skillDir, "agents"), "agents");
|
|
32
|
-
const resources = await collectRelativeFiles(path.join(skillDir, "references"), "references");
|
|
33
|
-
const scripts = await collectRelativeFiles(path.join(skillDir, "scripts"), "scripts");
|
|
34
|
-
const assets = await collectRelativeFiles(path.join(skillDir, "assets"), "assets");
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
name: displayName,
|
|
38
|
-
description: frontDescription || "",
|
|
39
|
-
body: parsed.body.trim(),
|
|
40
|
-
agents,
|
|
41
|
-
resources: [...resources, ...scripts, ...assets],
|
|
42
|
-
sourcePath: skillDir
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function collectRelativeFiles(
|
|
47
|
-
dirPath: string,
|
|
48
|
-
prefix: string
|
|
49
|
-
): Promise<string[]> {
|
|
50
|
-
if (!(await pathExists(dirPath))) return [];
|
|
51
|
-
const result: string[] = [];
|
|
52
|
-
await walk(dirPath, (absolutePath) => {
|
|
53
|
-
const relative = path.relative(path.dirname(dirPath), absolutePath);
|
|
54
|
-
result.push(relative.replaceAll("\\", "/").replace(`${prefix}/./`, `${prefix}/`));
|
|
55
|
-
});
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function walk(
|
|
60
|
-
dirPath: string,
|
|
61
|
-
onFile: (absolutePath: string) => void
|
|
62
|
-
): Promise<void> {
|
|
63
|
-
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
64
|
-
for (const entry of entries) {
|
|
65
|
-
const absolute = path.join(dirPath, entry.name);
|
|
66
|
-
if (entry.isDirectory()) {
|
|
67
|
-
await walk(absolute, onFile);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
onFile(absolute);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
6
|
-
import { ensureDir, listDirs, pathExists } from "../utils/fs.js";
|
|
7
|
-
import { SkillManifest } from "./types.js";
|
|
8
|
-
|
|
9
|
-
const DEFAULT_SKILL_NAME = "openspec-core";
|
|
10
|
-
const BUILTIN_TEMPLATE_DIR = path.resolve(
|
|
11
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
12
|
-
"../../templates/default-skill"
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
export async function cleanupLegacyDefaultSkillDir(cwd: string): Promise<void> {
|
|
16
|
-
await rm(path.join(cwd, ".codex", DEFAULT_SKILL_NAME), { recursive: true, force: true });
|
|
17
|
-
await rm(path.join(cwd, ".cdspec", "seed-skills"), { recursive: true, force: true });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function loadDefaultSkillManifest(cwd: string): Promise<SkillManifest> {
|
|
21
|
-
const projectTemplateManifests = await loadProjectTemplateManifests(cwd);
|
|
22
|
-
if (projectTemplateManifests.length > 0) return projectTemplateManifests[0];
|
|
23
|
-
|
|
24
|
-
if (await pathExists(path.join(BUILTIN_TEMPLATE_DIR, "SKILL.md"))) {
|
|
25
|
-
return buildManifestFromDir(BUILTIN_TEMPLATE_DIR);
|
|
26
|
-
}
|
|
27
|
-
const tempDir = await buildFallbackTemplateInTemp();
|
|
28
|
-
return buildManifestFromDir(tempDir);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function loadProjectTemplateManifests(cwd: string): Promise<SkillManifest[]> {
|
|
32
|
-
return loadFromProjectTemplates(path.join(cwd, "templates"));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function buildManifestFromDir(skillDir: string): Promise<SkillManifest> {
|
|
36
|
-
const raw = await readFile(path.join(skillDir, "SKILL.md"), "utf8");
|
|
37
|
-
const parsed = parseFrontmatter(raw);
|
|
38
|
-
const name = parsed.attributes.name?.trim() || DEFAULT_SKILL_NAME;
|
|
39
|
-
const description = parsed.attributes.description?.trim() || "OpenSpec default skill";
|
|
40
|
-
const agents = await collectRelativeFiles(path.join(skillDir, "agents"));
|
|
41
|
-
const references = await collectRelativeFiles(path.join(skillDir, "references"));
|
|
42
|
-
const scripts = await collectRelativeFiles(path.join(skillDir, "scripts"));
|
|
43
|
-
const assets = await collectRelativeFiles(path.join(skillDir, "assets"));
|
|
44
|
-
return {
|
|
45
|
-
name,
|
|
46
|
-
description,
|
|
47
|
-
body: parsed.body.trim(),
|
|
48
|
-
agents,
|
|
49
|
-
resources: [...references, ...scripts, ...assets],
|
|
50
|
-
sourcePath: skillDir
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function collectRelativeFiles(dirPath: string): Promise<string[]> {
|
|
55
|
-
if (!(await pathExists(dirPath))) return [];
|
|
56
|
-
const base = path.dirname(dirPath);
|
|
57
|
-
const result: string[] = [];
|
|
58
|
-
const dirs = [dirPath];
|
|
59
|
-
while (dirs.length > 0) {
|
|
60
|
-
const current = dirs.pop()!;
|
|
61
|
-
const children = await listDirs(current);
|
|
62
|
-
for (const child of children) {
|
|
63
|
-
dirs.push(path.join(current, child));
|
|
64
|
-
}
|
|
65
|
-
const entries = await readdir(current, { withFileTypes: true });
|
|
66
|
-
for (const entry of entries) {
|
|
67
|
-
if (!entry.isFile()) continue;
|
|
68
|
-
const fullPath = path.join(current, entry.name);
|
|
69
|
-
result.push(path.relative(base, fullPath).replaceAll("\\", "/"));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function buildFallbackTemplateInTemp(): Promise<string> {
|
|
76
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-skill-"));
|
|
77
|
-
await ensureDir(path.join(root, "agents"));
|
|
78
|
-
await ensureDir(path.join(root, "references"));
|
|
79
|
-
const skillMd = [
|
|
80
|
-
"---",
|
|
81
|
-
"name: openspec-core",
|
|
82
|
-
"description: OpenSpec-style change workflow skill. Use for proposing, exploring, applying, and archiving spec-driven changes.",
|
|
83
|
-
"---",
|
|
84
|
-
"",
|
|
85
|
-
"# OpenSpec Core Skill",
|
|
86
|
-
"",
|
|
87
|
-
"1. /opsx-propose",
|
|
88
|
-
"2. /opsx-explore",
|
|
89
|
-
"3. /opsx-apply",
|
|
90
|
-
"4. /opsx-archive"
|
|
91
|
-
].join("\n");
|
|
92
|
-
const openaiYaml = [
|
|
93
|
-
"interface:",
|
|
94
|
-
' display_name: "OpenSpec Core"',
|
|
95
|
-
' short_description: "Spec-driven change workflow with propose/explore/apply/archive"',
|
|
96
|
-
' default_prompt: "Use $openspec-core to run OpenSpec-style workflows in this repository."'
|
|
97
|
-
].join("\n");
|
|
98
|
-
await writeFile(path.join(root, "SKILL.md"), `${skillMd}\n`, "utf8");
|
|
99
|
-
await writeFile(path.join(root, "agents", "openai.yaml"), `${openaiYaml}\n`, "utf8");
|
|
100
|
-
await writeFile(path.join(root, "references", "project_notes.md"), "# OpenSpec Workflow Notes\n");
|
|
101
|
-
return root;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function loadFromProjectTemplates(templateRoot: string): Promise<SkillManifest[]> {
|
|
105
|
-
if (!(await pathExists(templateRoot))) return [];
|
|
106
|
-
const manifests: SkillManifest[] = [];
|
|
107
|
-
|
|
108
|
-
const childDirs = await listDirs(templateRoot);
|
|
109
|
-
for (const child of childDirs) {
|
|
110
|
-
const full = path.join(templateRoot, child);
|
|
111
|
-
if (await pathExists(path.join(full, "SKILL.md"))) {
|
|
112
|
-
manifests.push(await buildManifestFromDir(full));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (manifests.length > 0) return manifests;
|
|
117
|
-
|
|
118
|
-
const markdownFiles = await collectMarkdownFiles(templateRoot);
|
|
119
|
-
for (const selected of markdownFiles.sort()) {
|
|
120
|
-
manifests.push(await buildManifestFromMarkdown(templateRoot, selected));
|
|
121
|
-
}
|
|
122
|
-
return manifests;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function buildManifestFromMarkdown(
|
|
126
|
-
templateRoot: string,
|
|
127
|
-
selected: string
|
|
128
|
-
): Promise<SkillManifest> {
|
|
129
|
-
const raw = await readFile(selected, "utf8");
|
|
130
|
-
const parsed = parseFrontmatter(raw);
|
|
131
|
-
const baseName = path.basename(selected, path.extname(selected));
|
|
132
|
-
const skillName = normalizeSkillName(parsed.attributes.name || baseName);
|
|
133
|
-
const description =
|
|
134
|
-
parsed.attributes.description ||
|
|
135
|
-
`Generated from templates/${path.relative(templateRoot, selected).replaceAll("\\", "/")}`;
|
|
136
|
-
const body = parsed.body.trim() || raw.trim();
|
|
137
|
-
|
|
138
|
-
const tmp = await mkdtemp(path.join(os.tmpdir(), "cdspec-template-"));
|
|
139
|
-
await ensureDir(path.join(tmp, "agents"));
|
|
140
|
-
await ensureDir(path.join(tmp, "references"));
|
|
141
|
-
const skillMd = ["---", `name: ${skillName}`, `description: ${description}`, "---", "", body].join(
|
|
142
|
-
"\n"
|
|
143
|
-
);
|
|
144
|
-
const openaiYaml = [
|
|
145
|
-
"interface:",
|
|
146
|
-
` display_name: "${humanizeName(skillName)}"`,
|
|
147
|
-
` short_description: "${escapeYaml(description)}"`,
|
|
148
|
-
` default_prompt: "Use $${skillName} to follow this template skill."`
|
|
149
|
-
].join("\n");
|
|
150
|
-
await writeFile(path.join(tmp, "SKILL.md"), `${skillMd}\n`, "utf8");
|
|
151
|
-
await writeFile(path.join(tmp, "agents", "openai.yaml"), `${openaiYaml}\n`, "utf8");
|
|
152
|
-
await writeFile(path.join(tmp, "references", path.basename(selected)), raw, "utf8");
|
|
153
|
-
return buildManifestFromDir(tmp);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function collectMarkdownFiles(root: string): Promise<string[]> {
|
|
157
|
-
const files: string[] = [];
|
|
158
|
-
const queue = [root];
|
|
159
|
-
while (queue.length > 0) {
|
|
160
|
-
const current = queue.pop()!;
|
|
161
|
-
const dirs = await listDirs(current);
|
|
162
|
-
for (const dir of dirs) {
|
|
163
|
-
queue.push(path.join(current, dir));
|
|
164
|
-
}
|
|
165
|
-
const entries = await readdir(current, { withFileTypes: true });
|
|
166
|
-
for (const entry of entries) {
|
|
167
|
-
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
168
|
-
files.push(path.join(current, entry.name));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return files;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function normalizeSkillName(input: string): string {
|
|
176
|
-
return input
|
|
177
|
-
.toLowerCase()
|
|
178
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
179
|
-
.replace(/^-+|-+$/g, "") || DEFAULT_SKILL_NAME;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function humanizeName(input: string): string {
|
|
183
|
-
return input
|
|
184
|
-
.split("-")
|
|
185
|
-
.filter(Boolean)
|
|
186
|
-
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
187
|
-
.join(" ");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function escapeYaml(value: string): string {
|
|
191
|
-
return value.replace(/"/g, '\\"');
|
|
192
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { rm } from "node:fs/promises";
|
|
3
|
-
import { loadConfig } from "../config/loader.js";
|
|
4
|
-
import { resolveAgentRoot } from "../config/path.js";
|
|
5
|
-
import { CDSpecConfig } from "../config/types.js";
|
|
6
|
-
import { loadAllSkillManifests } from "./manifest-loader.js";
|
|
7
|
-
import { expandTargets, getAdapter } from "./adapters/index.js";
|
|
8
|
-
import {
|
|
9
|
-
installToolInteractionTemplates,
|
|
10
|
-
writeSharedAgentsStub
|
|
11
|
-
} from "./tool-interactions.js";
|
|
12
|
-
import {
|
|
13
|
-
cleanupLegacyDefaultSkillDir,
|
|
14
|
-
loadDefaultSkillManifest,
|
|
15
|
-
loadProjectTemplateManifests
|
|
16
|
-
} from "./scaffold.js";
|
|
17
|
-
import { SkillManifest } from "./types.js";
|
|
18
|
-
import { validateManifest } from "./validator.js";
|
|
19
|
-
|
|
20
|
-
export async function initSkills(
|
|
21
|
-
cwd: string,
|
|
22
|
-
agentsRaw: string,
|
|
23
|
-
force: boolean
|
|
24
|
-
): Promise<string[]> {
|
|
25
|
-
await cleanupLegacyDefaultSkillDir(cwd);
|
|
26
|
-
const baseConfig = await loadConfig(cwd);
|
|
27
|
-
const projectTemplateManifests = await loadProjectTemplateManifests(cwd);
|
|
28
|
-
let manifests = projectTemplateManifests.length > 0
|
|
29
|
-
? projectTemplateManifests
|
|
30
|
-
: await loadAllSkillManifests(cwd);
|
|
31
|
-
if (manifests.length === 0) manifests = [await loadDefaultSkillManifest(cwd)];
|
|
32
|
-
const config = buildInitConfig(baseConfig, manifests);
|
|
33
|
-
const targets = expandTargets(agentsRaw);
|
|
34
|
-
if (force) {
|
|
35
|
-
for (const target of targets) {
|
|
36
|
-
const root = resolveAgentRoot(cwd, config.agents[target].rootDir);
|
|
37
|
-
await rm(path.join(root, "skills"), {
|
|
38
|
-
recursive: true,
|
|
39
|
-
force: true
|
|
40
|
-
});
|
|
41
|
-
await rm(path.join(root, config.agents[target].commandsDir), {
|
|
42
|
-
recursive: true,
|
|
43
|
-
force: true
|
|
44
|
-
});
|
|
45
|
-
const guide = config.agents[target].guideAtProjectRoot
|
|
46
|
-
? path.join(cwd, config.agents[target].guideFile)
|
|
47
|
-
: path.join(root, config.agents[target].guideFile);
|
|
48
|
-
await rm(guide, { force: true });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
for (const manifest of manifests) {
|
|
52
|
-
const diagnostics = [...validateManifest(manifest)];
|
|
53
|
-
for (const target of targets) {
|
|
54
|
-
diagnostics.push(...getAdapter(target).validate(manifest));
|
|
55
|
-
}
|
|
56
|
-
failIfErrors(diagnostics);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
for (const manifest of manifests) {
|
|
60
|
-
for (const target of targets) {
|
|
61
|
-
const outDir = path.join(resolveAgentRoot(cwd, config.agents[target].rootDir), "skills", manifest.name);
|
|
62
|
-
await getAdapter(target).emit(manifest, outDir, force);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const files: string[] = [];
|
|
67
|
-
for (const target of targets) {
|
|
68
|
-
files.push(...(await installToolInteractionTemplates(cwd, target, manifests, config)));
|
|
69
|
-
}
|
|
70
|
-
files.push(await writeSharedAgentsStub(cwd, targets, manifests, config));
|
|
71
|
-
return files;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function buildInitConfig(
|
|
75
|
-
config: CDSpecConfig,
|
|
76
|
-
manifests: SkillManifest[]
|
|
77
|
-
): CDSpecConfig {
|
|
78
|
-
const available = new Set(manifests.map((item) => item.name));
|
|
79
|
-
const skillDrivenBindings = buildSkillDrivenBindings(manifests);
|
|
80
|
-
return {
|
|
81
|
-
...config,
|
|
82
|
-
commandBindings:
|
|
83
|
-
skillDrivenBindings.length > 0
|
|
84
|
-
? skillDrivenBindings
|
|
85
|
-
: config.commandBindings.map((item) =>
|
|
86
|
-
available.has(item.skill) ? item : { ...item, skill: manifests[0]?.name || item.skill }
|
|
87
|
-
),
|
|
88
|
-
agents: {
|
|
89
|
-
codex: { ...config.agents.codex, commandFilePattern: "{id}.md", slashPattern: "/{id}" },
|
|
90
|
-
claudecode: {
|
|
91
|
-
...config.agents.claudecode,
|
|
92
|
-
commandFilePattern: "{id}.md",
|
|
93
|
-
slashPattern: "/{id}"
|
|
94
|
-
},
|
|
95
|
-
iflow: { ...config.agents.iflow, commandFilePattern: "{id}.md", slashPattern: "/{id}" }
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function buildSkillDrivenBindings(manifests: SkillManifest[]) {
|
|
101
|
-
const used = new Set<string>();
|
|
102
|
-
return manifests.map((manifest) => {
|
|
103
|
-
const base = commandIdForSkillName(manifest.name);
|
|
104
|
-
let id = base;
|
|
105
|
-
let i = 2;
|
|
106
|
-
while (used.has(id)) {
|
|
107
|
-
id = `${base}${i}`;
|
|
108
|
-
i += 1;
|
|
109
|
-
}
|
|
110
|
-
used.add(id);
|
|
111
|
-
return {
|
|
112
|
-
id,
|
|
113
|
-
skill: manifest.name,
|
|
114
|
-
description: `Run skill ${manifest.name}`
|
|
115
|
-
};
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function commandIdForSkillName(skillName: string): string {
|
|
120
|
-
const normalizedName = skillName
|
|
121
|
-
.toLowerCase()
|
|
122
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
123
|
-
.replace(/^-+|-+$/g, "");
|
|
124
|
-
return `cd-${normalizedName || "skill"}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function failIfErrors(
|
|
128
|
-
diagnostics: Array<{ level: "error" | "warning"; message: string }>
|
|
129
|
-
): void {
|
|
130
|
-
const warnings = diagnostics.filter((item) => item.level === "warning");
|
|
131
|
-
warnings.forEach((warning) => console.warn(`[warn] ${warning.message}`));
|
|
132
|
-
|
|
133
|
-
const errors = diagnostics.filter((item) => item.level === "error");
|
|
134
|
-
if (errors.length > 0) {
|
|
135
|
-
throw new Error(errors.map((error) => error.message).join("\n"));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { resolveAgentRoot } from "../config/path.js";
|
|
4
|
-
import { CDSpecConfig } from "../config/types.js";
|
|
5
|
-
import { ensureDir } from "../utils/fs.js";
|
|
6
|
-
import { SkillManifest, Target } from "./types.js";
|
|
7
|
-
|
|
8
|
-
export async function installToolInteractionTemplates(
|
|
9
|
-
cwd: string,
|
|
10
|
-
target: Target,
|
|
11
|
-
skills: SkillManifest[],
|
|
12
|
-
config: CDSpecConfig
|
|
13
|
-
): Promise<string[]> {
|
|
14
|
-
const agent = config.agents[target];
|
|
15
|
-
const root = resolveAgentRoot(cwd, agent.rootDir);
|
|
16
|
-
const commandsDir = path.join(root, agent.commandsDir);
|
|
17
|
-
await ensureDir(commandsDir);
|
|
18
|
-
const created: string[] = [];
|
|
19
|
-
|
|
20
|
-
for (const binding of config.commandBindings) {
|
|
21
|
-
const fileName = agent.commandFilePattern.replace("{id}", binding.id);
|
|
22
|
-
const file = path.join(commandsDir, fileName);
|
|
23
|
-
await writeFile(file, renderCommandTemplate(target, binding.id, binding.skill, binding.description), "utf8");
|
|
24
|
-
created.push(file);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const guidePath = agent.guideAtProjectRoot
|
|
28
|
-
? path.join(cwd, agent.guideFile)
|
|
29
|
-
: path.join(root, agent.guideFile);
|
|
30
|
-
await writeFile(guidePath, renderGuide(target, skills, config), "utf8");
|
|
31
|
-
created.push(guidePath);
|
|
32
|
-
return created;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function writeSharedAgentsStub(
|
|
36
|
-
cwd: string,
|
|
37
|
-
targets: Target[],
|
|
38
|
-
skills: SkillManifest[],
|
|
39
|
-
config: CDSpecConfig
|
|
40
|
-
): Promise<string> {
|
|
41
|
-
const file = path.join(cwd, "AGENTS.md");
|
|
42
|
-
const lines = [
|
|
43
|
-
"# AGENTS instructions",
|
|
44
|
-
"",
|
|
45
|
-
"<INSTRUCTIONS>",
|
|
46
|
-
"## OpenSpec Setup",
|
|
47
|
-
`Enabled targets: ${targets.join(", ")}`,
|
|
48
|
-
"### Skills",
|
|
49
|
-
...skills.map((skill) => `- ${skill.name}: ${skill.description || "No description"}`),
|
|
50
|
-
"### Command Bindings",
|
|
51
|
-
...config.commandBindings.map((binding) => `- ${binding.id} -> ${binding.skill}`),
|
|
52
|
-
"</INSTRUCTIONS>",
|
|
53
|
-
""
|
|
54
|
-
];
|
|
55
|
-
await writeFile(file, lines.join("\n"), "utf8");
|
|
56
|
-
return file;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function renderCommandTemplate(
|
|
60
|
-
target: Target,
|
|
61
|
-
commandId: string,
|
|
62
|
-
skillName: string,
|
|
63
|
-
description: string
|
|
64
|
-
): string {
|
|
65
|
-
return [
|
|
66
|
-
`# ${commandId} (${target})`,
|
|
67
|
-
"",
|
|
68
|
-
description,
|
|
69
|
-
"",
|
|
70
|
-
"## Skill binding",
|
|
71
|
-
`- skill: ${skillName}`,
|
|
72
|
-
"",
|
|
73
|
-
"## Required output",
|
|
74
|
-
"- Change ID",
|
|
75
|
-
"- Updated files",
|
|
76
|
-
"- Validation summary"
|
|
77
|
-
].join("\n");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function renderGuide(target: Target, skills: SkillManifest[], config: CDSpecConfig): string {
|
|
81
|
-
const agent = config.agents[target];
|
|
82
|
-
const commandList = config.commandBindings.map((binding) =>
|
|
83
|
-
`- ${agent.slashPattern.replace("{id}", binding.id)} -> ${binding.skill}`
|
|
84
|
-
);
|
|
85
|
-
return [
|
|
86
|
-
`# ${target} OpenSpec-style setup`,
|
|
87
|
-
"",
|
|
88
|
-
"## Installed skills",
|
|
89
|
-
...skills.map((skill) => `- ${skill.name}: ${skill.description || "No description"}`),
|
|
90
|
-
"",
|
|
91
|
-
"## Commands",
|
|
92
|
-
...commandList,
|
|
93
|
-
""
|
|
94
|
-
].join("\n");
|
|
95
|
-
}
|
package/src/skill-core/types.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export type Target = "codex" | "claudecode" | "iflow";
|
|
2
|
-
|
|
3
|
-
export interface SkillManifest {
|
|
4
|
-
name: string;
|
|
5
|
-
description: string;
|
|
6
|
-
body: string;
|
|
7
|
-
agents: string[];
|
|
8
|
-
resources: string[];
|
|
9
|
-
sourcePath: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface Diagnostic {
|
|
13
|
-
level: "error" | "warning";
|
|
14
|
-
message: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface TargetAdapter {
|
|
18
|
-
target: Target;
|
|
19
|
-
validate(manifest: SkillManifest): Diagnostic[];
|
|
20
|
-
emit(manifest: SkillManifest, outDir: string, force: boolean): Promise<void>;
|
|
21
|
-
}
|
|
22
|
-
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { Diagnostic, SkillManifest } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export function validateManifest(manifest: SkillManifest): Diagnostic[] {
|
|
4
|
-
const diagnostics: Diagnostic[] = [];
|
|
5
|
-
if (!manifest.name) {
|
|
6
|
-
diagnostics.push({ level: "error", message: "Skill name is required." });
|
|
7
|
-
}
|
|
8
|
-
if (!manifest.description) {
|
|
9
|
-
diagnostics.push({
|
|
10
|
-
level: "error",
|
|
11
|
-
message: `Skill "${manifest.name}" is missing frontmatter description.`
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
if (!manifest.body) {
|
|
15
|
-
diagnostics.push({
|
|
16
|
-
level: "warning",
|
|
17
|
-
message: `Skill "${manifest.name}" has empty SKILL.md body.`
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
if (!/^[a-z0-9-]+$/.test(manifest.name)) {
|
|
21
|
-
diagnostics.push({
|
|
22
|
-
level: "warning",
|
|
23
|
-
message: `Skill "${manifest.name}" should use lowercase letters, numbers, and hyphens.`
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
return diagnostics;
|
|
27
|
-
}
|
|
28
|
-
|
package/src/types/yaml.d.ts
DELETED
package/src/utils/frontmatter.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
export interface FrontmatterResult {
|
|
2
|
-
attributes: Record<string, string>;
|
|
3
|
-
body: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function parseFrontmatter(content: string): FrontmatterResult {
|
|
7
|
-
const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
8
|
-
if (!normalized.startsWith("---\n")) {
|
|
9
|
-
return { attributes: {}, body: normalized };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const endIndex = normalized.indexOf("\n---\n", 4);
|
|
13
|
-
if (endIndex === -1) {
|
|
14
|
-
return { attributes: {}, body: normalized };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const header = normalized.slice(4, endIndex);
|
|
18
|
-
const body = normalized.slice(endIndex + 5);
|
|
19
|
-
const attributes: Record<string, string> = {};
|
|
20
|
-
|
|
21
|
-
for (const rawLine of header.split("\n")) {
|
|
22
|
-
const line = rawLine.trim();
|
|
23
|
-
if (!line || line.startsWith("#")) continue;
|
|
24
|
-
const idx = line.indexOf(":");
|
|
25
|
-
if (idx <= 0) continue;
|
|
26
|
-
|
|
27
|
-
const key = line.slice(0, idx).trim();
|
|
28
|
-
let value = line.slice(idx + 1).trim();
|
|
29
|
-
value = stripQuotes(value);
|
|
30
|
-
attributes[key] = value;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return { attributes, body };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function stringifyFrontmatter(
|
|
37
|
-
attributes: Record<string, string>,
|
|
38
|
-
body: string
|
|
39
|
-
): string {
|
|
40
|
-
const lines = Object.entries(attributes).map(([key, value]) => {
|
|
41
|
-
const escaped = value.replace(/"/g, '\\"');
|
|
42
|
-
return `${key}: "${escaped}"`;
|
|
43
|
-
});
|
|
44
|
-
return `---\n${lines.join("\n")}\n---\n\n${body.trimEnd()}\n`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function stripQuotes(value: string): string {
|
|
48
|
-
if (
|
|
49
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
50
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
51
|
-
) {
|
|
52
|
-
return value.slice(1, -1);
|
|
53
|
-
}
|
|
54
|
-
return value;
|
|
55
|
-
}
|
package/src/utils/fs.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { mkdir, readdir, rm, stat, copyFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
export async function ensureDir(dirPath: string): Promise<void> {
|
|
5
|
-
await mkdir(dirPath, { recursive: true });
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export async function pathExists(target: string): Promise<boolean> {
|
|
9
|
-
try {
|
|
10
|
-
await stat(target);
|
|
11
|
-
return true;
|
|
12
|
-
} catch {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function emptyDir(dirPath: string): Promise<void> {
|
|
18
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
19
|
-
await mkdir(dirPath, { recursive: true });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function copyDir(srcDir: string, dstDir: string): Promise<void> {
|
|
23
|
-
await ensureDir(dstDir);
|
|
24
|
-
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
27
|
-
const dstPath = path.join(dstDir, entry.name);
|
|
28
|
-
if (entry.isDirectory()) {
|
|
29
|
-
await copyDir(srcPath, dstPath);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
await copyFile(srcPath, dstPath);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function listDirs(dirPath: string): Promise<string[]> {
|
|
37
|
-
if (!(await pathExists(dirPath))) return [];
|
|
38
|
-
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
39
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
40
|
-
}
|
|
41
|
-
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
interface:
|
|
2
|
-
display_name: "后端开发规范指南"
|
|
3
|
-
short_description: "统一后端、数据库、架构、设计模式与JUC并发的执行技能"
|
|
4
|
-
default_prompt: "Use $department-backend-standards to implement backend changes with department standards for coding, DB design, layered or DDD architecture, design patterns, and JUC concurrency."
|