lee-spec-kit 0.4.2 → 0.4.4

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);
@@ -43,6 +43,368 @@ function getTemplatesDir() {
43
43
  return path4.join(rootDir, "templates");
44
44
  }
45
45
 
46
+ // src/utils/i18n.ts
47
+ var DEFAULT_LANG = "en";
48
+ function normalizeLang(lang) {
49
+ if (lang === "ko" || lang === "en") return lang;
50
+ return DEFAULT_LANG;
51
+ }
52
+ function formatTemplate(template, vars) {
53
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
54
+ const value = vars[key];
55
+ return value === void 0 ? `{${key}}` : String(value);
56
+ });
57
+ }
58
+ var I18N = {
59
+ ko: {
60
+ cli: {
61
+ "common.errorLabel": "\uC624\uB958:",
62
+ "common.canceled": "\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
63
+ "common.configNotFound": "\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.",
64
+ "common.docsNotFound": "docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
65
+ "status.noFeatures": "Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
66
+ "status.duplicateIds": "\uC911\uBCF5 Feature ID \uBC1C\uACAC:",
67
+ "status.missingIds": "Feature ID\uAC00 \uC5C6\uB294 \uD56D\uBAA9:",
68
+ "status.wrote": "\u2705 {path} \uC0DD\uC131 \uC644\uB8CC",
69
+ "feature.selectRepo": "\uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
70
+ "feature.folderExists": "\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uD3F4\uB354\uC785\uB2C8\uB2E4: {path}",
71
+ "feature.baseNotFound": "feature-base \uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
72
+ "feature.created": "\u2705 Feature \uD3F4\uB354 \uC0DD\uC131 \uC644\uB8CC: {path}",
73
+ "feature.nextStepsTitle": "\uB2E4\uC74C \uB2E8\uACC4:",
74
+ "feature.nextSteps1": " 1. {path}/spec.md \uC791\uC131",
75
+ "feature.nextSteps2": " 2. \uC0AC\uC6A9\uC790 \uB9AC\uBDF0 \uC694\uCCAD",
76
+ "feature.nextSteps3": " 3. \uC2B9\uC778 \uD6C4 plan.md \uC791\uC131",
77
+ "config.currentTitle": "\u{1F4CB} \uD604\uC7AC \uC124\uC815:",
78
+ "config.pathLabel": "\uACBD\uB85C",
79
+ "config.projectRootStandaloneOnly": "\u26A0\uFE0F projectRoot\uB294 standalone \uBAA8\uB4DC\uC5D0\uC11C\uB9CC \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.",
80
+ "config.selectRepoToUpdate": "\uC218\uC815\uD560 \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
81
+ "config.fullstackRepoRequired": "Fullstack \uD504\uB85C\uC81D\uD2B8\uB294 --repo fe \uB610\uB294 --repo be\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
82
+ "config.projectRootSet": "\u2705 {repo} projectRoot \uC124\uC815 \uC644\uB8CC: {path}",
83
+ "config.projectRootSetSingle": "\u2705 projectRoot \uC124\uC815 \uC644\uB8CC: {path}",
84
+ "update.start": "\u{1F4E6} \uD15C\uD50C\uB9BF \uC5C5\uB370\uC774\uD2B8\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4...",
85
+ "update.langLabel": "\uC5B8\uC5B4",
86
+ "update.typeLabel": "\uD0C0\uC785",
87
+ "update.updatingAgents": "\u{1F4C1} agents/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911...",
88
+ "update.agentsUpdated": "agents/ \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC",
89
+ "update.updatingFeatureBase": "\u{1F4C1} features/feature-base/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911...",
90
+ "update.filesUpdated": "{count}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC",
91
+ "update.updatedTotal": "\uCD1D {count}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!",
92
+ "update.changeDetected": "\uBCC0\uACBD \uAC10\uC9C0 (--force\uB85C \uB36E\uC5B4\uC4F0\uAE30)",
93
+ "update.fileUpdated": "{file} \uC5C5\uB370\uC774\uD2B8",
94
+ "doctor.title": "\u{1F50E} \uBB38\uC11C \uC9C4\uB2E8",
95
+ "doctor.envWarnings": "\u26A0\uFE0F \uD658\uACBD \uACBD\uACE0:",
96
+ "doctor.noIssues": "\u2705 \uBB38\uC81C\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
97
+ "doctor.errorsTitle": "\uC624\uB958",
98
+ "doctor.warningsTitle": "\uACBD\uACE0",
99
+ "doctor.tipJson": "Tip: \uC5D0\uC774\uC804\uD2B8\uC6A9 JSON \uCD9C\uB825: npx lee-spec-kit doctor --json{strictFlag}",
100
+ "doctor.issue.missingRequiredDir": "\uD544\uC218 \uD3F4\uB354\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4: {dir}",
101
+ "doctor.issue.missingConfig": "\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.",
102
+ "doctor.issue.noFeatures": "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.)",
103
+ "doctor.issue.placeholdersLeft": "\uD50C\uB808\uC774\uC2A4\uD640\uB354\uAC00 \uB0A8\uC544\uC788\uC2B5\uB2C8\uB2E4: {placeholders}",
104
+ "doctor.issue.missingSpec": "spec.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
105
+ "doctor.issue.specStatusUnset": "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)",
106
+ "doctor.issue.planStatusUnset": "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)",
107
+ "doctor.issue.tasksEmpty": "tasks.md\uC5D0 \uD0DC\uC2A4\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
108
+ "doctor.issue.duplicateFeatureId": "\uC911\uBCF5 Feature ID \uAC10\uC9C0: {id} ({count}\uAC1C)",
109
+ "doctor.issue.missingFeatureId": "Feature \uD3F4\uB354\uBA85\uC774 F001-... \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4. (ID\uB97C \uCD94\uCD9C\uD560 \uC218 \uC5C6\uC74C)",
110
+ "context.noActiveFeatures": "\u26A0\uFE0F \uC9C4\uD589 \uC911\uC778 Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
111
+ "context.envWarnings": "\u26A0\uFE0F \uD658\uACBD \uACBD\uACE0:",
112
+ "context.openFallbackSummary": "(\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: {inProgress}\uAC1C / \uC885\uB8CC \uB300\uAE30: {readyToClose}\uAC1C / \uC644\uB8CC: {done}\uAC1C)",
113
+ "context.sectionInProgress": "\uC9C4\uD589 \uC911",
114
+ "context.sectionReadyToClose": "\uC885\uB8CC \uC900\uBE44",
115
+ "context.tipDetails": "Tip: \uD2B9\uC815 Feature\uC758 \uC0C1\uC138 \uC815\uBCF4\uB97C \uBCF4\uB824\uBA74:",
116
+ "context.tipShowAll": "\uC804\uCCB4 \uBCF4\uAE30",
117
+ "context.tipShowDone": "\uC644\uB8CC\uB9CC \uBCF4\uAE30",
118
+ "context.okRequired": "[OK \uD544\uC694] ",
119
+ "context.list.docsCommitNeeded": "\uBB38\uC11C \uCEE4\uBC0B \uD544\uC694",
120
+ "context.list.issueNumberNeeded": "\uC774\uC288 \uBC88\uD638 \uAE30\uB85D \uD544\uC694",
121
+ "context.list.addPrMetadata": "PR \uBA54\uD0C0\uB370\uC774\uD130(PR/PR \uC0C1\uD0DC) \uCD94\uAC00",
122
+ "context.list.recordPrLink": "PR \uB9C1\uD06C \uAE30\uB85D",
123
+ "context.list.setPrStatus": "PR \uC0C1\uD0DC \uC124\uC815",
124
+ "context.list.prStatusToApproved": "PR \uC0C1\uD0DC {status} \u2192 Approved",
125
+ "context.list.approveSpec": "spec \uC2B9\uC778 \uD544\uC694",
126
+ "context.list.approvePlan": "plan \uC2B9\uC778 \uD544\uC694",
127
+ "init.selectLangPrompt": "\uBB38\uC11C \uC5B8\uC5B4\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
128
+ "init.currentDirectoryLabel": "\u{1F4CD} \uD604\uC7AC \uC704\uCE58",
129
+ "init.gitDetected": "\u2705 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0\uB428",
130
+ "init.insideProjectRoot": "\uD604\uC7AC \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 \uB0B4\uC5D0\uC11C \uC2E4\uD589\uD558\uACE0 \uACC4\uC2ED\uB2C8\uB2E4.",
131
+ "init.modeEmbeddedDesc": "\u2022 embedded: \uC5EC\uAE30\uC5D0 ./docs \uD3F4\uB354\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4. \uD504\uB85C\uC81D\uD2B8\uC640 \uD568\uAED8 \uAD00\uB9AC\uB429\uB2C8\uB2E4.",
132
+ "init.modeStandaloneDesc": "\u2022 standalone: \uBCC4\uB3C4 \uD3F4\uB354\uC5D0\uC11C \uB3C5\uB9BD docs \uB808\uD3EC\uB85C \uAD00\uB9AC\uD558\uB824\uBA74,",
133
+ "init.modeStandaloneMove": " \uD574\uB2F9 \uD3F4\uB354\uB85C \uC774\uB3D9 \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD574\uC8FC\uC138\uC694.",
134
+ "init.gitNotDetected": "\u26A0\uFE0F Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uAC00 \uAC10\uC9C0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.",
135
+ "init.gitNotDetectedDetail": "\uC0C8\uB85C\uC6B4 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uAC00 \uC0DD\uC131\uB429\uB2C8\uB2E4.",
136
+ "init.prompt.projectName": "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694:",
137
+ "init.prompt.projectType": "\uD504\uB85C\uC81D\uD2B8 \uD0C0\uC785\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
138
+ "init.choice.projectType.single.title": "Single - \uB2E8\uC77C \uB808\uD3EC \uD504\uB85C\uC81D\uD2B8",
139
+ "init.choice.projectType.single.desc": "features/ \uD3F4\uB354 \uD558\uB098\uB85C \uAD00\uB9AC",
140
+ "init.choice.projectType.fullstack.title": "Fullstack - FE/BE \uBD84\uB9AC \uD504\uB85C\uC81D\uD2B8",
141
+ "init.choice.projectType.fullstack.desc": "features/be/, features/fe/ \uBD84\uB9AC \uAD00\uB9AC",
142
+ "init.prompt.docsMode": "Docs \uAD00\uB9AC \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
143
+ "init.choice.docsRepo.embedded.title": "embedded - \uD504\uB85C\uC81D\uD2B8 \uB0B4 \uD3EC\uD568 (./docs)",
144
+ "init.choice.docsRepo.embedded.desc": "\uD504\uB85C\uC81D\uD2B8\uC640 \uD568\uAED8 push\uB429\uB2C8\uB2E4",
145
+ "init.choice.docsRepo.standalone.title": "standalone - \uBCC4\uB3C4 \uB3C5\uB9BD \uB808\uD3EC",
146
+ "init.choice.docsRepo.standalone.desc": "push \uC5EC\uBD80\uB97C \uBCC4\uB3C4\uB85C \uC124\uC815\uD569\uB2C8\uB2E4",
147
+ "init.prompt.feRepoPath": "Frontend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
148
+ "init.prompt.beRepoPath": "Backend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
149
+ "init.prompt.projectRepoPath": "\uD504\uB85C\uC81D\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
150
+ "init.validation.enterPath": "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694",
151
+ "init.prompt.pushMode": "Docs push \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
152
+ "init.choice.push.local": "local - \uB85C\uCEEC\uC5D0\uC11C\uB9CC \uAD00\uB9AC (push \uC548 \uD568)",
153
+ "init.choice.push.remote": "remote - \uC6D0\uACA9\uC5D0\uB3C4 push",
154
+ "init.prompt.remoteUrl": "\uC6D0\uACA9 \uB808\uD3EC URL\uC744 \uC785\uB825\uD558\uC138\uC694:",
155
+ "init.validation.enterUrl": "URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694",
156
+ "init.prompt.overwrite": "{dir} \uD3F4\uB354\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uB36E\uC5B4\uC4F0\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
157
+ "init.log.creatingDocs": "\u{1F4C1} docs \uAD6C\uC870 \uC0DD\uC131 \uC911...",
158
+ "init.log.projectLabel": "\uD504\uB85C\uC81D\uD2B8",
159
+ "init.log.typeLabel": "\uD0C0\uC785",
160
+ "init.log.langLabel": "\uC5B8\uC5B4",
161
+ "init.log.pathLabel": "\uACBD\uB85C",
162
+ "init.log.docsCreated": "\u2705 docs \uAD6C\uC870 \uC0DD\uC131 \uC644\uB8CC!",
163
+ "init.log.nextStepsTitle": "\uB2E4\uC74C \uB2E8\uACC4:",
164
+ "init.log.nextSteps1": " 1. {docsDir}/prd/README.md \uC791\uC131",
165
+ "init.log.nextSteps2": " 2. npx lee-spec-kit feature <name> \uC73C\uB85C \uAE30\uB2A5 \uCD94\uAC00",
166
+ "init.log.gitRepoDetectedCommit": "\u{1F4E6} Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0, docs \uCEE4\uBC0B \uC911...",
167
+ "init.log.gitInit": "\u{1F4E6} Git \uCD08\uAE30\uD654 \uC911...",
168
+ "init.warn.stagedChangesSkip": '\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)',
169
+ "init.warn.commitManually": " \uC218\uB3D9\uC73C\uB85C \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uD655\uC778\uD55C \uB4A4 \uCEE4\uBC0B\uD574\uC8FC\uC138\uC694.",
170
+ "init.log.gitRemoteSet": "\u2705 Git remote \uC124\uC815 \uC644\uB8CC: {remote}",
171
+ "init.warn.gitRemoteExists": "\u26A0\uFE0F Git remote\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4.",
172
+ "init.log.gitInitialCommitDone": "\u2705 Git \uCD08\uAE30 \uCEE4\uBC0B \uC644\uB8CC!",
173
+ "init.warn.skipGitInit": "\u26A0\uFE0F Git \uCD08\uAE30\uD654\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4 (\uC218\uB3D9\uC73C\uB85C \uCEE4\uBC0B\uD574\uC8FC\uC138\uC694)",
174
+ "init.error.templateNotFound": "\uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: {path}"
175
+ },
176
+ steps: {
177
+ featureFolder: "Feature \uD3F4\uB354 \uC0DD\uC131",
178
+ specWrite: "spec.md \uC791\uC131",
179
+ specApprove: "spec.md \uC2B9\uC778",
180
+ planWrite: "plan.md \uC791\uC131",
181
+ planApprove: "plan.md \uC2B9\uC778",
182
+ tasksWrite: "tasks.md \uC791\uC131",
183
+ docsCommitPlanning: "\uBB38\uC11C \uCEE4\uBC0B(\uB3D9\uAE30\uD654)",
184
+ issueCreate: "GitHub Issue \uC0DD\uC131",
185
+ branchCreate: "\uBE0C\uB79C\uCE58 \uC0DD\uC131",
186
+ tasksExecute: "\uD0DC\uC2A4\uD06C \uC2E4\uD589",
187
+ prCreate: "PR \uC0DD\uC131",
188
+ codeReview: "\uCF54\uB4DC \uB9AC\uBDF0",
189
+ featureDone: "Feature \uC644\uB8CC"
190
+ },
191
+ messages: {
192
+ specCreate: "spec.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uC791\uC131\uD558\uC138\uC694. (features/feature-base/spec.md \uCC38\uACE0)",
193
+ specImprove: "spec.md\uB97C \uBCF4\uC644\uD558\uACE0 \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
194
+ specApproval: "spec.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
195
+ planCreate: "plan.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uC791\uC131\uD558\uC138\uC694. (features/feature-base/plan.md \uCC38\uACE0)",
196
+ planImprove: "plan.md\uB97C \uBCF4\uC644\uD558\uACE0 \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
197
+ planApproval: "plan.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
198
+ 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)",
199
+ tasksNeedAtLeastOne: "tasks.md\uC5D0 \uCD5C\uC18C 1\uAC1C \uC774\uC0C1\uC758 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694.",
200
+ docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} \uAE30\uD68D \uBB38\uC11C"',
201
+ 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)",
202
+ docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
203
+ standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
204
+ createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
205
+ 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.',
206
+ tasksAllDoneButChecklist: "\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uAC00 \uC644\uC804\uD788 \uCCB4\uD06C\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. ({checked}/{total})",
207
+ finishDoingTask: '\uD604\uC7AC DOING/REVIEW \uC911\uC778 \uD0DC\uC2A4\uD06C\uB97C \uC644\uB8CC\uD558\uC138\uC694: "{title}" ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)',
208
+ startNextTodoTask: '\uB2E4\uC74C TODO \uD0DC\uC2A4\uD06C\uB97C \uC2DC\uC791\uD558\uC138\uC694: "{title}" ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)',
209
+ checkTaskStatuses: "\uD0DC\uC2A4\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)",
210
+ 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)",
211
+ 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)",
212
+ 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)",
213
+ 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)",
214
+ 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.",
215
+ 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.",
216
+ fallbackRerunContext: "\uC0C1\uD0DC\uB97C \uD310\uBCC4\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC11C\uB97C \uD655\uC778\uD55C \uB4A4 \uB2E4\uC2DC context\uB97C \uC2E4\uD589\uD558\uC138\uC694."
217
+ },
218
+ warnings: {
219
+ 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.)",
220
+ 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)",
221
+ docsUncommittedChanges: "\uBB38\uC11C \uBCC0\uACBD\uC0AC\uD56D\uC774 \uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (\uCD94\uAC00 \uBB38\uC11C \uCEE4\uBC0B \uD544\uC694)",
222
+ 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.",
223
+ 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.)",
224
+ 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.)",
225
+ workflowPrLinkMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uB9C1\uD06C\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (tasks.md\uC758 PR \uD544\uB4DC\uB97C \uCC44\uC6B0\uC138\uC694.)",
226
+ workflowPrStatusMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uC0C1\uD0DC\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Draft/Review/Approved \uC911 \uD558\uB098\uB85C \uC124\uC815\uD558\uC138\uC694.)",
227
+ workflowPrStatusNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (merge \uD6C4 PR \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)"
228
+ }
229
+ },
230
+ en: {
231
+ cli: {
232
+ "common.errorLabel": "Error:",
233
+ "common.canceled": "Operation canceled.",
234
+ "common.configNotFound": "Config file not found. Run `init` first.",
235
+ "common.docsNotFound": "docs folder not found. Run `init` first.",
236
+ "status.noFeatures": "No features found.",
237
+ "status.duplicateIds": "Duplicate Feature IDs found:",
238
+ "status.missingIds": "Entries missing Feature ID:",
239
+ "status.wrote": "\u2705 Wrote {path}",
240
+ "feature.selectRepo": "Select a repository:",
241
+ "feature.folderExists": "Folder already exists: {path}",
242
+ "feature.baseNotFound": "feature-base template not found.",
243
+ "feature.created": "\u2705 Feature folder created: {path}",
244
+ "feature.nextStepsTitle": "Next steps:",
245
+ "feature.nextSteps1": " 1. Write {path}/spec.md",
246
+ "feature.nextSteps2": " 2. Ask for review",
247
+ "feature.nextSteps3": " 3. After approval, write plan.md",
248
+ "config.currentTitle": "\u{1F4CB} Current config:",
249
+ "config.pathLabel": "Path",
250
+ "config.projectRootStandaloneOnly": "\u26A0\uFE0F projectRoot can only be set in standalone mode.",
251
+ "config.selectRepoToUpdate": "Select a repository to update:",
252
+ "config.fullstackRepoRequired": "For fullstack projects, you must specify `--repo fe` or `--repo be`.",
253
+ "config.projectRootSet": "\u2705 {repo} projectRoot set: {path}",
254
+ "config.projectRootSetSingle": "\u2705 projectRoot set: {path}",
255
+ "update.start": "\u{1F4E6} Starting template update...",
256
+ "update.langLabel": "Lang",
257
+ "update.typeLabel": "Type",
258
+ "update.updatingAgents": "\u{1F4C1} Updating agents/ folder...",
259
+ "update.agentsUpdated": "agents/ updated",
260
+ "update.updatingFeatureBase": "\u{1F4C1} Updating features/feature-base/ folder...",
261
+ "update.filesUpdated": "{count} files updated",
262
+ "update.updatedTotal": "Updated {count} files!",
263
+ "update.changeDetected": "changes detected (use --force to overwrite)",
264
+ "update.fileUpdated": "{file} updated",
265
+ "doctor.title": "\u{1F50E} Docs Doctor",
266
+ "doctor.envWarnings": "\u26A0\uFE0F Environment warnings:",
267
+ "doctor.noIssues": "\u2705 No issues found.",
268
+ "doctor.errorsTitle": "Errors",
269
+ "doctor.warningsTitle": "Warnings",
270
+ "doctor.tipJson": "Tip: Agent JSON output: npx lee-spec-kit doctor --json{strictFlag}",
271
+ "doctor.issue.missingRequiredDir": "Missing required directory: {dir}",
272
+ "doctor.issue.missingConfig": "Missing .lee-spec-kit.json. Some commands may rely on folder-structure heuristics.",
273
+ "doctor.issue.noFeatures": "No feature folders found. (Only feature-base exists, or no features created yet.)",
274
+ "doctor.issue.placeholdersLeft": "Leftover placeholders detected: {placeholders}",
275
+ "doctor.issue.missingSpec": "Missing spec.md.",
276
+ "doctor.issue.specStatusUnset": "spec.md Status is not set. (May still be a template)",
277
+ "doctor.issue.planStatusUnset": "plan.md Status is not set. (May still be a template)",
278
+ "doctor.issue.tasksEmpty": "tasks.md has no tasks.",
279
+ "doctor.issue.duplicateFeatureId": "Duplicate Feature ID detected: {id} ({count})",
280
+ "doctor.issue.missingFeatureId": "Feature folder name is not in F001-... format. (Cannot extract ID)",
281
+ "context.noActiveFeatures": "\u26A0\uFE0F No active features found.",
282
+ "context.envWarnings": "\u26A0\uFE0F Environment warnings:",
283
+ "context.openFallbackSummary": "(Could not detect a feature from the branch, so showing only open features. In Progress: {inProgress} / Ready To Close: {readyToClose} / Done: {done})",
284
+ "context.sectionInProgress": "In Progress",
285
+ "context.sectionReadyToClose": "Ready To Close",
286
+ "context.tipDetails": "Tip: To view details for a feature:",
287
+ "context.tipShowAll": "Show all",
288
+ "context.tipShowDone": "Show done only",
289
+ "context.okRequired": "[OK required] ",
290
+ "context.list.docsCommitNeeded": "Commit docs changes",
291
+ "context.list.issueNumberNeeded": "Fill issue number in docs",
292
+ "context.list.addPrMetadata": "Add PR metadata (PR/PR Status)",
293
+ "context.list.recordPrLink": "Record PR link",
294
+ "context.list.setPrStatus": "Set PR Status",
295
+ "context.list.prStatusToApproved": "PR Status {status} \u2192 Approved",
296
+ "context.list.approveSpec": "Approve spec",
297
+ "context.list.approvePlan": "Approve plan",
298
+ "init.selectLangPrompt": "Select docs language:",
299
+ "init.currentDirectoryLabel": "\u{1F4CD} Current directory",
300
+ "init.gitDetected": "\u2705 Git repository detected",
301
+ "init.insideProjectRoot": "You are running inside your project root.",
302
+ "init.modeEmbeddedDesc": "\u2022 embedded: creates ./docs here and manages it with the project.",
303
+ "init.modeStandaloneDesc": "\u2022 standalone: to manage docs as a separate repo,",
304
+ "init.modeStandaloneMove": " move to that folder and run again.",
305
+ "init.gitNotDetected": "\u26A0\uFE0F Git repository not detected.",
306
+ "init.gitNotDetectedDetail": "A new Git repo will be initialized.",
307
+ "init.prompt.projectName": "Enter project name:",
308
+ "init.prompt.projectType": "Select project type:",
309
+ "init.choice.projectType.single.title": "Single - single repo project",
310
+ "init.choice.projectType.single.desc": "Manage with a single features/ folder",
311
+ "init.choice.projectType.fullstack.title": "Fullstack - split FE/BE repos",
312
+ "init.choice.projectType.fullstack.desc": "Manage with features/be/ and features/fe/",
313
+ "init.prompt.docsMode": "Select docs mode:",
314
+ "init.choice.docsRepo.embedded.title": "embedded - inside the project (./docs)",
315
+ "init.choice.docsRepo.embedded.desc": "Pushed together with the project",
316
+ "init.choice.docsRepo.standalone.title": "standalone - separate docs repo",
317
+ "init.choice.docsRepo.standalone.desc": "Configure push settings separately",
318
+ "init.prompt.feRepoPath": "Enter frontend repository path:",
319
+ "init.prompt.beRepoPath": "Enter backend repository path:",
320
+ "init.prompt.projectRepoPath": "Enter project repository path:",
321
+ "init.validation.enterPath": "Please enter a path",
322
+ "init.prompt.pushMode": "Select docs push mode:",
323
+ "init.choice.push.local": "local - manage locally (no push)",
324
+ "init.choice.push.remote": "remote - push to remote",
325
+ "init.prompt.remoteUrl": "Enter remote repository URL:",
326
+ "init.validation.enterUrl": "Please enter a URL",
327
+ "init.prompt.overwrite": "{dir} already exists. Overwrite?",
328
+ "init.log.creatingDocs": "\u{1F4C1} Creating docs structure...",
329
+ "init.log.projectLabel": "Project",
330
+ "init.log.typeLabel": "Type",
331
+ "init.log.langLabel": "Lang",
332
+ "init.log.pathLabel": "Path",
333
+ "init.log.docsCreated": "\u2705 Docs structure created!",
334
+ "init.log.nextStepsTitle": "Next steps:",
335
+ "init.log.nextSteps1": " 1. Write {docsDir}/prd/README.md",
336
+ "init.log.nextSteps2": " 2. Add a feature with: npx lee-spec-kit feature <name>",
337
+ "init.log.gitRepoDetectedCommit": "\u{1F4E6} Git repo detected, committing docs...",
338
+ "init.log.gitInit": "\u{1F4E6} Initializing Git...",
339
+ "init.warn.stagedChangesSkip": '\u26A0\uFE0F There are already staged changes in the Git index. (With --dir ".", commit scope cannot be safely restricted, so auto-commit is skipped.)',
340
+ "init.warn.commitManually": " Review the changes and commit manually.",
341
+ "init.log.gitRemoteSet": "\u2705 Git remote set: {remote}",
342
+ "init.warn.gitRemoteExists": "\u26A0\uFE0F Git remote already exists.",
343
+ "init.log.gitInitialCommitDone": "\u2705 Initial Git commit created!",
344
+ "init.warn.skipGitInit": "\u26A0\uFE0F Skipping Git initialization (please commit manually)",
345
+ "init.error.templateNotFound": "Template not found: {path}"
346
+ },
347
+ steps: {
348
+ featureFolder: "Create feature folder",
349
+ specWrite: "Write spec.md",
350
+ specApprove: "Approve spec.md",
351
+ planWrite: "Write plan.md",
352
+ planApprove: "Approve plan.md",
353
+ tasksWrite: "Write tasks.md",
354
+ docsCommitPlanning: "Commit docs (sync)",
355
+ issueCreate: "Create GitHub Issue",
356
+ branchCreate: "Create branch",
357
+ tasksExecute: "Execute tasks",
358
+ prCreate: "Create PR",
359
+ codeReview: "Code review",
360
+ featureDone: "Feature done"
361
+ },
362
+ messages: {
363
+ specCreate: "Create spec.md by copying the template. (See features/feature-base/spec.md)",
364
+ specImprove: "Improve spec.md and change Status to Review.",
365
+ specApproval: "Share spec.md with the user and get approval (OK).",
366
+ planCreate: "Create plan.md by copying the template. (See features/feature-base/plan.md)",
367
+ planImprove: "Improve plan.md and change Status to Review.",
368
+ planApproval: "Share plan.md with the user and get approval (OK).",
369
+ tasksCreate: "Create tasks.md by copying the template. (See features/feature-base/tasks.md)",
370
+ tasksNeedAtLeastOne: "Write at least 1 task in tasks.md.",
371
+ docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} planning docs"',
372
+ issueCreateAndWrite: "Create a GitHub Issue, fill the issue number in spec.md/tasks.md, then prepare a docs commit. (See skills/create-issue.md)",
373
+ docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
374
+ standaloneNeedsProjectRoot: "Standalone mode requires projectRoot. (npx lee-spec-kit config --project-root ...)",
375
+ createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
376
+ tasksAllDoneButNoChecklist: 'All tasks are DONE, but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
377
+ tasksAllDoneButChecklist: "All tasks are DONE, but the completion checklist is not fully checked. ({checked}/{total})",
378
+ finishDoingTask: 'Finish the current DOING/REVIEW task: "{title}" ({done}/{total}) (See skills/execute-task.md)',
379
+ startNextTodoTask: 'Start the next TODO task: "{title}" ({done}/{total}) (See skills/execute-task.md)',
380
+ checkTaskStatuses: "Check task statuses. ({done}/{total}) (See skills/execute-task.md)",
381
+ prLegacyAsk: "tasks.md is missing PR/PR Status fields. Update to the latest template format? (OK required)",
382
+ prCreate: "Create a PR and record the PR link in tasks.md. (See skills/create-pr.md)",
383
+ prFillStatus: "Set PR Status in tasks.md to Draft/Review/Approved. (After merge, update it to Approved.)",
384
+ prResolveReview: "Resolve review comments and update PR Status. (PR Status: Review \u2192 Approved)",
385
+ prRequestReview: "Request review and update PR Status to Review.",
386
+ featureDone: "PR is Approved and all tasks/completion criteria are satisfied. This feature is done.",
387
+ fallbackRerunContext: "Cannot determine status. Check the docs and run context again."
388
+ },
389
+ warnings: {
390
+ projectBranchUnavailable: "Cannot determine project branch. (In standalone mode, projectRoot is required.)",
391
+ docsGitUnavailable: "Cannot read git status for the docs repo. (Check repo location / git init.)",
392
+ docsUncommittedChanges: "Docs changes are not committed. (Additional docs commit needed.)",
393
+ legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps.",
394
+ workflowSpecNotApproved: "Implementation is done but spec.md Status is not Approved. (Update spec.md Status to Approved.)",
395
+ workflowPlanNotApproved: "Implementation is done but plan.md Status is not Approved. (Update plan.md Status to Approved.)",
396
+ workflowPrLinkMissing: "Implementation is done but PR link is missing. (Fill the PR field in tasks.md.)",
397
+ workflowPrStatusMissing: "Implementation is done but PR Status is missing. (Set PR Status to Draft/Review/Approved in tasks.md.)",
398
+ workflowPrStatusNotApproved: "Implementation is done but PR Status is not Approved. (After merge, update PR Status to Approved in tasks.md.)"
399
+ }
400
+ }
401
+ };
402
+ function tr(lang, category, key, vars = {}) {
403
+ const safeLang = normalizeLang(lang);
404
+ const template = I18N[safeLang]?.[category]?.[key] ?? I18N[DEFAULT_LANG]?.[category]?.[key] ?? I18N.ko?.[category]?.[key] ?? `${category}.${key}`;
405
+ return formatTemplate(template, vars);
406
+ }
407
+
46
408
  // src/utils/validation.ts
47
409
  var VALID_PROJECT_TYPES = ["single", "fullstack"];
48
410
  var VALID_LANGUAGES = ["ko", "en"];
@@ -149,15 +511,19 @@ function checkGitRepo(cwd) {
149
511
  }
150
512
  }
151
513
  function initCommand(program2) {
152
- program2.command("init").description("Initialize project documentation structure").option("-n, --name <name>", "Project name (default: current folder name)").option("-t, --type <type>", "Project type: single | fullstack").option("-l, --lang <lang>", "Language: ko | en (default: ko)").option("-d, --dir <dir>", "Target directory (default: ./docs)", "./docs").option("-y, --yes", "Skip prompts and use defaults").action(async (options) => {
514
+ program2.command("init").description("Initialize project documentation structure").option("-n, --name <name>", "Project name (default: current folder name)").option("-t, --type <type>", "Project type: single | fullstack").option("-l, --lang <lang>", "Language: ko | en (default: en)").option("-d, --dir <dir>", "Target directory (default: ./docs)", "./docs").option("-y, --yes", "Skip prompts and use defaults").action(async (options) => {
153
515
  try {
154
516
  await runInit(options);
155
517
  } catch (error) {
156
518
  if (error instanceof Error && error.message === "canceled") {
157
- console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
519
+ const lang = options.lang ?? DEFAULT_LANG;
520
+ console.log(
521
+ chalk6.yellow(`
522
+ ${tr(lang, "cli", "common.canceled")}`)
523
+ );
158
524
  process.exit(0);
159
525
  }
160
- console.error(chalk6.red("\uC624\uB958:"), error);
526
+ console.error(chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")), error);
161
527
  process.exit(1);
162
528
  }
163
529
  });
@@ -167,7 +533,7 @@ async function runInit(options) {
167
533
  const defaultName = path4.basename(cwd);
168
534
  let projectName = options.name || defaultName;
169
535
  let projectType = options.type;
170
- let lang = options.lang || "ko";
536
+ let lang = options.lang || "en";
171
537
  let docsRepo = "embedded";
172
538
  let pushDocs;
173
539
  let docsRemote;
@@ -175,24 +541,62 @@ async function runInit(options) {
175
541
  const targetDir = path4.resolve(cwd, options.dir || "./docs");
176
542
  const isInsideGitRepo = checkGitRepo(cwd);
177
543
  if (!options.yes) {
544
+ if (!options.lang) {
545
+ const langResponse = await prompts(
546
+ [
547
+ {
548
+ type: "select",
549
+ name: "lang",
550
+ message: tr(DEFAULT_LANG, "cli", "init.selectLangPrompt"),
551
+ choices: [
552
+ { title: "English (en)", value: "en" },
553
+ { title: "\uD55C\uAD6D\uC5B4 (ko)", value: "ko" }
554
+ ],
555
+ initial: 0
556
+ }
557
+ ],
558
+ {
559
+ onCancel: () => {
560
+ throw new Error("canceled");
561
+ }
562
+ }
563
+ );
564
+ lang = langResponse.lang || lang;
565
+ }
178
566
  console.log();
179
- console.log(chalk6.blue(`\u{1F4CD} \uD604\uC7AC \uC704\uCE58: ${cwd}`));
567
+ console.log(
568
+ chalk6.blue(`${tr(lang, "cli", "init.currentDirectoryLabel")}: ${cwd}`)
569
+ );
180
570
  if (isInsideGitRepo) {
181
- console.log(chalk6.green("\u2705 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0\uB428"));
571
+ console.log(chalk6.green(tr(lang, "cli", "init.gitDetected")));
182
572
  console.log();
183
- console.log(chalk6.gray("\uD604\uC7AC \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 \uB0B4\uC5D0\uC11C \uC2E4\uD589\uD558\uACE0 \uACC4\uC2ED\uB2C8\uB2E4."));
184
573
  console.log(
185
574
  chalk6.gray(
186
- "\u2022 embedded: \uC5EC\uAE30\uC5D0 ./docs \uD3F4\uB354\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4. \uD504\uB85C\uC81D\uD2B8\uC640 \uD568\uAED8 \uAD00\uB9AC\uB429\uB2C8\uB2E4."
575
+ tr(lang, "cli", "init.insideProjectRoot")
187
576
  )
188
577
  );
189
578
  console.log(
190
- chalk6.gray("\u2022 standalone: \uBCC4\uB3C4 \uD3F4\uB354\uC5D0\uC11C \uB3C5\uB9BD docs \uB808\uD3EC\uB85C \uAD00\uB9AC\uD558\uB824\uBA74,")
579
+ chalk6.gray(
580
+ tr(lang, "cli", "init.modeEmbeddedDesc")
581
+ )
582
+ );
583
+ console.log(
584
+ chalk6.gray(
585
+ tr(lang, "cli", "init.modeStandaloneDesc")
586
+ )
587
+ );
588
+ console.log(
589
+ chalk6.gray(
590
+ tr(lang, "cli", "init.modeStandaloneMove")
591
+ )
191
592
  );
192
- console.log(chalk6.gray(" \uD574\uB2F9 \uD3F4\uB354\uB85C \uC774\uB3D9 \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD574\uC8FC\uC138\uC694."));
193
593
  } else {
194
- console.log(chalk6.yellow("\u26A0\uFE0F Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uAC00 \uAC10\uC9C0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."));
195
- console.log(chalk6.gray("\uC0C8\uB85C\uC6B4 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uAC00 \uC0DD\uC131\uB429\uB2C8\uB2E4."));
594
+ console.log(
595
+ chalk6.yellow(
596
+ tr(lang, "cli", "init.gitNotDetected")
597
+ )
598
+ );
599
+ console.log(chalk6.gray(tr(lang, "cli", "init.gitNotDetectedDetail")));
196
600
  }
197
601
  console.log();
198
602
  const response = await prompts(
@@ -200,51 +604,41 @@ async function runInit(options) {
200
604
  {
201
605
  type: options.name ? null : "text",
202
606
  name: "projectName",
203
- message: "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694:",
607
+ message: tr(lang, "cli", "init.prompt.projectName"),
204
608
  initial: defaultName
205
609
  },
206
610
  {
207
611
  type: options.type ? null : "select",
208
612
  name: "projectType",
209
- message: "\uD504\uB85C\uC81D\uD2B8 \uD0C0\uC785\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
613
+ message: tr(lang, "cli", "init.prompt.projectType"),
210
614
  choices: [
211
615
  {
212
- title: "Single - \uB2E8\uC77C \uB808\uD3EC \uD504\uB85C\uC81D\uD2B8",
616
+ title: tr(lang, "cli", "init.choice.projectType.single.title"),
213
617
  value: "single",
214
- description: "features/ \uD3F4\uB354 \uD558\uB098\uB85C \uAD00\uB9AC"
618
+ description: tr(lang, "cli", "init.choice.projectType.single.desc")
215
619
  },
216
620
  {
217
- title: "Fullstack - FE/BE \uBD84\uB9AC \uD504\uB85C\uC81D\uD2B8",
621
+ title: tr(lang, "cli", "init.choice.projectType.fullstack.title"),
218
622
  value: "fullstack",
219
- description: "features/be/, features/fe/ \uBD84\uB9AC \uAD00\uB9AC"
623
+ description: tr(lang, "cli", "init.choice.projectType.fullstack.desc")
220
624
  }
221
625
  ],
222
626
  initial: 0
223
627
  },
224
- {
225
- type: options.lang ? null : "select",
226
- name: "lang",
227
- message: "\uBB38\uC11C \uC5B8\uC5B4\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
228
- choices: [
229
- { title: "\uD55C\uAD6D\uC5B4 (ko)", value: "ko" },
230
- { title: "English (en)", value: "en" }
231
- ],
232
- initial: 0
233
- },
234
628
  {
235
629
  type: "select",
236
630
  name: "docsRepo",
237
- message: "Docs \uAD00\uB9AC \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
631
+ message: tr(lang, "cli", "init.prompt.docsMode"),
238
632
  choices: [
239
633
  {
240
- title: "embedded - \uD504\uB85C\uC81D\uD2B8 \uB0B4 \uD3EC\uD568 (./docs)",
634
+ title: tr(lang, "cli", "init.choice.docsRepo.embedded.title"),
241
635
  value: "embedded",
242
- description: "\uD504\uB85C\uC81D\uD2B8\uC640 \uD568\uAED8 push\uB429\uB2C8\uB2E4"
636
+ description: tr(lang, "cli", "init.choice.docsRepo.embedded.desc")
243
637
  },
244
638
  {
245
- title: "standalone - \uBCC4\uB3C4 \uB3C5\uB9BD \uB808\uD3EC",
639
+ title: tr(lang, "cli", "init.choice.docsRepo.standalone.title"),
246
640
  value: "standalone",
247
- description: "push \uC5EC\uBD80\uB97C \uBCC4\uB3C4\uB85C \uC124\uC815\uD569\uB2C8\uB2E4"
641
+ description: tr(lang, "cli", "init.choice.docsRepo.standalone.desc")
248
642
  }
249
643
  ],
250
644
  initial: 0
@@ -258,7 +652,6 @@ async function runInit(options) {
258
652
  );
259
653
  projectName = response.projectName || projectName;
260
654
  projectType = response.projectType || projectType;
261
- lang = response.lang || lang;
262
655
  docsRepo = response.docsRepo || "embedded";
263
656
  if (docsRepo === "standalone") {
264
657
  const resolvedType = projectType || response.projectType || "single";
@@ -268,14 +661,14 @@ async function runInit(options) {
268
661
  {
269
662
  type: "text",
270
663
  name: "feRoot",
271
- message: "Frontend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
272
- validate: (value) => value.trim() ? true : "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694"
664
+ message: tr(lang, "cli", "init.prompt.feRepoPath"),
665
+ validate: (value) => value.trim() ? true : tr(lang, "cli", "init.validation.enterPath")
273
666
  },
274
667
  {
275
668
  type: "text",
276
669
  name: "beRoot",
277
- message: "Backend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
278
- validate: (value) => value.trim() ? true : "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694"
670
+ message: tr(lang, "cli", "init.prompt.beRepoPath"),
671
+ validate: (value) => value.trim() ? true : tr(lang, "cli", "init.validation.enterPath")
279
672
  }
280
673
  ],
281
674
  {
@@ -294,8 +687,8 @@ async function runInit(options) {
294
687
  {
295
688
  type: "text",
296
689
  name: "projectRoot",
297
- message: "\uD504\uB85C\uC81D\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
298
- validate: (value) => value.trim() ? true : "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694"
690
+ message: tr(lang, "cli", "init.prompt.projectRepoPath"),
691
+ validate: (value) => value.trim() ? true : tr(lang, "cli", "init.validation.enterPath")
299
692
  }
300
693
  ],
301
694
  {
@@ -311,14 +704,14 @@ async function runInit(options) {
311
704
  {
312
705
  type: "select",
313
706
  name: "pushDocs",
314
- message: "Docs push \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
707
+ message: tr(lang, "cli", "init.prompt.pushMode"),
315
708
  choices: [
316
709
  {
317
- title: "local - \uB85C\uCEEC\uC5D0\uC11C\uB9CC \uAD00\uB9AC (push \uC548 \uD568)",
710
+ title: tr(lang, "cli", "init.choice.push.local"),
318
711
  value: false
319
712
  },
320
713
  {
321
- title: "remote - \uC6D0\uACA9\uC5D0\uB3C4 push",
714
+ title: tr(lang, "cli", "init.choice.push.remote"),
322
715
  value: true
323
716
  }
324
717
  ],
@@ -338,8 +731,8 @@ async function runInit(options) {
338
731
  {
339
732
  type: "text",
340
733
  name: "docsRemote",
341
- message: "\uC6D0\uACA9 \uB808\uD3EC URL\uC744 \uC785\uB825\uD558\uC138\uC694:",
342
- validate: (value) => value.trim() ? true : "URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694"
734
+ message: tr(lang, "cli", "init.prompt.remoteUrl"),
735
+ validate: (value) => value.trim() ? true : tr(lang, "cli", "init.validation.enterUrl")
343
736
  }
344
737
  ],
345
738
  {
@@ -358,36 +751,42 @@ async function runInit(options) {
358
751
  assertValid(validateSafeName(projectName), "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984");
359
752
  assertValid(validateProjectType(projectType), "\uD504\uB85C\uC81D\uD2B8 \uD0C0\uC785");
360
753
  assertValid(validateLanguage(lang), "\uC5B8\uC5B4");
361
- if (await fs6.pathExists(targetDir)) {
362
- const files = await fs6.readdir(targetDir);
754
+ if (await fs8.pathExists(targetDir)) {
755
+ const files = await fs8.readdir(targetDir);
363
756
  if (files.length > 0) {
364
757
  const { overwrite } = await prompts({
365
758
  type: "confirm",
366
759
  name: "overwrite",
367
- message: `${targetDir} \uD3F4\uB354\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uB36E\uC5B4\uC4F0\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`,
760
+ message: tr(lang, "cli", "init.prompt.overwrite", { dir: targetDir }),
368
761
  initial: false
369
762
  });
370
763
  if (!overwrite) {
371
- console.log(chalk6.yellow("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
764
+ console.log(chalk6.yellow(tr(lang, "cli", "common.canceled")));
372
765
  return;
373
766
  }
374
767
  }
375
768
  }
376
769
  console.log();
377
- console.log(chalk6.blue("\u{1F4C1} docs \uAD6C\uC870 \uC0DD\uC131 \uC911..."));
378
- console.log(chalk6.gray(` \uD504\uB85C\uC81D\uD2B8: ${projectName}`));
379
- console.log(chalk6.gray(` \uD0C0\uC785: ${projectType}`));
380
- console.log(chalk6.gray(` \uC5B8\uC5B4: ${lang}`));
381
- console.log(chalk6.gray(` \uACBD\uB85C: ${targetDir}`));
770
+ console.log(chalk6.blue(tr(lang, "cli", "init.log.creatingDocs")));
771
+ console.log(
772
+ chalk6.gray(` ${tr(lang, "cli", "init.log.projectLabel")}: ${projectName}`)
773
+ );
774
+ console.log(
775
+ chalk6.gray(` ${tr(lang, "cli", "init.log.typeLabel")}: ${projectType}`)
776
+ );
777
+ console.log(chalk6.gray(` ${tr(lang, "cli", "init.log.langLabel")}: ${lang}`));
778
+ console.log(
779
+ chalk6.gray(` ${tr(lang, "cli", "init.log.pathLabel")}: ${targetDir}`)
780
+ );
382
781
  console.log();
383
782
  const templatesDir = getTemplatesDir();
384
783
  const commonPath = path4.join(templatesDir, lang, "common");
385
784
  const typePath = path4.join(templatesDir, lang, projectType);
386
- if (await fs6.pathExists(commonPath)) {
785
+ if (await fs8.pathExists(commonPath)) {
387
786
  await copyTemplates(commonPath, targetDir);
388
787
  }
389
- if (!await fs6.pathExists(typePath)) {
390
- throw new Error(`\uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${typePath}`);
788
+ if (!await fs8.pathExists(typePath)) {
789
+ throw new Error(tr(lang, "cli", "init.error.templateNotFound", { path: typePath }));
391
790
  }
392
791
  await copyTemplates(typePath, targetDir);
393
792
  const featurePath = projectType === "fullstack" ? "docs/features/{be|fe}" : "docs/features";
@@ -414,51 +813,74 @@ async function runInit(options) {
414
813
  }
415
814
  }
416
815
  const configPath = path4.join(targetDir, ".lee-spec-kit.json");
417
- await fs6.writeJson(configPath, config, { spaces: 2 });
418
- console.log(chalk6.green("\u2705 docs \uAD6C\uC870 \uC0DD\uC131 \uC644\uB8CC!"));
816
+ await fs8.writeJson(configPath, config, { spaces: 2 });
817
+ console.log(chalk6.green(tr(lang, "cli", "init.log.docsCreated")));
419
818
  console.log();
420
- await initGit(cwd, targetDir, docsRepo, pushDocs, docsRemote);
421
- console.log(chalk6.blue("\uB2E4\uC74C \uB2E8\uACC4:"));
422
- console.log(chalk6.gray(` 1. ${targetDir}/prd/README.md \uC791\uC131`));
819
+ await initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote);
820
+ console.log(chalk6.blue(tr(lang, "cli", "init.log.nextStepsTitle")));
423
821
  console.log(
424
- chalk6.gray(" 2. npx lee-spec-kit feature <name> \uC73C\uB85C \uAE30\uB2A5 \uCD94\uAC00")
822
+ chalk6.gray(tr(lang, "cli", "init.log.nextSteps1", { docsDir: targetDir }))
425
823
  );
824
+ console.log(chalk6.gray(tr(lang, "cli", "init.log.nextSteps2")));
426
825
  console.log();
427
826
  }
428
- async function initGit(cwd, targetDir, docsRepo, pushDocs, docsRemote) {
827
+ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
429
828
  try {
829
+ const runGit = (args, workdir) => {
830
+ execFileSync("git", args, { cwd: workdir, stdio: "ignore" });
831
+ };
832
+ const getCachedStagedFiles = (workdir) => {
833
+ try {
834
+ const out = execFileSync("git", ["diff", "--cached", "--name-only"], {
835
+ cwd: workdir,
836
+ encoding: "utf-8",
837
+ stdio: ["ignore", "pipe", "ignore"]
838
+ }).trim();
839
+ if (!out) return [];
840
+ return out.split("\n").map((s) => s.trim()).filter(Boolean);
841
+ } catch {
842
+ return null;
843
+ }
844
+ };
430
845
  try {
431
- execSync("git rev-parse --is-inside-work-tree", {
432
- cwd,
433
- stdio: "ignore"
434
- });
435
- console.log(chalk6.blue("\u{1F4E6} Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0, docs \uCEE4\uBC0B \uC911..."));
846
+ runGit(["rev-parse", "--is-inside-work-tree"], cwd);
847
+ console.log(chalk6.blue(tr(lang, "cli", "init.log.gitRepoDetectedCommit")));
436
848
  } catch {
437
- console.log(chalk6.blue("\u{1F4E6} Git \uCD08\uAE30\uD654 \uC911..."));
438
- execSync("git init", { cwd, stdio: "ignore" });
849
+ console.log(chalk6.blue(tr(lang, "cli", "init.log.gitInit")));
850
+ runGit(["init"], cwd);
439
851
  }
440
852
  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
- });
853
+ const stagedBeforeAdd = getCachedStagedFiles(cwd);
854
+ if (relativePath === "." && stagedBeforeAdd && stagedBeforeAdd.length > 0) {
855
+ console.log(
856
+ chalk6.yellow(
857
+ tr(lang, "cli", "init.warn.stagedChangesSkip")
858
+ )
859
+ );
860
+ console.log(chalk6.gray(tr(lang, "cli", "init.warn.commitManually")));
861
+ console.log();
862
+ return;
863
+ }
864
+ runGit(["add", relativePath], cwd);
865
+ runGit(
866
+ ["commit", "-m", "init: docs \uAD6C\uC870 \uCD08\uAE30\uD654 (lee-spec-kit)", "--", relativePath],
867
+ cwd
868
+ );
446
869
  if (docsRepo === "standalone" && pushDocs && docsRemote) {
447
870
  try {
448
- execSync(`git remote add origin "${docsRemote}"`, {
449
- cwd,
450
- stdio: "ignore"
451
- });
452
- console.log(chalk6.green(`\u2705 Git remote \uC124\uC815 \uC644\uB8CC: ${docsRemote}`));
871
+ runGit(["remote", "add", "origin", docsRemote], cwd);
872
+ console.log(
873
+ chalk6.green(tr(lang, "cli", "init.log.gitRemoteSet", { remote: docsRemote }))
874
+ );
453
875
  } catch {
454
- console.log(chalk6.yellow("\u26A0\uFE0F Git remote\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4."));
876
+ console.log(chalk6.yellow(tr(lang, "cli", "init.warn.gitRemoteExists")));
455
877
  }
456
878
  }
457
- console.log(chalk6.green("\u2705 Git \uCD08\uAE30 \uCEE4\uBC0B \uC644\uB8CC!"));
879
+ console.log(chalk6.green(tr(lang, "cli", "init.log.gitInitialCommitDone")));
458
880
  console.log();
459
881
  } catch {
460
882
  console.log(
461
- chalk6.yellow("\u26A0\uFE0F Git \uCD08\uAE30\uD654\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4 (\uC218\uB3D9\uC73C\uB85C \uCEE4\uBC0B\uD574\uC8FC\uC138\uC694)")
883
+ chalk6.yellow(tr(lang, "cli", "init.warn.skipGitInit"))
462
884
  );
463
885
  console.log();
464
886
  }
@@ -492,9 +914,9 @@ async function getConfig(cwd) {
492
914
  if (visitedDocsDirs.has(resolvedDocsDir)) continue;
493
915
  visitedDocsDirs.add(resolvedDocsDir);
494
916
  const configPath = path4.join(resolvedDocsDir, ".lee-spec-kit.json");
495
- if (await fs6.pathExists(configPath)) {
917
+ if (await fs8.pathExists(configPath)) {
496
918
  try {
497
- const configFile = await fs6.readJson(configPath);
919
+ const configFile = await fs8.readJson(configPath);
498
920
  return {
499
921
  docsDir: resolvedDocsDir,
500
922
  projectName: configFile.projectName,
@@ -510,17 +932,15 @@ async function getConfig(cwd) {
510
932
  }
511
933
  const agentsPath = path4.join(resolvedDocsDir, "agents");
512
934
  const featuresPath = path4.join(resolvedDocsDir, "features");
513
- if (await fs6.pathExists(agentsPath) && await fs6.pathExists(featuresPath)) {
935
+ if (await fs8.pathExists(agentsPath) && await fs8.pathExists(featuresPath)) {
514
936
  const bePath = path4.join(featuresPath, "be");
515
937
  const fePath = path4.join(featuresPath, "fe");
516
- const projectType = await fs6.pathExists(bePath) || await fs6.pathExists(fePath) ? "fullstack" : "single";
938
+ const projectType = await fs8.pathExists(bePath) || await fs8.pathExists(fePath) ? "fullstack" : "single";
517
939
  const agentsMdPath = path4.join(agentsPath, "agents.md");
518
- let lang = "ko";
519
- if (await fs6.pathExists(agentsMdPath)) {
520
- const content = await fs6.readFile(agentsMdPath, "utf-8");
521
- if (!/[가-힣]/.test(content)) {
522
- lang = "en";
523
- }
940
+ let lang = "en";
941
+ if (await fs8.pathExists(agentsMdPath)) {
942
+ const content = await fs8.readFile(agentsMdPath, "utf-8");
943
+ if (/[가-힣]/.test(content)) lang = "ko";
524
944
  }
525
945
  return { docsDir: resolvedDocsDir, projectType, lang };
526
946
  }
@@ -536,10 +956,13 @@ function featureCommand(program2) {
536
956
  await runFeature(name, options);
537
957
  } catch (error) {
538
958
  if (error instanceof Error && error.message === "canceled") {
539
- console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
959
+ const config = await getConfig(process.cwd());
960
+ const lang = config?.lang ?? DEFAULT_LANG;
961
+ console.log(chalk6.yellow(`
962
+ ${tr(lang, "cli", "common.canceled")}`));
540
963
  process.exit(0);
541
964
  }
542
- console.error(chalk6.red("\uC624\uB958:"), error);
965
+ console.error(chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")), error);
543
966
  process.exit(1);
544
967
  }
545
968
  });
@@ -549,7 +972,9 @@ async function runFeature(name, options) {
549
972
  const config = await getConfig(cwd);
550
973
  if (!config) {
551
974
  console.error(
552
- chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
975
+ chalk6.red(
976
+ tr(DEFAULT_LANG, "cli", "common.docsNotFound")
977
+ )
553
978
  );
554
979
  process.exit(1);
555
980
  }
@@ -561,7 +986,7 @@ async function runFeature(name, options) {
561
986
  {
562
987
  type: "select",
563
988
  name: "repo",
564
- message: "\uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
989
+ message: tr(lang, "cli", "feature.selectRepo"),
565
990
  choices: [
566
991
  { title: "Backend (be)", value: "be" },
567
992
  { title: "Frontend (fe)", value: "fe" }
@@ -593,26 +1018,39 @@ async function runFeature(name, options) {
593
1018
  }
594
1019
  const featureFolderName = `${featureId}-${name}`;
595
1020
  const featureDir = path4.join(featuresDir, featureFolderName);
596
- if (await fs6.pathExists(featureDir)) {
597
- console.error(chalk6.red(`\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uD3F4\uB354\uC785\uB2C8\uB2E4: ${featureDir}`));
1021
+ if (await fs8.pathExists(featureDir)) {
1022
+ console.error(
1023
+ chalk6.red(
1024
+ tr(lang, "cli", "feature.folderExists", { path: featureDir })
1025
+ )
1026
+ );
598
1027
  process.exit(1);
599
1028
  }
600
1029
  const featureBasePath = path4.join(docsDir, "features", "feature-base");
601
- if (!await fs6.pathExists(featureBasePath)) {
602
- console.error(chalk6.red("feature-base \uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
1030
+ if (!await fs8.pathExists(featureBasePath)) {
1031
+ console.error(
1032
+ chalk6.red(
1033
+ tr(lang, "cli", "feature.baseNotFound")
1034
+ )
1035
+ );
603
1036
  process.exit(1);
604
1037
  }
605
- await fs6.copy(featureBasePath, featureDir);
1038
+ await fs8.copy(featureBasePath, featureDir);
606
1039
  const idNumber = featureId.replace("F", "");
607
1040
  const repoName = projectType === "fullstack" && repo ? `{{projectName}}-${repo}` : "{{projectName}}";
608
1041
  const replacements = {
1042
+ // ko placeholders
609
1043
  "{\uAE30\uB2A5\uBA85}": name,
610
1044
  "{\uBC88\uD638}": idNumber,
611
1045
  "YYYY-MM-DD": (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
612
1046
  "{be|fe}": repo || "",
613
- "git-dungeon-{be|fe}": repoName,
614
1047
  "{\uC774\uC288\uBC88\uD638}": "",
615
- "{{description}}": options.desc || ""
1048
+ "{{description}}": options.desc || "",
1049
+ // en placeholders
1050
+ "{feature-name}": name,
1051
+ "{number}": idNumber,
1052
+ "{issue-number}": "",
1053
+ "{{projectName}}-{be|fe}": repoName
616
1054
  };
617
1055
  if (lang === "en") {
618
1056
  replacements["\uAE30\uB2A5 ID"] = "Feature ID";
@@ -624,12 +1062,24 @@ async function runFeature(name, options) {
624
1062
  }
625
1063
  await replaceInFiles(featureDir, replacements);
626
1064
  console.log();
627
- console.log(chalk6.green(`\u2705 Feature \uD3F4\uB354 \uC0DD\uC131 \uC644\uB8CC: ${featureDir}`));
1065
+ console.log(
1066
+ chalk6.green(
1067
+ tr(lang, "cli", "feature.created", { path: featureDir })
1068
+ )
1069
+ );
628
1070
  console.log();
629
- console.log(chalk6.blue("\uB2E4\uC74C \uB2E8\uACC4:"));
630
- console.log(chalk6.gray(` 1. ${featureDir}/spec.md \uC791\uC131`));
631
- console.log(chalk6.gray(" 2. \uC0AC\uC6A9\uC790 \uB9AC\uBDF0 \uC694\uCCAD"));
632
- console.log(chalk6.gray(" 3. \uC2B9\uC778 \uD6C4 plan.md \uC791\uC131"));
1071
+ console.log(chalk6.blue(tr(lang, "cli", "feature.nextStepsTitle")));
1072
+ console.log(
1073
+ chalk6.gray(
1074
+ tr(lang, "cli", "feature.nextSteps1", { path: featureDir })
1075
+ )
1076
+ );
1077
+ console.log(chalk6.gray(tr(lang, "cli", "feature.nextSteps2")));
1078
+ console.log(
1079
+ chalk6.gray(
1080
+ tr(lang, "cli", "feature.nextSteps3")
1081
+ )
1082
+ );
633
1083
  console.log();
634
1084
  }
635
1085
  async function getNextFeatureId(docsDir, projectType) {
@@ -643,8 +1093,8 @@ async function getNextFeatureId(docsDir, projectType) {
643
1093
  scanDirs.push(featuresDir);
644
1094
  }
645
1095
  for (const dir of scanDirs) {
646
- if (!await fs6.pathExists(dir)) continue;
647
- const entries = await fs6.readdir(dir, { withFileTypes: true });
1096
+ if (!await fs8.pathExists(dir)) continue;
1097
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
648
1098
  for (const entry of entries) {
649
1099
  if (!entry.isDirectory()) continue;
650
1100
  const match = entry.name.match(/^F(\d+)-/);
@@ -658,537 +1108,56 @@ async function getNextFeatureId(docsDir, projectType) {
658
1108
  const width = Math.max(3, String(next).length);
659
1109
  return `F${String(next).padStart(width, "0")}`;
660
1110
  }
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
- });
1111
+
1112
+ // src/utils/context/steps.ts
1113
+ function isCompletionChecklistDone(feature) {
1114
+ return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
670
1115
  }
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";
1116
+ function isImplementationDone(feature) {
1117
+ return feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature);
1118
+ }
1119
+ function isPrMetadataConfigured(feature) {
1120
+ return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
1121
+ }
1122
+ function isFeatureDone(feature) {
1123
+ 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";
1124
+ }
1125
+ function getStepDefinitions(lang) {
1126
+ return [
1127
+ {
1128
+ step: 1,
1129
+ name: tr(lang, "steps", "featureFolder"),
1130
+ checklist: { done: () => true }
1131
+ },
1132
+ {
1133
+ step: 2,
1134
+ name: tr(lang, "steps", "specWrite"),
1135
+ checklist: {
1136
+ done: (f) => f.specStatus === "Review" || f.specStatus === "Approved"
1137
+ },
1138
+ current: {
1139
+ when: (f) => !f.docs.specExists || !f.specStatus || f.specStatus === "Draft",
1140
+ actions: (f) => [
1141
+ {
1142
+ type: "instruction",
1143
+ message: !f.docs.specExists ? tr(lang, "messages", "specCreate") : tr(lang, "messages", "specImprove")
1144
+ }
1145
+ ]
718
1146
  }
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
-
1036
- // src/utils/context/i18n.ts
1037
- function formatTemplate(template, vars) {
1038
- return template.replace(/\{(\w+)\}/g, (_, key) => {
1039
- const value = vars[key];
1040
- return value === void 0 ? `{${key}}` : String(value);
1041
- });
1042
- }
1043
- var I18N = {
1044
- ko: {
1045
- steps: {
1046
- featureFolder: "Feature \uD3F4\uB354 \uC0DD\uC131",
1047
- specWrite: "spec.md \uC791\uC131",
1048
- specApprove: "spec.md \uC2B9\uC778",
1049
- planWrite: "plan.md \uC791\uC131",
1050
- planApprove: "plan.md \uC2B9\uC778",
1051
- tasksWrite: "tasks.md \uC791\uC131",
1052
- docsCommitPlanning: "\uBB38\uC11C \uCEE4\uBC0B(\uB3D9\uAE30\uD654)",
1053
- issueCreate: "GitHub Issue \uC0DD\uC131",
1054
- branchCreate: "\uBE0C\uB79C\uCE58 \uC0DD\uC131",
1055
- tasksExecute: "\uD0DC\uC2A4\uD06C \uC2E4\uD589",
1056
- prCreate: "PR \uC0DD\uC131",
1057
- codeReview: "\uCF54\uB4DC \uB9AC\uBDF0",
1058
- featureDone: "Feature \uC644\uB8CC"
1059
- },
1060
- messages: {
1061
- specCreate: "spec.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uC791\uC131\uD558\uC138\uC694. (features/feature-base/spec.md \uCC38\uACE0)",
1062
- specImprove: "spec.md\uB97C \uBCF4\uC644\uD558\uACE0 \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
1063
- specApproval: "spec.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
1064
- planCreate: "plan.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uC791\uC131\uD558\uC138\uC694. (features/feature-base/plan.md \uCC38\uACE0)",
1065
- planImprove: "plan.md\uB97C \uBCF4\uC644\uD558\uACE0 \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
1066
- planApproval: "plan.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
1067
- 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
- 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(planning): {folderName} \uAE30\uD68D \uBB38\uC11C"',
1070
- 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(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
1072
- standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
1073
- createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
1074
- 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.',
1075
- tasksAllDoneButChecklist: "\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uAC00 \uC644\uC804\uD788 \uCCB4\uD06C\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. ({checked}/{total})",
1076
- finishDoingTask: '\uD604\uC7AC DOING/REVIEW \uC911\uC778 \uD0DC\uC2A4\uD06C\uB97C \uC644\uB8CC\uD558\uC138\uC694: "{title}" ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)',
1077
- startNextTodoTask: '\uB2E4\uC74C TODO \uD0DC\uC2A4\uD06C\uB97C \uC2DC\uC791\uD558\uC138\uC694: "{title}" ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)',
1078
- checkTaskStatuses: "\uD0DC\uC2A4\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)",
1079
- 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
- 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)",
1081
- 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
- 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
- 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.",
1084
- fallbackRerunContext: "\uC0C1\uD0DC\uB97C \uD310\uBCC4\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC11C\uB97C \uD655\uC778\uD55C \uB4A4 \uB2E4\uC2DC context\uB97C \uC2E4\uD589\uD558\uC138\uC694."
1085
- },
1086
- warnings: {
1087
- 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
- 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."
1090
- }
1091
- },
1092
- en: {
1093
- steps: {
1094
- featureFolder: "Create feature folder",
1095
- specWrite: "Write spec.md",
1096
- specApprove: "Approve spec.md",
1097
- planWrite: "Write plan.md",
1098
- planApprove: "Approve plan.md",
1099
- tasksWrite: "Write tasks.md",
1100
- docsCommitPlanning: "Commit docs (sync)",
1101
- issueCreate: "Create GitHub Issue",
1102
- branchCreate: "Create branch",
1103
- tasksExecute: "Execute tasks",
1104
- prCreate: "Create PR",
1105
- codeReview: "Code review",
1106
- featureDone: "Feature done"
1107
- },
1108
- messages: {
1109
- specCreate: "Copy the spec.md template and write it. (See features/feature-base/spec.md)",
1110
- specImprove: "Improve spec.md and set Status to Review.",
1111
- specApproval: "Share spec.md with the user and get approval (OK).",
1112
- planCreate: "Copy the plan.md template and write it. (See features/feature-base/plan.md)",
1113
- planImprove: "Improve plan.md and set Status to Review.",
1114
- planApproval: "Share plan.md with the user and get approval (OK).",
1115
- tasksCreate: "Copy the tasks.md template and write tasks. (See features/feature-base/tasks.md)",
1116
- tasksNeedAtLeastOne: "Add at least one task to tasks.md.",
1117
- docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} planning docs"',
1118
- 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(#{issueNumber}): {folderName} docs update"',
1120
- standaloneNeedsProjectRoot: "In standalone mode, projectRoot is required. (npx lee-spec-kit config --project-root ...)",
1121
- createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
1122
- tasksAllDoneButNoChecklist: 'All tasks are DONE but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
1123
- tasksAllDoneButChecklist: "All tasks are DONE but the completion checklist is not fully checked. ({checked}/{total})",
1124
- finishDoingTask: 'Finish the active DOING/REVIEW task: "{title}" ({done}/{total}) (See skills/execute-task.md)',
1125
- startNextTodoTask: 'Start the next TODO task: "{title}" ({done}/{total}) (See skills/execute-task.md)',
1126
- checkTaskStatuses: "Check task statuses. ({done}/{total}) (See skills/execute-task.md)",
1127
- prLegacyAsk: "Legacy tasks.md format detected (missing PR/PR Status fields). Update to the latest format? (OK required)",
1128
- prCreate: "Create a PR and record the PR link in tasks.md. (See skills/create-pr.md)",
1129
- prResolveReview: "Resolve review comments and update PR status. (PR Status: Review \u2192 Approved)",
1130
- prRequestReview: "Request reviews and update PR status to Review.",
1131
- featureDone: "PR is Approved and all tasks/completion criteria are satisfied. This feature is done.",
1132
- fallbackRerunContext: "Unable to determine current state. Verify docs and run context again."
1133
- },
1134
- warnings: {
1135
- projectBranchUnavailable: "Cannot determine project branch. (In standalone mode, projectRoot is required.)",
1136
- 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."
1138
- }
1139
- }
1140
- };
1141
- function tr(lang, category, key, vars = {}) {
1142
- const template = I18N[lang][category][key] ?? I18N.ko[category][key] ?? `${category}.${key}`;
1143
- return formatTemplate(template, vars);
1144
- }
1145
-
1146
- // src/utils/context/steps.ts
1147
- function isCompletionChecklistDone(feature) {
1148
- return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
1149
- }
1150
- function isPrMetadataConfigured(feature) {
1151
- return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
1152
- }
1153
- 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";
1155
- }
1156
- function getStepDefinitions(lang) {
1157
- return [
1158
- {
1159
- step: 1,
1160
- name: tr(lang, "steps", "featureFolder"),
1161
- checklist: { done: () => true }
1162
- },
1163
- {
1164
- step: 2,
1165
- name: tr(lang, "steps", "specWrite"),
1166
- checklist: {
1167
- done: (f) => f.specStatus === "Review" || f.specStatus === "Approved"
1168
- },
1169
- current: {
1170
- when: (f) => !f.docs.specExists || !f.specStatus || f.specStatus === "Draft",
1171
- actions: (f) => [
1172
- {
1173
- type: "instruction",
1174
- message: !f.docs.specExists ? tr(lang, "messages", "specCreate") : tr(lang, "messages", "specImprove")
1175
- }
1176
- ]
1177
- }
1178
- },
1179
- {
1180
- step: 3,
1181
- name: tr(lang, "steps", "specApprove"),
1182
- checklist: { done: (f) => f.specStatus === "Approved" },
1183
- current: {
1184
- when: (f) => f.specStatus === "Review",
1185
- actions: () => [
1186
- {
1187
- type: "instruction",
1188
- requiresUserOk: true,
1189
- message: tr(lang, "messages", "specApproval")
1190
- }
1191
- ]
1147
+ },
1148
+ {
1149
+ step: 3,
1150
+ name: tr(lang, "steps", "specApprove"),
1151
+ checklist: { done: (f) => f.specStatus === "Approved" },
1152
+ current: {
1153
+ when: (f) => f.specStatus === "Review",
1154
+ actions: () => [
1155
+ {
1156
+ type: "instruction",
1157
+ requiresUserOk: true,
1158
+ message: tr(lang, "messages", "specApproval")
1159
+ }
1160
+ ]
1192
1161
  }
1193
1162
  },
1194
1163
  {
@@ -1226,21 +1195,12 @@ function getStepDefinitions(lang) {
1226
1195
  step: 6,
1227
1196
  name: tr(lang, "steps", "tasksWrite"),
1228
1197
  checklist: {
1229
- done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.docs.prFieldExists && f.docs.prStatusFieldExists,
1198
+ done: (f) => f.docs.tasksExists && f.tasks.total > 0,
1230
1199
  detail: (f) => f.tasks.total > 0 ? `(${f.tasks.total})` : ""
1231
1200
  },
1232
1201
  current: {
1233
- when: (f) => f.planStatus === "Approved" && (!f.docs.tasksExists || f.tasks.total === 0 || f.docs.tasksExists && f.tasks.total > 0 && (!f.docs.prFieldExists || !f.docs.prStatusFieldExists)),
1202
+ when: (f) => f.planStatus === "Approved" && (!f.docs.tasksExists || f.tasks.total === 0),
1234
1203
  actions: (f) => {
1235
- if (f.docs.tasksExists && f.tasks.total > 0 && (!f.docs.prFieldExists || !f.docs.prStatusFieldExists)) {
1236
- return [
1237
- {
1238
- type: "instruction",
1239
- requiresUserOk: true,
1240
- message: tr(lang, "messages", "prLegacyAsk")
1241
- }
1242
- ];
1243
- }
1244
1204
  if (!f.docs.tasksExists) {
1245
1205
  return [
1246
1206
  {
@@ -1262,10 +1222,10 @@ function getStepDefinitions(lang) {
1262
1222
  step: 7,
1263
1223
  name: tr(lang, "steps", "docsCommitPlanning"),
1264
1224
  checklist: {
1265
- done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.git.docsHasUncommittedChanges
1225
+ done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && f.git.docsEverCommitted
1266
1226
  },
1267
1227
  current: {
1268
- when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.activeTask && f.git.docsHasUncommittedChanges,
1228
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.activeTask && !f.git.docsEverCommitted && f.git.docsHasUncommittedChanges,
1269
1229
  actions: (f) => {
1270
1230
  if (f.issueNumber) {
1271
1231
  return [
@@ -1323,7 +1283,7 @@ function getStepDefinitions(lang) {
1323
1283
  name: tr(lang, "steps", "branchCreate"),
1324
1284
  checklist: { done: (f) => f.git.onExpectedBranch },
1325
1285
  current: {
1326
- when: (f) => !!f.issueNumber && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
1286
+ when: (f) => !!f.issueNumber && !isImplementationDone(f) && !isFeatureDone(f) && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
1327
1287
  actions: (f) => {
1328
1288
  if (!f.git.projectBranchAvailable || !f.git.projectGitCwd) {
1329
1289
  return [
@@ -1444,6 +1404,15 @@ function getStepDefinitions(lang) {
1444
1404
  current: {
1445
1405
  when: (f) => isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
1446
1406
  actions: (f) => {
1407
+ if (!f.pr.status) {
1408
+ return [
1409
+ {
1410
+ type: "instruction",
1411
+ requiresUserOk: true,
1412
+ message: tr(lang, "messages", "prFillStatus")
1413
+ }
1414
+ ];
1415
+ }
1447
1416
  if (f.pr.status === "Review") {
1448
1417
  return [
1449
1418
  {
@@ -1532,6 +1501,18 @@ function getGitStatusPorcelain(cwd, relativePaths) {
1532
1501
  return void 0;
1533
1502
  }
1534
1503
  }
1504
+ function getLastCommitForPath(cwd, relativePath) {
1505
+ try {
1506
+ const out = execSync(`git rev-list -n 1 HEAD -- "${relativePath}"`, {
1507
+ cwd,
1508
+ encoding: "utf-8",
1509
+ stdio: ["ignore", "pipe", "pipe"]
1510
+ }).trim();
1511
+ return out || void 0;
1512
+ } catch {
1513
+ return void 0;
1514
+ }
1515
+ }
1535
1516
  function getGitTopLevel(cwd) {
1536
1517
  try {
1537
1518
  return execSync("git rev-parse --show-toplevel", {
@@ -1589,7 +1570,7 @@ function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
1589
1570
  function escapeRegExp(value) {
1590
1571
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1591
1572
  }
1592
- function extractSpecValue2(content, key) {
1573
+ function extractSpecValue(content, key) {
1593
1574
  const regex = new RegExp(
1594
1575
  `^\\s*-\\s*\\*\\*${escapeRegExp(key)}\\*\\*\\s*:\\s*(.*)$`,
1595
1576
  "m"
@@ -1599,7 +1580,7 @@ function extractSpecValue2(content, key) {
1599
1580
  }
1600
1581
  function extractFirstSpecValue(content, keys) {
1601
1582
  for (const key of keys) {
1602
- const value = extractSpecValue2(content, key);
1583
+ const value = extractSpecValue(content, key);
1603
1584
  if (value) return value;
1604
1585
  }
1605
1586
  return void 0;
@@ -1669,6 +1650,12 @@ function parseCompletionChecklist(content) {
1669
1650
  }
1670
1651
  return total > 0 ? { total, checked } : void 0;
1671
1652
  }
1653
+ function isCompletionChecklistDone2(feature) {
1654
+ return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
1655
+ }
1656
+ function isPrMetadataConfigured2(feature) {
1657
+ return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
1658
+ }
1672
1659
  async function parseFeature(featurePath, type, context, options) {
1673
1660
  const lang = options.lang;
1674
1661
  const folderName = path4.basename(featurePath);
@@ -1680,22 +1667,22 @@ async function parseFeature(featurePath, type, context, options) {
1680
1667
  const tasksPath = path4.join(featurePath, "tasks.md");
1681
1668
  let specStatus;
1682
1669
  let issueNumber;
1683
- const specExists = await fs6.pathExists(specPath);
1670
+ const specExists = await fs8.pathExists(specPath);
1684
1671
  if (specExists) {
1685
- const content = await fs6.readFile(specPath, "utf-8");
1672
+ const content = await fs8.readFile(specPath, "utf-8");
1686
1673
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
1687
1674
  specStatus = parseDocStatus(statusValue);
1688
1675
  const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
1689
1676
  issueNumber = parseIssueNumber(issueValue);
1690
1677
  }
1691
1678
  let planStatus;
1692
- const planExists = await fs6.pathExists(planPath);
1679
+ const planExists = await fs8.pathExists(planPath);
1693
1680
  if (planExists) {
1694
- const content = await fs6.readFile(planPath, "utf-8");
1681
+ const content = await fs8.readFile(planPath, "utf-8");
1695
1682
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
1696
1683
  planStatus = parseDocStatus(statusValue);
1697
1684
  }
1698
- const tasksExists = await fs6.pathExists(tasksPath);
1685
+ const tasksExists = await fs8.pathExists(tasksPath);
1699
1686
  const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
1700
1687
  let activeTask;
1701
1688
  let nextTodoTask;
@@ -1705,7 +1692,7 @@ async function parseFeature(featurePath, type, context, options) {
1705
1692
  let prFieldExists = false;
1706
1693
  let prStatusFieldExists = false;
1707
1694
  if (tasksExists) {
1708
- const content = await fs6.readFile(tasksPath, "utf-8");
1695
+ const content = await fs8.readFile(tasksPath, "utf-8");
1709
1696
  const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
1710
1697
  tasksSummary.total = summary.total;
1711
1698
  tasksSummary.todo = summary.todo;
@@ -1738,18 +1725,47 @@ async function parseFeature(featurePath, type, context, options) {
1738
1725
  const relativeFeaturePathFromDocs = path4.relative(context.docsDir, featurePath);
1739
1726
  const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [relativeFeaturePathFromDocs]);
1740
1727
  const docsHasUncommittedChanges = docsStatus === void 0 ? true : docsStatus.trim().length > 0;
1728
+ const docsLastCommit = getLastCommitForPath(
1729
+ context.docsGitCwd,
1730
+ relativeFeaturePathFromDocs
1731
+ );
1732
+ const docsEverCommitted = !!docsLastCommit;
1741
1733
  if (docsStatus === void 0) {
1742
1734
  warnings.push(tr(lang, "warnings", "docsGitUnavailable"));
1743
1735
  }
1744
1736
  if (tasksExists && (!prFieldExists || !prStatusFieldExists)) {
1745
1737
  warnings.push(tr(lang, "warnings", "legacyTasksPrFields"));
1746
1738
  }
1739
+ if (docsEverCommitted && docsHasUncommittedChanges) {
1740
+ warnings.push(tr(lang, "warnings", "docsUncommittedChanges"));
1741
+ }
1742
+ const implementationDone = tasksExists && tasksSummary.total > 0 && tasksSummary.total === tasksSummary.done && isCompletionChecklistDone2({ completionChecklist });
1743
+ const workflowDone = implementationDone && specStatus === "Approved" && planStatus === "Approved" && isPrMetadataConfigured2({ docs: { prFieldExists, prStatusFieldExists } }) && !!prLink && prStatus === "Approved";
1744
+ if (implementationDone && !workflowDone) {
1745
+ if (specStatus !== "Approved") {
1746
+ warnings.push(tr(lang, "warnings", "workflowSpecNotApproved"));
1747
+ }
1748
+ if (planStatus !== "Approved") {
1749
+ warnings.push(tr(lang, "warnings", "workflowPlanNotApproved"));
1750
+ }
1751
+ if (prFieldExists && prStatusFieldExists) {
1752
+ if (!prLink) warnings.push(tr(lang, "warnings", "workflowPrLinkMissing"));
1753
+ if (!prStatus) warnings.push(tr(lang, "warnings", "workflowPrStatusMissing"));
1754
+ if (prStatus && prStatus !== "Approved") {
1755
+ warnings.push(tr(lang, "warnings", "workflowPrStatusNotApproved"));
1756
+ }
1757
+ }
1758
+ }
1747
1759
  const featureState = {
1748
1760
  id,
1749
1761
  slug,
1750
1762
  folderName,
1751
1763
  type,
1752
1764
  path: featurePath,
1765
+ completion: {
1766
+ implementationDone,
1767
+ workflowDone
1768
+ },
1753
1769
  issueNumber,
1754
1770
  specStatus,
1755
1771
  planStatus,
@@ -1765,6 +1781,7 @@ async function parseFeature(featurePath, type, context, options) {
1765
1781
  docsGitCwd: context.docsGitCwd,
1766
1782
  projectGitCwd: context.projectGitCwd,
1767
1783
  onExpectedBranch,
1784
+ docsEverCommitted,
1768
1785
  docsHasUncommittedChanges
1769
1786
  },
1770
1787
  docs: {
@@ -1815,7 +1832,7 @@ async function scanFeatures(config) {
1815
1832
  ignore: ["**/feature-base/**"]
1816
1833
  });
1817
1834
  for (const dir of featureDirs) {
1818
- if ((await fs6.stat(dir)).isDirectory()) {
1835
+ if ((await fs8.stat(dir)).isDirectory()) {
1819
1836
  features.push(
1820
1837
  await parseFeature(
1821
1838
  dir,
@@ -1837,7 +1854,7 @@ async function scanFeatures(config) {
1837
1854
  const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
1838
1855
  const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
1839
1856
  for (const dir of feDirs) {
1840
- if ((await fs6.stat(dir)).isDirectory()) {
1857
+ if ((await fs8.stat(dir)).isDirectory()) {
1841
1858
  features.push(
1842
1859
  await parseFeature(
1843
1860
  dir,
@@ -1855,39 +1872,453 @@ async function scanFeatures(config) {
1855
1872
  );
1856
1873
  }
1857
1874
  }
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
- );
1875
+ for (const dir of beDirs) {
1876
+ if ((await fs8.stat(dir)).isDirectory()) {
1877
+ features.push(
1878
+ await parseFeature(
1879
+ dir,
1880
+ "be",
1881
+ {
1882
+ projectBranch: projectBranches.be,
1883
+ docsBranch,
1884
+ docsGitCwd: config.docsDir,
1885
+ projectGitCwd: beProject?.cwd ?? void 0,
1886
+ docsDir: config.docsDir,
1887
+ projectBranchAvailable: Boolean(beProject?.cwd)
1888
+ },
1889
+ { lang: config.lang, stepDefinitions }
1890
+ )
1891
+ );
1892
+ }
1893
+ }
1894
+ }
1895
+ return {
1896
+ features,
1897
+ branches: {
1898
+ docs: docsBranch,
1899
+ project: config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
1900
+ },
1901
+ warnings
1902
+ };
1903
+ }
1904
+
1905
+ // src/commands/status.ts
1906
+ function statusCommand(program2) {
1907
+ 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) => {
1908
+ try {
1909
+ await runStatus(options);
1910
+ } catch (error) {
1911
+ console.error(chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")), error);
1912
+ process.exit(1);
1913
+ }
1914
+ });
1915
+ }
1916
+ async function runStatus(options) {
1917
+ const cwd = process.cwd();
1918
+ const config = await getConfig(cwd);
1919
+ if (!config) {
1920
+ console.error(chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")));
1921
+ console.error(
1922
+ chalk6.red(
1923
+ tr(DEFAULT_LANG, "cli", "common.docsNotFound")
1924
+ )
1925
+ );
1926
+ process.exit(1);
1927
+ }
1928
+ const { docsDir, projectType, projectName, lang } = config;
1929
+ const featuresDir = path4.join(docsDir, "features");
1930
+ const scan = await scanFeatures(config);
1931
+ const features = [];
1932
+ const idMap = /* @__PURE__ */ new Map();
1933
+ for (const f of scan.features) {
1934
+ if (!f.docs.specExists || !f.docs.tasksExists) continue;
1935
+ const id = f.id || "UNKNOWN";
1936
+ const name = await getFeatureNameFromSpec(f.path, f.slug, f.folderName);
1937
+ const repo = projectType === "fullstack" ? `${projectName ?? "{{projectName}}"}-${f.type === "single" ? "" : f.type}`.replace(
1938
+ /-$/,
1939
+ ""
1940
+ ) : projectName ?? "{{projectName}}";
1941
+ const issue = f.issueNumber ? `#${f.issueNumber}` : "-";
1942
+ const relPath = path4.relative(docsDir, f.path);
1943
+ if (!idMap.has(id)) idMap.set(id, []);
1944
+ idMap.get(id).push(relPath);
1945
+ const total = f.tasks.total;
1946
+ const done = f.tasks.done;
1947
+ const doing = f.tasks.doing;
1948
+ const todo = f.tasks.todo;
1949
+ let status = "TODO";
1950
+ if (total > 0 && done === total) status = "DONE";
1951
+ else if (doing > 0) status = "DOING";
1952
+ else if (todo > 0) status = "TODO";
1953
+ else if (total === 0) status = "NO_TASKS";
1954
+ features.push({
1955
+ id,
1956
+ name,
1957
+ repo,
1958
+ issue,
1959
+ status,
1960
+ progress: `${done}/${total}`,
1961
+ path: relPath
1962
+ });
1963
+ }
1964
+ if (features.length === 0) {
1965
+ console.log(chalk6.yellow(tr(lang, "cli", "status.noFeatures")));
1966
+ return;
1967
+ }
1968
+ if (options.strict) {
1969
+ const duplicates = [...idMap.entries()].filter(
1970
+ ([, paths]) => paths.length > 1
1971
+ );
1972
+ if (duplicates.length > 0) {
1973
+ console.error(chalk6.red(tr(lang, "cli", "status.duplicateIds")));
1974
+ for (const [id, paths] of duplicates) {
1975
+ console.error(chalk6.red(` ${id}:`));
1976
+ for (const p of paths) {
1977
+ console.error(chalk6.red(` - ${p}`));
1978
+ }
1979
+ }
1980
+ process.exit(1);
1981
+ }
1982
+ const unknowns = [...idMap.entries()].filter(([id]) => id === "UNKNOWN");
1983
+ if (unknowns.length > 0) {
1984
+ console.error(chalk6.red(tr(lang, "cli", "status.missingIds")));
1985
+ for (const [, paths] of unknowns) {
1986
+ for (const p of paths) {
1987
+ console.error(chalk6.red(` - ${p}`));
1988
+ }
1989
+ }
1990
+ process.exit(1);
1991
+ }
1992
+ }
1993
+ features.sort((a, b) => a.id.localeCompare(b.id));
1994
+ const header = "| ID | Name | Repo | Issue | Status | Progress | Path |";
1995
+ const separator = "| --- | --- | --- | --- | --- | --- | --- |";
1996
+ console.log();
1997
+ console.log(header);
1998
+ console.log(separator);
1999
+ for (const f of features) {
2000
+ const statusColor = f.status === "DONE" ? chalk6.green : f.status === "DOING" ? chalk6.yellow : chalk6.gray;
2001
+ console.log(
2002
+ `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${statusColor(f.status)} | ${f.progress} | ${f.path} |`
2003
+ );
2004
+ }
2005
+ console.log();
2006
+ if (options.write) {
2007
+ const outputPath = path4.join(featuresDir, "status.md");
2008
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2009
+ const content = [
2010
+ "# Feature Status",
2011
+ "",
2012
+ `- Generated: ${date}`,
2013
+ "- Source: `tasks.md`, `spec.md`",
2014
+ "",
2015
+ header,
2016
+ separator,
2017
+ ...features.map(
2018
+ (f) => `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${f.status} | ${f.progress} | ${f.path} |`
2019
+ ),
2020
+ ""
2021
+ ].join("\n");
2022
+ await fs8.writeFile(outputPath, content, "utf-8");
2023
+ console.log(
2024
+ chalk6.green(
2025
+ tr(lang, "cli", "status.wrote", { path: outputPath })
2026
+ )
2027
+ );
2028
+ }
2029
+ }
2030
+ function escapeRegExp2(value) {
2031
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2032
+ }
2033
+ async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderName) {
2034
+ try {
2035
+ const specPath = path4.join(featureDir, "spec.md");
2036
+ if (!await fs8.pathExists(specPath)) return fallbackSlug;
2037
+ const content = await fs8.readFile(specPath, "utf-8");
2038
+ const keys = ["\uAE30\uB2A5\uBA85", "Feature Name"];
2039
+ for (const key of keys) {
2040
+ const regex = new RegExp(
2041
+ `^\\s*-\\s*\\*\\*${escapeRegExp2(key)}\\*\\*\\s*:\\s*(.*)$`,
2042
+ "m"
2043
+ );
2044
+ const match = content.match(regex);
2045
+ const value = match?.[1]?.trim();
2046
+ if (value) return value;
2047
+ }
2048
+ } catch {
2049
+ }
2050
+ return fallbackSlug || fallbackFolderName;
2051
+ }
2052
+ function updateCommand(program2) {
2053
+ 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) => {
2054
+ try {
2055
+ await runUpdate(options);
2056
+ } catch (error) {
2057
+ if (error instanceof Error && error.message === "canceled") {
2058
+ const config = await getConfig(process.cwd());
2059
+ const lang = config?.lang ?? DEFAULT_LANG;
2060
+ console.log(chalk6.yellow(`
2061
+ ${tr(lang, "cli", "common.canceled")}`));
2062
+ process.exit(0);
2063
+ }
2064
+ console.error(chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")), error);
2065
+ process.exit(1);
2066
+ }
2067
+ });
2068
+ }
2069
+ async function runUpdate(options) {
2070
+ const cwd = process.cwd();
2071
+ const config = await getConfig(cwd);
2072
+ if (!config) {
2073
+ console.error(chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")));
2074
+ console.error(
2075
+ chalk6.red(
2076
+ tr(DEFAULT_LANG, "cli", "common.docsNotFound")
2077
+ )
2078
+ );
2079
+ process.exit(1);
2080
+ }
2081
+ const { docsDir, projectType, lang } = config;
2082
+ const templatesDir = getTemplatesDir();
2083
+ const sourceDir = path4.join(templatesDir, lang, projectType);
2084
+ const updateAgents = options.agents || !options.agents && !options.templates;
2085
+ const updateTemplates = options.templates || !options.agents && !options.templates;
2086
+ console.log(chalk6.blue(tr(lang, "cli", "update.start")));
2087
+ console.log(chalk6.gray(` - ${tr(lang, "cli", "update.langLabel")}: ${lang}`));
2088
+ console.log(
2089
+ chalk6.gray(` - ${tr(lang, "cli", "update.typeLabel")}: ${projectType}`)
2090
+ );
2091
+ console.log();
2092
+ let updatedCount = 0;
2093
+ if (updateAgents) {
2094
+ console.log(chalk6.blue(tr(lang, "cli", "update.updatingAgents")));
2095
+ const commonAgents = path4.join(templatesDir, lang, "common", "agents");
2096
+ const typeAgents = path4.join(templatesDir, lang, projectType, "agents");
2097
+ const targetAgents = path4.join(docsDir, "agents");
2098
+ const featurePath = projectType === "fullstack" ? "docs/features/{be|fe}" : "docs/features";
2099
+ const replacements = {
2100
+ "{{featurePath}}": featurePath
2101
+ };
2102
+ if (await fs8.pathExists(commonAgents)) {
2103
+ const count = await updateFolder(
2104
+ commonAgents,
2105
+ targetAgents,
2106
+ options.force,
2107
+ replacements,
2108
+ lang
2109
+ );
2110
+ updatedCount += count;
2111
+ }
2112
+ if (await fs8.pathExists(typeAgents)) {
2113
+ const count = await updateFolder(
2114
+ typeAgents,
2115
+ targetAgents,
2116
+ options.force,
2117
+ void 0,
2118
+ lang
2119
+ );
2120
+ updatedCount += count;
2121
+ }
2122
+ console.log(chalk6.green(` \u2705 ${tr(lang, "cli", "update.agentsUpdated")}`));
2123
+ }
2124
+ if (updateTemplates) {
2125
+ console.log(chalk6.blue(tr(lang, "cli", "update.updatingFeatureBase")));
2126
+ const sourceFeatureBase = path4.join(sourceDir, "features", "feature-base");
2127
+ const targetFeatureBase = path4.join(docsDir, "features", "feature-base");
2128
+ if (await fs8.pathExists(sourceFeatureBase)) {
2129
+ const count = await updateFolder(
2130
+ sourceFeatureBase,
2131
+ targetFeatureBase,
2132
+ options.force,
2133
+ void 0,
2134
+ lang
2135
+ );
2136
+ updatedCount += count;
2137
+ console.log(
2138
+ chalk6.green(
2139
+ ` \u2705 ${tr(lang, "cli", "update.filesUpdated", { count })}`
2140
+ )
2141
+ );
2142
+ }
2143
+ }
2144
+ console.log();
2145
+ console.log(
2146
+ chalk6.green(
2147
+ `\u2705 ${tr(lang, "cli", "update.updatedTotal", { count: updatedCount })}`
2148
+ )
2149
+ );
2150
+ }
2151
+ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DEFAULT_LANG) {
2152
+ const protectedFiles = /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
2153
+ await fs8.ensureDir(targetDir);
2154
+ const files = await fs8.readdir(sourceDir);
2155
+ let updatedCount = 0;
2156
+ for (const file of files) {
2157
+ const sourcePath = path4.join(sourceDir, file);
2158
+ const targetPath = path4.join(targetDir, file);
2159
+ const stat = await fs8.stat(sourcePath);
2160
+ if (stat.isFile()) {
2161
+ if (protectedFiles.has(file)) {
2162
+ continue;
2163
+ }
2164
+ let sourceContent = await fs8.readFile(sourcePath, "utf-8");
2165
+ if (replacements) {
2166
+ for (const [key, value] of Object.entries(replacements)) {
2167
+ sourceContent = sourceContent.replaceAll(key, value);
2168
+ }
2169
+ }
2170
+ let shouldUpdate = true;
2171
+ if (await fs8.pathExists(targetPath)) {
2172
+ const targetContent = await fs8.readFile(targetPath, "utf-8");
2173
+ if (sourceContent === targetContent) {
2174
+ continue;
2175
+ }
2176
+ if (!force) {
2177
+ console.log(
2178
+ chalk6.yellow(
2179
+ ` \u26A0\uFE0F ${file} - ${tr(lang, "cli", "update.changeDetected")}`
2180
+ )
2181
+ );
2182
+ shouldUpdate = false;
2183
+ }
2184
+ }
2185
+ if (shouldUpdate) {
2186
+ await fs8.writeFile(targetPath, sourceContent);
2187
+ console.log(
2188
+ chalk6.gray(` \u{1F4C4} ${tr(lang, "cli", "update.fileUpdated", { file })}`)
2189
+ );
2190
+ updatedCount++;
2191
+ }
2192
+ } else if (stat.isDirectory()) {
2193
+ const subCount = await updateFolder(
2194
+ sourcePath,
2195
+ targetPath,
2196
+ force,
2197
+ replacements,
2198
+ lang
2199
+ );
2200
+ updatedCount += subCount;
2201
+ }
2202
+ }
2203
+ return updatedCount;
2204
+ }
2205
+ function configCommand(program2) {
2206
+ 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) => {
2207
+ try {
2208
+ await runConfig(options);
2209
+ } catch (error) {
2210
+ if (error instanceof Error && error.message === "canceled") {
2211
+ const config = await getConfig(process.cwd());
2212
+ const lang = config?.lang ?? DEFAULT_LANG;
2213
+ console.log(chalk6.yellow(`
2214
+ ${tr(lang, "cli", "common.canceled")}`));
2215
+ process.exit(0);
1875
2216
  }
2217
+ console.error(chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")), error);
2218
+ process.exit(1);
1876
2219
  }
2220
+ });
2221
+ }
2222
+ async function runConfig(options) {
2223
+ const cwd = process.cwd();
2224
+ const config = await getConfig(cwd);
2225
+ if (!config) {
2226
+ console.log(
2227
+ chalk6.red(
2228
+ tr(DEFAULT_LANG, "cli", "common.configNotFound")
2229
+ )
2230
+ );
2231
+ process.exit(1);
1877
2232
  }
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
- };
2233
+ const configPath = path4.join(config.docsDir, ".lee-spec-kit.json");
2234
+ if (!options.projectRoot) {
2235
+ console.log();
2236
+ console.log(chalk6.blue(tr(config.lang, "cli", "config.currentTitle")));
2237
+ console.log();
2238
+ console.log(
2239
+ chalk6.gray(
2240
+ ` ${tr(config.lang, "cli", "config.pathLabel")}: ${configPath}`
2241
+ )
2242
+ );
2243
+ console.log();
2244
+ const configFile2 = await fs8.readJson(configPath);
2245
+ console.log(JSON.stringify(configFile2, null, 2));
2246
+ console.log();
2247
+ return;
2248
+ }
2249
+ const configFile = await fs8.readJson(configPath);
2250
+ if (configFile.docsRepo !== "standalone") {
2251
+ console.log(
2252
+ chalk6.yellow(
2253
+ tr(config.lang, "cli", "config.projectRootStandaloneOnly")
2254
+ )
2255
+ );
2256
+ return;
2257
+ }
2258
+ const projectType = configFile.projectType;
2259
+ if (projectType === "fullstack") {
2260
+ if (!options.repo) {
2261
+ const response = await prompts(
2262
+ [
2263
+ {
2264
+ type: "select",
2265
+ name: "repo",
2266
+ message: tr(config.lang, "cli", "config.selectRepoToUpdate"),
2267
+ choices: [
2268
+ { title: "Frontend (fe)", value: "fe" },
2269
+ { title: "Backend (be)", value: "be" }
2270
+ ]
2271
+ }
2272
+ ],
2273
+ {
2274
+ onCancel: () => {
2275
+ throw new Error("canceled");
2276
+ }
2277
+ }
2278
+ );
2279
+ options.repo = response.repo;
2280
+ }
2281
+ if (!options.repo || !["fe", "be"].includes(options.repo)) {
2282
+ console.log(
2283
+ chalk6.red(
2284
+ tr(config.lang, "cli", "config.fullstackRepoRequired")
2285
+ )
2286
+ );
2287
+ return;
2288
+ }
2289
+ const currentRoot = configFile.projectRoot || { fe: "", be: "" };
2290
+ if (typeof currentRoot === "string") {
2291
+ configFile.projectRoot = {
2292
+ fe: options.repo === "fe" ? options.projectRoot : "",
2293
+ be: options.repo === "be" ? options.projectRoot : ""
2294
+ };
2295
+ } else {
2296
+ currentRoot[options.repo] = options.projectRoot;
2297
+ configFile.projectRoot = currentRoot;
2298
+ }
2299
+ console.log(
2300
+ chalk6.green(
2301
+ tr(config.lang, "cli", "config.projectRootSet", {
2302
+ repo: options.repo.toUpperCase(),
2303
+ path: options.projectRoot
2304
+ })
2305
+ )
2306
+ );
2307
+ } else {
2308
+ configFile.projectRoot = options.projectRoot;
2309
+ console.log(
2310
+ chalk6.green(
2311
+ tr(config.lang, "cli", "config.projectRootSetSingle", {
2312
+ path: options.projectRoot
2313
+ })
2314
+ )
2315
+ );
2316
+ }
2317
+ await fs8.writeJson(configPath, configFile, { spaces: 2 });
2318
+ console.log();
1886
2319
  }
1887
-
1888
- // src/commands/context.ts
1889
2320
  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(
2321
+ 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
2322
  async (featureName, options) => {
1892
2323
  try {
1893
2324
  await runContext(featureName, options);
@@ -1900,7 +2331,10 @@ function contextCommand(program2) {
1900
2331
  })
1901
2332
  );
1902
2333
  } else {
1903
- console.error(chalk6.red("\uC624\uB958:"), error);
2334
+ console.error(
2335
+ chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")),
2336
+ error
2337
+ );
1904
2338
  }
1905
2339
  process.exit(1);
1906
2340
  }
@@ -1923,22 +2357,63 @@ function detectFromBranch(branchName, features) {
1923
2357
  (f) => f.slug.toLowerCase() === detected.toLowerCase() || f.folderName.toLowerCase() === detected.toLowerCase()
1924
2358
  );
1925
2359
  }
2360
+ function getListLabel(f, stepsMap, lang) {
2361
+ if (f.completion.implementationDone && !f.completion.workflowDone) {
2362
+ if (f.git.docsHasUncommittedChanges) {
2363
+ return tr(lang, "cli", "context.list.docsCommitNeeded");
2364
+ }
2365
+ if (!f.issueNumber) {
2366
+ return tr(lang, "cli", "context.list.issueNumberNeeded");
2367
+ }
2368
+ if (!f.docs.prFieldExists || !f.docs.prStatusFieldExists) {
2369
+ return tr(lang, "cli", "context.list.addPrMetadata");
2370
+ }
2371
+ if (!f.pr.link) {
2372
+ return tr(lang, "cli", "context.list.recordPrLink");
2373
+ }
2374
+ if (!f.pr.status) {
2375
+ return tr(lang, "cli", "context.list.setPrStatus");
2376
+ }
2377
+ if (f.pr.status !== "Approved") {
2378
+ return tr(lang, "cli", "context.list.prStatusToApproved", {
2379
+ status: f.pr.status
2380
+ });
2381
+ }
2382
+ if (f.specStatus !== "Approved") {
2383
+ return tr(lang, "cli", "context.list.approveSpec");
2384
+ }
2385
+ if (f.planStatus !== "Approved") {
2386
+ return tr(lang, "cli", "context.list.approvePlan");
2387
+ }
2388
+ }
2389
+ return stepsMap[f.currentStep] || "Unknown";
2390
+ }
1926
2391
  async function runContext(featureName, options) {
1927
2392
  const cwd = process.cwd();
1928
2393
  const config = await getConfig(cwd);
1929
- const lang = config?.lang ?? "ko";
2394
+ const lang = config?.lang ?? "en";
1930
2395
  if (!config) {
1931
- throw new Error("\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
2396
+ throw new Error(tr(DEFAULT_LANG, "cli", "common.configNotFound"));
1932
2397
  }
1933
2398
  const stepDefinitions = getStepDefinitions(lang);
1934
2399
  const stepsMap = getStepsMap(lang);
1935
2400
  const { features, branches, warnings } = await scanFeatures(config);
2401
+ const doneFeatures = features.filter((f2) => f2.completion.workflowDone);
2402
+ const openFeatures = features.filter((f2) => !f2.completion.workflowDone);
2403
+ const inProgressFeatures = openFeatures.filter(
2404
+ (f2) => !f2.completion.implementationDone
2405
+ );
2406
+ const readyToCloseFeatures = openFeatures.filter(
2407
+ (f2) => f2.completion.implementationDone
2408
+ );
1936
2409
  let targetFeatures = [];
2410
+ let selectionMode = "explicit";
1937
2411
  if (featureName) {
1938
2412
  targetFeatures = features.filter((f2) => matchesFeatureSelector(f2, featureName));
1939
2413
  if (options.repo) {
1940
2414
  targetFeatures = targetFeatures.filter((f2) => f2.type === options.repo);
1941
2415
  }
2416
+ selectionMode = "explicit";
1942
2417
  } else {
1943
2418
  if (config.projectType === "single") {
1944
2419
  const branchName = branches.project.single || "";
@@ -1960,15 +2435,33 @@ async function runContext(featureName, options) {
1960
2435
  ) : [];
1961
2436
  targetFeatures = [...feMatches, ...beMatches];
1962
2437
  }
1963
- if (targetFeatures.length === 0) targetFeatures = features;
2438
+ if (targetFeatures.length > 0) {
2439
+ selectionMode = "branch";
2440
+ } else if (options.all) {
2441
+ targetFeatures = features;
2442
+ selectionMode = "all";
2443
+ } else if (options.done) {
2444
+ targetFeatures = doneFeatures;
2445
+ selectionMode = "done";
2446
+ } else {
2447
+ targetFeatures = openFeatures;
2448
+ selectionMode = "open";
2449
+ }
1964
2450
  }
1965
2451
  if (options.json) {
2452
+ const isNoOpen = selectionMode === "open" && features.length > 0 && openFeatures.length === 0;
1966
2453
  const result = {
1967
- status: features.length === 0 ? "no_features" : targetFeatures.length === 1 ? "single_matched" : targetFeatures.length > 1 ? "multiple_active" : "no_match",
2454
+ status: features.length === 0 ? "no_features" : isNoOpen ? "no_open" : targetFeatures.length === 1 ? "single_matched" : targetFeatures.length > 1 ? "multiple_active" : "no_match",
2455
+ selectionMode,
1968
2456
  branches,
1969
2457
  warnings,
1970
2458
  matchedFeature: targetFeatures.length === 1 ? targetFeatures[0] : null,
1971
2459
  candidates: targetFeatures.length > 1 ? targetFeatures : [],
2460
+ // "Completed" now means workflow-done.
2461
+ completedCandidates: selectionMode === "open" ? doneFeatures : [],
2462
+ openCandidates: selectionMode === "open" ? openFeatures : [],
2463
+ inProgressCandidates: selectionMode === "open" ? inProgressFeatures : [],
2464
+ readyToCloseCandidates: selectionMode === "open" ? readyToCloseFeatures : [],
1972
2465
  actions: targetFeatures.length === 1 ? targetFeatures[0].actions : [],
1973
2466
  recommendation: ""
1974
2467
  };
@@ -2014,41 +2507,99 @@ async function runContext(featureName, options) {
2014
2507
  console.log(chalk6.gray("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
2015
2508
  console.log();
2016
2509
  if (features.length === 0) {
2017
- console.log(chalk6.yellow("\u26A0\uFE0F \uC9C4\uD589 \uC911\uC778 Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
2510
+ console.log(
2511
+ chalk6.yellow(
2512
+ tr(lang, "cli", "context.noActiveFeatures")
2513
+ )
2514
+ );
2018
2515
  console.log();
2019
2516
  return;
2020
2517
  }
2021
2518
  if (warnings.length > 0) {
2022
- console.log(chalk6.yellow("\u26A0\uFE0F \uD658\uACBD \uACBD\uACE0:"));
2519
+ console.log(chalk6.yellow(tr(lang, "cli", "context.envWarnings")));
2023
2520
  warnings.forEach((w) => console.log(chalk6.yellow(` - ${w}`)));
2024
2521
  console.log();
2025
2522
  }
2026
2523
  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})`) : "";
2524
+ if (selectionMode === "open") {
2034
2525
  console.log(
2035
- ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
2526
+ chalk6.gray(
2527
+ ` ${tr(lang, "cli", "context.openFallbackSummary", {
2528
+ inProgress: inProgressFeatures.length,
2529
+ readyToClose: readyToCloseFeatures.length,
2530
+ done: doneFeatures.length
2531
+ })}`
2532
+ )
2036
2533
  );
2037
- });
2534
+ console.log();
2535
+ }
2536
+ if (selectionMode === "open") {
2537
+ console.log(
2538
+ chalk6.blue(
2539
+ `\u{1F539} ${tr(lang, "cli", "context.sectionInProgress")} (${inProgressFeatures.length})`
2540
+ )
2541
+ );
2542
+ inProgressFeatures.forEach((f2) => {
2543
+ const stepName2 = getListLabel(f2, stepsMap, lang);
2544
+ const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
2545
+ console.log(
2546
+ ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
2547
+ );
2548
+ });
2549
+ console.log();
2550
+ console.log(
2551
+ chalk6.blue(
2552
+ `\u{1F538} ${tr(lang, "cli", "context.sectionReadyToClose")} (${readyToCloseFeatures.length})`
2553
+ )
2554
+ );
2555
+ readyToCloseFeatures.forEach((f2) => {
2556
+ const stepName2 = getListLabel(f2, stepsMap, lang);
2557
+ const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
2558
+ console.log(
2559
+ ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
2560
+ );
2561
+ });
2562
+ } else {
2563
+ const title = selectionMode === "all" ? `\u{1F539} ${targetFeatures.length} Features:` : selectionMode === "done" ? `\u{1F539} ${targetFeatures.length} Done Features:` : `\u{1F539} ${targetFeatures.length} Features Detected:`;
2564
+ console.log(chalk6.blue(title));
2565
+ console.log();
2566
+ targetFeatures.forEach((f2) => {
2567
+ const stepName2 = getListLabel(f2, stepsMap, lang);
2568
+ const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
2569
+ console.log(
2570
+ ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
2571
+ );
2572
+ });
2573
+ }
2038
2574
  console.log();
2039
- console.log(chalk6.gray("Tip: \uD2B9\uC815 Feature\uC758 \uC0C1\uC138 \uC815\uBCF4\uB97C \uBCF4\uB824\uBA74:"));
2575
+ console.log(chalk6.gray(tr(lang, "cli", "context.tipDetails")));
2040
2576
  console.log(
2041
2577
  chalk6.gray(" $ npx lee-spec-kit context <slug|F001|F001-slug> [--repo fe|be]")
2042
2578
  );
2579
+ if (selectionMode === "open") {
2580
+ console.log(
2581
+ chalk6.gray(
2582
+ ` $ npx lee-spec-kit context --all # ${tr(lang, "cli", "context.tipShowAll")}`
2583
+ )
2584
+ );
2585
+ console.log(
2586
+ chalk6.gray(
2587
+ ` $ npx lee-spec-kit context --done # ${tr(lang, "cli", "context.tipShowDone")}`
2588
+ )
2589
+ );
2590
+ }
2043
2591
  console.log();
2044
2592
  return;
2045
2593
  }
2046
2594
  const f = targetFeatures[0];
2047
2595
  const stepName = stepsMap[f.currentStep] || "Unknown";
2048
- const okTag = (requiresUserOk) => requiresUserOk ? chalk6.yellow(lang === "ko" ? "[OK \uD544\uC694] " : "[OK required] ") : "";
2596
+ const okTag = (requiresUserOk) => requiresUserOk ? chalk6.yellow(tr(lang, "cli", "context.okRequired")) : "";
2049
2597
  console.log(
2050
2598
  `\u{1F539} Feature: ${chalk6.bold(f.folderName)} ${config.projectType === "fullstack" ? chalk6.cyan(`(${f.type})`) : ""}`
2051
2599
  );
2600
+ console.log(
2601
+ ` \u2022 Completion: ${f.completion.implementationDone ? chalk6.green("Implementation \u2705") : chalk6.gray("Implementation \u25EF")} / ${f.completion.workflowDone ? chalk6.green("Workflow \u2705") : chalk6.yellow("Workflow \u25EF")}`
2602
+ );
2052
2603
  if (f.issueNumber) {
2053
2604
  console.log(` \u2022 Issue: #${f.issueNumber}`);
2054
2605
  }
@@ -2116,13 +2667,248 @@ function printChecklist(f, stepDefinitions) {
2116
2667
  console.log(` ${mark} ${definition.step}. ${label} ${detail}`);
2117
2668
  });
2118
2669
  }
2670
+ function formatPath(cwd, p) {
2671
+ if (!p) return "";
2672
+ return path4.isAbsolute(p) ? path4.relative(cwd, p) : p;
2673
+ }
2674
+ function detectPlaceholders(content) {
2675
+ const patterns = [
2676
+ { key: "{{projectName}}", re: /\{\{projectName\}\}/g },
2677
+ { key: "{{date}}", re: /\{\{date\}\}/g },
2678
+ { key: "{{featurePath}}", re: /\{\{featurePath\}\}/g },
2679
+ { key: "{{description}}", re: /\{\{description\}\}/g },
2680
+ { key: "{\uAE30\uB2A5\uBA85}", re: /\{기능명\}/g },
2681
+ { key: "{\uBC88\uD638}", re: /\{번호\}/g },
2682
+ { key: "{\uC774\uC288\uBC88\uD638}", re: /\{이슈번호\}/g },
2683
+ { key: "{feature-name}", re: /\{feature-name\}/g },
2684
+ { key: "{number}", re: /\{number\}/g },
2685
+ { key: "{issue-number}", re: /\{issue-number\}/g },
2686
+ { key: "{be|fe}", re: /\{be\|fe\}/g },
2687
+ { key: "YYYY-MM-DD", re: /\bYYYY-MM-DD\b/g }
2688
+ ];
2689
+ const hits = [];
2690
+ for (const { key, re } of patterns) {
2691
+ if (re.test(content)) hits.push(key);
2692
+ }
2693
+ return hits;
2694
+ }
2695
+ async function checkDocsStructure(config, cwd) {
2696
+ const issues = [];
2697
+ const requiredDirs = ["agents", "features", "prd", "designs", "ideas"];
2698
+ for (const dir of requiredDirs) {
2699
+ const p = path4.join(config.docsDir, dir);
2700
+ if (!await fs8.pathExists(p)) {
2701
+ issues.push({
2702
+ level: "error",
2703
+ code: "missing_dir",
2704
+ message: tr(config.lang, "cli", "doctor.issue.missingRequiredDir", { dir }),
2705
+ path: formatPath(cwd, p)
2706
+ });
2707
+ }
2708
+ }
2709
+ const configPath = path4.join(config.docsDir, ".lee-spec-kit.json");
2710
+ if (!await fs8.pathExists(configPath)) {
2711
+ issues.push({
2712
+ level: "warn",
2713
+ code: "missing_config",
2714
+ message: tr(config.lang, "cli", "doctor.issue.missingConfig"),
2715
+ path: formatPath(cwd, configPath)
2716
+ });
2717
+ }
2718
+ return issues;
2719
+ }
2720
+ async function checkFeatures(config, cwd, features) {
2721
+ const issues = [];
2722
+ if (features.length === 0) {
2723
+ issues.push({
2724
+ level: "warn",
2725
+ code: "no_features",
2726
+ message: tr(config.lang, "cli", "doctor.issue.noFeatures")
2727
+ });
2728
+ return issues;
2729
+ }
2730
+ const idMap = /* @__PURE__ */ new Map();
2731
+ for (const f of features) {
2732
+ const rel = f.docs.featurePathFromDocs || path4.relative(config.docsDir, f.path);
2733
+ const id = f.id || "UNKNOWN";
2734
+ if (!idMap.has(id)) idMap.set(id, []);
2735
+ idMap.get(id).push(rel);
2736
+ const featureDocs = ["spec.md", "plan.md", "tasks.md", "decisions.md"];
2737
+ for (const file of featureDocs) {
2738
+ const p = path4.join(f.path, file);
2739
+ if (!await fs8.pathExists(p)) continue;
2740
+ const content = await fs8.readFile(p, "utf-8");
2741
+ const placeholders = detectPlaceholders(content);
2742
+ if (placeholders.length === 0) continue;
2743
+ issues.push({
2744
+ level: "warn",
2745
+ code: "placeholder_left",
2746
+ message: tr(config.lang, "cli", "doctor.issue.placeholdersLeft", {
2747
+ placeholders: placeholders.join(", ")
2748
+ }),
2749
+ path: formatPath(cwd, p)
2750
+ });
2751
+ }
2752
+ if (!f.docs.specExists) {
2753
+ issues.push({
2754
+ level: "warn",
2755
+ code: "missing_spec",
2756
+ message: tr(config.lang, "cli", "doctor.issue.missingSpec"),
2757
+ path: formatPath(cwd, f.path)
2758
+ });
2759
+ } else if (!f.specStatus) {
2760
+ issues.push({
2761
+ level: "warn",
2762
+ code: "spec_status_unset",
2763
+ message: tr(config.lang, "cli", "doctor.issue.specStatusUnset"),
2764
+ path: formatPath(cwd, path4.join(f.path, "spec.md"))
2765
+ });
2766
+ }
2767
+ if (f.docs.planExists && !f.planStatus) {
2768
+ issues.push({
2769
+ level: "warn",
2770
+ code: "plan_status_unset",
2771
+ message: tr(config.lang, "cli", "doctor.issue.planStatusUnset"),
2772
+ path: formatPath(cwd, path4.join(f.path, "plan.md"))
2773
+ });
2774
+ }
2775
+ if (f.docs.tasksExists && f.tasks.total === 0) {
2776
+ issues.push({
2777
+ level: "warn",
2778
+ code: "tasks_empty",
2779
+ message: tr(config.lang, "cli", "doctor.issue.tasksEmpty"),
2780
+ path: formatPath(cwd, path4.join(f.path, "tasks.md"))
2781
+ });
2782
+ }
2783
+ }
2784
+ const duplicates = [...idMap.entries()].filter(
2785
+ ([id, paths]) => id !== "UNKNOWN" && paths.length > 1
2786
+ );
2787
+ for (const [id, paths] of duplicates) {
2788
+ issues.push({
2789
+ level: "warn",
2790
+ code: "duplicate_feature_id",
2791
+ message: tr(config.lang, "cli", "doctor.issue.duplicateFeatureId", {
2792
+ id,
2793
+ count: String(paths.length)
2794
+ }),
2795
+ path: formatPath(cwd, paths[0])
2796
+ });
2797
+ }
2798
+ const unknowns = idMap.get("UNKNOWN") || [];
2799
+ for (const p of unknowns) {
2800
+ issues.push({
2801
+ level: "warn",
2802
+ code: "missing_feature_id",
2803
+ message: tr(config.lang, "cli", "doctor.issue.missingFeatureId"),
2804
+ path: formatPath(cwd, path4.join(config.docsDir, p))
2805
+ });
2806
+ }
2807
+ return issues;
2808
+ }
2809
+ function doctorCommand(program2) {
2810
+ 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) => {
2811
+ const cwd = process.cwd();
2812
+ const config = await getConfig(cwd);
2813
+ if (!config) {
2814
+ const message = tr(DEFAULT_LANG, "cli", "common.configNotFound");
2815
+ if (options.json) {
2816
+ console.log(JSON.stringify({ status: "error", error: message }, null, 2));
2817
+ } else {
2818
+ console.error(chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")), message);
2819
+ }
2820
+ process.exit(1);
2821
+ }
2822
+ const { docsDir, projectType, lang } = config;
2823
+ const { features, branches, warnings } = await scanFeatures(config);
2824
+ const issues = [];
2825
+ issues.push(...await checkDocsStructure({ docsDir, lang }, cwd));
2826
+ issues.push(...await checkFeatures({ docsDir, lang }, cwd, features));
2827
+ const hasIssues = issues.length > 0;
2828
+ const hasErrors = issues.some((i) => i.level === "error");
2829
+ const exitCode = options.strict && hasIssues ? 1 : 0;
2830
+ if (options.json) {
2831
+ console.log(
2832
+ JSON.stringify(
2833
+ {
2834
+ status: hasErrors ? "error" : hasIssues ? "warn" : "ok",
2835
+ meta: { docsDir, projectType, lang },
2836
+ branches,
2837
+ warnings,
2838
+ counts: {
2839
+ features: features.length,
2840
+ issues: issues.length,
2841
+ errors: issues.filter((i) => i.level === "error").length,
2842
+ warnings: issues.filter((i) => i.level === "warn").length
2843
+ },
2844
+ issues
2845
+ },
2846
+ null,
2847
+ 2
2848
+ )
2849
+ );
2850
+ process.exit(exitCode);
2851
+ }
2852
+ console.log();
2853
+ console.log(chalk6.bold(tr(lang, "cli", "doctor.title")));
2854
+ console.log(chalk6.gray(`- Docs: ${path4.relative(cwd, docsDir)}`));
2855
+ console.log(chalk6.gray(`- Type: ${projectType}`));
2856
+ console.log(chalk6.gray(`- Lang: ${lang}`));
2857
+ console.log();
2858
+ if (warnings.length > 0) {
2859
+ console.log(
2860
+ chalk6.yellow(tr(lang, "cli", "doctor.envWarnings"))
2861
+ );
2862
+ warnings.forEach((w) => console.log(chalk6.yellow(` - ${w}`)));
2863
+ console.log();
2864
+ }
2865
+ if (!hasIssues) {
2866
+ console.log(chalk6.green(tr(lang, "cli", "doctor.noIssues")));
2867
+ console.log();
2868
+ process.exit(0);
2869
+ }
2870
+ const errors = issues.filter((i) => i.level === "error");
2871
+ const warns = issues.filter((i) => i.level === "warn");
2872
+ if (errors.length > 0) {
2873
+ console.log(
2874
+ chalk6.red(`\u274C ${tr(lang, "cli", "doctor.errorsTitle")} (${errors.length})`)
2875
+ );
2876
+ errors.forEach(
2877
+ (i) => console.log(chalk6.red(` - ${i.message}${i.path ? ` (${i.path})` : ""}`))
2878
+ );
2879
+ console.log();
2880
+ }
2881
+ if (warns.length > 0) {
2882
+ console.log(
2883
+ chalk6.yellow(
2884
+ `\u26A0\uFE0F ${tr(lang, "cli", "doctor.warningsTitle")} (${warns.length})`
2885
+ )
2886
+ );
2887
+ warns.forEach(
2888
+ (i) => console.log(
2889
+ chalk6.yellow(` - ${i.message}${i.path ? ` (${i.path})` : ""}`)
2890
+ )
2891
+ );
2892
+ console.log();
2893
+ }
2894
+ console.log(
2895
+ chalk6.gray(
2896
+ tr(lang, "cli", "doctor.tipJson", {
2897
+ strictFlag: options.strict ? " --strict" : ""
2898
+ })
2899
+ )
2900
+ );
2901
+ console.log();
2902
+ process.exit(exitCode);
2903
+ });
2904
+ }
2119
2905
  var CACHE_FILE = path4.join(os.homedir(), ".lee-spec-kit-version-cache.json");
2120
2906
  var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
2121
2907
  function getCurrentVersion() {
2122
2908
  try {
2123
2909
  const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
2124
- if (fs6.existsSync(packageJsonPath)) {
2125
- const pkg = fs6.readJsonSync(packageJsonPath);
2910
+ if (fs8.existsSync(packageJsonPath)) {
2911
+ const pkg = fs8.readJsonSync(packageJsonPath);
2126
2912
  return pkg.version;
2127
2913
  }
2128
2914
  } catch {
@@ -2131,8 +2917,8 @@ function getCurrentVersion() {
2131
2917
  }
2132
2918
  function readCache() {
2133
2919
  try {
2134
- if (fs6.existsSync(CACHE_FILE)) {
2135
- return fs6.readJsonSync(CACHE_FILE);
2920
+ if (fs8.existsSync(CACHE_FILE)) {
2921
+ return fs8.readJsonSync(CACHE_FILE);
2136
2922
  }
2137
2923
  } catch {
2138
2924
  }
@@ -2196,12 +2982,23 @@ function checkForUpdates() {
2196
2982
  }
2197
2983
 
2198
2984
  // src/index.ts
2199
- checkForUpdates();
2985
+ function shouldCheckForUpdates() {
2986
+ const argv = process.argv.slice(2);
2987
+ const hasJsonFlag = argv.includes("--json");
2988
+ const isHelpOrVersion = argv.includes("--help") || argv.includes("-h") || argv.includes("--version") || argv.includes("-V");
2989
+ const disabledByEnv = (process.env.LSK_NO_UPDATE_CHECK || "").trim() === "1" || (process.env.LEE_SPEC_KIT_NO_UPDATE_CHECK || "").trim() === "1";
2990
+ if (hasJsonFlag) return false;
2991
+ if (!process.stdout.isTTY) return false;
2992
+ if (isHelpOrVersion) return false;
2993
+ if (disabledByEnv) return false;
2994
+ return true;
2995
+ }
2996
+ if (shouldCheckForUpdates()) checkForUpdates();
2200
2997
  function getCliVersion() {
2201
2998
  try {
2202
2999
  const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
2203
- if (fs6.existsSync(packageJsonPath)) {
2204
- const pkg = fs6.readJsonSync(packageJsonPath);
3000
+ if (fs8.existsSync(packageJsonPath)) {
3001
+ const pkg = fs8.readJsonSync(packageJsonPath);
2205
3002
  if (pkg?.version) return String(pkg.version);
2206
3003
  }
2207
3004
  } catch {
@@ -2217,4 +3014,5 @@ statusCommand(program);
2217
3014
  updateCommand(program);
2218
3015
  configCommand(program);
2219
3016
  contextCommand(program);
2220
- program.parse();
3017
+ doctorCommand(program);
3018
+ await program.parseAsync();