lee-spec-kit 0.6.3 → 0.6.5
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 +5 -0
- package/README.md +5 -0
- package/dist/index.js +1231 -345
- package/package.json +1 -1
- package/templates/en/common/agents/skills/create-issue.md +1 -1
- package/templates/en/common/agents/skills/create-pr.md +4 -1
- package/templates/ko/common/agents/skills/create-issue.md +1 -1
- package/templates/ko/common/agents/skills/create-pr.md +4 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import path18 from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { program } from 'commander';
|
|
5
|
-
import
|
|
5
|
+
import fs14 from 'fs-extra';
|
|
6
6
|
import prompts from 'prompts';
|
|
7
7
|
import chalk6 from 'chalk';
|
|
8
8
|
import { glob } from 'glob';
|
|
@@ -11,10 +11,10 @@ import { createHash } from 'crypto';
|
|
|
11
11
|
import os from 'os';
|
|
12
12
|
|
|
13
13
|
var getFilename = () => fileURLToPath(import.meta.url);
|
|
14
|
-
var getDirname = () =>
|
|
14
|
+
var getDirname = () => path18.dirname(getFilename());
|
|
15
15
|
var __dirname$1 = /* @__PURE__ */ getDirname();
|
|
16
16
|
async function copyTemplates(src, dest) {
|
|
17
|
-
await
|
|
17
|
+
await fs14.copy(src, dest, {
|
|
18
18
|
overwrite: true,
|
|
19
19
|
errorOnExist: false
|
|
20
20
|
});
|
|
@@ -30,22 +30,22 @@ function applyReplacements(content, replacements) {
|
|
|
30
30
|
async function replaceInFiles(dir, replacements) {
|
|
31
31
|
const files = await glob("**/*.md", { cwd: dir, absolute: true });
|
|
32
32
|
for (const file of files) {
|
|
33
|
-
let content = await
|
|
33
|
+
let content = await fs14.readFile(file, "utf-8");
|
|
34
34
|
content = applyReplacements(content, replacements);
|
|
35
|
-
await
|
|
35
|
+
await fs14.writeFile(file, content, "utf-8");
|
|
36
36
|
}
|
|
37
37
|
const shFiles = await glob("**/*.sh", { cwd: dir, absolute: true });
|
|
38
38
|
for (const file of shFiles) {
|
|
39
|
-
let content = await
|
|
39
|
+
let content = await fs14.readFile(file, "utf-8");
|
|
40
40
|
content = applyReplacements(content, replacements);
|
|
41
|
-
await
|
|
41
|
+
await fs14.writeFile(file, content, "utf-8");
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
45
|
-
var __dirname2 =
|
|
45
|
+
var __dirname2 = path18.dirname(__filename2);
|
|
46
46
|
function getTemplatesDir() {
|
|
47
|
-
const rootDir =
|
|
48
|
-
return
|
|
47
|
+
const rootDir = path18.resolve(__dirname2, "..");
|
|
48
|
+
return path18.join(rootDir, "templates");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// src/utils/i18n.ts
|
|
@@ -134,6 +134,8 @@ var I18N = {
|
|
|
134
134
|
"context.checkPolicyHint": "\u2139\uFE0F \uC0AC\uC6A9\uC790 \uD655\uC778 \uC815\uCC45\uC740 `npx lee-spec-kit docs get agents --json`\uC73C\uB85C \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694. (git push/merge/merge commit \uD3EC\uD568) [\uD655\uC778 \uD544\uC694]\uAC00 \uC788\uC73C\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` (\uC608: `A`, `A OK`) \uC751\uB2F5\uC744 \uBC1B\uC740 \uB4A4 \uC9C4\uD589 (config: approval\uB85C \uC870\uC815 \uAC00\uB2A5)",
|
|
135
135
|
"context.actionOptionHint": "\uC2B9\uC778 \uC751\uB2F5 \uD615\uC2DD: `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` (\uC608: `A`, `A OK`)",
|
|
136
136
|
"context.actionExplainHint": "\uC2B9\uC778 \uC694\uCCAD \uC804, \uAC01 \uB77C\uBCA8\uC774 \uBB34\uC5C7\uC744 \uC2E4\uD589/\uBCC0\uACBD\uD558\uB294\uC9C0 \uD55C \uC904 \uC694\uC57D\uACFC \uD568\uAED8 \uC124\uBA85\uD558\uC138\uC694.",
|
|
137
|
+
"context.finalLabelPrompt": "\uD604\uC7AC \uC120\uD0DD \uAC00\uB2A5\uD55C \uB77C\uBCA8: {labels}. \uB9C8\uC9C0\uB9C9 \uC751\uB2F5\uC740 `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` \uD615\uC2DD\uC73C\uB85C \uBC1B\uC73C\uC138\uC694. (\uC608: {example})",
|
|
138
|
+
"context.finalLabelCommandHint": "\uB77C\uBCA8\uC744 \uBC1B\uC73C\uBA74 \uBC14\uB85C \uC2E4\uD589: {command}",
|
|
137
139
|
"context.readBuiltinDocFirst": "\uBA3C\uC800 \uB0B4\uC7A5 \uBB38\uC11C\uB97C \uD655\uC778\uD558\uC138\uC694: {command}",
|
|
138
140
|
"context.tipDocsCommitRules": "\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uADDC\uCE59\uC740 `npx lee-spec-kit docs get git-workflow --json`\uC73C\uB85C \uD655\uC778\uD558\uC138\uC694.",
|
|
139
141
|
"context.list.docsCommitNeeded": "\uBB38\uC11C \uCEE4\uBC0B \uD544\uC694",
|
|
@@ -218,6 +220,8 @@ var I18N = {
|
|
|
218
220
|
"github.optPrMerge": "\uC7AC\uC2DC\uB3C4/\uD5E4\uB4DC \uAC31\uC2E0\uACFC \uD568\uAED8 PR merge \uC218\uD589",
|
|
219
221
|
"github.optPrConfirm": "\uC6D0\uACA9 \uC791\uC5C5(--create/--merge)\uC6A9 \uBA85\uC2DC\uC801 \uC2B9\uC778 \uD1A0\uD070. \uC0AC\uC6A9\uAC12: OK",
|
|
220
222
|
"github.optPrRetry": "merge \uC7AC\uC2DC\uB3C4 \uD69F\uC218 (\uAE30\uBCF8: 3)",
|
|
223
|
+
"github.optPrScreenshots": "PR \uC2A4\uD06C\uB9B0\uC0F7 \uC139\uC158 \uBAA8\uB4DC (auto|on|off, \uAE30\uBCF8: auto)",
|
|
224
|
+
"github.optPrMermaid": "PR Mermaid \uC139\uC158 \uBAA8\uB4DC (auto|on|off, \uAE30\uBCF8: auto)",
|
|
221
225
|
"github.optPrNoSyncTasks": "tasks.md PR URL/PR \uC0C1\uD0DC \uB3D9\uAE30\uD654\uB97C \uAC74\uB108\uB700",
|
|
222
226
|
"github.optPrCommitSync": "tasks.md \uB3D9\uAE30\uD654 \uBCC0\uACBD\uC744 \uC790\uB3D9 commit/push",
|
|
223
227
|
"github.invalidRepoComponentMismatch": "`--repo`\uC640 `--component`\uB97C \uD568\uAED8 \uC4F8 \uB54C\uB294 \uAC19\uC740 \uAC12\uC744 \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
|
|
@@ -227,6 +231,12 @@ var I18N = {
|
|
|
227
231
|
"github.ghEmptyJson": "GitHub CLI JSON \uCD9C\uB825\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
228
232
|
"github.ghInvalidJson": "GitHub CLI JSON \uD30C\uC2F1\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4: {snippet}",
|
|
229
233
|
"github.sectionsMissing": "{kind} \uBCF8\uBB38\uC5D0 \uD544\uC218 \uC139\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: {sections}",
|
|
234
|
+
"github.todoPlaceholdersRemain": "{kind} \uBCF8\uBB38\uC5D0 TODO \uD56D\uBAA9\uC774 \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4. \uBAA9\uD45C/\uC644\uB8CC \uAE30\uC900 \uB4F1\uC744 \uCC44\uC6B4 \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
|
|
235
|
+
"github.artifactModeInvalid": "`--{kind}` \uAC12\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: {value}. \uD5C8\uC6A9\uAC12: auto,on,off",
|
|
236
|
+
"github.prScreenshotsSectionMissing": "PR \uBCF8\uBB38\uC5D0 \uD544\uC218 \uC139\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: {section}",
|
|
237
|
+
"github.prScreenshotImageMissing": "PR \uBCF8\uBB38\uC758 `{section}` \uC139\uC158\uC5D0 \uC774\uBBF8\uC9C0 \uB9C8\uD06C\uB2E4\uC6B4(``)\uC744 \uCD94\uAC00\uD558\uC138\uC694.",
|
|
238
|
+
"github.prMermaidSectionMissing": "PR \uBCF8\uBB38\uC5D0 \uD544\uC218 \uC139\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: {section}",
|
|
239
|
+
"github.prMermaidBlockMissing": "PR \uBCF8\uBB38\uC758 `{section}` \uC139\uC158\uC5D0 ```mermaid \uCF54\uB4DC \uBE14\uB85D\uC744 \uCD94\uAC00\uD558\uC138\uC694.",
|
|
230
240
|
"github.docsMissing": "\uAD00\uB828 \uBB38\uC11C \uACBD\uB85C\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: {paths}",
|
|
231
241
|
"github.noFeatures": "Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
232
242
|
"github.multipleFeaturesMatched": "\uC5EC\uB7EC Feature\uAC00 \uB9E4\uCE6D\uB418\uC5C8\uC2B5\uB2C8\uB2E4. feature \uC774\uB984(slug | F001 | F001-slug)\uC744 \uBA85\uC2DC\uD558\uC138\uC694.",
|
|
@@ -383,7 +393,7 @@ var I18N = {
|
|
|
383
393
|
tasksImprove: "tasks.md\uB97C \uBCF4\uC644\uD558\uACE0 \uBB38\uC11C \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
|
|
384
394
|
tasksApproval: "tasks.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC9C4\uD589 \uC2B9\uC778(`A` \uB610\uB294 `A OK` \uD615\uC2DD)\uC744 \uBC1B\uC73C\uC138\uC694. (\uC2B9\uC778 \uD6C4 \uBB38\uC11C \uC0C1\uD0DC\uB97C Approved\uB85C \uBCC0\uACBD)",
|
|
385
395
|
docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} \uAE30\uD68D \uBB38\uC11C"',
|
|
386
|
-
issueCreateAndWrite: "`npx lee-spec-kit docs get create-issue --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github issue {featureRef} --json`\uC73C\uB85C \uCD08\uC548\uC744 \uC0DD\uC131\uD558\uC138\uC694.
|
|
396
|
+
issueCreateAndWrite: "`npx lee-spec-kit docs get create-issue --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github issue {featureRef} --json`\uC73C\uB85C \uCD08\uC548\uC744 \uC0DD\uC131\uD558\uC138\uC694. \uBAA9\uD45C/\uC644\uB8CC \uAE30\uC900\uC744 \uAC80\uD1A0\xB7\uBCF4\uC644\uD558\uACE0 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `--create --confirm OK`\uB85C \uC0DD\uC131\uD55C \uB2E4\uC74C, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694.",
|
|
387
397
|
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
|
|
388
398
|
docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
|
|
389
399
|
projectCommitIssueUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat(#{issueNumber}): {folderName} \uAD6C\uD604 \uC5C5\uB370\uC774\uD2B8"',
|
|
@@ -400,7 +410,7 @@ var I18N = {
|
|
|
400
410
|
prePrReviewRun: "PR \uC0DD\uC131 \uC804 \uC0AC\uC804 \uCF54\uB4DC\uB9AC\uBDF0\uB97C \uC9C4\uD589\uD558\uC138\uC694. \uC6B0\uC120\uC21C\uC704 \uC2A4\uD0AC: {skills} (\uC124\uCE58\uB41C \uB354 \uC801\uD569\uD55C \uC2A4\uD0AC\uC774 \uC788\uB2E4\uBA74 \uBA3C\uC800 \uC81C\uC548 \uD6C4 \uC0AC\uC6A9). \uC2A4\uD0AC\uC744 \uC4F8 \uC218 \uC5C6\uC73C\uBA74 `{fallback}` \uC815\uCC45\uC73C\uB85C \uC9C4\uD589\uD558\uACE0 `PR \uC804 \uB9AC\uBDF0`\uB97C Done\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694. Findings \uC815\uCC45: {findingsPolicy}",
|
|
401
411
|
prePrReviewFindingsBlock: "\uC911\uC694 \uC774\uC288\uB294 \uC218\uC815/\uD569\uC758 \uD6C4\uC5D0\uB9CC PR \uC0DD\uC131",
|
|
402
412
|
prePrReviewFindingsWarn: "\uB9AC\uC2A4\uD06C\uB97C \uACF5\uC720\uD558\uBA74 PR \uC0DD\uC131 \uC9C4\uD589 \uAC00\uB2A5",
|
|
403
|
-
prCreate: "`npx lee-spec-kit docs get create-pr --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github pr {featureRef} --json`\uC73C\uB85C \uCD08\uC548\uC744 \uC0DD\uC131\uD558\uC138\uC694.
|
|
413
|
+
prCreate: "`npx lee-spec-kit docs get create-pr --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github pr {featureRef} --json`\uC73C\uB85C \uCD08\uC548\uC744 \uC0DD\uC131\uD558\uC138\uC694. \uBCC0\uACBD \uC0AC\uD56D/\uD14C\uC2A4\uD2B8 \uC139\uC158\uC744 \uAC80\uD1A0\xB7\uBCF4\uC644\uD558\uACE0 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `--create --confirm OK`\uB85C \uC0DD\uC131\uD55C \uB2E4\uC74C tasks.md\uC5D0 PR \uB9C1\uD06C\uB97C \uAE30\uB85D\uD558\uC138\uC694.",
|
|
404
414
|
prFillStatus: "tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Review/Approved \uC911 \uD558\uB098\uB85C \uC124\uC815\uD558\uC138\uC694. (merge \uD6C4 Approved\uB85C \uC5C5\uB370\uC774\uD2B8)",
|
|
405
415
|
prResolveReview: "\uB9AC\uBDF0 \uCF54\uBA58\uD2B8\uB97C \uD574\uACB0\uD558\uACE0 PR \uC0C1\uD0DC\uB97C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694. (PR \uC0C1\uD0DC: Review \u2192 Approved)",
|
|
406
416
|
prRequestReview: "\uB9AC\uBDF0\uC5B4\uC5D0\uAC8C \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uACE0 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.",
|
|
@@ -500,6 +510,8 @@ var I18N = {
|
|
|
500
510
|
"context.checkPolicyHint": "\u2139\uFE0F Check user-approval policy first with `npx lee-spec-kit docs get agents --json` (includes git push/merge and merge commits). If you see [CHECK required], wait for `<label>` or `<label> OK` (e.g. `A`, `A OK`) before proceeding (config: approval can override)",
|
|
501
511
|
"context.actionOptionHint": "Approval reply format: `<label>` or `<label> OK` (e.g. `A`, `A OK`)",
|
|
502
512
|
"context.actionExplainHint": "Before requesting approval, explain what each label will run/change with a one-line summary.",
|
|
513
|
+
"context.finalLabelPrompt": "Available labels now: {labels}. End with a label request in `<label>` or `<label> OK` format. (e.g. {example})",
|
|
514
|
+
"context.finalLabelCommandHint": "When a label is provided, run immediately: {command}",
|
|
503
515
|
"context.readBuiltinDocFirst": "Read built-in docs first: {command}",
|
|
504
516
|
"context.tipDocsCommitRules": "Check commit message rules with `npx lee-spec-kit docs get git-workflow --json`.",
|
|
505
517
|
"context.list.docsCommitNeeded": "Commit docs changes",
|
|
@@ -584,6 +596,8 @@ var I18N = {
|
|
|
584
596
|
"github.optPrMerge": "Merge PR with retry and head-branch refresh",
|
|
585
597
|
"github.optPrConfirm": "Explicit user approval token for remote operations (--create/--merge). Use: OK",
|
|
586
598
|
"github.optPrRetry": "Retry count for merge (default: 3)",
|
|
599
|
+
"github.optPrScreenshots": "PR screenshots section mode (auto|on|off, default: auto)",
|
|
600
|
+
"github.optPrMermaid": "PR Mermaid section mode (auto|on|off, default: auto)",
|
|
587
601
|
"github.optPrNoSyncTasks": "Do not sync PR URL/PR status into tasks.md",
|
|
588
602
|
"github.optPrCommitSync": "Commit and push tasks.md metadata sync automatically",
|
|
589
603
|
"github.invalidRepoComponentMismatch": "`--repo` and `--component` must reference the same value when both are provided.",
|
|
@@ -593,6 +607,12 @@ var I18N = {
|
|
|
593
607
|
"github.ghEmptyJson": "GitHub CLI returned empty JSON output.",
|
|
594
608
|
"github.ghInvalidJson": "GitHub CLI returned invalid JSON: {snippet}",
|
|
595
609
|
"github.sectionsMissing": "{kind} body is missing required sections: {sections}",
|
|
610
|
+
"github.todoPlaceholdersRemain": "{kind} body still contains TODO placeholders. Fill goals/completion criteria before creating remotely.",
|
|
611
|
+
"github.artifactModeInvalid": "Invalid value for `--{kind}`: {value}. Allowed: auto,on,off",
|
|
612
|
+
"github.prScreenshotsSectionMissing": "PR body is missing required section: {section}",
|
|
613
|
+
"github.prScreenshotImageMissing": "Add image markdown (``) to the `{section}` section in PR body.",
|
|
614
|
+
"github.prMermaidSectionMissing": "PR body is missing required section: {section}",
|
|
615
|
+
"github.prMermaidBlockMissing": "Add a ```mermaid code block to the `{section}` section in PR body.",
|
|
596
616
|
"github.docsMissing": "Related document paths do not exist: {paths}",
|
|
597
617
|
"github.noFeatures": "No features found.",
|
|
598
618
|
"github.multipleFeaturesMatched": "Multiple features matched. Specify feature name (slug | F001 | F001-slug).",
|
|
@@ -749,7 +769,7 @@ var I18N = {
|
|
|
749
769
|
tasksImprove: "Improve tasks.md and change Doc Status to Review.",
|
|
750
770
|
tasksApproval: "Share tasks.md with the user and get progress approval (`A` or `A OK` format). (Then set Doc Status to Approved)",
|
|
751
771
|
docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} planning docs"',
|
|
752
|
-
issueCreateAndWrite: "Review procedure with `npx lee-spec-kit docs get create-issue --json`, then generate a draft via `npx lee-spec-kit github issue {featureRef} --json`.
|
|
772
|
+
issueCreateAndWrite: "Review procedure with `npx lee-spec-kit docs get create-issue --json`, then generate a draft via `npx lee-spec-kit github issue {featureRef} --json`. Refine goals/completion criteria, get explicit user OK, run `--create --confirm OK`, then update issue number in spec.md/tasks.md and prepare a docs commit.",
|
|
753
773
|
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
|
|
754
774
|
docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} docs update"',
|
|
755
775
|
projectCommitIssueUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat(#{issueNumber}): {folderName} implementation update"',
|
|
@@ -766,7 +786,7 @@ var I18N = {
|
|
|
766
786
|
prePrReviewRun: "Run a pre-PR code review before creating the PR. Preferred skills: {skills} (if a better installed skill fits this change, propose it first). If no skill can run, use `{fallback}` and set `Pre-PR Review` to Done in tasks.md. Findings policy: {findingsPolicy}",
|
|
767
787
|
prePrReviewFindingsBlock: "major findings must be fixed/aligned before PR creation",
|
|
768
788
|
prePrReviewFindingsWarn: "you may proceed after sharing the risks",
|
|
769
|
-
prCreate: "Review procedure with `npx lee-spec-kit docs get create-pr --json`, then generate a draft via `npx lee-spec-kit github pr {featureRef} --json`.
|
|
789
|
+
prCreate: "Review procedure with `npx lee-spec-kit docs get create-pr --json`, then generate a draft via `npx lee-spec-kit github pr {featureRef} --json`. Refine changes/tests sections, get explicit user OK, run `--create --confirm OK`, then record the PR link in tasks.md.",
|
|
770
790
|
prFillStatus: "Set PR Status in tasks.md to Review/Approved. (After merge, update it to Approved.)",
|
|
771
791
|
prResolveReview: "Resolve review comments and update PR Status. (PR Status: Review \u2192 Approved)",
|
|
772
792
|
prRequestReview: "Request review and update PR Status to Review.",
|
|
@@ -1183,17 +1203,17 @@ function sleep(ms) {
|
|
|
1183
1203
|
return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
|
|
1184
1204
|
}
|
|
1185
1205
|
function getDocsLockPath(docsDir) {
|
|
1186
|
-
return
|
|
1206
|
+
return path18.join(docsDir, ".lee-spec-kit.lock");
|
|
1187
1207
|
}
|
|
1188
1208
|
function getInitLockPath(targetDir) {
|
|
1189
|
-
return
|
|
1190
|
-
|
|
1191
|
-
`.lee-spec-kit.${
|
|
1209
|
+
return path18.join(
|
|
1210
|
+
path18.dirname(targetDir),
|
|
1211
|
+
`.lee-spec-kit.${path18.basename(targetDir)}.lock`
|
|
1192
1212
|
);
|
|
1193
1213
|
}
|
|
1194
1214
|
async function isStaleLock(lockPath, staleMs) {
|
|
1195
1215
|
try {
|
|
1196
|
-
const stat = await
|
|
1216
|
+
const stat = await fs14.stat(lockPath);
|
|
1197
1217
|
if (Date.now() - stat.mtimeMs <= staleMs) {
|
|
1198
1218
|
return false;
|
|
1199
1219
|
}
|
|
@@ -1208,7 +1228,7 @@ async function isStaleLock(lockPath, staleMs) {
|
|
|
1208
1228
|
}
|
|
1209
1229
|
async function readLockPayload(lockPath) {
|
|
1210
1230
|
try {
|
|
1211
|
-
const raw = await
|
|
1231
|
+
const raw = await fs14.readFile(lockPath, "utf8");
|
|
1212
1232
|
const parsed = JSON.parse(raw);
|
|
1213
1233
|
if (!parsed || typeof parsed !== "object") return null;
|
|
1214
1234
|
return parsed;
|
|
@@ -1230,17 +1250,17 @@ function isProcessAlive(pid) {
|
|
|
1230
1250
|
}
|
|
1231
1251
|
}
|
|
1232
1252
|
async function tryAcquire(lockPath, owner) {
|
|
1233
|
-
await
|
|
1253
|
+
await fs14.ensureDir(path18.dirname(lockPath));
|
|
1234
1254
|
try {
|
|
1235
|
-
const fd = await
|
|
1255
|
+
const fd = await fs14.open(lockPath, "wx");
|
|
1236
1256
|
const payload = JSON.stringify(
|
|
1237
1257
|
{ pid: process.pid, owner: owner ?? "unknown", createdAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1238
1258
|
null,
|
|
1239
1259
|
2
|
|
1240
1260
|
);
|
|
1241
|
-
await
|
|
1261
|
+
await fs14.writeFile(fd, `${payload}
|
|
1242
1262
|
`, { encoding: "utf8" });
|
|
1243
|
-
await
|
|
1263
|
+
await fs14.close(fd);
|
|
1244
1264
|
return true;
|
|
1245
1265
|
} catch (error) {
|
|
1246
1266
|
if (error.code === "EEXIST") {
|
|
@@ -1254,9 +1274,9 @@ async function waitForLockRelease(lockPath, options = {}) {
|
|
|
1254
1274
|
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
1255
1275
|
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
1256
1276
|
const startedAt = Date.now();
|
|
1257
|
-
while (await
|
|
1277
|
+
while (await fs14.pathExists(lockPath)) {
|
|
1258
1278
|
if (await isStaleLock(lockPath, staleMs)) {
|
|
1259
|
-
await
|
|
1279
|
+
await fs14.remove(lockPath);
|
|
1260
1280
|
break;
|
|
1261
1281
|
}
|
|
1262
1282
|
if (Date.now() - startedAt > timeoutMs) {
|
|
@@ -1274,7 +1294,7 @@ async function withFileLock(lockPath, task, options = {}) {
|
|
|
1274
1294
|
const acquired = await tryAcquire(lockPath, options.owner);
|
|
1275
1295
|
if (acquired) break;
|
|
1276
1296
|
if (await isStaleLock(lockPath, staleMs)) {
|
|
1277
|
-
await
|
|
1297
|
+
await fs14.remove(lockPath);
|
|
1278
1298
|
continue;
|
|
1279
1299
|
}
|
|
1280
1300
|
if (Date.now() - startedAt > timeoutMs) {
|
|
@@ -1288,7 +1308,7 @@ async function withFileLock(lockPath, task, options = {}) {
|
|
|
1288
1308
|
try {
|
|
1289
1309
|
return await task();
|
|
1290
1310
|
} finally {
|
|
1291
|
-
await
|
|
1311
|
+
await fs14.remove(lockPath).catch(() => {
|
|
1292
1312
|
});
|
|
1293
1313
|
}
|
|
1294
1314
|
}
|
|
@@ -1307,30 +1327,30 @@ var ENGINE_MANAGED_AGENT_FILES = [
|
|
|
1307
1327
|
"pr-template.md"
|
|
1308
1328
|
];
|
|
1309
1329
|
var ENGINE_MANAGED_AGENT_DIRS = ["skills"];
|
|
1310
|
-
var ENGINE_MANAGED_FEATURE_PATH =
|
|
1330
|
+
var ENGINE_MANAGED_FEATURE_PATH = path18.join(
|
|
1311
1331
|
"features",
|
|
1312
1332
|
"feature-base"
|
|
1313
1333
|
);
|
|
1314
1334
|
async function pruneEngineManagedDocs(docsDir) {
|
|
1315
1335
|
const removed = [];
|
|
1316
1336
|
for (const file of ENGINE_MANAGED_AGENT_FILES) {
|
|
1317
|
-
const target =
|
|
1318
|
-
if (await
|
|
1319
|
-
await
|
|
1320
|
-
removed.push(
|
|
1337
|
+
const target = path18.join(docsDir, "agents", file);
|
|
1338
|
+
if (await fs14.pathExists(target)) {
|
|
1339
|
+
await fs14.remove(target);
|
|
1340
|
+
removed.push(path18.relative(docsDir, target));
|
|
1321
1341
|
}
|
|
1322
1342
|
}
|
|
1323
1343
|
for (const dir of ENGINE_MANAGED_AGENT_DIRS) {
|
|
1324
|
-
const target =
|
|
1325
|
-
if (await
|
|
1326
|
-
await
|
|
1327
|
-
removed.push(
|
|
1344
|
+
const target = path18.join(docsDir, "agents", dir);
|
|
1345
|
+
if (await fs14.pathExists(target)) {
|
|
1346
|
+
await fs14.remove(target);
|
|
1347
|
+
removed.push(path18.relative(docsDir, target));
|
|
1328
1348
|
}
|
|
1329
1349
|
}
|
|
1330
|
-
const featureBasePath =
|
|
1331
|
-
if (await
|
|
1332
|
-
await
|
|
1333
|
-
removed.push(
|
|
1350
|
+
const featureBasePath = path18.join(docsDir, ENGINE_MANAGED_FEATURE_PATH);
|
|
1351
|
+
if (await fs14.pathExists(featureBasePath)) {
|
|
1352
|
+
await fs14.remove(featureBasePath);
|
|
1353
|
+
removed.push(path18.relative(docsDir, featureBasePath));
|
|
1334
1354
|
}
|
|
1335
1355
|
return removed;
|
|
1336
1356
|
}
|
|
@@ -1383,7 +1403,7 @@ ${tr(lang2, "cli", "common.canceled")}`)
|
|
|
1383
1403
|
}
|
|
1384
1404
|
async function runInit(options) {
|
|
1385
1405
|
const cwd = process.cwd();
|
|
1386
|
-
const defaultName =
|
|
1406
|
+
const defaultName = path18.basename(cwd);
|
|
1387
1407
|
let projectName = options.name || defaultName;
|
|
1388
1408
|
let projectType = options.type;
|
|
1389
1409
|
let components = parseComponentsOption(options.components);
|
|
@@ -1393,7 +1413,7 @@ async function runInit(options) {
|
|
|
1393
1413
|
let pushDocs = typeof options.pushDocs === "boolean" ? options.pushDocs : void 0;
|
|
1394
1414
|
let docsRemote = options.docsRemote;
|
|
1395
1415
|
let projectRoot;
|
|
1396
|
-
const targetDir =
|
|
1416
|
+
const targetDir = path18.resolve(cwd, options.dir || "./docs");
|
|
1397
1417
|
const skipPrompts = !!options.yes || !!options.nonInteractive;
|
|
1398
1418
|
if (options.docsRepo && !["embedded", "standalone"].includes(options.docsRepo)) {
|
|
1399
1419
|
throw createCliError(
|
|
@@ -1711,8 +1731,8 @@ async function runInit(options) {
|
|
|
1711
1731
|
await withFileLock(
|
|
1712
1732
|
initLockPath,
|
|
1713
1733
|
async () => {
|
|
1714
|
-
if (await
|
|
1715
|
-
const files = await
|
|
1734
|
+
if (await fs14.pathExists(targetDir)) {
|
|
1735
|
+
const files = await fs14.readdir(targetDir);
|
|
1716
1736
|
if (files.length > 0) {
|
|
1717
1737
|
if (options.force) {
|
|
1718
1738
|
} else if (options.nonInteractive) {
|
|
@@ -1748,28 +1768,28 @@ async function runInit(options) {
|
|
|
1748
1768
|
);
|
|
1749
1769
|
console.log();
|
|
1750
1770
|
const templatesDir = getTemplatesDir();
|
|
1751
|
-
const commonPath =
|
|
1771
|
+
const commonPath = path18.join(templatesDir, lang, "common");
|
|
1752
1772
|
const templateProjectType = toTemplateProjectType(projectType);
|
|
1753
|
-
const typePath =
|
|
1754
|
-
if (await
|
|
1773
|
+
const typePath = path18.join(templatesDir, lang, templateProjectType);
|
|
1774
|
+
if (await fs14.pathExists(commonPath)) {
|
|
1755
1775
|
await copyTemplates(commonPath, targetDir);
|
|
1756
1776
|
}
|
|
1757
|
-
if (!await
|
|
1777
|
+
if (!await fs14.pathExists(typePath)) {
|
|
1758
1778
|
throw new Error(
|
|
1759
1779
|
tr(lang, "cli", "init.error.templateNotFound", { path: typePath })
|
|
1760
1780
|
);
|
|
1761
1781
|
}
|
|
1762
1782
|
await copyTemplates(typePath, targetDir);
|
|
1763
1783
|
if (projectType === "multi" && !isDefaultFullstackComponents(components)) {
|
|
1764
|
-
const featuresRoot =
|
|
1765
|
-
await
|
|
1766
|
-
await
|
|
1784
|
+
const featuresRoot = path18.join(targetDir, "features");
|
|
1785
|
+
await fs14.remove(path18.join(featuresRoot, "fe"));
|
|
1786
|
+
await fs14.remove(path18.join(featuresRoot, "be"));
|
|
1767
1787
|
for (const component of components) {
|
|
1768
|
-
const componentDir =
|
|
1769
|
-
await
|
|
1770
|
-
const readmePath =
|
|
1771
|
-
if (!await
|
|
1772
|
-
await
|
|
1788
|
+
const componentDir = path18.join(featuresRoot, component);
|
|
1789
|
+
await fs14.ensureDir(componentDir);
|
|
1790
|
+
const readmePath = path18.join(componentDir, "README.md");
|
|
1791
|
+
if (!await fs14.pathExists(readmePath)) {
|
|
1792
|
+
await fs14.writeFile(
|
|
1773
1793
|
readmePath,
|
|
1774
1794
|
`# ${component.toUpperCase()} Features
|
|
1775
1795
|
|
|
@@ -1818,8 +1838,8 @@ Store ${component} feature specs here.
|
|
|
1818
1838
|
config.projectRoot = projectRoot;
|
|
1819
1839
|
}
|
|
1820
1840
|
}
|
|
1821
|
-
const configPath =
|
|
1822
|
-
await
|
|
1841
|
+
const configPath = path18.join(targetDir, ".lee-spec-kit.json");
|
|
1842
|
+
await fs14.writeJson(configPath, config, { spaces: 2 });
|
|
1823
1843
|
console.log(chalk6.green(tr(lang, "cli", "init.log.docsCreated")));
|
|
1824
1844
|
console.log();
|
|
1825
1845
|
await initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote);
|
|
@@ -1887,7 +1907,7 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
|
1887
1907
|
console.log(chalk6.blue(tr(lang, "cli", "init.log.gitInit")));
|
|
1888
1908
|
runGit(["init"], cwd);
|
|
1889
1909
|
}
|
|
1890
|
-
const relativePath =
|
|
1910
|
+
const relativePath = path18.relative(cwd, targetDir);
|
|
1891
1911
|
const stagedBeforeAdd = getCachedStagedFiles(cwd);
|
|
1892
1912
|
if (relativePath === "." && stagedBeforeAdd && stagedBeforeAdd.length > 0) {
|
|
1893
1913
|
console.log(
|
|
@@ -1944,17 +1964,17 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
|
1944
1964
|
}
|
|
1945
1965
|
function getAncestorDirs(startDir) {
|
|
1946
1966
|
const dirs = [];
|
|
1947
|
-
let current =
|
|
1967
|
+
let current = path18.resolve(startDir);
|
|
1948
1968
|
while (true) {
|
|
1949
1969
|
dirs.push(current);
|
|
1950
|
-
const parent =
|
|
1970
|
+
const parent = path18.dirname(current);
|
|
1951
1971
|
if (parent === current) break;
|
|
1952
1972
|
current = parent;
|
|
1953
1973
|
}
|
|
1954
1974
|
return dirs;
|
|
1955
1975
|
}
|
|
1956
1976
|
function hasWorkspaceBoundary(dir) {
|
|
1957
|
-
return
|
|
1977
|
+
return fs14.existsSync(path18.join(dir, "package.json")) || fs14.existsSync(path18.join(dir, ".git"));
|
|
1958
1978
|
}
|
|
1959
1979
|
function getSearchBaseDirs(cwd) {
|
|
1960
1980
|
const ancestors = getAncestorDirs(cwd);
|
|
@@ -1967,24 +1987,24 @@ function getSearchBaseDirs(cwd) {
|
|
|
1967
1987
|
async function getConfig(cwd) {
|
|
1968
1988
|
const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
|
|
1969
1989
|
const baseDirs = [
|
|
1970
|
-
...explicitDocsDir ? [
|
|
1990
|
+
...explicitDocsDir ? [path18.resolve(explicitDocsDir)] : [],
|
|
1971
1991
|
...getSearchBaseDirs(cwd)
|
|
1972
1992
|
];
|
|
1973
1993
|
const visitedBaseDirs = /* @__PURE__ */ new Set();
|
|
1974
1994
|
const visitedDocsDirs = /* @__PURE__ */ new Set();
|
|
1975
1995
|
for (const baseDir of baseDirs) {
|
|
1976
|
-
const resolvedBaseDir =
|
|
1996
|
+
const resolvedBaseDir = path18.resolve(baseDir);
|
|
1977
1997
|
if (visitedBaseDirs.has(resolvedBaseDir)) continue;
|
|
1978
1998
|
visitedBaseDirs.add(resolvedBaseDir);
|
|
1979
|
-
const possibleDocsDirs = [
|
|
1999
|
+
const possibleDocsDirs = [path18.join(resolvedBaseDir, "docs"), resolvedBaseDir];
|
|
1980
2000
|
for (const docsDir of possibleDocsDirs) {
|
|
1981
|
-
const resolvedDocsDir =
|
|
2001
|
+
const resolvedDocsDir = path18.resolve(docsDir);
|
|
1982
2002
|
if (visitedDocsDirs.has(resolvedDocsDir)) continue;
|
|
1983
2003
|
visitedDocsDirs.add(resolvedDocsDir);
|
|
1984
|
-
const configPath =
|
|
1985
|
-
if (await
|
|
2004
|
+
const configPath = path18.join(resolvedDocsDir, ".lee-spec-kit.json");
|
|
2005
|
+
if (await fs14.pathExists(configPath)) {
|
|
1986
2006
|
try {
|
|
1987
|
-
const configFile = await
|
|
2007
|
+
const configFile = await fs14.readJson(configPath);
|
|
1988
2008
|
const projectType = normalizeProjectType(configFile.projectType);
|
|
1989
2009
|
const components = resolveProjectComponents(
|
|
1990
2010
|
projectType,
|
|
@@ -2007,22 +2027,22 @@ async function getConfig(cwd) {
|
|
|
2007
2027
|
} catch {
|
|
2008
2028
|
}
|
|
2009
2029
|
}
|
|
2010
|
-
const agentsPath =
|
|
2011
|
-
const featuresPath =
|
|
2012
|
-
if (await
|
|
2013
|
-
const bePath =
|
|
2014
|
-
const fePath =
|
|
2015
|
-
const projectType = await
|
|
2030
|
+
const agentsPath = path18.join(resolvedDocsDir, "agents");
|
|
2031
|
+
const featuresPath = path18.join(resolvedDocsDir, "features");
|
|
2032
|
+
if (await fs14.pathExists(agentsPath) && await fs14.pathExists(featuresPath)) {
|
|
2033
|
+
const bePath = path18.join(featuresPath, "be");
|
|
2034
|
+
const fePath = path18.join(featuresPath, "fe");
|
|
2035
|
+
const projectType = await fs14.pathExists(bePath) || await fs14.pathExists(fePath) ? "multi" : "single";
|
|
2016
2036
|
const components = projectType === "multi" ? resolveProjectComponents("multi", ["fe", "be"]) : void 0;
|
|
2017
2037
|
const langProbeCandidates = [
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2038
|
+
path18.join(agentsPath, "custom.md"),
|
|
2039
|
+
path18.join(agentsPath, "constitution.md"),
|
|
2040
|
+
path18.join(agentsPath, "agents.md")
|
|
2021
2041
|
];
|
|
2022
2042
|
let lang = "en";
|
|
2023
2043
|
for (const candidate of langProbeCandidates) {
|
|
2024
|
-
if (!await
|
|
2025
|
-
const content = await
|
|
2044
|
+
if (!await fs14.pathExists(candidate)) continue;
|
|
2045
|
+
const content = await fs14.readFile(candidate, "utf-8");
|
|
2026
2046
|
if (/[가-힣]/.test(content)) {
|
|
2027
2047
|
lang = "ko";
|
|
2028
2048
|
break;
|
|
@@ -2068,14 +2088,14 @@ function sanitizeTasksForLocal(content, lang) {
|
|
|
2068
2088
|
return normalizeTrailingBlankLines(next);
|
|
2069
2089
|
}
|
|
2070
2090
|
async function patchMarkdownIfExists(filePath, transform) {
|
|
2071
|
-
if (!await
|
|
2072
|
-
const content = await
|
|
2073
|
-
await
|
|
2091
|
+
if (!await fs14.pathExists(filePath)) return;
|
|
2092
|
+
const content = await fs14.readFile(filePath, "utf-8");
|
|
2093
|
+
await fs14.writeFile(filePath, transform(content), "utf-8");
|
|
2074
2094
|
}
|
|
2075
2095
|
async function applyLocalWorkflowTemplateToFeatureDir(featureDir, lang) {
|
|
2076
|
-
await patchMarkdownIfExists(
|
|
2096
|
+
await patchMarkdownIfExists(path18.join(featureDir, "spec.md"), sanitizeSpecForLocal);
|
|
2077
2097
|
await patchMarkdownIfExists(
|
|
2078
|
-
|
|
2098
|
+
path18.join(featureDir, "tasks.md"),
|
|
2079
2099
|
(content) => sanitizeTasksForLocal(content, lang)
|
|
2080
2100
|
);
|
|
2081
2101
|
}
|
|
@@ -2225,29 +2245,29 @@ async function runFeature(name, options) {
|
|
|
2225
2245
|
}
|
|
2226
2246
|
let featuresDir;
|
|
2227
2247
|
if (projectType === "multi") {
|
|
2228
|
-
featuresDir =
|
|
2248
|
+
featuresDir = path18.join(docsDir, "features", component);
|
|
2229
2249
|
} else {
|
|
2230
|
-
featuresDir =
|
|
2250
|
+
featuresDir = path18.join(docsDir, "features");
|
|
2231
2251
|
}
|
|
2232
2252
|
const featureFolderName = `${featureId}-${name}`;
|
|
2233
|
-
const featureDir =
|
|
2234
|
-
if (await
|
|
2253
|
+
const featureDir = path18.join(featuresDir, featureFolderName);
|
|
2254
|
+
if (await fs14.pathExists(featureDir)) {
|
|
2235
2255
|
throw createCliError(
|
|
2236
2256
|
"INVALID_ARGUMENT",
|
|
2237
2257
|
tr(lang, "cli", "feature.folderExists", { path: featureDir })
|
|
2238
2258
|
);
|
|
2239
2259
|
}
|
|
2240
|
-
const featureBasePath =
|
|
2260
|
+
const featureBasePath = path18.join(
|
|
2241
2261
|
getTemplatesDir(),
|
|
2242
2262
|
lang,
|
|
2243
2263
|
toTemplateProjectType(projectType),
|
|
2244
2264
|
"features",
|
|
2245
2265
|
"feature-base"
|
|
2246
2266
|
);
|
|
2247
|
-
if (!await
|
|
2267
|
+
if (!await fs14.pathExists(featureBasePath)) {
|
|
2248
2268
|
throw createCliError("DOCS_NOT_FOUND", tr(lang, "cli", "feature.baseNotFound"));
|
|
2249
2269
|
}
|
|
2250
|
-
await
|
|
2270
|
+
await fs14.copy(featureBasePath, featureDir);
|
|
2251
2271
|
const idNumber = featureId.replace("F", "");
|
|
2252
2272
|
const repoName = projectType === "multi" ? `{{projectName}}-${component}` : "{{projectName}}";
|
|
2253
2273
|
const replacements = {
|
|
@@ -2294,7 +2314,7 @@ async function runFeature(name, options) {
|
|
|
2294
2314
|
featureName: name,
|
|
2295
2315
|
component: projectType === "multi" ? component : void 0,
|
|
2296
2316
|
featurePath: featureDir,
|
|
2297
|
-
featurePathFromDocs:
|
|
2317
|
+
featurePathFromDocs: path18.relative(docsDir, featureDir)
|
|
2298
2318
|
};
|
|
2299
2319
|
},
|
|
2300
2320
|
{ owner: "feature" }
|
|
@@ -2306,9 +2326,9 @@ function sleep2(ms) {
|
|
|
2306
2326
|
async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
|
|
2307
2327
|
const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
|
|
2308
2328
|
const candidates = [
|
|
2309
|
-
...explicitDocsDir ? [
|
|
2310
|
-
|
|
2311
|
-
|
|
2329
|
+
...explicitDocsDir ? [path18.resolve(explicitDocsDir)] : [],
|
|
2330
|
+
path18.resolve(cwd, "docs"),
|
|
2331
|
+
path18.resolve(cwd)
|
|
2312
2332
|
];
|
|
2313
2333
|
const endAt = Date.now() + timeoutMs;
|
|
2314
2334
|
while (Date.now() < endAt) {
|
|
@@ -2319,7 +2339,7 @@ async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
|
|
|
2319
2339
|
const initLockPath = getInitLockPath(dir);
|
|
2320
2340
|
const docsLockPath = getDocsLockPath(dir);
|
|
2321
2341
|
for (const lockPath of [initLockPath, docsLockPath]) {
|
|
2322
|
-
if (await
|
|
2342
|
+
if (await fs14.pathExists(lockPath)) {
|
|
2323
2343
|
sawLock = true;
|
|
2324
2344
|
await waitForLockRelease(lockPath, {
|
|
2325
2345
|
timeoutMs: Math.max(200, endAt - Date.now()),
|
|
@@ -2335,17 +2355,17 @@ async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
|
|
|
2335
2355
|
return getConfig(cwd);
|
|
2336
2356
|
}
|
|
2337
2357
|
async function getNextFeatureId(docsDir, projectType, components) {
|
|
2338
|
-
const featuresDir =
|
|
2358
|
+
const featuresDir = path18.join(docsDir, "features");
|
|
2339
2359
|
let max = 0;
|
|
2340
2360
|
const scanDirs = [];
|
|
2341
2361
|
if (projectType === "multi") {
|
|
2342
|
-
scanDirs.push(...components.map((component) =>
|
|
2362
|
+
scanDirs.push(...components.map((component) => path18.join(featuresDir, component)));
|
|
2343
2363
|
} else {
|
|
2344
2364
|
scanDirs.push(featuresDir);
|
|
2345
2365
|
}
|
|
2346
2366
|
for (const dir of scanDirs) {
|
|
2347
|
-
if (!await
|
|
2348
|
-
const entries = await
|
|
2367
|
+
if (!await fs14.pathExists(dir)) continue;
|
|
2368
|
+
const entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
2349
2369
|
for (const entry of entries) {
|
|
2350
2370
|
if (!entry.isDirectory()) continue;
|
|
2351
2371
|
const match = entry.name.match(/^F(\d+)-/);
|
|
@@ -3113,6 +3133,59 @@ function getGitStatusPorcelain(cwd, relativePaths) {
|
|
|
3113
3133
|
return void 0;
|
|
3114
3134
|
}
|
|
3115
3135
|
}
|
|
3136
|
+
function normalizeInputPath(value) {
|
|
3137
|
+
return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
3138
|
+
}
|
|
3139
|
+
function toUniqueNormalizedPaths(relativePaths) {
|
|
3140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3141
|
+
const out = [];
|
|
3142
|
+
for (const value of relativePaths) {
|
|
3143
|
+
const normalized = normalizeInputPath(value);
|
|
3144
|
+
if (!normalized) continue;
|
|
3145
|
+
if (seen.has(normalized)) continue;
|
|
3146
|
+
seen.add(normalized);
|
|
3147
|
+
out.push(normalized);
|
|
3148
|
+
}
|
|
3149
|
+
return out;
|
|
3150
|
+
}
|
|
3151
|
+
function getTrackedGitPaths(cwd, relativePaths) {
|
|
3152
|
+
const inputs = toUniqueNormalizedPaths(relativePaths);
|
|
3153
|
+
if (inputs.length === 0) return /* @__PURE__ */ new Set();
|
|
3154
|
+
try {
|
|
3155
|
+
const out = execFileSync("git", ["ls-files", "--", ...inputs], {
|
|
3156
|
+
cwd,
|
|
3157
|
+
encoding: "utf-8",
|
|
3158
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3159
|
+
});
|
|
3160
|
+
return new Set(
|
|
3161
|
+
out.split("\n").map((line) => normalizeInputPath(line)).filter(Boolean)
|
|
3162
|
+
);
|
|
3163
|
+
} catch {
|
|
3164
|
+
return void 0;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
function getIgnoredGitPaths(cwd, relativePaths) {
|
|
3168
|
+
const inputs = toUniqueNormalizedPaths(relativePaths);
|
|
3169
|
+
if (inputs.length === 0) return /* @__PURE__ */ new Set();
|
|
3170
|
+
try {
|
|
3171
|
+
const out = execFileSync("git", ["check-ignore", "--stdin"], {
|
|
3172
|
+
cwd,
|
|
3173
|
+
encoding: "utf-8",
|
|
3174
|
+
input: `${inputs.join("\n")}
|
|
3175
|
+
`,
|
|
3176
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3177
|
+
});
|
|
3178
|
+
return new Set(
|
|
3179
|
+
out.split("\n").map((line) => normalizeInputPath(line)).filter(Boolean)
|
|
3180
|
+
);
|
|
3181
|
+
} catch (error) {
|
|
3182
|
+
if (error && typeof error === "object" && "status" in error) {
|
|
3183
|
+
const status = error.status;
|
|
3184
|
+
if (status === 1) return /* @__PURE__ */ new Set();
|
|
3185
|
+
}
|
|
3186
|
+
return void 0;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3116
3189
|
function getLastCommitForPath(cwd, relativePath) {
|
|
3117
3190
|
try {
|
|
3118
3191
|
const out = execSync(`git rev-list -n 1 HEAD -- "${relativePath}"`, {
|
|
@@ -3260,13 +3333,13 @@ function parsePrLink(value) {
|
|
|
3260
3333
|
return trimmed;
|
|
3261
3334
|
}
|
|
3262
3335
|
function normalizeGitPath(value) {
|
|
3263
|
-
return value.split(
|
|
3336
|
+
return value.split(path18.sep).join("/");
|
|
3264
3337
|
}
|
|
3265
3338
|
function resolveProjectStatusPaths(projectGitCwd, docsDir) {
|
|
3266
|
-
const relativeDocsDir =
|
|
3339
|
+
const relativeDocsDir = path18.relative(projectGitCwd, docsDir);
|
|
3267
3340
|
if (!relativeDocsDir) return [];
|
|
3268
|
-
if (
|
|
3269
|
-
if (relativeDocsDir === ".." || relativeDocsDir.startsWith(`..${
|
|
3341
|
+
if (path18.isAbsolute(relativeDocsDir)) return [];
|
|
3342
|
+
if (relativeDocsDir === ".." || relativeDocsDir.startsWith(`..${path18.sep}`)) {
|
|
3270
3343
|
return [];
|
|
3271
3344
|
}
|
|
3272
3345
|
const normalizedDocsDir = normalizeGitPath(relativeDocsDir).replace(/\/+$/, "");
|
|
@@ -3285,6 +3358,8 @@ function uniqueNormalizedPaths(values) {
|
|
|
3285
3358
|
}
|
|
3286
3359
|
return out;
|
|
3287
3360
|
}
|
|
3361
|
+
var PROJECT_DIRTY_STATUS_CACHE = /* @__PURE__ */ new Map();
|
|
3362
|
+
var COMPONENT_STATUS_PATH_CACHE = /* @__PURE__ */ new Map();
|
|
3288
3363
|
async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
|
|
3289
3364
|
const configured = workflow?.componentPaths?.[component];
|
|
3290
3365
|
const configuredCandidates = Array.isArray(configured) ? configured.map((value) => String(value).trim()).filter(Boolean) : [];
|
|
@@ -3298,19 +3373,27 @@ async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
|
|
|
3298
3373
|
const normalizedCandidates = uniqueNormalizedPaths(
|
|
3299
3374
|
candidates.map((candidate) => {
|
|
3300
3375
|
if (!candidate) return "";
|
|
3301
|
-
if (!
|
|
3302
|
-
const relative =
|
|
3376
|
+
if (!path18.isAbsolute(candidate)) return candidate;
|
|
3377
|
+
const relative = path18.relative(projectGitCwd, candidate);
|
|
3303
3378
|
if (!relative) return "";
|
|
3304
|
-
if (relative === ".." || relative.startsWith(`..${
|
|
3379
|
+
if (relative === ".." || relative.startsWith(`..${path18.sep}`)) return "";
|
|
3305
3380
|
return relative;
|
|
3306
3381
|
}).filter(Boolean)
|
|
3307
3382
|
);
|
|
3383
|
+
const cacheKey = JSON.stringify({
|
|
3384
|
+
projectGitCwd,
|
|
3385
|
+
component,
|
|
3386
|
+
normalizedCandidates
|
|
3387
|
+
});
|
|
3388
|
+
const cached = COMPONENT_STATUS_PATH_CACHE.get(cacheKey);
|
|
3389
|
+
if (cached) return [...cached];
|
|
3308
3390
|
const existing = [];
|
|
3309
3391
|
for (const candidate of normalizedCandidates) {
|
|
3310
|
-
if (await
|
|
3392
|
+
if (await fs14.pathExists(path18.join(projectGitCwd, candidate))) {
|
|
3311
3393
|
existing.push(candidate);
|
|
3312
3394
|
}
|
|
3313
3395
|
}
|
|
3396
|
+
COMPONENT_STATUS_PATH_CACHE.set(cacheKey, [...existing]);
|
|
3314
3397
|
return existing;
|
|
3315
3398
|
}
|
|
3316
3399
|
function parseTasks(content) {
|
|
@@ -3370,31 +3453,31 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3370
3453
|
const lang = options.lang;
|
|
3371
3454
|
const workflowPolicy = resolveWorkflowPolicy(options.workflow);
|
|
3372
3455
|
const prePrReviewPolicy = resolvePrePrReviewPolicy(options.workflow);
|
|
3373
|
-
const folderName =
|
|
3456
|
+
const folderName = path18.basename(featurePath);
|
|
3374
3457
|
const match = folderName.match(/^(F\d+)-(.+)$/);
|
|
3375
3458
|
const id = match?.[1];
|
|
3376
3459
|
const slug = match?.[2] || folderName;
|
|
3377
|
-
const specPath =
|
|
3378
|
-
const planPath =
|
|
3379
|
-
const tasksPath =
|
|
3460
|
+
const specPath = path18.join(featurePath, "spec.md");
|
|
3461
|
+
const planPath = path18.join(featurePath, "plan.md");
|
|
3462
|
+
const tasksPath = path18.join(featurePath, "tasks.md");
|
|
3380
3463
|
let specStatus;
|
|
3381
3464
|
let issueNumber;
|
|
3382
|
-
const specExists = await
|
|
3465
|
+
const specExists = await fs14.pathExists(specPath);
|
|
3383
3466
|
if (specExists) {
|
|
3384
|
-
const content = await
|
|
3467
|
+
const content = await fs14.readFile(specPath, "utf-8");
|
|
3385
3468
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
3386
3469
|
specStatus = parseDocStatus(statusValue);
|
|
3387
3470
|
const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
|
|
3388
3471
|
issueNumber = parseIssueNumber(issueValue);
|
|
3389
3472
|
}
|
|
3390
3473
|
let planStatus;
|
|
3391
|
-
const planExists = await
|
|
3474
|
+
const planExists = await fs14.pathExists(planPath);
|
|
3392
3475
|
if (planExists) {
|
|
3393
|
-
const content = await
|
|
3476
|
+
const content = await fs14.readFile(planPath, "utf-8");
|
|
3394
3477
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
3395
3478
|
planStatus = parseDocStatus(statusValue);
|
|
3396
3479
|
}
|
|
3397
|
-
const tasksExists = await
|
|
3480
|
+
const tasksExists = await fs14.pathExists(tasksPath);
|
|
3398
3481
|
const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
|
|
3399
3482
|
let activeTask;
|
|
3400
3483
|
let nextTodoTask;
|
|
@@ -3408,7 +3491,7 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3408
3491
|
let prStatusFieldExists = false;
|
|
3409
3492
|
let prePrReviewFieldExists = false;
|
|
3410
3493
|
if (tasksExists) {
|
|
3411
|
-
const content = await
|
|
3494
|
+
const content = await fs14.readFile(tasksPath, "utf-8");
|
|
3412
3495
|
const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
|
|
3413
3496
|
tasksSummary.total = summary.total;
|
|
3414
3497
|
tasksSummary.todo = summary.todo;
|
|
@@ -3450,44 +3533,77 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3450
3533
|
slug,
|
|
3451
3534
|
folderName
|
|
3452
3535
|
);
|
|
3453
|
-
const relativeFeaturePathFromDocs =
|
|
3454
|
-
const
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
const componentStatusPaths = await resolveComponentStatusPaths(
|
|
3465
|
-
context.projectGitCwd,
|
|
3466
|
-
type,
|
|
3467
|
-
options.workflow
|
|
3468
|
-
);
|
|
3469
|
-
projectStatusPaths = componentStatusPaths.length > 0 ? componentStatusPaths : resolveProjectStatusPaths(context.projectGitCwd, context.docsDir);
|
|
3536
|
+
const relativeFeaturePathFromDocs = path18.relative(context.docsDir, featurePath);
|
|
3537
|
+
const normalizedFeaturePathFromDocs = normalizeGitPath(relativeFeaturePathFromDocs);
|
|
3538
|
+
const docsPathIgnored = typeof context.docsPathIgnored === "boolean" ? context.docsPathIgnored : isGitPathIgnored(context.docsGitCwd, normalizedFeaturePathFromDocs);
|
|
3539
|
+
let docsHasUncommittedChanges = typeof context.docsHasUncommittedChanges === "boolean" ? context.docsHasUncommittedChanges : false;
|
|
3540
|
+
let docsEverCommitted = typeof context.docsEverCommitted === "boolean" ? context.docsEverCommitted : false;
|
|
3541
|
+
let docsGitUnavailable = !!context.docsGitUnavailable;
|
|
3542
|
+
if (typeof context.docsHasUncommittedChanges !== "boolean") {
|
|
3543
|
+
const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [normalizedFeaturePathFromDocs]);
|
|
3544
|
+
if (docsStatus === void 0) {
|
|
3545
|
+
docsGitUnavailable = true;
|
|
3546
|
+
docsHasUncommittedChanges = true;
|
|
3470
3547
|
} else {
|
|
3471
|
-
|
|
3548
|
+
docsHasUncommittedChanges = docsStatus.trim().length > 0;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
if (typeof context.docsEverCommitted !== "boolean") {
|
|
3552
|
+
const docsLastCommit = getLastCommitForPath(
|
|
3553
|
+
context.docsGitCwd,
|
|
3554
|
+
normalizedFeaturePathFromDocs
|
|
3555
|
+
);
|
|
3556
|
+
docsEverCommitted = !!docsLastCommit;
|
|
3557
|
+
}
|
|
3558
|
+
let projectHasUncommittedChanges = typeof context.projectHasUncommittedChanges === "boolean" ? context.projectHasUncommittedChanges : false;
|
|
3559
|
+
let projectStatusUnavailable = !!context.projectStatusUnavailable;
|
|
3560
|
+
if (typeof context.projectHasUncommittedChanges !== "boolean" && context.projectGitCwd) {
|
|
3561
|
+
const dirtyScopePolicy = resolveCodeDirtyScopePolicy(options.workflow, options.projectType);
|
|
3562
|
+
const projectCacheKey = JSON.stringify({
|
|
3563
|
+
projectGitCwd: context.projectGitCwd,
|
|
3564
|
+
docsDir: context.docsDir,
|
|
3565
|
+
type,
|
|
3566
|
+
dirtyScopePolicy,
|
|
3567
|
+
componentPaths: options.workflow?.componentPaths?.[type] || []
|
|
3568
|
+
});
|
|
3569
|
+
const cachedStatus = PROJECT_DIRTY_STATUS_CACHE.get(projectCacheKey);
|
|
3570
|
+
if (cachedStatus) {
|
|
3571
|
+
projectHasUncommittedChanges = cachedStatus.hasUncommittedChanges;
|
|
3572
|
+
projectStatusUnavailable = cachedStatus.statusUnavailable;
|
|
3573
|
+
} else {
|
|
3574
|
+
let projectStatusPaths = [];
|
|
3575
|
+
if (dirtyScopePolicy === "component" && type !== "single") {
|
|
3576
|
+
const componentStatusPaths = await resolveComponentStatusPaths(
|
|
3577
|
+
context.projectGitCwd,
|
|
3578
|
+
type,
|
|
3579
|
+
options.workflow
|
|
3580
|
+
);
|
|
3581
|
+
projectStatusPaths = componentStatusPaths.length > 0 ? componentStatusPaths : resolveProjectStatusPaths(context.projectGitCwd, context.docsDir);
|
|
3582
|
+
} else {
|
|
3583
|
+
projectStatusPaths = resolveProjectStatusPaths(
|
|
3584
|
+
context.projectGitCwd,
|
|
3585
|
+
context.docsDir
|
|
3586
|
+
);
|
|
3587
|
+
}
|
|
3588
|
+
const projectStatus = getGitStatusPorcelain(
|
|
3472
3589
|
context.projectGitCwd,
|
|
3473
|
-
|
|
3590
|
+
projectStatusPaths
|
|
3474
3591
|
);
|
|
3592
|
+
projectStatusUnavailable = projectStatus === void 0;
|
|
3593
|
+
projectHasUncommittedChanges = projectStatus === void 0 ? false : projectStatus.trim().length > 0;
|
|
3594
|
+
PROJECT_DIRTY_STATUS_CACHE.set(projectCacheKey, {
|
|
3595
|
+
hasUncommittedChanges: projectHasUncommittedChanges,
|
|
3596
|
+
statusUnavailable: projectStatusUnavailable
|
|
3597
|
+
});
|
|
3475
3598
|
}
|
|
3476
3599
|
}
|
|
3477
|
-
|
|
3478
|
-
const projectHasUncommittedChanges = projectStatus === void 0 ? false : projectStatus.trim().length > 0;
|
|
3479
|
-
const docsLastCommit = getLastCommitForPath(
|
|
3480
|
-
context.docsGitCwd,
|
|
3481
|
-
relativeFeaturePathFromDocs
|
|
3482
|
-
);
|
|
3483
|
-
const docsEverCommitted = !!docsLastCommit;
|
|
3484
|
-
if (docsStatus === void 0) {
|
|
3600
|
+
if (docsGitUnavailable) {
|
|
3485
3601
|
warnings.push(tr(lang, "warnings", "docsGitUnavailable"));
|
|
3486
3602
|
}
|
|
3487
3603
|
if (docsPathIgnored === true) {
|
|
3488
3604
|
warnings.push(
|
|
3489
3605
|
tr(lang, "warnings", "docsPathIgnored", {
|
|
3490
|
-
path:
|
|
3606
|
+
path: normalizedFeaturePathFromDocs
|
|
3491
3607
|
})
|
|
3492
3608
|
);
|
|
3493
3609
|
}
|
|
@@ -3592,90 +3708,181 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3592
3708
|
);
|
|
3593
3709
|
return { ...featureState, currentStep, actions, nextAction, warnings };
|
|
3594
3710
|
}
|
|
3711
|
+
function normalizeRelPath(value) {
|
|
3712
|
+
return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
3713
|
+
}
|
|
3714
|
+
function parsePorcelainChangedPaths(porcelain) {
|
|
3715
|
+
const changed = [];
|
|
3716
|
+
for (const rawLine of porcelain.split("\n")) {
|
|
3717
|
+
if (!rawLine.trim()) continue;
|
|
3718
|
+
const payload = rawLine.slice(3).trim();
|
|
3719
|
+
if (!payload) continue;
|
|
3720
|
+
const pathCandidate = payload.includes(" -> ") ? payload.split(" -> ").at(-1) || "" : payload;
|
|
3721
|
+
const normalized = normalizeRelPath(pathCandidate.replace(/^"+|"+$/g, ""));
|
|
3722
|
+
if (!normalized) continue;
|
|
3723
|
+
changed.push(normalized);
|
|
3724
|
+
}
|
|
3725
|
+
return changed;
|
|
3726
|
+
}
|
|
3727
|
+
function findFeaturePathPrefix(normalizedPath, relativeFeaturePaths) {
|
|
3728
|
+
for (const featurePath of relativeFeaturePaths) {
|
|
3729
|
+
if (normalizedPath === featurePath) return featurePath;
|
|
3730
|
+
if (normalizedPath.startsWith(`${featurePath}/`)) return featurePath;
|
|
3731
|
+
const nestedPrefix = `/${featurePath}`;
|
|
3732
|
+
if (normalizedPath.endsWith(nestedPrefix)) return featurePath;
|
|
3733
|
+
if (normalizedPath.includes(`${nestedPrefix}/`)) return featurePath;
|
|
3734
|
+
}
|
|
3735
|
+
return void 0;
|
|
3736
|
+
}
|
|
3737
|
+
function buildDefaultDocsFeatureGitMeta(relativeFeaturePaths) {
|
|
3738
|
+
const map = /* @__PURE__ */ new Map();
|
|
3739
|
+
for (const featurePath of relativeFeaturePaths) {
|
|
3740
|
+
map.set(featurePath, {
|
|
3741
|
+
docsPathIgnored: false,
|
|
3742
|
+
docsHasUncommittedChanges: false,
|
|
3743
|
+
docsEverCommitted: false,
|
|
3744
|
+
docsGitUnavailable: false
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3747
|
+
return map;
|
|
3748
|
+
}
|
|
3749
|
+
function buildDocsFeatureGitMeta(docsGitCwd, relativeFeaturePaths) {
|
|
3750
|
+
const normalizedFeaturePaths = relativeFeaturePaths.map(
|
|
3751
|
+
(value) => normalizeRelPath(value)
|
|
3752
|
+
);
|
|
3753
|
+
const map = buildDefaultDocsFeatureGitMeta(normalizedFeaturePaths);
|
|
3754
|
+
if (normalizedFeaturePaths.length === 0) return map;
|
|
3755
|
+
const docsStatus = getGitStatusPorcelain(docsGitCwd, normalizedFeaturePaths);
|
|
3756
|
+
if (docsStatus === void 0) {
|
|
3757
|
+
for (const featurePath of normalizedFeaturePaths) {
|
|
3758
|
+
const current = map.get(featurePath);
|
|
3759
|
+
if (!current) continue;
|
|
3760
|
+
current.docsGitUnavailable = true;
|
|
3761
|
+
current.docsHasUncommittedChanges = true;
|
|
3762
|
+
}
|
|
3763
|
+
} else {
|
|
3764
|
+
const changedPaths = parsePorcelainChangedPaths(docsStatus);
|
|
3765
|
+
for (const changedPath of changedPaths) {
|
|
3766
|
+
const featurePath = findFeaturePathPrefix(changedPath, normalizedFeaturePaths);
|
|
3767
|
+
if (!featurePath) continue;
|
|
3768
|
+
const current = map.get(featurePath);
|
|
3769
|
+
if (!current) continue;
|
|
3770
|
+
current.docsHasUncommittedChanges = true;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
const trackedPaths = getTrackedGitPaths(docsGitCwd, normalizedFeaturePaths);
|
|
3774
|
+
if (trackedPaths) {
|
|
3775
|
+
for (const trackedPath of trackedPaths) {
|
|
3776
|
+
const featurePath = findFeaturePathPrefix(
|
|
3777
|
+
normalizeRelPath(trackedPath),
|
|
3778
|
+
normalizedFeaturePaths
|
|
3779
|
+
);
|
|
3780
|
+
if (!featurePath) continue;
|
|
3781
|
+
const current = map.get(featurePath);
|
|
3782
|
+
if (!current) continue;
|
|
3783
|
+
current.docsEverCommitted = true;
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
const ignoredPaths = getIgnoredGitPaths(docsGitCwd, normalizedFeaturePaths);
|
|
3787
|
+
if (ignoredPaths) {
|
|
3788
|
+
for (const ignoredPath of ignoredPaths) {
|
|
3789
|
+
const featurePath = findFeaturePathPrefix(
|
|
3790
|
+
normalizeRelPath(ignoredPath),
|
|
3791
|
+
normalizedFeaturePaths
|
|
3792
|
+
);
|
|
3793
|
+
if (!featurePath) continue;
|
|
3794
|
+
const current = map.get(featurePath);
|
|
3795
|
+
if (!current) continue;
|
|
3796
|
+
current.docsPathIgnored = true;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
return map;
|
|
3800
|
+
}
|
|
3595
3801
|
async function scanFeatures(config) {
|
|
3596
3802
|
const features = [];
|
|
3597
3803
|
const warnings = [];
|
|
3598
3804
|
const stepDefinitions = getStepDefinitions(config.lang, config.workflow);
|
|
3599
3805
|
const docsBranch = getCurrentBranch(config.docsDir);
|
|
3600
3806
|
const projectBranches = {};
|
|
3807
|
+
const projectGitCwds = {};
|
|
3601
3808
|
let singleProject;
|
|
3602
3809
|
if (config.projectType === "single") {
|
|
3603
3810
|
singleProject = resolveProjectGitCwd(config, "single", config.lang);
|
|
3604
3811
|
if (singleProject.warning) warnings.push(singleProject.warning);
|
|
3605
3812
|
projectBranches.single = singleProject.cwd ? getCurrentBranch(singleProject.cwd) : "";
|
|
3813
|
+
projectGitCwds.single = singleProject.cwd ?? void 0;
|
|
3606
3814
|
} else {
|
|
3607
3815
|
const components = resolveProjectComponents(config.projectType, config.components);
|
|
3608
3816
|
for (const component of components) {
|
|
3609
3817
|
const project = resolveProjectGitCwd(config, component, config.lang);
|
|
3610
3818
|
if (project.warning) warnings.push(project.warning);
|
|
3611
3819
|
projectBranches[component] = project.cwd ? getCurrentBranch(project.cwd) : "";
|
|
3820
|
+
projectGitCwds[component] = project.cwd ?? void 0;
|
|
3612
3821
|
}
|
|
3613
3822
|
}
|
|
3823
|
+
const allFeatureDirs = [];
|
|
3824
|
+
const componentFeatureDirs = /* @__PURE__ */ new Map();
|
|
3614
3825
|
if (config.projectType === "single") {
|
|
3615
3826
|
const featureDirs = await glob("features/*/", {
|
|
3616
3827
|
cwd: config.docsDir,
|
|
3617
3828
|
absolute: true,
|
|
3618
3829
|
ignore: ["**/feature-base/**"]
|
|
3619
3830
|
});
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
features.push(
|
|
3623
|
-
await parseFeature(
|
|
3624
|
-
dir,
|
|
3625
|
-
"single",
|
|
3626
|
-
{
|
|
3627
|
-
projectBranch: projectBranches.single,
|
|
3628
|
-
docsBranch,
|
|
3629
|
-
docsGitCwd: config.docsDir,
|
|
3630
|
-
projectGitCwd: singleProject?.cwd ?? void 0,
|
|
3631
|
-
docsDir: config.docsDir,
|
|
3632
|
-
projectBranchAvailable: Boolean(singleProject?.cwd)
|
|
3633
|
-
},
|
|
3634
|
-
{
|
|
3635
|
-
lang: config.lang,
|
|
3636
|
-
stepDefinitions,
|
|
3637
|
-
approval: config.approval,
|
|
3638
|
-
workflow: config.workflow,
|
|
3639
|
-
projectType: config.projectType
|
|
3640
|
-
}
|
|
3641
|
-
)
|
|
3642
|
-
);
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
3831
|
+
componentFeatureDirs.set("single", featureDirs);
|
|
3832
|
+
allFeatureDirs.push(...featureDirs);
|
|
3645
3833
|
} else {
|
|
3646
3834
|
const components = resolveProjectComponents(config.projectType, config.components);
|
|
3647
3835
|
for (const component of components) {
|
|
3648
|
-
const project = resolveProjectGitCwd(config, component, config.lang);
|
|
3649
3836
|
const componentDirs = await glob(`features/${component}/*/`, {
|
|
3650
3837
|
cwd: config.docsDir,
|
|
3651
3838
|
absolute: true
|
|
3652
3839
|
});
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
features.push(
|
|
3656
|
-
await parseFeature(
|
|
3657
|
-
dir,
|
|
3658
|
-
component,
|
|
3659
|
-
{
|
|
3660
|
-
projectBranch: projectBranches[component] || "",
|
|
3661
|
-
docsBranch,
|
|
3662
|
-
docsGitCwd: config.docsDir,
|
|
3663
|
-
projectGitCwd: project.cwd ?? void 0,
|
|
3664
|
-
docsDir: config.docsDir,
|
|
3665
|
-
projectBranchAvailable: Boolean(project.cwd)
|
|
3666
|
-
},
|
|
3667
|
-
{
|
|
3668
|
-
lang: config.lang,
|
|
3669
|
-
stepDefinitions,
|
|
3670
|
-
approval: config.approval,
|
|
3671
|
-
workflow: config.workflow,
|
|
3672
|
-
projectType: config.projectType
|
|
3673
|
-
}
|
|
3674
|
-
)
|
|
3675
|
-
);
|
|
3676
|
-
}
|
|
3840
|
+
componentFeatureDirs.set(component, componentDirs);
|
|
3841
|
+
allFeatureDirs.push(...componentDirs);
|
|
3677
3842
|
}
|
|
3678
3843
|
}
|
|
3844
|
+
const relativeFeaturePaths = allFeatureDirs.map(
|
|
3845
|
+
(dir) => normalizeRelPath(path18.relative(config.docsDir, dir))
|
|
3846
|
+
);
|
|
3847
|
+
const docsGitMeta = buildDocsFeatureGitMeta(config.docsDir, relativeFeaturePaths);
|
|
3848
|
+
const parseTargets = config.projectType === "single" ? [{ type: "single", dirs: componentFeatureDirs.get("single") || [] }] : resolveProjectComponents(config.projectType, config.components).map((component) => ({
|
|
3849
|
+
type: component,
|
|
3850
|
+
dirs: componentFeatureDirs.get(component) || []
|
|
3851
|
+
}));
|
|
3852
|
+
for (const target of parseTargets) {
|
|
3853
|
+
const parsed = await Promise.all(
|
|
3854
|
+
target.dirs.map(async (dir) => {
|
|
3855
|
+
const relativeFeaturePathFromDocs = normalizeRelPath(
|
|
3856
|
+
path18.relative(config.docsDir, dir)
|
|
3857
|
+
);
|
|
3858
|
+
const docsMeta = docsGitMeta.get(relativeFeaturePathFromDocs);
|
|
3859
|
+
return parseFeature(
|
|
3860
|
+
dir,
|
|
3861
|
+
target.type,
|
|
3862
|
+
{
|
|
3863
|
+
projectBranch: projectBranches[target.type] || "",
|
|
3864
|
+
docsBranch,
|
|
3865
|
+
docsGitCwd: config.docsDir,
|
|
3866
|
+
projectGitCwd: projectGitCwds[target.type],
|
|
3867
|
+
docsDir: config.docsDir,
|
|
3868
|
+
projectBranchAvailable: Boolean(projectGitCwds[target.type]),
|
|
3869
|
+
docsPathIgnored: docsMeta?.docsPathIgnored,
|
|
3870
|
+
docsHasUncommittedChanges: docsMeta?.docsHasUncommittedChanges,
|
|
3871
|
+
docsEverCommitted: docsMeta?.docsEverCommitted,
|
|
3872
|
+
docsGitUnavailable: docsMeta?.docsGitUnavailable
|
|
3873
|
+
},
|
|
3874
|
+
{
|
|
3875
|
+
lang: config.lang,
|
|
3876
|
+
stepDefinitions,
|
|
3877
|
+
approval: config.approval,
|
|
3878
|
+
workflow: config.workflow,
|
|
3879
|
+
projectType: config.projectType
|
|
3880
|
+
}
|
|
3881
|
+
);
|
|
3882
|
+
})
|
|
3883
|
+
);
|
|
3884
|
+
features.push(...parsed);
|
|
3885
|
+
}
|
|
3679
3886
|
return {
|
|
3680
3887
|
features,
|
|
3681
3888
|
branches: {
|
|
@@ -3715,13 +3922,13 @@ async function runStatus(options) {
|
|
|
3715
3922
|
);
|
|
3716
3923
|
}
|
|
3717
3924
|
const { docsDir, projectType, projectName, lang } = config;
|
|
3718
|
-
const featuresDir =
|
|
3925
|
+
const featuresDir = path18.join(docsDir, "features");
|
|
3719
3926
|
const scan = await scanFeatures(config);
|
|
3720
3927
|
const features = [];
|
|
3721
3928
|
const idMap = /* @__PURE__ */ new Map();
|
|
3722
3929
|
for (const f of scan.features) {
|
|
3723
3930
|
const id = f.id || "UNKNOWN";
|
|
3724
|
-
const relPath =
|
|
3931
|
+
const relPath = path18.relative(docsDir, f.path);
|
|
3725
3932
|
if (!idMap.has(id)) idMap.set(id, []);
|
|
3726
3933
|
idMap.get(id).push(relPath);
|
|
3727
3934
|
if (!f.docs.specExists || !f.docs.tasksExists) continue;
|
|
@@ -3802,7 +4009,7 @@ async function runStatus(options) {
|
|
|
3802
4009
|
}
|
|
3803
4010
|
console.log();
|
|
3804
4011
|
if (options.write) {
|
|
3805
|
-
const outputPath =
|
|
4012
|
+
const outputPath = path18.join(featuresDir, "status.md");
|
|
3806
4013
|
const date = getLocalDateString();
|
|
3807
4014
|
const content = [
|
|
3808
4015
|
"# Feature Status",
|
|
@@ -3817,7 +4024,7 @@ async function runStatus(options) {
|
|
|
3817
4024
|
),
|
|
3818
4025
|
""
|
|
3819
4026
|
].join("\n");
|
|
3820
|
-
await
|
|
4027
|
+
await fs14.writeFile(outputPath, content, "utf-8");
|
|
3821
4028
|
console.log(
|
|
3822
4029
|
chalk6.green(
|
|
3823
4030
|
tr(lang, "cli", "status.wrote", { path: outputPath })
|
|
@@ -3830,9 +4037,9 @@ function escapeRegExp2(value) {
|
|
|
3830
4037
|
}
|
|
3831
4038
|
async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderName) {
|
|
3832
4039
|
try {
|
|
3833
|
-
const specPath =
|
|
3834
|
-
if (!await
|
|
3835
|
-
const content = await
|
|
4040
|
+
const specPath = path18.join(featureDir, "spec.md");
|
|
4041
|
+
if (!await fs14.pathExists(specPath)) return fallbackSlug;
|
|
4042
|
+
const content = await fs14.readFile(specPath, "utf-8");
|
|
3836
4043
|
const keys = ["\uAE30\uB2A5\uBA85", "Feature Name"];
|
|
3837
4044
|
for (const key of keys) {
|
|
3838
4045
|
const regex = new RegExp(
|
|
@@ -3911,17 +4118,17 @@ async function runUpdate(options) {
|
|
|
3911
4118
|
console.log(chalk6.blue(tr(lang, "cli", "update.updatingAgents")));
|
|
3912
4119
|
}
|
|
3913
4120
|
if (agentsMode === "all") {
|
|
3914
|
-
const commonAgentsBase =
|
|
3915
|
-
const typeAgentsBase =
|
|
4121
|
+
const commonAgentsBase = path18.join(templatesDir, lang, "common", "agents");
|
|
4122
|
+
const typeAgentsBase = path18.join(
|
|
3916
4123
|
templatesDir,
|
|
3917
4124
|
lang,
|
|
3918
4125
|
toTemplateProjectType(projectType),
|
|
3919
4126
|
"agents"
|
|
3920
4127
|
);
|
|
3921
|
-
const targetAgentsBase =
|
|
3922
|
-
const commonAgents = agentsMode === "skills" ?
|
|
3923
|
-
const typeAgents = agentsMode === "skills" ?
|
|
3924
|
-
const targetAgents = agentsMode === "skills" ?
|
|
4128
|
+
const targetAgentsBase = path18.join(docsDir, "agents");
|
|
4129
|
+
const commonAgents = agentsMode === "skills" ? path18.join(commonAgentsBase, "skills") : commonAgentsBase;
|
|
4130
|
+
const typeAgents = agentsMode === "skills" ? path18.join(typeAgentsBase, "skills") : typeAgentsBase;
|
|
4131
|
+
const targetAgents = agentsMode === "skills" ? path18.join(targetAgentsBase, "skills") : targetAgentsBase;
|
|
3925
4132
|
const featurePath = projectType === "multi" ? isDefaultFullstackComponents(config.components || []) ? "docs/features/{be|fe}" : "docs/features/{component}" : "docs/features";
|
|
3926
4133
|
const projectName = config.projectName ?? "{{projectName}}";
|
|
3927
4134
|
const commonReplacements = {
|
|
@@ -3931,7 +4138,7 @@ async function runUpdate(options) {
|
|
|
3931
4138
|
const typeReplacements = {
|
|
3932
4139
|
"{{projectName}}": projectName
|
|
3933
4140
|
};
|
|
3934
|
-
if (await
|
|
4141
|
+
if (await fs14.pathExists(commonAgents)) {
|
|
3935
4142
|
const count = await updateFolder(
|
|
3936
4143
|
commonAgents,
|
|
3937
4144
|
targetAgents,
|
|
@@ -3949,7 +4156,7 @@ async function runUpdate(options) {
|
|
|
3949
4156
|
);
|
|
3950
4157
|
updatedCount += count;
|
|
3951
4158
|
}
|
|
3952
|
-
if (await
|
|
4159
|
+
if (await fs14.pathExists(typeAgents)) {
|
|
3953
4160
|
const count = await updateFolder(
|
|
3954
4161
|
typeAgents,
|
|
3955
4162
|
targetAgents,
|
|
@@ -3999,24 +4206,24 @@ async function runUpdate(options) {
|
|
|
3999
4206
|
async function updateFolder(sourceDir, targetDir, force, replacements, lang = DEFAULT_LANG, options = {}) {
|
|
4000
4207
|
const protectedFiles = options.protectedFiles ?? /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
|
|
4001
4208
|
const skipDirectories = options.skipDirectories ?? /* @__PURE__ */ new Set();
|
|
4002
|
-
await
|
|
4003
|
-
const files = await
|
|
4209
|
+
await fs14.ensureDir(targetDir);
|
|
4210
|
+
const files = await fs14.readdir(sourceDir);
|
|
4004
4211
|
let updatedCount = 0;
|
|
4005
4212
|
for (const file of files) {
|
|
4006
|
-
const sourcePath =
|
|
4007
|
-
const targetPath =
|
|
4008
|
-
const stat = await
|
|
4213
|
+
const sourcePath = path18.join(sourceDir, file);
|
|
4214
|
+
const targetPath = path18.join(targetDir, file);
|
|
4215
|
+
const stat = await fs14.stat(sourcePath);
|
|
4009
4216
|
if (stat.isFile()) {
|
|
4010
4217
|
if (protectedFiles.has(file)) {
|
|
4011
4218
|
continue;
|
|
4012
4219
|
}
|
|
4013
|
-
let sourceContent = await
|
|
4220
|
+
let sourceContent = await fs14.readFile(sourcePath, "utf-8");
|
|
4014
4221
|
if (replacements) {
|
|
4015
4222
|
sourceContent = applyReplacements(sourceContent, replacements);
|
|
4016
4223
|
}
|
|
4017
4224
|
let shouldUpdate = true;
|
|
4018
|
-
if (await
|
|
4019
|
-
const targetContent = await
|
|
4225
|
+
if (await fs14.pathExists(targetPath)) {
|
|
4226
|
+
const targetContent = await fs14.readFile(targetPath, "utf-8");
|
|
4020
4227
|
if (sourceContent === targetContent) {
|
|
4021
4228
|
continue;
|
|
4022
4229
|
}
|
|
@@ -4030,7 +4237,7 @@ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DE
|
|
|
4030
4237
|
}
|
|
4031
4238
|
}
|
|
4032
4239
|
if (shouldUpdate) {
|
|
4033
|
-
await
|
|
4240
|
+
await fs14.writeFile(targetPath, sourceContent);
|
|
4034
4241
|
console.log(
|
|
4035
4242
|
chalk6.gray(` \u{1F4C4} ${tr(lang, "cli", "update.fileUpdated", { file })}`)
|
|
4036
4243
|
);
|
|
@@ -4087,7 +4294,7 @@ function extractPorcelainPaths(line) {
|
|
|
4087
4294
|
function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
|
|
4088
4295
|
const top = getGitTopLevel2(docsDir);
|
|
4089
4296
|
if (!top) return null;
|
|
4090
|
-
const rel =
|
|
4297
|
+
const rel = path18.relative(top, docsDir) || ".";
|
|
4091
4298
|
try {
|
|
4092
4299
|
const output = execFileSync("git", ["status", "--porcelain=v1", "--", rel], {
|
|
4093
4300
|
cwd: top,
|
|
@@ -4099,7 +4306,7 @@ function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
|
|
|
4099
4306
|
}
|
|
4100
4307
|
const ignoredRelPaths = new Set(
|
|
4101
4308
|
ignoredAbsPaths.map(
|
|
4102
|
-
(absPath) => normalizeGitPath2(
|
|
4309
|
+
(absPath) => normalizeGitPath2(path18.relative(top, absPath) || ".")
|
|
4103
4310
|
)
|
|
4104
4311
|
);
|
|
4105
4312
|
const filtered = output.split("\n").filter((line) => {
|
|
@@ -4156,7 +4363,7 @@ ${tr(lang2, "cli", "common.canceled")}`));
|
|
|
4156
4363
|
}
|
|
4157
4364
|
async function runConfig(options) {
|
|
4158
4365
|
const cwd = process.cwd();
|
|
4159
|
-
const targetCwd = options.dir ?
|
|
4366
|
+
const targetCwd = options.dir ? path18.resolve(cwd, options.dir) : cwd;
|
|
4160
4367
|
const config = await getConfig(targetCwd);
|
|
4161
4368
|
if (!config) {
|
|
4162
4369
|
throw createCliError(
|
|
@@ -4164,7 +4371,7 @@ async function runConfig(options) {
|
|
|
4164
4371
|
tr(DEFAULT_LANG, "cli", "common.configNotFound")
|
|
4165
4372
|
);
|
|
4166
4373
|
}
|
|
4167
|
-
const configPath =
|
|
4374
|
+
const configPath = path18.join(config.docsDir, ".lee-spec-kit.json");
|
|
4168
4375
|
if (!options.projectRoot) {
|
|
4169
4376
|
console.log();
|
|
4170
4377
|
console.log(chalk6.blue(tr(config.lang, "cli", "config.currentTitle")));
|
|
@@ -4175,7 +4382,7 @@ async function runConfig(options) {
|
|
|
4175
4382
|
)
|
|
4176
4383
|
);
|
|
4177
4384
|
console.log();
|
|
4178
|
-
const configFile = await
|
|
4385
|
+
const configFile = await fs14.readJson(configPath);
|
|
4179
4386
|
console.log(JSON.stringify(configFile, null, 2));
|
|
4180
4387
|
console.log();
|
|
4181
4388
|
return;
|
|
@@ -4183,7 +4390,7 @@ async function runConfig(options) {
|
|
|
4183
4390
|
await withFileLock(
|
|
4184
4391
|
getDocsLockPath(config.docsDir),
|
|
4185
4392
|
async () => {
|
|
4186
|
-
const configFile = await
|
|
4393
|
+
const configFile = await fs14.readJson(configPath);
|
|
4187
4394
|
if (configFile.docsRepo !== "standalone") {
|
|
4188
4395
|
console.log(
|
|
4189
4396
|
chalk6.yellow(tr(config.lang, "cli", "config.projectRootStandaloneOnly"))
|
|
@@ -4262,7 +4469,7 @@ async function runConfig(options) {
|
|
|
4262
4469
|
)
|
|
4263
4470
|
);
|
|
4264
4471
|
}
|
|
4265
|
-
await
|
|
4472
|
+
await fs14.writeJson(configPath, configFile, { spaces: 2 });
|
|
4266
4473
|
console.log();
|
|
4267
4474
|
},
|
|
4268
4475
|
{ owner: "config" }
|
|
@@ -4516,42 +4723,42 @@ var BUILTIN_DOC_DEFINITIONS = [
|
|
|
4516
4723
|
{
|
|
4517
4724
|
id: "agents",
|
|
4518
4725
|
title: { ko: "\uC5D0\uC774\uC804\uD2B8 \uC6B4\uC601 \uADDC\uCE59", en: "Agent Operating Rules" },
|
|
4519
|
-
relativePath: (projectType, lang) =>
|
|
4726
|
+
relativePath: (projectType, lang) => path18.join(lang, toTemplateProjectType(projectType), "agents", "agents.md")
|
|
4520
4727
|
},
|
|
4521
4728
|
{
|
|
4522
4729
|
id: "git-workflow",
|
|
4523
4730
|
title: { ko: "Git \uC6CC\uD06C\uD50C\uB85C\uC6B0", en: "Git Workflow" },
|
|
4524
|
-
relativePath: (_, lang) =>
|
|
4731
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "git-workflow.md")
|
|
4525
4732
|
},
|
|
4526
4733
|
{
|
|
4527
4734
|
id: "issue-template",
|
|
4528
4735
|
title: { ko: "Issue \uD15C\uD50C\uB9BF", en: "Issue Template" },
|
|
4529
|
-
relativePath: (_, lang) =>
|
|
4736
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "issue-template.md")
|
|
4530
4737
|
},
|
|
4531
4738
|
{
|
|
4532
4739
|
id: "pr-template",
|
|
4533
4740
|
title: { ko: "PR \uD15C\uD50C\uB9BF", en: "PR Template" },
|
|
4534
|
-
relativePath: (_, lang) =>
|
|
4741
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "pr-template.md")
|
|
4535
4742
|
},
|
|
4536
4743
|
{
|
|
4537
4744
|
id: "create-feature",
|
|
4538
4745
|
title: { ko: "create-feature \uC2A4\uD0AC", en: "create-feature skill" },
|
|
4539
|
-
relativePath: (_, lang) =>
|
|
4746
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "create-feature.md")
|
|
4540
4747
|
},
|
|
4541
4748
|
{
|
|
4542
4749
|
id: "execute-task",
|
|
4543
4750
|
title: { ko: "execute-task \uC2A4\uD0AC", en: "execute-task skill" },
|
|
4544
|
-
relativePath: (_, lang) =>
|
|
4751
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "execute-task.md")
|
|
4545
4752
|
},
|
|
4546
4753
|
{
|
|
4547
4754
|
id: "create-issue",
|
|
4548
4755
|
title: { ko: "create-issue \uC2A4\uD0AC", en: "create-issue skill" },
|
|
4549
|
-
relativePath: (_, lang) =>
|
|
4756
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "create-issue.md")
|
|
4550
4757
|
},
|
|
4551
4758
|
{
|
|
4552
4759
|
id: "create-pr",
|
|
4553
4760
|
title: { ko: "create-pr \uC2A4\uD0AC", en: "create-pr skill" },
|
|
4554
|
-
relativePath: (_, lang) =>
|
|
4761
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "create-pr.md")
|
|
4555
4762
|
}
|
|
4556
4763
|
];
|
|
4557
4764
|
var DOC_FOLLOWUPS = {
|
|
@@ -4635,7 +4842,7 @@ function listBuiltinDocs(projectType, lang) {
|
|
|
4635
4842
|
id: doc.id,
|
|
4636
4843
|
title: doc.title[lang],
|
|
4637
4844
|
relativePath,
|
|
4638
|
-
absolutePath:
|
|
4845
|
+
absolutePath: path18.join(templatesDir, relativePath)
|
|
4639
4846
|
};
|
|
4640
4847
|
});
|
|
4641
4848
|
}
|
|
@@ -4644,7 +4851,7 @@ async function getBuiltinDoc(docId, projectType, lang) {
|
|
|
4644
4851
|
if (!entry) {
|
|
4645
4852
|
throw new Error(`Unknown builtin doc: ${docId}`);
|
|
4646
4853
|
}
|
|
4647
|
-
const content = await
|
|
4854
|
+
const content = await fs14.readFile(entry.absolutePath, "utf-8");
|
|
4648
4855
|
const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
4649
4856
|
return {
|
|
4650
4857
|
entry,
|
|
@@ -4673,6 +4880,25 @@ function listLabels(actionOptions) {
|
|
|
4673
4880
|
if (actionOptions.length === 0) return "-";
|
|
4674
4881
|
return actionOptions.map((o) => o.label).join(", ");
|
|
4675
4882
|
}
|
|
4883
|
+
function resolveFeatureRefForApproval(state, featureName) {
|
|
4884
|
+
const raw = featureName?.trim() || state.matchedFeature?.folderName || "<slug|F001|F001-slug>";
|
|
4885
|
+
return raw;
|
|
4886
|
+
}
|
|
4887
|
+
function buildApprovalCommand(state, featureName, selectedComponent, execute) {
|
|
4888
|
+
const featureRef = resolveFeatureRefForApproval(state, featureName);
|
|
4889
|
+
const componentArg = selectedComponent ? ` --component ${selectedComponent}` : "";
|
|
4890
|
+
const executeArg = execute ? " --execute" : "";
|
|
4891
|
+
return `npx lee-spec-kit context ${featureRef}${componentArg} --approve <LABEL>${executeArg}`;
|
|
4892
|
+
}
|
|
4893
|
+
function buildFinalApprovalPrompt(lang, actionOptions) {
|
|
4894
|
+
if (actionOptions.length === 0) return "";
|
|
4895
|
+
const labels = listLabels(actionOptions);
|
|
4896
|
+
const example = actionOptions[0]?.label || "A";
|
|
4897
|
+
return tr(lang, "cli", "context.finalLabelPrompt", {
|
|
4898
|
+
labels,
|
|
4899
|
+
example
|
|
4900
|
+
});
|
|
4901
|
+
}
|
|
4676
4902
|
function formatActionSummary2(action) {
|
|
4677
4903
|
if (action.type === "command") {
|
|
4678
4904
|
return `(${action.scope}) ${action.cmd}`;
|
|
@@ -4710,7 +4936,7 @@ function getCommandExecutionLockPath(action, config) {
|
|
|
4710
4936
|
if (action.scope === "docs") {
|
|
4711
4937
|
return getDocsLockPath(config.docsDir);
|
|
4712
4938
|
}
|
|
4713
|
-
return
|
|
4939
|
+
return path18.join(action.cwd, ".lee-spec-kit.project.lock");
|
|
4714
4940
|
}
|
|
4715
4941
|
function contextCommand(program2) {
|
|
4716
4942
|
program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Component name for multi projects").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("--approve <reply>", "Approve one labeled option: A or A OK").option("--execute", "Execute approved option when it is a command").option(
|
|
@@ -4848,6 +5074,19 @@ async function runContext(featureName, options) {
|
|
|
4848
5074
|
}
|
|
4849
5075
|
if (options.json) {
|
|
4850
5076
|
const primaryAction = state.actionOptions[0] ?? null;
|
|
5077
|
+
const finalApprovalPrompt = buildFinalApprovalPrompt(lang, state.actionOptions);
|
|
5078
|
+
const approveCommand = buildApprovalCommand(
|
|
5079
|
+
state,
|
|
5080
|
+
featureName,
|
|
5081
|
+
selectedComponent,
|
|
5082
|
+
false
|
|
5083
|
+
);
|
|
5084
|
+
const executeCommand = buildApprovalCommand(
|
|
5085
|
+
state,
|
|
5086
|
+
featureName,
|
|
5087
|
+
selectedComponent,
|
|
5088
|
+
true
|
|
5089
|
+
);
|
|
4851
5090
|
const result = {
|
|
4852
5091
|
status: state.status,
|
|
4853
5092
|
reasonCode: toReasonCode(state.status),
|
|
@@ -4888,6 +5127,10 @@ async function runContext(featureName, options) {
|
|
|
4888
5127
|
},
|
|
4889
5128
|
approvalRequest: {
|
|
4890
5129
|
guidance: "Present each label with summary (e.g. `A: <summary>`) before asking for approval.",
|
|
5130
|
+
finalPrompt: finalApprovalPrompt,
|
|
5131
|
+
labels: state.actionOptions.map((o) => o.label),
|
|
5132
|
+
approveCommand,
|
|
5133
|
+
executeCommand,
|
|
4891
5134
|
options: state.actionOptions.map((o) => ({
|
|
4892
5135
|
label: o.label,
|
|
4893
5136
|
summary: o.summary,
|
|
@@ -5087,7 +5330,7 @@ async function runContext(featureName, options) {
|
|
|
5087
5330
|
if (f.issueNumber) {
|
|
5088
5331
|
console.log(` \u2022 Issue: #${f.issueNumber}`);
|
|
5089
5332
|
}
|
|
5090
|
-
console.log(` \u2022 Path: ${
|
|
5333
|
+
console.log(` \u2022 Path: ${path18.relative(cwd, f.path)}`);
|
|
5091
5334
|
if (f.git.projectBranch) {
|
|
5092
5335
|
console.log(` \u2022 Project Branch: ${f.git.projectBranch}`);
|
|
5093
5336
|
}
|
|
@@ -5151,6 +5394,23 @@ async function runContext(featureName, options) {
|
|
|
5151
5394
|
);
|
|
5152
5395
|
}
|
|
5153
5396
|
}
|
|
5397
|
+
if (actionOptions.length > 0) {
|
|
5398
|
+
const finalApprovalPrompt = buildFinalApprovalPrompt(lang, actionOptions);
|
|
5399
|
+
const executeCommand = buildApprovalCommand(
|
|
5400
|
+
state,
|
|
5401
|
+
featureName,
|
|
5402
|
+
selectedComponent,
|
|
5403
|
+
true
|
|
5404
|
+
);
|
|
5405
|
+
console.log(chalk6.cyan(` \u21B3 ${finalApprovalPrompt}`));
|
|
5406
|
+
console.log(
|
|
5407
|
+
chalk6.gray(
|
|
5408
|
+
` \u21B3 ${tr(lang, "cli", "context.finalLabelCommandHint", {
|
|
5409
|
+
command: executeCommand
|
|
5410
|
+
})}`
|
|
5411
|
+
)
|
|
5412
|
+
);
|
|
5413
|
+
}
|
|
5154
5414
|
console.log();
|
|
5155
5415
|
}
|
|
5156
5416
|
async function runApprovedOption(state, config, lang, featureName, selectionOptions, options) {
|
|
@@ -5324,7 +5584,7 @@ var FIXABLE_ISSUE_CODES = /* @__PURE__ */ new Set([
|
|
|
5324
5584
|
]);
|
|
5325
5585
|
function formatPath(cwd, p) {
|
|
5326
5586
|
if (!p) return "";
|
|
5327
|
-
return
|
|
5587
|
+
return path18.isAbsolute(p) ? path18.relative(cwd, p) : p;
|
|
5328
5588
|
}
|
|
5329
5589
|
function detectPlaceholders(content) {
|
|
5330
5590
|
const patterns = [
|
|
@@ -5467,7 +5727,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
5467
5727
|
const placeholderContext = {
|
|
5468
5728
|
projectName: config.projectName,
|
|
5469
5729
|
featureName: f.slug,
|
|
5470
|
-
featurePath: f.docs.featurePathFromDocs ||
|
|
5730
|
+
featurePath: f.docs.featurePathFromDocs || path18.relative(config.docsDir, f.path),
|
|
5471
5731
|
repoType: f.type,
|
|
5472
5732
|
featureNumber
|
|
5473
5733
|
};
|
|
@@ -5477,9 +5737,9 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
5477
5737
|
"tasks.md"
|
|
5478
5738
|
];
|
|
5479
5739
|
for (const file of files) {
|
|
5480
|
-
const fullPath =
|
|
5481
|
-
if (!await
|
|
5482
|
-
const original = await
|
|
5740
|
+
const fullPath = path18.join(f.path, file);
|
|
5741
|
+
if (!await fs14.pathExists(fullPath)) continue;
|
|
5742
|
+
const original = await fs14.readFile(fullPath, "utf-8");
|
|
5483
5743
|
let next = original;
|
|
5484
5744
|
const changes = [];
|
|
5485
5745
|
const placeholderFix = applyPlaceholderFixes(next, placeholderContext, config.lang);
|
|
@@ -5503,7 +5763,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
5503
5763
|
}
|
|
5504
5764
|
if (next === original) continue;
|
|
5505
5765
|
if (!dryRun) {
|
|
5506
|
-
await
|
|
5766
|
+
await fs14.writeFile(fullPath, next, "utf-8");
|
|
5507
5767
|
}
|
|
5508
5768
|
entries.push({
|
|
5509
5769
|
path: formatPath(cwd, fullPath),
|
|
@@ -5522,8 +5782,8 @@ async function checkDocsStructure(config, cwd) {
|
|
|
5522
5782
|
const issues = [];
|
|
5523
5783
|
const requiredDirs = ["agents", "features", "prd", "designs", "ideas"];
|
|
5524
5784
|
for (const dir of requiredDirs) {
|
|
5525
|
-
const p =
|
|
5526
|
-
if (!await
|
|
5785
|
+
const p = path18.join(config.docsDir, dir);
|
|
5786
|
+
if (!await fs14.pathExists(p)) {
|
|
5527
5787
|
issues.push({
|
|
5528
5788
|
level: "error",
|
|
5529
5789
|
code: "missing_dir",
|
|
@@ -5532,8 +5792,8 @@ async function checkDocsStructure(config, cwd) {
|
|
|
5532
5792
|
});
|
|
5533
5793
|
}
|
|
5534
5794
|
}
|
|
5535
|
-
const configPath =
|
|
5536
|
-
if (!await
|
|
5795
|
+
const configPath = path18.join(config.docsDir, ".lee-spec-kit.json");
|
|
5796
|
+
if (!await fs14.pathExists(configPath)) {
|
|
5537
5797
|
issues.push({
|
|
5538
5798
|
level: "warn",
|
|
5539
5799
|
code: "missing_config",
|
|
@@ -5555,7 +5815,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5555
5815
|
}
|
|
5556
5816
|
const idMap = /* @__PURE__ */ new Map();
|
|
5557
5817
|
for (const f of features) {
|
|
5558
|
-
const rel = f.docs.featurePathFromDocs ||
|
|
5818
|
+
const rel = f.docs.featurePathFromDocs || path18.relative(config.docsDir, f.path);
|
|
5559
5819
|
const id = f.id || "UNKNOWN";
|
|
5560
5820
|
if (!idMap.has(id)) idMap.set(id, []);
|
|
5561
5821
|
idMap.get(id).push(rel);
|
|
@@ -5563,9 +5823,9 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5563
5823
|
if (!isInitialTemplateState) {
|
|
5564
5824
|
const featureDocs = ["spec.md", "plan.md", "tasks.md"];
|
|
5565
5825
|
for (const file of featureDocs) {
|
|
5566
|
-
const p =
|
|
5567
|
-
if (!await
|
|
5568
|
-
const content = await
|
|
5826
|
+
const p = path18.join(f.path, file);
|
|
5827
|
+
if (!await fs14.pathExists(p)) continue;
|
|
5828
|
+
const content = await fs14.readFile(p, "utf-8");
|
|
5569
5829
|
const placeholders = detectPlaceholders(content);
|
|
5570
5830
|
if (placeholders.length === 0) continue;
|
|
5571
5831
|
issues.push({
|
|
@@ -5578,9 +5838,9 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5578
5838
|
});
|
|
5579
5839
|
}
|
|
5580
5840
|
if (decisionsPlaceholderMode !== "off") {
|
|
5581
|
-
const decisionsPath =
|
|
5582
|
-
if (await
|
|
5583
|
-
const content = await
|
|
5841
|
+
const decisionsPath = path18.join(f.path, "decisions.md");
|
|
5842
|
+
if (await fs14.pathExists(decisionsPath)) {
|
|
5843
|
+
const content = await fs14.readFile(decisionsPath, "utf-8");
|
|
5584
5844
|
const placeholders = detectPlaceholders(content);
|
|
5585
5845
|
if (placeholders.length > 0) {
|
|
5586
5846
|
issues.push({
|
|
@@ -5607,7 +5867,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5607
5867
|
level: "warn",
|
|
5608
5868
|
code: "spec_status_unset",
|
|
5609
5869
|
message: tr(config.lang, "cli", "doctor.issue.specStatusUnset"),
|
|
5610
|
-
path: formatPath(cwd,
|
|
5870
|
+
path: formatPath(cwd, path18.join(f.path, "spec.md"))
|
|
5611
5871
|
});
|
|
5612
5872
|
}
|
|
5613
5873
|
if (f.docs.planExists && !f.planStatus && !isInitialTemplateState) {
|
|
@@ -5615,7 +5875,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5615
5875
|
level: "warn",
|
|
5616
5876
|
code: "plan_status_unset",
|
|
5617
5877
|
message: tr(config.lang, "cli", "doctor.issue.planStatusUnset"),
|
|
5618
|
-
path: formatPath(cwd,
|
|
5878
|
+
path: formatPath(cwd, path18.join(f.path, "plan.md"))
|
|
5619
5879
|
});
|
|
5620
5880
|
}
|
|
5621
5881
|
if (f.docs.tasksExists && f.tasks.total === 0 && !isInitialTemplateState) {
|
|
@@ -5623,7 +5883,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5623
5883
|
level: "warn",
|
|
5624
5884
|
code: "tasks_empty",
|
|
5625
5885
|
message: tr(config.lang, "cli", "doctor.issue.tasksEmpty"),
|
|
5626
|
-
path: formatPath(cwd,
|
|
5886
|
+
path: formatPath(cwd, path18.join(f.path, "tasks.md"))
|
|
5627
5887
|
});
|
|
5628
5888
|
}
|
|
5629
5889
|
if (f.docs.tasksExists && !f.docs.tasksDocStatusFieldExists && !isInitialTemplateState) {
|
|
@@ -5631,7 +5891,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5631
5891
|
level: "warn",
|
|
5632
5892
|
code: "tasks_doc_status_missing",
|
|
5633
5893
|
message: tr(config.lang, "cli", "doctor.issue.tasksDocStatusMissing"),
|
|
5634
|
-
path: formatPath(cwd,
|
|
5894
|
+
path: formatPath(cwd, path18.join(f.path, "tasks.md"))
|
|
5635
5895
|
});
|
|
5636
5896
|
}
|
|
5637
5897
|
if (f.docs.tasksExists && f.docs.tasksDocStatusFieldExists && !f.tasksDocStatus && !isInitialTemplateState) {
|
|
@@ -5639,7 +5899,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5639
5899
|
level: "warn",
|
|
5640
5900
|
code: "tasks_doc_status_unset",
|
|
5641
5901
|
message: tr(config.lang, "cli", "doctor.issue.tasksDocStatusUnset"),
|
|
5642
|
-
path: formatPath(cwd,
|
|
5902
|
+
path: formatPath(cwd, path18.join(f.path, "tasks.md"))
|
|
5643
5903
|
});
|
|
5644
5904
|
}
|
|
5645
5905
|
}
|
|
@@ -5663,7 +5923,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5663
5923
|
level: "warn",
|
|
5664
5924
|
code: "missing_feature_id",
|
|
5665
5925
|
message: tr(config.lang, "cli", "doctor.issue.missingFeatureId"),
|
|
5666
|
-
path: formatPath(cwd,
|
|
5926
|
+
path: formatPath(cwd, path18.join(config.docsDir, p))
|
|
5667
5927
|
});
|
|
5668
5928
|
}
|
|
5669
5929
|
return issues;
|
|
@@ -5785,7 +6045,7 @@ function doctorCommand(program2) {
|
|
|
5785
6045
|
}
|
|
5786
6046
|
console.log();
|
|
5787
6047
|
console.log(chalk6.bold(tr(lang, "cli", "doctor.title")));
|
|
5788
|
-
console.log(chalk6.gray(`- Docs: ${
|
|
6048
|
+
console.log(chalk6.gray(`- Docs: ${path18.relative(cwd, docsDir)}`));
|
|
5789
6049
|
console.log(chalk6.gray(`- Type: ${projectType}`));
|
|
5790
6050
|
console.log(chalk6.gray(`- Lang: ${lang}`));
|
|
5791
6051
|
console.log();
|
|
@@ -5964,7 +6224,7 @@ async function runView(featureName, options) {
|
|
|
5964
6224
|
}
|
|
5965
6225
|
console.log();
|
|
5966
6226
|
console.log(chalk6.bold("\u{1F4CA} Workflow View"));
|
|
5967
|
-
console.log(chalk6.gray(`- Docs: ${
|
|
6227
|
+
console.log(chalk6.gray(`- Docs: ${path18.relative(cwd, config.docsDir)}`));
|
|
5968
6228
|
console.log(
|
|
5969
6229
|
chalk6.gray(
|
|
5970
6230
|
`- Features: ${state.features.length} (open ${state.openFeatures.length} / done ${state.doneFeatures.length})`
|
|
@@ -6245,45 +6505,109 @@ async function runFlow(featureName, options) {
|
|
|
6245
6505
|
console.log(chalk6.gray("Tip: add --approve <LABEL> [--execute] to run the selected atomic action."));
|
|
6246
6506
|
console.log();
|
|
6247
6507
|
}
|
|
6508
|
+
|
|
6509
|
+
// src/utils/github-draft-contract.ts
|
|
6510
|
+
var ISSUE_CONTRACT = {
|
|
6511
|
+
kind: "issue",
|
|
6512
|
+
requiredSections: {
|
|
6513
|
+
ko: ["\uAC1C\uC694", "\uBAA9\uD45C", "\uC644\uB8CC \uAE30\uC900", "\uAD00\uB828 \uBB38\uC11C", "\uB77C\uBCA8"],
|
|
6514
|
+
en: ["Overview", "Goals", "Completion Criteria", "Related Documents", "Labels"]
|
|
6515
|
+
},
|
|
6516
|
+
artifacts: []
|
|
6517
|
+
};
|
|
6518
|
+
var PR_CONTRACT = {
|
|
6519
|
+
kind: "pr",
|
|
6520
|
+
requiredSections: {
|
|
6521
|
+
ko: ["\uAC1C\uC694", "\uBCC0\uACBD \uC0AC\uD56D", "\uD14C\uC2A4\uD2B8", "\uAD00\uB828 \uBB38\uC11C"],
|
|
6522
|
+
en: ["Overview", "Changes", "Tests", "Related Documents"]
|
|
6523
|
+
},
|
|
6524
|
+
artifacts: [
|
|
6525
|
+
{
|
|
6526
|
+
id: "screenshots",
|
|
6527
|
+
headings: { ko: "\uC2A4\uD06C\uB9B0\uC0F7", en: "Screenshots" },
|
|
6528
|
+
bodyRule: "image-markdown"
|
|
6529
|
+
},
|
|
6530
|
+
{
|
|
6531
|
+
id: "mermaid",
|
|
6532
|
+
headings: { ko: "\uC544\uD0A4\uD14D\uCC98 \uB2E4\uC774\uC5B4\uADF8\uB7A8", en: "Architecture Diagram" },
|
|
6533
|
+
bodyRule: "mermaid-fence"
|
|
6534
|
+
}
|
|
6535
|
+
]
|
|
6536
|
+
};
|
|
6537
|
+
var CONTRACTS = {
|
|
6538
|
+
issue: ISSUE_CONTRACT,
|
|
6539
|
+
pr: PR_CONTRACT
|
|
6540
|
+
};
|
|
6541
|
+
function getGithubDraftRequiredSections(kind, lang) {
|
|
6542
|
+
return [...CONTRACTS[kind].requiredSections[lang]];
|
|
6543
|
+
}
|
|
6544
|
+
function getGithubDraftArtifactHeading(kind, artifactId, lang) {
|
|
6545
|
+
const artifact = CONTRACTS[kind].artifacts.find((item) => item.id === artifactId);
|
|
6546
|
+
if (!artifact) return null;
|
|
6547
|
+
return artifact.headings[lang];
|
|
6548
|
+
}
|
|
6549
|
+
function getGithubDraftContractView(kind, lang) {
|
|
6550
|
+
const definition = CONTRACTS[kind];
|
|
6551
|
+
return {
|
|
6552
|
+
kind: definition.kind,
|
|
6553
|
+
requiredSections: [...definition.requiredSections[lang]],
|
|
6554
|
+
artifacts: definition.artifacts.map((artifact) => ({
|
|
6555
|
+
id: artifact.id,
|
|
6556
|
+
section: artifact.headings[lang],
|
|
6557
|
+
bodyRule: artifact.bodyRule
|
|
6558
|
+
}))
|
|
6559
|
+
};
|
|
6560
|
+
}
|
|
6561
|
+
function getGithubDraftContractForBuiltinDoc(docId, lang) {
|
|
6562
|
+
if (docId === "create-issue" || docId === "issue-template") {
|
|
6563
|
+
return getGithubDraftContractView("issue", lang);
|
|
6564
|
+
}
|
|
6565
|
+
if (docId === "create-pr" || docId === "pr-template") {
|
|
6566
|
+
return getGithubDraftContractView("pr", lang);
|
|
6567
|
+
}
|
|
6568
|
+
return null;
|
|
6569
|
+
}
|
|
6570
|
+
|
|
6571
|
+
// src/commands/github.ts
|
|
6248
6572
|
function tg(lang, key, vars = {}) {
|
|
6249
6573
|
return tr(lang, "cli", `github.${key}`, vars);
|
|
6250
6574
|
}
|
|
6251
6575
|
function detectGithubCliLangSync(cwd) {
|
|
6252
6576
|
const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
|
|
6253
|
-
const startDirs = [explicitDocsDir ?
|
|
6577
|
+
const startDirs = [explicitDocsDir ? path18.resolve(explicitDocsDir) : "", path18.resolve(cwd)].filter(Boolean);
|
|
6254
6578
|
const scanOrder = [];
|
|
6255
6579
|
const seen = /* @__PURE__ */ new Set();
|
|
6256
6580
|
for (const start of startDirs) {
|
|
6257
6581
|
let current = start;
|
|
6258
6582
|
while (true) {
|
|
6259
|
-
const abs =
|
|
6583
|
+
const abs = path18.resolve(current);
|
|
6260
6584
|
if (!seen.has(abs)) {
|
|
6261
6585
|
scanOrder.push(abs);
|
|
6262
6586
|
seen.add(abs);
|
|
6263
6587
|
}
|
|
6264
|
-
const parent =
|
|
6588
|
+
const parent = path18.dirname(abs);
|
|
6265
6589
|
if (parent === abs) break;
|
|
6266
6590
|
current = parent;
|
|
6267
6591
|
}
|
|
6268
6592
|
}
|
|
6269
6593
|
for (const base of scanOrder) {
|
|
6270
|
-
for (const docsDir of [
|
|
6271
|
-
const configPath =
|
|
6272
|
-
if (
|
|
6594
|
+
for (const docsDir of [path18.join(base, "docs"), base]) {
|
|
6595
|
+
const configPath = path18.join(docsDir, ".lee-spec-kit.json");
|
|
6596
|
+
if (fs14.existsSync(configPath)) {
|
|
6273
6597
|
try {
|
|
6274
|
-
const parsed =
|
|
6598
|
+
const parsed = fs14.readJsonSync(configPath);
|
|
6275
6599
|
if (parsed?.lang === "ko" || parsed?.lang === "en") return parsed.lang;
|
|
6276
6600
|
} catch {
|
|
6277
6601
|
}
|
|
6278
6602
|
}
|
|
6279
|
-
const agentsPath =
|
|
6280
|
-
const featuresPath =
|
|
6281
|
-
if (!
|
|
6603
|
+
const agentsPath = path18.join(docsDir, "agents");
|
|
6604
|
+
const featuresPath = path18.join(docsDir, "features");
|
|
6605
|
+
if (!fs14.existsSync(agentsPath) || !fs14.existsSync(featuresPath)) continue;
|
|
6282
6606
|
for (const probe of ["custom.md", "constitution.md", "agents.md"]) {
|
|
6283
|
-
const file =
|
|
6284
|
-
if (!
|
|
6607
|
+
const file = path18.join(agentsPath, probe);
|
|
6608
|
+
if (!fs14.existsSync(file)) continue;
|
|
6285
6609
|
try {
|
|
6286
|
-
const content =
|
|
6610
|
+
const content = fs14.readFileSync(file, "utf-8");
|
|
6287
6611
|
if (/[가-힣]/.test(content)) return "ko";
|
|
6288
6612
|
} catch {
|
|
6289
6613
|
}
|
|
@@ -6384,7 +6708,7 @@ function ensureSections(body, sections, kind, lang) {
|
|
|
6384
6708
|
}
|
|
6385
6709
|
function ensureDocsExist(docsDir, relativePaths, lang) {
|
|
6386
6710
|
const missing = relativePaths.filter(
|
|
6387
|
-
(relativePath) => !
|
|
6711
|
+
(relativePath) => !fs14.existsSync(path18.join(docsDir, relativePath))
|
|
6388
6712
|
);
|
|
6389
6713
|
if (missing.length > 0) {
|
|
6390
6714
|
throw createCliError(
|
|
@@ -6394,13 +6718,62 @@ function ensureDocsExist(docsDir, relativePaths, lang) {
|
|
|
6394
6718
|
}
|
|
6395
6719
|
}
|
|
6396
6720
|
function buildDefaultBodyFileName(kind, docsDir, component) {
|
|
6397
|
-
const key = `${
|
|
6721
|
+
const key = `${path18.resolve(docsDir)}::${component.trim().toLowerCase()}`;
|
|
6398
6722
|
const digest = createHash("sha1").update(key).digest("hex").slice(0, 12);
|
|
6399
6723
|
return `lee-spec-kit.${digest}.${kind}.md`;
|
|
6400
6724
|
}
|
|
6401
6725
|
function toBodyFilePath(raw, kind, docsDir, component) {
|
|
6402
|
-
const selected = raw?.trim() ||
|
|
6403
|
-
return
|
|
6726
|
+
const selected = raw?.trim() || path18.join(os.tmpdir(), buildDefaultBodyFileName(kind, docsDir, component));
|
|
6727
|
+
return path18.resolve(selected);
|
|
6728
|
+
}
|
|
6729
|
+
function toProjectRootDocsPath(relativePathFromDocs) {
|
|
6730
|
+
const normalized = relativePathFromDocs.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
6731
|
+
if (normalized.startsWith("docs/")) return normalized;
|
|
6732
|
+
return `docs/${normalized}`;
|
|
6733
|
+
}
|
|
6734
|
+
function toBodyDocPaths(paths) {
|
|
6735
|
+
return {
|
|
6736
|
+
...paths,
|
|
6737
|
+
specPath: toProjectRootDocsPath(paths.specPath),
|
|
6738
|
+
planPath: toProjectRootDocsPath(paths.planPath),
|
|
6739
|
+
tasksPath: toProjectRootDocsPath(paths.tasksPath)
|
|
6740
|
+
};
|
|
6741
|
+
}
|
|
6742
|
+
var TODO_PLACEHOLDER_PATTERN = /(^|\n)\s*-\s*\[[ xX]\]\s*TODO:/m;
|
|
6743
|
+
function ensureNoTodoPlaceholders(body, kind, lang) {
|
|
6744
|
+
if (!TODO_PLACEHOLDER_PATTERN.test(body)) return;
|
|
6745
|
+
throw createCliError(
|
|
6746
|
+
"PRECONDITION_FAILED",
|
|
6747
|
+
tg(lang, "todoPlaceholdersRemain", { kind })
|
|
6748
|
+
);
|
|
6749
|
+
}
|
|
6750
|
+
function parsePrArtifactMode(raw, kind, lang) {
|
|
6751
|
+
const value = (raw || "auto").trim().toLowerCase();
|
|
6752
|
+
if (value === "auto" || value === "on" || value === "off") {
|
|
6753
|
+
return value;
|
|
6754
|
+
}
|
|
6755
|
+
throw createCliError(
|
|
6756
|
+
"INVALID_ARGUMENT",
|
|
6757
|
+
tg(lang, "artifactModeInvalid", { kind, value })
|
|
6758
|
+
);
|
|
6759
|
+
}
|
|
6760
|
+
function resolvePrArtifactPolicy(config, feature, options) {
|
|
6761
|
+
const screenshotsMode = parsePrArtifactMode(
|
|
6762
|
+
options.screenshots,
|
|
6763
|
+
"screenshots",
|
|
6764
|
+
config.lang
|
|
6765
|
+
);
|
|
6766
|
+
const mermaidMode = parsePrArtifactMode(
|
|
6767
|
+
options.mermaid,
|
|
6768
|
+
"mermaid",
|
|
6769
|
+
config.lang
|
|
6770
|
+
);
|
|
6771
|
+
const includeScreenshots = screenshotsMode === "on" ? true : screenshotsMode === "off" ? false : config.pr?.screenshots?.upload ?? false;
|
|
6772
|
+
const includeMermaid = mermaidMode === "on" ? true : mermaidMode === "off" ? false : feature.type === "be";
|
|
6773
|
+
return {
|
|
6774
|
+
includeScreenshots,
|
|
6775
|
+
includeMermaid
|
|
6776
|
+
};
|
|
6404
6777
|
}
|
|
6405
6778
|
async function resolveFeatureOrThrow(featureName, options, lang) {
|
|
6406
6779
|
const config = await getConfig(process.cwd());
|
|
@@ -6440,27 +6813,38 @@ function getFeatureDocPaths(feature) {
|
|
|
6440
6813
|
function normalizeHeading(value) {
|
|
6441
6814
|
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
6442
6815
|
}
|
|
6443
|
-
function
|
|
6816
|
+
function extractMarkdownByHeadings(content, headings, levels) {
|
|
6444
6817
|
const targets = new Set(headings.map((heading) => normalizeHeading(heading)));
|
|
6445
6818
|
const lines = content.split("\n");
|
|
6446
6819
|
let start = -1;
|
|
6820
|
+
let startLevel = 0;
|
|
6821
|
+
const levelSet = new Set(levels);
|
|
6447
6822
|
for (let i = 0; i < lines.length; i++) {
|
|
6448
|
-
const match = lines[i].match(/^\s
|
|
6823
|
+
const match = lines[i].match(/^\s*(#{2,6})\s+(.+?)\s*$/);
|
|
6449
6824
|
if (!match) continue;
|
|
6450
|
-
|
|
6825
|
+
const level = match[1].length;
|
|
6826
|
+
if (!levelSet.has(level)) continue;
|
|
6827
|
+
if (!targets.has(normalizeHeading(match[2]))) continue;
|
|
6451
6828
|
start = i + 1;
|
|
6829
|
+
startLevel = level;
|
|
6452
6830
|
break;
|
|
6453
6831
|
}
|
|
6454
6832
|
if (start < 0) return void 0;
|
|
6455
6833
|
let end = lines.length;
|
|
6456
6834
|
for (let i = start; i < lines.length; i++) {
|
|
6457
|
-
|
|
6835
|
+
const heading = lines[i].match(/^\s*(#{2,6})\s+(.+?)\s*$/);
|
|
6836
|
+
if (!heading) continue;
|
|
6837
|
+
const level = heading[1].length;
|
|
6838
|
+
if (level <= startLevel) {
|
|
6458
6839
|
end = i;
|
|
6459
6840
|
break;
|
|
6460
6841
|
}
|
|
6461
6842
|
}
|
|
6462
6843
|
return lines.slice(start, end).join("\n");
|
|
6463
6844
|
}
|
|
6845
|
+
function extractMarkdownSection(content, headings) {
|
|
6846
|
+
return extractMarkdownByHeadings(content, headings, [2]);
|
|
6847
|
+
}
|
|
6464
6848
|
function isTemplateLine(line) {
|
|
6465
6849
|
const trimmed = line.trim();
|
|
6466
6850
|
if (!trimmed) return true;
|
|
@@ -6476,6 +6860,337 @@ function sanitizeOverviewSection(raw) {
|
|
|
6476
6860
|
if (lines.length === 0) return void 0;
|
|
6477
6861
|
return lines.slice(0, 5).join("\n");
|
|
6478
6862
|
}
|
|
6863
|
+
function sanitizeDraftItem(raw) {
|
|
6864
|
+
const trimmed = raw.replace(/^\s*-\s*\[[ xX]\]\s*/, "").replace(/^\s*-\s+/, "").replace(/^\s*###\s+/, "").trim();
|
|
6865
|
+
const plain = trimmed.replace(/\*\*/g, "").trim();
|
|
6866
|
+
if (!plain) return void 0;
|
|
6867
|
+
if (isTemplateLine(plain)) return void 0;
|
|
6868
|
+
if (/^todo:/i.test(plain)) return void 0;
|
|
6869
|
+
if (/\{[^}]*\}/.test(plain)) return void 0;
|
|
6870
|
+
if (/^(as a|i want|so that)\b/i.test(plain)) return void 0;
|
|
6871
|
+
if (/^acceptance criteria:?$/i.test(plain)) return void 0;
|
|
6872
|
+
return plain.replace(/\s+/g, " ");
|
|
6873
|
+
}
|
|
6874
|
+
function uniqItems(items) {
|
|
6875
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6876
|
+
const ordered = [];
|
|
6877
|
+
for (const item of items) {
|
|
6878
|
+
const normalized = item.trim().toLowerCase();
|
|
6879
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
6880
|
+
seen.add(normalized);
|
|
6881
|
+
ordered.push(item.trim());
|
|
6882
|
+
}
|
|
6883
|
+
return ordered;
|
|
6884
|
+
}
|
|
6885
|
+
function normalizeSemanticKey(value) {
|
|
6886
|
+
return value.toLowerCase().replace(/[`'"(){}\[\].,:;!?/\\\-_|]/g, " ").replace(/\s+/g, " ").trim().replace(/\s/g, "");
|
|
6887
|
+
}
|
|
6888
|
+
function uniqItemsByContainment(items) {
|
|
6889
|
+
const kept = [];
|
|
6890
|
+
const keys = [];
|
|
6891
|
+
for (const item of items) {
|
|
6892
|
+
const clean = item.trim();
|
|
6893
|
+
if (!clean) continue;
|
|
6894
|
+
const key = normalizeSemanticKey(clean);
|
|
6895
|
+
if (!key) continue;
|
|
6896
|
+
let replaced = false;
|
|
6897
|
+
for (let i = 0; i < keys.length; i++) {
|
|
6898
|
+
const current = keys[i];
|
|
6899
|
+
if (!current.includes(key) && !key.includes(current)) continue;
|
|
6900
|
+
if (key.length > current.length) {
|
|
6901
|
+
keys[i] = key;
|
|
6902
|
+
kept[i] = clean;
|
|
6903
|
+
}
|
|
6904
|
+
replaced = true;
|
|
6905
|
+
break;
|
|
6906
|
+
}
|
|
6907
|
+
if (!replaced) {
|
|
6908
|
+
keys.push(key);
|
|
6909
|
+
kept.push(clean);
|
|
6910
|
+
}
|
|
6911
|
+
}
|
|
6912
|
+
return kept;
|
|
6913
|
+
}
|
|
6914
|
+
function extractSectionLines(raw) {
|
|
6915
|
+
if (!raw) return [];
|
|
6916
|
+
return uniqItems(
|
|
6917
|
+
raw.split("\n").map((line) => sanitizeDraftItem(line)).filter((line) => !!line)
|
|
6918
|
+
);
|
|
6919
|
+
}
|
|
6920
|
+
function extractSectionHeadings(raw) {
|
|
6921
|
+
if (!raw) return [];
|
|
6922
|
+
return uniqItems(
|
|
6923
|
+
raw.split("\n").map((line) => line.match(/^\s*###\s+(.+?)\s*$/)?.[1] || "").map((line) => sanitizeDraftItem(line)).filter((line) => !!line).map((line) => line.replace(/^FR-\d+:\s*/i, "").trim())
|
|
6924
|
+
);
|
|
6925
|
+
}
|
|
6926
|
+
function toChecklistLines(items) {
|
|
6927
|
+
return items.map((item) => `- [ ] ${item}`).join("\n");
|
|
6928
|
+
}
|
|
6929
|
+
function extractChecklistItems(raw) {
|
|
6930
|
+
if (!raw) return [];
|
|
6931
|
+
return uniqItems(
|
|
6932
|
+
raw.split("\n").map((line) => {
|
|
6933
|
+
const match = line.match(/^\s*-\s*\[[ xX]\]\s+(.+?)\s*$/);
|
|
6934
|
+
if (!match) return void 0;
|
|
6935
|
+
return sanitizeDraftItem(match[1]);
|
|
6936
|
+
}).filter((line) => !!line)
|
|
6937
|
+
);
|
|
6938
|
+
}
|
|
6939
|
+
function extractTaskTitles(tasksContent) {
|
|
6940
|
+
return uniqItems(
|
|
6941
|
+
tasksContent.split("\n").map((line) => {
|
|
6942
|
+
const match = line.match(
|
|
6943
|
+
/^\s*-\s*\[(?:TODO|DOING|DONE)\][^\n]*?\s+(?:T-[A-Z0-9-]+\s+)?(.+?)\s*$/
|
|
6944
|
+
);
|
|
6945
|
+
if (!match) return void 0;
|
|
6946
|
+
return sanitizeDraftItem(match[1]);
|
|
6947
|
+
}).filter((line) => !!line)
|
|
6948
|
+
);
|
|
6949
|
+
}
|
|
6950
|
+
function extractTasksAcceptanceItems(tasksContent) {
|
|
6951
|
+
const lines = tasksContent.split("\n");
|
|
6952
|
+
const accepted = [];
|
|
6953
|
+
let inAcceptance = false;
|
|
6954
|
+
for (const line of lines) {
|
|
6955
|
+
if (/^\s*-\s*Acceptance\s*:\s*$/i.test(line)) {
|
|
6956
|
+
inAcceptance = true;
|
|
6957
|
+
continue;
|
|
6958
|
+
}
|
|
6959
|
+
if (inAcceptance && /^\s*-\s*Checklist\s*:\s*$/i.test(line)) {
|
|
6960
|
+
inAcceptance = false;
|
|
6961
|
+
continue;
|
|
6962
|
+
}
|
|
6963
|
+
if (!inAcceptance) continue;
|
|
6964
|
+
const match = line.match(/^\s*-\s*\[[ xX]\]\s+(.+?)\s*$/) || line.match(/^\s*-\s+(.+?)\s*$/);
|
|
6965
|
+
if (!match) continue;
|
|
6966
|
+
const item = sanitizeDraftItem(match[1]);
|
|
6967
|
+
if (!item) continue;
|
|
6968
|
+
accepted.push(item);
|
|
6969
|
+
}
|
|
6970
|
+
return uniqItems(accepted);
|
|
6971
|
+
}
|
|
6972
|
+
function extractScopeItemsFromPlan(planContent, lang) {
|
|
6973
|
+
const section = extractMarkdownSection(
|
|
6974
|
+
planContent,
|
|
6975
|
+
["\uBC94\uC704(\uBA85\uD655\uD654)", "\uBC94\uC704", "Scope", "Scope Clarification"]
|
|
6976
|
+
);
|
|
6977
|
+
if (!section) return { include: [], exclude: [] };
|
|
6978
|
+
const lines = section.split("\n");
|
|
6979
|
+
const include = [];
|
|
6980
|
+
const exclude = [];
|
|
6981
|
+
let mode = null;
|
|
6982
|
+
const includePatterns = lang === "ko" ? [/포함/] : [/in\s*scope/i, /^include$/i, /included/i];
|
|
6983
|
+
const excludePatterns = lang === "ko" ? [/비포함/, /제외/] : [/out\s*of\s*scope/i, /^exclude$/i, /excluded/i];
|
|
6984
|
+
for (const line of lines) {
|
|
6985
|
+
const plain = line.replace(/\*\*/g, "").trim();
|
|
6986
|
+
if (!plain) continue;
|
|
6987
|
+
if (excludePatterns.some((re) => re.test(plain))) {
|
|
6988
|
+
mode = "exclude";
|
|
6989
|
+
continue;
|
|
6990
|
+
}
|
|
6991
|
+
if (includePatterns.some((re) => re.test(plain))) {
|
|
6992
|
+
mode = "include";
|
|
6993
|
+
continue;
|
|
6994
|
+
}
|
|
6995
|
+
const bullet = line.match(/^\s*-\s+(.+?)\s*$/)?.[1];
|
|
6996
|
+
if (!bullet) continue;
|
|
6997
|
+
const item = sanitizeDraftItem(bullet);
|
|
6998
|
+
if (!item) continue;
|
|
6999
|
+
if (mode === "include") {
|
|
7000
|
+
include.push(item);
|
|
7001
|
+
} else if (mode === "exclude") {
|
|
7002
|
+
exclude.push(item);
|
|
7003
|
+
}
|
|
7004
|
+
}
|
|
7005
|
+
return {
|
|
7006
|
+
include: uniqItems(include),
|
|
7007
|
+
exclude: uniqItems(exclude)
|
|
7008
|
+
};
|
|
7009
|
+
}
|
|
7010
|
+
function getIssueGoalsAndCriteria(specContent, planContent, tasksContent, overview, lang) {
|
|
7011
|
+
const purposeLines = extractSectionLines(
|
|
7012
|
+
extractMarkdownSection(specContent, ["\uBAA9\uC801", "Purpose"])
|
|
7013
|
+
);
|
|
7014
|
+
const requirementHeadings = extractSectionHeadings(
|
|
7015
|
+
extractMarkdownSection(specContent, ["\uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D", "Functional Requirements"])
|
|
7016
|
+
);
|
|
7017
|
+
const userStoryChecklist = extractChecklistItems(
|
|
7018
|
+
extractMarkdownSection(specContent, ["\uC0AC\uC6A9\uC790 \uC2A4\uD1A0\uB9AC", "User Stories"])
|
|
7019
|
+
);
|
|
7020
|
+
const tasksAcceptance = extractTasksAcceptanceItems(tasksContent);
|
|
7021
|
+
const scopeFromPlan = extractScopeItemsFromPlan(planContent, lang);
|
|
7022
|
+
const taskTitles = extractTaskTitles(tasksContent);
|
|
7023
|
+
const goals = uniqItemsByContainment(uniqItems([
|
|
7024
|
+
...requirementHeadings,
|
|
7025
|
+
...scopeFromPlan.include,
|
|
7026
|
+
...purposeLines.slice(0, 1),
|
|
7027
|
+
sanitizeDraftItem(overview) || ""
|
|
7028
|
+
])).slice(0, 5);
|
|
7029
|
+
while (goals.length < 3) {
|
|
7030
|
+
goals.push(
|
|
7031
|
+
lang === "ko" ? goals.length === 0 ? "spec.md \uBAA9\uC801\uC5D0 \uB9DE\uCDB0 \uAD6C\uD604 \uBC94\uC704\uB97C \uD655\uC815\uD55C\uB2E4." : goals.length === 1 ? "\uD3EC\uD568/\uC81C\uC678 \uBC94\uC704\uB97C \uBA85\uD655\uD788 \uC815\uC758\uD558\uACE0 \uBB38\uC11C\uC640 \uAD6C\uD604\uC744 \uC77C\uCE58\uC2DC\uD0A8\uB2E4." : "\uAD00\uB828 \uD14C\uC2A4\uD2B8 \uBC0F \uAC80\uC99D \uACBD\uB85C\uB97C \uD3EC\uD568\uD574 \uAE30\uB2A5 \uC644\uC131\uB3C4\uB97C \uD655\uBCF4\uD55C\uB2E4." : goals.length === 0 ? "Define implementation scope aligned with spec.md purpose." : goals.length === 1 ? "Clarify in-scope and out-of-scope boundaries and keep docs/code aligned." : "Ensure feature completeness with concrete validation paths."
|
|
7032
|
+
);
|
|
7033
|
+
}
|
|
7034
|
+
const criteria = uniqItemsByContainment(
|
|
7035
|
+
uniqItems([...userStoryChecklist, ...tasksAcceptance])
|
|
7036
|
+
).slice(0, 6);
|
|
7037
|
+
while (criteria.length < 4) {
|
|
7038
|
+
criteria.push(
|
|
7039
|
+
lang === "ko" ? criteria.length === 0 ? "\uD575\uC2EC \uC0AC\uC6A9\uC790 \uC2DC\uB098\uB9AC\uC624\uC5D0\uC11C \uBAA9\uD45C \uB3D9\uC791\uC774 \uC7AC\uD604\uB41C\uB2E4." : criteria.length === 1 ? "\uC694\uAD6C\uC0AC\uD56D\uBCC4 \uC218\uC6A9 \uAE30\uC900\uC774 \uBAA8\uB450 \uCDA9\uC871\uB41C\uB2E4." : criteria.length === 2 ? "\uAC80\uC99D \uBC29\uBC95(\uD14C\uC2A4\uD2B8/\uC218\uB3D9 \uC2DC\uB098\uB9AC\uC624)\uC744 \uAE30\uB85D\uD574 \uC644\uB8CC\uB97C \uD655\uC778\uD560 \uC218 \uC788\uB2E4." : "\uD68C\uADC0 \uC5C6\uC774 \uAE30\uC874 \uB3D9\uC791\uACFC\uC758 \uD638\uD658\uC131\uC744 \uC720\uC9C0\uD55C\uB2E4." : criteria.length === 0 ? "Core user scenarios reproduce expected behavior." : criteria.length === 1 ? "All requirement-level acceptance criteria are satisfied." : criteria.length === 2 ? "Validation method (tests/manual scenarios) is documented for completion checks." : "Compatibility is preserved without regressions."
|
|
7040
|
+
);
|
|
7041
|
+
}
|
|
7042
|
+
const scope = uniqItemsByContainment(
|
|
7043
|
+
uniqItems([...scopeFromPlan.include, ...taskTitles])
|
|
7044
|
+
).slice(0, 6);
|
|
7045
|
+
return {
|
|
7046
|
+
goals: goals.slice(0, 5),
|
|
7047
|
+
criteria: criteria.slice(0, 6),
|
|
7048
|
+
scope
|
|
7049
|
+
};
|
|
7050
|
+
}
|
|
7051
|
+
function extractPlanChangeTargets(planContent, lang) {
|
|
7052
|
+
const section = extractMarkdownSection(
|
|
7053
|
+
planContent,
|
|
7054
|
+
[
|
|
7055
|
+
"\uBCC0\uACBD \uB300\uC0C1(\uC608\uC0C1)",
|
|
7056
|
+
"\uBCC0\uACBD \uB300\uC0C1",
|
|
7057
|
+
"Changed Files",
|
|
7058
|
+
"Change Targets",
|
|
7059
|
+
"Expected Changes"
|
|
7060
|
+
]
|
|
7061
|
+
);
|
|
7062
|
+
if (!section) return [];
|
|
7063
|
+
const lines = section.split("\n");
|
|
7064
|
+
const out = [];
|
|
7065
|
+
let inCode = false;
|
|
7066
|
+
for (const line of lines) {
|
|
7067
|
+
if (/^\s*```/.test(line)) {
|
|
7068
|
+
inCode = !inCode;
|
|
7069
|
+
continue;
|
|
7070
|
+
}
|
|
7071
|
+
if (inCode) {
|
|
7072
|
+
const trimmed = line.trim();
|
|
7073
|
+
if (!trimmed) continue;
|
|
7074
|
+
if (!/[\\/]/.test(trimmed)) continue;
|
|
7075
|
+
out.push(
|
|
7076
|
+
lang === "ko" ? `\`${trimmed}\` \uBCC0\uACBD` : `Update \`${trimmed}\``
|
|
7077
|
+
);
|
|
7078
|
+
continue;
|
|
7079
|
+
}
|
|
7080
|
+
const bullet = line.match(/^\s*-\s+(.+?)\s*$/)?.[1];
|
|
7081
|
+
if (!bullet) continue;
|
|
7082
|
+
const item = sanitizeDraftItem(bullet);
|
|
7083
|
+
if (!item) continue;
|
|
7084
|
+
out.push(item);
|
|
7085
|
+
}
|
|
7086
|
+
return uniqItemsByContainment(uniqItems(out));
|
|
7087
|
+
}
|
|
7088
|
+
function extractCommandsFromSection(raw) {
|
|
7089
|
+
if (!raw) return [];
|
|
7090
|
+
const commands = [];
|
|
7091
|
+
for (const match of raw.matchAll(/`([^`]+)`/g)) {
|
|
7092
|
+
const candidate = match[1].trim();
|
|
7093
|
+
if (!candidate) continue;
|
|
7094
|
+
if (/\{[^}]*\}/.test(candidate)) continue;
|
|
7095
|
+
if (!/\b(pnpm|npm|yarn|bun|vitest|jest|tsx?|node)\b/.test(candidate)) continue;
|
|
7096
|
+
commands.push(candidate);
|
|
7097
|
+
}
|
|
7098
|
+
for (const line of raw.split("\n")) {
|
|
7099
|
+
const trimmed = line.trim().replace(/^-+\s*/, "");
|
|
7100
|
+
if (!trimmed) continue;
|
|
7101
|
+
if (!/\b(pnpm|npm|yarn|bun|vitest|jest|tsx?|node)\b/.test(trimmed)) continue;
|
|
7102
|
+
if (/\{[^}]*\}/.test(trimmed)) continue;
|
|
7103
|
+
commands.push(trimmed);
|
|
7104
|
+
}
|
|
7105
|
+
return uniqItems(commands);
|
|
7106
|
+
}
|
|
7107
|
+
function extractRecordedTestLines(tasksContent, planContent, lang) {
|
|
7108
|
+
const section = extractMarkdownByHeadings(
|
|
7109
|
+
tasksContent,
|
|
7110
|
+
["\uD14C\uC2A4\uD2B8 \uC2E4\uD589 \uAE30\uB85D", "Tests Run", "Test Run Log", "Test Execution Log"],
|
|
7111
|
+
[3, 2]
|
|
7112
|
+
);
|
|
7113
|
+
const records = [];
|
|
7114
|
+
if (section) {
|
|
7115
|
+
for (const line of section.split("\n")) {
|
|
7116
|
+
if (!line.trim().startsWith("|")) continue;
|
|
7117
|
+
const cells = line.split("|").slice(1, -1).map((cell) => cell.trim().replace(/`/g, ""));
|
|
7118
|
+
if (cells.length < 3) continue;
|
|
7119
|
+
const [cmd, time, result] = cells;
|
|
7120
|
+
if (!cmd || /^명령어$/i.test(cmd) || /^command$/i.test(cmd) || /^-+$/.test(cmd)) continue;
|
|
7121
|
+
if (/\{[^}]*\}/.test(cmd) || /\{[^}]*\}/.test(result || "")) continue;
|
|
7122
|
+
const renderedResult = result || (lang === "ko" ? "\uBBF8\uAE30\uB85D" : "not recorded");
|
|
7123
|
+
const renderedTime = time && time !== "-" ? ` (${time})` : "";
|
|
7124
|
+
records.push(`\`${cmd}\` \u2014 ${renderedResult}${renderedTime}`);
|
|
7125
|
+
}
|
|
7126
|
+
const lines = section.split("\n");
|
|
7127
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7128
|
+
const commandMatch = lines[i].match(/^\s*-\s*(?:명령어|Command)\s*:\s*`?([^`]+?)`?\s*$/i);
|
|
7129
|
+
if (!commandMatch) continue;
|
|
7130
|
+
const command = commandMatch[1].trim();
|
|
7131
|
+
if (!command || /\{[^}]*\}/.test(command)) continue;
|
|
7132
|
+
let result = "";
|
|
7133
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 5); j++) {
|
|
7134
|
+
const resultMatch = lines[j].match(/^\s*-\s*(?:결과|Result)\s*:\s*(.+?)\s*$/i);
|
|
7135
|
+
if (!resultMatch) continue;
|
|
7136
|
+
result = resultMatch[1].replace(/`/g, "").trim();
|
|
7137
|
+
break;
|
|
7138
|
+
}
|
|
7139
|
+
if (!result || /\{[^}]*\}/.test(result)) continue;
|
|
7140
|
+
records.push(`\`${command}\` \u2014 ${result}`);
|
|
7141
|
+
}
|
|
7142
|
+
}
|
|
7143
|
+
if (records.length > 0) {
|
|
7144
|
+
return uniqItemsByContainment(uniqItems(records));
|
|
7145
|
+
}
|
|
7146
|
+
const plannedCommands = extractCommandsFromSection(
|
|
7147
|
+
extractMarkdownByHeadings(
|
|
7148
|
+
planContent,
|
|
7149
|
+
["\uAC80\uC99D \uBA85\uB839(\uC608\uC815)", "Validation Commands", "Verification Commands"],
|
|
7150
|
+
[3, 2]
|
|
7151
|
+
)
|
|
7152
|
+
);
|
|
7153
|
+
if (plannedCommands.length > 0) {
|
|
7154
|
+
return plannedCommands.map(
|
|
7155
|
+
(command) => lang === "ko" ? `\`${command}\` \u2014 \uC2E4\uD589 \uACB0\uACFC\uB97C \uAE30\uB85D\uD558\uC138\uC694.` : `\`${command}\` \u2014 record execution result.`
|
|
7156
|
+
);
|
|
7157
|
+
}
|
|
7158
|
+
return [];
|
|
7159
|
+
}
|
|
7160
|
+
function getPrChangesAndTests(specContent, planContent, tasksContent, overview, lang) {
|
|
7161
|
+
const requirementHeadings = extractSectionHeadings(
|
|
7162
|
+
extractMarkdownSection(specContent, ["\uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D", "Functional Requirements"])
|
|
7163
|
+
);
|
|
7164
|
+
const scopeFromPlan = extractScopeItemsFromPlan(planContent, lang).include;
|
|
7165
|
+
const planTargets = extractPlanChangeTargets(planContent, lang);
|
|
7166
|
+
const taskTitles = extractTaskTitles(tasksContent);
|
|
7167
|
+
const changes = uniqItemsByContainment(
|
|
7168
|
+
uniqItems([
|
|
7169
|
+
...taskTitles,
|
|
7170
|
+
...scopeFromPlan,
|
|
7171
|
+
...requirementHeadings,
|
|
7172
|
+
...planTargets,
|
|
7173
|
+
sanitizeDraftItem(overview) || ""
|
|
7174
|
+
])
|
|
7175
|
+
).slice(0, 6);
|
|
7176
|
+
while (changes.length < 3) {
|
|
7177
|
+
changes.push(
|
|
7178
|
+
lang === "ko" ? changes.length === 0 ? "\uD575\uC2EC \uAD6C\uD604 \uBCC0\uACBD \uC0AC\uD56D\uC744 \uC694\uC57D\uD574 \uBC18\uC601\uD55C\uB2E4." : changes.length === 1 ? "\uC601\uD5A5 \uBC94\uC704(\uD638\uD658\uC131/\uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD3EC\uD568)\uB97C \uBA85\uC2DC\uD55C\uB2E4." : "\uBB38\uC11C\uC640 \uAD6C\uD604 \uAC04 \uBD88\uC77C\uCE58\uAC00 \uC5C6\uB3C4\uB85D \uC815\uD569\uC131\uC744 \uC810\uAC80\uD55C\uB2E4." : changes.length === 0 ? "Summarize key implementation changes." : changes.length === 1 ? "Document impact scope (including compatibility/migration)." : "Verify document and implementation consistency."
|
|
7179
|
+
);
|
|
7180
|
+
}
|
|
7181
|
+
const tests = uniqItemsByContainment(
|
|
7182
|
+
uniqItems(extractRecordedTestLines(tasksContent, planContent, lang))
|
|
7183
|
+
).slice(0, 4);
|
|
7184
|
+
while (tests.length < 2) {
|
|
7185
|
+
tests.push(
|
|
7186
|
+
lang === "ko" ? tests.length === 0 ? "\uAD00\uB828 \uD14C\uC2A4\uD2B8\uB97C \uC2E4\uD589\uD558\uACE0 \uACB0\uACFC\uB97C \uAE30\uB85D\uD55C\uB2E4." : "\uBBF8\uC2E4\uD589 \uD14C\uC2A4\uD2B8\uAC00 \uC788\uC73C\uBA74 \uC0AC\uC720\uC640 \uB9AC\uC2A4\uD06C\uB97C \uAE30\uB85D\uD55C\uB2E4." : tests.length === 0 ? "Run relevant tests and record results." : "If tests were not run, record rationale and risk."
|
|
7187
|
+
);
|
|
7188
|
+
}
|
|
7189
|
+
return {
|
|
7190
|
+
changes: changes.slice(0, 6),
|
|
7191
|
+
tests: tests.slice(0, 4)
|
|
7192
|
+
};
|
|
7193
|
+
}
|
|
6479
7194
|
function resolveOverviewFromSpec(specContent, feature, lang) {
|
|
6480
7195
|
const fromPurpose = sanitizeOverviewSection(
|
|
6481
7196
|
extractMarkdownSection(specContent, ["\uBAA9\uC801", "Purpose"])
|
|
@@ -6487,7 +7202,112 @@ function resolveOverviewFromSpec(specContent, feature, lang) {
|
|
|
6487
7202
|
if (fromOverview) return fromOverview;
|
|
6488
7203
|
return lang === "ko" ? `\`${feature.folderName}\` \uAE30\uB2A5 \uAC1C\uC694\uB97C spec.md \uAE30\uC900\uC73C\uB85C \uC791\uC131\uD558\uC138\uC694.` : `Summarize feature \`${feature.folderName}\` from spec.md.`;
|
|
6489
7204
|
}
|
|
6490
|
-
function
|
|
7205
|
+
function getPrScreenshotsHeading(lang) {
|
|
7206
|
+
return getGithubDraftArtifactHeading("pr", "screenshots", lang) || (lang === "ko" ? "\uC2A4\uD06C\uB9B0\uC0F7" : "Screenshots");
|
|
7207
|
+
}
|
|
7208
|
+
function getPrMermaidHeading(lang) {
|
|
7209
|
+
return getGithubDraftArtifactHeading("pr", "mermaid", lang) || (lang === "ko" ? "\uC544\uD0A4\uD14D\uCC98 \uB2E4\uC774\uC5B4\uADF8\uB7A8" : "Architecture Diagram");
|
|
7210
|
+
}
|
|
7211
|
+
function buildPrScreenshotsSection(lang) {
|
|
7212
|
+
if (lang === "ko") {
|
|
7213
|
+
return `
|
|
7214
|
+
## \uC2A4\uD06C\uB9B0\uC0F7
|
|
7215
|
+
|
|
7216
|
+
- [ ] \uC5C5\uB85C\uB4DC\uD55C \uC2A4\uD06C\uB9B0\uC0F7 URL\uC744 \uD3EC\uD568\uD558\uC138\uC694. (\uC608: \`\`)
|
|
7217
|
+
`;
|
|
7218
|
+
}
|
|
7219
|
+
return `
|
|
7220
|
+
## Screenshots
|
|
7221
|
+
|
|
7222
|
+
- [ ] Include uploaded screenshot URL(s). (e.g. \`\`)
|
|
7223
|
+
`;
|
|
7224
|
+
}
|
|
7225
|
+
function buildPrMermaidSection(lang) {
|
|
7226
|
+
if (lang === "ko") {
|
|
7227
|
+
return `
|
|
7228
|
+
## \uC544\uD0A4\uD14D\uCC98 \uB2E4\uC774\uC5B4\uADF8\uB7A8
|
|
7229
|
+
|
|
7230
|
+
\`\`\`mermaid
|
|
7231
|
+
sequenceDiagram
|
|
7232
|
+
participant Client
|
|
7233
|
+
participant API
|
|
7234
|
+
participant Service
|
|
7235
|
+
participant DB
|
|
7236
|
+
Client->>API: Request
|
|
7237
|
+
API->>Service: Execute
|
|
7238
|
+
Service->>DB: Query/Command
|
|
7239
|
+
DB-->>Service: Result
|
|
7240
|
+
Service-->>API: Response DTO
|
|
7241
|
+
API-->>Client: Response
|
|
7242
|
+
\`\`\`
|
|
7243
|
+
`;
|
|
7244
|
+
}
|
|
7245
|
+
return `
|
|
7246
|
+
## Architecture Diagram
|
|
7247
|
+
|
|
7248
|
+
\`\`\`mermaid
|
|
7249
|
+
sequenceDiagram
|
|
7250
|
+
participant Client
|
|
7251
|
+
participant API
|
|
7252
|
+
participant Service
|
|
7253
|
+
participant DB
|
|
7254
|
+
Client->>API: Request
|
|
7255
|
+
API->>Service: Execute
|
|
7256
|
+
Service->>DB: Query/Command
|
|
7257
|
+
DB-->>Service: Result
|
|
7258
|
+
Service-->>API: Response DTO
|
|
7259
|
+
API-->>Client: Response
|
|
7260
|
+
\`\`\`
|
|
7261
|
+
`;
|
|
7262
|
+
}
|
|
7263
|
+
function ensurePrArtifacts(body, policy, lang) {
|
|
7264
|
+
if (policy.includeScreenshots) {
|
|
7265
|
+
const heading = getPrScreenshotsHeading(lang);
|
|
7266
|
+
const section = extractMarkdownByHeadings(body, [heading], [2]);
|
|
7267
|
+
if (!section) {
|
|
7268
|
+
throw createCliError(
|
|
7269
|
+
"PRECONDITION_FAILED",
|
|
7270
|
+
tg(lang, "prScreenshotsSectionMissing", { section: heading })
|
|
7271
|
+
);
|
|
7272
|
+
}
|
|
7273
|
+
if (!/!\[[^\]]*]\((?!\s*\))[^)]+\)/m.test(section)) {
|
|
7274
|
+
throw createCliError(
|
|
7275
|
+
"PRECONDITION_FAILED",
|
|
7276
|
+
tg(lang, "prScreenshotImageMissing", { section: heading })
|
|
7277
|
+
);
|
|
7278
|
+
}
|
|
7279
|
+
}
|
|
7280
|
+
if (policy.includeMermaid) {
|
|
7281
|
+
const heading = getPrMermaidHeading(lang);
|
|
7282
|
+
const section = extractMarkdownByHeadings(body, [heading], [2]);
|
|
7283
|
+
if (!section) {
|
|
7284
|
+
throw createCliError(
|
|
7285
|
+
"PRECONDITION_FAILED",
|
|
7286
|
+
tg(lang, "prMermaidSectionMissing", { section: heading })
|
|
7287
|
+
);
|
|
7288
|
+
}
|
|
7289
|
+
if (!/```mermaid[\s\S]*?```/m.test(section)) {
|
|
7290
|
+
throw createCliError(
|
|
7291
|
+
"PRECONDITION_FAILED",
|
|
7292
|
+
tg(lang, "prMermaidBlockMissing", { section: heading })
|
|
7293
|
+
);
|
|
7294
|
+
}
|
|
7295
|
+
}
|
|
7296
|
+
}
|
|
7297
|
+
function buildIssueBody(specContent, planContent, tasksContent, overview, labels, paths, lang) {
|
|
7298
|
+
const bodyPaths = toBodyDocPaths(paths);
|
|
7299
|
+
const draft = getIssueGoalsAndCriteria(specContent, planContent, tasksContent, overview, lang);
|
|
7300
|
+
const goals = toChecklistLines(draft.goals);
|
|
7301
|
+
const criteria = toChecklistLines(draft.criteria);
|
|
7302
|
+
const scopeSection = draft.scope.length > 0 ? lang === "ko" ? `
|
|
7303
|
+
## \uC791\uC5C5 \uBC94\uC704(\uC608\uC815)
|
|
7304
|
+
|
|
7305
|
+
${draft.scope.map((item) => `- ${item}`).join("\n")}
|
|
7306
|
+
` : `
|
|
7307
|
+
## Planned Scope
|
|
7308
|
+
|
|
7309
|
+
${draft.scope.map((item) => `- ${item}`).join("\n")}
|
|
7310
|
+
` : "";
|
|
6491
7311
|
if (lang === "ko") {
|
|
6492
7312
|
return `## \uAC1C\uC694
|
|
6493
7313
|
|
|
@@ -6495,19 +7315,18 @@ ${overview}
|
|
|
6495
7315
|
|
|
6496
7316
|
## \uBAA9\uD45C
|
|
6497
7317
|
|
|
6498
|
-
|
|
6499
|
-
- [ ] TODO: \uAD6C\uD604 \uBC94\uC704(\uD3EC\uD568/\uC81C\uC678)\uB97C \uAD6C\uCCB4\uC801\uC73C\uB85C \uC791\uC131\uD558\uC138\uC694.
|
|
7318
|
+
${goals}
|
|
6500
7319
|
|
|
6501
7320
|
## \uC644\uB8CC \uAE30\uC900
|
|
6502
7321
|
|
|
6503
|
-
|
|
6504
|
-
|
|
7322
|
+
${criteria}
|
|
7323
|
+
${scopeSection}
|
|
6505
7324
|
|
|
6506
7325
|
## \uAD00\uB828 \uBB38\uC11C
|
|
6507
7326
|
|
|
6508
|
-
- **Spec**: \`${
|
|
6509
|
-
- **Plan**: \`${
|
|
6510
|
-
- **Tasks**: \`${
|
|
7327
|
+
- **Spec**: \`${bodyPaths.specPath}\`
|
|
7328
|
+
- **Plan**: \`${bodyPaths.planPath}\`
|
|
7329
|
+
- **Tasks**: \`${bodyPaths.tasksPath}\`
|
|
6511
7330
|
|
|
6512
7331
|
## \uB77C\uBCA8
|
|
6513
7332
|
|
|
@@ -6520,29 +7339,40 @@ ${overview}
|
|
|
6520
7339
|
|
|
6521
7340
|
## Goals
|
|
6522
7341
|
|
|
6523
|
-
|
|
6524
|
-
- [ ] TODO: Clarify in-scope and out-of-scope boundaries.
|
|
7342
|
+
${goals}
|
|
6525
7343
|
|
|
6526
7344
|
## Completion Criteria
|
|
6527
7345
|
|
|
6528
|
-
|
|
6529
|
-
|
|
7346
|
+
${criteria}
|
|
7347
|
+
${scopeSection}
|
|
6530
7348
|
|
|
6531
7349
|
## Related Documents
|
|
6532
7350
|
|
|
6533
|
-
- **Spec**: \`${
|
|
6534
|
-
- **Plan**: \`${
|
|
6535
|
-
- **Tasks**: \`${
|
|
7351
|
+
- **Spec**: \`${bodyPaths.specPath}\`
|
|
7352
|
+
- **Plan**: \`${bodyPaths.planPath}\`
|
|
7353
|
+
- **Tasks**: \`${bodyPaths.tasksPath}\`
|
|
6536
7354
|
|
|
6537
7355
|
## Labels
|
|
6538
7356
|
|
|
6539
7357
|
${labels.map((label) => `- \`${label}\``).join("\n")}
|
|
6540
7358
|
`;
|
|
6541
7359
|
}
|
|
6542
|
-
function buildPrBody(feature, overview, paths, lang) {
|
|
7360
|
+
function buildPrBody(feature, specContent, planContent, tasksContent, overview, paths, artifactPolicy, lang) {
|
|
7361
|
+
const bodyPaths = toBodyDocPaths(paths);
|
|
6543
7362
|
const closes = feature.issueNumber ? `
|
|
6544
7363
|
Closes #${feature.issueNumber}
|
|
6545
7364
|
` : "\n";
|
|
7365
|
+
const draft = getPrChangesAndTests(
|
|
7366
|
+
specContent,
|
|
7367
|
+
planContent,
|
|
7368
|
+
tasksContent,
|
|
7369
|
+
overview,
|
|
7370
|
+
lang
|
|
7371
|
+
);
|
|
7372
|
+
const changes = toChecklistLines(draft.changes);
|
|
7373
|
+
const tests = toChecklistLines(draft.tests);
|
|
7374
|
+
const screenshotsSection = artifactPolicy.includeScreenshots ? buildPrScreenshotsSection(lang) : "";
|
|
7375
|
+
const mermaidSection = artifactPolicy.includeMermaid ? buildPrMermaidSection(lang) : "";
|
|
6546
7376
|
if (lang === "ko") {
|
|
6547
7377
|
return `## \uAC1C\uC694
|
|
6548
7378
|
|
|
@@ -6550,20 +7380,20 @@ ${overview}
|
|
|
6550
7380
|
|
|
6551
7381
|
## \uBCC0\uACBD \uC0AC\uD56D
|
|
6552
7382
|
|
|
6553
|
-
|
|
6554
|
-
- [ ] TODO: \uC601\uD5A5 \uBC94\uC704/\uD638\uD658\uC131(\uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD3EC\uD568)\uC744 \uC791\uC131\uD558\uC138\uC694.
|
|
7383
|
+
${changes}
|
|
6555
7384
|
|
|
6556
7385
|
## \uD14C\uC2A4\uD2B8
|
|
6557
7386
|
|
|
6558
7387
|
### \uC2E4\uD589\uD55C \uD14C\uC2A4\uD2B8
|
|
6559
7388
|
|
|
6560
|
-
|
|
6561
|
-
|
|
7389
|
+
${tests}
|
|
7390
|
+
${screenshotsSection}
|
|
7391
|
+
${mermaidSection}
|
|
6562
7392
|
|
|
6563
7393
|
## \uAD00\uB828 \uBB38\uC11C
|
|
6564
7394
|
|
|
6565
|
-
- **Spec**: \`${
|
|
6566
|
-
- **Tasks**: \`${
|
|
7395
|
+
- **Spec**: \`${bodyPaths.specPath}\`
|
|
7396
|
+
- **Tasks**: \`${bodyPaths.tasksPath}\`${closes}`;
|
|
6567
7397
|
}
|
|
6568
7398
|
return `## Overview
|
|
6569
7399
|
|
|
@@ -6571,26 +7401,26 @@ ${overview}
|
|
|
6571
7401
|
|
|
6572
7402
|
## Changes
|
|
6573
7403
|
|
|
6574
|
-
|
|
6575
|
-
- [ ] TODO: Describe impact/scope (including migration if any).
|
|
7404
|
+
${changes}
|
|
6576
7405
|
|
|
6577
7406
|
## Tests
|
|
6578
7407
|
|
|
6579
7408
|
### Tests Run
|
|
6580
7409
|
|
|
6581
|
-
|
|
6582
|
-
|
|
7410
|
+
${tests}
|
|
7411
|
+
${screenshotsSection}
|
|
7412
|
+
${mermaidSection}
|
|
6583
7413
|
|
|
6584
7414
|
## Related Documents
|
|
6585
7415
|
|
|
6586
|
-
- **Spec**: \`${
|
|
6587
|
-
- **Tasks**: \`${
|
|
7416
|
+
- **Spec**: \`${bodyPaths.specPath}\`
|
|
7417
|
+
- **Tasks**: \`${bodyPaths.tasksPath}\`${closes}`;
|
|
6588
7418
|
}
|
|
6589
7419
|
function getRequiredIssueSections(lang) {
|
|
6590
|
-
return
|
|
7420
|
+
return getGithubDraftRequiredSections("issue", lang);
|
|
6591
7421
|
}
|
|
6592
7422
|
function getRequiredPrSections(lang) {
|
|
6593
|
-
return
|
|
7423
|
+
return getGithubDraftRequiredSections("pr", lang);
|
|
6594
7424
|
}
|
|
6595
7425
|
function replaceListField(content, keys, value) {
|
|
6596
7426
|
for (const key of keys) {
|
|
@@ -6621,10 +7451,10 @@ function insertFieldInGithubIssueSection(content, key, value) {
|
|
|
6621
7451
|
return { content: lines.join("\n"), changed: true };
|
|
6622
7452
|
}
|
|
6623
7453
|
function syncTasksPrMetadata(tasksPath, prUrl, nextStatus, lang) {
|
|
6624
|
-
if (!
|
|
7454
|
+
if (!fs14.existsSync(tasksPath)) {
|
|
6625
7455
|
throw createCliError("DOCS_NOT_FOUND", tg(lang, "tasksNotFound", { path: tasksPath }));
|
|
6626
7456
|
}
|
|
6627
|
-
const original =
|
|
7457
|
+
const original = fs14.readFileSync(tasksPath, "utf-8");
|
|
6628
7458
|
let next = original;
|
|
6629
7459
|
let changed = false;
|
|
6630
7460
|
const prReplaced = replaceListField(next, ["PR", "Pull Request"], prUrl);
|
|
@@ -6648,7 +7478,7 @@ function syncTasksPrMetadata(tasksPath, prUrl, nextStatus, lang) {
|
|
|
6648
7478
|
changed = changed || inserted.changed;
|
|
6649
7479
|
}
|
|
6650
7480
|
if (changed) {
|
|
6651
|
-
|
|
7481
|
+
fs14.writeFileSync(tasksPath, next, "utf-8");
|
|
6652
7482
|
}
|
|
6653
7483
|
return { changed, path: tasksPath };
|
|
6654
7484
|
}
|
|
@@ -6676,7 +7506,7 @@ function ensureCleanWorktree(cwd, lang) {
|
|
|
6676
7506
|
}
|
|
6677
7507
|
}
|
|
6678
7508
|
function commitAndPushPath(cwd, absPath, message, lang) {
|
|
6679
|
-
const relativePath =
|
|
7509
|
+
const relativePath = path18.relative(cwd, absPath) || absPath;
|
|
6680
7510
|
const status = runProcessOrThrow(
|
|
6681
7511
|
"git",
|
|
6682
7512
|
["status", "--porcelain=v1", "--", relativePath],
|
|
@@ -6808,15 +7638,25 @@ function githubCommand(program2) {
|
|
|
6808
7638
|
[paths.specPath, paths.planPath, paths.tasksPath],
|
|
6809
7639
|
config.lang
|
|
6810
7640
|
);
|
|
6811
|
-
const specContent = await
|
|
7641
|
+
const specContent = await fs14.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
|
|
7642
|
+
const planContent = await fs14.readFile(path18.join(config.docsDir, paths.planPath), "utf-8");
|
|
7643
|
+
const tasksContent = await fs14.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
|
|
6812
7644
|
const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
|
|
6813
7645
|
const title = options.title?.trim() || tg(config.lang, "issueDefaultTitle", {
|
|
6814
7646
|
slug: feature.slug,
|
|
6815
7647
|
folder: feature.folderName
|
|
6816
7648
|
});
|
|
6817
|
-
const
|
|
7649
|
+
const generatedBody = buildIssueBody(
|
|
7650
|
+
specContent,
|
|
7651
|
+
planContent,
|
|
7652
|
+
tasksContent,
|
|
7653
|
+
overview,
|
|
7654
|
+
labels,
|
|
7655
|
+
paths,
|
|
7656
|
+
config.lang
|
|
7657
|
+
);
|
|
6818
7658
|
ensureSections(
|
|
6819
|
-
|
|
7659
|
+
generatedBody,
|
|
6820
7660
|
getRequiredIssueSections(config.lang),
|
|
6821
7661
|
tg(config.lang, "kindIssue"),
|
|
6822
7662
|
config.lang
|
|
@@ -6827,10 +7667,23 @@ function githubCommand(program2) {
|
|
|
6827
7667
|
config.docsDir,
|
|
6828
7668
|
feature.type
|
|
6829
7669
|
);
|
|
6830
|
-
|
|
6831
|
-
|
|
7670
|
+
const explicitBodyFile = (options.bodyFile || "").trim();
|
|
7671
|
+
let body = generatedBody;
|
|
7672
|
+
if (options.create && explicitBodyFile && await fs14.pathExists(bodyFile)) {
|
|
7673
|
+
body = await fs14.readFile(bodyFile, "utf-8");
|
|
7674
|
+
ensureSections(
|
|
7675
|
+
body,
|
|
7676
|
+
getRequiredIssueSections(config.lang),
|
|
7677
|
+
tg(config.lang, "kindIssue"),
|
|
7678
|
+
config.lang
|
|
7679
|
+
);
|
|
7680
|
+
} else {
|
|
7681
|
+
await fs14.ensureDir(path18.dirname(bodyFile));
|
|
7682
|
+
await fs14.writeFile(bodyFile, generatedBody, "utf-8");
|
|
7683
|
+
}
|
|
6832
7684
|
let issueUrl;
|
|
6833
7685
|
if (options.create) {
|
|
7686
|
+
ensureNoTodoPlaceholders(body, tg(config.lang, "kindIssue"), config.lang);
|
|
6834
7687
|
assertRemoteApproval(
|
|
6835
7688
|
options.confirm,
|
|
6836
7689
|
tg(config.lang, "operationIssueCreate"),
|
|
@@ -6914,7 +7767,7 @@ function githubCommand(program2) {
|
|
|
6914
7767
|
github.command("pr [feature-name]").description(tg(commandLang, "cmdPrDescription")).option("--json", tg(commandLang, "optJson")).option("--repo <repo>", tg(commandLang, "optRepo")).option("--component <component>", tg(commandLang, "optComponent")).option("--title <title>", tg(commandLang, "optPrTitle")).option("--labels <labels>", tg(commandLang, "optLabels")).option("--body-file <path>", tg(commandLang, "optPrBodyFile")).option("--assignee <assignee>", tg(commandLang, "optPrAssignee")).option("--base <branch>", tg(commandLang, "optPrBase"), "main").option("--create", tg(commandLang, "optPrCreate")).option("--pr <ref>", tg(commandLang, "optPrRef")).option("--merge", tg(commandLang, "optPrMerge")).option(
|
|
6915
7768
|
"--confirm <reply>",
|
|
6916
7769
|
tg(commandLang, "optPrConfirm")
|
|
6917
|
-
).option("--retry <count>", tg(commandLang, "optPrRetry")).option("--no-sync-tasks", tg(commandLang, "optPrNoSyncTasks")).option("--commit-sync", tg(commandLang, "optPrCommitSync")).action(async (featureName, options) => {
|
|
7770
|
+
).option("--retry <count>", tg(commandLang, "optPrRetry")).option("--screenshots <mode>", tg(commandLang, "optPrScreenshots"), "auto").option("--mermaid <mode>", tg(commandLang, "optPrMermaid"), "auto").option("--no-sync-tasks", tg(commandLang, "optPrNoSyncTasks")).option("--commit-sync", tg(commandLang, "optPrCommitSync")).action(async (featureName, options) => {
|
|
6918
7771
|
try {
|
|
6919
7772
|
const selectedComponent = resolveComponentOption4(options, commandLang);
|
|
6920
7773
|
const { config, feature } = await resolveFeatureOrThrow(featureName, {
|
|
@@ -6923,7 +7776,10 @@ function githubCommand(program2) {
|
|
|
6923
7776
|
const labels = parseLabels(options.labels, config.lang);
|
|
6924
7777
|
const paths = getFeatureDocPaths(feature);
|
|
6925
7778
|
ensureDocsExist(config.docsDir, [paths.specPath, paths.tasksPath], config.lang);
|
|
6926
|
-
const specContent = await
|
|
7779
|
+
const specContent = await fs14.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
|
|
7780
|
+
const planPath = path18.join(config.docsDir, paths.planPath);
|
|
7781
|
+
const planContent = await fs14.pathExists(planPath) ? await fs14.readFile(planPath, "utf-8") : "";
|
|
7782
|
+
const tasksContent = await fs14.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
|
|
6927
7783
|
const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
|
|
6928
7784
|
const defaultTitle = feature.issueNumber ? tg(config.lang, "prDefaultTitleWithIssue", {
|
|
6929
7785
|
issue: feature.issueNumber,
|
|
@@ -6932,9 +7788,19 @@ function githubCommand(program2) {
|
|
|
6932
7788
|
slug: feature.slug
|
|
6933
7789
|
});
|
|
6934
7790
|
const title = options.title?.trim() || defaultTitle;
|
|
6935
|
-
const
|
|
7791
|
+
const artifactPolicy = resolvePrArtifactPolicy(config, feature, options);
|
|
7792
|
+
const generatedBody = buildPrBody(
|
|
7793
|
+
feature,
|
|
7794
|
+
specContent,
|
|
7795
|
+
planContent,
|
|
7796
|
+
tasksContent,
|
|
7797
|
+
overview,
|
|
7798
|
+
paths,
|
|
7799
|
+
artifactPolicy,
|
|
7800
|
+
config.lang
|
|
7801
|
+
);
|
|
6936
7802
|
ensureSections(
|
|
6937
|
-
|
|
7803
|
+
generatedBody,
|
|
6938
7804
|
getRequiredPrSections(config.lang),
|
|
6939
7805
|
tg(config.lang, "kindPr"),
|
|
6940
7806
|
config.lang
|
|
@@ -6945,13 +7811,27 @@ function githubCommand(program2) {
|
|
|
6945
7811
|
config.docsDir,
|
|
6946
7812
|
feature.type
|
|
6947
7813
|
);
|
|
6948
|
-
|
|
6949
|
-
|
|
7814
|
+
const explicitBodyFile = (options.bodyFile || "").trim();
|
|
7815
|
+
let body = generatedBody;
|
|
7816
|
+
if (options.create && explicitBodyFile && await fs14.pathExists(bodyFile)) {
|
|
7817
|
+
body = await fs14.readFile(bodyFile, "utf-8");
|
|
7818
|
+
ensureSections(
|
|
7819
|
+
body,
|
|
7820
|
+
getRequiredPrSections(config.lang),
|
|
7821
|
+
tg(config.lang, "kindPr"),
|
|
7822
|
+
config.lang
|
|
7823
|
+
);
|
|
7824
|
+
} else {
|
|
7825
|
+
await fs14.ensureDir(path18.dirname(bodyFile));
|
|
7826
|
+
await fs14.writeFile(bodyFile, generatedBody, "utf-8");
|
|
7827
|
+
}
|
|
6950
7828
|
const retryCount = toRetryCount(options.retry, config.lang);
|
|
6951
7829
|
let prUrl = options.pr?.trim() || "";
|
|
6952
7830
|
let mergedAttempts;
|
|
6953
7831
|
let syncChanged = false;
|
|
6954
7832
|
if (options.create) {
|
|
7833
|
+
ensureNoTodoPlaceholders(body, tg(config.lang, "kindPr"), config.lang);
|
|
7834
|
+
ensurePrArtifacts(body, artifactPolicy, config.lang);
|
|
6955
7835
|
assertRemoteApproval(
|
|
6956
7836
|
options.confirm,
|
|
6957
7837
|
tg(config.lang, "operationPrCreate"),
|
|
@@ -6995,7 +7875,7 @@ function githubCommand(program2) {
|
|
|
6995
7875
|
}
|
|
6996
7876
|
if (prUrl && options.syncTasks !== false) {
|
|
6997
7877
|
const synced = syncTasksPrMetadata(
|
|
6998
|
-
|
|
7878
|
+
path18.join(config.docsDir, paths.tasksPath),
|
|
6999
7879
|
prUrl,
|
|
7000
7880
|
"Review",
|
|
7001
7881
|
config.lang
|
|
@@ -7046,6 +7926,10 @@ function githubCommand(program2) {
|
|
|
7046
7926
|
labels,
|
|
7047
7927
|
body,
|
|
7048
7928
|
bodyFile,
|
|
7929
|
+
artifactPolicy: {
|
|
7930
|
+
screenshots: artifactPolicy.includeScreenshots,
|
|
7931
|
+
mermaid: artifactPolicy.includeMermaid
|
|
7932
|
+
},
|
|
7049
7933
|
prUrl: prUrl || void 0,
|
|
7050
7934
|
syncChanged,
|
|
7051
7935
|
merged: !!options.merge,
|
|
@@ -7187,6 +8071,7 @@ function docsCommand(program2) {
|
|
|
7187
8071
|
id,
|
|
7188
8072
|
command: toBuiltinDocCommand(id)
|
|
7189
8073
|
}));
|
|
8074
|
+
const contract = getGithubDraftContractForBuiltinDoc(docId, config.lang);
|
|
7190
8075
|
if (options.json) {
|
|
7191
8076
|
console.log(
|
|
7192
8077
|
JSON.stringify(
|
|
@@ -7202,7 +8087,8 @@ function docsCommand(program2) {
|
|
|
7202
8087
|
hash: loaded.hash,
|
|
7203
8088
|
content: loaded.content
|
|
7204
8089
|
},
|
|
7205
|
-
requiredDocs: followups
|
|
8090
|
+
requiredDocs: followups,
|
|
8091
|
+
contract: contract || void 0
|
|
7206
8092
|
},
|
|
7207
8093
|
null,
|
|
7208
8094
|
2
|
|
@@ -7210,7 +8096,7 @@ function docsCommand(program2) {
|
|
|
7210
8096
|
);
|
|
7211
8097
|
return;
|
|
7212
8098
|
}
|
|
7213
|
-
const relativeFromCwd =
|
|
8099
|
+
const relativeFromCwd = path18.relative(process.cwd(), loaded.entry.absolutePath);
|
|
7214
8100
|
console.log();
|
|
7215
8101
|
console.log(chalk6.bold(`\u{1F4C4} ${loaded.entry.id}: ${loaded.entry.title}`));
|
|
7216
8102
|
console.log(
|
|
@@ -7300,13 +8186,13 @@ ${version}
|
|
|
7300
8186
|
}
|
|
7301
8187
|
return `${ascii}${footer}`;
|
|
7302
8188
|
}
|
|
7303
|
-
var CACHE_FILE =
|
|
8189
|
+
var CACHE_FILE = path18.join(os.homedir(), ".lee-spec-kit-version-cache.json");
|
|
7304
8190
|
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
7305
8191
|
function getCurrentVersion() {
|
|
7306
8192
|
try {
|
|
7307
|
-
const packageJsonPath =
|
|
7308
|
-
if (
|
|
7309
|
-
const pkg =
|
|
8193
|
+
const packageJsonPath = path18.join(__dirname$1, "..", "package.json");
|
|
8194
|
+
if (fs14.existsSync(packageJsonPath)) {
|
|
8195
|
+
const pkg = fs14.readJsonSync(packageJsonPath);
|
|
7310
8196
|
return pkg.version;
|
|
7311
8197
|
}
|
|
7312
8198
|
} catch {
|
|
@@ -7315,8 +8201,8 @@ function getCurrentVersion() {
|
|
|
7315
8201
|
}
|
|
7316
8202
|
function readCache() {
|
|
7317
8203
|
try {
|
|
7318
|
-
if (
|
|
7319
|
-
return
|
|
8204
|
+
if (fs14.existsSync(CACHE_FILE)) {
|
|
8205
|
+
return fs14.readJsonSync(CACHE_FILE);
|
|
7320
8206
|
}
|
|
7321
8207
|
} catch {
|
|
7322
8208
|
}
|
|
@@ -7404,9 +8290,9 @@ function shouldCheckForUpdates() {
|
|
|
7404
8290
|
if (shouldCheckForUpdates()) checkForUpdates();
|
|
7405
8291
|
function getCliVersion() {
|
|
7406
8292
|
try {
|
|
7407
|
-
const packageJsonPath =
|
|
7408
|
-
if (
|
|
7409
|
-
const pkg =
|
|
8293
|
+
const packageJsonPath = path18.join(__dirname$1, "..", "package.json");
|
|
8294
|
+
if (fs14.existsSync(packageJsonPath)) {
|
|
8295
|
+
const pkg = fs14.readJsonSync(packageJsonPath);
|
|
7410
8296
|
if (pkg?.version) return String(pkg.version);
|
|
7411
8297
|
}
|
|
7412
8298
|
} catch {
|