lee-spec-kit 0.4.2 → 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,394 +669,20 @@ 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
- const match = entry.name.match(/^F(\d+)-/);
651
- if (match) {
652
- const num = parseInt(match[1], 10);
653
- if (num > max) max = num;
654
- }
655
- }
656
- }
657
- const next = max + 1;
658
- const width = Math.max(3, String(next).length);
659
- return `F${String(next).padStart(width, "0")}`;
660
- }
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
- );
676
+ const match = entry.name.match(/^F(\d+)-/);
677
+ if (match) {
678
+ const num = parseInt(match[1], 10);
679
+ if (num > max) max = num;
680
+ }
681
+ }
1031
682
  }
1032
- await fs6.writeJson(configPath, configFile, { spaces: 2 });
1033
- console.log();
683
+ const next = max + 1;
684
+ const width = Math.max(3, String(next).length);
685
+ return `F${String(next).padStart(width, "0")}`;
1034
686
  }
1035
687
 
1036
688
  // src/utils/context/i18n.ts
@@ -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: {
@@ -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 [
@@ -1323,7 +990,7 @@ function getStepDefinitions(lang) {
1323
990
  name: tr(lang, "steps", "branchCreate"),
1324
991
  checklist: { done: (f) => f.git.onExpectedBranch },
1325
992
  current: {
1326
- 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),
1327
994
  actions: (f) => {
1328
995
  if (!f.git.projectBranchAvailable || !f.git.projectGitCwd) {
1329
996
  return [
@@ -1444,6 +1111,15 @@ function getStepDefinitions(lang) {
1444
1111
  current: {
1445
1112
  when: (f) => isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
1446
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
+ }
1447
1123
  if (f.pr.status === "Review") {
1448
1124
  return [
1449
1125
  {
@@ -1589,7 +1265,7 @@ function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
1589
1265
  function escapeRegExp(value) {
1590
1266
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1591
1267
  }
1592
- function extractSpecValue2(content, key) {
1268
+ function extractSpecValue(content, key) {
1593
1269
  const regex = new RegExp(
1594
1270
  `^\\s*-\\s*\\*\\*${escapeRegExp(key)}\\*\\*\\s*:\\s*(.*)$`,
1595
1271
  "m"
@@ -1599,7 +1275,7 @@ function extractSpecValue2(content, key) {
1599
1275
  }
1600
1276
  function extractFirstSpecValue(content, keys) {
1601
1277
  for (const key of keys) {
1602
- const value = extractSpecValue2(content, key);
1278
+ const value = extractSpecValue(content, key);
1603
1279
  if (value) return value;
1604
1280
  }
1605
1281
  return void 0;
@@ -1669,6 +1345,12 @@ function parseCompletionChecklist(content) {
1669
1345
  }
1670
1346
  return total > 0 ? { total, checked } : void 0;
1671
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
+ }
1672
1354
  async function parseFeature(featurePath, type, context, options) {
1673
1355
  const lang = options.lang;
1674
1356
  const folderName = path4.basename(featurePath);
@@ -1680,22 +1362,22 @@ async function parseFeature(featurePath, type, context, options) {
1680
1362
  const tasksPath = path4.join(featurePath, "tasks.md");
1681
1363
  let specStatus;
1682
1364
  let issueNumber;
1683
- const specExists = await fs6.pathExists(specPath);
1365
+ const specExists = await fs8.pathExists(specPath);
1684
1366
  if (specExists) {
1685
- const content = await fs6.readFile(specPath, "utf-8");
1367
+ const content = await fs8.readFile(specPath, "utf-8");
1686
1368
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
1687
1369
  specStatus = parseDocStatus(statusValue);
1688
1370
  const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
1689
1371
  issueNumber = parseIssueNumber(issueValue);
1690
1372
  }
1691
1373
  let planStatus;
1692
- const planExists = await fs6.pathExists(planPath);
1374
+ const planExists = await fs8.pathExists(planPath);
1693
1375
  if (planExists) {
1694
- const content = await fs6.readFile(planPath, "utf-8");
1376
+ const content = await fs8.readFile(planPath, "utf-8");
1695
1377
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
1696
1378
  planStatus = parseDocStatus(statusValue);
1697
1379
  }
1698
- const tasksExists = await fs6.pathExists(tasksPath);
1380
+ const tasksExists = await fs8.pathExists(tasksPath);
1699
1381
  const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
1700
1382
  let activeTask;
1701
1383
  let nextTodoTask;
@@ -1705,7 +1387,7 @@ async function parseFeature(featurePath, type, context, options) {
1705
1387
  let prFieldExists = false;
1706
1388
  let prStatusFieldExists = false;
1707
1389
  if (tasksExists) {
1708
- const content = await fs6.readFile(tasksPath, "utf-8");
1390
+ const content = await fs8.readFile(tasksPath, "utf-8");
1709
1391
  const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
1710
1392
  tasksSummary.total = summary.total;
1711
1393
  tasksSummary.todo = summary.todo;
@@ -1744,12 +1426,33 @@ async function parseFeature(featurePath, type, context, options) {
1744
1426
  if (tasksExists && (!prFieldExists || !prStatusFieldExists)) {
1745
1427
  warnings.push(tr(lang, "warnings", "legacyTasksPrFields"));
1746
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
+ }
1747
1446
  const featureState = {
1748
1447
  id,
1749
1448
  slug,
1750
1449
  folderName,
1751
1450
  type,
1752
1451
  path: featurePath,
1452
+ completion: {
1453
+ implementationDone,
1454
+ workflowDone
1455
+ },
1753
1456
  issueNumber,
1754
1457
  specStatus,
1755
1458
  planStatus,
@@ -1815,7 +1518,7 @@ async function scanFeatures(config) {
1815
1518
  ignore: ["**/feature-base/**"]
1816
1519
  });
1817
1520
  for (const dir of featureDirs) {
1818
- if ((await fs6.stat(dir)).isDirectory()) {
1521
+ if ((await fs8.stat(dir)).isDirectory()) {
1819
1522
  features.push(
1820
1523
  await parseFeature(
1821
1524
  dir,
@@ -1832,62 +1535,421 @@ async function scanFeatures(config) {
1832
1535
  )
1833
1536
  );
1834
1537
  }
1835
- }
1836
- } else {
1837
- const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
1838
- const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
1839
- for (const dir of feDirs) {
1840
- if ((await fs6.stat(dir)).isDirectory()) {
1841
- features.push(
1842
- await parseFeature(
1843
- dir,
1844
- "fe",
1845
- {
1846
- projectBranch: projectBranches.fe,
1847
- docsBranch,
1848
- docsGitCwd: config.docsDir,
1849
- projectGitCwd: feProject?.cwd ?? void 0,
1850
- docsDir: config.docsDir,
1851
- projectBranchAvailable: Boolean(feProject?.cwd)
1852
- },
1853
- { lang: config.lang, stepDefinitions }
1854
- )
1855
- );
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++;
1856
1841
  }
1842
+ } else if (stat.isDirectory()) {
1843
+ const subCount = await updateFolder(
1844
+ sourcePath,
1845
+ targetPath,
1846
+ force,
1847
+ replacements
1848
+ );
1849
+ updatedCount += subCount;
1857
1850
  }
1858
- for (const dir of beDirs) {
1859
- if ((await fs6.stat(dir)).isDirectory()) {
1860
- features.push(
1861
- await parseFeature(
1862
- dir,
1863
- "be",
1864
- {
1865
- projectBranch: projectBranches.be,
1866
- docsBranch,
1867
- docsGitCwd: config.docsDir,
1868
- projectGitCwd: beProject?.cwd ?? void 0,
1869
- docsDir: config.docsDir,
1870
- projectBranchAvailable: Boolean(beProject?.cwd)
1871
- },
1872
- { lang: config.lang, stepDefinitions }
1873
- )
1874
- );
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);
1875
1862
  }
1863
+ console.error(chalk6.red("\uC624\uB958:"), error);
1864
+ process.exit(1);
1876
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);
1877
1876
  }
1878
- return {
1879
- features,
1880
- branches: {
1881
- docs: docsBranch,
1882
- project: config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
1883
- },
1884
- warnings
1885
- };
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();
1886
1950
  }
1887
-
1888
- // src/commands/context.ts
1889
1951
  function contextCommand(program2) {
1890
- 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(
1891
1953
  async (featureName, options) => {
1892
1954
  try {
1893
1955
  await runContext(featureName, options);
@@ -1933,12 +1995,22 @@ async function runContext(featureName, options) {
1933
1995
  const stepDefinitions = getStepDefinitions(lang);
1934
1996
  const stepsMap = getStepsMap(lang);
1935
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
+ );
1936
2006
  let targetFeatures = [];
2007
+ let selectionMode = "explicit";
1937
2008
  if (featureName) {
1938
2009
  targetFeatures = features.filter((f2) => matchesFeatureSelector(f2, featureName));
1939
2010
  if (options.repo) {
1940
2011
  targetFeatures = targetFeatures.filter((f2) => f2.type === options.repo);
1941
2012
  }
2013
+ selectionMode = "explicit";
1942
2014
  } else {
1943
2015
  if (config.projectType === "single") {
1944
2016
  const branchName = branches.project.single || "";
@@ -1960,15 +2032,33 @@ async function runContext(featureName, options) {
1960
2032
  ) : [];
1961
2033
  targetFeatures = [...feMatches, ...beMatches];
1962
2034
  }
1963
- 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
+ }
1964
2047
  }
1965
2048
  if (options.json) {
2049
+ const isNoOpen = selectionMode === "open" && features.length > 0 && openFeatures.length === 0;
1966
2050
  const result = {
1967
- 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,
1968
2053
  branches,
1969
2054
  warnings,
1970
2055
  matchedFeature: targetFeatures.length === 1 ? targetFeatures[0] : null,
1971
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 : [],
1972
2062
  actions: targetFeatures.length === 1 ? targetFeatures[0].actions : [],
1973
2063
  recommendation: ""
1974
2064
  };
@@ -2024,22 +2114,53 @@ async function runContext(featureName, options) {
2024
2114
  console.log();
2025
2115
  }
2026
2116
  if (targetFeatures.length > 1) {
2027
- console.log(
2028
- chalk6.blue(`\u{1F539} ${targetFeatures.length} Active Features Detected:`)
2029
- );
2030
- console.log();
2031
- targetFeatures.forEach((f2) => {
2032
- const stepName2 = stepsMap[f2.currentStep] || "Unknown";
2033
- const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
2117
+ if (selectionMode === "open") {
2034
2118
  console.log(
2035
- ` \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
+ )
2036
2122
  );
2037
- });
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
+ }
2038
2155
  console.log();
2039
2156
  console.log(chalk6.gray("Tip: \uD2B9\uC815 Feature\uC758 \uC0C1\uC138 \uC815\uBCF4\uB97C \uBCF4\uB824\uBA74:"));
2040
2157
  console.log(
2041
2158
  chalk6.gray(" $ npx lee-spec-kit context <slug|F001|F001-slug> [--repo fe|be]")
2042
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
+ }
2043
2164
  console.log();
2044
2165
  return;
2045
2166
  }
@@ -2049,6 +2170,9 @@ async function runContext(featureName, options) {
2049
2170
  console.log(
2050
2171
  `\u{1F539} Feature: ${chalk6.bold(f.folderName)} ${config.projectType === "fullstack" ? chalk6.cyan(`(${f.type})`) : ""}`
2051
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
+ );
2052
2176
  if (f.issueNumber) {
2053
2177
  console.log(` \u2022 Issue: #${f.issueNumber}`);
2054
2178
  }
@@ -2116,13 +2240,276 @@ function printChecklist(f, stepDefinitions) {
2116
2240
  console.log(` ${mark} ${definition.step}. ${label} ${detail}`);
2117
2241
  });
2118
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
+ }
2119
2506
  var CACHE_FILE = path4.join(os.homedir(), ".lee-spec-kit-version-cache.json");
2120
2507
  var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
2121
2508
  function getCurrentVersion() {
2122
2509
  try {
2123
2510
  const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
2124
- if (fs6.existsSync(packageJsonPath)) {
2125
- const pkg = fs6.readJsonSync(packageJsonPath);
2511
+ if (fs8.existsSync(packageJsonPath)) {
2512
+ const pkg = fs8.readJsonSync(packageJsonPath);
2126
2513
  return pkg.version;
2127
2514
  }
2128
2515
  } catch {
@@ -2131,8 +2518,8 @@ function getCurrentVersion() {
2131
2518
  }
2132
2519
  function readCache() {
2133
2520
  try {
2134
- if (fs6.existsSync(CACHE_FILE)) {
2135
- return fs6.readJsonSync(CACHE_FILE);
2521
+ if (fs8.existsSync(CACHE_FILE)) {
2522
+ return fs8.readJsonSync(CACHE_FILE);
2136
2523
  }
2137
2524
  } catch {
2138
2525
  }
@@ -2196,12 +2583,23 @@ function checkForUpdates() {
2196
2583
  }
2197
2584
 
2198
2585
  // src/index.ts
2199
- 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();
2200
2598
  function getCliVersion() {
2201
2599
  try {
2202
2600
  const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
2203
- if (fs6.existsSync(packageJsonPath)) {
2204
- const pkg = fs6.readJsonSync(packageJsonPath);
2601
+ if (fs8.existsSync(packageJsonPath)) {
2602
+ const pkg = fs8.readJsonSync(packageJsonPath);
2205
2603
  if (pkg?.version) return String(pkg.version);
2206
2604
  }
2207
2605
  } catch {
@@ -2217,4 +2615,5 @@ statusCommand(program);
2217
2615
  updateCommand(program);
2218
2616
  configCommand(program);
2219
2617
  contextCommand(program);
2220
- program.parse();
2618
+ doctorCommand(program);
2619
+ await program.parseAsync();