cdspec 0.1.0 → 0.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.
Files changed (67) hide show
  1. package/README.md +86 -33
  2. package/dist/cli.js +1 -93
  3. package/dist/config/default.js +1 -48
  4. package/dist/config/loader.js +1 -30
  5. package/dist/config/path.js +1 -11
  6. package/dist/config/types.js +1 -1
  7. package/dist/skill-core/adapters/claudecode-adapter.js +1 -35
  8. package/dist/skill-core/adapters/codex-adapter.js +1 -28
  9. package/dist/skill-core/adapters/iflow-adapter.js +1 -39
  10. package/dist/skill-core/adapters/index.js +1 -34
  11. package/dist/skill-core/adapters/shared.js +1 -36
  12. package/dist/skill-core/manifest-loader.js +1 -63
  13. package/dist/skill-core/scaffold.js +1 -169
  14. package/dist/skill-core/service.js +1 -156
  15. package/dist/skill-core/tool-interactions.js +1 -70
  16. package/dist/skill-core/types.js +1 -1
  17. package/dist/skill-core/validator.js +1 -25
  18. package/dist/utils/frontmatter.js +1 -40
  19. package/dist/utils/fs.js +1 -37
  20. package/package.json +12 -2
  21. package/templates/{standards-backend → backend-standard}/SKILL.md +55 -55
  22. package/templates/backend-standard/agents/openai.yaml +4 -0
  23. package/templates/{standards-backend → backend-standard}/references/DDD/346/236/266/346/236/204/347/272/246/346/235/237.md +103 -103
  24. package/templates/{standards-backend → backend-standard}/references/JUC/345/271/266/345/217/221/350/247/204/350/214/203.md +232 -232
  25. 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
  26. 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
  27. 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
  28. 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
  29. package/templates/{frontend-develop-standard → frontend-standard}/SKILL.md +63 -63
  30. package/templates/frontend-standard/agents/openai.yaml +4 -0
  31. package/templates/{frontend-develop-standard/references/frontend_develop_standard.md → frontend-standard/references/frontend_standard.md} +28 -321
  32. package/AGENTS.md +0 -14
  33. package/CLAUDE.md +0 -10
  34. package/dist/skill-core/agent-config.js +0 -40
  35. package/dist/task-core/parser.js +0 -70
  36. package/dist/task-core/service.js +0 -28
  37. package/dist/task-core/storage.js +0 -159
  38. package/dist/task-core/types.js +0 -1
  39. package/src/cli.ts +0 -105
  40. package/src/config/default.ts +0 -51
  41. package/src/config/loader.ts +0 -37
  42. package/src/config/path.ts +0 -13
  43. package/src/config/types.ts +0 -22
  44. package/src/skill-core/adapters/claudecode-adapter.ts +0 -45
  45. package/src/skill-core/adapters/codex-adapter.ts +0 -36
  46. package/src/skill-core/adapters/iflow-adapter.ts +0 -49
  47. package/src/skill-core/adapters/index.ts +0 -39
  48. package/src/skill-core/adapters/shared.ts +0 -45
  49. package/src/skill-core/manifest-loader.ts +0 -79
  50. package/src/skill-core/scaffold.ts +0 -192
  51. package/src/skill-core/service.ts +0 -199
  52. package/src/skill-core/tool-interactions.ts +0 -95
  53. package/src/skill-core/types.ts +0 -22
  54. package/src/skill-core/validator.ts +0 -28
  55. package/src/task-core/parser.ts +0 -89
  56. package/src/task-core/service.ts +0 -49
  57. package/src/task-core/storage.ts +0 -177
  58. package/src/task-core/types.ts +0 -15
  59. package/src/types/yaml.d.ts +0 -4
  60. package/src/utils/frontmatter.ts +0 -55
  61. package/src/utils/fs.ts +0 -41
  62. package/templates/frontend-develop-standard/agents/openai.yaml +0 -4
  63. package/templates/standards-backend/agents/openai.yaml +0 -4
  64. package/tests/skill.test.ts +0 -191
  65. package/tests/task.test.ts +0 -55
  66. package/tsconfig.json +0 -16
  67. package/vitest.config.ts +0 -9
@@ -1,89 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import path from "node:path";
3
- import { TaskItem } from "./types.js";
4
-
5
- export function splitMarkdownToTasks(
6
- markdown: string,
7
- sourcePath: string,
8
- rootTitle: string
9
- ): TaskItem[] {
10
- const normalized = markdown.replace(/\r\n/g, "\n");
11
- const lines = normalized.split("\n");
12
- const headingStack: string[] = [];
13
- const extracted: string[] = [];
14
-
15
- for (const line of lines) {
16
- const headingMatch = line.match(/^(#{1,6})\s+(.+?)\s*$/);
17
- if (headingMatch) {
18
- const level = headingMatch[1].length;
19
- const title = headingMatch[2].trim();
20
- headingStack.splice(level - 1);
21
- headingStack[level - 1] = title;
22
- continue;
23
- }
24
-
25
- const listMatch = line.match(/^\s*(?:[-*+]|\d+\.)\s+(.+?)\s*$/);
26
- if (!listMatch) continue;
27
- const item = listMatch[1].trim();
28
- const prefix = headingStack.filter(Boolean).join(" / ");
29
- extracted.push(prefix ? `${prefix} - ${item}` : item);
30
- }
31
-
32
- if (extracted.length === 0) {
33
- for (const line of lines) {
34
- const headingMatch = line.match(/^#{2,6}\s+(.+?)\s*$/);
35
- if (headingMatch) extracted.push(headingMatch[1].trim());
36
- }
37
- }
38
-
39
- if (extracted.length === 0) {
40
- extracted.push(rootTitle);
41
- }
42
-
43
- return dedupe(extracted).map((title, idx) =>
44
- createTaskItem({
45
- sourcePath,
46
- title,
47
- order: idx + 1
48
- })
49
- );
50
- }
51
-
52
- function createTaskItem(input: {
53
- sourcePath: string;
54
- title: string;
55
- order: number;
56
- }): TaskItem {
57
- const now = new Date().toISOString();
58
- return {
59
- id: buildStableId(input.sourcePath, input.title, input.order),
60
- title: input.title,
61
- status: "todo",
62
- source: normalizePath(input.sourcePath),
63
- createdAt: now,
64
- updatedAt: now
65
- };
66
- }
67
-
68
- function buildStableId(sourcePath: string, title: string, order: number): string {
69
- const digest = createHash("sha1")
70
- .update(`${normalizePath(sourcePath)}::${title}::${order}`)
71
- .digest("hex");
72
- return digest.slice(0, 10);
73
- }
74
-
75
- function normalizePath(input: string): string {
76
- return input.replaceAll("\\", "/").replaceAll(path.sep, "/");
77
- }
78
-
79
- function dedupe(items: string[]): string[] {
80
- const seen = new Set<string>();
81
- const result: string[] = [];
82
- for (const item of items) {
83
- if (seen.has(item)) continue;
84
- seen.add(item);
85
- result.push(item);
86
- }
87
- return result;
88
- }
89
-
@@ -1,49 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { pathExists } from "../utils/fs.js";
4
- import { splitMarkdownToTasks } from "./parser.js";
5
- import {
6
- archiveTask,
7
- initTaskWorkspace,
8
- refreshArchiveIndex,
9
- refreshTaskIndex,
10
- saveTask,
11
- updateTaskStatus
12
- } from "./storage.js";
13
- import { TaskStatus } from "./types.js";
14
-
15
- export async function splitTasks(
16
- cwd: string,
17
- fromFile: string,
18
- title: string
19
- ): Promise<number> {
20
- const absoluteInput = path.resolve(cwd, fromFile);
21
- if (!(await pathExists(absoluteInput))) {
22
- throw new Error(`Input file not found: ${fromFile}`);
23
- }
24
-
25
- const markdown = await readFile(absoluteInput, "utf8");
26
- const tasks = splitMarkdownToTasks(markdown, fromFile, title);
27
- await initTaskWorkspace(cwd);
28
- for (const task of tasks) {
29
- await saveTask(cwd, task);
30
- }
31
- await refreshTaskIndex(cwd);
32
- return tasks.length;
33
- }
34
-
35
- export async function archiveTaskById(cwd: string, id: string): Promise<void> {
36
- await archiveTask(cwd, id);
37
- await refreshTaskIndex(cwd);
38
- await refreshArchiveIndex(cwd);
39
- }
40
-
41
- export async function updateTask(
42
- cwd: string,
43
- id: string,
44
- status: TaskStatus
45
- ): Promise<void> {
46
- await updateTaskStatus(cwd, id, status);
47
- await refreshTaskIndex(cwd);
48
- }
49
-
@@ -1,177 +0,0 @@
1
- import { readFile, rm, writeFile, readdir } from "node:fs/promises";
2
- import path from "node:path";
3
- import { ensureDir, pathExists } from "../utils/fs.js";
4
- import { parseFrontmatter, stringifyFrontmatter } from "../utils/frontmatter.js";
5
- import { ArchiveRecord, TaskItem, TaskStatus } from "./types.js";
6
-
7
- const ROOT_DIR = ".cdspec";
8
- const TASKS_DIR = "tasks";
9
- const ARCHIVE_DIR = "archive";
10
-
11
- export function taskDir(cwd: string): string {
12
- return path.join(cwd, ROOT_DIR, TASKS_DIR);
13
- }
14
-
15
- export function archiveDir(cwd: string): string {
16
- return path.join(cwd, ROOT_DIR, ARCHIVE_DIR);
17
- }
18
-
19
- export async function initTaskWorkspace(cwd: string): Promise<void> {
20
- await ensureDir(taskDir(cwd));
21
- await ensureDir(archiveDir(cwd));
22
- }
23
-
24
- export async function saveTask(cwd: string, task: TaskItem): Promise<void> {
25
- await initTaskWorkspace(cwd);
26
- const targetFile = path.join(taskDir(cwd), `${task.id}.md`);
27
- const body = `# ${task.title}\n\n- status: ${task.status}\n- source: ${task.source}\n`;
28
- const content = stringifyFrontmatter(
29
- {
30
- id: task.id,
31
- title: task.title,
32
- status: task.status,
33
- source: task.source,
34
- created_at: task.createdAt,
35
- updated_at: task.updatedAt
36
- },
37
- body
38
- );
39
- await writeFile(targetFile, content, "utf8");
40
- }
41
-
42
- export async function loadTask(cwd: string, id: string): Promise<TaskItem | null> {
43
- const file = path.join(taskDir(cwd), `${id}.md`);
44
- if (!(await pathExists(file))) return null;
45
- const raw = await readFile(file, "utf8");
46
- const parsed = parseFrontmatter(raw);
47
- return {
48
- id: parsed.attributes.id,
49
- title: parsed.attributes.title,
50
- status: parseStatus(parsed.attributes.status),
51
- source: parsed.attributes.source,
52
- createdAt: parsed.attributes.created_at,
53
- updatedAt: parsed.attributes.updated_at
54
- };
55
- }
56
-
57
- export async function updateTaskStatus(
58
- cwd: string,
59
- id: string,
60
- next: TaskStatus
61
- ): Promise<TaskItem> {
62
- const current = await loadTask(cwd, id);
63
- if (!current) {
64
- throw new Error(`Task "${id}" not found.`);
65
- }
66
- if (!canTransition(current.status, next)) {
67
- throw new Error(`Invalid status transition: ${current.status} -> ${next}.`);
68
- }
69
- const updated: TaskItem = {
70
- ...current,
71
- status: next,
72
- updatedAt: new Date().toISOString()
73
- };
74
- await saveTask(cwd, updated);
75
- return updated;
76
- }
77
-
78
- export async function archiveTask(cwd: string, id: string): Promise<ArchiveRecord> {
79
- const current = await loadTask(cwd, id);
80
- if (!current) {
81
- throw new Error(`Task "${id}" not found.`);
82
- }
83
- if (current.status !== "done") {
84
- throw new Error(`Task "${id}" must be done before archive.`);
85
- }
86
-
87
- const archived: ArchiveRecord = {
88
- ...current,
89
- archivedAt: new Date().toISOString()
90
- };
91
- const sourceFile = path.join(taskDir(cwd), `${id}.md`);
92
- const archiveFile = path.join(archiveDir(cwd), `${id}.md`);
93
- const body = `# ${archived.title}\n\nArchived from ${sourceFile.replaceAll("\\", "/")}.\n`;
94
- const content = stringifyFrontmatter(
95
- {
96
- id: archived.id,
97
- title: archived.title,
98
- status: archived.status,
99
- source: archived.source,
100
- created_at: archived.createdAt,
101
- updated_at: archived.updatedAt,
102
- archived_at: archived.archivedAt
103
- },
104
- body
105
- );
106
- await writeFile(archiveFile, content, "utf8");
107
- await rm(sourceFile, { force: true });
108
- return archived;
109
- }
110
-
111
- export async function refreshTaskIndex(cwd: string): Promise<void> {
112
- await initTaskWorkspace(cwd);
113
- const tasks = await loadByDir(taskDir(cwd));
114
- const grouped = {
115
- todo: tasks.filter((task) => task.status === "todo"),
116
- in_progress: tasks.filter((task) => task.status === "in_progress"),
117
- done: tasks.filter((task) => task.status === "done")
118
- };
119
- const lines = [
120
- "# Task Index",
121
- "",
122
- "## todo",
123
- ...grouped.todo.map((task) => `- [${task.id}] ${task.title}`),
124
- "",
125
- "## in_progress",
126
- ...grouped.in_progress.map((task) => `- [${task.id}] ${task.title}`),
127
- "",
128
- "## done",
129
- ...grouped.done.map((task) => `- [${task.id}] ${task.title}`),
130
- ""
131
- ];
132
- await writeFile(path.join(taskDir(cwd), "index.md"), lines.join("\n"), "utf8");
133
- }
134
-
135
- export async function refreshArchiveIndex(cwd: string): Promise<void> {
136
- await initTaskWorkspace(cwd);
137
- const records = await loadByDir(archiveDir(cwd));
138
- const lines = [
139
- "# Archive Index",
140
- "",
141
- ...records.map((record) => `- [${record.id}] ${record.title}`),
142
- ""
143
- ];
144
- await writeFile(path.join(archiveDir(cwd), "index.md"), lines.join("\n"), "utf8");
145
- }
146
-
147
- async function loadByDir(dirPath: string): Promise<TaskItem[]> {
148
- if (!(await pathExists(dirPath))) return [];
149
- const entries = await readdir(dirPath, { withFileTypes: true });
150
- const result: TaskItem[] = [];
151
- for (const entry of entries) {
152
- if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name === "index.md") continue;
153
- const raw = await readFile(path.join(dirPath, entry.name), "utf8");
154
- const parsed = parseFrontmatter(raw);
155
- result.push({
156
- id: parsed.attributes.id,
157
- title: parsed.attributes.title,
158
- status: parseStatus(parsed.attributes.status),
159
- source: parsed.attributes.source,
160
- createdAt: parsed.attributes.created_at,
161
- updatedAt: parsed.attributes.updated_at
162
- });
163
- }
164
- return result;
165
- }
166
-
167
- function parseStatus(input: string): TaskStatus {
168
- if (input === "todo" || input === "in_progress" || input === "done") return input;
169
- return "todo";
170
- }
171
-
172
- function canTransition(current: TaskStatus, next: TaskStatus): boolean {
173
- if (current === next) return true;
174
- if (current === "todo" && next === "in_progress") return true;
175
- if (current === "in_progress" && next === "done") return true;
176
- return false;
177
- }
@@ -1,15 +0,0 @@
1
- export type TaskStatus = "todo" | "in_progress" | "done";
2
-
3
- export interface TaskItem {
4
- id: string;
5
- title: string;
6
- status: TaskStatus;
7
- source: string;
8
- createdAt: string;
9
- updatedAt: string;
10
- }
11
-
12
- export interface ArchiveRecord extends TaskItem {
13
- archivedAt: string;
14
- }
15
-
@@ -1,4 +0,0 @@
1
- declare module "yaml" {
2
- export function parse(input: string): unknown;
3
- }
4
-
@@ -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: "Frontend Develop Standard"
3
- short_description: "Generated from templates/frontend_develop_standard.md"
4
- default_prompt: "Use $frontend-develop-standard to follow this template skill."
@@ -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."
@@ -1,191 +0,0 @@
1
- import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- import { addSkill, initSkills, listSkills } from "../src/skill-core/service.js";
6
-
7
- async function seedSkillRepo(): Promise<string> {
8
- const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-skill-"));
9
- const skillDir = path.join(root, ".codex", "demo-skill");
10
- await mkdir(path.join(skillDir, "agents"), { recursive: true });
11
- await mkdir(path.join(skillDir, "references"), { recursive: true });
12
-
13
- await writeFile(
14
- path.join(skillDir, "SKILL.md"),
15
- [
16
- "---",
17
- "name: demo-skill",
18
- "description: demo description",
19
- "---",
20
- "",
21
- "# Demo Skill",
22
- "",
23
- "body"
24
- ].join("\n"),
25
- "utf8"
26
- );
27
- await writeFile(path.join(skillDir, "agents", "openai.yaml"), "x: 1\n", "utf8");
28
- await writeFile(path.join(skillDir, "references", "readme.md"), "ref\n", "utf8");
29
- return root;
30
- }
31
-
32
- describe("skill service", () => {
33
- it("lists valid skills", async () => {
34
- const root = await seedSkillRepo();
35
- const names = await listSkills(root);
36
- expect(names).toEqual(["demo-skill"]);
37
- });
38
-
39
- it("exports skill to all targets", async () => {
40
- const root = await seedSkillRepo();
41
- await addSkill(root, "demo-skill", "all", false);
42
-
43
- const codexSkill = await readFile(
44
- path.join(root, ".codex", "skills", "demo-skill", "SKILL.md"),
45
- "utf8"
46
- );
47
- const claudecodeMeta = await readFile(
48
- path.join(root, ".claude", "skills", "demo-skill", "claudecode.skill.json"),
49
- "utf8"
50
- );
51
- const iflowMeta = await readFile(
52
- path.join(root, ".iflow", "skills", "demo-skill", "iflow.skill.yaml"),
53
- "utf8"
54
- );
55
-
56
- expect(codexSkill).toContain("name: demo-skill");
57
- expect(claudecodeMeta).toContain('"name": "demo-skill"');
58
- expect(iflowMeta).toContain('name: "demo-skill"');
59
- });
60
-
61
- it("rejects conflict without force", async () => {
62
- const root = await seedSkillRepo();
63
- await addSkill(root, "demo-skill", "codex", false);
64
- await expect(addSkill(root, "demo-skill", "codex", false)).rejects.toThrow(
65
- /Use --force/
66
- );
67
- });
68
-
69
- it("supports ifow alias", async () => {
70
- const root = await seedSkillRepo();
71
- await addSkill(root, "demo-skill", "ifow", false);
72
- const iflowMeta = await readFile(
73
- path.join(root, ".iflow", "skills", "demo-skill", "iflow.skill.yaml"),
74
- "utf8"
75
- );
76
- expect(iflowMeta).toContain('entry: "prompt.md"');
77
- });
78
-
79
- it("generates agent config files on init", async () => {
80
- const root = await seedSkillRepo();
81
- const files = await initSkills(root, "codex,ifow", false);
82
- expect(files.some((file) => file.endsWith(path.join(".codex", "AGENTS.md")))).toBe(true);
83
- expect(files.some((file) => file.endsWith(path.join(".iflow", "IFLOW.md")))).toBe(true);
84
- expect(files.some((file) => file.endsWith(path.join("AGENTS.md")))).toBe(true);
85
-
86
- const agents = await readFile(path.join(root, ".codex", "AGENTS.md"), "utf8");
87
- const iflow = await readFile(path.join(root, ".iflow", "IFLOW.md"), "utf8");
88
- expect(agents).toContain("demo-skill");
89
- expect(iflow).toContain("/cd-demo-skill -> demo-skill");
90
- const command = await readFile(path.join(root, ".codex", "prompts", "cd-demo-skill.md"), "utf8");
91
- expect(command).toContain("skill: demo-skill");
92
- });
93
-
94
- it("creates default skill when .codex is empty", async () => {
95
- const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-empty-"));
96
- const files = await initSkills(root, "codex", false);
97
- expect(files.some((file) => file.endsWith(path.join(".codex", "AGENTS.md")))).toBe(true);
98
- expect(files.some((file) => file.endsWith(path.join("AGENTS.md")))).toBe(true);
99
- const exported = await readFile(
100
- path.join(root, ".codex", "skills", "openspec-core", "SKILL.md"),
101
- "utf8"
102
- );
103
- await expect(readFile(path.join(root, ".cdspec", "seed-skills", "openspec-core", "SKILL.md"), "utf8")).rejects.toThrow();
104
- await expect(readFile(path.join(root, ".codex", "openspec-core", "SKILL.md"), "utf8")).rejects.toThrow();
105
- expect(exported).toContain("OpenSpec Core Skill");
106
- });
107
-
108
- it("supports configurable agent output paths", async () => {
109
- const root = await seedSkillRepo();
110
- await writeFile(
111
- path.join(root, "cdspec.config.yaml"),
112
- [
113
- "commandBindings:",
114
- " - id: plan",
115
- " skill: demo-skill",
116
- " description: Plan workflow",
117
- "agents:",
118
- " codex:",
119
- " rootDir: .codex",
120
- " commandsDir: prompts",
121
- " commandFilePattern: custom-{id}.md",
122
- " slashPattern: /custom-{id}",
123
- " guideFile: AGENTS.md",
124
- " claudecode:",
125
- " rootDir: .claude",
126
- " commandsDir: commands/opsx",
127
- " commandFilePattern: \"{id}.md\"",
128
- " slashPattern: /opsx:{id}",
129
- " guideFile: CLAUDE.md",
130
- " guideAtProjectRoot: true",
131
- " iflow:",
132
- " rootDir: .iflow",
133
- " commandsDir: commands",
134
- " commandFilePattern: opsx-{id}.md",
135
- " slashPattern: /opsx-{id}",
136
- " guideFile: IFLOW.md"
137
- ].join("\n"),
138
- "utf8"
139
- );
140
-
141
- await addSkill(root, "demo-skill", "codex", false);
142
- const exported = await readFile(path.join(root, ".codex", "skills", "demo-skill", "SKILL.md"), "utf8");
143
- expect(exported).toContain("name: demo-skill");
144
- });
145
-
146
- it("binds commands to template-derived skill when no .codex skills exist", async () => {
147
- const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-template-bind-"));
148
- await mkdir(path.join(root, "templates"), { recursive: true });
149
- await writeFile(
150
- path.join(root, "templates", "frontend_develop_standard.md"),
151
- "# Frontend Develop Standard\n\nTemplate body",
152
- "utf8"
153
- );
154
- await initSkills(root, "codex", false);
155
-
156
- const exported = await readFile(
157
- path.join(root, ".codex", "skills", "frontend-develop-standard", "SKILL.md"),
158
- "utf8"
159
- );
160
- expect(exported).toContain("name: frontend-develop-standard");
161
-
162
- const prompt = await readFile(path.join(root, ".codex", "prompts", "cd-frontend-develop-standard.md"), "utf8");
163
- expect(prompt).toContain("skill: frontend-develop-standard");
164
- });
165
-
166
- it("prioritizes templates over existing .codex skills for init binding", async () => {
167
- const root = await seedSkillRepo();
168
- await mkdir(path.join(root, "templates"), { recursive: true });
169
- await writeFile(path.join(root, "templates", "my_template.md"), "# My Template\n", "utf8");
170
-
171
- await initSkills(root, "codex", true);
172
- const prompt = await readFile(path.join(root, ".codex", "prompts", "cd-my-template.md"), "utf8");
173
- expect(prompt).toContain("skill: my-template");
174
- const exported = await readFile(path.join(root, ".codex", "skills", "my-template", "SKILL.md"), "utf8");
175
- expect(exported).toContain("name: my-template");
176
- });
177
-
178
- it("uses full skill name for command ids", async () => {
179
- const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-fullname-"));
180
- await mkdir(path.join(root, "templates", "design-doc", "agents"), { recursive: true });
181
- await writeFile(
182
- path.join(root, "templates", "design-doc", "SKILL.md"),
183
- ["---", "name: design-doc", "description: d", "---", "", "# d"].join("\n"),
184
- "utf8"
185
- );
186
- await writeFile(path.join(root, "templates", "design-doc", "agents", "openai.yaml"), "x: 1\n");
187
- await initSkills(root, "codex", false);
188
- const prompt = await readFile(path.join(root, ".codex", "prompts", "cd-design-doc.md"), "utf8");
189
- expect(prompt).toContain("skill: design-doc");
190
- });
191
- });