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
@@ -0,0 +1,40 @@
1
+ export function parseFrontmatter(content) {
2
+ const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
3
+ if (!normalized.startsWith("---\n")) {
4
+ return { attributes: {}, body: normalized };
5
+ }
6
+ const endIndex = normalized.indexOf("\n---\n", 4);
7
+ if (endIndex === -1) {
8
+ return { attributes: {}, body: normalized };
9
+ }
10
+ const header = normalized.slice(4, endIndex);
11
+ const body = normalized.slice(endIndex + 5);
12
+ const attributes = {};
13
+ for (const rawLine of header.split("\n")) {
14
+ const line = rawLine.trim();
15
+ if (!line || line.startsWith("#"))
16
+ continue;
17
+ const idx = line.indexOf(":");
18
+ if (idx <= 0)
19
+ continue;
20
+ const key = line.slice(0, idx).trim();
21
+ let value = line.slice(idx + 1).trim();
22
+ value = stripQuotes(value);
23
+ attributes[key] = value;
24
+ }
25
+ return { attributes, body };
26
+ }
27
+ export function stringifyFrontmatter(attributes, body) {
28
+ const lines = Object.entries(attributes).map(([key, value]) => {
29
+ const escaped = value.replace(/"/g, '\\"');
30
+ return `${key}: "${escaped}"`;
31
+ });
32
+ return `---\n${lines.join("\n")}\n---\n\n${body.trimEnd()}\n`;
33
+ }
34
+ function stripQuotes(value) {
35
+ if ((value.startsWith('"') && value.endsWith('"')) ||
36
+ (value.startsWith("'") && value.endsWith("'"))) {
37
+ return value.slice(1, -1);
38
+ }
39
+ return value;
40
+ }
@@ -0,0 +1,37 @@
1
+ import { mkdir, readdir, rm, stat, copyFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function ensureDir(dirPath) {
4
+ await mkdir(dirPath, { recursive: true });
5
+ }
6
+ export async function pathExists(target) {
7
+ try {
8
+ await stat(target);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ export async function emptyDir(dirPath) {
16
+ await rm(dirPath, { recursive: true, force: true });
17
+ await mkdir(dirPath, { recursive: true });
18
+ }
19
+ export async function copyDir(srcDir, dstDir) {
20
+ await ensureDir(dstDir);
21
+ const entries = await readdir(srcDir, { withFileTypes: true });
22
+ for (const entry of entries) {
23
+ const srcPath = path.join(srcDir, entry.name);
24
+ const dstPath = path.join(dstDir, entry.name);
25
+ if (entry.isDirectory()) {
26
+ await copyDir(srcPath, dstPath);
27
+ continue;
28
+ }
29
+ await copyFile(srcPath, dstPath);
30
+ }
31
+ }
32
+ export async function listDirs(dirPath) {
33
+ if (!(await pathExists(dirPath)))
34
+ return [];
35
+ const entries = await readdir(dirPath, { withFileTypes: true });
36
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
37
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "cdspec",
3
+ "version": "0.1.0",
4
+ "description": "CDSpec CLI for skill sync and task split/archive workflow",
5
+ "type": "module",
6
+ "bin": {
7
+ "cdspec": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.json",
11
+ "dev": "tsx src/cli.ts",
12
+ "test": "vitest run",
13
+ "typecheck": "tsc -p tsconfig.json --noEmit"
14
+ },
15
+ "engines": {
16
+ "node": ">=20.0.0"
17
+ },
18
+ "dependencies": {
19
+ "@inquirer/checkbox": "^4.3.2",
20
+ "commander": "^14.0.1",
21
+ "yaml": "^2.8.1"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^24.0.0",
25
+ "tsx": "^4.20.3",
26
+ "typescript": "^5.9.3",
27
+ "vitest": "^3.2.4"
28
+ }
29
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,105 @@
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
+
9
+ const program = new Command()
10
+
11
+ program.name('cdspec').description('Skill sync + task split/archive workflow CLI').version('0.1.0')
12
+
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: { agents?: string }) => {
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
+
24
+ program
25
+ .command('skill')
26
+ .description('Skill operations')
27
+ .addCommand(
28
+ new Command('list').action(async () => {
29
+ const names = await listSkills(process.cwd())
30
+ if (names.length === 0) {
31
+ console.log('No skill found under .codex/.')
32
+ return
33
+ }
34
+ names.forEach(name => console.log(name))
35
+ })
36
+ )
37
+ .addCommand(
38
+ new Command('add')
39
+ .argument('<name>', 'skill folder name under .codex')
40
+ .option('--target <target>', 'codex|claudecode|iflow|all or comma-separated', 'all')
41
+ .option('--force', 'overwrite existing output', false)
42
+ .action(async (name: string, options: { target: string; force: boolean }) => {
43
+ await addSkill(process.cwd(), name, options.target, options.force)
44
+ console.log(`Skill "${name}" exported to tool folders for target "${options.target}".`)
45
+ })
46
+ )
47
+ .addCommand(
48
+ new Command('sync')
49
+ .option('--target <target>', 'codex|claudecode|iflow|all or comma-separated', 'all')
50
+ .option('--force', 'overwrite existing output', false)
51
+ .action(async (options: { target: string; force: boolean }) => {
52
+ await syncSkills(process.cwd(), options.target, options.force)
53
+ console.log(`All skills synced to tool folders for target "${options.target}".`)
54
+ })
55
+ )
56
+
57
+ program
58
+ .command('task')
59
+ .description('Task split/archive operations')
60
+ .addCommand(
61
+ new Command('split')
62
+ .requiredOption('--from <file>', 'source markdown file')
63
+ .requiredOption('--title <title>', 'task group title')
64
+ .action(async (options: { from: string; title: string }) => {
65
+ const count = await splitTasks(process.cwd(), options.from, options.title)
66
+ console.log(`Generated ${count} tasks from "${options.from}".`)
67
+ })
68
+ )
69
+ .addCommand(
70
+ new Command('status')
71
+ .requiredOption('--id <id>', 'task id')
72
+ .requiredOption('--to <status>', 'todo|in_progress|done')
73
+ .action(async (options: { id: string; to: 'todo' | 'in_progress' | 'done' }) => {
74
+ await updateTask(process.cwd(), options.id, options.to)
75
+ console.log(`Task "${options.id}" updated to "${options.to}".`)
76
+ })
77
+ )
78
+ .addCommand(
79
+ new Command('archive').requiredOption('--id <id>', 'task id').action(async (options: { id: string }) => {
80
+ await archiveTaskById(process.cwd(), options.id)
81
+ console.log(`Task "${options.id}" archived.`)
82
+ })
83
+ )
84
+
85
+ program.parseAsync(process.argv).catch((error: unknown) => {
86
+ const message = error instanceof Error ? error.message : String(error)
87
+ console.error(message)
88
+ process.exitCode = 1
89
+ })
90
+
91
+ async function askAgentsSelection(): Promise<string> {
92
+ if (!process.stdin.isTTY) return 'all'
93
+ const values = await checkbox({
94
+ message: 'Select coding agents',
95
+ choices: [
96
+ { name: 'codex', value: 'codex', checked: true },
97
+ { name: 'claudecode', value: 'claudecode' },
98
+ { name: 'iflow', value: 'iflow' },
99
+ { name: 'all', value: 'all' }
100
+ ]
101
+ })
102
+ if (values.length === 0) return 'all'
103
+ if (values.includes('all')) return 'all'
104
+ return values.join(',')
105
+ }
@@ -0,0 +1,51 @@
1
+ import { CDSpecConfig } from "./types.js";
2
+
3
+ export const defaultConfig: CDSpecConfig = {
4
+ commandBindings: [
5
+ {
6
+ id: "propose",
7
+ skill: "openspec-core",
8
+ description: "Create a change proposal with scope and impacted specs."
9
+ },
10
+ {
11
+ id: "explore",
12
+ skill: "openspec-core",
13
+ description: "Analyze existing specs and produce planned deltas."
14
+ },
15
+ {
16
+ id: "apply",
17
+ skill: "openspec-core",
18
+ description: "Implement approved tasks and keep spec updates in sync."
19
+ },
20
+ {
21
+ id: "archive",
22
+ skill: "openspec-core",
23
+ description: "Archive completed changes and update baseline specs."
24
+ }
25
+ ],
26
+ agents: {
27
+ codex: {
28
+ rootDir: ".codex",
29
+ commandsDir: "prompts",
30
+ commandFilePattern: "opsx-{id}.md",
31
+ slashPattern: "/opsx-{id}",
32
+ guideFile: "AGENTS.md"
33
+ },
34
+ claudecode: {
35
+ rootDir: ".claude",
36
+ commandsDir: "commands/opsx",
37
+ commandFilePattern: "{id}.md",
38
+ slashPattern: "/opsx:{id}",
39
+ guideFile: "CLAUDE.md",
40
+ guideAtProjectRoot: true
41
+ },
42
+ iflow: {
43
+ rootDir: ".iflow",
44
+ commandsDir: "commands",
45
+ commandFilePattern: "opsx-{id}.md",
46
+ slashPattern: "/opsx-{id}",
47
+ guideFile: "IFLOW.md"
48
+ }
49
+ }
50
+ };
51
+
@@ -0,0 +1,37 @@
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
+ import { CDSpecConfig } from "./types.js";
7
+
8
+ const CONFIG_FILE = "cdspec.config.yaml";
9
+
10
+ export async function loadConfig(cwd: string): Promise<CDSpecConfig> {
11
+ const configPath = path.join(cwd, CONFIG_FILE);
12
+ if (!(await pathExists(configPath))) {
13
+ return defaultConfig;
14
+ }
15
+
16
+ const raw = await readFile(configPath, "utf8");
17
+ const parsed = parse(raw) as Partial<CDSpecConfig> | null;
18
+ if (!parsed) return defaultConfig;
19
+
20
+ const merged: CDSpecConfig = {
21
+ commandBindings:
22
+ parsed.commandBindings && parsed.commandBindings.length > 0
23
+ ? parsed.commandBindings
24
+ : defaultConfig.commandBindings,
25
+ agents: {
26
+ codex: { ...defaultConfig.agents.codex, ...(parsed.agents?.codex || {}) },
27
+ claudecode: {
28
+ ...defaultConfig.agents.claudecode,
29
+ ...(parsed.agents?.claudecode || {})
30
+ },
31
+ iflow: { ...defaultConfig.agents.iflow, ...(parsed.agents?.iflow || {}) }
32
+ }
33
+ };
34
+
35
+ return merged;
36
+ }
37
+
@@ -0,0 +1,13 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ export function resolveAgentRoot(cwd: string, rootDir: string): string {
5
+ if (rootDir.startsWith("~/") || rootDir.startsWith("~\\")) {
6
+ return path.join(os.homedir(), rootDir.slice(2));
7
+ }
8
+ if (path.win32.isAbsolute(rootDir) || path.posix.isAbsolute(rootDir)) {
9
+ return path.normalize(rootDir);
10
+ }
11
+ return path.join(cwd, rootDir);
12
+ }
13
+
@@ -0,0 +1,22 @@
1
+ import { Target } from "../skill-core/types.js";
2
+
3
+ export interface CommandBinding {
4
+ id: string;
5
+ skill: string;
6
+ description: string;
7
+ }
8
+
9
+ export interface AgentConfig {
10
+ rootDir: string;
11
+ commandsDir: string;
12
+ commandFilePattern: string;
13
+ slashPattern: string;
14
+ guideFile: string;
15
+ guideAtProjectRoot?: boolean;
16
+ }
17
+
18
+ export interface CDSpecConfig {
19
+ commandBindings: CommandBinding[];
20
+ agents: Record<Target, AgentConfig>;
21
+ }
22
+
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+ import { emptyDir, pathExists } from "../../utils/fs.js";
3
+ import { Diagnostic, SkillManifest, TargetAdapter } from "../types.js";
4
+ import { emitWithLayout } from "./shared.js";
5
+
6
+ export class ClaudeCodeAdapter implements TargetAdapter {
7
+ target = "claudecode" as const;
8
+
9
+ validate(manifest: SkillManifest): Diagnostic[] {
10
+ const diagnostics: Diagnostic[] = [];
11
+ if (!manifest.name) {
12
+ diagnostics.push({ level: "error", message: "[claudecode] name is required." });
13
+ }
14
+ return diagnostics;
15
+ }
16
+
17
+ async emit(manifest: SkillManifest, outDir: string, force: boolean): Promise<void> {
18
+ if ((await pathExists(outDir)) && !force) {
19
+ throw new Error(
20
+ `Target already exists for skill "${manifest.name}" at ${outDir}. Use --force to overwrite.`
21
+ );
22
+ }
23
+ if (force) {
24
+ await emptyDir(outDir);
25
+ }
26
+
27
+ const meta = {
28
+ name: manifest.name,
29
+ description: manifest.description,
30
+ entry: "SKILL.md"
31
+ };
32
+
33
+ await emitWithLayout(manifest, outDir, true, [
34
+ {
35
+ relativePath: "claudecode.skill.json",
36
+ content: `${JSON.stringify(meta, null, 2)}\n`
37
+ }
38
+ ]);
39
+ }
40
+ }
41
+
42
+ export function claudecodeSkillPath(baseOutDir: string, skillName: string): string {
43
+ return path.join(baseOutDir, "claudecode", skillName);
44
+ }
45
+
@@ -0,0 +1,36 @@
1
+ import path from "node:path";
2
+ import { emptyDir, pathExists } from "../../utils/fs.js";
3
+ import { Diagnostic, SkillManifest, TargetAdapter } from "../types.js";
4
+ import { emitWithLayout } from "./shared.js";
5
+
6
+ export class CodexAdapter implements TargetAdapter {
7
+ target = "codex" as const;
8
+
9
+ validate(manifest: SkillManifest): Diagnostic[] {
10
+ const diagnostics: Diagnostic[] = [];
11
+ if (!manifest.description) {
12
+ diagnostics.push({
13
+ level: "error",
14
+ message: `[codex] ${manifest.name}: description is required.`
15
+ });
16
+ }
17
+ return diagnostics;
18
+ }
19
+
20
+ async emit(manifest: SkillManifest, outDir: string, force: boolean): Promise<void> {
21
+ if ((await pathExists(outDir)) && !force) {
22
+ throw new Error(
23
+ `Target already exists for skill "${manifest.name}" at ${outDir}. Use --force to overwrite.`
24
+ );
25
+ }
26
+ if (force) {
27
+ await emptyDir(outDir);
28
+ }
29
+ await emitWithLayout(manifest, outDir, true, []);
30
+ }
31
+ }
32
+
33
+ export function codexSkillPath(baseOutDir: string, skillName: string): string {
34
+ return path.join(baseOutDir, "codex", skillName);
35
+ }
36
+
@@ -0,0 +1,49 @@
1
+ import path from "node:path";
2
+ import { emptyDir, pathExists } from "../../utils/fs.js";
3
+ import { Diagnostic, SkillManifest, TargetAdapter } from "../types.js";
4
+ import { emitWithLayout } from "./shared.js";
5
+
6
+ export class IFlowAdapter implements TargetAdapter {
7
+ target = "iflow" as const;
8
+
9
+ validate(manifest: SkillManifest): Diagnostic[] {
10
+ const diagnostics: Diagnostic[] = [];
11
+ if (!manifest.description) {
12
+ diagnostics.push({
13
+ level: "warning",
14
+ message: `[iflow] ${manifest.name}: description is empty.`
15
+ });
16
+ }
17
+ return diagnostics;
18
+ }
19
+
20
+ async emit(manifest: SkillManifest, outDir: string, force: boolean): Promise<void> {
21
+ if ((await pathExists(outDir)) && !force) {
22
+ throw new Error(
23
+ `Target already exists for skill "${manifest.name}" at ${outDir}. Use --force to overwrite.`
24
+ );
25
+ }
26
+ if (force) {
27
+ await emptyDir(outDir);
28
+ }
29
+
30
+ const skillYaml = [
31
+ `name: "${manifest.name}"`,
32
+ `description: "${manifest.description.replace(/"/g, '\\"')}"`,
33
+ 'entry: "prompt.md"'
34
+ ].join("\n");
35
+
36
+ await emitWithLayout(manifest, outDir, false, [
37
+ {
38
+ relativePath: "prompt.md",
39
+ content: `# ${manifest.name}\n\n${manifest.body}\n`
40
+ },
41
+ { relativePath: "iflow.skill.yaml", content: `${skillYaml}\n` }
42
+ ]);
43
+ }
44
+ }
45
+
46
+ export function iflowSkillPath(baseOutDir: string, skillName: string): string {
47
+ return path.join(baseOutDir, "iflow", skillName);
48
+ }
49
+
@@ -0,0 +1,39 @@
1
+ import { Target, TargetAdapter } from "../types.js";
2
+ import { ClaudeCodeAdapter } from "./claudecode-adapter.js";
3
+ import { CodexAdapter } from "./codex-adapter.js";
4
+ import { IFlowAdapter } from "./iflow-adapter.js";
5
+
6
+ const adapters: Record<Target, TargetAdapter> = {
7
+ codex: new CodexAdapter(),
8
+ claudecode: new ClaudeCodeAdapter(),
9
+ iflow: new IFlowAdapter()
10
+ };
11
+
12
+ export function getAdapter(target: Target): TargetAdapter {
13
+ return adapters[target];
14
+ }
15
+
16
+ export function expandTargets(raw: string): Target[] {
17
+ const value = raw.trim().toLowerCase();
18
+ if (value === "all") return ["codex", "claudecode", "iflow"];
19
+ const parts = value
20
+ .split(",")
21
+ .map((item) => item.trim())
22
+ .filter(Boolean)
23
+ .map(normalizeTargetAlias);
24
+ if (parts.length === 0) {
25
+ throw new Error(`Unknown target "${raw}". Use codex|claudecode|iflow|all.`);
26
+ }
27
+ const invalid = parts.filter(
28
+ (item) => item !== "codex" && item !== "claudecode" && item !== "iflow"
29
+ );
30
+ if (invalid.length > 0) {
31
+ throw new Error(`Unknown target "${invalid[0]}". Use codex|claudecode|iflow|all.`);
32
+ }
33
+ return Array.from(new Set(parts)) as Target[];
34
+ }
35
+
36
+ function normalizeTargetAlias(value: string): string {
37
+ if (value === "ifow") return "iflow";
38
+ return value;
39
+ }
@@ -0,0 +1,45 @@
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
+ import { SkillManifest } from "../types.js";
5
+
6
+ export async function emitWithLayout(
7
+ manifest: SkillManifest,
8
+ outDir: string,
9
+ includeOriginalSkillMd: boolean,
10
+ generatedFiles: Array<{ relativePath: string; content: string }>
11
+ ): Promise<void> {
12
+ await ensureDir(outDir);
13
+ if (includeOriginalSkillMd) {
14
+ const srcSkillMd = path.join(manifest.sourcePath, "SKILL.md");
15
+ await copyFile(srcSkillMd, path.join(outDir, "SKILL.md"));
16
+ }
17
+
18
+ for (const file of generatedFiles) {
19
+ const targetFile = path.join(outDir, file.relativePath);
20
+ await ensureDir(path.dirname(targetFile));
21
+ await writeFile(targetFile, file.content, "utf8");
22
+ }
23
+
24
+ const folders = ["agents", "references", "scripts", "assets"];
25
+ for (const folder of folders) {
26
+ const sourceFolder = path.join(manifest.sourcePath, folder);
27
+ if (!(await pathExists(sourceFolder))) continue;
28
+ await copyFolder(sourceFolder, path.join(outDir, folder));
29
+ }
30
+ }
31
+
32
+ async function copyFolder(srcDir: string, dstDir: string): Promise<void> {
33
+ await ensureDir(dstDir);
34
+ const entries = await readdir(srcDir, { withFileTypes: true });
35
+ for (const entry of entries) {
36
+ const src = path.join(srcDir, entry.name);
37
+ const dst = path.join(dstDir, entry.name);
38
+ if (entry.isDirectory()) {
39
+ await copyFolder(src, dst);
40
+ continue;
41
+ }
42
+ const data = await readFile(src);
43
+ await writeFile(dst, data);
44
+ }
45
+ }
@@ -0,0 +1,79 @@
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
+ export async function loadSkillManifestByName(
22
+ cwd: string,
23
+ name: string
24
+ ): Promise<SkillManifest | null> {
25
+ return loadSkillManifest(path.join(cwd, CODEX_DIR, name));
26
+ }
27
+
28
+ async function loadSkillManifest(skillDir: string): Promise<SkillManifest | null> {
29
+ const skillMdPath = path.join(skillDir, "SKILL.md");
30
+ if (!(await pathExists(skillMdPath))) return null;
31
+
32
+ const content = await readFile(skillMdPath, "utf8");
33
+ const parsed = parseFrontmatter(content);
34
+ const frontName = parsed.attributes.name?.trim();
35
+ const frontDescription = parsed.attributes.description?.trim();
36
+ const displayName = frontName || path.basename(skillDir);
37
+
38
+ const agents = await collectRelativeFiles(path.join(skillDir, "agents"), "agents");
39
+ const resources = await collectRelativeFiles(path.join(skillDir, "references"), "references");
40
+ const scripts = await collectRelativeFiles(path.join(skillDir, "scripts"), "scripts");
41
+ const assets = await collectRelativeFiles(path.join(skillDir, "assets"), "assets");
42
+
43
+ return {
44
+ name: displayName,
45
+ description: frontDescription || "",
46
+ body: parsed.body.trim(),
47
+ agents,
48
+ resources: [...resources, ...scripts, ...assets],
49
+ sourcePath: skillDir
50
+ };
51
+ }
52
+
53
+ async function collectRelativeFiles(
54
+ dirPath: string,
55
+ prefix: string
56
+ ): Promise<string[]> {
57
+ if (!(await pathExists(dirPath))) return [];
58
+ const result: string[] = [];
59
+ await walk(dirPath, (absolutePath) => {
60
+ const relative = path.relative(path.dirname(dirPath), absolutePath);
61
+ result.push(relative.replaceAll("\\", "/").replace(`${prefix}/./`, `${prefix}/`));
62
+ });
63
+ return result;
64
+ }
65
+
66
+ async function walk(
67
+ dirPath: string,
68
+ onFile: (absolutePath: string) => void
69
+ ): Promise<void> {
70
+ const entries = await readdir(dirPath, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ const absolute = path.join(dirPath, entry.name);
73
+ if (entry.isDirectory()) {
74
+ await walk(absolute, onFile);
75
+ continue;
76
+ }
77
+ onFile(absolute);
78
+ }
79
+ }