@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.
- package/dist/agent/agent-defaults.d.ts +3 -0
- package/dist/agent/agent-defaults.d.ts.map +1 -0
- package/dist/agent/agent-defaults.js +13 -0
- package/dist/agent/agent-defaults.js.map +1 -0
- package/dist/agent/agent-runner.d.ts +9 -0
- package/dist/agent/agent-runner.d.ts.map +1 -0
- package/dist/agent/agent-runner.js +43 -0
- package/dist/agent/agent-runner.js.map +1 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/prompt/apply-prompt-generator.d.ts +3 -0
- package/dist/prompt/apply-prompt-generator.d.ts.map +1 -0
- package/dist/prompt/apply-prompt-generator.js +48 -0
- package/dist/prompt/apply-prompt-generator.js.map +1 -0
- package/dist/prompt/prompt-generator.d.ts.map +1 -1
- package/dist/prompt/prompt-generator.js +7 -11
- package/dist/prompt/prompt-generator.js.map +1 -1
- package/dist/scaffold/init.d.ts +1 -1
- package/dist/scaffold/init.d.ts.map +1 -1
- package/dist/scaffold/init.js +10 -29
- package/dist/scaffold/init.js.map +1 -1
- package/dist/scaffold/skill-adapters.d.ts +39 -0
- package/dist/scaffold/skill-adapters.d.ts.map +1 -0
- package/dist/scaffold/skill-adapters.js +224 -0
- package/dist/scaffold/skill-adapters.js.map +1 -0
- package/dist/scaffold/templates.d.ts +5 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +203 -55
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/sdd.d.ts +7 -3
- package/dist/sdd.d.ts.map +1 -1
- package/dist/sdd.js +36 -18
- package/dist/sdd.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-defaults.ts +12 -0
- package/src/agent/agent-runner.ts +54 -0
- package/src/index.ts +17 -5
- package/src/prompt/apply-prompt-generator.ts +58 -0
- package/src/prompt/prompt-generator.ts +8 -18
- package/src/scaffold/init.ts +17 -38
- package/src/scaffold/skill-adapters.ts +322 -0
- package/src/scaffold/templates.ts +207 -54
- package/src/sdd.ts +57 -31
- package/src/types.ts +2 -0
- package/tests/apply.test.ts +119 -0
- package/tests/integration.test.ts +94 -51
- package/dist/delta/hasher.d.ts +0 -2
- package/dist/delta/hasher.d.ts.map +0 -1
- package/dist/delta/hasher.js +0 -8
- package/dist/delta/hasher.js.map +0 -1
- package/dist/lock/lock-manager.d.ts +0 -6
- package/dist/lock/lock-manager.d.ts.map +0 -1
- package/dist/lock/lock-manager.js +0 -39
- 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
|
|
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
|
|
20
|
-
export { SDDError, LockFileNotFoundError, ParseError, ProjectNotInitializedError } from
|
|
21
|
-
export type { ProjectInfo } from
|
|
22
|
-
export { isSDDProject, readConfig } from
|
|
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
|
-
|
|
13
|
-
return sections.join('\n\n');
|
|
6
|
+
return 'Nothing to do — all files are synced.';
|
|
14
7
|
}
|
|
15
8
|
|
|
16
|
-
const
|
|
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
|
-
//
|
|
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(`##
|
|
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}
|
|
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
|
}
|
package/src/scaffold/init.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { mkdir
|
|
2
|
-
import { existsSync } from
|
|
3
|
-
import { resolve } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
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(
|
|
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(
|
|
30
|
+
createdFiles.push(".sdd/config.yaml");
|
|
30
31
|
|
|
31
32
|
// Create directory structure
|
|
32
|
-
const dirs = [
|
|
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
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|