sdd-tool 0.6.2 → 0.7.1

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/cli/index.js CHANGED
@@ -128,10 +128,10 @@ var init_base = __esm({
128
128
  };
129
129
  FileSystemError = class extends SddError {
130
130
  path;
131
- constructor(code, path29) {
132
- super(code, formatMessage(code, path29), ExitCode.FILE_SYSTEM_ERROR);
131
+ constructor(code, path36) {
132
+ super(code, formatMessage(code, path36), ExitCode.FILE_SYSTEM_ERROR);
133
133
  this.name = "FileSystemError";
134
- this.path = path29;
134
+ this.path = path36;
135
135
  }
136
136
  };
137
137
  ValidationError = class extends SddError {
@@ -3934,9 +3934,9 @@ async function validateSpecs(targetPath, options = {}) {
3934
3934
  async function findSpecFiles(dirPath) {
3935
3935
  const files = [];
3936
3936
  async function scanDir(dir) {
3937
- const { promises: fs16 } = await import("fs");
3937
+ const { promises: fs22 } = await import("fs");
3938
3938
  try {
3939
- const entries = await fs16.readdir(dir, { withFileTypes: true });
3939
+ const entries = await fs22.readdir(dir, { withFileTypes: true });
3940
3940
  for (const entry of entries) {
3941
3941
  const fullPath = path3.join(dir, entry.name);
3942
3942
  if (entry.isDirectory()) {
@@ -6059,19 +6059,19 @@ function detectCircularDependencies(graph) {
6059
6059
  const cycles = [];
6060
6060
  const visited = /* @__PURE__ */ new Set();
6061
6061
  const recStack = /* @__PURE__ */ new Set();
6062
- function dfs(nodeId, path29) {
6062
+ function dfs(nodeId, path36) {
6063
6063
  visited.add(nodeId);
6064
6064
  recStack.add(nodeId);
6065
6065
  const node = graph.nodes.get(nodeId);
6066
6066
  if (!node) return false;
6067
6067
  for (const depId of node.dependsOn) {
6068
6068
  if (!visited.has(depId)) {
6069
- if (dfs(depId, [...path29, nodeId])) {
6069
+ if (dfs(depId, [...path36, nodeId])) {
6070
6070
  return true;
6071
6071
  }
6072
6072
  } else if (recStack.has(depId)) {
6073
- const cycleStart = path29.indexOf(depId);
6074
- const cycle = cycleStart >= 0 ? [...path29.slice(cycleStart), nodeId, depId] : [nodeId, depId];
6073
+ const cycleStart = path36.indexOf(depId);
6074
+ const cycle = cycleStart >= 0 ? [...path36.slice(cycleStart), nodeId, depId] : [nodeId, depId];
6075
6075
  cycles.push({
6076
6076
  cycle,
6077
6077
  description: `\uC21C\uD658 \uC758\uC874\uC131: ${cycle.join(" \u2192 ")}`
@@ -11835,6 +11835,1179 @@ async function executeSearch(query, options) {
11835
11835
  }
11836
11836
  }
11837
11837
 
11838
+ // src/cli/commands/prepare.ts
11839
+ import path35 from "path";
11840
+
11841
+ // src/core/prepare/schemas.ts
11842
+ import { z as z8 } from "zod";
11843
+ var ToolTypeSchema = z8.enum(["agent", "skill"]);
11844
+ var ToolStatusSchema = z8.enum(["exists", "missing", "outdated"]);
11845
+ var DetectionSourceSchema = z8.object({
11846
+ file: z8.string(),
11847
+ line: z8.number(),
11848
+ text: z8.string(),
11849
+ keyword: z8.string()
11850
+ });
11851
+ var DetectedToolSchema = z8.object({
11852
+ type: ToolTypeSchema,
11853
+ name: z8.string(),
11854
+ description: z8.string(),
11855
+ sources: z8.array(DetectionSourceSchema)
11856
+ });
11857
+ var AgentMetadataSchema = z8.object({
11858
+ name: z8.string(),
11859
+ description: z8.string(),
11860
+ tools: z8.array(z8.string()).optional(),
11861
+ model: z8.string().optional()
11862
+ });
11863
+ var ScannedAgentSchema = z8.object({
11864
+ name: z8.string(),
11865
+ filePath: z8.string(),
11866
+ metadata: AgentMetadataSchema,
11867
+ content: z8.string()
11868
+ });
11869
+ var SkillMetadataSchema = z8.object({
11870
+ name: z8.string(),
11871
+ description: z8.string(),
11872
+ "allowed-tools": z8.array(z8.string()).optional()
11873
+ });
11874
+ var ScannedSkillSchema = z8.object({
11875
+ name: z8.string(),
11876
+ dirPath: z8.string(),
11877
+ filePath: z8.string(),
11878
+ metadata: SkillMetadataSchema,
11879
+ content: z8.string()
11880
+ });
11881
+ var ToolCheckResultSchema = z8.object({
11882
+ tool: DetectedToolSchema,
11883
+ status: ToolStatusSchema,
11884
+ filePath: z8.string().optional(),
11885
+ action: z8.string()
11886
+ });
11887
+ var PrepareReportSchema = z8.object({
11888
+ feature: z8.string(),
11889
+ totalTasks: z8.number(),
11890
+ agents: z8.object({
11891
+ required: z8.number(),
11892
+ existing: z8.number(),
11893
+ missing: z8.number(),
11894
+ checks: z8.array(ToolCheckResultSchema)
11895
+ }),
11896
+ skills: z8.object({
11897
+ required: z8.number(),
11898
+ existing: z8.number(),
11899
+ missing: z8.number(),
11900
+ checks: z8.array(ToolCheckResultSchema)
11901
+ }),
11902
+ proposals: z8.array(z8.object({
11903
+ type: ToolTypeSchema,
11904
+ name: z8.string(),
11905
+ filePath: z8.string(),
11906
+ content: z8.string()
11907
+ })),
11908
+ createdAt: z8.string()
11909
+ });
11910
+ var KeywordMappingSchema = z8.object({
11911
+ keywords: z8.array(z8.string()),
11912
+ agent: z8.string().nullable(),
11913
+ skill: z8.string(),
11914
+ agentDescription: z8.string().optional(),
11915
+ skillDescription: z8.string().optional()
11916
+ });
11917
+ var DEFAULT_KEYWORD_MAPPINGS = [
11918
+ {
11919
+ keywords: ["test", "\uD14C\uC2A4\uD2B8", "vitest", "jest", "\uB2E8\uC704 \uD14C\uC2A4\uD2B8", "unit test"],
11920
+ agent: "test-runner",
11921
+ skill: "test",
11922
+ agentDescription: "\uD14C\uC2A4\uD2B8 \uC2E4\uD589 \uBC0F \uACB0\uACFC \uBD84\uC11D \uC5D0\uC774\uC804\uD2B8",
11923
+ skillDescription: "\uD14C\uC2A4\uD2B8 \uC791\uC131 \uBC0F \uC2E4\uD589 \uC2A4\uD0AC"
11924
+ },
11925
+ {
11926
+ keywords: ["api", "rest", "endpoint", "\uC5D4\uB4DC\uD3EC\uC778\uD2B8", "graphql"],
11927
+ agent: "api-scaffold",
11928
+ skill: "gen-api",
11929
+ agentDescription: "REST API \uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8 \uC0DD\uC131 \uC5D0\uC774\uC804\uD2B8",
11930
+ skillDescription: "API \uC5D4\uB4DC\uD3EC\uC778\uD2B8 \uC0DD\uC131 \uC2A4\uD0AC"
11931
+ },
11932
+ {
11933
+ keywords: ["component", "\uCEF4\uD3EC\uB10C\uD2B8", "react", "vue", "ui"],
11934
+ agent: "component-gen",
11935
+ skill: "gen-component",
11936
+ agentDescription: "UI \uCEF4\uD3EC\uB10C\uD2B8 \uC0DD\uC131 \uC5D0\uC774\uC804\uD2B8",
11937
+ skillDescription: "\uCEF4\uD3EC\uB10C\uD2B8 \uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8 \uC0DD\uC131 \uC2A4\uD0AC"
11938
+ },
11939
+ {
11940
+ keywords: ["db", "database", "prisma", "migration", "\uB9C8\uC774\uADF8\uB808\uC774\uC158", "\uB370\uC774\uD130\uBCA0\uC774\uC2A4"],
11941
+ agent: null,
11942
+ skill: "db-migrate",
11943
+ skillDescription: "\uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC2A4\uD0AC"
11944
+ },
11945
+ {
11946
+ keywords: ["doc", "\uBB38\uC11C", "readme", "jsdoc", "\uC8FC\uC11D", "documentation"],
11947
+ agent: "doc-generator",
11948
+ skill: "doc",
11949
+ agentDescription: "\uBB38\uC11C \uC0DD\uC131 \uC5D0\uC774\uC804\uD2B8",
11950
+ skillDescription: "\uBB38\uC11C\uD654 \uC2A4\uD0AC"
11951
+ },
11952
+ {
11953
+ keywords: ["type", "\uD0C0\uC785", "schema", "zod", "typescript"],
11954
+ agent: null,
11955
+ skill: "gen-types",
11956
+ skillDescription: "\uD0C0\uC785 \uC815\uC758 \uC0DD\uC131 \uC2A4\uD0AC"
11957
+ },
11958
+ {
11959
+ keywords: ["lint", "eslint", "prettier", "\uD3EC\uB9F7", "format"],
11960
+ agent: null,
11961
+ skill: "lint",
11962
+ skillDescription: "\uCF54\uB4DC \uB9B0\uD2B8 \uBC0F \uD3EC\uB9F7 \uC2A4\uD0AC"
11963
+ },
11964
+ {
11965
+ keywords: ["review", "\uB9AC\uBDF0", "\uAC80\uC99D", "code review", "\uCF54\uB4DC \uB9AC\uBDF0"],
11966
+ agent: "code-reviewer",
11967
+ skill: "review",
11968
+ agentDescription: "\uCF54\uB4DC \uB9AC\uBDF0 \uBC0F \uD488\uC9C8 \uAC80\uC99D \uC5D0\uC774\uC804\uD2B8",
11969
+ skillDescription: "\uCF54\uB4DC \uB9AC\uBDF0 \uC2A4\uD0AC"
11970
+ }
11971
+ ];
11972
+
11973
+ // src/core/prepare/document-analyzer.ts
11974
+ import * as fs16 from "fs";
11975
+ import * as path29 from "path";
11976
+ var DocumentAnalyzer = class {
11977
+ sddDir;
11978
+ constructor(projectRoot) {
11979
+ this.sddDir = path29.join(projectRoot, ".sdd");
11980
+ }
11981
+ /**
11982
+ * 기능 디렉토리의 모든 문서 분석
11983
+ */
11984
+ async analyzeFeature(featureName) {
11985
+ const featureDir = path29.join(this.sddDir, "specs", featureName);
11986
+ if (!fs16.existsSync(featureDir)) {
11987
+ throw new Error(`\uAE30\uB2A5 \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${featureDir}`);
11988
+ }
11989
+ const results = [];
11990
+ const documents = [
11991
+ { file: "tasks.md", type: "tasks" },
11992
+ { file: "plan.md", type: "plan" },
11993
+ { file: "spec.md", type: "spec" }
11994
+ ];
11995
+ for (const doc of documents) {
11996
+ const filePath = path29.join(featureDir, doc.file);
11997
+ if (fs16.existsSync(filePath)) {
11998
+ const analysis = await this.analyzeDocument(filePath, doc.type);
11999
+ results.push(analysis);
12000
+ }
12001
+ }
12002
+ return results;
12003
+ }
12004
+ /**
12005
+ * 단일 문서 분석
12006
+ */
12007
+ async analyzeDocument(filePath, type) {
12008
+ const content = fs16.readFileSync(filePath, "utf-8");
12009
+ const lines = content.split("\n");
12010
+ const analyzedLines = [];
12011
+ let taskCount = 0;
12012
+ for (let i = 0; i < lines.length; i++) {
12013
+ const line = lines[i];
12014
+ const keywords = this.extractKeywords(line);
12015
+ if (/^[\s]*-\s*\[[\sx]\]/.test(line)) {
12016
+ taskCount++;
12017
+ }
12018
+ if (keywords.length > 0) {
12019
+ analyzedLines.push({
12020
+ lineNumber: i + 1,
12021
+ content: line.trim(),
12022
+ keywords
12023
+ });
12024
+ }
12025
+ }
12026
+ return {
12027
+ file: filePath,
12028
+ type,
12029
+ lines: analyzedLines,
12030
+ taskCount
12031
+ };
12032
+ }
12033
+ /**
12034
+ * 라인에서 키워드 추출
12035
+ */
12036
+ extractKeywords(line) {
12037
+ const keywords = [];
12038
+ const lowerLine = line.toLowerCase();
12039
+ if (/test|테스트|vitest|jest|단위\s*테스트|unit\s*test/i.test(lowerLine)) {
12040
+ keywords.push("test");
12041
+ }
12042
+ if (/\bapi\b|rest|endpoint|엔드포인트|graphql/i.test(lowerLine)) {
12043
+ keywords.push("api");
12044
+ }
12045
+ if (/component|컴포넌트|\breact\b|\bvue\b|\bui\b/i.test(lowerLine)) {
12046
+ keywords.push("component");
12047
+ }
12048
+ if (/\bdb\b|database|prisma|migration|마이그레이션|데이터베이스/i.test(lowerLine)) {
12049
+ keywords.push("database");
12050
+ }
12051
+ if (/\bdoc\b|문서|readme|jsdoc|주석|documentation/i.test(lowerLine)) {
12052
+ keywords.push("doc");
12053
+ }
12054
+ if (/\btype\b|타입|schema|스키마|\bzod\b|typescript/i.test(lowerLine)) {
12055
+ keywords.push("type");
12056
+ }
12057
+ if (/lint|eslint|prettier|포맷|format/i.test(lowerLine)) {
12058
+ keywords.push("lint");
12059
+ }
12060
+ if (/review|리뷰|검증|code\s*review|코드\s*리뷰/i.test(lowerLine)) {
12061
+ keywords.push("review");
12062
+ }
12063
+ return [...new Set(keywords)];
12064
+ }
12065
+ /**
12066
+ * 분석 결과에서 DetectionSource 배열 생성
12067
+ */
12068
+ static toDetectionSources(analyses) {
12069
+ const sources = [];
12070
+ for (const analysis of analyses) {
12071
+ for (const line of analysis.lines) {
12072
+ for (const keyword of line.keywords) {
12073
+ sources.push({
12074
+ file: path29.basename(analysis.file),
12075
+ line: line.lineNumber,
12076
+ text: line.content,
12077
+ keyword
12078
+ });
12079
+ }
12080
+ }
12081
+ }
12082
+ return sources;
12083
+ }
12084
+ /**
12085
+ * 전체 태스크 수 계산
12086
+ */
12087
+ static getTotalTaskCount(analyses) {
12088
+ return analyses.reduce((sum, a) => sum + a.taskCount, 0);
12089
+ }
12090
+ };
12091
+
12092
+ // src/core/prepare/tool-detector.ts
12093
+ var ToolDetector = class {
12094
+ mappings;
12095
+ constructor(customMappings) {
12096
+ this.mappings = customMappings ?? DEFAULT_KEYWORD_MAPPINGS;
12097
+ }
12098
+ /**
12099
+ * 문서 분석 결과에서 필요한 도구 감지
12100
+ */
12101
+ detect(analyses) {
12102
+ const sources = DocumentAnalyzer.toDetectionSources(analyses);
12103
+ return this.detectFromSources(sources);
12104
+ }
12105
+ /**
12106
+ * DetectionSource 배열에서 도구 감지
12107
+ */
12108
+ detectFromSources(sources) {
12109
+ const agentMap = /* @__PURE__ */ new Map();
12110
+ const skillMap = /* @__PURE__ */ new Map();
12111
+ for (const source of sources) {
12112
+ const mapping = this.findMapping(source.keyword);
12113
+ if (!mapping) continue;
12114
+ if (mapping.agent) {
12115
+ const existing2 = agentMap.get(mapping.agent);
12116
+ if (existing2) {
12117
+ existing2.sources.push(source);
12118
+ } else {
12119
+ agentMap.set(mapping.agent, {
12120
+ type: "agent",
12121
+ name: mapping.agent,
12122
+ description: mapping.agentDescription ?? `${mapping.agent} \uC5D0\uC774\uC804\uD2B8`,
12123
+ sources: [source]
12124
+ });
12125
+ }
12126
+ }
12127
+ const existing = skillMap.get(mapping.skill);
12128
+ if (existing) {
12129
+ existing.sources.push(source);
12130
+ } else {
12131
+ skillMap.set(mapping.skill, {
12132
+ type: "skill",
12133
+ name: mapping.skill,
12134
+ description: mapping.skillDescription ?? `${mapping.skill} \uC2A4\uD0AC`,
12135
+ sources: [source]
12136
+ });
12137
+ }
12138
+ }
12139
+ return {
12140
+ agents: Array.from(agentMap.values()),
12141
+ skills: Array.from(skillMap.values()),
12142
+ totalSources: sources.length
12143
+ };
12144
+ }
12145
+ /**
12146
+ * 키워드에 해당하는 매핑 찾기
12147
+ */
12148
+ findMapping(keyword) {
12149
+ const lowerKeyword = keyword.toLowerCase();
12150
+ for (const mapping of this.mappings) {
12151
+ for (const kw of mapping.keywords) {
12152
+ if (lowerKeyword.includes(kw.toLowerCase()) || kw.toLowerCase().includes(lowerKeyword)) {
12153
+ return mapping;
12154
+ }
12155
+ }
12156
+ }
12157
+ return void 0;
12158
+ }
12159
+ /**
12160
+ * 감지된 도구 요약
12161
+ */
12162
+ static summarize(result) {
12163
+ const lines = [];
12164
+ lines.push(`## \uAC10\uC9C0\uB41C \uB3C4\uAD6C`);
12165
+ lines.push("");
12166
+ if (result.agents.length > 0) {
12167
+ lines.push(`### \uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8 (${result.agents.length}\uAC1C)`);
12168
+ lines.push("");
12169
+ lines.push("| \uC774\uB984 | \uC124\uBA85 | \uAC10\uC9C0 \uD69F\uC218 |");
12170
+ lines.push("|------|------|-----------|");
12171
+ for (const agent of result.agents) {
12172
+ lines.push(`| ${agent.name} | ${agent.description} | ${agent.sources.length} |`);
12173
+ }
12174
+ lines.push("");
12175
+ }
12176
+ if (result.skills.length > 0) {
12177
+ lines.push(`### \uC2A4\uD0AC (${result.skills.length}\uAC1C)`);
12178
+ lines.push("");
12179
+ lines.push("| \uC774\uB984 | \uC124\uBA85 | \uAC10\uC9C0 \uD69F\uC218 |");
12180
+ lines.push("|------|------|-----------|");
12181
+ for (const skill of result.skills) {
12182
+ lines.push(`| ${skill.name} | ${skill.description} | ${skill.sources.length} |`);
12183
+ }
12184
+ lines.push("");
12185
+ }
12186
+ return lines.join("\n");
12187
+ }
12188
+ };
12189
+
12190
+ // src/core/prepare/agent-scanner.ts
12191
+ import * as fs17 from "fs";
12192
+ import * as path30 from "path";
12193
+ import matter6 from "gray-matter";
12194
+ var AgentScanner = class {
12195
+ agentsDir;
12196
+ constructor(projectRoot) {
12197
+ this.agentsDir = path30.join(projectRoot, ".claude", "agents");
12198
+ }
12199
+ /**
12200
+ * 에이전트 디렉토리 존재 여부
12201
+ */
12202
+ exists() {
12203
+ return fs17.existsSync(this.agentsDir);
12204
+ }
12205
+ /**
12206
+ * 모든 에이전트 스캔
12207
+ */
12208
+ async scanAll() {
12209
+ if (!this.exists()) {
12210
+ return [];
12211
+ }
12212
+ const agents = [];
12213
+ const files = fs17.readdirSync(this.agentsDir);
12214
+ for (const file of files) {
12215
+ if (!file.endsWith(".md")) continue;
12216
+ const filePath = path30.join(this.agentsDir, file);
12217
+ const agent = await this.scanAgent(filePath);
12218
+ if (agent) {
12219
+ agents.push(agent);
12220
+ }
12221
+ }
12222
+ return agents;
12223
+ }
12224
+ /**
12225
+ * 단일 에이전트 파일 스캔
12226
+ */
12227
+ async scanAgent(filePath) {
12228
+ try {
12229
+ const content = fs17.readFileSync(filePath, "utf-8");
12230
+ const { data, content: body } = matter6(content);
12231
+ const metadata = this.parseMetadata(data, filePath);
12232
+ if (!metadata) {
12233
+ return null;
12234
+ }
12235
+ return {
12236
+ name: metadata.name,
12237
+ filePath,
12238
+ metadata,
12239
+ content: body.trim()
12240
+ };
12241
+ } catch {
12242
+ return null;
12243
+ }
12244
+ }
12245
+ /**
12246
+ * 특정 에이전트 존재 확인
12247
+ */
12248
+ hasAgent(name) {
12249
+ const filePath = path30.join(this.agentsDir, `${name}.md`);
12250
+ return fs17.existsSync(filePath);
12251
+ }
12252
+ /**
12253
+ * 특정 에이전트 가져오기
12254
+ */
12255
+ async getAgent(name) {
12256
+ const filePath = path30.join(this.agentsDir, `${name}.md`);
12257
+ if (!fs17.existsSync(filePath)) {
12258
+ return null;
12259
+ }
12260
+ return this.scanAgent(filePath);
12261
+ }
12262
+ /**
12263
+ * 메타데이터 파싱
12264
+ */
12265
+ parseMetadata(data, filePath) {
12266
+ try {
12267
+ if (typeof data.tools === "string") {
12268
+ data.tools = data.tools.split(",").map((t) => t.trim());
12269
+ }
12270
+ const result = AgentMetadataSchema.safeParse(data);
12271
+ if (result.success) {
12272
+ return result.data;
12273
+ }
12274
+ const fileName = path30.basename(filePath, ".md");
12275
+ const withName = { ...data, name: data.name ?? fileName };
12276
+ const retryResult = AgentMetadataSchema.safeParse(withName);
12277
+ if (retryResult.success) {
12278
+ return retryResult.data;
12279
+ }
12280
+ return null;
12281
+ } catch {
12282
+ return null;
12283
+ }
12284
+ }
12285
+ /**
12286
+ * 에이전트 디렉토리 경로
12287
+ */
12288
+ getAgentsDir() {
12289
+ return this.agentsDir;
12290
+ }
12291
+ /**
12292
+ * 에이전트 파일 경로 생성
12293
+ */
12294
+ getAgentFilePath(name) {
12295
+ return path30.join(this.agentsDir, `${name}.md`);
12296
+ }
12297
+ };
12298
+
12299
+ // src/core/prepare/skill-scanner.ts
12300
+ import * as fs18 from "fs";
12301
+ import * as path31 from "path";
12302
+ import matter7 from "gray-matter";
12303
+ var SkillScanner = class {
12304
+ skillsDir;
12305
+ constructor(projectRoot) {
12306
+ this.skillsDir = path31.join(projectRoot, ".claude", "skills");
12307
+ }
12308
+ /**
12309
+ * 스킬 디렉토리 존재 여부
12310
+ */
12311
+ exists() {
12312
+ return fs18.existsSync(this.skillsDir);
12313
+ }
12314
+ /**
12315
+ * 모든 스킬 스캔
12316
+ */
12317
+ async scanAll() {
12318
+ if (!this.exists()) {
12319
+ return [];
12320
+ }
12321
+ const skills = [];
12322
+ const entries = fs18.readdirSync(this.skillsDir, { withFileTypes: true });
12323
+ for (const entry of entries) {
12324
+ if (!entry.isDirectory()) continue;
12325
+ const skillDir = path31.join(this.skillsDir, entry.name);
12326
+ const skill = await this.scanSkill(skillDir);
12327
+ if (skill) {
12328
+ skills.push(skill);
12329
+ }
12330
+ }
12331
+ return skills;
12332
+ }
12333
+ /**
12334
+ * 단일 스킬 디렉토리 스캔
12335
+ */
12336
+ async scanSkill(skillDir) {
12337
+ const skillFile = path31.join(skillDir, "SKILL.md");
12338
+ if (!fs18.existsSync(skillFile)) {
12339
+ return null;
12340
+ }
12341
+ try {
12342
+ const content = fs18.readFileSync(skillFile, "utf-8");
12343
+ const { data, content: body } = matter7(content);
12344
+ const metadata = this.parseMetadata(data, skillDir);
12345
+ if (!metadata) {
12346
+ return null;
12347
+ }
12348
+ return {
12349
+ name: metadata.name,
12350
+ dirPath: skillDir,
12351
+ filePath: skillFile,
12352
+ metadata,
12353
+ content: body.trim()
12354
+ };
12355
+ } catch {
12356
+ return null;
12357
+ }
12358
+ }
12359
+ /**
12360
+ * 특정 스킬 존재 확인
12361
+ */
12362
+ hasSkill(name) {
12363
+ const skillDir = path31.join(this.skillsDir, name);
12364
+ const skillFile = path31.join(skillDir, "SKILL.md");
12365
+ return fs18.existsSync(skillFile);
12366
+ }
12367
+ /**
12368
+ * 특정 스킬 가져오기
12369
+ */
12370
+ async getSkill(name) {
12371
+ const skillDir = path31.join(this.skillsDir, name);
12372
+ if (!fs18.existsSync(skillDir)) {
12373
+ return null;
12374
+ }
12375
+ return this.scanSkill(skillDir);
12376
+ }
12377
+ /**
12378
+ * 메타데이터 파싱
12379
+ */
12380
+ parseMetadata(data, skillDir) {
12381
+ try {
12382
+ if (typeof data["allowed-tools"] === "string") {
12383
+ data["allowed-tools"] = data["allowed-tools"].split(",").map((t) => t.trim());
12384
+ }
12385
+ const result = SkillMetadataSchema.safeParse(data);
12386
+ if (result.success) {
12387
+ return result.data;
12388
+ }
12389
+ const dirName = path31.basename(skillDir);
12390
+ const withName = { ...data, name: data.name ?? dirName };
12391
+ const retryResult = SkillMetadataSchema.safeParse(withName);
12392
+ if (retryResult.success) {
12393
+ return retryResult.data;
12394
+ }
12395
+ return null;
12396
+ } catch {
12397
+ return null;
12398
+ }
12399
+ }
12400
+ /**
12401
+ * 스킬 디렉토리 경로
12402
+ */
12403
+ getSkillsDir() {
12404
+ return this.skillsDir;
12405
+ }
12406
+ /**
12407
+ * 스킬 디렉토리 경로 생성
12408
+ */
12409
+ getSkillDirPath(name) {
12410
+ return path31.join(this.skillsDir, name);
12411
+ }
12412
+ /**
12413
+ * 스킬 파일 경로 생성
12414
+ */
12415
+ getSkillFilePath(name) {
12416
+ return path31.join(this.skillsDir, name, "SKILL.md");
12417
+ }
12418
+ };
12419
+
12420
+ // src/core/prepare/agent-generator.ts
12421
+ import * as fs19 from "fs";
12422
+ import * as path32 from "path";
12423
+ var AgentGenerator = class {
12424
+ agentsDir;
12425
+ constructor(projectRoot) {
12426
+ this.agentsDir = path32.join(projectRoot, ".claude", "agents");
12427
+ }
12428
+ /**
12429
+ * 감지된 도구에서 에이전트 초안 생성
12430
+ */
12431
+ generate(tool, options) {
12432
+ const model = options?.model ?? "sonnet";
12433
+ const tools = options?.tools ?? this.getDefaultTools(tool.name);
12434
+ const content = this.generateContent(tool, model, tools);
12435
+ const filePath = path32.join(this.agentsDir, `${tool.name}.md`);
12436
+ return {
12437
+ name: tool.name,
12438
+ filePath,
12439
+ content
12440
+ };
12441
+ }
12442
+ /**
12443
+ * 에이전트 파일 생성
12444
+ */
12445
+ async writeAgent(agent) {
12446
+ if (!fs19.existsSync(this.agentsDir)) {
12447
+ fs19.mkdirSync(this.agentsDir, { recursive: true });
12448
+ }
12449
+ fs19.writeFileSync(agent.filePath, agent.content, "utf-8");
12450
+ }
12451
+ /**
12452
+ * 에이전트 콘텐츠 생성
12453
+ */
12454
+ generateContent(tool, model, tools) {
12455
+ const lines = [];
12456
+ lines.push("---");
12457
+ lines.push(`name: ${tool.name}`);
12458
+ lines.push(`description: ${tool.description}`);
12459
+ lines.push(`tools: ${tools.join(", ")}`);
12460
+ lines.push(`model: ${model}`);
12461
+ lines.push("---");
12462
+ lines.push("");
12463
+ lines.push(`# ${this.formatTitle(tool.name)} Agent`);
12464
+ lines.push("");
12465
+ lines.push(tool.description);
12466
+ lines.push("");
12467
+ lines.push("## Instructions");
12468
+ lines.push("");
12469
+ lines.push(...this.getInstructions(tool.name));
12470
+ lines.push("");
12471
+ if (tool.sources.length > 0) {
12472
+ lines.push("## Detection Sources");
12473
+ lines.push("");
12474
+ lines.push("> \uC774 \uC5D0\uC774\uC804\uD2B8\uB294 \uB2E4\uC74C \uBB38\uC11C\uC5D0\uC11C \uAC10\uC9C0\uB41C \uD0A4\uC6CC\uB4DC\uB97C \uAE30\uBC18\uC73C\uB85C \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
12475
+ lines.push("");
12476
+ for (const source of tool.sources.slice(0, 5)) {
12477
+ lines.push(`- ${source.file}:${source.line} - "${source.text.substring(0, 50)}..."`);
12478
+ }
12479
+ lines.push("");
12480
+ }
12481
+ return lines.join("\n");
12482
+ }
12483
+ /**
12484
+ * 에이전트별 기본 도구
12485
+ */
12486
+ getDefaultTools(name) {
12487
+ const toolMap = {
12488
+ "test-runner": ["Read", "Glob", "Grep", "Bash"],
12489
+ "api-scaffold": ["Read", "Write", "Edit", "Glob", "Grep"],
12490
+ "component-gen": ["Read", "Write", "Edit", "Glob"],
12491
+ "doc-generator": ["Read", "Write", "Edit", "Glob", "Grep"],
12492
+ "code-reviewer": ["Read", "Glob", "Grep", "Bash"]
12493
+ };
12494
+ return toolMap[name] ?? ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
12495
+ }
12496
+ /**
12497
+ * 에이전트별 지침
12498
+ */
12499
+ getInstructions(name) {
12500
+ const instructionMap = {
12501
+ "test-runner": [
12502
+ "1. \uD14C\uC2A4\uD2B8 \uD30C\uC77C \uD0D0\uC0C9 (`*.test.ts`, `*.spec.ts`)",
12503
+ "2. \uD14C\uC2A4\uD2B8 \uC2E4\uD589 (`vitest run` \uB610\uB294 `jest`)",
12504
+ "3. \uC2E4\uD328\uD55C \uD14C\uC2A4\uD2B8 \uBD84\uC11D",
12505
+ "4. \uCEE4\uBC84\uB9AC\uC9C0 \uB9AC\uD3EC\uD2B8 \uC0DD\uC131"
12506
+ ],
12507
+ "api-scaffold": [
12508
+ "1. API \uC0AC\uC591 \uBD84\uC11D",
12509
+ "2. \uB77C\uC6B0\uD130/\uCEE8\uD2B8\uB864\uB7EC \uBCF4\uC77C\uB7EC\uD50C\uB808\uC774\uD2B8 \uC0DD\uC131",
12510
+ "3. \uD0C0\uC785 \uC815\uC758 \uC0DD\uC131",
12511
+ "4. \uAE30\uBCF8 \uD14C\uC2A4\uD2B8 \uCF00\uC774\uC2A4 \uC0DD\uC131"
12512
+ ],
12513
+ "component-gen": [
12514
+ "1. \uCEF4\uD3EC\uB10C\uD2B8 \uC694\uAD6C\uC0AC\uD56D \uBD84\uC11D",
12515
+ "2. \uCEF4\uD3EC\uB10C\uD2B8 \uD30C\uC77C \uC0DD\uC131",
12516
+ "3. \uC2A4\uD0C0\uC77C \uD30C\uC77C \uC0DD\uC131",
12517
+ "4. \uAE30\uBCF8 \uD14C\uC2A4\uD2B8 \uC0DD\uC131"
12518
+ ],
12519
+ "doc-generator": [
12520
+ "1. \uCF54\uB4DC\uBCA0\uC774\uC2A4 \uBD84\uC11D",
12521
+ "2. JSDoc/TSDoc \uC8FC\uC11D \uC0DD\uC131",
12522
+ "3. README \uC5C5\uB370\uC774\uD2B8",
12523
+ "4. API \uBB38\uC11C \uC0DD\uC131"
12524
+ ],
12525
+ "code-reviewer": [
12526
+ "1. \uBCC0\uACBD\uB41C \uD30C\uC77C \uBD84\uC11D",
12527
+ "2. \uCF54\uB4DC \uD488\uC9C8 \uAC80\uC0AC",
12528
+ "3. \uBCF4\uC548 \uCDE8\uC57D\uC810 \uAC80\uC0AC",
12529
+ "4. \uAC1C\uC120 \uC81C\uC548 \uC791\uC131"
12530
+ ]
12531
+ };
12532
+ return instructionMap[name] ?? [
12533
+ "1. \uC791\uC5C5 \uC694\uAD6C\uC0AC\uD56D \uBD84\uC11D",
12534
+ "2. \uD544\uC694\uD55C \uD30C\uC77C \uD0D0\uC0C9",
12535
+ "3. \uC791\uC5C5 \uC218\uD589",
12536
+ "4. \uACB0\uACFC \uAC80\uC99D"
12537
+ ];
12538
+ }
12539
+ /**
12540
+ * 이름 포맷팅 (kebab-case → Title Case)
12541
+ */
12542
+ formatTitle(name) {
12543
+ return name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
12544
+ }
12545
+ };
12546
+
12547
+ // src/core/prepare/skill-generator.ts
12548
+ import * as fs20 from "fs";
12549
+ import * as path33 from "path";
12550
+ var SkillGenerator = class {
12551
+ skillsDir;
12552
+ constructor(projectRoot) {
12553
+ this.skillsDir = path33.join(projectRoot, ".claude", "skills");
12554
+ }
12555
+ /**
12556
+ * 감지된 도구에서 스킬 초안 생성
12557
+ */
12558
+ generate(tool, options) {
12559
+ const allowedTools = options?.allowedTools ?? this.getDefaultTools(tool.name);
12560
+ const content = this.generateContent(tool, allowedTools);
12561
+ const dirPath = path33.join(this.skillsDir, tool.name);
12562
+ const filePath = path33.join(dirPath, "SKILL.md");
12563
+ return {
12564
+ name: tool.name,
12565
+ dirPath,
12566
+ filePath,
12567
+ content
12568
+ };
12569
+ }
12570
+ /**
12571
+ * 스킬 파일 생성
12572
+ */
12573
+ async writeSkill(skill) {
12574
+ if (!fs20.existsSync(skill.dirPath)) {
12575
+ fs20.mkdirSync(skill.dirPath, { recursive: true });
12576
+ }
12577
+ fs20.writeFileSync(skill.filePath, skill.content, "utf-8");
12578
+ }
12579
+ /**
12580
+ * 스킬 콘텐츠 생성
12581
+ */
12582
+ generateContent(tool, allowedTools) {
12583
+ const lines = [];
12584
+ lines.push("---");
12585
+ lines.push(`name: ${tool.name}`);
12586
+ lines.push(`description: ${tool.description}`);
12587
+ lines.push(`allowed-tools: ${allowedTools.join(", ")}`);
12588
+ lines.push("---");
12589
+ lines.push("");
12590
+ lines.push(`# ${this.formatTitle(tool.name)} Skill`);
12591
+ lines.push("");
12592
+ lines.push(tool.description);
12593
+ lines.push("");
12594
+ lines.push("## Instructions");
12595
+ lines.push("");
12596
+ lines.push(...this.getInstructions(tool.name));
12597
+ lines.push("");
12598
+ lines.push("## Examples");
12599
+ lines.push("");
12600
+ lines.push(...this.getExamples(tool.name));
12601
+ lines.push("");
12602
+ if (tool.sources.length > 0) {
12603
+ lines.push("## Detection Sources");
12604
+ lines.push("");
12605
+ lines.push("> \uC774 \uC2A4\uD0AC\uC740 \uB2E4\uC74C \uBB38\uC11C\uC5D0\uC11C \uAC10\uC9C0\uB41C \uD0A4\uC6CC\uB4DC\uB97C \uAE30\uBC18\uC73C\uB85C \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
12606
+ lines.push("");
12607
+ for (const source of tool.sources.slice(0, 5)) {
12608
+ lines.push(`- ${source.file}:${source.line} - "${source.text.substring(0, 50)}..."`);
12609
+ }
12610
+ lines.push("");
12611
+ }
12612
+ return lines.join("\n");
12613
+ }
12614
+ /**
12615
+ * 스킬별 기본 도구
12616
+ */
12617
+ getDefaultTools(name) {
12618
+ const toolMap = {
12619
+ "test": ["Read", "Glob", "Grep", "Bash"],
12620
+ "gen-api": ["Read", "Write", "Edit", "Glob"],
12621
+ "gen-component": ["Read", "Write", "Edit", "Glob"],
12622
+ "db-migrate": ["Read", "Write", "Bash"],
12623
+ "doc": ["Read", "Write", "Edit", "Glob", "Grep"],
12624
+ "gen-types": ["Read", "Write", "Edit", "Glob"],
12625
+ "lint": ["Read", "Bash"],
12626
+ "review": ["Read", "Glob", "Grep"]
12627
+ };
12628
+ return toolMap[name] ?? ["Read", "Write", "Edit", "Glob"];
12629
+ }
12630
+ /**
12631
+ * 스킬별 지침
12632
+ */
12633
+ getInstructions(name) {
12634
+ const instructionMap = {
12635
+ "test": [
12636
+ "1. \uB300\uC0C1 \uD30C\uC77C \uB610\uB294 \uBAA8\uB4C8 \uBD84\uC11D",
12637
+ "2. \uD14C\uC2A4\uD2B8 \uCF00\uC774\uC2A4 \uC791\uC131",
12638
+ "3. \uD14C\uC2A4\uD2B8 \uC2E4\uD589 \uBC0F \uACB0\uACFC \uD655\uC778",
12639
+ "4. \uCEE4\uBC84\uB9AC\uC9C0 \uAC1C\uC120 \uC81C\uC548"
12640
+ ],
12641
+ "gen-api": [
12642
+ "1. API \uC694\uAD6C\uC0AC\uD56D \uBD84\uC11D",
12643
+ "2. \uC5D4\uB4DC\uD3EC\uC778\uD2B8 \uAD6C\uC870 \uC124\uACC4",
12644
+ "3. \uB77C\uC6B0\uD130/\uCEE8\uD2B8\uB864\uB7EC \uCF54\uB4DC \uC0DD\uC131",
12645
+ "4. \uD0C0\uC785 \uC815\uC758 \uC0DD\uC131"
12646
+ ],
12647
+ "gen-component": [
12648
+ "1. \uCEF4\uD3EC\uB10C\uD2B8 \uC694\uAD6C\uC0AC\uD56D \uBD84\uC11D",
12649
+ "2. \uCEF4\uD3EC\uB10C\uD2B8 \uAD6C\uC870 \uC124\uACC4",
12650
+ "3. \uCEF4\uD3EC\uB10C\uD2B8 \uCF54\uB4DC \uC0DD\uC131",
12651
+ "4. \uC2A4\uD0C0\uC77C \uBC0F \uD14C\uC2A4\uD2B8 \uC0DD\uC131"
12652
+ ],
12653
+ "db-migrate": [
12654
+ "1. \uC2A4\uD0A4\uB9C8 \uBCC0\uACBD\uC0AC\uD56D \uBD84\uC11D",
12655
+ "2. \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD30C\uC77C \uC0DD\uC131",
12656
+ "3. \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC2E4\uD589",
12657
+ "4. \uB864\uBC31 \uC2A4\uD06C\uB9BD\uD2B8 \uAC80\uC99D"
12658
+ ],
12659
+ "doc": [
12660
+ "1. \uCF54\uB4DC\uBCA0\uC774\uC2A4 \uBD84\uC11D",
12661
+ "2. \uBB38\uC11C\uD654 \uB300\uC0C1 \uC2DD\uBCC4",
12662
+ "3. \uBB38\uC11C \uC0DD\uC131 (README, API \uBB38\uC11C \uB4F1)",
12663
+ "4. \uAE30\uC874 \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"
12664
+ ],
12665
+ "gen-types": [
12666
+ "1. \uB370\uC774\uD130 \uAD6C\uC870 \uBD84\uC11D",
12667
+ "2. TypeScript \uD0C0\uC785/\uC778\uD130\uD398\uC774\uC2A4 \uC0DD\uC131",
12668
+ "3. Zod \uC2A4\uD0A4\uB9C8 \uC0DD\uC131 (\uD544\uC694\uC2DC)",
12669
+ "4. \uD0C0\uC785 \uB0B4\uBCF4\uB0B4\uAE30 \uC124\uC815"
12670
+ ],
12671
+ "lint": [
12672
+ "1. \uB9B0\uD2B8 \uADDC\uCE59 \uD655\uC778",
12673
+ "2. \uB9B0\uD2B8 \uC624\uB958 \uAC80\uC0AC",
12674
+ "3. \uC790\uB3D9 \uC218\uC815 \uAC00\uB2A5\uD55C \uC624\uB958 \uC218\uC815",
12675
+ "4. \uC218\uB3D9 \uC218\uC815 \uD544\uC694\uD55C \uD56D\uBAA9 \uB9AC\uD3EC\uD2B8"
12676
+ ],
12677
+ "review": [
12678
+ "1. \uBCC0\uACBD\uB41C \uCF54\uB4DC \uBD84\uC11D",
12679
+ "2. \uCF54\uB4DC \uD488\uC9C8 \uAC80\uC0AC",
12680
+ "3. \uC7A0\uC7AC\uC801 \uBB38\uC81C\uC810 \uC2DD\uBCC4",
12681
+ "4. \uAC1C\uC120 \uC81C\uC548 \uC791\uC131"
12682
+ ]
12683
+ };
12684
+ return instructionMap[name] ?? [
12685
+ "1. \uC694\uAD6C\uC0AC\uD56D \uBD84\uC11D",
12686
+ "2. \uC791\uC5C5 \uC218\uD589",
12687
+ "3. \uACB0\uACFC \uAC80\uC99D",
12688
+ "4. \uB9AC\uD3EC\uD2B8 \uC791\uC131"
12689
+ ];
12690
+ }
12691
+ /**
12692
+ * 스킬별 사용 예시
12693
+ */
12694
+ getExamples(name) {
12695
+ const exampleMap = {
12696
+ "test": [
12697
+ "**\uC0AC\uC6A9\uC790**: UserService\uC5D0 \uB300\uD55C \uD14C\uC2A4\uD2B8\uB97C \uC791\uC131\uD574\uC918",
12698
+ "",
12699
+ "**\uC751\uB2F5**: UserService\uC758 \uC8FC\uC694 \uBA54\uC11C\uB4DC\uC5D0 \uB300\uD55C \uD14C\uC2A4\uD2B8 \uCF00\uC774\uC2A4\uB97C \uC791\uC131\uD558\uACA0\uC2B5\uB2C8\uB2E4..."
12700
+ ],
12701
+ "gen-api": [
12702
+ "**\uC0AC\uC6A9\uC790**: \uC0AC\uC6A9\uC790 CRUD API\uB97C \uC0DD\uC131\uD574\uC918",
12703
+ "",
12704
+ "**\uC751\uB2F5**: \uC0AC\uC6A9\uC790 \uAD00\uB9AC\uB97C \uC704\uD55C CRUD API \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uB97C \uC0DD\uC131\uD558\uACA0\uC2B5\uB2C8\uB2E4..."
12705
+ ],
12706
+ "gen-component": [
12707
+ "**\uC0AC\uC6A9\uC790**: \uBAA8\uB2EC \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uB9CC\uB4E4\uC5B4\uC918",
12708
+ "",
12709
+ "**\uC751\uB2F5**: \uC7AC\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uBAA8\uB2EC \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uC0DD\uC131\uD558\uACA0\uC2B5\uB2C8\uB2E4..."
12710
+ ],
12711
+ "doc": [
12712
+ "**\uC0AC\uC6A9\uC790**: API \uBB38\uC11C\uB97C \uC5C5\uB370\uC774\uD2B8\uD574\uC918",
12713
+ "",
12714
+ "**\uC751\uB2F5**: \uD604\uC7AC API \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uB97C \uBD84\uC11D\uD558\uC5EC \uBB38\uC11C\uB97C \uC5C5\uB370\uC774\uD2B8\uD558\uACA0\uC2B5\uB2C8\uB2E4..."
12715
+ ]
12716
+ };
12717
+ return exampleMap[name] ?? [
12718
+ `**\uC0AC\uC6A9\uC790**: ${this.formatTitle(name)} \uC791\uC5C5\uC744 \uC218\uD589\uD574\uC918`,
12719
+ "",
12720
+ "**\uC751\uB2F5**: \uC694\uCCAD\uD558\uC2E0 \uC791\uC5C5\uC744 \uC218\uD589\uD558\uACA0\uC2B5\uB2C8\uB2E4..."
12721
+ ];
12722
+ }
12723
+ /**
12724
+ * 이름 포맷팅 (kebab-case → Title Case)
12725
+ */
12726
+ formatTitle(name) {
12727
+ return name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
12728
+ }
12729
+ };
12730
+
12731
+ // src/core/prepare/prepare-report.ts
12732
+ import * as fs21 from "fs";
12733
+ import * as path34 from "path";
12734
+ var PrepareReportGenerator = class {
12735
+ sddDir;
12736
+ constructor(projectRoot) {
12737
+ this.sddDir = path34.join(projectRoot, ".sdd");
12738
+ }
12739
+ /**
12740
+ * 보고서 생성
12741
+ */
12742
+ generate(input) {
12743
+ const agentMissing = input.agentChecks.filter((c) => c.status === "missing").length;
12744
+ const agentExisting = input.agentChecks.filter((c) => c.status === "exists").length;
12745
+ const skillMissing = input.skillChecks.filter((c) => c.status === "missing").length;
12746
+ const skillExisting = input.skillChecks.filter((c) => c.status === "exists").length;
12747
+ return {
12748
+ feature: input.feature,
12749
+ totalTasks: input.totalTasks,
12750
+ agents: {
12751
+ required: input.agentChecks.length,
12752
+ existing: agentExisting,
12753
+ missing: agentMissing,
12754
+ checks: input.agentChecks
12755
+ },
12756
+ skills: {
12757
+ required: input.skillChecks.length,
12758
+ existing: skillExisting,
12759
+ missing: skillMissing,
12760
+ checks: input.skillChecks
12761
+ },
12762
+ proposals: [
12763
+ ...input.generatedAgents.map((a) => ({
12764
+ type: "agent",
12765
+ name: a.name,
12766
+ filePath: a.filePath,
12767
+ content: a.content
12768
+ })),
12769
+ ...input.generatedSkills.map((s) => ({
12770
+ type: "skill",
12771
+ name: s.name,
12772
+ filePath: s.filePath,
12773
+ content: s.content
12774
+ }))
12775
+ ],
12776
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
12777
+ };
12778
+ }
12779
+ /**
12780
+ * 마크다운 보고서 생성
12781
+ */
12782
+ toMarkdown(report) {
12783
+ const lines = [];
12784
+ lines.push(`# Prepare: ${report.feature}`);
12785
+ lines.push("");
12786
+ lines.push(`> \uC0DD\uC131\uC77C: ${report.createdAt.split("T")[0]}`);
12787
+ lines.push("");
12788
+ lines.push("## \uBD84\uC11D \uC694\uC57D");
12789
+ lines.push("");
12790
+ lines.push(`- \uCD1D \uC791\uC5C5 \uC218: ${report.totalTasks}\uAC1C`);
12791
+ lines.push(`- \uD544\uC694 \uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8: ${report.agents.required}\uAC1C (\uC874\uC7AC: ${report.agents.existing}, \uB204\uB77D: ${report.agents.missing})`);
12792
+ lines.push(`- \uD544\uC694 \uC2A4\uD0AC: ${report.skills.required}\uAC1C (\uC874\uC7AC: ${report.skills.existing}, \uB204\uB77D: ${report.skills.missing})`);
12793
+ lines.push("");
12794
+ if (report.agents.checks.length > 0) {
12795
+ lines.push("## \uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8 \uC810\uAC80");
12796
+ lines.push("");
12797
+ lines.push("> \uC704\uCE58: `.claude/agents/`");
12798
+ lines.push("");
12799
+ lines.push("| \uC5D0\uC774\uC804\uD2B8 | \uC124\uBA85 | \uC0C1\uD0DC | \uC870\uCE58 |");
12800
+ lines.push("|----------|------|------|------|");
12801
+ for (const check of report.agents.checks) {
12802
+ const status = check.status === "exists" ? "\u2705 \uC874\uC7AC" : "\u274C \uC5C6\uC74C";
12803
+ lines.push(`| ${check.tool.name} | ${check.tool.description} | ${status} | ${check.action} |`);
12804
+ }
12805
+ lines.push("");
12806
+ }
12807
+ if (report.skills.checks.length > 0) {
12808
+ lines.push("## \uC2A4\uD0AC \uC810\uAC80");
12809
+ lines.push("");
12810
+ lines.push("> \uC704\uCE58: `.claude/skills/<name>/SKILL.md`");
12811
+ lines.push("");
12812
+ lines.push("| \uC2A4\uD0AC | \uC124\uBA85 | \uC0C1\uD0DC | \uC870\uCE58 |");
12813
+ lines.push("|------|------|------|------|");
12814
+ for (const check of report.skills.checks) {
12815
+ const status = check.status === "exists" ? "\u2705 \uC874\uC7AC" : "\u274C \uC5C6\uC74C";
12816
+ lines.push(`| ${check.tool.name} | ${check.tool.description} | ${status} | ${check.action} |`);
12817
+ }
12818
+ lines.push("");
12819
+ }
12820
+ const proposals = report.proposals.filter((p) => p.content);
12821
+ if (proposals.length > 0) {
12822
+ lines.push("## \uCD94\uAC00/\uC218\uC815 \uC81C\uC548");
12823
+ lines.push("");
12824
+ for (const proposal of proposals) {
12825
+ const typeLabel = proposal.type === "agent" ? "\uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8" : "\uC2A4\uD0AC";
12826
+ lines.push(`### \uC0C8 ${typeLabel}: ${proposal.name}`);
12827
+ lines.push("");
12828
+ lines.push(`**\uD30C\uC77C:** \`${proposal.filePath}\``);
12829
+ lines.push("");
12830
+ lines.push("```markdown");
12831
+ lines.push(proposal.content);
12832
+ lines.push("```");
12833
+ lines.push("");
12834
+ }
12835
+ }
12836
+ const pendingApprovals = report.proposals.filter((p) => p.content);
12837
+ if (pendingApprovals.length > 0) {
12838
+ lines.push("## \uC2B9\uC778 \uB300\uAE30");
12839
+ lines.push("");
12840
+ for (const proposal of pendingApprovals) {
12841
+ const typeLabel = proposal.type === "agent" ? "\uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8" : "\uC2A4\uD0AC";
12842
+ lines.push(`- [ ] ${proposal.name} ${typeLabel} \uCD94\uAC00 \u2192 \`${proposal.filePath}\``);
12843
+ }
12844
+ lines.push("");
12845
+ }
12846
+ return lines.join("\n");
12847
+ }
12848
+ /**
12849
+ * 보고서 파일 저장
12850
+ */
12851
+ async writeReport(feature, report) {
12852
+ const featureDir = path34.join(this.sddDir, "specs", feature);
12853
+ if (!fs21.existsSync(featureDir)) {
12854
+ fs21.mkdirSync(featureDir, { recursive: true });
12855
+ }
12856
+ const filePath = path34.join(featureDir, "prepare.md");
12857
+ const content = this.toMarkdown(report);
12858
+ fs21.writeFileSync(filePath, content, "utf-8");
12859
+ return filePath;
12860
+ }
12861
+ };
12862
+
12863
+ // src/cli/commands/prepare.ts
12864
+ init_fs();
12865
+ init_errors();
12866
+ init_types();
12867
+ async function executePrepare(feature, options, projectRoot) {
12868
+ try {
12869
+ const docAnalyzer = new DocumentAnalyzer(projectRoot);
12870
+ const analyses = await docAnalyzer.analyzeFeature(feature);
12871
+ if (analyses.length === 0) {
12872
+ return failure(new Error(`\uAE30\uB2A5 '${feature}'\uC758 \uBB38\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`));
12873
+ }
12874
+ const toolDetector = new ToolDetector();
12875
+ const detected = toolDetector.detect(analyses);
12876
+ const agentScanner = new AgentScanner(projectRoot);
12877
+ const existingAgents = await agentScanner.scanAll();
12878
+ const existingAgentNames = new Set(existingAgents.map((a) => a.name));
12879
+ const skillScanner = new SkillScanner(projectRoot);
12880
+ const existingSkills = await skillScanner.scanAll();
12881
+ const existingSkillNames = new Set(existingSkills.map((s) => s.name));
12882
+ const agentChecks = detected.agents.map((agent) => ({
12883
+ tool: agent,
12884
+ status: existingAgentNames.has(agent.name) ? "exists" : "missing",
12885
+ filePath: agentScanner.getAgentFilePath(agent.name),
12886
+ action: existingAgentNames.has(agent.name) ? "-" : "\uCD94\uAC00 \uD544\uC694"
12887
+ }));
12888
+ const skillChecks = detected.skills.map((skill) => ({
12889
+ tool: skill,
12890
+ status: existingSkillNames.has(skill.name) ? "exists" : "missing",
12891
+ filePath: skillScanner.getSkillFilePath(skill.name),
12892
+ action: existingSkillNames.has(skill.name) ? "-" : "\uCD94\uAC00 \uD544\uC694"
12893
+ }));
12894
+ const agentGenerator = new AgentGenerator(projectRoot);
12895
+ const skillGenerator = new SkillGenerator(projectRoot);
12896
+ const generatedAgents = [];
12897
+ const generatedSkills = [];
12898
+ for (const check of agentChecks) {
12899
+ if (check.status === "missing") {
12900
+ generatedAgents.push(agentGenerator.generate(check.tool));
12901
+ }
12902
+ }
12903
+ for (const check of skillChecks) {
12904
+ if (check.status === "missing") {
12905
+ generatedSkills.push(skillGenerator.generate(check.tool));
12906
+ }
12907
+ }
12908
+ const reportGenerator = new PrepareReportGenerator(projectRoot);
12909
+ const totalTasks = DocumentAnalyzer.getTotalTaskCount(analyses);
12910
+ const report = reportGenerator.generate({
12911
+ feature,
12912
+ totalTasks,
12913
+ agentChecks,
12914
+ skillChecks,
12915
+ generatedAgents,
12916
+ generatedSkills
12917
+ });
12918
+ const result = {
12919
+ report,
12920
+ created: {
12921
+ agents: [],
12922
+ skills: []
12923
+ }
12924
+ };
12925
+ if (!options.dryRun) {
12926
+ result.reportPath = await reportGenerator.writeReport(feature, report);
12927
+ if (options.autoApprove) {
12928
+ for (const agent of generatedAgents) {
12929
+ await agentGenerator.writeAgent(agent);
12930
+ result.created.agents.push(agent.filePath);
12931
+ }
12932
+ for (const skill of generatedSkills) {
12933
+ await skillGenerator.writeSkill(skill);
12934
+ result.created.skills.push(skill.filePath);
12935
+ }
12936
+ }
12937
+ }
12938
+ return success(result);
12939
+ } catch (error2) {
12940
+ return failure(error2 instanceof Error ? error2 : new Error(String(error2)));
12941
+ }
12942
+ }
12943
+ function registerPrepareCommand(program2) {
12944
+ program2.command("prepare <feature>").description("\uAD6C\uD604 \uC804 \uD544\uC694\uD55C \uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8\uC640 \uC2A4\uD0AC\uC744 \uC810\uAC80\uD569\uB2C8\uB2E4").option("--dry-run", "\uBCC0\uACBD \uC5C6\uC774 \uBD84\uC11D \uACB0\uACFC\uB9CC \uCD9C\uB825").option("--auto-approve", "\uB204\uB77D\uB41C \uB3C4\uAD6C\uB97C \uC790\uB3D9\uC73C\uB85C \uC0DD\uC131").option("--json", "JSON \uD615\uC2DD \uCD9C\uB825").action(async (feature, options) => {
12945
+ try {
12946
+ await runPrepare(feature, options);
12947
+ } catch (error2) {
12948
+ error(error2 instanceof Error ? error2.message : String(error2));
12949
+ process.exit(ExitCode.GENERAL_ERROR);
12950
+ }
12951
+ });
12952
+ }
12953
+ async function runPrepare(feature, options) {
12954
+ const projectRoot = await findSddRoot();
12955
+ if (!projectRoot) {
12956
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
12957
+ process.exit(ExitCode.GENERAL_ERROR);
12958
+ }
12959
+ info(`\uAE30\uB2A5 '${feature}' \uC900\uBE44 \uC810\uAC80 \uC911...`);
12960
+ newline();
12961
+ const result = await executePrepare(feature, options, projectRoot);
12962
+ if (!result.success) {
12963
+ error(result.error.message);
12964
+ process.exit(ExitCode.GENERAL_ERROR);
12965
+ }
12966
+ const { report, reportPath, created } = result.data;
12967
+ if (options.json) {
12968
+ console.log(JSON.stringify(report, null, 2));
12969
+ return;
12970
+ }
12971
+ success2("\uBD84\uC11D \uC644\uB8CC");
12972
+ newline();
12973
+ console.log(`\uCD1D \uC791\uC5C5 \uC218: ${report.totalTasks}\uAC1C`);
12974
+ console.log(`\uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8: ${report.agents.required}\uAC1C \uD544\uC694 (\uC874\uC7AC: ${report.agents.existing}, \uB204\uB77D: ${report.agents.missing})`);
12975
+ console.log(`\uC2A4\uD0AC: ${report.skills.required}\uAC1C \uD544\uC694 (\uC874\uC7AC: ${report.skills.existing}, \uB204\uB77D: ${report.skills.missing})`);
12976
+ newline();
12977
+ const missingAgents = report.agents.checks.filter((c) => c.status === "missing");
12978
+ const missingSkills = report.skills.checks.filter((c) => c.status === "missing");
12979
+ if (missingAgents.length > 0) {
12980
+ warn("\uB204\uB77D\uB41C \uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8:");
12981
+ for (const check of missingAgents) {
12982
+ console.log(` - ${check.tool.name}: ${check.tool.description}`);
12983
+ }
12984
+ newline();
12985
+ }
12986
+ if (missingSkills.length > 0) {
12987
+ warn("\uB204\uB77D\uB41C \uC2A4\uD0AC:");
12988
+ for (const check of missingSkills) {
12989
+ console.log(` - ${check.tool.name}: ${check.tool.description}`);
12990
+ }
12991
+ newline();
12992
+ }
12993
+ if (created.agents.length > 0 || created.skills.length > 0) {
12994
+ success2("\uC0DD\uC131\uB41C \uD30C\uC77C:");
12995
+ for (const file of [...created.agents, ...created.skills]) {
12996
+ console.log(` - ${path35.relative(projectRoot, file)}`);
12997
+ }
12998
+ newline();
12999
+ }
13000
+ if (reportPath) {
13001
+ info(`\uBCF4\uACE0\uC11C: ${path35.relative(projectRoot, reportPath)}`);
13002
+ }
13003
+ if (missingAgents.length > 0 || missingSkills.length > 0) {
13004
+ if (!options.autoApprove && !options.dryRun) {
13005
+ newline();
13006
+ info("\uB204\uB77D\uB41C \uB3C4\uAD6C\uB97C \uC790\uB3D9 \uC0DD\uC131\uD558\uB824\uBA74: sdd prepare " + feature + " --auto-approve");
13007
+ }
13008
+ }
13009
+ }
13010
+
11838
13011
  // src/cli/index.ts
11839
13012
  var require2 = createRequire(import.meta.url);
11840
13013
  var pkg = require2("../../package.json");
@@ -11857,6 +13030,7 @@ registerWatchCommand(program);
11857
13030
  registerQualityCommand(program);
11858
13031
  registerReportCommand(program);
11859
13032
  registerSearchCommand(program);
13033
+ registerPrepareCommand(program);
11860
13034
  function run() {
11861
13035
  program.parse();
11862
13036
  }