cdspec 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/AGENTS.md +14 -0
  2. package/CLAUDE.md +10 -0
  3. package/README.md +55 -0
  4. package/cdspec.config.yaml +34 -0
  5. package/dist/cli.js +94 -0
  6. package/dist/config/default.js +48 -0
  7. package/dist/config/loader.js +30 -0
  8. package/dist/config/path.js +11 -0
  9. package/dist/config/types.js +1 -0
  10. package/dist/skill-core/adapters/claudecode-adapter.js +35 -0
  11. package/dist/skill-core/adapters/codex-adapter.js +28 -0
  12. package/dist/skill-core/adapters/iflow-adapter.js +39 -0
  13. package/dist/skill-core/adapters/index.js +34 -0
  14. package/dist/skill-core/adapters/shared.js +36 -0
  15. package/dist/skill-core/agent-config.js +40 -0
  16. package/dist/skill-core/manifest-loader.js +63 -0
  17. package/dist/skill-core/scaffold.js +169 -0
  18. package/dist/skill-core/service.js +156 -0
  19. package/dist/skill-core/tool-interactions.js +70 -0
  20. package/dist/skill-core/types.js +1 -0
  21. package/dist/skill-core/validator.js +25 -0
  22. package/dist/task-core/parser.js +70 -0
  23. package/dist/task-core/service.js +28 -0
  24. package/dist/task-core/storage.js +159 -0
  25. package/dist/task-core/types.js +1 -0
  26. package/dist/utils/frontmatter.js +40 -0
  27. package/dist/utils/fs.js +37 -0
  28. package/package.json +29 -0
  29. package/src/cli.ts +105 -0
  30. package/src/config/default.ts +51 -0
  31. package/src/config/loader.ts +37 -0
  32. package/src/config/path.ts +13 -0
  33. package/src/config/types.ts +22 -0
  34. package/src/skill-core/adapters/claudecode-adapter.ts +45 -0
  35. package/src/skill-core/adapters/codex-adapter.ts +36 -0
  36. package/src/skill-core/adapters/iflow-adapter.ts +49 -0
  37. package/src/skill-core/adapters/index.ts +39 -0
  38. package/src/skill-core/adapters/shared.ts +45 -0
  39. package/src/skill-core/manifest-loader.ts +79 -0
  40. package/src/skill-core/scaffold.ts +192 -0
  41. package/src/skill-core/service.ts +199 -0
  42. package/src/skill-core/tool-interactions.ts +95 -0
  43. package/src/skill-core/types.ts +22 -0
  44. package/src/skill-core/validator.ts +28 -0
  45. package/src/task-core/parser.ts +89 -0
  46. package/src/task-core/service.ts +49 -0
  47. package/src/task-core/storage.ts +177 -0
  48. package/src/task-core/types.ts +15 -0
  49. package/src/types/yaml.d.ts +4 -0
  50. package/src/utils/frontmatter.ts +55 -0
  51. package/src/utils/fs.ts +41 -0
  52. package/templates/design-doc/SKILL.md +99 -0
  53. package/templates/design-doc/agents/openai.yaml +4 -0
  54. package/templates/design-doc/references//345/237/272/347/272/277/346/250/241/346/235/277.md +46 -0
  55. package/templates/design-doc/references//345/242/236/351/207/217/351/234/200/346/261/202/346/250/241/346/235/277.md +32 -0
  56. package/templates/design-doc/references//345/275/222/346/241/243/346/243/200/346/237/245/346/270/205/345/215/225.md +15 -0
  57. package/templates/design-doc/references//347/224/237/344/272/247/345/267/245/345/215/225/345/237/272/347/272/277/347/244/272/344/276/213.md +470 -0
  58. package/templates/design-doc/scripts/validate_doc_layout.sh +49 -0
  59. package/templates/frontend-develop-standard/SKILL.md +63 -0
  60. package/templates/frontend-develop-standard/agents/openai.yaml +4 -0
  61. package/templates/frontend-develop-standard/references/frontend_develop_standard.md +749 -0
  62. package/templates/standards-backend/SKILL.md +55 -0
  63. package/templates/standards-backend/agents/openai.yaml +4 -0
  64. package/templates/standards-backend/references/DDD/346/236/266/346/236/204/347/272/246/346/235/237.md +103 -0
  65. package/templates/standards-backend/references/JUC/345/271/266/345/217/221/350/247/204/350/214/203.md +232 -0
  66. package/templates/standards-backend/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 -0
  67. package/templates/standards-backend/references//345/220/216/347/253/257/345/274/200/345/217/221/350/247/204/350/214/203.md +49 -0
  68. package/templates/standards-backend/references//346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/350/247/204/350/214/203.md +116 -0
  69. package/templates/standards-backend/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 -0
  70. package/tests/skill.test.ts +191 -0
  71. package/tests/task.test.ts +55 -0
  72. package/tsconfig.json +16 -0
  73. package/vitest.config.ts +9 -0
package/AGENTS.md ADDED
@@ -0,0 +1,14 @@
1
+ # AGENTS instructions
2
+
3
+ <INSTRUCTIONS>
4
+ ## OpenSpec Setup
5
+ Enabled targets: codex
6
+ ### Skills
7
+ - design-doc: 管理业务详细设计文档的全生命周期:首次基线、增量 feature 文档、归档合并。适用于首次开发与迭代改造,避免小改动频繁新建基线版本文件,同时保留 feature 变更历史。
8
+ - frontend-standards: 部门级前端开发规范执行技能,覆盖 Vue3 编码规范、工程配置规范、Git 提交规范与前端最佳实践。用于前端需求开发、重构、代码评审、工程初始化与联调前自检;当任务涉及 Vue3/TypeScript 页面与组件实现、样式规范、ESLint/Prettier/Husky/Commitlint 配置时触发。
9
+ - standards-backend: 部门级后端开发规范执行技能,覆盖后端编码规范、数据库设计规范、传统三层架构约束、DDD架构约束、常用设计模式落地与JUC并发编排规范,并内置数据表基线字段与关联表例外规则。用于需求开发、重构、代码评审、建表SQL设计、接口设计、并发优化与联调前自检等场景;当任务涉及 Java 后端实现、SQL/表结构、分层落位、架构选型(三层或DDD)、可扩展性设计(策略/工厂/责任链/适配器/模板/单例)或线程池/CompletableFuture/CountDownLatch/Semaphore/ReentrantLock/Atomic* 等并发处理时触发。
10
+ ### Command Bindings
11
+ - cd-design-doc -> design-doc
12
+ - cd-frontend-standards -> frontend-standards
13
+ - cd-standards-backend -> standards-backend
14
+ </INSTRUCTIONS>
package/CLAUDE.md ADDED
@@ -0,0 +1,10 @@
1
+ # claudecode OpenSpec-style setup
2
+
3
+ ## Installed skills
4
+ - frontend-develop-standard: Generated from templates/frontend_develop_standard.md
5
+
6
+ ## Commands
7
+ - /opsx:propose -> frontend-develop-standard
8
+ - /opsx:explore -> frontend-develop-standard
9
+ - /opsx:apply -> frontend-develop-standard
10
+ - /opsx:archive -> frontend-develop-standard
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # cdspec
2
+
3
+ CLI for:
4
+
5
+ - Exporting project-local skills from `.codex/*` to `.codex/.claude/.iflow` tool layouts.
6
+ - Installing OpenSpec-style interaction commands (`opsx-*`).
7
+ - Splitting markdown into task cards and archiving completed tasks.
8
+ - Config-driven command-skill bindings via `cdspec.config.yaml`.
9
+
10
+ ## Quick start
11
+
12
+ ```bash
13
+ npm install
14
+ npm run build
15
+ node dist/cli.js --help
16
+ ```
17
+
18
+ ## Commands
19
+
20
+ ```bash
21
+ cdspec init --agents codex,claudecode,iflow
22
+
23
+ cdspec skill list
24
+ cdspec skill add <name> --target all
25
+ cdspec skill sync --target all
26
+
27
+ cdspec task split --from plan.md --title "My Task"
28
+ cdspec task status --id <task-id> --to in_progress
29
+ cdspec task status --id <task-id> --to done
30
+ cdspec task archive --id <task-id>
31
+ ```
32
+
33
+ `cdspec init` generates tool-local files:
34
+
35
+ - `.codex/skills/*`, `.codex/prompts/opsx-*.md`, `.codex/AGENTS.md`
36
+ - `.claude/skills/*`, `.claude/commands/opsx/*.md`, `CLAUDE.md`
37
+ - `.iflow/skills/*`, `.iflow/commands/opsx-*.md`, `.iflow/IFLOW.md`
38
+
39
+ If `.codex` has no skills, `cdspec init` exports template skill directly into target `skills/` folders.
40
+ It does not create extra source folders in your project tree.
41
+ When `templates/*.md` exists, the exported skill name is derived from template filename, and
42
+ command bindings are auto-pointed to that generated skill.
43
+
44
+ ## Output
45
+
46
+ - Skill output: `.{tool}/skills/{skill-name}`
47
+ - Task workspace: `.cdspec/tasks` and `.cdspec/archive`
48
+
49
+ ## Config
50
+
51
+ `cdspec.config.yaml` controls:
52
+
53
+ - command -> skill bindings
54
+ - per-agent root directory and command file pattern
55
+ - slash command display pattern
@@ -0,0 +1,34 @@
1
+ commandBindings:
2
+ - id: propose
3
+ skill: openspec-core
4
+ description: Create a change proposal with scope and impacted specs.
5
+ - id: explore
6
+ skill: openspec-core
7
+ description: Analyze existing specs and produce planned deltas.
8
+ - id: apply
9
+ skill: openspec-core
10
+ description: Implement approved tasks and keep spec updates in sync.
11
+ - id: archive
12
+ skill: openspec-core
13
+ description: Archive completed changes and update baseline specs.
14
+
15
+ agents:
16
+ codex:
17
+ rootDir: ~/.codex
18
+ commandsDir: prompts
19
+ commandFilePattern: opsx-{id}.md
20
+ slashPattern: /opsx-{id}
21
+ guideFile: AGENTS.md
22
+ claudecode:
23
+ rootDir: .claude
24
+ commandsDir: commands/opsx
25
+ commandFilePattern: "{id}.md"
26
+ slashPattern: /opsx:{id}
27
+ guideFile: CLAUDE.md
28
+ guideAtProjectRoot: true
29
+ iflow:
30
+ rootDir: .iflow
31
+ commandsDir: commands
32
+ commandFilePattern: opsx-{id}.md
33
+ slashPattern: /opsx-{id}
34
+ guideFile: IFLOW.md
package/dist/cli.js ADDED
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import checkbox from "@inquirer/checkbox";
3
+ import { Command } from "commander";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { addSkill, initSkills, listSkills, syncSkills } from "./skill-core/service.js";
7
+ import { archiveTaskById, splitTasks, updateTask } from "./task-core/service.js";
8
+ const program = new Command();
9
+ program
10
+ .name("cdspec")
11
+ .description("Skill sync + task split/archive workflow CLI")
12
+ .version("0.1.0");
13
+ program
14
+ .command("init")
15
+ .description("Initialize skills for selected coding agents")
16
+ .option("--agents <agents>", "codex|claudecode|iflow|all or comma-separated")
17
+ .action(async (options) => {
18
+ const selected = options.agents ?? (await askAgentsSelection());
19
+ const files = await initSkills(process.cwd(), selected, true);
20
+ console.log(`Initialized OpenSpec-style setup for "${selected}".`);
21
+ console.log(`Generated files: ${files.map((x) => path.relative(process.cwd(), x)).join(", ")}`);
22
+ });
23
+ program
24
+ .command("skill")
25
+ .description("Skill operations")
26
+ .addCommand(new Command("list").action(async () => {
27
+ const names = await listSkills(process.cwd());
28
+ if (names.length === 0) {
29
+ console.log("No skill found under .codex/.");
30
+ return;
31
+ }
32
+ names.forEach((name) => console.log(name));
33
+ }))
34
+ .addCommand(new Command("add")
35
+ .argument("<name>", "skill folder name under .codex")
36
+ .option("--target <target>", "codex|claudecode|iflow|all or comma-separated", "all")
37
+ .option("--force", "overwrite existing output", false)
38
+ .action(async (name, options) => {
39
+ await addSkill(process.cwd(), name, options.target, options.force);
40
+ console.log(`Skill "${name}" exported to tool folders for target "${options.target}".`);
41
+ }))
42
+ .addCommand(new Command("sync")
43
+ .option("--target <target>", "codex|claudecode|iflow|all or comma-separated", "all")
44
+ .option("--force", "overwrite existing output", false)
45
+ .action(async (options) => {
46
+ await syncSkills(process.cwd(), options.target, options.force);
47
+ console.log(`All skills synced to tool folders for target "${options.target}".`);
48
+ }));
49
+ program
50
+ .command("task")
51
+ .description("Task split/archive operations")
52
+ .addCommand(new Command("split")
53
+ .requiredOption("--from <file>", "source markdown file")
54
+ .requiredOption("--title <title>", "task group title")
55
+ .action(async (options) => {
56
+ const count = await splitTasks(process.cwd(), options.from, options.title);
57
+ console.log(`Generated ${count} tasks from "${options.from}".`);
58
+ }))
59
+ .addCommand(new Command("status")
60
+ .requiredOption("--id <id>", "task id")
61
+ .requiredOption("--to <status>", "todo|in_progress|done")
62
+ .action(async (options) => {
63
+ await updateTask(process.cwd(), options.id, options.to);
64
+ console.log(`Task "${options.id}" updated to "${options.to}".`);
65
+ }))
66
+ .addCommand(new Command("archive")
67
+ .requiredOption("--id <id>", "task id")
68
+ .action(async (options) => {
69
+ await archiveTaskById(process.cwd(), options.id);
70
+ console.log(`Task "${options.id}" archived.`);
71
+ }));
72
+ program.parseAsync(process.argv).catch((error) => {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ console.error(message);
75
+ process.exitCode = 1;
76
+ });
77
+ async function askAgentsSelection() {
78
+ if (!process.stdin.isTTY)
79
+ return "all";
80
+ const values = await checkbox({
81
+ message: "Select coding agents",
82
+ choices: [
83
+ { name: "codex", value: "codex", checked: true },
84
+ { name: "claudecode", value: "claudecode" },
85
+ { name: "iflow", value: "iflow" },
86
+ { name: "all", value: "all" }
87
+ ]
88
+ });
89
+ if (values.length === 0)
90
+ return "all";
91
+ if (values.includes("all"))
92
+ return "all";
93
+ return values.join(",");
94
+ }
@@ -0,0 +1,48 @@
1
+ export const defaultConfig = {
2
+ commandBindings: [
3
+ {
4
+ id: "propose",
5
+ skill: "openspec-core",
6
+ description: "Create a change proposal with scope and impacted specs."
7
+ },
8
+ {
9
+ id: "explore",
10
+ skill: "openspec-core",
11
+ description: "Analyze existing specs and produce planned deltas."
12
+ },
13
+ {
14
+ id: "apply",
15
+ skill: "openspec-core",
16
+ description: "Implement approved tasks and keep spec updates in sync."
17
+ },
18
+ {
19
+ id: "archive",
20
+ skill: "openspec-core",
21
+ description: "Archive completed changes and update baseline specs."
22
+ }
23
+ ],
24
+ agents: {
25
+ codex: {
26
+ rootDir: ".codex",
27
+ commandsDir: "prompts",
28
+ commandFilePattern: "opsx-{id}.md",
29
+ slashPattern: "/opsx-{id}",
30
+ guideFile: "AGENTS.md"
31
+ },
32
+ claudecode: {
33
+ rootDir: ".claude",
34
+ commandsDir: "commands/opsx",
35
+ commandFilePattern: "{id}.md",
36
+ slashPattern: "/opsx:{id}",
37
+ guideFile: "CLAUDE.md",
38
+ guideAtProjectRoot: true
39
+ },
40
+ iflow: {
41
+ rootDir: ".iflow",
42
+ commandsDir: "commands",
43
+ commandFilePattern: "opsx-{id}.md",
44
+ slashPattern: "/opsx-{id}",
45
+ guideFile: "IFLOW.md"
46
+ }
47
+ }
48
+ };
@@ -0,0 +1,30 @@
1
+ import path from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ import { parse } from "yaml";
4
+ import { pathExists } from "../utils/fs.js";
5
+ import { defaultConfig } from "./default.js";
6
+ const CONFIG_FILE = "cdspec.config.yaml";
7
+ export async function loadConfig(cwd) {
8
+ const configPath = path.join(cwd, CONFIG_FILE);
9
+ if (!(await pathExists(configPath))) {
10
+ return defaultConfig;
11
+ }
12
+ const raw = await readFile(configPath, "utf8");
13
+ const parsed = parse(raw);
14
+ if (!parsed)
15
+ return defaultConfig;
16
+ const merged = {
17
+ commandBindings: parsed.commandBindings && parsed.commandBindings.length > 0
18
+ ? parsed.commandBindings
19
+ : defaultConfig.commandBindings,
20
+ agents: {
21
+ codex: { ...defaultConfig.agents.codex, ...(parsed.agents?.codex || {}) },
22
+ claudecode: {
23
+ ...defaultConfig.agents.claudecode,
24
+ ...(parsed.agents?.claudecode || {})
25
+ },
26
+ iflow: { ...defaultConfig.agents.iflow, ...(parsed.agents?.iflow || {}) }
27
+ }
28
+ };
29
+ return merged;
30
+ }
@@ -0,0 +1,11 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export function resolveAgentRoot(cwd, rootDir) {
4
+ if (rootDir.startsWith("~/") || rootDir.startsWith("~\\")) {
5
+ return path.join(os.homedir(), rootDir.slice(2));
6
+ }
7
+ if (path.win32.isAbsolute(rootDir) || path.posix.isAbsolute(rootDir)) {
8
+ return path.normalize(rootDir);
9
+ }
10
+ return path.join(cwd, rootDir);
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+ import { emptyDir, pathExists } from "../../utils/fs.js";
3
+ import { emitWithLayout } from "./shared.js";
4
+ export class ClaudeCodeAdapter {
5
+ target = "claudecode";
6
+ validate(manifest) {
7
+ const diagnostics = [];
8
+ if (!manifest.name) {
9
+ diagnostics.push({ level: "error", message: "[claudecode] name is required." });
10
+ }
11
+ return diagnostics;
12
+ }
13
+ async emit(manifest, outDir, force) {
14
+ if ((await pathExists(outDir)) && !force) {
15
+ throw new Error(`Target already exists for skill "${manifest.name}" at ${outDir}. Use --force to overwrite.`);
16
+ }
17
+ if (force) {
18
+ await emptyDir(outDir);
19
+ }
20
+ const meta = {
21
+ name: manifest.name,
22
+ description: manifest.description,
23
+ entry: "SKILL.md"
24
+ };
25
+ await emitWithLayout(manifest, outDir, true, [
26
+ {
27
+ relativePath: "claudecode.skill.json",
28
+ content: `${JSON.stringify(meta, null, 2)}\n`
29
+ }
30
+ ]);
31
+ }
32
+ }
33
+ export function claudecodeSkillPath(baseOutDir, skillName) {
34
+ return path.join(baseOutDir, "claudecode", skillName);
35
+ }
@@ -0,0 +1,28 @@
1
+ import path from "node:path";
2
+ import { emptyDir, pathExists } from "../../utils/fs.js";
3
+ import { emitWithLayout } from "./shared.js";
4
+ export class CodexAdapter {
5
+ target = "codex";
6
+ validate(manifest) {
7
+ const diagnostics = [];
8
+ if (!manifest.description) {
9
+ diagnostics.push({
10
+ level: "error",
11
+ message: `[codex] ${manifest.name}: description is required.`
12
+ });
13
+ }
14
+ return diagnostics;
15
+ }
16
+ async emit(manifest, outDir, force) {
17
+ if ((await pathExists(outDir)) && !force) {
18
+ throw new Error(`Target already exists for skill "${manifest.name}" at ${outDir}. Use --force to overwrite.`);
19
+ }
20
+ if (force) {
21
+ await emptyDir(outDir);
22
+ }
23
+ await emitWithLayout(manifest, outDir, true, []);
24
+ }
25
+ }
26
+ export function codexSkillPath(baseOutDir, skillName) {
27
+ return path.join(baseOutDir, "codex", skillName);
28
+ }
@@ -0,0 +1,39 @@
1
+ import path from "node:path";
2
+ import { emptyDir, pathExists } from "../../utils/fs.js";
3
+ import { emitWithLayout } from "./shared.js";
4
+ export class IFlowAdapter {
5
+ target = "iflow";
6
+ validate(manifest) {
7
+ const diagnostics = [];
8
+ if (!manifest.description) {
9
+ diagnostics.push({
10
+ level: "warning",
11
+ message: `[iflow] ${manifest.name}: description is empty.`
12
+ });
13
+ }
14
+ return diagnostics;
15
+ }
16
+ async emit(manifest, outDir, force) {
17
+ if ((await pathExists(outDir)) && !force) {
18
+ throw new Error(`Target already exists for skill "${manifest.name}" at ${outDir}. Use --force to overwrite.`);
19
+ }
20
+ if (force) {
21
+ await emptyDir(outDir);
22
+ }
23
+ const skillYaml = [
24
+ `name: "${manifest.name}"`,
25
+ `description: "${manifest.description.replace(/"/g, '\\"')}"`,
26
+ 'entry: "prompt.md"'
27
+ ].join("\n");
28
+ await emitWithLayout(manifest, outDir, false, [
29
+ {
30
+ relativePath: "prompt.md",
31
+ content: `# ${manifest.name}\n\n${manifest.body}\n`
32
+ },
33
+ { relativePath: "iflow.skill.yaml", content: `${skillYaml}\n` }
34
+ ]);
35
+ }
36
+ }
37
+ export function iflowSkillPath(baseOutDir, skillName) {
38
+ return path.join(baseOutDir, "iflow", skillName);
39
+ }
@@ -0,0 +1,34 @@
1
+ import { ClaudeCodeAdapter } from "./claudecode-adapter.js";
2
+ import { CodexAdapter } from "./codex-adapter.js";
3
+ import { IFlowAdapter } from "./iflow-adapter.js";
4
+ const adapters = {
5
+ codex: new CodexAdapter(),
6
+ claudecode: new ClaudeCodeAdapter(),
7
+ iflow: new IFlowAdapter()
8
+ };
9
+ export function getAdapter(target) {
10
+ return adapters[target];
11
+ }
12
+ export function expandTargets(raw) {
13
+ const value = raw.trim().toLowerCase();
14
+ if (value === "all")
15
+ return ["codex", "claudecode", "iflow"];
16
+ const parts = value
17
+ .split(",")
18
+ .map((item) => item.trim())
19
+ .filter(Boolean)
20
+ .map(normalizeTargetAlias);
21
+ if (parts.length === 0) {
22
+ throw new Error(`Unknown target "${raw}". Use codex|claudecode|iflow|all.`);
23
+ }
24
+ const invalid = parts.filter((item) => item !== "codex" && item !== "claudecode" && item !== "iflow");
25
+ if (invalid.length > 0) {
26
+ throw new Error(`Unknown target "${invalid[0]}". Use codex|claudecode|iflow|all.`);
27
+ }
28
+ return Array.from(new Set(parts));
29
+ }
30
+ function normalizeTargetAlias(value) {
31
+ if (value === "ifow")
32
+ return "iflow";
33
+ return value;
34
+ }
@@ -0,0 +1,36 @@
1
+ import { copyFile, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ensureDir, pathExists } from "../../utils/fs.js";
4
+ export async function emitWithLayout(manifest, outDir, includeOriginalSkillMd, generatedFiles) {
5
+ await ensureDir(outDir);
6
+ if (includeOriginalSkillMd) {
7
+ const srcSkillMd = path.join(manifest.sourcePath, "SKILL.md");
8
+ await copyFile(srcSkillMd, path.join(outDir, "SKILL.md"));
9
+ }
10
+ for (const file of generatedFiles) {
11
+ const targetFile = path.join(outDir, file.relativePath);
12
+ await ensureDir(path.dirname(targetFile));
13
+ await writeFile(targetFile, file.content, "utf8");
14
+ }
15
+ const folders = ["agents", "references", "scripts", "assets"];
16
+ for (const folder of folders) {
17
+ const sourceFolder = path.join(manifest.sourcePath, folder);
18
+ if (!(await pathExists(sourceFolder)))
19
+ continue;
20
+ await copyFolder(sourceFolder, path.join(outDir, folder));
21
+ }
22
+ }
23
+ async function copyFolder(srcDir, dstDir) {
24
+ await ensureDir(dstDir);
25
+ const entries = await readdir(srcDir, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ const src = path.join(srcDir, entry.name);
28
+ const dst = path.join(dstDir, entry.name);
29
+ if (entry.isDirectory()) {
30
+ await copyFolder(src, dst);
31
+ continue;
32
+ }
33
+ const data = await readFile(src);
34
+ await writeFile(dst, data);
35
+ }
36
+ }
@@ -0,0 +1,40 @@
1
+ import { basename, join } from "node:path";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { ensureDir } from "../utils/fs.js";
4
+ const CONFIG_FILE_BY_TARGET = {
5
+ codex: "AGENTS.md",
6
+ claudecode: "CLAUDE.md",
7
+ iflow: "IFLOW.md"
8
+ };
9
+ export async function writeAgentConfig(cwd, target, skills) {
10
+ await ensureDir(cwd);
11
+ const filename = CONFIG_FILE_BY_TARGET[target];
12
+ const filePath = join(cwd, filename);
13
+ const content = renderConfig(target, skills);
14
+ await writeFile(filePath, content, "utf8");
15
+ return filePath;
16
+ }
17
+ function renderConfig(target, skills) {
18
+ const title = `# ${filenameForTarget(target)} instructions`;
19
+ const lines = [title, "", "<INSTRUCTIONS>", "## Skills"];
20
+ lines.push("A skill is a local capability package. The list below is generated by `cdspec init`.");
21
+ lines.push("### Available skills");
22
+ if (skills.length === 0) {
23
+ lines.push("- (none)");
24
+ }
25
+ else {
26
+ for (const skill of skills) {
27
+ const desc = skill.description || "No description.";
28
+ lines.push(`- ${skill.name}: ${desc} (file: skills/${target}/${skill.name}/SKILL.md)`);
29
+ }
30
+ }
31
+ lines.push("### How to use");
32
+ lines.push("- Mention the skill name in your request, or use `/use <skill-name>`.");
33
+ lines.push("- In Codex, `$<skill-name>` is also supported.");
34
+ lines.push("- Skills are project-local and loaded from the generated `skills/` folder.");
35
+ lines.push("</INSTRUCTIONS>", "");
36
+ return lines.join("\n");
37
+ }
38
+ function filenameForTarget(target) {
39
+ return basename(CONFIG_FILE_BY_TARGET[target], ".md").toUpperCase();
40
+ }
@@ -0,0 +1,63 @@
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
+ const CODEX_DIR = ".codex";
6
+ export async function loadAllSkillManifests(cwd) {
7
+ const sourceRoot = path.join(cwd, CODEX_DIR);
8
+ const skillDirs = await listDirs(sourceRoot);
9
+ const manifests = [];
10
+ for (const dirName of skillDirs) {
11
+ const fullPath = path.join(sourceRoot, dirName);
12
+ const manifest = await loadSkillManifest(fullPath);
13
+ if (manifest)
14
+ manifests.push(manifest);
15
+ }
16
+ return manifests;
17
+ }
18
+ export async function loadSkillManifestByName(cwd, name) {
19
+ return loadSkillManifest(path.join(cwd, CODEX_DIR, name));
20
+ }
21
+ async function loadSkillManifest(skillDir) {
22
+ const skillMdPath = path.join(skillDir, "SKILL.md");
23
+ if (!(await pathExists(skillMdPath)))
24
+ return null;
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
+ const agents = await collectRelativeFiles(path.join(skillDir, "agents"), "agents");
31
+ const resources = await collectRelativeFiles(path.join(skillDir, "references"), "references");
32
+ const scripts = await collectRelativeFiles(path.join(skillDir, "scripts"), "scripts");
33
+ const assets = await collectRelativeFiles(path.join(skillDir, "assets"), "assets");
34
+ return {
35
+ name: displayName,
36
+ description: frontDescription || "",
37
+ body: parsed.body.trim(),
38
+ agents,
39
+ resources: [...resources, ...scripts, ...assets],
40
+ sourcePath: skillDir
41
+ };
42
+ }
43
+ async function collectRelativeFiles(dirPath, prefix) {
44
+ if (!(await pathExists(dirPath)))
45
+ return [];
46
+ const result = [];
47
+ await walk(dirPath, (absolutePath) => {
48
+ const relative = path.relative(path.dirname(dirPath), absolutePath);
49
+ result.push(relative.replaceAll("\\", "/").replace(`${prefix}/./`, `${prefix}/`));
50
+ });
51
+ return result;
52
+ }
53
+ async function walk(dirPath, onFile) {
54
+ const entries = await readdir(dirPath, { withFileTypes: true });
55
+ for (const entry of entries) {
56
+ const absolute = path.join(dirPath, entry.name);
57
+ if (entry.isDirectory()) {
58
+ await walk(absolute, onFile);
59
+ continue;
60
+ }
61
+ onFile(absolute);
62
+ }
63
+ }