lee-spec-kit 0.6.16 → 0.6.18
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 +56 -20
- package/README.md +57 -21
- package/dist/index.js +1117 -229
- package/package.json +8 -3
- package/templates/en/common/README.md +6 -2
- package/templates/en/common/agents/agents.md +1 -1
- package/templates/en/common/agents/git-workflow.md +8 -1
- package/templates/en/common/features/feature-base/issue.md +3 -7
- package/templates/en/common/features/feature-base/pr.md +3 -3
- package/templates/ko/common/README.md +6 -2
- package/templates/ko/common/agents/agents.md +1 -1
- package/templates/ko/common/agents/git-workflow.md +8 -1
- package/templates/ko/common/features/feature-base/issue.md +3 -7
- package/templates/ko/common/features/feature-base/pr.md +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import path20 from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { program } from 'commander';
|
|
5
5
|
import fs15 from 'fs-extra';
|
|
@@ -11,7 +11,7 @@ import os from 'os';
|
|
|
11
11
|
import { createHash, randomUUID } from 'crypto';
|
|
12
12
|
|
|
13
13
|
var getFilename = () => fileURLToPath(import.meta.url);
|
|
14
|
-
var getDirname = () =>
|
|
14
|
+
var getDirname = () => path20.dirname(getFilename());
|
|
15
15
|
var __dirname$1 = /* @__PURE__ */ getDirname();
|
|
16
16
|
async function copyTemplates(src, dest) {
|
|
17
17
|
await fs15.copy(src, dest, {
|
|
@@ -42,10 +42,10 @@ async function replaceInFiles(dir, replacements) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
45
|
-
var __dirname2 =
|
|
45
|
+
var __dirname2 = path20.dirname(__filename2);
|
|
46
46
|
function getTemplatesDir() {
|
|
47
|
-
const rootDir =
|
|
48
|
-
return
|
|
47
|
+
const rootDir = path20.resolve(__dirname2, "..");
|
|
48
|
+
return path20.join(rootDir, "templates");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// src/utils/i18n.ts
|
|
@@ -139,6 +139,7 @@ var I18N = {
|
|
|
139
139
|
"context.suggestionCommandHint": "\uB77C\uBCA8 \uCC38\uACE0 \uBA85\uB839: {command}",
|
|
140
140
|
"context.suggestionFinalPrompt": "\uD604\uC7AC \uCD94\uCC9C \uB77C\uBCA8: {labels}. \uC751\uB2F5\uC740 \uB77C\uBCA8 \uD1A0\uD070 \uD3EC\uD568 \uD615\uC2DD\uC73C\uB85C \uD574\uC8FC\uC138\uC694. (\uC608: {example}, `A \uC9C4\uD589\uD574`)",
|
|
141
141
|
"context.suggestion.createFeature": "\uC0C8 Feature\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4",
|
|
142
|
+
"context.suggestion.runOnboard": "\uCD08\uAE30 \uC124\uC815 \uC810\uAC80(onboard)\uC744 \uC2E4\uD589\uD569\uB2C8\uB2E4",
|
|
142
143
|
"context.suggestion.showDone": "\uC644\uB8CC\uB41C Feature \uBAA9\uB85D\uC744 \uD655\uC778\uD569\uB2C8\uB2E4",
|
|
143
144
|
"context.suggestion.showAll": "\uC804\uCCB4 Feature \uBAA9\uB85D\uC744 \uD655\uC778\uD569\uB2C8\uB2E4",
|
|
144
145
|
"context.suggestion.selectFeature": "\uC9C4\uD589\uD560 Feature\uB97C \uC120\uD0DD\uD574 \uC0C1\uC138 \uCEE8\uD14D\uC2A4\uD2B8\uB97C \uC5FD\uB2C8\uB2E4",
|
|
@@ -202,6 +203,7 @@ var I18N = {
|
|
|
202
203
|
"init.log.nextStepsTitle": "\uB2E4\uC74C \uB2E8\uACC4:",
|
|
203
204
|
"init.log.nextSteps1": " 1. {docsDir}/prd/README.md \uC791\uC131",
|
|
204
205
|
"init.log.nextSteps2": " 2. npx lee-spec-kit feature <name> \uC73C\uB85C \uAE30\uB2A5 \uCD94\uAC00",
|
|
206
|
+
"init.log.nextSteps3": " 3. npx lee-spec-kit onboard --strict \uB85C \uCD08\uAE30 \uC124\uC815 \uC810\uAC80",
|
|
205
207
|
"init.log.gitRepoDetectedCommit": "\u{1F4E6} Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0, docs \uCEE4\uBC0B \uC911...",
|
|
206
208
|
"init.log.gitInit": "\u{1F4E6} Git \uCD08\uAE30\uD654 \uC911...",
|
|
207
209
|
"init.warn.stagedChangesSkip": '\u26A0\uFE0F \uD604\uC7AC Git index\uC5D0 \uC774\uBBF8 stage\uB41C \uBCC0\uACBD\uC774 \uC788\uC2B5\uB2C8\uB2E4. (--dir "." \uC778 \uACBD\uC6B0 \uCEE4\uBC0B \uBC94\uC704\uB97C \uC548\uC804\uD558\uAC8C \uC81C\uD55C\uD560 \uC218 \uC5C6\uC5B4 \uC790\uB3D9 \uCEE4\uBC0B\uC744 \uAC74\uB108\uB701\uB2C8\uB2E4)',
|
|
@@ -274,7 +276,7 @@ var I18N = {
|
|
|
274
276
|
"github.operationPrMerge": "GitHub PR merge",
|
|
275
277
|
"github.createIssueFailed": "GitHub issue \uC0DD\uC131\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
|
|
276
278
|
"github.createPrFailed": "GitHub PR \uC0DD\uC131\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
|
|
277
|
-
"github.mergeRequiresPr": "`--merge`\uB97C \uC0AC\uC6A9\uD558\uB824\uBA74 `--create
|
|
279
|
+
"github.mergeRequiresPr": "`--merge`\uB97C \uC0AC\uC6A9\uD558\uB824\uBA74 `--create`, `--pr <url|number>`, \uB610\uB294 tasks.md\uC758 PR \uB9C1\uD06C\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
|
|
278
280
|
"github.checkoutBaseAfterMergeFailed": "merge \uD6C4 {base} \uBE0C\uB79C\uCE58 checkout\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
|
|
279
281
|
"github.pullBaseAfterMergeFailed": "merge \uD6C4 {base} \uBE0C\uB79C\uCE58 \uCD5C\uC2E0\uD654\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
|
|
280
282
|
"github.issueDefaultTitle": "{slug} ({summary})",
|
|
@@ -431,7 +433,7 @@ var I18N = {
|
|
|
431
433
|
reviewFixCommitIssueGuidance: 'PR \uB9AC\uBDF0 \uC218\uC815 \uCEE4\uBC0B\uC744 \uC9C4\uD589\uD558\uC138\uC694. \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uB294 \uD574\uACB0\uD55C \uB9AC\uBDF0 \uC9C0\uC801\uC0AC\uD56D\uC744 \uC694\uC57D\uD574\uC57C \uD558\uBA70, \uD0DC\uC2A4\uD06C \uC81C\uBAA9\uC744 \uC7AC\uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694. \uC608: `cd "{projectGitCwd}" && (git diff --cached --quiet && echo "\uC2A4\uD14C\uC774\uC9D5\uB41C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uB9AC\uBDF0 \uC218\uC815 \uBC18\uC601 \uD30C\uC77C\uB9CC git add [files] \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694." && exit 1 || git commit -m "fix(#{issueNumber}): <review-fix-summary>")` (`<review-fix-summary>`\uB294 \uC774\uBC88 \uCEE4\uBC0B\uC5D0\uC11C \uC2E4\uC81C\uB85C \uD574\uACB0\uD55C \uB9AC\uBDF0 \uD56D\uBAA9\uC73C\uB85C \uC9C1\uC811 \uC791\uC131)',
|
|
432
434
|
reviewFixCommitGuidance: 'PR \uB9AC\uBDF0 \uC218\uC815 \uCEE4\uBC0B\uC744 \uC9C4\uD589\uD558\uC138\uC694. \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uB294 \uD574\uACB0\uD55C \uB9AC\uBDF0 \uC9C0\uC801\uC0AC\uD56D\uC744 \uC694\uC57D\uD574\uC57C \uD558\uBA70, \uD0DC\uC2A4\uD06C \uC81C\uBAA9\uC744 \uC7AC\uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694. \uC608: `cd "{projectGitCwd}" && (git diff --cached --quiet && echo "\uC2A4\uD14C\uC774\uC9D5\uB41C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uB9AC\uBDF0 \uC218\uC815 \uBC18\uC601 \uD30C\uC77C\uB9CC git add [files] \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694." && exit 1 || git commit -m "fix(review): <review-fix-summary>")` (`<review-fix-summary>`\uB294 \uC774\uBC88 \uCEE4\uBC0B\uC5D0\uC11C \uC2E4\uC81C\uB85C \uD574\uACB0\uD55C \uB9AC\uBDF0 \uD56D\uBAA9\uC73C\uB85C \uC9C1\uC811 \uC791\uC131)',
|
|
433
435
|
standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
|
|
434
|
-
createBranch: 'cd "{projectGitCwd}" && git
|
|
436
|
+
createBranch: 'cd "{projectGitCwd}" && mkdir -p .worktrees && (git worktree add ".worktrees/feat-{issueNumber}-{slug}" "feat/{issueNumber}-{slug}" || git worktree add -b "feat/{issueNumber}-{slug}" ".worktrees/feat-{issueNumber}-{slug}") && echo "worktree: {projectGitCwd}/.worktrees/feat-{issueNumber}-{slug}"',
|
|
435
437
|
tasksAllDoneButNoChecklist: '\uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uB97C \uC791\uC131\uD558\uC138\uC694. tasks.md\uC758 "\uC644\uB8CC \uC870\uAC74" \uC139\uC158\uC5D0 \uAC80\uC99D \uD56D\uBAA9\uC744 \uCD94\uAC00\uD558\uACE0, \uC0AC\uC6A9\uC790\uC640 \uD655\uC778 \uD6C4 \uCDA9\uC871 \uD56D\uBAA9\uC744 [x]\uB85C \uCCB4\uD06C\uD558\uC138\uC694. \uCD5C\uC885 \uC2B9\uC778(OK)\uB3C4 \uBC18\uC601\uD558\uC138\uC694.',
|
|
436
438
|
tasksAllDoneButChecklist: "\uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uC758 \uB0A8\uC740 \uD56D\uBAA9\uC744 \uC9C4\uD589\uD558\uC138\uC694. \uD604\uC7AC \uC9C4\uD589: ({checked}/{total}) \uC0AC\uC6A9\uC790\uC640 \uD655\uC778 \uD6C4 \uCDA9\uC871 \uD56D\uBAA9\uC744 [x]\uB85C \uCCB4\uD06C\uD558\uACE0 \uCD5C\uC885 \uC2B9\uC778(OK)\uC744 \uBC18\uC601\uD558\uC138\uC694.",
|
|
437
439
|
finishDoingTask: '\uD604\uC7AC DOING/REVIEW \uD0DC\uC2A4\uD06C\uB97C \uC644\uB8CC\uD569\uB2C8\uB2E4: "{title}" ({done}/{total}) \uACB0\uACFC/\uAC80\uC99D \uACF5\uC720 + \uC2B9\uC778(`A` \uB610\uB294 `A OK` \uD615\uC2DD) \uD6C4 DONE \uCC98\uB9AC',
|
|
@@ -441,7 +443,7 @@ var I18N = {
|
|
|
441
443
|
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.",
|
|
442
444
|
taskCommitGateReasonNoTasksCommit: "\uCD5C\uADFC \uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uCEE4\uBC0B\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
|
|
443
445
|
taskCommitGateReasonTasksFileUnavailable: "\uCD5C\uADFC \uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uCEE4\uBC0B \uC774\uB825\uC744 \uD310\uB3C5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
|
|
444
|
-
taskCommitGateReasonDoneCount: "\uCD5C\
|
|
446
|
+
taskCommitGateReasonDoneCount: "\uCD5C\uC2E0 tasks.md \uCEE4\uBC0B\uC5D0\uC11C DONE \uC804\uD658\uC774 {count}\uAC74 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
|
|
445
447
|
taskCommitGateReasonMismatchLastDone: "\uCD5C\uADFC \uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uCEE4\uBC0B\uC774 \uC9C1\uC804 \uC644\uB8CC \uD0DC\uC2A4\uD06C\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
|
|
446
448
|
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)",
|
|
447
449
|
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)",
|
|
@@ -465,16 +467,21 @@ var I18N = {
|
|
|
465
467
|
prCreateExecute: "\uD655\uC815\uB41C PR \uBCF8\uBB38\uC73C\uB85C PR\uC744 \uC0DD\uC131\uD558\uACE0, \uC0DD\uC131\uB41C PR \uB9C1\uD06C\uB97C tasks.md\uC758 PR \uD544\uB4DC\uC5D0 \uAE30\uB85D\uD558\uC138\uC694.",
|
|
466
468
|
prCreateRequiredSequence: "PR \uC0DD\uC131\uC740 \uD544\uC218 2\uB2E8\uACC4\uC785\uB2C8\uB2E4: (1) PR \uBCF8\uBB38 \uD15C\uD50C\uB9BF \uC0DD\uC131/\uBCF4\uC644 + \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK), (2) PR \uC0DD\uC131 + tasks.md PR \uB9C1\uD06C \uAE30\uB85D. \uC704 \uC21C\uC11C\uB97C \uBAA8\uB450 \uC644\uB8CC\uD558\uC138\uC694.",
|
|
467
469
|
prFillStatus: "tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC124\uC815\uD558\uC138\uC694. (PR \uC0DD\uC131/\uB9AC\uBDF0 \uB2E8\uACC4\uC5D0\uC11C\uB294 Review\uB97C \uC720\uC9C0\uD569\uB2C8\uB2E4.)",
|
|
470
|
+
prReviewMergedSyncStatus: "\uC6D0\uACA9 PR\uC774 \uC774\uBBF8 \uBA38\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4. tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694. (PR/\uB9AC\uBDF0 \uADFC\uAC70 \uD544\uB4DC\uB3C4 \uCD5C\uC2E0 \uC0C1\uD0DC\uB85C \uD655\uC778)",
|
|
468
471
|
prResolveReview: "\uB9AC\uBDF0 \uCF54\uBA58\uD2B8\uB97C \uD574\uACB0\uD558\uB294 \uB3D9\uC548 PR \uC0C1\uD0DC\uB294 Review\uB85C \uC720\uC9C0\uD558\uC138\uC694. \uB9AC\uBDF0 \uC218\uC815 \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uB294 \uD0DC\uC2A4\uD06C\uBA85\uC774 \uC544\uB2C8\uB77C \uC2E4\uC81C\uB85C \uD574\uACB0\uD55C \uB9AC\uBDF0 \uC9C0\uC801\uC0AC\uD56D \uC694\uC57D\uC73C\uB85C \uC791\uC131\uD558\uC138\uC694. \uBA38\uC9C0 \uC900\uBE44\uAC00 \uB418\uBA74 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `npx lee-spec-kit github pr {featureRef} --merge --confirm OK`\uB97C \uC2E4\uD589\uD558\uC138\uC694. (\uC131\uACF5 \uC2DC PR \uC0C1\uD0DC\uAC00 Approved\uB85C \uB3D9\uAE30\uD654\uB429\uB2C8\uB2E4.)",
|
|
469
472
|
prReviewResolve: "\uB9AC\uBDF0 \uCF54\uBA58\uD2B8\uB97C \uBA3C\uC800 \uD655\uC778/\uBD84\uC11D\uD55C \uB4A4 \uD544\uC694\uD55C \uC218\uC815 \uC791\uC5C5\uC744 \uC9C4\uD589\uD558\uC138\uC694. \uC9C4\uD589 \uC911\uC5D0\uB294 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC720\uC9C0\uD558\uC138\uC694. \uB9AC\uBDF0 \uC218\uC815 \uCEE4\uBC0B \uBA54\uC2DC\uC9C0\uB294 \uD0DC\uC2A4\uD06C\uBA85\uC774 \uC544\uB2C8\uB77C \uC2E4\uC81C\uB85C \uD574\uACB0\uD55C \uB9AC\uBDF0 \uC9C0\uC801\uC0AC\uD56D \uC694\uC57D\uC73C\uB85C \uC791\uC131\uD558\uC138\uC694. `PR \uB9AC\uBDF0 Findings/Evidence`\uB97C \uCD5C\uC2E0\uC73C\uB85C \uAE30\uB85D\uD558\uACE0, \uCEE4\uBC0B \uD6C4 \uC6D0\uACA9 \uBC18\uC601(push)\uB3C4 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK)\uC744 \uBC1B\uC740 \uB4A4 \uC9C4\uD589\uD558\uC138\uC694.",
|
|
470
|
-
prReviewPush: '
|
|
473
|
+
prReviewPush: 'cd "{projectGitCwd}" && git push',
|
|
471
474
|
prReviewRemoteBlocked: "\uC6D0\uACA9 PR \uC0C1\uD0DC\uB97C \uD655\uC778\uD55C \uACB0\uACFC \uC544\uC9C1 \uBA38\uC9C0 \uC900\uBE44\uAC00 \uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4: {reasons}. \uB9AC\uBDF0 \uCF54\uBA58\uD2B8/\uCCB4\uD06C \uC0C1\uD0DC\uB97C \uC815\uB9AC\uD55C \uB4A4 \uB2E4\uC2DC \uD655\uC778\uD558\uC138\uC694.",
|
|
472
475
|
prReviewRemoteReasonChangesRequested: "\uB9AC\uBDF0 \uC2B9\uC778 \uC0C1\uD0DC\uAC00 \uBCC0\uACBD \uC694\uCCAD \uB610\uB294 \uCD94\uAC00 \uB9AC\uBDF0 \uD544\uC694 \uC0C1\uD0DC\uC785\uB2C8\uB2E4",
|
|
476
|
+
prReviewRemoteReasonClosed: "PR\uC774 \uBA38\uC9C0\uB418\uC9C0 \uC54A\uC740 \uCC44 \uB2EB\uD600 \uC788\uC2B5\uB2C8\uB2E4 (reopen \uB610\uB294 \uC0C8 PR \uD544\uC694)",
|
|
473
477
|
prReviewRemoteReasonChecksFailing: "\uC2E4\uD328\uD55C \uCCB4\uD06C\uAC00 {count}\uAC74 \uC788\uC2B5\uB2C8\uB2E4",
|
|
474
478
|
prReviewRemoteReasonChecksPending: "\uB300\uAE30 \uC911\uC778 \uCCB4\uD06C\uAC00 {count}\uAC74 \uC788\uC2B5\uB2C8\uB2E4",
|
|
475
479
|
prReviewRemoteReasonMergeBlocked: "\uBA38\uC9C0 \uC0C1\uD0DC\uAC00 `{status}`\uB85C \uCC28\uB2E8\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4",
|
|
480
|
+
prReviewRemoteReasonUnavailable: "\uC6D0\uACA9 PR \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 (gh \uC778\uC99D/\uB124\uD2B8\uC6CC\uD06C/\uAD8C\uD55C \uD655\uC778 \uD544\uC694)",
|
|
476
481
|
prReviewMerge: "\uBA38\uC9C0 \uC900\uBE44\uAC00 \uB418\uBA74 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `npx lee-spec-kit github pr {featureRef} --merge --confirm OK`\uB97C \uC2E4\uD589\uD558\uC138\uC694. (\uC131\uACF5 \uC2DC PR \uC0C1\uD0DC\uAC00 Approved\uB85C \uB3D9\uAE30\uD654\uB429\uB2C8\uB2E4.)",
|
|
482
|
+
prReviewMergeCommand: "npx lee-spec-kit github pr {featureRef} --merge --confirm OK",
|
|
477
483
|
prRequestReview: "\uB9AC\uBDF0\uC5B4\uC5D0\uAC8C \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uACE0 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC124\uC815/\uC720\uC9C0\uD558\uC138\uC694.",
|
|
484
|
+
userRequestReplan: "\uD604\uC7AC \uB2E8\uACC4\uC640 \uBCC4\uAC1C\uB85C \uC0AC\uC6A9\uC790\uAC00 \uC81C\uC548\uD55C \uC0C8 \uC694\uAD6C\uB97C \uBA3C\uC800 \uBC18\uC601\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC694\uAD6C\uC0AC\uD56D\uC744 \uC694\uC57D\uD574 tasks.md\uC5D0 \uCD94\uAC00\uD558\uAC70\uB098 \uBCC4\uB3C4 Feature\uB85C \uBD84\uB9AC\uD55C \uB4A4, \uBB38\uC11C \uC0C1\uD0DC\uB97C \uB9DE\uCD94\uACE0 context\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
|
|
478
485
|
featureDone: "\uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC694\uAD6C\uC0AC\uD56D\uACFC \uBAA8\uB4E0 \uD0DC\uC2A4\uD06C/\uC644\uB8CC \uC870\uAC74\uC774 \uCDA9\uC871\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC774 Feature\uB294 \uC644\uB8CC \uC0C1\uD0DC\uC785\uB2C8\uB2E4.",
|
|
479
486
|
fallbackRerunContext: "\uC0C1\uD0DC\uB97C \uD310\uBCC4\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC11C\uB97C \uD655\uC778\uD55C \uB4A4 \uB2E4\uC2DC context\uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
480
487
|
},
|
|
@@ -589,6 +596,7 @@ var I18N = {
|
|
|
589
596
|
"context.suggestionCommandHint": "Reference command: {command}",
|
|
590
597
|
"context.suggestionFinalPrompt": "Recommended labels now: {labels}. Please reply with a format that includes a label token. (e.g. {example}, `A proceed`)",
|
|
591
598
|
"context.suggestion.createFeature": "Create a new feature",
|
|
599
|
+
"context.suggestion.runOnboard": "Run onboarding checks",
|
|
592
600
|
"context.suggestion.showDone": "Show completed features",
|
|
593
601
|
"context.suggestion.showAll": "Show all features",
|
|
594
602
|
"context.suggestion.selectFeature": "Select a feature and open detailed context",
|
|
@@ -652,6 +660,7 @@ var I18N = {
|
|
|
652
660
|
"init.log.nextStepsTitle": "Next steps:",
|
|
653
661
|
"init.log.nextSteps1": " 1. Write {docsDir}/prd/README.md",
|
|
654
662
|
"init.log.nextSteps2": " 2. Add a feature with: npx lee-spec-kit feature <name>",
|
|
663
|
+
"init.log.nextSteps3": " 3. Run setup checks: npx lee-spec-kit onboard --strict",
|
|
655
664
|
"init.log.gitRepoDetectedCommit": "\u{1F4E6} Git repo detected, committing docs...",
|
|
656
665
|
"init.log.gitInit": "\u{1F4E6} Initializing Git...",
|
|
657
666
|
"init.warn.stagedChangesSkip": '\u26A0\uFE0F There are already staged changes in the Git index. (With --dir ".", commit scope cannot be safely restricted, so auto-commit is skipped.)',
|
|
@@ -724,7 +733,7 @@ var I18N = {
|
|
|
724
733
|
"github.operationPrMerge": "GitHub PR merge",
|
|
725
734
|
"github.createIssueFailed": "Failed to create GitHub issue",
|
|
726
735
|
"github.createPrFailed": "Failed to create GitHub PR",
|
|
727
|
-
"github.mergeRequiresPr": "`--merge` requires `--create
|
|
736
|
+
"github.mergeRequiresPr": "`--merge` requires `--create`, `--pr <url|number>`, or a PR link in tasks.md.",
|
|
728
737
|
"github.checkoutBaseAfterMergeFailed": "Failed to checkout {base} after merge",
|
|
729
738
|
"github.pullBaseAfterMergeFailed": "Failed to update {base} after merge",
|
|
730
739
|
"github.issueDefaultTitle": "{slug} ({summary})",
|
|
@@ -881,7 +890,7 @@ var I18N = {
|
|
|
881
890
|
reviewFixCommitIssueGuidance: 'Commit PR review fixes. The commit message must summarize review comments resolved in this commit, not reuse a task title. Example: `cd "{projectGitCwd}" && (git diff --cached --quiet && echo "No staged files. Stage only files that implement review fixes with git add [files], then run again." && exit 1 || git commit -m "fix(#{issueNumber}): <review-fix-summary>")` (replace `<review-fix-summary>` with what this commit actually resolves).',
|
|
882
891
|
reviewFixCommitGuidance: 'Commit PR review fixes. The commit message must summarize review comments resolved in this commit, not reuse a task title. Example: `cd "{projectGitCwd}" && (git diff --cached --quiet && echo "No staged files. Stage only files that implement review fixes with git add [files], then run again." && exit 1 || git commit -m "fix(review): <review-fix-summary>")` (replace `<review-fix-summary>` with what this commit actually resolves).',
|
|
883
892
|
standaloneNeedsProjectRoot: "Standalone mode requires projectRoot. (npx lee-spec-kit config --project-root ...)",
|
|
884
|
-
createBranch: 'cd "{projectGitCwd}" && git
|
|
893
|
+
createBranch: 'cd "{projectGitCwd}" && mkdir -p .worktrees && (git worktree add ".worktrees/feat-{issueNumber}-{slug}" "feat/{issueNumber}-{slug}" || git worktree add -b "feat/{issueNumber}-{slug}" ".worktrees/feat-{issueNumber}-{slug}") && echo "worktree: {projectGitCwd}/.worktrees/feat-{issueNumber}-{slug}"',
|
|
885
894
|
tasksAllDoneButNoChecklist: 'Create the completion checklist. Add verification items to the tasks.md "Completion Criteria" section, then mark satisfied items as [x] after user confirmation. Record final approval (OK) as well.',
|
|
886
895
|
tasksAllDoneButChecklist: "Proceed with remaining completion checklist items. Current progress: ({checked}/{total}) Mark items as [x] only after user confirmation and real verification. Record final approval (OK) as well.",
|
|
887
896
|
finishDoingTask: 'Finish the current DOING/REVIEW task: "{title}" ({done}/{total}) Share outcome/verification + get OK before marking DONE',
|
|
@@ -891,7 +900,7 @@ var I18N = {
|
|
|
891
900
|
taskCommitGateWarnProceed: "\u26A0\uFE0F Task commit boundary warning: {reason}. You may continue, but `1 task = 1 commit` is recommended.",
|
|
892
901
|
taskCommitGateReasonNoTasksCommit: "No recent project code commit was found",
|
|
893
902
|
taskCommitGateReasonTasksFileUnavailable: "Cannot read recent project code commit history",
|
|
894
|
-
taskCommitGateReasonDoneCount: "
|
|
903
|
+
taskCommitGateReasonDoneCount: "DONE transitions detected in the latest tasks.md commit ({count})",
|
|
895
904
|
taskCommitGateReasonMismatchLastDone: "The latest project code commit does not match the last completed task",
|
|
896
905
|
prLegacyAsk: "tasks.md is missing PR/PR Status fields. Update to the latest template format? (CHECK required)",
|
|
897
906
|
prePrReviewFieldMissing: "tasks.md is missing the `Pre-PR Review` field. Add `- **Pre-PR Review**: Pending | Done` and run context again. (CHECK required)",
|
|
@@ -915,16 +924,21 @@ var I18N = {
|
|
|
915
924
|
prCreateExecute: "Create the PR with the finalized body, then record the created PR link in tasks.md.",
|
|
916
925
|
prCreateRequiredSequence: "PR creation is a required 2-step sequence: (1) generate/refine PR body template + explicit user OK, (2) create PR + record PR link in tasks.md. Complete both in order.",
|
|
917
926
|
prFillStatus: "Set PR Status in tasks.md to Review. (Keep Review during PR creation/review stages.)",
|
|
927
|
+
prReviewMergedSyncStatus: "The remote PR is already merged. Update PR Status in tasks.md to Approved. (Also verify PR/review evidence fields are up to date.)",
|
|
918
928
|
prResolveReview: "Keep PR Status as Review while addressing comments. For review-fix commits, use commit messages that summarize resolved review feedback (not task titles). Once ready to merge, get explicit user OK and run `npx lee-spec-kit github pr {featureRef} --merge --confirm OK`. (On success, PR Status is synced to Approved.)",
|
|
919
929
|
prReviewResolve: "First review/analyze the PR comments, then apply the required fixes. Keep PR Status as Review while addressing comments. For review-fix commits, use commit messages that summarize resolved review feedback (not task titles). Keep `PR Review Findings/Evidence` updated, then run push only after explicit user OK.",
|
|
920
|
-
prReviewPush: '
|
|
930
|
+
prReviewPush: 'cd "{projectGitCwd}" && git push',
|
|
921
931
|
prReviewRemoteBlocked: "Remote PR checks indicate this PR is not ready to merge yet: {reasons}. Resolve review comments/check statuses, then re-check.",
|
|
922
932
|
prReviewRemoteReasonChangesRequested: "review decision is changes requested or additional review required",
|
|
933
|
+
prReviewRemoteReasonClosed: "PR is closed without merge (reopen or create a new PR)",
|
|
923
934
|
prReviewRemoteReasonChecksFailing: "{count} failing check(s) detected",
|
|
924
935
|
prReviewRemoteReasonChecksPending: "{count} pending check(s) detected",
|
|
925
936
|
prReviewRemoteReasonMergeBlocked: "merge state is blocked (`{status}`)",
|
|
937
|
+
prReviewRemoteReasonUnavailable: "remote PR status could not be verified (check gh auth/network/permissions)",
|
|
926
938
|
prReviewMerge: "When ready to merge, get explicit user OK and run `npx lee-spec-kit github pr {featureRef} --merge --confirm OK`. (On success, PR Status is synced to Approved.)",
|
|
939
|
+
prReviewMergeCommand: "npx lee-spec-kit github pr {featureRef} --merge --confirm OK",
|
|
927
940
|
prRequestReview: "Request review and set/keep PR Status as Review.",
|
|
941
|
+
userRequestReplan: "You can pause this step and handle a newly requested user requirement first. Summarize it, add it to tasks.md or split it into a separate Feature, then align document statuses and rerun context.",
|
|
928
942
|
featureDone: "Workflow requirements and all tasks/completion criteria are satisfied. This feature is done.",
|
|
929
943
|
fallbackRerunContext: "Cannot determine status. Check the docs and run context again."
|
|
930
944
|
},
|
|
@@ -1345,10 +1359,10 @@ function sleep(ms) {
|
|
|
1345
1359
|
return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
|
|
1346
1360
|
}
|
|
1347
1361
|
function toScopeKey(value) {
|
|
1348
|
-
return createHash("sha1").update(
|
|
1362
|
+
return createHash("sha1").update(path20.resolve(value)).digest("hex").slice(0, 16);
|
|
1349
1363
|
}
|
|
1350
1364
|
function getTempRuntimeDir(scopePath) {
|
|
1351
|
-
return
|
|
1365
|
+
return path20.join(os.tmpdir(), RUNTIME_TEMP_DIRNAME, toScopeKey(scopePath));
|
|
1352
1366
|
}
|
|
1353
1367
|
function resolveGitRuntimeDir(cwd) {
|
|
1354
1368
|
try {
|
|
@@ -1362,38 +1376,38 @@ function resolveGitRuntimeDir(cwd) {
|
|
|
1362
1376
|
}
|
|
1363
1377
|
).trim();
|
|
1364
1378
|
if (!out) return null;
|
|
1365
|
-
return
|
|
1379
|
+
return path20.isAbsolute(out) ? out : path20.resolve(cwd, out);
|
|
1366
1380
|
} catch {
|
|
1367
1381
|
return null;
|
|
1368
1382
|
}
|
|
1369
1383
|
}
|
|
1370
1384
|
function getRuntimeStateDir(cwd) {
|
|
1371
|
-
const resolved =
|
|
1385
|
+
const resolved = path20.resolve(cwd);
|
|
1372
1386
|
return resolveGitRuntimeDir(resolved) ?? getTempRuntimeDir(resolved);
|
|
1373
1387
|
}
|
|
1374
1388
|
function getDocsLockPath(docsDir) {
|
|
1375
|
-
return
|
|
1389
|
+
return path20.join(
|
|
1376
1390
|
getRuntimeStateDir(docsDir),
|
|
1377
1391
|
"locks",
|
|
1378
1392
|
`docs-${toScopeKey(docsDir)}.lock`
|
|
1379
1393
|
);
|
|
1380
1394
|
}
|
|
1381
1395
|
function getInitLockPath(targetDir) {
|
|
1382
|
-
return
|
|
1383
|
-
getRuntimeStateDir(
|
|
1396
|
+
return path20.join(
|
|
1397
|
+
getRuntimeStateDir(path20.dirname(path20.resolve(targetDir))),
|
|
1384
1398
|
"locks",
|
|
1385
1399
|
`init-${toScopeKey(targetDir)}.lock`
|
|
1386
1400
|
);
|
|
1387
1401
|
}
|
|
1388
1402
|
function getApprovalTicketStorePath(docsDir) {
|
|
1389
|
-
return
|
|
1403
|
+
return path20.join(
|
|
1390
1404
|
getRuntimeStateDir(docsDir),
|
|
1391
1405
|
"tickets",
|
|
1392
1406
|
`approval-${toScopeKey(docsDir)}.json`
|
|
1393
1407
|
);
|
|
1394
1408
|
}
|
|
1395
1409
|
function getProjectExecutionLockPath(cwd) {
|
|
1396
|
-
return
|
|
1410
|
+
return path20.join(getRuntimeStateDir(cwd), "locks", "project.lock");
|
|
1397
1411
|
}
|
|
1398
1412
|
async function isStaleLock(lockPath, staleMs) {
|
|
1399
1413
|
try {
|
|
@@ -1434,7 +1448,7 @@ function isProcessAlive(pid) {
|
|
|
1434
1448
|
}
|
|
1435
1449
|
}
|
|
1436
1450
|
async function tryAcquire(lockPath, owner) {
|
|
1437
|
-
await fs15.ensureDir(
|
|
1451
|
+
await fs15.ensureDir(path20.dirname(lockPath));
|
|
1438
1452
|
try {
|
|
1439
1453
|
const fd = await fs15.open(lockPath, "wx");
|
|
1440
1454
|
const payload = JSON.stringify(
|
|
@@ -1511,30 +1525,30 @@ var ENGINE_MANAGED_AGENT_FILES = [
|
|
|
1511
1525
|
"pr-template.md"
|
|
1512
1526
|
];
|
|
1513
1527
|
var ENGINE_MANAGED_AGENT_DIRS = ["skills"];
|
|
1514
|
-
var ENGINE_MANAGED_FEATURE_PATH =
|
|
1528
|
+
var ENGINE_MANAGED_FEATURE_PATH = path20.join(
|
|
1515
1529
|
"features",
|
|
1516
1530
|
"feature-base"
|
|
1517
1531
|
);
|
|
1518
1532
|
async function pruneEngineManagedDocs(docsDir) {
|
|
1519
1533
|
const removed = [];
|
|
1520
1534
|
for (const file of ENGINE_MANAGED_AGENT_FILES) {
|
|
1521
|
-
const target =
|
|
1535
|
+
const target = path20.join(docsDir, "agents", file);
|
|
1522
1536
|
if (await fs15.pathExists(target)) {
|
|
1523
1537
|
await fs15.remove(target);
|
|
1524
|
-
removed.push(
|
|
1538
|
+
removed.push(path20.relative(docsDir, target));
|
|
1525
1539
|
}
|
|
1526
1540
|
}
|
|
1527
1541
|
for (const dir of ENGINE_MANAGED_AGENT_DIRS) {
|
|
1528
|
-
const target =
|
|
1542
|
+
const target = path20.join(docsDir, "agents", dir);
|
|
1529
1543
|
if (await fs15.pathExists(target)) {
|
|
1530
1544
|
await fs15.remove(target);
|
|
1531
|
-
removed.push(
|
|
1545
|
+
removed.push(path20.relative(docsDir, target));
|
|
1532
1546
|
}
|
|
1533
1547
|
}
|
|
1534
|
-
const featureBasePath =
|
|
1548
|
+
const featureBasePath = path20.join(docsDir, ENGINE_MANAGED_FEATURE_PATH);
|
|
1535
1549
|
if (await fs15.pathExists(featureBasePath)) {
|
|
1536
1550
|
await fs15.remove(featureBasePath);
|
|
1537
|
-
removed.push(
|
|
1551
|
+
removed.push(path20.relative(docsDir, featureBasePath));
|
|
1538
1552
|
}
|
|
1539
1553
|
return removed;
|
|
1540
1554
|
}
|
|
@@ -1675,7 +1689,7 @@ ${tr(lang2, "cli", "common.canceled")}`)
|
|
|
1675
1689
|
}
|
|
1676
1690
|
async function runInit(options) {
|
|
1677
1691
|
const cwd = process.cwd();
|
|
1678
|
-
const defaultName =
|
|
1692
|
+
const defaultName = path20.basename(cwd);
|
|
1679
1693
|
let projectName = options.name || defaultName;
|
|
1680
1694
|
let projectType = options.type;
|
|
1681
1695
|
let components = parseComponentsOption(options.components);
|
|
@@ -1688,7 +1702,7 @@ async function runInit(options) {
|
|
|
1688
1702
|
const componentProjectRoots = parseComponentProjectRootsOption(
|
|
1689
1703
|
options.componentProjectRoots
|
|
1690
1704
|
);
|
|
1691
|
-
const targetDir =
|
|
1705
|
+
const targetDir = path20.resolve(cwd, options.dir || "./docs");
|
|
1692
1706
|
const skipPrompts = !!options.yes || !!options.nonInteractive;
|
|
1693
1707
|
if (options.docsRepo && !["embedded", "standalone"].includes(options.docsRepo)) {
|
|
1694
1708
|
throw createCliError(
|
|
@@ -2077,7 +2091,7 @@ async function runInit(options) {
|
|
|
2077
2091
|
);
|
|
2078
2092
|
console.log();
|
|
2079
2093
|
const templatesDir = getTemplatesDir();
|
|
2080
|
-
const commonPath =
|
|
2094
|
+
const commonPath = path20.join(templatesDir, lang, "common");
|
|
2081
2095
|
if (!await fs15.pathExists(commonPath)) {
|
|
2082
2096
|
throw new Error(
|
|
2083
2097
|
tr(lang, "cli", "init.error.templateNotFound", { path: commonPath })
|
|
@@ -2085,11 +2099,11 @@ async function runInit(options) {
|
|
|
2085
2099
|
}
|
|
2086
2100
|
await copyTemplates(commonPath, targetDir);
|
|
2087
2101
|
if (projectType === "multi") {
|
|
2088
|
-
const featuresRoot =
|
|
2102
|
+
const featuresRoot = path20.join(targetDir, "features");
|
|
2089
2103
|
for (const component of components) {
|
|
2090
|
-
const componentDir =
|
|
2104
|
+
const componentDir = path20.join(featuresRoot, component);
|
|
2091
2105
|
await fs15.ensureDir(componentDir);
|
|
2092
|
-
const readmePath =
|
|
2106
|
+
const readmePath = path20.join(componentDir, "README.md");
|
|
2093
2107
|
if (!await fs15.pathExists(readmePath)) {
|
|
2094
2108
|
await fs15.writeFile(
|
|
2095
2109
|
readmePath,
|
|
@@ -2118,7 +2132,7 @@ async function runInit(options) {
|
|
|
2118
2132
|
workflow: {
|
|
2119
2133
|
mode: workflowMode,
|
|
2120
2134
|
codeDirtyScope: "auto",
|
|
2121
|
-
taskCommitGate: "
|
|
2135
|
+
taskCommitGate: "warn",
|
|
2122
2136
|
prePrReview: {
|
|
2123
2137
|
skills: ["code-review-excellence"],
|
|
2124
2138
|
minorPolicy: "warn"
|
|
@@ -2142,7 +2156,7 @@ async function runInit(options) {
|
|
|
2142
2156
|
config.projectRoot = projectRoot;
|
|
2143
2157
|
}
|
|
2144
2158
|
}
|
|
2145
|
-
const configPath =
|
|
2159
|
+
const configPath = path20.join(targetDir, ".lee-spec-kit.json");
|
|
2146
2160
|
await fs15.writeJson(configPath, config, { spaces: 2 });
|
|
2147
2161
|
console.log(chalk6.green(tr(lang, "cli", "init.log.docsCreated")));
|
|
2148
2162
|
console.log();
|
|
@@ -2152,6 +2166,7 @@ async function runInit(options) {
|
|
|
2152
2166
|
chalk6.gray(tr(lang, "cli", "init.log.nextSteps1", { docsDir: targetDir }))
|
|
2153
2167
|
);
|
|
2154
2168
|
console.log(chalk6.gray(tr(lang, "cli", "init.log.nextSteps2")));
|
|
2169
|
+
console.log(chalk6.gray(tr(lang, "cli", "init.log.nextSteps3")));
|
|
2155
2170
|
console.log();
|
|
2156
2171
|
},
|
|
2157
2172
|
{ owner: "init" }
|
|
@@ -2159,7 +2174,8 @@ async function runInit(options) {
|
|
|
2159
2174
|
}
|
|
2160
2175
|
async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
2161
2176
|
try {
|
|
2162
|
-
const
|
|
2177
|
+
const gitWorkdir = docsRepo === "standalone" ? targetDir : cwd;
|
|
2178
|
+
const runGit2 = (args, workdir) => {
|
|
2163
2179
|
execFileSync("git", args, { cwd: workdir, stdio: "ignore" });
|
|
2164
2180
|
};
|
|
2165
2181
|
const getCachedStagedFiles = (workdir) => {
|
|
@@ -2205,14 +2221,14 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
|
2205
2221
|
}
|
|
2206
2222
|
};
|
|
2207
2223
|
try {
|
|
2208
|
-
|
|
2224
|
+
runGit2(["rev-parse", "--is-inside-work-tree"], gitWorkdir);
|
|
2209
2225
|
console.log(chalk6.blue(tr(lang, "cli", "init.log.gitRepoDetectedCommit")));
|
|
2210
2226
|
} catch {
|
|
2211
2227
|
console.log(chalk6.blue(tr(lang, "cli", "init.log.gitInit")));
|
|
2212
|
-
|
|
2228
|
+
runGit2(["init"], gitWorkdir);
|
|
2213
2229
|
}
|
|
2214
|
-
const relativePath =
|
|
2215
|
-
const stagedBeforeAdd = getCachedStagedFiles(
|
|
2230
|
+
const relativePath = docsRepo === "standalone" ? "." : path20.relative(cwd, targetDir);
|
|
2231
|
+
const stagedBeforeAdd = getCachedStagedFiles(gitWorkdir);
|
|
2216
2232
|
if (relativePath === "." && stagedBeforeAdd && stagedBeforeAdd.length > 0) {
|
|
2217
2233
|
console.log(
|
|
2218
2234
|
chalk6.yellow(
|
|
@@ -2223,8 +2239,8 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
|
2223
2239
|
console.log();
|
|
2224
2240
|
return;
|
|
2225
2241
|
}
|
|
2226
|
-
if (relativePath !== "." && isPathIgnored(
|
|
2227
|
-
const repoRelativePath = toRepoRelativePath(
|
|
2242
|
+
if (relativePath !== "." && isPathIgnored(gitWorkdir, relativePath)) {
|
|
2243
|
+
const repoRelativePath = toRepoRelativePath(gitWorkdir, relativePath);
|
|
2228
2244
|
console.log(
|
|
2229
2245
|
chalk6.yellow(
|
|
2230
2246
|
tr(lang, "cli", "init.warn.docsPathIgnoredSkipCommit", {
|
|
@@ -2242,14 +2258,14 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
|
2242
2258
|
console.log();
|
|
2243
2259
|
return;
|
|
2244
2260
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2261
|
+
runGit2(["add", relativePath], gitWorkdir);
|
|
2262
|
+
runGit2(
|
|
2247
2263
|
["commit", "-m", "init: docs \uAD6C\uC870 \uCD08\uAE30\uD654 (lee-spec-kit)", "--", relativePath],
|
|
2248
|
-
|
|
2264
|
+
gitWorkdir
|
|
2249
2265
|
);
|
|
2250
2266
|
if (docsRepo === "standalone" && pushDocs && docsRemote) {
|
|
2251
2267
|
try {
|
|
2252
|
-
|
|
2268
|
+
runGit2(["remote", "add", "origin", docsRemote], gitWorkdir);
|
|
2253
2269
|
console.log(
|
|
2254
2270
|
chalk6.green(tr(lang, "cli", "init.log.gitRemoteSet", { remote: docsRemote }))
|
|
2255
2271
|
);
|
|
@@ -2268,17 +2284,17 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
|
2268
2284
|
}
|
|
2269
2285
|
function getAncestorDirs(startDir) {
|
|
2270
2286
|
const dirs = [];
|
|
2271
|
-
let current =
|
|
2287
|
+
let current = path20.resolve(startDir);
|
|
2272
2288
|
while (true) {
|
|
2273
2289
|
dirs.push(current);
|
|
2274
|
-
const parent =
|
|
2290
|
+
const parent = path20.dirname(current);
|
|
2275
2291
|
if (parent === current) break;
|
|
2276
2292
|
current = parent;
|
|
2277
2293
|
}
|
|
2278
2294
|
return dirs;
|
|
2279
2295
|
}
|
|
2280
2296
|
function hasWorkspaceBoundary(dir) {
|
|
2281
|
-
return fs15.existsSync(
|
|
2297
|
+
return fs15.existsSync(path20.join(dir, "package.json")) || fs15.existsSync(path20.join(dir, ".git"));
|
|
2282
2298
|
}
|
|
2283
2299
|
function getSearchBaseDirs(cwd) {
|
|
2284
2300
|
const ancestors = getAncestorDirs(cwd);
|
|
@@ -2294,7 +2310,7 @@ function normalizeComponentKeys(value) {
|
|
|
2294
2310
|
return Object.keys(value).map((key) => key.trim().toLowerCase()).filter(Boolean);
|
|
2295
2311
|
}
|
|
2296
2312
|
async function inferComponentsFromFeaturesDir(docsDir) {
|
|
2297
|
-
const featuresPath =
|
|
2313
|
+
const featuresPath = path20.join(docsDir, "features");
|
|
2298
2314
|
if (!await fs15.pathExists(featuresPath)) return [];
|
|
2299
2315
|
const entries = await fs15.readdir(featuresPath, { withFileTypes: true });
|
|
2300
2316
|
const inferred = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name.trim().toLowerCase()).filter(
|
|
@@ -2305,21 +2321,21 @@ async function inferComponentsFromFeaturesDir(docsDir) {
|
|
|
2305
2321
|
async function getConfig(cwd) {
|
|
2306
2322
|
const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
|
|
2307
2323
|
const baseDirs = [
|
|
2308
|
-
...explicitDocsDir ? [
|
|
2324
|
+
...explicitDocsDir ? [path20.resolve(explicitDocsDir)] : [],
|
|
2309
2325
|
...getSearchBaseDirs(cwd)
|
|
2310
2326
|
];
|
|
2311
2327
|
const visitedBaseDirs = /* @__PURE__ */ new Set();
|
|
2312
2328
|
const visitedDocsDirs = /* @__PURE__ */ new Set();
|
|
2313
2329
|
for (const baseDir of baseDirs) {
|
|
2314
|
-
const resolvedBaseDir =
|
|
2330
|
+
const resolvedBaseDir = path20.resolve(baseDir);
|
|
2315
2331
|
if (visitedBaseDirs.has(resolvedBaseDir)) continue;
|
|
2316
2332
|
visitedBaseDirs.add(resolvedBaseDir);
|
|
2317
|
-
const possibleDocsDirs = [
|
|
2333
|
+
const possibleDocsDirs = [path20.join(resolvedBaseDir, "docs"), resolvedBaseDir];
|
|
2318
2334
|
for (const docsDir of possibleDocsDirs) {
|
|
2319
|
-
const resolvedDocsDir =
|
|
2335
|
+
const resolvedDocsDir = path20.resolve(docsDir);
|
|
2320
2336
|
if (visitedDocsDirs.has(resolvedDocsDir)) continue;
|
|
2321
2337
|
visitedDocsDirs.add(resolvedDocsDir);
|
|
2322
|
-
const configPath =
|
|
2338
|
+
const configPath = path20.join(resolvedDocsDir, ".lee-spec-kit.json");
|
|
2323
2339
|
if (await fs15.pathExists(configPath)) {
|
|
2324
2340
|
try {
|
|
2325
2341
|
const configFile = await fs15.readJson(configPath);
|
|
@@ -2349,16 +2365,16 @@ async function getConfig(cwd) {
|
|
|
2349
2365
|
} catch {
|
|
2350
2366
|
}
|
|
2351
2367
|
}
|
|
2352
|
-
const agentsPath =
|
|
2353
|
-
const featuresPath =
|
|
2368
|
+
const agentsPath = path20.join(resolvedDocsDir, "agents");
|
|
2369
|
+
const featuresPath = path20.join(resolvedDocsDir, "features");
|
|
2354
2370
|
if (await fs15.pathExists(agentsPath) && await fs15.pathExists(featuresPath)) {
|
|
2355
2371
|
const inferredComponents = await inferComponentsFromFeaturesDir(resolvedDocsDir);
|
|
2356
2372
|
const projectType = inferredComponents.length > 0 ? "multi" : "single";
|
|
2357
2373
|
const components = projectType === "multi" ? resolveProjectComponents("multi", inferredComponents) : void 0;
|
|
2358
2374
|
const langProbeCandidates = [
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2375
|
+
path20.join(agentsPath, "custom.md"),
|
|
2376
|
+
path20.join(agentsPath, "constitution.md"),
|
|
2377
|
+
path20.join(agentsPath, "agents.md")
|
|
2362
2378
|
];
|
|
2363
2379
|
let lang = "en";
|
|
2364
2380
|
for (const candidate of langProbeCandidates) {
|
|
@@ -2424,13 +2440,13 @@ async function patchMarkdownIfExists(filePath, transform) {
|
|
|
2424
2440
|
await fs15.writeFile(filePath, transform(content), "utf-8");
|
|
2425
2441
|
}
|
|
2426
2442
|
async function applyLocalWorkflowTemplateToFeatureDir(featureDir, lang) {
|
|
2427
|
-
await patchMarkdownIfExists(
|
|
2443
|
+
await patchMarkdownIfExists(path20.join(featureDir, "spec.md"), sanitizeSpecForLocal);
|
|
2428
2444
|
await patchMarkdownIfExists(
|
|
2429
|
-
|
|
2445
|
+
path20.join(featureDir, "tasks.md"),
|
|
2430
2446
|
(content) => sanitizeTasksForLocal(content, lang)
|
|
2431
2447
|
);
|
|
2432
|
-
await fs15.remove(
|
|
2433
|
-
await fs15.remove(
|
|
2448
|
+
await fs15.remove(path20.join(featureDir, "issue.md"));
|
|
2449
|
+
await fs15.remove(path20.join(featureDir, "pr.md"));
|
|
2434
2450
|
}
|
|
2435
2451
|
|
|
2436
2452
|
// src/commands/feature.ts
|
|
@@ -2575,19 +2591,19 @@ async function runFeature(name, options) {
|
|
|
2575
2591
|
}
|
|
2576
2592
|
let featuresDir;
|
|
2577
2593
|
if (projectType === "multi") {
|
|
2578
|
-
featuresDir =
|
|
2594
|
+
featuresDir = path20.join(docsDir, "features", component);
|
|
2579
2595
|
} else {
|
|
2580
|
-
featuresDir =
|
|
2596
|
+
featuresDir = path20.join(docsDir, "features");
|
|
2581
2597
|
}
|
|
2582
2598
|
const featureFolderName = `${featureId}-${name}`;
|
|
2583
|
-
const featureDir =
|
|
2599
|
+
const featureDir = path20.join(featuresDir, featureFolderName);
|
|
2584
2600
|
if (await fs15.pathExists(featureDir)) {
|
|
2585
2601
|
throw createCliError(
|
|
2586
2602
|
"INVALID_ARGUMENT",
|
|
2587
2603
|
tr(lang, "cli", "feature.folderExists", { path: featureDir })
|
|
2588
2604
|
);
|
|
2589
2605
|
}
|
|
2590
|
-
const featureBasePath =
|
|
2606
|
+
const featureBasePath = path20.join(
|
|
2591
2607
|
getTemplatesDir(),
|
|
2592
2608
|
lang,
|
|
2593
2609
|
"common",
|
|
@@ -2646,7 +2662,7 @@ async function runFeature(name, options) {
|
|
|
2646
2662
|
featureName: name,
|
|
2647
2663
|
component: projectType === "multi" ? component : void 0,
|
|
2648
2664
|
featurePath: featureDir,
|
|
2649
|
-
featurePathFromDocs:
|
|
2665
|
+
featurePathFromDocs: path20.relative(docsDir, featureDir)
|
|
2650
2666
|
};
|
|
2651
2667
|
},
|
|
2652
2668
|
{ owner: "feature" }
|
|
@@ -2658,9 +2674,9 @@ function sleep2(ms) {
|
|
|
2658
2674
|
async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
|
|
2659
2675
|
const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
|
|
2660
2676
|
const candidates = [
|
|
2661
|
-
...explicitDocsDir ? [
|
|
2662
|
-
|
|
2663
|
-
|
|
2677
|
+
...explicitDocsDir ? [path20.resolve(explicitDocsDir)] : [],
|
|
2678
|
+
path20.resolve(cwd, "docs"),
|
|
2679
|
+
path20.resolve(cwd)
|
|
2664
2680
|
];
|
|
2665
2681
|
const endAt = Date.now() + timeoutMs;
|
|
2666
2682
|
while (Date.now() < endAt) {
|
|
@@ -2687,11 +2703,11 @@ async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
|
|
|
2687
2703
|
return getConfig(cwd);
|
|
2688
2704
|
}
|
|
2689
2705
|
async function getNextFeatureId(docsDir, projectType, components) {
|
|
2690
|
-
const featuresDir =
|
|
2706
|
+
const featuresDir = path20.join(docsDir, "features");
|
|
2691
2707
|
let max = 0;
|
|
2692
2708
|
const scanDirs = [];
|
|
2693
2709
|
if (projectType === "multi") {
|
|
2694
|
-
scanDirs.push(...components.map((component) =>
|
|
2710
|
+
scanDirs.push(...components.map((component) => path20.join(featuresDir, component)));
|
|
2695
2711
|
} else {
|
|
2696
2712
|
scanDirs.push(featuresDir);
|
|
2697
2713
|
}
|
|
@@ -2840,6 +2856,9 @@ function getPrReviewRemoteBlockReasons(feature, lang) {
|
|
|
2840
2856
|
const remote = feature.pr.remote;
|
|
2841
2857
|
if (!remote || !remote.available) return [];
|
|
2842
2858
|
const reasons = [];
|
|
2859
|
+
if (remote.state === "CLOSED" && !remote.isMerged) {
|
|
2860
|
+
reasons.push(tr(lang, "messages", "prReviewRemoteReasonClosed"));
|
|
2861
|
+
}
|
|
2843
2862
|
if (remote.hasBlockingReview) {
|
|
2844
2863
|
reasons.push(tr(lang, "messages", "prReviewRemoteReasonChangesRequested"));
|
|
2845
2864
|
}
|
|
@@ -2905,14 +2924,90 @@ function normalizeTaskTopic(value) {
|
|
|
2905
2924
|
function normalizeCommitSubjectForGate(value) {
|
|
2906
2925
|
return normalizeCommitTopicText(value).replace(/^[a-z]+(?:\([^)]*\))?!?:\s*/i, "").toLowerCase();
|
|
2907
2926
|
}
|
|
2927
|
+
function toTaskKey(rawTitle) {
|
|
2928
|
+
const trimmed = normalizeCommitTopicText(rawTitle);
|
|
2929
|
+
if (!trimmed) return "";
|
|
2930
|
+
const idMatch = trimmed.match(/^(T-[A-Za-z0-9-]+)/i);
|
|
2931
|
+
if (idMatch) return idMatch[1].toUpperCase();
|
|
2932
|
+
return normalizeTaskTopic(trimmed).toLowerCase();
|
|
2933
|
+
}
|
|
2934
|
+
function countDoneTransitionsInLatestTasksCommit(feature) {
|
|
2935
|
+
const docsGitCwd = feature.git.docsGitCwd;
|
|
2936
|
+
const tasksRelativePath = normalizeGitRelativePath(
|
|
2937
|
+
path20.join(feature.docs.featurePathFromDocs, "tasks.md")
|
|
2938
|
+
);
|
|
2939
|
+
const diff = readGitText(docsGitCwd, [
|
|
2940
|
+
"diff",
|
|
2941
|
+
"--unified=0",
|
|
2942
|
+
"--no-color",
|
|
2943
|
+
"HEAD~1",
|
|
2944
|
+
"HEAD",
|
|
2945
|
+
"--",
|
|
2946
|
+
tasksRelativePath
|
|
2947
|
+
]);
|
|
2948
|
+
if (diff === void 0) return 0;
|
|
2949
|
+
if (!diff.trim()) return 0;
|
|
2950
|
+
const removedByTask = /* @__PURE__ */ new Map();
|
|
2951
|
+
const addedByTask = /* @__PURE__ */ new Map();
|
|
2952
|
+
const parseTaskLine = (line) => {
|
|
2953
|
+
const match = line.match(/^\s*-\s*\[(TODO|DOING|DONE|REVIEW)\]\s+(.+?)\s*$/i);
|
|
2954
|
+
if (!match) return null;
|
|
2955
|
+
const key = toTaskKey(match[2]);
|
|
2956
|
+
if (!key) return null;
|
|
2957
|
+
return {
|
|
2958
|
+
key,
|
|
2959
|
+
status: match[1].toUpperCase()
|
|
2960
|
+
};
|
|
2961
|
+
};
|
|
2962
|
+
for (const line of diff.split("\n")) {
|
|
2963
|
+
if (line.startsWith("---") || line.startsWith("+++")) continue;
|
|
2964
|
+
if (line.startsWith("-")) {
|
|
2965
|
+
const parsed = parseTaskLine(line.slice(1));
|
|
2966
|
+
if (!parsed) continue;
|
|
2967
|
+
const existing = removedByTask.get(parsed.key) || /* @__PURE__ */ new Set();
|
|
2968
|
+
existing.add(parsed.status);
|
|
2969
|
+
removedByTask.set(parsed.key, existing);
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
if (line.startsWith("+")) {
|
|
2973
|
+
const parsed = parseTaskLine(line.slice(1));
|
|
2974
|
+
if (!parsed) continue;
|
|
2975
|
+
const existing = addedByTask.get(parsed.key) || /* @__PURE__ */ new Set();
|
|
2976
|
+
existing.add(parsed.status);
|
|
2977
|
+
addedByTask.set(parsed.key, existing);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
let doneTransitions = 0;
|
|
2981
|
+
for (const [taskKey, addedStatuses] of addedByTask.entries()) {
|
|
2982
|
+
if (!addedStatuses.has("DONE")) continue;
|
|
2983
|
+
const removedStatuses = removedByTask.get(taskKey);
|
|
2984
|
+
if (!removedStatuses) continue;
|
|
2985
|
+
const transitionedFromOpen = removedStatuses.has("TODO") || removedStatuses.has("DOING") || removedStatuses.has("REVIEW");
|
|
2986
|
+
if (transitionedFromOpen) {
|
|
2987
|
+
doneTransitions += 1;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
return doneTransitions;
|
|
2991
|
+
}
|
|
2908
2992
|
function checkTaskCommitGate(feature) {
|
|
2993
|
+
const doneTransitions = countDoneTransitionsInLatestTasksCommit(feature);
|
|
2994
|
+
if (doneTransitions === 0) {
|
|
2995
|
+
return { pass: true, doneTransitions };
|
|
2996
|
+
}
|
|
2997
|
+
if (typeof doneTransitions === "number" && doneTransitions > 1) {
|
|
2998
|
+
return {
|
|
2999
|
+
pass: false,
|
|
3000
|
+
reason: "DONE_TRANSITIONS_COUNT",
|
|
3001
|
+
doneTransitions
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
2909
3004
|
const projectGitCwd = feature.git.projectGitCwd;
|
|
2910
3005
|
const lastDoneTopic = normalizeTaskTopic(feature.lastDoneTask?.title || "");
|
|
2911
3006
|
if (!projectGitCwd || !lastDoneTopic) {
|
|
2912
3007
|
return { pass: true };
|
|
2913
3008
|
}
|
|
2914
3009
|
const args = ["log", "-n", "1", "--pretty=%s", "--", "."];
|
|
2915
|
-
const relativeDocsDir =
|
|
3010
|
+
const relativeDocsDir = path20.relative(projectGitCwd, feature.git.docsGitCwd);
|
|
2916
3011
|
const normalizedDocsDir = normalizeGitRelativePath(relativeDocsDir);
|
|
2917
3012
|
if (normalizedDocsDir && normalizedDocsDir !== "." && normalizedDocsDir !== ".." && !normalizedDocsDir.startsWith("../")) {
|
|
2918
3013
|
args.push(`:(exclude)${normalizedDocsDir}/**`);
|
|
@@ -2933,6 +3028,10 @@ function checkTaskCommitGate(feature) {
|
|
|
2933
3028
|
}
|
|
2934
3029
|
function getTaskCommitGateReasonText(lang, check) {
|
|
2935
3030
|
switch (check.reason) {
|
|
3031
|
+
case "DONE_TRANSITIONS_COUNT":
|
|
3032
|
+
return tr(lang, "messages", "taskCommitGateReasonDoneCount", {
|
|
3033
|
+
count: check.doneTransitions || 0
|
|
3034
|
+
});
|
|
2936
3035
|
case "NO_PROJECT_COMMIT":
|
|
2937
3036
|
return tr(lang, "messages", "taskCommitGateReasonNoTasksCommit");
|
|
2938
3037
|
case "PROJECT_LOG_UNAVAILABLE":
|
|
@@ -3746,6 +3845,16 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
|
|
|
3746
3845
|
];
|
|
3747
3846
|
}
|
|
3748
3847
|
if (f.pr.status === "Review") {
|
|
3848
|
+
if (f.pr.remote?.available && f.pr.remote.isMerged) {
|
|
3849
|
+
return [
|
|
3850
|
+
{
|
|
3851
|
+
type: "instruction",
|
|
3852
|
+
category: "pr_status_update",
|
|
3853
|
+
requiresUserCheck: true,
|
|
3854
|
+
message: tr(lang, "messages", "prReviewMergedSyncStatus")
|
|
3855
|
+
}
|
|
3856
|
+
];
|
|
3857
|
+
}
|
|
3749
3858
|
if (!f.docs.prReviewFindingsFieldExists) {
|
|
3750
3859
|
return [
|
|
3751
3860
|
{
|
|
@@ -3787,6 +3896,7 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
|
|
|
3787
3896
|
];
|
|
3788
3897
|
}
|
|
3789
3898
|
const remoteBlockReasons = getPrReviewRemoteBlockReasons(f, lang);
|
|
3899
|
+
const remoteUnavailable = workflowPolicy.mode === "github" && !!f.pr.link && (!f.pr.remote || !f.pr.remote.available);
|
|
3790
3900
|
const actions = [
|
|
3791
3901
|
{
|
|
3792
3902
|
type: "instruction",
|
|
@@ -3814,16 +3924,41 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
|
|
|
3814
3924
|
})
|
|
3815
3925
|
});
|
|
3816
3926
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3927
|
+
if (remoteBlockReasons.length > 0 || remoteUnavailable) {
|
|
3928
|
+
const reasons = [...remoteBlockReasons];
|
|
3929
|
+
if (remoteUnavailable) {
|
|
3930
|
+
reasons.push(tr(lang, "messages", "prReviewRemoteReasonUnavailable"));
|
|
3931
|
+
}
|
|
3932
|
+
actions.push({
|
|
3933
|
+
type: "instruction",
|
|
3934
|
+
category: "code_review",
|
|
3935
|
+
requiresUserCheck: true,
|
|
3936
|
+
message: tr(lang, "messages", "prReviewRemoteBlocked", {
|
|
3937
|
+
reasons: reasons.join("; ")
|
|
3938
|
+
})
|
|
3939
|
+
});
|
|
3940
|
+
} else if (f.git.docsGitCwd) {
|
|
3941
|
+
actions.push({
|
|
3942
|
+
type: "command",
|
|
3943
|
+
category: "code_review",
|
|
3944
|
+
requiresUserCheck: true,
|
|
3945
|
+
operationType: "remote",
|
|
3946
|
+
scope: "docs",
|
|
3947
|
+
cwd: f.git.docsGitCwd,
|
|
3948
|
+
cmd: tr(lang, "messages", "prReviewMergeCommand", {
|
|
3949
|
+
featureRef: f.id || f.folderName
|
|
3950
|
+
})
|
|
3951
|
+
});
|
|
3952
|
+
} else {
|
|
3953
|
+
actions.push({
|
|
3954
|
+
type: "instruction",
|
|
3955
|
+
category: "code_review",
|
|
3956
|
+
requiresUserCheck: true,
|
|
3957
|
+
message: tr(lang, "messages", "prReviewMerge", {
|
|
3958
|
+
featureRef: f.id || f.folderName
|
|
3959
|
+
})
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3827
3962
|
return actions;
|
|
3828
3963
|
}
|
|
3829
3964
|
return [
|
|
@@ -3901,14 +4036,32 @@ function applyApprovalPolicy(step, actions, approval) {
|
|
|
3901
4036
|
return { ...a, requiresUserCheck };
|
|
3902
4037
|
});
|
|
3903
4038
|
}
|
|
4039
|
+
function withUserRequestReplanOption(actions, lang) {
|
|
4040
|
+
if (actions.some((action) => action.category === "user_request_replan")) {
|
|
4041
|
+
return actions;
|
|
4042
|
+
}
|
|
4043
|
+
return [
|
|
4044
|
+
...actions,
|
|
4045
|
+
{
|
|
4046
|
+
type: "instruction",
|
|
4047
|
+
category: "user_request_replan",
|
|
4048
|
+
requiresUserCheck: true,
|
|
4049
|
+
message: tr(lang, "messages", "userRequestReplan")
|
|
4050
|
+
}
|
|
4051
|
+
];
|
|
4052
|
+
}
|
|
3904
4053
|
function resolveFeatureProgress(feature, stepDefinitions, lang, approval) {
|
|
3905
4054
|
const ordered = [...stepDefinitions].sort((a, b) => a.step - b.step);
|
|
3906
4055
|
for (const definition of ordered) {
|
|
3907
4056
|
if (!definition.current) continue;
|
|
3908
4057
|
if (definition.current.when(feature)) {
|
|
4058
|
+
const currentActions = withUserRequestReplanOption(
|
|
4059
|
+
definition.current.actions(feature),
|
|
4060
|
+
lang
|
|
4061
|
+
);
|
|
3909
4062
|
const actions = applyApprovalPolicy(
|
|
3910
4063
|
definition.step,
|
|
3911
|
-
|
|
4064
|
+
currentActions,
|
|
3912
4065
|
approval
|
|
3913
4066
|
);
|
|
3914
4067
|
return {
|
|
@@ -4034,6 +4187,7 @@ function isGitPathIgnored(cwd, relativePath) {
|
|
|
4034
4187
|
return void 0;
|
|
4035
4188
|
}
|
|
4036
4189
|
}
|
|
4190
|
+
var GIT_WORKTREE_CACHE = /* @__PURE__ */ new Map();
|
|
4037
4191
|
function getGitTopLevel(cwd) {
|
|
4038
4192
|
try {
|
|
4039
4193
|
return execSync("git rev-parse --show-toplevel", {
|
|
@@ -4045,6 +4199,52 @@ function getGitTopLevel(cwd) {
|
|
|
4045
4199
|
return null;
|
|
4046
4200
|
}
|
|
4047
4201
|
}
|
|
4202
|
+
function listGitWorktrees(cwd) {
|
|
4203
|
+
const topLevel = getGitTopLevel(cwd) || cwd;
|
|
4204
|
+
const cacheKey = path20.resolve(topLevel);
|
|
4205
|
+
const cached = GIT_WORKTREE_CACHE.get(cacheKey);
|
|
4206
|
+
if (cached) return cached;
|
|
4207
|
+
try {
|
|
4208
|
+
const out = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
4209
|
+
cwd: topLevel,
|
|
4210
|
+
encoding: "utf-8",
|
|
4211
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4212
|
+
});
|
|
4213
|
+
const entries = [];
|
|
4214
|
+
let current = null;
|
|
4215
|
+
for (const rawLine of out.split("\n")) {
|
|
4216
|
+
const line = rawLine.trim();
|
|
4217
|
+
if (!line) {
|
|
4218
|
+
if (current?.path) entries.push(current);
|
|
4219
|
+
current = null;
|
|
4220
|
+
continue;
|
|
4221
|
+
}
|
|
4222
|
+
if (line.startsWith("worktree ")) {
|
|
4223
|
+
if (current?.path) entries.push(current);
|
|
4224
|
+
current = { path: line.slice("worktree ".length).trim() };
|
|
4225
|
+
continue;
|
|
4226
|
+
}
|
|
4227
|
+
if (line.startsWith("branch ")) {
|
|
4228
|
+
if (!current) continue;
|
|
4229
|
+
const fullRef = line.slice("branch ".length).trim();
|
|
4230
|
+
current.branch = fullRef.replace(/^refs\/heads\//, "");
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
if (current?.path) entries.push(current);
|
|
4234
|
+
GIT_WORKTREE_CACHE.set(cacheKey, entries);
|
|
4235
|
+
return entries;
|
|
4236
|
+
} catch {
|
|
4237
|
+
return void 0;
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
function findWorktreePathForBranch(cwd, branchName) {
|
|
4241
|
+
const target = branchName.trim();
|
|
4242
|
+
if (!target) return void 0;
|
|
4243
|
+
const entries = listGitWorktrees(cwd);
|
|
4244
|
+
if (!entries) return void 0;
|
|
4245
|
+
const match = entries.find((entry) => entry.branch === target);
|
|
4246
|
+
return match?.path;
|
|
4247
|
+
}
|
|
4048
4248
|
function resolveProjectGitCwd(config, repo, lang = config.lang ?? DEFAULT_LANG) {
|
|
4049
4249
|
const docsRepo = config.docsRepo;
|
|
4050
4250
|
if (docsRepo !== "standalone") {
|
|
@@ -4198,13 +4398,13 @@ function parsePrLink(value) {
|
|
|
4198
4398
|
return trimmed;
|
|
4199
4399
|
}
|
|
4200
4400
|
function normalizeGitPath(value) {
|
|
4201
|
-
return value.split(
|
|
4401
|
+
return value.split(path20.sep).join("/");
|
|
4202
4402
|
}
|
|
4203
4403
|
function resolveProjectStatusPaths(projectGitCwd, docsDir) {
|
|
4204
|
-
const relativeDocsDir =
|
|
4404
|
+
const relativeDocsDir = path20.relative(projectGitCwd, docsDir);
|
|
4205
4405
|
if (!relativeDocsDir) return [];
|
|
4206
|
-
if (
|
|
4207
|
-
if (relativeDocsDir === ".." || relativeDocsDir.startsWith(`..${
|
|
4406
|
+
if (path20.isAbsolute(relativeDocsDir)) return [];
|
|
4407
|
+
if (relativeDocsDir === ".." || relativeDocsDir.startsWith(`..${path20.sep}`)) {
|
|
4208
4408
|
return [];
|
|
4209
4409
|
}
|
|
4210
4410
|
const normalizedDocsDir = normalizeGitPath(relativeDocsDir).replace(/\/+$/, "");
|
|
@@ -4226,6 +4426,27 @@ function uniqueNormalizedPaths(values) {
|
|
|
4226
4426
|
var PROJECT_DIRTY_STATUS_CACHE = /* @__PURE__ */ new Map();
|
|
4227
4427
|
var COMPONENT_STATUS_PATH_CACHE = /* @__PURE__ */ new Map();
|
|
4228
4428
|
var PR_REMOTE_STATUS_CACHE = /* @__PURE__ */ new Map();
|
|
4429
|
+
var FEATURE_WORKTREE_CACHE = /* @__PURE__ */ new Map();
|
|
4430
|
+
function resolveFeatureWorktreePath(projectGitCwd, issueNumber, slug, folderName) {
|
|
4431
|
+
const expectedBranches = [
|
|
4432
|
+
`feat/${issueNumber}-${slug}`,
|
|
4433
|
+
`feat/${issueNumber}-${folderName}`
|
|
4434
|
+
];
|
|
4435
|
+
for (const branchName of expectedBranches) {
|
|
4436
|
+
const cacheKey = `${projectGitCwd}::${branchName}`;
|
|
4437
|
+
let foundPath = FEATURE_WORKTREE_CACHE.get(cacheKey);
|
|
4438
|
+
if (typeof foundPath === "undefined") {
|
|
4439
|
+
foundPath = findWorktreePathForBranch(projectGitCwd, branchName) || null;
|
|
4440
|
+
FEATURE_WORKTREE_CACHE.set(cacheKey, foundPath);
|
|
4441
|
+
}
|
|
4442
|
+
if (!foundPath) continue;
|
|
4443
|
+
return {
|
|
4444
|
+
cwd: foundPath,
|
|
4445
|
+
branch: getCurrentBranch(foundPath) || branchName
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
return void 0;
|
|
4449
|
+
}
|
|
4229
4450
|
function toUpperToken(value) {
|
|
4230
4451
|
if (typeof value !== "string") return void 0;
|
|
4231
4452
|
const normalized = value.trim().toUpperCase();
|
|
@@ -4268,7 +4489,7 @@ function resolvePrRemoteStatus(prRef, projectGitCwd) {
|
|
|
4268
4489
|
"view",
|
|
4269
4490
|
prRef,
|
|
4270
4491
|
"--json",
|
|
4271
|
-
"reviewDecision,mergeStateStatus,isDraft,statusCheckRollup"
|
|
4492
|
+
"state,mergedAt,reviewDecision,mergeStateStatus,isDraft,statusCheckRollup"
|
|
4272
4493
|
],
|
|
4273
4494
|
{
|
|
4274
4495
|
cwd: projectGitCwd,
|
|
@@ -4283,6 +4504,9 @@ function resolvePrRemoteStatus(prRef, projectGitCwd) {
|
|
|
4283
4504
|
return null;
|
|
4284
4505
|
}
|
|
4285
4506
|
const parsed = JSON.parse(raw);
|
|
4507
|
+
const state = toUpperToken(parsed.state);
|
|
4508
|
+
const mergedAt = typeof parsed.mergedAt === "string" && parsed.mergedAt.trim().length > 0 ? parsed.mergedAt.trim() : void 0;
|
|
4509
|
+
const isMerged = state === "MERGED" || !!mergedAt;
|
|
4286
4510
|
const reviewDecision = toUpperToken(parsed.reviewDecision);
|
|
4287
4511
|
const mergeStateStatus = toUpperToken(parsed.mergeStateStatus);
|
|
4288
4512
|
const isDraft = parsed.isDraft === true;
|
|
@@ -4297,11 +4521,14 @@ function resolvePrRemoteStatus(prRef, projectGitCwd) {
|
|
|
4297
4521
|
const remote = {
|
|
4298
4522
|
source: "gh",
|
|
4299
4523
|
available: true,
|
|
4524
|
+
state,
|
|
4525
|
+
mergedAt,
|
|
4526
|
+
isMerged,
|
|
4300
4527
|
reviewDecision,
|
|
4301
4528
|
mergeStateStatus,
|
|
4302
4529
|
isDraft,
|
|
4303
4530
|
hasBlockingReview: reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED",
|
|
4304
|
-
mergeBlocked: isDraft || isMergeBlockedState(mergeStateStatus),
|
|
4531
|
+
mergeBlocked: !isMerged && (isDraft || isMergeBlockedState(mergeStateStatus)),
|
|
4305
4532
|
failingChecks,
|
|
4306
4533
|
pendingChecks
|
|
4307
4534
|
};
|
|
@@ -4325,10 +4552,10 @@ async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
|
|
|
4325
4552
|
const normalizedCandidates = uniqueNormalizedPaths(
|
|
4326
4553
|
candidates.map((candidate) => {
|
|
4327
4554
|
if (!candidate) return "";
|
|
4328
|
-
if (!
|
|
4329
|
-
const relative =
|
|
4555
|
+
if (!path20.isAbsolute(candidate)) return candidate;
|
|
4556
|
+
const relative = path20.relative(projectGitCwd, candidate);
|
|
4330
4557
|
if (!relative) return "";
|
|
4331
|
-
if (relative === ".." || relative.startsWith(`..${
|
|
4558
|
+
if (relative === ".." || relative.startsWith(`..${path20.sep}`)) return "";
|
|
4332
4559
|
return relative;
|
|
4333
4560
|
}).filter(Boolean)
|
|
4334
4561
|
);
|
|
@@ -4341,7 +4568,7 @@ async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
|
|
|
4341
4568
|
if (cached) return [...cached];
|
|
4342
4569
|
const existing = [];
|
|
4343
4570
|
for (const candidate of normalizedCandidates) {
|
|
4344
|
-
if (await fs15.pathExists(
|
|
4571
|
+
if (await fs15.pathExists(path20.join(projectGitCwd, candidate))) {
|
|
4345
4572
|
existing.push(candidate);
|
|
4346
4573
|
}
|
|
4347
4574
|
}
|
|
@@ -4430,15 +4657,15 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
4430
4657
|
const lang = options.lang;
|
|
4431
4658
|
const workflowPolicy = resolveWorkflowPolicy(options.workflow);
|
|
4432
4659
|
const prePrReviewPolicy = resolvePrePrReviewPolicy(options.workflow);
|
|
4433
|
-
const folderName =
|
|
4660
|
+
const folderName = path20.basename(featurePath);
|
|
4434
4661
|
const match = folderName.match(/^(F\d+)-(.+)$/);
|
|
4435
4662
|
const id = match?.[1];
|
|
4436
4663
|
const slug = match?.[2] || folderName;
|
|
4437
|
-
const specPath =
|
|
4438
|
-
const planPath =
|
|
4439
|
-
const tasksPath =
|
|
4440
|
-
const issueDocPath =
|
|
4441
|
-
const prDocPath =
|
|
4664
|
+
const specPath = path20.join(featurePath, "spec.md");
|
|
4665
|
+
const planPath = path20.join(featurePath, "plan.md");
|
|
4666
|
+
const tasksPath = path20.join(featurePath, "tasks.md");
|
|
4667
|
+
const issueDocPath = path20.join(featurePath, "issue.md");
|
|
4668
|
+
const prDocPath = path20.join(featurePath, "pr.md");
|
|
4442
4669
|
let specStatus;
|
|
4443
4670
|
let issueNumber;
|
|
4444
4671
|
const specExists = await fs15.pathExists(specPath);
|
|
@@ -4449,6 +4676,30 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
4449
4676
|
const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
|
|
4450
4677
|
issueNumber = parseIssueNumber(issueValue);
|
|
4451
4678
|
}
|
|
4679
|
+
let effectiveProjectGitCwd = context.projectGitCwd;
|
|
4680
|
+
let effectiveProjectBranch = context.projectBranch;
|
|
4681
|
+
let effectiveProjectBranchAvailable = context.projectBranchAvailable;
|
|
4682
|
+
if (effectiveProjectGitCwd && issueNumber) {
|
|
4683
|
+
const alreadyExpected = isExpectedFeatureBranch(
|
|
4684
|
+
effectiveProjectBranch,
|
|
4685
|
+
issueNumber,
|
|
4686
|
+
slug,
|
|
4687
|
+
folderName
|
|
4688
|
+
);
|
|
4689
|
+
if (!alreadyExpected) {
|
|
4690
|
+
const worktree = resolveFeatureWorktreePath(
|
|
4691
|
+
effectiveProjectGitCwd,
|
|
4692
|
+
issueNumber,
|
|
4693
|
+
slug,
|
|
4694
|
+
folderName
|
|
4695
|
+
);
|
|
4696
|
+
if (worktree) {
|
|
4697
|
+
effectiveProjectGitCwd = worktree.cwd;
|
|
4698
|
+
effectiveProjectBranch = worktree.branch;
|
|
4699
|
+
effectiveProjectBranchAvailable = true;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4452
4703
|
let planStatus;
|
|
4453
4704
|
const planExists = await fs15.pathExists(planPath);
|
|
4454
4705
|
if (planExists) {
|
|
@@ -4588,20 +4839,20 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
4588
4839
|
prDocPrFieldExists = hasAnySpecKey(content, ["PR", "Pull Request"]);
|
|
4589
4840
|
prDocReviewStatusFieldExists = hasAnySpecKey(content, ["PR \uC0C1\uD0DC", "PR Status"]);
|
|
4590
4841
|
}
|
|
4591
|
-
if (workflowPolicy.requireReview && prStatus === "Review" && prLink &&
|
|
4592
|
-
prRemote = resolvePrRemoteStatus(prLink,
|
|
4842
|
+
if (workflowPolicy.requireReview && prStatus === "Review" && prLink && effectiveProjectGitCwd) {
|
|
4843
|
+
prRemote = resolvePrRemoteStatus(prLink, effectiveProjectGitCwd) || void 0;
|
|
4593
4844
|
}
|
|
4594
4845
|
const warnings = [];
|
|
4595
|
-
if (
|
|
4846
|
+
if (effectiveProjectBranchAvailable === false) {
|
|
4596
4847
|
warnings.push(tr(lang, "warnings", "projectBranchUnavailable"));
|
|
4597
4848
|
}
|
|
4598
4849
|
const onExpectedBranch = isExpectedFeatureBranch(
|
|
4599
|
-
|
|
4850
|
+
effectiveProjectBranch,
|
|
4600
4851
|
issueNumber,
|
|
4601
4852
|
slug,
|
|
4602
4853
|
folderName
|
|
4603
4854
|
);
|
|
4604
|
-
const relativeFeaturePathFromDocs =
|
|
4855
|
+
const relativeFeaturePathFromDocs = path20.relative(context.docsDir, featurePath);
|
|
4605
4856
|
const normalizedFeaturePathFromDocs = normalizeGitPath(relativeFeaturePathFromDocs);
|
|
4606
4857
|
const docsPathIgnored = typeof context.docsPathIgnored === "boolean" ? context.docsPathIgnored : isGitPathIgnored(context.docsGitCwd, normalizedFeaturePathFromDocs);
|
|
4607
4858
|
let docsHasUncommittedChanges = typeof context.docsHasUncommittedChanges === "boolean" ? context.docsHasUncommittedChanges : false;
|
|
@@ -4625,10 +4876,10 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
4625
4876
|
}
|
|
4626
4877
|
let projectHasUncommittedChanges = typeof context.projectHasUncommittedChanges === "boolean" ? context.projectHasUncommittedChanges : false;
|
|
4627
4878
|
let projectStatusUnavailable = !!context.projectStatusUnavailable;
|
|
4628
|
-
if (typeof context.projectHasUncommittedChanges !== "boolean" &&
|
|
4879
|
+
if (typeof context.projectHasUncommittedChanges !== "boolean" && effectiveProjectGitCwd) {
|
|
4629
4880
|
const dirtyScopePolicy = resolveCodeDirtyScopePolicy(options.workflow, options.projectType);
|
|
4630
4881
|
const projectCacheKey = JSON.stringify({
|
|
4631
|
-
projectGitCwd:
|
|
4882
|
+
projectGitCwd: effectiveProjectGitCwd,
|
|
4632
4883
|
docsDir: context.docsDir,
|
|
4633
4884
|
type,
|
|
4634
4885
|
dirtyScopePolicy,
|
|
@@ -4642,19 +4893,19 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
4642
4893
|
let projectStatusPaths = [];
|
|
4643
4894
|
if (dirtyScopePolicy === "component" && type !== "single") {
|
|
4644
4895
|
const componentStatusPaths = await resolveComponentStatusPaths(
|
|
4645
|
-
|
|
4896
|
+
effectiveProjectGitCwd,
|
|
4646
4897
|
type,
|
|
4647
4898
|
options.workflow
|
|
4648
4899
|
);
|
|
4649
|
-
projectStatusPaths = componentStatusPaths.length > 0 ? componentStatusPaths : resolveProjectStatusPaths(
|
|
4900
|
+
projectStatusPaths = componentStatusPaths.length > 0 ? componentStatusPaths : resolveProjectStatusPaths(effectiveProjectGitCwd, context.docsDir);
|
|
4650
4901
|
} else {
|
|
4651
4902
|
projectStatusPaths = resolveProjectStatusPaths(
|
|
4652
|
-
|
|
4903
|
+
effectiveProjectGitCwd,
|
|
4653
4904
|
context.docsDir
|
|
4654
4905
|
);
|
|
4655
4906
|
}
|
|
4656
4907
|
const projectStatus = getGitStatusPorcelain(
|
|
4657
|
-
|
|
4908
|
+
effectiveProjectGitCwd,
|
|
4658
4909
|
projectStatusPaths
|
|
4659
4910
|
);
|
|
4660
4911
|
projectStatusUnavailable = projectStatus === void 0;
|
|
@@ -4823,10 +5074,10 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
4823
5074
|
pr: { link: prLink, status: prStatus, remote: prRemote },
|
|
4824
5075
|
git: {
|
|
4825
5076
|
docsBranch: context.docsBranch,
|
|
4826
|
-
projectBranch:
|
|
4827
|
-
projectBranchAvailable:
|
|
5077
|
+
projectBranch: effectiveProjectBranch,
|
|
5078
|
+
projectBranchAvailable: effectiveProjectBranchAvailable,
|
|
4828
5079
|
docsGitCwd: context.docsGitCwd,
|
|
4829
|
-
projectGitCwd:
|
|
5080
|
+
projectGitCwd: effectiveProjectGitCwd,
|
|
4830
5081
|
onExpectedBranch,
|
|
4831
5082
|
docsEverCommitted,
|
|
4832
5083
|
docsHasUncommittedChanges,
|
|
@@ -4999,7 +5250,7 @@ async function scanFeatures(config) {
|
|
|
4999
5250
|
}
|
|
5000
5251
|
}
|
|
5001
5252
|
const relativeFeaturePaths = allFeatureDirs.map(
|
|
5002
|
-
(dir) => normalizeRelPath(
|
|
5253
|
+
(dir) => normalizeRelPath(path20.relative(config.docsDir, dir))
|
|
5003
5254
|
);
|
|
5004
5255
|
const docsGitMeta = buildDocsFeatureGitMeta(config.docsDir, relativeFeaturePaths);
|
|
5005
5256
|
const parseTargets = config.projectType === "single" ? [{ type: "single", dirs: componentFeatureDirs.get("single") || [] }] : resolveProjectComponents(config.projectType, config.components).map((component) => ({
|
|
@@ -5010,7 +5261,7 @@ async function scanFeatures(config) {
|
|
|
5010
5261
|
const parsed = await Promise.all(
|
|
5011
5262
|
target.dirs.map(async (dir) => {
|
|
5012
5263
|
const relativeFeaturePathFromDocs = normalizeRelPath(
|
|
5013
|
-
|
|
5264
|
+
path20.relative(config.docsDir, dir)
|
|
5014
5265
|
);
|
|
5015
5266
|
const docsMeta = docsGitMeta.get(relativeFeaturePathFromDocs);
|
|
5016
5267
|
return parseFeature(
|
|
@@ -5079,13 +5330,13 @@ async function runStatus(options) {
|
|
|
5079
5330
|
);
|
|
5080
5331
|
}
|
|
5081
5332
|
const { docsDir, projectType, projectName, lang } = config;
|
|
5082
|
-
const featuresDir =
|
|
5333
|
+
const featuresDir = path20.join(docsDir, "features");
|
|
5083
5334
|
const scan = await scanFeatures(config);
|
|
5084
5335
|
const features = [];
|
|
5085
5336
|
const idMap = /* @__PURE__ */ new Map();
|
|
5086
5337
|
for (const f of scan.features) {
|
|
5087
5338
|
const id = f.id || "UNKNOWN";
|
|
5088
|
-
const relPath =
|
|
5339
|
+
const relPath = path20.relative(docsDir, f.path);
|
|
5089
5340
|
if (!idMap.has(id)) idMap.set(id, []);
|
|
5090
5341
|
idMap.get(id).push(relPath);
|
|
5091
5342
|
if (!f.docs.specExists || !f.docs.tasksExists) continue;
|
|
@@ -5166,7 +5417,7 @@ async function runStatus(options) {
|
|
|
5166
5417
|
}
|
|
5167
5418
|
console.log();
|
|
5168
5419
|
if (options.write) {
|
|
5169
|
-
const outputPath =
|
|
5420
|
+
const outputPath = path20.join(featuresDir, "status.md");
|
|
5170
5421
|
const date = getLocalDateString();
|
|
5171
5422
|
const content = [
|
|
5172
5423
|
"# Feature Status",
|
|
@@ -5194,7 +5445,7 @@ function escapeRegExp2(value) {
|
|
|
5194
5445
|
}
|
|
5195
5446
|
async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderName) {
|
|
5196
5447
|
try {
|
|
5197
|
-
const specPath =
|
|
5448
|
+
const specPath = path20.join(featureDir, "spec.md");
|
|
5198
5449
|
if (!await fs15.pathExists(specPath)) return fallbackSlug;
|
|
5199
5450
|
const content = await fs15.readFile(specPath, "utf-8");
|
|
5200
5451
|
const keys = ["\uAE30\uB2A5\uBA85", "Feature Name"];
|
|
@@ -5276,10 +5527,10 @@ async function runUpdate(options) {
|
|
|
5276
5527
|
console.log(chalk6.blue(tr(lang, "cli", "update.updatingAgents")));
|
|
5277
5528
|
}
|
|
5278
5529
|
if (agentsMode === "all") {
|
|
5279
|
-
const commonAgentsBase =
|
|
5280
|
-
const targetAgentsBase =
|
|
5281
|
-
const commonAgents =
|
|
5282
|
-
const targetAgents =
|
|
5530
|
+
const commonAgentsBase = path20.join(templatesDir, lang, "common", "agents");
|
|
5531
|
+
const targetAgentsBase = path20.join(docsDir, "agents");
|
|
5532
|
+
const commonAgents = commonAgentsBase;
|
|
5533
|
+
const targetAgents = targetAgentsBase;
|
|
5283
5534
|
const featurePath = projectType === "multi" ? "docs/features/{component}" : "docs/features";
|
|
5284
5535
|
const projectName = config.projectName ?? "{{projectName}}";
|
|
5285
5536
|
const commonReplacements = {
|
|
@@ -5306,7 +5557,7 @@ async function runUpdate(options) {
|
|
|
5306
5557
|
}
|
|
5307
5558
|
console.log(
|
|
5308
5559
|
chalk6.green(
|
|
5309
|
-
` \u2705 ${
|
|
5560
|
+
` \u2705 ${tr(lang, "cli", "update.agentsUpdated")}`
|
|
5310
5561
|
)
|
|
5311
5562
|
);
|
|
5312
5563
|
}
|
|
@@ -5359,7 +5610,7 @@ function normalizeSkillList2(raw) {
|
|
|
5359
5610
|
return [...deduped];
|
|
5360
5611
|
}
|
|
5361
5612
|
async function backfillMissingConfigDefaults(docsDir) {
|
|
5362
|
-
const configPath =
|
|
5613
|
+
const configPath = path20.join(docsDir, ".lee-spec-kit.json");
|
|
5363
5614
|
if (!await fs15.pathExists(configPath)) {
|
|
5364
5615
|
return { changed: false, changedPaths: [] };
|
|
5365
5616
|
}
|
|
@@ -5380,7 +5631,7 @@ async function backfillMissingConfigDefaults(docsDir) {
|
|
|
5380
5631
|
const workflow = raw.workflow;
|
|
5381
5632
|
setIfMissing(workflow, "mode", "github", "workflow.mode");
|
|
5382
5633
|
setIfMissing(workflow, "codeDirtyScope", "auto", "workflow.codeDirtyScope");
|
|
5383
|
-
setIfMissing(workflow, "taskCommitGate", "
|
|
5634
|
+
setIfMissing(workflow, "taskCommitGate", "warn", "workflow.taskCommitGate");
|
|
5384
5635
|
if (!isPlainObject(workflow.prePrReview)) {
|
|
5385
5636
|
workflow.prePrReview = {};
|
|
5386
5637
|
changedPaths.push("workflow.prePrReview");
|
|
@@ -5432,8 +5683,8 @@ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DE
|
|
|
5432
5683
|
const files = await fs15.readdir(sourceDir);
|
|
5433
5684
|
let updatedCount = 0;
|
|
5434
5685
|
for (const file of files) {
|
|
5435
|
-
const sourcePath =
|
|
5436
|
-
const targetPath =
|
|
5686
|
+
const sourcePath = path20.join(sourceDir, file);
|
|
5687
|
+
const targetPath = path20.join(targetDir, file);
|
|
5437
5688
|
const stat = await fs15.stat(sourcePath);
|
|
5438
5689
|
if (stat.isFile()) {
|
|
5439
5690
|
if (protectedFiles.has(file)) {
|
|
@@ -5516,7 +5767,7 @@ function extractPorcelainPaths(line) {
|
|
|
5516
5767
|
function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
|
|
5517
5768
|
const top = getGitTopLevel2(docsDir);
|
|
5518
5769
|
if (!top) return null;
|
|
5519
|
-
const rel =
|
|
5770
|
+
const rel = path20.relative(top, docsDir) || ".";
|
|
5520
5771
|
try {
|
|
5521
5772
|
const output = execFileSync("git", ["status", "--porcelain=v1", "--", rel], {
|
|
5522
5773
|
cwd: top,
|
|
@@ -5528,7 +5779,7 @@ function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
|
|
|
5528
5779
|
}
|
|
5529
5780
|
const ignoredRelPaths = new Set(
|
|
5530
5781
|
ignoredAbsPaths.map(
|
|
5531
|
-
(absPath) => normalizeGitPath2(
|
|
5782
|
+
(absPath) => normalizeGitPath2(path20.relative(top, absPath) || ".")
|
|
5532
5783
|
)
|
|
5533
5784
|
);
|
|
5534
5785
|
const filtered = output.split("\n").filter((line) => {
|
|
@@ -5585,7 +5836,7 @@ ${tr(lang2, "cli", "common.canceled")}`));
|
|
|
5585
5836
|
}
|
|
5586
5837
|
async function runConfig(options) {
|
|
5587
5838
|
const cwd = process.cwd();
|
|
5588
|
-
const targetCwd = options.dir ?
|
|
5839
|
+
const targetCwd = options.dir ? path20.resolve(cwd, options.dir) : cwd;
|
|
5589
5840
|
const config = await getConfig(targetCwd);
|
|
5590
5841
|
if (!config) {
|
|
5591
5842
|
throw createCliError(
|
|
@@ -5593,7 +5844,7 @@ async function runConfig(options) {
|
|
|
5593
5844
|
tr(DEFAULT_LANG, "cli", "common.configNotFound")
|
|
5594
5845
|
);
|
|
5595
5846
|
}
|
|
5596
|
-
const configPath =
|
|
5847
|
+
const configPath = path20.join(config.docsDir, ".lee-spec-kit.json");
|
|
5597
5848
|
if (!options.projectRoot) {
|
|
5598
5849
|
console.log();
|
|
5599
5850
|
console.log(chalk6.blue(tr(config.lang, "cli", "config.currentTitle")));
|
|
@@ -5609,6 +5860,7 @@ async function runConfig(options) {
|
|
|
5609
5860
|
console.log();
|
|
5610
5861
|
return;
|
|
5611
5862
|
}
|
|
5863
|
+
const projectRoot = options.projectRoot;
|
|
5612
5864
|
await withFileLock(
|
|
5613
5865
|
getDocsLockPath(config.docsDir),
|
|
5614
5866
|
async () => {
|
|
@@ -5662,13 +5914,13 @@ async function runConfig(options) {
|
|
|
5662
5914
|
}
|
|
5663
5915
|
assertAllowedComponent(targetComponent, components);
|
|
5664
5916
|
const currentRoot = typeof configFile.projectRoot === "object" && configFile.projectRoot ? configFile.projectRoot : {};
|
|
5665
|
-
currentRoot[targetComponent] =
|
|
5917
|
+
currentRoot[targetComponent] = projectRoot;
|
|
5666
5918
|
configFile.projectRoot = currentRoot;
|
|
5667
5919
|
console.log(
|
|
5668
5920
|
chalk6.green(
|
|
5669
5921
|
tr(config.lang, "cli", "config.projectRootSet", {
|
|
5670
5922
|
repo: targetComponent.toUpperCase(),
|
|
5671
|
-
path:
|
|
5923
|
+
path: projectRoot
|
|
5672
5924
|
})
|
|
5673
5925
|
)
|
|
5674
5926
|
);
|
|
@@ -5679,11 +5931,11 @@ async function runConfig(options) {
|
|
|
5679
5931
|
"`--component` is only valid for multi projectRoot updates."
|
|
5680
5932
|
);
|
|
5681
5933
|
}
|
|
5682
|
-
configFile.projectRoot =
|
|
5934
|
+
configFile.projectRoot = projectRoot;
|
|
5683
5935
|
console.log(
|
|
5684
5936
|
chalk6.green(
|
|
5685
5937
|
tr(config.lang, "cli", "config.projectRootSetSingle", {
|
|
5686
|
-
path:
|
|
5938
|
+
path: projectRoot
|
|
5687
5939
|
})
|
|
5688
5940
|
)
|
|
5689
5941
|
);
|
|
@@ -5752,6 +6004,7 @@ function getActionSummary(action) {
|
|
|
5752
6004
|
if (action.category === "pre_pr_review") return "Run pre-PR self review";
|
|
5753
6005
|
if (action.category === "pr_status_update") return "Update PR status";
|
|
5754
6006
|
if (action.category === "code_review") return "Process code review feedback";
|
|
6007
|
+
if (action.category === "user_request_replan") return "Handle a new user request first";
|
|
5755
6008
|
if (action.category === "task_execute") return "Proceed with task execution";
|
|
5756
6009
|
if (action.category === "review_fix_commit") return "Commit review feedback fixes";
|
|
5757
6010
|
if (action.category === "feature_done") return "Feature is complete";
|
|
@@ -5959,42 +6212,42 @@ var BUILTIN_DOC_DEFINITIONS = [
|
|
|
5959
6212
|
{
|
|
5960
6213
|
id: "agents",
|
|
5961
6214
|
title: { ko: "\uC5D0\uC774\uC804\uD2B8 \uC6B4\uC601 \uADDC\uCE59", en: "Agent Operating Rules" },
|
|
5962
|
-
relativePath: (_, lang) =>
|
|
6215
|
+
relativePath: (_, lang) => path20.join(lang, "common", "agents", "agents.md")
|
|
5963
6216
|
},
|
|
5964
6217
|
{
|
|
5965
6218
|
id: "git-workflow",
|
|
5966
6219
|
title: { ko: "Git \uC6CC\uD06C\uD50C\uB85C\uC6B0", en: "Git Workflow" },
|
|
5967
|
-
relativePath: (_, lang) =>
|
|
6220
|
+
relativePath: (_, lang) => path20.join(lang, "common", "agents", "git-workflow.md")
|
|
5968
6221
|
},
|
|
5969
6222
|
{
|
|
5970
6223
|
id: "issue-doc",
|
|
5971
6224
|
title: { ko: "Issue \uBB38\uC11C \uD15C\uD50C\uB9BF", en: "Issue Document Template" },
|
|
5972
|
-
relativePath: (_, lang) =>
|
|
6225
|
+
relativePath: (_, lang) => path20.join(lang, "common", "features", "feature-base", "issue.md")
|
|
5973
6226
|
},
|
|
5974
6227
|
{
|
|
5975
6228
|
id: "pr-doc",
|
|
5976
6229
|
title: { ko: "PR \uBB38\uC11C \uD15C\uD50C\uB9BF", en: "PR Document Template" },
|
|
5977
|
-
relativePath: (_, lang) =>
|
|
6230
|
+
relativePath: (_, lang) => path20.join(lang, "common", "features", "feature-base", "pr.md")
|
|
5978
6231
|
},
|
|
5979
6232
|
{
|
|
5980
6233
|
id: "create-feature",
|
|
5981
6234
|
title: { ko: "create-feature \uC2A4\uD0AC", en: "create-feature skill" },
|
|
5982
|
-
relativePath: (_, lang) =>
|
|
6235
|
+
relativePath: (_, lang) => path20.join(lang, "common", "agents", "skills", "create-feature.md")
|
|
5983
6236
|
},
|
|
5984
6237
|
{
|
|
5985
6238
|
id: "execute-task",
|
|
5986
6239
|
title: { ko: "execute-task \uC2A4\uD0AC", en: "execute-task skill" },
|
|
5987
|
-
relativePath: (_, lang) =>
|
|
6240
|
+
relativePath: (_, lang) => path20.join(lang, "common", "agents", "skills", "execute-task.md")
|
|
5988
6241
|
},
|
|
5989
6242
|
{
|
|
5990
6243
|
id: "create-issue",
|
|
5991
6244
|
title: { ko: "create-issue \uC2A4\uD0AC", en: "create-issue skill" },
|
|
5992
|
-
relativePath: (_, lang) =>
|
|
6245
|
+
relativePath: (_, lang) => path20.join(lang, "common", "agents", "skills", "create-issue.md")
|
|
5993
6246
|
},
|
|
5994
6247
|
{
|
|
5995
6248
|
id: "create-pr",
|
|
5996
6249
|
title: { ko: "create-pr \uC2A4\uD0AC", en: "create-pr skill" },
|
|
5997
|
-
relativePath: (_, lang) =>
|
|
6250
|
+
relativePath: (_, lang) => path20.join(lang, "common", "agents", "skills", "create-pr.md")
|
|
5998
6251
|
}
|
|
5999
6252
|
];
|
|
6000
6253
|
var DOC_FOLLOWUPS = {
|
|
@@ -6030,7 +6283,8 @@ var CATEGORY_DOC_MAP = {
|
|
|
6030
6283
|
pre_pr_review: ["create-pr"],
|
|
6031
6284
|
pr_create: ["create-pr", "pr-doc", "git-workflow"],
|
|
6032
6285
|
pr_status_update: ["create-pr"],
|
|
6033
|
-
code_review: ["create-pr"]
|
|
6286
|
+
code_review: ["create-pr"],
|
|
6287
|
+
user_request_replan: ["agents", "execute-task"]
|
|
6034
6288
|
};
|
|
6035
6289
|
function getBuiltinDocIds() {
|
|
6036
6290
|
return BUILTIN_DOC_DEFINITIONS.map((doc) => doc.id);
|
|
@@ -6081,7 +6335,7 @@ function listBuiltinDocs(projectType, lang) {
|
|
|
6081
6335
|
id: doc.id,
|
|
6082
6336
|
title: doc.title[lang],
|
|
6083
6337
|
relativePath,
|
|
6084
|
-
absolutePath:
|
|
6338
|
+
absolutePath: path20.join(templatesDir, relativePath)
|
|
6085
6339
|
};
|
|
6086
6340
|
});
|
|
6087
6341
|
}
|
|
@@ -6130,7 +6384,7 @@ function parseApprovalLabel(input, validLabels) {
|
|
|
6130
6384
|
function getApprovalTicketPaths(config) {
|
|
6131
6385
|
return {
|
|
6132
6386
|
runtimePath: getApprovalTicketStorePath(config.docsDir),
|
|
6133
|
-
legacyPath:
|
|
6387
|
+
legacyPath: path20.join(config.docsDir, LEGACY_APPROVAL_TICKET_FILENAME)
|
|
6134
6388
|
};
|
|
6135
6389
|
}
|
|
6136
6390
|
function getApprovalSessionId() {
|
|
@@ -6158,7 +6412,7 @@ async function loadApprovalTicketStore(storePath) {
|
|
|
6158
6412
|
}
|
|
6159
6413
|
}
|
|
6160
6414
|
async function saveApprovalTicketStore(storePath, payload) {
|
|
6161
|
-
await fs15.ensureDir(
|
|
6415
|
+
await fs15.ensureDir(path20.dirname(storePath));
|
|
6162
6416
|
await fs15.writeJson(storePath, payload, { spaces: 2 });
|
|
6163
6417
|
}
|
|
6164
6418
|
async function resolveApprovalTicketStoreAndPath(config, nowMs) {
|
|
@@ -6376,9 +6630,14 @@ function buildSuggestionOptions(lang, state, projectType, selectedComponent) {
|
|
|
6376
6630
|
const showDoneCommand = `npx lee-spec-kit context --done${componentArg}`;
|
|
6377
6631
|
const showAllCommand = `npx lee-spec-kit context --all${componentArg}`;
|
|
6378
6632
|
const showOpenCommand = `npx lee-spec-kit context${componentArg}`;
|
|
6633
|
+
const runOnboardCommand = "npx lee-spec-kit onboard --strict";
|
|
6379
6634
|
const rawSuggestions = [];
|
|
6380
6635
|
switch (state.status) {
|
|
6381
6636
|
case "no_features":
|
|
6637
|
+
rawSuggestions.push({
|
|
6638
|
+
summary: tr(lang, "cli", "context.suggestion.runOnboard"),
|
|
6639
|
+
command: runOnboardCommand
|
|
6640
|
+
});
|
|
6382
6641
|
rawSuggestions.push({
|
|
6383
6642
|
summary: tr(lang, "cli", "context.suggestion.createFeature"),
|
|
6384
6643
|
command: createFeatureCommand
|
|
@@ -6487,7 +6746,10 @@ function getCommandExecutionLockPath(action, config) {
|
|
|
6487
6746
|
return getProjectExecutionLockPath(action.cwd);
|
|
6488
6747
|
}
|
|
6489
6748
|
function contextCommand(program2) {
|
|
6490
|
-
program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option(
|
|
6749
|
+
program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option(
|
|
6750
|
+
"--json-compact",
|
|
6751
|
+
"Output compact JSON for agents (implies --json, reduced duplication)"
|
|
6752
|
+
).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(
|
|
6491
6753
|
"--approve <reply>",
|
|
6492
6754
|
"Approve one labeled option (examples: A, A OK, A proceed, A \uC9C4\uD589\uD574)"
|
|
6493
6755
|
).option(
|
|
@@ -6508,7 +6770,7 @@ function contextCommand(program2) {
|
|
|
6508
6770
|
const lang = config?.lang ?? DEFAULT_LANG;
|
|
6509
6771
|
const cliError = toCliError(error);
|
|
6510
6772
|
const suggestions = getCliErrorSuggestions(cliError.code, lang);
|
|
6511
|
-
if (options.json) {
|
|
6773
|
+
if (options.json || options.jsonCompact) {
|
|
6512
6774
|
console.log(
|
|
6513
6775
|
JSON.stringify({
|
|
6514
6776
|
status: "error",
|
|
@@ -6600,6 +6862,111 @@ function getMultipleFeaturesRecommendation(projectType, selectedComponent) {
|
|
|
6600
6862
|
}
|
|
6601
6863
|
return "Multiple features detected across components. Please specify feature name (slug | F001 | F001-slug) or use --component.";
|
|
6602
6864
|
}
|
|
6865
|
+
function getFeatureRef(feature) {
|
|
6866
|
+
return feature.folderName || `${feature.type}:${feature.slug}`;
|
|
6867
|
+
}
|
|
6868
|
+
function toCompactFeature(feature) {
|
|
6869
|
+
if (!feature) return null;
|
|
6870
|
+
return {
|
|
6871
|
+
ref: getFeatureRef(feature),
|
|
6872
|
+
id: feature.id ?? null,
|
|
6873
|
+
slug: feature.slug,
|
|
6874
|
+
folderName: feature.folderName,
|
|
6875
|
+
type: feature.type,
|
|
6876
|
+
path: feature.path,
|
|
6877
|
+
currentStep: feature.currentStep,
|
|
6878
|
+
nextAction: feature.nextAction,
|
|
6879
|
+
completion: feature.completion,
|
|
6880
|
+
specStatus: feature.specStatus,
|
|
6881
|
+
planStatus: feature.planStatus,
|
|
6882
|
+
tasks: feature.tasks,
|
|
6883
|
+
prePrReview: {
|
|
6884
|
+
status: feature.prePrReview.status,
|
|
6885
|
+
findings: feature.prePrReview.findings,
|
|
6886
|
+
evidenceProvided: feature.prePrReview.evidenceProvided
|
|
6887
|
+
},
|
|
6888
|
+
prReview: {
|
|
6889
|
+
findings: feature.prReview.findings,
|
|
6890
|
+
evidenceProvided: feature.prReview.evidenceProvided
|
|
6891
|
+
},
|
|
6892
|
+
pr: {
|
|
6893
|
+
link: feature.pr.link,
|
|
6894
|
+
status: feature.pr.status,
|
|
6895
|
+
remote: feature.pr.remote
|
|
6896
|
+
},
|
|
6897
|
+
git: {
|
|
6898
|
+
docsBranch: feature.git.docsBranch,
|
|
6899
|
+
projectBranch: feature.git.projectBranch,
|
|
6900
|
+
projectBranchAvailable: feature.git.projectBranchAvailable,
|
|
6901
|
+
onExpectedBranch: feature.git.onExpectedBranch,
|
|
6902
|
+
docsEverCommitted: feature.git.docsEverCommitted,
|
|
6903
|
+
docsHasUncommittedChanges: feature.git.docsHasUncommittedChanges,
|
|
6904
|
+
projectHasUncommittedChanges: feature.git.projectHasUncommittedChanges,
|
|
6905
|
+
docsPathIgnored: feature.git.docsPathIgnored
|
|
6906
|
+
},
|
|
6907
|
+
docs: {
|
|
6908
|
+
specExists: feature.docs.specExists,
|
|
6909
|
+
planExists: feature.docs.planExists,
|
|
6910
|
+
tasksExists: feature.docs.tasksExists,
|
|
6911
|
+
issueDocIssueFieldExists: feature.docs.issueDocIssueFieldExists,
|
|
6912
|
+
prDocPrFieldExists: feature.docs.prDocPrFieldExists,
|
|
6913
|
+
prDocReviewStatusFieldExists: feature.docs.prDocReviewStatusFieldExists,
|
|
6914
|
+
prFieldExists: feature.docs.prFieldExists,
|
|
6915
|
+
prStatusFieldExists: feature.docs.prStatusFieldExists,
|
|
6916
|
+
prePrReviewFieldExists: feature.docs.prePrReviewFieldExists,
|
|
6917
|
+
prePrFindingsFieldExists: feature.docs.prePrFindingsFieldExists,
|
|
6918
|
+
prePrEvidenceFieldExists: feature.docs.prePrEvidenceFieldExists,
|
|
6919
|
+
prReviewFindingsFieldExists: feature.docs.prReviewFindingsFieldExists,
|
|
6920
|
+
prReviewEvidenceFieldExists: feature.docs.prReviewEvidenceFieldExists
|
|
6921
|
+
},
|
|
6922
|
+
warnings: feature.warnings
|
|
6923
|
+
};
|
|
6924
|
+
}
|
|
6925
|
+
function toCompactActionOption(option) {
|
|
6926
|
+
const base = {
|
|
6927
|
+
label: option.label,
|
|
6928
|
+
summary: option.summary,
|
|
6929
|
+
detail: option.detail,
|
|
6930
|
+
approvalPrompt: option.approvalPrompt,
|
|
6931
|
+
actionType: option.action.type,
|
|
6932
|
+
category: option.action.category,
|
|
6933
|
+
operationType: option.action.operationType,
|
|
6934
|
+
requiresUserCheck: !!option.action.requiresUserCheck
|
|
6935
|
+
};
|
|
6936
|
+
if (option.action.type === "command") {
|
|
6937
|
+
base.scope = option.action.scope;
|
|
6938
|
+
base.cwd = option.action.cwd;
|
|
6939
|
+
base.cmd = option.action.cmd;
|
|
6940
|
+
return base;
|
|
6941
|
+
}
|
|
6942
|
+
base.message = option.action.message;
|
|
6943
|
+
return base;
|
|
6944
|
+
}
|
|
6945
|
+
function toCompactSuggestionOption(option) {
|
|
6946
|
+
return {
|
|
6947
|
+
label: option.label,
|
|
6948
|
+
summary: option.summary,
|
|
6949
|
+
command: option.command
|
|
6950
|
+
};
|
|
6951
|
+
}
|
|
6952
|
+
function resolveContextRecommendation(state, projectType, selectedComponent) {
|
|
6953
|
+
if (state.status === "multiple_active") {
|
|
6954
|
+
return getMultipleFeaturesRecommendation(projectType, selectedComponent);
|
|
6955
|
+
}
|
|
6956
|
+
if (state.status === "no_features") {
|
|
6957
|
+
return "No features found. Run onboarding checks first, then create a feature.";
|
|
6958
|
+
}
|
|
6959
|
+
if (state.status === "no_open") {
|
|
6960
|
+
return "No open features found. Use `context --done` to inspect completed features.";
|
|
6961
|
+
}
|
|
6962
|
+
if (state.status === "no_match") {
|
|
6963
|
+
return "No features found.";
|
|
6964
|
+
}
|
|
6965
|
+
if (state.targetFeatures.length === 1) {
|
|
6966
|
+
return state.targetFeatures[0].nextAction;
|
|
6967
|
+
}
|
|
6968
|
+
return "No matched feature.";
|
|
6969
|
+
}
|
|
6603
6970
|
async function runContext(featureName, options) {
|
|
6604
6971
|
const cwd = process.cwd();
|
|
6605
6972
|
const config = await getConfig(cwd);
|
|
@@ -6659,7 +7026,8 @@ async function runContext(featureName, options) {
|
|
|
6659
7026
|
);
|
|
6660
7027
|
return;
|
|
6661
7028
|
}
|
|
6662
|
-
|
|
7029
|
+
const jsonMode = !!options.json || !!options.jsonCompact;
|
|
7030
|
+
if (jsonMode) {
|
|
6663
7031
|
const primaryAction = state.actionOptions[0] ?? null;
|
|
6664
7032
|
const finalApprovalPrompt = buildFinalApprovalPrompt(lang, state.actionOptions);
|
|
6665
7033
|
const approveCommand = buildApprovalCommand(
|
|
@@ -6674,6 +7042,76 @@ async function runContext(featureName, options) {
|
|
|
6674
7042
|
selectedComponent,
|
|
6675
7043
|
true
|
|
6676
7044
|
);
|
|
7045
|
+
const recommendation = resolveContextRecommendation(
|
|
7046
|
+
state,
|
|
7047
|
+
config.projectType,
|
|
7048
|
+
selectedComponent
|
|
7049
|
+
);
|
|
7050
|
+
if (options.jsonCompact) {
|
|
7051
|
+
const compactResult = {
|
|
7052
|
+
schema: "context.v2.compact",
|
|
7053
|
+
status: state.status,
|
|
7054
|
+
reasonCode: toReasonCode(state.status),
|
|
7055
|
+
selectionMode: state.selectionMode,
|
|
7056
|
+
selectionFallback: state.selectionFallback,
|
|
7057
|
+
branches: state.branches,
|
|
7058
|
+
warnings: state.warnings,
|
|
7059
|
+
contextVersion: state.contextVersion,
|
|
7060
|
+
matchedFeature: toCompactFeature(state.matchedFeature),
|
|
7061
|
+
candidateRefs: state.targetFeatures.length > 1 ? state.targetFeatures.map((feature) => getFeatureRef(feature)) : [],
|
|
7062
|
+
completedCandidateRefs: state.selectionMode === "open" ? state.doneFeatures.map((feature) => getFeatureRef(feature)) : [],
|
|
7063
|
+
openCandidateRefs: state.selectionMode === "open" ? state.openFeatures.map((feature) => getFeatureRef(feature)) : [],
|
|
7064
|
+
inProgressCandidateRefs: state.selectionMode === "open" ? state.inProgressFeatures.map((feature) => getFeatureRef(feature)) : [],
|
|
7065
|
+
readyToCloseCandidateRefs: state.selectionMode === "open" ? state.readyToCloseFeatures.map((feature) => getFeatureRef(feature)) : [],
|
|
7066
|
+
actionOptions: state.actionOptions.map((option) => toCompactActionOption(option)),
|
|
7067
|
+
suggestionOptions: suggestionOptions.map(
|
|
7068
|
+
(option) => toCompactSuggestionOption(option)
|
|
7069
|
+
),
|
|
7070
|
+
primaryActionLabel: primaryAction?.label ?? null,
|
|
7071
|
+
workflowPolicy,
|
|
7072
|
+
taskCommitGatePolicy,
|
|
7073
|
+
prePrReviewPolicy,
|
|
7074
|
+
checkPolicy: {
|
|
7075
|
+
docPath: "builtin://agents/policy",
|
|
7076
|
+
token: "<LABEL>",
|
|
7077
|
+
acceptedTokens: ["<LABEL>", "<LABEL> OK", "<LABEL> ...", "... <LABEL> ..."],
|
|
7078
|
+
tokenPattern: "^.*\\b([A-Z]+)\\b.*$",
|
|
7079
|
+
validLabels: state.actionOptions.map((o) => o.label),
|
|
7080
|
+
oneApprovalPerAction: true,
|
|
7081
|
+
requireFreshContext: true,
|
|
7082
|
+
contextVersion: state.contextVersion,
|
|
7083
|
+
config: config.approval ?? { mode: "builtin" }
|
|
7084
|
+
},
|
|
7085
|
+
approvalRequest: {
|
|
7086
|
+
finalPrompt: finalApprovalPrompt,
|
|
7087
|
+
userFacingLines: [
|
|
7088
|
+
...state.actionOptions.map((o) => o.approvalPrompt),
|
|
7089
|
+
finalApprovalPrompt
|
|
7090
|
+
].filter((line) => line.length > 0),
|
|
7091
|
+
labels: state.actionOptions.map((o) => o.label),
|
|
7092
|
+
approveCommand,
|
|
7093
|
+
executeCommand,
|
|
7094
|
+
executeRequiresTicket: !!state.actionOptions[0]?.action?.requiresUserCheck
|
|
7095
|
+
},
|
|
7096
|
+
suggestionRequest: {
|
|
7097
|
+
finalPrompt: suggestionFinalPrompt,
|
|
7098
|
+
userFacingLines: [
|
|
7099
|
+
...suggestionOptions.map((o) => `${o.label}: ${o.summary}`),
|
|
7100
|
+
suggestionFinalPrompt
|
|
7101
|
+
].filter((line) => line.length > 0),
|
|
7102
|
+
labels: suggestionOptions.map((o) => o.label)
|
|
7103
|
+
},
|
|
7104
|
+
prPolicy: {
|
|
7105
|
+
screenshots: {
|
|
7106
|
+
upload: config.pr?.screenshots?.upload ?? false
|
|
7107
|
+
}
|
|
7108
|
+
},
|
|
7109
|
+
requiredDocs,
|
|
7110
|
+
recommendation
|
|
7111
|
+
};
|
|
7112
|
+
console.log(JSON.stringify(compactResult, null, 2));
|
|
7113
|
+
return;
|
|
7114
|
+
}
|
|
6677
7115
|
const result = {
|
|
6678
7116
|
status: state.status,
|
|
6679
7117
|
reasonCode: toReasonCode(state.status),
|
|
@@ -6765,24 +7203,8 @@ async function runContext(featureName, options) {
|
|
|
6765
7203
|
}
|
|
6766
7204
|
},
|
|
6767
7205
|
requiredDocs,
|
|
6768
|
-
recommendation
|
|
7206
|
+
recommendation
|
|
6769
7207
|
};
|
|
6770
|
-
if (result.status === "multiple_active") {
|
|
6771
|
-
result.recommendation = getMultipleFeaturesRecommendation(
|
|
6772
|
-
config.projectType,
|
|
6773
|
-
selectedComponent
|
|
6774
|
-
);
|
|
6775
|
-
} else if (result.status === "no_features") {
|
|
6776
|
-
result.recommendation = "No features found. Create a feature first.";
|
|
6777
|
-
} else if (result.status === "no_open") {
|
|
6778
|
-
result.recommendation = "No open features found. Use `context --done` to inspect completed features.";
|
|
6779
|
-
} else if (result.status === "no_match") {
|
|
6780
|
-
result.recommendation = "No features found.";
|
|
6781
|
-
} else if (state.targetFeatures.length === 1) {
|
|
6782
|
-
result.recommendation = state.targetFeatures[0].nextAction;
|
|
6783
|
-
} else {
|
|
6784
|
-
result.recommendation = "No matched feature.";
|
|
6785
|
-
}
|
|
6786
7208
|
console.log(JSON.stringify(result, null, 2));
|
|
6787
7209
|
return;
|
|
6788
7210
|
}
|
|
@@ -6956,7 +7378,7 @@ async function runContext(featureName, options) {
|
|
|
6956
7378
|
if (f.issueNumber) {
|
|
6957
7379
|
console.log(` \u2022 Issue: #${f.issueNumber}`);
|
|
6958
7380
|
}
|
|
6959
|
-
console.log(` \u2022 Path: ${
|
|
7381
|
+
console.log(` \u2022 Path: ${path20.relative(cwd, f.path)}`);
|
|
6960
7382
|
if (f.git.projectBranch) {
|
|
6961
7383
|
console.log(` \u2022 Project Branch: ${f.git.projectBranch}`);
|
|
6962
7384
|
}
|
|
@@ -7055,6 +7477,7 @@ async function runContext(featureName, options) {
|
|
|
7055
7477
|
async function runApprovedOption(state, config, lang, featureName, selectionOptions, options) {
|
|
7056
7478
|
const approval = options.approve || "";
|
|
7057
7479
|
const ticketToken = (options.ticket || "").trim();
|
|
7480
|
+
const jsonMode = !!options.json || !!options.jsonCompact;
|
|
7058
7481
|
let parsedLabel = null;
|
|
7059
7482
|
if (state.status !== "single_matched" || !state.matchedFeature) {
|
|
7060
7483
|
throw createCliError(
|
|
@@ -7115,7 +7538,7 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
|
|
|
7115
7538
|
label: parsedLabel,
|
|
7116
7539
|
featureRef
|
|
7117
7540
|
}) : null;
|
|
7118
|
-
if (
|
|
7541
|
+
if (jsonMode) {
|
|
7119
7542
|
console.log(
|
|
7120
7543
|
JSON.stringify(
|
|
7121
7544
|
{
|
|
@@ -7194,7 +7617,7 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
|
|
|
7194
7617
|
`Approved label "${parsedLabel}" is instruction-only. Re-run without \`--execute\` or pick a command option.`
|
|
7195
7618
|
);
|
|
7196
7619
|
}
|
|
7197
|
-
if (
|
|
7620
|
+
if (jsonMode) {
|
|
7198
7621
|
console.log(
|
|
7199
7622
|
JSON.stringify(
|
|
7200
7623
|
{
|
|
@@ -7219,7 +7642,7 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
|
|
|
7219
7642
|
console.log();
|
|
7220
7643
|
return;
|
|
7221
7644
|
}
|
|
7222
|
-
if (!
|
|
7645
|
+
if (!jsonMode) {
|
|
7223
7646
|
console.log();
|
|
7224
7647
|
console.log(chalk6.blue(`\u25B6 Executing option ${parsedLabel}...`));
|
|
7225
7648
|
console.log(chalk6.gray(` ${selectedAction.cmd}`));
|
|
@@ -7231,12 +7654,12 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
|
|
|
7231
7654
|
lockPath,
|
|
7232
7655
|
async () => executeCommandAction(
|
|
7233
7656
|
selectedAction.cmd,
|
|
7234
|
-
|
|
7657
|
+
jsonMode,
|
|
7235
7658
|
selectedAction.cwd
|
|
7236
7659
|
),
|
|
7237
7660
|
{ owner: `context-execute:${selectedAction.scope}` }
|
|
7238
7661
|
);
|
|
7239
|
-
if (
|
|
7662
|
+
if (jsonMode) {
|
|
7240
7663
|
console.log(
|
|
7241
7664
|
JSON.stringify(
|
|
7242
7665
|
{
|
|
@@ -7285,7 +7708,7 @@ var FIXABLE_ISSUE_CODES = /* @__PURE__ */ new Set([
|
|
|
7285
7708
|
]);
|
|
7286
7709
|
function formatPath(cwd, p) {
|
|
7287
7710
|
if (!p) return "";
|
|
7288
|
-
return
|
|
7711
|
+
return path20.isAbsolute(p) ? path20.relative(cwd, p) : p;
|
|
7289
7712
|
}
|
|
7290
7713
|
function detectPlaceholders(content) {
|
|
7291
7714
|
const patterns = [
|
|
@@ -7434,7 +7857,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
7434
7857
|
const placeholderContext = {
|
|
7435
7858
|
projectName: config.projectName,
|
|
7436
7859
|
featureName: f.slug,
|
|
7437
|
-
featurePath: f.docs.featurePathFromDocs ||
|
|
7860
|
+
featurePath: f.docs.featurePathFromDocs || path20.relative(config.docsDir, f.path),
|
|
7438
7861
|
repoType: f.type,
|
|
7439
7862
|
featureNumber
|
|
7440
7863
|
};
|
|
@@ -7444,7 +7867,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
7444
7867
|
"tasks.md"
|
|
7445
7868
|
];
|
|
7446
7869
|
for (const file of files) {
|
|
7447
|
-
const fullPath =
|
|
7870
|
+
const fullPath = path20.join(f.path, file);
|
|
7448
7871
|
if (!await fs15.pathExists(fullPath)) continue;
|
|
7449
7872
|
const original = await fs15.readFile(fullPath, "utf-8");
|
|
7450
7873
|
let next = original;
|
|
@@ -7489,7 +7912,7 @@ async function checkDocsStructure(config, cwd) {
|
|
|
7489
7912
|
const issues = [];
|
|
7490
7913
|
const requiredDirs = ["agents", "features", "prd", "designs", "ideas"];
|
|
7491
7914
|
for (const dir of requiredDirs) {
|
|
7492
|
-
const p =
|
|
7915
|
+
const p = path20.join(config.docsDir, dir);
|
|
7493
7916
|
if (!await fs15.pathExists(p)) {
|
|
7494
7917
|
issues.push({
|
|
7495
7918
|
level: "error",
|
|
@@ -7499,7 +7922,7 @@ async function checkDocsStructure(config, cwd) {
|
|
|
7499
7922
|
});
|
|
7500
7923
|
}
|
|
7501
7924
|
}
|
|
7502
|
-
const configPath =
|
|
7925
|
+
const configPath = path20.join(config.docsDir, ".lee-spec-kit.json");
|
|
7503
7926
|
if (!await fs15.pathExists(configPath)) {
|
|
7504
7927
|
issues.push({
|
|
7505
7928
|
level: "warn",
|
|
@@ -7522,7 +7945,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
7522
7945
|
}
|
|
7523
7946
|
const idMap = /* @__PURE__ */ new Map();
|
|
7524
7947
|
for (const f of features) {
|
|
7525
|
-
const rel = f.docs.featurePathFromDocs ||
|
|
7948
|
+
const rel = f.docs.featurePathFromDocs || path20.relative(config.docsDir, f.path);
|
|
7526
7949
|
const id = f.id || "UNKNOWN";
|
|
7527
7950
|
if (!idMap.has(id)) idMap.set(id, []);
|
|
7528
7951
|
idMap.get(id).push(rel);
|
|
@@ -7530,7 +7953,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
7530
7953
|
if (!isInitialTemplateState) {
|
|
7531
7954
|
const featureDocs = ["spec.md", "plan.md", "tasks.md"];
|
|
7532
7955
|
for (const file of featureDocs) {
|
|
7533
|
-
const p =
|
|
7956
|
+
const p = path20.join(f.path, file);
|
|
7534
7957
|
if (!await fs15.pathExists(p)) continue;
|
|
7535
7958
|
const content = await fs15.readFile(p, "utf-8");
|
|
7536
7959
|
const placeholders = detectPlaceholders(content);
|
|
@@ -7545,7 +7968,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
7545
7968
|
});
|
|
7546
7969
|
}
|
|
7547
7970
|
if (decisionsPlaceholderMode !== "off") {
|
|
7548
|
-
const decisionsPath =
|
|
7971
|
+
const decisionsPath = path20.join(f.path, "decisions.md");
|
|
7549
7972
|
if (await fs15.pathExists(decisionsPath)) {
|
|
7550
7973
|
const content = await fs15.readFile(decisionsPath, "utf-8");
|
|
7551
7974
|
const placeholders = detectPlaceholders(content);
|
|
@@ -7574,7 +7997,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
7574
7997
|
level: "warn",
|
|
7575
7998
|
code: "spec_status_unset",
|
|
7576
7999
|
message: tr(config.lang, "cli", "doctor.issue.specStatusUnset"),
|
|
7577
|
-
path: formatPath(cwd,
|
|
8000
|
+
path: formatPath(cwd, path20.join(f.path, "spec.md"))
|
|
7578
8001
|
});
|
|
7579
8002
|
}
|
|
7580
8003
|
if (f.docs.planExists && !f.planStatus && !isInitialTemplateState) {
|
|
@@ -7582,7 +8005,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
7582
8005
|
level: "warn",
|
|
7583
8006
|
code: "plan_status_unset",
|
|
7584
8007
|
message: tr(config.lang, "cli", "doctor.issue.planStatusUnset"),
|
|
7585
|
-
path: formatPath(cwd,
|
|
8008
|
+
path: formatPath(cwd, path20.join(f.path, "plan.md"))
|
|
7586
8009
|
});
|
|
7587
8010
|
}
|
|
7588
8011
|
if (f.docs.tasksExists && f.tasks.total === 0 && !isInitialTemplateState) {
|
|
@@ -7590,7 +8013,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
7590
8013
|
level: "warn",
|
|
7591
8014
|
code: "tasks_empty",
|
|
7592
8015
|
message: tr(config.lang, "cli", "doctor.issue.tasksEmpty"),
|
|
7593
|
-
path: formatPath(cwd,
|
|
8016
|
+
path: formatPath(cwd, path20.join(f.path, "tasks.md"))
|
|
7594
8017
|
});
|
|
7595
8018
|
}
|
|
7596
8019
|
if (f.docs.tasksExists && !f.docs.tasksDocStatusFieldExists && !isInitialTemplateState) {
|
|
@@ -7598,7 +8021,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
7598
8021
|
level: "warn",
|
|
7599
8022
|
code: "tasks_doc_status_missing",
|
|
7600
8023
|
message: tr(config.lang, "cli", "doctor.issue.tasksDocStatusMissing"),
|
|
7601
|
-
path: formatPath(cwd,
|
|
8024
|
+
path: formatPath(cwd, path20.join(f.path, "tasks.md"))
|
|
7602
8025
|
});
|
|
7603
8026
|
}
|
|
7604
8027
|
if (f.docs.tasksExists && f.docs.tasksDocStatusFieldExists && !f.tasksDocStatus && !isInitialTemplateState) {
|
|
@@ -7606,7 +8029,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
7606
8029
|
level: "warn",
|
|
7607
8030
|
code: "tasks_doc_status_unset",
|
|
7608
8031
|
message: tr(config.lang, "cli", "doctor.issue.tasksDocStatusUnset"),
|
|
7609
|
-
path: formatPath(cwd,
|
|
8032
|
+
path: formatPath(cwd, path20.join(f.path, "tasks.md"))
|
|
7610
8033
|
});
|
|
7611
8034
|
}
|
|
7612
8035
|
}
|
|
@@ -7630,7 +8053,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
7630
8053
|
level: "warn",
|
|
7631
8054
|
code: "missing_feature_id",
|
|
7632
8055
|
message: tr(config.lang, "cli", "doctor.issue.missingFeatureId"),
|
|
7633
|
-
path: formatPath(cwd,
|
|
8056
|
+
path: formatPath(cwd, path20.join(config.docsDir, p))
|
|
7634
8057
|
});
|
|
7635
8058
|
}
|
|
7636
8059
|
return issues;
|
|
@@ -7752,7 +8175,7 @@ function doctorCommand(program2) {
|
|
|
7752
8175
|
}
|
|
7753
8176
|
console.log();
|
|
7754
8177
|
console.log(chalk6.bold(tr(lang, "cli", "doctor.title")));
|
|
7755
|
-
console.log(chalk6.gray(`- Docs: ${
|
|
8178
|
+
console.log(chalk6.gray(`- Docs: ${path20.relative(cwd, docsDir)}`));
|
|
7756
8179
|
console.log(chalk6.gray(`- Type: ${projectType}`));
|
|
7757
8180
|
console.log(chalk6.gray(`- Lang: ${lang}`));
|
|
7758
8181
|
console.log();
|
|
@@ -7925,7 +8348,7 @@ async function runView(featureName, options) {
|
|
|
7925
8348
|
}
|
|
7926
8349
|
console.log();
|
|
7927
8350
|
console.log(chalk6.bold("\u{1F4CA} Workflow View"));
|
|
7928
|
-
console.log(chalk6.gray(`- Docs: ${
|
|
8351
|
+
console.log(chalk6.gray(`- Docs: ${path20.relative(cwd, config.docsDir)}`));
|
|
7929
8352
|
console.log(
|
|
7930
8353
|
chalk6.gray(
|
|
7931
8354
|
`- Features: ${state.features.length} (open ${state.openFeatures.length} / done ${state.doneFeatures.length})`
|
|
@@ -8298,25 +8721,25 @@ function tg(lang, key, vars = {}) {
|
|
|
8298
8721
|
}
|
|
8299
8722
|
function detectGithubCliLangSync(cwd) {
|
|
8300
8723
|
const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
|
|
8301
|
-
const startDirs = [explicitDocsDir ?
|
|
8724
|
+
const startDirs = [explicitDocsDir ? path20.resolve(explicitDocsDir) : "", path20.resolve(cwd)].filter(Boolean);
|
|
8302
8725
|
const scanOrder = [];
|
|
8303
8726
|
const seen = /* @__PURE__ */ new Set();
|
|
8304
8727
|
for (const start of startDirs) {
|
|
8305
8728
|
let current = start;
|
|
8306
8729
|
while (true) {
|
|
8307
|
-
const abs =
|
|
8730
|
+
const abs = path20.resolve(current);
|
|
8308
8731
|
if (!seen.has(abs)) {
|
|
8309
8732
|
scanOrder.push(abs);
|
|
8310
8733
|
seen.add(abs);
|
|
8311
8734
|
}
|
|
8312
|
-
const parent =
|
|
8735
|
+
const parent = path20.dirname(abs);
|
|
8313
8736
|
if (parent === abs) break;
|
|
8314
8737
|
current = parent;
|
|
8315
8738
|
}
|
|
8316
8739
|
}
|
|
8317
8740
|
for (const base of scanOrder) {
|
|
8318
|
-
for (const docsDir of [
|
|
8319
|
-
const configPath =
|
|
8741
|
+
for (const docsDir of [path20.join(base, "docs"), base]) {
|
|
8742
|
+
const configPath = path20.join(docsDir, ".lee-spec-kit.json");
|
|
8320
8743
|
if (fs15.existsSync(configPath)) {
|
|
8321
8744
|
try {
|
|
8322
8745
|
const parsed = fs15.readJsonSync(configPath);
|
|
@@ -8324,11 +8747,11 @@ function detectGithubCliLangSync(cwd) {
|
|
|
8324
8747
|
} catch {
|
|
8325
8748
|
}
|
|
8326
8749
|
}
|
|
8327
|
-
const agentsPath =
|
|
8328
|
-
const featuresPath =
|
|
8750
|
+
const agentsPath = path20.join(docsDir, "agents");
|
|
8751
|
+
const featuresPath = path20.join(docsDir, "features");
|
|
8329
8752
|
if (!fs15.existsSync(agentsPath) || !fs15.existsSync(featuresPath)) continue;
|
|
8330
8753
|
for (const probe of ["custom.md", "constitution.md", "agents.md"]) {
|
|
8331
|
-
const file =
|
|
8754
|
+
const file = path20.join(agentsPath, probe);
|
|
8332
8755
|
if (!fs15.existsSync(file)) continue;
|
|
8333
8756
|
try {
|
|
8334
8757
|
const content = fs15.readFileSync(file, "utf-8");
|
|
@@ -8426,7 +8849,7 @@ function ensureSections(body, sections, kind, lang) {
|
|
|
8426
8849
|
}
|
|
8427
8850
|
function ensureDocsExist(docsDir, relativePaths, lang) {
|
|
8428
8851
|
const missing = relativePaths.filter(
|
|
8429
|
-
(relativePath) => !fs15.existsSync(
|
|
8852
|
+
(relativePath) => !fs15.existsSync(path20.join(docsDir, relativePath))
|
|
8430
8853
|
);
|
|
8431
8854
|
if (missing.length > 0) {
|
|
8432
8855
|
throw createCliError(
|
|
@@ -8436,13 +8859,13 @@ function ensureDocsExist(docsDir, relativePaths, lang) {
|
|
|
8436
8859
|
}
|
|
8437
8860
|
}
|
|
8438
8861
|
function buildDefaultBodyFileName(kind, docsDir, component) {
|
|
8439
|
-
const key = `${
|
|
8862
|
+
const key = `${path20.resolve(docsDir)}::${component.trim().toLowerCase()}`;
|
|
8440
8863
|
const digest = createHash("sha1").update(key).digest("hex").slice(0, 12);
|
|
8441
8864
|
return `lee-spec-kit.${digest}.${kind}.md`;
|
|
8442
8865
|
}
|
|
8443
8866
|
function toBodyFilePath(raw, kind, docsDir, component) {
|
|
8444
|
-
const selected = raw?.trim() ||
|
|
8445
|
-
return
|
|
8867
|
+
const selected = raw?.trim() || path20.join(os.tmpdir(), buildDefaultBodyFileName(kind, docsDir, component));
|
|
8868
|
+
return path20.resolve(selected);
|
|
8446
8869
|
}
|
|
8447
8870
|
function toProjectRootDocsPath(relativePathFromDocs) {
|
|
8448
8871
|
const normalized = relativePathFromDocs.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
@@ -8535,6 +8958,10 @@ function resolveGithubDocsCwd(config, feature) {
|
|
|
8535
8958
|
if (docsGitCwd) return docsGitCwd;
|
|
8536
8959
|
return config.docsDir;
|
|
8537
8960
|
}
|
|
8961
|
+
function shouldPushDocsSync(config) {
|
|
8962
|
+
if (config.docsRepo !== "standalone") return true;
|
|
8963
|
+
return config.pushDocs === true;
|
|
8964
|
+
}
|
|
8538
8965
|
function getFeatureDocPaths(feature) {
|
|
8539
8966
|
const featurePathFromDocs = feature.docs.featurePathFromDocs;
|
|
8540
8967
|
return {
|
|
@@ -9257,8 +9684,8 @@ function ensureCleanWorktree(cwd, lang) {
|
|
|
9257
9684
|
);
|
|
9258
9685
|
}
|
|
9259
9686
|
}
|
|
9260
|
-
function commitAndPushPath(cwd, absPath, message, lang) {
|
|
9261
|
-
const relativePath =
|
|
9687
|
+
function commitAndPushPath(cwd, absPath, message, lang, options) {
|
|
9688
|
+
const relativePath = path20.relative(cwd, absPath) || absPath;
|
|
9262
9689
|
const status = runProcessOrThrow(
|
|
9263
9690
|
"git",
|
|
9264
9691
|
["status", "--porcelain=v1", "--", relativePath],
|
|
@@ -9268,6 +9695,7 @@ function commitAndPushPath(cwd, absPath, message, lang) {
|
|
|
9268
9695
|
if (status.stdout.trim().length === 0) return;
|
|
9269
9696
|
runProcessOrThrow("git", ["add", "--", relativePath], cwd, tg(lang, "stageFileFailed"));
|
|
9270
9697
|
runProcessOrThrow("git", ["commit", "-m", message], cwd, tg(lang, "commitSyncFailed"));
|
|
9698
|
+
if (options?.pushToOrigin === false) return;
|
|
9271
9699
|
const branch = gitCurrentBranch(cwd, lang);
|
|
9272
9700
|
runProcessOrThrow(
|
|
9273
9701
|
"git",
|
|
@@ -9390,9 +9818,9 @@ function githubCommand(program2) {
|
|
|
9390
9818
|
[paths.specPath, paths.planPath, paths.tasksPath],
|
|
9391
9819
|
config.lang
|
|
9392
9820
|
);
|
|
9393
|
-
const specContent = await fs15.readFile(
|
|
9394
|
-
const planContent = await fs15.readFile(
|
|
9395
|
-
const tasksContent = await fs15.readFile(
|
|
9821
|
+
const specContent = await fs15.readFile(path20.join(config.docsDir, paths.specPath), "utf-8");
|
|
9822
|
+
const planContent = await fs15.readFile(path20.join(config.docsDir, paths.planPath), "utf-8");
|
|
9823
|
+
const tasksContent = await fs15.readFile(path20.join(config.docsDir, paths.tasksPath), "utf-8");
|
|
9396
9824
|
const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
|
|
9397
9825
|
const title = options.title?.trim() || tg(config.lang, "issueDefaultTitle", {
|
|
9398
9826
|
slug: feature.slug,
|
|
@@ -9430,7 +9858,7 @@ function githubCommand(program2) {
|
|
|
9430
9858
|
config.lang
|
|
9431
9859
|
);
|
|
9432
9860
|
} else {
|
|
9433
|
-
await fs15.ensureDir(
|
|
9861
|
+
await fs15.ensureDir(path20.dirname(bodyFile));
|
|
9434
9862
|
await fs15.writeFile(bodyFile, generatedBody, "utf-8");
|
|
9435
9863
|
}
|
|
9436
9864
|
let issueUrl;
|
|
@@ -9529,10 +9957,10 @@ function githubCommand(program2) {
|
|
|
9529
9957
|
const labels = parseLabels(options.labels, config.lang);
|
|
9530
9958
|
const paths = getFeatureDocPaths(feature);
|
|
9531
9959
|
ensureDocsExist(config.docsDir, [paths.specPath, paths.tasksPath], config.lang);
|
|
9532
|
-
const specContent = await fs15.readFile(
|
|
9533
|
-
const planPath =
|
|
9960
|
+
const specContent = await fs15.readFile(path20.join(config.docsDir, paths.specPath), "utf-8");
|
|
9961
|
+
const planPath = path20.join(config.docsDir, paths.planPath);
|
|
9534
9962
|
const planContent = await fs15.pathExists(planPath) ? await fs15.readFile(planPath, "utf-8") : "";
|
|
9535
|
-
const tasksContent = await fs15.readFile(
|
|
9963
|
+
const tasksContent = await fs15.readFile(path20.join(config.docsDir, paths.tasksPath), "utf-8");
|
|
9536
9964
|
const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
|
|
9537
9965
|
const defaultTitle = feature.issueNumber ? tg(config.lang, "prDefaultTitleWithIssue", {
|
|
9538
9966
|
issue: feature.issueNumber,
|
|
@@ -9575,13 +10003,14 @@ function githubCommand(program2) {
|
|
|
9575
10003
|
config.lang
|
|
9576
10004
|
);
|
|
9577
10005
|
} else {
|
|
9578
|
-
await fs15.ensureDir(
|
|
10006
|
+
await fs15.ensureDir(path20.dirname(bodyFile));
|
|
9579
10007
|
await fs15.writeFile(bodyFile, generatedBody, "utf-8");
|
|
9580
10008
|
}
|
|
9581
10009
|
const retryCount = toRetryCount(options.retry, config.lang);
|
|
9582
10010
|
let prUrl = options.pr?.trim() || "";
|
|
9583
10011
|
let mergedAttempts;
|
|
9584
10012
|
let syncChanged = false;
|
|
10013
|
+
const pushDocsSync = shouldPushDocsSync(config);
|
|
9585
10014
|
if (options.create) {
|
|
9586
10015
|
const projectGitCwd = resolveGithubProjectCwd(config, feature);
|
|
9587
10016
|
ensureNoTodoPlaceholders(body, tg(config.lang, "kindPr"), config.lang);
|
|
@@ -9614,6 +10043,9 @@ function githubCommand(program2) {
|
|
|
9614
10043
|
);
|
|
9615
10044
|
prUrl = created.stdout.trim();
|
|
9616
10045
|
}
|
|
10046
|
+
if (!prUrl && options.merge) {
|
|
10047
|
+
prUrl = (feature.pr.link || "").trim();
|
|
10048
|
+
}
|
|
9617
10049
|
if (!prUrl && options.merge) {
|
|
9618
10050
|
throw createCliError(
|
|
9619
10051
|
"INVALID_ARGUMENT",
|
|
@@ -9629,7 +10061,7 @@ function githubCommand(program2) {
|
|
|
9629
10061
|
}
|
|
9630
10062
|
if (prUrl && options.syncTasks !== false) {
|
|
9631
10063
|
const synced = syncTasksPrMetadata(
|
|
9632
|
-
|
|
10064
|
+
path20.join(config.docsDir, paths.tasksPath),
|
|
9633
10065
|
prUrl,
|
|
9634
10066
|
"Review",
|
|
9635
10067
|
config.lang
|
|
@@ -9648,7 +10080,8 @@ function githubCommand(program2) {
|
|
|
9648
10080
|
docsGitCwd,
|
|
9649
10081
|
synced.path,
|
|
9650
10082
|
message,
|
|
9651
|
-
config.lang
|
|
10083
|
+
config.lang,
|
|
10084
|
+
{ pushToOrigin: pushDocsSync }
|
|
9652
10085
|
);
|
|
9653
10086
|
}
|
|
9654
10087
|
}
|
|
@@ -9676,7 +10109,7 @@ function githubCommand(program2) {
|
|
|
9676
10109
|
);
|
|
9677
10110
|
if (prUrl && options.syncTasks !== false) {
|
|
9678
10111
|
const mergedSync = syncTasksPrMetadata(
|
|
9679
|
-
|
|
10112
|
+
path20.join(config.docsDir, paths.tasksPath),
|
|
9680
10113
|
prUrl,
|
|
9681
10114
|
"Approved",
|
|
9682
10115
|
config.lang
|
|
@@ -9694,7 +10127,8 @@ function githubCommand(program2) {
|
|
|
9694
10127
|
docsGitCwd,
|
|
9695
10128
|
mergedSync.path,
|
|
9696
10129
|
message,
|
|
9697
|
-
config.lang
|
|
10130
|
+
config.lang,
|
|
10131
|
+
{ pushToOrigin: pushDocsSync }
|
|
9698
10132
|
);
|
|
9699
10133
|
}
|
|
9700
10134
|
}
|
|
@@ -9881,7 +10315,7 @@ function docsCommand(program2) {
|
|
|
9881
10315
|
);
|
|
9882
10316
|
return;
|
|
9883
10317
|
}
|
|
9884
|
-
const relativeFromCwd =
|
|
10318
|
+
const relativeFromCwd = path20.relative(process.cwd(), loaded.entry.absolutePath);
|
|
9885
10319
|
console.log();
|
|
9886
10320
|
console.log(chalk6.bold(`\u{1F4C4} ${loaded.entry.id}: ${loaded.entry.title}`));
|
|
9887
10321
|
console.log(
|
|
@@ -9959,7 +10393,7 @@ function detectCommand(program2) {
|
|
|
9959
10393
|
}
|
|
9960
10394
|
async function runDetect(options) {
|
|
9961
10395
|
const cwd = process.cwd();
|
|
9962
|
-
const targetCwd = options.dir ?
|
|
10396
|
+
const targetCwd = options.dir ? path20.resolve(cwd, options.dir) : cwd;
|
|
9963
10397
|
const config = await getConfig(targetCwd);
|
|
9964
10398
|
const detected = !!config;
|
|
9965
10399
|
const reasonCode = detected ? "PROJECT_DETECTED" : "PROJECT_NOT_DETECTED";
|
|
@@ -9986,7 +10420,7 @@ async function runDetect(options) {
|
|
|
9986
10420
|
);
|
|
9987
10421
|
return;
|
|
9988
10422
|
}
|
|
9989
|
-
const configPath2 =
|
|
10423
|
+
const configPath2 = path20.join(config.docsDir, ".lee-spec-kit.json");
|
|
9990
10424
|
const configFilePresent2 = await fs15.pathExists(configPath2);
|
|
9991
10425
|
const detectionSource2 = configFilePresent2 ? "config" : "heuristic";
|
|
9992
10426
|
console.log(
|
|
@@ -10020,7 +10454,7 @@ async function runDetect(options) {
|
|
|
10020
10454
|
console.log();
|
|
10021
10455
|
return;
|
|
10022
10456
|
}
|
|
10023
|
-
const configPath =
|
|
10457
|
+
const configPath = path20.join(config.docsDir, ".lee-spec-kit.json");
|
|
10024
10458
|
const configFilePresent = await fs15.pathExists(configPath);
|
|
10025
10459
|
const detectionSource = configFilePresent ? "config" : "heuristic";
|
|
10026
10460
|
console.log(chalk6.green(`- ${tr(lang, "cli", "detect.resultDetected")}`));
|
|
@@ -10054,6 +10488,459 @@ async function runDetect(options) {
|
|
|
10054
10488
|
}
|
|
10055
10489
|
console.log();
|
|
10056
10490
|
}
|
|
10491
|
+
function t(lang, ko, en) {
|
|
10492
|
+
return lang === "ko" ? ko : en;
|
|
10493
|
+
}
|
|
10494
|
+
function quotePath(value) {
|
|
10495
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
10496
|
+
}
|
|
10497
|
+
function toSlug(value) {
|
|
10498
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
10499
|
+
}
|
|
10500
|
+
function runGit(args, cwd) {
|
|
10501
|
+
try {
|
|
10502
|
+
return execFileSync("git", args, {
|
|
10503
|
+
cwd,
|
|
10504
|
+
encoding: "utf-8",
|
|
10505
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
10506
|
+
}).trim();
|
|
10507
|
+
} catch {
|
|
10508
|
+
return void 0;
|
|
10509
|
+
}
|
|
10510
|
+
}
|
|
10511
|
+
function isGitRepo(cwd) {
|
|
10512
|
+
return runGit(["rev-parse", "--is-inside-work-tree"], cwd) === "true";
|
|
10513
|
+
}
|
|
10514
|
+
function hasHeadCommit(cwd) {
|
|
10515
|
+
return !!runGit(["rev-parse", "--verify", "HEAD"], cwd);
|
|
10516
|
+
}
|
|
10517
|
+
function getOriginUrl(cwd) {
|
|
10518
|
+
const out = runGit(["remote", "get-url", "origin"], cwd);
|
|
10519
|
+
return out || void 0;
|
|
10520
|
+
}
|
|
10521
|
+
function hasTemplateMarkers(content) {
|
|
10522
|
+
const patterns = [
|
|
10523
|
+
/\{\{projectName\}\}/,
|
|
10524
|
+
/\{\{date\}\}/,
|
|
10525
|
+
/\(Write your project mission here\)/,
|
|
10526
|
+
/\(Write project-specific architecture principles here/i,
|
|
10527
|
+
/\(Write project code quality standards here/i,
|
|
10528
|
+
/\(Write project security principles here/i,
|
|
10529
|
+
/\(Write your project-specific rules here\)/,
|
|
10530
|
+
/\(Override default rules or add additional rules here\)/,
|
|
10531
|
+
/\(Write project-specific workflows here\)/,
|
|
10532
|
+
/\(Write other rules here\)/,
|
|
10533
|
+
/\(프로젝트의 미션을 작성하세요\)/,
|
|
10534
|
+
/\(프로젝트별 아키텍처 원칙을 작성하세요/,
|
|
10535
|
+
/\(프로젝트의 코드 품질 기준을 작성하세요/,
|
|
10536
|
+
/\(프로젝트의 보안 원칙을 작성하세요/,
|
|
10537
|
+
/\(여기에 프로젝트만의 규칙을 작성하세요\)/,
|
|
10538
|
+
/\(기본 규칙을 오버라이드하거나 추가 규칙을 작성하세요\)/,
|
|
10539
|
+
/\(프로젝트만의 워크플로우가 있다면 작성하세요\)/,
|
|
10540
|
+
/\(기타 규칙을 작성하세요\)/
|
|
10541
|
+
];
|
|
10542
|
+
return patterns.some((pattern) => pattern.test(content));
|
|
10543
|
+
}
|
|
10544
|
+
async function countFeatureDirs(docsDir, projectType) {
|
|
10545
|
+
const pattern = projectType === "single" ? "features/*/" : "features/*/*/";
|
|
10546
|
+
const dirs = await glob(pattern, {
|
|
10547
|
+
cwd: docsDir,
|
|
10548
|
+
absolute: false,
|
|
10549
|
+
ignore: ["**/feature-base/**"]
|
|
10550
|
+
});
|
|
10551
|
+
return dirs.map((value) => value.replace(/\\/g, "/").replace(/\/+$/, "")).filter((value) => {
|
|
10552
|
+
const base = path20.posix.basename(value);
|
|
10553
|
+
return !!base && base !== "feature-base";
|
|
10554
|
+
}).length;
|
|
10555
|
+
}
|
|
10556
|
+
async function hasUserPrdFile(prdDir) {
|
|
10557
|
+
if (!await fs15.pathExists(prdDir)) return false;
|
|
10558
|
+
const files = await glob("**/*.md", {
|
|
10559
|
+
cwd: prdDir,
|
|
10560
|
+
nodir: true,
|
|
10561
|
+
absolute: false,
|
|
10562
|
+
ignore: ["**/node_modules/**"]
|
|
10563
|
+
});
|
|
10564
|
+
return files.some((relativePath) => path20.basename(relativePath).toLowerCase() !== "readme.md");
|
|
10565
|
+
}
|
|
10566
|
+
function finalizeChecks(checks) {
|
|
10567
|
+
const summary = checks.reduce(
|
|
10568
|
+
(acc, check) => {
|
|
10569
|
+
acc[check.status] += 1;
|
|
10570
|
+
return acc;
|
|
10571
|
+
},
|
|
10572
|
+
{ ok: 0, warn: 0, block: 0 }
|
|
10573
|
+
);
|
|
10574
|
+
const status = summary.block > 0 ? "blocked" : summary.warn > 0 ? "needs_action" : "ready";
|
|
10575
|
+
return { checks, summary, status };
|
|
10576
|
+
}
|
|
10577
|
+
function printOnboardResult(lang, result) {
|
|
10578
|
+
console.log();
|
|
10579
|
+
console.log(chalk6.bold(t(lang, "\u{1F9ED} Onboarding \uC810\uAC80", "\u{1F9ED} Onboarding Checks")));
|
|
10580
|
+
for (const check of result.checks) {
|
|
10581
|
+
const mark = check.status === "ok" ? chalk6.green("\u2705") : check.status === "warn" ? chalk6.yellow("\u26A0\uFE0F") : chalk6.red("\u274C");
|
|
10582
|
+
const level = check.status === "ok" ? chalk6.green("OK") : check.status === "warn" ? chalk6.yellow("WARN") : chalk6.red("BLOCK");
|
|
10583
|
+
console.log(`${mark} [${level}] ${check.title}`);
|
|
10584
|
+
console.log(` ${check.message}`);
|
|
10585
|
+
if (check.path) console.log(chalk6.gray(` path: ${check.path}`));
|
|
10586
|
+
if (check.suggestedCommand) {
|
|
10587
|
+
console.log(chalk6.gray(` ${t(lang, "\uB2E4\uC74C \uBA85\uB839", "next")}: ${check.suggestedCommand}`));
|
|
10588
|
+
}
|
|
10589
|
+
}
|
|
10590
|
+
console.log();
|
|
10591
|
+
console.log(
|
|
10592
|
+
chalk6.bold(
|
|
10593
|
+
t(
|
|
10594
|
+
lang,
|
|
10595
|
+
`\uC694\uC57D: OK ${result.summary.ok}, WARN ${result.summary.warn}, BLOCK ${result.summary.block}`,
|
|
10596
|
+
`Summary: OK ${result.summary.ok}, WARN ${result.summary.warn}, BLOCK ${result.summary.block}`
|
|
10597
|
+
)
|
|
10598
|
+
)
|
|
10599
|
+
);
|
|
10600
|
+
if (result.status === "ready") {
|
|
10601
|
+
console.log(chalk6.green(t(lang, "\uC628\uBCF4\uB529 \uC900\uBE44\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "Onboarding checks passed.")));
|
|
10602
|
+
} else if (result.status === "needs_action") {
|
|
10603
|
+
console.log(chalk6.yellow(t(lang, "\uCD94\uAC00 \uC815\uB9AC\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.", "Some onboarding actions are required.")));
|
|
10604
|
+
} else {
|
|
10605
|
+
console.log(chalk6.red(t(lang, "\uC628\uBCF4\uB529 \uC120\uD589 \uC791\uC5C5\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", "Onboarding is blocked by required setup.")));
|
|
10606
|
+
}
|
|
10607
|
+
console.log();
|
|
10608
|
+
}
|
|
10609
|
+
async function runOnboardChecks(config) {
|
|
10610
|
+
const lang = config.lang;
|
|
10611
|
+
const checks = [];
|
|
10612
|
+
const docsDir = config.docsDir;
|
|
10613
|
+
const docsGitReady = isGitRepo(docsDir);
|
|
10614
|
+
if (!docsGitReady) {
|
|
10615
|
+
checks.push({
|
|
10616
|
+
id: "docs_git_repo",
|
|
10617
|
+
status: "block",
|
|
10618
|
+
title: t(lang, "docs Git \uB808\uD3EC \uCD08\uAE30\uD654", "Docs git repository initialized"),
|
|
10619
|
+
message: t(
|
|
10620
|
+
lang,
|
|
10621
|
+
"docs \uACBD\uB85C\uAC00 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uAC00 \uC544\uB2D9\uB2C8\uB2E4.",
|
|
10622
|
+
"Docs directory is not a git repository."
|
|
10623
|
+
),
|
|
10624
|
+
path: docsDir,
|
|
10625
|
+
suggestedCommand: `git -C ${quotePath(docsDir)} init`
|
|
10626
|
+
});
|
|
10627
|
+
} else {
|
|
10628
|
+
checks.push({
|
|
10629
|
+
id: "docs_git_repo",
|
|
10630
|
+
status: "ok",
|
|
10631
|
+
title: t(lang, "docs Git \uB808\uD3EC \uCD08\uAE30\uD654", "Docs git repository initialized"),
|
|
10632
|
+
message: t(lang, "docs Git \uB808\uD3EC\uAC00 \uD655\uC778\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "Docs git repository is available."),
|
|
10633
|
+
path: docsDir
|
|
10634
|
+
});
|
|
10635
|
+
if (!hasHeadCommit(docsDir)) {
|
|
10636
|
+
checks.push({
|
|
10637
|
+
id: "docs_initial_commit",
|
|
10638
|
+
status: "warn",
|
|
10639
|
+
title: t(lang, "docs \uCD08\uAE30 \uCEE4\uBC0B", "Docs initial commit"),
|
|
10640
|
+
message: t(
|
|
10641
|
+
lang,
|
|
10642
|
+
"docs \uCCAB \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uCD08\uAE30 \uC124\uC815 \uCEE4\uBC0B\uC744 \uBA3C\uC800 \uC0DD\uC131\uD558\uC138\uC694.",
|
|
10643
|
+
"No initial commit found in docs repo. Create an initial setup commit first."
|
|
10644
|
+
),
|
|
10645
|
+
path: docsDir,
|
|
10646
|
+
suggestedCommand: `git -C ${quotePath(docsDir)} add . && git -C ${quotePath(docsDir)} commit -m "docs: onboard setup"`
|
|
10647
|
+
});
|
|
10648
|
+
} else {
|
|
10649
|
+
checks.push({
|
|
10650
|
+
id: "docs_initial_commit",
|
|
10651
|
+
status: "ok",
|
|
10652
|
+
title: t(lang, "docs \uCD08\uAE30 \uCEE4\uBC0B", "Docs initial commit"),
|
|
10653
|
+
message: t(lang, "docs \uCD08\uAE30 \uCEE4\uBC0B\uC774 \uC874\uC7AC\uD569\uB2C8\uB2E4.", "Initial commit exists in docs repo."),
|
|
10654
|
+
path: docsDir
|
|
10655
|
+
});
|
|
10656
|
+
}
|
|
10657
|
+
const docsDirty = runGit(["status", "--porcelain=v1"], docsDir);
|
|
10658
|
+
if (docsDirty === void 0) {
|
|
10659
|
+
checks.push({
|
|
10660
|
+
id: "docs_worktree",
|
|
10661
|
+
status: "warn",
|
|
10662
|
+
title: t(lang, "docs \uC791\uC5C5 \uD2B8\uB9AC \uC0C1\uD0DC", "Docs worktree status"),
|
|
10663
|
+
message: t(
|
|
10664
|
+
lang,
|
|
10665
|
+
"docs \uBCC0\uACBD \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
10666
|
+
"Unable to read docs worktree status."
|
|
10667
|
+
),
|
|
10668
|
+
path: docsDir
|
|
10669
|
+
});
|
|
10670
|
+
} else if (docsDirty.trim().length > 0) {
|
|
10671
|
+
checks.push({
|
|
10672
|
+
id: "docs_worktree",
|
|
10673
|
+
status: "warn",
|
|
10674
|
+
title: t(lang, "docs \uC791\uC5C5 \uD2B8\uB9AC \uC0C1\uD0DC", "Docs worktree status"),
|
|
10675
|
+
message: t(
|
|
10676
|
+
lang,
|
|
10677
|
+
"\uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC740 docs \uBCC0\uACBD\uC0AC\uD56D\uC774 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
10678
|
+
"Uncommitted docs changes were found."
|
|
10679
|
+
),
|
|
10680
|
+
path: docsDir,
|
|
10681
|
+
suggestedCommand: `git -C ${quotePath(docsDir)} add . && git -C ${quotePath(docsDir)} commit -m "docs: onboard updates"`
|
|
10682
|
+
});
|
|
10683
|
+
} else {
|
|
10684
|
+
checks.push({
|
|
10685
|
+
id: "docs_worktree",
|
|
10686
|
+
status: "ok",
|
|
10687
|
+
title: t(lang, "docs \uC791\uC5C5 \uD2B8\uB9AC \uC0C1\uD0DC", "Docs worktree status"),
|
|
10688
|
+
message: t(lang, "docs \uC791\uC5C5 \uD2B8\uB9AC\uAC00 \uAE68\uB057\uD569\uB2C8\uB2E4.", "Docs worktree is clean."),
|
|
10689
|
+
path: docsDir
|
|
10690
|
+
});
|
|
10691
|
+
}
|
|
10692
|
+
}
|
|
10693
|
+
const constitutionPath = path20.join(docsDir, "agents", "constitution.md");
|
|
10694
|
+
if (!await fs15.pathExists(constitutionPath)) {
|
|
10695
|
+
checks.push({
|
|
10696
|
+
id: "constitution_exists",
|
|
10697
|
+
status: "block",
|
|
10698
|
+
title: t(lang, "Constitution \uC791\uC131", "Constitution setup"),
|
|
10699
|
+
message: t(
|
|
10700
|
+
lang,
|
|
10701
|
+
"`agents/constitution.md` \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
10702
|
+
"`agents/constitution.md` is missing."
|
|
10703
|
+
),
|
|
10704
|
+
path: constitutionPath,
|
|
10705
|
+
suggestedCommand: `npx lee-spec-kit update --agents`
|
|
10706
|
+
});
|
|
10707
|
+
} else {
|
|
10708
|
+
const content = await fs15.readFile(constitutionPath, "utf-8");
|
|
10709
|
+
if (hasTemplateMarkers(content)) {
|
|
10710
|
+
checks.push({
|
|
10711
|
+
id: "constitution_filled",
|
|
10712
|
+
status: "block",
|
|
10713
|
+
title: t(lang, "Constitution \uC791\uC131", "Constitution setup"),
|
|
10714
|
+
message: t(
|
|
10715
|
+
lang,
|
|
10716
|
+
"Constitution\uC5D0 \uD15C\uD50C\uB9BF placeholder\uAC00 \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4. \uD504\uB85C\uC81D\uD2B8 \uAE30\uC900\uC73C\uB85C \uBA3C\uC800 \uC791\uC131\uD558\uC138\uC694.",
|
|
10717
|
+
"Constitution still contains template placeholders. Fill project-specific content first."
|
|
10718
|
+
),
|
|
10719
|
+
path: constitutionPath
|
|
10720
|
+
});
|
|
10721
|
+
} else {
|
|
10722
|
+
checks.push({
|
|
10723
|
+
id: "constitution_filled",
|
|
10724
|
+
status: "ok",
|
|
10725
|
+
title: t(lang, "Constitution \uC791\uC131", "Constitution setup"),
|
|
10726
|
+
message: t(lang, "Constitution\uC774 \uC791\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "Constitution looks filled."),
|
|
10727
|
+
path: constitutionPath
|
|
10728
|
+
});
|
|
10729
|
+
}
|
|
10730
|
+
}
|
|
10731
|
+
const customPath = path20.join(docsDir, "agents", "custom.md");
|
|
10732
|
+
if (await fs15.pathExists(customPath)) {
|
|
10733
|
+
const content = await fs15.readFile(customPath, "utf-8");
|
|
10734
|
+
if (hasTemplateMarkers(content)) {
|
|
10735
|
+
checks.push({
|
|
10736
|
+
id: "custom_optional",
|
|
10737
|
+
status: "warn",
|
|
10738
|
+
title: t(lang, "Custom \uADDC\uCE59 \uBB38\uC11C", "Custom rules doc"),
|
|
10739
|
+
message: t(
|
|
10740
|
+
lang,
|
|
10741
|
+
"`agents/custom.md`\uB294 \uC120\uD0DD \uD56D\uBAA9\uC774\uC9C0\uB9CC, \uD604\uC7AC \uD15C\uD50C\uB9BF \uC0C1\uD0DC\uC785\uB2C8\uB2E4. \uD544\uC694\uD558\uBA74 \uADDC\uCE59\uC744 \uC791\uC131\uD558\uC138\uC694.",
|
|
10742
|
+
"`agents/custom.md` is optional, but it still looks like template content. Fill it if your project needs custom rules."
|
|
10743
|
+
),
|
|
10744
|
+
path: customPath
|
|
10745
|
+
});
|
|
10746
|
+
} else {
|
|
10747
|
+
checks.push({
|
|
10748
|
+
id: "custom_optional",
|
|
10749
|
+
status: "ok",
|
|
10750
|
+
title: t(lang, "Custom \uADDC\uCE59 \uBB38\uC11C", "Custom rules doc"),
|
|
10751
|
+
message: t(lang, "Custom \uADDC\uCE59 \uBB38\uC11C\uAC00 \uAD6C\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "Custom rules doc looks configured."),
|
|
10752
|
+
path: customPath
|
|
10753
|
+
});
|
|
10754
|
+
}
|
|
10755
|
+
}
|
|
10756
|
+
const prdDir = path20.join(docsDir, "prd");
|
|
10757
|
+
const featureCount = await countFeatureDirs(docsDir, config.projectType);
|
|
10758
|
+
const prdReady = await hasUserPrdFile(prdDir);
|
|
10759
|
+
if (!prdReady) {
|
|
10760
|
+
checks.push({
|
|
10761
|
+
id: "prd_ready",
|
|
10762
|
+
status: featureCount === 0 ? "block" : "warn",
|
|
10763
|
+
title: t(lang, "PRD \uC900\uBE44 \uC0C1\uD0DC", "PRD readiness"),
|
|
10764
|
+
message: featureCount === 0 ? t(
|
|
10765
|
+
lang,
|
|
10766
|
+
"PRD \uBB38\uC11C\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. Feature \uC0DD\uC131 \uC804\uC5D0 PRD\uBD80\uD130 \uC791\uC131\uD558\uC138\uC694.",
|
|
10767
|
+
"PRD is empty. Write PRD first before creating features."
|
|
10768
|
+
) : t(
|
|
10769
|
+
lang,
|
|
10770
|
+
"PRD \uBB38\uC11C\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC774\uBBF8 Feature\uAC00 \uC788\uB2E4\uBA74 PRD\uB97C \uBCF4\uAC15\uD558\uC138\uC694.",
|
|
10771
|
+
"PRD is empty. If features already exist, fill PRD as soon as possible."
|
|
10772
|
+
),
|
|
10773
|
+
path: prdDir,
|
|
10774
|
+
suggestedCommand: `touch ${quotePath(path20.join(prdDir, `${toSlug(config.projectName || "project")}-prd.md`))}`
|
|
10775
|
+
});
|
|
10776
|
+
} else {
|
|
10777
|
+
checks.push({
|
|
10778
|
+
id: "prd_ready",
|
|
10779
|
+
status: "ok",
|
|
10780
|
+
title: t(lang, "PRD \uC900\uBE44 \uC0C1\uD0DC", "PRD readiness"),
|
|
10781
|
+
message: t(lang, "PRD \uBB38\uC11C\uAC00 \uD655\uC778\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "PRD document is present."),
|
|
10782
|
+
path: prdDir
|
|
10783
|
+
});
|
|
10784
|
+
}
|
|
10785
|
+
const workflowPolicy = resolveWorkflowPolicy(config.workflow);
|
|
10786
|
+
if (workflowPolicy.mode === "github") {
|
|
10787
|
+
const projectKeys = config.projectType === "multi" ? resolveProjectComponents(config.projectType, config.components) : ["single"];
|
|
10788
|
+
for (const key of projectKeys) {
|
|
10789
|
+
const resolved = resolveProjectGitCwd(config, key, lang);
|
|
10790
|
+
const title = t(
|
|
10791
|
+
lang,
|
|
10792
|
+
config.projectType === "multi" ? `\uD504\uB85C\uC81D\uD2B8 Git \uC5F0\uACB0 (${key})` : "\uD504\uB85C\uC81D\uD2B8 Git \uC5F0\uACB0",
|
|
10793
|
+
config.projectType === "multi" ? `Project git connectivity (${key})` : "Project git connectivity"
|
|
10794
|
+
);
|
|
10795
|
+
if (resolved.warning || !resolved.cwd) {
|
|
10796
|
+
checks.push({
|
|
10797
|
+
id: `project_git_${key}`,
|
|
10798
|
+
status: "block",
|
|
10799
|
+
title,
|
|
10800
|
+
message: resolved.warning || t(
|
|
10801
|
+
lang,
|
|
10802
|
+
"\uD504\uB85C\uC81D\uD2B8 \uB808\uD3EC \uACBD\uB85C\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
10803
|
+
"Project repository path could not be resolved."
|
|
10804
|
+
)
|
|
10805
|
+
});
|
|
10806
|
+
continue;
|
|
10807
|
+
}
|
|
10808
|
+
if (!isGitRepo(resolved.cwd)) {
|
|
10809
|
+
checks.push({
|
|
10810
|
+
id: `project_git_${key}`,
|
|
10811
|
+
status: "block",
|
|
10812
|
+
title,
|
|
10813
|
+
message: t(
|
|
10814
|
+
lang,
|
|
10815
|
+
"\uD504\uB85C\uC81D\uD2B8 \uACBD\uB85C\uAC00 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uAC00 \uC544\uB2D9\uB2C8\uB2E4.",
|
|
10816
|
+
"Project path is not a git repository."
|
|
10817
|
+
),
|
|
10818
|
+
path: resolved.cwd,
|
|
10819
|
+
suggestedCommand: `git -C ${quotePath(resolved.cwd)} init`
|
|
10820
|
+
});
|
|
10821
|
+
continue;
|
|
10822
|
+
}
|
|
10823
|
+
const origin = getOriginUrl(resolved.cwd);
|
|
10824
|
+
if (!origin) {
|
|
10825
|
+
checks.push({
|
|
10826
|
+
id: `project_origin_${key}`,
|
|
10827
|
+
status: "block",
|
|
10828
|
+
title: t(
|
|
10829
|
+
lang,
|
|
10830
|
+
config.projectType === "multi" ? `\uD504\uB85C\uC81D\uD2B8 origin \uC124\uC815 (${key})` : "\uD504\uB85C\uC81D\uD2B8 origin \uC124\uC815",
|
|
10831
|
+
config.projectType === "multi" ? `Project origin configured (${key})` : "Project origin configured"
|
|
10832
|
+
),
|
|
10833
|
+
message: t(
|
|
10834
|
+
lang,
|
|
10835
|
+
"GitHub \uC6CC\uD06C\uD50C\uB85C\uC6B0\uB97C \uC704\uD574 \uD504\uB85C\uC81D\uD2B8 \uB808\uD3EC\uC758 origin remote\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
|
|
10836
|
+
"Project repo origin remote is required for github workflow."
|
|
10837
|
+
),
|
|
10838
|
+
path: resolved.cwd,
|
|
10839
|
+
suggestedCommand: `git -C ${quotePath(resolved.cwd)} remote add origin <git-url>`
|
|
10840
|
+
});
|
|
10841
|
+
} else {
|
|
10842
|
+
checks.push({
|
|
10843
|
+
id: `project_origin_${key}`,
|
|
10844
|
+
status: "ok",
|
|
10845
|
+
title: t(
|
|
10846
|
+
lang,
|
|
10847
|
+
config.projectType === "multi" ? `\uD504\uB85C\uC81D\uD2B8 origin \uC124\uC815 (${key})` : "\uD504\uB85C\uC81D\uD2B8 origin \uC124\uC815",
|
|
10848
|
+
config.projectType === "multi" ? `Project origin configured (${key})` : "Project origin configured"
|
|
10849
|
+
),
|
|
10850
|
+
message: t(
|
|
10851
|
+
lang,
|
|
10852
|
+
`origin\uC774 \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4: ${origin}`,
|
|
10853
|
+
`origin is configured: ${origin}`
|
|
10854
|
+
),
|
|
10855
|
+
path: resolved.cwd
|
|
10856
|
+
});
|
|
10857
|
+
}
|
|
10858
|
+
}
|
|
10859
|
+
}
|
|
10860
|
+
if (config.docsRepo === "standalone" && config.pushDocs) {
|
|
10861
|
+
const origin = getOriginUrl(docsDir);
|
|
10862
|
+
if (!origin) {
|
|
10863
|
+
checks.push({
|
|
10864
|
+
id: "docs_origin",
|
|
10865
|
+
status: "block",
|
|
10866
|
+
title: t(lang, "docs origin \uC124\uC815", "Docs origin configured"),
|
|
10867
|
+
message: t(
|
|
10868
|
+
lang,
|
|
10869
|
+
"standalone + pushDocs=true \uC124\uC815\uC5D0\uC11C\uB294 docs origin remote\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
|
|
10870
|
+
"docs origin remote is required when standalone + pushDocs=true."
|
|
10871
|
+
),
|
|
10872
|
+
path: docsDir,
|
|
10873
|
+
suggestedCommand: `git -C ${quotePath(docsDir)} remote add origin <docs-git-url>`
|
|
10874
|
+
});
|
|
10875
|
+
} else {
|
|
10876
|
+
checks.push({
|
|
10877
|
+
id: "docs_origin",
|
|
10878
|
+
status: "ok",
|
|
10879
|
+
title: t(lang, "docs origin \uC124\uC815", "Docs origin configured"),
|
|
10880
|
+
message: t(lang, `origin\uC774 \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4: ${origin}`, `origin is configured: ${origin}`),
|
|
10881
|
+
path: docsDir
|
|
10882
|
+
});
|
|
10883
|
+
}
|
|
10884
|
+
}
|
|
10885
|
+
return finalizeChecks(checks);
|
|
10886
|
+
}
|
|
10887
|
+
function onboardCommand(program2) {
|
|
10888
|
+
program2.command("onboard").description("Run onboarding checks for initial setup").option("--json", "Output in JSON format for agents").option("--strict", "Exit with code 1 when WARN/BLOCK exists").action(async (options) => {
|
|
10889
|
+
try {
|
|
10890
|
+
await runOnboard(options);
|
|
10891
|
+
} catch (error) {
|
|
10892
|
+
const config = await getConfig(process.cwd());
|
|
10893
|
+
const lang = config?.lang ?? DEFAULT_LANG;
|
|
10894
|
+
const cliError = toCliError(error);
|
|
10895
|
+
const suggestions = getCliErrorSuggestions(cliError.code, lang);
|
|
10896
|
+
if (options.json) {
|
|
10897
|
+
console.log(
|
|
10898
|
+
JSON.stringify({
|
|
10899
|
+
status: "error",
|
|
10900
|
+
reasonCode: cliError.code,
|
|
10901
|
+
error: cliError.message,
|
|
10902
|
+
suggestions
|
|
10903
|
+
})
|
|
10904
|
+
);
|
|
10905
|
+
} else {
|
|
10906
|
+
console.error(
|
|
10907
|
+
chalk6.red(tr(lang, "cli", "common.errorLabel")),
|
|
10908
|
+
chalk6.red(`[${cliError.code}] ${cliError.message}`)
|
|
10909
|
+
);
|
|
10910
|
+
printCliErrorSuggestions(suggestions, lang);
|
|
10911
|
+
}
|
|
10912
|
+
process.exit(1);
|
|
10913
|
+
}
|
|
10914
|
+
});
|
|
10915
|
+
}
|
|
10916
|
+
async function runOnboard(options) {
|
|
10917
|
+
const config = await getConfig(process.cwd());
|
|
10918
|
+
if (!config) {
|
|
10919
|
+
throw createCliError(
|
|
10920
|
+
"CONFIG_NOT_FOUND",
|
|
10921
|
+
tr(DEFAULT_LANG, "cli", "common.configNotFound")
|
|
10922
|
+
);
|
|
10923
|
+
}
|
|
10924
|
+
const lang = config.lang;
|
|
10925
|
+
const result = await runOnboardChecks(config);
|
|
10926
|
+
if (options.json) {
|
|
10927
|
+
const payload = {
|
|
10928
|
+
status: "ok",
|
|
10929
|
+
reasonCode: result.status === "ready" ? "ONBOARD_READY" : result.status === "needs_action" ? "ONBOARD_NEEDS_ACTION" : "ONBOARD_BLOCKED",
|
|
10930
|
+
docsDir: config.docsDir,
|
|
10931
|
+
docsRepo: config.docsRepo || "embedded",
|
|
10932
|
+
workflow: resolveWorkflowPolicy(config.workflow),
|
|
10933
|
+
summary: result.summary,
|
|
10934
|
+
checks: result.checks
|
|
10935
|
+
};
|
|
10936
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
10937
|
+
} else {
|
|
10938
|
+
printOnboardResult(lang, result);
|
|
10939
|
+
}
|
|
10940
|
+
if (options.strict && (result.summary.warn > 0 || result.summary.block > 0)) {
|
|
10941
|
+
process.exitCode = 1;
|
|
10942
|
+
}
|
|
10943
|
+
}
|
|
10057
10944
|
function isBannerDisabled() {
|
|
10058
10945
|
const v = (process.env.LEE_SPEC_KIT_NO_BANNER || "").trim();
|
|
10059
10946
|
return v === "1";
|
|
@@ -10097,11 +10984,11 @@ ${version}
|
|
|
10097
10984
|
}
|
|
10098
10985
|
return `${ascii}${footer}`;
|
|
10099
10986
|
}
|
|
10100
|
-
var CACHE_FILE =
|
|
10987
|
+
var CACHE_FILE = path20.join(os.homedir(), ".lee-spec-kit-version-cache.json");
|
|
10101
10988
|
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
10102
10989
|
function getCurrentVersion() {
|
|
10103
10990
|
try {
|
|
10104
|
-
const packageJsonPath =
|
|
10991
|
+
const packageJsonPath = path20.join(__dirname$1, "..", "package.json");
|
|
10105
10992
|
if (fs15.existsSync(packageJsonPath)) {
|
|
10106
10993
|
const pkg = fs15.readJsonSync(packageJsonPath);
|
|
10107
10994
|
return pkg.version;
|
|
@@ -10205,7 +11092,7 @@ function shouldCheckForUpdates() {
|
|
|
10205
11092
|
if (shouldCheckForUpdates()) checkForUpdates();
|
|
10206
11093
|
function getCliVersion() {
|
|
10207
11094
|
try {
|
|
10208
|
-
const packageJsonPath =
|
|
11095
|
+
const packageJsonPath = path20.join(__dirname$1, "..", "package.json");
|
|
10209
11096
|
if (fs15.existsSync(packageJsonPath)) {
|
|
10210
11097
|
const pkg = fs15.readJsonSync(packageJsonPath);
|
|
10211
11098
|
if (pkg?.version) return String(pkg.version);
|
|
@@ -10233,4 +11120,5 @@ flowCommand(program);
|
|
|
10233
11120
|
githubCommand(program);
|
|
10234
11121
|
docsCommand(program);
|
|
10235
11122
|
detectCommand(program);
|
|
11123
|
+
onboardCommand(program);
|
|
10236
11124
|
await program.parseAsync();
|