lee-spec-kit 0.5.0 → 0.5.2
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 +47 -5
- package/README.md +49 -5
- package/dist/index.js +1120 -282
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -128,10 +128,12 @@ var I18N = {
|
|
|
128
128
|
"context.tipShowAll": "\uC804\uCCB4 \uBCF4\uAE30",
|
|
129
129
|
"context.tipShowDone": "\uC644\uB8CC\uB9CC \uBCF4\uAE30",
|
|
130
130
|
"context.checkRequired": "[\uD655\uC778 \uD544\uC694] ",
|
|
131
|
-
"context.checkPolicyHint": "\u2139\uFE0F \uC0AC\uC6A9\uC790 \uD655\uC778 \
|
|
131
|
+
"context.checkPolicyHint": "\u2139\uFE0F \uC0AC\uC6A9\uC790 \uD655\uC778 \uC815\uCC45 \uC548\uB0B4(\uD604\uC7AC Next Action \uC544\uB2D8): /docs/agents/agents.md \uCC38\uACE0 (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)",
|
|
132
132
|
"context.actionOptionHint": "\uC2B9\uC778 \uC751\uB2F5 \uD615\uC2DD: `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` (\uC608: `A`, `A OK`)",
|
|
133
|
+
"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.",
|
|
133
134
|
"context.tipDocsCommitRules": "\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uADDC\uCE59: /docs/agents/git-workflow.md \uCC38\uACE0",
|
|
134
135
|
"context.list.docsCommitNeeded": "\uBB38\uC11C \uCEE4\uBC0B \uD544\uC694",
|
|
136
|
+
"context.list.projectCommitNeeded": "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uCEE4\uBC0B \uD544\uC694",
|
|
135
137
|
"context.list.issueNumberNeeded": "\uC774\uC288 \uBC88\uD638 \uAE30\uB85D \uD544\uC694",
|
|
136
138
|
"context.list.addPrMetadata": "PR \uBA54\uD0C0\uB370\uC774\uD130(PR/PR \uC0C1\uD0DC) \uCD94\uAC00",
|
|
137
139
|
"context.list.recordPrLink": "PR \uB9C1\uD06C \uAE30\uB85D",
|
|
@@ -222,6 +224,8 @@ var I18N = {
|
|
|
222
224
|
issueCreateAndWrite: "GitHub Issue\uB97C \uC0DD\uC131\uD55C \uB4A4, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694. (skills/create-issue.md \uCC38\uACE0)",
|
|
223
225
|
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
|
|
224
226
|
docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
|
|
227
|
+
projectCommitIssueUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat(#{issueNumber}): {folderName} \uAD6C\uD604 \uC5C5\uB370\uC774\uD2B8"',
|
|
228
|
+
projectCommitUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat: {folderName} \uAD6C\uD604 \uC5C5\uB370\uC774\uD2B8"',
|
|
225
229
|
standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
|
|
226
230
|
createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
|
|
227
231
|
tasksAllDoneButNoChecklist: '\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC139\uC158\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. tasks.md\uC758 "\uC644\uB8CC \uC870\uAC74" \uC139\uC158\uC744 \uCD94\uAC00/\uD655\uC778\uD558\uC138\uC694.',
|
|
@@ -242,11 +246,13 @@ var I18N = {
|
|
|
242
246
|
docsGitUnavailable: "docs \uB808\uD3EC\uC758 git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (\uB808\uD3EC \uC704\uCE58 / git init \uD655\uC778)",
|
|
243
247
|
docsPathIgnored: "\uD604\uC7AC Feature \uBB38\uC11C \uACBD\uB85C\uAC00 git ignore \uB300\uC0C1\uC785\uB2C8\uB2E4: {path} (docs \uCEE4\uBC0B \uAC10\uC9C0\uAC00 \uC81C\uD55C\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.)",
|
|
244
248
|
docsUncommittedChanges: "\uBB38\uC11C \uBCC0\uACBD\uC0AC\uD56D\uC774 \uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (\uCD94\uAC00 \uBB38\uC11C \uCEE4\uBC0B \uD544\uC694) \uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uADDC\uCE59: /docs/agents/git-workflow.md \uCC38\uACE0",
|
|
249
|
+
projectUncommittedChanges: "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uBCC0\uACBD\uC0AC\uD56D\uC774 \uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (\uCD94\uAC00 \uCF54\uB4DC \uCEE4\uBC0B \uD544\uC694)",
|
|
245
250
|
legacyTasksDocStatusField: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. `\uBB38\uC11C \uC0C1\uD0DC` \uD544\uB4DC(Review/Approved)\uB97C \uCD94\uAC00\uD574 \uD0DC\uC2A4\uD06C \uC2B9\uC778 \uB2E8\uACC4\uB97C \uD65C\uC131\uD654\uD558\uC138\uC694.",
|
|
246
251
|
legacyTasksPrFields: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR` \uBC0F `PR \uC0C1\uD0DC` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694.",
|
|
247
252
|
workflowSpecNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC spec.md \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (spec.md\uC758 \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)",
|
|
248
253
|
workflowPlanNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC plan.md \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (plan.md\uC758 \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)",
|
|
249
254
|
workflowIssueMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC \uC774\uC288 \uBC88\uD638\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uC138\uC694.)",
|
|
255
|
+
workflowProjectUncommittedChanges: "\uC644\uB8CC \uC870\uAC74 \uC774\uC804\uC5D0 \uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uBCC0\uACBD\uC0AC\uD56D\uC744 \uCEE4\uBC0B\uD574\uC57C \uD569\uB2C8\uB2E4. (\uD504\uB85C\uC81D\uD2B8 \uC6CC\uD06C\uD2B8\uB9AC \uBBF8\uCEE4\uBC0B \uBCC0\uACBD \uC874\uC7AC)",
|
|
250
256
|
workflowPrLinkMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uB9C1\uD06C\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (tasks.md\uC758 PR \uD544\uB4DC\uB97C \uCC44\uC6B0\uC138\uC694.)",
|
|
251
257
|
workflowPrStatusMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uC0C1\uD0DC\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Review/Approved \uC911 \uD558\uB098\uB85C \uC124\uC815\uD558\uC138\uC694.)",
|
|
252
258
|
workflowPrStatusNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (merge \uD6C4 PR \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)"
|
|
@@ -319,10 +325,12 @@ var I18N = {
|
|
|
319
325
|
"context.tipShowAll": "Show all",
|
|
320
326
|
"context.tipShowDone": "Show done only",
|
|
321
327
|
"context.checkRequired": "[CHECK required] ",
|
|
322
|
-
"context.checkPolicyHint": "\u2139\uFE0F User check policy: see /docs/agents/agents.md (includes git push/merge and merge commits)
|
|
328
|
+
"context.checkPolicyHint": "\u2139\uFE0F User check policy notice (not the current next action): see /docs/agents/agents.md (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)",
|
|
323
329
|
"context.actionOptionHint": "Approval reply format: `<label>` or `<label> OK` (e.g. `A`, `A OK`)",
|
|
330
|
+
"context.actionExplainHint": "Before requesting approval, explain what each label will run/change with a one-line summary.",
|
|
324
331
|
"context.tipDocsCommitRules": "Commit message rules: /docs/agents/git-workflow.md",
|
|
325
332
|
"context.list.docsCommitNeeded": "Commit docs changes",
|
|
333
|
+
"context.list.projectCommitNeeded": "Commit project code changes",
|
|
326
334
|
"context.list.issueNumberNeeded": "Fill issue number in docs",
|
|
327
335
|
"context.list.addPrMetadata": "Add PR metadata (PR/PR Status)",
|
|
328
336
|
"context.list.recordPrLink": "Record PR link",
|
|
@@ -413,6 +421,8 @@ var I18N = {
|
|
|
413
421
|
issueCreateAndWrite: "Create a GitHub Issue, fill the issue number in spec.md/tasks.md, then prepare a docs commit. (See skills/create-issue.md)",
|
|
414
422
|
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
|
|
415
423
|
docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} docs update"',
|
|
424
|
+
projectCommitIssueUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat(#{issueNumber}): {folderName} implementation update"',
|
|
425
|
+
projectCommitUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat: {folderName} implementation update"',
|
|
416
426
|
standaloneNeedsProjectRoot: "Standalone mode requires projectRoot. (npx lee-spec-kit config --project-root ...)",
|
|
417
427
|
createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
|
|
418
428
|
tasksAllDoneButNoChecklist: 'All tasks are DONE, but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
|
|
@@ -433,11 +443,13 @@ var I18N = {
|
|
|
433
443
|
docsGitUnavailable: "Cannot read git status for the docs repo. (Check repo location / git init.)",
|
|
434
444
|
docsPathIgnored: "Current feature docs path is ignored by git: {path} (docs commit detection may be limited).",
|
|
435
445
|
docsUncommittedChanges: "Docs changes are not committed. (Additional docs commit needed.) Commit message rules: /docs/agents/git-workflow.md",
|
|
446
|
+
projectUncommittedChanges: "Project code changes are not committed. (Additional code commit needed.)",
|
|
436
447
|
legacyTasksDocStatusField: "Legacy tasks.md format detected. Add a `Doc Status` field (Review/Approved) to enable tasks approval.",
|
|
437
448
|
legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps.",
|
|
438
449
|
workflowSpecNotApproved: "Implementation is done but spec.md Status is not Approved. (Update spec.md Status to Approved.)",
|
|
439
450
|
workflowPlanNotApproved: "Implementation is done but plan.md Status is not Approved. (Update plan.md Status to Approved.)",
|
|
440
451
|
workflowIssueMissing: "Implementation is done but Issue Number is missing. (Fill Issue Number in spec.md/tasks.md.)",
|
|
452
|
+
workflowProjectUncommittedChanges: "Commit project code changes before completing workflow. (Project worktree has uncommitted changes.)",
|
|
441
453
|
workflowPrLinkMissing: "Implementation is done but PR link is missing. (Fill the PR field in tasks.md.)",
|
|
442
454
|
workflowPrStatusMissing: "Implementation is done but PR Status is missing. (Set PR Status to Review/Approved in tasks.md.)",
|
|
443
455
|
workflowPrStatusNotApproved: "Implementation is done but PR Status is not Approved. (After merge, update PR Status to Approved in tasks.md.)"
|
|
@@ -1628,7 +1640,7 @@ Store ${component} feature specs here.
|
|
|
1628
1640
|
lang,
|
|
1629
1641
|
createdAt: getLocalDateString(),
|
|
1630
1642
|
docsRepo,
|
|
1631
|
-
workflow: { mode: workflowMode },
|
|
1643
|
+
workflow: { mode: workflowMode, codeDirtyScope: "auto" },
|
|
1632
1644
|
pr: {
|
|
1633
1645
|
screenshots: { upload: false }
|
|
1634
1646
|
},
|
|
@@ -1858,13 +1870,39 @@ async function getConfig(cwd) {
|
|
|
1858
1870
|
|
|
1859
1871
|
// src/commands/feature.ts
|
|
1860
1872
|
function featureCommand(program2) {
|
|
1861
|
-
program2.command("feature <name>").description("Create a new feature folder").option("-r, --repo <repo>", "Component name (multi only)").option("--component <component>", "Component name (multi only)").option("--id <id>", "Feature ID (default: auto)").option("-d, --desc <description>", "Feature description for spec.md").option("--non-interactive", "Fail instead of prompting for input").action(async (name, options) => {
|
|
1873
|
+
program2.command("feature <name>").description("Create a new feature folder").option("-r, --repo <repo>", "Component name (multi only)").option("--component <component>", "Component name (multi only)").option("--id <id>", "Feature ID (default: auto)").option("-d, --desc <description>", "Feature description for spec.md").option("--non-interactive", "Fail instead of prompting for input").option("--json", "Output in JSON format for agents").action(async (name, options) => {
|
|
1862
1874
|
try {
|
|
1863
|
-
await runFeature(name, options);
|
|
1875
|
+
const result = await runFeature(name, options);
|
|
1876
|
+
if (options.json) {
|
|
1877
|
+
console.log(
|
|
1878
|
+
JSON.stringify(
|
|
1879
|
+
{
|
|
1880
|
+
status: "ok",
|
|
1881
|
+
reasonCode: "FEATURE_CREATED",
|
|
1882
|
+
featureId: result.featureId,
|
|
1883
|
+
featureName: result.featureName,
|
|
1884
|
+
component: result.component,
|
|
1885
|
+
featurePath: result.featurePath,
|
|
1886
|
+
featurePathFromDocs: result.featurePathFromDocs
|
|
1887
|
+
},
|
|
1888
|
+
null,
|
|
1889
|
+
2
|
|
1890
|
+
)
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1864
1893
|
} catch (error) {
|
|
1865
1894
|
if (error instanceof Error && error.message === "canceled") {
|
|
1866
1895
|
const config2 = await getConfig(process.cwd());
|
|
1867
1896
|
const lang2 = config2?.lang ?? DEFAULT_LANG;
|
|
1897
|
+
if (options.json) {
|
|
1898
|
+
console.log(
|
|
1899
|
+
JSON.stringify({
|
|
1900
|
+
status: "canceled",
|
|
1901
|
+
reasonCode: "CANCELED"
|
|
1902
|
+
})
|
|
1903
|
+
);
|
|
1904
|
+
process.exit(0);
|
|
1905
|
+
}
|
|
1868
1906
|
console.log(chalk6.yellow(`
|
|
1869
1907
|
${tr(lang2, "cli", "common.canceled")}`));
|
|
1870
1908
|
process.exit(0);
|
|
@@ -1873,6 +1911,17 @@ ${tr(lang2, "cli", "common.canceled")}`));
|
|
|
1873
1911
|
const lang = config?.lang ?? DEFAULT_LANG;
|
|
1874
1912
|
const cliError = toCliError(error);
|
|
1875
1913
|
const suggestions = getCliErrorSuggestions(cliError.code, lang);
|
|
1914
|
+
if (options.json) {
|
|
1915
|
+
console.log(
|
|
1916
|
+
JSON.stringify({
|
|
1917
|
+
status: "error",
|
|
1918
|
+
reasonCode: cliError.code,
|
|
1919
|
+
error: cliError.message,
|
|
1920
|
+
suggestions
|
|
1921
|
+
})
|
|
1922
|
+
);
|
|
1923
|
+
process.exit(1);
|
|
1924
|
+
}
|
|
1876
1925
|
console.error(
|
|
1877
1926
|
chalk6.red(tr(lang, "cli", "common.errorLabel")),
|
|
1878
1927
|
chalk6.red(`[${cliError.code}] ${cliError.message}`)
|
|
@@ -1944,7 +1993,7 @@ async function runFeature(name, options) {
|
|
|
1944
1993
|
assertAllowedComponent(component, configuredComponents);
|
|
1945
1994
|
}
|
|
1946
1995
|
const docsLockPath = getDocsLockPath(docsDir);
|
|
1947
|
-
|
|
1996
|
+
return withFileLock(
|
|
1948
1997
|
docsLockPath,
|
|
1949
1998
|
async () => {
|
|
1950
1999
|
let featureId;
|
|
@@ -1980,6 +2029,8 @@ async function runFeature(name, options) {
|
|
|
1980
2029
|
// ko placeholders
|
|
1981
2030
|
"{\uAE30\uB2A5\uBA85}": name,
|
|
1982
2031
|
"{\uBC88\uD638}": idNumber,
|
|
2032
|
+
"{\uACB0\uC815 \uC81C\uBAA9}": `${name} \uACB0\uC815`,
|
|
2033
|
+
"{YYYY-MM-DD}": getLocalDateString(),
|
|
1983
2034
|
"YYYY-MM-DD": getLocalDateString(),
|
|
1984
2035
|
"{be|fe}": component || "",
|
|
1985
2036
|
"{\uC774\uC288\uBC88\uD638}": "",
|
|
@@ -1987,6 +2038,7 @@ async function runFeature(name, options) {
|
|
|
1987
2038
|
// en placeholders
|
|
1988
2039
|
"{feature-name}": name,
|
|
1989
2040
|
"{number}": idNumber,
|
|
2041
|
+
"{Decision Title}": `${name} design decision`,
|
|
1990
2042
|
"{issue-number}": "",
|
|
1991
2043
|
"{{projectName}}-{be|fe}": repoName
|
|
1992
2044
|
};
|
|
@@ -2002,14 +2054,23 @@ async function runFeature(name, options) {
|
|
|
2002
2054
|
if (config.workflow?.mode === "local") {
|
|
2003
2055
|
await applyLocalWorkflowTemplateToFeatureDir(featureDir, lang);
|
|
2004
2056
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2057
|
+
if (!options.json) {
|
|
2058
|
+
console.log();
|
|
2059
|
+
console.log(chalk6.green(tr(lang, "cli", "feature.created", { path: featureDir })));
|
|
2060
|
+
console.log();
|
|
2061
|
+
console.log(chalk6.blue(tr(lang, "cli", "feature.nextStepsTitle")));
|
|
2062
|
+
console.log(chalk6.gray(tr(lang, "cli", "feature.nextSteps1", { path: featureDir })));
|
|
2063
|
+
console.log(chalk6.gray(tr(lang, "cli", "feature.nextSteps2")));
|
|
2064
|
+
console.log(chalk6.gray(tr(lang, "cli", "feature.nextSteps3")));
|
|
2065
|
+
console.log();
|
|
2066
|
+
}
|
|
2067
|
+
return {
|
|
2068
|
+
featureId,
|
|
2069
|
+
featureName: name,
|
|
2070
|
+
component: projectType === "multi" ? component : void 0,
|
|
2071
|
+
featurePath: featureDir,
|
|
2072
|
+
featurePathFromDocs: path13.relative(docsDir, featureDir)
|
|
2073
|
+
};
|
|
2013
2074
|
},
|
|
2014
2075
|
{ owner: "feature" }
|
|
2015
2076
|
);
|
|
@@ -2112,6 +2173,15 @@ function resolveWorkflowPolicy(workflow) {
|
|
|
2112
2173
|
}
|
|
2113
2174
|
return policy;
|
|
2114
2175
|
}
|
|
2176
|
+
function resolveCodeDirtyScopePolicy(workflow, projectType) {
|
|
2177
|
+
const raw = workflow?.codeDirtyScope;
|
|
2178
|
+
if (!raw) return "repo";
|
|
2179
|
+
if (raw === "repo") return "repo";
|
|
2180
|
+
if (raw === "component") {
|
|
2181
|
+
return projectType === "multi" ? "component" : "repo";
|
|
2182
|
+
}
|
|
2183
|
+
return projectType === "multi" ? "component" : "repo";
|
|
2184
|
+
}
|
|
2115
2185
|
|
|
2116
2186
|
// src/utils/context/steps.ts
|
|
2117
2187
|
function isCompletionChecklistDone(feature) {
|
|
@@ -2127,7 +2197,7 @@ function isPrMetadataConfigured(feature) {
|
|
|
2127
2197
|
return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
|
|
2128
2198
|
}
|
|
2129
2199
|
function isFeatureDone(feature, workflowPolicy) {
|
|
2130
|
-
return feature.specStatus === "Approved" && feature.planStatus === "Approved" && feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isTasksDocApproved(feature) && (!workflowPolicy.requireIssue || !!feature.issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured(feature) && !!feature.pr.link) && (!workflowPolicy.requireReview || feature.pr.status === "Approved");
|
|
2200
|
+
return feature.specStatus === "Approved" && feature.planStatus === "Approved" && !feature.git.docsHasUncommittedChanges && !feature.git.projectHasUncommittedChanges && feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isTasksDocApproved(feature) && (!workflowPolicy.requireIssue || !!feature.issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured(feature) && !!feature.pr.link) && (!workflowPolicy.requireReview || feature.pr.status === "Approved");
|
|
2131
2201
|
}
|
|
2132
2202
|
function getStepDefinitions(lang, workflow) {
|
|
2133
2203
|
const workflowPolicy = resolveWorkflowPolicy(workflow);
|
|
@@ -2460,29 +2530,59 @@ function getStepDefinitions(lang, workflow) {
|
|
|
2460
2530
|
step: 11,
|
|
2461
2531
|
name: tr(lang, "steps", "docsCommitSync"),
|
|
2462
2532
|
checklist: {
|
|
2463
|
-
done: (f) => !f.git.docsHasUncommittedChanges
|
|
2533
|
+
done: (f) => !f.git.docsHasUncommittedChanges && !f.git.projectHasUncommittedChanges
|
|
2464
2534
|
},
|
|
2465
2535
|
current: {
|
|
2466
|
-
when: (f) => isImplementationDone(f) && f.git.docsHasUncommittedChanges,
|
|
2467
|
-
actions: (f) =>
|
|
2468
|
-
{
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2536
|
+
when: (f) => isImplementationDone(f) && (f.git.docsHasUncommittedChanges || f.git.projectHasUncommittedChanges),
|
|
2537
|
+
actions: (f) => {
|
|
2538
|
+
if (f.git.docsHasUncommittedChanges) {
|
|
2539
|
+
return [
|
|
2540
|
+
{
|
|
2541
|
+
type: "command",
|
|
2542
|
+
category: "docs_commit",
|
|
2543
|
+
requiresUserCheck: true,
|
|
2544
|
+
scope: "docs",
|
|
2545
|
+
cwd: f.git.docsGitCwd,
|
|
2546
|
+
cmd: f.issueNumber ? tr(lang, "messages", "docsCommitIssueUpdate", {
|
|
2547
|
+
docsGitCwd: f.git.docsGitCwd,
|
|
2548
|
+
featurePath: f.docs.featurePathFromDocs,
|
|
2549
|
+
issueNumber: f.issueNumber,
|
|
2550
|
+
folderName: f.folderName
|
|
2551
|
+
}) : tr(lang, "messages", "docsCommitUpdate", {
|
|
2552
|
+
docsGitCwd: f.git.docsGitCwd,
|
|
2553
|
+
featurePath: f.docs.featurePathFromDocs,
|
|
2554
|
+
folderName: f.folderName
|
|
2555
|
+
})
|
|
2556
|
+
}
|
|
2557
|
+
];
|
|
2484
2558
|
}
|
|
2485
|
-
|
|
2559
|
+
if (!f.git.projectGitCwd) {
|
|
2560
|
+
return [
|
|
2561
|
+
{
|
|
2562
|
+
type: "instruction",
|
|
2563
|
+
category: "task_execute",
|
|
2564
|
+
message: tr(lang, "messages", "standaloneNeedsProjectRoot")
|
|
2565
|
+
}
|
|
2566
|
+
];
|
|
2567
|
+
}
|
|
2568
|
+
return [
|
|
2569
|
+
{
|
|
2570
|
+
type: "command",
|
|
2571
|
+
category: "task_execute",
|
|
2572
|
+
requiresUserCheck: true,
|
|
2573
|
+
scope: "project",
|
|
2574
|
+
cwd: f.git.projectGitCwd,
|
|
2575
|
+
cmd: f.issueNumber ? tr(lang, "messages", "projectCommitIssueUpdate", {
|
|
2576
|
+
projectGitCwd: f.git.projectGitCwd,
|
|
2577
|
+
issueNumber: f.issueNumber,
|
|
2578
|
+
folderName: f.folderName
|
|
2579
|
+
}) : tr(lang, "messages", "projectCommitUpdate", {
|
|
2580
|
+
projectGitCwd: f.git.projectGitCwd,
|
|
2581
|
+
folderName: f.folderName
|
|
2582
|
+
})
|
|
2583
|
+
}
|
|
2584
|
+
];
|
|
2585
|
+
}
|
|
2486
2586
|
}
|
|
2487
2587
|
},
|
|
2488
2588
|
{
|
|
@@ -2805,6 +2905,60 @@ function parsePrLink(value) {
|
|
|
2805
2905
|
if (trimmed.includes("{") || trimmed.includes("}")) return void 0;
|
|
2806
2906
|
return trimmed;
|
|
2807
2907
|
}
|
|
2908
|
+
function normalizeGitPath(value) {
|
|
2909
|
+
return value.split(path13.sep).join("/");
|
|
2910
|
+
}
|
|
2911
|
+
function resolveProjectStatusPaths(projectGitCwd, docsDir) {
|
|
2912
|
+
const relativeDocsDir = path13.relative(projectGitCwd, docsDir);
|
|
2913
|
+
if (!relativeDocsDir) return [];
|
|
2914
|
+
if (path13.isAbsolute(relativeDocsDir)) return [];
|
|
2915
|
+
if (relativeDocsDir === ".." || relativeDocsDir.startsWith(`..${path13.sep}`)) {
|
|
2916
|
+
return [];
|
|
2917
|
+
}
|
|
2918
|
+
const normalizedDocsDir = normalizeGitPath(relativeDocsDir).replace(/\/+$/, "");
|
|
2919
|
+
if (!normalizedDocsDir) return [];
|
|
2920
|
+
return [".", `:(exclude)${normalizedDocsDir}/**`];
|
|
2921
|
+
}
|
|
2922
|
+
function uniqueNormalizedPaths(values) {
|
|
2923
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2924
|
+
const out = [];
|
|
2925
|
+
for (const value of values) {
|
|
2926
|
+
const normalized = normalizeGitPath(value).replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
2927
|
+
if (!normalized || normalized === "." || normalized === "..") continue;
|
|
2928
|
+
if (seen.has(normalized)) continue;
|
|
2929
|
+
seen.add(normalized);
|
|
2930
|
+
out.push(normalized);
|
|
2931
|
+
}
|
|
2932
|
+
return out;
|
|
2933
|
+
}
|
|
2934
|
+
async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
|
|
2935
|
+
const configured = workflow?.componentPaths?.[component];
|
|
2936
|
+
const configuredCandidates = Array.isArray(configured) ? configured.map((value) => String(value).trim()).filter(Boolean) : [];
|
|
2937
|
+
const candidates = configuredCandidates.length > 0 ? configuredCandidates : [
|
|
2938
|
+
component,
|
|
2939
|
+
`apps/${component}`,
|
|
2940
|
+
`packages/${component}`,
|
|
2941
|
+
`services/${component}`,
|
|
2942
|
+
`modules/${component}`
|
|
2943
|
+
];
|
|
2944
|
+
const normalizedCandidates = uniqueNormalizedPaths(
|
|
2945
|
+
candidates.map((candidate) => {
|
|
2946
|
+
if (!candidate) return "";
|
|
2947
|
+
if (!path13.isAbsolute(candidate)) return candidate;
|
|
2948
|
+
const relative = path13.relative(projectGitCwd, candidate);
|
|
2949
|
+
if (!relative) return "";
|
|
2950
|
+
if (relative === ".." || relative.startsWith(`..${path13.sep}`)) return "";
|
|
2951
|
+
return relative;
|
|
2952
|
+
}).filter(Boolean)
|
|
2953
|
+
);
|
|
2954
|
+
const existing = [];
|
|
2955
|
+
for (const candidate of normalizedCandidates) {
|
|
2956
|
+
if (await fs2.pathExists(path13.join(projectGitCwd, candidate))) {
|
|
2957
|
+
existing.push(candidate);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
return existing;
|
|
2961
|
+
}
|
|
2808
2962
|
function parseTasks(content) {
|
|
2809
2963
|
const summary = { total: 0, todo: 0, doing: 0, done: 0 };
|
|
2810
2964
|
let activeTask;
|
|
@@ -2937,6 +3091,25 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
2937
3091
|
);
|
|
2938
3092
|
const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [relativeFeaturePathFromDocs]);
|
|
2939
3093
|
const docsHasUncommittedChanges = docsStatus === void 0 ? true : docsStatus.trim().length > 0;
|
|
3094
|
+
const dirtyScopePolicy = resolveCodeDirtyScopePolicy(options.workflow, options.projectType);
|
|
3095
|
+
let projectStatusPaths = [];
|
|
3096
|
+
if (context.projectGitCwd) {
|
|
3097
|
+
if (dirtyScopePolicy === "component" && type !== "single") {
|
|
3098
|
+
const componentStatusPaths = await resolveComponentStatusPaths(
|
|
3099
|
+
context.projectGitCwd,
|
|
3100
|
+
type,
|
|
3101
|
+
options.workflow
|
|
3102
|
+
);
|
|
3103
|
+
projectStatusPaths = componentStatusPaths.length > 0 ? componentStatusPaths : resolveProjectStatusPaths(context.projectGitCwd, context.docsDir);
|
|
3104
|
+
} else {
|
|
3105
|
+
projectStatusPaths = resolveProjectStatusPaths(
|
|
3106
|
+
context.projectGitCwd,
|
|
3107
|
+
context.docsDir
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
const projectStatus = context.projectGitCwd ? getGitStatusPorcelain(context.projectGitCwd, projectStatusPaths) : void 0;
|
|
3112
|
+
const projectHasUncommittedChanges = projectStatus === void 0 ? false : projectStatus.trim().length > 0;
|
|
2940
3113
|
const docsLastCommit = getLastCommitForPath(
|
|
2941
3114
|
context.docsGitCwd,
|
|
2942
3115
|
relativeFeaturePathFromDocs
|
|
@@ -2961,9 +3134,12 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
2961
3134
|
if (docsEverCommitted && docsHasUncommittedChanges) {
|
|
2962
3135
|
warnings.push(tr(lang, "warnings", "docsUncommittedChanges"));
|
|
2963
3136
|
}
|
|
3137
|
+
if (projectHasUncommittedChanges) {
|
|
3138
|
+
warnings.push(tr(lang, "warnings", "projectUncommittedChanges"));
|
|
3139
|
+
}
|
|
2964
3140
|
const tasksDocApproved = !tasksDocStatusFieldExists || tasksDocStatus === "Approved";
|
|
2965
3141
|
const implementationDone = tasksExists && tasksSummary.total > 0 && tasksSummary.total === tasksSummary.done && isCompletionChecklistDone2({ completionChecklist }) && tasksDocApproved;
|
|
2966
|
-
const workflowDone = implementationDone && specStatus === "Approved" && planStatus === "Approved" && (!workflowPolicy.requireIssue || !!issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured2({ docs: { prFieldExists, prStatusFieldExists } }) && !!prLink) && (!workflowPolicy.requireReview || prStatus === "Approved");
|
|
3142
|
+
const workflowDone = implementationDone && !docsHasUncommittedChanges && !projectHasUncommittedChanges && specStatus === "Approved" && planStatus === "Approved" && (!workflowPolicy.requireIssue || !!issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured2({ docs: { prFieldExists, prStatusFieldExists } }) && !!prLink) && (!workflowPolicy.requireReview || prStatus === "Approved");
|
|
2967
3143
|
if (implementationDone && !workflowDone) {
|
|
2968
3144
|
if (specStatus !== "Approved") {
|
|
2969
3145
|
warnings.push(tr(lang, "warnings", "workflowSpecNotApproved"));
|
|
@@ -2974,6 +3150,9 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
2974
3150
|
if (workflowPolicy.requireIssue && !issueNumber) {
|
|
2975
3151
|
warnings.push(tr(lang, "warnings", "workflowIssueMissing"));
|
|
2976
3152
|
}
|
|
3153
|
+
if (projectHasUncommittedChanges) {
|
|
3154
|
+
warnings.push(tr(lang, "warnings", "workflowProjectUncommittedChanges"));
|
|
3155
|
+
}
|
|
2977
3156
|
if (workflowPolicy.requirePr && prFieldExists && prStatusFieldExists) {
|
|
2978
3157
|
if (!prLink) warnings.push(tr(lang, "warnings", "workflowPrLinkMissing"));
|
|
2979
3158
|
if (workflowPolicy.requireReview) {
|
|
@@ -3012,6 +3191,7 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3012
3191
|
onExpectedBranch,
|
|
3013
3192
|
docsEverCommitted,
|
|
3014
3193
|
docsHasUncommittedChanges,
|
|
3194
|
+
projectHasUncommittedChanges,
|
|
3015
3195
|
docsPathIgnored: docsPathIgnored === true
|
|
3016
3196
|
},
|
|
3017
3197
|
docs: {
|
|
@@ -3075,7 +3255,8 @@ async function scanFeatures(config) {
|
|
|
3075
3255
|
lang: config.lang,
|
|
3076
3256
|
stepDefinitions,
|
|
3077
3257
|
approval: config.approval,
|
|
3078
|
-
workflow: config.workflow
|
|
3258
|
+
workflow: config.workflow,
|
|
3259
|
+
projectType: config.projectType
|
|
3079
3260
|
}
|
|
3080
3261
|
)
|
|
3081
3262
|
);
|
|
@@ -3107,7 +3288,8 @@ async function scanFeatures(config) {
|
|
|
3107
3288
|
lang: config.lang,
|
|
3108
3289
|
stepDefinitions,
|
|
3109
3290
|
approval: config.approval,
|
|
3110
|
-
workflow: config.workflow
|
|
3291
|
+
workflow: config.workflow,
|
|
3292
|
+
projectType: config.projectType
|
|
3111
3293
|
}
|
|
3112
3294
|
)
|
|
3113
3295
|
);
|
|
@@ -3326,7 +3508,8 @@ async function runUpdate(options) {
|
|
|
3326
3508
|
async () => {
|
|
3327
3509
|
const templatesDir = getTemplatesDir();
|
|
3328
3510
|
const sourceDir = path13.join(templatesDir, lang, toTemplateProjectType(projectType));
|
|
3329
|
-
const
|
|
3511
|
+
const docsLockPath = getDocsLockPath(docsDir);
|
|
3512
|
+
const forceOverwrite = !!options.force || await isDocsWorktreeCleanOrThrow(docsDir, lang, [docsLockPath]);
|
|
3330
3513
|
const hasExplicitSelection = !!(options.agents || options.skills || options.templates);
|
|
3331
3514
|
const updateAgents = options.agents || options.skills || !hasExplicitSelection;
|
|
3332
3515
|
const updateTemplates = options.templates || !hasExplicitSelection;
|
|
@@ -3483,22 +3666,56 @@ function getGitTopLevel2(cwd) {
|
|
|
3483
3666
|
return null;
|
|
3484
3667
|
}
|
|
3485
3668
|
}
|
|
3486
|
-
function
|
|
3669
|
+
function normalizeGitPath2(input) {
|
|
3670
|
+
return input.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
3671
|
+
}
|
|
3672
|
+
function stripOuterQuotes(input) {
|
|
3673
|
+
const trimmed = input.trim();
|
|
3674
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
|
|
3675
|
+
return trimmed.slice(1, -1).replace(/\\"/g, '"');
|
|
3676
|
+
}
|
|
3677
|
+
return trimmed;
|
|
3678
|
+
}
|
|
3679
|
+
function extractPorcelainPaths(line) {
|
|
3680
|
+
if (line.length < 4) return [];
|
|
3681
|
+
const body = line.slice(3).trim();
|
|
3682
|
+
if (!body) return [];
|
|
3683
|
+
if (body.includes(" -> ")) {
|
|
3684
|
+
return body.split(" -> ").map((part) => normalizeGitPath2(stripOuterQuotes(part)));
|
|
3685
|
+
}
|
|
3686
|
+
return [normalizeGitPath2(stripOuterQuotes(body))];
|
|
3687
|
+
}
|
|
3688
|
+
function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
|
|
3487
3689
|
const top = getGitTopLevel2(docsDir);
|
|
3488
3690
|
if (!top) return null;
|
|
3489
3691
|
const rel = path13.relative(top, docsDir) || ".";
|
|
3490
3692
|
try {
|
|
3491
|
-
|
|
3693
|
+
const output = execFileSync("git", ["status", "--porcelain=v1", "--", rel], {
|
|
3492
3694
|
cwd: top,
|
|
3493
3695
|
encoding: "utf-8",
|
|
3494
3696
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3495
3697
|
});
|
|
3698
|
+
if (ignoredAbsPaths.length === 0) {
|
|
3699
|
+
return output;
|
|
3700
|
+
}
|
|
3701
|
+
const ignoredRelPaths = new Set(
|
|
3702
|
+
ignoredAbsPaths.map(
|
|
3703
|
+
(absPath) => normalizeGitPath2(path13.relative(top, absPath) || ".")
|
|
3704
|
+
)
|
|
3705
|
+
);
|
|
3706
|
+
const filtered = output.split("\n").filter((line) => {
|
|
3707
|
+
if (!line.trim()) return false;
|
|
3708
|
+
const touchedPaths = extractPorcelainPaths(line);
|
|
3709
|
+
if (touchedPaths.length === 0) return true;
|
|
3710
|
+
return touchedPaths.some((p) => !ignoredRelPaths.has(p));
|
|
3711
|
+
}).join("\n");
|
|
3712
|
+
return filtered;
|
|
3496
3713
|
} catch {
|
|
3497
3714
|
return null;
|
|
3498
3715
|
}
|
|
3499
3716
|
}
|
|
3500
|
-
async function isDocsWorktreeCleanOrThrow(docsDir, lang) {
|
|
3501
|
-
const status = getDocsPorcelainStatus(docsDir);
|
|
3717
|
+
async function isDocsWorktreeCleanOrThrow(docsDir, lang, ignoredAbsPaths = []) {
|
|
3718
|
+
const status = getDocsPorcelainStatus(docsDir, ignoredAbsPaths);
|
|
3502
3719
|
if (status === null) {
|
|
3503
3720
|
throw createCliError(
|
|
3504
3721
|
"PRECONDITION_FAILED",
|
|
@@ -3652,6 +3869,22 @@ async function runConfig(options) {
|
|
|
3652
3869
|
{ owner: "config" }
|
|
3653
3870
|
);
|
|
3654
3871
|
}
|
|
3872
|
+
var REMOTE_ACTION_CATEGORIES = /* @__PURE__ */ new Set([
|
|
3873
|
+
"issue_create",
|
|
3874
|
+
"pr_create",
|
|
3875
|
+
"pr_status_update",
|
|
3876
|
+
"code_review"
|
|
3877
|
+
]);
|
|
3878
|
+
var LOCAL_ACTION_CATEGORIES = /* @__PURE__ */ new Set([
|
|
3879
|
+
"docs_commit",
|
|
3880
|
+
"branch_create",
|
|
3881
|
+
"task_execute"
|
|
3882
|
+
]);
|
|
3883
|
+
var REMOTE_COMMAND_PATTERN = /\b(?:git\s+push|git\s+merge|gh\s+(?:issue|pr)\b)/i;
|
|
3884
|
+
function resolveComponentOption(options) {
|
|
3885
|
+
const component = (options.component || options.repo || "").trim().toLowerCase();
|
|
3886
|
+
return component || void 0;
|
|
3887
|
+
}
|
|
3655
3888
|
function getActionLabel(index) {
|
|
3656
3889
|
let n = index + 1;
|
|
3657
3890
|
let label = "";
|
|
@@ -3662,11 +3895,67 @@ function getActionLabel(index) {
|
|
|
3662
3895
|
}
|
|
3663
3896
|
return label;
|
|
3664
3897
|
}
|
|
3898
|
+
function resolveActionOperationType(action) {
|
|
3899
|
+
if (action.operationType) return action.operationType;
|
|
3900
|
+
if (action.type === "command") {
|
|
3901
|
+
if (REMOTE_COMMAND_PATTERN.test(action.cmd)) return "remote";
|
|
3902
|
+
return "local";
|
|
3903
|
+
}
|
|
3904
|
+
if (action.category && REMOTE_ACTION_CATEGORIES.has(action.category)) {
|
|
3905
|
+
return "remote";
|
|
3906
|
+
}
|
|
3907
|
+
if (action.category && LOCAL_ACTION_CATEGORIES.has(action.category)) {
|
|
3908
|
+
return "local";
|
|
3909
|
+
}
|
|
3910
|
+
return "manual";
|
|
3911
|
+
}
|
|
3912
|
+
function annotateActionOperationType(action) {
|
|
3913
|
+
return {
|
|
3914
|
+
...action,
|
|
3915
|
+
operationType: resolveActionOperationType(action)
|
|
3916
|
+
};
|
|
3917
|
+
}
|
|
3918
|
+
function annotateActions(actions) {
|
|
3919
|
+
return actions.map((action) => annotateActionOperationType(action));
|
|
3920
|
+
}
|
|
3921
|
+
function getActionSummary(action) {
|
|
3922
|
+
if (action.category === "docs_commit") return "Commit docs updates";
|
|
3923
|
+
if (action.category === "issue_create") return "Create and record issue";
|
|
3924
|
+
if (action.category === "branch_create") return "Create feature branch";
|
|
3925
|
+
if (action.category === "pr_create") return "Create PR and record link";
|
|
3926
|
+
if (action.category === "pr_status_update") return "Update PR status";
|
|
3927
|
+
if (action.category === "code_review") return "Process code review feedback";
|
|
3928
|
+
if (action.category === "task_execute") return "Proceed with task execution";
|
|
3929
|
+
if (action.category === "feature_done") return "Feature is complete";
|
|
3930
|
+
if (action.category === "spec_approve") return "Request spec approval";
|
|
3931
|
+
if (action.category === "plan_approve") return "Request plan approval";
|
|
3932
|
+
if (action.category === "tasks_approve") return "Request tasks approval";
|
|
3933
|
+
if (action.category === "pr_metadata_migrate") return "Update tasks.md to latest PR fields";
|
|
3934
|
+
if (action.category === "fallback") return "Re-check context and rerun";
|
|
3935
|
+
if (action.type === "command") {
|
|
3936
|
+
return action.scope === "docs" ? "Run docs command" : "Run project command";
|
|
3937
|
+
}
|
|
3938
|
+
return action.message;
|
|
3939
|
+
}
|
|
3940
|
+
function formatActionSummary(action) {
|
|
3941
|
+
if (action.type === "command") {
|
|
3942
|
+
return `(${action.scope}) ${action.cmd}`;
|
|
3943
|
+
}
|
|
3944
|
+
return action.message;
|
|
3945
|
+
}
|
|
3665
3946
|
function toActionOptions(actions) {
|
|
3666
|
-
return actions.map((action, index) =>
|
|
3667
|
-
label
|
|
3668
|
-
action
|
|
3669
|
-
|
|
3947
|
+
return actions.map((action, index) => {
|
|
3948
|
+
const label = getActionLabel(index);
|
|
3949
|
+
const summary = getActionSummary(action);
|
|
3950
|
+
const detail = formatActionSummary(action);
|
|
3951
|
+
return {
|
|
3952
|
+
label,
|
|
3953
|
+
summary,
|
|
3954
|
+
detail,
|
|
3955
|
+
approvalPrompt: `${label}: ${summary}`,
|
|
3956
|
+
action
|
|
3957
|
+
};
|
|
3958
|
+
});
|
|
3670
3959
|
}
|
|
3671
3960
|
function buildActionSnapshot(actionOptions) {
|
|
3672
3961
|
return actionOptions.map(({ label, action }) => {
|
|
@@ -3678,6 +3967,7 @@ function buildActionSnapshot(actionOptions) {
|
|
|
3678
3967
|
cwd: action.cwd,
|
|
3679
3968
|
cmd: action.cmd,
|
|
3680
3969
|
category: action.category,
|
|
3970
|
+
operationType: action.operationType,
|
|
3681
3971
|
requiresUserCheck: !!action.requiresUserCheck
|
|
3682
3972
|
};
|
|
3683
3973
|
}
|
|
@@ -3686,6 +3976,7 @@ function buildActionSnapshot(actionOptions) {
|
|
|
3686
3976
|
type: action.type,
|
|
3687
3977
|
message: action.message,
|
|
3688
3978
|
category: action.category,
|
|
3979
|
+
operationType: action.operationType,
|
|
3689
3980
|
requiresUserCheck: !!action.requiresUserCheck
|
|
3690
3981
|
};
|
|
3691
3982
|
});
|
|
@@ -3700,20 +3991,21 @@ function getContextVersion(feature, actionOptions) {
|
|
|
3700
3991
|
});
|
|
3701
3992
|
return createHash("sha256").update(payload).digest("hex").slice(0, 12);
|
|
3702
3993
|
}
|
|
3703
|
-
function
|
|
3704
|
-
const
|
|
3705
|
-
if (!
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
return actionOptions.map((o) => o.label).join(", ");
|
|
3994
|
+
function matchesFeatureSelector(f, selector) {
|
|
3995
|
+
const s = selector.trim();
|
|
3996
|
+
if (!s) return false;
|
|
3997
|
+
if (f.folderName.toLowerCase() === s.toLowerCase()) return true;
|
|
3998
|
+
if (f.slug.toLowerCase() === s.toLowerCase()) return true;
|
|
3999
|
+
if (f.id && f.id.toLowerCase() === s.toLowerCase()) return true;
|
|
4000
|
+
return false;
|
|
3711
4001
|
}
|
|
3712
|
-
function
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
return
|
|
4002
|
+
function detectFromBranch(branchName, features) {
|
|
4003
|
+
const match = branchName.match(/^feat\/\d+-(.+)$/);
|
|
4004
|
+
if (!match) return [];
|
|
4005
|
+
const detected = match[1];
|
|
4006
|
+
return features.filter(
|
|
4007
|
+
(f) => f.slug.toLowerCase() === detected.toLowerCase() || f.folderName.toLowerCase() === detected.toLowerCase()
|
|
4008
|
+
);
|
|
3717
4009
|
}
|
|
3718
4010
|
function toSelectionStatus(features, selectionMode, openFeatures, targetFeatures) {
|
|
3719
4011
|
const isNoOpen = selectionMode === "open" && features.length > 0 && openFeatures.length === 0;
|
|
@@ -3730,15 +4022,9 @@ function toReasonCode(status) {
|
|
|
3730
4022
|
if (status === "multiple_active") return "MULTIPLE_ACTIVE_FEATURES";
|
|
3731
4023
|
return "NO_MATCHED_FEATURES";
|
|
3732
4024
|
}
|
|
3733
|
-
async function
|
|
3734
|
-
if (!config) {
|
|
3735
|
-
throw createCliError(
|
|
3736
|
-
"CONFIG_NOT_FOUND",
|
|
3737
|
-
tr(DEFAULT_LANG, "cli", "common.configNotFound")
|
|
3738
|
-
);
|
|
3739
|
-
}
|
|
4025
|
+
async function resolveContextSelection(config, featureName, options) {
|
|
3740
4026
|
const { features, branches, warnings } = await scanFeatures(config);
|
|
3741
|
-
const selectedComponent = (options
|
|
4027
|
+
const selectedComponent = resolveComponentOption(options);
|
|
3742
4028
|
const scopedFeatures = selectedComponent ? features.filter((f) => f.type === selectedComponent) : features;
|
|
3743
4029
|
const doneFeatures = scopedFeatures.filter((f) => f.completion.workflowDone);
|
|
3744
4030
|
const openFeatures = scopedFeatures.filter((f) => !f.completion.workflowDone);
|
|
@@ -3750,6 +4036,7 @@ async function resolveContextState(config, featureName, options) {
|
|
|
3750
4036
|
);
|
|
3751
4037
|
let targetFeatures = [];
|
|
3752
4038
|
let selectionMode = "explicit";
|
|
4039
|
+
let selectionFallback = "none";
|
|
3753
4040
|
if (featureName) {
|
|
3754
4041
|
targetFeatures = scopedFeatures.filter(
|
|
3755
4042
|
(f) => matchesFeatureSelector(f, featureName)
|
|
@@ -3782,15 +4069,19 @@ async function resolveContextState(config, featureName, options) {
|
|
|
3782
4069
|
}
|
|
3783
4070
|
if (targetFeatures.length > 0) {
|
|
3784
4071
|
selectionMode = "branch";
|
|
4072
|
+
selectionFallback = "none";
|
|
3785
4073
|
} else if (options.all) {
|
|
3786
4074
|
targetFeatures = scopedFeatures;
|
|
3787
4075
|
selectionMode = "all";
|
|
4076
|
+
selectionFallback = "all_features";
|
|
3788
4077
|
} else if (options.done) {
|
|
3789
4078
|
targetFeatures = doneFeatures;
|
|
3790
4079
|
selectionMode = "done";
|
|
4080
|
+
selectionFallback = "done_features";
|
|
3791
4081
|
} else {
|
|
3792
4082
|
targetFeatures = openFeatures;
|
|
3793
4083
|
selectionMode = "open";
|
|
4084
|
+
selectionFallback = "open_features";
|
|
3794
4085
|
}
|
|
3795
4086
|
}
|
|
3796
4087
|
const status = toSelectionStatus(
|
|
@@ -3800,7 +4091,7 @@ async function resolveContextState(config, featureName, options) {
|
|
|
3800
4091
|
targetFeatures
|
|
3801
4092
|
);
|
|
3802
4093
|
const matchedFeature = targetFeatures.length === 1 ? targetFeatures[0] : null;
|
|
3803
|
-
const actions = matchedFeature?.actions ?? [];
|
|
4094
|
+
const actions = annotateActions(matchedFeature?.actions ?? []);
|
|
3804
4095
|
const actionOptions = toActionOptions(actions);
|
|
3805
4096
|
const contextVersion = getContextVersion(matchedFeature, actionOptions);
|
|
3806
4097
|
return {
|
|
@@ -3812,6 +4103,7 @@ async function resolveContextState(config, featureName, options) {
|
|
|
3812
4103
|
inProgressFeatures,
|
|
3813
4104
|
readyToCloseFeatures,
|
|
3814
4105
|
selectionMode,
|
|
4106
|
+
selectionFallback,
|
|
3815
4107
|
targetFeatures,
|
|
3816
4108
|
status,
|
|
3817
4109
|
matchedFeature,
|
|
@@ -3820,6 +4112,32 @@ async function resolveContextState(config, featureName, options) {
|
|
|
3820
4112
|
contextVersion
|
|
3821
4113
|
};
|
|
3822
4114
|
}
|
|
4115
|
+
|
|
4116
|
+
// src/commands/context.ts
|
|
4117
|
+
async function resolveContextState(config, featureName, options) {
|
|
4118
|
+
if (!config) {
|
|
4119
|
+
throw createCliError(
|
|
4120
|
+
"CONFIG_NOT_FOUND",
|
|
4121
|
+
tr(DEFAULT_LANG, "cli", "common.configNotFound")
|
|
4122
|
+
);
|
|
4123
|
+
}
|
|
4124
|
+
return resolveContextSelection(config, featureName, options);
|
|
4125
|
+
}
|
|
4126
|
+
function parseApprovalLabel(input) {
|
|
4127
|
+
const match = input.trim().match(/^([A-Z]+)(?:\s+OK)?$/i);
|
|
4128
|
+
if (!match) return null;
|
|
4129
|
+
return match[1].toUpperCase();
|
|
4130
|
+
}
|
|
4131
|
+
function listLabels(actionOptions) {
|
|
4132
|
+
if (actionOptions.length === 0) return "-";
|
|
4133
|
+
return actionOptions.map((o) => o.label).join(", ");
|
|
4134
|
+
}
|
|
4135
|
+
function formatActionSummary2(action) {
|
|
4136
|
+
if (action.type === "command") {
|
|
4137
|
+
return `(${action.scope}) ${action.cmd}`;
|
|
4138
|
+
}
|
|
4139
|
+
return action.message;
|
|
4140
|
+
}
|
|
3823
4141
|
function executeCommandAction(cmd, jsonMode, cwd) {
|
|
3824
4142
|
const shellPath = process.env.SHELL || (process.platform === "win32" ? process.env.ComSpec || "cmd.exe" : "/bin/sh");
|
|
3825
4143
|
if (jsonMode) {
|
|
@@ -3878,27 +4196,14 @@ function contextCommand(program2) {
|
|
|
3878
4196
|
}
|
|
3879
4197
|
);
|
|
3880
4198
|
}
|
|
3881
|
-
function matchesFeatureSelector(f, selector) {
|
|
3882
|
-
const s = selector.trim();
|
|
3883
|
-
if (!s) return false;
|
|
3884
|
-
if (f.folderName.toLowerCase() === s.toLowerCase()) return true;
|
|
3885
|
-
if (f.slug.toLowerCase() === s.toLowerCase()) return true;
|
|
3886
|
-
if (f.id && f.id.toLowerCase() === s.toLowerCase()) return true;
|
|
3887
|
-
return false;
|
|
3888
|
-
}
|
|
3889
|
-
function detectFromBranch(branchName, features) {
|
|
3890
|
-
const match = branchName.match(/^feat\/\d+-(.+)$/);
|
|
3891
|
-
if (!match) return [];
|
|
3892
|
-
const detected = match[1];
|
|
3893
|
-
return features.filter(
|
|
3894
|
-
(f) => f.slug.toLowerCase() === detected.toLowerCase() || f.folderName.toLowerCase() === detected.toLowerCase()
|
|
3895
|
-
);
|
|
3896
|
-
}
|
|
3897
4199
|
function getListLabel(f, stepsMap, lang, workflowPolicy) {
|
|
3898
4200
|
if (f.completion.implementationDone && !f.completion.workflowDone) {
|
|
3899
4201
|
if (f.git.docsHasUncommittedChanges) {
|
|
3900
4202
|
return tr(lang, "cli", "context.list.docsCommitNeeded");
|
|
3901
4203
|
}
|
|
4204
|
+
if (f.git.projectHasUncommittedChanges) {
|
|
4205
|
+
return tr(lang, "cli", "context.list.projectCommitNeeded");
|
|
4206
|
+
}
|
|
3902
4207
|
if (workflowPolicy.requireIssue && !f.issueNumber) {
|
|
3903
4208
|
return tr(lang, "cli", "context.list.issueNumberNeeded");
|
|
3904
4209
|
}
|
|
@@ -3984,10 +4289,12 @@ async function runContext(featureName, options) {
|
|
|
3984
4289
|
return;
|
|
3985
4290
|
}
|
|
3986
4291
|
if (options.json) {
|
|
4292
|
+
const primaryAction = state.actionOptions[0] ?? null;
|
|
3987
4293
|
const result = {
|
|
3988
4294
|
status: state.status,
|
|
3989
4295
|
reasonCode: toReasonCode(state.status),
|
|
3990
4296
|
selectionMode: state.selectionMode,
|
|
4297
|
+
selectionFallback: state.selectionFallback,
|
|
3991
4298
|
branches: state.branches,
|
|
3992
4299
|
warnings: state.warnings,
|
|
3993
4300
|
matchedFeature: state.matchedFeature,
|
|
@@ -3999,19 +4306,37 @@ async function runContext(featureName, options) {
|
|
|
3999
4306
|
readyToCloseCandidates: state.selectionMode === "open" ? state.readyToCloseFeatures : [],
|
|
4000
4307
|
actions: state.actions,
|
|
4001
4308
|
actionOptions: state.actionOptions,
|
|
4309
|
+
primaryActionLabel: primaryAction?.label ?? null,
|
|
4310
|
+
primaryActionType: primaryAction?.action.type ?? null,
|
|
4311
|
+
primaryActionCategory: primaryAction?.action.category ?? null,
|
|
4312
|
+
primaryActionOperationType: primaryAction?.action.operationType ?? null,
|
|
4002
4313
|
workflowPolicy,
|
|
4003
4314
|
checkPolicy: {
|
|
4004
4315
|
docPath: "/docs/agents/agents.md",
|
|
4005
4316
|
hint: tr(lang, "cli", "context.checkPolicyHint"),
|
|
4317
|
+
policyOnly: true,
|
|
4006
4318
|
token: "<LABEL>",
|
|
4007
4319
|
acceptedTokens: ["<LABEL>", "<LABEL> OK"],
|
|
4008
4320
|
tokenPattern: "^([A-Z]+)(?:\\s+OK)?$",
|
|
4009
4321
|
validLabels: state.actionOptions.map((o) => o.label),
|
|
4322
|
+
requireExplanationBeforeApproval: true,
|
|
4323
|
+
requiredExplanationFields: ["actionOptions[].summary", "actionOptions[].approvalPrompt"],
|
|
4324
|
+
recommendation: "Before asking for approval, explain each label with summary and then ask for `<LABEL>` or `<LABEL> OK`.",
|
|
4010
4325
|
oneApprovalPerAction: true,
|
|
4011
4326
|
requireFreshContext: true,
|
|
4012
4327
|
contextVersion: state.contextVersion,
|
|
4013
4328
|
config: config.approval ?? { mode: "builtin" }
|
|
4014
4329
|
},
|
|
4330
|
+
approvalRequest: {
|
|
4331
|
+
guidance: "Present each label with summary (e.g. `A: <summary>`) before asking for approval.",
|
|
4332
|
+
options: state.actionOptions.map((o) => ({
|
|
4333
|
+
label: o.label,
|
|
4334
|
+
summary: o.summary,
|
|
4335
|
+
approvalPrompt: o.approvalPrompt,
|
|
4336
|
+
requiresUserCheck: !!o.action.requiresUserCheck,
|
|
4337
|
+
operationType: o.action.operationType
|
|
4338
|
+
}))
|
|
4339
|
+
},
|
|
4015
4340
|
prPolicy: {
|
|
4016
4341
|
screenshots: {
|
|
4017
4342
|
upload: config.pr?.screenshots?.upload ?? false
|
|
@@ -4217,7 +4542,7 @@ async function runContext(featureName, options) {
|
|
|
4217
4542
|
console.log();
|
|
4218
4543
|
return;
|
|
4219
4544
|
}
|
|
4220
|
-
const actionOptions =
|
|
4545
|
+
const actionOptions = state.actionOptions;
|
|
4221
4546
|
console.log(chalk6.green(chalk6.bold("\u{1F449} Next Options (Atomic):")));
|
|
4222
4547
|
let hasDocsCommand = false;
|
|
4223
4548
|
actionOptions.forEach(({ label, action }) => {
|
|
@@ -4235,6 +4560,7 @@ async function runContext(featureName, options) {
|
|
|
4235
4560
|
}
|
|
4236
4561
|
if (hasCheckAction) {
|
|
4237
4562
|
console.log(chalk6.gray(` \u21B3 ${tr(lang, "cli", "context.actionOptionHint")}`));
|
|
4563
|
+
console.log(chalk6.gray(` \u21B3 ${tr(lang, "cli", "context.actionExplainHint")}`));
|
|
4238
4564
|
}
|
|
4239
4565
|
console.log();
|
|
4240
4566
|
}
|
|
@@ -4302,7 +4628,7 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
|
|
|
4302
4628
|
}
|
|
4303
4629
|
console.log();
|
|
4304
4630
|
console.log(chalk6.green(`\u2705 Approved option: ${parsedLabel}`));
|
|
4305
|
-
console.log(chalk6.gray(` - Action: ${
|
|
4631
|
+
console.log(chalk6.gray(` - Action: ${formatActionSummary2(selectedAction)}`));
|
|
4306
4632
|
if (selectedAction.type === "command") {
|
|
4307
4633
|
console.log(chalk6.gray(" - Run with: --execute"));
|
|
4308
4634
|
} else {
|
|
@@ -4559,8 +4885,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
4559
4885
|
const files = [
|
|
4560
4886
|
"spec.md",
|
|
4561
4887
|
"plan.md",
|
|
4562
|
-
"tasks.md"
|
|
4563
|
-
"decisions.md"
|
|
4888
|
+
"tasks.md"
|
|
4564
4889
|
];
|
|
4565
4890
|
for (const file of files) {
|
|
4566
4891
|
const fullPath = path13.join(f.path, file);
|
|
@@ -4629,7 +4954,7 @@ async function checkDocsStructure(config, cwd) {
|
|
|
4629
4954
|
}
|
|
4630
4955
|
return issues;
|
|
4631
4956
|
}
|
|
4632
|
-
async function checkFeatures(config, cwd, features) {
|
|
4957
|
+
async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
4633
4958
|
const issues = [];
|
|
4634
4959
|
if (features.length === 0) {
|
|
4635
4960
|
issues.push({
|
|
@@ -4647,7 +4972,7 @@ async function checkFeatures(config, cwd, features) {
|
|
|
4647
4972
|
idMap.get(id).push(rel);
|
|
4648
4973
|
const isInitialTemplateState = f.docs.specExists && f.docs.planExists && f.docs.tasksExists && !f.specStatus && !f.planStatus && f.tasks.total === 0 && (!f.docs.tasksDocStatusFieldExists || !f.tasksDocStatus || f.tasksDocStatus === "Draft");
|
|
4649
4974
|
if (!isInitialTemplateState) {
|
|
4650
|
-
const featureDocs = ["spec.md", "plan.md", "tasks.md"
|
|
4975
|
+
const featureDocs = ["spec.md", "plan.md", "tasks.md"];
|
|
4651
4976
|
for (const file of featureDocs) {
|
|
4652
4977
|
const p = path13.join(f.path, file);
|
|
4653
4978
|
if (!await fs2.pathExists(p)) continue;
|
|
@@ -4663,6 +4988,23 @@ async function checkFeatures(config, cwd, features) {
|
|
|
4663
4988
|
path: formatPath(cwd, p)
|
|
4664
4989
|
});
|
|
4665
4990
|
}
|
|
4991
|
+
if (decisionsPlaceholderMode !== "off") {
|
|
4992
|
+
const decisionsPath = path13.join(f.path, "decisions.md");
|
|
4993
|
+
if (await fs2.pathExists(decisionsPath)) {
|
|
4994
|
+
const content = await fs2.readFile(decisionsPath, "utf-8");
|
|
4995
|
+
const placeholders = detectPlaceholders(content);
|
|
4996
|
+
if (placeholders.length > 0) {
|
|
4997
|
+
issues.push({
|
|
4998
|
+
level: decisionsPlaceholderMode,
|
|
4999
|
+
code: "placeholder_left_decisions",
|
|
5000
|
+
message: tr(config.lang, "cli", "doctor.issue.placeholdersLeft", {
|
|
5001
|
+
placeholders: placeholders.join(", ")
|
|
5002
|
+
}),
|
|
5003
|
+
path: formatPath(cwd, decisionsPath)
|
|
5004
|
+
});
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
4666
5008
|
}
|
|
4667
5009
|
if (!f.docs.specExists) {
|
|
4668
5010
|
issues.push({
|
|
@@ -4741,7 +5083,11 @@ function hasFixableIssues(issues) {
|
|
|
4741
5083
|
return issues.some((issue) => FIXABLE_ISSUE_CODES.has(issue.code));
|
|
4742
5084
|
}
|
|
4743
5085
|
function doctorCommand(program2) {
|
|
4744
|
-
program2.command("doctor").description("Validate docs structure and feature metadata").option("--json", "Output in JSON format for agents").option("--fix", "Automatically apply safe fixes for common docs issues").option("--dry-run", "Show potential fixes without writing files (requires --fix)").option(
|
|
5086
|
+
program2.command("doctor").description("Validate docs structure and feature metadata").option("--json", "Output in JSON format for agents").option("--fix", "Automatically apply safe fixes for common docs issues").option("--dry-run", "Show potential fixes without writing files (requires --fix)").option(
|
|
5087
|
+
"--decisions-placeholders <mode>",
|
|
5088
|
+
"decisions.md placeholder severity: off | info | warn (default: info)",
|
|
5089
|
+
"info"
|
|
5090
|
+
).option("-s, --strict", "Exit with non-zero code when issues are found").action(async (options) => {
|
|
4745
5091
|
try {
|
|
4746
5092
|
const cwd = process.cwd();
|
|
4747
5093
|
const config = await getConfig(cwd);
|
|
@@ -4757,6 +5103,14 @@ function doctorCommand(program2) {
|
|
|
4757
5103
|
"`--dry-run` requires `--fix`."
|
|
4758
5104
|
);
|
|
4759
5105
|
}
|
|
5106
|
+
const rawDecisionsMode = (options.decisionsPlaceholders || "info").trim().toLowerCase();
|
|
5107
|
+
if (rawDecisionsMode !== "off" && rawDecisionsMode !== "info" && rawDecisionsMode !== "warn") {
|
|
5108
|
+
throw createCliError(
|
|
5109
|
+
"INVALID_ARGUMENT",
|
|
5110
|
+
"`--decisions-placeholders` must be one of: off, info, warn."
|
|
5111
|
+
);
|
|
5112
|
+
}
|
|
5113
|
+
const decisionsPlaceholderMode = rawDecisionsMode;
|
|
4760
5114
|
const { docsDir, projectType, lang } = config;
|
|
4761
5115
|
let scan = await scanFeatures(config);
|
|
4762
5116
|
let features = scan.features;
|
|
@@ -4764,7 +5118,14 @@ function doctorCommand(program2) {
|
|
|
4764
5118
|
let warnings = scan.warnings;
|
|
4765
5119
|
let issues = [];
|
|
4766
5120
|
issues.push(...await checkDocsStructure({ docsDir, projectType, lang }, cwd));
|
|
4767
|
-
issues.push(
|
|
5121
|
+
issues.push(
|
|
5122
|
+
...await checkFeatures(
|
|
5123
|
+
{ docsDir, projectType, lang },
|
|
5124
|
+
cwd,
|
|
5125
|
+
features,
|
|
5126
|
+
decisionsPlaceholderMode
|
|
5127
|
+
)
|
|
5128
|
+
);
|
|
4768
5129
|
let fixResult = null;
|
|
4769
5130
|
if (options.fix) {
|
|
4770
5131
|
if (hasFixableIssues(issues)) {
|
|
@@ -4789,25 +5150,35 @@ function doctorCommand(program2) {
|
|
|
4789
5150
|
warnings = scan.warnings;
|
|
4790
5151
|
issues = [];
|
|
4791
5152
|
issues.push(...await checkDocsStructure({ docsDir, projectType, lang }, cwd));
|
|
4792
|
-
issues.push(
|
|
5153
|
+
issues.push(
|
|
5154
|
+
...await checkFeatures(
|
|
5155
|
+
{ docsDir, projectType, lang },
|
|
5156
|
+
cwd,
|
|
5157
|
+
features,
|
|
5158
|
+
decisionsPlaceholderMode
|
|
5159
|
+
)
|
|
5160
|
+
);
|
|
4793
5161
|
}
|
|
4794
5162
|
}
|
|
4795
|
-
const
|
|
5163
|
+
const nonInfoIssues = issues.filter((i) => i.level !== "info");
|
|
5164
|
+
const hasIssues = nonInfoIssues.length > 0;
|
|
4796
5165
|
const hasErrors = issues.some((i) => i.level === "error");
|
|
5166
|
+
const hasWarns = issues.some((i) => i.level === "warn");
|
|
4797
5167
|
const exitCode = options.strict && hasIssues ? 1 : 0;
|
|
4798
5168
|
if (options.json) {
|
|
4799
5169
|
console.log(
|
|
4800
5170
|
JSON.stringify(
|
|
4801
5171
|
{
|
|
4802
|
-
status: hasErrors ? "error" :
|
|
5172
|
+
status: hasErrors ? "error" : hasWarns ? "warn" : "ok",
|
|
4803
5173
|
meta: { docsDir, projectType, lang },
|
|
4804
5174
|
branches,
|
|
4805
5175
|
warnings,
|
|
4806
5176
|
counts: {
|
|
4807
5177
|
features: features.length,
|
|
4808
|
-
issues:
|
|
5178
|
+
issues: nonInfoIssues.length,
|
|
4809
5179
|
errors: issues.filter((i) => i.level === "error").length,
|
|
4810
|
-
warnings: issues.filter((i) => i.level === "warn").length
|
|
5180
|
+
warnings: issues.filter((i) => i.level === "warn").length,
|
|
5181
|
+
infos: issues.filter((i) => i.level === "info").length
|
|
4811
5182
|
},
|
|
4812
5183
|
fixes: fixResult ? {
|
|
4813
5184
|
enabled: fixResult.enabled,
|
|
@@ -4844,13 +5215,18 @@ function doctorCommand(program2) {
|
|
|
4844
5215
|
}
|
|
4845
5216
|
console.log();
|
|
4846
5217
|
}
|
|
4847
|
-
|
|
5218
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
5219
|
+
const warns = issues.filter((i) => i.level === "warn");
|
|
5220
|
+
const infos = issues.filter((i) => i.level === "info");
|
|
5221
|
+
if (!hasIssues && infos.length === 0) {
|
|
4848
5222
|
console.log(chalk6.green(tr(lang, "cli", "doctor.noIssues")));
|
|
4849
5223
|
console.log();
|
|
4850
5224
|
process.exit(0);
|
|
4851
5225
|
}
|
|
4852
|
-
|
|
4853
|
-
|
|
5226
|
+
if (!hasIssues && infos.length > 0) {
|
|
5227
|
+
console.log(chalk6.green(tr(lang, "cli", "doctor.noIssues")));
|
|
5228
|
+
console.log();
|
|
5229
|
+
}
|
|
4854
5230
|
if (errors.length > 0) {
|
|
4855
5231
|
console.log(
|
|
4856
5232
|
chalk6.red(
|
|
@@ -4875,6 +5251,13 @@ function doctorCommand(program2) {
|
|
|
4875
5251
|
);
|
|
4876
5252
|
console.log();
|
|
4877
5253
|
}
|
|
5254
|
+
if (infos.length > 0) {
|
|
5255
|
+
console.log(chalk6.blue(`\u2139\uFE0F Info (${infos.length})`));
|
|
5256
|
+
infos.forEach(
|
|
5257
|
+
(i) => console.log(chalk6.blue(` - ${i.message}${i.path ? ` (${i.path})` : ""}`))
|
|
5258
|
+
);
|
|
5259
|
+
console.log();
|
|
5260
|
+
}
|
|
4878
5261
|
console.log(
|
|
4879
5262
|
chalk6.gray(
|
|
4880
5263
|
tr(lang, "cli", "doctor.tipJson", {
|
|
@@ -4913,182 +5296,13 @@ function doctorCommand(program2) {
|
|
|
4913
5296
|
}
|
|
4914
5297
|
});
|
|
4915
5298
|
}
|
|
4916
|
-
function
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
while (n > 0) {
|
|
4924
|
-
const rem = (n - 1) % 26;
|
|
4925
|
-
label = String.fromCharCode(65 + rem) + label;
|
|
4926
|
-
n = Math.floor((n - 1) / 26);
|
|
4927
|
-
}
|
|
4928
|
-
return label;
|
|
4929
|
-
}
|
|
4930
|
-
function toActionOptions2(actions) {
|
|
4931
|
-
return actions.map((action, index) => ({
|
|
4932
|
-
label: getActionLabel2(index),
|
|
4933
|
-
action
|
|
4934
|
-
}));
|
|
4935
|
-
}
|
|
4936
|
-
function buildActionSnapshot2(actionOptions) {
|
|
4937
|
-
return actionOptions.map(({ label, action }) => {
|
|
4938
|
-
if (action.type === "command") {
|
|
4939
|
-
return {
|
|
4940
|
-
label,
|
|
4941
|
-
type: action.type,
|
|
4942
|
-
scope: action.scope,
|
|
4943
|
-
cwd: action.cwd,
|
|
4944
|
-
cmd: action.cmd,
|
|
4945
|
-
category: action.category,
|
|
4946
|
-
requiresUserCheck: !!action.requiresUserCheck
|
|
4947
|
-
};
|
|
4948
|
-
}
|
|
4949
|
-
return {
|
|
4950
|
-
label,
|
|
4951
|
-
type: action.type,
|
|
4952
|
-
message: action.message,
|
|
4953
|
-
category: action.category,
|
|
4954
|
-
requiresUserCheck: !!action.requiresUserCheck
|
|
4955
|
-
};
|
|
4956
|
-
});
|
|
4957
|
-
}
|
|
4958
|
-
function getContextVersion2(feature, actionOptions) {
|
|
4959
|
-
if (!feature) return null;
|
|
4960
|
-
const payload = JSON.stringify({
|
|
4961
|
-
id: feature.id || "",
|
|
4962
|
-
folderName: feature.folderName,
|
|
4963
|
-
currentStep: feature.currentStep,
|
|
4964
|
-
actionSnapshot: buildActionSnapshot2(actionOptions)
|
|
4965
|
-
});
|
|
4966
|
-
return createHash("sha256").update(payload).digest("hex").slice(0, 12);
|
|
4967
|
-
}
|
|
4968
|
-
function matchesFeatureSelector2(f, selector) {
|
|
4969
|
-
const s = selector.trim();
|
|
4970
|
-
if (!s) return false;
|
|
4971
|
-
if (f.folderName.toLowerCase() === s.toLowerCase()) return true;
|
|
4972
|
-
if (f.slug.toLowerCase() === s.toLowerCase()) return true;
|
|
4973
|
-
if (f.id && f.id.toLowerCase() === s.toLowerCase()) return true;
|
|
4974
|
-
return false;
|
|
4975
|
-
}
|
|
4976
|
-
function detectFromBranch2(branchName, features) {
|
|
4977
|
-
const match = branchName.match(/^feat\/\d+-(.+)$/);
|
|
4978
|
-
if (!match) return [];
|
|
4979
|
-
const detected = match[1];
|
|
4980
|
-
return features.filter(
|
|
4981
|
-
(f) => f.slug.toLowerCase() === detected.toLowerCase() || f.folderName.toLowerCase() === detected.toLowerCase()
|
|
4982
|
-
);
|
|
4983
|
-
}
|
|
4984
|
-
function toSelectionStatus2(features, selectionMode, openFeatures, targetFeatures) {
|
|
4985
|
-
const isNoOpen = selectionMode === "open" && features.length > 0 && openFeatures.length === 0;
|
|
4986
|
-
if (features.length === 0) return "no_features";
|
|
4987
|
-
if (isNoOpen) return "no_open";
|
|
4988
|
-
if (targetFeatures.length === 1) return "single_matched";
|
|
4989
|
-
if (targetFeatures.length > 1) return "multiple_active";
|
|
4990
|
-
return "no_match";
|
|
4991
|
-
}
|
|
4992
|
-
function toReasonCode2(status) {
|
|
4993
|
-
if (status === "no_features") return "NO_FEATURES";
|
|
4994
|
-
if (status === "no_open") return "NO_OPEN_FEATURES";
|
|
4995
|
-
if (status === "single_matched") return "SINGLE_MATCHED";
|
|
4996
|
-
if (status === "multiple_active") return "MULTIPLE_ACTIVE_FEATURES";
|
|
4997
|
-
return "NO_MATCHED_FEATURES";
|
|
4998
|
-
}
|
|
4999
|
-
async function resolveContextSelection(config, featureName, options) {
|
|
5000
|
-
const { features, branches, warnings } = await scanFeatures(config);
|
|
5001
|
-
const selectedComponent = resolveComponentOption(options);
|
|
5002
|
-
const scopedFeatures = selectedComponent ? features.filter((f) => f.type === selectedComponent) : features;
|
|
5003
|
-
const doneFeatures = scopedFeatures.filter((f) => f.completion.workflowDone);
|
|
5004
|
-
const openFeatures = scopedFeatures.filter((f) => !f.completion.workflowDone);
|
|
5005
|
-
const inProgressFeatures = openFeatures.filter(
|
|
5006
|
-
(f) => !f.completion.implementationDone
|
|
5007
|
-
);
|
|
5008
|
-
const readyToCloseFeatures = openFeatures.filter(
|
|
5009
|
-
(f) => f.completion.implementationDone
|
|
5010
|
-
);
|
|
5011
|
-
let targetFeatures = [];
|
|
5012
|
-
let selectionMode = "explicit";
|
|
5013
|
-
if (featureName) {
|
|
5014
|
-
targetFeatures = scopedFeatures.filter(
|
|
5015
|
-
(f) => matchesFeatureSelector2(f, featureName)
|
|
5016
|
-
);
|
|
5017
|
-
selectionMode = "explicit";
|
|
5018
|
-
} else {
|
|
5019
|
-
if (config.projectType === "single") {
|
|
5020
|
-
const branchName = branches.project.single || "";
|
|
5021
|
-
targetFeatures = detectFromBranch2(branchName, scopedFeatures);
|
|
5022
|
-
} else if (selectedComponent) {
|
|
5023
|
-
const branchName = branches.project[selectedComponent] || "";
|
|
5024
|
-
targetFeatures = detectFromBranch2(
|
|
5025
|
-
branchName,
|
|
5026
|
-
scopedFeatures
|
|
5027
|
-
);
|
|
5028
|
-
} else {
|
|
5029
|
-
const matches = [];
|
|
5030
|
-
const componentKeys = [...new Set(scopedFeatures.map((f) => f.type))].filter((key) => key !== "single");
|
|
5031
|
-
for (const component of componentKeys) {
|
|
5032
|
-
const branchName = branches.project[component] || "";
|
|
5033
|
-
if (!branchName) continue;
|
|
5034
|
-
matches.push(
|
|
5035
|
-
...detectFromBranch2(
|
|
5036
|
-
branchName,
|
|
5037
|
-
scopedFeatures.filter((f) => f.type === component)
|
|
5038
|
-
)
|
|
5039
|
-
);
|
|
5040
|
-
}
|
|
5041
|
-
targetFeatures = matches;
|
|
5042
|
-
}
|
|
5043
|
-
if (targetFeatures.length > 0) {
|
|
5044
|
-
selectionMode = "branch";
|
|
5045
|
-
} else if (options.all) {
|
|
5046
|
-
targetFeatures = scopedFeatures;
|
|
5047
|
-
selectionMode = "all";
|
|
5048
|
-
} else if (options.done) {
|
|
5049
|
-
targetFeatures = doneFeatures;
|
|
5050
|
-
selectionMode = "done";
|
|
5051
|
-
} else {
|
|
5052
|
-
targetFeatures = openFeatures;
|
|
5053
|
-
selectionMode = "open";
|
|
5054
|
-
}
|
|
5055
|
-
}
|
|
5056
|
-
const status = toSelectionStatus2(
|
|
5057
|
-
scopedFeatures,
|
|
5058
|
-
selectionMode,
|
|
5059
|
-
openFeatures,
|
|
5060
|
-
targetFeatures
|
|
5061
|
-
);
|
|
5062
|
-
const matchedFeature = targetFeatures.length === 1 ? targetFeatures[0] : null;
|
|
5063
|
-
const actions = matchedFeature?.actions ?? [];
|
|
5064
|
-
const actionOptions = toActionOptions2(actions);
|
|
5065
|
-
const contextVersion = getContextVersion2(matchedFeature, actionOptions);
|
|
5066
|
-
return {
|
|
5067
|
-
features: scopedFeatures,
|
|
5068
|
-
branches,
|
|
5069
|
-
warnings,
|
|
5070
|
-
doneFeatures,
|
|
5071
|
-
openFeatures,
|
|
5072
|
-
inProgressFeatures,
|
|
5073
|
-
readyToCloseFeatures,
|
|
5074
|
-
selectionMode,
|
|
5075
|
-
targetFeatures,
|
|
5076
|
-
status,
|
|
5077
|
-
matchedFeature,
|
|
5078
|
-
actions,
|
|
5079
|
-
actionOptions,
|
|
5080
|
-
contextVersion
|
|
5081
|
-
};
|
|
5082
|
-
}
|
|
5083
|
-
|
|
5084
|
-
// src/commands/view.ts
|
|
5085
|
-
function resolveComponentOption2(options) {
|
|
5086
|
-
if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
|
|
5087
|
-
throw createCliError(
|
|
5088
|
-
"INVALID_ARGUMENT",
|
|
5089
|
-
"`--repo` and `--component` must reference the same value when both are provided."
|
|
5090
|
-
);
|
|
5091
|
-
}
|
|
5299
|
+
function resolveComponentOption2(options) {
|
|
5300
|
+
if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
|
|
5301
|
+
throw createCliError(
|
|
5302
|
+
"INVALID_ARGUMENT",
|
|
5303
|
+
"`--repo` and `--component` must reference the same value when both are provided."
|
|
5304
|
+
);
|
|
5305
|
+
}
|
|
5092
5306
|
const component = (options.component || options.repo || "").trim().toLowerCase();
|
|
5093
5307
|
return component || void 0;
|
|
5094
5308
|
}
|
|
@@ -5139,8 +5353,9 @@ async function runView(featureName, options) {
|
|
|
5139
5353
|
if (options.json) {
|
|
5140
5354
|
const payload = {
|
|
5141
5355
|
status: state.status,
|
|
5142
|
-
reasonCode:
|
|
5356
|
+
reasonCode: toReasonCode(state.status),
|
|
5143
5357
|
selectionMode: state.selectionMode,
|
|
5358
|
+
selectionFallback: state.selectionFallback,
|
|
5144
5359
|
counts: {
|
|
5145
5360
|
features: state.features.length,
|
|
5146
5361
|
open: state.openFeatures.length,
|
|
@@ -5187,7 +5402,7 @@ async function runView(featureName, options) {
|
|
|
5187
5402
|
}
|
|
5188
5403
|
if (!state.matchedFeature) {
|
|
5189
5404
|
console.log();
|
|
5190
|
-
console.log(chalk6.blue(`Selection: ${state.status} (${
|
|
5405
|
+
console.log(chalk6.blue(`Selection: ${state.status} (${toReasonCode(state.status)})`));
|
|
5191
5406
|
const rows = state.targetFeatures.length > 0 ? state.targetFeatures : state.features;
|
|
5192
5407
|
for (const f2 of rows) {
|
|
5193
5408
|
const statusText = f2.completion.workflowDone ? chalk6.green("WORKFLOW_DONE") : f2.completion.implementationDone ? chalk6.cyan("DONE") : chalk6.yellow("IN_PROGRESS");
|
|
@@ -5375,16 +5590,18 @@ async function runFlow(featureName, options) {
|
|
|
5375
5590
|
context: {
|
|
5376
5591
|
before: {
|
|
5377
5592
|
status: before.status,
|
|
5378
|
-
reasonCode:
|
|
5593
|
+
reasonCode: toReasonCode(before.status),
|
|
5379
5594
|
selectionMode: before.selectionMode,
|
|
5595
|
+
selectionFallback: before.selectionFallback,
|
|
5380
5596
|
matchedFeature: before.matchedFeature,
|
|
5381
5597
|
actionOptions: before.actionOptions,
|
|
5382
5598
|
contextVersion: before.contextVersion
|
|
5383
5599
|
},
|
|
5384
5600
|
after: {
|
|
5385
5601
|
status: after.status,
|
|
5386
|
-
reasonCode:
|
|
5602
|
+
reasonCode: toReasonCode(after.status),
|
|
5387
5603
|
selectionMode: after.selectionMode,
|
|
5604
|
+
selectionFallback: after.selectionFallback,
|
|
5388
5605
|
matchedFeature: after.matchedFeature,
|
|
5389
5606
|
actionOptions: after.actionOptions,
|
|
5390
5607
|
contextVersion: after.contextVersion
|
|
@@ -5403,7 +5620,7 @@ async function runFlow(featureName, options) {
|
|
|
5403
5620
|
console.log(chalk6.bold("\u{1F501} Flow Summary"));
|
|
5404
5621
|
console.log(
|
|
5405
5622
|
chalk6.gray(
|
|
5406
|
-
`- Before: ${before.status} (${
|
|
5623
|
+
`- Before: ${before.status} (${toReasonCode(before.status)}) / After: ${after.status} (${toReasonCode(after.status)})`
|
|
5407
5624
|
)
|
|
5408
5625
|
);
|
|
5409
5626
|
if (approvalResult && typeof approvalResult === "object") {
|
|
@@ -5439,6 +5656,626 @@ async function runFlow(featureName, options) {
|
|
|
5439
5656
|
console.log(chalk6.gray("Tip: add --approve <LABEL> [--execute] to run the selected atomic action."));
|
|
5440
5657
|
console.log();
|
|
5441
5658
|
}
|
|
5659
|
+
function resolveComponentOption4(options) {
|
|
5660
|
+
if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
|
|
5661
|
+
throw createCliError(
|
|
5662
|
+
"INVALID_ARGUMENT",
|
|
5663
|
+
"`--repo` and `--component` must reference the same value when both are provided."
|
|
5664
|
+
);
|
|
5665
|
+
}
|
|
5666
|
+
const component = (options.component || options.repo || "").trim().toLowerCase();
|
|
5667
|
+
return component || void 0;
|
|
5668
|
+
}
|
|
5669
|
+
function parseLabels(raw) {
|
|
5670
|
+
const labels = (raw || "enhancement").split(",").map((part) => part.trim()).filter(Boolean);
|
|
5671
|
+
if (labels.length === 0) {
|
|
5672
|
+
throw createCliError(
|
|
5673
|
+
"INVALID_ARGUMENT",
|
|
5674
|
+
"At least one label is required. Use `--labels enhancement`."
|
|
5675
|
+
);
|
|
5676
|
+
}
|
|
5677
|
+
return [...new Set(labels)];
|
|
5678
|
+
}
|
|
5679
|
+
function runProcess(bin, args, cwd) {
|
|
5680
|
+
const result = spawnSync(bin, args, {
|
|
5681
|
+
cwd,
|
|
5682
|
+
encoding: "utf-8",
|
|
5683
|
+
env: {
|
|
5684
|
+
...process.env,
|
|
5685
|
+
LEE_SPEC_KIT_NO_UPDATE_CHECK: "1",
|
|
5686
|
+
LEE_SPEC_KIT_NO_BANNER: "1"
|
|
5687
|
+
}
|
|
5688
|
+
});
|
|
5689
|
+
return {
|
|
5690
|
+
code: result.status ?? 1,
|
|
5691
|
+
stdout: result.stdout || "",
|
|
5692
|
+
stderr: result.stderr || ""
|
|
5693
|
+
};
|
|
5694
|
+
}
|
|
5695
|
+
function runProcessOrThrow(bin, args, cwd, failureMessage) {
|
|
5696
|
+
const result = runProcess(bin, args, cwd);
|
|
5697
|
+
if (result.code !== 0) {
|
|
5698
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
5699
|
+
throw createCliError(
|
|
5700
|
+
"EXECUTION_FAILED",
|
|
5701
|
+
`${failureMessage}${detail ? `: ${detail}` : ""}`
|
|
5702
|
+
);
|
|
5703
|
+
}
|
|
5704
|
+
return result;
|
|
5705
|
+
}
|
|
5706
|
+
function runGhJson(args, cwd) {
|
|
5707
|
+
const result = runProcessOrThrow("gh", args, cwd, "GitHub CLI command failed");
|
|
5708
|
+
const text = result.stdout.trim();
|
|
5709
|
+
if (!text) {
|
|
5710
|
+
throw createCliError("EXECUTION_FAILED", "GitHub CLI returned empty JSON output.");
|
|
5711
|
+
}
|
|
5712
|
+
try {
|
|
5713
|
+
return JSON.parse(text);
|
|
5714
|
+
} catch {
|
|
5715
|
+
throw createCliError(
|
|
5716
|
+
"EXECUTION_FAILED",
|
|
5717
|
+
`GitHub CLI returned invalid JSON: ${text.slice(0, 160)}`
|
|
5718
|
+
);
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
function ensureSections(body, sections, kind) {
|
|
5722
|
+
const missing = sections.filter((section) => {
|
|
5723
|
+
const re = new RegExp(`^##\\s+${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
|
5724
|
+
return !re.test(body);
|
|
5725
|
+
});
|
|
5726
|
+
if (missing.length > 0) {
|
|
5727
|
+
throw createCliError(
|
|
5728
|
+
"PRECONDITION_FAILED",
|
|
5729
|
+
`${kind} body is missing required sections: ${missing.join(", ")}`
|
|
5730
|
+
);
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
function ensureDocsExist(docsDir, relativePaths) {
|
|
5734
|
+
const missing = relativePaths.filter(
|
|
5735
|
+
(relativePath) => !fs2.existsSync(path13.join(docsDir, relativePath))
|
|
5736
|
+
);
|
|
5737
|
+
if (missing.length > 0) {
|
|
5738
|
+
throw createCliError(
|
|
5739
|
+
"PRECONDITION_FAILED",
|
|
5740
|
+
`Related document paths do not exist: ${missing.join(", ")}`
|
|
5741
|
+
);
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
function toBodyFilePath(raw, fallbackName) {
|
|
5745
|
+
const selected = raw?.trim() || path13.join(os.tmpdir(), fallbackName);
|
|
5746
|
+
return path13.resolve(selected);
|
|
5747
|
+
}
|
|
5748
|
+
async function resolveFeatureOrThrow(featureName, options) {
|
|
5749
|
+
const config = await getConfig(process.cwd());
|
|
5750
|
+
if (!config) {
|
|
5751
|
+
throw createCliError(
|
|
5752
|
+
"CONFIG_NOT_FOUND",
|
|
5753
|
+
tr(DEFAULT_LANG, "cli", "common.configNotFound")
|
|
5754
|
+
);
|
|
5755
|
+
}
|
|
5756
|
+
const state = await resolveContextSelection(config, featureName, options);
|
|
5757
|
+
if (!state.matchedFeature) {
|
|
5758
|
+
if (state.status === "no_features") {
|
|
5759
|
+
throw createCliError("PRECONDITION_FAILED", "No features found.");
|
|
5760
|
+
}
|
|
5761
|
+
if (state.status === "multiple_active") {
|
|
5762
|
+
throw createCliError(
|
|
5763
|
+
"CONTEXT_SELECTION_REQUIRED",
|
|
5764
|
+
"Multiple features matched. Specify feature name (slug | F001 | F001-slug)."
|
|
5765
|
+
);
|
|
5766
|
+
}
|
|
5767
|
+
throw createCliError(
|
|
5768
|
+
"CONTEXT_SELECTION_REQUIRED",
|
|
5769
|
+
"Failed to auto-select a feature. Specify feature name explicitly."
|
|
5770
|
+
);
|
|
5771
|
+
}
|
|
5772
|
+
return { config, feature: state.matchedFeature };
|
|
5773
|
+
}
|
|
5774
|
+
function getFeatureDocPaths(feature) {
|
|
5775
|
+
const featurePathFromDocs = feature.docs.featurePathFromDocs;
|
|
5776
|
+
return {
|
|
5777
|
+
featurePathFromDocs,
|
|
5778
|
+
specPath: `${featurePathFromDocs}/spec.md`,
|
|
5779
|
+
planPath: `${featurePathFromDocs}/plan.md`,
|
|
5780
|
+
tasksPath: `${featurePathFromDocs}/tasks.md`
|
|
5781
|
+
};
|
|
5782
|
+
}
|
|
5783
|
+
function buildIssueBody(feature, labels, paths) {
|
|
5784
|
+
return `## Overview
|
|
5785
|
+
|
|
5786
|
+
Implement feature \`${feature.folderName}\`.
|
|
5787
|
+
|
|
5788
|
+
## Goals
|
|
5789
|
+
|
|
5790
|
+
- Finalize feature scope and implementation outcome
|
|
5791
|
+
- Keep spec/plan/tasks aligned with delivery
|
|
5792
|
+
|
|
5793
|
+
## Completion Criteria
|
|
5794
|
+
|
|
5795
|
+
- [ ] Scope and approach are documented clearly
|
|
5796
|
+
- [ ] Tasks are complete and verifiable
|
|
5797
|
+
- [ ] Related docs are synchronized
|
|
5798
|
+
|
|
5799
|
+
## Related Documents
|
|
5800
|
+
|
|
5801
|
+
- **Spec**: \`${paths.specPath}\`
|
|
5802
|
+
- **Plan**: \`${paths.planPath}\`
|
|
5803
|
+
- **Tasks**: \`${paths.tasksPath}\`
|
|
5804
|
+
|
|
5805
|
+
## Labels
|
|
5806
|
+
|
|
5807
|
+
${labels.map((label) => `- \`${label}\``).join("\n")}
|
|
5808
|
+
`;
|
|
5809
|
+
}
|
|
5810
|
+
function buildPrBody(feature, paths) {
|
|
5811
|
+
const closes = feature.issueNumber ? `
|
|
5812
|
+
Closes #${feature.issueNumber}
|
|
5813
|
+
` : "\n";
|
|
5814
|
+
return `## Overview
|
|
5815
|
+
|
|
5816
|
+
Implement and document feature \`${feature.folderName}\`.
|
|
5817
|
+
|
|
5818
|
+
## Changes
|
|
5819
|
+
|
|
5820
|
+
- Deliver implementation for the feature scope
|
|
5821
|
+
- Update docs to match implementation and workflow state
|
|
5822
|
+
- Keep PR metadata synchronized in tasks.md
|
|
5823
|
+
|
|
5824
|
+
## Tests
|
|
5825
|
+
|
|
5826
|
+
### Tests Run
|
|
5827
|
+
|
|
5828
|
+
- [x] \`<test command>\` \u2014 PASS
|
|
5829
|
+
|
|
5830
|
+
## Related Documents
|
|
5831
|
+
|
|
5832
|
+
- **Spec**: \`${paths.specPath}\`
|
|
5833
|
+
- **Tasks**: \`${paths.tasksPath}\`${closes}`;
|
|
5834
|
+
}
|
|
5835
|
+
function replaceListField(content, keys, value) {
|
|
5836
|
+
for (const key of keys) {
|
|
5837
|
+
const re = new RegExp(
|
|
5838
|
+
`^(\\s*-\\s*\\*\\*${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\*\\*\\s*:\\s*).*$`,
|
|
5839
|
+
"m"
|
|
5840
|
+
);
|
|
5841
|
+
if (!re.test(content)) continue;
|
|
5842
|
+
const next = content.replace(re, `$1${value}`);
|
|
5843
|
+
return { content: next, changed: next !== content, found: true };
|
|
5844
|
+
}
|
|
5845
|
+
return { content, changed: false, found: false };
|
|
5846
|
+
}
|
|
5847
|
+
function insertFieldInGithubIssueSection(content, key, value) {
|
|
5848
|
+
const lines = content.split("\n");
|
|
5849
|
+
const headingIndex = lines.findIndex(
|
|
5850
|
+
(line) => /^\s*##\s+(GitHub Issue|로컬 추적 정보|Local Tracking)\s*$/.test(line)
|
|
5851
|
+
);
|
|
5852
|
+
if (headingIndex < 0) return { content, changed: false };
|
|
5853
|
+
let end = lines.length;
|
|
5854
|
+
for (let i = headingIndex + 1; i < lines.length; i++) {
|
|
5855
|
+
if (/^\s*##\s+/.test(lines[i])) {
|
|
5856
|
+
end = i;
|
|
5857
|
+
break;
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
lines.splice(end, 0, `- **${key}**: ${value}`);
|
|
5861
|
+
return { content: lines.join("\n"), changed: true };
|
|
5862
|
+
}
|
|
5863
|
+
function syncTasksPrMetadata(tasksPath, prUrl, nextStatus) {
|
|
5864
|
+
if (!fs2.existsSync(tasksPath)) {
|
|
5865
|
+
throw createCliError("DOCS_NOT_FOUND", `tasks.md not found: ${tasksPath}`);
|
|
5866
|
+
}
|
|
5867
|
+
const original = fs2.readFileSync(tasksPath, "utf-8");
|
|
5868
|
+
let next = original;
|
|
5869
|
+
let changed = false;
|
|
5870
|
+
const prReplaced = replaceListField(next, ["PR", "Pull Request"], prUrl);
|
|
5871
|
+
next = prReplaced.content;
|
|
5872
|
+
changed = changed || prReplaced.changed;
|
|
5873
|
+
if (!prReplaced.found) {
|
|
5874
|
+
const inserted = insertFieldInGithubIssueSection(next, "PR", prUrl);
|
|
5875
|
+
next = inserted.content;
|
|
5876
|
+
changed = changed || inserted.changed;
|
|
5877
|
+
}
|
|
5878
|
+
const statusReplaced = replaceListField(
|
|
5879
|
+
next,
|
|
5880
|
+
["PR Status", "PR \uC0C1\uD0DC"],
|
|
5881
|
+
nextStatus
|
|
5882
|
+
);
|
|
5883
|
+
next = statusReplaced.content;
|
|
5884
|
+
changed = changed || statusReplaced.changed;
|
|
5885
|
+
if (!statusReplaced.found) {
|
|
5886
|
+
const inserted = insertFieldInGithubIssueSection(next, "PR Status", nextStatus);
|
|
5887
|
+
next = inserted.content;
|
|
5888
|
+
changed = changed || inserted.changed;
|
|
5889
|
+
}
|
|
5890
|
+
if (changed) {
|
|
5891
|
+
fs2.writeFileSync(tasksPath, next, "utf-8");
|
|
5892
|
+
}
|
|
5893
|
+
return { changed, path: tasksPath };
|
|
5894
|
+
}
|
|
5895
|
+
function gitCurrentBranch(cwd) {
|
|
5896
|
+
const result = runProcessOrThrow(
|
|
5897
|
+
"git",
|
|
5898
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
5899
|
+
cwd,
|
|
5900
|
+
"Failed to detect current git branch"
|
|
5901
|
+
);
|
|
5902
|
+
return result.stdout.trim();
|
|
5903
|
+
}
|
|
5904
|
+
function ensureCleanWorktree(cwd) {
|
|
5905
|
+
const result = runProcessOrThrow(
|
|
5906
|
+
"git",
|
|
5907
|
+
["status", "--porcelain=v1"],
|
|
5908
|
+
cwd,
|
|
5909
|
+
"Failed to inspect git worktree"
|
|
5910
|
+
);
|
|
5911
|
+
if (result.stdout.trim().length > 0) {
|
|
5912
|
+
throw createCliError(
|
|
5913
|
+
"PRECONDITION_FAILED",
|
|
5914
|
+
"Git worktree is not clean. Commit or stash changes before merge retry sync."
|
|
5915
|
+
);
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
function commitAndPushPath(cwd, absPath, message) {
|
|
5919
|
+
const relativePath = path13.relative(cwd, absPath) || absPath;
|
|
5920
|
+
const status = runProcessOrThrow(
|
|
5921
|
+
"git",
|
|
5922
|
+
["status", "--porcelain=v1", "--", relativePath],
|
|
5923
|
+
cwd,
|
|
5924
|
+
"Failed to inspect git file status"
|
|
5925
|
+
);
|
|
5926
|
+
if (status.stdout.trim().length === 0) return;
|
|
5927
|
+
runProcessOrThrow("git", ["add", "--", relativePath], cwd, "Failed to stage file");
|
|
5928
|
+
runProcessOrThrow("git", ["commit", "-m", message], cwd, "Failed to commit synced metadata");
|
|
5929
|
+
const branch = gitCurrentBranch(cwd);
|
|
5930
|
+
runProcessOrThrow(
|
|
5931
|
+
"git",
|
|
5932
|
+
["push", "-u", "origin", branch],
|
|
5933
|
+
cwd,
|
|
5934
|
+
"Failed to push synced metadata commit"
|
|
5935
|
+
);
|
|
5936
|
+
}
|
|
5937
|
+
function shouldRefreshHeadBranch(stderr, stdout) {
|
|
5938
|
+
const text = `${stderr}
|
|
5939
|
+
${stdout}`;
|
|
5940
|
+
return /out of date|not possible to fast-forward|must be up to date|not up to date/i.test(
|
|
5941
|
+
text
|
|
5942
|
+
);
|
|
5943
|
+
}
|
|
5944
|
+
function refreshPrHeadBranch(prRef, cwd) {
|
|
5945
|
+
ensureCleanWorktree(cwd);
|
|
5946
|
+
const meta = runGhJson(
|
|
5947
|
+
["pr", "view", prRef, "--json", "url,headRefName,baseRefName"],
|
|
5948
|
+
cwd
|
|
5949
|
+
);
|
|
5950
|
+
const originalBranch = gitCurrentBranch(cwd);
|
|
5951
|
+
runProcessOrThrow(
|
|
5952
|
+
"git",
|
|
5953
|
+
["fetch", "origin", meta.baseRefName, meta.headRefName],
|
|
5954
|
+
cwd,
|
|
5955
|
+
"Failed to fetch PR branches"
|
|
5956
|
+
);
|
|
5957
|
+
const hasLocalHead = runProcess(
|
|
5958
|
+
"git",
|
|
5959
|
+
["show-ref", "--verify", "--quiet", `refs/heads/${meta.headRefName}`],
|
|
5960
|
+
cwd
|
|
5961
|
+
).code === 0;
|
|
5962
|
+
if (hasLocalHead) {
|
|
5963
|
+
runProcessOrThrow(
|
|
5964
|
+
"git",
|
|
5965
|
+
["checkout", meta.headRefName],
|
|
5966
|
+
cwd,
|
|
5967
|
+
"Failed to checkout PR head branch"
|
|
5968
|
+
);
|
|
5969
|
+
} else {
|
|
5970
|
+
runProcessOrThrow(
|
|
5971
|
+
"git",
|
|
5972
|
+
["checkout", "-B", meta.headRefName, `origin/${meta.headRefName}`],
|
|
5973
|
+
cwd,
|
|
5974
|
+
"Failed to create local PR head branch"
|
|
5975
|
+
);
|
|
5976
|
+
}
|
|
5977
|
+
runProcessOrThrow(
|
|
5978
|
+
"git",
|
|
5979
|
+
["rebase", `origin/${meta.baseRefName}`],
|
|
5980
|
+
cwd,
|
|
5981
|
+
"Failed to rebase PR head branch"
|
|
5982
|
+
);
|
|
5983
|
+
runProcessOrThrow(
|
|
5984
|
+
"git",
|
|
5985
|
+
["push", "--force-with-lease", "origin", meta.headRefName],
|
|
5986
|
+
cwd,
|
|
5987
|
+
"Failed to push rebased PR head branch"
|
|
5988
|
+
);
|
|
5989
|
+
if (originalBranch !== meta.headRefName) {
|
|
5990
|
+
runProcessOrThrow(
|
|
5991
|
+
"git",
|
|
5992
|
+
["checkout", originalBranch],
|
|
5993
|
+
cwd,
|
|
5994
|
+
"Failed to restore previous branch after PR refresh"
|
|
5995
|
+
);
|
|
5996
|
+
}
|
|
5997
|
+
}
|
|
5998
|
+
function mergePrWithRetry(prRef, cwd, retryCount) {
|
|
5999
|
+
const attempts = Number.isFinite(retryCount) ? Math.max(1, retryCount) : 3;
|
|
6000
|
+
let lastError = "";
|
|
6001
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
6002
|
+
const merged = runProcess(
|
|
6003
|
+
"gh",
|
|
6004
|
+
["pr", "merge", prRef, "--squash", "--delete-branch"],
|
|
6005
|
+
cwd
|
|
6006
|
+
);
|
|
6007
|
+
if (merged.code === 0) {
|
|
6008
|
+
return { merged: true, attempts: attempt };
|
|
6009
|
+
}
|
|
6010
|
+
lastError = (merged.stderr || merged.stdout || "").trim();
|
|
6011
|
+
if (shouldRefreshHeadBranch(merged.stderr, merged.stdout)) {
|
|
6012
|
+
refreshPrHeadBranch(prRef, cwd);
|
|
6013
|
+
continue;
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
throw createCliError(
|
|
6017
|
+
"EXECUTION_FAILED",
|
|
6018
|
+
`Failed to merge PR after retry attempts.${lastError ? ` Last error: ${lastError}` : ""}`
|
|
6019
|
+
);
|
|
6020
|
+
}
|
|
6021
|
+
function toRetryCount(raw) {
|
|
6022
|
+
if (!raw) return 3;
|
|
6023
|
+
const parsed = Number.parseInt(raw, 10);
|
|
6024
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
6025
|
+
throw createCliError("INVALID_ARGUMENT", "`--retry` must be a positive integer.");
|
|
6026
|
+
}
|
|
6027
|
+
return parsed;
|
|
6028
|
+
}
|
|
6029
|
+
function githubCommand(program2) {
|
|
6030
|
+
const github = program2.command("github").description("GitHub workflow helpers (issue/pr templates, validation, merge retry)");
|
|
6031
|
+
github.command("issue [feature-name]").description("Generate/create GitHub issue body from feature docs with validation").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("--title <title>", "Issue title").option("--labels <labels>", "Comma-separated labels (default: enhancement)").option("--body-file <path>", "Issue body file output path").option("--assignee <assignee>", "Issue assignee (default: @me)").option("--create", "Create issue via gh CLI").action(async (featureName, options) => {
|
|
6032
|
+
try {
|
|
6033
|
+
const selectedComponent = resolveComponentOption4(options);
|
|
6034
|
+
const { config, feature } = await resolveFeatureOrThrow(featureName, {
|
|
6035
|
+
component: selectedComponent
|
|
6036
|
+
});
|
|
6037
|
+
const labels = parseLabels(options.labels);
|
|
6038
|
+
const paths = getFeatureDocPaths(feature);
|
|
6039
|
+
ensureDocsExist(config.docsDir, [paths.specPath, paths.planPath, paths.tasksPath]);
|
|
6040
|
+
const title = options.title?.trim() || `${feature.slug} (${feature.folderName} documentation update)`;
|
|
6041
|
+
const body = buildIssueBody(feature, labels, paths);
|
|
6042
|
+
ensureSections(body, ["Overview", "Goals", "Completion Criteria", "Related Documents", "Labels"], "Issue");
|
|
6043
|
+
const bodyFile = toBodyFilePath(
|
|
6044
|
+
options.bodyFile,
|
|
6045
|
+
`lee-spec-kit.issue.${feature.folderName}.md`
|
|
6046
|
+
);
|
|
6047
|
+
await fs2.ensureDir(path13.dirname(bodyFile));
|
|
6048
|
+
await fs2.writeFile(bodyFile, body, "utf-8");
|
|
6049
|
+
let issueUrl;
|
|
6050
|
+
if (options.create) {
|
|
6051
|
+
const args = [
|
|
6052
|
+
"issue",
|
|
6053
|
+
"create",
|
|
6054
|
+
"--title",
|
|
6055
|
+
title,
|
|
6056
|
+
"--body-file",
|
|
6057
|
+
bodyFile,
|
|
6058
|
+
"--assignee",
|
|
6059
|
+
options.assignee?.trim() || "@me"
|
|
6060
|
+
];
|
|
6061
|
+
for (const label of labels) {
|
|
6062
|
+
args.push("--label", label);
|
|
6063
|
+
}
|
|
6064
|
+
const created = runProcessOrThrow(
|
|
6065
|
+
"gh",
|
|
6066
|
+
args,
|
|
6067
|
+
process.cwd(),
|
|
6068
|
+
"Failed to create GitHub issue"
|
|
6069
|
+
);
|
|
6070
|
+
issueUrl = created.stdout.trim() || void 0;
|
|
6071
|
+
}
|
|
6072
|
+
if (options.json) {
|
|
6073
|
+
console.log(
|
|
6074
|
+
JSON.stringify(
|
|
6075
|
+
{
|
|
6076
|
+
status: "ok",
|
|
6077
|
+
reasonCode: options.create ? "ISSUE_CREATED" : "ISSUE_TEMPLATE_GENERATED",
|
|
6078
|
+
feature: feature.folderName,
|
|
6079
|
+
component: feature.type,
|
|
6080
|
+
title,
|
|
6081
|
+
labels,
|
|
6082
|
+
bodyFile,
|
|
6083
|
+
issueUrl
|
|
6084
|
+
},
|
|
6085
|
+
null,
|
|
6086
|
+
2
|
|
6087
|
+
)
|
|
6088
|
+
);
|
|
6089
|
+
return;
|
|
6090
|
+
}
|
|
6091
|
+
console.log();
|
|
6092
|
+
console.log(chalk6.bold("\u{1F9FE} GitHub Issue Helper"));
|
|
6093
|
+
console.log(chalk6.gray(`- Feature: ${feature.folderName}`));
|
|
6094
|
+
console.log(chalk6.gray(`- Body file: ${bodyFile}`));
|
|
6095
|
+
console.log(chalk6.gray(`- Labels: ${labels.join(", ")}`));
|
|
6096
|
+
if (issueUrl) {
|
|
6097
|
+
console.log(chalk6.green(`\u2705 Created: ${issueUrl}`));
|
|
6098
|
+
} else {
|
|
6099
|
+
console.log(chalk6.blue("Template generated. Add --create to open the issue automatically."));
|
|
6100
|
+
}
|
|
6101
|
+
console.log();
|
|
6102
|
+
} catch (error) {
|
|
6103
|
+
const config = await getConfig(process.cwd());
|
|
6104
|
+
const lang = config?.lang ?? DEFAULT_LANG;
|
|
6105
|
+
const cliError = toCliError(error);
|
|
6106
|
+
const suggestions = getCliErrorSuggestions(cliError.code, lang);
|
|
6107
|
+
if (options.json) {
|
|
6108
|
+
console.log(
|
|
6109
|
+
JSON.stringify({
|
|
6110
|
+
status: "error",
|
|
6111
|
+
reasonCode: cliError.code,
|
|
6112
|
+
error: cliError.message,
|
|
6113
|
+
suggestions
|
|
6114
|
+
})
|
|
6115
|
+
);
|
|
6116
|
+
} else {
|
|
6117
|
+
console.error(
|
|
6118
|
+
chalk6.red(tr(lang, "cli", "common.errorLabel")),
|
|
6119
|
+
chalk6.red(`[${cliError.code}] ${cliError.message}`)
|
|
6120
|
+
);
|
|
6121
|
+
printCliErrorSuggestions(suggestions, lang);
|
|
6122
|
+
}
|
|
6123
|
+
process.exit(1);
|
|
6124
|
+
}
|
|
6125
|
+
});
|
|
6126
|
+
github.command("pr [feature-name]").description(
|
|
6127
|
+
"Generate/create GitHub PR body with validation, tasks PR sync, and merge retry"
|
|
6128
|
+
).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("--title <title>", "PR title").option("--labels <labels>", "Comma-separated labels (default: enhancement)").option("--body-file <path>", "PR body file output path").option("--assignee <assignee>", "PR assignee (default: @me)").option("--base <branch>", "PR base branch (default: main)", "main").option("--create", "Create PR via gh CLI").option("--pr <ref>", "Existing PR URL/number (used by --merge)").option("--merge", "Merge PR with retry and head-branch refresh").option("--retry <count>", "Retry count for merge (default: 3)").option("--no-sync-tasks", "Do not sync PR URL/PR status into tasks.md").option("--commit-sync", "Commit and push tasks.md metadata sync automatically").action(async (featureName, options) => {
|
|
6129
|
+
try {
|
|
6130
|
+
const selectedComponent = resolveComponentOption4(options);
|
|
6131
|
+
const { config, feature } = await resolveFeatureOrThrow(featureName, {
|
|
6132
|
+
component: selectedComponent
|
|
6133
|
+
});
|
|
6134
|
+
const labels = parseLabels(options.labels);
|
|
6135
|
+
const paths = getFeatureDocPaths(feature);
|
|
6136
|
+
ensureDocsExist(config.docsDir, [paths.specPath, paths.tasksPath]);
|
|
6137
|
+
const defaultTitle = feature.issueNumber ? `feat(#${feature.issueNumber}): ${feature.slug} (implementation update)` : `feat: ${feature.slug} (implementation update)`;
|
|
6138
|
+
const title = options.title?.trim() || defaultTitle;
|
|
6139
|
+
const body = buildPrBody(feature, paths);
|
|
6140
|
+
ensureSections(body, ["Overview", "Changes", "Tests", "Related Documents"], "PR");
|
|
6141
|
+
const bodyFile = toBodyFilePath(
|
|
6142
|
+
options.bodyFile,
|
|
6143
|
+
`lee-spec-kit.pr.${feature.folderName}.md`
|
|
6144
|
+
);
|
|
6145
|
+
await fs2.ensureDir(path13.dirname(bodyFile));
|
|
6146
|
+
await fs2.writeFile(bodyFile, body, "utf-8");
|
|
6147
|
+
const retryCount = toRetryCount(options.retry);
|
|
6148
|
+
let prUrl = options.pr?.trim() || "";
|
|
6149
|
+
let mergedAttempts;
|
|
6150
|
+
let syncChanged = false;
|
|
6151
|
+
if (options.create) {
|
|
6152
|
+
const args = [
|
|
6153
|
+
"pr",
|
|
6154
|
+
"create",
|
|
6155
|
+
"--title",
|
|
6156
|
+
title,
|
|
6157
|
+
"--body-file",
|
|
6158
|
+
bodyFile,
|
|
6159
|
+
"--base",
|
|
6160
|
+
options.base || "main",
|
|
6161
|
+
"--assignee",
|
|
6162
|
+
options.assignee?.trim() || "@me"
|
|
6163
|
+
];
|
|
6164
|
+
for (const label of labels) {
|
|
6165
|
+
args.push("--label", label);
|
|
6166
|
+
}
|
|
6167
|
+
const created = runProcessOrThrow(
|
|
6168
|
+
"gh",
|
|
6169
|
+
args,
|
|
6170
|
+
process.cwd(),
|
|
6171
|
+
"Failed to create GitHub PR"
|
|
6172
|
+
);
|
|
6173
|
+
prUrl = created.stdout.trim();
|
|
6174
|
+
}
|
|
6175
|
+
if (!prUrl && options.merge) {
|
|
6176
|
+
throw createCliError(
|
|
6177
|
+
"INVALID_ARGUMENT",
|
|
6178
|
+
"`--merge` requires `--create` or `--pr <url|number>`."
|
|
6179
|
+
);
|
|
6180
|
+
}
|
|
6181
|
+
if (prUrl && options.syncTasks !== false) {
|
|
6182
|
+
const synced = syncTasksPrMetadata(
|
|
6183
|
+
path13.join(config.docsDir, paths.tasksPath),
|
|
6184
|
+
prUrl,
|
|
6185
|
+
"Review"
|
|
6186
|
+
);
|
|
6187
|
+
syncChanged = synced.changed;
|
|
6188
|
+
const shouldCommitSync = !!options.commitSync || !!options.merge;
|
|
6189
|
+
if (syncChanged && shouldCommitSync) {
|
|
6190
|
+
const issueSuffix = feature.issueNumber ? `#${feature.issueNumber}` : feature.folderName;
|
|
6191
|
+
commitAndPushPath(
|
|
6192
|
+
process.cwd(),
|
|
6193
|
+
synced.path,
|
|
6194
|
+
`docs(${issueSuffix}): sync PR metadata for ${feature.folderName}`
|
|
6195
|
+
);
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
6198
|
+
if (options.merge) {
|
|
6199
|
+
const merged = mergePrWithRetry(prUrl, process.cwd(), retryCount);
|
|
6200
|
+
mergedAttempts = merged.attempts;
|
|
6201
|
+
const baseBranch = options.base || "main";
|
|
6202
|
+
runProcessOrThrow(
|
|
6203
|
+
"git",
|
|
6204
|
+
["checkout", baseBranch],
|
|
6205
|
+
process.cwd(),
|
|
6206
|
+
`Failed to checkout ${baseBranch} after merge`
|
|
6207
|
+
);
|
|
6208
|
+
runProcessOrThrow(
|
|
6209
|
+
"git",
|
|
6210
|
+
["pull", "--rebase", "origin", baseBranch],
|
|
6211
|
+
process.cwd(),
|
|
6212
|
+
`Failed to update ${baseBranch} after merge`
|
|
6213
|
+
);
|
|
6214
|
+
}
|
|
6215
|
+
if (options.json) {
|
|
6216
|
+
console.log(
|
|
6217
|
+
JSON.stringify(
|
|
6218
|
+
{
|
|
6219
|
+
status: "ok",
|
|
6220
|
+
reasonCode: options.merge ? "PR_CREATED_SYNCED_MERGED" : options.create ? "PR_CREATED_SYNCED" : "PR_TEMPLATE_GENERATED",
|
|
6221
|
+
feature: feature.folderName,
|
|
6222
|
+
component: feature.type,
|
|
6223
|
+
title,
|
|
6224
|
+
labels,
|
|
6225
|
+
bodyFile,
|
|
6226
|
+
prUrl: prUrl || void 0,
|
|
6227
|
+
syncChanged,
|
|
6228
|
+
merged: !!options.merge,
|
|
6229
|
+
mergeAttempts: mergedAttempts
|
|
6230
|
+
},
|
|
6231
|
+
null,
|
|
6232
|
+
2
|
|
6233
|
+
)
|
|
6234
|
+
);
|
|
6235
|
+
return;
|
|
6236
|
+
}
|
|
6237
|
+
console.log();
|
|
6238
|
+
console.log(chalk6.bold("\u{1F500} GitHub PR Helper"));
|
|
6239
|
+
console.log(chalk6.gray(`- Feature: ${feature.folderName}`));
|
|
6240
|
+
console.log(chalk6.gray(`- Body file: ${bodyFile}`));
|
|
6241
|
+
console.log(chalk6.gray(`- Labels: ${labels.join(", ")}`));
|
|
6242
|
+
if (prUrl) {
|
|
6243
|
+
console.log(chalk6.gray(`- PR: ${prUrl}`));
|
|
6244
|
+
}
|
|
6245
|
+
if (syncChanged) {
|
|
6246
|
+
console.log(chalk6.green("\u2705 tasks.md PR metadata synced."));
|
|
6247
|
+
}
|
|
6248
|
+
if (options.merge) {
|
|
6249
|
+
console.log(chalk6.green(`\u2705 PR merged (attempts: ${mergedAttempts ?? 1}).`));
|
|
6250
|
+
} else if (!options.create) {
|
|
6251
|
+
console.log(chalk6.blue("Template generated. Add --create to open the PR automatically."));
|
|
6252
|
+
}
|
|
6253
|
+
console.log();
|
|
6254
|
+
} catch (error) {
|
|
6255
|
+
const config = await getConfig(process.cwd());
|
|
6256
|
+
const lang = config?.lang ?? DEFAULT_LANG;
|
|
6257
|
+
const cliError = toCliError(error);
|
|
6258
|
+
const suggestions = getCliErrorSuggestions(cliError.code, lang);
|
|
6259
|
+
if (options.json) {
|
|
6260
|
+
console.log(
|
|
6261
|
+
JSON.stringify({
|
|
6262
|
+
status: "error",
|
|
6263
|
+
reasonCode: cliError.code,
|
|
6264
|
+
error: cliError.message,
|
|
6265
|
+
suggestions
|
|
6266
|
+
})
|
|
6267
|
+
);
|
|
6268
|
+
} else {
|
|
6269
|
+
console.error(
|
|
6270
|
+
chalk6.red(tr(lang, "cli", "common.errorLabel")),
|
|
6271
|
+
chalk6.red(`[${cliError.code}] ${cliError.message}`)
|
|
6272
|
+
);
|
|
6273
|
+
printCliErrorSuggestions(suggestions, lang);
|
|
6274
|
+
}
|
|
6275
|
+
process.exit(1);
|
|
6276
|
+
}
|
|
6277
|
+
});
|
|
6278
|
+
}
|
|
5442
6279
|
function isBannerDisabled() {
|
|
5443
6280
|
const v = (process.env.LEE_SPEC_KIT_NO_BANNER || "").trim();
|
|
5444
6281
|
return v === "1";
|
|
@@ -5607,4 +6444,5 @@ contextCommand(program);
|
|
|
5607
6444
|
doctorCommand(program);
|
|
5608
6445
|
viewCommand(program);
|
|
5609
6446
|
flowCommand(program);
|
|
6447
|
+
githubCommand(program);
|
|
5610
6448
|
await program.parseAsync();
|