lee-spec-kit 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,18 +2,18 @@
2
2
  import path4 from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { program } from 'commander';
5
- import fs6 from 'fs-extra';
5
+ import fs8 from 'fs-extra';
6
6
  import prompts from 'prompts';
7
7
  import chalk6 from 'chalk';
8
8
  import { glob } from 'glob';
9
- import { spawn, execSync } from 'child_process';
9
+ import { spawn, execSync, execFileSync } from 'child_process';
10
10
  import os from 'os';
11
11
 
12
12
  var getFilename = () => fileURLToPath(import.meta.url);
13
13
  var getDirname = () => path4.dirname(getFilename());
14
14
  var __dirname$1 = /* @__PURE__ */ getDirname();
15
15
  async function copyTemplates(src, dest) {
16
- await fs6.copy(src, dest, {
16
+ await fs8.copy(src, dest, {
17
17
  overwrite: true,
18
18
  errorOnExist: false
19
19
  });
@@ -21,19 +21,19 @@ async function copyTemplates(src, dest) {
21
21
  async function replaceInFiles(dir, replacements) {
22
22
  const files = await glob("**/*.md", { cwd: dir, absolute: true });
23
23
  for (const file of files) {
24
- let content = await fs6.readFile(file, "utf-8");
24
+ let content = await fs8.readFile(file, "utf-8");
25
25
  for (const [search, replace] of Object.entries(replacements)) {
26
26
  content = content.replaceAll(search, replace);
27
27
  }
28
- await fs6.writeFile(file, content, "utf-8");
28
+ await fs8.writeFile(file, content, "utf-8");
29
29
  }
30
30
  const shFiles = await glob("**/*.sh", { cwd: dir, absolute: true });
31
31
  for (const file of shFiles) {
32
- let content = await fs6.readFile(file, "utf-8");
32
+ let content = await fs8.readFile(file, "utf-8");
33
33
  for (const [search, replace] of Object.entries(replacements)) {
34
34
  content = content.replaceAll(search, replace);
35
35
  }
36
- await fs6.writeFile(file, content, "utf-8");
36
+ await fs8.writeFile(file, content, "utf-8");
37
37
  }
38
38
  }
39
39
  var __filename2 = fileURLToPath(import.meta.url);
@@ -358,8 +358,8 @@ async function runInit(options) {
358
358
  assertValid(validateSafeName(projectName), "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984");
359
359
  assertValid(validateProjectType(projectType), "\uD504\uB85C\uC81D\uD2B8 \uD0C0\uC785");
360
360
  assertValid(validateLanguage(lang), "\uC5B8\uC5B4");
361
- if (await fs6.pathExists(targetDir)) {
362
- const files = await fs6.readdir(targetDir);
361
+ if (await fs8.pathExists(targetDir)) {
362
+ const files = await fs8.readdir(targetDir);
363
363
  if (files.length > 0) {
364
364
  const { overwrite } = await prompts({
365
365
  type: "confirm",
@@ -383,10 +383,10 @@ async function runInit(options) {
383
383
  const templatesDir = getTemplatesDir();
384
384
  const commonPath = path4.join(templatesDir, lang, "common");
385
385
  const typePath = path4.join(templatesDir, lang, projectType);
386
- if (await fs6.pathExists(commonPath)) {
386
+ if (await fs8.pathExists(commonPath)) {
387
387
  await copyTemplates(commonPath, targetDir);
388
388
  }
389
- if (!await fs6.pathExists(typePath)) {
389
+ if (!await fs8.pathExists(typePath)) {
390
390
  throw new Error(`\uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${typePath}`);
391
391
  }
392
392
  await copyTemplates(typePath, targetDir);
@@ -414,7 +414,7 @@ async function runInit(options) {
414
414
  }
415
415
  }
416
416
  const configPath = path4.join(targetDir, ".lee-spec-kit.json");
417
- await fs6.writeJson(configPath, config, { spaces: 2 });
417
+ await fs8.writeJson(configPath, config, { spaces: 2 });
418
418
  console.log(chalk6.green("\u2705 docs \uAD6C\uC870 \uC0DD\uC131 \uC644\uB8CC!"));
419
419
  console.log();
420
420
  await initGit(cwd, targetDir, docsRepo, pushDocs, docsRemote);
@@ -427,28 +427,49 @@ async function runInit(options) {
427
427
  }
428
428
  async function initGit(cwd, targetDir, docsRepo, pushDocs, docsRemote) {
429
429
  try {
430
+ const runGit = (args, workdir) => {
431
+ execFileSync("git", args, { cwd: workdir, stdio: "ignore" });
432
+ };
433
+ const getCachedStagedFiles = (workdir) => {
434
+ try {
435
+ const out = execFileSync("git", ["diff", "--cached", "--name-only"], {
436
+ cwd: workdir,
437
+ encoding: "utf-8",
438
+ stdio: ["ignore", "pipe", "ignore"]
439
+ }).trim();
440
+ if (!out) return [];
441
+ return out.split("\n").map((s) => s.trim()).filter(Boolean);
442
+ } catch {
443
+ return null;
444
+ }
445
+ };
430
446
  try {
431
- execSync("git rev-parse --is-inside-work-tree", {
432
- cwd,
433
- stdio: "ignore"
434
- });
447
+ runGit(["rev-parse", "--is-inside-work-tree"], cwd);
435
448
  console.log(chalk6.blue("\u{1F4E6} Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0, docs \uCEE4\uBC0B \uC911..."));
436
449
  } catch {
437
450
  console.log(chalk6.blue("\u{1F4E6} Git \uCD08\uAE30\uD654 \uC911..."));
438
- execSync("git init", { cwd, stdio: "ignore" });
451
+ runGit(["init"], cwd);
439
452
  }
440
453
  const relativePath = path4.relative(cwd, targetDir);
441
- execSync(`git add "${relativePath}"`, { cwd, stdio: "ignore" });
442
- execSync('git commit -m "init: docs \uAD6C\uC870 \uCD08\uAE30\uD654 (lee-spec-kit)"', {
443
- cwd,
444
- stdio: "ignore"
445
- });
454
+ const stagedBeforeAdd = getCachedStagedFiles(cwd);
455
+ if (relativePath === "." && stagedBeforeAdd && stagedBeforeAdd.length > 0) {
456
+ console.log(
457
+ chalk6.yellow(
458
+ '\u26A0\uFE0F \uD604\uC7AC Git index\uC5D0 \uC774\uBBF8 stage\uB41C \uBCC0\uACBD\uC774 \uC788\uC2B5\uB2C8\uB2E4. (--dir "." \uC778 \uACBD\uC6B0 \uCEE4\uBC0B \uBC94\uC704\uB97C \uC548\uC804\uD558\uAC8C \uC81C\uD55C\uD560 \uC218 \uC5C6\uC5B4 \uC790\uB3D9 \uCEE4\uBC0B\uC744 \uAC74\uB108\uB701\uB2C8\uB2E4)'
459
+ )
460
+ );
461
+ console.log(chalk6.gray(" \uC218\uB3D9\uC73C\uB85C \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uD655\uC778\uD55C \uB4A4 \uCEE4\uBC0B\uD574\uC8FC\uC138\uC694."));
462
+ console.log();
463
+ return;
464
+ }
465
+ runGit(["add", relativePath], cwd);
466
+ runGit(
467
+ ["commit", "-m", "init: docs \uAD6C\uC870 \uCD08\uAE30\uD654 (lee-spec-kit)", "--", relativePath],
468
+ cwd
469
+ );
446
470
  if (docsRepo === "standalone" && pushDocs && docsRemote) {
447
471
  try {
448
- execSync(`git remote add origin "${docsRemote}"`, {
449
- cwd,
450
- stdio: "ignore"
451
- });
472
+ runGit(["remote", "add", "origin", docsRemote], cwd);
452
473
  console.log(chalk6.green(`\u2705 Git remote \uC124\uC815 \uC644\uB8CC: ${docsRemote}`));
453
474
  } catch {
454
475
  console.log(chalk6.yellow("\u26A0\uFE0F Git remote\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4."));
@@ -492,9 +513,9 @@ async function getConfig(cwd) {
492
513
  if (visitedDocsDirs.has(resolvedDocsDir)) continue;
493
514
  visitedDocsDirs.add(resolvedDocsDir);
494
515
  const configPath = path4.join(resolvedDocsDir, ".lee-spec-kit.json");
495
- if (await fs6.pathExists(configPath)) {
516
+ if (await fs8.pathExists(configPath)) {
496
517
  try {
497
- const configFile = await fs6.readJson(configPath);
518
+ const configFile = await fs8.readJson(configPath);
498
519
  return {
499
520
  docsDir: resolvedDocsDir,
500
521
  projectName: configFile.projectName,
@@ -510,14 +531,14 @@ async function getConfig(cwd) {
510
531
  }
511
532
  const agentsPath = path4.join(resolvedDocsDir, "agents");
512
533
  const featuresPath = path4.join(resolvedDocsDir, "features");
513
- if (await fs6.pathExists(agentsPath) && await fs6.pathExists(featuresPath)) {
534
+ if (await fs8.pathExists(agentsPath) && await fs8.pathExists(featuresPath)) {
514
535
  const bePath = path4.join(featuresPath, "be");
515
536
  const fePath = path4.join(featuresPath, "fe");
516
- const projectType = await fs6.pathExists(bePath) || await fs6.pathExists(fePath) ? "fullstack" : "single";
537
+ const projectType = await fs8.pathExists(bePath) || await fs8.pathExists(fePath) ? "fullstack" : "single";
517
538
  const agentsMdPath = path4.join(agentsPath, "agents.md");
518
539
  let lang = "ko";
519
- if (await fs6.pathExists(agentsMdPath)) {
520
- const content = await fs6.readFile(agentsMdPath, "utf-8");
540
+ if (await fs8.pathExists(agentsMdPath)) {
541
+ const content = await fs8.readFile(agentsMdPath, "utf-8");
521
542
  if (!/[가-힣]/.test(content)) {
522
543
  lang = "en";
523
544
  }
@@ -593,26 +614,31 @@ async function runFeature(name, options) {
593
614
  }
594
615
  const featureFolderName = `${featureId}-${name}`;
595
616
  const featureDir = path4.join(featuresDir, featureFolderName);
596
- if (await fs6.pathExists(featureDir)) {
617
+ if (await fs8.pathExists(featureDir)) {
597
618
  console.error(chalk6.red(`\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uD3F4\uB354\uC785\uB2C8\uB2E4: ${featureDir}`));
598
619
  process.exit(1);
599
620
  }
600
621
  const featureBasePath = path4.join(docsDir, "features", "feature-base");
601
- if (!await fs6.pathExists(featureBasePath)) {
622
+ if (!await fs8.pathExists(featureBasePath)) {
602
623
  console.error(chalk6.red("feature-base \uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
603
624
  process.exit(1);
604
625
  }
605
- await fs6.copy(featureBasePath, featureDir);
626
+ await fs8.copy(featureBasePath, featureDir);
606
627
  const idNumber = featureId.replace("F", "");
607
628
  const repoName = projectType === "fullstack" && repo ? `{{projectName}}-${repo}` : "{{projectName}}";
608
629
  const replacements = {
630
+ // ko placeholders
609
631
  "{\uAE30\uB2A5\uBA85}": name,
610
632
  "{\uBC88\uD638}": idNumber,
611
633
  "YYYY-MM-DD": (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
612
634
  "{be|fe}": repo || "",
613
- "git-dungeon-{be|fe}": repoName,
614
635
  "{\uC774\uC288\uBC88\uD638}": "",
615
- "{{description}}": options.desc || ""
636
+ "{{description}}": options.desc || "",
637
+ // en placeholders
638
+ "{feature-name}": name,
639
+ "{number}": idNumber,
640
+ "{issue-number}": "",
641
+ "{{projectName}}-{be|fe}": repoName
616
642
  };
617
643
  if (lang === "en") {
618
644
  replacements["\uAE30\uB2A5 ID"] = "Feature ID";
@@ -643,8 +669,8 @@ async function getNextFeatureId(docsDir, projectType) {
643
669
  scanDirs.push(featuresDir);
644
670
  }
645
671
  for (const dir of scanDirs) {
646
- if (!await fs6.pathExists(dir)) continue;
647
- const entries = await fs6.readdir(dir, { withFileTypes: true });
672
+ if (!await fs8.pathExists(dir)) continue;
673
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
648
674
  for (const entry of entries) {
649
675
  if (!entry.isDirectory()) continue;
650
676
  const match = entry.name.match(/^F(\d+)-/);
@@ -658,380 +684,6 @@ async function getNextFeatureId(docsDir, projectType) {
658
684
  const width = Math.max(3, String(next).length);
659
685
  return `F${String(next).padStart(width, "0")}`;
660
686
  }
661
- function statusCommand(program2) {
662
- program2.command("status").description("Show feature status").option("-w, --write", "Write status.md file").option("-s, --strict", "Fail on missing/duplicate feature IDs").action(async (options) => {
663
- try {
664
- await runStatus(options);
665
- } catch (error) {
666
- console.error(chalk6.red("\uC624\uB958:"), error);
667
- process.exit(1);
668
- }
669
- });
670
- }
671
- async function runStatus(options) {
672
- const cwd = process.cwd();
673
- const config = await getConfig(cwd);
674
- if (!config) {
675
- console.error(
676
- chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
677
- );
678
- process.exit(1);
679
- }
680
- const { docsDir, projectType } = config;
681
- const featuresDir = path4.join(docsDir, "features");
682
- const features = [];
683
- const idMap = /* @__PURE__ */ new Map();
684
- const scopes = projectType === "fullstack" ? ["be", "fe"] : [""];
685
- for (const scope of scopes) {
686
- const scanDir = scope ? path4.join(featuresDir, scope) : featuresDir;
687
- if (!await fs6.pathExists(scanDir)) continue;
688
- const entries = await fs6.readdir(scanDir, { withFileTypes: true });
689
- for (const entry of entries) {
690
- if (!entry.isDirectory()) continue;
691
- if (entry.name === "feature-base") continue;
692
- const featureDir = path4.join(scanDir, entry.name);
693
- const specPath = path4.join(featureDir, "spec.md");
694
- const tasksPath = path4.join(featureDir, "tasks.md");
695
- if (!await fs6.pathExists(specPath)) continue;
696
- if (!await fs6.pathExists(tasksPath)) continue;
697
- const specContent = await fs6.readFile(specPath, "utf-8");
698
- const tasksContent = await fs6.readFile(tasksPath, "utf-8");
699
- const id = extractSpecValue(specContent, "\uAE30\uB2A5 ID") || extractSpecValue(specContent, "Feature ID") || "UNKNOWN";
700
- const name = extractSpecValue(specContent, "\uAE30\uB2A5\uBA85") || extractSpecValue(specContent, "Feature Name") || entry.name;
701
- const repo = extractSpecValue(specContent, "\uB300\uC0C1 \uB808\uD3EC") || extractSpecValue(specContent, "Target Repo") || (scope ? `{{projectName}}-${scope}` : "{{projectName}}");
702
- const issue = extractSpecValue(specContent, "\uC774\uC288 \uBC88\uD638") || extractSpecValue(specContent, "Issue Number") || "-";
703
- const relPath = path4.relative(docsDir, featureDir);
704
- if (!idMap.has(id)) {
705
- idMap.set(id, []);
706
- }
707
- idMap.get(id).push(relPath);
708
- const { total, done, doing, todo } = countTasks(tasksContent);
709
- let status = "TODO";
710
- if (total > 0 && done === total) {
711
- status = "DONE";
712
- } else if (doing > 0) {
713
- status = "DOING";
714
- } else if (todo > 0) {
715
- status = "TODO";
716
- } else if (total === 0) {
717
- status = "NO_TASKS";
718
- }
719
- features.push({
720
- id,
721
- name,
722
- repo,
723
- issue,
724
- status,
725
- progress: `${done}/${total}`,
726
- path: relPath
727
- });
728
- }
729
- }
730
- if (features.length === 0) {
731
- console.log(chalk6.yellow("Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
732
- return;
733
- }
734
- if (options.strict) {
735
- const duplicates = [...idMap.entries()].filter(
736
- ([, paths]) => paths.length > 1
737
- );
738
- if (duplicates.length > 0) {
739
- console.error(chalk6.red("\uC911\uBCF5 Feature ID \uBC1C\uACAC:"));
740
- for (const [id, paths] of duplicates) {
741
- console.error(chalk6.red(` ${id}:`));
742
- for (const p of paths) {
743
- console.error(chalk6.red(` - ${p}`));
744
- }
745
- }
746
- process.exit(1);
747
- }
748
- const unknowns = [...idMap.entries()].filter(([id]) => id === "UNKNOWN");
749
- if (unknowns.length > 0) {
750
- console.error(chalk6.red("Feature ID\uAC00 \uC5C6\uB294 \uD56D\uBAA9:"));
751
- for (const [, paths] of unknowns) {
752
- for (const p of paths) {
753
- console.error(chalk6.red(` - ${p}`));
754
- }
755
- }
756
- process.exit(1);
757
- }
758
- }
759
- features.sort((a, b) => a.id.localeCompare(b.id));
760
- const header = "| ID | Name | Repo | Issue | Status | Progress | Path |";
761
- const separator = "| --- | --- | --- | --- | --- | --- | --- |";
762
- console.log();
763
- console.log(header);
764
- console.log(separator);
765
- for (const f of features) {
766
- const statusColor = f.status === "DONE" ? chalk6.green : f.status === "DOING" ? chalk6.yellow : chalk6.gray;
767
- console.log(
768
- `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${statusColor(f.status)} | ${f.progress} | ${f.path} |`
769
- );
770
- }
771
- console.log();
772
- if (options.write) {
773
- const outputPath = path4.join(featuresDir, "status.md");
774
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
775
- const content = [
776
- "# Feature Status",
777
- "",
778
- `- Generated: ${date}`,
779
- "- Source: `tasks.md`, `spec.md`",
780
- "",
781
- header,
782
- separator,
783
- ...features.map(
784
- (f) => `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${f.status} | ${f.progress} | ${f.path} |`
785
- ),
786
- ""
787
- ].join("\n");
788
- await fs6.writeFile(outputPath, content, "utf-8");
789
- console.log(chalk6.green(`\u2705 ${outputPath} \uC0DD\uC131 \uC644\uB8CC`));
790
- }
791
- }
792
- function extractSpecValue(content, key) {
793
- const regex = new RegExp(`^- \\*\\*${key}\\*\\*:\\s*(.*)$`, "m");
794
- const match = content.match(regex);
795
- return match ? match[1].trim() : "";
796
- }
797
- function countTasks(content) {
798
- let total = 0;
799
- let done = 0;
800
- let doing = 0;
801
- let todo = 0;
802
- const lines = content.split("\n");
803
- for (const line of lines) {
804
- const match = line.match(/^- \[([A-Z]+)\]/);
805
- if (match) {
806
- total++;
807
- const status = match[1];
808
- if (status === "DONE") done++;
809
- else if (status === "DOING" || status === "REVIEW") doing++;
810
- else if (status === "TODO") todo++;
811
- }
812
- }
813
- return { total, done, doing, todo };
814
- }
815
- function updateCommand(program2) {
816
- program2.command("update").description("Update docs templates to the latest version").option("--agents", "Update agents/ folder only").option("--templates", "Update feature-base/ folder only").option("-f, --force", "Force overwrite without confirmation").action(async (options) => {
817
- try {
818
- await runUpdate(options);
819
- } catch (error) {
820
- if (error instanceof Error && error.message === "canceled") {
821
- console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
822
- process.exit(0);
823
- }
824
- console.error(chalk6.red("\uC624\uB958:"), error);
825
- process.exit(1);
826
- }
827
- });
828
- }
829
- async function runUpdate(options) {
830
- const cwd = process.cwd();
831
- const config = await getConfig(cwd);
832
- if (!config) {
833
- console.error(
834
- chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
835
- );
836
- process.exit(1);
837
- }
838
- const { docsDir, projectType, lang } = config;
839
- const templatesDir = getTemplatesDir();
840
- const sourceDir = path4.join(templatesDir, lang, projectType);
841
- const updateAgents = options.agents || !options.agents && !options.templates;
842
- const updateTemplates = options.templates || !options.agents && !options.templates;
843
- console.log(chalk6.blue("\u{1F4E6} \uD15C\uD50C\uB9BF \uC5C5\uB370\uC774\uD2B8\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4..."));
844
- console.log(chalk6.gray(` - \uC5B8\uC5B4: ${lang}`));
845
- console.log(chalk6.gray(` - \uD0C0\uC785: ${projectType}`));
846
- console.log();
847
- let updatedCount = 0;
848
- if (updateAgents) {
849
- console.log(chalk6.blue("\u{1F4C1} agents/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
850
- const commonAgents = path4.join(templatesDir, lang, "common", "agents");
851
- const typeAgents = path4.join(templatesDir, lang, projectType, "agents");
852
- const targetAgents = path4.join(docsDir, "agents");
853
- const featurePath = projectType === "fullstack" ? "docs/features/{be|fe}" : "docs/features";
854
- const replacements = {
855
- "{{featurePath}}": featurePath
856
- };
857
- if (await fs6.pathExists(commonAgents)) {
858
- const count = await updateFolder(
859
- commonAgents,
860
- targetAgents,
861
- options.force,
862
- replacements
863
- );
864
- updatedCount += count;
865
- }
866
- if (await fs6.pathExists(typeAgents)) {
867
- const count = await updateFolder(typeAgents, targetAgents, options.force);
868
- updatedCount += count;
869
- }
870
- console.log(chalk6.green(` \u2705 agents/ \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
871
- }
872
- if (updateTemplates) {
873
- console.log(chalk6.blue("\u{1F4C1} features/feature-base/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
874
- const sourceFeatureBase = path4.join(sourceDir, "features", "feature-base");
875
- const targetFeatureBase = path4.join(docsDir, "features", "feature-base");
876
- if (await fs6.pathExists(sourceFeatureBase)) {
877
- const count = await updateFolder(
878
- sourceFeatureBase,
879
- targetFeatureBase,
880
- options.force
881
- );
882
- updatedCount += count;
883
- console.log(chalk6.green(` \u2705 ${count}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
884
- }
885
- }
886
- console.log();
887
- console.log(chalk6.green(`\u2705 \uCD1D ${updatedCount}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
888
- }
889
- async function updateFolder(sourceDir, targetDir, force, replacements) {
890
- const protectedFiles = /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
891
- await fs6.ensureDir(targetDir);
892
- const files = await fs6.readdir(sourceDir);
893
- let updatedCount = 0;
894
- for (const file of files) {
895
- const sourcePath = path4.join(sourceDir, file);
896
- const targetPath = path4.join(targetDir, file);
897
- const stat = await fs6.stat(sourcePath);
898
- if (stat.isFile()) {
899
- if (protectedFiles.has(file)) {
900
- continue;
901
- }
902
- let sourceContent = await fs6.readFile(sourcePath, "utf-8");
903
- if (replacements) {
904
- for (const [key, value] of Object.entries(replacements)) {
905
- sourceContent = sourceContent.replaceAll(key, value);
906
- }
907
- }
908
- let shouldUpdate = true;
909
- if (await fs6.pathExists(targetPath)) {
910
- const targetContent = await fs6.readFile(targetPath, "utf-8");
911
- if (sourceContent === targetContent) {
912
- continue;
913
- }
914
- if (!force) {
915
- console.log(
916
- chalk6.yellow(` \u26A0\uFE0F ${file} - \uBCC0\uACBD \uAC10\uC9C0 (--force\uB85C \uB36E\uC5B4\uC4F0\uAE30)`)
917
- );
918
- shouldUpdate = false;
919
- }
920
- }
921
- if (shouldUpdate) {
922
- await fs6.writeFile(targetPath, sourceContent);
923
- console.log(chalk6.gray(` \u{1F4C4} ${file} \uC5C5\uB370\uC774\uD2B8`));
924
- updatedCount++;
925
- }
926
- } else if (stat.isDirectory()) {
927
- const subCount = await updateFolder(
928
- sourcePath,
929
- targetPath,
930
- force,
931
- replacements
932
- );
933
- updatedCount += subCount;
934
- }
935
- }
936
- return updatedCount;
937
- }
938
- function configCommand(program2) {
939
- program2.command("config").description("View or modify project configuration").option("--project-root <path>", "Set project root path").option("--repo <repo>", "Repository type for fullstack: fe | be").action(async (options) => {
940
- try {
941
- await runConfig(options);
942
- } catch (error) {
943
- if (error instanceof Error && error.message === "canceled") {
944
- console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
945
- process.exit(0);
946
- }
947
- console.error(chalk6.red("\uC624\uB958:"), error);
948
- process.exit(1);
949
- }
950
- });
951
- }
952
- async function runConfig(options) {
953
- const cwd = process.cwd();
954
- const config = await getConfig(cwd);
955
- if (!config) {
956
- console.log(
957
- chalk6.red("\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.")
958
- );
959
- process.exit(1);
960
- }
961
- const configPath = path4.join(config.docsDir, ".lee-spec-kit.json");
962
- if (!options.projectRoot) {
963
- console.log();
964
- console.log(chalk6.blue("\u{1F4CB} \uD604\uC7AC \uC124\uC815:"));
965
- console.log();
966
- console.log(chalk6.gray(` \uACBD\uB85C: ${configPath}`));
967
- console.log();
968
- const configFile2 = await fs6.readJson(configPath);
969
- console.log(JSON.stringify(configFile2, null, 2));
970
- console.log();
971
- return;
972
- }
973
- const configFile = await fs6.readJson(configPath);
974
- if (configFile.docsRepo !== "standalone") {
975
- console.log(
976
- chalk6.yellow("\u26A0\uFE0F projectRoot\uB294 standalone \uBAA8\uB4DC\uC5D0\uC11C\uB9CC \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.")
977
- );
978
- return;
979
- }
980
- const projectType = configFile.projectType;
981
- if (projectType === "fullstack") {
982
- if (!options.repo) {
983
- const response = await prompts(
984
- [
985
- {
986
- type: "select",
987
- name: "repo",
988
- message: "\uC218\uC815\uD560 \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
989
- choices: [
990
- { title: "Frontend (fe)", value: "fe" },
991
- { title: "Backend (be)", value: "be" }
992
- ]
993
- }
994
- ],
995
- {
996
- onCancel: () => {
997
- throw new Error("canceled");
998
- }
999
- }
1000
- );
1001
- options.repo = response.repo;
1002
- }
1003
- if (!options.repo || !["fe", "be"].includes(options.repo)) {
1004
- console.log(
1005
- chalk6.red(
1006
- "Fullstack \uD504\uB85C\uC81D\uD2B8\uB294 --repo fe \uB610\uB294 --repo be\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."
1007
- )
1008
- );
1009
- return;
1010
- }
1011
- const currentRoot = configFile.projectRoot || { fe: "", be: "" };
1012
- if (typeof currentRoot === "string") {
1013
- configFile.projectRoot = {
1014
- fe: options.repo === "fe" ? options.projectRoot : "",
1015
- be: options.repo === "be" ? options.projectRoot : ""
1016
- };
1017
- } else {
1018
- currentRoot[options.repo] = options.projectRoot;
1019
- configFile.projectRoot = currentRoot;
1020
- }
1021
- console.log(
1022
- chalk6.green(
1023
- `\u2705 ${options.repo.toUpperCase()} projectRoot \uC124\uC815 \uC644\uB8CC: ${options.projectRoot}`
1024
- )
1025
- );
1026
- } else {
1027
- configFile.projectRoot = options.projectRoot;
1028
- console.log(
1029
- chalk6.green(`\u2705 projectRoot \uC124\uC815 \uC644\uB8CC: ${options.projectRoot}`)
1030
- );
1031
- }
1032
- await fs6.writeJson(configPath, configFile, { spaces: 2 });
1033
- console.log();
1034
- }
1035
687
 
1036
688
  // src/utils/context/i18n.ts
1037
689
  function formatTemplate(template, vars) {
@@ -1049,7 +701,7 @@ var I18N = {
1049
701
  planWrite: "plan.md \uC791\uC131",
1050
702
  planApprove: "plan.md \uC2B9\uC778",
1051
703
  tasksWrite: "tasks.md \uC791\uC131",
1052
- docsCommitPlanning: "\uBB38\uC11C \uCEE4\uBC0B(\uAE30\uD68D)",
704
+ docsCommitPlanning: "\uBB38\uC11C \uCEE4\uBC0B(\uB3D9\uAE30\uD654)",
1053
705
  issueCreate: "GitHub Issue \uC0DD\uC131",
1054
706
  branchCreate: "\uBE0C\uB79C\uCE58 \uC0DD\uC131",
1055
707
  tasksExecute: "\uD0DC\uC2A4\uD06C \uC2E4\uD589",
@@ -1066,9 +718,9 @@ var I18N = {
1066
718
  planApproval: "plan.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
1067
719
  tasksCreate: "tasks.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694. (features/feature-base/tasks.md \uCC38\uACE0)",
1068
720
  tasksNeedAtLeastOne: "tasks.md\uC5D0 \uCD5C\uC18C 1\uAC1C \uC774\uC0C1\uC758 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694.",
1069
- docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): \uAE30\uD68D \uBB38\uC11C \uC791\uC131"',
721
+ docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} \uAE30\uD68D \uBB38\uC11C"',
1070
722
  issueCreateAndWrite: "GitHub Issue\uB97C \uC0DD\uC131\uD55C \uB4A4, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694. (skills/create-issue.md \uCC38\uACE0)",
1071
- docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): \uC774\uC288 #{issueNumber} \uBC18\uC601"',
723
+ docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
1072
724
  standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
1073
725
  createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
1074
726
  tasksAllDoneButNoChecklist: '\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC139\uC158\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. tasks.md\uC758 "\uC644\uB8CC \uC870\uAC74" \uC139\uC158\uC744 \uCD94\uAC00/\uD655\uC778\uD558\uC138\uC694.',
@@ -1078,6 +730,7 @@ var I18N = {
1078
730
  checkTaskStatuses: "\uD0DC\uC2A4\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)",
1079
731
  prLegacyAsk: "tasks.md\uC5D0 PR/PR \uC0C1\uD0DC \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uD15C\uD50C\uB9BF\uC744 \uCD5C\uC2E0 \uD3EC\uB9F7\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD560\uAE4C\uC694? (OK \uD544\uC694)",
1080
732
  prCreate: "PR\uC744 \uC0DD\uC131\uD558\uACE0 tasks.md\uC5D0 PR \uB9C1\uD06C\uB97C \uAE30\uB85D\uD558\uC138\uC694. (skills/create-pr.md \uCC38\uACE0)",
733
+ prFillStatus: "tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Draft/Review/Approved \uC911 \uD558\uB098\uB85C \uC124\uC815\uD558\uC138\uC694. (merge \uD6C4 Approved\uB85C \uC5C5\uB370\uC774\uD2B8)",
1081
734
  prResolveReview: "\uB9AC\uBDF0 \uCF54\uBA58\uD2B8\uB97C \uD574\uACB0\uD558\uACE0 PR \uC0C1\uD0DC\uB97C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694. (PR \uC0C1\uD0DC: Review \u2192 Approved)",
1082
735
  prRequestReview: "\uB9AC\uBDF0\uC5B4\uC5D0\uAC8C \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uACE0 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.",
1083
736
  featureDone: "PR\uC774 Approved\uC774\uACE0 \uBAA8\uB4E0 \uD0DC\uC2A4\uD06C/\uC644\uB8CC \uC870\uAC74\uC774 \uCDA9\uC871\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC774 Feature\uB294 \uC644\uB8CC \uC0C1\uD0DC\uC785\uB2C8\uB2E4.",
@@ -1086,7 +739,12 @@ var I18N = {
1086
739
  warnings: {
1087
740
  projectBranchUnavailable: "\uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.)",
1088
741
  docsGitUnavailable: "docs \uB808\uD3EC\uC758 git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (\uB808\uD3EC \uC704\uCE58 / git init \uD655\uC778)",
1089
- legacyTasksPrFields: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR` \uBC0F `PR \uC0C1\uD0DC` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694."
742
+ legacyTasksPrFields: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR` \uBC0F `PR \uC0C1\uD0DC` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694.",
743
+ workflowSpecNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC spec.md \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (spec.md\uC758 \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)",
744
+ workflowPlanNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC plan.md \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (plan.md\uC758 \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)",
745
+ workflowPrLinkMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. (tasks.md\uC758 PR \uD544\uB4DC\uB97C \uCC44\uC6B0\uC138\uC694.)",
746
+ workflowPrStatusMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uC0C1\uD0DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. (tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Draft/Review/Approved \uC911 \uD558\uB098\uB85C \uC124\uC815\uD558\uC138\uC694.)",
747
+ workflowPrStatusNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (merge \uD6C4 tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)"
1090
748
  }
1091
749
  },
1092
750
  en: {
@@ -1097,7 +755,7 @@ var I18N = {
1097
755
  planWrite: "Write plan.md",
1098
756
  planApprove: "Approve plan.md",
1099
757
  tasksWrite: "Write tasks.md",
1100
- docsCommitPlanning: "Commit planning docs",
758
+ docsCommitPlanning: "Commit docs (sync)",
1101
759
  issueCreate: "Create GitHub Issue",
1102
760
  branchCreate: "Create branch",
1103
761
  tasksExecute: "Execute tasks",
@@ -1114,9 +772,9 @@ var I18N = {
1114
772
  planApproval: "Share plan.md with the user and get approval (OK).",
1115
773
  tasksCreate: "Copy the tasks.md template and write tasks. (See features/feature-base/tasks.md)",
1116
774
  tasksNeedAtLeastOne: "Add at least one task to tasks.md.",
1117
- docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): planning docs"',
775
+ docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} planning docs"',
1118
776
  issueCreateAndWrite: "Create a GitHub Issue, then fill in the issue number in spec.md/tasks.md and prepare to commit docs. (See skills/create-issue.md)",
1119
- docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): issue #{issueNumber}"',
777
+ docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
1120
778
  standaloneNeedsProjectRoot: "In standalone mode, projectRoot is required. (npx lee-spec-kit config --project-root ...)",
1121
779
  createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
1122
780
  tasksAllDoneButNoChecklist: 'All tasks are DONE but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
@@ -1126,6 +784,7 @@ var I18N = {
1126
784
  checkTaskStatuses: "Check task statuses. ({done}/{total}) (See skills/execute-task.md)",
1127
785
  prLegacyAsk: "Legacy tasks.md format detected (missing PR/PR Status fields). Update to the latest format? (OK required)",
1128
786
  prCreate: "Create a PR and record the PR link in tasks.md. (See skills/create-pr.md)",
787
+ prFillStatus: "Set PR Status in tasks.md to Draft/Review/Approved. (After merge, update to Approved)",
1129
788
  prResolveReview: "Resolve review comments and update PR status. (PR Status: Review \u2192 Approved)",
1130
789
  prRequestReview: "Request reviews and update PR status to Review.",
1131
790
  featureDone: "PR is Approved and all tasks/completion criteria are satisfied. This feature is done.",
@@ -1134,7 +793,12 @@ var I18N = {
1134
793
  warnings: {
1135
794
  projectBranchUnavailable: "Cannot determine project branch. (In standalone mode, projectRoot is required.)",
1136
795
  docsGitUnavailable: "Cannot read git status for the docs repo. (Check repo location / git init.)",
1137
- legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps."
796
+ legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps.",
797
+ workflowSpecNotApproved: "Implementation is done but spec.md Status is not Approved. (Update spec.md Status to Approved.)",
798
+ workflowPlanNotApproved: "Implementation is done but plan.md Status is not Approved. (Update plan.md Status to Approved.)",
799
+ workflowPrLinkMissing: "Implementation is done but PR link is missing. (Fill the PR field in tasks.md.)",
800
+ workflowPrStatusMissing: "Implementation is done but PR Status is missing. (Set PR Status to Draft/Review/Approved in tasks.md.)",
801
+ workflowPrStatusNotApproved: "Implementation is done but PR Status is not Approved. (After merge, update PR Status to Approved in tasks.md.)"
1138
802
  }
1139
803
  }
1140
804
  };
@@ -1147,11 +811,14 @@ function tr(lang, category, key, vars = {}) {
1147
811
  function isCompletionChecklistDone(feature) {
1148
812
  return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
1149
813
  }
814
+ function isImplementationDone(feature) {
815
+ return feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature);
816
+ }
1150
817
  function isPrMetadataConfigured(feature) {
1151
818
  return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
1152
819
  }
1153
820
  function isFeatureDone(feature) {
1154
- return feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isPrMetadataConfigured(feature) && !!feature.pr.link && feature.pr.status === "Approved";
821
+ return feature.specStatus === "Approved" && feature.planStatus === "Approved" && feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isPrMetadataConfigured(feature) && !!feature.pr.link && feature.pr.status === "Approved";
1155
822
  }
1156
823
  function getStepDefinitions(lang) {
1157
824
  return [
@@ -1262,55 +929,57 @@ function getStepDefinitions(lang) {
1262
929
  step: 7,
1263
930
  name: tr(lang, "steps", "docsCommitPlanning"),
1264
931
  checklist: {
1265
- done: (f) => f.issueNumber ? true : f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.git.docsHasUncommittedChanges
932
+ done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.git.docsHasUncommittedChanges
1266
933
  },
1267
934
  current: {
1268
- when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.issueNumber && f.git.docsHasUncommittedChanges,
1269
- actions: (f) => [
1270
- {
1271
- type: "command",
1272
- requiresUserOk: true,
1273
- scope: "docs",
1274
- cwd: f.git.docsGitCwd,
1275
- cmd: tr(lang, "messages", "docsCommitPlanning", {
1276
- docsGitCwd: f.git.docsGitCwd,
1277
- featurePath: f.docs.featurePathFromDocs,
1278
- folderName: f.folderName
1279
- })
935
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.activeTask && f.git.docsHasUncommittedChanges,
936
+ actions: (f) => {
937
+ if (f.issueNumber) {
938
+ return [
939
+ {
940
+ type: "command",
941
+ requiresUserOk: true,
942
+ scope: "docs",
943
+ cwd: f.git.docsGitCwd,
944
+ cmd: tr(lang, "messages", "docsCommitIssueUpdate", {
945
+ docsGitCwd: f.git.docsGitCwd,
946
+ featurePath: f.docs.featurePathFromDocs,
947
+ issueNumber: f.issueNumber,
948
+ folderName: f.folderName
949
+ })
950
+ }
951
+ ];
1280
952
  }
1281
- ]
953
+ return [
954
+ {
955
+ type: "command",
956
+ requiresUserOk: true,
957
+ scope: "docs",
958
+ cwd: f.git.docsGitCwd,
959
+ cmd: tr(lang, "messages", "docsCommitPlanning", {
960
+ docsGitCwd: f.git.docsGitCwd,
961
+ featurePath: f.docs.featurePathFromDocs,
962
+ folderName: f.folderName
963
+ })
964
+ }
965
+ ];
966
+ }
1282
967
  }
1283
968
  },
1284
969
  {
1285
970
  step: 8,
1286
971
  name: tr(lang, "steps", "issueCreate"),
1287
972
  checklist: {
1288
- done: (f) => !!f.issueNumber && !f.git.docsHasUncommittedChanges
973
+ done: (f) => !!f.issueNumber
1289
974
  },
1290
- current: {
1291
- when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && (!f.issueNumber || f.git.docsHasUncommittedChanges),
1292
- actions: (f) => {
1293
- if (!f.issueNumber) {
1294
- return [
1295
- {
1296
- type: "instruction",
1297
- requiresUserOk: true,
1298
- message: tr(lang, "messages", "issueCreateAndWrite")
1299
- }
1300
- ];
1301
- }
975
+ current: {
976
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.issueNumber,
977
+ actions: (f) => {
1302
978
  return [
1303
979
  {
1304
- type: "command",
980
+ type: "instruction",
1305
981
  requiresUserOk: true,
1306
- scope: "docs",
1307
- cwd: f.git.docsGitCwd,
1308
- cmd: tr(lang, "messages", "docsCommitIssueUpdate", {
1309
- docsGitCwd: f.git.docsGitCwd,
1310
- featurePath: f.docs.featurePathFromDocs,
1311
- issueNumber: f.issueNumber,
1312
- folderName: f.folderName
1313
- })
982
+ message: tr(lang, "messages", "issueCreateAndWrite")
1314
983
  }
1315
984
  ];
1316
985
  }
@@ -1321,7 +990,7 @@ function getStepDefinitions(lang) {
1321
990
  name: tr(lang, "steps", "branchCreate"),
1322
991
  checklist: { done: (f) => f.git.onExpectedBranch },
1323
992
  current: {
1324
- when: (f) => !!f.issueNumber && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
993
+ when: (f) => !!f.issueNumber && !isImplementationDone(f) && !isFeatureDone(f) && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
1325
994
  actions: (f) => {
1326
995
  if (!f.git.projectBranchAvailable || !f.git.projectGitCwd) {
1327
996
  return [
@@ -1442,6 +1111,15 @@ function getStepDefinitions(lang) {
1442
1111
  current: {
1443
1112
  when: (f) => isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
1444
1113
  actions: (f) => {
1114
+ if (!f.pr.status) {
1115
+ return [
1116
+ {
1117
+ type: "instruction",
1118
+ requiresUserOk: true,
1119
+ message: tr(lang, "messages", "prFillStatus")
1120
+ }
1121
+ ];
1122
+ }
1445
1123
  if (f.pr.status === "Review") {
1446
1124
  return [
1447
1125
  {
@@ -1587,7 +1265,7 @@ function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
1587
1265
  function escapeRegExp(value) {
1588
1266
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1589
1267
  }
1590
- function extractSpecValue2(content, key) {
1268
+ function extractSpecValue(content, key) {
1591
1269
  const regex = new RegExp(
1592
1270
  `^\\s*-\\s*\\*\\*${escapeRegExp(key)}\\*\\*\\s*:\\s*(.*)$`,
1593
1271
  "m"
@@ -1597,7 +1275,7 @@ function extractSpecValue2(content, key) {
1597
1275
  }
1598
1276
  function extractFirstSpecValue(content, keys) {
1599
1277
  for (const key of keys) {
1600
- const value = extractSpecValue2(content, key);
1278
+ const value = extractSpecValue(content, key);
1601
1279
  if (value) return value;
1602
1280
  }
1603
1281
  return void 0;
@@ -1667,6 +1345,12 @@ function parseCompletionChecklist(content) {
1667
1345
  }
1668
1346
  return total > 0 ? { total, checked } : void 0;
1669
1347
  }
1348
+ function isCompletionChecklistDone2(feature) {
1349
+ return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
1350
+ }
1351
+ function isPrMetadataConfigured2(feature) {
1352
+ return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
1353
+ }
1670
1354
  async function parseFeature(featurePath, type, context, options) {
1671
1355
  const lang = options.lang;
1672
1356
  const folderName = path4.basename(featurePath);
@@ -1678,22 +1362,22 @@ async function parseFeature(featurePath, type, context, options) {
1678
1362
  const tasksPath = path4.join(featurePath, "tasks.md");
1679
1363
  let specStatus;
1680
1364
  let issueNumber;
1681
- const specExists = await fs6.pathExists(specPath);
1365
+ const specExists = await fs8.pathExists(specPath);
1682
1366
  if (specExists) {
1683
- const content = await fs6.readFile(specPath, "utf-8");
1367
+ const content = await fs8.readFile(specPath, "utf-8");
1684
1368
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
1685
1369
  specStatus = parseDocStatus(statusValue);
1686
1370
  const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
1687
1371
  issueNumber = parseIssueNumber(issueValue);
1688
1372
  }
1689
1373
  let planStatus;
1690
- const planExists = await fs6.pathExists(planPath);
1374
+ const planExists = await fs8.pathExists(planPath);
1691
1375
  if (planExists) {
1692
- const content = await fs6.readFile(planPath, "utf-8");
1376
+ const content = await fs8.readFile(planPath, "utf-8");
1693
1377
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
1694
1378
  planStatus = parseDocStatus(statusValue);
1695
1379
  }
1696
- const tasksExists = await fs6.pathExists(tasksPath);
1380
+ const tasksExists = await fs8.pathExists(tasksPath);
1697
1381
  const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
1698
1382
  let activeTask;
1699
1383
  let nextTodoTask;
@@ -1703,7 +1387,7 @@ async function parseFeature(featurePath, type, context, options) {
1703
1387
  let prFieldExists = false;
1704
1388
  let prStatusFieldExists = false;
1705
1389
  if (tasksExists) {
1706
- const content = await fs6.readFile(tasksPath, "utf-8");
1390
+ const content = await fs8.readFile(tasksPath, "utf-8");
1707
1391
  const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
1708
1392
  tasksSummary.total = summary.total;
1709
1393
  tasksSummary.todo = summary.todo;
@@ -1742,12 +1426,33 @@ async function parseFeature(featurePath, type, context, options) {
1742
1426
  if (tasksExists && (!prFieldExists || !prStatusFieldExists)) {
1743
1427
  warnings.push(tr(lang, "warnings", "legacyTasksPrFields"));
1744
1428
  }
1429
+ const implementationDone = tasksExists && tasksSummary.total > 0 && tasksSummary.total === tasksSummary.done && isCompletionChecklistDone2({ completionChecklist });
1430
+ const workflowDone = implementationDone && specStatus === "Approved" && planStatus === "Approved" && isPrMetadataConfigured2({ docs: { prFieldExists, prStatusFieldExists } }) && !!prLink && prStatus === "Approved";
1431
+ if (implementationDone && !workflowDone) {
1432
+ if (specStatus !== "Approved") {
1433
+ warnings.push(tr(lang, "warnings", "workflowSpecNotApproved"));
1434
+ }
1435
+ if (planStatus !== "Approved") {
1436
+ warnings.push(tr(lang, "warnings", "workflowPlanNotApproved"));
1437
+ }
1438
+ if (prFieldExists && prStatusFieldExists) {
1439
+ if (!prLink) warnings.push(tr(lang, "warnings", "workflowPrLinkMissing"));
1440
+ if (!prStatus) warnings.push(tr(lang, "warnings", "workflowPrStatusMissing"));
1441
+ if (prStatus && prStatus !== "Approved") {
1442
+ warnings.push(tr(lang, "warnings", "workflowPrStatusNotApproved"));
1443
+ }
1444
+ }
1445
+ }
1745
1446
  const featureState = {
1746
1447
  id,
1747
1448
  slug,
1748
1449
  folderName,
1749
1450
  type,
1750
1451
  path: featurePath,
1452
+ completion: {
1453
+ implementationDone,
1454
+ workflowDone
1455
+ },
1751
1456
  issueNumber,
1752
1457
  specStatus,
1753
1458
  planStatus,
@@ -1813,7 +1518,7 @@ async function scanFeatures(config) {
1813
1518
  ignore: ["**/feature-base/**"]
1814
1519
  });
1815
1520
  for (const dir of featureDirs) {
1816
- if ((await fs6.stat(dir)).isDirectory()) {
1521
+ if ((await fs8.stat(dir)).isDirectory()) {
1817
1522
  features.push(
1818
1523
  await parseFeature(
1819
1524
  dir,
@@ -1830,62 +1535,421 @@ async function scanFeatures(config) {
1830
1535
  )
1831
1536
  );
1832
1537
  }
1833
- }
1834
- } else {
1835
- const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
1836
- const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
1837
- for (const dir of feDirs) {
1838
- if ((await fs6.stat(dir)).isDirectory()) {
1839
- features.push(
1840
- await parseFeature(
1841
- dir,
1842
- "fe",
1843
- {
1844
- projectBranch: projectBranches.fe,
1845
- docsBranch,
1846
- docsGitCwd: config.docsDir,
1847
- projectGitCwd: feProject?.cwd ?? void 0,
1848
- docsDir: config.docsDir,
1849
- projectBranchAvailable: Boolean(feProject?.cwd)
1850
- },
1851
- { lang: config.lang, stepDefinitions }
1852
- )
1853
- );
1538
+ }
1539
+ } else {
1540
+ const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
1541
+ const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
1542
+ for (const dir of feDirs) {
1543
+ if ((await fs8.stat(dir)).isDirectory()) {
1544
+ features.push(
1545
+ await parseFeature(
1546
+ dir,
1547
+ "fe",
1548
+ {
1549
+ projectBranch: projectBranches.fe,
1550
+ docsBranch,
1551
+ docsGitCwd: config.docsDir,
1552
+ projectGitCwd: feProject?.cwd ?? void 0,
1553
+ docsDir: config.docsDir,
1554
+ projectBranchAvailable: Boolean(feProject?.cwd)
1555
+ },
1556
+ { lang: config.lang, stepDefinitions }
1557
+ )
1558
+ );
1559
+ }
1560
+ }
1561
+ for (const dir of beDirs) {
1562
+ if ((await fs8.stat(dir)).isDirectory()) {
1563
+ features.push(
1564
+ await parseFeature(
1565
+ dir,
1566
+ "be",
1567
+ {
1568
+ projectBranch: projectBranches.be,
1569
+ docsBranch,
1570
+ docsGitCwd: config.docsDir,
1571
+ projectGitCwd: beProject?.cwd ?? void 0,
1572
+ docsDir: config.docsDir,
1573
+ projectBranchAvailable: Boolean(beProject?.cwd)
1574
+ },
1575
+ { lang: config.lang, stepDefinitions }
1576
+ )
1577
+ );
1578
+ }
1579
+ }
1580
+ }
1581
+ return {
1582
+ features,
1583
+ branches: {
1584
+ docs: docsBranch,
1585
+ project: config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
1586
+ },
1587
+ warnings
1588
+ };
1589
+ }
1590
+
1591
+ // src/commands/status.ts
1592
+ function statusCommand(program2) {
1593
+ program2.command("status").description("Show feature status").option("-w, --write", "Write status.md file").option("-s, --strict", "Fail on missing/duplicate feature IDs").action(async (options) => {
1594
+ try {
1595
+ await runStatus(options);
1596
+ } catch (error) {
1597
+ console.error(chalk6.red("\uC624\uB958:"), error);
1598
+ process.exit(1);
1599
+ }
1600
+ });
1601
+ }
1602
+ async function runStatus(options) {
1603
+ const cwd = process.cwd();
1604
+ const config = await getConfig(cwd);
1605
+ if (!config) {
1606
+ console.error(
1607
+ chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
1608
+ );
1609
+ process.exit(1);
1610
+ }
1611
+ const { docsDir, projectType, projectName } = config;
1612
+ const featuresDir = path4.join(docsDir, "features");
1613
+ const scan = await scanFeatures(config);
1614
+ const features = [];
1615
+ const idMap = /* @__PURE__ */ new Map();
1616
+ for (const f of scan.features) {
1617
+ if (!f.docs.specExists || !f.docs.tasksExists) continue;
1618
+ const id = f.id || "UNKNOWN";
1619
+ const name = await getFeatureNameFromSpec(f.path, f.slug, f.folderName);
1620
+ const repo = projectType === "fullstack" ? `${projectName ?? "{{projectName}}"}-${f.type === "single" ? "" : f.type}`.replace(
1621
+ /-$/,
1622
+ ""
1623
+ ) : projectName ?? "{{projectName}}";
1624
+ const issue = f.issueNumber ? `#${f.issueNumber}` : "-";
1625
+ const relPath = path4.relative(docsDir, f.path);
1626
+ if (!idMap.has(id)) idMap.set(id, []);
1627
+ idMap.get(id).push(relPath);
1628
+ const total = f.tasks.total;
1629
+ const done = f.tasks.done;
1630
+ const doing = f.tasks.doing;
1631
+ const todo = f.tasks.todo;
1632
+ let status = "TODO";
1633
+ if (total > 0 && done === total) status = "DONE";
1634
+ else if (doing > 0) status = "DOING";
1635
+ else if (todo > 0) status = "TODO";
1636
+ else if (total === 0) status = "NO_TASKS";
1637
+ features.push({
1638
+ id,
1639
+ name,
1640
+ repo,
1641
+ issue,
1642
+ status,
1643
+ progress: `${done}/${total}`,
1644
+ path: relPath
1645
+ });
1646
+ }
1647
+ if (features.length === 0) {
1648
+ console.log(chalk6.yellow("Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
1649
+ return;
1650
+ }
1651
+ if (options.strict) {
1652
+ const duplicates = [...idMap.entries()].filter(
1653
+ ([, paths]) => paths.length > 1
1654
+ );
1655
+ if (duplicates.length > 0) {
1656
+ console.error(chalk6.red("\uC911\uBCF5 Feature ID \uBC1C\uACAC:"));
1657
+ for (const [id, paths] of duplicates) {
1658
+ console.error(chalk6.red(` ${id}:`));
1659
+ for (const p of paths) {
1660
+ console.error(chalk6.red(` - ${p}`));
1661
+ }
1662
+ }
1663
+ process.exit(1);
1664
+ }
1665
+ const unknowns = [...idMap.entries()].filter(([id]) => id === "UNKNOWN");
1666
+ if (unknowns.length > 0) {
1667
+ console.error(chalk6.red("Feature ID\uAC00 \uC5C6\uB294 \uD56D\uBAA9:"));
1668
+ for (const [, paths] of unknowns) {
1669
+ for (const p of paths) {
1670
+ console.error(chalk6.red(` - ${p}`));
1671
+ }
1672
+ }
1673
+ process.exit(1);
1674
+ }
1675
+ }
1676
+ features.sort((a, b) => a.id.localeCompare(b.id));
1677
+ const header = "| ID | Name | Repo | Issue | Status | Progress | Path |";
1678
+ const separator = "| --- | --- | --- | --- | --- | --- | --- |";
1679
+ console.log();
1680
+ console.log(header);
1681
+ console.log(separator);
1682
+ for (const f of features) {
1683
+ const statusColor = f.status === "DONE" ? chalk6.green : f.status === "DOING" ? chalk6.yellow : chalk6.gray;
1684
+ console.log(
1685
+ `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${statusColor(f.status)} | ${f.progress} | ${f.path} |`
1686
+ );
1687
+ }
1688
+ console.log();
1689
+ if (options.write) {
1690
+ const outputPath = path4.join(featuresDir, "status.md");
1691
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1692
+ const content = [
1693
+ "# Feature Status",
1694
+ "",
1695
+ `- Generated: ${date}`,
1696
+ "- Source: `tasks.md`, `spec.md`",
1697
+ "",
1698
+ header,
1699
+ separator,
1700
+ ...features.map(
1701
+ (f) => `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${f.status} | ${f.progress} | ${f.path} |`
1702
+ ),
1703
+ ""
1704
+ ].join("\n");
1705
+ await fs8.writeFile(outputPath, content, "utf-8");
1706
+ console.log(chalk6.green(`\u2705 ${outputPath} \uC0DD\uC131 \uC644\uB8CC`));
1707
+ }
1708
+ }
1709
+ function escapeRegExp2(value) {
1710
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1711
+ }
1712
+ async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderName) {
1713
+ try {
1714
+ const specPath = path4.join(featureDir, "spec.md");
1715
+ if (!await fs8.pathExists(specPath)) return fallbackSlug;
1716
+ const content = await fs8.readFile(specPath, "utf-8");
1717
+ const keys = ["\uAE30\uB2A5\uBA85", "Feature Name"];
1718
+ for (const key of keys) {
1719
+ const regex = new RegExp(
1720
+ `^\\s*-\\s*\\*\\*${escapeRegExp2(key)}\\*\\*\\s*:\\s*(.*)$`,
1721
+ "m"
1722
+ );
1723
+ const match = content.match(regex);
1724
+ const value = match?.[1]?.trim();
1725
+ if (value) return value;
1726
+ }
1727
+ } catch {
1728
+ }
1729
+ return fallbackSlug || fallbackFolderName;
1730
+ }
1731
+ function updateCommand(program2) {
1732
+ program2.command("update").description("Update docs templates to the latest version").option("--agents", "Update agents/ folder only").option("--templates", "Update feature-base/ folder only").option("-f, --force", "Force overwrite without confirmation").action(async (options) => {
1733
+ try {
1734
+ await runUpdate(options);
1735
+ } catch (error) {
1736
+ if (error instanceof Error && error.message === "canceled") {
1737
+ console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
1738
+ process.exit(0);
1739
+ }
1740
+ console.error(chalk6.red("\uC624\uB958:"), error);
1741
+ process.exit(1);
1742
+ }
1743
+ });
1744
+ }
1745
+ async function runUpdate(options) {
1746
+ const cwd = process.cwd();
1747
+ const config = await getConfig(cwd);
1748
+ if (!config) {
1749
+ console.error(
1750
+ chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
1751
+ );
1752
+ process.exit(1);
1753
+ }
1754
+ const { docsDir, projectType, lang } = config;
1755
+ const templatesDir = getTemplatesDir();
1756
+ const sourceDir = path4.join(templatesDir, lang, projectType);
1757
+ const updateAgents = options.agents || !options.agents && !options.templates;
1758
+ const updateTemplates = options.templates || !options.agents && !options.templates;
1759
+ console.log(chalk6.blue("\u{1F4E6} \uD15C\uD50C\uB9BF \uC5C5\uB370\uC774\uD2B8\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4..."));
1760
+ console.log(chalk6.gray(` - \uC5B8\uC5B4: ${lang}`));
1761
+ console.log(chalk6.gray(` - \uD0C0\uC785: ${projectType}`));
1762
+ console.log();
1763
+ let updatedCount = 0;
1764
+ if (updateAgents) {
1765
+ console.log(chalk6.blue("\u{1F4C1} agents/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
1766
+ const commonAgents = path4.join(templatesDir, lang, "common", "agents");
1767
+ const typeAgents = path4.join(templatesDir, lang, projectType, "agents");
1768
+ const targetAgents = path4.join(docsDir, "agents");
1769
+ const featurePath = projectType === "fullstack" ? "docs/features/{be|fe}" : "docs/features";
1770
+ const replacements = {
1771
+ "{{featurePath}}": featurePath
1772
+ };
1773
+ if (await fs8.pathExists(commonAgents)) {
1774
+ const count = await updateFolder(
1775
+ commonAgents,
1776
+ targetAgents,
1777
+ options.force,
1778
+ replacements
1779
+ );
1780
+ updatedCount += count;
1781
+ }
1782
+ if (await fs8.pathExists(typeAgents)) {
1783
+ const count = await updateFolder(typeAgents, targetAgents, options.force);
1784
+ updatedCount += count;
1785
+ }
1786
+ console.log(chalk6.green(` \u2705 agents/ \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
1787
+ }
1788
+ if (updateTemplates) {
1789
+ console.log(chalk6.blue("\u{1F4C1} features/feature-base/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
1790
+ const sourceFeatureBase = path4.join(sourceDir, "features", "feature-base");
1791
+ const targetFeatureBase = path4.join(docsDir, "features", "feature-base");
1792
+ if (await fs8.pathExists(sourceFeatureBase)) {
1793
+ const count = await updateFolder(
1794
+ sourceFeatureBase,
1795
+ targetFeatureBase,
1796
+ options.force
1797
+ );
1798
+ updatedCount += count;
1799
+ console.log(chalk6.green(` \u2705 ${count}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
1800
+ }
1801
+ }
1802
+ console.log();
1803
+ console.log(chalk6.green(`\u2705 \uCD1D ${updatedCount}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
1804
+ }
1805
+ async function updateFolder(sourceDir, targetDir, force, replacements) {
1806
+ const protectedFiles = /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
1807
+ await fs8.ensureDir(targetDir);
1808
+ const files = await fs8.readdir(sourceDir);
1809
+ let updatedCount = 0;
1810
+ for (const file of files) {
1811
+ const sourcePath = path4.join(sourceDir, file);
1812
+ const targetPath = path4.join(targetDir, file);
1813
+ const stat = await fs8.stat(sourcePath);
1814
+ if (stat.isFile()) {
1815
+ if (protectedFiles.has(file)) {
1816
+ continue;
1817
+ }
1818
+ let sourceContent = await fs8.readFile(sourcePath, "utf-8");
1819
+ if (replacements) {
1820
+ for (const [key, value] of Object.entries(replacements)) {
1821
+ sourceContent = sourceContent.replaceAll(key, value);
1822
+ }
1823
+ }
1824
+ let shouldUpdate = true;
1825
+ if (await fs8.pathExists(targetPath)) {
1826
+ const targetContent = await fs8.readFile(targetPath, "utf-8");
1827
+ if (sourceContent === targetContent) {
1828
+ continue;
1829
+ }
1830
+ if (!force) {
1831
+ console.log(
1832
+ chalk6.yellow(` \u26A0\uFE0F ${file} - \uBCC0\uACBD \uAC10\uC9C0 (--force\uB85C \uB36E\uC5B4\uC4F0\uAE30)`)
1833
+ );
1834
+ shouldUpdate = false;
1835
+ }
1836
+ }
1837
+ if (shouldUpdate) {
1838
+ await fs8.writeFile(targetPath, sourceContent);
1839
+ console.log(chalk6.gray(` \u{1F4C4} ${file} \uC5C5\uB370\uC774\uD2B8`));
1840
+ updatedCount++;
1854
1841
  }
1842
+ } else if (stat.isDirectory()) {
1843
+ const subCount = await updateFolder(
1844
+ sourcePath,
1845
+ targetPath,
1846
+ force,
1847
+ replacements
1848
+ );
1849
+ updatedCount += subCount;
1855
1850
  }
1856
- for (const dir of beDirs) {
1857
- if ((await fs6.stat(dir)).isDirectory()) {
1858
- features.push(
1859
- await parseFeature(
1860
- dir,
1861
- "be",
1862
- {
1863
- projectBranch: projectBranches.be,
1864
- docsBranch,
1865
- docsGitCwd: config.docsDir,
1866
- projectGitCwd: beProject?.cwd ?? void 0,
1867
- docsDir: config.docsDir,
1868
- projectBranchAvailable: Boolean(beProject?.cwd)
1869
- },
1870
- { lang: config.lang, stepDefinitions }
1871
- )
1872
- );
1851
+ }
1852
+ return updatedCount;
1853
+ }
1854
+ function configCommand(program2) {
1855
+ program2.command("config").description("View or modify project configuration").option("--project-root <path>", "Set project root path").option("--repo <repo>", "Repository type for fullstack: fe | be").action(async (options) => {
1856
+ try {
1857
+ await runConfig(options);
1858
+ } catch (error) {
1859
+ if (error instanceof Error && error.message === "canceled") {
1860
+ console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
1861
+ process.exit(0);
1873
1862
  }
1863
+ console.error(chalk6.red("\uC624\uB958:"), error);
1864
+ process.exit(1);
1874
1865
  }
1866
+ });
1867
+ }
1868
+ async function runConfig(options) {
1869
+ const cwd = process.cwd();
1870
+ const config = await getConfig(cwd);
1871
+ if (!config) {
1872
+ console.log(
1873
+ chalk6.red("\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.")
1874
+ );
1875
+ process.exit(1);
1875
1876
  }
1876
- return {
1877
- features,
1878
- branches: {
1879
- docs: docsBranch,
1880
- project: config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
1881
- },
1882
- warnings
1883
- };
1877
+ const configPath = path4.join(config.docsDir, ".lee-spec-kit.json");
1878
+ if (!options.projectRoot) {
1879
+ console.log();
1880
+ console.log(chalk6.blue("\u{1F4CB} \uD604\uC7AC \uC124\uC815:"));
1881
+ console.log();
1882
+ console.log(chalk6.gray(` \uACBD\uB85C: ${configPath}`));
1883
+ console.log();
1884
+ const configFile2 = await fs8.readJson(configPath);
1885
+ console.log(JSON.stringify(configFile2, null, 2));
1886
+ console.log();
1887
+ return;
1888
+ }
1889
+ const configFile = await fs8.readJson(configPath);
1890
+ if (configFile.docsRepo !== "standalone") {
1891
+ console.log(
1892
+ chalk6.yellow("\u26A0\uFE0F projectRoot\uB294 standalone \uBAA8\uB4DC\uC5D0\uC11C\uB9CC \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.")
1893
+ );
1894
+ return;
1895
+ }
1896
+ const projectType = configFile.projectType;
1897
+ if (projectType === "fullstack") {
1898
+ if (!options.repo) {
1899
+ const response = await prompts(
1900
+ [
1901
+ {
1902
+ type: "select",
1903
+ name: "repo",
1904
+ message: "\uC218\uC815\uD560 \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
1905
+ choices: [
1906
+ { title: "Frontend (fe)", value: "fe" },
1907
+ { title: "Backend (be)", value: "be" }
1908
+ ]
1909
+ }
1910
+ ],
1911
+ {
1912
+ onCancel: () => {
1913
+ throw new Error("canceled");
1914
+ }
1915
+ }
1916
+ );
1917
+ options.repo = response.repo;
1918
+ }
1919
+ if (!options.repo || !["fe", "be"].includes(options.repo)) {
1920
+ console.log(
1921
+ chalk6.red(
1922
+ "Fullstack \uD504\uB85C\uC81D\uD2B8\uB294 --repo fe \uB610\uB294 --repo be\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."
1923
+ )
1924
+ );
1925
+ return;
1926
+ }
1927
+ const currentRoot = configFile.projectRoot || { fe: "", be: "" };
1928
+ if (typeof currentRoot === "string") {
1929
+ configFile.projectRoot = {
1930
+ fe: options.repo === "fe" ? options.projectRoot : "",
1931
+ be: options.repo === "be" ? options.projectRoot : ""
1932
+ };
1933
+ } else {
1934
+ currentRoot[options.repo] = options.projectRoot;
1935
+ configFile.projectRoot = currentRoot;
1936
+ }
1937
+ console.log(
1938
+ chalk6.green(
1939
+ `\u2705 ${options.repo.toUpperCase()} projectRoot \uC124\uC815 \uC644\uB8CC: ${options.projectRoot}`
1940
+ )
1941
+ );
1942
+ } else {
1943
+ configFile.projectRoot = options.projectRoot;
1944
+ console.log(
1945
+ chalk6.green(`\u2705 projectRoot \uC124\uC815 \uC644\uB8CC: ${options.projectRoot}`)
1946
+ );
1947
+ }
1948
+ await fs8.writeJson(configPath, configFile, { spaces: 2 });
1949
+ console.log();
1884
1950
  }
1885
-
1886
- // src/commands/context.ts
1887
1951
  function contextCommand(program2) {
1888
- program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Repository type for fullstack: fe | be").action(
1952
+ program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Repository type for fullstack: fe | be").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").action(
1889
1953
  async (featureName, options) => {
1890
1954
  try {
1891
1955
  await runContext(featureName, options);
@@ -1931,12 +1995,22 @@ async function runContext(featureName, options) {
1931
1995
  const stepDefinitions = getStepDefinitions(lang);
1932
1996
  const stepsMap = getStepsMap(lang);
1933
1997
  const { features, branches, warnings } = await scanFeatures(config);
1998
+ const doneFeatures = features.filter((f2) => f2.completion.workflowDone);
1999
+ const openFeatures = features.filter((f2) => !f2.completion.workflowDone);
2000
+ const inProgressFeatures = openFeatures.filter(
2001
+ (f2) => !f2.completion.implementationDone
2002
+ );
2003
+ const readyToCloseFeatures = openFeatures.filter(
2004
+ (f2) => f2.completion.implementationDone
2005
+ );
1934
2006
  let targetFeatures = [];
2007
+ let selectionMode = "explicit";
1935
2008
  if (featureName) {
1936
2009
  targetFeatures = features.filter((f2) => matchesFeatureSelector(f2, featureName));
1937
2010
  if (options.repo) {
1938
2011
  targetFeatures = targetFeatures.filter((f2) => f2.type === options.repo);
1939
2012
  }
2013
+ selectionMode = "explicit";
1940
2014
  } else {
1941
2015
  if (config.projectType === "single") {
1942
2016
  const branchName = branches.project.single || "";
@@ -1958,15 +2032,33 @@ async function runContext(featureName, options) {
1958
2032
  ) : [];
1959
2033
  targetFeatures = [...feMatches, ...beMatches];
1960
2034
  }
1961
- if (targetFeatures.length === 0) targetFeatures = features;
2035
+ if (targetFeatures.length > 0) {
2036
+ selectionMode = "branch";
2037
+ } else if (options.all) {
2038
+ targetFeatures = features;
2039
+ selectionMode = "all";
2040
+ } else if (options.done) {
2041
+ targetFeatures = doneFeatures;
2042
+ selectionMode = "done";
2043
+ } else {
2044
+ targetFeatures = openFeatures;
2045
+ selectionMode = "open";
2046
+ }
1962
2047
  }
1963
2048
  if (options.json) {
2049
+ const isNoOpen = selectionMode === "open" && features.length > 0 && openFeatures.length === 0;
1964
2050
  const result = {
1965
- status: features.length === 0 ? "no_features" : targetFeatures.length === 1 ? "single_matched" : targetFeatures.length > 1 ? "multiple_active" : "no_match",
2051
+ status: features.length === 0 ? "no_features" : isNoOpen ? "no_open" : targetFeatures.length === 1 ? "single_matched" : targetFeatures.length > 1 ? "multiple_active" : "no_match",
2052
+ selectionMode,
1966
2053
  branches,
1967
2054
  warnings,
1968
2055
  matchedFeature: targetFeatures.length === 1 ? targetFeatures[0] : null,
1969
2056
  candidates: targetFeatures.length > 1 ? targetFeatures : [],
2057
+ // "Completed" now means workflow-done.
2058
+ completedCandidates: selectionMode === "open" ? doneFeatures : [],
2059
+ openCandidates: selectionMode === "open" ? openFeatures : [],
2060
+ inProgressCandidates: selectionMode === "open" ? inProgressFeatures : [],
2061
+ readyToCloseCandidates: selectionMode === "open" ? readyToCloseFeatures : [],
1970
2062
  actions: targetFeatures.length === 1 ? targetFeatures[0].actions : [],
1971
2063
  recommendation: ""
1972
2064
  };
@@ -2022,22 +2114,53 @@ async function runContext(featureName, options) {
2022
2114
  console.log();
2023
2115
  }
2024
2116
  if (targetFeatures.length > 1) {
2025
- console.log(
2026
- chalk6.blue(`\u{1F539} ${targetFeatures.length} Active Features Detected:`)
2027
- );
2028
- console.log();
2029
- targetFeatures.forEach((f2) => {
2030
- const stepName2 = stepsMap[f2.currentStep] || "Unknown";
2031
- const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
2117
+ if (selectionMode === "open") {
2032
2118
  console.log(
2033
- ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
2119
+ chalk6.gray(
2120
+ ` (\uBE0C\uB79C\uCE58\uB85C Feature\uB97C \uD2B9\uC815\uD558\uC9C0 \uBABB\uD574 \uBBF8\uC644\uB8CC Feature\uB9CC \uD45C\uC2DC\uD569\uB2C8\uB2E4. \uC9C4\uD589 \uC911: ${inProgressFeatures.length}\uAC1C / \uC885\uB8CC \uB300\uAE30: ${readyToCloseFeatures.length}\uAC1C / \uC644\uB8CC: ${doneFeatures.length}\uAC1C)`
2121
+ )
2034
2122
  );
2035
- });
2123
+ console.log();
2124
+ }
2125
+ if (selectionMode === "open") {
2126
+ console.log(chalk6.blue(`\u{1F539} In Progress (${inProgressFeatures.length})`));
2127
+ inProgressFeatures.forEach((f2) => {
2128
+ const stepName2 = stepsMap[f2.currentStep] || "Unknown";
2129
+ const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
2130
+ console.log(
2131
+ ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
2132
+ );
2133
+ });
2134
+ console.log();
2135
+ console.log(chalk6.blue(`\u{1F538} Ready To Close (${readyToCloseFeatures.length})`));
2136
+ readyToCloseFeatures.forEach((f2) => {
2137
+ const stepName2 = stepsMap[f2.currentStep] || "Unknown";
2138
+ const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
2139
+ console.log(
2140
+ ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
2141
+ );
2142
+ });
2143
+ } else {
2144
+ const title = selectionMode === "all" ? `\u{1F539} ${targetFeatures.length} Features:` : selectionMode === "done" ? `\u{1F539} ${targetFeatures.length} Done Features:` : `\u{1F539} ${targetFeatures.length} Features Detected:`;
2145
+ console.log(chalk6.blue(title));
2146
+ console.log();
2147
+ targetFeatures.forEach((f2) => {
2148
+ const stepName2 = stepsMap[f2.currentStep] || "Unknown";
2149
+ const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
2150
+ console.log(
2151
+ ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
2152
+ );
2153
+ });
2154
+ }
2036
2155
  console.log();
2037
2156
  console.log(chalk6.gray("Tip: \uD2B9\uC815 Feature\uC758 \uC0C1\uC138 \uC815\uBCF4\uB97C \uBCF4\uB824\uBA74:"));
2038
2157
  console.log(
2039
2158
  chalk6.gray(" $ npx lee-spec-kit context <slug|F001|F001-slug> [--repo fe|be]")
2040
2159
  );
2160
+ if (selectionMode === "open") {
2161
+ console.log(chalk6.gray(" $ npx lee-spec-kit context --all # \uC804\uCCB4 \uBCF4\uAE30"));
2162
+ console.log(chalk6.gray(" $ npx lee-spec-kit context --done # \uC644\uB8CC\uB9CC \uBCF4\uAE30"));
2163
+ }
2041
2164
  console.log();
2042
2165
  return;
2043
2166
  }
@@ -2047,6 +2170,9 @@ async function runContext(featureName, options) {
2047
2170
  console.log(
2048
2171
  `\u{1F539} Feature: ${chalk6.bold(f.folderName)} ${config.projectType === "fullstack" ? chalk6.cyan(`(${f.type})`) : ""}`
2049
2172
  );
2173
+ console.log(
2174
+ ` \u2022 Completion: ${f.completion.implementationDone ? chalk6.green("Implementation \u2705") : chalk6.gray("Implementation \u25EF")} / ${f.completion.workflowDone ? chalk6.green("Workflow \u2705") : chalk6.yellow("Workflow \u25EF")}`
2175
+ );
2050
2176
  if (f.issueNumber) {
2051
2177
  console.log(` \u2022 Issue: #${f.issueNumber}`);
2052
2178
  }
@@ -2114,13 +2240,276 @@ function printChecklist(f, stepDefinitions) {
2114
2240
  console.log(` ${mark} ${definition.step}. ${label} ${detail}`);
2115
2241
  });
2116
2242
  }
2243
+ function msg(lang, ko, en) {
2244
+ return lang === "en" ? en : ko;
2245
+ }
2246
+ function formatPath(cwd, p) {
2247
+ if (!p) return "";
2248
+ return path4.isAbsolute(p) ? path4.relative(cwd, p) : p;
2249
+ }
2250
+ function detectPlaceholders(content) {
2251
+ const patterns = [
2252
+ { key: "{{projectName}}", re: /\{\{projectName\}\}/g },
2253
+ { key: "{{date}}", re: /\{\{date\}\}/g },
2254
+ { key: "{{featurePath}}", re: /\{\{featurePath\}\}/g },
2255
+ { key: "{{description}}", re: /\{\{description\}\}/g },
2256
+ { key: "{\uAE30\uB2A5\uBA85}", re: /\{기능명\}/g },
2257
+ { key: "{\uBC88\uD638}", re: /\{번호\}/g },
2258
+ { key: "{\uC774\uC288\uBC88\uD638}", re: /\{이슈번호\}/g },
2259
+ { key: "{feature-name}", re: /\{feature-name\}/g },
2260
+ { key: "{number}", re: /\{number\}/g },
2261
+ { key: "{issue-number}", re: /\{issue-number\}/g },
2262
+ { key: "{be|fe}", re: /\{be\|fe\}/g },
2263
+ { key: "YYYY-MM-DD", re: /\bYYYY-MM-DD\b/g }
2264
+ ];
2265
+ const hits = [];
2266
+ for (const { key, re } of patterns) {
2267
+ if (re.test(content)) hits.push(key);
2268
+ }
2269
+ return hits;
2270
+ }
2271
+ async function checkDocsStructure(config, cwd) {
2272
+ const issues = [];
2273
+ const requiredDirs = ["agents", "features", "prd", "designs", "ideas"];
2274
+ for (const dir of requiredDirs) {
2275
+ const p = path4.join(config.docsDir, dir);
2276
+ if (!await fs8.pathExists(p)) {
2277
+ issues.push({
2278
+ level: "error",
2279
+ code: "missing_dir",
2280
+ message: msg(
2281
+ config.lang,
2282
+ `\uD544\uC218 \uD3F4\uB354\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4: ${dir}`,
2283
+ `Missing required directory: ${dir}`
2284
+ ),
2285
+ path: formatPath(cwd, p)
2286
+ });
2287
+ }
2288
+ }
2289
+ const configPath = path4.join(config.docsDir, ".lee-spec-kit.json");
2290
+ if (!await fs8.pathExists(configPath)) {
2291
+ issues.push({
2292
+ level: "warn",
2293
+ code: "missing_config",
2294
+ message: msg(
2295
+ config.lang,
2296
+ "\uC124\uC815 \uD30C\uC77C(.lee-spec-kit.json)\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uC77C\uBD80 \uAE30\uB2A5\uC774 \uD3F4\uB354 \uAD6C\uC870 \uCD94\uC815\uC73C\uB85C \uB3D9\uC791\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
2297
+ "Missing .lee-spec-kit.json. Some commands may rely on folder-structure heuristics."
2298
+ ),
2299
+ path: formatPath(cwd, configPath)
2300
+ });
2301
+ }
2302
+ return issues;
2303
+ }
2304
+ async function checkFeatures(config, cwd, features) {
2305
+ const issues = [];
2306
+ if (features.length === 0) {
2307
+ issues.push({
2308
+ level: "warn",
2309
+ code: "no_features",
2310
+ message: msg(
2311
+ config.lang,
2312
+ "Feature \uD3F4\uB354\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. (feature-base\uB9CC \uC874\uC7AC\uD558\uAC70\uB098 \uC544\uC9C1 feature\uB97C \uB9CC\uB4E4\uC9C0 \uC54A\uC558\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.)",
2313
+ "No feature folders found. (Only feature-base exists, or no features created yet.)"
2314
+ )
2315
+ });
2316
+ return issues;
2317
+ }
2318
+ const idMap = /* @__PURE__ */ new Map();
2319
+ for (const f of features) {
2320
+ const rel = f.docs.featurePathFromDocs || path4.relative(config.docsDir, f.path);
2321
+ const id = f.id || "UNKNOWN";
2322
+ if (!idMap.has(id)) idMap.set(id, []);
2323
+ idMap.get(id).push(rel);
2324
+ const featureDocs = ["spec.md", "plan.md", "tasks.md", "decisions.md"];
2325
+ for (const file of featureDocs) {
2326
+ const p = path4.join(f.path, file);
2327
+ if (!await fs8.pathExists(p)) continue;
2328
+ const content = await fs8.readFile(p, "utf-8");
2329
+ const placeholders = detectPlaceholders(content);
2330
+ if (placeholders.length === 0) continue;
2331
+ issues.push({
2332
+ level: "warn",
2333
+ code: "placeholder_left",
2334
+ message: msg(
2335
+ config.lang,
2336
+ `\uD50C\uB808\uC774\uC2A4\uD640\uB354\uAC00 \uB0A8\uC544\uC788\uC2B5\uB2C8\uB2E4: ${placeholders.join(", ")}`,
2337
+ `Leftover placeholders detected: ${placeholders.join(", ")}`
2338
+ ),
2339
+ path: formatPath(cwd, p)
2340
+ });
2341
+ }
2342
+ if (!f.docs.specExists) {
2343
+ issues.push({
2344
+ level: "warn",
2345
+ code: "missing_spec",
2346
+ message: msg(
2347
+ config.lang,
2348
+ "spec.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
2349
+ "Missing spec.md."
2350
+ ),
2351
+ path: formatPath(cwd, f.path)
2352
+ });
2353
+ } else if (!f.specStatus) {
2354
+ issues.push({
2355
+ level: "warn",
2356
+ code: "spec_status_unset",
2357
+ message: msg(
2358
+ config.lang,
2359
+ "spec.md\uC758 Status(\uC0C1\uD0DC)\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (\uD15C\uD50C\uB9BF \uADF8\uB300\uB85C\uC77C \uC218 \uC788\uC74C)",
2360
+ "spec.md Status is not set. (May still be a template)"
2361
+ ),
2362
+ path: formatPath(cwd, path4.join(f.path, "spec.md"))
2363
+ });
2364
+ }
2365
+ if (f.docs.planExists && !f.planStatus) {
2366
+ issues.push({
2367
+ level: "warn",
2368
+ code: "plan_status_unset",
2369
+ message: msg(
2370
+ config.lang,
2371
+ "plan.md\uC758 Status(\uC0C1\uD0DC)\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (\uD15C\uD50C\uB9BF \uADF8\uB300\uB85C\uC77C \uC218 \uC788\uC74C)",
2372
+ "plan.md Status is not set. (May still be a template)"
2373
+ ),
2374
+ path: formatPath(cwd, path4.join(f.path, "plan.md"))
2375
+ });
2376
+ }
2377
+ if (f.docs.tasksExists && f.tasks.total === 0) {
2378
+ issues.push({
2379
+ level: "warn",
2380
+ code: "tasks_empty",
2381
+ message: msg(
2382
+ config.lang,
2383
+ "tasks.md\uC5D0 \uD0DC\uC2A4\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
2384
+ "tasks.md has no tasks."
2385
+ ),
2386
+ path: formatPath(cwd, path4.join(f.path, "tasks.md"))
2387
+ });
2388
+ }
2389
+ }
2390
+ const duplicates = [...idMap.entries()].filter(
2391
+ ([id, paths]) => id !== "UNKNOWN" && paths.length > 1
2392
+ );
2393
+ for (const [id, paths] of duplicates) {
2394
+ issues.push({
2395
+ level: "warn",
2396
+ code: "duplicate_feature_id",
2397
+ message: msg(
2398
+ config.lang,
2399
+ `\uC911\uBCF5 Feature ID \uAC10\uC9C0: ${id} (${paths.length}\uAC1C)`,
2400
+ `Duplicate Feature ID detected: ${id} (${paths.length})`
2401
+ ),
2402
+ path: formatPath(cwd, paths[0])
2403
+ });
2404
+ }
2405
+ const unknowns = idMap.get("UNKNOWN") || [];
2406
+ for (const p of unknowns) {
2407
+ issues.push({
2408
+ level: "warn",
2409
+ code: "missing_feature_id",
2410
+ message: msg(
2411
+ config.lang,
2412
+ "Feature \uD3F4\uB354\uBA85\uC774 F001-... \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4. (ID\uB97C \uCD94\uCD9C\uD560 \uC218 \uC5C6\uC74C)",
2413
+ "Feature folder name is not in F001-... format. (Cannot extract ID)"
2414
+ ),
2415
+ path: formatPath(cwd, path4.join(config.docsDir, p))
2416
+ });
2417
+ }
2418
+ return issues;
2419
+ }
2420
+ function doctorCommand(program2) {
2421
+ program2.command("doctor").description("Validate docs structure and feature metadata").option("--json", "Output in JSON format for agents").option("-s, --strict", "Exit with non-zero code when issues are found").action(async (options) => {
2422
+ const cwd = process.cwd();
2423
+ const config = await getConfig(cwd);
2424
+ if (!config) {
2425
+ const message = "\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.";
2426
+ if (options.json) {
2427
+ console.log(JSON.stringify({ status: "error", error: message }, null, 2));
2428
+ } else {
2429
+ console.error(chalk6.red("\uC624\uB958:"), message);
2430
+ }
2431
+ process.exit(1);
2432
+ }
2433
+ const { docsDir, projectType, lang } = config;
2434
+ const { features, branches, warnings } = await scanFeatures(config);
2435
+ const issues = [];
2436
+ issues.push(...await checkDocsStructure({ docsDir, lang }, cwd));
2437
+ issues.push(...await checkFeatures({ docsDir, lang }, cwd, features));
2438
+ const hasIssues = issues.length > 0;
2439
+ const hasErrors = issues.some((i) => i.level === "error");
2440
+ const exitCode = options.strict && hasIssues ? 1 : 0;
2441
+ if (options.json) {
2442
+ console.log(
2443
+ JSON.stringify(
2444
+ {
2445
+ status: hasErrors ? "error" : hasIssues ? "warn" : "ok",
2446
+ meta: { docsDir, projectType, lang },
2447
+ branches,
2448
+ warnings,
2449
+ counts: {
2450
+ features: features.length,
2451
+ issues: issues.length,
2452
+ errors: issues.filter((i) => i.level === "error").length,
2453
+ warnings: issues.filter((i) => i.level === "warn").length
2454
+ },
2455
+ issues
2456
+ },
2457
+ null,
2458
+ 2
2459
+ )
2460
+ );
2461
+ process.exit(exitCode);
2462
+ }
2463
+ console.log();
2464
+ console.log(chalk6.bold("\u{1F50E} Docs Doctor"));
2465
+ console.log(chalk6.gray(`- Docs: ${path4.relative(cwd, docsDir)}`));
2466
+ console.log(chalk6.gray(`- Type: ${projectType}`));
2467
+ console.log(chalk6.gray(`- Lang: ${lang}`));
2468
+ console.log();
2469
+ if (warnings.length > 0) {
2470
+ console.log(chalk6.yellow("\u26A0\uFE0F Environment warnings:"));
2471
+ warnings.forEach((w) => console.log(chalk6.yellow(` - ${w}`)));
2472
+ console.log();
2473
+ }
2474
+ if (!hasIssues) {
2475
+ console.log(chalk6.green("\u2705 \uBB38\uC81C\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."));
2476
+ console.log();
2477
+ process.exit(0);
2478
+ }
2479
+ const errors = issues.filter((i) => i.level === "error");
2480
+ const warns = issues.filter((i) => i.level === "warn");
2481
+ if (errors.length > 0) {
2482
+ console.log(chalk6.red(`\u274C Errors (${errors.length})`));
2483
+ errors.forEach(
2484
+ (i) => console.log(chalk6.red(` - ${i.message}${i.path ? ` (${i.path})` : ""}`))
2485
+ );
2486
+ console.log();
2487
+ }
2488
+ if (warns.length > 0) {
2489
+ console.log(chalk6.yellow(`\u26A0\uFE0F Warnings (${warns.length})`));
2490
+ warns.forEach(
2491
+ (i) => console.log(
2492
+ chalk6.yellow(` - ${i.message}${i.path ? ` (${i.path})` : ""}`)
2493
+ )
2494
+ );
2495
+ console.log();
2496
+ }
2497
+ console.log(
2498
+ chalk6.gray(
2499
+ `Tip: \uC5D0\uC774\uC804\uD2B8\uC6A9 JSON \uCD9C\uB825: npx lee-spec-kit doctor --json${options.strict ? " --strict" : ""}`
2500
+ )
2501
+ );
2502
+ console.log();
2503
+ process.exit(exitCode);
2504
+ });
2505
+ }
2117
2506
  var CACHE_FILE = path4.join(os.homedir(), ".lee-spec-kit-version-cache.json");
2118
2507
  var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
2119
2508
  function getCurrentVersion() {
2120
2509
  try {
2121
2510
  const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
2122
- if (fs6.existsSync(packageJsonPath)) {
2123
- const pkg = fs6.readJsonSync(packageJsonPath);
2511
+ if (fs8.existsSync(packageJsonPath)) {
2512
+ const pkg = fs8.readJsonSync(packageJsonPath);
2124
2513
  return pkg.version;
2125
2514
  }
2126
2515
  } catch {
@@ -2129,8 +2518,8 @@ function getCurrentVersion() {
2129
2518
  }
2130
2519
  function readCache() {
2131
2520
  try {
2132
- if (fs6.existsSync(CACHE_FILE)) {
2133
- return fs6.readJsonSync(CACHE_FILE);
2521
+ if (fs8.existsSync(CACHE_FILE)) {
2522
+ return fs8.readJsonSync(CACHE_FILE);
2134
2523
  }
2135
2524
  } catch {
2136
2525
  }
@@ -2194,12 +2583,23 @@ function checkForUpdates() {
2194
2583
  }
2195
2584
 
2196
2585
  // src/index.ts
2197
- checkForUpdates();
2586
+ function shouldCheckForUpdates() {
2587
+ const argv = process.argv.slice(2);
2588
+ const hasJsonFlag = argv.includes("--json");
2589
+ const isHelpOrVersion = argv.includes("--help") || argv.includes("-h") || argv.includes("--version") || argv.includes("-V");
2590
+ const disabledByEnv = (process.env.LSK_NO_UPDATE_CHECK || "").trim() === "1" || (process.env.LEE_SPEC_KIT_NO_UPDATE_CHECK || "").trim() === "1";
2591
+ if (hasJsonFlag) return false;
2592
+ if (!process.stdout.isTTY) return false;
2593
+ if (isHelpOrVersion) return false;
2594
+ if (disabledByEnv) return false;
2595
+ return true;
2596
+ }
2597
+ if (shouldCheckForUpdates()) checkForUpdates();
2198
2598
  function getCliVersion() {
2199
2599
  try {
2200
2600
  const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
2201
- if (fs6.existsSync(packageJsonPath)) {
2202
- const pkg = fs6.readJsonSync(packageJsonPath);
2601
+ if (fs8.existsSync(packageJsonPath)) {
2602
+ const pkg = fs8.readJsonSync(packageJsonPath);
2203
2603
  if (pkg?.version) return String(pkg.version);
2204
2604
  }
2205
2605
  } catch {
@@ -2215,4 +2615,5 @@ statusCommand(program);
2215
2615
  updateCommand(program);
2216
2616
  configCommand(program);
2217
2617
  contextCommand(program);
2218
- program.parse();
2618
+ doctorCommand(program);
2619
+ await program.parseAsync();