sdd-tool 0.4.0 → 0.5.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, path20) {
132
- super(code, formatMessage(code, path20), ExitCode.FILE_SYSTEM_ERROR);
131
+ constructor(code, path27) {
132
+ super(code, formatMessage(code, path27), ExitCode.FILE_SYSTEM_ERROR);
133
133
  this.name = "FileSystemError";
134
- this.path = path20;
134
+ this.path = path27;
135
135
  }
136
136
  };
137
137
  ValidationError = class extends SddError {
@@ -2503,28 +2503,9 @@ sdd transition guide
2503
2503
  }
2504
2504
 
2505
2505
  // src/cli/commands/init.ts
2506
- function registerInitCommand(program2) {
2507
- program2.command("init").description("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4").option("-f, --force", "\uAE30\uC874 .sdd/ \uB514\uB809\uD1A0\uB9AC \uB36E\uC5B4\uC4F0\uAE30").action(async (options) => {
2508
- try {
2509
- await runInit(options);
2510
- } catch (error2) {
2511
- error(error2 instanceof Error ? error2.message : String(error2));
2512
- process.exit(ExitCode.GENERAL_ERROR);
2513
- }
2514
- });
2515
- }
2516
- async function runInit(options) {
2517
- const cwd = process.cwd();
2518
- const sddPath = path2.join(cwd, ".sdd");
2519
- if (await directoryExists(sddPath)) {
2520
- if (!options.force) {
2521
- error(".sdd/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. --force \uC635\uC158\uC73C\uB85C \uB36E\uC5B4\uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
2522
- process.exit(ExitCode.GENERAL_ERROR);
2523
- }
2524
- warn("\uAE30\uC874 .sdd/ \uB514\uB809\uD1A0\uB9AC\uB97C \uB36E\uC5B4\uC501\uB2C8\uB2E4.");
2525
- }
2526
- info("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4...");
2527
- const directories = [
2506
+ init_types();
2507
+ function getInitDirectories() {
2508
+ return [
2528
2509
  ".sdd",
2529
2510
  ".sdd/specs",
2530
2511
  ".sdd/changes",
@@ -2533,48 +2514,10 @@ async function runInit(options) {
2533
2514
  ".claude",
2534
2515
  ".claude/commands"
2535
2516
  ];
2536
- for (const dir of directories) {
2537
- const result = await ensureDir(path2.join(cwd, dir));
2538
- if (!result.success) {
2539
- error(`\uB514\uB809\uD1A0\uB9AC \uC0DD\uC131 \uC2E4\uD328: ${dir}`);
2540
- process.exit(ExitCode.FILE_SYSTEM_ERROR);
2541
- }
2542
- }
2543
- await createDefaultFiles(cwd);
2544
- await copyTemplates(cwd);
2545
- await createClaudeCommands(cwd);
2546
- success2("SDD \uD504\uB85C\uC81D\uD2B8\uAC00 \uCD08\uAE30\uD654\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
2547
- newline();
2548
- info("\uC0DD\uC131\uB41C \uAD6C\uC870:");
2549
- listItem(".sdd/");
2550
- listItem("AGENTS.md", 1);
2551
- listItem("constitution.md", 1);
2552
- listItem("specs/", 1);
2553
- listItem("changes/", 1);
2554
- listItem("archive/", 1);
2555
- listItem("templates/", 1);
2556
- listItem(".claude/");
2557
- listItem("commands/", 1);
2558
- newline();
2559
- info("Claude \uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC:");
2560
- listItem("/sdd.start - \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC2DC\uC791 (\uD1B5\uD569 \uC9C4\uC785\uC810)");
2561
- listItem("/sdd.constitution - \uD504\uB85C\uC81D\uD2B8 \uC6D0\uCE59 \uAD00\uB9AC");
2562
- listItem("/sdd.new - \uC0C8 \uAE30\uB2A5 \uBA85\uC138 \uC791\uC131");
2563
- listItem("/sdd.plan - \uAD6C\uD604 \uACC4\uD68D \uC791\uC131");
2564
- listItem("/sdd.tasks - \uC791\uC5C5 \uBD84\uD574");
2565
- listItem("/sdd.implement - \uAD6C\uD604 \uC9C4\uD589");
2566
- listItem("/sdd.validate - \uC2A4\uD399 \uAC80\uC99D");
2567
- listItem("/sdd.status - \uC0C1\uD0DC \uD655\uC778");
2568
- listItem("/sdd.change - \uBCC0\uACBD \uC81C\uC548");
2569
- newline();
2570
- info("\uB2E4\uC74C \uB2E8\uACC4:");
2571
- listItem("constitution.md\uB97C \uC218\uC815\uD558\uC5EC \uD504\uB85C\uC81D\uD2B8 \uC6D0\uCE59\uC744 \uC815\uC758\uD558\uC138\uC694");
2572
- listItem("/sdd.new \uB85C \uCCAB \uBC88\uC9F8 \uAE30\uB2A5 \uBA85\uC138\uB97C \uC791\uC131\uD558\uC138\uC694");
2573
2517
  }
2574
- async function createDefaultFiles(cwd) {
2575
- const projectName = path2.basename(cwd);
2518
+ function generateConstitutionContent(projectName) {
2576
2519
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2577
- const constitution = `---
2520
+ return `---
2578
2521
  version: 1.0.0
2579
2522
  created: ${today}
2580
2523
  ---
@@ -2609,13 +2552,10 @@ created: ${today}
2609
2552
 
2610
2553
  - \uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0: 80% \uC774\uC0C1(SHOULD)
2611
2554
  `;
2612
- await writeFile(path2.join(cwd, ".sdd", "constitution.md"), constitution);
2613
- const agents = generateAgentsMd({ projectName });
2614
- await writeFile(path2.join(cwd, ".sdd", "AGENTS.md"), agents);
2615
2555
  }
2616
- async function copyTemplates(cwd) {
2556
+ function generateSpecTemplate() {
2617
2557
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2618
- const specTemplate = `---
2558
+ return `---
2619
2559
  status: draft
2620
2560
  created: ${today}
2621
2561
  depends: null
@@ -2643,7 +2583,69 @@ depends: null
2643
2583
 
2644
2584
  \uCD94\uAC00 \uC124\uBA85\uC774\uB098 \uC81C\uC57D \uC870\uAC74
2645
2585
  `;
2646
- const proposalTemplate = `---
2586
+ }
2587
+ async function executeInit(projectPath, options) {
2588
+ const sddPath = path2.join(projectPath, ".sdd");
2589
+ const claudePath = path2.join(projectPath, ".claude");
2590
+ if (await directoryExists(sddPath)) {
2591
+ if (!options.force) {
2592
+ return failure(new Error(".sdd/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. --force \uC635\uC158\uC73C\uB85C \uB36E\uC5B4\uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4."));
2593
+ }
2594
+ }
2595
+ const directories = getInitDirectories();
2596
+ const createdDirs = [];
2597
+ for (const dir of directories) {
2598
+ const result = await ensureDir(path2.join(projectPath, dir));
2599
+ if (!result.success) {
2600
+ return failure(new Error(`\uB514\uB809\uD1A0\uB9AC \uC0DD\uC131 \uC2E4\uD328: ${dir}`));
2601
+ }
2602
+ createdDirs.push(dir);
2603
+ }
2604
+ const createdFiles = [];
2605
+ const projectName = path2.basename(projectPath);
2606
+ const constitutionContent = generateConstitutionContent(projectName);
2607
+ await writeFile(path2.join(sddPath, "constitution.md"), constitutionContent);
2608
+ createdFiles.push(".sdd/constitution.md");
2609
+ const agentsContent = generateAgentsMd({ projectName });
2610
+ await writeFile(path2.join(sddPath, "AGENTS.md"), agentsContent);
2611
+ createdFiles.push(".sdd/AGENTS.md");
2612
+ const templateFiles = await createTemplateFiles(projectPath);
2613
+ createdFiles.push(...templateFiles);
2614
+ const commandFiles = await createCommandFiles(projectPath);
2615
+ createdFiles.push(...commandFiles);
2616
+ return success({
2617
+ sddPath,
2618
+ claudePath,
2619
+ directories: createdDirs,
2620
+ files: createdFiles
2621
+ });
2622
+ }
2623
+ async function createTemplateFiles(projectPath) {
2624
+ const templatesPath = path2.join(projectPath, ".sdd", "templates");
2625
+ const files = [];
2626
+ await writeFile(path2.join(templatesPath, "spec.md"), generateSpecTemplate());
2627
+ files.push(".sdd/templates/spec.md");
2628
+ await writeFile(path2.join(templatesPath, "proposal.md"), generateProposalTemplate());
2629
+ files.push(".sdd/templates/proposal.md");
2630
+ await writeFile(path2.join(templatesPath, "delta.md"), generateDeltaTemplate());
2631
+ files.push(".sdd/templates/delta.md");
2632
+ await writeFile(path2.join(templatesPath, "tasks.md"), generateTasksTemplate());
2633
+ files.push(".sdd/templates/tasks.md");
2634
+ return files;
2635
+ }
2636
+ async function createCommandFiles(projectPath) {
2637
+ const commandsPath = path2.join(projectPath, ".claude", "commands");
2638
+ const files = [];
2639
+ const commands = generateClaudeCommands();
2640
+ for (const cmd of commands) {
2641
+ await writeFile(path2.join(commandsPath, `${cmd.name}.md`), cmd.content);
2642
+ files.push(`.claude/commands/${cmd.name}.md`);
2643
+ }
2644
+ return files;
2645
+ }
2646
+ function generateProposalTemplate() {
2647
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2648
+ return `---
2647
2649
  id: CHG-{{ID}}
2648
2650
  status: draft
2649
2651
  created: ${today}
@@ -2706,7 +2708,10 @@ created: ${today}
2706
2708
  - \uC601\uD5A5\uB3C4: \uB0AE\uC74C/\uC911\uAC04/\uB192\uC74C
2707
2709
  - \uBCF5\uC7A1\uB3C4: \uB0AE\uC74C/\uC911\uAC04/\uB192\uC74C
2708
2710
  `;
2709
- const deltaTemplate = `---
2711
+ }
2712
+ function generateDeltaTemplate() {
2713
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2714
+ return `---
2710
2715
  proposal: CHG-{{ID}}
2711
2716
  created: ${today}
2712
2717
  ---
@@ -2737,7 +2742,10 @@ created: ${today}
2737
2742
 
2738
2743
  (\uC0AD\uC81C\uB418\uB294 \uC2A4\uD399 \uCC38\uC870)
2739
2744
  `;
2740
- const tasksTemplate = `---
2745
+ }
2746
+ function generateTasksTemplate() {
2747
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2748
+ return `---
2741
2749
  spec: {{SPEC_ID}}
2742
2750
  created: ${today}
2743
2751
  ---
@@ -2788,19 +2796,57 @@ graph LR
2788
2796
  | [\u2192T] | \uD14C\uC2A4\uD2B8 \uD544\uC694 |
2789
2797
  | [US] | \uBD88\uD655\uC2E4/\uAC80\uD1A0 \uD544\uC694 |
2790
2798
  `;
2791
- await writeFile(path2.join(cwd, ".sdd", "templates", "spec.md"), specTemplate);
2792
- await writeFile(path2.join(cwd, ".sdd", "templates", "proposal.md"), proposalTemplate);
2793
- await writeFile(path2.join(cwd, ".sdd", "templates", "delta.md"), deltaTemplate);
2794
- await writeFile(path2.join(cwd, ".sdd", "templates", "tasks.md"), tasksTemplate);
2795
2799
  }
2796
- async function createClaudeCommands(cwd) {
2797
- const commands = generateClaudeCommands();
2798
- for (const cmd of commands) {
2799
- await writeFile(
2800
- path2.join(cwd, ".claude", "commands", `${cmd.name}.md`),
2801
- cmd.content
2802
- );
2800
+ function registerInitCommand(program2) {
2801
+ program2.command("init").description("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4").option("-f, --force", "\uAE30\uC874 .sdd/ \uB514\uB809\uD1A0\uB9AC \uB36E\uC5B4\uC4F0\uAE30").action(async (options) => {
2802
+ try {
2803
+ await runInit(options);
2804
+ } catch (error2) {
2805
+ error(error2 instanceof Error ? error2.message : String(error2));
2806
+ process.exit(ExitCode.GENERAL_ERROR);
2807
+ }
2808
+ });
2809
+ }
2810
+ async function runInit(options) {
2811
+ const cwd = process.cwd();
2812
+ if (await directoryExists(path2.join(cwd, ".sdd"))) {
2813
+ if (options.force) {
2814
+ warn("\uAE30\uC874 .sdd/ \uB514\uB809\uD1A0\uB9AC\uB97C \uB36E\uC5B4\uC501\uB2C8\uB2E4.");
2815
+ }
2816
+ }
2817
+ info("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4...");
2818
+ const result = await executeInit(cwd, options);
2819
+ if (!result.success) {
2820
+ error(result.error.message);
2821
+ process.exit(ExitCode.GENERAL_ERROR);
2803
2822
  }
2823
+ success2("SDD \uD504\uB85C\uC81D\uD2B8\uAC00 \uCD08\uAE30\uD654\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
2824
+ newline();
2825
+ info("\uC0DD\uC131\uB41C \uAD6C\uC870:");
2826
+ listItem(".sdd/");
2827
+ listItem("AGENTS.md", 1);
2828
+ listItem("constitution.md", 1);
2829
+ listItem("specs/", 1);
2830
+ listItem("changes/", 1);
2831
+ listItem("archive/", 1);
2832
+ listItem("templates/", 1);
2833
+ listItem(".claude/");
2834
+ listItem("commands/", 1);
2835
+ newline();
2836
+ info("Claude \uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC:");
2837
+ listItem("/sdd.start - \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC2DC\uC791 (\uD1B5\uD569 \uC9C4\uC785\uC810)");
2838
+ listItem("/sdd.constitution - \uD504\uB85C\uC81D\uD2B8 \uC6D0\uCE59 \uAD00\uB9AC");
2839
+ listItem("/sdd.new - \uC0C8 \uAE30\uB2A5 \uBA85\uC138 \uC791\uC131");
2840
+ listItem("/sdd.plan - \uAD6C\uD604 \uACC4\uD68D \uC791\uC131");
2841
+ listItem("/sdd.tasks - \uC791\uC5C5 \uBD84\uD574");
2842
+ listItem("/sdd.implement - \uAD6C\uD604 \uC9C4\uD589");
2843
+ listItem("/sdd.validate - \uC2A4\uD399 \uAC80\uC99D");
2844
+ listItem("/sdd.status - \uC0C1\uD0DC \uD655\uC778");
2845
+ listItem("/sdd.change - \uBCC0\uACBD \uC81C\uC548");
2846
+ newline();
2847
+ info("\uB2E4\uC74C \uB2E8\uACC4:");
2848
+ listItem("constitution.md\uB97C \uC218\uC815\uD558\uC5EC \uD504\uB85C\uC81D\uD2B8 \uC6D0\uCE59\uC744 \uC815\uC758\uD558\uC138\uC694");
2849
+ listItem("/sdd.new \uB85C \uCCAB \uBC88\uC9F8 \uAE30\uB2A5 \uBA85\uC138\uB97C \uC791\uC131\uD558\uC138\uC694");
2804
2850
  }
2805
2851
 
2806
2852
  // src/cli/commands/validate.ts
@@ -3631,9 +3677,9 @@ async function validateSpecs(targetPath, options = {}) {
3631
3677
  async function findSpecFiles(dirPath) {
3632
3678
  const files = [];
3633
3679
  async function scanDir(dir) {
3634
- const { promises: fs12 } = await import("fs");
3680
+ const { promises: fs15 } = await import("fs");
3635
3681
  try {
3636
- const entries = await fs12.readdir(dir, { withFileTypes: true });
3682
+ const entries = await fs15.readdir(dir, { withFileTypes: true });
3637
3683
  for (const entry of entries) {
3638
3684
  const fullPath = path3.join(dir, entry.name);
3639
3685
  if (entry.isDirectory()) {
@@ -3661,26 +3707,15 @@ async function findSpecFiles(dirPath) {
3661
3707
  // src/cli/commands/validate.ts
3662
3708
  init_errors();
3663
3709
  init_fs();
3664
- function registerValidateCommand(program2) {
3665
- program2.command("validate").description("\uC2A4\uD399 \uD30C\uC77C \uD615\uC2DD\uC744 \uAC80\uC99D\uD569\uB2C8\uB2E4").argument("[path]", "\uAC80\uC99D\uD560 \uD30C\uC77C \uB610\uB294 \uB514\uB809\uD1A0\uB9AC", "").option("-s, --strict", "\uACBD\uACE0\uB3C4 \uC5D0\uB7EC\uB85C \uCC98\uB9AC").option("-q, --quiet", "\uC694\uC57D\uB9CC \uCD9C\uB825").option("-l, --check-links", "\uCC38\uC870 \uB9C1\uD06C \uC720\uD6A8\uC131 \uAC80\uC0AC").option("-c, --constitution", "Constitution \uC704\uBC18 \uAC80\uC0AC (\uAE30\uBCF8\uAC12)").option("--no-constitution", "Constitution \uAC80\uC0AC \uAC74\uB108\uB6F0\uAE30").action(async (targetPath, options) => {
3666
- try {
3667
- await runValidate(targetPath, options);
3668
- } catch (error2) {
3669
- error(error2 instanceof Error ? error2.message : String(error2));
3670
- process.exit(ExitCode.GENERAL_ERROR);
3671
- }
3672
- });
3673
- }
3674
- async function runValidate(targetPath, options) {
3710
+ init_types();
3711
+ async function createValidateContext(targetPath, options, sddRoot) {
3675
3712
  let resolvedPath;
3676
3713
  let specsRoot;
3677
- const sddRoot = await findSddRoot();
3678
3714
  if (targetPath) {
3679
3715
  resolvedPath = path4.resolve(targetPath);
3680
3716
  } else {
3681
3717
  if (!sddRoot) {
3682
- error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
3683
- process.exit(ExitCode.GENERAL_ERROR);
3718
+ return failure(new Error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."));
3684
3719
  }
3685
3720
  resolvedPath = path4.join(sddRoot, ".sdd", "specs");
3686
3721
  }
@@ -3693,39 +3728,75 @@ async function runValidate(targetPath, options) {
3693
3728
  const constitutionPath = path4.join(sddRoot, ".sdd", "constitution.md");
3694
3729
  hasConstitution = await fileExists(constitutionPath);
3695
3730
  }
3731
+ return success({
3732
+ resolvedPath,
3733
+ specsRoot,
3734
+ checkConstitution,
3735
+ hasConstitution,
3736
+ sddRoot: sddRoot || void 0
3737
+ });
3738
+ }
3739
+ async function executeValidate(options, context) {
3740
+ const result = await validateSpecs(context.resolvedPath, {
3741
+ strict: options.strict,
3742
+ checkLinks: options.checkLinks,
3743
+ specsRoot: context.specsRoot,
3744
+ checkConstitution: context.checkConstitution && context.hasConstitution,
3745
+ sddRoot: context.sddRoot
3746
+ });
3747
+ if (!result.success) {
3748
+ return failure(result.error);
3749
+ }
3750
+ return success(result.data);
3751
+ }
3752
+ function formatValidateSummary(result, useColors = true) {
3753
+ const { passed, failed, warnings } = result;
3754
+ const passedText = useColors ? chalk2.green(`${passed} passed`) : `${passed} passed`;
3755
+ const failedText = failed > 0 ? useColors ? chalk2.red(`${failed} failed`) : `${failed} failed` : `${failed} failed`;
3756
+ const warningsText = warnings > 0 ? useColors ? chalk2.yellow(`${warnings} warnings`) : `${warnings} warnings` : "";
3757
+ return [passedText, failedText, warningsText].filter(Boolean).join(", ");
3758
+ }
3759
+ function registerValidateCommand(program2) {
3760
+ program2.command("validate").description("\uC2A4\uD399 \uD30C\uC77C \uD615\uC2DD\uC744 \uAC80\uC99D\uD569\uB2C8\uB2E4").argument("[path]", "\uAC80\uC99D\uD560 \uD30C\uC77C \uB610\uB294 \uB514\uB809\uD1A0\uB9AC", "").option("-s, --strict", "\uACBD\uACE0\uB3C4 \uC5D0\uB7EC\uB85C \uCC98\uB9AC").option("-q, --quiet", "\uC694\uC57D\uB9CC \uCD9C\uB825").option("-l, --check-links", "\uCC38\uC870 \uB9C1\uD06C \uC720\uD6A8\uC131 \uAC80\uC0AC").option("-c, --constitution", "Constitution \uC704\uBC18 \uAC80\uC0AC (\uAE30\uBCF8\uAC12)").option("--no-constitution", "Constitution \uAC80\uC0AC \uAC74\uB108\uB6F0\uAE30").action(async (targetPath, options) => {
3761
+ try {
3762
+ await runValidate(targetPath, options);
3763
+ } catch (error2) {
3764
+ error(error2 instanceof Error ? error2.message : String(error2));
3765
+ process.exit(ExitCode.GENERAL_ERROR);
3766
+ }
3767
+ });
3768
+ }
3769
+ async function runValidate(targetPath, options) {
3770
+ const sddRoot = await findSddRoot();
3771
+ const contextResult = await createValidateContext(targetPath, options, sddRoot);
3772
+ if (!contextResult.success) {
3773
+ error(contextResult.error.message);
3774
+ process.exit(ExitCode.GENERAL_ERROR);
3775
+ }
3776
+ const context = contextResult.data;
3696
3777
  if (!options.quiet) {
3697
- info(`\uAC80\uC99D \uC911: ${resolvedPath}`);
3778
+ info(`\uAC80\uC99D \uC911: ${context.resolvedPath}`);
3698
3779
  if (options.checkLinks) {
3699
3780
  info("(\uCC38\uC870 \uB9C1\uD06C \uAC80\uC99D \uD3EC\uD568)");
3700
3781
  }
3701
- if (checkConstitution && hasConstitution) {
3782
+ if (context.checkConstitution && context.hasConstitution) {
3702
3783
  info("(Constitution \uC704\uBC18 \uAC80\uC0AC \uD3EC\uD568)");
3703
3784
  }
3704
3785
  newline();
3705
3786
  }
3706
- const result = await validateSpecs(resolvedPath, {
3707
- strict: options.strict,
3708
- checkLinks: options.checkLinks,
3709
- specsRoot,
3710
- checkConstitution: checkConstitution && hasConstitution,
3711
- sddRoot: sddRoot || void 0
3712
- });
3787
+ const result = await executeValidate(options, context);
3713
3788
  if (!result.success) {
3714
3789
  error(result.error.message);
3715
3790
  process.exit(ExitCode.FILE_SYSTEM_ERROR);
3716
3791
  }
3717
- const { passed, failed, warnings, files } = result.data;
3792
+ const { failed, files } = result.data;
3718
3793
  if (!options.quiet) {
3719
3794
  for (const file of files) {
3720
- printFileResult(file, resolvedPath);
3795
+ printFileResult(file, context.resolvedPath);
3721
3796
  }
3722
3797
  newline();
3723
3798
  }
3724
- const passedText = chalk2.green(`${passed} passed`);
3725
- const failedText = failed > 0 ? chalk2.red(`${failed} failed`) : `${failed} failed`;
3726
- const warningsText = warnings > 0 ? chalk2.yellow(`${warnings} warnings`) : "";
3727
- const summary = [passedText, failedText, warningsText].filter(Boolean).join(", ");
3728
- console.log(`Result: ${summary}`);
3799
+ console.log(`Result: ${formatValidateSummary(result.data)}`);
3729
3800
  if (failed > 0) {
3730
3801
  process.exit(ExitCode.VALIDATION_FAILED);
3731
3802
  }
@@ -4917,57 +4988,200 @@ async function listPendingChanges(sddPath) {
4917
4988
  // src/cli/commands/change.ts
4918
4989
  init_fs();
4919
4990
  init_errors();
4920
- function registerChangeCommand(program2) {
4921
- const change = program2.command("change [id]").description("\uBCC0\uACBD \uC81C\uC548\uC744 \uC0DD\uC131\uD558\uAC70\uB098 \uAD00\uB9AC\uD569\uB2C8\uB2E4").option("-l, --list", "\uC9C4\uD589 \uC911\uC778 \uBCC0\uACBD \uBAA9\uB85D").option("-t, --title <title>", "\uBCC0\uACBD \uC81C\uC548 \uC81C\uBAA9").option("-s, --spec <spec>", "\uB300\uC0C1 \uC2A4\uD399 \uACBD\uB85C").action(async (id, options) => {
4922
- try {
4923
- await runChange(id, options);
4924
- } catch (error2) {
4925
- error(error2 instanceof Error ? error2.message : String(error2));
4926
- process.exit(ExitCode.GENERAL_ERROR);
4927
- }
4928
- });
4929
- change.command("apply <id>").description("\uBCC0\uACBD \uC81C\uC548\uC744 \uC2A4\uD399\uC5D0 \uC801\uC6A9\uD569\uB2C8\uB2E4").action(async (id) => {
4930
- try {
4931
- await runApply(id);
4932
- } catch (error2) {
4933
- error(error2 instanceof Error ? error2.message : String(error2));
4934
- process.exit(ExitCode.GENERAL_ERROR);
4935
- }
4991
+ init_types();
4992
+ async function getChangeListItems(sddPath) {
4993
+ const result = await listPendingChanges(sddPath);
4994
+ if (!result.success) {
4995
+ return failure(result.error);
4996
+ }
4997
+ return success(result.data.map((change) => ({
4998
+ id: change.id,
4999
+ title: change.title || null,
5000
+ status: change.status
5001
+ })));
5002
+ }
5003
+ async function getChangeInfo(changePath) {
5004
+ const proposalPath = path6.join(changePath, "proposal.md");
5005
+ if (!await fileExists(proposalPath)) {
5006
+ return failure(new Error("proposal.md\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
5007
+ }
5008
+ const contentResult = await readFile(proposalPath);
5009
+ if (!contentResult.success) {
5010
+ return failure(new Error("proposal.md\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
5011
+ }
5012
+ const parseResult = parseProposal(contentResult.data);
5013
+ if (!parseResult.success) {
5014
+ return failure(new Error(`proposal.md \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`));
5015
+ }
5016
+ return success({
5017
+ id: parseResult.data.metadata.id,
5018
+ title: parseResult.data.title,
5019
+ status: parseResult.data.metadata.status,
5020
+ created: parseResult.data.metadata.created,
5021
+ affectedSpecs: parseResult.data.affectedSpecs
4936
5022
  });
4937
- change.command("archive <id>").description("\uC644\uB8CC\uB41C \uBCC0\uACBD\uC744 \uC544\uCE74\uC774\uBE0C\uD569\uB2C8\uB2E4").action(async (id) => {
4938
- try {
4939
- await runArchive(id);
4940
- } catch (error2) {
4941
- error(error2 instanceof Error ? error2.message : String(error2));
4942
- process.exit(ExitCode.GENERAL_ERROR);
4943
- }
5023
+ }
5024
+ async function createChange(sddPath, options) {
5025
+ const changesPath = path6.join(sddPath, "changes");
5026
+ await ensureDir(changesPath);
5027
+ const existingIds = [];
5028
+ try {
5029
+ const dirs = await fs3.readdir(changesPath);
5030
+ existingIds.push(...dirs.filter((d) => d.startsWith("CHG-")));
5031
+ } catch {
5032
+ }
5033
+ const newId = generateChangeId(existingIds);
5034
+ const title2 = options.title || "\uC0C8 \uBCC0\uACBD \uC81C\uC548";
5035
+ const affectedSpecs = options.spec ? [options.spec] : [];
5036
+ const changePath = path6.join(changesPath, newId);
5037
+ await ensureDir(changePath);
5038
+ const proposal = generateProposal({
5039
+ id: newId,
5040
+ title: title2,
5041
+ affectedSpecs
4944
5042
  });
4945
- change.command("diff <id>").description("\uBCC0\uACBD \uC81C\uC548\uC758 diff\uB97C \uD45C\uC2DC\uD569\uB2C8\uB2E4").action(async (id) => {
4946
- try {
4947
- await runDiff(id);
4948
- } catch (error2) {
4949
- error(error2 instanceof Error ? error2.message : String(error2));
4950
- process.exit(ExitCode.GENERAL_ERROR);
4951
- }
5043
+ const proposalPath = path6.join(changePath, "proposal.md");
5044
+ await writeFile(proposalPath, proposal);
5045
+ const delta = generateDelta({
5046
+ proposalId: newId,
5047
+ title: title2
4952
5048
  });
4953
- change.command("validate <id>").description("\uBCC0\uACBD \uC81C\uC548\uC758 \uC720\uD6A8\uC131\uC744 \uAC80\uC99D\uD569\uB2C8\uB2E4").action(async (id) => {
4954
- try {
4955
- await runValidateChange(id);
4956
- } catch (error2) {
4957
- error(error2 instanceof Error ? error2.message : String(error2));
4958
- process.exit(ExitCode.GENERAL_ERROR);
4959
- }
5049
+ const deltaPath = path6.join(changePath, "delta.md");
5050
+ await writeFile(deltaPath, delta);
5051
+ return success({
5052
+ id: newId,
5053
+ proposalPath,
5054
+ deltaPath
4960
5055
  });
4961
5056
  }
4962
- async function runChange(id, options) {
4963
- const projectRoot = await findSddRoot();
4964
- if (!projectRoot) {
4965
- error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
4966
- process.exit(ExitCode.GENERAL_ERROR);
5057
+ async function applyChange(changePath) {
5058
+ const proposalPath = path6.join(changePath, "proposal.md");
5059
+ if (!await fileExists(proposalPath)) {
5060
+ return failure(new Error("proposal.md\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
4967
5061
  }
4968
- const sddPath = path6.join(projectRoot, ".sdd");
4969
- if (options.list) {
4970
- const result = await listPendingChanges(sddPath);
5062
+ const contentResult = await readFile(proposalPath);
5063
+ if (!contentResult.success) {
5064
+ return failure(new Error("proposal.md\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
5065
+ }
5066
+ const updateResult = updateProposalStatus(contentResult.data, "applied");
5067
+ if (!updateResult.success) {
5068
+ return failure(new Error("proposal.md\uB97C \uC5C5\uB370\uC774\uD2B8\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
5069
+ }
5070
+ await writeFile(proposalPath, updateResult.data);
5071
+ return success(void 0);
5072
+ }
5073
+ async function getDeltaInfo(changePath) {
5074
+ const deltaPath = path6.join(changePath, "delta.md");
5075
+ if (!await fileExists(deltaPath)) {
5076
+ return failure(new Error("delta.md\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
5077
+ }
5078
+ const deltaResult = await readFile(deltaPath);
5079
+ if (!deltaResult.success) {
5080
+ return failure(new Error("delta.md\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
5081
+ }
5082
+ const parseResult = parseDelta(deltaResult.data);
5083
+ if (!parseResult.success) {
5084
+ return failure(new Error(`Delta \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`));
5085
+ }
5086
+ return success({
5087
+ added: parseResult.data.added,
5088
+ modified: parseResult.data.modified,
5089
+ removed: parseResult.data.removed
5090
+ });
5091
+ }
5092
+ async function validateChange(changePath) {
5093
+ const result = {
5094
+ proposalValid: false,
5095
+ deltaValid: false,
5096
+ hasDelta: false
5097
+ };
5098
+ const proposalPath = path6.join(changePath, "proposal.md");
5099
+ if (await fileExists(proposalPath)) {
5100
+ const proposalResult = await readFile(proposalPath);
5101
+ if (proposalResult.success) {
5102
+ const parsed = parseProposal(proposalResult.data);
5103
+ if (parsed.success) {
5104
+ result.proposalValid = true;
5105
+ result.proposalTitle = parsed.data.title;
5106
+ } else {
5107
+ result.proposalError = parsed.error.message;
5108
+ }
5109
+ }
5110
+ } else {
5111
+ result.proposalError = "proposal.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.";
5112
+ }
5113
+ const deltaPath = path6.join(changePath, "delta.md");
5114
+ if (await fileExists(deltaPath)) {
5115
+ result.hasDelta = true;
5116
+ const deltaResult = await readFile(deltaPath);
5117
+ if (deltaResult.success) {
5118
+ const validation = validateDelta(deltaResult.data);
5119
+ if (validation.valid) {
5120
+ result.deltaValid = true;
5121
+ const types = [];
5122
+ if (validation.hasAdded) types.push("ADDED");
5123
+ if (validation.hasModified) types.push("MODIFIED");
5124
+ if (validation.hasRemoved) types.push("REMOVED");
5125
+ result.deltaTypes = types;
5126
+ result.deltaWarnings = validation.warnings;
5127
+ } else {
5128
+ result.deltaErrors = validation.errors;
5129
+ }
5130
+ }
5131
+ }
5132
+ return success(result);
5133
+ }
5134
+ function registerChangeCommand(program2) {
5135
+ const change = program2.command("change [id]").description("\uBCC0\uACBD \uC81C\uC548\uC744 \uC0DD\uC131\uD558\uAC70\uB098 \uAD00\uB9AC\uD569\uB2C8\uB2E4").option("-l, --list", "\uC9C4\uD589 \uC911\uC778 \uBCC0\uACBD \uBAA9\uB85D").option("-t, --title <title>", "\uBCC0\uACBD \uC81C\uC548 \uC81C\uBAA9").option("-s, --spec <spec>", "\uB300\uC0C1 \uC2A4\uD399 \uACBD\uB85C").action(async (id, options) => {
5136
+ try {
5137
+ await runChange(id, options);
5138
+ } catch (error2) {
5139
+ error(error2 instanceof Error ? error2.message : String(error2));
5140
+ process.exit(ExitCode.GENERAL_ERROR);
5141
+ }
5142
+ });
5143
+ change.command("apply <id>").description("\uBCC0\uACBD \uC81C\uC548\uC744 \uC2A4\uD399\uC5D0 \uC801\uC6A9\uD569\uB2C8\uB2E4").action(async (id) => {
5144
+ try {
5145
+ await runApply(id);
5146
+ } catch (error2) {
5147
+ error(error2 instanceof Error ? error2.message : String(error2));
5148
+ process.exit(ExitCode.GENERAL_ERROR);
5149
+ }
5150
+ });
5151
+ change.command("archive <id>").description("\uC644\uB8CC\uB41C \uBCC0\uACBD\uC744 \uC544\uCE74\uC774\uBE0C\uD569\uB2C8\uB2E4").action(async (id) => {
5152
+ try {
5153
+ await runArchive(id);
5154
+ } catch (error2) {
5155
+ error(error2 instanceof Error ? error2.message : String(error2));
5156
+ process.exit(ExitCode.GENERAL_ERROR);
5157
+ }
5158
+ });
5159
+ change.command("diff <id>").description("\uBCC0\uACBD \uC81C\uC548\uC758 diff\uB97C \uD45C\uC2DC\uD569\uB2C8\uB2E4").action(async (id) => {
5160
+ try {
5161
+ await runDiff(id);
5162
+ } catch (error2) {
5163
+ error(error2 instanceof Error ? error2.message : String(error2));
5164
+ process.exit(ExitCode.GENERAL_ERROR);
5165
+ }
5166
+ });
5167
+ change.command("validate <id>").description("\uBCC0\uACBD \uC81C\uC548\uC758 \uC720\uD6A8\uC131\uC744 \uAC80\uC99D\uD569\uB2C8\uB2E4").action(async (id) => {
5168
+ try {
5169
+ await runValidateChange(id);
5170
+ } catch (error2) {
5171
+ error(error2 instanceof Error ? error2.message : String(error2));
5172
+ process.exit(ExitCode.GENERAL_ERROR);
5173
+ }
5174
+ });
5175
+ }
5176
+ async function runChange(id, options) {
5177
+ const projectRoot = await findSddRoot();
5178
+ if (!projectRoot) {
5179
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
5180
+ process.exit(ExitCode.GENERAL_ERROR);
5181
+ }
5182
+ const sddPath = path6.join(projectRoot, ".sdd");
5183
+ if (options.list) {
5184
+ const result = await getChangeListItems(sddPath);
4971
5185
  if (!result.success) {
4972
5186
  error(result.error.message);
4973
5187
  process.exit(ExitCode.GENERAL_ERROR);
@@ -4985,53 +5199,31 @@ async function runChange(id, options) {
4985
5199
  return;
4986
5200
  }
4987
5201
  if (id) {
4988
- const changePath2 = path6.join(sddPath, "changes", id);
4989
- if (!await directoryExists(changePath2)) {
5202
+ const changePath = path6.join(sddPath, "changes", id);
5203
+ if (!await directoryExists(changePath)) {
4990
5204
  error(`\uBCC0\uACBD\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${id}`);
4991
5205
  process.exit(ExitCode.GENERAL_ERROR);
4992
5206
  }
4993
- const proposalPath = path6.join(changePath2, "proposal.md");
4994
- try {
4995
- const content = await fs3.readFile(proposalPath, "utf-8");
4996
- const parseResult = parseProposal(content);
4997
- if (parseResult.success) {
4998
- info(`\uBCC0\uACBD \uC81C\uC548: ${parseResult.data.title}`);
4999
- info(`\uC0C1\uD0DC: ${parseResult.data.metadata.status}`);
5000
- info(`\uC0DD\uC131: ${parseResult.data.metadata.created}`);
5001
- if (parseResult.data.affectedSpecs.length > 0) {
5002
- info("\uC601\uD5A5 \uC2A4\uD399:");
5003
- parseResult.data.affectedSpecs.forEach((spec) => listItem(spec, 1));
5004
- }
5207
+ const infoResult = await getChangeInfo(changePath);
5208
+ if (infoResult.success) {
5209
+ info(`\uBCC0\uACBD \uC81C\uC548: ${infoResult.data.title}`);
5210
+ info(`\uC0C1\uD0DC: ${infoResult.data.status}`);
5211
+ info(`\uC0DD\uC131: ${infoResult.data.created}`);
5212
+ if (infoResult.data.affectedSpecs.length > 0) {
5213
+ info("\uC601\uD5A5 \uC2A4\uD399:");
5214
+ infoResult.data.affectedSpecs.forEach((spec) => listItem(spec, 1));
5005
5215
  }
5006
- } catch {
5007
- error("proposal.md\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
5216
+ } else {
5217
+ error(infoResult.error.message);
5008
5218
  }
5009
5219
  return;
5010
5220
  }
5011
- const changesPath = path6.join(sddPath, "changes");
5012
- await ensureDir(changesPath);
5013
- const existingIds = [];
5014
- try {
5015
- const dirs = await fs3.readdir(changesPath);
5016
- existingIds.push(...dirs.filter((d) => d.startsWith("CHG-")));
5017
- } catch {
5221
+ const createResult = await createChange(sddPath, options);
5222
+ if (!createResult.success) {
5223
+ error(createResult.error.message);
5224
+ process.exit(ExitCode.GENERAL_ERROR);
5018
5225
  }
5019
- const newId = generateChangeId(existingIds);
5020
- const title2 = options.title || "\uC0C8 \uBCC0\uACBD \uC81C\uC548";
5021
- const affectedSpecs = options.spec ? [options.spec] : [];
5022
- const changePath = path6.join(changesPath, newId);
5023
- await ensureDir(changePath);
5024
- const proposal = generateProposal({
5025
- id: newId,
5026
- title: title2,
5027
- affectedSpecs
5028
- });
5029
- await writeFile(path6.join(changePath, "proposal.md"), proposal);
5030
- const delta = generateDelta({
5031
- proposalId: newId,
5032
- title: title2
5033
- });
5034
- await writeFile(path6.join(changePath, "delta.md"), delta);
5226
+ const newId = createResult.data.id;
5035
5227
  success2(`\uBCC0\uACBD \uC81C\uC548\uC774 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${newId}`);
5036
5228
  newline();
5037
5229
  info("\uC0DD\uC131\uB41C \uD30C\uC77C:");
@@ -5055,15 +5247,9 @@ async function runApply(id) {
5055
5247
  error(`\uBCC0\uACBD\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${id}`);
5056
5248
  process.exit(ExitCode.GENERAL_ERROR);
5057
5249
  }
5058
- const proposalPath = path6.join(changePath, "proposal.md");
5059
- try {
5060
- const content = await fs3.readFile(proposalPath, "utf-8");
5061
- const updateResult = updateProposalStatus(content, "applied");
5062
- if (updateResult.success) {
5063
- await fs3.writeFile(proposalPath, updateResult.data);
5064
- }
5065
- } catch {
5066
- error("proposal.md\uB97C \uC5C5\uB370\uC774\uD2B8\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
5250
+ const result = await applyChange(changePath);
5251
+ if (!result.success) {
5252
+ error(result.error.message);
5067
5253
  process.exit(ExitCode.GENERAL_ERROR);
5068
5254
  }
5069
5255
  success2(`\uBCC0\uACBD\uC774 \uC801\uC6A9 \uC0C1\uD0DC\uB85C \uBCC0\uACBD\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${id}`);
@@ -5099,22 +5285,12 @@ async function runDiff(id) {
5099
5285
  error(`\uBCC0\uACBD\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${id}`);
5100
5286
  process.exit(ExitCode.GENERAL_ERROR);
5101
5287
  }
5102
- const deltaPath = path6.join(changePath, "delta.md");
5103
- if (!await fileExists(deltaPath)) {
5104
- error("delta.md\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
5105
- process.exit(ExitCode.GENERAL_ERROR);
5106
- }
5107
- const deltaResult = await readFile(deltaPath);
5288
+ const deltaResult = await getDeltaInfo(changePath);
5108
5289
  if (!deltaResult.success) {
5109
- error("delta.md\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
5290
+ error(deltaResult.error.message);
5110
5291
  process.exit(ExitCode.FILE_SYSTEM_ERROR);
5111
5292
  }
5112
- const parseResult = parseDelta(deltaResult.data);
5113
- if (!parseResult.success) {
5114
- error(`Delta \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`);
5115
- process.exit(ExitCode.VALIDATION_ERROR);
5116
- }
5117
- const delta = parseResult.data;
5293
+ const delta = deltaResult.data;
5118
5294
  info(`\uBCC0\uACBD Diff: ${id}`);
5119
5295
  newline();
5120
5296
  if (delta.added.length > 0 && delta.added[0].content !== "(\uCD94\uAC00\uB418\uB294 \uC2A4\uD399 \uB0B4\uC6A9)") {
@@ -5162,44 +5338,35 @@ async function runValidateChange(id) {
5162
5338
  error(`\uBCC0\uACBD\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${id}`);
5163
5339
  process.exit(ExitCode.GENERAL_ERROR);
5164
5340
  }
5341
+ const validationResult = await validateChange(changePath);
5342
+ if (!validationResult.success) {
5343
+ error(validationResult.error.message);
5344
+ process.exit(ExitCode.GENERAL_ERROR);
5345
+ }
5346
+ const result = validationResult.data;
5165
5347
  let hasErrors = false;
5166
- const proposalPath = path6.join(changePath, "proposal.md");
5167
- if (await fileExists(proposalPath)) {
5168
- const proposalResult = await readFile(proposalPath);
5169
- if (proposalResult.success) {
5170
- const parsed = parseProposal(proposalResult.data);
5171
- if (parsed.success) {
5172
- success2(`\u2713 proposal.md \uC720\uD6A8 (${parsed.data.title})`);
5173
- } else {
5174
- error(`\u2717 proposal.md \uC624\uB958: ${parsed.error.message}`);
5175
- hasErrors = true;
5176
- }
5177
- }
5348
+ if (result.proposalValid) {
5349
+ success2(`\u2713 proposal.md \uC720\uD6A8 (${result.proposalTitle})`);
5178
5350
  } else {
5179
- error("\u2717 proposal.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
5351
+ error(`\u2717 proposal.md \uC624\uB958: ${result.proposalError}`);
5180
5352
  hasErrors = true;
5181
5353
  }
5182
- const deltaPath = path6.join(changePath, "delta.md");
5183
- if (await fileExists(deltaPath)) {
5184
- const deltaResult = await readFile(deltaPath);
5185
- if (deltaResult.success) {
5186
- const validation = validateDelta(deltaResult.data);
5187
- if (validation.valid) {
5188
- const types = [];
5189
- if (validation.hasAdded) types.push("ADDED");
5190
- if (validation.hasModified) types.push("MODIFIED");
5191
- if (validation.hasRemoved) types.push("REMOVED");
5192
- success2(`\u2713 delta.md \uC720\uD6A8 (${types.join(", ")})`);
5193
- for (const warning of validation.warnings) {
5354
+ if (result.hasDelta) {
5355
+ if (result.deltaValid) {
5356
+ success2(`\u2713 delta.md \uC720\uD6A8 (${result.deltaTypes?.join(", ")})`);
5357
+ if (result.deltaWarnings) {
5358
+ for (const warning of result.deltaWarnings) {
5194
5359
  warn(` \u26A0 ${warning}`);
5195
5360
  }
5196
- } else {
5197
- error(`\u2717 delta.md \uC624\uB958:`);
5198
- for (const error2 of validation.errors) {
5361
+ }
5362
+ } else {
5363
+ error(`\u2717 delta.md \uC624\uB958:`);
5364
+ if (result.deltaErrors) {
5365
+ for (const error2 of result.deltaErrors) {
5199
5366
  error(` - ${error2}`);
5200
5367
  }
5201
- hasErrors = true;
5202
5368
  }
5369
+ hasErrors = true;
5203
5370
  }
5204
5371
  } else {
5205
5372
  warn("\u26A0 delta.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
@@ -5207,7 +5374,7 @@ async function runValidateChange(id) {
5207
5374
  newline();
5208
5375
  if (hasErrors) {
5209
5376
  error(`\uAC80\uC99D \uC2E4\uD328: ${id}`);
5210
- process.exit(ExitCode.VALIDATION_ERROR);
5377
+ process.exit(ExitCode.VALIDATION_FAILED);
5211
5378
  } else {
5212
5379
  success2(`\uAC80\uC99D \uD1B5\uACFC: ${id}`);
5213
5380
  }
@@ -5635,19 +5802,19 @@ function detectCircularDependencies(graph) {
5635
5802
  const cycles = [];
5636
5803
  const visited = /* @__PURE__ */ new Set();
5637
5804
  const recStack = /* @__PURE__ */ new Set();
5638
- function dfs(nodeId, path20) {
5805
+ function dfs(nodeId, path27) {
5639
5806
  visited.add(nodeId);
5640
5807
  recStack.add(nodeId);
5641
5808
  const node = graph.nodes.get(nodeId);
5642
5809
  if (!node) return false;
5643
5810
  for (const depId of node.dependsOn) {
5644
5811
  if (!visited.has(depId)) {
5645
- if (dfs(depId, [...path20, nodeId])) {
5812
+ if (dfs(depId, [...path27, nodeId])) {
5646
5813
  return true;
5647
5814
  }
5648
5815
  } else if (recStack.has(depId)) {
5649
- const cycleStart = path20.indexOf(depId);
5650
- const cycle = cycleStart >= 0 ? [...path20.slice(cycleStart), nodeId, depId] : [nodeId, depId];
5816
+ const cycleStart = path27.indexOf(depId);
5817
+ const cycle = cycleStart >= 0 ? [...path27.slice(cycleStart), nodeId, depId] : [nodeId, depId];
5651
5818
  cycles.push({
5652
5819
  cycle,
5653
5820
  description: `\uC21C\uD658 \uC758\uC874\uC131: ${cycle.join(" \u2192 ")}`
@@ -6635,6 +6802,108 @@ function formatCodeImpactResult(result) {
6635
6802
  // src/cli/commands/impact.ts
6636
6803
  init_fs();
6637
6804
  init_errors();
6805
+ init_types();
6806
+ function resolveProposalPath(proposalPath, sddPath) {
6807
+ if (path10.isAbsolute(proposalPath)) {
6808
+ return proposalPath;
6809
+ }
6810
+ const changesPath = path10.join(sddPath, "changes", proposalPath);
6811
+ if (proposalPath.endsWith(".md")) {
6812
+ return changesPath;
6813
+ }
6814
+ return path10.join(changesPath, "proposal.md");
6815
+ }
6816
+ async function executeImpactAnalysis(sddPath, feature) {
6817
+ const result = await analyzeImpact(sddPath, feature);
6818
+ if (!result.success) {
6819
+ return failure(result.error);
6820
+ }
6821
+ return success(result.data);
6822
+ }
6823
+ async function executeGraphAnalysis(specsPath, feature, asJson = false) {
6824
+ const graphResult = await buildDependencyGraph(specsPath);
6825
+ if (!graphResult.success) {
6826
+ return failure(graphResult.error);
6827
+ }
6828
+ const mermaid = generateMermaidGraph(graphResult.data, feature);
6829
+ if (asJson) {
6830
+ return success({
6831
+ mermaid,
6832
+ nodes: Array.from(graphResult.data.nodes.values()),
6833
+ edges: graphResult.data.edges
6834
+ });
6835
+ }
6836
+ return success({ mermaid });
6837
+ }
6838
+ async function executeCodeImpactAnalysis(projectRoot, sddPath, feature) {
6839
+ const result = await analyzeCodeImpact(projectRoot, sddPath, feature);
6840
+ if (!result.success) {
6841
+ return failure(result.error);
6842
+ }
6843
+ return success(result.data);
6844
+ }
6845
+ async function executeImpactReport(sddPath) {
6846
+ const result = await generateImpactReport(sddPath);
6847
+ if (!result.success) {
6848
+ return failure(result.error);
6849
+ }
6850
+ return success(result.data);
6851
+ }
6852
+ async function executeChangeImpact(sddPath, changeId) {
6853
+ const result = await analyzeChangeImpact(sddPath, changeId);
6854
+ if (!result.success) {
6855
+ return failure(result.error);
6856
+ }
6857
+ return success(result.data);
6858
+ }
6859
+ function formatChangeImpactOutput(data) {
6860
+ const lines = [];
6861
+ lines.push(`\u{1F4CA} \uBCC0\uACBD \uC601\uD5A5 \uBD84\uC11D: ${data.changeId}`);
6862
+ if (data.title) {
6863
+ lines.push(`\uC81C\uBAA9: ${data.title}`);
6864
+ }
6865
+ lines.push(`\uC0C1\uD0DC: ${data.status}`);
6866
+ lines.push("");
6867
+ if (data.affectedSpecs.length > 0) {
6868
+ lines.push("\u26A0\uFE0F \uC9C1\uC811 \uC601\uD5A5 \uBC1B\uB294 \uC2A4\uD399:");
6869
+ for (const spec of data.affectedSpecs) {
6870
+ lines.push(` - ${spec.id} - ${spec.reason}`);
6871
+ }
6872
+ lines.push("");
6873
+ }
6874
+ if (data.transitiveAffected.length > 0) {
6875
+ lines.push("\u{1F504} \uAC04\uC811 \uC601\uD5A5 \uBC1B\uB294 \uC2A4\uD399:");
6876
+ for (const spec of data.transitiveAffected) {
6877
+ lines.push(` - ${spec.id} (${spec.reason})`);
6878
+ }
6879
+ lines.push("");
6880
+ }
6881
+ const riskIcon = data.riskLevel === "high" ? "\u{1F534}" : data.riskLevel === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
6882
+ lines.push(`\uCD1D \uC601\uD5A5 \uBC94\uC704: ${data.totalImpact}\uAC1C \uC2A4\uD399 ${riskIcon}`);
6883
+ lines.push("");
6884
+ if (data.recommendations.length > 0) {
6885
+ lines.push("\u{1F4A1} \uAD8C\uC7A5\uC0AC\uD56D:");
6886
+ for (const rec of data.recommendations) {
6887
+ lines.push(` - ${rec}`);
6888
+ }
6889
+ }
6890
+ return lines.join("\n");
6891
+ }
6892
+ async function executeSimulation(specsPath, feature, proposalPath) {
6893
+ const deltaResult = await parseDeltaFromProposal(proposalPath);
6894
+ if (!deltaResult.success) {
6895
+ return failure(deltaResult.error);
6896
+ }
6897
+ const deltas = deltaResult.data;
6898
+ if (deltas.length === 0) {
6899
+ return failure(new Error("\uBCC0\uACBD \uC81C\uC548\uC5D0\uC11C \uB378\uD0C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
6900
+ }
6901
+ const simResult = await runSimulation(specsPath, feature, deltas);
6902
+ if (!simResult.success) {
6903
+ return failure(simResult.error);
6904
+ }
6905
+ return success({ deltas, result: simResult.data });
6906
+ }
6638
6907
  function registerImpactCommand(program2) {
6639
6908
  const impact = program2.command("impact [feature]").description("\uC2A4\uD399 \uBCC0\uACBD\uC758 \uC601\uD5A5\uB3C4\uB97C \uBD84\uC11D\uD569\uB2C8\uB2E4").option("-g, --graph", "\uC758\uC874\uC131 \uADF8\uB798\uD504 \uCD9C\uB825 (Mermaid)").option("-r, --reverse", "\uC5ED\uBC29\uD5A5 \uC601\uD5A5\uB3C4 \uBD84\uC11D").option("-c, --code", "\uCF54\uB4DC \uC601\uD5A5\uB3C4 \uBD84\uC11D").option("--json", "JSON \uD615\uC2DD \uCD9C\uB825").action(async (feature, options) => {
6640
6909
  try {
@@ -6676,25 +6945,25 @@ async function runImpact(feature, options) {
6676
6945
  process.exit(ExitCode.GENERAL_ERROR);
6677
6946
  }
6678
6947
  const sddPath = path10.join(projectRoot, ".sdd");
6948
+ const specsPath = path10.join(sddPath, "specs");
6679
6949
  if (options.graph) {
6680
- const graphResult = await buildDependencyGraph(path10.join(sddPath, "specs"));
6950
+ const graphResult = await executeGraphAnalysis(specsPath, feature, options.json);
6681
6951
  if (!graphResult.success) {
6682
6952
  error(graphResult.error.message);
6683
6953
  process.exit(ExitCode.GENERAL_ERROR);
6684
6954
  }
6685
- const mermaid = generateMermaidGraph(graphResult.data, feature);
6686
6955
  if (options.json) {
6687
6956
  console.log(JSON.stringify({
6688
6957
  format: "mermaid",
6689
- content: mermaid,
6690
- nodes: Array.from(graphResult.data.nodes.values()),
6958
+ content: graphResult.data.mermaid,
6959
+ nodes: graphResult.data.nodes,
6691
6960
  edges: graphResult.data.edges
6692
6961
  }, null, 2));
6693
6962
  } else {
6694
6963
  info("\uC758\uC874\uC131 \uADF8\uB798\uD504 (Mermaid):");
6695
6964
  newline();
6696
6965
  console.log("```mermaid");
6697
- console.log(mermaid);
6966
+ console.log(graphResult.data.mermaid);
6698
6967
  console.log("```");
6699
6968
  }
6700
6969
  return;
@@ -6708,7 +6977,7 @@ async function runImpact(feature, options) {
6708
6977
  }
6709
6978
  info(`\u{1F4BB} \uCF54\uB4DC \uC601\uD5A5\uB3C4 \uBD84\uC11D: ${feature}`);
6710
6979
  newline();
6711
- const codeResult = await analyzeCodeImpact(projectRoot, sddPath, feature);
6980
+ const codeResult = await executeCodeImpactAnalysis(projectRoot, sddPath, feature);
6712
6981
  if (!codeResult.success) {
6713
6982
  error(codeResult.error.message);
6714
6983
  process.exit(ExitCode.GENERAL_ERROR);
@@ -6726,7 +6995,7 @@ async function runImpact(feature, options) {
6726
6995
  info("\uC608\uC2DC: sdd impact auth");
6727
6996
  process.exit(ExitCode.GENERAL_ERROR);
6728
6997
  }
6729
- const result = await analyzeImpact(sddPath, feature);
6998
+ const result = await executeImpactAnalysis(sddPath, feature);
6730
6999
  if (!result.success) {
6731
7000
  error(result.error.message);
6732
7001
  process.exit(ExitCode.GENERAL_ERROR);
@@ -6744,7 +7013,7 @@ async function runImpactReport(options) {
6744
7013
  process.exit(ExitCode.GENERAL_ERROR);
6745
7014
  }
6746
7015
  const sddPath = path10.join(projectRoot, ".sdd");
6747
- const result = await generateImpactReport(sddPath);
7016
+ const result = await executeImpactReport(sddPath);
6748
7017
  if (!result.success) {
6749
7018
  error(result.error.message);
6750
7019
  process.exit(ExitCode.GENERAL_ERROR);
@@ -6762,7 +7031,7 @@ async function runChangeImpact(changeId, options) {
6762
7031
  process.exit(ExitCode.GENERAL_ERROR);
6763
7032
  }
6764
7033
  const sddPath = path10.join(projectRoot, ".sdd");
6765
- const result = await analyzeChangeImpact(sddPath, changeId);
7034
+ const result = await executeChangeImpact(sddPath, changeId);
6766
7035
  if (!result.success) {
6767
7036
  error(result.error.message);
6768
7037
  process.exit(ExitCode.GENERAL_ERROR);
@@ -6770,36 +7039,7 @@ async function runChangeImpact(changeId, options) {
6770
7039
  if (options.json) {
6771
7040
  console.log(JSON.stringify(result.data, null, 2));
6772
7041
  } else {
6773
- const data = result.data;
6774
- info(`\u{1F4CA} \uBCC0\uACBD \uC601\uD5A5 \uBD84\uC11D: ${data.changeId}`);
6775
- if (data.title) {
6776
- info(`\uC81C\uBAA9: ${data.title}`);
6777
- }
6778
- info(`\uC0C1\uD0DC: ${data.status}`);
6779
- newline();
6780
- if (data.affectedSpecs.length > 0) {
6781
- info("\u26A0\uFE0F \uC9C1\uC811 \uC601\uD5A5 \uBC1B\uB294 \uC2A4\uD399:");
6782
- for (const spec of data.affectedSpecs) {
6783
- listItem(`${spec.id} - ${spec.reason}`, 1);
6784
- }
6785
- newline();
6786
- }
6787
- if (data.transitiveAffected.length > 0) {
6788
- info("\u{1F504} \uAC04\uC811 \uC601\uD5A5 \uBC1B\uB294 \uC2A4\uD399:");
6789
- for (const spec of data.transitiveAffected) {
6790
- listItem(`${spec.id} (${spec.reason})`, 1);
6791
- }
6792
- newline();
6793
- }
6794
- const riskIcon = data.riskLevel === "high" ? "\u{1F534}" : data.riskLevel === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
6795
- info(`\uCD1D \uC601\uD5A5 \uBC94\uC704: ${data.totalImpact}\uAC1C \uC2A4\uD399 ${riskIcon}`);
6796
- newline();
6797
- if (data.recommendations.length > 0) {
6798
- info("\u{1F4A1} \uAD8C\uC7A5\uC0AC\uD56D:");
6799
- for (const rec of data.recommendations) {
6800
- listItem(rec, 1);
6801
- }
6802
- }
7042
+ console.log(formatChangeImpactOutput(result.data));
6803
7043
  }
6804
7044
  }
6805
7045
  async function runSimulate(feature, proposalPath, options) {
@@ -6810,65 +7050,208 @@ async function runSimulate(feature, proposalPath, options) {
6810
7050
  }
6811
7051
  const sddPath = path10.join(projectRoot, ".sdd");
6812
7052
  const specsPath = path10.join(sddPath, "specs");
6813
- let fullProposalPath = proposalPath;
6814
- if (!path10.isAbsolute(proposalPath)) {
6815
- const changesPath = path10.join(sddPath, "changes", proposalPath);
6816
- if (proposalPath.endsWith(".md")) {
6817
- fullProposalPath = changesPath;
6818
- } else {
6819
- fullProposalPath = path10.join(changesPath, "proposal.md");
6820
- }
6821
- }
7053
+ const fullProposalPath = resolveProposalPath(proposalPath, sddPath);
6822
7054
  info(`\u{1F4CA} What-if \uC2DC\uBBAC\uB808\uC774\uC158`);
6823
7055
  info(`\uB300\uC0C1 \uC2A4\uD399: ${feature}`);
6824
7056
  info(`\uBCC0\uACBD \uC81C\uC548: ${fullProposalPath}`);
6825
7057
  newline();
6826
- const deltaResult = await parseDeltaFromProposal(fullProposalPath);
6827
- if (!deltaResult.success) {
6828
- error(deltaResult.error.message);
7058
+ const result = await executeSimulation(specsPath, feature, fullProposalPath);
7059
+ if (!result.success) {
7060
+ if (result.error.message.includes("\uB378\uD0C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4")) {
7061
+ warn(result.error.message);
7062
+ info("ADDED, MODIFIED, REMOVED \uC139\uC158\uC744 \uD655\uC778\uD558\uC138\uC694.");
7063
+ return;
7064
+ }
7065
+ error(result.error.message);
6829
7066
  process.exit(ExitCode.GENERAL_ERROR);
6830
7067
  }
6831
- const deltas = deltaResult.data;
6832
- if (deltas.length === 0) {
6833
- warn("\uBCC0\uACBD \uC81C\uC548\uC5D0\uC11C \uB378\uD0C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
6834
- info("ADDED, MODIFIED, REMOVED \uC139\uC158\uC744 \uD655\uC778\uD558\uC138\uC694.");
6835
- return;
6836
- }
7068
+ const { deltas, result: simResult } = result.data;
6837
7069
  info(`\uAC10\uC9C0\uB41C \uBCC0\uACBD: ${deltas.length}\uAC74`);
6838
7070
  for (const delta of deltas) {
6839
7071
  const icon = delta.type === "ADDED" ? "\u2795" : delta.type === "REMOVED" ? "\u2796" : "\u270F\uFE0F";
6840
7072
  listItem(`${icon} ${delta.type}: ${delta.specId}`, 1);
6841
7073
  }
6842
7074
  newline();
6843
- const simResult = await runSimulation(specsPath, feature, deltas);
6844
- if (!simResult.success) {
6845
- error(simResult.error.message);
6846
- process.exit(ExitCode.GENERAL_ERROR);
6847
- }
6848
7075
  if (options.json) {
6849
- console.log(JSON.stringify(simResult.data, null, 2));
7076
+ console.log(JSON.stringify(simResult, null, 2));
6850
7077
  } else {
6851
- console.log(formatSimulationResult(simResult.data, feature));
7078
+ console.log(formatSimulationResult(simResult, feature));
6852
7079
  }
6853
7080
  }
6854
7081
 
6855
7082
  // src/cli/commands/new.ts
6856
7083
  init_new();
6857
7084
  import path12 from "path";
6858
- import { promises as fs7 } from "fs";
6859
7085
 
6860
7086
  // src/utils/index.ts
6861
7087
  init_fs();
6862
7088
 
6863
7089
  // src/cli/commands/new.ts
6864
7090
  init_fs();
6865
- function registerNewCommand(program2) {
6866
- const newCmd = program2.command("new").description("\uC0C8\uB85C\uC6B4 \uAE30\uB2A5 \uC0DD\uC131").argument("[name]", "\uAE30\uB2A5 \uC774\uB984").option("--title <title>", "\uAE30\uB2A5 \uC81C\uBAA9").option("--description <desc>", "\uAE30\uB2A5 \uC124\uBA85").option("--no-branch", "\uBE0C\uB79C\uCE58 \uC0DD\uC131 \uC548 \uD568").option("--numbered", "\uC790\uB3D9 \uBC88\uD638 \uBD80\uC5EC (feature/001-name \uD615\uC2DD)").option("--plan", "\uACC4\uD68D \uD30C\uC77C\uB3C4 \uD568\uAED8 \uC0DD\uC131").option("--tasks", "\uC791\uC5C5 \uBD84\uD574 \uD30C\uC77C\uB3C4 \uD568\uAED8 \uC0DD\uC131").option("--all", "\uBAA8\uB4E0 \uD30C\uC77C \uC0DD\uC131 (spec, plan, tasks)").option("--checklist", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131").action(async (name, options) => {
6867
- await handleNew(name, options);
6868
- });
6869
- newCmd.command("plan").description("\uAE30\uB2A5 \uAD6C\uD604 \uACC4\uD68D \uC0DD\uC131").argument("<feature>", "\uAE30\uB2A5 ID").option("--title <title>", "\uACC4\uD68D \uC81C\uBAA9").action(async (feature, opts) => {
6870
- await handlePlan(feature, opts);
6871
- });
7091
+ init_types();
7092
+ async function getConstitutionVersion(sddPath) {
7093
+ const constitutionPath = path12.join(sddPath, "constitution.md");
7094
+ if (!await fileExists(constitutionPath)) {
7095
+ return void 0;
7096
+ }
7097
+ const constResult = await readFile(constitutionPath);
7098
+ if (!constResult.success) {
7099
+ return void 0;
7100
+ }
7101
+ const parseResult = parseConstitution(constResult.data);
7102
+ if (!parseResult.success) {
7103
+ return void 0;
7104
+ }
7105
+ return parseResult.data.metadata.version;
7106
+ }
7107
+ async function createFeature(sddPath, name, options) {
7108
+ let featureId;
7109
+ let branchName;
7110
+ if (options.numbered) {
7111
+ const numberResult = await getNextFeatureNumber(sddPath, name);
7112
+ if (!numberResult.success) {
7113
+ return failure(new Error(`\uBC88\uD638 \uC0DD\uC131 \uC2E4\uD328: ${numberResult.error.message}`));
7114
+ }
7115
+ featureId = numberResult.data.fullId;
7116
+ branchName = numberResult.data.branchName;
7117
+ } else {
7118
+ featureId = generateFeatureId(name);
7119
+ }
7120
+ const title2 = options.title || name;
7121
+ const description = options.description || `${title2} \uAE30\uB2A5 \uBA85\uC138`;
7122
+ const featurePath = path12.join(sddPath, "specs", featureId);
7123
+ const dirResult = await ensureDir(featurePath);
7124
+ if (!dirResult.success) {
7125
+ return failure(new Error(`\uB514\uB809\uD1A0\uB9AC \uC0DD\uC131 \uC2E4\uD328: ${featurePath}`));
7126
+ }
7127
+ const constitutionVersion = await getConstitutionVersion(sddPath);
7128
+ const filesCreated = [];
7129
+ const specContent = generateSpec({
7130
+ id: featureId,
7131
+ title: title2,
7132
+ description,
7133
+ constitutionVersion
7134
+ });
7135
+ await writeFile(path12.join(featurePath, "spec.md"), specContent);
7136
+ filesCreated.push("spec.md");
7137
+ if (options.plan || options.all) {
7138
+ const planContent = generatePlan({
7139
+ featureId,
7140
+ featureTitle: title2,
7141
+ overview: description
7142
+ });
7143
+ await writeFile(path12.join(featurePath, "plan.md"), planContent);
7144
+ filesCreated.push("plan.md");
7145
+ }
7146
+ if (options.tasks || options.all) {
7147
+ const tasksContent = generateTasks({
7148
+ featureId,
7149
+ featureTitle: title2,
7150
+ tasks: [
7151
+ { title: "\uAE30\uBC18 \uAD6C\uC870 \uC124\uC815", priority: "high" },
7152
+ { title: "\uD575\uC2EC \uAE30\uB2A5 \uAD6C\uD604", priority: "high" },
7153
+ { title: "\uD14C\uC2A4\uD2B8 \uC791\uC131", priority: "medium" },
7154
+ { title: "\uBB38\uC11C \uC5C5\uB370\uC774\uD2B8", priority: "low" }
7155
+ ]
7156
+ });
7157
+ await writeFile(path12.join(featurePath, "tasks.md"), tasksContent);
7158
+ filesCreated.push("tasks.md");
7159
+ }
7160
+ if (options.checklist || options.all) {
7161
+ const checklistContent = generateFullChecklistMarkdown();
7162
+ await writeFile(path12.join(featurePath, "checklist.md"), checklistContent);
7163
+ filesCreated.push("checklist.md");
7164
+ }
7165
+ return success({
7166
+ featureId,
7167
+ featurePath,
7168
+ branchName,
7169
+ filesCreated
7170
+ });
7171
+ }
7172
+ async function createPlan(featurePath, featureId, title2) {
7173
+ if (!await fileExists(featurePath)) {
7174
+ return failure(new Error(`\uAE30\uB2A5 '${featureId}'\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`));
7175
+ }
7176
+ let featureTitle = title2 || featureId;
7177
+ const specPath = path12.join(featurePath, "spec.md");
7178
+ if (await fileExists(specPath)) {
7179
+ const specResult = await readFile(specPath);
7180
+ if (specResult.success) {
7181
+ const titleMatch = specResult.data.match(/title:\s*"?([^"\n]+)"?/);
7182
+ if (titleMatch) {
7183
+ featureTitle = titleMatch[1];
7184
+ }
7185
+ }
7186
+ }
7187
+ const planContent = generatePlan({
7188
+ featureId,
7189
+ featureTitle,
7190
+ overview: `${featureTitle} \uAD6C\uD604 \uACC4\uD68D`
7191
+ });
7192
+ const planPath = path12.join(featurePath, "plan.md");
7193
+ await writeFile(planPath, planContent);
7194
+ return success(planPath);
7195
+ }
7196
+ async function createTasks(featurePath, featureId) {
7197
+ if (!await fileExists(featurePath)) {
7198
+ return failure(new Error(`\uAE30\uB2A5 '${featureId}'\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`));
7199
+ }
7200
+ let featureTitle = featureId;
7201
+ const specPath = path12.join(featurePath, "spec.md");
7202
+ if (await fileExists(specPath)) {
7203
+ const specResult = await readFile(specPath);
7204
+ if (specResult.success) {
7205
+ const titleMatch = specResult.data.match(/title:\s*"?([^"\n]+)"?/);
7206
+ if (titleMatch) {
7207
+ featureTitle = titleMatch[1];
7208
+ }
7209
+ }
7210
+ }
7211
+ const tasksContent = generateTasks({
7212
+ featureId,
7213
+ featureTitle,
7214
+ tasks: [
7215
+ { title: "\uAE30\uBC18 \uAD6C\uC870 \uC124\uC815", priority: "high" },
7216
+ { title: "\uD575\uC2EC \uAE30\uB2A5 \uAD6C\uD604", priority: "high" },
7217
+ { title: "\uD14C\uC2A4\uD2B8 \uC791\uC131", priority: "medium" },
7218
+ { title: "\uBB38\uC11C \uC5C5\uB370\uC774\uD2B8", priority: "low" }
7219
+ ]
7220
+ });
7221
+ const tasksPath = path12.join(featurePath, "tasks.md");
7222
+ await writeFile(tasksPath, tasksContent);
7223
+ return success(tasksPath);
7224
+ }
7225
+ async function createChecklist2(sddPath) {
7226
+ if (!await fileExists(sddPath)) {
7227
+ return failure(new Error(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
7228
+ }
7229
+ const checklistContent = generateFullChecklistMarkdown();
7230
+ const outputPath = path12.join(sddPath, "checklist.md");
7231
+ await writeFile(outputPath, checklistContent);
7232
+ return success(outputPath);
7233
+ }
7234
+ async function getCounterStatus(sddPath) {
7235
+ const peekResult = await peekNextFeatureNumber(sddPath);
7236
+ if (!peekResult.success) {
7237
+ return failure(new Error(`\uCE74\uC6B4\uD130 \uC870\uD68C \uC2E4\uD328: ${peekResult.error.message}`));
7238
+ }
7239
+ const historyResult = await getFeatureHistory(sddPath);
7240
+ if (!historyResult.success) {
7241
+ return failure(new Error(`\uC774\uB825 \uC870\uD68C \uC2E4\uD328: ${historyResult.error.message}`));
7242
+ }
7243
+ return success({
7244
+ nextNumber: peekResult.data,
7245
+ totalFeatures: historyResult.data.length
7246
+ });
7247
+ }
7248
+ function registerNewCommand(program2) {
7249
+ const newCmd = program2.command("new").description("\uC0C8\uB85C\uC6B4 \uAE30\uB2A5 \uC0DD\uC131").argument("[name]", "\uAE30\uB2A5 \uC774\uB984").option("--title <title>", "\uAE30\uB2A5 \uC81C\uBAA9").option("--description <desc>", "\uAE30\uB2A5 \uC124\uBA85").option("--no-branch", "\uBE0C\uB79C\uCE58 \uC0DD\uC131 \uC548 \uD568").option("--numbered", "\uC790\uB3D9 \uBC88\uD638 \uBD80\uC5EC (feature/001-name \uD615\uC2DD)").option("--plan", "\uACC4\uD68D \uD30C\uC77C\uB3C4 \uD568\uAED8 \uC0DD\uC131").option("--tasks", "\uC791\uC5C5 \uBD84\uD574 \uD30C\uC77C\uB3C4 \uD568\uAED8 \uC0DD\uC131").option("--all", "\uBAA8\uB4E0 \uD30C\uC77C \uC0DD\uC131 (spec, plan, tasks)").option("--checklist", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131").action(async (name, options) => {
7250
+ await handleNew(name, options);
7251
+ });
7252
+ newCmd.command("plan").description("\uAE30\uB2A5 \uAD6C\uD604 \uACC4\uD68D \uC0DD\uC131").argument("<feature>", "\uAE30\uB2A5 ID").option("--title <title>", "\uACC4\uD68D \uC81C\uBAA9").action(async (feature, opts) => {
7253
+ await handlePlan(feature, opts);
7254
+ });
6872
7255
  newCmd.command("tasks").description("\uC791\uC5C5 \uBD84\uD574 \uC0DD\uC131").argument("<feature>", "\uAE30\uB2A5 ID").action(async (feature) => {
6873
7256
  await handleTasks(feature);
6874
7257
  });
@@ -6886,193 +7269,88 @@ async function handleNew(name, options) {
6886
7269
  }
6887
7270
  const cwd = process.cwd();
6888
7271
  const sddPath = path12.join(cwd, ".sdd");
6889
- let featureId;
6890
- let branchName;
6891
- if (options.numbered) {
6892
- const numberResult = await getNextFeatureNumber(sddPath, name);
6893
- if (!numberResult.success) {
6894
- logger_exports.error(`\uBC88\uD638 \uC0DD\uC131 \uC2E4\uD328: ${numberResult.error.message}`);
6895
- process.exit(1);
6896
- }
6897
- featureId = numberResult.data.fullId;
6898
- branchName = numberResult.data.branchName;
6899
- logger_exports.info(`\uC790\uB3D9 \uBC88\uD638 \uBD80\uC5EC: #${numberResult.data.number.toString().padStart(3, "0")}`);
6900
- } else {
6901
- featureId = generateFeatureId(name);
7272
+ if (!await fileExists(sddPath)) {
7273
+ logger_exports.error(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 sdd init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
7274
+ process.exit(1);
6902
7275
  }
6903
- const title2 = options.title || name;
6904
- const description = options.description || `${title2} \uAE30\uB2A5 \uBA85\uC138`;
6905
- const featurePath = path12.join(sddPath, "specs", featureId);
6906
- try {
6907
- if (!await fileExists(sddPath)) {
6908
- logger_exports.error(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 sdd init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
6909
- process.exit(1);
6910
- }
6911
- await ensureDir(featurePath);
6912
- let constitutionVersion;
6913
- const constitutionPath = path12.join(sddPath, "constitution.md");
6914
- if (await fileExists(constitutionPath)) {
6915
- const constResult = await readFile(constitutionPath);
6916
- if (constResult.success) {
6917
- const parseResult = parseConstitution(constResult.data);
6918
- if (parseResult.success) {
6919
- constitutionVersion = parseResult.data.metadata.version;
6920
- }
6921
- }
7276
+ const result = await createFeature(sddPath, name, options);
7277
+ if (!result.success) {
7278
+ logger_exports.error(`\uAE30\uB2A5 \uC0DD\uC131 \uC2E4\uD328: ${result.error.message}`);
7279
+ process.exit(1);
7280
+ }
7281
+ const { featureId, featurePath, branchName, filesCreated } = result.data;
7282
+ if (options.numbered && branchName) {
7283
+ const numberMatch = branchName.match(/feature\/(\d+)-/);
7284
+ if (numberMatch) {
7285
+ logger_exports.info(`\uC790\uB3D9 \uBC88\uD638 \uBD80\uC5EC: #${numberMatch[1]}`);
6922
7286
  }
6923
- const specContent = generateSpec({
6924
- id: featureId,
6925
- title: title2,
6926
- description,
6927
- constitutionVersion
6928
- });
6929
- await fs7.writeFile(path12.join(featurePath, "spec.md"), specContent, "utf-8");
6930
- logger_exports.info(`\u2705 \uBA85\uC138 \uC0DD\uC131: ${featurePath}/spec.md`);
6931
- if (options.branch !== false) {
6932
- if (await isGitRepository(cwd)) {
6933
- const branchToCreate = branchName || featureId;
6934
- const result = await createBranch(branchToCreate, { checkout: true, cwd });
6935
- if (result.success) {
6936
- logger_exports.info(`\u2705 \uBE0C\uB79C\uCE58 \uC0DD\uC131: ${result.data}`);
6937
- } else {
6938
- logger_exports.warn(`\u26A0\uFE0F \uBE0C\uB79C\uCE58 \uC0DD\uC131 \uC2E4\uD328: ${result.error.message}`);
6939
- }
7287
+ }
7288
+ for (const file of filesCreated) {
7289
+ logger_exports.info(`\u2705 ${file} \uC0DD\uC131: ${featurePath}/${file}`);
7290
+ }
7291
+ if (options.branch !== false) {
7292
+ if (await isGitRepository(cwd)) {
7293
+ const branchToCreate = branchName || featureId;
7294
+ const branchResult = await createBranch(branchToCreate, { checkout: true, cwd });
7295
+ if (branchResult.success) {
7296
+ logger_exports.info(`\u2705 \uBE0C\uB79C\uCE58 \uC0DD\uC131: ${branchResult.data}`);
6940
7297
  } else {
6941
- logger_exports.warn("\u26A0\uFE0F Git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uBE0C\uB79C\uCE58 \uC0DD\uC131\uC744 \uAC74\uB108\uB701\uB2C8\uB2E4.");
7298
+ logger_exports.warn(`\u26A0\uFE0F \uBE0C\uB79C\uCE58 \uC0DD\uC131 \uC2E4\uD328: ${branchResult.error.message}`);
6942
7299
  }
7300
+ } else {
7301
+ logger_exports.warn("\u26A0\uFE0F Git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uBE0C\uB79C\uCE58 \uC0DD\uC131\uC744 \uAC74\uB108\uB701\uB2C8\uB2E4.");
6943
7302
  }
6944
- if (options.plan || options.all) {
6945
- const planContent = generatePlan({
6946
- featureId,
6947
- featureTitle: title2,
6948
- overview: description
6949
- });
6950
- await fs7.writeFile(path12.join(featurePath, "plan.md"), planContent, "utf-8");
6951
- logger_exports.info(`\u2705 \uACC4\uD68D \uC0DD\uC131: ${featurePath}/plan.md`);
6952
- }
6953
- if (options.tasks || options.all) {
6954
- const tasksContent = generateTasks({
6955
- featureId,
6956
- featureTitle: title2,
6957
- tasks: [
6958
- { title: "\uAE30\uBC18 \uAD6C\uC870 \uC124\uC815", priority: "high" },
6959
- { title: "\uD575\uC2EC \uAE30\uB2A5 \uAD6C\uD604", priority: "high" },
6960
- { title: "\uD14C\uC2A4\uD2B8 \uC791\uC131", priority: "medium" },
6961
- { title: "\uBB38\uC11C \uC5C5\uB370\uC774\uD2B8", priority: "low" }
6962
- ]
6963
- });
6964
- await fs7.writeFile(path12.join(featurePath, "tasks.md"), tasksContent, "utf-8");
6965
- logger_exports.info(`\u2705 \uC791\uC5C5 \uBD84\uD574 \uC0DD\uC131: ${featurePath}/tasks.md`);
6966
- }
6967
- if (options.checklist || options.all) {
6968
- const checklistContent = generateFullChecklistMarkdown();
6969
- await fs7.writeFile(path12.join(featurePath, "checklist.md"), checklistContent, "utf-8");
6970
- logger_exports.info(`\u2705 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC0DD\uC131: ${featurePath}/checklist.md`);
6971
- }
6972
- logger_exports.info("");
6973
- logger_exports.info(`\u{1F389} \uAE30\uB2A5 '${featureId}' \uC0DD\uC131 \uC644\uB8CC!`);
6974
- logger_exports.info("");
6975
- logger_exports.info("\uB2E4\uC74C \uB2E8\uACC4:");
6976
- logger_exports.info(` 1. ${featurePath}/spec.md \uD3B8\uC9D1`);
6977
- if (!(options.plan || options.all)) {
6978
- logger_exports.info(" 2. sdd new plan " + featureId + " - \uACC4\uD68D \uC791\uC131");
6979
- }
6980
- if (!(options.tasks || options.all)) {
6981
- logger_exports.info(" 3. sdd new tasks " + featureId + " - \uC791\uC5C5 \uBD84\uD574");
6982
- }
6983
- logger_exports.info(" 4. sdd validate - \uBA85\uC138 \uAC80\uC99D");
6984
- } catch (error2) {
6985
- logger_exports.error(`\uAE30\uB2A5 \uC0DD\uC131 \uC2E4\uD328: ${error2}`);
6986
- process.exit(1);
6987
7303
  }
7304
+ logger_exports.info("");
7305
+ logger_exports.info(`\u{1F389} \uAE30\uB2A5 '${featureId}' \uC0DD\uC131 \uC644\uB8CC!`);
7306
+ logger_exports.info("");
7307
+ logger_exports.info("\uB2E4\uC74C \uB2E8\uACC4:");
7308
+ logger_exports.info(` 1. ${featurePath}/spec.md \uD3B8\uC9D1`);
7309
+ if (!(options.plan || options.all)) {
7310
+ logger_exports.info(" 2. sdd new plan " + featureId + " - \uACC4\uD68D \uC791\uC131");
7311
+ }
7312
+ if (!(options.tasks || options.all)) {
7313
+ logger_exports.info(" 3. sdd new tasks " + featureId + " - \uC791\uC5C5 \uBD84\uD574");
7314
+ }
7315
+ logger_exports.info(" 4. sdd validate - \uBA85\uC138 \uAC80\uC99D");
6988
7316
  }
6989
7317
  async function handlePlan(feature, options) {
6990
7318
  const cwd = process.cwd();
6991
7319
  const featurePath = path12.join(cwd, ".sdd", "specs", feature);
6992
- try {
6993
- if (!await fileExists(featurePath)) {
6994
- logger_exports.error(`\uAE30\uB2A5 '${feature}'\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
6995
- process.exit(1);
6996
- }
6997
- let title2 = options.title || feature;
6998
- const specPath = path12.join(featurePath, "spec.md");
6999
- if (await fileExists(specPath)) {
7000
- const specContent = await fs7.readFile(specPath, "utf-8");
7001
- const titleMatch = specContent.match(/title:\s*"?([^"\n]+)"?/);
7002
- if (titleMatch) {
7003
- title2 = titleMatch[1];
7004
- }
7005
- }
7006
- const planContent = generatePlan({
7007
- featureId: feature,
7008
- featureTitle: title2,
7009
- overview: `${title2} \uAD6C\uD604 \uACC4\uD68D`
7010
- });
7011
- await fs7.writeFile(path12.join(featurePath, "plan.md"), planContent, "utf-8");
7012
- logger_exports.info(`\u2705 \uACC4\uD68D \uC0DD\uC131: ${featurePath}/plan.md`);
7013
- logger_exports.info("");
7014
- logger_exports.info("\uB2E4\uC74C \uB2E8\uACC4:");
7015
- logger_exports.info(` 1. ${featurePath}/plan.md \uD3B8\uC9D1`);
7016
- logger_exports.info(" 2. sdd new tasks " + feature + " - \uC791\uC5C5 \uBD84\uD574");
7017
- } catch (error2) {
7018
- logger_exports.error(`\uACC4\uD68D \uC0DD\uC131 \uC2E4\uD328: ${error2}`);
7320
+ const result = await createPlan(featurePath, feature, options.title);
7321
+ if (!result.success) {
7322
+ logger_exports.error(result.error.message);
7019
7323
  process.exit(1);
7020
7324
  }
7325
+ logger_exports.info(`\u2705 \uACC4\uD68D \uC0DD\uC131: ${result.data}`);
7326
+ logger_exports.info("");
7327
+ logger_exports.info("\uB2E4\uC74C \uB2E8\uACC4:");
7328
+ logger_exports.info(` 1. ${featurePath}/plan.md \uD3B8\uC9D1`);
7329
+ logger_exports.info(" 2. sdd new tasks " + feature + " - \uC791\uC5C5 \uBD84\uD574");
7021
7330
  }
7022
7331
  async function handleTasks(feature) {
7023
7332
  const cwd = process.cwd();
7024
7333
  const featurePath = path12.join(cwd, ".sdd", "specs", feature);
7025
- try {
7026
- if (!await fileExists(featurePath)) {
7027
- logger_exports.error(`\uAE30\uB2A5 '${feature}'\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
7028
- process.exit(1);
7029
- }
7030
- let title2 = feature;
7031
- const specPath = path12.join(featurePath, "spec.md");
7032
- if (await fileExists(specPath)) {
7033
- const specContent = await fs7.readFile(specPath, "utf-8");
7034
- const titleMatch = specContent.match(/title:\s*"?([^"\n]+)"?/);
7035
- if (titleMatch) {
7036
- title2 = titleMatch[1];
7037
- }
7038
- }
7039
- const tasksContent = generateTasks({
7040
- featureId: feature,
7041
- featureTitle: title2,
7042
- tasks: [
7043
- { title: "\uAE30\uBC18 \uAD6C\uC870 \uC124\uC815", priority: "high" },
7044
- { title: "\uD575\uC2EC \uAE30\uB2A5 \uAD6C\uD604", priority: "high" },
7045
- { title: "\uD14C\uC2A4\uD2B8 \uC791\uC131", priority: "medium" },
7046
- { title: "\uBB38\uC11C \uC5C5\uB370\uC774\uD2B8", priority: "low" }
7047
- ]
7048
- });
7049
- await fs7.writeFile(path12.join(featurePath, "tasks.md"), tasksContent, "utf-8");
7050
- logger_exports.info(`\u2705 \uC791\uC5C5 \uBD84\uD574 \uC0DD\uC131: ${featurePath}/tasks.md`);
7051
- logger_exports.info("");
7052
- logger_exports.info("\uB2E4\uC74C \uB2E8\uACC4:");
7053
- logger_exports.info(` 1. ${featurePath}/tasks.md \uD3B8\uC9D1`);
7054
- logger_exports.info(" 2. \uAC01 \uC791\uC5C5 \uC21C\uCC28\uC801\uC73C\uB85C \uAD6C\uD604");
7055
- } catch (error2) {
7056
- logger_exports.error(`\uC791\uC5C5 \uBD84\uD574 \uC0DD\uC131 \uC2E4\uD328: ${error2}`);
7334
+ const result = await createTasks(featurePath, feature);
7335
+ if (!result.success) {
7336
+ logger_exports.error(result.error.message);
7057
7337
  process.exit(1);
7058
7338
  }
7339
+ logger_exports.info(`\u2705 \uC791\uC5C5 \uBD84\uD574 \uC0DD\uC131: ${result.data}`);
7340
+ logger_exports.info("");
7341
+ logger_exports.info("\uB2E4\uC74C \uB2E8\uACC4:");
7342
+ logger_exports.info(` 1. ${featurePath}/tasks.md \uD3B8\uC9D1`);
7343
+ logger_exports.info(" 2. \uAC01 \uC791\uC5C5 \uC21C\uCC28\uC801\uC73C\uB85C \uAD6C\uD604");
7059
7344
  }
7060
7345
  async function handleChecklist() {
7061
7346
  const cwd = process.cwd();
7062
7347
  const sddPath = path12.join(cwd, ".sdd");
7063
- try {
7064
- if (!await fileExists(sddPath)) {
7065
- logger_exports.error(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 sdd init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
7066
- process.exit(1);
7067
- }
7068
- const checklistContent = generateFullChecklistMarkdown();
7069
- const outputPath = path12.join(sddPath, "checklist.md");
7070
- await fs7.writeFile(outputPath, checklistContent, "utf-8");
7071
- logger_exports.info(`\u2705 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC0DD\uC131: ${outputPath}`);
7072
- } catch (error2) {
7073
- logger_exports.error(`\uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC0DD\uC131 \uC2E4\uD328: ${error2}`);
7348
+ const result = await createChecklist2(sddPath);
7349
+ if (!result.success) {
7350
+ logger_exports.error(result.error.message);
7074
7351
  process.exit(1);
7075
7352
  }
7353
+ logger_exports.info(`\u2705 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC0DD\uC131: ${result.data}`);
7076
7354
  }
7077
7355
  async function handleCounter(options) {
7078
7356
  const cwd = process.cwd();
@@ -7131,39 +7409,65 @@ async function handleCounter(options) {
7131
7409
  }
7132
7410
  return;
7133
7411
  }
7134
- const peekResult = await peekNextFeatureNumber(sddPath);
7135
- const historyResult = await getFeatureHistory(sddPath);
7136
- if (peekResult.success && historyResult.success) {
7412
+ const statusResult = await getCounterStatus(sddPath);
7413
+ if (statusResult.success) {
7137
7414
  logger_exports.info("=== \uAE30\uB2A5 \uBC88\uD638 \uCE74\uC6B4\uD130 \uC0C1\uD0DC ===");
7138
7415
  logger_exports.info("");
7139
- logger_exports.info(`\uB2E4\uC74C \uBC88\uD638: #${String(peekResult.data).padStart(3, "0")}`);
7140
- logger_exports.info(`\uC0DD\uC131\uB41C \uAE30\uB2A5 \uC218: ${historyResult.data.length}\uAC1C`);
7416
+ logger_exports.info(`\uB2E4\uC74C \uBC88\uD638: #${String(statusResult.data.nextNumber).padStart(3, "0")}`);
7417
+ logger_exports.info(`\uC0DD\uC131\uB41C \uAE30\uB2A5 \uC218: ${statusResult.data.totalFeatures}\uAC1C`);
7141
7418
  logger_exports.info("");
7142
7419
  logger_exports.info("\uC635\uC158:");
7143
7420
  logger_exports.info(" --peek \uB2E4\uC74C \uBC88\uD638 \uD655\uC778");
7144
7421
  logger_exports.info(" --history \uC0DD\uC131 \uC774\uB825 \uC870\uD68C");
7145
7422
  logger_exports.info(" --set <n> \uB2E4\uC74C \uBC88\uD638 \uC124\uC815");
7146
7423
  } else {
7147
- logger_exports.error("\uCE74\uC6B4\uD130 \uC0C1\uD0DC \uC870\uD68C \uC2E4\uD328");
7424
+ logger_exports.error(statusResult.error.message);
7148
7425
  process.exit(1);
7149
7426
  }
7150
7427
  }
7151
7428
 
7152
7429
  // src/cli/commands/status.ts
7153
7430
  import path13 from "path";
7154
- import { promises as fs8 } from "fs";
7431
+ import { promises as fs7 } from "fs";
7155
7432
  init_fs();
7156
7433
  init_spec_generator();
7157
7434
  init_task_generator();
7158
7435
  init_branch();
7159
- function registerStatusCommand(program2) {
7160
- program2.command("status").description("SDD \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uC870\uD68C").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--verbose", "\uC0C1\uC138 \uC815\uBCF4 \uCD9C\uB825").action(async (options) => {
7161
- await handleStatus(options);
7162
- });
7436
+ async function getFeatureInfo(id, featurePath) {
7437
+ const info2 = {
7438
+ id,
7439
+ title: id,
7440
+ status: "unknown",
7441
+ hasSpec: false,
7442
+ hasPlan: false,
7443
+ hasTasks: false
7444
+ };
7445
+ const specPath = path13.join(featurePath, "spec.md");
7446
+ if (await fileExists(specPath)) {
7447
+ info2.hasSpec = true;
7448
+ const content = await fs7.readFile(specPath, "utf-8");
7449
+ const metadata = parseSpecMetadata(content);
7450
+ if (metadata) {
7451
+ info2.title = metadata.title;
7452
+ info2.status = metadata.status;
7453
+ }
7454
+ }
7455
+ info2.hasPlan = await fileExists(path13.join(featurePath, "plan.md"));
7456
+ const tasksPath = path13.join(featurePath, "tasks.md");
7457
+ if (await fileExists(tasksPath)) {
7458
+ info2.hasTasks = true;
7459
+ const content = await fs7.readFile(tasksPath, "utf-8");
7460
+ const tasks = parseTasks(content);
7461
+ const completed = tasks.filter((t) => t.status === "completed").length;
7462
+ info2.taskProgress = {
7463
+ completed,
7464
+ total: tasks.length
7465
+ };
7466
+ }
7467
+ return info2;
7163
7468
  }
7164
- async function handleStatus(options) {
7165
- const cwd = process.cwd();
7166
- const sddPath = path13.join(cwd, ".sdd");
7469
+ async function getProjectStatus(projectPath) {
7470
+ const sddPath = path13.join(projectPath, ".sdd");
7167
7471
  const status = {
7168
7472
  initialized: false,
7169
7473
  hasConstitution: false,
@@ -7175,13 +7479,7 @@ async function handleStatus(options) {
7175
7479
  };
7176
7480
  status.initialized = await fileExists(sddPath);
7177
7481
  if (!status.initialized) {
7178
- if (options.json) {
7179
- console.log(JSON.stringify(status, null, 2));
7180
- } else {
7181
- logger_exports.warn("SDD \uD504\uB85C\uC81D\uD2B8\uAC00 \uCD08\uAE30\uD654\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
7182
- logger_exports.info("sdd init \uBA85\uB839\uC5B4\uB85C \uCD08\uAE30\uD654\uD558\uC138\uC694.");
7183
- }
7184
- return;
7482
+ return status;
7185
7483
  }
7186
7484
  status.hasConstitution = await fileExists(path13.join(sddPath, "constitution.md"));
7187
7485
  status.hasAgents = await fileExists(path13.join(sddPath, "AGENTS.md"));
@@ -7191,7 +7489,7 @@ async function handleStatus(options) {
7191
7489
  if (specsResult.success) {
7192
7490
  for (const entry of specsResult.data) {
7193
7491
  const featurePath = path13.join(specsPath, entry);
7194
- const stat = await fs8.stat(featurePath);
7492
+ const stat = await fs7.stat(featurePath);
7195
7493
  if (stat.isDirectory()) {
7196
7494
  const featureInfo = await getFeatureInfo(entry, featurePath);
7197
7495
  status.features.push(featureInfo);
@@ -7207,53 +7505,57 @@ async function handleStatus(options) {
7207
7505
  if (archiveResult.success) {
7208
7506
  status.archivedChanges = archiveResult.data.length;
7209
7507
  }
7210
- const currentBranchResult = await getCurrentBranch(cwd);
7508
+ const currentBranchResult = await getCurrentBranch(projectPath);
7211
7509
  if (currentBranchResult.success) {
7212
7510
  status.currentBranch = currentBranchResult.data;
7213
7511
  }
7214
- const featureBranchesResult = await listFeatureBranches(cwd);
7512
+ const featureBranchesResult = await listFeatureBranches(projectPath);
7215
7513
  if (featureBranchesResult.success) {
7216
7514
  status.featureBranches = featureBranchesResult.data;
7217
7515
  }
7516
+ return status;
7517
+ }
7518
+ function getStatusIcon(status) {
7519
+ switch (status) {
7520
+ case "draft":
7521
+ return "\u{1F4DD}";
7522
+ case "specified":
7523
+ return "\u{1F4C4}";
7524
+ case "planned":
7525
+ return "\u{1F4CB}";
7526
+ case "tasked":
7527
+ return "\u270F\uFE0F";
7528
+ case "implementing":
7529
+ return "\u{1F528}";
7530
+ case "completed":
7531
+ return "\u2705";
7532
+ default:
7533
+ return "\u2753";
7534
+ }
7535
+ }
7536
+ function registerStatusCommand(program2) {
7537
+ program2.command("status").description("SDD \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uC870\uD68C").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--verbose", "\uC0C1\uC138 \uC815\uBCF4 \uCD9C\uB825").action(async (options) => {
7538
+ await handleStatus(options);
7539
+ });
7540
+ }
7541
+ async function handleStatus(options) {
7542
+ const cwd = process.cwd();
7543
+ const status = await getProjectStatus(cwd);
7544
+ if (!status.initialized) {
7545
+ if (options.json) {
7546
+ console.log(JSON.stringify(status, null, 2));
7547
+ } else {
7548
+ logger_exports.warn("SDD \uD504\uB85C\uC81D\uD2B8\uAC00 \uCD08\uAE30\uD654\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
7549
+ logger_exports.info("sdd init \uBA85\uB839\uC5B4\uB85C \uCD08\uAE30\uD654\uD558\uC138\uC694.");
7550
+ }
7551
+ return;
7552
+ }
7218
7553
  if (options.json) {
7219
7554
  console.log(JSON.stringify(status, null, 2));
7220
7555
  } else {
7221
7556
  printStatus(status, options.verbose);
7222
7557
  }
7223
7558
  }
7224
- async function getFeatureInfo(id, featurePath) {
7225
- const info2 = {
7226
- id,
7227
- title: id,
7228
- status: "unknown",
7229
- hasSpec: false,
7230
- hasPlan: false,
7231
- hasTasks: false
7232
- };
7233
- const specPath = path13.join(featurePath, "spec.md");
7234
- if (await fileExists(specPath)) {
7235
- info2.hasSpec = true;
7236
- const content = await fs8.readFile(specPath, "utf-8");
7237
- const metadata = parseSpecMetadata(content);
7238
- if (metadata) {
7239
- info2.title = metadata.title;
7240
- info2.status = metadata.status;
7241
- }
7242
- }
7243
- info2.hasPlan = await fileExists(path13.join(featurePath, "plan.md"));
7244
- const tasksPath = path13.join(featurePath, "tasks.md");
7245
- if (await fileExists(tasksPath)) {
7246
- info2.hasTasks = true;
7247
- const content = await fs8.readFile(tasksPath, "utf-8");
7248
- const tasks = parseTasks(content);
7249
- const completed = tasks.filter((t) => t.status === "completed").length;
7250
- info2.taskProgress = {
7251
- completed,
7252
- total: tasks.length
7253
- };
7254
- }
7255
- return info2;
7256
- }
7257
7559
  function printStatus(status, verbose) {
7258
7560
  console.log("");
7259
7561
  console.log("\u{1F4CA} SDD \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC");
@@ -7333,7 +7635,13 @@ function printStatus(status, verbose) {
7333
7635
  }
7334
7636
  console.log("");
7335
7637
  }
7336
- function getStatusIcon(status) {
7638
+
7639
+ // src/cli/commands/list.ts
7640
+ import path14 from "path";
7641
+ import { promises as fs8 } from "fs";
7642
+ init_fs();
7643
+ init_spec_generator();
7644
+ function getListStatusIcon(status) {
7337
7645
  switch (status) {
7338
7646
  case "draft":
7339
7647
  return "\u{1F4DD}";
@@ -7351,50 +7659,23 @@ function getStatusIcon(status) {
7351
7659
  return "\u2753";
7352
7660
  }
7353
7661
  }
7354
-
7355
- // src/cli/commands/list.ts
7356
- import path14 from "path";
7357
- import { promises as fs9 } from "fs";
7358
- init_fs();
7359
- init_spec_generator();
7360
- function registerListCommand(program2) {
7361
- const listCmd = program2.command("list").alias("ls").description("\uD56D\uBAA9 \uBAA9\uB85D \uC870\uD68C");
7362
- listCmd.command("features").alias("f").description("\uAE30\uB2A5 \uBAA9\uB85D \uC870\uD68C").option("--status <status>", "\uC0C1\uD0DC\uBCC4 \uD544\uD130 (draft, specified, planned, etc.)").action(async (options) => {
7363
- await listFeatures(options);
7364
- });
7365
- listCmd.command("changes").alias("c").description("\uBCC0\uACBD \uC81C\uC548 \uBAA9\uB85D \uC870\uD68C").option("--pending", "\uB300\uAE30 \uC911\uC778 \uBCC0\uACBD\uB9CC \uD45C\uC2DC").option("--archived", "\uC544\uCE74\uC774\uBE0C\uB41C \uBCC0\uACBD\uB9CC \uD45C\uC2DC").action(async (options) => {
7366
- await listChanges(options);
7367
- });
7368
- listCmd.command("specs").alias("s").description("\uC2A4\uD399 \uD30C\uC77C \uBAA9\uB85D \uC870\uD68C").action(async () => {
7369
- await listSpecs();
7370
- });
7371
- listCmd.command("templates").alias("t").description("\uD15C\uD50C\uB9BF \uBAA9\uB85D \uC870\uD68C").action(async () => {
7372
- await listTemplates();
7373
- });
7374
- listCmd.action(async () => {
7375
- await listSummary();
7376
- });
7377
- }
7378
- async function listFeatures(options) {
7379
- const cwd = process.cwd();
7380
- const specsPath = path14.join(cwd, ".sdd", "specs");
7662
+ async function getFeatureList(projectPath, options = {}) {
7663
+ const specsPath = path14.join(projectPath, ".sdd", "specs");
7381
7664
  if (!await fileExists(specsPath)) {
7382
- logger_exports.warn("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
7383
- return;
7665
+ return [];
7384
7666
  }
7385
7667
  const result = await readDir(specsPath);
7386
7668
  if (!result.success) {
7387
- logger_exports.error("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
7388
- return;
7669
+ return [];
7389
7670
  }
7390
7671
  const features = [];
7391
7672
  for (const entry of result.data) {
7392
7673
  const featurePath = path14.join(specsPath, entry);
7393
- const stat = await fs9.stat(featurePath);
7674
+ const stat = await fs8.stat(featurePath);
7394
7675
  if (stat.isDirectory()) {
7395
7676
  const specPath = path14.join(featurePath, "spec.md");
7396
7677
  if (await fileExists(specPath)) {
7397
- const content = await fs9.readFile(specPath, "utf-8");
7678
+ const content = await fs8.readFile(specPath, "utf-8");
7398
7679
  const metadata = parseSpecMetadata(content);
7399
7680
  if (metadata) {
7400
7681
  if (!options.status || metadata.status === options.status) {
@@ -7408,6 +7689,119 @@ async function listFeatures(options) {
7408
7689
  }
7409
7690
  }
7410
7691
  }
7692
+ return features;
7693
+ }
7694
+ async function getChangeList(projectPath, options = {}) {
7695
+ const sddPath = path14.join(projectPath, ".sdd");
7696
+ const result = {
7697
+ pending: [],
7698
+ archived: []
7699
+ };
7700
+ if (!await fileExists(sddPath)) {
7701
+ return result;
7702
+ }
7703
+ if (!options.archived) {
7704
+ const pendingResult = await listPendingChanges(sddPath);
7705
+ if (pendingResult.success) {
7706
+ result.pending = pendingResult.data.map((c) => String(c));
7707
+ }
7708
+ }
7709
+ if (!options.pending) {
7710
+ const archiveResult = await listArchives(sddPath);
7711
+ if (archiveResult.success) {
7712
+ result.archived = archiveResult.data.map((a) => a.id);
7713
+ }
7714
+ }
7715
+ return result;
7716
+ }
7717
+ async function getSpecFileTree(specsPath) {
7718
+ if (!await fileExists(specsPath)) {
7719
+ return [];
7720
+ }
7721
+ return walkSpecsTree(specsPath);
7722
+ }
7723
+ async function walkSpecsTree(basePath) {
7724
+ const result = await readDir(basePath);
7725
+ if (!result.success) return [];
7726
+ const items = [];
7727
+ for (const entry of result.data) {
7728
+ const fullPath = path14.join(basePath, entry);
7729
+ const stat = await fs8.stat(fullPath);
7730
+ if (stat.isDirectory()) {
7731
+ const children = await walkSpecsTree(fullPath);
7732
+ items.push({
7733
+ path: fullPath,
7734
+ name: entry,
7735
+ isDirectory: true,
7736
+ children
7737
+ });
7738
+ } else if (entry.endsWith(".md")) {
7739
+ items.push({
7740
+ path: fullPath,
7741
+ name: entry,
7742
+ isDirectory: false
7743
+ });
7744
+ }
7745
+ }
7746
+ return items;
7747
+ }
7748
+ async function getTemplateList(projectPath) {
7749
+ const templatesPath = path14.join(projectPath, ".sdd", "templates");
7750
+ if (!await fileExists(templatesPath)) {
7751
+ return [];
7752
+ }
7753
+ const result = await readDir(templatesPath);
7754
+ if (!result.success) {
7755
+ return [];
7756
+ }
7757
+ return result.data.filter((f) => f.endsWith(".md"));
7758
+ }
7759
+ async function getProjectSummary(projectPath) {
7760
+ const sddPath = path14.join(projectPath, ".sdd");
7761
+ if (!await fileExists(sddPath)) {
7762
+ return null;
7763
+ }
7764
+ const specsPath = path14.join(sddPath, "specs");
7765
+ let featureCount = 0;
7766
+ if (await fileExists(specsPath)) {
7767
+ const result = await readDir(specsPath);
7768
+ if (result.success) {
7769
+ for (const entry of result.data) {
7770
+ const stat = await fs8.stat(path14.join(specsPath, entry));
7771
+ if (stat.isDirectory()) featureCount++;
7772
+ }
7773
+ }
7774
+ }
7775
+ const pendingResult = await listPendingChanges(sddPath);
7776
+ const pendingChangeCount = pendingResult.success ? pendingResult.data.length : 0;
7777
+ const archiveResult = await listArchives(sddPath);
7778
+ const archivedChangeCount = archiveResult.success ? archiveResult.data.length : 0;
7779
+ return {
7780
+ featureCount,
7781
+ pendingChangeCount,
7782
+ archivedChangeCount
7783
+ };
7784
+ }
7785
+ function registerListCommand(program2) {
7786
+ const listCmd = program2.command("list").alias("ls").description("\uD56D\uBAA9 \uBAA9\uB85D \uC870\uD68C");
7787
+ listCmd.command("features").alias("f").description("\uAE30\uB2A5 \uBAA9\uB85D \uC870\uD68C").option("--status <status>", "\uC0C1\uD0DC\uBCC4 \uD544\uD130 (draft, specified, planned, etc.)").action(async (options) => {
7788
+ await listFeatures(options);
7789
+ });
7790
+ listCmd.command("changes").alias("c").description("\uBCC0\uACBD \uC81C\uC548 \uBAA9\uB85D \uC870\uD68C").option("--pending", "\uB300\uAE30 \uC911\uC778 \uBCC0\uACBD\uB9CC \uD45C\uC2DC").option("--archived", "\uC544\uCE74\uC774\uBE0C\uB41C \uBCC0\uACBD\uB9CC \uD45C\uC2DC").action(async (options) => {
7791
+ await listChanges(options);
7792
+ });
7793
+ listCmd.command("specs").alias("s").description("\uC2A4\uD399 \uD30C\uC77C \uBAA9\uB85D \uC870\uD68C").action(async () => {
7794
+ await listSpecs();
7795
+ });
7796
+ listCmd.command("templates").alias("t").description("\uD15C\uD50C\uB9BF \uBAA9\uB85D \uC870\uD68C").action(async () => {
7797
+ await listTemplates();
7798
+ });
7799
+ listCmd.action(async () => {
7800
+ await listSummary();
7801
+ });
7802
+ }
7803
+ async function listFeatures(options) {
7804
+ const features = await getFeatureList(process.cwd(), options);
7411
7805
  if (features.length === 0) {
7412
7806
  logger_exports.info("\uAE30\uB2A5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
7413
7807
  return;
@@ -7416,25 +7810,19 @@ async function listFeatures(options) {
7416
7810
  console.log("\u{1F4CB} \uAE30\uB2A5 \uBAA9\uB85D");
7417
7811
  console.log("\u2500".repeat(50));
7418
7812
  for (const f of features) {
7419
- const statusIcon = getStatusIcon2(f.status);
7813
+ const statusIcon = getListStatusIcon(f.status);
7420
7814
  console.log(`${statusIcon} ${f.title} (${f.id}) - ${f.status}`);
7421
7815
  }
7422
7816
  console.log("");
7423
7817
  }
7424
7818
  async function listChanges(options) {
7425
- const cwd = process.cwd();
7426
- const sddPath = path14.join(cwd, ".sdd");
7427
- if (!await fileExists(sddPath)) {
7428
- logger_exports.warn(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
7429
- return;
7430
- }
7819
+ const result = await getChangeList(process.cwd(), options);
7431
7820
  console.log("");
7432
7821
  if (!options.archived) {
7433
- const pendingResult = await listPendingChanges(sddPath);
7434
- if (pendingResult.success && pendingResult.data.length > 0) {
7822
+ if (result.pending.length > 0) {
7435
7823
  console.log("\u{1F4DD} \uB300\uAE30 \uC911\uC778 \uBCC0\uACBD");
7436
7824
  console.log("\u2500".repeat(30));
7437
- for (const change of pendingResult.data) {
7825
+ for (const change of result.pending) {
7438
7826
  console.log(` - ${change}`);
7439
7827
  }
7440
7828
  console.log("");
@@ -7443,11 +7831,10 @@ async function listChanges(options) {
7443
7831
  }
7444
7832
  }
7445
7833
  if (!options.pending) {
7446
- const archiveResult = await listArchives(sddPath);
7447
- if (archiveResult.success && archiveResult.data.length > 0) {
7834
+ if (result.archived.length > 0) {
7448
7835
  console.log("\u{1F4E6} \uC544\uCE74\uC774\uBE0C\uB41C \uBCC0\uACBD");
7449
7836
  console.log("\u2500".repeat(30));
7450
- for (const archive of archiveResult.data) {
7837
+ for (const archive of result.archived) {
7451
7838
  console.log(` - ${archive}`);
7452
7839
  }
7453
7840
  console.log("");
@@ -7457,80 +7844,56 @@ async function listChanges(options) {
7457
7844
  }
7458
7845
  }
7459
7846
  async function listSpecs() {
7460
- const cwd = process.cwd();
7461
- const specsPath = path14.join(cwd, ".sdd", "specs");
7462
- if (!await fileExists(specsPath)) {
7847
+ const specsPath = path14.join(process.cwd(), ".sdd", "specs");
7848
+ const tree = await getSpecFileTree(specsPath);
7849
+ if (tree.length === 0) {
7463
7850
  logger_exports.warn("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
7464
7851
  return;
7465
7852
  }
7466
7853
  console.log("");
7467
7854
  console.log("\u{1F4C4} \uC2A4\uD399 \uD30C\uC77C \uBAA9\uB85D");
7468
7855
  console.log("\u2500".repeat(50));
7469
- await walkSpecs(specsPath, "");
7856
+ printSpecTree(tree, "");
7470
7857
  console.log("");
7471
7858
  }
7472
- async function walkSpecs(basePath, prefix) {
7473
- const result = await readDir(basePath);
7474
- if (!result.success) return;
7475
- for (const entry of result.data) {
7476
- const fullPath = path14.join(basePath, entry);
7477
- const stat = await fs9.stat(fullPath);
7478
- if (stat.isDirectory()) {
7479
- console.log(`${prefix}\u{1F4C1} ${entry}/`);
7480
- await walkSpecs(fullPath, prefix + " ");
7481
- } else if (entry.endsWith(".md")) {
7482
- console.log(`${prefix}\u{1F4C4} ${entry}`);
7859
+ function printSpecTree(items, prefix) {
7860
+ for (const item of items) {
7861
+ if (item.isDirectory) {
7862
+ console.log(`${prefix}\u{1F4C1} ${item.name}/`);
7863
+ if (item.children) {
7864
+ printSpecTree(item.children, prefix + " ");
7865
+ }
7866
+ } else {
7867
+ console.log(`${prefix}\u{1F4C4} ${item.name}`);
7483
7868
  }
7484
7869
  }
7485
7870
  }
7486
7871
  async function listTemplates() {
7487
- const cwd = process.cwd();
7488
- const templatesPath = path14.join(cwd, ".sdd", "templates");
7489
- if (!await fileExists(templatesPath)) {
7872
+ const templates = await getTemplateList(process.cwd());
7873
+ if (templates.length === 0) {
7490
7874
  logger_exports.warn("\uD15C\uD50C\uB9BF \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
7491
7875
  return;
7492
7876
  }
7493
- const result = await readDir(templatesPath);
7494
- if (!result.success) {
7495
- logger_exports.error("\uD15C\uD50C\uB9BF \uB514\uB809\uD1A0\uB9AC\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
7496
- return;
7497
- }
7498
7877
  console.log("");
7499
7878
  console.log("\u{1F4D1} \uD15C\uD50C\uB9BF \uBAA9\uB85D");
7500
7879
  console.log("\u2500".repeat(30));
7501
- for (const template of result.data.filter((f) => f.endsWith(".md"))) {
7880
+ for (const template of templates) {
7502
7881
  console.log(` - ${template}`);
7503
7882
  }
7504
7883
  console.log("");
7505
7884
  }
7506
7885
  async function listSummary() {
7507
- const cwd = process.cwd();
7508
- const sddPath = path14.join(cwd, ".sdd");
7509
- if (!await fileExists(sddPath)) {
7886
+ const summary = await getProjectSummary(process.cwd());
7887
+ if (!summary) {
7510
7888
  logger_exports.warn(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
7511
7889
  return;
7512
7890
  }
7513
7891
  console.log("");
7514
7892
  console.log("\u{1F4CA} SDD \uD504\uB85C\uC81D\uD2B8 \uC694\uC57D");
7515
7893
  console.log("\u2550".repeat(40));
7516
- const specsPath = path14.join(sddPath, "specs");
7517
- let featureCount = 0;
7518
- if (await fileExists(specsPath)) {
7519
- const result = await readDir(specsPath);
7520
- if (result.success) {
7521
- for (const entry of result.data) {
7522
- const stat = await fs9.stat(path14.join(specsPath, entry));
7523
- if (stat.isDirectory()) featureCount++;
7524
- }
7525
- }
7526
- }
7527
- console.log(`\u{1F4CB} \uAE30\uB2A5: ${featureCount}\uAC1C`);
7528
- const pendingResult = await listPendingChanges(sddPath);
7529
- const pendingCount = pendingResult.success ? pendingResult.data.length : 0;
7530
- console.log(`\u{1F4DD} \uB300\uAE30 \uC911\uC778 \uBCC0\uACBD: ${pendingCount}\uAC1C`);
7531
- const archiveResult = await listArchives(sddPath);
7532
- const archiveCount = archiveResult.success ? archiveResult.data.length : 0;
7533
- console.log(`\u{1F4E6} \uC544\uCE74\uC774\uBE0C\uB41C \uBCC0\uACBD: ${archiveCount}\uAC1C`);
7894
+ console.log(`\u{1F4CB} \uAE30\uB2A5: ${summary.featureCount}\uAC1C`);
7895
+ console.log(`\u{1F4DD} \uB300\uAE30 \uC911\uC778 \uBCC0\uACBD: ${summary.pendingChangeCount}\uAC1C`);
7896
+ console.log(`\u{1F4E6} \uC544\uCE74\uC774\uBE0C\uB41C \uBCC0\uACBD: ${summary.archivedChangeCount}\uAC1C`);
7534
7897
  console.log("");
7535
7898
  console.log("\uC0C1\uC138 \uC815\uBCF4:");
7536
7899
  console.log(" sdd list features - \uAE30\uB2A5 \uBAA9\uB85D");
@@ -7539,29 +7902,134 @@ async function listSummary() {
7539
7902
  console.log(" sdd status - \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC");
7540
7903
  console.log("");
7541
7904
  }
7542
- function getStatusIcon2(status) {
7543
- switch (status) {
7544
- case "draft":
7545
- return "\u{1F4DD}";
7546
- case "specified":
7547
- return "\u{1F4C4}";
7548
- case "planned":
7549
- return "\u{1F4CB}";
7550
- case "tasked":
7551
- return "\u270F\uFE0F";
7552
- case "implementing":
7553
- return "\u{1F528}";
7554
- case "completed":
7555
- return "\u2705";
7556
- default:
7557
- return "\u2753";
7558
- }
7559
- }
7560
7905
 
7561
7906
  // src/cli/commands/constitution.ts
7562
7907
  init_fs();
7563
7908
  init_errors();
7564
7909
  import path15 from "path";
7910
+ init_types();
7911
+ function determineBumpType(options) {
7912
+ if (options.major) return "major";
7913
+ if (options.minor) return "minor";
7914
+ if (options.patch) return "patch";
7915
+ return null;
7916
+ }
7917
+ async function readConstitution(projectPath) {
7918
+ const constitutionPath = path15.join(projectPath, ".sdd", "constitution.md");
7919
+ if (!await fileExists(constitutionPath)) {
7920
+ return failure(new Error("Constitution\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC73C\uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654\uD558\uC138\uC694."));
7921
+ }
7922
+ const contentResult = await readFile(constitutionPath);
7923
+ if (!contentResult.success) {
7924
+ return failure(new Error("Constitution \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
7925
+ }
7926
+ const parseResult = parseConstitution(contentResult.data);
7927
+ if (!parseResult.success) {
7928
+ return failure(new Error(`Constitution \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`));
7929
+ }
7930
+ return success({
7931
+ content: contentResult.data,
7932
+ parsed: parseResult.data
7933
+ });
7934
+ }
7935
+ function constitutionToJson(constitution) {
7936
+ return {
7937
+ projectName: constitution.projectName,
7938
+ version: constitution.metadata.version,
7939
+ created: constitution.metadata.created,
7940
+ updated: constitution.metadata.updated,
7941
+ principles: constitution.principles,
7942
+ forbidden: constitution.forbidden,
7943
+ techStack: constitution.techStack,
7944
+ qualityStandards: constitution.qualityStandards
7945
+ };
7946
+ }
7947
+ async function executeBump(projectPath, bumpType, message) {
7948
+ const constitutionPath = path15.join(projectPath, ".sdd", "constitution.md");
7949
+ const changelogPath = path15.join(projectPath, ".sdd", "CHANGELOG.md");
7950
+ const readResult = await readConstitution(projectPath);
7951
+ if (!readResult.success) {
7952
+ return failure(readResult.error);
7953
+ }
7954
+ const { content, parsed } = readResult.data;
7955
+ const currentVersion = parsed.metadata.version;
7956
+ const newVersion = bumpVersion(currentVersion, bumpType);
7957
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
7958
+ let updatedContent = content;
7959
+ updatedContent = updatedContent.replace(
7960
+ /^version:\s*.+$/m,
7961
+ `version: ${newVersion}`
7962
+ );
7963
+ if (/^updated:/m.test(updatedContent)) {
7964
+ updatedContent = updatedContent.replace(
7965
+ /^updated:\s*.+$/m,
7966
+ `updated: ${today}`
7967
+ );
7968
+ } else {
7969
+ updatedContent = updatedContent.replace(
7970
+ /^(version:\s*.+)$/m,
7971
+ `$1
7972
+ updated: ${today}`
7973
+ );
7974
+ }
7975
+ await writeFile(constitutionPath, updatedContent);
7976
+ const changeType = bumpType === "major" ? "changed" : bumpType === "minor" ? "added" : "fixed";
7977
+ const changeDescription = message || `Constitution ${bumpType} \uC5C5\uB370\uC774\uD2B8`;
7978
+ const newEntry = createChangelogEntry(
7979
+ currentVersion,
7980
+ bumpType,
7981
+ [{ type: changeType, description: changeDescription }],
7982
+ message
7983
+ );
7984
+ let existingEntries = [];
7985
+ if (await fileExists(changelogPath)) {
7986
+ const changelogContent = await readFile(changelogPath);
7987
+ if (changelogContent.success) {
7988
+ const parsed2 = parseChangelog(changelogContent.data);
7989
+ if (parsed2.success) {
7990
+ existingEntries = parsed2.data;
7991
+ }
7992
+ }
7993
+ }
7994
+ const allEntries = [newEntry, ...existingEntries];
7995
+ const newChangelog = generateChangelog(allEntries);
7996
+ await writeFile(changelogPath, newChangelog);
7997
+ return success({
7998
+ previousVersion: currentVersion,
7999
+ newVersion,
8000
+ changelogPath
8001
+ });
8002
+ }
8003
+ async function executeValidateConstitution(projectPath) {
8004
+ const readResult = await readConstitution(projectPath);
8005
+ if (!readResult.success) {
8006
+ return failure(readResult.error);
8007
+ }
8008
+ const validationResult = validateConstitution(readResult.data.parsed);
8009
+ if (!validationResult.success) {
8010
+ return failure(new Error(`Constitution \uAC80\uC99D \uC2E4\uD328: ${validationResult.error.message}`));
8011
+ }
8012
+ return success(readResult.data.parsed);
8013
+ }
8014
+ async function getHistory(projectPath, count = 10) {
8015
+ const changelogPath = path15.join(projectPath, ".sdd", "CHANGELOG.md");
8016
+ if (!await fileExists(changelogPath)) {
8017
+ return success({ entries: [], totalCount: 0 });
8018
+ }
8019
+ const contentResult = await readFile(changelogPath);
8020
+ if (!contentResult.success) {
8021
+ return failure(new Error("CHANGELOG \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
8022
+ }
8023
+ const parseResult = parseChangelog(contentResult.data);
8024
+ if (!parseResult.success) {
8025
+ return failure(new Error(`CHANGELOG \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`));
8026
+ }
8027
+ const entries = parseResult.data.slice(0, count);
8028
+ return success({
8029
+ entries,
8030
+ totalCount: parseResult.data.length
8031
+ });
8032
+ }
7565
8033
  function registerConstitutionCommand(program2) {
7566
8034
  const constitution = program2.command("constitution").description("Constitution(\uD504\uB85C\uC81D\uD2B8 \uC6D0\uCE59) \uAD00\uB9AC");
7567
8035
  constitution.command("show").description("\uD604\uC7AC Constitution \uB0B4\uC6A9 \uD45C\uC2DC").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").action(async (options) => {
@@ -7610,33 +8078,14 @@ function registerConstitutionCommand(program2) {
7610
8078
  }
7611
8079
  async function runShow(options) {
7612
8080
  const cwd = process.cwd();
7613
- const constitutionPath = path15.join(cwd, ".sdd", "constitution.md");
7614
- if (!await fileExists(constitutionPath)) {
7615
- error("Constitution\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC73C\uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654\uD558\uC138\uC694.");
7616
- process.exit(ExitCode.FILE_NOT_FOUND);
7617
- }
7618
- const contentResult = await readFile(constitutionPath);
7619
- if (!contentResult.success) {
7620
- error("Constitution \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
8081
+ const result = await readConstitution(cwd);
8082
+ if (!result.success) {
8083
+ error(result.error.message);
7621
8084
  process.exit(ExitCode.FILE_SYSTEM_ERROR);
7622
8085
  }
7623
- const parseResult = parseConstitution(contentResult.data);
7624
- if (!parseResult.success) {
7625
- error(`Constitution \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`);
7626
- process.exit(ExitCode.VALIDATION_ERROR);
7627
- }
7628
- const constitution = parseResult.data;
8086
+ const constitution = result.data.parsed;
7629
8087
  if (options.json) {
7630
- console.log(JSON.stringify({
7631
- projectName: constitution.projectName,
7632
- version: constitution.metadata.version,
7633
- created: constitution.metadata.created,
7634
- updated: constitution.metadata.updated,
7635
- principles: constitution.principles,
7636
- forbidden: constitution.forbidden,
7637
- techStack: constitution.techStack,
7638
- qualityStandards: constitution.qualityStandards
7639
- }, null, 2));
8088
+ console.log(JSON.stringify(constitutionToJson(constitution), null, 2));
7640
8089
  return;
7641
8090
  }
7642
8091
  info(`Constitution: ${constitution.projectName}`);
@@ -7678,116 +8127,37 @@ async function runShow(options) {
7678
8127
  }
7679
8128
  async function runVersion() {
7680
8129
  const cwd = process.cwd();
7681
- const constitutionPath = path15.join(cwd, ".sdd", "constitution.md");
7682
- if (!await fileExists(constitutionPath)) {
7683
- error("Constitution\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
7684
- process.exit(ExitCode.FILE_NOT_FOUND);
7685
- }
7686
- const contentResult = await readFile(constitutionPath);
7687
- if (!contentResult.success) {
7688
- error("Constitution \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
8130
+ const result = await readConstitution(cwd);
8131
+ if (!result.success) {
8132
+ error(result.error.message);
7689
8133
  process.exit(ExitCode.FILE_SYSTEM_ERROR);
7690
8134
  }
7691
- const parseResult = parseConstitution(contentResult.data);
7692
- if (!parseResult.success) {
7693
- error(`Constitution \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`);
7694
- process.exit(ExitCode.VALIDATION_ERROR);
7695
- }
7696
- console.log(parseResult.data.metadata.version);
8135
+ console.log(result.data.parsed.metadata.version);
7697
8136
  }
7698
8137
  async function runBump(options) {
7699
8138
  const cwd = process.cwd();
7700
- const constitutionPath = path15.join(cwd, ".sdd", "constitution.md");
7701
- const changelogPath = path15.join(cwd, ".sdd", "CHANGELOG.md");
7702
- if (!await fileExists(constitutionPath)) {
7703
- error("Constitution\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
7704
- process.exit(ExitCode.FILE_NOT_FOUND);
7705
- }
7706
- let bumpType;
7707
- if (options.major) {
7708
- bumpType = "major";
7709
- } else if (options.minor) {
7710
- bumpType = "minor";
7711
- } else if (options.patch) {
7712
- bumpType = "patch";
7713
- } else {
8139
+ const bumpType = determineBumpType(options);
8140
+ if (!bumpType) {
7714
8141
  error("\uBC84\uC804 \uC720\uD615\uC744 \uC9C0\uC815\uD558\uC138\uC694: --major, --minor, \uB610\uB294 --patch");
7715
8142
  process.exit(ExitCode.GENERAL_ERROR);
7716
8143
  }
7717
- const contentResult = await readFile(constitutionPath);
7718
- if (!contentResult.success) {
7719
- error("Constitution \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
8144
+ const result = await executeBump(cwd, bumpType, options.message);
8145
+ if (!result.success) {
8146
+ error(result.error.message);
7720
8147
  process.exit(ExitCode.FILE_SYSTEM_ERROR);
7721
8148
  }
7722
- const parseResult = parseConstitution(contentResult.data);
7723
- if (!parseResult.success) {
7724
- error(`Constitution \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`);
7725
- process.exit(ExitCode.VALIDATION_ERROR);
7726
- }
7727
- const currentVersion = parseResult.data.metadata.version;
7728
- const newVersion = bumpVersion(currentVersion, bumpType);
7729
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
7730
- let updatedContent = contentResult.data;
7731
- updatedContent = updatedContent.replace(
7732
- /^version:\s*.+$/m,
7733
- `version: ${newVersion}`
7734
- );
7735
- if (/^updated:/m.test(updatedContent)) {
7736
- updatedContent = updatedContent.replace(
7737
- /^updated:\s*.+$/m,
7738
- `updated: ${today}`
7739
- );
7740
- } else {
7741
- updatedContent = updatedContent.replace(
7742
- /^(version:\s*.+)$/m,
7743
- `$1
7744
- updated: ${today}`
7745
- );
7746
- }
7747
- await writeFile(constitutionPath, updatedContent);
7748
- const changeType = bumpType === "major" ? "changed" : bumpType === "minor" ? "added" : "fixed";
7749
- const changeDescription = options.message || `Constitution ${bumpType} \uC5C5\uB370\uC774\uD2B8`;
7750
- const newEntry = createChangelogEntry(
7751
- currentVersion,
7752
- bumpType,
7753
- [{ type: changeType, description: changeDescription }],
7754
- options.message
7755
- );
7756
- let existingEntries = [];
7757
- if (await fileExists(changelogPath)) {
7758
- const changelogContent = await readFile(changelogPath);
7759
- if (changelogContent.success) {
7760
- const parsed = parseChangelog(changelogContent.data);
7761
- if (parsed.success) {
7762
- existingEntries = parsed.data;
7763
- }
7764
- }
7765
- }
7766
- const allEntries = [newEntry, ...existingEntries];
7767
- const newChangelog = generateChangelog(allEntries);
7768
- await writeFile(changelogPath, newChangelog);
7769
- success2(`Constitution \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8: ${currentVersion} \u2192 ${newVersion}`);
7770
- info(`CHANGELOG \uC5C5\uB370\uC774\uD2B8: ${changelogPath}`);
8149
+ success2(`Constitution \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8: ${result.data.previousVersion} \u2192 ${result.data.newVersion}`);
8150
+ info(`CHANGELOG \uC5C5\uB370\uC774\uD2B8: ${result.data.changelogPath}`);
7771
8151
  }
7772
8152
  async function runHistory(options) {
7773
8153
  const cwd = process.cwd();
7774
- const changelogPath = path15.join(cwd, ".sdd", "CHANGELOG.md");
7775
- if (!await fileExists(changelogPath)) {
7776
- warn("CHANGELOG\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd constitution bump`\uB85C \uBC84\uC804\uC744 \uC5C5\uB370\uC774\uD2B8\uD558\uBA74 \uC0DD\uC131\uB429\uB2C8\uB2E4.");
7777
- return;
7778
- }
7779
- const contentResult = await readFile(changelogPath);
7780
- if (!contentResult.success) {
7781
- error("CHANGELOG \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
8154
+ const count = parseInt(options.count, 10) || 10;
8155
+ const result = await getHistory(cwd, count);
8156
+ if (!result.success) {
8157
+ error(result.error.message);
7782
8158
  process.exit(ExitCode.FILE_SYSTEM_ERROR);
7783
8159
  }
7784
- const parseResult = parseChangelog(contentResult.data);
7785
- if (!parseResult.success) {
7786
- error(`CHANGELOG \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`);
7787
- process.exit(ExitCode.VALIDATION_ERROR);
7788
- }
7789
- const count = parseInt(options.count, 10) || 10;
7790
- const entries = parseResult.data.slice(0, count);
8160
+ const { entries } = result.data;
7791
8161
  if (entries.length === 0) {
7792
8162
  info("\uBCC0\uACBD \uC774\uB825\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
7793
8163
  return;
@@ -7807,36 +8177,22 @@ async function runHistory(options) {
7807
8177
  }
7808
8178
  async function runValidate2() {
7809
8179
  const cwd = process.cwd();
7810
- const constitutionPath = path15.join(cwd, ".sdd", "constitution.md");
7811
- if (!await fileExists(constitutionPath)) {
7812
- error("Constitution\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
7813
- process.exit(ExitCode.FILE_NOT_FOUND);
7814
- }
7815
- const contentResult = await readFile(constitutionPath);
7816
- if (!contentResult.success) {
7817
- error("Constitution \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
7818
- process.exit(ExitCode.FILE_SYSTEM_ERROR);
7819
- }
7820
- const parseResult = parseConstitution(contentResult.data);
7821
- if (!parseResult.success) {
7822
- error(`Constitution \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`);
7823
- process.exit(ExitCode.VALIDATION_ERROR);
7824
- }
7825
- const validationResult = validateConstitution(parseResult.data);
7826
- if (!validationResult.success) {
7827
- error(`Constitution \uAC80\uC99D \uC2E4\uD328: ${validationResult.error.message}`);
7828
- process.exit(ExitCode.VALIDATION_ERROR);
8180
+ const result = await executeValidateConstitution(cwd);
8181
+ if (!result.success) {
8182
+ error(result.error.message);
8183
+ process.exit(ExitCode.VALIDATION_FAILED);
7829
8184
  }
8185
+ const constitution = result.data;
7830
8186
  success2("Constitution \uAC80\uC99D \uD1B5\uACFC");
7831
- info(`\uD504\uB85C\uC81D\uD2B8: ${parseResult.data.projectName}`);
7832
- info(`\uBC84\uC804: ${parseResult.data.metadata.version}`);
7833
- info(`\uC6D0\uCE59 \uC218: ${parseResult.data.principles.length}`);
7834
- info(`\uAE08\uC9C0 \uC0AC\uD56D \uC218: ${parseResult.data.forbidden.length}`);
8187
+ info(`\uD504\uB85C\uC81D\uD2B8: ${constitution.projectName}`);
8188
+ info(`\uBC84\uC804: ${constitution.metadata.version}`);
8189
+ info(`\uC6D0\uCE59 \uC218: ${constitution.principles.length}`);
8190
+ info(`\uAE08\uC9C0 \uC0AC\uD56D \uC218: ${constitution.forbidden.length}`);
7835
8191
  }
7836
8192
 
7837
8193
  // src/cli/commands/start.ts
7838
8194
  import path16 from "path";
7839
- import { promises as fs10 } from "fs";
8195
+ import { promises as fs9 } from "fs";
7840
8196
  init_errors();
7841
8197
  init_fs();
7842
8198
  function registerStartCommand(program2) {
@@ -7850,7 +8206,7 @@ function registerStartCommand(program2) {
7850
8206
  });
7851
8207
  }
7852
8208
  async function runStart(options) {
7853
- const projectStatus = await getProjectStatus();
8209
+ const projectStatus = await getProjectStatus2();
7854
8210
  if (!projectStatus.initialized) {
7855
8211
  info("SDD \uD504\uB85C\uC81D\uD2B8\uAC00 \uCD08\uAE30\uD654\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
7856
8212
  newline();
@@ -7874,7 +8230,7 @@ async function runStart(options) {
7874
8230
  newline();
7875
8231
  displayWorkflowMenu(projectStatus);
7876
8232
  }
7877
- async function getProjectStatus() {
8233
+ async function getProjectStatus2() {
7878
8234
  const projectRoot = await findSddRoot();
7879
8235
  if (!projectRoot) {
7880
8236
  return {
@@ -7903,7 +8259,7 @@ async function getProjectStatus() {
7903
8259
  const specs = [];
7904
8260
  if (await directoryExists(specsPath)) {
7905
8261
  try {
7906
- const dirs = await fs10.readdir(specsPath);
8262
+ const dirs = await fs9.readdir(specsPath);
7907
8263
  for (const dir of dirs) {
7908
8264
  const specPath = path16.join(specsPath, dir, "spec.md");
7909
8265
  if (await fileExists(specPath)) {
@@ -8139,12 +8495,354 @@ function displayConstitutionGuide() {
8139
8495
  }
8140
8496
 
8141
8497
  // src/cli/commands/migrate.ts
8142
- import path17 from "path";
8498
+ import path18 from "path";
8143
8499
  import { promises as fs11 } from "fs";
8144
8500
  init_errors();
8145
8501
  init_fs();
8146
8502
  init_new();
8147
8503
  init_schemas();
8504
+
8505
+ // src/core/migrate/detector.ts
8506
+ init_types();
8507
+ init_errors();
8508
+ init_fs();
8509
+ import path17 from "path";
8510
+ import fs10 from "fs/promises";
8511
+ async function detectExternalTools(projectRoot) {
8512
+ try {
8513
+ const results = [];
8514
+ const openspecResult = await detectOpenSpec(projectRoot);
8515
+ if (openspecResult) {
8516
+ results.push(openspecResult);
8517
+ }
8518
+ const speckitResult = await detectSpecKit(projectRoot);
8519
+ if (speckitResult) {
8520
+ results.push(speckitResult);
8521
+ }
8522
+ const sddResult = await detectSdd(projectRoot);
8523
+ if (sddResult) {
8524
+ results.push(sddResult);
8525
+ }
8526
+ return success(results);
8527
+ } catch (error2) {
8528
+ return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
8529
+ }
8530
+ }
8531
+ async function detectOpenSpec(projectRoot) {
8532
+ const openspecPath = path17.join(projectRoot, "openspec");
8533
+ if (!await directoryExists(openspecPath)) {
8534
+ return null;
8535
+ }
8536
+ const specsPath = path17.join(openspecPath, "specs");
8537
+ const changesPath = path17.join(openspecPath, "changes");
8538
+ const agentsPath = path17.join(openspecPath, "AGENTS.md");
8539
+ const hasAgents = await fileExists(agentsPath);
8540
+ const hasSpecs = await directoryExists(specsPath);
8541
+ const hasChanges = await directoryExists(changesPath);
8542
+ if (!hasSpecs && !hasChanges && !hasAgents) {
8543
+ return null;
8544
+ }
8545
+ const specs = [];
8546
+ if (hasSpecs) {
8547
+ const specDirs = await fs10.readdir(specsPath, { withFileTypes: true });
8548
+ for (const entry of specDirs) {
8549
+ if (entry.isDirectory()) {
8550
+ const specPath = path17.join(specsPath, entry.name);
8551
+ const specFile = path17.join(specPath, "spec.md");
8552
+ if (await fileExists(specFile)) {
8553
+ const content = await fs10.readFile(specFile, "utf-8");
8554
+ const title2 = extractTitle2(content);
8555
+ const status = extractFrontmatterField(content, "status");
8556
+ specs.push({
8557
+ id: entry.name,
8558
+ title: title2,
8559
+ path: specPath,
8560
+ status
8561
+ });
8562
+ }
8563
+ }
8564
+ }
8565
+ }
8566
+ return {
8567
+ tool: "openspec",
8568
+ path: openspecPath,
8569
+ specCount: specs.length,
8570
+ specs,
8571
+ confidence: hasAgents ? "high" : hasSpecs && hasChanges ? "medium" : "low"
8572
+ };
8573
+ }
8574
+ async function detectSpecKit(projectRoot) {
8575
+ const specifyPath = path17.join(projectRoot, ".specify");
8576
+ if (!await directoryExists(specifyPath)) {
8577
+ return null;
8578
+ }
8579
+ const specsPath = path17.join(specifyPath, "specs");
8580
+ const memoryPath = path17.join(projectRoot, "memory");
8581
+ const constitutionPath = path17.join(memoryPath, "constitution.md");
8582
+ const hasSpecs = await directoryExists(specsPath);
8583
+ const hasConstitution = await fileExists(constitutionPath);
8584
+ if (!hasSpecs) {
8585
+ return null;
8586
+ }
8587
+ const specs = [];
8588
+ const specDirs = await fs10.readdir(specsPath, { withFileTypes: true });
8589
+ for (const entry of specDirs) {
8590
+ if (entry.isDirectory()) {
8591
+ const specPath = path17.join(specsPath, entry.name);
8592
+ const specFile = path17.join(specPath, "spec.md");
8593
+ const planFile = path17.join(specPath, "plan.md");
8594
+ const tasksFile = path17.join(specPath, "tasks.md");
8595
+ const hasSpec = await fileExists(specFile);
8596
+ const hasPlan = await fileExists(planFile);
8597
+ const hasTasks = await fileExists(tasksFile);
8598
+ if (hasSpec || hasPlan) {
8599
+ let title2;
8600
+ let status;
8601
+ if (hasSpec) {
8602
+ const content = await fs10.readFile(specFile, "utf-8");
8603
+ title2 = extractTitle2(content);
8604
+ status = extractFrontmatterField(content, "status");
8605
+ }
8606
+ specs.push({
8607
+ id: entry.name,
8608
+ title: title2,
8609
+ path: specPath,
8610
+ status: hasTasks ? "in-progress" : status
8611
+ });
8612
+ }
8613
+ }
8614
+ }
8615
+ return {
8616
+ tool: "speckit",
8617
+ path: specifyPath,
8618
+ specCount: specs.length,
8619
+ specs,
8620
+ confidence: hasConstitution ? "high" : "medium"
8621
+ };
8622
+ }
8623
+ async function detectSdd(projectRoot) {
8624
+ const sddPath = path17.join(projectRoot, ".sdd");
8625
+ if (!await directoryExists(sddPath)) {
8626
+ return null;
8627
+ }
8628
+ const specsPath = path17.join(sddPath, "specs");
8629
+ const configPath = path17.join(sddPath, "config.yaml");
8630
+ if (!await directoryExists(specsPath)) {
8631
+ return null;
8632
+ }
8633
+ const specs = [];
8634
+ const specDirs = await fs10.readdir(specsPath, { withFileTypes: true });
8635
+ for (const entry of specDirs) {
8636
+ if (entry.isDirectory()) {
8637
+ const specPath = path17.join(specsPath, entry.name);
8638
+ const specFile = path17.join(specPath, "spec.md");
8639
+ if (await fileExists(specFile)) {
8640
+ const content = await fs10.readFile(specFile, "utf-8");
8641
+ const title2 = extractTitle2(content);
8642
+ const status = extractFrontmatterField(content, "status");
8643
+ specs.push({
8644
+ id: entry.name,
8645
+ title: title2,
8646
+ path: specPath,
8647
+ status
8648
+ });
8649
+ }
8650
+ }
8651
+ }
8652
+ return {
8653
+ tool: "sdd",
8654
+ path: sddPath,
8655
+ specCount: specs.length,
8656
+ specs,
8657
+ confidence: await fileExists(configPath) ? "high" : "medium"
8658
+ };
8659
+ }
8660
+ async function migrateFromOpenSpec(sourcePath, targetPath, options = {}) {
8661
+ try {
8662
+ const specsPath = path17.join(sourcePath, "specs");
8663
+ const targetSpecsPath = path17.join(targetPath, "specs");
8664
+ let specsCreated = 0;
8665
+ let specsSkipped = 0;
8666
+ const errors = [];
8667
+ if (!await directoryExists(specsPath)) {
8668
+ return failure(new ChangeError("OpenSpec specs \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
8669
+ }
8670
+ const specDirs = await fs10.readdir(specsPath, { withFileTypes: true });
8671
+ for (const entry of specDirs) {
8672
+ if (!entry.isDirectory()) continue;
8673
+ const sourceSpecPath = path17.join(specsPath, entry.name);
8674
+ const targetSpecPath = path17.join(targetSpecsPath, entry.name);
8675
+ if (await directoryExists(targetSpecPath)) {
8676
+ if (!options.overwrite) {
8677
+ specsSkipped++;
8678
+ continue;
8679
+ }
8680
+ }
8681
+ try {
8682
+ if (!options.dryRun) {
8683
+ await fs10.mkdir(targetSpecPath, { recursive: true });
8684
+ const files = await fs10.readdir(sourceSpecPath);
8685
+ for (const file of files) {
8686
+ const sourceFile = path17.join(sourceSpecPath, file);
8687
+ const targetFile = path17.join(targetSpecPath, file);
8688
+ const stat = await fs10.stat(sourceFile);
8689
+ if (stat.isFile()) {
8690
+ let content = await fs10.readFile(sourceFile, "utf-8");
8691
+ if (file === "spec.md") {
8692
+ content = convertOpenSpecToSdd(content, entry.name);
8693
+ }
8694
+ await fs10.writeFile(targetFile, content);
8695
+ }
8696
+ }
8697
+ }
8698
+ specsCreated++;
8699
+ } catch (error2) {
8700
+ errors.push(`${entry.name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
8701
+ }
8702
+ }
8703
+ return success({
8704
+ source: "openspec",
8705
+ targetPath,
8706
+ specsCreated,
8707
+ specsSkipped,
8708
+ errors
8709
+ });
8710
+ } catch (error2) {
8711
+ return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
8712
+ }
8713
+ }
8714
+ async function migrateFromSpecKit(sourcePath, targetPath, options = {}) {
8715
+ try {
8716
+ const specsPath = path17.join(sourcePath, "specs");
8717
+ const targetSpecsPath = path17.join(targetPath, "specs");
8718
+ let specsCreated = 0;
8719
+ let specsSkipped = 0;
8720
+ const errors = [];
8721
+ if (!await directoryExists(specsPath)) {
8722
+ return failure(new ChangeError("Spec Kit specs \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
8723
+ }
8724
+ const specDirs = await fs10.readdir(specsPath, { withFileTypes: true });
8725
+ for (const entry of specDirs) {
8726
+ if (!entry.isDirectory()) continue;
8727
+ const sourceSpecPath = path17.join(specsPath, entry.name);
8728
+ const targetSpecPath = path17.join(targetSpecsPath, entry.name);
8729
+ if (await directoryExists(targetSpecPath)) {
8730
+ if (!options.overwrite) {
8731
+ specsSkipped++;
8732
+ continue;
8733
+ }
8734
+ }
8735
+ try {
8736
+ if (!options.dryRun) {
8737
+ await fs10.mkdir(targetSpecPath, { recursive: true });
8738
+ const files = await fs10.readdir(sourceSpecPath);
8739
+ for (const file of files) {
8740
+ const sourceFile = path17.join(sourceSpecPath, file);
8741
+ const targetFile = path17.join(targetSpecPath, file);
8742
+ const stat = await fs10.stat(sourceFile);
8743
+ if (stat.isFile()) {
8744
+ let content = await fs10.readFile(sourceFile, "utf-8");
8745
+ if (file === "spec.md") {
8746
+ content = convertSpecKitToSdd(content, entry.name);
8747
+ }
8748
+ await fs10.writeFile(targetFile, content);
8749
+ }
8750
+ }
8751
+ }
8752
+ specsCreated++;
8753
+ } catch (error2) {
8754
+ errors.push(`${entry.name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
8755
+ }
8756
+ }
8757
+ return success({
8758
+ source: "speckit",
8759
+ targetPath,
8760
+ specsCreated,
8761
+ specsSkipped,
8762
+ errors
8763
+ });
8764
+ } catch (error2) {
8765
+ return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
8766
+ }
8767
+ }
8768
+ function convertOpenSpecToSdd(content, specId) {
8769
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
8770
+ if (!frontmatterMatch) {
8771
+ const title2 = extractTitle2(content) || specId;
8772
+ return `---
8773
+ id: ${specId}
8774
+ title: ${title2}
8775
+ phase: migrated
8776
+ status: draft
8777
+ source: openspec
8778
+ migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}
8779
+ ---
8780
+
8781
+ ${content}`;
8782
+ }
8783
+ let newFrontmatter = frontmatterMatch[1];
8784
+ if (!newFrontmatter.includes("phase:")) {
8785
+ newFrontmatter += "\nphase: migrated";
8786
+ }
8787
+ if (!newFrontmatter.includes("source:")) {
8788
+ newFrontmatter += "\nsource: openspec";
8789
+ }
8790
+ if (!newFrontmatter.includes("migrated_at:")) {
8791
+ newFrontmatter += `
8792
+ migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}`;
8793
+ }
8794
+ return content.replace(/^---\n[\s\S]*?\n---/, `---
8795
+ ${newFrontmatter}
8796
+ ---`);
8797
+ }
8798
+ function convertSpecKitToSdd(content, specId) {
8799
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
8800
+ if (!frontmatterMatch) {
8801
+ const title2 = extractTitle2(content) || specId;
8802
+ return `---
8803
+ id: ${specId}
8804
+ title: ${title2}
8805
+ phase: migrated
8806
+ status: draft
8807
+ source: speckit
8808
+ migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}
8809
+ ---
8810
+
8811
+ ${content}`;
8812
+ }
8813
+ let newFrontmatter = frontmatterMatch[1];
8814
+ if (!newFrontmatter.includes("phase:")) {
8815
+ newFrontmatter += "\nphase: migrated";
8816
+ }
8817
+ if (!newFrontmatter.includes("source:")) {
8818
+ newFrontmatter += "\nsource: speckit";
8819
+ }
8820
+ if (!newFrontmatter.includes("migrated_at:")) {
8821
+ newFrontmatter += `
8822
+ migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}`;
8823
+ }
8824
+ return content.replace(/^---\n[\s\S]*?\n---/, `---
8825
+ ${newFrontmatter}
8826
+ ---`);
8827
+ }
8828
+ function extractTitle2(content) {
8829
+ const fmMatch = content.match(/^---\n[\s\S]*?title:\s*['"]?([^'"\n]+)['"]?\n[\s\S]*?\n---/);
8830
+ if (fmMatch) {
8831
+ return fmMatch[1].trim();
8832
+ }
8833
+ const h1Match = content.match(/^#\s+(.+)$/m);
8834
+ if (h1Match) {
8835
+ return h1Match[1].trim();
8836
+ }
8837
+ return void 0;
8838
+ }
8839
+ function extractFrontmatterField(content, field) {
8840
+ const regex = new RegExp(`^---\\n[\\s\\S]*?${field}:\\s*['"]?([^'"\\n]+)['"]?\\n[\\s\\S]*?\\n---`);
8841
+ const match = content.match(regex);
8842
+ return match ? match[1].trim() : void 0;
8843
+ }
8844
+
8845
+ // src/cli/commands/migrate.ts
8148
8846
  function registerMigrateCommand(program2) {
8149
8847
  const migrate = program2.command("migrate").description("\uAE30\uC874 \uBB38\uC11C\uB97C SDD \uD615\uC2DD\uC73C\uB85C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4");
8150
8848
  migrate.command("docs <source>").description("\uB9C8\uD06C\uB2E4\uC6B4 \uBB38\uC11C\uB97C spec.md \uD615\uC2DD\uC73C\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4").option("-o, --output <dir>", "\uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC").option("--dry-run", "\uC2E4\uC81C \uD30C\uC77C \uC0DD\uC131 \uC5C6\uC774 \uBBF8\uB9AC\uBCF4\uAE30").action(async (source, options) => {
@@ -8171,6 +8869,30 @@ function registerMigrateCommand(program2) {
8171
8869
  process.exit(ExitCode.GENERAL_ERROR);
8172
8870
  }
8173
8871
  });
8872
+ migrate.command("detect").description("\uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uC678\uBD80 SDD \uB3C4\uAD6C\uB97C \uAC10\uC9C0\uD569\uB2C8\uB2E4").option("-p, --path <path>", "\uAC80\uC0C9 \uACBD\uB85C").action(async (options) => {
8873
+ try {
8874
+ await runDetect(options);
8875
+ } catch (error2) {
8876
+ error(error2 instanceof Error ? error2.message : String(error2));
8877
+ process.exit(ExitCode.GENERAL_ERROR);
8878
+ }
8879
+ });
8880
+ migrate.command("openspec [source]").description("OpenSpec \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4").option("--dry-run", "\uC2E4\uC81C \uD30C\uC77C \uC0DD\uC131 \uC5C6\uC774 \uBBF8\uB9AC\uBCF4\uAE30").option("--overwrite", "\uAE30\uC874 \uC2A4\uD399 \uB36E\uC5B4\uC4F0\uAE30").action(async (source, options) => {
8881
+ try {
8882
+ await runMigrateOpenSpec(source, options);
8883
+ } catch (error2) {
8884
+ error(error2 instanceof Error ? error2.message : String(error2));
8885
+ process.exit(ExitCode.GENERAL_ERROR);
8886
+ }
8887
+ });
8888
+ migrate.command("speckit [source]").description("Spec Kit \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4").option("--dry-run", "\uC2E4\uC81C \uD30C\uC77C \uC0DD\uC131 \uC5C6\uC774 \uBBF8\uB9AC\uBCF4\uAE30").option("--overwrite", "\uAE30\uC874 \uC2A4\uD399 \uB36E\uC5B4\uC4F0\uAE30").action(async (source, options) => {
8889
+ try {
8890
+ await runMigrateSpecKit(source, options);
8891
+ } catch (error2) {
8892
+ error(error2 instanceof Error ? error2.message : String(error2));
8893
+ process.exit(ExitCode.GENERAL_ERROR);
8894
+ }
8895
+ });
8174
8896
  }
8175
8897
  async function runMigrateDocs(source, options) {
8176
8898
  const projectRoot = await findSddRoot();
@@ -8178,7 +8900,7 @@ async function runMigrateDocs(source, options) {
8178
8900
  error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --output \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uAC70\uB098 sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
8179
8901
  process.exit(ExitCode.GENERAL_ERROR);
8180
8902
  }
8181
- const sourcePath = path17.resolve(source);
8903
+ const sourcePath = path18.resolve(source);
8182
8904
  let files = [];
8183
8905
  try {
8184
8906
  const stat = await fs11.stat(sourcePath);
@@ -8197,7 +8919,7 @@ async function runMigrateDocs(source, options) {
8197
8919
  }
8198
8920
  info(`${files.length}\uAC1C \uD30C\uC77C \uBC1C\uACAC`);
8199
8921
  newline();
8200
- const outputDir = options.output ? path17.resolve(options.output) : path17.join(projectRoot, ".sdd", "specs");
8922
+ const outputDir = options.output ? path18.resolve(options.output) : path18.join(projectRoot, ".sdd", "specs");
8201
8923
  const summary = {
8202
8924
  total: files.length,
8203
8925
  succeeded: 0,
@@ -8209,10 +8931,10 @@ async function runMigrateDocs(source, options) {
8209
8931
  summary.results.push(result);
8210
8932
  if (result.success) {
8211
8933
  summary.succeeded++;
8212
- info(`\u2705 ${path17.basename(file)} \u2192 ${result.target}`);
8934
+ info(`\u2705 ${path18.basename(file)} \u2192 ${result.target}`);
8213
8935
  } else {
8214
8936
  summary.failed++;
8215
- error(`\u274C ${path17.basename(file)}: ${result.error}`);
8937
+ error(`\u274C ${path18.basename(file)}: ${result.error}`);
8216
8938
  }
8217
8939
  }
8218
8940
  newline();
@@ -8226,23 +8948,23 @@ async function migrateDocument(filePath, outputDir, dryRun) {
8226
8948
  try {
8227
8949
  const content = await fs11.readFile(filePath, "utf-8");
8228
8950
  const analysis = analyzeDocument(content);
8229
- const featureId = generateFeatureId(analysis.title || path17.basename(filePath, ".md"));
8951
+ const featureId = generateFeatureId(analysis.title || path18.basename(filePath, ".md"));
8230
8952
  const specContent = generateSpec({
8231
8953
  id: featureId,
8232
- title: analysis.title || path17.basename(filePath, ".md"),
8954
+ title: analysis.title || path18.basename(filePath, ".md"),
8233
8955
  description: analysis.description || "",
8234
8956
  requirements: analysis.requirements,
8235
8957
  scenarios: analysis.scenarios
8236
8958
  });
8237
- const targetDir = path17.join(outputDir, featureId);
8238
- const targetPath = path17.join(targetDir, "spec.md");
8959
+ const targetDir = path18.join(outputDir, featureId);
8960
+ const targetPath = path18.join(targetDir, "spec.md");
8239
8961
  if (!dryRun) {
8240
8962
  await ensureDir(targetDir);
8241
8963
  await writeFile(targetPath, specContent);
8242
8964
  }
8243
8965
  return {
8244
8966
  source: filePath,
8245
- target: path17.relative(process.cwd(), targetPath),
8967
+ target: path18.relative(process.cwd(), targetPath),
8246
8968
  success: true
8247
8969
  };
8248
8970
  } catch (error2) {
@@ -8287,14 +9009,14 @@ function analyzeDocument(content) {
8287
9009
  };
8288
9010
  }
8289
9011
  async function runAnalyze(file) {
8290
- const filePath = path17.resolve(file);
9012
+ const filePath = path18.resolve(file);
8291
9013
  if (!await fileExists(filePath)) {
8292
9014
  error(`\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${file}`);
8293
9015
  process.exit(ExitCode.FILE_SYSTEM_ERROR);
8294
9016
  }
8295
9017
  const content = await fs11.readFile(filePath, "utf-8");
8296
9018
  const analysis = analyzeDocument(content);
8297
- info(`\u{1F4CA} \uBB38\uC11C \uBD84\uC11D: ${path17.basename(file)}`);
9019
+ info(`\u{1F4CA} \uBB38\uC11C \uBD84\uC11D: ${path18.basename(file)}`);
8298
9020
  newline();
8299
9021
  info(`\uC81C\uBAA9: ${analysis.title || "(\uC5C6\uC74C)"}`);
8300
9022
  info(`\uC124\uBA85: ${analysis.description || "(\uC5C6\uC74C)"}`);
@@ -8337,7 +9059,7 @@ async function runAnalyze(file) {
8337
9059
  }
8338
9060
  }
8339
9061
  async function runScan(dir, options) {
8340
- const dirPath = path17.resolve(dir);
9062
+ const dirPath = path18.resolve(dir);
8341
9063
  if (!await directoryExists(dirPath)) {
8342
9064
  error(`\uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dir}`);
8343
9065
  process.exit(ExitCode.FILE_SYSTEM_ERROR);
@@ -8368,7 +9090,7 @@ async function runScan(dir, options) {
8368
9090
  const partial = [];
8369
9091
  const notReady = [];
8370
9092
  for (const { file, analysis } of results) {
8371
- const relativePath = path17.relative(process.cwd(), file);
9093
+ const relativePath = path18.relative(process.cwd(), file);
8372
9094
  if (analysis.hasRfc2119 && analysis.hasScenarios) {
8373
9095
  ready.push(relativePath);
8374
9096
  } else if (analysis.hasRfc2119 || analysis.hasScenarios || analysis.requirements.length > 0) {
@@ -8417,13 +9139,13 @@ async function collectFilesWithExtensions(dirPath, extensions) {
8417
9139
  async function scan(dir) {
8418
9140
  const entries = await fs11.readdir(dir, { withFileTypes: true });
8419
9141
  for (const entry of entries) {
8420
- const fullPath = path17.join(dir, entry.name);
9142
+ const fullPath = path18.join(dir, entry.name);
8421
9143
  if (entry.isDirectory()) {
8422
9144
  if (!["node_modules", ".git", ".sdd", "dist", "build"].includes(entry.name)) {
8423
9145
  await scan(fullPath);
8424
9146
  }
8425
9147
  } else if (entry.isFile()) {
8426
- const ext = path17.extname(entry.name).toLowerCase();
9148
+ const ext = path18.extname(entry.name).toLowerCase();
8427
9149
  if (extensions.some((e) => e.toLowerCase() === ext)) {
8428
9150
  if (!["agents.md", "readme.md", "changelog.md", "license.md"].includes(entry.name.toLowerCase())) {
8429
9151
  files.push(fullPath);
@@ -8435,21 +9157,212 @@ async function collectFilesWithExtensions(dirPath, extensions) {
8435
9157
  await scan(dirPath);
8436
9158
  return files;
8437
9159
  }
8438
-
8439
- // src/cli/commands/cicd.ts
8440
- import path18 from "path";
8441
- init_errors();
8442
- init_fs();
8443
- function registerCicdCommand(program2) {
8444
- const cicd = program2.command("cicd").description("CI/CD \uD30C\uC774\uD504\uB77C\uC778 \uD1B5\uD569 \uC124\uC815");
8445
- cicd.command("setup [platform]").description("CI \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uD30C\uC77C\uC744 \uC0DD\uC131\uD569\uB2C8\uB2E4").option("--strict", "\uC5C4\uACA9 \uBAA8\uB4DC (\uACBD\uACE0\uB3C4 \uC5D0\uB7EC\uB85C \uCC98\uB9AC)").action(async (platform, options) => {
8446
- try {
8447
- await runSetup(platform || "github", options);
8448
- } catch (error2) {
8449
- error(error2 instanceof Error ? error2.message : String(error2));
8450
- process.exit(ExitCode.GENERAL_ERROR);
9160
+ async function runDetect(options) {
9161
+ const projectRoot = options.path ? path18.resolve(options.path) : process.cwd();
9162
+ info("\u{1F50D} \uC678\uBD80 SDD \uB3C4\uAD6C \uAC10\uC9C0 \uC911...");
9163
+ info(` \uACBD\uB85C: ${projectRoot}`);
9164
+ newline();
9165
+ const result = await detectExternalTools(projectRoot);
9166
+ if (!result.success) {
9167
+ error(result.error.message);
9168
+ process.exit(ExitCode.GENERAL_ERROR);
9169
+ }
9170
+ const tools = result.data;
9171
+ if (tools.length === 0) {
9172
+ info("\uAC10\uC9C0\uB41C \uC678\uBD80 SDD \uB3C4\uAD6C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
9173
+ return;
9174
+ }
9175
+ for (const tool of tools) {
9176
+ const icon = getToolIcon(tool.tool);
9177
+ const confidence = getConfidenceLabel(tool.confidence);
9178
+ info(`${icon} ${getToolName(tool.tool)}`);
9179
+ info(` \uACBD\uB85C: ${tool.path}`);
9180
+ info(` \uC2E0\uB8B0\uB3C4: ${confidence}`);
9181
+ info(` \uC2A4\uD399 \uC218: ${tool.specCount}\uAC1C`);
9182
+ if (tool.specs.length > 0) {
9183
+ newline();
9184
+ info(" \uBC1C\uACAC\uB41C \uC2A4\uD399:");
9185
+ for (const spec of tool.specs.slice(0, 5)) {
9186
+ const status = spec.status ? ` [${spec.status}]` : "";
9187
+ listItem(`${spec.id}: ${spec.title || "(\uC81C\uBAA9 \uC5C6\uC74C)"}${status}`, 2);
9188
+ }
9189
+ if (tool.specs.length > 5) {
9190
+ info(` ... \uC678 ${tool.specs.length - 5}\uAC1C`);
9191
+ }
8451
9192
  }
8452
- });
9193
+ newline();
9194
+ }
9195
+ const openspec = tools.find((t) => t.tool === "openspec");
9196
+ const speckit = tools.find((t) => t.tool === "speckit");
9197
+ if (openspec || speckit) {
9198
+ info("\u{1F4A1} \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uBA85\uB839\uC5B4:");
9199
+ if (openspec) {
9200
+ listItem(`sdd migrate openspec "${openspec.path}"`, 1);
9201
+ }
9202
+ if (speckit) {
9203
+ listItem(`sdd migrate speckit "${speckit.path}"`, 1);
9204
+ }
9205
+ }
9206
+ }
9207
+ async function runMigrateOpenSpec(source, options) {
9208
+ const projectRoot = await findSddRoot();
9209
+ if (!projectRoot) {
9210
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
9211
+ process.exit(ExitCode.GENERAL_ERROR);
9212
+ }
9213
+ let sourcePath;
9214
+ if (source) {
9215
+ sourcePath = path18.resolve(source);
9216
+ } else {
9217
+ const detectResult = await detectExternalTools(projectRoot);
9218
+ if (!detectResult.success) {
9219
+ error(detectResult.error.message);
9220
+ process.exit(ExitCode.GENERAL_ERROR);
9221
+ }
9222
+ const openspec = detectResult.data.find((t) => t.tool === "openspec");
9223
+ if (!openspec) {
9224
+ error("OpenSpec \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uACBD\uB85C\uB97C \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694.");
9225
+ process.exit(ExitCode.GENERAL_ERROR);
9226
+ }
9227
+ sourcePath = openspec.path;
9228
+ }
9229
+ const sddPath = path18.join(projectRoot, ".sdd");
9230
+ info("\u{1F504} OpenSpec\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC911...");
9231
+ info(` \uC18C\uC2A4: ${sourcePath}`);
9232
+ info(` \uB300\uC0C1: ${sddPath}`);
9233
+ if (options.dryRun) {
9234
+ warn(" (dry-run \uBAA8\uB4DC)");
9235
+ }
9236
+ newline();
9237
+ const result = await migrateFromOpenSpec(sourcePath, sddPath, {
9238
+ dryRun: options.dryRun,
9239
+ overwrite: options.overwrite
9240
+ });
9241
+ if (!result.success) {
9242
+ error(result.error.message);
9243
+ process.exit(ExitCode.GENERAL_ERROR);
9244
+ }
9245
+ const data = result.data;
9246
+ success2("\u2705 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC644\uB8CC");
9247
+ info(` \uC0DD\uC131: ${data.specsCreated}\uAC1C`);
9248
+ info(` \uC2A4\uD0B5: ${data.specsSkipped}\uAC1C`);
9249
+ if (data.errors.length > 0) {
9250
+ newline();
9251
+ warn("\u26A0\uFE0F \uC77C\uBD80 \uC624\uB958 \uBC1C\uC0DD:");
9252
+ for (const error2 of data.errors) {
9253
+ error(` - ${error2}`);
9254
+ }
9255
+ }
9256
+ if (options.dryRun) {
9257
+ newline();
9258
+ info("\uC2E4\uC81C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC744 \uC218\uD589\uD558\uB824\uBA74 --dry-run \uC635\uC158\uC744 \uC81C\uAC70\uD558\uC138\uC694.");
9259
+ }
9260
+ }
9261
+ async function runMigrateSpecKit(source, options) {
9262
+ const projectRoot = await findSddRoot();
9263
+ if (!projectRoot) {
9264
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
9265
+ process.exit(ExitCode.GENERAL_ERROR);
9266
+ }
9267
+ let sourcePath;
9268
+ if (source) {
9269
+ sourcePath = path18.resolve(source);
9270
+ } else {
9271
+ const detectResult = await detectExternalTools(projectRoot);
9272
+ if (!detectResult.success) {
9273
+ error(detectResult.error.message);
9274
+ process.exit(ExitCode.GENERAL_ERROR);
9275
+ }
9276
+ const speckit = detectResult.data.find((t) => t.tool === "speckit");
9277
+ if (!speckit) {
9278
+ error("Spec Kit \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uACBD\uB85C\uB97C \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694.");
9279
+ process.exit(ExitCode.GENERAL_ERROR);
9280
+ }
9281
+ sourcePath = speckit.path;
9282
+ }
9283
+ const sddPath = path18.join(projectRoot, ".sdd");
9284
+ info("\u{1F504} Spec Kit\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC911...");
9285
+ info(` \uC18C\uC2A4: ${sourcePath}`);
9286
+ info(` \uB300\uC0C1: ${sddPath}`);
9287
+ if (options.dryRun) {
9288
+ warn(" (dry-run \uBAA8\uB4DC)");
9289
+ }
9290
+ newline();
9291
+ const result = await migrateFromSpecKit(sourcePath, sddPath, {
9292
+ dryRun: options.dryRun,
9293
+ overwrite: options.overwrite
9294
+ });
9295
+ if (!result.success) {
9296
+ error(result.error.message);
9297
+ process.exit(ExitCode.GENERAL_ERROR);
9298
+ }
9299
+ const data = result.data;
9300
+ success2("\u2705 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC644\uB8CC");
9301
+ info(` \uC0DD\uC131: ${data.specsCreated}\uAC1C`);
9302
+ info(` \uC2A4\uD0B5: ${data.specsSkipped}\uAC1C`);
9303
+ if (data.errors.length > 0) {
9304
+ newline();
9305
+ warn("\u26A0\uFE0F \uC77C\uBD80 \uC624\uB958 \uBC1C\uC0DD:");
9306
+ for (const error2 of data.errors) {
9307
+ error(` - ${error2}`);
9308
+ }
9309
+ }
9310
+ if (options.dryRun) {
9311
+ newline();
9312
+ info("\uC2E4\uC81C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC744 \uC218\uD589\uD558\uB824\uBA74 --dry-run \uC635\uC158\uC744 \uC81C\uAC70\uD558\uC138\uC694.");
9313
+ }
9314
+ }
9315
+ function getToolIcon(tool) {
9316
+ switch (tool) {
9317
+ case "openspec":
9318
+ return "\u{1F4E6}";
9319
+ case "speckit":
9320
+ return "\u{1F527}";
9321
+ case "sdd":
9322
+ return "\u{1F4CB}";
9323
+ default:
9324
+ return "\u2753";
9325
+ }
9326
+ }
9327
+ function getToolName(tool) {
9328
+ switch (tool) {
9329
+ case "openspec":
9330
+ return "OpenSpec";
9331
+ case "speckit":
9332
+ return "Spec Kit";
9333
+ case "sdd":
9334
+ return "SDD";
9335
+ default:
9336
+ return tool;
9337
+ }
9338
+ }
9339
+ function getConfidenceLabel(confidence) {
9340
+ switch (confidence) {
9341
+ case "high":
9342
+ return "\uB192\uC74C \u2713";
9343
+ case "medium":
9344
+ return "\uC911\uAC04";
9345
+ case "low":
9346
+ return "\uB0AE\uC74C";
9347
+ default:
9348
+ return confidence;
9349
+ }
9350
+ }
9351
+
9352
+ // src/cli/commands/cicd.ts
9353
+ import path19 from "path";
9354
+ init_errors();
9355
+ init_fs();
9356
+ function registerCicdCommand(program2) {
9357
+ const cicd = program2.command("cicd").description("CI/CD \uD30C\uC774\uD504\uB77C\uC778 \uD1B5\uD569 \uC124\uC815");
9358
+ cicd.command("setup [platform]").description("CI \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uD30C\uC77C\uC744 \uC0DD\uC131\uD569\uB2C8\uB2E4").option("--strict", "\uC5C4\uACA9 \uBAA8\uB4DC (\uACBD\uACE0\uB3C4 \uC5D0\uB7EC\uB85C \uCC98\uB9AC)").action(async (platform, options) => {
9359
+ try {
9360
+ await runSetup(platform || "github", options);
9361
+ } catch (error2) {
9362
+ error(error2 instanceof Error ? error2.message : String(error2));
9363
+ process.exit(ExitCode.GENERAL_ERROR);
9364
+ }
9365
+ });
8453
9366
  cicd.command("hooks [type]").description("Git hooks\uB97C \uC124\uC815\uD569\uB2C8\uB2E4").option("--install", "husky \uC124\uCE58 \uD3EC\uD568").action(async (type, options) => {
8454
9367
  try {
8455
9368
  await runHooksSetup(type, options);
@@ -8489,16 +9402,16 @@ async function runSetup(platform, options) {
8489
9402
  listItem("PR/MR \uC0DD\uC131 \uC2DC \uC790\uB3D9\uC73C\uB85C \uC2A4\uD399 \uAC80\uC99D\uC774 \uC2E4\uD589\uB429\uB2C8\uB2E4");
8490
9403
  }
8491
9404
  async function setupGitHubActions(projectRoot, strict) {
8492
- const workflowDir = path18.join(projectRoot, ".github", "workflows");
9405
+ const workflowDir = path19.join(projectRoot, ".github", "workflows");
8493
9406
  await ensureDir(workflowDir);
8494
9407
  const workflowContent = generateGitHubWorkflow(strict);
8495
- const workflowPath = path18.join(workflowDir, "sdd-validate.yml");
9408
+ const workflowPath = path19.join(workflowDir, "sdd-validate.yml");
8496
9409
  await writeFile(workflowPath, workflowContent);
8497
9410
  info(`\u2705 GitHub Actions \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC0DD\uC131: .github/workflows/sdd-validate.yml`);
8498
9411
  }
8499
9412
  async function setupGitLabCI(projectRoot, strict) {
8500
9413
  const ciContent = generateGitLabCI(strict);
8501
- const ciPath = path18.join(projectRoot, ".gitlab-ci-sdd.yml");
9414
+ const ciPath = path19.join(projectRoot, ".gitlab-ci-sdd.yml");
8502
9415
  await writeFile(ciPath, ciContent);
8503
9416
  info(`\u2705 GitLab CI \uAD6C\uC131 \uC0DD\uC131: .gitlab-ci-sdd.yml`);
8504
9417
  info(" (\uAE30\uC874 .gitlab-ci.yml\uC5D0 include\uD558\uAC70\uB098 \uBCD1\uD569\uD558\uC138\uC694)");
@@ -8594,7 +9507,7 @@ async function runHooksSetup(type, options) {
8594
9507
  error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
8595
9508
  process.exit(ExitCode.GENERAL_ERROR);
8596
9509
  }
8597
- const hooksDir = path18.join(projectRoot, ".husky");
9510
+ const hooksDir = path19.join(projectRoot, ".husky");
8598
9511
  if (options.install) {
8599
9512
  info("husky \uC124\uCE58 \uBC29\uBC95:");
8600
9513
  newline();
@@ -8608,7 +9521,7 @@ async function runHooksSetup(type, options) {
8608
9521
  const hooks = type ? [type] : ["pre-commit", "pre-push"];
8609
9522
  for (const hook of hooks) {
8610
9523
  const hookContent = generateHookScript(hook);
8611
- const hookPath = path18.join(hooksDir, hook);
9524
+ const hookPath = path19.join(hooksDir, hook);
8612
9525
  await writeFile(hookPath, hookContent);
8613
9526
  info(`\u2705 ${hook} \uD6C5 \uC0DD\uC131: .husky/${hook}`);
8614
9527
  }
@@ -8690,7 +9603,7 @@ async function runCiCheck(options) {
8690
9603
  let hasErrors = false;
8691
9604
  let hasWarnings = false;
8692
9605
  info("1. Constitution \uAC80\uC99D...");
8693
- const constitutionPath = path18.join(projectRoot, ".sdd", "constitution.md");
9606
+ const constitutionPath = path19.join(projectRoot, ".sdd", "constitution.md");
8694
9607
  if (await fileExists(constitutionPath)) {
8695
9608
  info(" \u2705 constitution.md \uC874\uC7AC");
8696
9609
  } else {
@@ -8698,7 +9611,7 @@ async function runCiCheck(options) {
8698
9611
  hasWarnings = true;
8699
9612
  }
8700
9613
  info("2. \uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC \uD655\uC778...");
8701
- const specsPath = path18.join(projectRoot, ".sdd", "specs");
9614
+ const specsPath = path19.join(projectRoot, ".sdd", "specs");
8702
9615
  if (await directoryExists(specsPath)) {
8703
9616
  info(" \u2705 specs/ \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC");
8704
9617
  } else {
@@ -8708,7 +9621,7 @@ async function runCiCheck(options) {
8708
9621
  info("3. \uAE30\uBCF8 \uAD6C\uC870 \uD655\uC778...");
8709
9622
  const requiredDirs = ["changes", "archive", "templates"];
8710
9623
  for (const dir of requiredDirs) {
8711
- const dirPath = path18.join(projectRoot, ".sdd", dir);
9624
+ const dirPath = path19.join(projectRoot, ".sdd", dir);
8712
9625
  if (await directoryExists(dirPath)) {
8713
9626
  info(` \u2705 ${dir}/ \uC874\uC7AC`);
8714
9627
  } else {
@@ -8736,9 +9649,130 @@ async function runCiCheck(options) {
8736
9649
  }
8737
9650
 
8738
9651
  // src/cli/commands/transition.ts
8739
- import path19 from "path";
9652
+ import path20 from "path";
9653
+ import { promises as fs12 } from "fs";
8740
9654
  init_errors();
8741
9655
  init_fs();
9656
+ init_types();
9657
+ async function getExistingChangeIds(sddPath) {
9658
+ const changesPath = path20.join(sddPath, "changes");
9659
+ try {
9660
+ const dirs = await fs12.readdir(changesPath);
9661
+ return dirs.filter((d) => d.startsWith("CHG-"));
9662
+ } catch {
9663
+ return [];
9664
+ }
9665
+ }
9666
+ async function extractProposalTitle(proposalPath) {
9667
+ if (!await fileExists(proposalPath)) {
9668
+ return "";
9669
+ }
9670
+ const content = await readFile(proposalPath);
9671
+ if (!content.success) {
9672
+ return "";
9673
+ }
9674
+ const titleMatch = content.data.match(/^#\s+(.+)$/m);
9675
+ return titleMatch ? titleMatch[1] : "";
9676
+ }
9677
+ async function transitionNewToChange(sddPath, specId, options) {
9678
+ const specsPath = path20.join(sddPath, "specs");
9679
+ const specPath = path20.join(specsPath, specId, "spec.md");
9680
+ if (!await fileExists(specPath)) {
9681
+ return failure(new Error(`\uC2A4\uD399\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${specId}`));
9682
+ }
9683
+ const existingIds = await getExistingChangeIds(sddPath);
9684
+ const changeId = generateChangeId(existingIds);
9685
+ const changesPath = path20.join(sddPath, "changes");
9686
+ const changePath = path20.join(changesPath, changeId);
9687
+ await ensureDir(changePath);
9688
+ const title2 = options.title || `${specId} \uAE30\uB2A5 \uD655\uC7A5`;
9689
+ const reason = options.reason || "new \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC5D0\uC11C \uC804\uD658\uB428";
9690
+ const proposalContent = generateTransitionProposal(specId, title2, reason, "new-to-change");
9691
+ await writeFile(path20.join(changePath, "proposal.md"), proposalContent);
9692
+ const deltaContent = generateDeltaTemplate2(specId);
9693
+ await writeFile(path20.join(changePath, "delta.md"), deltaContent);
9694
+ const tasksContent = generateTasksTemplate2();
9695
+ await writeFile(path20.join(changePath, "tasks.md"), tasksContent);
9696
+ return success({
9697
+ changeId,
9698
+ changePath,
9699
+ filesCreated: ["proposal.md", "delta.md", "tasks.md"]
9700
+ });
9701
+ }
9702
+ async function transitionChangeToNew(sddPath, changeId, options) {
9703
+ const changePath = path20.join(sddPath, "changes", changeId);
9704
+ if (!await directoryExists(changePath)) {
9705
+ return failure(new Error(`\uBCC0\uACBD \uC81C\uC548\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${changeId}`));
9706
+ }
9707
+ const proposalPath = path20.join(changePath, "proposal.md");
9708
+ const extractedTitle = await extractProposalTitle(proposalPath);
9709
+ const featureName = options.name || extractedTitle.toLowerCase().replace(/\s+/g, "-") || `feature-from-${changeId}`;
9710
+ const specsPath = path20.join(sddPath, "specs");
9711
+ const newSpecPath = path20.join(specsPath, featureName);
9712
+ if (await directoryExists(newSpecPath)) {
9713
+ return failure(new Error(`\uC2A4\uD399\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${featureName}`));
9714
+ }
9715
+ await ensureDir(newSpecPath);
9716
+ const reason = options.reason || "change \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC5D0\uC11C \uC804\uD658\uB428";
9717
+ const specContent = generateTransitionSpec(featureName, extractedTitle || featureName, reason, changeId);
9718
+ await writeFile(path20.join(newSpecPath, "spec.md"), specContent);
9719
+ const planContent = generatePlanTemplate(featureName);
9720
+ await writeFile(path20.join(newSpecPath, "plan.md"), planContent);
9721
+ const tasksContent = generateTasksTemplate2();
9722
+ await writeFile(path20.join(newSpecPath, "tasks.md"), tasksContent);
9723
+ const statusPath = path20.join(changePath, ".status");
9724
+ await writeFile(statusPath, JSON.stringify({
9725
+ status: "transitioned",
9726
+ transitionedTo: featureName,
9727
+ transitionedAt: (/* @__PURE__ */ new Date()).toISOString(),
9728
+ reason
9729
+ }, null, 2));
9730
+ return success({
9731
+ featureName,
9732
+ featurePath: newSpecPath,
9733
+ filesCreated: ["spec.md", "plan.md", "tasks.md"],
9734
+ originalChangeId: changeId
9735
+ });
9736
+ }
9737
+ function getTransitionGuide() {
9738
+ return `=== \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC804\uD658 \uAC00\uC774\uB4DC ===
9739
+
9740
+ ## new \u2192 change \uC804\uD658
9741
+
9742
+ \uC0AC\uC6A9 \uC2DC\uC810:
9743
+ - \uC0C8 \uAE30\uB2A5 \uC791\uC131 \uC911 \uAE30\uC874 \uC2A4\uD399\uACFC \uC911\uBCF5 \uBC1C\uACAC
9744
+ - \uAE30\uC874 \uAE30\uB2A5 \uD655\uC7A5\uC774 \uB354 \uC801\uC808\uD55C \uACBD\uC6B0
9745
+ - \uC758\uC874\uC131 \uBD84\uC11D \uACB0\uACFC \uAE30\uC874 \uC2A4\uD399 \uC218\uC815 \uD544\uC694
9746
+
9747
+ \uBA85\uB839\uC5B4:
9748
+ sdd transition new-to-change <spec-id>
9749
+ -t, --title <title> : \uBCC0\uACBD \uC81C\uC548 \uC81C\uBAA9
9750
+ -r, --reason <reason>: \uC804\uD658 \uC0AC\uC720
9751
+
9752
+ ## change \u2192 new \uC804\uD658
9753
+
9754
+ \uC0AC\uC6A9 \uC2DC\uC810:
9755
+ - \uBCC0\uACBD \uBC94\uC704\uAC00 \uB108\uBB34 \uCEE4\uC11C \uBCC4\uB3C4 \uAE30\uB2A5\uC73C\uB85C \uBD84\uB9AC \uD544\uC694
9756
+ - \uAE30\uC874 \uC2A4\uD399\uACFC \uB3C5\uB9BD\uC801\uC778 \uC0C8 \uAE30\uB2A5\uC73C\uB85C \uBC1C\uC804
9757
+ - \uC601\uD5A5\uB3C4 \uBD84\uC11D \uACB0\uACFC \uBD84\uB9AC\uAC00 \uC548\uC804
9758
+
9759
+ \uBA85\uB839\uC5B4:
9760
+ sdd transition change-to-new <change-id>
9761
+ -n, --name <name> : \uC0C8 \uAE30\uB2A5 \uC774\uB984
9762
+ -r, --reason <reason>: \uC804\uD658 \uC0AC\uC720
9763
+
9764
+ ## \uC804\uD658 \uD310\uB2E8 \uAE30\uC900
9765
+
9766
+ new \u2192 change:
9767
+ - \uC601\uD5A5\uBC1B\uB294 \uC2A4\uD399 \uC218 \u2264 3\uAC1C
9768
+ - \uBCC0\uACBD\uC774 \uAE30\uC874 \uAE30\uB2A5\uC758 \uC790\uC5F0\uC2A4\uB7EC\uC6B4 \uD655\uC7A5
9769
+ - \uC0C8 \uC2DC\uB098\uB9AC\uC624 \uCD94\uAC00\uBCF4\uB2E4 \uAE30\uC874 \uC2DC\uB098\uB9AC\uC624 \uC218\uC815 \uC911\uC2EC
9770
+
9771
+ change \u2192 new:
9772
+ - \uC601\uD5A5\uBC1B\uB294 \uC2A4\uD399 \uC218 > 3\uAC1C
9773
+ - \uC0C8\uB85C\uC6B4 \uAC1C\uB150/\uB3C4\uBA54\uC778 \uB3C4\uC785
9774
+ - \uAE30\uC874 \uC2A4\uD399\uACFC \uB3C5\uB9BD\uC801\uC73C\uB85C \uD14C\uC2A4\uD2B8 \uAC00\uB2A5`;
9775
+ }
8742
9776
  function registerTransitionCommand(program2) {
8743
9777
  const transition = program2.command("transition").description("\uC6CC\uD06C\uD50C\uB85C\uC6B0 \uAC04 \uC804\uD658\uC744 \uC218\uD589\uD569\uB2C8\uB2E4");
8744
9778
  transition.command("new-to-change <spec-id>").description("\uC0C8 \uAE30\uB2A5 \uC791\uC5C5\uC744 \uAE30\uC874 \uC2A4\uD399 \uBCC0\uACBD\uC73C\uB85C \uC804\uD658\uD569\uB2C8\uB2E4").option("-t, --title <title>", "\uBCC0\uACBD \uC81C\uC548 \uC81C\uBAA9").option("-r, --reason <reason>", "\uC804\uD658 \uC0AC\uC720").action(async (specId, options) => {
@@ -8767,33 +9801,17 @@ async function runNewToChange(specId, options) {
8767
9801
  error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
8768
9802
  process.exit(ExitCode.GENERAL_ERROR);
8769
9803
  }
8770
- const sddPath = path19.join(projectRoot, ".sdd");
8771
- const specsPath = path19.join(sddPath, "specs");
8772
- const specPath = path19.join(specsPath, specId, "spec.md");
8773
- if (!await fileExists(specPath)) {
8774
- error(`\uC2A4\uD399\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${specId}`);
8775
- info("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uC2A4\uD399 \uBAA9\uB85D\uC740 `sdd list`\uB85C \uD655\uC778\uD558\uC138\uC694.");
8776
- process.exit(ExitCode.GENERAL_ERROR);
8777
- }
9804
+ const sddPath = path20.join(projectRoot, ".sdd");
8778
9805
  info("=== \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC804\uD658: new \u2192 change ===");
8779
9806
  newline();
8780
9807
  info(`\uB300\uC0C1 \uC2A4\uD399: ${specId}`);
8781
- const changeId = generateChangeId();
8782
- const changePath = path19.join(sddPath, "changes", changeId);
8783
- await ensureDir(changePath);
8784
- const specContent = await readFile(specPath);
8785
- if (!specContent.success) {
8786
- error("\uC2A4\uD399 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
9808
+ const result = await transitionNewToChange(sddPath, specId, options);
9809
+ if (!result.success) {
9810
+ error(result.error.message);
9811
+ info("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uC2A4\uD399 \uBAA9\uB85D\uC740 `sdd list`\uB85C \uD655\uC778\uD558\uC138\uC694.");
8787
9812
  process.exit(ExitCode.GENERAL_ERROR);
8788
9813
  }
8789
- const title2 = options.title || `${specId} \uAE30\uB2A5 \uD655\uC7A5`;
8790
- const reason = options.reason || "new \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC5D0\uC11C \uC804\uD658\uB428";
8791
- const proposalContent = generateTransitionProposal(specId, title2, reason, "new-to-change");
8792
- await writeFile(path19.join(changePath, "proposal.md"), proposalContent);
8793
- const deltaContent = generateDeltaTemplate(specId);
8794
- await writeFile(path19.join(changePath, "delta.md"), deltaContent);
8795
- const tasksContent = generateTasksTemplate();
8796
- await writeFile(path19.join(changePath, "tasks.md"), tasksContent);
9814
+ const { changeId } = result.data;
8797
9815
  newline();
8798
9816
  success2(`\uC804\uD658 \uC644\uB8CC! \uBCC0\uACBD \uC81C\uC548\uC774 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
8799
9817
  newline();
@@ -8815,56 +9833,27 @@ async function runChangeToNew(changeId, options) {
8815
9833
  error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
8816
9834
  process.exit(ExitCode.GENERAL_ERROR);
8817
9835
  }
8818
- const sddPath = path19.join(projectRoot, ".sdd");
8819
- const changePath = path19.join(sddPath, "changes", changeId);
8820
- if (!await directoryExists(changePath)) {
8821
- error(`\uBCC0\uACBD \uC81C\uC548\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${changeId}`);
8822
- info("\uC9C4\uD589 \uC911\uC778 \uBCC0\uACBD \uBAA9\uB85D\uC740 `sdd change -l`\uB85C \uD655\uC778\uD558\uC138\uC694.");
8823
- process.exit(ExitCode.GENERAL_ERROR);
8824
- }
9836
+ const sddPath = path20.join(projectRoot, ".sdd");
8825
9837
  info("=== \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC804\uD658: change \u2192 new ===");
8826
9838
  newline();
8827
9839
  info(`\uC6D0\uBCF8 \uBCC0\uACBD: ${changeId}`);
8828
- const proposalPath = path19.join(changePath, "proposal.md");
8829
- let extractedTitle = "";
8830
- if (await fileExists(proposalPath)) {
8831
- const proposalContent = await readFile(proposalPath);
8832
- if (proposalContent.success) {
8833
- const titleMatch = proposalContent.data.match(/^#\s+(.+)$/m);
8834
- if (titleMatch) {
8835
- extractedTitle = titleMatch[1];
8836
- }
9840
+ const result = await transitionChangeToNew(sddPath, changeId, options);
9841
+ if (!result.success) {
9842
+ error(result.error.message);
9843
+ if (result.error.message.includes("\uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4")) {
9844
+ info("\uC9C4\uD589 \uC911\uC778 \uBCC0\uACBD \uBAA9\uB85D\uC740 `sdd change -l`\uB85C \uD655\uC778\uD558\uC138\uC694.");
9845
+ } else if (result.error.message.includes("\uC774\uBBF8 \uC874\uC7AC")) {
9846
+ info("\uB2E4\uB978 \uC774\uB984\uC744 \uC9C0\uC815\uD558\uC138\uC694: --name <name>");
8837
9847
  }
8838
- }
8839
- const featureName = options.name || extractedTitle.toLowerCase().replace(/\s+/g, "-") || `feature-from-${changeId}`;
8840
- const specsPath = path19.join(sddPath, "specs");
8841
- const newSpecPath = path19.join(specsPath, featureName);
8842
- if (await directoryExists(newSpecPath)) {
8843
- error(`\uC2A4\uD399\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${featureName}`);
8844
- info("\uB2E4\uB978 \uC774\uB984\uC744 \uC9C0\uC815\uD558\uC138\uC694: --name <name>");
8845
9848
  process.exit(ExitCode.GENERAL_ERROR);
8846
9849
  }
8847
- await ensureDir(newSpecPath);
8848
- const reason = options.reason || "change \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC5D0\uC11C \uC804\uD658\uB428";
8849
- const specContent = generateTransitionSpec(featureName, extractedTitle || featureName, reason, changeId);
8850
- await writeFile(path19.join(newSpecPath, "spec.md"), specContent);
8851
- const planContent = generatePlanTemplate(featureName);
8852
- await writeFile(path19.join(newSpecPath, "plan.md"), planContent);
8853
- const tasksContent = generateTasksTemplate();
8854
- await writeFile(path19.join(newSpecPath, "tasks.md"), tasksContent);
8855
- const statusPath = path19.join(changePath, ".status");
8856
- await writeFile(statusPath, JSON.stringify({
8857
- status: "transitioned",
8858
- transitionedTo: featureName,
8859
- transitionedAt: (/* @__PURE__ */ new Date()).toISOString(),
8860
- reason
8861
- }, null, 2));
9850
+ const { featureName, originalChangeId } = result.data;
8862
9851
  newline();
8863
9852
  success2(`\uC804\uD658 \uC644\uB8CC! \uC0C8 \uAE30\uB2A5 \uC2A4\uD399\uC774 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
8864
9853
  newline();
8865
9854
  info(`\uAE30\uB2A5 \uC774\uB984: ${featureName}`);
8866
9855
  info(`\uC704\uCE58: .sdd/specs/${featureName}/`);
8867
- info(`\uC6D0\uBCF8 \uBCC0\uACBD ${changeId}\uC740 'transitioned' \uC0C1\uD0DC\uB85C \uBCC0\uACBD\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
9856
+ info(`\uC6D0\uBCF8 \uBCC0\uACBD ${originalChangeId}\uC740 'transitioned' \uC0C1\uD0DC\uB85C \uBCC0\uACBD\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
8868
9857
  newline();
8869
9858
  info("\uB2E4\uC74C \uB2E8\uACC4:");
8870
9859
  listItem(`1. .sdd/specs/${featureName}/spec.md \uD3B8\uC9D1`);
@@ -8876,43 +9865,7 @@ async function runChangeToNew(changeId, options) {
8876
9865
  listItem("/sdd.new - \uBA85\uC138 \uC791\uC131 \uB3C4\uC6C0");
8877
9866
  }
8878
9867
  function displayTransitionGuide() {
8879
- info("=== \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC804\uD658 \uAC00\uC774\uB4DC ===");
8880
- newline();
8881
- info("## new \u2192 change \uC804\uD658");
8882
- newline();
8883
- info("\uC0AC\uC6A9 \uC2DC\uC810:");
8884
- listItem("\uC0C8 \uAE30\uB2A5 \uC791\uC131 \uC911 \uAE30\uC874 \uC2A4\uD399\uACFC \uC911\uBCF5 \uBC1C\uACAC");
8885
- listItem("\uAE30\uC874 \uAE30\uB2A5 \uD655\uC7A5\uC774 \uB354 \uC801\uC808\uD55C \uACBD\uC6B0");
8886
- listItem("\uC758\uC874\uC131 \uBD84\uC11D \uACB0\uACFC \uAE30\uC874 \uC2A4\uD399 \uC218\uC815 \uD544\uC694");
8887
- newline();
8888
- info("\uBA85\uB839\uC5B4:");
8889
- listItem("sdd transition new-to-change <spec-id>");
8890
- listItem(" -t, --title <title> : \uBCC0\uACBD \uC81C\uC548 \uC81C\uBAA9");
8891
- listItem(" -r, --reason <reason>: \uC804\uD658 \uC0AC\uC720");
8892
- newline();
8893
- info("## change \u2192 new \uC804\uD658");
8894
- newline();
8895
- info("\uC0AC\uC6A9 \uC2DC\uC810:");
8896
- listItem("\uBCC0\uACBD \uBC94\uC704\uAC00 \uB108\uBB34 \uCEE4\uC11C \uBCC4\uB3C4 \uAE30\uB2A5\uC73C\uB85C \uBD84\uB9AC \uD544\uC694");
8897
- listItem("\uAE30\uC874 \uC2A4\uD399\uACFC \uB3C5\uB9BD\uC801\uC778 \uC0C8 \uAE30\uB2A5\uC73C\uB85C \uBC1C\uC804");
8898
- listItem("\uC601\uD5A5\uB3C4 \uBD84\uC11D \uACB0\uACFC \uBD84\uB9AC\uAC00 \uC548\uC804");
8899
- newline();
8900
- info("\uBA85\uB839\uC5B4:");
8901
- listItem("sdd transition change-to-new <change-id>");
8902
- listItem(" -n, --name <name> : \uC0C8 \uAE30\uB2A5 \uC774\uB984");
8903
- listItem(" -r, --reason <reason>: \uC804\uD658 \uC0AC\uC720");
8904
- newline();
8905
- info("## \uC804\uD658 \uD310\uB2E8 \uAE30\uC900");
8906
- newline();
8907
- info("new \u2192 change:");
8908
- listItem("\uC601\uD5A5\uBC1B\uB294 \uC2A4\uD399 \uC218 \u2264 3\uAC1C");
8909
- listItem("\uBCC0\uACBD\uC774 \uAE30\uC874 \uAE30\uB2A5\uC758 \uC790\uC5F0\uC2A4\uB7EC\uC6B4 \uD655\uC7A5");
8910
- listItem("\uC0C8 \uC2DC\uB098\uB9AC\uC624 \uCD94\uAC00\uBCF4\uB2E4 \uAE30\uC874 \uC2DC\uB098\uB9AC\uC624 \uC218\uC815 \uC911\uC2EC");
8911
- newline();
8912
- info("change \u2192 new:");
8913
- listItem("\uC601\uD5A5\uBC1B\uB294 \uC2A4\uD399 \uC218 > 3\uAC1C");
8914
- listItem("\uC0C8\uB85C\uC6B4 \uAC1C\uB150/\uB3C4\uBA54\uC778 \uB3C4\uC785");
8915
- listItem("\uAE30\uC874 \uC2A4\uD399\uACFC \uB3C5\uB9BD\uC801\uC73C\uB85C \uD14C\uC2A4\uD2B8 \uAC00\uB2A5");
9868
+ console.log(getTransitionGuide());
8916
9869
  }
8917
9870
  function generateTransitionProposal(specId, title2, reason, direction) {
8918
9871
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -8954,7 +9907,7 @@ transition_reason: "${reason}"
8954
9907
  <!-- \uCD94\uAC00 \uCC38\uACE0 \uC815\uBCF4\uAC00 \uC788\uB2E4\uBA74 \uC791\uC131\uD558\uC138\uC694 -->
8955
9908
  `;
8956
9909
  }
8957
- function generateDeltaTemplate(specId) {
9910
+ function generateDeltaTemplate2(specId) {
8958
9911
  return `---
8959
9912
  target: ${specId}
8960
9913
  ---
@@ -9065,7 +10018,7 @@ status: draft
9065
10018
  <!-- \uAD6C\uD604 \uC2DC \uACE0\uB824\uD560 \uC704\uD5D8 \uC694\uC18C -->
9066
10019
  `;
9067
10020
  }
9068
- function generateTasksTemplate() {
10021
+ function generateTasksTemplate2() {
9069
10022
  return `---
9070
10023
  status: pending
9071
10024
  ---
@@ -9086,6 +10039,1156 @@ status: pending
9086
10039
  `;
9087
10040
  }
9088
10041
 
10042
+ // src/cli/commands/watch.ts
10043
+ import path22 from "path";
10044
+
10045
+ // src/core/watch/watcher.ts
10046
+ import chokidar from "chokidar";
10047
+ import path21 from "path";
10048
+ import { EventEmitter } from "events";
10049
+ var SpecWatcher = class extends EventEmitter {
10050
+ watcher = null;
10051
+ specsPath;
10052
+ debounceMs;
10053
+ debounceTimer = null;
10054
+ pendingEvents = [];
10055
+ isRunning = false;
10056
+ constructor(options) {
10057
+ super();
10058
+ this.specsPath = options.specsPath;
10059
+ this.debounceMs = options.debounceMs ?? 500;
10060
+ }
10061
+ /**
10062
+ * 감시 시작
10063
+ */
10064
+ start() {
10065
+ if (this.isRunning) {
10066
+ return;
10067
+ }
10068
+ const ignored = [
10069
+ "**/node_modules/**",
10070
+ "**/.git/**",
10071
+ "**/.*"
10072
+ ];
10073
+ this.watcher = chokidar.watch(this.specsPath, {
10074
+ persistent: true,
10075
+ ignoreInitial: true,
10076
+ ignored,
10077
+ awaitWriteFinish: {
10078
+ stabilityThreshold: 100,
10079
+ pollInterval: 100
10080
+ }
10081
+ });
10082
+ this.watcher.on("add", (filePath) => this.handleEvent("add", filePath)).on("change", (filePath) => this.handleEvent("change", filePath)).on("unlink", (filePath) => this.handleEvent("unlink", filePath)).on("error", (error2) => this.emit("error", error2)).on("ready", () => {
10083
+ this.isRunning = true;
10084
+ this.emit("ready");
10085
+ });
10086
+ }
10087
+ /**
10088
+ * 감시 중지
10089
+ */
10090
+ async stop() {
10091
+ if (this.debounceTimer) {
10092
+ clearTimeout(this.debounceTimer);
10093
+ this.debounceTimer = null;
10094
+ }
10095
+ if (this.watcher) {
10096
+ await this.watcher.close();
10097
+ this.watcher = null;
10098
+ }
10099
+ this.isRunning = false;
10100
+ this.pendingEvents = [];
10101
+ }
10102
+ /**
10103
+ * 실행 상태 확인
10104
+ */
10105
+ get running() {
10106
+ return this.isRunning;
10107
+ }
10108
+ /**
10109
+ * 파일 이벤트 처리
10110
+ */
10111
+ handleEvent(type, filePath) {
10112
+ if (!filePath.endsWith(".md")) {
10113
+ return;
10114
+ }
10115
+ const event = {
10116
+ type,
10117
+ path: filePath,
10118
+ relativePath: path21.relative(this.specsPath, filePath),
10119
+ timestamp: /* @__PURE__ */ new Date()
10120
+ };
10121
+ this.pendingEvents.push(event);
10122
+ if (this.debounceTimer) {
10123
+ clearTimeout(this.debounceTimer);
10124
+ }
10125
+ this.debounceTimer = setTimeout(() => {
10126
+ this.flushEvents();
10127
+ }, this.debounceMs);
10128
+ }
10129
+ /**
10130
+ * 대기 중인 이벤트 처리
10131
+ */
10132
+ flushEvents() {
10133
+ if (this.pendingEvents.length === 0) {
10134
+ return;
10135
+ }
10136
+ const events = [...this.pendingEvents];
10137
+ this.pendingEvents = [];
10138
+ this.debounceTimer = null;
10139
+ this.emit("change", events);
10140
+ }
10141
+ };
10142
+ function createWatcher(options) {
10143
+ return new SpecWatcher(options);
10144
+ }
10145
+
10146
+ // src/cli/commands/watch.ts
10147
+ init_fs();
10148
+ init_errors();
10149
+ function registerWatchCommand(program2) {
10150
+ program2.command("watch").description("\uC2A4\uD399 \uD30C\uC77C \uBCC0\uACBD\uC744 \uC2E4\uC2DC\uAC04 \uAC10\uC2DC\uD558\uACE0 \uC790\uB3D9 \uAC80\uC99D\uD569\uB2C8\uB2E4").option("--no-validate", "\uC790\uB3D9 \uAC80\uC99D \uBE44\uD65C\uC131\uD654").option("--impact", "\uC601\uD5A5\uB3C4 \uBD84\uC11D \uD3EC\uD568").option("-q, --quiet", "\uC131\uACF5 \uC2DC \uCD9C\uB825 \uC0DD\uB7B5").option("--debounce <ms>", "\uB514\uBC14\uC6B4\uC2A4 \uC2DC\uAC04 (\uAE30\uBCF8: 500ms)", "500").action(async (options) => {
10151
+ try {
10152
+ await runWatch(options);
10153
+ } catch (error2) {
10154
+ error(error2 instanceof Error ? error2.message : String(error2));
10155
+ process.exit(ExitCode.GENERAL_ERROR);
10156
+ }
10157
+ });
10158
+ }
10159
+ async function runWatch(options) {
10160
+ const projectRoot = await findSddRoot();
10161
+ if (!projectRoot) {
10162
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
10163
+ process.exit(ExitCode.GENERAL_ERROR);
10164
+ }
10165
+ const sddPath = path22.join(projectRoot, ".sdd");
10166
+ const specsPath = path22.join(sddPath, "specs");
10167
+ const debounceMs = parseInt(options.debounce || "500", 10);
10168
+ info("\u{1F441}\uFE0F Watch \uBAA8\uB4DC \uC2DC\uC791");
10169
+ info(` \uACBD\uB85C: ${specsPath}`);
10170
+ info(` \uB514\uBC14\uC6B4\uC2A4: ${debounceMs}ms`);
10171
+ info(` \uAC80\uC99D: ${options.validate !== false ? "\uD65C\uC131\uD654" : "\uBE44\uD65C\uC131\uD654"}`);
10172
+ newline();
10173
+ info("\uD30C\uC77C \uBCC0\uACBD\uC744 \uAC10\uC2DC \uC911... (Ctrl+C\uB85C \uC885\uB8CC)");
10174
+ newline();
10175
+ const watcher = createWatcher({
10176
+ specsPath,
10177
+ debounceMs
10178
+ });
10179
+ let validationCount = 0;
10180
+ let errorCount = 0;
10181
+ watcher.on("change", async (events) => {
10182
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
10183
+ const addCount = events.filter((e) => e.type === "add").length;
10184
+ const changeCount = events.filter((e) => e.type === "change").length;
10185
+ const unlinkCount = events.filter((e) => e.type === "unlink").length;
10186
+ const parts = [];
10187
+ if (addCount > 0) parts.push(`\uCD94\uAC00 ${addCount}`);
10188
+ if (changeCount > 0) parts.push(`\uC218\uC815 ${changeCount}`);
10189
+ if (unlinkCount > 0) parts.push(`\uC0AD\uC81C ${unlinkCount}`);
10190
+ info(`[${timestamp}] \uBCC0\uACBD \uAC10\uC9C0: ${parts.join(", ")}`);
10191
+ for (const event of events) {
10192
+ const icon = event.type === "add" ? "\u2795" : event.type === "change" ? "\u270F\uFE0F" : "\u274C";
10193
+ info(` ${icon} ${event.relativePath}`);
10194
+ }
10195
+ if (options.validate !== false) {
10196
+ newline();
10197
+ info("\u{1F50D} \uAC80\uC99D \uC2E4\uD589 \uC911...");
10198
+ const result = await validateSpecs(sddPath, { strict: false });
10199
+ validationCount++;
10200
+ if (result.success) {
10201
+ const data = result.data;
10202
+ const hasErrors = data.results.some((r) => r.errors.length > 0);
10203
+ const hasWarnings = data.results.some((r) => r.warnings.length > 0);
10204
+ if (hasErrors) {
10205
+ errorCount++;
10206
+ error(`\u274C \uAC80\uC99D \uC2E4\uD328: ${data.errorCount}\uAC1C \uC5D0\uB7EC, ${data.warningCount}\uAC1C \uACBD\uACE0`);
10207
+ for (const specResult of data.results) {
10208
+ if (specResult.errors.length > 0) {
10209
+ error(` ${specResult.file}:`);
10210
+ for (const err of specResult.errors) {
10211
+ error(` - ${err}`);
10212
+ }
10213
+ }
10214
+ }
10215
+ } else if (hasWarnings) {
10216
+ if (!options.quiet) {
10217
+ warn(`\u26A0\uFE0F \uAC80\uC99D \uC644\uB8CC: ${data.warningCount}\uAC1C \uACBD\uACE0`);
10218
+ }
10219
+ } else {
10220
+ if (!options.quiet) {
10221
+ success2(`\u2705 \uAC80\uC99D \uD1B5\uACFC (${data.validCount}\uAC1C \uC2A4\uD399)`);
10222
+ }
10223
+ }
10224
+ } else {
10225
+ errorCount++;
10226
+ error(`\u274C \uAC80\uC99D \uC624\uB958: ${result.error.message}`);
10227
+ }
10228
+ newline();
10229
+ }
10230
+ });
10231
+ watcher.on("error", (error2) => {
10232
+ error(`\uAC10\uC2DC \uC624\uB958: ${error2.message}`);
10233
+ });
10234
+ watcher.on("ready", () => {
10235
+ success2("\u2705 \uAC10\uC2DC \uC900\uBE44 \uC644\uB8CC");
10236
+ newline();
10237
+ });
10238
+ const cleanup = async () => {
10239
+ newline();
10240
+ info("Watch \uBAA8\uB4DC \uC885\uB8CC \uC911...");
10241
+ await watcher.stop();
10242
+ newline();
10243
+ info("\u{1F4CA} \uC138\uC158 \uC694\uC57D:");
10244
+ info(` \uAC80\uC99D \uC2E4\uD589: ${validationCount}\uD68C`);
10245
+ info(` \uC5D0\uB7EC \uBC1C\uC0DD: ${errorCount}\uD68C`);
10246
+ process.exit(0);
10247
+ };
10248
+ process.on("SIGINT", cleanup);
10249
+ process.on("SIGTERM", cleanup);
10250
+ watcher.start();
10251
+ await new Promise(() => {
10252
+ });
10253
+ }
10254
+
10255
+ // src/cli/commands/quality.ts
10256
+ import path24 from "path";
10257
+
10258
+ // src/core/quality/analyzer.ts
10259
+ init_types();
10260
+ init_errors();
10261
+ init_fs();
10262
+ import path23 from "path";
10263
+ import { promises as fs13 } from "fs";
10264
+ function getGrade(percentage) {
10265
+ if (percentage >= 90) return "A";
10266
+ if (percentage >= 80) return "B";
10267
+ if (percentage >= 70) return "C";
10268
+ if (percentage >= 60) return "D";
10269
+ return "F";
10270
+ }
10271
+ function scoreRfc2119(content) {
10272
+ const maxScore = 10;
10273
+ const details = [];
10274
+ const suggestions = [];
10275
+ const keywords = ["SHALL", "MUST", "SHOULD", "MAY", "SHALL NOT", "MUST NOT", "SHOULD NOT"];
10276
+ const found = [];
10277
+ for (const kw of keywords) {
10278
+ const regex = new RegExp(`\\b${kw}\\b`, "gi");
10279
+ const matches = content.match(regex);
10280
+ if (matches && matches.length > 0) {
10281
+ found.push(`${kw}: ${matches.length}\uAC1C`);
10282
+ }
10283
+ }
10284
+ let score = 0;
10285
+ if (found.length > 0) {
10286
+ score = Math.min(maxScore, found.length * 2);
10287
+ details.push(`\uBC1C\uACAC\uB41C \uD0A4\uC6CC\uB4DC: ${found.join(", ")}`);
10288
+ } else {
10289
+ details.push("RFC 2119 \uD0A4\uC6CC\uB4DC\uAC00 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC74C");
10290
+ suggestions.push("\uC694\uAD6C\uC0AC\uD56D\uC5D0 SHALL, MUST, SHOULD, MAY \uD0A4\uC6CC\uB4DC\uB97C \uC0AC\uC6A9\uD558\uC138\uC694");
10291
+ }
10292
+ return {
10293
+ name: "RFC 2119 \uD0A4\uC6CC\uB4DC",
10294
+ score,
10295
+ maxScore,
10296
+ percentage: score / maxScore * 100,
10297
+ details,
10298
+ suggestions
10299
+ };
10300
+ }
10301
+ function scoreScenarios(content) {
10302
+ const maxScore = 20;
10303
+ const details = [];
10304
+ const suggestions = [];
10305
+ const givenCount = (content.match(/\*\*GIVEN\*\*|\bGIVEN\b/gi) || []).length;
10306
+ const whenCount = (content.match(/\*\*WHEN\*\*|\bWHEN\b/gi) || []).length;
10307
+ const thenCount = (content.match(/\*\*THEN\*\*|\bTHEN\b/gi) || []).length;
10308
+ const scenarioCount = Math.min(givenCount, whenCount, thenCount);
10309
+ let score = 0;
10310
+ if (scenarioCount > 0) {
10311
+ score = Math.min(maxScore, scenarioCount * 5);
10312
+ details.push(`\uC644\uC804\uD55C \uC2DC\uB098\uB9AC\uC624: ${scenarioCount}\uAC1C`);
10313
+ details.push(`GIVEN: ${givenCount}, WHEN: ${whenCount}, THEN: ${thenCount}`);
10314
+ } else {
10315
+ details.push("GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624\uAC00 \uC5C6\uC74C");
10316
+ suggestions.push("\uCD5C\uC18C 2\uAC1C \uC774\uC0C1\uC758 GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624\uB97C \uC791\uC131\uD558\uC138\uC694");
10317
+ }
10318
+ if (scenarioCount < 2 && scenarioCount > 0) {
10319
+ suggestions.push("\uCD94\uAC00 \uC2DC\uB098\uB9AC\uC624 \uC791\uC131\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4 (\uCD5C\uC18C 2\uAC1C)");
10320
+ }
10321
+ return {
10322
+ name: "GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624",
10323
+ score,
10324
+ maxScore,
10325
+ percentage: score / maxScore * 100,
10326
+ details,
10327
+ suggestions
10328
+ };
10329
+ }
10330
+ function scoreRequirements(content) {
10331
+ const maxScore = 15;
10332
+ const details = [];
10333
+ const suggestions = [];
10334
+ const reqIdPattern = /REQ-\d+|REQ-[A-Z]+-\d+/gi;
10335
+ const reqIds = content.match(reqIdPattern) || [];
10336
+ const hasRequirementsSection = /^##\s*(요구사항|Requirements)/im.test(content);
10337
+ let score = 0;
10338
+ if (hasRequirementsSection) {
10339
+ score += 5;
10340
+ details.push("\uC694\uAD6C\uC0AC\uD56D \uC139\uC158\uC774 \uC874\uC7AC\uD568");
10341
+ } else {
10342
+ suggestions.push("## \uC694\uAD6C\uC0AC\uD56D \uC139\uC158\uC744 \uCD94\uAC00\uD558\uC138\uC694");
10343
+ }
10344
+ if (reqIds.length > 0) {
10345
+ score += Math.min(10, reqIds.length * 2);
10346
+ details.push(`\uC694\uAD6C\uC0AC\uD56D ID: ${reqIds.length}\uAC1C (${[...new Set(reqIds)].slice(0, 3).join(", ")}...)`);
10347
+ } else {
10348
+ suggestions.push("\uC694\uAD6C\uC0AC\uD56D\uC5D0 REQ-01 \uD615\uC2DD\uC758 ID\uB97C \uBD80\uC5EC\uD558\uC138\uC694");
10349
+ }
10350
+ return {
10351
+ name: "\uC694\uAD6C\uC0AC\uD56D \uBA85\uD655\uC131",
10352
+ score,
10353
+ maxScore,
10354
+ percentage: score / maxScore * 100,
10355
+ details,
10356
+ suggestions
10357
+ };
10358
+ }
10359
+ function scoreDependencies(spec) {
10360
+ const maxScore = 10;
10361
+ const details = [];
10362
+ const suggestions = [];
10363
+ let score = 0;
10364
+ if (spec.metadata.depends) {
10365
+ const deps = Array.isArray(spec.metadata.depends) ? spec.metadata.depends : [spec.metadata.depends];
10366
+ if (deps.length > 0 && deps[0] !== null) {
10367
+ score = maxScore;
10368
+ details.push(`\uC758\uC874\uC131: ${deps.join(", ")}`);
10369
+ } else {
10370
+ score = 5;
10371
+ details.push("\uC758\uC874\uC131 \uC5C6\uC74C (\uBA85\uC2DC\uC801 \uC120\uC5B8)");
10372
+ }
10373
+ } else {
10374
+ score = 5;
10375
+ details.push("\uC758\uC874\uC131 \uD544\uB4DC \uC5C6\uC74C (\uC554\uC2DC\uC801 \uC5C6\uC74C)");
10376
+ }
10377
+ return {
10378
+ name: "\uC758\uC874\uC131 \uBA85\uC2DC",
10379
+ score,
10380
+ maxScore,
10381
+ percentage: score / maxScore * 100,
10382
+ details,
10383
+ suggestions
10384
+ };
10385
+ }
10386
+ function scoreStructure(content) {
10387
+ const maxScore = 15;
10388
+ const details = [];
10389
+ const suggestions = [];
10390
+ const requiredSections = [
10391
+ { pattern: /^#\s+.+/m, name: "\uC81C\uBAA9 (H1)" },
10392
+ { pattern: /^##\s*(요구사항|Requirements)/im, name: "\uC694\uAD6C\uC0AC\uD56D \uC139\uC158" },
10393
+ { pattern: /^##\s*(시나리오|Scenario)/im, name: "\uC2DC\uB098\uB9AC\uC624 \uC139\uC158" }
10394
+ ];
10395
+ const optionalSections = [
10396
+ { pattern: /^##\s*(개요|Overview|설명|Description)/im, name: "\uAC1C\uC694/\uC124\uBA85 \uC139\uC158" },
10397
+ { pattern: /^##\s*(제약|Constraints|제한)/im, name: "\uC81C\uC57D\uC0AC\uD56D \uC139\uC158" },
10398
+ { pattern: /^##\s*(비고|Notes|참고)/im, name: "\uBE44\uACE0 \uC139\uC158" }
10399
+ ];
10400
+ let score = 0;
10401
+ const foundRequired = [];
10402
+ const missingRequired = [];
10403
+ for (const section of requiredSections) {
10404
+ if (section.pattern.test(content)) {
10405
+ foundRequired.push(section.name);
10406
+ score += 4;
10407
+ } else {
10408
+ missingRequired.push(section.name);
10409
+ }
10410
+ }
10411
+ for (const section of optionalSections) {
10412
+ if (section.pattern.test(content)) {
10413
+ score += 1;
10414
+ }
10415
+ }
10416
+ score = Math.min(maxScore, score);
10417
+ if (foundRequired.length > 0) {
10418
+ details.push(`\uD544\uC218 \uC139\uC158: ${foundRequired.join(", ")}`);
10419
+ }
10420
+ if (missingRequired.length > 0) {
10421
+ suggestions.push(`\uB204\uB77D\uB41C \uC139\uC158: ${missingRequired.join(", ")}`);
10422
+ }
10423
+ return {
10424
+ name: "\uBB38\uC11C \uAD6C\uC870",
10425
+ score,
10426
+ maxScore,
10427
+ percentage: score / maxScore * 100,
10428
+ details,
10429
+ suggestions
10430
+ };
10431
+ }
10432
+ function scoreConstitution(spec, hasConstitution) {
10433
+ const maxScore = 10;
10434
+ const details = [];
10435
+ const suggestions = [];
10436
+ let score = 0;
10437
+ if (!hasConstitution) {
10438
+ score = maxScore;
10439
+ details.push("Constitution \uBBF8\uC124\uC815 (\uAC80\uC0AC \uC0DD\uB7B5)");
10440
+ } else if (spec.metadata.constitution_version) {
10441
+ score = maxScore;
10442
+ details.push(`Constitution \uBC84\uC804: ${spec.metadata.constitution_version}`);
10443
+ } else {
10444
+ details.push("constitution_version \uD544\uB4DC \uC5C6\uC74C");
10445
+ suggestions.push("frontmatter\uC5D0 constitution_version\uC744 \uCD94\uAC00\uD558\uC138\uC694");
10446
+ }
10447
+ return {
10448
+ name: "Constitution \uC900\uC218",
10449
+ score,
10450
+ maxScore,
10451
+ percentage: score / maxScore * 100,
10452
+ details,
10453
+ suggestions
10454
+ };
10455
+ }
10456
+ function scoreLinks(content) {
10457
+ const maxScore = 10;
10458
+ const details = [];
10459
+ const suggestions = [];
10460
+ const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
10461
+ const links = [...content.matchAll(linkPattern)];
10462
+ let score = 5;
10463
+ if (links.length > 0) {
10464
+ score = Math.min(maxScore, 5 + links.length);
10465
+ details.push(`\uB9C1\uD06C: ${links.length}\uAC1C`);
10466
+ } else {
10467
+ details.push("\uB9C1\uD06C \uC5C6\uC74C");
10468
+ suggestions.push("\uAD00\uB828 \uBB38\uC11C\uB098 \uC678\uBD80 \uCC38\uC870 \uB9C1\uD06C\uB97C \uCD94\uAC00\uD558\uBA74 \uC88B\uC2B5\uB2C8\uB2E4");
10469
+ }
10470
+ return {
10471
+ name: "\uCC38\uC870 \uB9C1\uD06C",
10472
+ score,
10473
+ maxScore,
10474
+ percentage: score / maxScore * 100,
10475
+ details,
10476
+ suggestions
10477
+ };
10478
+ }
10479
+ function scoreMetadata(spec) {
10480
+ const maxScore = 10;
10481
+ const details = [];
10482
+ const suggestions = [];
10483
+ const requiredFields = ["id", "title", "status"];
10484
+ const optionalFields = ["created", "updated", "author", "version"];
10485
+ let score = 0;
10486
+ const missingRequired = [];
10487
+ for (const field of requiredFields) {
10488
+ if (spec.metadata[field]) {
10489
+ score += 2;
10490
+ } else {
10491
+ missingRequired.push(field);
10492
+ }
10493
+ }
10494
+ for (const field of optionalFields) {
10495
+ if (spec.metadata[field]) {
10496
+ score += 1;
10497
+ }
10498
+ }
10499
+ score = Math.min(maxScore, score);
10500
+ const presentFields = Object.keys(spec.metadata).filter(
10501
+ (k) => spec.metadata[k] !== null && spec.metadata[k] !== void 0
10502
+ );
10503
+ details.push(`\uBA54\uD0C0\uB370\uC774\uD130 \uD544\uB4DC: ${presentFields.length}\uAC1C`);
10504
+ if (missingRequired.length > 0) {
10505
+ suggestions.push(`\uD544\uC218 \uD544\uB4DC \uB204\uB77D: ${missingRequired.join(", ")}`);
10506
+ }
10507
+ return {
10508
+ name: "\uBA54\uD0C0\uB370\uC774\uD130 \uC644\uC131\uB3C4",
10509
+ score,
10510
+ maxScore,
10511
+ percentage: score / maxScore * 100,
10512
+ details,
10513
+ suggestions
10514
+ };
10515
+ }
10516
+ async function analyzeSpecQuality(specPath, sddPath) {
10517
+ try {
10518
+ if (!await fileExists(specPath)) {
10519
+ return failure(new ChangeError(`\uC2A4\uD399 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${specPath}`));
10520
+ }
10521
+ const contentResult = await readFile(specPath);
10522
+ if (!contentResult.success) {
10523
+ return failure(new ChangeError("\uC2A4\uD399 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
10524
+ }
10525
+ const content = contentResult.data;
10526
+ const parseResult = parseSpec(content);
10527
+ if (!parseResult.success) {
10528
+ return failure(new ChangeError(`\uC2A4\uD399 \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`));
10529
+ }
10530
+ const spec = parseResult.data;
10531
+ const constitutionPath = path23.join(sddPath, "constitution.md");
10532
+ const hasConstitution = await fileExists(constitutionPath);
10533
+ const items = [
10534
+ scoreRfc2119(content),
10535
+ scoreScenarios(content),
10536
+ scoreRequirements(content),
10537
+ scoreDependencies(spec),
10538
+ scoreStructure(content),
10539
+ scoreConstitution(spec, hasConstitution),
10540
+ scoreLinks(content),
10541
+ scoreMetadata(spec)
10542
+ ];
10543
+ const totalScore = items.reduce((sum, item) => sum + item.score, 0);
10544
+ const maxScore = items.reduce((sum, item) => sum + item.maxScore, 0);
10545
+ const percentage = Math.round(totalScore / maxScore * 100);
10546
+ const grade = getGrade(percentage);
10547
+ const topSuggestions = items.filter((item) => item.suggestions.length > 0).sort((a, b) => a.percentage - b.percentage).slice(0, 3).flatMap((item) => item.suggestions);
10548
+ const specId = spec.metadata.id || path23.basename(path23.dirname(specPath));
10549
+ const summary = `\uC2A4\uD399 '${specId}'\uC758 \uD488\uC9C8 \uC810\uC218: ${totalScore}/${maxScore} (${percentage}%, \uB4F1\uAE09: ${grade})`;
10550
+ return success({
10551
+ specId,
10552
+ specPath,
10553
+ totalScore,
10554
+ maxScore,
10555
+ percentage,
10556
+ grade,
10557
+ items,
10558
+ summary,
10559
+ topSuggestions
10560
+ });
10561
+ } catch (error2) {
10562
+ return failure(
10563
+ new ChangeError(
10564
+ `\uD488\uC9C8 \uBD84\uC11D \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
10565
+ )
10566
+ );
10567
+ }
10568
+ }
10569
+ async function analyzeProjectQuality(sddPath) {
10570
+ try {
10571
+ const specsPath = path23.join(sddPath, "specs");
10572
+ if (!await directoryExists(specsPath)) {
10573
+ return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
10574
+ }
10575
+ const specFiles = [];
10576
+ await findSpecFiles2(specsPath, specFiles);
10577
+ if (specFiles.length === 0) {
10578
+ return failure(new ChangeError("\uC2A4\uD399 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
10579
+ }
10580
+ const specResults = [];
10581
+ for (const specFile of specFiles) {
10582
+ const result = await analyzeSpecQuality(specFile, sddPath);
10583
+ if (result.success) {
10584
+ specResults.push(result.data);
10585
+ }
10586
+ }
10587
+ if (specResults.length === 0) {
10588
+ return failure(new ChangeError("\uBD84\uC11D \uAC00\uB2A5\uD55C \uC2A4\uD399\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
10589
+ }
10590
+ const totalPercentage = specResults.reduce((sum, r) => sum + r.percentage, 0);
10591
+ const averagePercentage = Math.round(totalPercentage / specResults.length);
10592
+ const averageScore = Math.round(
10593
+ specResults.reduce((sum, r) => sum + r.totalScore, 0) / specResults.length
10594
+ );
10595
+ const grade = getGrade(averagePercentage);
10596
+ const summary = `\uD504\uB85C\uC81D\uD2B8 \uD488\uC9C8: \uD3C9\uADE0 ${averagePercentage}% (\uB4F1\uAE09: ${grade}), ${specResults.length}\uAC1C \uC2A4\uD399 \uBD84\uC11D`;
10597
+ return success({
10598
+ averageScore,
10599
+ averagePercentage,
10600
+ grade,
10601
+ totalSpecs: specResults.length,
10602
+ specResults,
10603
+ summary
10604
+ });
10605
+ } catch (error2) {
10606
+ return failure(
10607
+ new ChangeError(
10608
+ `\uD504\uB85C\uC81D\uD2B8 \uD488\uC9C8 \uBD84\uC11D \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
10609
+ )
10610
+ );
10611
+ }
10612
+ }
10613
+ async function findSpecFiles2(dir, files) {
10614
+ const entries = await fs13.readdir(dir, { withFileTypes: true });
10615
+ for (const entry of entries) {
10616
+ const fullPath = path23.join(dir, entry.name);
10617
+ if (entry.isDirectory()) {
10618
+ await findSpecFiles2(fullPath, files);
10619
+ } else if (entry.name === "spec.md") {
10620
+ files.push(fullPath);
10621
+ }
10622
+ }
10623
+ }
10624
+ function formatQualityResult(result) {
10625
+ const lines = [];
10626
+ const gradeIcon = result.grade === "A" ? "\u{1F3C6}" : result.grade === "B" ? "\u2705" : result.grade === "C" ? "\u{1F7E1}" : result.grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
10627
+ lines.push(`\u{1F4CA} \uD488\uC9C8 \uBD84\uC11D: ${result.specId}`);
10628
+ lines.push(` ${gradeIcon} \uB4F1\uAE09: ${result.grade} (${result.percentage}%)`);
10629
+ lines.push(` \u{1F4C8} \uC810\uC218: ${result.totalScore}/${result.maxScore}`);
10630
+ lines.push("");
10631
+ lines.push("\u{1F4CB} \uD56D\uBAA9\uBCC4 \uC810\uC218:");
10632
+ for (const item of result.items) {
10633
+ const icon = item.percentage >= 80 ? "\u2705" : item.percentage >= 60 ? "\u{1F7E1}" : "\u{1F534}";
10634
+ lines.push(` ${icon} ${item.name}: ${item.score}/${item.maxScore} (${Math.round(item.percentage)}%)`);
10635
+ for (const detail of item.details) {
10636
+ lines.push(` \u2514\u2500 ${detail}`);
10637
+ }
10638
+ }
10639
+ lines.push("");
10640
+ if (result.topSuggestions.length > 0) {
10641
+ lines.push("\u{1F4A1} \uAC1C\uC120 \uC81C\uC548:");
10642
+ for (const suggestion of result.topSuggestions) {
10643
+ lines.push(` - ${suggestion}`);
10644
+ }
10645
+ }
10646
+ return lines.join("\n");
10647
+ }
10648
+ function formatProjectQualityResult(result) {
10649
+ const lines = [];
10650
+ const gradeIcon = result.grade === "A" ? "\u{1F3C6}" : result.grade === "B" ? "\u2705" : result.grade === "C" ? "\u{1F7E1}" : result.grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
10651
+ lines.push("\u{1F4CA} \uD504\uB85C\uC81D\uD2B8 \uD488\uC9C8 \uBD84\uC11D");
10652
+ lines.push(` ${gradeIcon} \uD3C9\uADE0 \uB4F1\uAE09: ${result.grade} (${result.averagePercentage}%)`);
10653
+ lines.push(` \u{1F4C8} \uBD84\uC11D\uB41C \uC2A4\uD399: ${result.totalSpecs}\uAC1C`);
10654
+ lines.push("");
10655
+ const gradeCount = { A: 0, B: 0, C: 0, D: 0, F: 0 };
10656
+ for (const spec of result.specResults) {
10657
+ gradeCount[spec.grade]++;
10658
+ }
10659
+ lines.push("\u{1F4C8} \uB4F1\uAE09 \uBD84\uD3EC:");
10660
+ if (gradeCount.A > 0) lines.push(` \u{1F3C6} A: ${gradeCount.A}\uAC1C`);
10661
+ if (gradeCount.B > 0) lines.push(` \u2705 B: ${gradeCount.B}\uAC1C`);
10662
+ if (gradeCount.C > 0) lines.push(` \u{1F7E1} C: ${gradeCount.C}\uAC1C`);
10663
+ if (gradeCount.D > 0) lines.push(` \u{1F7E0} D: ${gradeCount.D}\uAC1C`);
10664
+ if (gradeCount.F > 0) lines.push(` \u{1F534} F: ${gradeCount.F}\uAC1C`);
10665
+ lines.push("");
10666
+ lines.push("\u{1F4CB} \uC2A4\uD399\uBCC4 \uC810\uC218:");
10667
+ const sortedSpecs = [...result.specResults].sort((a, b) => b.percentage - a.percentage);
10668
+ for (const spec of sortedSpecs) {
10669
+ const icon = spec.grade === "A" ? "\u{1F3C6}" : spec.grade === "B" ? "\u2705" : spec.grade === "C" ? "\u{1F7E1}" : spec.grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
10670
+ lines.push(` ${icon} ${spec.specId}: ${spec.percentage}% (${spec.grade})`);
10671
+ }
10672
+ return lines.join("\n");
10673
+ }
10674
+
10675
+ // src/cli/commands/quality.ts
10676
+ init_fs();
10677
+ init_errors();
10678
+ init_types();
10679
+ async function executeQuality(feature, options, projectRoot) {
10680
+ const sddPath = path24.join(projectRoot, ".sdd");
10681
+ const minScore = parseInt(options.minScore || "0", 10);
10682
+ if (options.all || !feature) {
10683
+ const result2 = await analyzeProjectQuality(sddPath);
10684
+ if (!result2.success) {
10685
+ return failure(result2.error);
10686
+ }
10687
+ const formatted2 = options.json ? JSON.stringify(result2.data, null, 2) : formatProjectQualityResult(result2.data);
10688
+ return success({
10689
+ type: "project",
10690
+ data: result2.data,
10691
+ formatted: formatted2,
10692
+ passed: result2.data.averagePercentage >= minScore
10693
+ });
10694
+ }
10695
+ const specPath = path24.join(sddPath, "specs", feature, "spec.md");
10696
+ const result = await analyzeSpecQuality(specPath, sddPath);
10697
+ if (!result.success) {
10698
+ return failure(result.error);
10699
+ }
10700
+ const formatted = options.json ? JSON.stringify(result.data, null, 2) : formatQualityResult(result.data);
10701
+ return success({
10702
+ type: "spec",
10703
+ data: result.data,
10704
+ formatted,
10705
+ passed: result.data.percentage >= minScore
10706
+ });
10707
+ }
10708
+ function registerQualityCommand(program2) {
10709
+ program2.command("quality [feature]").description("\uC2A4\uD399 \uD488\uC9C8\uC744 \uBD84\uC11D\uD558\uACE0 \uC810\uC218\uB97C \uC0B0\uCD9C\uD569\uB2C8\uB2E4").option("-a, --all", "\uC804\uCCB4 \uD504\uB85C\uC81D\uD2B8 \uBD84\uC11D").option("--json", "JSON \uD615\uC2DD \uCD9C\uB825").option("--min-score <score>", "\uCD5C\uC18C \uC810\uC218 \uAE30\uC900 (\uC774\uD558 \uC2DC \uC5D0\uB7EC)", "0").action(async (feature, options) => {
10710
+ try {
10711
+ await runQuality(feature, options);
10712
+ } catch (error2) {
10713
+ error(error2 instanceof Error ? error2.message : String(error2));
10714
+ process.exit(ExitCode.GENERAL_ERROR);
10715
+ }
10716
+ });
10717
+ }
10718
+ async function runQuality(feature, options) {
10719
+ const projectRoot = await findSddRoot();
10720
+ if (!projectRoot) {
10721
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
10722
+ process.exit(ExitCode.GENERAL_ERROR);
10723
+ }
10724
+ const result = await executeQuality(feature, options, projectRoot);
10725
+ if (!result.success) {
10726
+ error(result.error.message);
10727
+ process.exit(ExitCode.GENERAL_ERROR);
10728
+ }
10729
+ console.log(result.data.formatted);
10730
+ if (!result.data.passed) {
10731
+ const minScore = parseInt(options.minScore || "0", 10);
10732
+ newline();
10733
+ error(`\uD488\uC9C8 \uC810\uC218\uAC00 \uCD5C\uC18C \uAE30\uC900(${minScore}%) \uBBF8\uB2EC\uC785\uB2C8\uB2E4.`);
10734
+ process.exit(ExitCode.VALIDATION_FAILED);
10735
+ }
10736
+ }
10737
+
10738
+ // src/cli/commands/report.ts
10739
+ import path26 from "path";
10740
+
10741
+ // src/core/report/reporter.ts
10742
+ init_types();
10743
+ init_errors();
10744
+ import path25 from "path";
10745
+ import fs14 from "fs/promises";
10746
+ init_fs();
10747
+ async function loadSpecList(specsPath) {
10748
+ try {
10749
+ if (!await fileExists(specsPath)) {
10750
+ return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
10751
+ }
10752
+ const result = await readDir(specsPath);
10753
+ if (!result.success) {
10754
+ return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
10755
+ }
10756
+ const specs = [];
10757
+ for (const entry of result.data) {
10758
+ const featurePath = path25.join(specsPath, entry);
10759
+ const stat = await fs14.stat(featurePath);
10760
+ if (stat.isDirectory()) {
10761
+ const specFile = path25.join(featurePath, "spec.md");
10762
+ if (await fileExists(specFile)) {
10763
+ const content = await fs14.readFile(specFile, "utf-8");
10764
+ const metadata = parseSpecMetadata2(content);
10765
+ specs.push({
10766
+ id: entry,
10767
+ title: metadata?.title,
10768
+ phase: metadata?.phase,
10769
+ status: metadata?.status,
10770
+ description: metadata?.description
10771
+ });
10772
+ }
10773
+ }
10774
+ }
10775
+ return success(specs);
10776
+ } catch (error2) {
10777
+ return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
10778
+ }
10779
+ }
10780
+ function parseSpecMetadata2(content) {
10781
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
10782
+ if (!frontmatterMatch) return null;
10783
+ const frontmatter = frontmatterMatch[1];
10784
+ const result = {};
10785
+ const lines = frontmatter.split("\n");
10786
+ for (const line of lines) {
10787
+ const match = line.match(/^(\w+):\s*['"]?([^'"]+)['"]?$/);
10788
+ if (match) {
10789
+ result[match[1]] = match[2].trim();
10790
+ }
10791
+ }
10792
+ return {
10793
+ title: result.title,
10794
+ phase: result.phase,
10795
+ status: result.status,
10796
+ description: result.description
10797
+ };
10798
+ }
10799
+ async function generateReport(sddPath, options) {
10800
+ try {
10801
+ const specsPath = path25.join(sddPath, "specs");
10802
+ const specsResult = await loadSpecList(specsPath);
10803
+ if (!specsResult.success) {
10804
+ return failure(specsResult.error);
10805
+ }
10806
+ const specs = specsResult.data;
10807
+ const reportData = {
10808
+ title: options.title || "SDD \uD504\uB85C\uC81D\uD2B8 \uB9AC\uD3EC\uD2B8",
10809
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
10810
+ projectPath: sddPath,
10811
+ specs,
10812
+ summary: {
10813
+ totalSpecs: specs.length,
10814
+ byPhase: {},
10815
+ byStatus: {}
10816
+ }
10817
+ };
10818
+ for (const spec of specs) {
10819
+ const phase = spec.phase || "unknown";
10820
+ reportData.summary.byPhase[phase] = (reportData.summary.byPhase[phase] || 0) + 1;
10821
+ const status = spec.status || "unknown";
10822
+ reportData.summary.byStatus[status] = (reportData.summary.byStatus[status] || 0) + 1;
10823
+ }
10824
+ if (options.includeQuality !== false) {
10825
+ const qualityResult = await analyzeProjectQuality(sddPath);
10826
+ if (qualityResult.success) {
10827
+ reportData.quality = qualityResult.data;
10828
+ reportData.summary.averageQuality = qualityResult.data.averagePercentage;
10829
+ }
10830
+ }
10831
+ if (options.includeValidation !== false) {
10832
+ const validationResult = await validateSpecs(sddPath, { strict: false });
10833
+ if (validationResult.success) {
10834
+ reportData.validation = validationResult.data;
10835
+ reportData.summary.validationErrors = validationResult.data.errorCount;
10836
+ reportData.summary.validationWarnings = validationResult.data.warningCount;
10837
+ }
10838
+ }
10839
+ let content;
10840
+ switch (options.format) {
10841
+ case "html":
10842
+ content = renderHtmlReport(reportData);
10843
+ break;
10844
+ case "markdown":
10845
+ content = renderMarkdownReport(reportData);
10846
+ break;
10847
+ case "json":
10848
+ content = JSON.stringify(reportData, null, 2);
10849
+ break;
10850
+ default:
10851
+ return failure(new ChangeError(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD615\uC2DD\uC785\uB2C8\uB2E4: ${options.format}`));
10852
+ }
10853
+ if (options.outputPath) {
10854
+ await fs14.mkdir(path25.dirname(options.outputPath), { recursive: true });
10855
+ await fs14.writeFile(options.outputPath, content, "utf-8");
10856
+ }
10857
+ return success({
10858
+ format: options.format,
10859
+ content,
10860
+ outputPath: options.outputPath
10861
+ });
10862
+ } catch (error2) {
10863
+ return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
10864
+ }
10865
+ }
10866
+ function renderHtmlReport(data) {
10867
+ const gradeColor = (grade) => {
10868
+ switch (grade) {
10869
+ case "A":
10870
+ return "#22c55e";
10871
+ case "B":
10872
+ return "#84cc16";
10873
+ case "C":
10874
+ return "#eab308";
10875
+ case "D":
10876
+ return "#f97316";
10877
+ case "F":
10878
+ return "#ef4444";
10879
+ default:
10880
+ return "#6b7280";
10881
+ }
10882
+ };
10883
+ const statusBadge = (status) => {
10884
+ const colors = {
10885
+ draft: "#6b7280",
10886
+ review: "#3b82f6",
10887
+ approved: "#22c55e",
10888
+ implemented: "#8b5cf6",
10889
+ deprecated: "#ef4444"
10890
+ };
10891
+ const color = colors[status] || "#6b7280";
10892
+ return `<span style="background:${color};color:white;padding:2px 8px;border-radius:4px;font-size:12px;">${status}</span>`;
10893
+ };
10894
+ const specRows = data.specs.map((spec) => `
10895
+ <tr>
10896
+ <td><strong>${spec.id}</strong></td>
10897
+ <td>${spec.title || "-"}</td>
10898
+ <td>${spec.phase || "-"}</td>
10899
+ <td>${statusBadge(spec.status || "unknown")}</td>
10900
+ <td>${spec.description || "-"}</td>
10901
+ </tr>
10902
+ `).join("");
10903
+ const qualityRows = data.quality?.specResults?.map((q) => `
10904
+ <tr>
10905
+ <td>${q.specId}</td>
10906
+ <td>${q.percentage}%</td>
10907
+ <td style="color:${gradeColor(q.grade)};font-weight:bold;">${q.grade}</td>
10908
+ <td>${q.totalScore}/${q.maxScore}</td>
10909
+ </tr>
10910
+ `).join("") || "";
10911
+ const validationRows = data.validation?.files?.map((v) => `
10912
+ <tr>
10913
+ <td>${v.file}</td>
10914
+ <td style="color:${v.errors.length > 0 ? "#ef4444" : "#22c55e"};">${v.errors.length > 0 ? "\u274C \uC2E4\uD328" : "\u2705 \uD1B5\uACFC"}</td>
10915
+ <td>${v.errors.length}</td>
10916
+ <td>${v.warnings.length}</td>
10917
+ </tr>
10918
+ `).join("") || "";
10919
+ return `<!DOCTYPE html>
10920
+ <html lang="ko">
10921
+ <head>
10922
+ <meta charset="UTF-8">
10923
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
10924
+ <title>${data.title}</title>
10925
+ <style>
10926
+ * { box-sizing: border-box; margin: 0; padding: 0; }
10927
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1f2937; background: #f9fafb; padding: 2rem; }
10928
+ .container { max-width: 1200px; margin: 0 auto; }
10929
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #111827; }
10930
+ h2 { font-size: 1.5rem; margin: 2rem 0 1rem; color: #374151; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.5rem; }
10931
+ .meta { color: #6b7280; margin-bottom: 2rem; }
10932
+ .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
10933
+ .card { background: white; border-radius: 8px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
10934
+ .card-title { font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem; }
10935
+ .card-value { font-size: 2rem; font-weight: bold; color: #111827; }
10936
+ .card-value.success { color: #22c55e; }
10937
+ .card-value.warning { color: #eab308; }
10938
+ .card-value.error { color: #ef4444; }
10939
+ table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem; }
10940
+ th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
10941
+ th { background: #f3f4f6; font-weight: 600; color: #374151; }
10942
+ tr:hover { background: #f9fafb; }
10943
+ .phase-chart { display: flex; gap: 0.5rem; flex-wrap: wrap; }
10944
+ .phase-item { background: #e0e7ff; color: #3730a3; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.875rem; }
10945
+ </style>
10946
+ </head>
10947
+ <body>
10948
+ <div class="container">
10949
+ <h1>${data.title}</h1>
10950
+ <p class="meta">\uC0DD\uC131: ${new Date(data.generatedAt).toLocaleString("ko-KR")} | \uACBD\uB85C: ${data.projectPath}</p>
10951
+
10952
+ <div class="summary">
10953
+ <div class="card">
10954
+ <div class="card-title">\uCD1D \uC2A4\uD399 \uC218</div>
10955
+ <div class="card-value">${data.summary.totalSpecs}</div>
10956
+ </div>
10957
+ ${data.summary.averageQuality !== void 0 ? `
10958
+ <div class="card">
10959
+ <div class="card-title">\uD3C9\uADE0 \uD488\uC9C8</div>
10960
+ <div class="card-value ${data.summary.averageQuality >= 80 ? "success" : data.summary.averageQuality >= 60 ? "warning" : "error"}">${data.summary.averageQuality.toFixed(1)}%</div>
10961
+ </div>` : ""}
10962
+ ${data.summary.validationErrors !== void 0 ? `
10963
+ <div class="card">
10964
+ <div class="card-title">\uAC80\uC99D \uC5D0\uB7EC</div>
10965
+ <div class="card-value ${data.summary.validationErrors === 0 ? "success" : "error"}">${data.summary.validationErrors}</div>
10966
+ </div>` : ""}
10967
+ ${data.summary.validationWarnings !== void 0 ? `
10968
+ <div class="card">
10969
+ <div class="card-title">\uAC80\uC99D \uACBD\uACE0</div>
10970
+ <div class="card-value ${data.summary.validationWarnings === 0 ? "success" : "warning"}">${data.summary.validationWarnings}</div>
10971
+ </div>` : ""}
10972
+ </div>
10973
+
10974
+ <h2>Phase\uBCC4 \uBD84\uD3EC</h2>
10975
+ <div class="phase-chart">
10976
+ ${Object.entries(data.summary.byPhase).map(([phase, count]) => `
10977
+ <span class="phase-item">${phase}: ${count}</span>
10978
+ `).join("")}
10979
+ </div>
10980
+
10981
+ <h2>\uC2A4\uD399 \uBAA9\uB85D</h2>
10982
+ <table>
10983
+ <thead>
10984
+ <tr>
10985
+ <th>ID</th>
10986
+ <th>\uC81C\uBAA9</th>
10987
+ <th>Phase</th>
10988
+ <th>\uC0C1\uD0DC</th>
10989
+ <th>\uC124\uBA85</th>
10990
+ </tr>
10991
+ </thead>
10992
+ <tbody>
10993
+ ${specRows}
10994
+ </tbody>
10995
+ </table>
10996
+
10997
+ ${data.quality ? `
10998
+ <h2>\uD488\uC9C8 \uBD84\uC11D</h2>
10999
+ <table>
11000
+ <thead>
11001
+ <tr>
11002
+ <th>\uC2A4\uD399 ID</th>
11003
+ <th>\uC810\uC218</th>
11004
+ <th>\uB4F1\uAE09</th>
11005
+ <th>\uC0C1\uC138</th>
11006
+ </tr>
11007
+ </thead>
11008
+ <tbody>
11009
+ ${qualityRows}
11010
+ </tbody>
11011
+ </table>` : ""}
11012
+
11013
+ ${data.validation ? `
11014
+ <h2>\uAC80\uC99D \uACB0\uACFC</h2>
11015
+ <table>
11016
+ <thead>
11017
+ <tr>
11018
+ <th>\uD30C\uC77C</th>
11019
+ <th>\uC0C1\uD0DC</th>
11020
+ <th>\uC5D0\uB7EC</th>
11021
+ <th>\uACBD\uACE0</th>
11022
+ </tr>
11023
+ </thead>
11024
+ <tbody>
11025
+ ${validationRows}
11026
+ </tbody>
11027
+ </table>` : ""}
11028
+
11029
+ <footer style="margin-top:3rem;padding-top:1rem;border-top:1px solid #e5e7eb;color:#6b7280;font-size:0.875rem;">
11030
+ Generated by SDD CLI v0.5.0
11031
+ </footer>
11032
+ </div>
11033
+ </body>
11034
+ </html>`;
11035
+ }
11036
+ function renderMarkdownReport(data) {
11037
+ const lines = [];
11038
+ lines.push(`# ${data.title}`);
11039
+ lines.push("");
11040
+ lines.push(`> \uC0DD\uC131: ${new Date(data.generatedAt).toLocaleString("ko-KR")}`);
11041
+ lines.push(`> \uACBD\uB85C: ${data.projectPath}`);
11042
+ lines.push("");
11043
+ lines.push("## \uC694\uC57D");
11044
+ lines.push("");
11045
+ lines.push(`| \uD56D\uBAA9 | \uAC12 |`);
11046
+ lines.push(`|------|-----|`);
11047
+ lines.push(`| \uCD1D \uC2A4\uD399 \uC218 | ${data.summary.totalSpecs} |`);
11048
+ if (data.summary.averageQuality !== void 0) {
11049
+ lines.push(`| \uD3C9\uADE0 \uD488\uC9C8 | ${data.summary.averageQuality.toFixed(1)}% |`);
11050
+ }
11051
+ if (data.summary.validationErrors !== void 0) {
11052
+ lines.push(`| \uAC80\uC99D \uC5D0\uB7EC | ${data.summary.validationErrors} |`);
11053
+ }
11054
+ if (data.summary.validationWarnings !== void 0) {
11055
+ lines.push(`| \uAC80\uC99D \uACBD\uACE0 | ${data.summary.validationWarnings} |`);
11056
+ }
11057
+ lines.push("");
11058
+ lines.push("## Phase\uBCC4 \uBD84\uD3EC");
11059
+ lines.push("");
11060
+ for (const [phase, count] of Object.entries(data.summary.byPhase)) {
11061
+ lines.push(`- **${phase}**: ${count}\uAC1C`);
11062
+ }
11063
+ lines.push("");
11064
+ lines.push("## \uC0C1\uD0DC\uBCC4 \uBD84\uD3EC");
11065
+ lines.push("");
11066
+ for (const [status, count] of Object.entries(data.summary.byStatus)) {
11067
+ lines.push(`- **${status}**: ${count}\uAC1C`);
11068
+ }
11069
+ lines.push("");
11070
+ lines.push("## \uC2A4\uD399 \uBAA9\uB85D");
11071
+ lines.push("");
11072
+ lines.push("| ID | \uC81C\uBAA9 | Phase | \uC0C1\uD0DC |");
11073
+ lines.push("|----|------|-------|------|");
11074
+ for (const spec of data.specs) {
11075
+ lines.push(`| ${spec.id} | ${spec.title || "-"} | ${spec.phase || "-"} | ${spec.status || "-"} |`);
11076
+ }
11077
+ lines.push("");
11078
+ if (data.quality) {
11079
+ lines.push("## \uD488\uC9C8 \uBD84\uC11D");
11080
+ lines.push("");
11081
+ lines.push(`\uD3C9\uADE0 \uC810\uC218: **${data.quality.averagePercentage.toFixed(1)}%** (${data.quality.grade})`);
11082
+ lines.push("");
11083
+ lines.push("| \uC2A4\uD399 ID | \uC810\uC218 | \uB4F1\uAE09 |");
11084
+ lines.push("|---------|------|------|");
11085
+ for (const q of data.quality.specResults || []) {
11086
+ lines.push(`| ${q.specId} | ${q.percentage}% | ${q.grade} |`);
11087
+ }
11088
+ lines.push("");
11089
+ }
11090
+ if (data.validation) {
11091
+ lines.push("## \uAC80\uC99D \uACB0\uACFC");
11092
+ lines.push("");
11093
+ lines.push(`- \uD1B5\uACFC: ${data.validation.passed}\uAC1C`);
11094
+ lines.push(`- \uC2E4\uD328: ${data.validation.failed}\uAC1C`);
11095
+ lines.push(`- \uACBD\uACE0: ${data.validation.warnings}\uAC1C`);
11096
+ lines.push("");
11097
+ if (data.validation.failed > 0 || data.validation.warnings > 0) {
11098
+ lines.push("### \uC0C1\uC138 \uACB0\uACFC");
11099
+ lines.push("");
11100
+ for (const v of data.validation.files || []) {
11101
+ if (v.errors.length > 0 || v.warnings.length > 0) {
11102
+ lines.push(`#### ${v.file}`);
11103
+ for (const e of v.errors) {
11104
+ lines.push(`- ${e}`);
11105
+ }
11106
+ for (const w of v.warnings) {
11107
+ lines.push(`- ${w}`);
11108
+ }
11109
+ lines.push("");
11110
+ }
11111
+ }
11112
+ }
11113
+ }
11114
+ lines.push("---");
11115
+ lines.push("*Generated by SDD CLI v0.5.0*");
11116
+ return lines.join("\n");
11117
+ }
11118
+
11119
+ // src/cli/commands/report.ts
11120
+ init_fs();
11121
+ init_errors();
11122
+ init_types();
11123
+ function isValidReportFormat(format) {
11124
+ return ["html", "markdown", "json"].includes(format);
11125
+ }
11126
+ function resolveOutputPath(format, output, projectRoot) {
11127
+ if (!output) {
11128
+ const ext = format === "markdown" ? "md" : format;
11129
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
11130
+ return path26.join(projectRoot, `sdd-report-${timestamp}.${ext}`);
11131
+ }
11132
+ return path26.isAbsolute(output) ? output : path26.join(projectRoot, output);
11133
+ }
11134
+ async function executeReport(options, projectRoot) {
11135
+ const sddPath = path26.join(projectRoot, ".sdd");
11136
+ const format = options.format || "html";
11137
+ if (!isValidReportFormat(format)) {
11138
+ return failure(new Error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD615\uC2DD\uC785\uB2C8\uB2E4: ${format}. \uC9C0\uC6D0 \uD615\uC2DD: html, markdown, json`));
11139
+ }
11140
+ const outputPath = resolveOutputPath(format, options.output, projectRoot);
11141
+ const result = await generateReport(sddPath, {
11142
+ format,
11143
+ outputPath,
11144
+ title: options.title,
11145
+ includeQuality: options.quality !== false,
11146
+ includeValidation: options.validation !== false
11147
+ });
11148
+ if (!result.success) {
11149
+ return failure(result.error);
11150
+ }
11151
+ return success({
11152
+ format,
11153
+ outputPath: result.data.outputPath || outputPath,
11154
+ content: result.data.content
11155
+ });
11156
+ }
11157
+ function registerReportCommand(program2) {
11158
+ program2.command("report").description("\uC2A4\uD399 \uB9AC\uD3EC\uD2B8\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4").option("-f, --format <format>", "\uCD9C\uB825 \uD615\uC2DD (html, markdown, json)", "html").option("-o, --output <path>", "\uCD9C\uB825 \uD30C\uC77C \uACBD\uB85C").option("--title <title>", "\uB9AC\uD3EC\uD2B8 \uC81C\uBAA9").option("--no-quality", "\uD488\uC9C8 \uBD84\uC11D \uC81C\uC678").option("--no-validation", "\uAC80\uC99D \uACB0\uACFC \uC81C\uC678").action(async (options) => {
11159
+ try {
11160
+ await runReport(options);
11161
+ } catch (error2) {
11162
+ error(error2 instanceof Error ? error2.message : String(error2));
11163
+ process.exit(ExitCode.GENERAL_ERROR);
11164
+ }
11165
+ });
11166
+ }
11167
+ async function runReport(options) {
11168
+ const projectRoot = await findSddRoot();
11169
+ if (!projectRoot) {
11170
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
11171
+ process.exit(ExitCode.GENERAL_ERROR);
11172
+ }
11173
+ const format = options.format || "html";
11174
+ info("\u{1F4CA} \uB9AC\uD3EC\uD2B8 \uC0DD\uC131 \uC911...");
11175
+ info(` \uD615\uC2DD: ${format}`);
11176
+ info(` \uD488\uC9C8 \uBD84\uC11D: ${options.quality !== false ? "\uD3EC\uD568" : "\uC81C\uC678"}`);
11177
+ info(` \uAC80\uC99D \uACB0\uACFC: ${options.validation !== false ? "\uD3EC\uD568" : "\uC81C\uC678"}`);
11178
+ newline();
11179
+ const result = await executeReport(options, projectRoot);
11180
+ if (!result.success) {
11181
+ error(result.error.message);
11182
+ process.exit(ExitCode.GENERAL_ERROR);
11183
+ }
11184
+ success2(`\u2705 \uB9AC\uD3EC\uD2B8\uAC00 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
11185
+ info(` \uACBD\uB85C: ${result.data.outputPath}`);
11186
+ if (!options.output && format === "json") {
11187
+ newline();
11188
+ console.log(result.data.content);
11189
+ }
11190
+ }
11191
+
9089
11192
  // src/cli/index.ts
9090
11193
  var require2 = createRequire(import.meta.url);
9091
11194
  var pkg = require2("../../package.json");
@@ -9104,6 +11207,9 @@ registerStartCommand(program);
9104
11207
  registerMigrateCommand(program);
9105
11208
  registerCicdCommand(program);
9106
11209
  registerTransitionCommand(program);
11210
+ registerWatchCommand(program);
11211
+ registerQualityCommand(program);
11212
+ registerReportCommand(program);
9107
11213
  function run() {
9108
11214
  program.parse();
9109
11215
  }