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/README.md +70 -2
- package/dist/cli/index.js +3049 -943
- package/dist/cli/index.js.map +1 -1
- package/package.json +10 -9
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,
|
|
132
|
-
super(code, formatMessage(code,
|
|
131
|
+
constructor(code, path27) {
|
|
132
|
+
super(code, formatMessage(code, path27), ExitCode.FILE_SYSTEM_ERROR);
|
|
133
133
|
this.name = "FileSystemError";
|
|
134
|
-
this.path =
|
|
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
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
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
|
-
|
|
2575
|
-
const projectName = path2.basename(cwd);
|
|
2518
|
+
function generateConstitutionContent(projectName) {
|
|
2576
2519
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2577
|
-
|
|
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
|
-
|
|
2556
|
+
function generateSpecTemplate() {
|
|
2617
2557
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2618
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
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:
|
|
3680
|
+
const { promises: fs15 } = await import("fs");
|
|
3635
3681
|
try {
|
|
3636
|
-
const entries = await
|
|
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
|
-
|
|
3665
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
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
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
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
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
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
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
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
|
|
4963
|
-
const
|
|
4964
|
-
if (!
|
|
4965
|
-
|
|
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
|
|
4969
|
-
if (
|
|
4970
|
-
|
|
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
|
|
4989
|
-
if (!await directoryExists(
|
|
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
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
info(
|
|
5000
|
-
|
|
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
|
-
}
|
|
5007
|
-
error(
|
|
5216
|
+
} else {
|
|
5217
|
+
error(infoResult.error.message);
|
|
5008
5218
|
}
|
|
5009
5219
|
return;
|
|
5010
5220
|
}
|
|
5011
|
-
const
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
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 =
|
|
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
|
|
5059
|
-
|
|
5060
|
-
|
|
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
|
|
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(
|
|
5290
|
+
error(deltaResult.error.message);
|
|
5110
5291
|
process.exit(ExitCode.FILE_SYSTEM_ERROR);
|
|
5111
5292
|
}
|
|
5112
|
-
const
|
|
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
|
-
|
|
5167
|
-
|
|
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(
|
|
5351
|
+
error(`\u2717 proposal.md \uC624\uB958: ${result.proposalError}`);
|
|
5180
5352
|
hasErrors = true;
|
|
5181
5353
|
}
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
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
|
-
}
|
|
5197
|
-
|
|
5198
|
-
|
|
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.
|
|
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,
|
|
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, [...
|
|
5812
|
+
if (dfs(depId, [...path27, nodeId])) {
|
|
5646
5813
|
return true;
|
|
5647
5814
|
}
|
|
5648
5815
|
} else if (recStack.has(depId)) {
|
|
5649
|
-
const cycleStart =
|
|
5650
|
-
const cycle = cycleStart >= 0 ? [...
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6827
|
-
if (!
|
|
6828
|
-
|
|
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 =
|
|
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
|
|
7076
|
+
console.log(JSON.stringify(simResult, null, 2));
|
|
6850
7077
|
} else {
|
|
6851
|
-
console.log(formatSimulationResult(simResult
|
|
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
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
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
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
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
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
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
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
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(
|
|
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
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
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
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
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
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
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
|
|
7135
|
-
|
|
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(
|
|
7140
|
-
logger_exports.info(`\uC0DD\uC131\uB41C \uAE30\uB2A5 \uC218: ${
|
|
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(
|
|
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
|
|
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
|
|
7160
|
-
|
|
7161
|
-
|
|
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
|
|
7165
|
-
const
|
|
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
|
-
|
|
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
|
|
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(
|
|
7508
|
+
const currentBranchResult = await getCurrentBranch(projectPath);
|
|
7211
7509
|
if (currentBranchResult.success) {
|
|
7212
7510
|
status.currentBranch = currentBranchResult.data;
|
|
7213
7511
|
}
|
|
7214
|
-
const featureBranchesResult = await listFeatureBranches(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7383
|
-
return;
|
|
7665
|
+
return [];
|
|
7384
7666
|
}
|
|
7385
7667
|
const result = await readDir(specsPath);
|
|
7386
7668
|
if (!result.success) {
|
|
7387
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
7461
|
-
const
|
|
7462
|
-
if (
|
|
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
|
-
|
|
7856
|
+
printSpecTree(tree, "");
|
|
7470
7857
|
console.log("");
|
|
7471
7858
|
}
|
|
7472
|
-
|
|
7473
|
-
const
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
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
|
|
7488
|
-
|
|
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
|
|
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
|
|
7508
|
-
|
|
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
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
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
|
|
7614
|
-
if (!
|
|
7615
|
-
error(
|
|
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
|
|
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
|
|
7682
|
-
if (!
|
|
7683
|
-
error(
|
|
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
|
-
|
|
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
|
|
7701
|
-
|
|
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
|
|
7718
|
-
if (!
|
|
7719
|
-
error(
|
|
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
|
-
|
|
7723
|
-
|
|
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
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
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
|
|
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
|
|
7811
|
-
if (!
|
|
7812
|
-
error(
|
|
7813
|
-
process.exit(ExitCode.
|
|
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: ${
|
|
7832
|
-
info(`\uBC84\uC804: ${
|
|
7833
|
-
info(`\uC6D0\uCE59 \uC218: ${
|
|
7834
|
-
info(`\uAE08\uC9C0 \uC0AC\uD56D \uC218: ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 ?
|
|
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 ${
|
|
8934
|
+
info(`\u2705 ${path18.basename(file)} \u2192 ${result.target}`);
|
|
8213
8935
|
} else {
|
|
8214
8936
|
summary.failed++;
|
|
8215
|
-
error(`\u274C ${
|
|
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 ||
|
|
8951
|
+
const featureId = generateFeatureId(analysis.title || path18.basename(filePath, ".md"));
|
|
8230
8952
|
const specContent = generateSpec({
|
|
8231
8953
|
id: featureId,
|
|
8232
|
-
title: analysis.title ||
|
|
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 =
|
|
8238
|
-
const targetPath =
|
|
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:
|
|
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 =
|
|
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: ${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
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 =
|
|
9405
|
+
const workflowDir = path19.join(projectRoot, ".github", "workflows");
|
|
8493
9406
|
await ensureDir(workflowDir);
|
|
8494
9407
|
const workflowContent = generateGitHubWorkflow(strict);
|
|
8495
|
-
const workflowPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
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
|
|
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 =
|
|
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
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|