@applica-software-guru/sdd-core 1.0.0 → 1.3.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 (58) hide show
  1. package/dist/agent/agent-defaults.d.ts +3 -0
  2. package/dist/agent/agent-defaults.d.ts.map +1 -0
  3. package/dist/agent/agent-defaults.js +13 -0
  4. package/dist/agent/agent-defaults.js.map +1 -0
  5. package/dist/agent/agent-runner.d.ts +9 -0
  6. package/dist/agent/agent-runner.d.ts.map +1 -0
  7. package/dist/agent/agent-runner.js +43 -0
  8. package/dist/agent/agent-runner.js.map +1 -0
  9. package/dist/index.d.ts +10 -5
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +11 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/prompt/apply-prompt-generator.d.ts +3 -0
  14. package/dist/prompt/apply-prompt-generator.d.ts.map +1 -0
  15. package/dist/prompt/apply-prompt-generator.js +48 -0
  16. package/dist/prompt/apply-prompt-generator.js.map +1 -0
  17. package/dist/prompt/prompt-generator.d.ts.map +1 -1
  18. package/dist/prompt/prompt-generator.js +7 -11
  19. package/dist/prompt/prompt-generator.js.map +1 -1
  20. package/dist/scaffold/init.d.ts +1 -1
  21. package/dist/scaffold/init.d.ts.map +1 -1
  22. package/dist/scaffold/init.js +10 -29
  23. package/dist/scaffold/init.js.map +1 -1
  24. package/dist/scaffold/skill-adapters.d.ts +39 -0
  25. package/dist/scaffold/skill-adapters.d.ts.map +1 -0
  26. package/dist/scaffold/skill-adapters.js +224 -0
  27. package/dist/scaffold/skill-adapters.js.map +1 -0
  28. package/dist/scaffold/templates.d.ts +5 -1
  29. package/dist/scaffold/templates.d.ts.map +1 -1
  30. package/dist/scaffold/templates.js +203 -55
  31. package/dist/scaffold/templates.js.map +1 -1
  32. package/dist/sdd.d.ts +7 -3
  33. package/dist/sdd.d.ts.map +1 -1
  34. package/dist/sdd.js +36 -18
  35. package/dist/sdd.js.map +1 -1
  36. package/dist/types.d.ts +2 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/agent/agent-defaults.ts +12 -0
  40. package/src/agent/agent-runner.ts +54 -0
  41. package/src/index.ts +17 -5
  42. package/src/prompt/apply-prompt-generator.ts +58 -0
  43. package/src/prompt/prompt-generator.ts +8 -18
  44. package/src/scaffold/init.ts +17 -38
  45. package/src/scaffold/skill-adapters.ts +322 -0
  46. package/src/scaffold/templates.ts +207 -54
  47. package/src/sdd.ts +57 -31
  48. package/src/types.ts +2 -0
  49. package/tests/apply.test.ts +119 -0
  50. package/tests/integration.test.ts +94 -51
  51. package/dist/delta/hasher.d.ts +0 -2
  52. package/dist/delta/hasher.d.ts.map +0 -1
  53. package/dist/delta/hasher.js +0 -8
  54. package/dist/delta/hasher.js.map +0 -1
  55. package/dist/lock/lock-manager.d.ts +0 -6
  56. package/dist/lock/lock-manager.d.ts.map +0 -1
  57. package/dist/lock/lock-manager.js +0 -39
  58. package/dist/lock/lock-manager.js.map +0 -1
@@ -0,0 +1,12 @@
1
+ export const DEFAULT_AGENTS: Record<string, string> = {
2
+ claude: 'claude -p "$(cat $PROMPT_FILE)" --dangerously-skip-permissions --verbose',
3
+ codex: 'codex -q "$(cat $PROMPT_FILE)"',
4
+ opencode: 'opencode -p "$(cat $PROMPT_FILE)"',
5
+ };
6
+
7
+ export function resolveAgentCommand(
8
+ name: string,
9
+ configAgents?: Record<string, string>
10
+ ): string | undefined {
11
+ return configAgents?.[name] ?? DEFAULT_AGENTS[name];
12
+ }
@@ -0,0 +1,54 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { writeFile, unlink } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { randomBytes } from 'node:crypto';
6
+ import { resolveAgentCommand } from './agent-defaults.js';
7
+
8
+ export interface AgentRunnerOptions {
9
+ root: string;
10
+ prompt: string;
11
+ agent: string;
12
+ agents?: Record<string, string>;
13
+ onOutput?: (data: string) => void;
14
+ }
15
+
16
+ export async function runAgent(options: AgentRunnerOptions): Promise<number> {
17
+ const { root, prompt, agent, agents, onOutput } = options;
18
+
19
+ const template = resolveAgentCommand(agent, agents);
20
+ if (!template) {
21
+ throw new Error(`Unknown agent "${agent}". Available: ${Object.keys(agents ?? {}).join(', ') || 'claude, codex, opencode'}`);
22
+ }
23
+
24
+ // Write prompt to temp file (too large for CLI arg)
25
+ const tmpFile = join(tmpdir(), `sdd-prompt-${randomBytes(6).toString('hex')}.md`);
26
+ await writeFile(tmpFile, prompt, 'utf-8');
27
+
28
+ // Replace $PROMPT_FILE with the temp file path in the command template
29
+ const command = template.replace(/\$PROMPT_FILE/g, tmpFile);
30
+
31
+ try {
32
+ const exitCode = await new Promise<number>((resolve, reject) => {
33
+ const child = spawn(command, {
34
+ cwd: root,
35
+ shell: true,
36
+ stdio: onOutput ? ['inherit', 'pipe', 'pipe'] : 'inherit',
37
+ });
38
+
39
+ if (onOutput && child.stdout) {
40
+ child.stdout.on('data', (data: Buffer) => onOutput(data.toString()));
41
+ }
42
+ if (onOutput && child.stderr) {
43
+ child.stderr.on('data', (data: Buffer) => onOutput(data.toString()));
44
+ }
45
+
46
+ child.on('error', reject);
47
+ child.on('close', (code) => resolve(code ?? 1));
48
+ });
49
+
50
+ return exitCode;
51
+ } finally {
52
+ await unlink(tmpFile).catch(() => {});
53
+ }
54
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { SDD } from './sdd.js';
1
+ export { SDD } from "./sdd.js";
2
2
  export type {
3
3
  StoryFrontmatter,
4
4
  StoryFile,
@@ -16,7 +16,19 @@ export type {
16
16
  Bug,
17
17
  BugFrontmatter,
18
18
  BugStatus,
19
- } from './types.js';
20
- export { SDDError, LockFileNotFoundError, ParseError, ProjectNotInitializedError } from './errors.js';
21
- export type { ProjectInfo } from './scaffold/templates.js';
22
- export { isSDDProject, readConfig } from './config/config-manager.js';
19
+ } from "./types.js";
20
+ export { SDDError, LockFileNotFoundError, ParseError, ProjectNotInitializedError } from "./errors.js";
21
+ export type { ProjectInfo } from "./scaffold/templates.js";
22
+ export { isSDDProject, readConfig, writeConfig } from "./config/config-manager.js";
23
+ export { runAgent } from "./agent/agent-runner.js";
24
+ export type { AgentRunnerOptions } from "./agent/agent-runner.js";
25
+ export { DEFAULT_AGENTS, resolveAgentCommand } from "./agent/agent-defaults.js";
26
+ export { listSupportedAdapters, SKILL_ADAPTERS, syncSkillAdapters } from "./scaffold/skill-adapters.js";
27
+ export type {
28
+ AdapterMode,
29
+ SkillAdapterDefinition,
30
+ SyncAdaptersOptions,
31
+ AdapterFileAction,
32
+ AdapterFileChange,
33
+ SyncAdaptersResult,
34
+ } from "./scaffold/skill-adapters.js";
@@ -0,0 +1,58 @@
1
+ import type { Bug, ChangeRequest, StoryFile } from '../types.js';
2
+ import { getFileDiff } from '../git/git.js';
3
+
4
+ export function generateApplyPrompt(
5
+ bugs: Bug[],
6
+ changeRequests: ChangeRequest[],
7
+ pendingFiles: StoryFile[],
8
+ root: string
9
+ ): string | null {
10
+ if (bugs.length === 0 && changeRequests.length === 0 && pendingFiles.length === 0) {
11
+ return null;
12
+ }
13
+
14
+ const sections: string[] = [];
15
+
16
+ // Bugs
17
+ if (bugs.length > 0) {
18
+ const lines = [`## Open bugs (${bugs.length})\n`];
19
+ for (const bug of bugs) {
20
+ lines.push(`### \`${bug.relativePath}\` — ${bug.frontmatter.title}\n`);
21
+ lines.push(bug.body.trim());
22
+ lines.push('');
23
+ }
24
+ sections.push(lines.join('\n'));
25
+ }
26
+
27
+ // Change Requests
28
+ if (changeRequests.length > 0) {
29
+ const lines = [`## Pending change requests (${changeRequests.length})\n`];
30
+ for (const cr of changeRequests) {
31
+ lines.push(`### \`${cr.relativePath}\` — ${cr.frontmatter.title}\n`);
32
+ lines.push(cr.body.trim());
33
+ lines.push('');
34
+ }
35
+ sections.push(lines.join('\n'));
36
+ }
37
+
38
+ // Pending files
39
+ if (pendingFiles.length > 0) {
40
+ const lines = [`## Pending files (${pendingFiles.length})\n`];
41
+ for (const f of pendingFiles) {
42
+ lines.push(`- \`${f.relativePath}\` — **${f.frontmatter.status}**`);
43
+ }
44
+
45
+ const changed = pendingFiles.filter((f) => f.frontmatter.status === 'changed');
46
+ for (const f of changed) {
47
+ const diff = getFileDiff(root, f.relativePath);
48
+ if (diff) {
49
+ lines.push('');
50
+ lines.push(`### Diff: \`${f.relativePath}\`\n\n\`\`\`diff\n${diff}\n\`\`\``);
51
+ }
52
+ }
53
+
54
+ sections.push(lines.join('\n'));
55
+ }
56
+
57
+ return sections.join('\n\n');
58
+ }
@@ -2,48 +2,38 @@ import type { StoryFile } from '../types.js';
2
2
  import { getFileDiff } from '../git/git.js';
3
3
 
4
4
  export function generatePrompt(files: StoryFile[], root?: string): string {
5
- const sections: string[] = [];
6
-
7
- sections.push(
8
- '# SDD Sync Prompt\n\nThis project uses Story Driven Development. Implement the changes described below.'
9
- );
10
-
11
5
  if (files.length === 0) {
12
- sections.push('Nothing to do — all files are synced.');
13
- return sections.join('\n\n');
6
+ return 'Nothing to do — all files are synced.';
14
7
  }
15
8
 
16
- const lines = [`## Files to process (${files.length})\n`];
9
+ const sections: string[] = [];
10
+
11
+ const lines = [`## Pending files (${files.length})\n`];
17
12
  for (const f of files) {
18
13
  lines.push(`- \`${f.relativePath}\` — **${f.frontmatter.status}**`);
19
14
  }
20
- lines.push('');
21
- lines.push('Read each file listed above before implementing.');
22
15
  sections.push(lines.join('\n'));
23
16
 
24
- // Show git diff for changed files so the agent knows what was modified
17
+ // Git diff for changed files
25
18
  if (root) {
26
19
  const changed = files.filter((f) => f.frontmatter.status === 'changed');
27
20
  for (const f of changed) {
28
21
  const diff = getFileDiff(root, f.relativePath);
29
22
  if (diff) {
30
- sections.push(`## Changes in \`${f.relativePath}\`\n\n\`\`\`diff\n${diff}\n\`\`\``);
23
+ sections.push(`## Diff: \`${f.relativePath}\`\n\n\`\`\`diff\n${diff}\n\`\`\``);
31
24
  }
32
25
  }
33
26
  }
34
27
 
28
+ // Deleted files
35
29
  const deleted = files.filter((f) => f.frontmatter.status === 'deleted');
36
30
  if (deleted.length > 0) {
37
31
  const delLines = ['## Files to remove\n'];
38
32
  for (const f of deleted) {
39
- delLines.push(`- \`${f.relativePath}\` — remove all related code in \`code/\``);
33
+ delLines.push(`- \`${f.relativePath}\``);
40
34
  }
41
35
  sections.push(delLines.join('\n'));
42
36
  }
43
37
 
44
- sections.push(
45
- `## Instructions\n\n1. Read each file listed above\n2. For **new** files: implement what the documentation describes\n3. For **changed** files: update the code to match the updated documentation (see diff above)\n4. For **deleted** files: remove the related code from \`code/\`\n5. If a file has a \`## Agent Notes\` section, respect those constraints\n6. All code goes inside \`code/\`\n7. After implementing each file, run \`sdd mark-synced <file>\`\n8. **Immediately after mark-synced, commit**: \`git add -A && git commit -m "sdd sync: <description>"\`\n9. When done with all files, list every file you created or modified`
46
- );
47
-
48
38
  return sections.join('\n\n');
49
39
  }
@@ -1,10 +1,11 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import { resolve } from 'node:path';
4
- import { AGENT_MD_TEMPLATE, type ProjectInfo } from './templates.js';
5
- import { writeConfig, sddDirPath } from '../config/config-manager.js';
6
- import { isGitRepo, gitInit } from '../git/git.js';
7
- import type { SDDConfig } from '../types.js';
1
+ import { mkdir } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { type ProjectInfo } from "./templates.js";
5
+ import { syncSkillAdapters } from "./skill-adapters.js";
6
+ import { writeConfig, sddDirPath } from "../config/config-manager.js";
7
+ import { isGitRepo, gitInit } from "../git/git.js";
8
+ import type { SDDConfig } from "../types.js";
8
9
 
9
10
  export async function initProject(root: string, info?: ProjectInfo): Promise<string[]> {
10
11
  const createdFiles: string[] = [];
@@ -13,7 +14,7 @@ export async function initProject(root: string, info?: ProjectInfo): Promise<str
13
14
  // Ensure git repo
14
15
  if (!isGitRepo(root)) {
15
16
  gitInit(root);
16
- createdFiles.push('.git');
17
+ createdFiles.push(".git");
17
18
  }
18
19
 
19
20
  // Create .sdd directory
@@ -23,13 +24,13 @@ export async function initProject(root: string, info?: ProjectInfo): Promise<str
23
24
 
24
25
  // Write config
25
26
  const config: SDDConfig = {
26
- description: info?.description ?? '',
27
+ description: info?.description ?? "",
27
28
  };
28
29
  await writeConfig(root, config);
29
- createdFiles.push('.sdd/config.yaml');
30
+ createdFiles.push(".sdd/config.yaml");
30
31
 
31
32
  // Create directory structure
32
- const dirs = ['product', 'product/features', 'system', 'code', 'change-requests', 'bugs'];
33
+ const dirs = ["product", "product/features", "system", "code", "change-requests", "bugs"];
33
34
  for (const dir of dirs) {
34
35
  const absDir = resolve(root, dir);
35
36
  if (!existsSync(absDir)) {
@@ -37,34 +38,12 @@ export async function initProject(root: string, info?: ProjectInfo): Promise<str
37
38
  }
38
39
  }
39
40
 
40
- // Create agent instructions
41
- const instructionsPath = resolve(root, 'INSTRUCTIONS.md');
42
- if (!existsSync(instructionsPath)) {
43
- await writeFile(instructionsPath, AGENT_MD_TEMPLATE, 'utf-8');
44
- createdFiles.push('INSTRUCTIONS.md');
45
- }
46
-
47
- // Create agent instruction pointers
48
- const POINTER = 'Read INSTRUCTIONS.md in the project root for all instructions.\n';
49
- const agentFiles: Array<{ path: string; dir?: string }> = [
50
- { path: '.claude/CLAUDE.md', dir: '.claude' },
51
- { path: '.github/copilot-instructions.md', dir: '.github' },
52
- { path: '.cursorrules' },
53
- ];
54
-
55
- for (const entry of agentFiles) {
56
- const absPath = resolve(root, entry.path);
57
- if (existsSync(absPath)) continue;
58
-
59
- if (entry.dir) {
60
- const absDir = resolve(root, entry.dir);
61
- if (!existsSync(absDir)) {
62
- await mkdir(absDir, { recursive: true });
63
- }
41
+ // Create canonical skill and agent adapters
42
+ const adapters = await syncSkillAdapters(root);
43
+ for (const change of [...adapters.canonical, ...adapters.adapters]) {
44
+ if (change.action === "created") {
45
+ createdFiles.push(change.path);
64
46
  }
65
-
66
- await writeFile(absPath, POINTER, 'utf-8');
67
- createdFiles.push(entry.path);
68
47
  }
69
48
 
70
49
  return createdFiles;
@@ -0,0 +1,322 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import {
5
+ BUGS_REFERENCE,
6
+ CHANGE_REQUESTS_REFERENCE,
7
+ FILE_FORMAT_REFERENCE,
8
+ SKILL_MD_TEMPLATE,
9
+ SKILL_UI_MD_TEMPLATE,
10
+ } from "./templates.js";
11
+
12
+ const MANAGED_HEADER =
13
+ "<!-- AUTO-GENERATED BY SDD. DO NOT EDIT MANUALLY. -->\n";
14
+
15
+ export type AdapterMode = "pointer" | "mirror";
16
+
17
+ export interface SkillPathsDefinition {
18
+ skillId: string;
19
+ paths: string[];
20
+ }
21
+
22
+ export interface SkillAdapterDefinition {
23
+ id: string;
24
+ mode: AdapterMode;
25
+ skills: SkillPathsDefinition[];
26
+ }
27
+
28
+ export interface SyncAdaptersOptions {
29
+ agents?: string[];
30
+ force?: boolean;
31
+ dryRun?: boolean;
32
+ }
33
+
34
+ export type AdapterFileAction = "created" | "updated" | "skipped";
35
+
36
+ export interface AdapterFileChange {
37
+ path: string;
38
+ action: AdapterFileAction;
39
+ reason?: string;
40
+ }
41
+
42
+ export interface SyncAdaptersResult {
43
+ canonical: AdapterFileChange[];
44
+ adapters: AdapterFileChange[];
45
+ selectedAgents: string[];
46
+ }
47
+
48
+ const CANONICAL_BASE_DIR = ".sdd/skill";
49
+
50
+ const CANONICAL_SKILLS: Array<{
51
+ id: string;
52
+ files: Array<{ path: string; content: string }>;
53
+ }> = [
54
+ {
55
+ id: "sdd",
56
+ files: [
57
+ {
58
+ path: `${CANONICAL_BASE_DIR}/sdd/SKILL.md`,
59
+ content: SKILL_MD_TEMPLATE,
60
+ },
61
+ {
62
+ path: `${CANONICAL_BASE_DIR}/sdd/references/file-format.md`,
63
+ content: FILE_FORMAT_REFERENCE,
64
+ },
65
+ {
66
+ path: `${CANONICAL_BASE_DIR}/sdd/references/change-requests.md`,
67
+ content: CHANGE_REQUESTS_REFERENCE,
68
+ },
69
+ {
70
+ path: `${CANONICAL_BASE_DIR}/sdd/references/bugs.md`,
71
+ content: BUGS_REFERENCE,
72
+ },
73
+ ],
74
+ },
75
+ {
76
+ id: "sdd-ui",
77
+ files: [
78
+ {
79
+ path: `${CANONICAL_BASE_DIR}/sdd-ui/SKILL.md`,
80
+ content: SKILL_UI_MD_TEMPLATE,
81
+ },
82
+ ],
83
+ },
84
+ ];
85
+
86
+ const CANONICAL_FILES: Array<{ path: string; content: string }> =
87
+ CANONICAL_SKILLS.flatMap((skill) => skill.files);
88
+
89
+ export interface SkillPathsDefinition {
90
+ skillId: string;
91
+ paths: string[];
92
+ }
93
+
94
+ export interface SkillAdapterDefinition {
95
+ id: string;
96
+ mode: AdapterMode;
97
+ skills: SkillPathsDefinition[];
98
+ }
99
+
100
+ export const SKILL_ADAPTERS: SkillAdapterDefinition[] = [
101
+ {
102
+ id: "claude",
103
+ mode: "mirror",
104
+ skills: [
105
+ {
106
+ skillId: "sdd",
107
+ paths: [
108
+ ".claude/skills/sdd/SKILL.md",
109
+ ".claude/skills/sdd/references/file-format.md",
110
+ ".claude/skills/sdd/references/change-requests.md",
111
+ ".claude/skills/sdd/references/bugs.md",
112
+ ],
113
+ },
114
+ {
115
+ skillId: "sdd-ui",
116
+ paths: [".claude/skills/sdd-ui/SKILL.md"],
117
+ },
118
+ ],
119
+ },
120
+ {
121
+ id: "universal",
122
+ mode: "mirror",
123
+ skills: [
124
+ {
125
+ skillId: "sdd",
126
+ paths: [
127
+ ".agents/skills/sdd/SKILL.md",
128
+ ".agents/skills/sdd/references/file-format.md",
129
+ ".agents/skills/sdd/references/change-requests.md",
130
+ ".agents/skills/sdd/references/bugs.md",
131
+ ],
132
+ },
133
+ {
134
+ skillId: "sdd-ui",
135
+ paths: [".agents/skills/sdd-ui/SKILL.md"],
136
+ },
137
+ ],
138
+ },
139
+ ];
140
+
141
+ export function listSupportedAdapters(): string[] {
142
+ return SKILL_ADAPTERS.map((adapter) => adapter.id);
143
+ }
144
+
145
+ export async function syncSkillAdapters(
146
+ root: string,
147
+ options?: SyncAdaptersOptions,
148
+ ): Promise<SyncAdaptersResult> {
149
+ const force = options?.force ?? false;
150
+ const dryRun = options?.dryRun ?? false;
151
+ const selected = resolveSelectedAdapters(options?.agents);
152
+
153
+ // Build a map of skillId → canonical files for quick lookup
154
+ const canonicalFilesBySkill = new Map<
155
+ string,
156
+ Array<{ path: string; content: string }>
157
+ >();
158
+ for (const skill of CANONICAL_SKILLS) {
159
+ canonicalFilesBySkill.set(skill.id, skill.files);
160
+ }
161
+
162
+ const canonicalChanges: AdapterFileChange[] = [];
163
+ for (const file of CANONICAL_FILES) {
164
+ const change = await upsertFile(root, file.path, file.content, {
165
+ force,
166
+ dryRun,
167
+ managed: false,
168
+ });
169
+ canonicalChanges.push(change);
170
+ }
171
+
172
+ const adapterChanges: AdapterFileChange[] = [];
173
+ for (const adapter of selected) {
174
+ for (const skillDef of adapter.skills) {
175
+ const canonicalFiles = canonicalFilesBySkill.get(skillDef.skillId);
176
+
177
+ if (adapter.mode === "pointer") {
178
+ const content = buildPointerContent(adapter.id, skillDef.skillId);
179
+ for (const path of skillDef.paths) {
180
+ const change = await upsertFile(root, path, content, {
181
+ force,
182
+ dryRun,
183
+ managed: true,
184
+ });
185
+ adapterChanges.push(change);
186
+ }
187
+ continue;
188
+ }
189
+
190
+ // mirror mode: copy canonical content to adapter paths
191
+ if (!canonicalFiles) {
192
+ for (const path of skillDef.paths) {
193
+ adapterChanges.push({
194
+ path,
195
+ action: "skipped",
196
+ reason: `No canonical files found for skill "${skillDef.skillId}"`,
197
+ });
198
+ }
199
+ continue;
200
+ }
201
+
202
+ for (let i = 0; i < skillDef.paths.length; i += 1) {
203
+ const canonical = canonicalFiles[i];
204
+ if (!canonical) {
205
+ adapterChanges.push({
206
+ path: skillDef.paths[i],
207
+ action: "skipped",
208
+ reason: "No corresponding canonical source file",
209
+ });
210
+ continue;
211
+ }
212
+
213
+ const change = await upsertFile(
214
+ root,
215
+ skillDef.paths[i],
216
+ canonical.content,
217
+ {
218
+ force,
219
+ dryRun,
220
+ managed: false,
221
+ },
222
+ );
223
+ adapterChanges.push(change);
224
+ }
225
+ }
226
+ }
227
+
228
+ return {
229
+ canonical: canonicalChanges,
230
+ adapters: adapterChanges,
231
+ selectedAgents: selected.map((adapter) => adapter.id),
232
+ };
233
+ }
234
+
235
+ function resolveSelectedAdapters(agents?: string[]): SkillAdapterDefinition[] {
236
+ if (!agents || agents.length === 0) {
237
+ return SKILL_ADAPTERS;
238
+ }
239
+
240
+ const normalized = new Set(
241
+ agents.map((agent) => agent.trim().toLowerCase()).filter(Boolean),
242
+ );
243
+ const selected = SKILL_ADAPTERS.filter((adapter) =>
244
+ normalized.has(adapter.id),
245
+ );
246
+
247
+ const missing = Array.from(normalized).filter(
248
+ (agent) => !SKILL_ADAPTERS.some((adapter) => adapter.id === agent),
249
+ );
250
+
251
+ if (missing.length > 0) {
252
+ throw new Error(`Unsupported adapters: ${missing.join(", ")}`);
253
+ }
254
+
255
+ return selected;
256
+ }
257
+
258
+ function buildPointerContent(adapterId: string, skillId: string): string {
259
+ if (adapterId === "cursor") {
260
+ return `---
261
+ description: SDD ${skillId} adapter
262
+ alwaysApply: false
263
+ ---
264
+
265
+ Read .sdd/skill/${skillId}/SKILL.md for the full SDD ${skillId} workflow.
266
+
267
+ If needed, also read the references in .sdd/skill/${skillId}/references/
268
+ `;
269
+ }
270
+
271
+ return `# SDD ${skillId} Adapter (${adapterId})
272
+
273
+ Primary instructions: .sdd/skill/${skillId}/SKILL.md
274
+
275
+ References:
276
+ - .sdd/skill/${skillId}/references/
277
+
278
+ Fallback (legacy): .claude/skills/${skillId}/SKILL.md
279
+ `;
280
+ }
281
+
282
+ async function upsertFile(
283
+ root: string,
284
+ relativePath: string,
285
+ content: string,
286
+ options: { force: boolean; dryRun: boolean; managed: boolean },
287
+ ): Promise<AdapterFileChange> {
288
+ const absPath = resolve(root, relativePath);
289
+ const finalContent = options.managed
290
+ ? `${MANAGED_HEADER}${content}`
291
+ : content;
292
+
293
+ if (!existsSync(absPath)) {
294
+ if (!options.dryRun) {
295
+ await mkdir(dirname(absPath), { recursive: true });
296
+ await writeFile(absPath, finalContent, "utf-8");
297
+ }
298
+ return { path: relativePath, action: "created" };
299
+ }
300
+
301
+ const current = await readFile(absPath, "utf-8");
302
+ if (current === finalContent) {
303
+ return {
304
+ path: relativePath,
305
+ action: "skipped",
306
+ reason: "Already up to date",
307
+ };
308
+ }
309
+
310
+ if (!options.force) {
311
+ return {
312
+ path: relativePath,
313
+ action: "skipped",
314
+ reason: "Exists (use --force to overwrite)",
315
+ };
316
+ }
317
+
318
+ if (!options.dryRun) {
319
+ await writeFile(absPath, finalContent, "utf-8");
320
+ }
321
+ return { path: relativePath, action: "updated" };
322
+ }