lee-spec-kit 0.6.7 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +84 -51
- package/README.md +84 -57
- package/dist/index.js +1096 -305
- package/package.json +1 -1
- package/templates/en/{single → common}/README.md +10 -10
- package/templates/en/common/agents/agents.md +77 -0
- package/templates/en/common/agents/skills/create-feature.md +1 -0
- package/templates/en/common/agents/skills/create-pr.md +9 -9
- package/templates/en/common/agents/skills/execute-task.md +1 -0
- package/templates/en/{fullstack → common}/features/README.md +8 -16
- package/templates/en/{fullstack → common}/features/feature-base/decisions.md +3 -0
- package/templates/ko/{single → common}/README.md +9 -9
- package/templates/ko/common/agents/agents.md +78 -0
- package/templates/ko/common/agents/skills/create-feature.md +1 -0
- package/templates/ko/common/agents/skills/create-pr.md +9 -9
- package/templates/ko/common/agents/skills/execute-task.md +1 -0
- package/templates/ko/{fullstack → common}/features/README.md +7 -37
- package/templates/ko/{fullstack → common}/features/feature-base/decisions.md +3 -0
- package/templates/en/fullstack/README.md +0 -60
- package/templates/en/fullstack/agents/agents.md +0 -116
- package/templates/en/fullstack/features/be/README.md +0 -5
- package/templates/en/fullstack/features/fe/README.md +0 -5
- package/templates/en/fullstack/prd/README.md +0 -20
- package/templates/en/single/agents/agents.md +0 -103
- package/templates/en/single/features/README.md +0 -56
- package/templates/en/single/features/feature-base/decisions.md +0 -15
- package/templates/en/single/features/feature-base/plan.md +0 -48
- package/templates/en/single/features/feature-base/spec.md +0 -57
- package/templates/en/single/features/feature-base/tasks.md +0 -60
- package/templates/ko/fullstack/README.md +0 -60
- package/templates/ko/fullstack/agents/agents.md +0 -146
- package/templates/ko/fullstack/features/be/README.md +0 -5
- package/templates/ko/fullstack/features/fe/README.md +0 -5
- package/templates/ko/single/agents/agents.md +0 -116
- package/templates/ko/single/features/README.md +0 -56
- package/templates/ko/single/features/feature-base/decisions.md +0 -15
- package/templates/ko/single/features/feature-base/plan.md +0 -48
- package/templates/ko/single/features/feature-base/spec.md +0 -57
- package/templates/ko/single/features/feature-base/tasks.md +0 -60
- package/templates/ko/single/prd/README.md +0 -20
- /package/templates/en/{fullstack → common}/features/feature-base/plan.md +0 -0
- /package/templates/en/{fullstack → common}/features/feature-base/spec.md +0 -0
- /package/templates/en/{fullstack → common}/features/feature-base/tasks.md +0 -0
- /package/templates/en/{single → common}/prd/README.md +0 -0
- /package/templates/ko/{fullstack → common}/features/feature-base/plan.md +0 -0
- /package/templates/ko/{fullstack → common}/features/feature-base/spec.md +0 -0
- /package/templates/ko/{fullstack → common}/features/feature-base/tasks.md +0 -0
- /package/templates/ko/{fullstack → common}/prd/README.md +0 -0
package/dist/index.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
import path18 from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { program } from 'commander';
|
|
5
|
-
import
|
|
5
|
+
import fs15 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, spawnSync
|
|
9
|
+
import { spawn, execFileSync, execSync, spawnSync } from 'child_process';
|
|
10
10
|
import os from 'os';
|
|
11
|
-
import { createHash } from 'crypto';
|
|
11
|
+
import { createHash, randomUUID } from 'crypto';
|
|
12
12
|
|
|
13
13
|
var getFilename = () => fileURLToPath(import.meta.url);
|
|
14
14
|
var getDirname = () => path18.dirname(getFilename());
|
|
15
15
|
var __dirname$1 = /* @__PURE__ */ getDirname();
|
|
16
16
|
async function copyTemplates(src, dest) {
|
|
17
|
-
await
|
|
17
|
+
await fs15.copy(src, dest, {
|
|
18
18
|
overwrite: true,
|
|
19
19
|
errorOnExist: false
|
|
20
20
|
});
|
|
@@ -30,15 +30,15 @@ function applyReplacements(content, replacements) {
|
|
|
30
30
|
async function replaceInFiles(dir, replacements) {
|
|
31
31
|
const files = await glob("**/*.md", { cwd: dir, absolute: true });
|
|
32
32
|
for (const file of files) {
|
|
33
|
-
let content = await
|
|
33
|
+
let content = await fs15.readFile(file, "utf-8");
|
|
34
34
|
content = applyReplacements(content, replacements);
|
|
35
|
-
await
|
|
35
|
+
await fs15.writeFile(file, content, "utf-8");
|
|
36
36
|
}
|
|
37
37
|
const shFiles = await glob("**/*.sh", { cwd: dir, absolute: true });
|
|
38
38
|
for (const file of shFiles) {
|
|
39
|
-
let content = await
|
|
39
|
+
let content = await fs15.readFile(file, "utf-8");
|
|
40
40
|
content = applyReplacements(content, replacements);
|
|
41
|
-
await
|
|
41
|
+
await fs15.writeFile(file, content, "utf-8");
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
@@ -83,7 +83,7 @@ var I18N = {
|
|
|
83
83
|
"config.pathLabel": "\uACBD\uB85C",
|
|
84
84
|
"config.projectRootStandaloneOnly": "\u26A0\uFE0F projectRoot\uB294 standalone \uBAA8\uB4DC\uC5D0\uC11C\uB9CC \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.",
|
|
85
85
|
"config.selectRepoToUpdate": "\uC218\uC815\uD560 \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
|
|
86
|
-
"config.fullstackRepoRequired": "Multi \uD504\uB85C\uC81D\uD2B8\uB294 --
|
|
86
|
+
"config.fullstackRepoRequired": "Multi \uD504\uB85C\uC81D\uD2B8\uB294 --component\uB85C \uB300\uC0C1 \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
|
|
87
87
|
"config.projectRootSet": "\u2705 {repo} projectRoot \uC124\uC815 \uC644\uB8CC: {path}",
|
|
88
88
|
"config.projectRootSetSingle": "\u2705 projectRoot \uC124\uC815 \uC644\uB8CC: {path}",
|
|
89
89
|
"update.start": "\u{1F4E6} \uD15C\uD50C\uB9BF \uC5C5\uB370\uC774\uD2B8\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4...",
|
|
@@ -132,10 +132,11 @@ var I18N = {
|
|
|
132
132
|
"context.tipShowDone": "\uC644\uB8CC\uB9CC \uBCF4\uAE30",
|
|
133
133
|
"context.checkRequired": "[\uD655\uC778 \uD544\uC694] ",
|
|
134
134
|
"context.checkPolicyHint": "\u2139\uFE0F \uC0AC\uC6A9\uC790 \uD655\uC778 \uC815\uCC45\uC740 `npx lee-spec-kit docs get agents --json`\uC73C\uB85C \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694. (git push/merge/merge commit \uD3EC\uD568) [\uD655\uC778 \uD544\uC694]\uAC00 \uC788\uC73C\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` (\uC608: `A`, `A OK`) \uC751\uB2F5\uC744 \uBC1B\uC740 \uB4A4 \uC9C4\uD589 (config: approval\uB85C \uC870\uC815 \uAC00\uB2A5)",
|
|
135
|
-
"context.actionOptionHint": "\uC2B9\uC778 \uC751\uB2F5 \uD615\uC2DD:
|
|
135
|
+
"context.actionOptionHint": "\uC2B9\uC778 \uC751\uB2F5 \uD615\uC2DD: \uB77C\uBCA8 \uD1A0\uD070 \uD3EC\uD568 (\uC608: `A`, `A OK`, `A \uC9C4\uD589\uD574`)",
|
|
136
136
|
"context.actionExplainHint": "\uC2B9\uC778 \uC694\uCCAD \uC804, \uAC01 \uB77C\uBCA8\uC774 \uBB34\uC5C7\uC744 \uC2E4\uD589/\uBCC0\uACBD\uD558\uB294\uC9C0 \uD55C \uC904 \uC694\uC57D\uACFC \uD568\uAED8 \uC124\uBA85\uD558\uC138\uC694.",
|
|
137
137
|
"context.finalLabelPrompt": "\uD604\uC7AC \uC120\uD0DD \uAC00\uB2A5\uD55C \uB77C\uBCA8: {labels}. \uB9C8\uC9C0\uB9C9 \uC751\uB2F5\uC740 `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` \uD615\uC2DD\uC73C\uB85C \uBC1B\uC73C\uC138\uC694. (\uC608: {example})",
|
|
138
|
-
"context.finalLabelCommandHint": "\uB77C\uBCA8\uC744 \uBC1B\uC73C\uBA74 \
|
|
138
|
+
"context.finalLabelCommandHint": "\uB77C\uBCA8\uC744 \uBC1B\uC73C\uBA74 \uC2B9\uC778 \uC120\uD0DD \uC2E4\uD589: {command}",
|
|
139
|
+
"context.finalTicketCommandHint": "\uBA85\uB839 \uC2E4\uD589\uC740 \uC2B9\uC778 \uACB0\uACFC\uC758 \uD2F0\uCF13\uC73C\uB85C \uC2E4\uD589: {command}",
|
|
139
140
|
"context.readBuiltinDocFirst": "\uBA3C\uC800 \uB0B4\uC7A5 \uBB38\uC11C\uB97C \uD655\uC778\uD558\uC138\uC694: {command}",
|
|
140
141
|
"context.tipDocsCommitRules": "\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uADDC\uCE59\uC740 `npx lee-spec-kit docs get git-workflow --json`\uC73C\uB85C \uD655\uC778\uD558\uC138\uC694.",
|
|
141
142
|
"context.list.docsCommitNeeded": "\uBB38\uC11C \uCEE4\uBC0B \uD544\uC694",
|
|
@@ -171,6 +172,7 @@ var I18N = {
|
|
|
171
172
|
"init.choice.docsRepo.standalone.desc": "push \uC5EC\uBD80\uB97C \uBCC4\uB3C4\uB85C \uC124\uC815\uD569\uB2C8\uB2E4",
|
|
172
173
|
"init.prompt.feRepoPath": "Frontend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
|
|
173
174
|
"init.prompt.beRepoPath": "Backend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
|
|
175
|
+
"init.prompt.componentRepoPath": "{component} \uCEF4\uD3EC\uB10C\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
|
|
174
176
|
"init.prompt.projectRepoPath": "\uD504\uB85C\uC81D\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
|
|
175
177
|
"init.validation.enterPath": "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694",
|
|
176
178
|
"init.prompt.pushMode": "Docs push \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
|
|
@@ -199,11 +201,10 @@ var I18N = {
|
|
|
199
201
|
"init.log.gitInitialCommitDone": "\u2705 Git \uCD08\uAE30 \uCEE4\uBC0B \uC644\uB8CC!",
|
|
200
202
|
"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)",
|
|
201
203
|
"init.error.templateNotFound": "\uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: {path}",
|
|
202
|
-
"github.cmdGithubDescription": "GitHub \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uB3C4\uC6B0\uBBF8 (issue/pr \
|
|
204
|
+
"github.cmdGithubDescription": "GitHub \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uB3C4\uC6B0\uBBF8 (issue/pr \uBCF8\uBB38 \uD15C\uD50C\uB9BF \uC0DD\uC131, \uAC80\uC99D, merge \uC7AC\uC2DC\uB3C4)",
|
|
203
205
|
"github.cmdIssueDescription": "feature \uBB38\uC11C \uAE30\uBC18 GitHub issue \uBCF8\uBB38 \uC0DD\uC131/\uC0DD\uC131",
|
|
204
206
|
"github.cmdPrDescription": "GitHub PR \uBCF8\uBB38 \uC0DD\uC131/\uC0DD\uC131 + tasks \uB3D9\uAE30\uD654 + merge \uC7AC\uC2DC\uB3C4",
|
|
205
207
|
"github.optJson": "\uC5D0\uC774\uC804\uD2B8\uC6A9 JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825",
|
|
206
|
-
"github.optRepo": "\uBA40\uD2F0 \uD504\uB85C\uC81D\uD2B8 \uCEF4\uD3EC\uB10C\uD2B8 \uC774\uB984",
|
|
207
208
|
"github.optComponent": "\uBA40\uD2F0 \uD504\uB85C\uC81D\uD2B8 \uCEF4\uD3EC\uB10C\uD2B8 \uC774\uB984",
|
|
208
209
|
"github.optIssueTitle": "Issue \uC81C\uBAA9",
|
|
209
210
|
"github.optLabels": "\uC27C\uD45C \uAD6C\uBD84 \uB77C\uBCA8 \uBAA9\uB85D (\uAE30\uBCF8: enhancement)",
|
|
@@ -224,7 +225,6 @@ var I18N = {
|
|
|
224
225
|
"github.optPrMermaid": "PR Mermaid \uC139\uC158 \uBAA8\uB4DC (auto|on|off, \uAE30\uBCF8: auto)",
|
|
225
226
|
"github.optPrNoSyncTasks": "tasks.md PR URL/PR \uC0C1\uD0DC \uB3D9\uAE30\uD654\uB97C \uAC74\uB108\uB700",
|
|
226
227
|
"github.optPrCommitSync": "tasks.md \uB3D9\uAE30\uD654 \uBCC0\uACBD\uC744 \uC790\uB3D9 commit/push",
|
|
227
|
-
"github.invalidRepoComponentMismatch": "`--repo`\uC640 `--component`\uB97C \uD568\uAED8 \uC4F8 \uB54C\uB294 \uAC19\uC740 \uAC12\uC744 \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
|
|
228
228
|
"github.labelsRequired": "\uCD5C\uC18C 1\uAC1C \uB77C\uBCA8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. `--labels enhancement`\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
|
|
229
229
|
"github.approvalRequired": "{operation}\uC740(\uB294) \uC0AC\uC6A9\uC790 \uBA85\uC2DC \uC2B9\uC778 \uD6C4\uC5D0\uB9CC \uC2E4\uD589\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uACC4\uD68D \uACF5\uC720 \uD6C4 `--confirm OK`\uB85C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
|
|
230
230
|
"github.ghCommandFailed": "GitHub CLI \uBA85\uB839 \uC2E4\uD589\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
|
|
@@ -275,10 +275,10 @@ var I18N = {
|
|
|
275
275
|
"github.labelLabels": "\uB77C\uBCA8",
|
|
276
276
|
"github.labelPr": "PR",
|
|
277
277
|
"github.issueCreated": "\u2705 \uC0DD\uC131 \uC644\uB8CC: {url}",
|
|
278
|
-
"github.issueTemplateGenerated": "\
|
|
278
|
+
"github.issueTemplateGenerated": "\uBCF8\uBB38 \uD15C\uD50C\uB9BF\uC744 \uC0DD\uC131\uD588\uC2B5\uB2C8\uB2E4. \uC6D0\uACA9\uC73C\uB85C \uC774\uC288\uB97C \uC0DD\uC131\uD558\uB824\uBA74 `--create`\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
|
|
279
279
|
"github.prTasksSynced": "\u2705 tasks.md PR \uBA54\uD0C0\uB370\uC774\uD130\uB97C \uB3D9\uAE30\uD654\uD588\uC2B5\uB2C8\uB2E4.",
|
|
280
280
|
"github.prMerged": "\u2705 PR merge \uC644\uB8CC (\uC2DC\uB3C4 \uD69F\uC218: {attempts})",
|
|
281
|
-
"github.prTemplateGenerated": "\
|
|
281
|
+
"github.prTemplateGenerated": "\uBCF8\uBB38 \uD15C\uD50C\uB9BF\uC744 \uC0DD\uC131\uD588\uC2B5\uB2C8\uB2E4. \uC6D0\uACA9\uC73C\uB85C PR\uC744 \uC0DD\uC131\uD558\uB824\uBA74 `--create`\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
|
|
282
282
|
"github.syncCommitWithIssue": "docs(#{issue}): {folder} PR \uBA54\uD0C0\uB370\uC774\uD130 \uB3D9\uAE30\uD654",
|
|
283
283
|
"github.syncCommitNoIssue": "docs: {folder} PR \uBA54\uD0C0\uB370\uC774\uD130 \uB3D9\uAE30\uD654",
|
|
284
284
|
"github.kindIssue": "Issue",
|
|
@@ -292,6 +292,22 @@ var I18N = {
|
|
|
292
292
|
"docs.nextDocs": "\uB2E4\uC74C \uBB38\uC11C",
|
|
293
293
|
"docs.sourceLabel": "source",
|
|
294
294
|
"docs.hashLabel": "hash",
|
|
295
|
+
"detect.cmdDescription": "\uD604\uC7AC \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 lee-spec-kit \uD504\uB85C\uC81D\uD2B8\uC778\uC9C0 \uAC10\uC9C0\uD569\uB2C8\uB2E4",
|
|
296
|
+
"detect.optDir": "\uAC10\uC9C0 \uAE30\uC900 \uACBD\uB85C (\uAE30\uBCF8: \uD604\uC7AC \uACBD\uB85C)",
|
|
297
|
+
"detect.optJson": "\uC5D0\uC774\uC804\uD2B8\uC6A9 JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825",
|
|
298
|
+
"detect.header": "\u{1F50E} Project Detection",
|
|
299
|
+
"detect.labelTarget": "Target",
|
|
300
|
+
"detect.resultDetected": "lee-spec-kit \uD504\uB85C\uC81D\uD2B8\uB97C \uAC10\uC9C0\uD588\uC2B5\uB2C8\uB2E4",
|
|
301
|
+
"detect.resultNotDetected": "lee-spec-kit \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4",
|
|
302
|
+
"detect.notDetectedHint": "`npx lee-spec-kit init`\uC73C\uB85C \uCD08\uAE30\uD654\uD558\uAC70\uB098 `--dir`\uB85C \uC62C\uBC14\uB978 \uACBD\uB85C\uB97C \uC9C0\uC815\uD558\uC138\uC694.",
|
|
303
|
+
"detect.labelDocsDir": "Docs",
|
|
304
|
+
"detect.labelConfigPath": "Config",
|
|
305
|
+
"detect.labelSource": "Source",
|
|
306
|
+
"detect.labelProjectType": "Project Type",
|
|
307
|
+
"detect.labelLang": "Lang",
|
|
308
|
+
"detect.labelProjectName": "Project",
|
|
309
|
+
"detect.sourceConfig": "config (.lee-spec-kit.json)",
|
|
310
|
+
"detect.sourceHeuristic": "heuristic (agents/features folder)",
|
|
295
311
|
"cliError.headerNextOptionsError": "\u{1F449} \uB2E4\uC74C \uC635\uC158 (\uC624\uB958):",
|
|
296
312
|
"cliError.promptBlocked.retryWithoutNonInteractive": "--non-interactive \uC5C6\uC774 \uAC19\uC740 \uBA85\uB839\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
|
|
297
313
|
"cliError.promptBlocked.passRequiredFlags": "\uD544\uC218 \uD50C\uB798\uADF8\uB97C \uBAA8\uB450 \uBA85\uC2DC\uD558\uAC70\uB098(`--force` \uD3EC\uD568) \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
|
|
@@ -321,7 +337,7 @@ var I18N = {
|
|
|
321
337
|
"cliError.approvalRequired.githubConfirmOk": "github \uC6D0\uACA9 \uC0DD\uC131/\uBA38\uC9C0\uBA74 --confirm OK\uB97C \uD568\uAED8 \uC804\uB2EC\uD558\uC138\uC694.",
|
|
322
338
|
"cliError.approvalRequired.shareAndGetApproval": "\uC2E4\uD589 \uC804\uC5D0 \uC81C\uBAA9/\uBCF8\uBB38/\uB77C\uBCA8(\uB610\uB294 \uBA38\uC9C0 \uACC4\uD68D)\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uBA85\uC2DC\uC801 \uC2B9\uC778\uC744 \uBC1B\uC73C\uC138\uC694.",
|
|
323
339
|
"cliError.contextSelection.specifySelector": "\uB2E8\uC77C Feature selector\uB97C \uBA85\uC2DC\uD558\uC138\uC694.",
|
|
324
|
-
"cliError.contextSelection.narrowByComponent": "multi \uBAA8\uB4DC\uC5D0\uC11C\uB294 --
|
|
340
|
+
"cliError.contextSelection.narrowByComponent": "multi \uBAA8\uB4DC\uC5D0\uC11C\uB294 --component\uB85C \uBC94\uC704\uB97C \uC881\uD788\uC138\uC694.",
|
|
325
341
|
"cliError.contextSelection.inspectAllCandidates": "\uBA3C\uC800 \uC804\uCCB4 \uD6C4\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
326
342
|
"cliError.noActionOptions.refreshContext": "\uD604\uC7AC \uC0C1\uD0DC\uB97C \uBCF4\uAE30 \uC704\uD574 context\uB97C \uC0C8\uB85C \uC870\uD68C\uD558\uC138\uC694.",
|
|
327
343
|
"cliError.noActionOptions.completeChecklist": "Feature \uBB38\uC11C\uB97C \uC5F4\uC5B4 \uB204\uB77D\uB41C \uCCB4\uD06C \uD56D\uBAA9\uC744 \uC644\uB8CC\uD558\uC138\uC694.",
|
|
@@ -393,7 +409,7 @@ var I18N = {
|
|
|
393
409
|
tasksImprove: "tasks.md\uB97C \uBCF4\uC644\uD558\uACE0 \uBB38\uC11C \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
|
|
394
410
|
tasksApproval: "tasks.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC9C4\uD589 \uC2B9\uC778(`A` \uB610\uB294 `A OK` \uD615\uC2DD)\uC744 \uBC1B\uC73C\uC138\uC694. (\uC2B9\uC778 \uD6C4 \uBB38\uC11C \uC0C1\uD0DC\uB97C Approved\uB85C \uBCC0\uACBD)",
|
|
395
411
|
docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} \uAE30\uD68D \uBB38\uC11C"',
|
|
396
|
-
issueCreateAndWrite: "`npx lee-spec-kit docs get create-issue --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github issue {featureRef} --json`\uC73C\uB85C \
|
|
412
|
+
issueCreateAndWrite: "`npx lee-spec-kit docs get create-issue --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github issue {featureRef} --json`\uC73C\uB85C \uBCF8\uBB38 \uD15C\uD50C\uB9BF\uC744 \uC0DD\uC131\uD558\uC138\uC694. \uBAA9\uD45C/\uC644\uB8CC \uAE30\uC900\uC744 \uAC80\uD1A0\xB7\uBCF4\uC644\uD558\uACE0 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `--create --confirm OK`\uB85C \uC0DD\uC131\uD55C \uB2E4\uC74C, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694.",
|
|
397
413
|
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
|
|
398
414
|
docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
|
|
399
415
|
projectCommitIssueUpdate: 'cd "{projectGitCwd}" && (git diff --cached --quiet && echo "\uC2A4\uD14C\uC774\uC9D5\uB41C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774\uBC88 \uD0DC\uC2A4\uD06C\uC5D0\uC11C \uC218\uC815\uD55C \uD30C\uC77C\uB9CC \uC120\uD0DD\uD574 git add [files] \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694." && exit 1 || git commit -m "feat(#{issueNumber}): {commitTopic}")',
|
|
@@ -405,12 +421,18 @@ var I18N = {
|
|
|
405
421
|
finishDoingTask: '\uD604\uC7AC DOING/REVIEW \uC911\uC778 \uD0DC\uC2A4\uD06C\uB97C \uC644\uB8CC\uD558\uC138\uC694: "{title}" ({done}/{total}) (\uC644\uB8CC \uC804 `npx lee-spec-kit docs get execute-task --json`\uC73C\uB85C \uC808\uCC28 \uD655\uC778 \u2192 \uACB0\uACFC/\uAC80\uC99D \uACF5\uC720 + \uC2B9\uC778(`A` \uB610\uB294 `A OK` \uD615\uC2DD) \uD6C4 DONE \uCC98\uB9AC)',
|
|
406
422
|
startNextTodoTask: '\uB2E4\uC74C TODO \uD0DC\uC2A4\uD06C\uB97C \uC2DC\uC791\uD558\uC138\uC694: "{title}" ({done}/{total}) (\uC2DC\uC791 \uC804 `npx lee-spec-kit docs get execute-task --json`\uC73C\uB85C \uC808\uCC28 \uD655\uC778 \u2192 \uC81C\uBAA9 \uACF5\uC720 + \uC2B9\uC778(`A` \uB610\uB294 `A OK` \uD615\uC2DD) \uD6C4 DOING \uCC98\uB9AC)',
|
|
407
423
|
checkTaskStatuses: "\uD0DC\uC2A4\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. ({done}/{total}) (`npx lee-spec-kit docs get execute-task --json` \uC808\uCC28\uB97C \uAE30\uC900\uC73C\uB85C \uC810\uAC80)",
|
|
424
|
+
taskCommitGateStrictBlock: "\uB2E4\uC74C TODO \uD0DC\uC2A4\uD06C\uB85C \uB118\uC5B4\uAC00\uAE30 \uC804\uC5D0 `1 \uD0DC\uC2A4\uD06C = 1 \uCEE4\uBC0B` \uADDC\uCE59\uC744 \uCDA9\uC871\uD574\uC57C \uD569\uB2C8\uB2E4. \uC810\uAC80 \uACB0\uACFC: {reason}. `npx lee-spec-kit docs get execute-task --json` \uC808\uCC28\uB97C \uAE30\uC900\uC73C\uB85C \uD0DC\uC2A4\uD06C \uCEE4\uBC0B \uB2E8\uC704\uB97C \uC815\uB9AC\uD55C \uB4A4 \uB2E4\uC2DC \uC9C4\uD589\uD558\uC138\uC694.",
|
|
425
|
+
taskCommitGateWarnProceed: "\u26A0\uFE0F \uD0DC\uC2A4\uD06C \uCEE4\uBC0B \uB2E8\uC704 \uC810\uAC80 \uACBD\uACE0: {reason}. \uD604\uC7AC\uB294 \uC9C4\uD589 \uAC00\uB2A5\uD558\uC9C0\uB9CC `1 \uD0DC\uC2A4\uD06C = 1 \uCEE4\uBC0B`\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4.",
|
|
426
|
+
taskCommitGateReasonNoTasksCommit: "\uCD5C\uADFC tasks.md \uCEE4\uBC0B\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
|
|
427
|
+
taskCommitGateReasonTasksFileUnavailable: "\uCD5C\uADFC \uCEE4\uBC0B\uC5D0\uC11C tasks.md \uC774\uB825\uC744 \uD310\uB3C5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
|
|
428
|
+
taskCommitGateReasonDoneCount: "\uCD5C\uADFC tasks.md \uCEE4\uBC0B\uC758 DONE \uC804\uD658 \uC218\uAC00 {count}\uAC1C\uC785\uB2C8\uB2E4 (\uC815\uC0C1: 1\uAC1C)",
|
|
429
|
+
taskCommitGateReasonMismatchLastDone: "\uCD5C\uADFC tasks.md \uCEE4\uBC0B\uC774 \uC9C1\uC804 \uC644\uB8CC \uD0DC\uC2A4\uD06C\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
|
|
408
430
|
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? (\uD655\uC778 \uD544\uC694)",
|
|
409
431
|
prePrReviewFieldMissing: "tasks.md\uC5D0 `PR \uC804 \uB9AC\uBDF0` \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. `- **PR \uC804 \uB9AC\uBDF0**: Pending | Done` \uD56D\uBAA9\uC744 \uCD94\uAC00\uD558\uACE0 \uB2E4\uC2DC context\uB97C \uC2E4\uD589\uD558\uC138\uC694. (\uD655\uC778 \uD544\uC694)",
|
|
410
432
|
prePrReviewRun: "PR \uC0DD\uC131 \uC804 \uC0AC\uC804 \uCF54\uB4DC\uB9AC\uBDF0\uB97C \uC9C4\uD589\uD558\uC138\uC694. \uC6B0\uC120\uC21C\uC704 \uC2A4\uD0AC: {skills} (\uC124\uCE58\uB41C \uB354 \uC801\uD569\uD55C \uC2A4\uD0AC\uC774 \uC788\uB2E4\uBA74 \uBA3C\uC800 \uC81C\uC548 \uD6C4 \uC0AC\uC6A9). \uC2A4\uD0AC\uC744 \uC4F8 \uC218 \uC5C6\uC73C\uBA74 `{fallback}` \uC815\uCC45\uC73C\uB85C \uC9C4\uD589\uD558\uACE0 `PR \uC804 \uB9AC\uBDF0`\uB97C Done\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694. Findings \uC815\uCC45: {findingsPolicy}",
|
|
411
433
|
prePrReviewFindingsBlock: "\uC911\uC694 \uC774\uC288\uB294 \uC218\uC815/\uD569\uC758 \uD6C4\uC5D0\uB9CC PR \uC0DD\uC131",
|
|
412
434
|
prePrReviewFindingsWarn: "\uB9AC\uC2A4\uD06C\uB97C \uACF5\uC720\uD558\uBA74 PR \uC0DD\uC131 \uC9C4\uD589 \uAC00\uB2A5",
|
|
413
|
-
prCreate: "`npx lee-spec-kit docs get create-pr --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github pr {featureRef} --json`\uC73C\uB85C \
|
|
435
|
+
prCreate: "`npx lee-spec-kit docs get create-pr --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github pr {featureRef} --json`\uC73C\uB85C \uBCF8\uBB38 \uD15C\uD50C\uB9BF\uC744 \uC0DD\uC131\uD558\uC138\uC694. \uBCC0\uACBD \uC0AC\uD56D/\uD14C\uC2A4\uD2B8 \uC139\uC158\uC744 \uAC80\uD1A0\xB7\uBCF4\uC644\uD558\uACE0 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `--create --confirm OK`\uB85C \uC0DD\uC131\uD55C \uB2E4\uC74C tasks.md\uC5D0 PR \uB9C1\uD06C\uB97C \uAE30\uB85D\uD558\uC138\uC694.",
|
|
414
436
|
prFillStatus: "tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Review/Approved \uC911 \uD558\uB098\uB85C \uC124\uC815\uD558\uC138\uC694. (merge \uD6C4 Approved\uB85C \uC5C5\uB370\uC774\uD2B8)",
|
|
415
437
|
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)",
|
|
416
438
|
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.",
|
|
@@ -459,7 +481,7 @@ var I18N = {
|
|
|
459
481
|
"config.pathLabel": "Path",
|
|
460
482
|
"config.projectRootStandaloneOnly": "\u26A0\uFE0F projectRoot can only be set in standalone mode.",
|
|
461
483
|
"config.selectRepoToUpdate": "Select a repository to update:",
|
|
462
|
-
"config.fullstackRepoRequired": "For multi projects, specify a target component via `--
|
|
484
|
+
"config.fullstackRepoRequired": "For multi projects, specify a target component via `--component`.",
|
|
463
485
|
"config.projectRootSet": "\u2705 {repo} projectRoot set: {path}",
|
|
464
486
|
"config.projectRootSetSingle": "\u2705 projectRoot set: {path}",
|
|
465
487
|
"update.start": "\u{1F4E6} Starting template update...",
|
|
@@ -508,10 +530,11 @@ var I18N = {
|
|
|
508
530
|
"context.tipShowDone": "Show done only",
|
|
509
531
|
"context.checkRequired": "[CHECK required] ",
|
|
510
532
|
"context.checkPolicyHint": "\u2139\uFE0F Check user-approval policy first with `npx lee-spec-kit docs get agents --json` (includes git push/merge and merge commits). If you see [CHECK required], wait for `<label>` or `<label> OK` (e.g. `A`, `A OK`) before proceeding (config: approval can override)",
|
|
511
|
-
"context.actionOptionHint": "Approval reply format:
|
|
533
|
+
"context.actionOptionHint": "Approval reply format: include a label token (e.g. `A`, `A OK`, `A proceed`)",
|
|
512
534
|
"context.actionExplainHint": "Before requesting approval, explain what each label will run/change with a one-line summary.",
|
|
513
535
|
"context.finalLabelPrompt": "Available labels now: {labels}. End with a label request in `<label>` or `<label> OK` format. (e.g. {example})",
|
|
514
|
-
"context.finalLabelCommandHint": "When a label is provided, run
|
|
536
|
+
"context.finalLabelCommandHint": "When a label is provided, run approval selection: {command}",
|
|
537
|
+
"context.finalTicketCommandHint": "Execute commands using the ticket from approval result: {command}",
|
|
515
538
|
"context.readBuiltinDocFirst": "Read built-in docs first: {command}",
|
|
516
539
|
"context.tipDocsCommitRules": "Check commit message rules with `npx lee-spec-kit docs get git-workflow --json`.",
|
|
517
540
|
"context.list.docsCommitNeeded": "Commit docs changes",
|
|
@@ -547,6 +570,7 @@ var I18N = {
|
|
|
547
570
|
"init.choice.docsRepo.standalone.desc": "Configure push settings separately",
|
|
548
571
|
"init.prompt.feRepoPath": "Enter frontend repository path:",
|
|
549
572
|
"init.prompt.beRepoPath": "Enter backend repository path:",
|
|
573
|
+
"init.prompt.componentRepoPath": 'Enter repository path for component "{component}":',
|
|
550
574
|
"init.prompt.projectRepoPath": "Enter project repository path:",
|
|
551
575
|
"init.validation.enterPath": "Please enter a path",
|
|
552
576
|
"init.prompt.pushMode": "Select docs push mode:",
|
|
@@ -579,7 +603,6 @@ var I18N = {
|
|
|
579
603
|
"github.cmdIssueDescription": "Generate/create GitHub issue body from feature docs with validation",
|
|
580
604
|
"github.cmdPrDescription": "Generate/create GitHub PR body with validation, tasks PR sync, and merge retry",
|
|
581
605
|
"github.optJson": "Output in JSON format for agents",
|
|
582
|
-
"github.optRepo": "Component name for multi projects",
|
|
583
606
|
"github.optComponent": "Component name for multi projects",
|
|
584
607
|
"github.optIssueTitle": "Issue title",
|
|
585
608
|
"github.optLabels": "Comma-separated labels (default: enhancement)",
|
|
@@ -600,7 +623,6 @@ var I18N = {
|
|
|
600
623
|
"github.optPrMermaid": "PR Mermaid section mode (auto|on|off, default: auto)",
|
|
601
624
|
"github.optPrNoSyncTasks": "Do not sync PR URL/PR status into tasks.md",
|
|
602
625
|
"github.optPrCommitSync": "Commit and push tasks.md metadata sync automatically",
|
|
603
|
-
"github.invalidRepoComponentMismatch": "`--repo` and `--component` must reference the same value when both are provided.",
|
|
604
626
|
"github.labelsRequired": "At least one label is required. Use `--labels enhancement`.",
|
|
605
627
|
"github.approvalRequired": "{operation} requires explicit user approval. Re-run with `--confirm OK` after sharing the plan with the user.",
|
|
606
628
|
"github.ghCommandFailed": "GitHub CLI command failed",
|
|
@@ -668,6 +690,22 @@ var I18N = {
|
|
|
668
690
|
"docs.nextDocs": "Next docs",
|
|
669
691
|
"docs.sourceLabel": "source",
|
|
670
692
|
"docs.hashLabel": "hash",
|
|
693
|
+
"detect.cmdDescription": "Detect whether the current workspace is a lee-spec-kit project",
|
|
694
|
+
"detect.optDir": "Target directory to probe (default: current directory)",
|
|
695
|
+
"detect.optJson": "Output in JSON format for agents",
|
|
696
|
+
"detect.header": "\u{1F50E} Project Detection",
|
|
697
|
+
"detect.labelTarget": "Target",
|
|
698
|
+
"detect.resultDetected": "Detected a lee-spec-kit project",
|
|
699
|
+
"detect.resultNotDetected": "No lee-spec-kit project detected",
|
|
700
|
+
"detect.notDetectedHint": "Run `npx lee-spec-kit init` or pass `--dir` to the correct path.",
|
|
701
|
+
"detect.labelDocsDir": "Docs",
|
|
702
|
+
"detect.labelConfigPath": "Config",
|
|
703
|
+
"detect.labelSource": "Source",
|
|
704
|
+
"detect.labelProjectType": "Project Type",
|
|
705
|
+
"detect.labelLang": "Lang",
|
|
706
|
+
"detect.labelProjectName": "Project",
|
|
707
|
+
"detect.sourceConfig": "config (.lee-spec-kit.json)",
|
|
708
|
+
"detect.sourceHeuristic": "heuristic (agents/features folder)",
|
|
671
709
|
"cliError.headerNextOptionsError": "\u{1F449} Next Options (Error):",
|
|
672
710
|
"cliError.promptBlocked.retryWithoutNonInteractive": "Run the same command without --non-interactive.",
|
|
673
711
|
"cliError.promptBlocked.passRequiredFlags": "Pass all required flags (including `--force` when needed), then run again.",
|
|
@@ -769,7 +807,7 @@ var I18N = {
|
|
|
769
807
|
tasksImprove: "Improve tasks.md and change Doc Status to Review.",
|
|
770
808
|
tasksApproval: "Share tasks.md with the user and get progress approval (`A` or `A OK` format). (Then set Doc Status to Approved)",
|
|
771
809
|
docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} planning docs"',
|
|
772
|
-
issueCreateAndWrite: "Review procedure with `npx lee-spec-kit docs get create-issue --json`, then generate a
|
|
810
|
+
issueCreateAndWrite: "Review procedure with `npx lee-spec-kit docs get create-issue --json`, then generate a body template via `npx lee-spec-kit github issue {featureRef} --json`. Refine goals/completion criteria, get explicit user OK, run `--create --confirm OK`, then update issue number in spec.md/tasks.md and prepare a docs commit.",
|
|
773
811
|
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
|
|
774
812
|
docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} docs update"',
|
|
775
813
|
projectCommitIssueUpdate: 'cd "{projectGitCwd}" && (git diff --cached --quiet && echo "No staged files. Stage only files changed in this task with git add [files], then run again." && exit 1 || git commit -m "feat(#{issueNumber}): {commitTopic}")',
|
|
@@ -781,12 +819,18 @@ var I18N = {
|
|
|
781
819
|
finishDoingTask: 'Finish the current DOING/REVIEW task: "{title}" ({done}/{total}) (Before finishing, read `npx lee-spec-kit docs get execute-task --json`, then share outcome/verification + get OK before marking DONE)',
|
|
782
820
|
startNextTodoTask: 'Start the next TODO task: "{title}" ({done}/{total}) (Before starting, read `npx lee-spec-kit docs get execute-task --json`, then share title + get OK before marking DOING)',
|
|
783
821
|
checkTaskStatuses: "Check task statuses. ({done}/{total}) (Use `npx lee-spec-kit docs get execute-task --json` as the procedure baseline)",
|
|
822
|
+
taskCommitGateStrictBlock: "Before moving to the next TODO task, you must satisfy the `1 task = 1 commit` rule. Check result: {reason}. Re-align task commit boundaries using `npx lee-spec-kit docs get execute-task --json`, then continue.",
|
|
823
|
+
taskCommitGateWarnProceed: "\u26A0\uFE0F Task commit boundary warning: {reason}. You may continue, but `1 task = 1 commit` is recommended.",
|
|
824
|
+
taskCommitGateReasonNoTasksCommit: "No recent tasks.md commit was found",
|
|
825
|
+
taskCommitGateReasonTasksFileUnavailable: "Cannot read tasks.md history from the latest commit",
|
|
826
|
+
taskCommitGateReasonDoneCount: "DONE transitions in the latest tasks.md commit: {count} (expected: 1)",
|
|
827
|
+
taskCommitGateReasonMismatchLastDone: "The latest tasks.md commit does not match the last completed task",
|
|
784
828
|
prLegacyAsk: "tasks.md is missing PR/PR Status fields. Update to the latest template format? (CHECK required)",
|
|
785
829
|
prePrReviewFieldMissing: "tasks.md is missing the `Pre-PR Review` field. Add `- **Pre-PR Review**: Pending | Done` and run context again. (CHECK required)",
|
|
786
830
|
prePrReviewRun: "Run a pre-PR code review before creating the PR. Preferred skills: {skills} (if a better installed skill fits this change, propose it first). If no skill can run, use `{fallback}` and set `Pre-PR Review` to Done in tasks.md. Findings policy: {findingsPolicy}",
|
|
787
831
|
prePrReviewFindingsBlock: "major findings must be fixed/aligned before PR creation",
|
|
788
832
|
prePrReviewFindingsWarn: "you may proceed after sharing the risks",
|
|
789
|
-
prCreate: "Review procedure with `npx lee-spec-kit docs get create-pr --json`, then generate a
|
|
833
|
+
prCreate: "Review procedure with `npx lee-spec-kit docs get create-pr --json`, then generate a body template via `npx lee-spec-kit github pr {featureRef} --json`. Refine changes/tests sections, get explicit user OK, run `--create --confirm OK`, then record the PR link in tasks.md.",
|
|
790
834
|
prFillStatus: "Set PR Status in tasks.md to Review/Approved. (After merge, update it to Approved.)",
|
|
791
835
|
prResolveReview: "Resolve review comments and update PR Status. (PR Status: Review \u2192 Approved)",
|
|
792
836
|
prRequestReview: "Request review and update PR Status to Review.",
|
|
@@ -943,7 +987,7 @@ var SUGGESTION_MAP = {
|
|
|
943
987
|
},
|
|
944
988
|
{
|
|
945
989
|
titleKey: "contextSelection.narrowByComponent",
|
|
946
|
-
command: "npx lee-spec-kit context --
|
|
990
|
+
command: "npx lee-spec-kit context --component <component>"
|
|
947
991
|
},
|
|
948
992
|
{
|
|
949
993
|
titleKey: "contextSelection.inspectAllCandidates",
|
|
@@ -1092,9 +1136,6 @@ function normalizeProjectType(input) {
|
|
|
1092
1136
|
if (input === "multi") return "multi";
|
|
1093
1137
|
return "single";
|
|
1094
1138
|
}
|
|
1095
|
-
function toTemplateProjectType(projectType) {
|
|
1096
|
-
return projectType === "multi" ? "fullstack" : "single";
|
|
1097
|
-
}
|
|
1098
1139
|
|
|
1099
1140
|
// src/utils/validation.ts
|
|
1100
1141
|
var VALID_PROJECT_TYPES = ["single", "multi", "fullstack"];
|
|
@@ -1234,7 +1275,7 @@ function getProjectExecutionLockPath(cwd) {
|
|
|
1234
1275
|
}
|
|
1235
1276
|
async function isStaleLock(lockPath, staleMs) {
|
|
1236
1277
|
try {
|
|
1237
|
-
const stat = await
|
|
1278
|
+
const stat = await fs15.stat(lockPath);
|
|
1238
1279
|
if (Date.now() - stat.mtimeMs <= staleMs) {
|
|
1239
1280
|
return false;
|
|
1240
1281
|
}
|
|
@@ -1249,7 +1290,7 @@ async function isStaleLock(lockPath, staleMs) {
|
|
|
1249
1290
|
}
|
|
1250
1291
|
async function readLockPayload(lockPath) {
|
|
1251
1292
|
try {
|
|
1252
|
-
const raw = await
|
|
1293
|
+
const raw = await fs15.readFile(lockPath, "utf8");
|
|
1253
1294
|
const parsed = JSON.parse(raw);
|
|
1254
1295
|
if (!parsed || typeof parsed !== "object") return null;
|
|
1255
1296
|
return parsed;
|
|
@@ -1271,17 +1312,17 @@ function isProcessAlive(pid) {
|
|
|
1271
1312
|
}
|
|
1272
1313
|
}
|
|
1273
1314
|
async function tryAcquire(lockPath, owner) {
|
|
1274
|
-
await
|
|
1315
|
+
await fs15.ensureDir(path18.dirname(lockPath));
|
|
1275
1316
|
try {
|
|
1276
|
-
const fd = await
|
|
1317
|
+
const fd = await fs15.open(lockPath, "wx");
|
|
1277
1318
|
const payload = JSON.stringify(
|
|
1278
1319
|
{ pid: process.pid, owner: owner ?? "unknown", createdAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1279
1320
|
null,
|
|
1280
1321
|
2
|
|
1281
1322
|
);
|
|
1282
|
-
await
|
|
1323
|
+
await fs15.writeFile(fd, `${payload}
|
|
1283
1324
|
`, { encoding: "utf8" });
|
|
1284
|
-
await
|
|
1325
|
+
await fs15.close(fd);
|
|
1285
1326
|
return true;
|
|
1286
1327
|
} catch (error) {
|
|
1287
1328
|
if (error.code === "EEXIST") {
|
|
@@ -1295,9 +1336,9 @@ async function waitForLockRelease(lockPath, options = {}) {
|
|
|
1295
1336
|
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
1296
1337
|
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
1297
1338
|
const startedAt = Date.now();
|
|
1298
|
-
while (await
|
|
1339
|
+
while (await fs15.pathExists(lockPath)) {
|
|
1299
1340
|
if (await isStaleLock(lockPath, staleMs)) {
|
|
1300
|
-
await
|
|
1341
|
+
await fs15.remove(lockPath);
|
|
1301
1342
|
break;
|
|
1302
1343
|
}
|
|
1303
1344
|
if (Date.now() - startedAt > timeoutMs) {
|
|
@@ -1315,7 +1356,7 @@ async function withFileLock(lockPath, task, options = {}) {
|
|
|
1315
1356
|
const acquired = await tryAcquire(lockPath, options.owner);
|
|
1316
1357
|
if (acquired) break;
|
|
1317
1358
|
if (await isStaleLock(lockPath, staleMs)) {
|
|
1318
|
-
await
|
|
1359
|
+
await fs15.remove(lockPath);
|
|
1319
1360
|
continue;
|
|
1320
1361
|
}
|
|
1321
1362
|
if (Date.now() - startedAt > timeoutMs) {
|
|
@@ -1329,7 +1370,7 @@ async function withFileLock(lockPath, task, options = {}) {
|
|
|
1329
1370
|
try {
|
|
1330
1371
|
return await task();
|
|
1331
1372
|
} finally {
|
|
1332
|
-
await
|
|
1373
|
+
await fs15.remove(lockPath).catch(() => {
|
|
1333
1374
|
});
|
|
1334
1375
|
}
|
|
1335
1376
|
}
|
|
@@ -1356,21 +1397,21 @@ async function pruneEngineManagedDocs(docsDir) {
|
|
|
1356
1397
|
const removed = [];
|
|
1357
1398
|
for (const file of ENGINE_MANAGED_AGENT_FILES) {
|
|
1358
1399
|
const target = path18.join(docsDir, "agents", file);
|
|
1359
|
-
if (await
|
|
1360
|
-
await
|
|
1400
|
+
if (await fs15.pathExists(target)) {
|
|
1401
|
+
await fs15.remove(target);
|
|
1361
1402
|
removed.push(path18.relative(docsDir, target));
|
|
1362
1403
|
}
|
|
1363
1404
|
}
|
|
1364
1405
|
for (const dir of ENGINE_MANAGED_AGENT_DIRS) {
|
|
1365
1406
|
const target = path18.join(docsDir, "agents", dir);
|
|
1366
|
-
if (await
|
|
1367
|
-
await
|
|
1407
|
+
if (await fs15.pathExists(target)) {
|
|
1408
|
+
await fs15.remove(target);
|
|
1368
1409
|
removed.push(path18.relative(docsDir, target));
|
|
1369
1410
|
}
|
|
1370
1411
|
}
|
|
1371
1412
|
const featureBasePath = path18.join(docsDir, ENGINE_MANAGED_FEATURE_PATH);
|
|
1372
|
-
if (await
|
|
1373
|
-
await
|
|
1413
|
+
if (await fs15.pathExists(featureBasePath)) {
|
|
1414
|
+
await fs15.remove(featureBasePath);
|
|
1374
1415
|
removed.push(path18.relative(docsDir, featureBasePath));
|
|
1375
1416
|
}
|
|
1376
1417
|
return removed;
|
|
@@ -1388,16 +1429,110 @@ function checkGitRepo(cwd) {
|
|
|
1388
1429
|
return false;
|
|
1389
1430
|
}
|
|
1390
1431
|
}
|
|
1432
|
+
function parseStandaloneMultiProjectRootJson(raw) {
|
|
1433
|
+
let parsed;
|
|
1434
|
+
try {
|
|
1435
|
+
parsed = JSON.parse(raw);
|
|
1436
|
+
} catch {
|
|
1437
|
+
throw createCliError(
|
|
1438
|
+
"INVALID_ARGUMENT",
|
|
1439
|
+
'`--project-root` for standalone multi must be a JSON object. Example: {"fe":"/path/fe","be":"/path/be"}'
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1443
|
+
throw createCliError(
|
|
1444
|
+
"INVALID_ARGUMENT",
|
|
1445
|
+
"`--project-root` for standalone multi must be a JSON object."
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
const out = {};
|
|
1449
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1450
|
+
const component = key.trim().toLowerCase();
|
|
1451
|
+
const root = typeof value === "string" ? value.trim() : "";
|
|
1452
|
+
if (!component || !root) {
|
|
1453
|
+
throw createCliError(
|
|
1454
|
+
"INVALID_ARGUMENT",
|
|
1455
|
+
"`--project-root` JSON entries must be non-empty component/path pairs."
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
assertValidComponentId(component);
|
|
1459
|
+
out[component] = root;
|
|
1460
|
+
}
|
|
1461
|
+
if (Object.keys(out).length === 0) {
|
|
1462
|
+
throw createCliError(
|
|
1463
|
+
"INVALID_ARGUMENT",
|
|
1464
|
+
"`--project-root` JSON object must include at least one component path."
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
return out;
|
|
1468
|
+
}
|
|
1469
|
+
function parseComponentProjectRootsOption(raw) {
|
|
1470
|
+
if (!raw) return {};
|
|
1471
|
+
const out = {};
|
|
1472
|
+
const entries = raw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
1473
|
+
if (entries.length === 0) {
|
|
1474
|
+
throw createCliError(
|
|
1475
|
+
"INVALID_ARGUMENT",
|
|
1476
|
+
"`--component-project-roots` must include at least one `component=/path` pair."
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
for (const entry of entries) {
|
|
1480
|
+
const index = entry.indexOf("=");
|
|
1481
|
+
if (index <= 0 || index === entry.length - 1) {
|
|
1482
|
+
throw createCliError(
|
|
1483
|
+
"INVALID_ARGUMENT",
|
|
1484
|
+
`Invalid --component-project-roots entry "${entry}". Use \`component=/path\`.`
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
const component = entry.slice(0, index).trim().toLowerCase();
|
|
1488
|
+
const root = entry.slice(index + 1).trim();
|
|
1489
|
+
assertValidComponentId(component);
|
|
1490
|
+
if (!root) {
|
|
1491
|
+
throw createCliError(
|
|
1492
|
+
"INVALID_ARGUMENT",
|
|
1493
|
+
`Invalid --component-project-roots entry "${entry}". Path must not be empty.`
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
out[component] = root;
|
|
1497
|
+
}
|
|
1498
|
+
return out;
|
|
1499
|
+
}
|
|
1500
|
+
function getComponentFeaturesReadme(lang, component) {
|
|
1501
|
+
if (lang === "ko") {
|
|
1502
|
+
return [
|
|
1503
|
+
`# ${component.toUpperCase()} Features`,
|
|
1504
|
+
"",
|
|
1505
|
+
`\uC774 \uD3F4\uB354\uC5D0 ${component} Feature\uB4E4\uC774 \uC0DD\uC131\uB429\uB2C8\uB2E4.`,
|
|
1506
|
+
"",
|
|
1507
|
+
`\`npx lee-spec-kit feature --component ${component} <name>\` \uBA85\uB839\uC5B4\uB85C \uC0C8 Feature\uB97C \uC0DD\uC131\uD558\uC138\uC694.`,
|
|
1508
|
+
""
|
|
1509
|
+
].join("\n");
|
|
1510
|
+
}
|
|
1511
|
+
return [
|
|
1512
|
+
`# ${component.toUpperCase()} Features`,
|
|
1513
|
+
"",
|
|
1514
|
+
`Features for component \`${component}\` are created in this folder.`,
|
|
1515
|
+
"",
|
|
1516
|
+
`Use \`npx lee-spec-kit feature --component ${component} <name>\` to create a new Feature.`,
|
|
1517
|
+
""
|
|
1518
|
+
].join("\n");
|
|
1519
|
+
}
|
|
1391
1520
|
function initCommand(program2) {
|
|
1392
1521
|
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 | multi (fullstack alias)").option(
|
|
1393
1522
|
"--components <list>",
|
|
1394
1523
|
"Component list for multi (comma-separated, e.g. fe,be,worker)"
|
|
1395
|
-
).option("-l, --lang <lang>", "Language: ko | en (default: en)").option("--workflow <mode>", "Workflow mode: github | local").option("-d, --dir <dir>", "Target directory (default: ./docs)", "./docs").option("--docs-repo <mode>", "Docs repository mode: embedded | standalone").option(
|
|
1524
|
+
).option("-l, --lang <lang>", "Language: ko | en (default: en)").option("--workflow <mode>", "Workflow mode: github | local").option("-d, --dir <dir>", "Target directory (default: ./docs)", "./docs").option("--docs-repo <mode>", "Docs repository mode: embedded | standalone").option(
|
|
1525
|
+
"--project-root <path>",
|
|
1526
|
+
"Project root path (standalone single) or JSON map for standalone multi"
|
|
1527
|
+
).option(
|
|
1396
1528
|
"--fe-project-root <path>",
|
|
1397
1529
|
"Frontend repository path (standalone multi)"
|
|
1398
1530
|
).option(
|
|
1399
1531
|
"--be-project-root <path>",
|
|
1400
1532
|
"Backend repository path (standalone multi)"
|
|
1533
|
+
).option(
|
|
1534
|
+
"--component-project-roots <pairs>",
|
|
1535
|
+
"Component roots for standalone multi (comma-separated, e.g. fe=/path/fe,be=/path/be,worker=/path/worker)"
|
|
1401
1536
|
).option("--push-docs", "Push standalone docs to remote").option("--docs-remote <url>", "Remote URL for standalone docs repository").option("-y, --yes", "Skip prompts and use defaults").option("-f, --force", "Overwrite target directory if not empty").option("--non-interactive", "Fail instead of prompting for input").action(async (options) => {
|
|
1402
1537
|
try {
|
|
1403
1538
|
await runInit(options);
|
|
@@ -1434,6 +1569,9 @@ async function runInit(options) {
|
|
|
1434
1569
|
let pushDocs = typeof options.pushDocs === "boolean" ? options.pushDocs : void 0;
|
|
1435
1570
|
let docsRemote = options.docsRemote;
|
|
1436
1571
|
let projectRoot;
|
|
1572
|
+
const componentProjectRoots = parseComponentProjectRootsOption(
|
|
1573
|
+
options.componentProjectRoots
|
|
1574
|
+
);
|
|
1437
1575
|
const targetDir = path18.resolve(cwd, options.dir || "./docs");
|
|
1438
1576
|
const skipPrompts = !!options.yes || !!options.nonInteractive;
|
|
1439
1577
|
if (options.docsRepo && !["embedded", "standalone"].includes(options.docsRepo)) {
|
|
@@ -1445,12 +1583,7 @@ async function runInit(options) {
|
|
|
1445
1583
|
if (docsRemote && typeof pushDocs === "undefined") {
|
|
1446
1584
|
pushDocs = true;
|
|
1447
1585
|
}
|
|
1448
|
-
if (options.
|
|
1449
|
-
projectRoot = {
|
|
1450
|
-
fe: options.feProjectRoot || "",
|
|
1451
|
-
be: options.beProjectRoot || ""
|
|
1452
|
-
};
|
|
1453
|
-
} else if (options.projectRoot) {
|
|
1586
|
+
if (options.projectRoot) {
|
|
1454
1587
|
projectRoot = options.projectRoot;
|
|
1455
1588
|
}
|
|
1456
1589
|
const isInsideGitRepo = checkGitRepo(cwd);
|
|
@@ -1572,32 +1705,46 @@ async function runInit(options) {
|
|
|
1572
1705
|
String(projectType || response.projectType || "single")
|
|
1573
1706
|
);
|
|
1574
1707
|
if (resolvedType === "multi") {
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1708
|
+
const promptComponents = components.length > 0 ? components : ["fe", "be"];
|
|
1709
|
+
const rootMap = {};
|
|
1710
|
+
if (typeof projectRoot === "string" && projectRoot.trim()) {
|
|
1711
|
+
Object.assign(rootMap, parseStandaloneMultiProjectRootJson(projectRoot));
|
|
1712
|
+
} else if (projectRoot && typeof projectRoot === "object") {
|
|
1713
|
+
for (const [component, root] of Object.entries(projectRoot)) {
|
|
1714
|
+
const normalized = component.trim().toLowerCase();
|
|
1715
|
+
const normalizedRoot = String(root || "").trim();
|
|
1716
|
+
if (!normalized || !normalizedRoot) continue;
|
|
1717
|
+
rootMap[normalized] = normalizedRoot;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
if (options.feProjectRoot?.trim()) rootMap.fe = options.feProjectRoot.trim();
|
|
1721
|
+
if (options.beProjectRoot?.trim()) rootMap.be = options.beProjectRoot.trim();
|
|
1722
|
+
Object.assign(rootMap, componentProjectRoots);
|
|
1723
|
+
for (const component of promptComponents) {
|
|
1724
|
+
const seeded = (rootMap[component] || "").trim();
|
|
1725
|
+
if (seeded) {
|
|
1726
|
+
rootMap[component] = seeded;
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
const message = component === "fe" ? tr(lang, "cli", "init.prompt.feRepoPath") : component === "be" ? tr(lang, "cli", "init.prompt.beRepoPath") : tr(lang, "cli", "init.prompt.componentRepoPath", { component });
|
|
1730
|
+
const response2 = await prompts(
|
|
1731
|
+
[
|
|
1732
|
+
{
|
|
1733
|
+
type: "text",
|
|
1734
|
+
name: "componentRoot",
|
|
1735
|
+
message,
|
|
1736
|
+
validate: (value) => value.trim() ? true : tr(lang, "cli", "init.validation.enterPath")
|
|
1737
|
+
}
|
|
1738
|
+
],
|
|
1583
1739
|
{
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
validate: (value) => value.trim() ? true : tr(lang, "cli", "init.validation.enterPath")
|
|
1588
|
-
}
|
|
1589
|
-
],
|
|
1590
|
-
{
|
|
1591
|
-
onCancel: () => {
|
|
1592
|
-
throw new Error("canceled");
|
|
1740
|
+
onCancel: () => {
|
|
1741
|
+
throw new Error("canceled");
|
|
1742
|
+
}
|
|
1593
1743
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
projectRoot =
|
|
1598
|
-
fe: projectRootResponse.feRoot || currentRoot?.fe || "",
|
|
1599
|
-
be: projectRootResponse.beRoot || currentRoot?.be || ""
|
|
1600
|
-
};
|
|
1744
|
+
);
|
|
1745
|
+
rootMap[component] = (response2.componentRoot || "").trim();
|
|
1746
|
+
}
|
|
1747
|
+
projectRoot = rootMap;
|
|
1601
1748
|
} else {
|
|
1602
1749
|
const projectRootResponse = await prompts(
|
|
1603
1750
|
[
|
|
@@ -1700,7 +1847,7 @@ async function runInit(options) {
|
|
|
1700
1847
|
components.forEach(assertValidComponentId);
|
|
1701
1848
|
}
|
|
1702
1849
|
if (docsRepo !== "standalone") {
|
|
1703
|
-
if (options.projectRoot || options.feProjectRoot || options.beProjectRoot || typeof options.pushDocs === "boolean" || options.docsRemote) {
|
|
1850
|
+
if (options.projectRoot || options.feProjectRoot || options.beProjectRoot || options.componentProjectRoots || typeof options.pushDocs === "boolean" || options.docsRemote) {
|
|
1704
1851
|
throw createCliError(
|
|
1705
1852
|
"INVALID_ARGUMENT",
|
|
1706
1853
|
"Standalone-only options require `--docs-repo standalone`."
|
|
@@ -1711,21 +1858,54 @@ async function runInit(options) {
|
|
|
1711
1858
|
docsRemote = void 0;
|
|
1712
1859
|
} else {
|
|
1713
1860
|
if (projectType === "multi") {
|
|
1714
|
-
|
|
1861
|
+
const multiRoot = {};
|
|
1862
|
+
if (typeof projectRoot === "string" && projectRoot.trim()) {
|
|
1863
|
+
Object.assign(multiRoot, parseStandaloneMultiProjectRootJson(projectRoot));
|
|
1864
|
+
} else if (projectRoot && typeof projectRoot === "object") {
|
|
1865
|
+
for (const [component, root] of Object.entries(projectRoot)) {
|
|
1866
|
+
const normalized = component.trim().toLowerCase();
|
|
1867
|
+
const normalizedRoot = String(root || "").trim();
|
|
1868
|
+
if (!normalized || !normalizedRoot) continue;
|
|
1869
|
+
multiRoot[normalized] = normalizedRoot;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
if (options.feProjectRoot?.trim()) multiRoot.fe = options.feProjectRoot.trim();
|
|
1873
|
+
if (options.beProjectRoot?.trim()) multiRoot.be = options.beProjectRoot.trim();
|
|
1874
|
+
Object.assign(multiRoot, componentProjectRoots);
|
|
1875
|
+
const unknownComponents = Object.keys(multiRoot).filter(
|
|
1876
|
+
(component) => !components.includes(component)
|
|
1877
|
+
);
|
|
1878
|
+
if (unknownComponents.length > 0) {
|
|
1715
1879
|
throw createCliError(
|
|
1716
1880
|
"INVALID_ARGUMENT",
|
|
1717
|
-
|
|
1881
|
+
`Standalone multi project roots contain unknown components: ${unknownComponents.join(", ")}. Allowed: ${components.join(", ")}`
|
|
1718
1882
|
);
|
|
1719
1883
|
}
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1884
|
+
const missingComponents = components.filter(
|
|
1885
|
+
(component) => !(multiRoot[component] || "").trim()
|
|
1886
|
+
);
|
|
1887
|
+
if (missingComponents.length > 0) {
|
|
1722
1888
|
throw createCliError(
|
|
1723
1889
|
"PROMPT_BLOCKED",
|
|
1724
|
-
|
|
1890
|
+
`Standalone multi mode requires project roots for all components: ${missingComponents.join(", ")}. Use \`--component-project-roots <component=/path,...>\` or \`--project-root '{"component":"/path"}'\`.`
|
|
1725
1891
|
);
|
|
1726
1892
|
}
|
|
1727
|
-
projectRoot =
|
|
1893
|
+
projectRoot = Object.fromEntries(
|
|
1894
|
+
components.map((component) => [component, multiRoot[component].trim()])
|
|
1895
|
+
);
|
|
1728
1896
|
} else {
|
|
1897
|
+
if (options.componentProjectRoots) {
|
|
1898
|
+
throw createCliError(
|
|
1899
|
+
"INVALID_ARGUMENT",
|
|
1900
|
+
"`--component-project-roots` can only be used when `--type multi`."
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
if (options.feProjectRoot || options.beProjectRoot) {
|
|
1904
|
+
throw createCliError(
|
|
1905
|
+
"INVALID_ARGUMENT",
|
|
1906
|
+
"`--fe-project-root` and `--be-project-root` can only be used when `--type multi`."
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1729
1909
|
const singleRoot = typeof projectRoot === "string" ? projectRoot.trim() : "";
|
|
1730
1910
|
if (!singleRoot) {
|
|
1731
1911
|
throw createCliError(
|
|
@@ -1752,8 +1932,8 @@ async function runInit(options) {
|
|
|
1752
1932
|
await withFileLock(
|
|
1753
1933
|
initLockPath,
|
|
1754
1934
|
async () => {
|
|
1755
|
-
if (await
|
|
1756
|
-
const files = await
|
|
1935
|
+
if (await fs15.pathExists(targetDir)) {
|
|
1936
|
+
const files = await fs15.readdir(targetDir);
|
|
1757
1937
|
if (files.length > 0) {
|
|
1758
1938
|
if (options.force) {
|
|
1759
1939
|
} else if (options.nonInteractive) {
|
|
@@ -1790,32 +1970,22 @@ async function runInit(options) {
|
|
|
1790
1970
|
console.log();
|
|
1791
1971
|
const templatesDir = getTemplatesDir();
|
|
1792
1972
|
const commonPath = path18.join(templatesDir, lang, "common");
|
|
1793
|
-
|
|
1794
|
-
const typePath = path18.join(templatesDir, lang, templateProjectType);
|
|
1795
|
-
if (await fs14.pathExists(commonPath)) {
|
|
1796
|
-
await copyTemplates(commonPath, targetDir);
|
|
1797
|
-
}
|
|
1798
|
-
if (!await fs14.pathExists(typePath)) {
|
|
1973
|
+
if (!await fs15.pathExists(commonPath)) {
|
|
1799
1974
|
throw new Error(
|
|
1800
|
-
tr(lang, "cli", "init.error.templateNotFound", { path:
|
|
1975
|
+
tr(lang, "cli", "init.error.templateNotFound", { path: commonPath })
|
|
1801
1976
|
);
|
|
1802
1977
|
}
|
|
1803
|
-
await copyTemplates(
|
|
1804
|
-
if (projectType === "multi"
|
|
1978
|
+
await copyTemplates(commonPath, targetDir);
|
|
1979
|
+
if (projectType === "multi") {
|
|
1805
1980
|
const featuresRoot = path18.join(targetDir, "features");
|
|
1806
|
-
await fs14.remove(path18.join(featuresRoot, "fe"));
|
|
1807
|
-
await fs14.remove(path18.join(featuresRoot, "be"));
|
|
1808
1981
|
for (const component of components) {
|
|
1809
1982
|
const componentDir = path18.join(featuresRoot, component);
|
|
1810
|
-
await
|
|
1983
|
+
await fs15.ensureDir(componentDir);
|
|
1811
1984
|
const readmePath = path18.join(componentDir, "README.md");
|
|
1812
|
-
if (!await
|
|
1813
|
-
await
|
|
1985
|
+
if (!await fs15.pathExists(readmePath)) {
|
|
1986
|
+
await fs15.writeFile(
|
|
1814
1987
|
readmePath,
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
Store ${component} feature specs here.
|
|
1818
|
-
`,
|
|
1988
|
+
getComponentFeaturesReadme(lang, component),
|
|
1819
1989
|
"utf-8"
|
|
1820
1990
|
);
|
|
1821
1991
|
}
|
|
@@ -1824,6 +1994,7 @@ Store ${component} feature specs here.
|
|
|
1824
1994
|
const featurePath = projectType === "multi" ? isDefaultFullstackComponents(components) ? "docs/features/{be|fe}" : "docs/features/{component}" : "docs/features";
|
|
1825
1995
|
const replacements = {
|
|
1826
1996
|
"{{projectName}}": projectName,
|
|
1997
|
+
"{{projectType}}": projectType,
|
|
1827
1998
|
"{{date}}": getLocalDateString(),
|
|
1828
1999
|
"{{featurePath}}": featurePath
|
|
1829
2000
|
};
|
|
@@ -1839,6 +2010,7 @@ Store ${component} feature specs here.
|
|
|
1839
2010
|
workflow: {
|
|
1840
2011
|
mode: workflowMode,
|
|
1841
2012
|
codeDirtyScope: "auto",
|
|
2013
|
+
taskCommitGate: "strict",
|
|
1842
2014
|
prePrReview: { skills: ["code-review-excellence"] }
|
|
1843
2015
|
},
|
|
1844
2016
|
pr: {
|
|
@@ -1860,7 +2032,7 @@ Store ${component} feature specs here.
|
|
|
1860
2032
|
}
|
|
1861
2033
|
}
|
|
1862
2034
|
const configPath = path18.join(targetDir, ".lee-spec-kit.json");
|
|
1863
|
-
await
|
|
2035
|
+
await fs15.writeJson(configPath, config, { spaces: 2 });
|
|
1864
2036
|
console.log(chalk6.green(tr(lang, "cli", "init.log.docsCreated")));
|
|
1865
2037
|
console.log();
|
|
1866
2038
|
await initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote);
|
|
@@ -1906,7 +2078,7 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
|
1906
2078
|
}
|
|
1907
2079
|
};
|
|
1908
2080
|
const toGitPath = (input) => input.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1909
|
-
const
|
|
2081
|
+
const toRepoRelativePath2 = (workdir, relativePath2) => {
|
|
1910
2082
|
if (relativePath2 === ".") return ".";
|
|
1911
2083
|
try {
|
|
1912
2084
|
const prefix = execFileSync("git", ["rev-parse", "--show-prefix"], {
|
|
@@ -1941,7 +2113,7 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
|
1941
2113
|
return;
|
|
1942
2114
|
}
|
|
1943
2115
|
if (relativePath !== "." && isPathIgnored(cwd, relativePath)) {
|
|
1944
|
-
const repoRelativePath =
|
|
2116
|
+
const repoRelativePath = toRepoRelativePath2(cwd, relativePath);
|
|
1945
2117
|
console.log(
|
|
1946
2118
|
chalk6.yellow(
|
|
1947
2119
|
tr(lang, "cli", "init.warn.docsPathIgnoredSkipCommit", {
|
|
@@ -1995,7 +2167,7 @@ function getAncestorDirs(startDir) {
|
|
|
1995
2167
|
return dirs;
|
|
1996
2168
|
}
|
|
1997
2169
|
function hasWorkspaceBoundary(dir) {
|
|
1998
|
-
return
|
|
2170
|
+
return fs15.existsSync(path18.join(dir, "package.json")) || fs15.existsSync(path18.join(dir, ".git"));
|
|
1999
2171
|
}
|
|
2000
2172
|
function getSearchBaseDirs(cwd) {
|
|
2001
2173
|
const ancestors = getAncestorDirs(cwd);
|
|
@@ -2023,9 +2195,9 @@ async function getConfig(cwd) {
|
|
|
2023
2195
|
if (visitedDocsDirs.has(resolvedDocsDir)) continue;
|
|
2024
2196
|
visitedDocsDirs.add(resolvedDocsDir);
|
|
2025
2197
|
const configPath = path18.join(resolvedDocsDir, ".lee-spec-kit.json");
|
|
2026
|
-
if (await
|
|
2198
|
+
if (await fs15.pathExists(configPath)) {
|
|
2027
2199
|
try {
|
|
2028
|
-
const configFile = await
|
|
2200
|
+
const configFile = await fs15.readJson(configPath);
|
|
2029
2201
|
const projectType = normalizeProjectType(configFile.projectType);
|
|
2030
2202
|
const components = resolveProjectComponents(
|
|
2031
2203
|
projectType,
|
|
@@ -2050,10 +2222,10 @@ async function getConfig(cwd) {
|
|
|
2050
2222
|
}
|
|
2051
2223
|
const agentsPath = path18.join(resolvedDocsDir, "agents");
|
|
2052
2224
|
const featuresPath = path18.join(resolvedDocsDir, "features");
|
|
2053
|
-
if (await
|
|
2225
|
+
if (await fs15.pathExists(agentsPath) && await fs15.pathExists(featuresPath)) {
|
|
2054
2226
|
const bePath = path18.join(featuresPath, "be");
|
|
2055
2227
|
const fePath = path18.join(featuresPath, "fe");
|
|
2056
|
-
const projectType = await
|
|
2228
|
+
const projectType = await fs15.pathExists(bePath) || await fs15.pathExists(fePath) ? "multi" : "single";
|
|
2057
2229
|
const components = projectType === "multi" ? resolveProjectComponents("multi", ["fe", "be"]) : void 0;
|
|
2058
2230
|
const langProbeCandidates = [
|
|
2059
2231
|
path18.join(agentsPath, "custom.md"),
|
|
@@ -2062,8 +2234,8 @@ async function getConfig(cwd) {
|
|
|
2062
2234
|
];
|
|
2063
2235
|
let lang = "en";
|
|
2064
2236
|
for (const candidate of langProbeCandidates) {
|
|
2065
|
-
if (!await
|
|
2066
|
-
const content = await
|
|
2237
|
+
if (!await fs15.pathExists(candidate)) continue;
|
|
2238
|
+
const content = await fs15.readFile(candidate, "utf-8");
|
|
2067
2239
|
if (/[가-힣]/.test(content)) {
|
|
2068
2240
|
lang = "ko";
|
|
2069
2241
|
break;
|
|
@@ -2109,9 +2281,9 @@ function sanitizeTasksForLocal(content, lang) {
|
|
|
2109
2281
|
return normalizeTrailingBlankLines(next);
|
|
2110
2282
|
}
|
|
2111
2283
|
async function patchMarkdownIfExists(filePath, transform) {
|
|
2112
|
-
if (!await
|
|
2113
|
-
const content = await
|
|
2114
|
-
await
|
|
2284
|
+
if (!await fs15.pathExists(filePath)) return;
|
|
2285
|
+
const content = await fs15.readFile(filePath, "utf-8");
|
|
2286
|
+
await fs15.writeFile(filePath, transform(content), "utf-8");
|
|
2115
2287
|
}
|
|
2116
2288
|
async function applyLocalWorkflowTemplateToFeatureDir(featureDir, lang) {
|
|
2117
2289
|
await patchMarkdownIfExists(path18.join(featureDir, "spec.md"), sanitizeSpecForLocal);
|
|
@@ -2123,7 +2295,7 @@ async function applyLocalWorkflowTemplateToFeatureDir(featureDir, lang) {
|
|
|
2123
2295
|
|
|
2124
2296
|
// src/commands/feature.ts
|
|
2125
2297
|
function featureCommand(program2) {
|
|
2126
|
-
program2.command("feature <name>").description("Create a new feature folder").option("
|
|
2298
|
+
program2.command("feature <name>").description("Create a new feature folder").option("--component <component>", "Component name (multi only)").option("--id <id>", "Feature ID (default: auto)").option("-d, --desc <description>", "Feature description for spec.md").option("--non-interactive", "Fail instead of prompting for input").option("--json", "Output in JSON format for agents").action(async (name, options) => {
|
|
2127
2299
|
try {
|
|
2128
2300
|
const result = await runFeature(name, options);
|
|
2129
2301
|
if (options.json) {
|
|
@@ -2207,25 +2379,19 @@ async function runFeature(name, options) {
|
|
|
2207
2379
|
tr(lang, "cli", "validation.context.featureName"),
|
|
2208
2380
|
lang
|
|
2209
2381
|
);
|
|
2210
|
-
|
|
2211
|
-
throw createCliError(
|
|
2212
|
-
"INVALID_ARGUMENT",
|
|
2213
|
-
"`--repo` and `--component` must reference the same value when both are provided."
|
|
2214
|
-
);
|
|
2215
|
-
}
|
|
2216
|
-
let component = (options.component || options.repo || "").trim().toLowerCase();
|
|
2382
|
+
let component = (options.component || "").trim().toLowerCase();
|
|
2217
2383
|
if (!component) component = "";
|
|
2218
2384
|
if (projectType === "single" && component) {
|
|
2219
2385
|
throw createCliError(
|
|
2220
2386
|
"INVALID_ARGUMENT",
|
|
2221
|
-
"`--
|
|
2387
|
+
"`--component` can only be used in multi mode."
|
|
2222
2388
|
);
|
|
2223
2389
|
}
|
|
2224
2390
|
if (projectType === "multi" && !component) {
|
|
2225
2391
|
if (options.nonInteractive) {
|
|
2226
2392
|
throw createCliError(
|
|
2227
2393
|
"PROMPT_BLOCKED",
|
|
2228
|
-
"`--component`
|
|
2394
|
+
"`--component` is required in multi mode when using `--non-interactive`."
|
|
2229
2395
|
);
|
|
2230
2396
|
}
|
|
2231
2397
|
const response = await prompts(
|
|
@@ -2272,7 +2438,7 @@ async function runFeature(name, options) {
|
|
|
2272
2438
|
}
|
|
2273
2439
|
const featureFolderName = `${featureId}-${name}`;
|
|
2274
2440
|
const featureDir = path18.join(featuresDir, featureFolderName);
|
|
2275
|
-
if (await
|
|
2441
|
+
if (await fs15.pathExists(featureDir)) {
|
|
2276
2442
|
throw createCliError(
|
|
2277
2443
|
"INVALID_ARGUMENT",
|
|
2278
2444
|
tr(lang, "cli", "feature.folderExists", { path: featureDir })
|
|
@@ -2281,14 +2447,14 @@ async function runFeature(name, options) {
|
|
|
2281
2447
|
const featureBasePath = path18.join(
|
|
2282
2448
|
getTemplatesDir(),
|
|
2283
2449
|
lang,
|
|
2284
|
-
|
|
2450
|
+
"common",
|
|
2285
2451
|
"features",
|
|
2286
2452
|
"feature-base"
|
|
2287
2453
|
);
|
|
2288
|
-
if (!await
|
|
2454
|
+
if (!await fs15.pathExists(featureBasePath)) {
|
|
2289
2455
|
throw createCliError("DOCS_NOT_FOUND", tr(lang, "cli", "feature.baseNotFound"));
|
|
2290
2456
|
}
|
|
2291
|
-
await
|
|
2457
|
+
await fs15.copy(featureBasePath, featureDir);
|
|
2292
2458
|
const idNumber = featureId.replace("F", "");
|
|
2293
2459
|
const repoName = projectType === "multi" ? `{{projectName}}-${component}` : "{{projectName}}";
|
|
2294
2460
|
const replacements = {
|
|
@@ -2360,7 +2526,7 @@ async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
|
|
|
2360
2526
|
const initLockPath = getInitLockPath(dir);
|
|
2361
2527
|
const docsLockPath = getDocsLockPath(dir);
|
|
2362
2528
|
for (const lockPath of [initLockPath, docsLockPath]) {
|
|
2363
|
-
if (await
|
|
2529
|
+
if (await fs15.pathExists(lockPath)) {
|
|
2364
2530
|
sawLock = true;
|
|
2365
2531
|
await waitForLockRelease(lockPath, {
|
|
2366
2532
|
timeoutMs: Math.max(200, endAt - Date.now()),
|
|
@@ -2385,8 +2551,8 @@ async function getNextFeatureId(docsDir, projectType, components) {
|
|
|
2385
2551
|
scanDirs.push(featuresDir);
|
|
2386
2552
|
}
|
|
2387
2553
|
for (const dir of scanDirs) {
|
|
2388
|
-
if (!await
|
|
2389
|
-
const entries = await
|
|
2554
|
+
if (!await fs15.pathExists(dir)) continue;
|
|
2555
|
+
const entries = await fs15.readdir(dir, { withFileTypes: true });
|
|
2390
2556
|
for (const entry of entries) {
|
|
2391
2557
|
if (!entry.isDirectory()) continue;
|
|
2392
2558
|
const match = entry.name.match(/^F(\d+)-/);
|
|
@@ -2449,6 +2615,11 @@ function resolveCodeDirtyScopePolicy(workflow, projectType) {
|
|
|
2449
2615
|
}
|
|
2450
2616
|
return projectType === "multi" ? "component" : "repo";
|
|
2451
2617
|
}
|
|
2618
|
+
function resolveTaskCommitGatePolicy(workflow) {
|
|
2619
|
+
const raw = workflow?.taskCommitGate;
|
|
2620
|
+
if (raw === "off" || raw === "warn" || raw === "strict") return raw;
|
|
2621
|
+
return "warn";
|
|
2622
|
+
}
|
|
2452
2623
|
function normalizeSkillList(input) {
|
|
2453
2624
|
if (!Array.isArray(input)) return [];
|
|
2454
2625
|
const deduped = /* @__PURE__ */ new Set();
|
|
@@ -2509,9 +2680,105 @@ function resolveProjectCommitTopic(feature) {
|
|
|
2509
2680
|
const topic = withoutTaskId || normalizeCommitTopicText(feature.folderName);
|
|
2510
2681
|
return toShellSafeCommitTopic(topic);
|
|
2511
2682
|
}
|
|
2683
|
+
function normalizeGitRelativePath(value) {
|
|
2684
|
+
return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
2685
|
+
}
|
|
2686
|
+
function toRepoRelativePath(cwd, relativePathFromCwd) {
|
|
2687
|
+
const prefix = (readGitText(cwd, ["rev-parse", "--show-prefix"]) || "").trim().replace(/\/+$/, "");
|
|
2688
|
+
if (!prefix) return normalizeGitRelativePath(relativePathFromCwd);
|
|
2689
|
+
return normalizeGitRelativePath(`${prefix}/${relativePathFromCwd}`);
|
|
2690
|
+
}
|
|
2691
|
+
function readGitText(cwd, args) {
|
|
2692
|
+
try {
|
|
2693
|
+
return execFileSync("git", args, {
|
|
2694
|
+
cwd,
|
|
2695
|
+
encoding: "utf-8",
|
|
2696
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2697
|
+
});
|
|
2698
|
+
} catch {
|
|
2699
|
+
return void 0;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
function normalizeTaskTopic(value) {
|
|
2703
|
+
return normalizeCommitTopicText(value).replace(/^T-[A-Za-z0-9-]+\s+/, "");
|
|
2704
|
+
}
|
|
2705
|
+
function parseDoneTaskTopicCounts(content) {
|
|
2706
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2707
|
+
const lines = content.split("\n");
|
|
2708
|
+
let inCodeBlock = false;
|
|
2709
|
+
for (const line of lines) {
|
|
2710
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
2711
|
+
inCodeBlock = !inCodeBlock;
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
if (inCodeBlock) continue;
|
|
2715
|
+
const match = line.match(/^\s*-\s*\[([A-Z]+)\]((?:\[[^\]]+\])*)\s*(.+?)\s*$/);
|
|
2716
|
+
if (!match) continue;
|
|
2717
|
+
if (match[1].toUpperCase() !== "DONE") continue;
|
|
2718
|
+
const topic = normalizeTaskTopic(match[3] || "");
|
|
2719
|
+
if (!topic) continue;
|
|
2720
|
+
counts.set(topic, (counts.get(topic) || 0) + 1);
|
|
2721
|
+
}
|
|
2722
|
+
return counts;
|
|
2723
|
+
}
|
|
2724
|
+
function checkTaskCommitGate(feature) {
|
|
2725
|
+
const tasksPath = normalizeGitRelativePath(`${feature.docs.featurePathFromDocs}/tasks.md`);
|
|
2726
|
+
const docsGitCwd = feature.git.docsGitCwd;
|
|
2727
|
+
const repoTasksPath = toRepoRelativePath(docsGitCwd, tasksPath);
|
|
2728
|
+
const latestTasksCommit = (readGitText(docsGitCwd, ["rev-list", "-n", "1", "HEAD", "--", tasksPath]) || "").trim();
|
|
2729
|
+
if (!latestTasksCommit) {
|
|
2730
|
+
return { pass: false, reason: "NO_TASKS_COMMIT" };
|
|
2731
|
+
}
|
|
2732
|
+
const currentContent = readGitText(docsGitCwd, ["show", `${latestTasksCommit}:${repoTasksPath}`]);
|
|
2733
|
+
if (currentContent === void 0) {
|
|
2734
|
+
return { pass: false, reason: "TASKS_FILE_UNAVAILABLE" };
|
|
2735
|
+
}
|
|
2736
|
+
const previousContent = readGitText(docsGitCwd, ["show", `${latestTasksCommit}^:${repoTasksPath}`]) || "";
|
|
2737
|
+
const currentDone = parseDoneTaskTopicCounts(currentContent);
|
|
2738
|
+
const previousDone = parseDoneTaskTopicCounts(previousContent);
|
|
2739
|
+
let newDoneCount = 0;
|
|
2740
|
+
for (const [topic, currentCount] of currentDone.entries()) {
|
|
2741
|
+
const previousCount = previousDone.get(topic) || 0;
|
|
2742
|
+
if (currentCount > previousCount) {
|
|
2743
|
+
newDoneCount += currentCount - previousCount;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (newDoneCount !== 1) {
|
|
2747
|
+
return {
|
|
2748
|
+
pass: false,
|
|
2749
|
+
reason: "MULTIPLE_DONE_TRANSITIONS",
|
|
2750
|
+
newDoneCount
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
const lastDoneTopic = normalizeTaskTopic(feature.lastDoneTask?.title || "");
|
|
2754
|
+
if (lastDoneTopic) {
|
|
2755
|
+
const previousCount = previousDone.get(lastDoneTopic) || 0;
|
|
2756
|
+
const currentCount = currentDone.get(lastDoneTopic) || 0;
|
|
2757
|
+
if (currentCount <= previousCount) {
|
|
2758
|
+
return { pass: false, reason: "MISMATCH_LAST_DONE", newDoneCount };
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
return { pass: true, reason: "MULTIPLE_DONE_TRANSITIONS", newDoneCount };
|
|
2762
|
+
}
|
|
2763
|
+
function getTaskCommitGateReasonText(lang, check) {
|
|
2764
|
+
switch (check.reason) {
|
|
2765
|
+
case "NO_TASKS_COMMIT":
|
|
2766
|
+
return tr(lang, "messages", "taskCommitGateReasonNoTasksCommit");
|
|
2767
|
+
case "TASKS_FILE_UNAVAILABLE":
|
|
2768
|
+
return tr(lang, "messages", "taskCommitGateReasonTasksFileUnavailable");
|
|
2769
|
+
case "MISMATCH_LAST_DONE":
|
|
2770
|
+
return tr(lang, "messages", "taskCommitGateReasonMismatchLastDone");
|
|
2771
|
+
case "MULTIPLE_DONE_TRANSITIONS":
|
|
2772
|
+
default:
|
|
2773
|
+
return tr(lang, "messages", "taskCommitGateReasonDoneCount", {
|
|
2774
|
+
count: check.newDoneCount ?? 0
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2512
2778
|
function getStepDefinitions(lang, workflow) {
|
|
2513
2779
|
const workflowPolicy = resolveWorkflowPolicy(workflow);
|
|
2514
2780
|
const prePrReviewPolicy = resolvePrePrReviewPolicy(workflow);
|
|
2781
|
+
const taskCommitGatePolicy = resolveTaskCommitGatePolicy(workflow);
|
|
2515
2782
|
return [
|
|
2516
2783
|
{
|
|
2517
2784
|
step: 1,
|
|
@@ -2842,6 +3109,39 @@ function getStepDefinitions(lang, workflow) {
|
|
|
2842
3109
|
}
|
|
2843
3110
|
];
|
|
2844
3111
|
}
|
|
3112
|
+
if (taskCommitGatePolicy !== "off" && f.lastDoneTask) {
|
|
3113
|
+
const commitGate = checkTaskCommitGate(f);
|
|
3114
|
+
if (!commitGate.pass) {
|
|
3115
|
+
const reasonText = getTaskCommitGateReasonText(lang, commitGate);
|
|
3116
|
+
if (taskCommitGatePolicy === "strict") {
|
|
3117
|
+
return [
|
|
3118
|
+
{
|
|
3119
|
+
type: "instruction",
|
|
3120
|
+
category: "task_execute",
|
|
3121
|
+
requiresUserCheck: true,
|
|
3122
|
+
message: tr(lang, "messages", "taskCommitGateStrictBlock", {
|
|
3123
|
+
reason: reasonText
|
|
3124
|
+
})
|
|
3125
|
+
}
|
|
3126
|
+
];
|
|
3127
|
+
}
|
|
3128
|
+
return [
|
|
3129
|
+
{
|
|
3130
|
+
type: "instruction",
|
|
3131
|
+
category: "task_execute",
|
|
3132
|
+
requiresUserCheck: true,
|
|
3133
|
+
message: `${tr(lang, "messages", "startNextTodoTask", {
|
|
3134
|
+
title: f.nextTodoTask.title,
|
|
3135
|
+
done: f.tasks.done,
|
|
3136
|
+
total: f.tasks.total
|
|
3137
|
+
})}
|
|
3138
|
+
${tr(lang, "messages", "taskCommitGateWarnProceed", {
|
|
3139
|
+
reason: reasonText
|
|
3140
|
+
})}`
|
|
3141
|
+
}
|
|
3142
|
+
];
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
2845
3145
|
return [
|
|
2846
3146
|
{
|
|
2847
3147
|
type: "instruction",
|
|
@@ -3429,7 +3729,7 @@ async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
|
|
|
3429
3729
|
if (cached) return [...cached];
|
|
3430
3730
|
const existing = [];
|
|
3431
3731
|
for (const candidate of normalizedCandidates) {
|
|
3432
|
-
if (await
|
|
3732
|
+
if (await fs15.pathExists(path18.join(projectGitCwd, candidate))) {
|
|
3433
3733
|
existing.push(candidate);
|
|
3434
3734
|
}
|
|
3435
3735
|
}
|
|
@@ -3453,18 +3753,20 @@ function parseTasks(content) {
|
|
|
3453
3753
|
if (!match) continue;
|
|
3454
3754
|
const status = match[1].toUpperCase();
|
|
3455
3755
|
const title = match[3].trim();
|
|
3756
|
+
const taskIdMatch = title.match(/\b(T-[A-Za-z0-9-]+)\b/);
|
|
3757
|
+
const taskId = taskIdMatch ? taskIdMatch[1] : void 0;
|
|
3456
3758
|
summary.total++;
|
|
3457
3759
|
if (status === "DONE") summary.done++;
|
|
3458
3760
|
else if (status === "DOING" || status === "REVIEW") summary.doing++;
|
|
3459
3761
|
else if (status === "TODO") summary.todo++;
|
|
3460
3762
|
if (!activeTask && (status === "DOING" || status === "REVIEW")) {
|
|
3461
|
-
activeTask = { status, title };
|
|
3763
|
+
activeTask = { id: taskId, status, title };
|
|
3462
3764
|
}
|
|
3463
3765
|
if (status === "DONE") {
|
|
3464
|
-
lastDoneTask = { status: "DONE", title };
|
|
3766
|
+
lastDoneTask = { id: taskId, status: "DONE", title };
|
|
3465
3767
|
}
|
|
3466
3768
|
if (!nextTodoTask && status === "TODO") {
|
|
3467
|
-
nextTodoTask = { status: "TODO", title };
|
|
3769
|
+
nextTodoTask = { id: taskId, status: "TODO", title };
|
|
3468
3770
|
}
|
|
3469
3771
|
}
|
|
3470
3772
|
return { summary, activeTask, lastDoneTask, nextTodoTask };
|
|
@@ -3506,22 +3808,22 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3506
3808
|
const tasksPath = path18.join(featurePath, "tasks.md");
|
|
3507
3809
|
let specStatus;
|
|
3508
3810
|
let issueNumber;
|
|
3509
|
-
const specExists = await
|
|
3811
|
+
const specExists = await fs15.pathExists(specPath);
|
|
3510
3812
|
if (specExists) {
|
|
3511
|
-
const content = await
|
|
3813
|
+
const content = await fs15.readFile(specPath, "utf-8");
|
|
3512
3814
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
3513
3815
|
specStatus = parseDocStatus(statusValue);
|
|
3514
3816
|
const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
|
|
3515
3817
|
issueNumber = parseIssueNumber(issueValue);
|
|
3516
3818
|
}
|
|
3517
3819
|
let planStatus;
|
|
3518
|
-
const planExists = await
|
|
3820
|
+
const planExists = await fs15.pathExists(planPath);
|
|
3519
3821
|
if (planExists) {
|
|
3520
|
-
const content = await
|
|
3822
|
+
const content = await fs15.readFile(planPath, "utf-8");
|
|
3521
3823
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
3522
3824
|
planStatus = parseDocStatus(statusValue);
|
|
3523
3825
|
}
|
|
3524
|
-
const tasksExists = await
|
|
3826
|
+
const tasksExists = await fs15.pathExists(tasksPath);
|
|
3525
3827
|
const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
|
|
3526
3828
|
let activeTask;
|
|
3527
3829
|
let lastDoneTask;
|
|
@@ -3536,7 +3838,7 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3536
3838
|
let prStatusFieldExists = false;
|
|
3537
3839
|
let prePrReviewFieldExists = false;
|
|
3538
3840
|
if (tasksExists) {
|
|
3539
|
-
const content = await
|
|
3841
|
+
const content = await fs15.readFile(tasksPath, "utf-8");
|
|
3540
3842
|
const {
|
|
3541
3843
|
summary,
|
|
3542
3844
|
activeTask: active,
|
|
@@ -4076,7 +4378,7 @@ async function runStatus(options) {
|
|
|
4076
4378
|
),
|
|
4077
4379
|
""
|
|
4078
4380
|
].join("\n");
|
|
4079
|
-
await
|
|
4381
|
+
await fs15.writeFile(outputPath, content, "utf-8");
|
|
4080
4382
|
console.log(
|
|
4081
4383
|
chalk6.green(
|
|
4082
4384
|
tr(lang, "cli", "status.wrote", { path: outputPath })
|
|
@@ -4090,8 +4392,8 @@ function escapeRegExp2(value) {
|
|
|
4090
4392
|
async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderName) {
|
|
4091
4393
|
try {
|
|
4092
4394
|
const specPath = path18.join(featureDir, "spec.md");
|
|
4093
|
-
if (!await
|
|
4094
|
-
const content = await
|
|
4395
|
+
if (!await fs15.pathExists(specPath)) return fallbackSlug;
|
|
4396
|
+
const content = await fs15.readFile(specPath, "utf-8");
|
|
4095
4397
|
const keys = ["\uAE30\uB2A5\uBA85", "Feature Name"];
|
|
4096
4398
|
for (const key of keys) {
|
|
4097
4399
|
const regex = new RegExp(
|
|
@@ -4148,6 +4450,7 @@ async function runUpdate(options) {
|
|
|
4148
4450
|
const templatesDir = getTemplatesDir();
|
|
4149
4451
|
const docsLockPath = getDocsLockPath(docsDir);
|
|
4150
4452
|
const forceOverwrite = !!options.force || await isDocsWorktreeCleanOrThrow(docsDir, lang, [docsLockPath]);
|
|
4453
|
+
const configBackfill = await backfillMissingConfigDefaults(docsDir);
|
|
4151
4454
|
const hasExplicitSelection = !!(options.agents || options.skills || options.templates);
|
|
4152
4455
|
const updateAgents = options.agents || options.skills || !hasExplicitSelection;
|
|
4153
4456
|
const updateTemplates = options.templates || !hasExplicitSelection;
|
|
@@ -4171,15 +4474,8 @@ async function runUpdate(options) {
|
|
|
4171
4474
|
}
|
|
4172
4475
|
if (agentsMode === "all") {
|
|
4173
4476
|
const commonAgentsBase = path18.join(templatesDir, lang, "common", "agents");
|
|
4174
|
-
const typeAgentsBase = path18.join(
|
|
4175
|
-
templatesDir,
|
|
4176
|
-
lang,
|
|
4177
|
-
toTemplateProjectType(projectType),
|
|
4178
|
-
"agents"
|
|
4179
|
-
);
|
|
4180
4477
|
const targetAgentsBase = path18.join(docsDir, "agents");
|
|
4181
4478
|
const commonAgents = agentsMode === "skills" ? path18.join(commonAgentsBase, "skills") : commonAgentsBase;
|
|
4182
|
-
const typeAgents = agentsMode === "skills" ? path18.join(typeAgentsBase, "skills") : typeAgentsBase;
|
|
4183
4479
|
const targetAgents = agentsMode === "skills" ? path18.join(targetAgentsBase, "skills") : targetAgentsBase;
|
|
4184
4480
|
const featurePath = projectType === "multi" ? isDefaultFullstackComponents(config.components || []) ? "docs/features/{be|fe}" : "docs/features/{component}" : "docs/features";
|
|
4185
4481
|
const projectName = config.projectName ?? "{{projectName}}";
|
|
@@ -4187,10 +4483,7 @@ async function runUpdate(options) {
|
|
|
4187
4483
|
"{{projectName}}": projectName,
|
|
4188
4484
|
"{{featurePath}}": featurePath
|
|
4189
4485
|
};
|
|
4190
|
-
|
|
4191
|
-
"{{projectName}}": projectName
|
|
4192
|
-
};
|
|
4193
|
-
if (await fs14.pathExists(commonAgents)) {
|
|
4486
|
+
if (await fs15.pathExists(commonAgents)) {
|
|
4194
4487
|
const count = await updateFolder(
|
|
4195
4488
|
commonAgents,
|
|
4196
4489
|
targetAgents,
|
|
@@ -4208,24 +4501,6 @@ async function runUpdate(options) {
|
|
|
4208
4501
|
);
|
|
4209
4502
|
updatedCount += count;
|
|
4210
4503
|
}
|
|
4211
|
-
if (await fs14.pathExists(typeAgents)) {
|
|
4212
|
-
const count = await updateFolder(
|
|
4213
|
-
typeAgents,
|
|
4214
|
-
targetAgents,
|
|
4215
|
-
forceOverwrite,
|
|
4216
|
-
typeReplacements,
|
|
4217
|
-
lang,
|
|
4218
|
-
{
|
|
4219
|
-
protectedFiles: /* @__PURE__ */ new Set([
|
|
4220
|
-
"custom.md",
|
|
4221
|
-
"constitution.md",
|
|
4222
|
-
...ENGINE_MANAGED_AGENT_FILES
|
|
4223
|
-
]),
|
|
4224
|
-
skipDirectories: new Set(ENGINE_MANAGED_AGENT_DIRS)
|
|
4225
|
-
}
|
|
4226
|
-
);
|
|
4227
|
-
updatedCount += count;
|
|
4228
|
-
}
|
|
4229
4504
|
console.log(
|
|
4230
4505
|
chalk6.green(
|
|
4231
4506
|
` \u2705 ${agentsMode === "skills" ? tr(lang, "cli", "update.skillsUpdated") : tr(lang, "cli", "update.agentsUpdated")}`
|
|
@@ -4248,6 +4523,18 @@ async function runUpdate(options) {
|
|
|
4248
4523
|
);
|
|
4249
4524
|
}
|
|
4250
4525
|
console.log();
|
|
4526
|
+
if (configBackfill.changed) {
|
|
4527
|
+
console.log(
|
|
4528
|
+
chalk6.gray(
|
|
4529
|
+
` - ${tr(lang, "cli", "update.fileUpdated", { file: ".lee-spec-kit.json" })}`
|
|
4530
|
+
)
|
|
4531
|
+
);
|
|
4532
|
+
console.log(
|
|
4533
|
+
chalk6.gray(
|
|
4534
|
+
` (${configBackfill.changedPaths.join(", ")})`
|
|
4535
|
+
)
|
|
4536
|
+
);
|
|
4537
|
+
}
|
|
4251
4538
|
console.log(
|
|
4252
4539
|
chalk6.green(`\u2705 ${tr(lang, "cli", "update.updatedTotal", { count: updatedCount })}`)
|
|
4253
4540
|
);
|
|
@@ -4255,27 +4542,106 @@ async function runUpdate(options) {
|
|
|
4255
4542
|
{ owner: "update" }
|
|
4256
4543
|
);
|
|
4257
4544
|
}
|
|
4545
|
+
function isPlainObject(value) {
|
|
4546
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4547
|
+
}
|
|
4548
|
+
function normalizeSkillList2(raw) {
|
|
4549
|
+
if (!Array.isArray(raw)) return [];
|
|
4550
|
+
const deduped = /* @__PURE__ */ new Set();
|
|
4551
|
+
for (const item of raw) {
|
|
4552
|
+
const value = String(item || "").trim();
|
|
4553
|
+
if (!value) continue;
|
|
4554
|
+
deduped.add(value);
|
|
4555
|
+
}
|
|
4556
|
+
return [...deduped];
|
|
4557
|
+
}
|
|
4558
|
+
async function backfillMissingConfigDefaults(docsDir) {
|
|
4559
|
+
const configPath = path18.join(docsDir, ".lee-spec-kit.json");
|
|
4560
|
+
if (!await fs15.pathExists(configPath)) {
|
|
4561
|
+
return { changed: false, changedPaths: [] };
|
|
4562
|
+
}
|
|
4563
|
+
const raw = await fs15.readJson(configPath);
|
|
4564
|
+
if (!isPlainObject(raw)) {
|
|
4565
|
+
return { changed: false, changedPaths: [] };
|
|
4566
|
+
}
|
|
4567
|
+
const changedPaths = [];
|
|
4568
|
+
const setIfMissing = (parent, key, nextValue, pathLabel) => {
|
|
4569
|
+
if (parent[key] !== void 0) return;
|
|
4570
|
+
parent[key] = nextValue;
|
|
4571
|
+
changedPaths.push(pathLabel);
|
|
4572
|
+
};
|
|
4573
|
+
if (!isPlainObject(raw.workflow)) {
|
|
4574
|
+
raw.workflow = {};
|
|
4575
|
+
changedPaths.push("workflow");
|
|
4576
|
+
}
|
|
4577
|
+
const workflow = raw.workflow;
|
|
4578
|
+
setIfMissing(workflow, "mode", "github", "workflow.mode");
|
|
4579
|
+
setIfMissing(workflow, "codeDirtyScope", "auto", "workflow.codeDirtyScope");
|
|
4580
|
+
setIfMissing(workflow, "taskCommitGate", "strict", "workflow.taskCommitGate");
|
|
4581
|
+
if (!isPlainObject(workflow.prePrReview)) {
|
|
4582
|
+
workflow.prePrReview = {};
|
|
4583
|
+
changedPaths.push("workflow.prePrReview");
|
|
4584
|
+
}
|
|
4585
|
+
const prePrReview = workflow.prePrReview;
|
|
4586
|
+
if (prePrReview.skills === void 0) {
|
|
4587
|
+
prePrReview.skills = ["code-review-excellence"];
|
|
4588
|
+
changedPaths.push("workflow.prePrReview.skills");
|
|
4589
|
+
} else {
|
|
4590
|
+
const normalizedSkills = normalizeSkillList2(prePrReview.skills);
|
|
4591
|
+
if (normalizedSkills.length === 0) {
|
|
4592
|
+
prePrReview.skills = ["code-review-excellence"];
|
|
4593
|
+
changedPaths.push("workflow.prePrReview.skills");
|
|
4594
|
+
} else if (JSON.stringify(normalizedSkills) !== JSON.stringify(prePrReview.skills)) {
|
|
4595
|
+
prePrReview.skills = normalizedSkills;
|
|
4596
|
+
changedPaths.push("workflow.prePrReview.skills");
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
setIfMissing(prePrReview, "fallback", "builtin-checklist", "workflow.prePrReview.fallback");
|
|
4600
|
+
setIfMissing(prePrReview, "blockOnFindings", true, "workflow.prePrReview.blockOnFindings");
|
|
4601
|
+
if (!isPlainObject(raw.pr)) {
|
|
4602
|
+
raw.pr = {};
|
|
4603
|
+
changedPaths.push("pr");
|
|
4604
|
+
}
|
|
4605
|
+
const pr = raw.pr;
|
|
4606
|
+
if (!isPlainObject(pr.screenshots)) {
|
|
4607
|
+
pr.screenshots = {};
|
|
4608
|
+
changedPaths.push("pr.screenshots");
|
|
4609
|
+
}
|
|
4610
|
+
const screenshots = pr.screenshots;
|
|
4611
|
+
setIfMissing(screenshots, "upload", false, "pr.screenshots.upload");
|
|
4612
|
+
if (!isPlainObject(raw.approval)) {
|
|
4613
|
+
raw.approval = {};
|
|
4614
|
+
changedPaths.push("approval");
|
|
4615
|
+
}
|
|
4616
|
+
const approval = raw.approval;
|
|
4617
|
+
setIfMissing(approval, "mode", "builtin", "approval.mode");
|
|
4618
|
+
if (changedPaths.length === 0) {
|
|
4619
|
+
return { changed: false, changedPaths: [] };
|
|
4620
|
+
}
|
|
4621
|
+
await fs15.writeJson(configPath, raw, { spaces: 2 });
|
|
4622
|
+
return { changed: true, changedPaths };
|
|
4623
|
+
}
|
|
4258
4624
|
async function updateFolder(sourceDir, targetDir, force, replacements, lang = DEFAULT_LANG, options = {}) {
|
|
4259
4625
|
const protectedFiles = options.protectedFiles ?? /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
|
|
4260
4626
|
const skipDirectories = options.skipDirectories ?? /* @__PURE__ */ new Set();
|
|
4261
|
-
await
|
|
4262
|
-
const files = await
|
|
4627
|
+
await fs15.ensureDir(targetDir);
|
|
4628
|
+
const files = await fs15.readdir(sourceDir);
|
|
4263
4629
|
let updatedCount = 0;
|
|
4264
4630
|
for (const file of files) {
|
|
4265
4631
|
const sourcePath = path18.join(sourceDir, file);
|
|
4266
4632
|
const targetPath = path18.join(targetDir, file);
|
|
4267
|
-
const stat = await
|
|
4633
|
+
const stat = await fs15.stat(sourcePath);
|
|
4268
4634
|
if (stat.isFile()) {
|
|
4269
4635
|
if (protectedFiles.has(file)) {
|
|
4270
4636
|
continue;
|
|
4271
4637
|
}
|
|
4272
|
-
let sourceContent = await
|
|
4638
|
+
let sourceContent = await fs15.readFile(sourcePath, "utf-8");
|
|
4273
4639
|
if (replacements) {
|
|
4274
4640
|
sourceContent = applyReplacements(sourceContent, replacements);
|
|
4275
4641
|
}
|
|
4276
4642
|
let shouldUpdate = true;
|
|
4277
|
-
if (await
|
|
4278
|
-
const targetContent = await
|
|
4643
|
+
if (await fs15.pathExists(targetPath)) {
|
|
4644
|
+
const targetContent = await fs15.readFile(targetPath, "utf-8");
|
|
4279
4645
|
if (sourceContent === targetContent) {
|
|
4280
4646
|
continue;
|
|
4281
4647
|
}
|
|
@@ -4289,7 +4655,7 @@ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DE
|
|
|
4289
4655
|
}
|
|
4290
4656
|
}
|
|
4291
4657
|
if (shouldUpdate) {
|
|
4292
|
-
await
|
|
4658
|
+
await fs15.writeFile(targetPath, sourceContent);
|
|
4293
4659
|
console.log(
|
|
4294
4660
|
chalk6.gray(` \u{1F4C4} ${tr(lang, "cli", "update.fileUpdated", { file })}`)
|
|
4295
4661
|
);
|
|
@@ -4389,7 +4755,7 @@ async function isDocsWorktreeCleanOrThrow(docsDir, lang, ignoredAbsPaths = []) {
|
|
|
4389
4755
|
return true;
|
|
4390
4756
|
}
|
|
4391
4757
|
function configCommand(program2) {
|
|
4392
|
-
program2.command("config").description("View or modify project configuration").option("--dir <dir>", "Docs directory or project path to target").option("--project-root <path>", "Set project root path").option("--
|
|
4758
|
+
program2.command("config").description("View or modify project configuration").option("--dir <dir>", "Docs directory or project path to target").option("--project-root <path>", "Set project root path").option("--component <component>", "Component name for multi projects").option("--non-interactive", "Fail instead of prompting for input").action(async (options) => {
|
|
4393
4759
|
try {
|
|
4394
4760
|
await runConfig(options);
|
|
4395
4761
|
} catch (error) {
|
|
@@ -4434,7 +4800,7 @@ async function runConfig(options) {
|
|
|
4434
4800
|
)
|
|
4435
4801
|
);
|
|
4436
4802
|
console.log();
|
|
4437
|
-
const configFile = await
|
|
4803
|
+
const configFile = await fs15.readJson(configPath);
|
|
4438
4804
|
console.log(JSON.stringify(configFile, null, 2));
|
|
4439
4805
|
console.log();
|
|
4440
4806
|
return;
|
|
@@ -4442,7 +4808,7 @@ async function runConfig(options) {
|
|
|
4442
4808
|
await withFileLock(
|
|
4443
4809
|
getDocsLockPath(config.docsDir),
|
|
4444
4810
|
async () => {
|
|
4445
|
-
const configFile = await
|
|
4811
|
+
const configFile = await fs15.readJson(configPath);
|
|
4446
4812
|
if (configFile.docsRepo !== "standalone") {
|
|
4447
4813
|
console.log(
|
|
4448
4814
|
chalk6.yellow(tr(config.lang, "cli", "config.projectRootStandaloneOnly"))
|
|
@@ -4450,13 +4816,7 @@ async function runConfig(options) {
|
|
|
4450
4816
|
return;
|
|
4451
4817
|
}
|
|
4452
4818
|
const projectType = normalizeProjectType(String(configFile.projectType || "single"));
|
|
4453
|
-
const targetFromOptions = options.component?.trim().toLowerCase()
|
|
4454
|
-
if (options.component && options.repo && options.component.trim().toLowerCase() !== options.repo.trim().toLowerCase()) {
|
|
4455
|
-
throw createCliError(
|
|
4456
|
-
"INVALID_ARGUMENT",
|
|
4457
|
-
"`--repo` and `--component` must reference the same value when both are provided."
|
|
4458
|
-
);
|
|
4459
|
-
}
|
|
4819
|
+
const targetFromOptions = options.component?.trim().toLowerCase();
|
|
4460
4820
|
if (projectType === "multi") {
|
|
4461
4821
|
const components = resolveProjectComponents(projectType, configFile.components);
|
|
4462
4822
|
let targetComponent = targetFromOptions;
|
|
@@ -4464,7 +4824,7 @@ async function runConfig(options) {
|
|
|
4464
4824
|
if (options.nonInteractive) {
|
|
4465
4825
|
throw createCliError(
|
|
4466
4826
|
"PROMPT_BLOCKED",
|
|
4467
|
-
"`--component`
|
|
4827
|
+
"`--component` is required for multi projectRoot update when using `--non-interactive`."
|
|
4468
4828
|
);
|
|
4469
4829
|
}
|
|
4470
4830
|
const response = await prompts(
|
|
@@ -4509,7 +4869,7 @@ async function runConfig(options) {
|
|
|
4509
4869
|
if (targetFromOptions) {
|
|
4510
4870
|
throw createCliError(
|
|
4511
4871
|
"INVALID_ARGUMENT",
|
|
4512
|
-
"`--
|
|
4872
|
+
"`--component` is only valid for multi projectRoot updates."
|
|
4513
4873
|
);
|
|
4514
4874
|
}
|
|
4515
4875
|
configFile.projectRoot = options.projectRoot;
|
|
@@ -4521,7 +4881,7 @@ async function runConfig(options) {
|
|
|
4521
4881
|
)
|
|
4522
4882
|
);
|
|
4523
4883
|
}
|
|
4524
|
-
await
|
|
4884
|
+
await fs15.writeJson(configPath, configFile, { spaces: 2 });
|
|
4525
4885
|
console.log();
|
|
4526
4886
|
},
|
|
4527
4887
|
{ owner: "config" }
|
|
@@ -4540,7 +4900,7 @@ var LOCAL_ACTION_CATEGORIES = /* @__PURE__ */ new Set([
|
|
|
4540
4900
|
]);
|
|
4541
4901
|
var REMOTE_COMMAND_PATTERN = /\b(?:git\s+push|git\s+merge|gh\s+(?:issue|pr)\b)/i;
|
|
4542
4902
|
function resolveComponentOption(options) {
|
|
4543
|
-
const component = (options.component ||
|
|
4903
|
+
const component = (options.component || "").trim().toLowerCase();
|
|
4544
4904
|
return component || void 0;
|
|
4545
4905
|
}
|
|
4546
4906
|
function getActionLabel(index) {
|
|
@@ -4611,7 +4971,7 @@ function toActionOptions(actions) {
|
|
|
4611
4971
|
label,
|
|
4612
4972
|
summary,
|
|
4613
4973
|
detail,
|
|
4614
|
-
approvalPrompt: `${label}: ${
|
|
4974
|
+
approvalPrompt: `${label}: ${detail}`,
|
|
4615
4975
|
action
|
|
4616
4976
|
};
|
|
4617
4977
|
});
|
|
@@ -4775,7 +5135,7 @@ var BUILTIN_DOC_DEFINITIONS = [
|
|
|
4775
5135
|
{
|
|
4776
5136
|
id: "agents",
|
|
4777
5137
|
title: { ko: "\uC5D0\uC774\uC804\uD2B8 \uC6B4\uC601 \uADDC\uCE59", en: "Agent Operating Rules" },
|
|
4778
|
-
relativePath: (
|
|
5138
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "agents.md")
|
|
4779
5139
|
},
|
|
4780
5140
|
{
|
|
4781
5141
|
id: "git-workflow",
|
|
@@ -4903,7 +5263,7 @@ async function getBuiltinDoc(docId, projectType, lang) {
|
|
|
4903
5263
|
if (!entry) {
|
|
4904
5264
|
throw new Error(`Unknown builtin doc: ${docId}`);
|
|
4905
5265
|
}
|
|
4906
|
-
const content = await
|
|
5266
|
+
const content = await fs15.readFile(entry.absolutePath, "utf-8");
|
|
4907
5267
|
const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
4908
5268
|
return {
|
|
4909
5269
|
entry,
|
|
@@ -4914,6 +5274,8 @@ async function getBuiltinDoc(docId, projectType, lang) {
|
|
|
4914
5274
|
}
|
|
4915
5275
|
|
|
4916
5276
|
// src/commands/context.ts
|
|
5277
|
+
var APPROVAL_TICKET_FILENAME = ".lee-spec-kit.approval-tickets.json";
|
|
5278
|
+
var APPROVAL_TICKET_TTL_MS = 5 * 60 * 1e3;
|
|
4917
5279
|
async function resolveContextState(config, featureName, options) {
|
|
4918
5280
|
if (!config) {
|
|
4919
5281
|
throw createCliError(
|
|
@@ -4923,10 +5285,176 @@ async function resolveContextState(config, featureName, options) {
|
|
|
4923
5285
|
}
|
|
4924
5286
|
return resolveContextSelection(config, featureName, options);
|
|
4925
5287
|
}
|
|
4926
|
-
function parseApprovalLabel(input) {
|
|
4927
|
-
const
|
|
4928
|
-
if (!
|
|
4929
|
-
|
|
5288
|
+
function parseApprovalLabel(input, validLabels) {
|
|
5289
|
+
const normalized = input.trim().toUpperCase();
|
|
5290
|
+
if (!normalized) return null;
|
|
5291
|
+
const validSet = new Set(
|
|
5292
|
+
validLabels.map((label) => label.trim().toUpperCase()).filter(Boolean)
|
|
5293
|
+
);
|
|
5294
|
+
if (validSet.size === 0) return null;
|
|
5295
|
+
const leading = normalized.match(/^[`"'([{<\s]*([A-Z]+)\b/);
|
|
5296
|
+
if (leading && validSet.has(leading[1])) return leading[1];
|
|
5297
|
+
const tokens = normalized.match(/[A-Z]+/g) || [];
|
|
5298
|
+
for (const token of tokens) {
|
|
5299
|
+
if (validSet.has(token)) return token;
|
|
5300
|
+
}
|
|
5301
|
+
return null;
|
|
5302
|
+
}
|
|
5303
|
+
function getApprovalTicketPath(config) {
|
|
5304
|
+
return path18.join(config.docsDir, APPROVAL_TICKET_FILENAME);
|
|
5305
|
+
}
|
|
5306
|
+
function getApprovalSessionId() {
|
|
5307
|
+
const explicit = (process.env.LEE_SPEC_KIT_SESSION_ID || "").trim();
|
|
5308
|
+
if (explicit) return explicit;
|
|
5309
|
+
const terminalSession = (process.env.TERM_SESSION_ID || process.env.WT_SESSION || process.env.TMUX_PANE || "").trim();
|
|
5310
|
+
if (terminalSession) return terminalSession;
|
|
5311
|
+
return `ppid:${process.ppid}`;
|
|
5312
|
+
}
|
|
5313
|
+
function toActionHash(option) {
|
|
5314
|
+
const payload = JSON.stringify({
|
|
5315
|
+
label: option.label,
|
|
5316
|
+
action: option.action
|
|
5317
|
+
});
|
|
5318
|
+
return createHash("sha256").update(payload).digest("hex").slice(0, 24);
|
|
5319
|
+
}
|
|
5320
|
+
async function loadApprovalTicketStore(storePath) {
|
|
5321
|
+
if (!await fs15.pathExists(storePath)) return { tickets: [] };
|
|
5322
|
+
try {
|
|
5323
|
+
const parsed = await fs15.readJson(storePath);
|
|
5324
|
+
if (!parsed || !Array.isArray(parsed.tickets)) return { tickets: [] };
|
|
5325
|
+
return { tickets: parsed.tickets };
|
|
5326
|
+
} catch {
|
|
5327
|
+
return { tickets: [] };
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
function pruneApprovalTickets(tickets, nowMs) {
|
|
5331
|
+
return tickets.filter((ticket) => {
|
|
5332
|
+
if (ticket.usedAt) return false;
|
|
5333
|
+
const expiresAtMs = Date.parse(ticket.expiresAt || "");
|
|
5334
|
+
if (!Number.isFinite(expiresAtMs)) return false;
|
|
5335
|
+
return expiresAtMs > nowMs;
|
|
5336
|
+
});
|
|
5337
|
+
}
|
|
5338
|
+
async function issueApprovalTicket(config, payload) {
|
|
5339
|
+
const sessionId = getApprovalSessionId();
|
|
5340
|
+
const nowMs = Date.now();
|
|
5341
|
+
const record = {
|
|
5342
|
+
token: randomUUID().replace(/-/g, ""),
|
|
5343
|
+
sessionId,
|
|
5344
|
+
contextVersion: payload.contextVersion,
|
|
5345
|
+
actionHash: payload.actionHash,
|
|
5346
|
+
label: payload.label,
|
|
5347
|
+
featureRef: payload.featureRef,
|
|
5348
|
+
createdAt: new Date(nowMs).toISOString(),
|
|
5349
|
+
expiresAt: new Date(nowMs + APPROVAL_TICKET_TTL_MS).toISOString()
|
|
5350
|
+
};
|
|
5351
|
+
const storePath = getApprovalTicketPath(config);
|
|
5352
|
+
const lockPath = getDocsLockPath(config.docsDir);
|
|
5353
|
+
return withFileLock(
|
|
5354
|
+
lockPath,
|
|
5355
|
+
async () => {
|
|
5356
|
+
const store = await loadApprovalTicketStore(storePath);
|
|
5357
|
+
const nextTickets = pruneApprovalTickets(store.tickets, nowMs);
|
|
5358
|
+
nextTickets.push(record);
|
|
5359
|
+
await fs15.writeJson(
|
|
5360
|
+
storePath,
|
|
5361
|
+
{
|
|
5362
|
+
tickets: nextTickets,
|
|
5363
|
+
updatedAt: new Date(nowMs).toISOString()
|
|
5364
|
+
},
|
|
5365
|
+
{ spaces: 2 }
|
|
5366
|
+
);
|
|
5367
|
+
return record;
|
|
5368
|
+
},
|
|
5369
|
+
{ owner: "context-approval-ticket:issue" }
|
|
5370
|
+
);
|
|
5371
|
+
}
|
|
5372
|
+
async function consumeApprovalTicket(config, token, expected) {
|
|
5373
|
+
const normalizedToken = token.trim();
|
|
5374
|
+
if (!normalizedToken) {
|
|
5375
|
+
throw createCliError(
|
|
5376
|
+
"APPROVAL_REQUIRED",
|
|
5377
|
+
"Execution requires an approval ticket. Run `context --approve <reply> --json` first and pass `--ticket <token>`."
|
|
5378
|
+
);
|
|
5379
|
+
}
|
|
5380
|
+
const storePath = getApprovalTicketPath(config);
|
|
5381
|
+
const lockPath = getDocsLockPath(config.docsDir);
|
|
5382
|
+
const sessionId = getApprovalSessionId();
|
|
5383
|
+
const nowMs = Date.now();
|
|
5384
|
+
return withFileLock(
|
|
5385
|
+
lockPath,
|
|
5386
|
+
async () => {
|
|
5387
|
+
const store = await loadApprovalTicketStore(storePath);
|
|
5388
|
+
const cleaned = pruneApprovalTickets(store.tickets, nowMs);
|
|
5389
|
+
const index = cleaned.findIndex((entry) => entry.token === normalizedToken);
|
|
5390
|
+
if (index < 0) {
|
|
5391
|
+
await fs15.writeJson(
|
|
5392
|
+
storePath,
|
|
5393
|
+
{ tickets: cleaned, updatedAt: new Date(nowMs).toISOString() },
|
|
5394
|
+
{ spaces: 2 }
|
|
5395
|
+
);
|
|
5396
|
+
throw createCliError(
|
|
5397
|
+
"INVALID_APPROVAL",
|
|
5398
|
+
"Unknown or expired approval ticket. Re-run `context` and approve again."
|
|
5399
|
+
);
|
|
5400
|
+
}
|
|
5401
|
+
const record = cleaned[index];
|
|
5402
|
+
const expiresAtMs = Date.parse(record.expiresAt || "");
|
|
5403
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) {
|
|
5404
|
+
cleaned.splice(index, 1);
|
|
5405
|
+
await fs15.writeJson(
|
|
5406
|
+
storePath,
|
|
5407
|
+
{ tickets: cleaned, updatedAt: new Date(nowMs).toISOString() },
|
|
5408
|
+
{ spaces: 2 }
|
|
5409
|
+
);
|
|
5410
|
+
throw createCliError(
|
|
5411
|
+
"CONTEXT_STALE",
|
|
5412
|
+
"Approval ticket expired. Run `context` again and re-approve."
|
|
5413
|
+
);
|
|
5414
|
+
}
|
|
5415
|
+
if (record.sessionId !== sessionId) {
|
|
5416
|
+
throw createCliError(
|
|
5417
|
+
"INVALID_APPROVAL",
|
|
5418
|
+
"Approval ticket session mismatch. Re-run `context` in the current session and approve again."
|
|
5419
|
+
);
|
|
5420
|
+
}
|
|
5421
|
+
if (record.label !== expected.label) {
|
|
5422
|
+
throw createCliError(
|
|
5423
|
+
"INVALID_APPROVAL",
|
|
5424
|
+
`Approval ticket label mismatch. Ticket=${record.label}, expected=${expected.label}.`
|
|
5425
|
+
);
|
|
5426
|
+
}
|
|
5427
|
+
if (record.contextVersion !== expected.contextVersion) {
|
|
5428
|
+
throw createCliError(
|
|
5429
|
+
"CONTEXT_STALE",
|
|
5430
|
+
"Context changed after approval. Run `context` again and re-approve."
|
|
5431
|
+
);
|
|
5432
|
+
}
|
|
5433
|
+
if (record.actionHash !== expected.actionHash) {
|
|
5434
|
+
throw createCliError(
|
|
5435
|
+
"CONTEXT_STALE",
|
|
5436
|
+
"Selected action changed after approval. Run `context` again and re-approve."
|
|
5437
|
+
);
|
|
5438
|
+
}
|
|
5439
|
+
if (record.featureRef !== expected.featureRef) {
|
|
5440
|
+
throw createCliError(
|
|
5441
|
+
"INVALID_APPROVAL",
|
|
5442
|
+
"Approval ticket feature mismatch. Re-run `context` for this feature and approve again."
|
|
5443
|
+
);
|
|
5444
|
+
}
|
|
5445
|
+
cleaned.splice(index, 1);
|
|
5446
|
+
await fs15.writeJson(
|
|
5447
|
+
storePath,
|
|
5448
|
+
{
|
|
5449
|
+
tickets: cleaned,
|
|
5450
|
+
updatedAt: new Date(nowMs).toISOString()
|
|
5451
|
+
},
|
|
5452
|
+
{ spaces: 2 }
|
|
5453
|
+
);
|
|
5454
|
+
return record;
|
|
5455
|
+
},
|
|
5456
|
+
{ owner: "context-approval-ticket:consume" }
|
|
5457
|
+
);
|
|
4930
5458
|
}
|
|
4931
5459
|
function listLabels(actionOptions) {
|
|
4932
5460
|
if (actionOptions.length === 0) return "-";
|
|
@@ -4939,8 +5467,10 @@ function resolveFeatureRefForApproval(state, featureName) {
|
|
|
4939
5467
|
function buildApprovalCommand(state, featureName, selectedComponent, execute) {
|
|
4940
5468
|
const featureRef = resolveFeatureRefForApproval(state, featureName);
|
|
4941
5469
|
const componentArg = selectedComponent ? ` --component ${selectedComponent}` : "";
|
|
4942
|
-
|
|
4943
|
-
|
|
5470
|
+
if (execute) {
|
|
5471
|
+
return `npx lee-spec-kit context ${featureRef}${componentArg} --approve <LABEL> --execute [--ticket <TICKET>]`;
|
|
5472
|
+
}
|
|
5473
|
+
return `npx lee-spec-kit context ${featureRef}${componentArg} --approve <LABEL>`;
|
|
4944
5474
|
}
|
|
4945
5475
|
function buildFinalApprovalPrompt(lang, actionOptions) {
|
|
4946
5476
|
if (actionOptions.length === 0) return "";
|
|
@@ -4991,7 +5521,16 @@ function getCommandExecutionLockPath(action, config) {
|
|
|
4991
5521
|
return getProjectExecutionLockPath(action.cwd);
|
|
4992
5522
|
}
|
|
4993
5523
|
function contextCommand(program2) {
|
|
4994
|
-
program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--
|
|
5524
|
+
program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--component <component>", "Component name for multi projects").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").option(
|
|
5525
|
+
"--approve <reply>",
|
|
5526
|
+
"Approve one labeled option (examples: A, A OK, A proceed, A \uC9C4\uD589\uD574)"
|
|
5527
|
+
).option(
|
|
5528
|
+
"--ticket <token>",
|
|
5529
|
+
"Approval ticket issued by `--approve` (required only when selected option requires user check)"
|
|
5530
|
+
).option(
|
|
5531
|
+
"--execute",
|
|
5532
|
+
"Execute approved option when it is a command (ticket required only for check-required options)"
|
|
5533
|
+
).option(
|
|
4995
5534
|
"--execute-strict",
|
|
4996
5535
|
"Fail when approved option is instruction-only (use with --execute)"
|
|
4997
5536
|
).action(
|
|
@@ -5079,6 +5618,7 @@ async function runContext(featureName, options) {
|
|
|
5079
5618
|
const lang = config?.lang ?? "en";
|
|
5080
5619
|
const workflowPolicy = resolveWorkflowPolicy(config?.workflow);
|
|
5081
5620
|
const prePrReviewPolicy = resolvePrePrReviewPolicy(config?.workflow);
|
|
5621
|
+
const taskCommitGatePolicy = resolveTaskCommitGatePolicy(config?.workflow);
|
|
5082
5622
|
if (!config) {
|
|
5083
5623
|
throw createCliError(
|
|
5084
5624
|
"CONFIG_NOT_FOUND",
|
|
@@ -5088,22 +5628,22 @@ async function runContext(featureName, options) {
|
|
|
5088
5628
|
if (options.execute && !options.approve) {
|
|
5089
5629
|
throw createCliError(
|
|
5090
5630
|
"APPROVAL_REQUIRED",
|
|
5091
|
-
"`--execute` requires `--approve <
|
|
5631
|
+
"`--execute` requires `--approve <reply>`."
|
|
5092
5632
|
);
|
|
5093
5633
|
}
|
|
5094
|
-
if (options.
|
|
5634
|
+
if (!options.execute && options.ticket) {
|
|
5095
5635
|
throw createCliError(
|
|
5096
5636
|
"INVALID_ARGUMENT",
|
|
5097
|
-
"`--
|
|
5637
|
+
"`--ticket` is only valid with `--execute`."
|
|
5098
5638
|
);
|
|
5099
5639
|
}
|
|
5100
|
-
if (options.
|
|
5640
|
+
if (options.executeStrict && !options.execute) {
|
|
5101
5641
|
throw createCliError(
|
|
5102
5642
|
"INVALID_ARGUMENT",
|
|
5103
|
-
"`--
|
|
5643
|
+
"`--execute-strict` requires `--execute`."
|
|
5104
5644
|
);
|
|
5105
5645
|
}
|
|
5106
|
-
const selectedComponent = (options.component ||
|
|
5646
|
+
const selectedComponent = (options.component || "").trim().toLowerCase();
|
|
5107
5647
|
const stepDefinitions = getStepDefinitions(lang, config.workflow);
|
|
5108
5648
|
const stepsMap = getStepsMap(lang, config.workflow);
|
|
5109
5649
|
const selectionOptions = {
|
|
@@ -5113,7 +5653,7 @@ async function runContext(featureName, options) {
|
|
|
5113
5653
|
};
|
|
5114
5654
|
const state = await resolveContextState(config, featureName, selectionOptions);
|
|
5115
5655
|
const requiredDocs = buildRequiredDocHints(state.actionOptions);
|
|
5116
|
-
if (options.approve) {
|
|
5656
|
+
if (options.approve || options.execute) {
|
|
5117
5657
|
await runApprovedOption(
|
|
5118
5658
|
state,
|
|
5119
5659
|
config,
|
|
@@ -5160,34 +5700,47 @@ async function runContext(featureName, options) {
|
|
|
5160
5700
|
primaryActionCategory: primaryAction?.action.category ?? null,
|
|
5161
5701
|
primaryActionOperationType: primaryAction?.action.operationType ?? null,
|
|
5162
5702
|
workflowPolicy,
|
|
5703
|
+
taskCommitGatePolicy,
|
|
5163
5704
|
prePrReviewPolicy,
|
|
5164
5705
|
checkPolicy: {
|
|
5165
5706
|
docPath: "builtin://agents/policy",
|
|
5166
5707
|
hint: tr(lang, "cli", "context.checkPolicyHint"),
|
|
5167
5708
|
policyOnly: true,
|
|
5168
5709
|
token: "<LABEL>",
|
|
5169
|
-
acceptedTokens: ["<LABEL>", "<LABEL> OK"],
|
|
5170
|
-
tokenPattern: "
|
|
5710
|
+
acceptedTokens: ["<LABEL>", "<LABEL> OK", "<LABEL> ...", "... <LABEL> ..."],
|
|
5711
|
+
tokenPattern: "^.*\\b([A-Z]+)\\b.*$",
|
|
5171
5712
|
validLabels: state.actionOptions.map((o) => o.label),
|
|
5172
5713
|
requireExplanationBeforeApproval: true,
|
|
5173
|
-
requiredExplanationFields: [
|
|
5174
|
-
|
|
5714
|
+
requiredExplanationFields: [
|
|
5715
|
+
"actionOptions[].label",
|
|
5716
|
+
"actionOptions[].detail",
|
|
5717
|
+
"actionOptions[].approvalPrompt"
|
|
5718
|
+
],
|
|
5719
|
+
recommendation: "Before asking for approval, present each label with exact CLI detail first (`A: <detail>`). Do not paraphrase command options. User replies should include the label token (e.g. `A`, `A OK`, `A proceed`, `A \uC9C4\uD589\uD574`). For command execution, require one-time `approvalTicket` only when the selected action has `requiresUserCheck=true`.",
|
|
5175
5720
|
oneApprovalPerAction: true,
|
|
5176
5721
|
requireFreshContext: true,
|
|
5177
5722
|
contextVersion: state.contextVersion,
|
|
5178
5723
|
config: config.approval ?? { mode: "builtin" }
|
|
5179
5724
|
},
|
|
5180
5725
|
approvalRequest: {
|
|
5181
|
-
guidance: "Present each label with
|
|
5726
|
+
guidance: "Present each label with exact CLI detail (e.g. `A: <detail>`). For command options, include the raw `cmd` unchanged, then ask for `<LABEL>` or `<LABEL> OK`.",
|
|
5182
5727
|
finalPrompt: finalApprovalPrompt,
|
|
5183
5728
|
labels: state.actionOptions.map((o) => o.label),
|
|
5184
5729
|
approveCommand,
|
|
5185
5730
|
executeCommand,
|
|
5731
|
+
executeRequiresTicket: !!state.actionOptions[0]?.action?.requiresUserCheck,
|
|
5186
5732
|
options: state.actionOptions.map((o) => ({
|
|
5187
5733
|
label: o.label,
|
|
5188
5734
|
summary: o.summary,
|
|
5735
|
+
detail: o.detail,
|
|
5189
5736
|
approvalPrompt: o.approvalPrompt,
|
|
5737
|
+
actionType: o.action.type,
|
|
5738
|
+
scope: o.action.type === "command" ? o.action.scope : void 0,
|
|
5739
|
+
cwd: o.action.type === "command" ? o.action.cwd : void 0,
|
|
5740
|
+
cmd: o.action.type === "command" ? o.action.cmd : void 0,
|
|
5741
|
+
message: o.action.type === "instruction" ? o.action.message : void 0,
|
|
5190
5742
|
requiresUserCheck: !!o.action.requiresUserCheck,
|
|
5743
|
+
executeRequiresTicket: !!o.action.requiresUserCheck,
|
|
5191
5744
|
operationType: o.action.operationType
|
|
5192
5745
|
}))
|
|
5193
5746
|
},
|
|
@@ -5448,6 +6001,12 @@ async function runContext(featureName, options) {
|
|
|
5448
6001
|
}
|
|
5449
6002
|
if (actionOptions.length > 0) {
|
|
5450
6003
|
const finalApprovalPrompt = buildFinalApprovalPrompt(lang, actionOptions);
|
|
6004
|
+
const approveCommand = buildApprovalCommand(
|
|
6005
|
+
state,
|
|
6006
|
+
featureName,
|
|
6007
|
+
selectedComponent,
|
|
6008
|
+
false
|
|
6009
|
+
);
|
|
5451
6010
|
const executeCommand = buildApprovalCommand(
|
|
5452
6011
|
state,
|
|
5453
6012
|
featureName,
|
|
@@ -5458,6 +6017,13 @@ async function runContext(featureName, options) {
|
|
|
5458
6017
|
console.log(
|
|
5459
6018
|
chalk6.gray(
|
|
5460
6019
|
` \u21B3 ${tr(lang, "cli", "context.finalLabelCommandHint", {
|
|
6020
|
+
command: approveCommand
|
|
6021
|
+
})}`
|
|
6022
|
+
)
|
|
6023
|
+
);
|
|
6024
|
+
console.log(
|
|
6025
|
+
chalk6.gray(
|
|
6026
|
+
` \u21B3 ${tr(lang, "cli", "context.finalTicketCommandHint", {
|
|
5461
6027
|
command: executeCommand
|
|
5462
6028
|
})}`
|
|
5463
6029
|
)
|
|
@@ -5467,13 +6033,8 @@ async function runContext(featureName, options) {
|
|
|
5467
6033
|
}
|
|
5468
6034
|
async function runApprovedOption(state, config, lang, featureName, selectionOptions, options) {
|
|
5469
6035
|
const approval = options.approve || "";
|
|
5470
|
-
const
|
|
5471
|
-
|
|
5472
|
-
throw createCliError(
|
|
5473
|
-
"INVALID_APPROVAL",
|
|
5474
|
-
"Invalid approval reply. Use `<label>` or `<label> OK` (e.g. `A`, `A OK`)."
|
|
5475
|
-
);
|
|
5476
|
-
}
|
|
6036
|
+
const ticketToken = (options.ticket || "").trim();
|
|
6037
|
+
let parsedLabel = null;
|
|
5477
6038
|
if (state.status !== "single_matched" || !state.matchedFeature) {
|
|
5478
6039
|
throw createCliError(
|
|
5479
6040
|
"CONTEXT_SELECTION_REQUIRED",
|
|
@@ -5483,6 +6044,16 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
|
|
|
5483
6044
|
if (state.actionOptions.length === 0) {
|
|
5484
6045
|
throw createCliError("NO_ACTION_OPTIONS", "No action options to approve.");
|
|
5485
6046
|
}
|
|
6047
|
+
parsedLabel = parseApprovalLabel(
|
|
6048
|
+
approval,
|
|
6049
|
+
state.actionOptions.map((o) => o.label)
|
|
6050
|
+
);
|
|
6051
|
+
if (!parsedLabel) {
|
|
6052
|
+
throw createCliError(
|
|
6053
|
+
"INVALID_APPROVAL",
|
|
6054
|
+
"Invalid approval reply. Include a valid label token (e.g. `A`, `A OK`, `A proceed`, `A \uC9C4\uD589\uD574`)."
|
|
6055
|
+
);
|
|
6056
|
+
}
|
|
5486
6057
|
const selected = state.actionOptions.find((o) => o.label === parsedLabel);
|
|
5487
6058
|
if (!selected) {
|
|
5488
6059
|
throw createCliError(
|
|
@@ -5506,8 +6077,23 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
|
|
|
5506
6077
|
`Approved label "${parsedLabel}" is no longer available. Run \`context\` again.`
|
|
5507
6078
|
);
|
|
5508
6079
|
}
|
|
6080
|
+
if (!freshState.matchedFeature || !freshState.contextVersion) {
|
|
6081
|
+
throw createCliError(
|
|
6082
|
+
"CONTEXT_STALE",
|
|
6083
|
+
"Context changed since approval was requested. Run `context` again and re-approve."
|
|
6084
|
+
);
|
|
6085
|
+
}
|
|
5509
6086
|
const selectedAction = freshSelected.action;
|
|
6087
|
+
const executeRequiresTicket = !!selectedAction.requiresUserCheck;
|
|
6088
|
+
const actionHash = toActionHash(freshSelected);
|
|
6089
|
+
const featureRef = freshState.matchedFeature.folderName;
|
|
5510
6090
|
if (!options.execute) {
|
|
6091
|
+
const ticket = executeRequiresTicket ? await issueApprovalTicket(config, {
|
|
6092
|
+
contextVersion: freshState.contextVersion,
|
|
6093
|
+
actionHash,
|
|
6094
|
+
label: parsedLabel,
|
|
6095
|
+
featureRef
|
|
6096
|
+
}) : null;
|
|
5511
6097
|
if (options.json) {
|
|
5512
6098
|
console.log(
|
|
5513
6099
|
JSON.stringify(
|
|
@@ -5519,7 +6105,17 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
|
|
|
5519
6105
|
action: selectedAction,
|
|
5520
6106
|
contextVersion: freshState.contextVersion,
|
|
5521
6107
|
executable: selectedAction.type === "command",
|
|
5522
|
-
|
|
6108
|
+
executeRequiresTicket,
|
|
6109
|
+
oneApprovalPerAction: true,
|
|
6110
|
+
approvalTicket: ticket ? {
|
|
6111
|
+
token: ticket.token,
|
|
6112
|
+
sessionId: ticket.sessionId,
|
|
6113
|
+
label: ticket.label,
|
|
6114
|
+
contextVersion: ticket.contextVersion,
|
|
6115
|
+
actionHash: ticket.actionHash,
|
|
6116
|
+
expiresAt: ticket.expiresAt,
|
|
6117
|
+
oneTime: true
|
|
6118
|
+
} : void 0
|
|
5523
6119
|
},
|
|
5524
6120
|
null,
|
|
5525
6121
|
2
|
|
@@ -5531,13 +6127,45 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
|
|
|
5531
6127
|
console.log(chalk6.green(`\u2705 Approved option: ${parsedLabel}`));
|
|
5532
6128
|
console.log(chalk6.gray(` - Action: ${formatActionSummary2(selectedAction)}`));
|
|
5533
6129
|
if (selectedAction.type === "command") {
|
|
5534
|
-
|
|
6130
|
+
const selectedComponent = selectionOptions.component || "";
|
|
6131
|
+
let executeCommand = buildApprovalCommand(
|
|
6132
|
+
freshState,
|
|
6133
|
+
featureName,
|
|
6134
|
+
selectedComponent,
|
|
6135
|
+
true
|
|
6136
|
+
).replace("<LABEL>", parsedLabel);
|
|
6137
|
+
if (ticket) {
|
|
6138
|
+
executeCommand = executeCommand.replace(
|
|
6139
|
+
"[--ticket <TICKET>]",
|
|
6140
|
+
`--ticket ${ticket.token}`
|
|
6141
|
+
);
|
|
6142
|
+
console.log(chalk6.gray(` - Ticket: ${ticket.token} (expires: ${ticket.expiresAt})`));
|
|
6143
|
+
} else {
|
|
6144
|
+
executeCommand = executeCommand.replace(" [--ticket <TICKET>]", "");
|
|
6145
|
+
}
|
|
6146
|
+
console.log(chalk6.gray(` - Run with: ${executeCommand}`));
|
|
5535
6147
|
} else {
|
|
5536
6148
|
console.log(chalk6.gray(" - Instruction-only action (no command execution)."));
|
|
5537
6149
|
}
|
|
5538
6150
|
console.log();
|
|
5539
6151
|
return;
|
|
5540
6152
|
}
|
|
6153
|
+
if (!ticketToken) {
|
|
6154
|
+
if (executeRequiresTicket) {
|
|
6155
|
+
throw createCliError(
|
|
6156
|
+
"APPROVAL_REQUIRED",
|
|
6157
|
+
"`--execute` requires `--ticket <token>` for check-required options. Run `context --approve <reply>` first."
|
|
6158
|
+
);
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
if (executeRequiresTicket) {
|
|
6162
|
+
await consumeApprovalTicket(config, ticketToken, {
|
|
6163
|
+
contextVersion: freshState.contextVersion,
|
|
6164
|
+
actionHash,
|
|
6165
|
+
label: parsedLabel,
|
|
6166
|
+
featureRef
|
|
6167
|
+
});
|
|
6168
|
+
}
|
|
5541
6169
|
if (selectedAction.type !== "command") {
|
|
5542
6170
|
if (options.executeStrict) {
|
|
5543
6171
|
throw createCliError(
|
|
@@ -5790,8 +6418,8 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
5790
6418
|
];
|
|
5791
6419
|
for (const file of files) {
|
|
5792
6420
|
const fullPath = path18.join(f.path, file);
|
|
5793
|
-
if (!await
|
|
5794
|
-
const original = await
|
|
6421
|
+
if (!await fs15.pathExists(fullPath)) continue;
|
|
6422
|
+
const original = await fs15.readFile(fullPath, "utf-8");
|
|
5795
6423
|
let next = original;
|
|
5796
6424
|
const changes = [];
|
|
5797
6425
|
const placeholderFix = applyPlaceholderFixes(next, placeholderContext, config.lang);
|
|
@@ -5815,7 +6443,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
5815
6443
|
}
|
|
5816
6444
|
if (next === original) continue;
|
|
5817
6445
|
if (!dryRun) {
|
|
5818
|
-
await
|
|
6446
|
+
await fs15.writeFile(fullPath, next, "utf-8");
|
|
5819
6447
|
}
|
|
5820
6448
|
entries.push({
|
|
5821
6449
|
path: formatPath(cwd, fullPath),
|
|
@@ -5835,7 +6463,7 @@ async function checkDocsStructure(config, cwd) {
|
|
|
5835
6463
|
const requiredDirs = ["agents", "features", "prd", "designs", "ideas"];
|
|
5836
6464
|
for (const dir of requiredDirs) {
|
|
5837
6465
|
const p = path18.join(config.docsDir, dir);
|
|
5838
|
-
if (!await
|
|
6466
|
+
if (!await fs15.pathExists(p)) {
|
|
5839
6467
|
issues.push({
|
|
5840
6468
|
level: "error",
|
|
5841
6469
|
code: "missing_dir",
|
|
@@ -5845,7 +6473,7 @@ async function checkDocsStructure(config, cwd) {
|
|
|
5845
6473
|
}
|
|
5846
6474
|
}
|
|
5847
6475
|
const configPath = path18.join(config.docsDir, ".lee-spec-kit.json");
|
|
5848
|
-
if (!await
|
|
6476
|
+
if (!await fs15.pathExists(configPath)) {
|
|
5849
6477
|
issues.push({
|
|
5850
6478
|
level: "warn",
|
|
5851
6479
|
code: "missing_config",
|
|
@@ -5876,8 +6504,8 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5876
6504
|
const featureDocs = ["spec.md", "plan.md", "tasks.md"];
|
|
5877
6505
|
for (const file of featureDocs) {
|
|
5878
6506
|
const p = path18.join(f.path, file);
|
|
5879
|
-
if (!await
|
|
5880
|
-
const content = await
|
|
6507
|
+
if (!await fs15.pathExists(p)) continue;
|
|
6508
|
+
const content = await fs15.readFile(p, "utf-8");
|
|
5881
6509
|
const placeholders = detectPlaceholders(content);
|
|
5882
6510
|
if (placeholders.length === 0) continue;
|
|
5883
6511
|
issues.push({
|
|
@@ -5891,8 +6519,8 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5891
6519
|
}
|
|
5892
6520
|
if (decisionsPlaceholderMode !== "off") {
|
|
5893
6521
|
const decisionsPath = path18.join(f.path, "decisions.md");
|
|
5894
|
-
if (await
|
|
5895
|
-
const content = await
|
|
6522
|
+
if (await fs15.pathExists(decisionsPath)) {
|
|
6523
|
+
const content = await fs15.readFile(decisionsPath, "utf-8");
|
|
5896
6524
|
const placeholders = detectPlaceholders(content);
|
|
5897
6525
|
if (placeholders.length > 0) {
|
|
5898
6526
|
issues.push({
|
|
@@ -6198,17 +6826,11 @@ function doctorCommand(program2) {
|
|
|
6198
6826
|
});
|
|
6199
6827
|
}
|
|
6200
6828
|
function resolveComponentOption2(options) {
|
|
6201
|
-
|
|
6202
|
-
throw createCliError(
|
|
6203
|
-
"INVALID_ARGUMENT",
|
|
6204
|
-
"`--repo` and `--component` must reference the same value when both are provided."
|
|
6205
|
-
);
|
|
6206
|
-
}
|
|
6207
|
-
const component = (options.component || options.repo || "").trim().toLowerCase();
|
|
6829
|
+
const component = (options.component || "").trim().toLowerCase();
|
|
6208
6830
|
return component || void 0;
|
|
6209
6831
|
}
|
|
6210
6832
|
function viewCommand(program2) {
|
|
6211
|
-
program2.command("view [feature-name]").description("Show workflow dashboard for features").option("--json", "Output in JSON format for agents").option("--
|
|
6833
|
+
program2.command("view [feature-name]").description("Show workflow dashboard for features").option("--json", "Output in JSON format for agents").option("--component <component>", "Component name for multi projects").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").action(async (featureName, options) => {
|
|
6212
6834
|
try {
|
|
6213
6835
|
await runView(featureName, options);
|
|
6214
6836
|
} catch (error) {
|
|
@@ -6348,13 +6970,7 @@ async function runView(featureName, options) {
|
|
|
6348
6970
|
console.log();
|
|
6349
6971
|
}
|
|
6350
6972
|
function resolveComponentOption3(options) {
|
|
6351
|
-
|
|
6352
|
-
throw createCliError(
|
|
6353
|
-
"INVALID_ARGUMENT",
|
|
6354
|
-
"`--repo` and `--component` must reference the same value when both are provided."
|
|
6355
|
-
);
|
|
6356
|
-
}
|
|
6357
|
-
const component = (options.component || options.repo || "").trim().toLowerCase();
|
|
6973
|
+
const component = (options.component || "").trim().toLowerCase();
|
|
6358
6974
|
return component || void 0;
|
|
6359
6975
|
}
|
|
6360
6976
|
function runSelfCli(args) {
|
|
@@ -6397,14 +7013,17 @@ function runSelfCliJson(args, allowFailure = false) {
|
|
|
6397
7013
|
function buildSelectionArgs(featureName, options) {
|
|
6398
7014
|
const args = [];
|
|
6399
7015
|
if (featureName) args.push(featureName);
|
|
6400
|
-
const component = (options.component ||
|
|
7016
|
+
const component = (options.component || "").trim();
|
|
6401
7017
|
if (component) args.push("--component", component);
|
|
6402
7018
|
if (options.all) args.push("--all");
|
|
6403
7019
|
if (options.done) args.push("--done");
|
|
6404
7020
|
return args;
|
|
6405
7021
|
}
|
|
6406
7022
|
function flowCommand(program2) {
|
|
6407
|
-
program2.command("flow [feature-name]").description("Run combined workflow checks (context + status + doctor)").option("--json", "Output in JSON format for agents").option("--
|
|
7023
|
+
program2.command("flow [feature-name]").description("Run combined workflow checks (context + status + doctor)").option("--json", "Output in JSON format for agents").option("--component <component>", "Component name for multi projects").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").option(
|
|
7024
|
+
"--approve <reply>",
|
|
7025
|
+
"Approve one labeled context option (examples: A, A OK, A proceed, A \uC9C4\uD589\uD574)"
|
|
7026
|
+
).option("--execute", "Execute approved option when it is a command").option(
|
|
6408
7027
|
"--execute-strict",
|
|
6409
7028
|
"With --execute, fail if approved option is instruction-only"
|
|
6410
7029
|
).option("--strict", "Also run status --strict and doctor --strict").action(async (featureName, options) => {
|
|
@@ -6449,6 +7068,12 @@ async function runFlow(featureName, options) {
|
|
|
6449
7068
|
"`--execute-strict` requires `--execute`."
|
|
6450
7069
|
);
|
|
6451
7070
|
}
|
|
7071
|
+
if (options.execute && !options.approve) {
|
|
7072
|
+
throw createCliError(
|
|
7073
|
+
"APPROVAL_REQUIRED",
|
|
7074
|
+
"`--execute` requires `--approve <reply>`."
|
|
7075
|
+
);
|
|
7076
|
+
}
|
|
6452
7077
|
const selectedComponent = resolveComponentOption3(options);
|
|
6453
7078
|
const selectionOptions = {
|
|
6454
7079
|
component: selectedComponent,
|
|
@@ -6461,9 +7086,29 @@ async function runFlow(featureName, options) {
|
|
|
6461
7086
|
let approvalResult = null;
|
|
6462
7087
|
if (options.approve) {
|
|
6463
7088
|
const approveArgs = [...contextArgs, "--approve", options.approve];
|
|
6464
|
-
|
|
6465
|
-
if (options.
|
|
6466
|
-
|
|
7089
|
+
const selected = runSelfCliJson(approveArgs, true);
|
|
7090
|
+
if (options.execute) {
|
|
7091
|
+
const selectedPayload = selected;
|
|
7092
|
+
const executeRequiresTicket = selectedPayload?.executeRequiresTicket === true;
|
|
7093
|
+
const ticket = selectedPayload?.approvalTicket?.token;
|
|
7094
|
+
if (selectedPayload?.status === "approved_selected") {
|
|
7095
|
+
const executeArgs = [
|
|
7096
|
+
...contextArgs,
|
|
7097
|
+
"--approve",
|
|
7098
|
+
options.approve,
|
|
7099
|
+
"--execute"
|
|
7100
|
+
];
|
|
7101
|
+
if (executeRequiresTicket && ticket) {
|
|
7102
|
+
executeArgs.push("--ticket", ticket);
|
|
7103
|
+
}
|
|
7104
|
+
if (options.executeStrict) executeArgs.push("--execute-strict");
|
|
7105
|
+
approvalResult = runSelfCliJson(executeArgs, true);
|
|
7106
|
+
} else {
|
|
7107
|
+
approvalResult = selected;
|
|
7108
|
+
}
|
|
7109
|
+
} else {
|
|
7110
|
+
approvalResult = selected;
|
|
7111
|
+
}
|
|
6467
7112
|
}
|
|
6468
7113
|
const after = await resolveContextSelection(config, featureName, selectionOptions);
|
|
6469
7114
|
const statusReport = runSelfCliJson(["status"]);
|
|
@@ -6645,21 +7290,21 @@ function detectGithubCliLangSync(cwd) {
|
|
|
6645
7290
|
for (const base of scanOrder) {
|
|
6646
7291
|
for (const docsDir of [path18.join(base, "docs"), base]) {
|
|
6647
7292
|
const configPath = path18.join(docsDir, ".lee-spec-kit.json");
|
|
6648
|
-
if (
|
|
7293
|
+
if (fs15.existsSync(configPath)) {
|
|
6649
7294
|
try {
|
|
6650
|
-
const parsed =
|
|
7295
|
+
const parsed = fs15.readJsonSync(configPath);
|
|
6651
7296
|
if (parsed?.lang === "ko" || parsed?.lang === "en") return parsed.lang;
|
|
6652
7297
|
} catch {
|
|
6653
7298
|
}
|
|
6654
7299
|
}
|
|
6655
7300
|
const agentsPath = path18.join(docsDir, "agents");
|
|
6656
7301
|
const featuresPath = path18.join(docsDir, "features");
|
|
6657
|
-
if (!
|
|
7302
|
+
if (!fs15.existsSync(agentsPath) || !fs15.existsSync(featuresPath)) continue;
|
|
6658
7303
|
for (const probe of ["custom.md", "constitution.md", "agents.md"]) {
|
|
6659
7304
|
const file = path18.join(agentsPath, probe);
|
|
6660
|
-
if (!
|
|
7305
|
+
if (!fs15.existsSync(file)) continue;
|
|
6661
7306
|
try {
|
|
6662
|
-
const content =
|
|
7307
|
+
const content = fs15.readFileSync(file, "utf-8");
|
|
6663
7308
|
if (/[가-힣]/.test(content)) return "ko";
|
|
6664
7309
|
} catch {
|
|
6665
7310
|
}
|
|
@@ -6669,14 +7314,8 @@ function detectGithubCliLangSync(cwd) {
|
|
|
6669
7314
|
}
|
|
6670
7315
|
return DEFAULT_LANG;
|
|
6671
7316
|
}
|
|
6672
|
-
function resolveComponentOption4(options
|
|
6673
|
-
|
|
6674
|
-
throw createCliError(
|
|
6675
|
-
"INVALID_ARGUMENT",
|
|
6676
|
-
tg(lang, "invalidRepoComponentMismatch")
|
|
6677
|
-
);
|
|
6678
|
-
}
|
|
6679
|
-
const component = (options.component || options.repo || "").trim().toLowerCase();
|
|
7317
|
+
function resolveComponentOption4(options) {
|
|
7318
|
+
const component = (options.component || "").trim().toLowerCase();
|
|
6680
7319
|
return component || void 0;
|
|
6681
7320
|
}
|
|
6682
7321
|
function parseLabels(raw, lang) {
|
|
@@ -6760,7 +7399,7 @@ function ensureSections(body, sections, kind, lang) {
|
|
|
6760
7399
|
}
|
|
6761
7400
|
function ensureDocsExist(docsDir, relativePaths, lang) {
|
|
6762
7401
|
const missing = relativePaths.filter(
|
|
6763
|
-
(relativePath) => !
|
|
7402
|
+
(relativePath) => !fs15.existsSync(path18.join(docsDir, relativePath))
|
|
6764
7403
|
);
|
|
6765
7404
|
if (missing.length > 0) {
|
|
6766
7405
|
throw createCliError(
|
|
@@ -6853,6 +7492,22 @@ async function resolveFeatureOrThrow(featureName, options, lang) {
|
|
|
6853
7492
|
}
|
|
6854
7493
|
return { config, feature: state.matchedFeature };
|
|
6855
7494
|
}
|
|
7495
|
+
function resolveGithubProjectCwd(config, feature) {
|
|
7496
|
+
const projectGitCwd = (feature.git.projectGitCwd || "").trim();
|
|
7497
|
+
if (projectGitCwd) return projectGitCwd;
|
|
7498
|
+
if (config.docsRepo === "standalone") {
|
|
7499
|
+
throw createCliError(
|
|
7500
|
+
"PRECONDITION_FAILED",
|
|
7501
|
+
tr(config.lang, "messages", "standaloneNeedsProjectRoot")
|
|
7502
|
+
);
|
|
7503
|
+
}
|
|
7504
|
+
return process.cwd();
|
|
7505
|
+
}
|
|
7506
|
+
function resolveGithubDocsCwd(config, feature) {
|
|
7507
|
+
const docsGitCwd = (feature.git.docsGitCwd || "").trim();
|
|
7508
|
+
if (docsGitCwd) return docsGitCwd;
|
|
7509
|
+
return config.docsDir;
|
|
7510
|
+
}
|
|
6856
7511
|
function getFeatureDocPaths(feature) {
|
|
6857
7512
|
const featurePathFromDocs = feature.docs.featurePathFromDocs;
|
|
6858
7513
|
return {
|
|
@@ -6935,7 +7590,7 @@ function uniqItems(items) {
|
|
|
6935
7590
|
return ordered;
|
|
6936
7591
|
}
|
|
6937
7592
|
function normalizeSemanticKey(value) {
|
|
6938
|
-
return value.toLowerCase().replace(/[
|
|
7593
|
+
return value.toLowerCase().replace(/[`\]\u005B'"(){}.,:;!?/\\_|-]/g, " ").replace(/\s+/g, " ").trim().replace(/\s/g, "");
|
|
6939
7594
|
}
|
|
6940
7595
|
function uniqItemsByContainment(items) {
|
|
6941
7596
|
const kept = [];
|
|
@@ -7503,10 +8158,10 @@ function insertFieldInGithubIssueSection(content, key, value) {
|
|
|
7503
8158
|
return { content: lines.join("\n"), changed: true };
|
|
7504
8159
|
}
|
|
7505
8160
|
function syncTasksPrMetadata(tasksPath, prUrl, nextStatus, lang) {
|
|
7506
|
-
if (!
|
|
8161
|
+
if (!fs15.existsSync(tasksPath)) {
|
|
7507
8162
|
throw createCliError("DOCS_NOT_FOUND", tg(lang, "tasksNotFound", { path: tasksPath }));
|
|
7508
8163
|
}
|
|
7509
|
-
const original =
|
|
8164
|
+
const original = fs15.readFileSync(tasksPath, "utf-8");
|
|
7510
8165
|
let next = original;
|
|
7511
8166
|
let changed = false;
|
|
7512
8167
|
const prReplaced = replaceListField(next, ["PR", "Pull Request"], prUrl);
|
|
@@ -7530,7 +8185,7 @@ function syncTasksPrMetadata(tasksPath, prUrl, nextStatus, lang) {
|
|
|
7530
8185
|
changed = changed || inserted.changed;
|
|
7531
8186
|
}
|
|
7532
8187
|
if (changed) {
|
|
7533
|
-
|
|
8188
|
+
fs15.writeFileSync(tasksPath, next, "utf-8");
|
|
7534
8189
|
}
|
|
7535
8190
|
return { changed, path: tasksPath };
|
|
7536
8191
|
}
|
|
@@ -7674,12 +8329,12 @@ function toRetryCount(raw, lang) {
|
|
|
7674
8329
|
function githubCommand(program2) {
|
|
7675
8330
|
const commandLang = detectGithubCliLangSync(process.cwd());
|
|
7676
8331
|
const github = program2.command("github").description(tg(commandLang, "cmdGithubDescription"));
|
|
7677
|
-
github.command("issue [feature-name]").description(tg(commandLang, "cmdIssueDescription")).option("--json", tg(commandLang, "optJson")).option("--
|
|
8332
|
+
github.command("issue [feature-name]").description(tg(commandLang, "cmdIssueDescription")).option("--json", tg(commandLang, "optJson")).option("--component <component>", tg(commandLang, "optComponent")).option("--title <title>", tg(commandLang, "optIssueTitle")).option("--labels <labels>", tg(commandLang, "optLabels")).option("--body-file <path>", tg(commandLang, "optIssueBodyFile")).option("--assignee <assignee>", tg(commandLang, "optIssueAssignee")).option("--create", tg(commandLang, "optIssueCreate")).option(
|
|
7678
8333
|
"--confirm <reply>",
|
|
7679
8334
|
tg(commandLang, "optIssueConfirm")
|
|
7680
8335
|
).action(async (featureName, options) => {
|
|
7681
8336
|
try {
|
|
7682
|
-
const selectedComponent = resolveComponentOption4(options
|
|
8337
|
+
const selectedComponent = resolveComponentOption4(options);
|
|
7683
8338
|
const { config, feature } = await resolveFeatureOrThrow(featureName, {
|
|
7684
8339
|
component: selectedComponent
|
|
7685
8340
|
}, commandLang);
|
|
@@ -7690,9 +8345,9 @@ function githubCommand(program2) {
|
|
|
7690
8345
|
[paths.specPath, paths.planPath, paths.tasksPath],
|
|
7691
8346
|
config.lang
|
|
7692
8347
|
);
|
|
7693
|
-
const specContent = await
|
|
7694
|
-
const planContent = await
|
|
7695
|
-
const tasksContent = await
|
|
8348
|
+
const specContent = await fs15.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
|
|
8349
|
+
const planContent = await fs15.readFile(path18.join(config.docsDir, paths.planPath), "utf-8");
|
|
8350
|
+
const tasksContent = await fs15.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
|
|
7696
8351
|
const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
|
|
7697
8352
|
const title = options.title?.trim() || tg(config.lang, "issueDefaultTitle", {
|
|
7698
8353
|
slug: feature.slug,
|
|
@@ -7721,8 +8376,8 @@ function githubCommand(program2) {
|
|
|
7721
8376
|
);
|
|
7722
8377
|
const explicitBodyFile = (options.bodyFile || "").trim();
|
|
7723
8378
|
let body = generatedBody;
|
|
7724
|
-
if (options.create && explicitBodyFile && await
|
|
7725
|
-
body = await
|
|
8379
|
+
if (options.create && explicitBodyFile && await fs15.pathExists(bodyFile)) {
|
|
8380
|
+
body = await fs15.readFile(bodyFile, "utf-8");
|
|
7726
8381
|
ensureSections(
|
|
7727
8382
|
body,
|
|
7728
8383
|
getRequiredIssueSections(config.lang),
|
|
@@ -7730,11 +8385,12 @@ function githubCommand(program2) {
|
|
|
7730
8385
|
config.lang
|
|
7731
8386
|
);
|
|
7732
8387
|
} else {
|
|
7733
|
-
await
|
|
7734
|
-
await
|
|
8388
|
+
await fs15.ensureDir(path18.dirname(bodyFile));
|
|
8389
|
+
await fs15.writeFile(bodyFile, generatedBody, "utf-8");
|
|
7735
8390
|
}
|
|
7736
8391
|
let issueUrl;
|
|
7737
8392
|
if (options.create) {
|
|
8393
|
+
const projectGitCwd = resolveGithubProjectCwd(config, feature);
|
|
7738
8394
|
ensureNoTodoPlaceholders(body, tg(config.lang, "kindIssue"), config.lang);
|
|
7739
8395
|
assertRemoteApproval(
|
|
7740
8396
|
options.confirm,
|
|
@@ -7757,7 +8413,7 @@ function githubCommand(program2) {
|
|
|
7757
8413
|
const created = runProcessOrThrow(
|
|
7758
8414
|
"gh",
|
|
7759
8415
|
args,
|
|
7760
|
-
|
|
8416
|
+
projectGitCwd,
|
|
7761
8417
|
tg(config.lang, "createIssueFailed")
|
|
7762
8418
|
);
|
|
7763
8419
|
issueUrl = created.stdout.trim() || void 0;
|
|
@@ -7816,22 +8472,22 @@ function githubCommand(program2) {
|
|
|
7816
8472
|
process.exit(1);
|
|
7817
8473
|
}
|
|
7818
8474
|
});
|
|
7819
|
-
github.command("pr [feature-name]").description(tg(commandLang, "cmdPrDescription")).option("--json", tg(commandLang, "optJson")).option("--
|
|
8475
|
+
github.command("pr [feature-name]").description(tg(commandLang, "cmdPrDescription")).option("--json", tg(commandLang, "optJson")).option("--component <component>", tg(commandLang, "optComponent")).option("--title <title>", tg(commandLang, "optPrTitle")).option("--labels <labels>", tg(commandLang, "optLabels")).option("--body-file <path>", tg(commandLang, "optPrBodyFile")).option("--assignee <assignee>", tg(commandLang, "optPrAssignee")).option("--base <branch>", tg(commandLang, "optPrBase"), "main").option("--create", tg(commandLang, "optPrCreate")).option("--pr <ref>", tg(commandLang, "optPrRef")).option("--merge", tg(commandLang, "optPrMerge")).option(
|
|
7820
8476
|
"--confirm <reply>",
|
|
7821
8477
|
tg(commandLang, "optPrConfirm")
|
|
7822
8478
|
).option("--retry <count>", tg(commandLang, "optPrRetry")).option("--screenshots <mode>", tg(commandLang, "optPrScreenshots"), "auto").option("--mermaid <mode>", tg(commandLang, "optPrMermaid"), "auto").option("--no-sync-tasks", tg(commandLang, "optPrNoSyncTasks")).option("--commit-sync", tg(commandLang, "optPrCommitSync")).action(async (featureName, options) => {
|
|
7823
8479
|
try {
|
|
7824
|
-
const selectedComponent = resolveComponentOption4(options
|
|
8480
|
+
const selectedComponent = resolveComponentOption4(options);
|
|
7825
8481
|
const { config, feature } = await resolveFeatureOrThrow(featureName, {
|
|
7826
8482
|
component: selectedComponent
|
|
7827
8483
|
}, commandLang);
|
|
7828
8484
|
const labels = parseLabels(options.labels, config.lang);
|
|
7829
8485
|
const paths = getFeatureDocPaths(feature);
|
|
7830
8486
|
ensureDocsExist(config.docsDir, [paths.specPath, paths.tasksPath], config.lang);
|
|
7831
|
-
const specContent = await
|
|
8487
|
+
const specContent = await fs15.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
|
|
7832
8488
|
const planPath = path18.join(config.docsDir, paths.planPath);
|
|
7833
|
-
const planContent = await
|
|
7834
|
-
const tasksContent = await
|
|
8489
|
+
const planContent = await fs15.pathExists(planPath) ? await fs15.readFile(planPath, "utf-8") : "";
|
|
8490
|
+
const tasksContent = await fs15.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
|
|
7835
8491
|
const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
|
|
7836
8492
|
const defaultTitle = feature.issueNumber ? tg(config.lang, "prDefaultTitleWithIssue", {
|
|
7837
8493
|
issue: feature.issueNumber,
|
|
@@ -7865,8 +8521,8 @@ function githubCommand(program2) {
|
|
|
7865
8521
|
);
|
|
7866
8522
|
const explicitBodyFile = (options.bodyFile || "").trim();
|
|
7867
8523
|
let body = generatedBody;
|
|
7868
|
-
if (options.create && explicitBodyFile && await
|
|
7869
|
-
body = await
|
|
8524
|
+
if (options.create && explicitBodyFile && await fs15.pathExists(bodyFile)) {
|
|
8525
|
+
body = await fs15.readFile(bodyFile, "utf-8");
|
|
7870
8526
|
ensureSections(
|
|
7871
8527
|
body,
|
|
7872
8528
|
getRequiredPrSections(config.lang),
|
|
@@ -7874,14 +8530,15 @@ function githubCommand(program2) {
|
|
|
7874
8530
|
config.lang
|
|
7875
8531
|
);
|
|
7876
8532
|
} else {
|
|
7877
|
-
await
|
|
7878
|
-
await
|
|
8533
|
+
await fs15.ensureDir(path18.dirname(bodyFile));
|
|
8534
|
+
await fs15.writeFile(bodyFile, generatedBody, "utf-8");
|
|
7879
8535
|
}
|
|
7880
8536
|
const retryCount = toRetryCount(options.retry, config.lang);
|
|
7881
8537
|
let prUrl = options.pr?.trim() || "";
|
|
7882
8538
|
let mergedAttempts;
|
|
7883
8539
|
let syncChanged = false;
|
|
7884
8540
|
if (options.create) {
|
|
8541
|
+
const projectGitCwd = resolveGithubProjectCwd(config, feature);
|
|
7885
8542
|
ensureNoTodoPlaceholders(body, tg(config.lang, "kindPr"), config.lang);
|
|
7886
8543
|
ensurePrArtifacts(body, artifactPolicy, config.lang);
|
|
7887
8544
|
assertRemoteApproval(
|
|
@@ -7907,7 +8564,7 @@ function githubCommand(program2) {
|
|
|
7907
8564
|
const created = runProcessOrThrow(
|
|
7908
8565
|
"gh",
|
|
7909
8566
|
args,
|
|
7910
|
-
|
|
8567
|
+
projectGitCwd,
|
|
7911
8568
|
tg(config.lang, "createPrFailed")
|
|
7912
8569
|
);
|
|
7913
8570
|
prUrl = created.stdout.trim();
|
|
@@ -7935,6 +8592,7 @@ function githubCommand(program2) {
|
|
|
7935
8592
|
syncChanged = synced.changed;
|
|
7936
8593
|
const shouldCommitSync = !!options.commitSync || !!options.merge;
|
|
7937
8594
|
if (syncChanged && shouldCommitSync) {
|
|
8595
|
+
const docsGitCwd = resolveGithubDocsCwd(config, feature);
|
|
7938
8596
|
const message = feature.issueNumber ? tg(config.lang, "syncCommitWithIssue", {
|
|
7939
8597
|
issue: feature.issueNumber,
|
|
7940
8598
|
folder: feature.folderName
|
|
@@ -7942,7 +8600,7 @@ function githubCommand(program2) {
|
|
|
7942
8600
|
folder: feature.folderName
|
|
7943
8601
|
});
|
|
7944
8602
|
commitAndPushPath(
|
|
7945
|
-
|
|
8603
|
+
docsGitCwd,
|
|
7946
8604
|
synced.path,
|
|
7947
8605
|
message,
|
|
7948
8606
|
config.lang
|
|
@@ -7950,19 +8608,25 @@ function githubCommand(program2) {
|
|
|
7950
8608
|
}
|
|
7951
8609
|
}
|
|
7952
8610
|
if (options.merge) {
|
|
7953
|
-
const
|
|
8611
|
+
const projectGitCwd = resolveGithubProjectCwd(config, feature);
|
|
8612
|
+
const merged = mergePrWithRetry(
|
|
8613
|
+
prUrl,
|
|
8614
|
+
projectGitCwd,
|
|
8615
|
+
retryCount,
|
|
8616
|
+
config.lang
|
|
8617
|
+
);
|
|
7954
8618
|
mergedAttempts = merged.attempts;
|
|
7955
8619
|
const baseBranch = options.base || "main";
|
|
7956
8620
|
runProcessOrThrow(
|
|
7957
8621
|
"git",
|
|
7958
8622
|
["checkout", baseBranch],
|
|
7959
|
-
|
|
8623
|
+
projectGitCwd,
|
|
7960
8624
|
tg(config.lang, "checkoutBaseAfterMergeFailed", { base: baseBranch })
|
|
7961
8625
|
);
|
|
7962
8626
|
runProcessOrThrow(
|
|
7963
8627
|
"git",
|
|
7964
8628
|
["pull", "--rebase", "origin", baseBranch],
|
|
7965
|
-
|
|
8629
|
+
projectGitCwd,
|
|
7966
8630
|
tg(config.lang, "pullBaseAfterMergeFailed", { base: baseBranch })
|
|
7967
8631
|
);
|
|
7968
8632
|
}
|
|
@@ -8195,6 +8859,132 @@ function docsCommand(program2) {
|
|
|
8195
8859
|
}
|
|
8196
8860
|
});
|
|
8197
8861
|
}
|
|
8862
|
+
function detectCommand(program2) {
|
|
8863
|
+
program2.command("detect").description(tr(DEFAULT_LANG, "cli", "detect.cmdDescription")).option("--dir <dir>", tr(DEFAULT_LANG, "cli", "detect.optDir")).option("--json", tr(DEFAULT_LANG, "cli", "detect.optJson")).action(async (options) => {
|
|
8864
|
+
try {
|
|
8865
|
+
await runDetect(options);
|
|
8866
|
+
} catch (error) {
|
|
8867
|
+
const config = await getConfig(process.cwd());
|
|
8868
|
+
const lang = config?.lang ?? DEFAULT_LANG;
|
|
8869
|
+
const cliError = toCliError(error);
|
|
8870
|
+
const suggestions = getCliErrorSuggestions(cliError.code, lang);
|
|
8871
|
+
if (options.json) {
|
|
8872
|
+
console.log(
|
|
8873
|
+
JSON.stringify({
|
|
8874
|
+
status: "error",
|
|
8875
|
+
reasonCode: cliError.code,
|
|
8876
|
+
error: cliError.message,
|
|
8877
|
+
suggestions
|
|
8878
|
+
})
|
|
8879
|
+
);
|
|
8880
|
+
} else {
|
|
8881
|
+
console.error(
|
|
8882
|
+
chalk6.red(tr(lang, "cli", "common.errorLabel")),
|
|
8883
|
+
chalk6.red(`[${cliError.code}] ${cliError.message}`)
|
|
8884
|
+
);
|
|
8885
|
+
printCliErrorSuggestions(suggestions, lang);
|
|
8886
|
+
}
|
|
8887
|
+
process.exit(1);
|
|
8888
|
+
}
|
|
8889
|
+
});
|
|
8890
|
+
}
|
|
8891
|
+
async function runDetect(options) {
|
|
8892
|
+
const cwd = process.cwd();
|
|
8893
|
+
const targetCwd = options.dir ? path18.resolve(cwd, options.dir) : cwd;
|
|
8894
|
+
const config = await getConfig(targetCwd);
|
|
8895
|
+
const detected = !!config;
|
|
8896
|
+
const reasonCode = detected ? "PROJECT_DETECTED" : "PROJECT_NOT_DETECTED";
|
|
8897
|
+
if (options.json) {
|
|
8898
|
+
if (!config) {
|
|
8899
|
+
console.log(
|
|
8900
|
+
JSON.stringify(
|
|
8901
|
+
{
|
|
8902
|
+
status: "ok",
|
|
8903
|
+
reasonCode,
|
|
8904
|
+
isLeeSpecKitProject: false,
|
|
8905
|
+
targetCwd,
|
|
8906
|
+
docsDir: null,
|
|
8907
|
+
configPath: null,
|
|
8908
|
+
configFilePresent: false,
|
|
8909
|
+
detectionSource: null,
|
|
8910
|
+
projectType: null,
|
|
8911
|
+
lang: null,
|
|
8912
|
+
projectName: null
|
|
8913
|
+
},
|
|
8914
|
+
null,
|
|
8915
|
+
2
|
|
8916
|
+
)
|
|
8917
|
+
);
|
|
8918
|
+
return;
|
|
8919
|
+
}
|
|
8920
|
+
const configPath2 = path18.join(config.docsDir, ".lee-spec-kit.json");
|
|
8921
|
+
const configFilePresent2 = await fs15.pathExists(configPath2);
|
|
8922
|
+
const detectionSource2 = configFilePresent2 ? "config" : "heuristic";
|
|
8923
|
+
console.log(
|
|
8924
|
+
JSON.stringify(
|
|
8925
|
+
{
|
|
8926
|
+
status: "ok",
|
|
8927
|
+
reasonCode,
|
|
8928
|
+
isLeeSpecKitProject: true,
|
|
8929
|
+
targetCwd,
|
|
8930
|
+
docsDir: config.docsDir,
|
|
8931
|
+
configPath: configFilePresent2 ? configPath2 : null,
|
|
8932
|
+
configFilePresent: configFilePresent2,
|
|
8933
|
+
detectionSource: detectionSource2,
|
|
8934
|
+
projectType: config.projectType,
|
|
8935
|
+
lang: config.lang,
|
|
8936
|
+
projectName: config.projectName ?? null
|
|
8937
|
+
},
|
|
8938
|
+
null,
|
|
8939
|
+
2
|
|
8940
|
+
)
|
|
8941
|
+
);
|
|
8942
|
+
return;
|
|
8943
|
+
}
|
|
8944
|
+
const lang = config?.lang ?? DEFAULT_LANG;
|
|
8945
|
+
console.log();
|
|
8946
|
+
console.log(chalk6.blue(tr(lang, "cli", "detect.header")));
|
|
8947
|
+
console.log(chalk6.gray(`- ${tr(lang, "cli", "detect.labelTarget")}: ${targetCwd}`));
|
|
8948
|
+
if (!config) {
|
|
8949
|
+
console.log(chalk6.yellow(`- ${tr(lang, "cli", "detect.resultNotDetected")}`));
|
|
8950
|
+
console.log(chalk6.gray(`- ${tr(lang, "cli", "detect.notDetectedHint")}`));
|
|
8951
|
+
console.log();
|
|
8952
|
+
return;
|
|
8953
|
+
}
|
|
8954
|
+
const configPath = path18.join(config.docsDir, ".lee-spec-kit.json");
|
|
8955
|
+
const configFilePresent = await fs15.pathExists(configPath);
|
|
8956
|
+
const detectionSource = configFilePresent ? "config" : "heuristic";
|
|
8957
|
+
console.log(chalk6.green(`- ${tr(lang, "cli", "detect.resultDetected")}`));
|
|
8958
|
+
console.log(chalk6.gray(`- ${tr(lang, "cli", "detect.labelDocsDir")}: ${config.docsDir}`));
|
|
8959
|
+
console.log(
|
|
8960
|
+
chalk6.gray(
|
|
8961
|
+
`- ${tr(lang, "cli", "detect.labelConfigPath")}: ${configFilePresent ? configPath : "-"}`
|
|
8962
|
+
)
|
|
8963
|
+
);
|
|
8964
|
+
console.log(
|
|
8965
|
+
chalk6.gray(
|
|
8966
|
+
`- ${tr(lang, "cli", "detect.labelSource")}: ${tr(
|
|
8967
|
+
lang,
|
|
8968
|
+
"cli",
|
|
8969
|
+
detectionSource === "config" ? "detect.sourceConfig" : "detect.sourceHeuristic"
|
|
8970
|
+
)}`
|
|
8971
|
+
)
|
|
8972
|
+
);
|
|
8973
|
+
console.log(
|
|
8974
|
+
chalk6.gray(
|
|
8975
|
+
`- ${tr(lang, "cli", "detect.labelProjectType")}: ${config.projectType}`
|
|
8976
|
+
)
|
|
8977
|
+
);
|
|
8978
|
+
console.log(chalk6.gray(`- ${tr(lang, "cli", "detect.labelLang")}: ${config.lang}`));
|
|
8979
|
+
if (config.projectName) {
|
|
8980
|
+
console.log(
|
|
8981
|
+
chalk6.gray(
|
|
8982
|
+
`- ${tr(lang, "cli", "detect.labelProjectName")}: ${config.projectName}`
|
|
8983
|
+
)
|
|
8984
|
+
);
|
|
8985
|
+
}
|
|
8986
|
+
console.log();
|
|
8987
|
+
}
|
|
8198
8988
|
function isBannerDisabled() {
|
|
8199
8989
|
const v = (process.env.LEE_SPEC_KIT_NO_BANNER || "").trim();
|
|
8200
8990
|
return v === "1";
|
|
@@ -8243,8 +9033,8 @@ var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
|
8243
9033
|
function getCurrentVersion() {
|
|
8244
9034
|
try {
|
|
8245
9035
|
const packageJsonPath = path18.join(__dirname$1, "..", "package.json");
|
|
8246
|
-
if (
|
|
8247
|
-
const pkg =
|
|
9036
|
+
if (fs15.existsSync(packageJsonPath)) {
|
|
9037
|
+
const pkg = fs15.readJsonSync(packageJsonPath);
|
|
8248
9038
|
return pkg.version;
|
|
8249
9039
|
}
|
|
8250
9040
|
} catch {
|
|
@@ -8253,8 +9043,8 @@ function getCurrentVersion() {
|
|
|
8253
9043
|
}
|
|
8254
9044
|
function readCache() {
|
|
8255
9045
|
try {
|
|
8256
|
-
if (
|
|
8257
|
-
return
|
|
9046
|
+
if (fs15.existsSync(CACHE_FILE)) {
|
|
9047
|
+
return fs15.readJsonSync(CACHE_FILE);
|
|
8258
9048
|
}
|
|
8259
9049
|
} catch {
|
|
8260
9050
|
}
|
|
@@ -8343,8 +9133,8 @@ if (shouldCheckForUpdates()) checkForUpdates();
|
|
|
8343
9133
|
function getCliVersion() {
|
|
8344
9134
|
try {
|
|
8345
9135
|
const packageJsonPath = path18.join(__dirname$1, "..", "package.json");
|
|
8346
|
-
if (
|
|
8347
|
-
const pkg =
|
|
9136
|
+
if (fs15.existsSync(packageJsonPath)) {
|
|
9137
|
+
const pkg = fs15.readJsonSync(packageJsonPath);
|
|
8348
9138
|
if (pkg?.version) return String(pkg.version);
|
|
8349
9139
|
}
|
|
8350
9140
|
} catch {
|
|
@@ -8369,4 +9159,5 @@ viewCommand(program);
|
|
|
8369
9159
|
flowCommand(program);
|
|
8370
9160
|
githubCommand(program);
|
|
8371
9161
|
docsCommand(program);
|
|
9162
|
+
detectCommand(program);
|
|
8372
9163
|
await program.parseAsync();
|