lee-spec-kit 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +594 -301
- package/package.json +1 -1
- package/templates/en/fullstack/features/feature-base/tasks.md +2 -0
- package/templates/en/single/features/feature-base/tasks.md +2 -0
- package/templates/ko/fullstack/features/feature-base/tasks.md +2 -0
- package/templates/ko/single/features/feature-base/tasks.md +2 -0
package/dist/index.js
CHANGED
|
@@ -887,6 +887,7 @@ async function runUpdate(options) {
|
|
|
887
887
|
console.log(chalk6.green(`\u2705 \uCD1D ${updatedCount}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
|
|
888
888
|
}
|
|
889
889
|
async function updateFolder(sourceDir, targetDir, force, replacements) {
|
|
890
|
+
const protectedFiles = /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
|
|
890
891
|
await fs6.ensureDir(targetDir);
|
|
891
892
|
const files = await fs6.readdir(sourceDir);
|
|
892
893
|
let updatedCount = 0;
|
|
@@ -895,7 +896,7 @@ async function updateFolder(sourceDir, targetDir, force, replacements) {
|
|
|
895
896
|
const targetPath = path4.join(targetDir, file);
|
|
896
897
|
const stat = await fs6.stat(sourcePath);
|
|
897
898
|
if (stat.isFile()) {
|
|
898
|
-
if (file
|
|
899
|
+
if (protectedFiles.has(file)) {
|
|
899
900
|
continue;
|
|
900
901
|
}
|
|
901
902
|
let sourceContent = await fs6.readFile(sourcePath, "utf-8");
|
|
@@ -1031,189 +1032,449 @@ async function runConfig(options) {
|
|
|
1031
1032
|
await fs6.writeJson(configPath, configFile, { spaces: 2 });
|
|
1032
1033
|
console.log();
|
|
1033
1034
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1035
|
+
|
|
1036
|
+
// src/utils/context/i18n.ts
|
|
1037
|
+
function formatTemplate(template, vars) {
|
|
1038
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
1039
|
+
const value = vars[key];
|
|
1040
|
+
return value === void 0 ? `{${key}}` : String(value);
|
|
1041
|
+
});
|
|
1036
1042
|
}
|
|
1037
|
-
var
|
|
1038
|
-
{
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1043
|
+
var I18N = {
|
|
1044
|
+
ko: {
|
|
1045
|
+
steps: {
|
|
1046
|
+
featureFolder: "Feature \uD3F4\uB354 \uC0DD\uC131",
|
|
1047
|
+
specWrite: "spec.md \uC791\uC131",
|
|
1048
|
+
specApprove: "spec.md \uC2B9\uC778",
|
|
1049
|
+
planWrite: "plan.md \uC791\uC131",
|
|
1050
|
+
planApprove: "plan.md \uC2B9\uC778",
|
|
1051
|
+
tasksWrite: "tasks.md \uC791\uC131",
|
|
1052
|
+
docsCommitPlanning: "\uBB38\uC11C \uCEE4\uBC0B(\uAE30\uD68D)",
|
|
1053
|
+
issueCreate: "GitHub Issue \uC0DD\uC131",
|
|
1054
|
+
branchCreate: "\uBE0C\uB79C\uCE58 \uC0DD\uC131",
|
|
1055
|
+
tasksExecute: "\uD0DC\uC2A4\uD06C \uC2E4\uD589",
|
|
1056
|
+
prCreate: "PR \uC0DD\uC131",
|
|
1057
|
+
codeReview: "\uCF54\uB4DC \uB9AC\uBDF0",
|
|
1058
|
+
featureDone: "Feature \uC644\uB8CC"
|
|
1059
|
+
},
|
|
1060
|
+
messages: {
|
|
1061
|
+
specCreate: "spec.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uC791\uC131\uD558\uC138\uC694. (features/feature-base/spec.md \uCC38\uACE0)",
|
|
1062
|
+
specImprove: "spec.md\uB97C \uBCF4\uC644\uD558\uACE0 \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
|
|
1063
|
+
specApproval: "spec.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
|
|
1064
|
+
planCreate: "plan.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uC791\uC131\uD558\uC138\uC694. (features/feature-base/plan.md \uCC38\uACE0)",
|
|
1065
|
+
planImprove: "plan.md\uB97C \uBCF4\uC644\uD558\uACE0 \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
|
|
1066
|
+
planApproval: "plan.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
|
|
1067
|
+
tasksCreate: "tasks.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694. (features/feature-base/tasks.md \uCC38\uACE0)",
|
|
1068
|
+
tasksNeedAtLeastOne: "tasks.md\uC5D0 \uCD5C\uC18C 1\uAC1C \uC774\uC0C1\uC758 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694.",
|
|
1069
|
+
docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): \uAE30\uD68D \uBB38\uC11C \uC791\uC131"',
|
|
1070
|
+
issueCreateAndWrite: "GitHub Issue\uB97C \uC0DD\uC131\uD55C \uB4A4, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694. (skills/create-issue.md \uCC38\uACE0)",
|
|
1071
|
+
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): \uC774\uC288 #{issueNumber} \uBC18\uC601"',
|
|
1072
|
+
standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
|
|
1073
|
+
createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
|
|
1074
|
+
tasksAllDoneButNoChecklist: '\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC139\uC158\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. tasks.md\uC758 "\uC644\uB8CC \uC870\uAC74" \uC139\uC158\uC744 \uCD94\uAC00/\uD655\uC778\uD558\uC138\uC694.',
|
|
1075
|
+
tasksAllDoneButChecklist: "\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uAC00 \uC644\uC804\uD788 \uCCB4\uD06C\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. ({checked}/{total})",
|
|
1076
|
+
finishDoingTask: '\uD604\uC7AC DOING/REVIEW \uC911\uC778 \uD0DC\uC2A4\uD06C\uB97C \uC644\uB8CC\uD558\uC138\uC694: "{title}" ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)',
|
|
1077
|
+
startNextTodoTask: '\uB2E4\uC74C TODO \uD0DC\uC2A4\uD06C\uB97C \uC2DC\uC791\uD558\uC138\uC694: "{title}" ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)',
|
|
1078
|
+
checkTaskStatuses: "\uD0DC\uC2A4\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)",
|
|
1079
|
+
prLegacyAsk: "tasks.md\uC5D0 PR/PR \uC0C1\uD0DC \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uD15C\uD50C\uB9BF\uC744 \uCD5C\uC2E0 \uD3EC\uB9F7\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD560\uAE4C\uC694? (OK \uD544\uC694)",
|
|
1080
|
+
prCreate: "PR\uC744 \uC0DD\uC131\uD558\uACE0 tasks.md\uC5D0 PR \uB9C1\uD06C\uB97C \uAE30\uB85D\uD558\uC138\uC694. (skills/create-pr.md \uCC38\uACE0)",
|
|
1081
|
+
prResolveReview: "\uB9AC\uBDF0 \uCF54\uBA58\uD2B8\uB97C \uD574\uACB0\uD558\uACE0 PR \uC0C1\uD0DC\uB97C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694. (PR \uC0C1\uD0DC: Review \u2192 Approved)",
|
|
1082
|
+
prRequestReview: "\uB9AC\uBDF0\uC5B4\uC5D0\uAC8C \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uACE0 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.",
|
|
1083
|
+
featureDone: "PR\uC774 Approved\uC774\uACE0 \uBAA8\uB4E0 \uD0DC\uC2A4\uD06C/\uC644\uB8CC \uC870\uAC74\uC774 \uCDA9\uC871\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC774 Feature\uB294 \uC644\uB8CC \uC0C1\uD0DC\uC785\uB2C8\uB2E4.",
|
|
1084
|
+
fallbackRerunContext: "\uC0C1\uD0DC\uB97C \uD310\uBCC4\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC11C\uB97C \uD655\uC778\uD55C \uB4A4 \uB2E4\uC2DC context\uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
1085
|
+
},
|
|
1086
|
+
warnings: {
|
|
1087
|
+
projectBranchUnavailable: "\uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.)",
|
|
1088
|
+
docsGitUnavailable: "docs \uB808\uD3EC\uC758 git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (\uB808\uD3EC \uC704\uCE58 / git init \uD655\uC778)",
|
|
1089
|
+
legacyTasksPrFields: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR` \uBC0F `PR \uC0C1\uD0DC` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694."
|
|
1069
1090
|
}
|
|
1070
1091
|
},
|
|
1071
|
-
{
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1092
|
+
en: {
|
|
1093
|
+
steps: {
|
|
1094
|
+
featureFolder: "Create feature folder",
|
|
1095
|
+
specWrite: "Write spec.md",
|
|
1096
|
+
specApprove: "Approve spec.md",
|
|
1097
|
+
planWrite: "Write plan.md",
|
|
1098
|
+
planApprove: "Approve plan.md",
|
|
1099
|
+
tasksWrite: "Write tasks.md",
|
|
1100
|
+
docsCommitPlanning: "Commit planning docs",
|
|
1101
|
+
issueCreate: "Create GitHub Issue",
|
|
1102
|
+
branchCreate: "Create branch",
|
|
1103
|
+
tasksExecute: "Execute tasks",
|
|
1104
|
+
prCreate: "Create PR",
|
|
1105
|
+
codeReview: "Code review",
|
|
1106
|
+
featureDone: "Feature done"
|
|
1107
|
+
},
|
|
1108
|
+
messages: {
|
|
1109
|
+
specCreate: "Copy the spec.md template and write it. (See features/feature-base/spec.md)",
|
|
1110
|
+
specImprove: "Improve spec.md and set Status to Review.",
|
|
1111
|
+
specApproval: "Share spec.md with the user and get approval (OK).",
|
|
1112
|
+
planCreate: "Copy the plan.md template and write it. (See features/feature-base/plan.md)",
|
|
1113
|
+
planImprove: "Improve plan.md and set Status to Review.",
|
|
1114
|
+
planApproval: "Share plan.md with the user and get approval (OK).",
|
|
1115
|
+
tasksCreate: "Copy the tasks.md template and write tasks. (See features/feature-base/tasks.md)",
|
|
1116
|
+
tasksNeedAtLeastOne: "Add at least one task to tasks.md.",
|
|
1117
|
+
docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): planning docs"',
|
|
1118
|
+
issueCreateAndWrite: "Create a GitHub Issue, then fill in the issue number in spec.md/tasks.md and prepare to commit docs. (See skills/create-issue.md)",
|
|
1119
|
+
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): issue #{issueNumber}"',
|
|
1120
|
+
standaloneNeedsProjectRoot: "In standalone mode, projectRoot is required. (npx lee-spec-kit config --project-root ...)",
|
|
1121
|
+
createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
|
|
1122
|
+
tasksAllDoneButNoChecklist: 'All tasks are DONE but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
|
|
1123
|
+
tasksAllDoneButChecklist: "All tasks are DONE but the completion checklist is not fully checked. ({checked}/{total})",
|
|
1124
|
+
finishDoingTask: 'Finish the active DOING/REVIEW task: "{title}" ({done}/{total}) (See skills/execute-task.md)',
|
|
1125
|
+
startNextTodoTask: 'Start the next TODO task: "{title}" ({done}/{total}) (See skills/execute-task.md)',
|
|
1126
|
+
checkTaskStatuses: "Check task statuses. ({done}/{total}) (See skills/execute-task.md)",
|
|
1127
|
+
prLegacyAsk: "Legacy tasks.md format detected (missing PR/PR Status fields). Update to the latest format? (OK required)",
|
|
1128
|
+
prCreate: "Create a PR and record the PR link in tasks.md. (See skills/create-pr.md)",
|
|
1129
|
+
prResolveReview: "Resolve review comments and update PR status. (PR Status: Review \u2192 Approved)",
|
|
1130
|
+
prRequestReview: "Request reviews and update PR status to Review.",
|
|
1131
|
+
featureDone: "PR is Approved and all tasks/completion criteria are satisfied. This feature is done.",
|
|
1132
|
+
fallbackRerunContext: "Unable to determine current state. Verify docs and run context again."
|
|
1133
|
+
},
|
|
1134
|
+
warnings: {
|
|
1135
|
+
projectBranchUnavailable: "Cannot determine project branch. (In standalone mode, projectRoot is required.)",
|
|
1136
|
+
docsGitUnavailable: "Cannot read git status for the docs repo. (Check repo location / git init.)",
|
|
1137
|
+
legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps."
|
|
1083
1138
|
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
function tr(lang, category, key, vars = {}) {
|
|
1142
|
+
const template = I18N[lang][category][key] ?? I18N.ko[category][key] ?? `${category}.${key}`;
|
|
1143
|
+
return formatTemplate(template, vars);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/utils/context/steps.ts
|
|
1147
|
+
function isCompletionChecklistDone(feature) {
|
|
1148
|
+
return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
|
|
1149
|
+
}
|
|
1150
|
+
function isPrMetadataConfigured(feature) {
|
|
1151
|
+
return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
|
|
1152
|
+
}
|
|
1153
|
+
function isFeatureDone(feature) {
|
|
1154
|
+
return feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isPrMetadataConfigured(feature) && !!feature.pr.link && feature.pr.status === "Approved";
|
|
1155
|
+
}
|
|
1156
|
+
function getStepDefinitions(lang) {
|
|
1157
|
+
return [
|
|
1158
|
+
{
|
|
1159
|
+
step: 1,
|
|
1160
|
+
name: tr(lang, "steps", "featureFolder"),
|
|
1161
|
+
checklist: { done: () => true }
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
step: 2,
|
|
1165
|
+
name: tr(lang, "steps", "specWrite"),
|
|
1166
|
+
checklist: {
|
|
1167
|
+
done: (f) => f.specStatus === "Review" || f.specStatus === "Approved"
|
|
1168
|
+
},
|
|
1169
|
+
current: {
|
|
1170
|
+
when: (f) => !f.docs.specExists || !f.specStatus || f.specStatus === "Draft",
|
|
1171
|
+
actions: (f) => [
|
|
1172
|
+
{
|
|
1173
|
+
type: "instruction",
|
|
1174
|
+
message: !f.docs.specExists ? tr(lang, "messages", "specCreate") : tr(lang, "messages", "specImprove")
|
|
1175
|
+
}
|
|
1176
|
+
]
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
step: 3,
|
|
1181
|
+
name: tr(lang, "steps", "specApprove"),
|
|
1182
|
+
checklist: { done: (f) => f.specStatus === "Approved" },
|
|
1183
|
+
current: {
|
|
1184
|
+
when: (f) => f.specStatus === "Review",
|
|
1185
|
+
actions: () => [
|
|
1186
|
+
{
|
|
1187
|
+
type: "instruction",
|
|
1188
|
+
requiresUserOk: true,
|
|
1189
|
+
message: tr(lang, "messages", "specApproval")
|
|
1190
|
+
}
|
|
1191
|
+
]
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
step: 4,
|
|
1196
|
+
name: tr(lang, "steps", "planWrite"),
|
|
1197
|
+
checklist: {
|
|
1198
|
+
done: (f) => f.planStatus === "Review" || f.planStatus === "Approved"
|
|
1199
|
+
},
|
|
1200
|
+
current: {
|
|
1201
|
+
when: (f) => f.specStatus === "Approved" && (!f.docs.planExists || !f.planStatus || f.planStatus === "Draft"),
|
|
1202
|
+
actions: (f) => [
|
|
1203
|
+
{
|
|
1204
|
+
type: "instruction",
|
|
1205
|
+
message: !f.docs.planExists ? tr(lang, "messages", "planCreate") : tr(lang, "messages", "planImprove")
|
|
1206
|
+
}
|
|
1207
|
+
]
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
step: 5,
|
|
1212
|
+
name: tr(lang, "steps", "planApprove"),
|
|
1213
|
+
checklist: { done: (f) => f.planStatus === "Approved" },
|
|
1214
|
+
current: {
|
|
1215
|
+
when: (f) => f.planStatus === "Review",
|
|
1216
|
+
actions: () => [
|
|
1217
|
+
{
|
|
1218
|
+
type: "instruction",
|
|
1219
|
+
requiresUserOk: true,
|
|
1220
|
+
message: tr(lang, "messages", "planApproval")
|
|
1221
|
+
}
|
|
1222
|
+
]
|
|
1223
|
+
}
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
step: 6,
|
|
1227
|
+
name: tr(lang, "steps", "tasksWrite"),
|
|
1228
|
+
checklist: {
|
|
1229
|
+
done: (f) => f.docs.tasksExists && f.tasks.total > 0,
|
|
1230
|
+
detail: (f) => f.tasks.total > 0 ? `(${f.tasks.total})` : ""
|
|
1231
|
+
},
|
|
1232
|
+
current: {
|
|
1233
|
+
when: (f) => f.planStatus === "Approved" && (!f.docs.tasksExists || f.tasks.total === 0),
|
|
1234
|
+
actions: (f) => {
|
|
1235
|
+
if (!f.docs.tasksExists) {
|
|
1236
|
+
return [
|
|
1237
|
+
{
|
|
1238
|
+
type: "instruction",
|
|
1239
|
+
message: tr(lang, "messages", "tasksCreate")
|
|
1240
|
+
}
|
|
1241
|
+
];
|
|
1242
|
+
}
|
|
1093
1243
|
return [
|
|
1094
1244
|
{
|
|
1095
1245
|
type: "instruction",
|
|
1096
|
-
message:
|
|
1246
|
+
message: tr(lang, "messages", "tasksNeedAtLeastOne")
|
|
1097
1247
|
}
|
|
1098
1248
|
];
|
|
1099
1249
|
}
|
|
1100
|
-
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
step: 7,
|
|
1254
|
+
name: tr(lang, "steps", "docsCommitPlanning"),
|
|
1255
|
+
checklist: {
|
|
1256
|
+
done: (f) => f.issueNumber ? true : f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.git.docsHasUncommittedChanges
|
|
1257
|
+
},
|
|
1258
|
+
current: {
|
|
1259
|
+
when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.issueNumber && f.git.docsHasUncommittedChanges,
|
|
1260
|
+
actions: (f) => [
|
|
1101
1261
|
{
|
|
1102
1262
|
type: "command",
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1263
|
+
requiresUserOk: true,
|
|
1264
|
+
scope: "docs",
|
|
1265
|
+
cwd: f.git.docsGitCwd,
|
|
1266
|
+
cmd: tr(lang, "messages", "docsCommitPlanning", {
|
|
1267
|
+
docsGitCwd: f.git.docsGitCwd,
|
|
1268
|
+
featurePath: f.docs.featurePathFromDocs,
|
|
1269
|
+
folderName: f.folderName
|
|
1270
|
+
})
|
|
1106
1271
|
}
|
|
1107
|
-
]
|
|
1272
|
+
]
|
|
1108
1273
|
}
|
|
1109
|
-
}
|
|
1110
|
-
},
|
|
1111
|
-
{
|
|
1112
|
-
step: 6,
|
|
1113
|
-
name: "plan.md \uC791\uC131",
|
|
1114
|
-
checklist: { done: (f) => f.planStatus === "Review" || f.planStatus === "Approved" },
|
|
1115
|
-
current: {
|
|
1116
|
-
when: (f) => f.git.onExpectedBranch && (!f.docs.planExists || !f.planStatus || f.planStatus === "Draft"),
|
|
1117
|
-
actions: (f) => [
|
|
1118
|
-
{
|
|
1119
|
-
type: "instruction",
|
|
1120
|
-
message: !f.docs.planExists ? "plan.md\uB97C \uC791\uC131\uD558\uC138\uC694. (\uC0C1\uD0DC: Draft\uBD80\uD130 \uC2DC\uC791)" : "plan.md\uB97C \uC791\uC131/\uBCF4\uC644\uD558\uACE0, Status\uB97C Review\uB85C \uBCC0\uACBD\uD55C \uB4A4 \uC0AC\uC6A9\uC790 \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uC138\uC694."
|
|
1121
|
-
}
|
|
1122
|
-
]
|
|
1123
|
-
}
|
|
1124
|
-
},
|
|
1125
|
-
{
|
|
1126
|
-
step: 7,
|
|
1127
|
-
name: "plan.md \uC2B9\uC778",
|
|
1128
|
-
checklist: { done: (f) => f.planStatus === "Approved" },
|
|
1129
|
-
current: {
|
|
1130
|
-
when: (f) => f.planStatus === "Review",
|
|
1131
|
-
actions: () => [
|
|
1132
|
-
{
|
|
1133
|
-
type: "instruction",
|
|
1134
|
-
message: "\uC0AC\uC6A9\uC790\uC5D0\uAC8C plan.md\uB97C \uACF5\uC720\uD558\uACE0 \uBA85\uC2DC\uC801 \uC2B9\uC778(Approved)\uC744 \uBC1B\uC73C\uC138\uC694."
|
|
1135
|
-
}
|
|
1136
|
-
]
|
|
1137
|
-
}
|
|
1138
|
-
},
|
|
1139
|
-
{
|
|
1140
|
-
step: 8,
|
|
1141
|
-
name: "tasks.md \uC791\uC131/\uC2E4\uD589",
|
|
1142
|
-
checklist: {
|
|
1143
|
-
done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done,
|
|
1144
|
-
detail: (f) => f.tasks.total > 0 ? `(${f.tasks.done}/${f.tasks.total})` : ""
|
|
1145
1274
|
},
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1275
|
+
{
|
|
1276
|
+
step: 8,
|
|
1277
|
+
name: tr(lang, "steps", "issueCreate"),
|
|
1278
|
+
checklist: {
|
|
1279
|
+
done: (f) => !!f.issueNumber && !f.git.docsHasUncommittedChanges
|
|
1280
|
+
},
|
|
1281
|
+
current: {
|
|
1282
|
+
when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && (!f.issueNumber || f.git.docsHasUncommittedChanges),
|
|
1283
|
+
actions: (f) => {
|
|
1284
|
+
if (!f.issueNumber) {
|
|
1285
|
+
return [
|
|
1286
|
+
{
|
|
1287
|
+
type: "instruction",
|
|
1288
|
+
requiresUserOk: true,
|
|
1289
|
+
message: tr(lang, "messages", "issueCreateAndWrite")
|
|
1290
|
+
}
|
|
1291
|
+
];
|
|
1292
|
+
}
|
|
1293
|
+
return [
|
|
1294
|
+
{
|
|
1295
|
+
type: "command",
|
|
1296
|
+
requiresUserOk: true,
|
|
1297
|
+
scope: "docs",
|
|
1298
|
+
cwd: f.git.docsGitCwd,
|
|
1299
|
+
cmd: tr(lang, "messages", "docsCommitIssueUpdate", {
|
|
1300
|
+
docsGitCwd: f.git.docsGitCwd,
|
|
1301
|
+
featurePath: f.docs.featurePathFromDocs,
|
|
1302
|
+
issueNumber: f.issueNumber,
|
|
1303
|
+
folderName: f.folderName
|
|
1304
|
+
})
|
|
1305
|
+
}
|
|
1306
|
+
];
|
|
1161
1307
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
step: 9,
|
|
1312
|
+
name: tr(lang, "steps", "branchCreate"),
|
|
1313
|
+
checklist: { done: (f) => f.git.onExpectedBranch },
|
|
1314
|
+
current: {
|
|
1315
|
+
when: (f) => !!f.issueNumber && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
|
|
1316
|
+
actions: (f) => {
|
|
1317
|
+
if (!f.git.projectBranchAvailable || !f.git.projectGitCwd) {
|
|
1318
|
+
return [
|
|
1319
|
+
{
|
|
1320
|
+
type: "instruction",
|
|
1321
|
+
message: tr(lang, "messages", "standaloneNeedsProjectRoot")
|
|
1322
|
+
}
|
|
1323
|
+
];
|
|
1324
|
+
}
|
|
1325
|
+
return [
|
|
1326
|
+
{
|
|
1327
|
+
type: "command",
|
|
1328
|
+
scope: "project",
|
|
1329
|
+
cwd: f.git.projectGitCwd,
|
|
1330
|
+
cmd: tr(lang, "messages", "createBranch", {
|
|
1331
|
+
projectGitCwd: f.git.projectGitCwd,
|
|
1332
|
+
issueNumber: f.issueNumber,
|
|
1333
|
+
slug: f.slug
|
|
1334
|
+
})
|
|
1335
|
+
}
|
|
1336
|
+
];
|
|
1165
1337
|
}
|
|
1166
|
-
message = `tasks.md\uC758 \uD0DC\uC2A4\uD06C \uC0C1\uD0DC([TODO]/[DOING]/[DONE])\uB97C \uD655\uC778\uD558\uC138\uC694. (\uC9C4\uD589\uB960: ${f.tasks.done}/${f.tasks.total})`;
|
|
1167
|
-
return [{ type: "instruction", message }];
|
|
1168
1338
|
}
|
|
1169
|
-
}
|
|
1170
|
-
},
|
|
1171
|
-
{
|
|
1172
|
-
step: 9,
|
|
1173
|
-
name: "\uBB38\uC11C \uCEE4\uBC0B \uC804 \uD655\uC778",
|
|
1174
|
-
checklist: {
|
|
1175
|
-
done: (f) => isCompletionChecklistDone(f),
|
|
1176
|
-
detail: (f) => f.completionChecklist ? `(${f.completionChecklist.checked}/${f.completionChecklist.total})` : ""
|
|
1177
1339
|
},
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1340
|
+
{
|
|
1341
|
+
step: 10,
|
|
1342
|
+
name: tr(lang, "steps", "tasksExecute"),
|
|
1343
|
+
checklist: {
|
|
1344
|
+
done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done && isCompletionChecklistDone(f),
|
|
1345
|
+
detail: (f) => f.tasks.total > 0 ? `(${f.tasks.done}/${f.tasks.total})` : ""
|
|
1346
|
+
},
|
|
1347
|
+
current: {
|
|
1348
|
+
when: (f) => f.git.onExpectedBranch && f.docs.tasksExists && f.tasks.total > 0 && (f.tasks.done < f.tasks.total || !isCompletionChecklistDone(f)),
|
|
1349
|
+
actions: (f) => {
|
|
1350
|
+
if (f.tasks.total === f.tasks.done && !isCompletionChecklistDone(f)) {
|
|
1351
|
+
return [
|
|
1352
|
+
{
|
|
1353
|
+
type: "instruction",
|
|
1354
|
+
requiresUserOk: true,
|
|
1355
|
+
message: !f.completionChecklist ? tr(lang, "messages", "tasksAllDoneButNoChecklist") : tr(lang, "messages", "tasksAllDoneButChecklist", {
|
|
1356
|
+
checked: f.completionChecklist.checked,
|
|
1357
|
+
total: f.completionChecklist.total
|
|
1358
|
+
})
|
|
1359
|
+
}
|
|
1360
|
+
];
|
|
1361
|
+
}
|
|
1362
|
+
if (f.activeTask) {
|
|
1363
|
+
return [
|
|
1364
|
+
{
|
|
1365
|
+
type: "instruction",
|
|
1366
|
+
requiresUserOk: true,
|
|
1367
|
+
message: tr(lang, "messages", "finishDoingTask", {
|
|
1368
|
+
title: f.activeTask.title,
|
|
1369
|
+
done: f.tasks.done,
|
|
1370
|
+
total: f.tasks.total
|
|
1371
|
+
})
|
|
1372
|
+
}
|
|
1373
|
+
];
|
|
1374
|
+
}
|
|
1375
|
+
if (f.nextTodoTask) {
|
|
1376
|
+
return [
|
|
1377
|
+
{
|
|
1378
|
+
type: "instruction",
|
|
1379
|
+
requiresUserOk: true,
|
|
1380
|
+
message: tr(lang, "messages", "startNextTodoTask", {
|
|
1381
|
+
title: f.nextTodoTask.title,
|
|
1382
|
+
done: f.tasks.done,
|
|
1383
|
+
total: f.tasks.total
|
|
1384
|
+
})
|
|
1385
|
+
}
|
|
1386
|
+
];
|
|
1387
|
+
}
|
|
1388
|
+
return [
|
|
1389
|
+
{
|
|
1390
|
+
type: "instruction",
|
|
1391
|
+
requiresUserOk: true,
|
|
1392
|
+
message: tr(lang, "messages", "checkTaskStatuses", {
|
|
1393
|
+
done: f.tasks.done,
|
|
1394
|
+
total: f.tasks.total
|
|
1395
|
+
})
|
|
1396
|
+
}
|
|
1397
|
+
];
|
|
1184
1398
|
}
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1187
|
-
},
|
|
1188
|
-
{
|
|
1189
|
-
step: 10,
|
|
1190
|
-
name: "\uBB38\uC11C \uCEE4\uBC0B",
|
|
1191
|
-
checklist: {
|
|
1192
|
-
done: (f) => isCompletionChecklistDone(f) && !f.git.docsHasUncommittedChanges
|
|
1399
|
+
}
|
|
1193
1400
|
},
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1401
|
+
{
|
|
1402
|
+
step: 11,
|
|
1403
|
+
name: tr(lang, "steps", "prCreate"),
|
|
1404
|
+
checklist: { done: (f) => isPrMetadataConfigured(f) && !!f.pr.link },
|
|
1405
|
+
current: {
|
|
1406
|
+
when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done && isCompletionChecklistDone(f) && (!isPrMetadataConfigured(f) || !f.pr.link),
|
|
1407
|
+
actions: (f) => {
|
|
1408
|
+
if (!isPrMetadataConfigured(f)) {
|
|
1409
|
+
return [
|
|
1410
|
+
{
|
|
1411
|
+
type: "instruction",
|
|
1412
|
+
requiresUserOk: true,
|
|
1413
|
+
message: tr(lang, "messages", "prLegacyAsk")
|
|
1414
|
+
}
|
|
1415
|
+
];
|
|
1416
|
+
}
|
|
1417
|
+
return [
|
|
1418
|
+
{
|
|
1419
|
+
type: "instruction",
|
|
1420
|
+
requiresUserOk: true,
|
|
1421
|
+
message: tr(lang, "messages", "prCreate")
|
|
1422
|
+
}
|
|
1423
|
+
];
|
|
1202
1424
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1425
|
+
}
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
step: 12,
|
|
1429
|
+
name: tr(lang, "steps", "codeReview"),
|
|
1430
|
+
checklist: {
|
|
1431
|
+
done: (f) => isPrMetadataConfigured(f) && f.pr.status === "Approved"
|
|
1432
|
+
},
|
|
1433
|
+
current: {
|
|
1434
|
+
when: (f) => isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
|
|
1435
|
+
actions: (f) => {
|
|
1436
|
+
if (f.pr.status === "Review") {
|
|
1437
|
+
return [
|
|
1438
|
+
{
|
|
1439
|
+
type: "instruction",
|
|
1440
|
+
message: tr(lang, "messages", "prResolveReview")
|
|
1441
|
+
}
|
|
1442
|
+
];
|
|
1443
|
+
}
|
|
1444
|
+
return [
|
|
1445
|
+
{
|
|
1446
|
+
type: "instruction",
|
|
1447
|
+
message: tr(lang, "messages", "prRequestReview")
|
|
1448
|
+
}
|
|
1449
|
+
];
|
|
1207
1450
|
}
|
|
1208
|
-
|
|
1451
|
+
}
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
step: 13,
|
|
1455
|
+
name: tr(lang, "steps", "featureDone"),
|
|
1456
|
+
checklist: { done: (f) => isFeatureDone(f) },
|
|
1457
|
+
current: {
|
|
1458
|
+
when: (f) => isFeatureDone(f),
|
|
1459
|
+
actions: () => [
|
|
1460
|
+
{
|
|
1461
|
+
type: "instruction",
|
|
1462
|
+
message: tr(lang, "messages", "featureDone")
|
|
1463
|
+
}
|
|
1464
|
+
]
|
|
1465
|
+
}
|
|
1209
1466
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1467
|
+
];
|
|
1468
|
+
}
|
|
1469
|
+
function getStepsMap(lang) {
|
|
1470
|
+
return Object.fromEntries(getStepDefinitions(lang).map((d) => [d.step, d.name]));
|
|
1471
|
+
}
|
|
1472
|
+
getStepDefinitions("ko");
|
|
1473
|
+
getStepsMap("ko");
|
|
1474
|
+
|
|
1475
|
+
// src/utils/context/progress.ts
|
|
1476
|
+
function resolveFeatureProgress(feature, stepDefinitions, lang) {
|
|
1477
|
+
const ordered = [...stepDefinitions].sort((a, b) => a.step - b.step);
|
|
1217
1478
|
for (const definition of ordered) {
|
|
1218
1479
|
if (!definition.current) continue;
|
|
1219
1480
|
if (definition.current.when(feature)) {
|
|
@@ -1229,9 +1490,12 @@ function resolveFeatureProgress(feature) {
|
|
|
1229
1490
|
return {
|
|
1230
1491
|
currentStep: lastStep?.step ?? 10,
|
|
1231
1492
|
actions: [
|
|
1232
|
-
{
|
|
1493
|
+
{
|
|
1494
|
+
type: "instruction",
|
|
1495
|
+
message: tr(lang, "messages", "fallbackRerunContext")
|
|
1496
|
+
}
|
|
1233
1497
|
],
|
|
1234
|
-
nextAction: "
|
|
1498
|
+
nextAction: tr(lang, "messages", "fallbackRerunContext")
|
|
1235
1499
|
};
|
|
1236
1500
|
}
|
|
1237
1501
|
function getCurrentBranch(cwd) {
|
|
@@ -1245,6 +1509,72 @@ function getCurrentBranch(cwd) {
|
|
|
1245
1509
|
return "";
|
|
1246
1510
|
}
|
|
1247
1511
|
}
|
|
1512
|
+
function getGitStatusPorcelain(cwd, relativePaths) {
|
|
1513
|
+
try {
|
|
1514
|
+
const args = relativePaths.length > 0 ? ` -- ${relativePaths.map((p) => `"${p}"`).join(" ")}` : "";
|
|
1515
|
+
return execSync(`git status --porcelain=v1${args}`, {
|
|
1516
|
+
cwd,
|
|
1517
|
+
encoding: "utf-8",
|
|
1518
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1519
|
+
});
|
|
1520
|
+
} catch {
|
|
1521
|
+
return void 0;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function getGitTopLevel(cwd) {
|
|
1525
|
+
try {
|
|
1526
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
1527
|
+
cwd,
|
|
1528
|
+
encoding: "utf-8",
|
|
1529
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1530
|
+
}).trim();
|
|
1531
|
+
} catch {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
function resolveProjectGitCwd(config, repo) {
|
|
1536
|
+
const docsRepo = config.docsRepo;
|
|
1537
|
+
if (docsRepo !== "standalone") {
|
|
1538
|
+
const topLevel = getGitTopLevel(process.cwd());
|
|
1539
|
+
return { cwd: topLevel || process.cwd() };
|
|
1540
|
+
}
|
|
1541
|
+
if (!config.projectRoot) {
|
|
1542
|
+
return {
|
|
1543
|
+
cwd: null,
|
|
1544
|
+
warning: "standalone \uBAA8\uB4DC\uC785\uB2C8\uB2E4. projectRoot\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58 \uD655\uC778\uC774 \uBD88\uAC00\uB2A5\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)"
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
if (config.projectType === "fullstack") {
|
|
1548
|
+
if (typeof config.projectRoot === "string") {
|
|
1549
|
+
return {
|
|
1550
|
+
cwd: null,
|
|
1551
|
+
warning: 'fullstack standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: { "fe": "...", "be": "..." })'
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
const root = config.projectRoot[repo];
|
|
1555
|
+
if (!root) {
|
|
1556
|
+
return {
|
|
1557
|
+
cwd: null,
|
|
1558
|
+
warning: `projectRoot.${repo}\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ... --repo ${repo})`
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
return { cwd: getGitTopLevel(root) || root };
|
|
1562
|
+
}
|
|
1563
|
+
if (typeof config.projectRoot !== "string") {
|
|
1564
|
+
return {
|
|
1565
|
+
cwd: null,
|
|
1566
|
+
warning: 'single standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: "/path/to/project")'
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
return { cwd: getGitTopLevel(config.projectRoot) || config.projectRoot };
|
|
1570
|
+
}
|
|
1571
|
+
function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
|
|
1572
|
+
if (!branchName || !issueNumber) return false;
|
|
1573
|
+
const match = branchName.match(new RegExp(`^feat\\/${issueNumber}-(.+)$`));
|
|
1574
|
+
if (!match) return false;
|
|
1575
|
+
const rest = match[1];
|
|
1576
|
+
return rest === slug || rest === folderName;
|
|
1577
|
+
}
|
|
1248
1578
|
function escapeRegExp(value) {
|
|
1249
1579
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1250
1580
|
}
|
|
@@ -1279,15 +1609,21 @@ function parseIssueNumber(value) {
|
|
|
1279
1609
|
const match = value.match(/#?(\d+)/);
|
|
1280
1610
|
return match ? match[1] : void 0;
|
|
1281
1611
|
}
|
|
1612
|
+
function parsePrLink(value) {
|
|
1613
|
+
if (!value) return void 0;
|
|
1614
|
+
const trimmed = value.trim();
|
|
1615
|
+
if (!trimmed) return void 0;
|
|
1616
|
+
if (trimmed === "#" || trimmed === "-") return void 0;
|
|
1617
|
+
if (trimmed.includes("{") || trimmed.includes("}")) return void 0;
|
|
1618
|
+
return trimmed;
|
|
1619
|
+
}
|
|
1282
1620
|
function parseTasks(content) {
|
|
1283
1621
|
const summary = { total: 0, todo: 0, doing: 0, done: 0 };
|
|
1284
1622
|
let activeTask;
|
|
1285
1623
|
let nextTodoTask;
|
|
1286
1624
|
const lines = content.split("\n");
|
|
1287
1625
|
for (const line of lines) {
|
|
1288
|
-
const match = line.match(
|
|
1289
|
-
/^\s*-\s*\[([A-Z]+)\]((?:\[[^\]]+\])*)\s*(.+?)\s*$/
|
|
1290
|
-
);
|
|
1626
|
+
const match = line.match(/^\s*-\s*\[([A-Z]+)\]((?:\[[^\]]+\])*)\s*(.+?)\s*$/);
|
|
1291
1627
|
if (!match) continue;
|
|
1292
1628
|
const status = match[1].toUpperCase();
|
|
1293
1629
|
const title = match[3].trim();
|
|
@@ -1322,75 +1658,10 @@ function parseCompletionChecklist(content) {
|
|
|
1322
1658
|
}
|
|
1323
1659
|
return total > 0 ? { total, checked } : void 0;
|
|
1324
1660
|
}
|
|
1325
|
-
function
|
|
1326
|
-
|
|
1327
|
-
const args = relativePaths.length > 0 ? ` -- ${relativePaths.map((p) => `"${p}"`).join(" ")}` : "";
|
|
1328
|
-
return execSync(`git status --porcelain=v1${args}`, {
|
|
1329
|
-
cwd,
|
|
1330
|
-
encoding: "utf-8",
|
|
1331
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1332
|
-
});
|
|
1333
|
-
} catch {
|
|
1334
|
-
return void 0;
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
function getGitTopLevel(cwd) {
|
|
1338
|
-
try {
|
|
1339
|
-
return execSync("git rev-parse --show-toplevel", {
|
|
1340
|
-
cwd,
|
|
1341
|
-
encoding: "utf-8",
|
|
1342
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1343
|
-
}).trim();
|
|
1344
|
-
} catch {
|
|
1345
|
-
return null;
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
function resolveProjectGitCwd(config, repo) {
|
|
1349
|
-
const docsRepo = config.docsRepo;
|
|
1350
|
-
if (docsRepo !== "standalone") {
|
|
1351
|
-
const topLevel = getGitTopLevel(process.cwd());
|
|
1352
|
-
return { cwd: topLevel || process.cwd() };
|
|
1353
|
-
}
|
|
1354
|
-
if (!config.projectRoot) {
|
|
1355
|
-
return {
|
|
1356
|
-
cwd: null,
|
|
1357
|
-
warning: "standalone \uBAA8\uB4DC\uC785\uB2C8\uB2E4. projectRoot\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58 \uD655\uC778\uC774 \uBD88\uAC00\uB2A5\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)"
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
if (config.projectType === "fullstack") {
|
|
1361
|
-
if (typeof config.projectRoot === "string") {
|
|
1362
|
-
return {
|
|
1363
|
-
cwd: null,
|
|
1364
|
-
warning: 'fullstack standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: { "fe": "...", "be": "..." })'
|
|
1365
|
-
};
|
|
1366
|
-
}
|
|
1367
|
-
const root = config.projectRoot[repo];
|
|
1368
|
-
if (!root) {
|
|
1369
|
-
return {
|
|
1370
|
-
cwd: null,
|
|
1371
|
-
warning: `projectRoot.${repo}\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ... --repo ${repo})`
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1374
|
-
return { cwd: getGitTopLevel(root) || root };
|
|
1375
|
-
}
|
|
1376
|
-
if (typeof config.projectRoot !== "string") {
|
|
1377
|
-
return {
|
|
1378
|
-
cwd: null,
|
|
1379
|
-
warning: 'single standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: "/path/to/project")'
|
|
1380
|
-
};
|
|
1381
|
-
}
|
|
1382
|
-
return { cwd: getGitTopLevel(config.projectRoot) || config.projectRoot };
|
|
1383
|
-
}
|
|
1384
|
-
function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
|
|
1385
|
-
if (!branchName || !issueNumber) return false;
|
|
1386
|
-
const match = branchName.match(new RegExp(`^feat\\/${issueNumber}-(.+)$`));
|
|
1387
|
-
if (!match) return false;
|
|
1388
|
-
const rest = match[1];
|
|
1389
|
-
return rest === slug || rest === folderName;
|
|
1390
|
-
}
|
|
1391
|
-
async function parseFeature(featurePath, type, context) {
|
|
1661
|
+
async function parseFeature(featurePath, type, context, options) {
|
|
1662
|
+
const lang = options.lang;
|
|
1392
1663
|
const folderName = path4.basename(featurePath);
|
|
1393
|
-
const match = folderName.match(/^(F
|
|
1664
|
+
const match = folderName.match(/^(F\\d+)-(.+)$/);
|
|
1394
1665
|
const id = match?.[1];
|
|
1395
1666
|
const slug = match?.[2] || folderName;
|
|
1396
1667
|
const specPath = path4.join(featurePath, "spec.md");
|
|
@@ -1403,11 +1674,7 @@ async function parseFeature(featurePath, type, context) {
|
|
|
1403
1674
|
const content = await fs6.readFile(specPath, "utf-8");
|
|
1404
1675
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
1405
1676
|
specStatus = parseDocStatus(statusValue);
|
|
1406
|
-
const issueValue = extractFirstSpecValue(content, [
|
|
1407
|
-
"\uC774\uC288 \uBC88\uD638",
|
|
1408
|
-
"Issue Number",
|
|
1409
|
-
"Issue"
|
|
1410
|
-
]);
|
|
1677
|
+
const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
|
|
1411
1678
|
issueNumber = parseIssueNumber(issueValue);
|
|
1412
1679
|
}
|
|
1413
1680
|
let planStatus;
|
|
@@ -1422,6 +1689,10 @@ async function parseFeature(featurePath, type, context) {
|
|
|
1422
1689
|
let activeTask;
|
|
1423
1690
|
let nextTodoTask;
|
|
1424
1691
|
let completionChecklist;
|
|
1692
|
+
let prLink;
|
|
1693
|
+
let prStatus;
|
|
1694
|
+
let prFieldExists = false;
|
|
1695
|
+
let prStatusFieldExists = false;
|
|
1425
1696
|
if (tasksExists) {
|
|
1426
1697
|
const content = await fs6.readFile(tasksPath, "utf-8");
|
|
1427
1698
|
const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
|
|
@@ -1432,12 +1703,20 @@ async function parseFeature(featurePath, type, context) {
|
|
|
1432
1703
|
activeTask = active;
|
|
1433
1704
|
nextTodoTask = nextTodo;
|
|
1434
1705
|
completionChecklist = parseCompletionChecklist(content);
|
|
1706
|
+
if (!issueNumber) {
|
|
1707
|
+
const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
|
|
1708
|
+
issueNumber = parseIssueNumber(issueValue);
|
|
1709
|
+
}
|
|
1710
|
+
const prValue = extractFirstSpecValue(content, ["PR", "Pull Request"]);
|
|
1711
|
+
prFieldExists = prValue !== void 0;
|
|
1712
|
+
prLink = parsePrLink(prValue);
|
|
1713
|
+
const prStatusValue = extractFirstSpecValue(content, ["PR \uC0C1\uD0DC", "PR Status"]);
|
|
1714
|
+
prStatusFieldExists = prStatusValue !== void 0;
|
|
1715
|
+
prStatus = parseDocStatus(prStatusValue);
|
|
1435
1716
|
}
|
|
1436
1717
|
const warnings = [];
|
|
1437
1718
|
if (context.projectBranchAvailable === false) {
|
|
1438
|
-
warnings.push(
|
|
1439
|
-
"\uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (standalone \uBAA8\uB4DC\uB77C\uBA74 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.)"
|
|
1440
|
-
);
|
|
1719
|
+
warnings.push(tr(lang, "warnings", "projectBranchUnavailable"));
|
|
1441
1720
|
}
|
|
1442
1721
|
const onExpectedBranch = isExpectedFeatureBranch(
|
|
1443
1722
|
context.projectBranch,
|
|
@@ -1446,14 +1725,13 @@ async function parseFeature(featurePath, type, context) {
|
|
|
1446
1725
|
folderName
|
|
1447
1726
|
);
|
|
1448
1727
|
const relativeFeaturePathFromDocs = path4.relative(context.docsDir, featurePath);
|
|
1449
|
-
const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [
|
|
1450
|
-
relativeFeaturePathFromDocs
|
|
1451
|
-
]);
|
|
1728
|
+
const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [relativeFeaturePathFromDocs]);
|
|
1452
1729
|
const docsHasUncommittedChanges = docsStatus === void 0 ? true : docsStatus.trim().length > 0;
|
|
1453
1730
|
if (docsStatus === void 0) {
|
|
1454
|
-
warnings.push(
|
|
1455
|
-
|
|
1456
|
-
|
|
1731
|
+
warnings.push(tr(lang, "warnings", "docsGitUnavailable"));
|
|
1732
|
+
}
|
|
1733
|
+
if (tasksExists && (!prFieldExists || !prStatusFieldExists)) {
|
|
1734
|
+
warnings.push(tr(lang, "warnings", "legacyTasksPrFields"));
|
|
1457
1735
|
}
|
|
1458
1736
|
const featureState = {
|
|
1459
1737
|
id,
|
|
@@ -1468,6 +1746,7 @@ async function parseFeature(featurePath, type, context) {
|
|
|
1468
1746
|
activeTask,
|
|
1469
1747
|
nextTodoTask,
|
|
1470
1748
|
completionChecklist,
|
|
1749
|
+
pr: { link: prLink, status: prStatus },
|
|
1471
1750
|
git: {
|
|
1472
1751
|
docsBranch: context.docsBranch,
|
|
1473
1752
|
projectBranch: context.projectBranch,
|
|
@@ -1481,21 +1760,22 @@ async function parseFeature(featurePath, type, context) {
|
|
|
1481
1760
|
featurePathFromDocs: relativeFeaturePathFromDocs,
|
|
1482
1761
|
specExists,
|
|
1483
1762
|
planExists,
|
|
1484
|
-
tasksExists
|
|
1763
|
+
tasksExists,
|
|
1764
|
+
prFieldExists,
|
|
1765
|
+
prStatusFieldExists
|
|
1485
1766
|
}
|
|
1486
1767
|
};
|
|
1487
|
-
const { currentStep, actions, nextAction } = resolveFeatureProgress(
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
warnings
|
|
1494
|
-
};
|
|
1768
|
+
const { currentStep, actions, nextAction } = resolveFeatureProgress(
|
|
1769
|
+
featureState,
|
|
1770
|
+
options.stepDefinitions,
|
|
1771
|
+
lang
|
|
1772
|
+
);
|
|
1773
|
+
return { ...featureState, currentStep, actions, nextAction, warnings };
|
|
1495
1774
|
}
|
|
1496
1775
|
async function scanFeatures(config) {
|
|
1497
1776
|
const features = [];
|
|
1498
1777
|
const warnings = [];
|
|
1778
|
+
const stepDefinitions = getStepDefinitions(config.lang);
|
|
1499
1779
|
const docsBranch = getCurrentBranch(config.docsDir);
|
|
1500
1780
|
const projectBranches = {
|
|
1501
1781
|
single: "",
|
|
@@ -1526,51 +1806,60 @@ async function scanFeatures(config) {
|
|
|
1526
1806
|
for (const dir of featureDirs) {
|
|
1527
1807
|
if ((await fs6.stat(dir)).isDirectory()) {
|
|
1528
1808
|
features.push(
|
|
1529
|
-
await parseFeature(
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1809
|
+
await parseFeature(
|
|
1810
|
+
dir,
|
|
1811
|
+
"single",
|
|
1812
|
+
{
|
|
1813
|
+
projectBranch: projectBranches.single,
|
|
1814
|
+
docsBranch,
|
|
1815
|
+
docsGitCwd: config.docsDir,
|
|
1816
|
+
projectGitCwd: singleProject?.cwd ?? void 0,
|
|
1817
|
+
docsDir: config.docsDir,
|
|
1818
|
+
projectBranchAvailable: Boolean(singleProject?.cwd)
|
|
1819
|
+
},
|
|
1820
|
+
{ lang: config.lang, stepDefinitions }
|
|
1821
|
+
)
|
|
1537
1822
|
);
|
|
1538
1823
|
}
|
|
1539
1824
|
}
|
|
1540
1825
|
} else {
|
|
1541
|
-
const feDirs = await glob("features/fe/*/", {
|
|
1542
|
-
|
|
1543
|
-
absolute: true
|
|
1544
|
-
});
|
|
1545
|
-
const beDirs = await glob("features/be/*/", {
|
|
1546
|
-
cwd: config.docsDir,
|
|
1547
|
-
absolute: true
|
|
1548
|
-
});
|
|
1826
|
+
const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
|
|
1827
|
+
const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
|
|
1549
1828
|
for (const dir of feDirs) {
|
|
1550
1829
|
if ((await fs6.stat(dir)).isDirectory()) {
|
|
1551
1830
|
features.push(
|
|
1552
|
-
await parseFeature(
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1831
|
+
await parseFeature(
|
|
1832
|
+
dir,
|
|
1833
|
+
"fe",
|
|
1834
|
+
{
|
|
1835
|
+
projectBranch: projectBranches.fe,
|
|
1836
|
+
docsBranch,
|
|
1837
|
+
docsGitCwd: config.docsDir,
|
|
1838
|
+
projectGitCwd: feProject?.cwd ?? void 0,
|
|
1839
|
+
docsDir: config.docsDir,
|
|
1840
|
+
projectBranchAvailable: Boolean(feProject?.cwd)
|
|
1841
|
+
},
|
|
1842
|
+
{ lang: config.lang, stepDefinitions }
|
|
1843
|
+
)
|
|
1560
1844
|
);
|
|
1561
1845
|
}
|
|
1562
1846
|
}
|
|
1563
1847
|
for (const dir of beDirs) {
|
|
1564
1848
|
if ((await fs6.stat(dir)).isDirectory()) {
|
|
1565
1849
|
features.push(
|
|
1566
|
-
await parseFeature(
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1850
|
+
await parseFeature(
|
|
1851
|
+
dir,
|
|
1852
|
+
"be",
|
|
1853
|
+
{
|
|
1854
|
+
projectBranch: projectBranches.be,
|
|
1855
|
+
docsBranch,
|
|
1856
|
+
docsGitCwd: config.docsDir,
|
|
1857
|
+
projectGitCwd: beProject?.cwd ?? void 0,
|
|
1858
|
+
docsDir: config.docsDir,
|
|
1859
|
+
projectBranchAvailable: Boolean(beProject?.cwd)
|
|
1860
|
+
},
|
|
1861
|
+
{ lang: config.lang, stepDefinitions }
|
|
1862
|
+
)
|
|
1574
1863
|
);
|
|
1575
1864
|
}
|
|
1576
1865
|
}
|
|
@@ -1579,9 +1868,7 @@ async function scanFeatures(config) {
|
|
|
1579
1868
|
features,
|
|
1580
1869
|
branches: {
|
|
1581
1870
|
docs: docsBranch,
|
|
1582
|
-
project: {
|
|
1583
|
-
...config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
|
|
1584
|
-
}
|
|
1871
|
+
project: config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
|
|
1585
1872
|
},
|
|
1586
1873
|
warnings
|
|
1587
1874
|
};
|
|
@@ -1628,9 +1915,12 @@ function detectFromBranch(branchName, features) {
|
|
|
1628
1915
|
async function runContext(featureName, options) {
|
|
1629
1916
|
const cwd = process.cwd();
|
|
1630
1917
|
const config = await getConfig(cwd);
|
|
1918
|
+
const lang = config?.lang ?? "ko";
|
|
1631
1919
|
if (!config) {
|
|
1632
1920
|
throw new Error("\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
|
|
1633
1921
|
}
|
|
1922
|
+
const stepDefinitions = getStepDefinitions(lang);
|
|
1923
|
+
const stepsMap = getStepsMap(lang);
|
|
1634
1924
|
const { features, branches, warnings } = await scanFeatures(config);
|
|
1635
1925
|
let targetFeatures = [];
|
|
1636
1926
|
if (featureName) {
|
|
@@ -1728,7 +2018,7 @@ async function runContext(featureName, options) {
|
|
|
1728
2018
|
);
|
|
1729
2019
|
console.log();
|
|
1730
2020
|
targetFeatures.forEach((f2) => {
|
|
1731
|
-
const stepName2 =
|
|
2021
|
+
const stepName2 = stepsMap[f2.currentStep] || "Unknown";
|
|
1732
2022
|
const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
|
|
1733
2023
|
console.log(
|
|
1734
2024
|
` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
|
|
@@ -1743,7 +2033,8 @@ async function runContext(featureName, options) {
|
|
|
1743
2033
|
return;
|
|
1744
2034
|
}
|
|
1745
2035
|
const f = targetFeatures[0];
|
|
1746
|
-
const stepName =
|
|
2036
|
+
const stepName = stepsMap[f.currentStep] || "Unknown";
|
|
2037
|
+
const okTag = (requiresUserOk) => requiresUserOk ? chalk6.yellow(lang === "ko" ? "[OK \uD544\uC694] " : "[OK required] ") : "";
|
|
1747
2038
|
console.log(
|
|
1748
2039
|
`\u{1F539} Feature: ${chalk6.bold(f.folderName)} ${config.projectType === "fullstack" ? chalk6.cyan(`(${f.type})`) : ""}`
|
|
1749
2040
|
);
|
|
@@ -1762,12 +2053,12 @@ async function runContext(featureName, options) {
|
|
|
1762
2053
|
console.log(
|
|
1763
2054
|
` \u2022 Active Task: ${chalk6.yellow(`[${f.activeTask.status}]`)} ${f.activeTask.title}`
|
|
1764
2055
|
);
|
|
1765
|
-
} else if (f.nextTodoTask && f.currentStep ===
|
|
2056
|
+
} else if (f.nextTodoTask && f.currentStep === 10) {
|
|
1766
2057
|
console.log(
|
|
1767
2058
|
` \u2022 Next TODO: ${chalk6.gray(`[${f.nextTodoTask.status}]`)} ${f.nextTodoTask.title}`
|
|
1768
2059
|
);
|
|
1769
2060
|
}
|
|
1770
|
-
printChecklist(f);
|
|
2061
|
+
printChecklist(f, stepDefinitions);
|
|
1771
2062
|
if (f.warnings.length > 0) {
|
|
1772
2063
|
console.log();
|
|
1773
2064
|
console.log(chalk6.yellow("\u26A0\uFE0F Feature Warnings:"));
|
|
@@ -1784,10 +2075,12 @@ async function runContext(featureName, options) {
|
|
|
1784
2075
|
const action = f.actions[0];
|
|
1785
2076
|
if (action.type === "command") {
|
|
1786
2077
|
console.log(
|
|
1787
|
-
`\u{1F449} Next Action (${chalk6.cyan(action.scope)}): ${chalk6.green(chalk6.bold(action.cmd))}`
|
|
2078
|
+
`\u{1F449} Next Action (${chalk6.cyan(action.scope)}): ${okTag(action.requiresUserOk)}${chalk6.green(chalk6.bold(action.cmd))}`
|
|
1788
2079
|
);
|
|
1789
2080
|
} else {
|
|
1790
|
-
console.log(
|
|
2081
|
+
console.log(
|
|
2082
|
+
`\u{1F449} Next Action: ${okTag(action.requiresUserOk)}${chalk6.green(chalk6.bold(action.message))}`
|
|
2083
|
+
);
|
|
1791
2084
|
}
|
|
1792
2085
|
console.log();
|
|
1793
2086
|
return;
|
|
@@ -1795,15 +2088,15 @@ async function runContext(featureName, options) {
|
|
|
1795
2088
|
console.log(chalk6.green(chalk6.bold("\u{1F449} Next Actions:")));
|
|
1796
2089
|
f.actions.forEach((action) => {
|
|
1797
2090
|
if (action.type === "command") {
|
|
1798
|
-
console.log(` \u2022 (${action.scope}) ${action.cmd}`);
|
|
2091
|
+
console.log(` \u2022 (${action.scope}) ${okTag(action.requiresUserOk)}${action.cmd}`);
|
|
1799
2092
|
} else {
|
|
1800
|
-
console.log(` \u2022 ${action.message}`);
|
|
2093
|
+
console.log(` \u2022 ${okTag(action.requiresUserOk)}${action.message}`);
|
|
1801
2094
|
}
|
|
1802
2095
|
});
|
|
1803
2096
|
console.log();
|
|
1804
2097
|
}
|
|
1805
|
-
function printChecklist(f) {
|
|
1806
|
-
const checklistSteps = [...
|
|
2098
|
+
function printChecklist(f, stepDefinitions) {
|
|
2099
|
+
const checklistSteps = [...stepDefinitions].sort((a, b) => a.step - b.step);
|
|
1807
2100
|
checklistSteps.forEach((definition) => {
|
|
1808
2101
|
const done = definition.checklist.done(f);
|
|
1809
2102
|
const detail = definition.checklist.detail?.(f) ?? "";
|
package/package.json
CHANGED