sdd-tool 0.5.1 → 0.6.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, path27) {
132
- super(code, formatMessage(code, path27), ExitCode.FILE_SYSTEM_ERROR);
131
+ constructor(code, path29) {
132
+ super(code, formatMessage(code, path29), ExitCode.FILE_SYSTEM_ERROR);
133
133
  this.name = "FileSystemError";
134
- this.path = path27;
134
+ this.path = path29;
135
135
  }
136
136
  };
137
137
  ValidationError = class extends SddError {
@@ -2497,6 +2497,263 @@ sdd transition change-to-new <change-id>
2497
2497
  \`\`\`bash
2498
2498
  sdd transition guide
2499
2499
  \`\`\`
2500
+ `
2501
+ },
2502
+ // CLI 래퍼 커맨드들
2503
+ {
2504
+ name: "sdd.search",
2505
+ content: `---
2506
+ description: SDD \uC2A4\uD399\uC744 \uAC80\uC0C9\uD569\uB2C8\uB2E4
2507
+ allowed-tools: Bash, Read
2508
+ argument-hint: [query] [--status draft] [--phase phase1]
2509
+ ---
2510
+
2511
+ sdd search \uBA85\uB839\uC5B4\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
2512
+
2513
+ \`\`\`bash
2514
+ sdd search $ARGUMENTS
2515
+ \`\`\`
2516
+
2517
+ ## \uC635\uC158
2518
+
2519
+ - \`--status <status>\`: \uC0C1\uD0DC \uD544\uD130 (draft, review, approved)
2520
+ - \`--phase <phase>\`: Phase \uD544\uD130
2521
+ - \`--author <name>\`: \uC791\uC131\uC790 \uD544\uD130
2522
+ - \`--tags <tags>\`: \uD0DC\uADF8 \uD544\uD130 (\uCF64\uB9C8 \uAD6C\uBD84)
2523
+ - \`--depends-on <spec>\`: \uC758\uC874\uC131 \uD544\uD130
2524
+ - \`--limit <n>\`: \uACB0\uACFC \uC81C\uD55C
2525
+ - \`--sort-by <field>\`: \uC815\uB82C \uAE30\uC900 (relevance, created, updated, title, status)
2526
+ - \`-r, --regex\`: \uC815\uADDC\uC2DD \uAC80\uC0C9
2527
+ - \`-c, --case-sensitive\`: \uB300\uC18C\uBB38\uC790 \uAD6C\uBD84
2528
+ - \`--json\`: JSON \uCD9C\uB825
2529
+
2530
+ \uAC80\uC0C9 \uACB0\uACFC\uB97C \uBD84\uC11D\uD558\uACE0 \uAD00\uB828 \uC2A4\uD399 \uC815\uBCF4\uB97C \uC694\uC57D\uD574\uC8FC\uC138\uC694.
2531
+ `
2532
+ },
2533
+ {
2534
+ name: "sdd.quality",
2535
+ content: `---
2536
+ description: SDD \uC2A4\uD399 \uD488\uC9C8\uC744 \uBD84\uC11D\uD569\uB2C8\uB2E4
2537
+ allowed-tools: Bash, Read
2538
+ argument-hint: [specId] [--all] [--json]
2539
+ ---
2540
+
2541
+ sdd quality \uBA85\uB839\uC5B4\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
2542
+
2543
+ \`\`\`bash
2544
+ sdd quality $ARGUMENTS
2545
+ \`\`\`
2546
+
2547
+ ## \uC635\uC158
2548
+
2549
+ - \`--all\`: \uBAA8\uB4E0 \uC2A4\uD399 \uBD84\uC11D
2550
+ - \`--json\`: JSON \uD615\uC2DD \uCD9C\uB825
2551
+ - \`--min-score <n>\`: \uCD5C\uC18C \uC810\uC218 \uAE30\uC900
2552
+
2553
+ ## \uB4F1\uAE09 \uAE30\uC900
2554
+
2555
+ | \uB4F1\uAE09 | \uC810\uC218 | \uC124\uBA85 |
2556
+ |------|------|------|
2557
+ | A | 90-100 | \uC6B0\uC218 |
2558
+ | B | 80-89 | \uC591\uD638 |
2559
+ | C | 70-79 | \uBCF4\uD1B5 |
2560
+ | D | 60-69 | \uBBF8\uD761 |
2561
+ | F | 0-59 | \uBD80\uC871 |
2562
+
2563
+ \uBD84\uC11D \uACB0\uACFC\uB97C \uBC14\uD0D5\uC73C\uB85C \uD488\uC9C8 \uAC1C\uC120 \uBC29\uC548\uC744 \uC81C\uC548\uD574\uC8FC\uC138\uC694.
2564
+ `
2565
+ },
2566
+ {
2567
+ name: "sdd.report",
2568
+ content: `---
2569
+ description: SDD \uD504\uB85C\uC81D\uD2B8 \uB9AC\uD3EC\uD2B8\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4
2570
+ allowed-tools: Bash, Read, Write
2571
+ argument-hint: [--format html] [-o report.html]
2572
+ ---
2573
+
2574
+ sdd report \uBA85\uB839\uC5B4\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
2575
+
2576
+ \`\`\`bash
2577
+ sdd report $ARGUMENTS
2578
+ \`\`\`
2579
+
2580
+ ## \uC635\uC158
2581
+
2582
+ - \`--format <type>\`: \uCD9C\uB825 \uD615\uC2DD (html, markdown, json)
2583
+ - \`-o, --output <path>\`: \uD30C\uC77C\uB85C \uC800\uC7A5
2584
+ - \`--title <title>\`: \uB9AC\uD3EC\uD2B8 \uC81C\uBAA9
2585
+ - \`--no-quality\`: \uD488\uC9C8 \uBD84\uC11D \uC81C\uC678
2586
+ - \`--no-validation\`: \uAC80\uC99D \uACB0\uACFC \uC81C\uC678
2587
+
2588
+ \uB9AC\uD3EC\uD2B8 \uB0B4\uC6A9\uC744 \uC694\uC57D\uD558\uACE0 \uC8FC\uC694 \uC9C0\uD45C\uB97C \uC124\uBA85\uD574\uC8FC\uC138\uC694.
2589
+ `
2590
+ },
2591
+ {
2592
+ name: "sdd.impact",
2593
+ content: `---
2594
+ description: SDD \uC2A4\uD399 \uBCC0\uACBD\uC758 \uC601\uD5A5\uB3C4\uB97C \uBD84\uC11D\uD569\uB2C8\uB2E4
2595
+ allowed-tools: Bash, Read
2596
+ argument-hint: <specId> [--graph] [--code] [--json]
2597
+ ---
2598
+
2599
+ sdd impact \uBA85\uB839\uC5B4\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
2600
+
2601
+ \`\`\`bash
2602
+ sdd impact $ARGUMENTS
2603
+ \`\`\`
2604
+
2605
+ ## \uC635\uC158
2606
+
2607
+ - \`--graph\`: Mermaid \uC758\uC874\uC131 \uADF8\uB798\uD504 \uCD9C\uB825
2608
+ - \`--code\`: \uCF54\uB4DC \uC601\uD5A5\uB3C4 \uBD84\uC11D
2609
+ - \`--json\`: JSON \uD615\uC2DD \uCD9C\uB825
2610
+
2611
+ ## \uC11C\uBE0C\uCEE4\uB9E8\uB4DC
2612
+
2613
+ - \`report\`: \uC804\uCCB4 \uD504\uB85C\uC81D\uD2B8 \uC601\uD5A5\uB3C4 \uB9AC\uD3EC\uD2B8
2614
+ - \`change <id>\`: \uBCC0\uACBD \uC81C\uC548\uC758 \uC601\uD5A5\uB3C4
2615
+ - \`simulate --remove <spec>\`: What-if \uC2DC\uBBAC\uB808\uC774\uC158
2616
+
2617
+ \uC601\uD5A5\uBC1B\uB294 \uC2A4\uD399\uACFC \uCF54\uB4DC\uB97C \uBD84\uC11D\uD558\uACE0 \uBCC0\uACBD \uC804\uB7B5\uC744 \uC81C\uC548\uD574\uC8FC\uC138\uC694.
2618
+ `
2619
+ },
2620
+ {
2621
+ name: "sdd.list",
2622
+ content: `---
2623
+ description: SDD \uD56D\uBAA9 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4
2624
+ allowed-tools: Bash, Read
2625
+ argument-hint: [features|changes|specs|templates] [--status draft]
2626
+ ---
2627
+
2628
+ sdd list \uBA85\uB839\uC5B4\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
2629
+
2630
+ \`\`\`bash
2631
+ sdd list $ARGUMENTS
2632
+ \`\`\`
2633
+
2634
+ ## \uC11C\uBE0C\uCEE4\uB9E8\uB4DC
2635
+
2636
+ - (\uC5C6\uC74C): \uD504\uB85C\uC81D\uD2B8 \uC694\uC57D
2637
+ - \`features\` (f): \uAE30\uB2A5 \uBAA9\uB85D
2638
+ - \`changes\` (c): \uBCC0\uACBD \uBAA9\uB85D
2639
+ - \`specs\` (s): \uC2A4\uD399 \uD30C\uC77C \uBAA9\uB85D
2640
+ - \`templates\` (t): \uD15C\uD50C\uB9BF \uBAA9\uB85D
2641
+
2642
+ ## \uC635\uC158
2643
+
2644
+ - \`--status <status>\`: \uC0C1\uD0DC\uBCC4 \uD544\uD130 (features)
2645
+ - \`--pending\`: \uB300\uAE30 \uC911\uB9CC (changes)
2646
+ - \`--archived\`: \uC544\uCE74\uC774\uBE0C\uB9CC (changes)
2647
+
2648
+ \uBAA9\uB85D\uC744 \uBD84\uC11D\uD558\uACE0 \uC694\uC57D\uD574\uC8FC\uC138\uC694.
2649
+ `
2650
+ },
2651
+ {
2652
+ name: "sdd.migrate",
2653
+ content: `---
2654
+ description: \uAE30\uC874 \uD504\uB85C\uC81D\uD2B8\uB97C SDD\uB85C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4
2655
+ allowed-tools: Bash, Read
2656
+ argument-hint: [detect|openspec|speckit|docs] [--dry-run]
2657
+ ---
2658
+
2659
+ sdd migrate \uBA85\uB839\uC5B4\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
2660
+
2661
+ \`\`\`bash
2662
+ sdd migrate $ARGUMENTS
2663
+ \`\`\`
2664
+
2665
+ ## \uC11C\uBE0C\uCEE4\uB9E8\uB4DC
2666
+
2667
+ - \`detect\`: \uAE30\uC874 \uB3C4\uAD6C \uAC10\uC9C0
2668
+ - \`openspec\`: OpenSpec\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158
2669
+ - \`speckit\`: SpecKit\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158
2670
+ - \`docs\`: \uB9C8\uD06C\uB2E4\uC6B4 \uBB38\uC11C \uBCC0\uD658
2671
+
2672
+ ## \uC635\uC158
2673
+
2674
+ - \`--dry-run\`: \uBBF8\uB9AC\uBCF4\uAE30
2675
+ - \`--overwrite\`: \uAE30\uC874 \uC2A4\uD399 \uB36E\uC5B4\uC4F0\uAE30
2676
+ - \`--source <path>\`: \uC18C\uC2A4 \uB514\uB809\uD1A0\uB9AC
2677
+
2678
+ \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uACB0\uACFC\uB97C \uD655\uC778\uD558\uACE0 \uD544\uC694\uD55C \uD6C4\uC18D \uC791\uC5C5\uC744 \uC548\uB0B4\uD574\uC8FC\uC138\uC694.
2679
+ `
2680
+ },
2681
+ {
2682
+ name: "sdd.cicd",
2683
+ content: `---
2684
+ description: CI/CD \uD30C\uC774\uD504\uB77C\uC778\uC744 \uC124\uC815\uD569\uB2C8\uB2E4
2685
+ allowed-tools: Bash, Read, Write
2686
+ argument-hint: [setup|hooks|check] [--platform github]
2687
+ ---
2688
+
2689
+ sdd cicd \uBA85\uB839\uC5B4\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
2690
+
2691
+ \`\`\`bash
2692
+ sdd cicd $ARGUMENTS
2693
+ \`\`\`
2694
+
2695
+ ## \uC11C\uBE0C\uCEE4\uB9E8\uB4DC
2696
+
2697
+ - \`setup\`: CI/CD \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC0DD\uC131
2698
+ - \`hooks\`: Git hooks \uC124\uC815
2699
+ - \`check\`: CI \uAC80\uC99D \uC2E4\uD589
2700
+
2701
+ ## \uC635\uC158
2702
+
2703
+ - \`--platform <type>\`: github, gitlab, all
2704
+
2705
+ \uC0DD\uC131\uB41C CI/CD \uC124\uC815\uC744 \uD655\uC778\uD558\uACE0 \uCD94\uAC00 \uC124\uC815\uC774 \uD544\uC694\uD558\uBA74 \uC548\uB0B4\uD574\uC8FC\uC138\uC694.
2706
+ `
2707
+ },
2708
+ {
2709
+ name: "sdd.watch",
2710
+ content: `---
2711
+ description: \uC2A4\uD399 \uD30C\uC77C \uBCC0\uACBD\uC744 \uAC10\uC2DC\uD569\uB2C8\uB2E4
2712
+ allowed-tools: Bash
2713
+ argument-hint: [--validate] [--path .sdd/specs]
2714
+ ---
2715
+
2716
+ sdd watch \uBA85\uB839\uC5B4\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
2717
+
2718
+ \`\`\`bash
2719
+ sdd watch $ARGUMENTS
2720
+ \`\`\`
2721
+
2722
+ ## \uC635\uC158
2723
+
2724
+ - \`--path <dir>\`: \uAC10\uC2DC\uD560 \uB514\uB809\uD1A0\uB9AC
2725
+ - \`--validate\`: \uBCC0\uACBD \uC2DC \uC790\uB3D9 \uAC80\uC99D
2726
+ - \`--quality\`: \uBCC0\uACBD \uC2DC \uD488\uC9C8 \uAC80\uC0AC
2727
+
2728
+ Ctrl+C\uB85C \uC885\uB8CC\uD569\uB2C8\uB2E4.
2729
+ `
2730
+ },
2731
+ {
2732
+ name: "sdd.prompt",
2733
+ content: `---
2734
+ description: SDD \uC791\uC5C5\uC6A9 AI \uD504\uB86C\uD504\uD2B8\uB97C \uCD9C\uB825\uD569\uB2C8\uB2E4
2735
+ allowed-tools: Bash, Read
2736
+ argument-hint: [change|apply|archive|validate] [--list]
2737
+ ---
2738
+
2739
+ sdd prompt \uBA85\uB839\uC5B4\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
2740
+
2741
+ \`\`\`bash
2742
+ sdd prompt $ARGUMENTS
2743
+ \`\`\`
2744
+
2745
+ ## \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uD504\uB86C\uD504\uD2B8
2746
+
2747
+ - \`change\`: \uBCC0\uACBD \uC81C\uC548 \uC791\uC131
2748
+ - \`apply\`: \uBCC0\uACBD \uC801\uC6A9
2749
+ - \`archive\`: \uBCC0\uACBD \uC544\uCE74\uC774\uBE0C
2750
+ - \`validate\`: \uC2A4\uD399 \uAC80\uC99D
2751
+
2752
+ ## \uC635\uC158
2753
+
2754
+ - \`--list\`: \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uD504\uB86C\uD504\uD2B8 \uBAA9\uB85D
2755
+
2756
+ \uD504\uB86C\uD504\uD2B8 \uB0B4\uC6A9\uC744 \uCD9C\uB825\uD558\uACE0 \uC0AC\uC6A9 \uBC29\uBC95\uC744 \uC548\uB0B4\uD574\uC8FC\uC138\uC694.
2500
2757
  `
2501
2758
  }
2502
2759
  ];
@@ -3677,9 +3934,9 @@ async function validateSpecs(targetPath, options = {}) {
3677
3934
  async function findSpecFiles(dirPath) {
3678
3935
  const files = [];
3679
3936
  async function scanDir(dir) {
3680
- const { promises: fs15 } = await import("fs");
3937
+ const { promises: fs16 } = await import("fs");
3681
3938
  try {
3682
- const entries = await fs15.readdir(dir, { withFileTypes: true });
3939
+ const entries = await fs16.readdir(dir, { withFileTypes: true });
3683
3940
  for (const entry of entries) {
3684
3941
  const fullPath = path3.join(dir, entry.name);
3685
3942
  if (entry.isDirectory()) {
@@ -5802,19 +6059,19 @@ function detectCircularDependencies(graph) {
5802
6059
  const cycles = [];
5803
6060
  const visited = /* @__PURE__ */ new Set();
5804
6061
  const recStack = /* @__PURE__ */ new Set();
5805
- function dfs(nodeId, path27) {
6062
+ function dfs(nodeId, path29) {
5806
6063
  visited.add(nodeId);
5807
6064
  recStack.add(nodeId);
5808
6065
  const node = graph.nodes.get(nodeId);
5809
6066
  if (!node) return false;
5810
6067
  for (const depId of node.dependsOn) {
5811
6068
  if (!visited.has(depId)) {
5812
- if (dfs(depId, [...path27, nodeId])) {
6069
+ if (dfs(depId, [...path29, nodeId])) {
5813
6070
  return true;
5814
6071
  }
5815
6072
  } else if (recStack.has(depId)) {
5816
- const cycleStart = path27.indexOf(depId);
5817
- const cycle = cycleStart >= 0 ? [...path27.slice(cycleStart), nodeId, depId] : [nodeId, depId];
6073
+ const cycleStart = path29.indexOf(depId);
6074
+ const cycle = cycleStart >= 0 ? [...path29.slice(cycleStart), nodeId, depId] : [nodeId, depId];
5818
6075
  cycles.push({
5819
6076
  cycle,
5820
6077
  description: `\uC21C\uD658 \uC758\uC874\uC131: ${cycle.join(" \u2192 ")}`
@@ -11189,6 +11446,395 @@ async function runReport(options) {
11189
11446
  }
11190
11447
  }
11191
11448
 
11449
+ // src/cli/commands/search.ts
11450
+ import path28 from "path";
11451
+ init_fs();
11452
+
11453
+ // src/core/search/searcher.ts
11454
+ init_types();
11455
+ init_errors();
11456
+ init_fs();
11457
+ import path27 from "path";
11458
+ import { promises as fs15 } from "fs";
11459
+ async function searchSpecs(sddPath, options = {}) {
11460
+ const startTime = Date.now();
11461
+ try {
11462
+ const specsPath = path27.join(sddPath, "specs");
11463
+ if (!await directoryExists(specsPath)) {
11464
+ return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
11465
+ }
11466
+ const indexResult = await buildSearchIndex(specsPath);
11467
+ if (!indexResult.success) {
11468
+ return failure(indexResult.error);
11469
+ }
11470
+ const index = indexResult.data;
11471
+ let results = filterByOptions(index, options);
11472
+ if (options.query) {
11473
+ results = searchByQuery(results, options.query, options);
11474
+ } else {
11475
+ results = results.map((item) => ({ ...item, score: 100, matches: [] }));
11476
+ }
11477
+ results = sortResults(results, options);
11478
+ if (options.limit && options.limit > 0) {
11479
+ results = results.slice(0, options.limit);
11480
+ }
11481
+ const duration = Date.now() - startTime;
11482
+ return success({
11483
+ query: options.query || "*",
11484
+ options,
11485
+ totalCount: results.length,
11486
+ items: results,
11487
+ duration
11488
+ });
11489
+ } catch (error2) {
11490
+ return failure(
11491
+ new ChangeError(
11492
+ `\uAC80\uC0C9 \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
11493
+ )
11494
+ );
11495
+ }
11496
+ }
11497
+ async function buildSearchIndex(specsPath) {
11498
+ try {
11499
+ const index = [];
11500
+ await collectSpecs(specsPath, specsPath, index);
11501
+ return success(index);
11502
+ } catch (error2) {
11503
+ return failure(
11504
+ new ChangeError(
11505
+ `\uC778\uB371\uC2A4 \uAD6C\uCD95 \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
11506
+ )
11507
+ );
11508
+ }
11509
+ }
11510
+ async function collectSpecs(basePath, currentPath, index) {
11511
+ const entries = await fs15.readdir(currentPath, { withFileTypes: true });
11512
+ for (const entry of entries) {
11513
+ const fullPath = path27.join(currentPath, entry.name);
11514
+ if (entry.isDirectory()) {
11515
+ await collectSpecs(basePath, fullPath, index);
11516
+ } else if (entry.name === "spec.md") {
11517
+ const content = await fs15.readFile(fullPath, "utf-8");
11518
+ const relativePath = path27.relative(basePath, fullPath);
11519
+ const specId = path27.dirname(relativePath);
11520
+ const metadata = parseMetadata(content);
11521
+ const stat = await fs15.stat(fullPath);
11522
+ index.push({
11523
+ id: specId === "." ? entry.name : specId,
11524
+ path: relativePath,
11525
+ title: metadata.title || specId,
11526
+ content,
11527
+ status: metadata.status || "unknown",
11528
+ phase: metadata.phase || "unknown",
11529
+ author: metadata.author || "",
11530
+ created: metadata.created || "",
11531
+ updated: metadata.updated || stat.mtime.toISOString().split("T")[0],
11532
+ depends: parseDependencies(metadata.depends),
11533
+ tags: parseTags(metadata.tags)
11534
+ });
11535
+ }
11536
+ }
11537
+ }
11538
+ function parseMetadata(content) {
11539
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
11540
+ if (!match) return {};
11541
+ const metadata = {};
11542
+ const lines = match[1].split("\n");
11543
+ for (const line of lines) {
11544
+ const kvMatch = line.match(/^(\w+):\s*(.*)$/);
11545
+ if (kvMatch) {
11546
+ let value = kvMatch[2].trim();
11547
+ if (value.startsWith('"') && value.endsWith('"')) {
11548
+ value = value.slice(1, -1);
11549
+ }
11550
+ if (value === "null" || value === "~") {
11551
+ value = null;
11552
+ }
11553
+ metadata[kvMatch[1]] = value;
11554
+ }
11555
+ }
11556
+ return metadata;
11557
+ }
11558
+ function parseDependencies(depends) {
11559
+ if (!depends) return [];
11560
+ if (Array.isArray(depends)) return depends.filter(Boolean);
11561
+ if (typeof depends === "string" && depends !== "null") {
11562
+ return depends.split(",").map((d) => d.trim()).filter(Boolean);
11563
+ }
11564
+ return [];
11565
+ }
11566
+ function parseTags(tags) {
11567
+ if (!tags) return [];
11568
+ if (Array.isArray(tags)) return tags.filter(Boolean);
11569
+ if (typeof tags === "string") {
11570
+ return tags.split(",").map((t) => t.trim()).filter(Boolean);
11571
+ }
11572
+ return [];
11573
+ }
11574
+ function filterByOptions(index, options) {
11575
+ return index.filter((item) => {
11576
+ if (options.status) {
11577
+ const statuses = Array.isArray(options.status) ? options.status : [options.status];
11578
+ if (!statuses.includes(item.status)) return false;
11579
+ }
11580
+ if (options.phase) {
11581
+ const phases = Array.isArray(options.phase) ? options.phase : [options.phase];
11582
+ if (!phases.includes(item.phase)) return false;
11583
+ }
11584
+ if (options.author) {
11585
+ if (!item.author.toLowerCase().includes(options.author.toLowerCase())) {
11586
+ return false;
11587
+ }
11588
+ }
11589
+ if (options.createdAfter && item.created) {
11590
+ if (item.created < options.createdAfter) return false;
11591
+ }
11592
+ if (options.createdBefore && item.created) {
11593
+ if (item.created > options.createdBefore) return false;
11594
+ }
11595
+ if (options.updatedAfter && item.updated) {
11596
+ if (item.updated < options.updatedAfter) return false;
11597
+ }
11598
+ if (options.updatedBefore && item.updated) {
11599
+ if (item.updated > options.updatedBefore) return false;
11600
+ }
11601
+ if (options.dependsOn) {
11602
+ if (!item.depends.includes(options.dependsOn)) return false;
11603
+ }
11604
+ if (options.tags && options.tags.length > 0) {
11605
+ const hasTag = options.tags.some(
11606
+ (tag) => item.tags.some((t) => t.toLowerCase() === tag.toLowerCase())
11607
+ );
11608
+ if (!hasTag) return false;
11609
+ }
11610
+ return true;
11611
+ });
11612
+ }
11613
+ function searchByQuery(items, query, options) {
11614
+ const results = [];
11615
+ let pattern;
11616
+ try {
11617
+ if (options.regex) {
11618
+ pattern = new RegExp(query, options.caseSensitive ? "g" : "gi");
11619
+ } else {
11620
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11621
+ pattern = new RegExp(escaped, options.caseSensitive ? "g" : "gi");
11622
+ }
11623
+ } catch {
11624
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11625
+ pattern = new RegExp(escaped, "gi");
11626
+ }
11627
+ for (const item of items) {
11628
+ const matches = [];
11629
+ let score = 0;
11630
+ if (pattern.test(item.title)) {
11631
+ score += 50;
11632
+ pattern.lastIndex = 0;
11633
+ }
11634
+ if (pattern.test(item.id)) {
11635
+ score += 30;
11636
+ pattern.lastIndex = 0;
11637
+ }
11638
+ const lines = item.content.split("\n");
11639
+ for (let i = 0; i < lines.length; i++) {
11640
+ const line = lines[i];
11641
+ pattern.lastIndex = 0;
11642
+ if (pattern.test(line)) {
11643
+ score += 10;
11644
+ const highlighted = line.replace(
11645
+ pattern,
11646
+ (match) => `**${match}**`
11647
+ );
11648
+ matches.push({
11649
+ line: i + 1,
11650
+ content: highlighted.trim(),
11651
+ original: line.trim()
11652
+ });
11653
+ }
11654
+ }
11655
+ if (score > 0) {
11656
+ results.push({
11657
+ id: item.id,
11658
+ path: item.path,
11659
+ title: item.title,
11660
+ status: item.status,
11661
+ phase: item.phase,
11662
+ author: item.author || void 0,
11663
+ created: item.created || void 0,
11664
+ updated: item.updated || void 0,
11665
+ depends: item.depends.length > 0 ? item.depends : void 0,
11666
+ tags: item.tags.length > 0 ? item.tags : void 0,
11667
+ score: Math.min(100, score),
11668
+ matches: matches.slice(0, 5)
11669
+ // 최대 5개 매칭 컨텍스트
11670
+ });
11671
+ }
11672
+ }
11673
+ return results;
11674
+ }
11675
+ function sortResults(results, options) {
11676
+ const sortBy = options.sortBy || "relevance";
11677
+ const sortOrder = options.sortOrder || "desc";
11678
+ return [...results].sort((a, b) => {
11679
+ let comparison = 0;
11680
+ switch (sortBy) {
11681
+ case "relevance":
11682
+ comparison = a.score - b.score;
11683
+ break;
11684
+ case "created":
11685
+ comparison = (a.created || "").localeCompare(b.created || "");
11686
+ break;
11687
+ case "updated":
11688
+ comparison = (a.updated || "").localeCompare(b.updated || "");
11689
+ break;
11690
+ case "title":
11691
+ comparison = (a.title || "").localeCompare(b.title || "");
11692
+ break;
11693
+ case "status":
11694
+ comparison = (a.status || "").localeCompare(b.status || "");
11695
+ break;
11696
+ default:
11697
+ comparison = a.score - b.score;
11698
+ }
11699
+ return sortOrder === "asc" ? comparison : -comparison;
11700
+ });
11701
+ }
11702
+ function formatSearchResult(result) {
11703
+ const lines = [];
11704
+ lines.push(`\u{1F50D} \uAC80\uC0C9 \uACB0\uACFC: "${result.query}"`);
11705
+ lines.push(` ${result.totalCount}\uAC1C \uBC1C\uACAC (${result.duration}ms)`);
11706
+ lines.push("");
11707
+ if (result.items.length === 0) {
11708
+ lines.push(" \uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
11709
+ return lines.join("\n");
11710
+ }
11711
+ for (const item of result.items) {
11712
+ const statusIcon = getStatusIcon2(item.status);
11713
+ const scoreBar = getScoreBar(item.score);
11714
+ lines.push(`${statusIcon} ${item.id}`);
11715
+ if (item.title && item.title !== item.id) {
11716
+ lines.push(` \uC81C\uBAA9: ${item.title}`);
11717
+ }
11718
+ lines.push(` \uC0C1\uD0DC: ${item.status || "unknown"} | Phase: ${item.phase || "unknown"} | \uC810\uC218: ${scoreBar}`);
11719
+ if (item.matches && item.matches.length > 0) {
11720
+ lines.push(" \uB9E4\uCE6D:");
11721
+ for (const match of item.matches.slice(0, 3)) {
11722
+ const truncated = match.content.length > 60 ? match.content.slice(0, 60) + "..." : match.content;
11723
+ lines.push(` L${match.line}: ${truncated}`);
11724
+ }
11725
+ }
11726
+ lines.push("");
11727
+ }
11728
+ return lines.join("\n");
11729
+ }
11730
+ function getStatusIcon2(status) {
11731
+ switch (status) {
11732
+ case "draft":
11733
+ return "\u{1F4DD}";
11734
+ case "review":
11735
+ return "\u{1F440}";
11736
+ case "approved":
11737
+ return "\u2705";
11738
+ case "implemented":
11739
+ return "\u{1F680}";
11740
+ case "deprecated":
11741
+ return "\u26A0\uFE0F";
11742
+ default:
11743
+ return "\u{1F4C4}";
11744
+ }
11745
+ }
11746
+ function getScoreBar(score) {
11747
+ const filled = Math.round(score / 10);
11748
+ const empty = 10 - filled;
11749
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty) + ` ${score}%`;
11750
+ }
11751
+ function formatSearchResultJson(result) {
11752
+ return JSON.stringify(result, null, 2);
11753
+ }
11754
+
11755
+ // src/cli/commands/search.ts
11756
+ function parseSearchCliOptions(query, options) {
11757
+ const searchOptions = {};
11758
+ if (query) {
11759
+ searchOptions.query = query;
11760
+ }
11761
+ if (options.status) {
11762
+ searchOptions.status = options.status.includes(",") ? options.status.split(",").map((s) => s.trim()) : options.status;
11763
+ }
11764
+ if (options.phase) {
11765
+ searchOptions.phase = options.phase.includes(",") ? options.phase.split(",").map((p) => p.trim()) : options.phase;
11766
+ }
11767
+ if (options.author) {
11768
+ searchOptions.author = options.author;
11769
+ }
11770
+ if (options.tags) {
11771
+ searchOptions.tags = options.tags.split(",").map((t) => t.trim());
11772
+ }
11773
+ if (options.createdAfter) {
11774
+ searchOptions.createdAfter = options.createdAfter;
11775
+ }
11776
+ if (options.createdBefore) {
11777
+ searchOptions.createdBefore = options.createdBefore;
11778
+ }
11779
+ if (options.updatedAfter) {
11780
+ searchOptions.updatedAfter = options.updatedAfter;
11781
+ }
11782
+ if (options.updatedBefore) {
11783
+ searchOptions.updatedBefore = options.updatedBefore;
11784
+ }
11785
+ if (options.dependsOn) {
11786
+ searchOptions.dependsOn = options.dependsOn;
11787
+ }
11788
+ if (options.limit) {
11789
+ const limit = parseInt(options.limit, 10);
11790
+ if (!isNaN(limit) && limit > 0) {
11791
+ searchOptions.limit = limit;
11792
+ }
11793
+ }
11794
+ if (options.sortBy) {
11795
+ const validSortBy = ["relevance", "created", "updated", "title", "status"];
11796
+ if (validSortBy.includes(options.sortBy)) {
11797
+ searchOptions.sortBy = options.sortBy;
11798
+ }
11799
+ }
11800
+ if (options.sortOrder) {
11801
+ if (options.sortOrder === "asc" || options.sortOrder === "desc") {
11802
+ searchOptions.sortOrder = options.sortOrder;
11803
+ }
11804
+ }
11805
+ if (options.regex) {
11806
+ searchOptions.regex = true;
11807
+ }
11808
+ if (options.caseSensitive) {
11809
+ searchOptions.caseSensitive = true;
11810
+ }
11811
+ return searchOptions;
11812
+ }
11813
+ function registerSearchCommand(program2) {
11814
+ program2.command("search [query]").description("\uC2A4\uD399 \uAC80\uC0C9").option("--status <status>", "\uC0C1\uD0DC \uD544\uD130 (\uCF64\uB9C8\uB85C \uAD6C\uBD84)").option("--phase <phase>", "Phase \uD544\uD130 (\uCF64\uB9C8\uB85C \uAD6C\uBD84)").option("--author <author>", "\uC791\uC131\uC790 \uD544\uD130").option("--tags <tags>", "\uD0DC\uADF8 \uD544\uD130 (\uCF64\uB9C8\uB85C \uAD6C\uBD84)").option("--created-after <date>", "\uC0DD\uC131\uC77C \uC774\uD6C4 (YYYY-MM-DD)").option("--created-before <date>", "\uC0DD\uC131\uC77C \uC774\uC804 (YYYY-MM-DD)").option("--updated-after <date>", "\uC218\uC815\uC77C \uC774\uD6C4 (YYYY-MM-DD)").option("--updated-before <date>", "\uC218\uC815\uC77C \uC774\uC804 (YYYY-MM-DD)").option("--depends-on <specId>", "\uC758\uC874\uC131 \uD544\uD130").option("--limit <n>", "\uACB0\uACFC \uC81C\uD55C \uC218").option("--sort-by <field>", "\uC815\uB82C \uAE30\uC900 (relevance, created, updated, title, status)").option("--sort-order <order>", "\uC815\uB82C \uBC29\uD5A5 (asc, desc)").option("-r, --regex", "\uC815\uADDC\uC2DD \uAC80\uC0C9").option("-c, --case-sensitive", "\uB300\uC18C\uBB38\uC790 \uAD6C\uBD84").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").action(async (query, options) => {
11815
+ await executeSearch(query, options);
11816
+ });
11817
+ }
11818
+ async function executeSearch(query, options) {
11819
+ const sddPath = path28.join(process.cwd(), ".sdd");
11820
+ if (!await fileExists(sddPath)) {
11821
+ logger_exports.error(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
11822
+ return;
11823
+ }
11824
+ const searchOptions = parseSearchCliOptions(query, options);
11825
+ const result = await searchSpecs(sddPath, searchOptions);
11826
+ if (!result.success) {
11827
+ logger_exports.error(`\uAC80\uC0C9 \uC2E4\uD328: ${result.error.message}`);
11828
+ return;
11829
+ }
11830
+ if (options.json) {
11831
+ console.log(formatSearchResultJson(result.data));
11832
+ } else {
11833
+ console.log("");
11834
+ console.log(formatSearchResult(result.data));
11835
+ }
11836
+ }
11837
+
11192
11838
  // src/cli/index.ts
11193
11839
  var require2 = createRequire(import.meta.url);
11194
11840
  var pkg = require2("../../package.json");
@@ -11210,6 +11856,7 @@ registerTransitionCommand(program);
11210
11856
  registerWatchCommand(program);
11211
11857
  registerQualityCommand(program);
11212
11858
  registerReportCommand(program);
11859
+ registerSearchCommand(program);
11213
11860
  function run() {
11214
11861
  program.parse();
11215
11862
  }