lee-spec-kit 0.4.8 → 0.4.10

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 CHANGED
@@ -145,6 +145,9 @@ npx lee-spec-kit doctor --json
145
145
 
146
146
  ### Update templates
147
147
 
148
+ By default, `update` runs only when the `docs/` working tree is clean; in that case it overwrites changed files without prompting.
149
+ If you want to update while you have uncommitted changes, use `--force`.
150
+
148
151
  ```bash
149
152
  npx lee-spec-kit update
150
153
  npx lee-spec-kit update --agents
@@ -176,11 +179,13 @@ Running `init` creates `.lee-spec-kit.json` in your docs root (default: `docs/`)
176
179
  | `lang` | `ko` or `en` |
177
180
  | `createdAt` | Creation date |
178
181
  | `docsRepo` | `embedded` or `standalone` |
179
- | `projectRoot` | (standalone only) path to the project repository |
182
+ | `pushDocs` | (standalone only) whether to manage/push docs repo as a separate git repo |
183
+ | `docsRemote` | (standalone + pushDocs) docs repo remote URL |
184
+ | `projectRoot` | (standalone only) project repo path (single: string, fullstack: {fe, be}) |
180
185
 
186
+ > In standalone mode, `init` can add `pushDocs`, `docsRemote`, and `projectRoot` to this config.
181
187
  > If you run the CLI outside the docs repo in standalone mode, set `LEE_SPEC_KIT_DOCS_DIR` to the docs repo path.
182
188
 
183
189
  ## Generated Structure
184
190
 
185
191
  See the Korean README for the full tree examples and workflow details: `README.md`.
186
-
package/README.md CHANGED
@@ -177,6 +177,9 @@ npx lee-spec-kit doctor --json
177
177
 
178
178
  ### 템플릿 업데이트
179
179
 
180
+ 기본 동작은 `docs/` 작업트리에 변경사항이 없을 때만 업데이트를 진행하며, 이 경우 변경된 파일은 확인 없이 덮어씁니다.
181
+ 변경사항이 있는 상태에서 업데이트하려면 `--force`를 사용하세요.
182
+
180
183
  ```bash
181
184
  # 전체 업데이트
182
185
  npx lee-spec-kit update
@@ -190,7 +193,7 @@ npx lee-spec-kit update --skills
190
193
  # feature-base/ 폴더만 업데이트
191
194
  npx lee-spec-kit update --templates
192
195
 
193
- # 확인 없이 강제 덮어쓰기
196
+ # 변경사항이 있어도 강제 덮어쓰기
194
197
  npx lee-spec-kit update --force
195
198
  ```
196
199
 
@@ -217,7 +220,9 @@ npx lee-spec-kit update --force
217
220
  | `lang` | `ko` 또는 `en` |
218
221
  | `createdAt` | 생성 날짜 |
219
222
  | `docsRepo` | `embedded` 또는 `standalone` |
220
- | `projectRoot` | (standalone만) 프로젝트 레포지토리 경로 |
223
+ | `pushDocs` | (standalone만) docs 레포를 별도 Git으로 관리/푸시할지 여부 |
224
+ | `docsRemote` | (standalone+pushDocs) docs 레포 remote URL |
225
+ | `projectRoot` | (standalone만) 프로젝트 레포지토리 경로 (single: string, fullstack: {fe, be}) |
221
226
 
222
227
  > `docsRepo: "standalone"`을 선택하면 `pushDocs`, `docsRemote`, `projectRoot`가 추가됩니다.
223
228
 
@@ -232,7 +237,10 @@ npx lee-spec-kit update --force
232
237
  {
233
238
  "projectName": "my-project",
234
239
  "projectType": "single",
240
+ "lang": "ko",
241
+ "createdAt": "YYYY-MM-DD",
235
242
  "docsRepo": "standalone",
243
+ "pushDocs": false,
236
244
  "projectRoot": "/path/to/my-project"
237
245
  }
238
246
  ```
@@ -243,7 +251,10 @@ npx lee-spec-kit update --force
243
251
  {
244
252
  "projectName": "my-project",
245
253
  "projectType": "fullstack",
254
+ "lang": "ko",
255
+ "createdAt": "YYYY-MM-DD",
246
256
  "docsRepo": "standalone",
257
+ "pushDocs": false,
247
258
  "projectRoot": {
248
259
  "fe": "/path/to/frontend",
249
260
  "be": "/path/to/backend"
package/dist/index.js CHANGED
@@ -18,21 +18,25 @@ async function copyTemplates(src, dest) {
18
18
  errorOnExist: false
19
19
  });
20
20
  }
21
+ function applyReplacements(content, replacements) {
22
+ const keys = Object.keys(replacements).sort((a, b) => b.length - a.length);
23
+ let next = content;
24
+ for (const key of keys) {
25
+ next = next.replaceAll(key, replacements[key]);
26
+ }
27
+ return next;
28
+ }
21
29
  async function replaceInFiles(dir, replacements) {
22
30
  const files = await glob("**/*.md", { cwd: dir, absolute: true });
23
31
  for (const file of files) {
24
32
  let content = await fs8.readFile(file, "utf-8");
25
- for (const [search, replace] of Object.entries(replacements)) {
26
- content = content.replaceAll(search, replace);
27
- }
33
+ content = applyReplacements(content, replacements);
28
34
  await fs8.writeFile(file, content, "utf-8");
29
35
  }
30
36
  const shFiles = await glob("**/*.sh", { cwd: dir, absolute: true });
31
37
  for (const file of shFiles) {
32
38
  let content = await fs8.readFile(file, "utf-8");
33
- for (const [search, replace] of Object.entries(replacements)) {
34
- content = content.replaceAll(search, replace);
35
- }
39
+ content = applyReplacements(content, replacements);
36
40
  await fs8.writeFile(file, content, "utf-8");
37
41
  }
38
42
  }
@@ -93,6 +97,8 @@ var I18N = {
93
97
  "update.updatedTotal": "\uCD1D {count}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!",
94
98
  "update.changeDetected": "\uBCC0\uACBD \uAC10\uC9C0 (--force\uB85C \uB36E\uC5B4\uC4F0\uAE30)",
95
99
  "update.fileUpdated": "{file} \uC5C5\uB370\uC774\uD2B8",
100
+ "update.gitStatusUnavailable": "git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (git repo\uAC00 \uC544\uB2C8\uAC70\uB098 git \uC2E4\uD589 \uBD88\uAC00) --force\uB85C \uAC15\uC81C \uB36E\uC5B4\uC4F0\uAE30\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
101
+ "update.docsWorktreeDirty": "docs \uC791\uC5C5\uD2B8\uB9AC\uC5D0 \uBCC0\uACBD\uC0AC\uD56D\uC774 \uC788\uC5B4 update\uB97C \uC9C4\uD589\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBCC0\uACBD\uC0AC\uD56D\uC744 \uCEE4\uBC0B/\uC2A4\uD0DC\uC2DC \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uAC70\uB098 --force\uB85C \uB36E\uC5B4\uC4F0\uC138\uC694.",
96
102
  "doctor.title": "\u{1F50E} \uBB38\uC11C \uC9C4\uB2E8",
97
103
  "doctor.envWarnings": "\u26A0\uFE0F \uD658\uACBD \uACBD\uACE0:",
98
104
  "doctor.noIssues": "\u2705 \uBB38\uC81C\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
@@ -182,10 +188,12 @@ var I18N = {
182
188
  planWrite: "plan.md \uC791\uC131",
183
189
  planApprove: "plan.md \uC2B9\uC778",
184
190
  tasksWrite: "tasks.md \uC791\uC131",
191
+ docsInitialCommit: "\uCD08\uAE30 \uBB38\uC11C \uCEE4\uBC0B",
185
192
  docsCommitPlanning: "\uBB38\uC11C \uCEE4\uBC0B(\uB3D9\uAE30\uD654)",
186
193
  issueCreate: "GitHub Issue \uC0DD\uC131",
187
194
  branchCreate: "\uBE0C\uB79C\uCE58 \uC0DD\uC131",
188
195
  tasksExecute: "\uD0DC\uC2A4\uD06C \uC2E4\uD589",
196
+ docsCommitSync: "\uBB38\uC11C \uCEE4\uBC0B(\uB3D9\uAE30\uD654)",
189
197
  prCreate: "PR \uC0DD\uC131",
190
198
  codeReview: "\uCF54\uB4DC \uB9AC\uBDF0",
191
199
  featureDone: "Feature \uC644\uB8CC"
@@ -202,6 +210,7 @@ var I18N = {
202
210
  docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} \uAE30\uD68D \uBB38\uC11C"',
203
211
  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)",
204
212
  docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
213
+ docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
205
214
  standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
206
215
  createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
207
216
  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.',
@@ -266,6 +275,8 @@ var I18N = {
266
275
  "update.updatedTotal": "Updated {count} files!",
267
276
  "update.changeDetected": "changes detected (use --force to overwrite)",
268
277
  "update.fileUpdated": "{file} updated",
278
+ "update.gitStatusUnavailable": "Cannot determine git status (not a git repo or git unavailable). Use --force to overwrite.",
279
+ "update.docsWorktreeDirty": "Docs working tree has changes. Commit/stash your changes, or run with --force to overwrite.",
269
280
  "doctor.title": "\u{1F50E} Docs Doctor",
270
281
  "doctor.envWarnings": "\u26A0\uFE0F Environment warnings:",
271
282
  "doctor.noIssues": "\u2705 No issues found.",
@@ -355,10 +366,12 @@ var I18N = {
355
366
  planWrite: "Write plan.md",
356
367
  planApprove: "Approve plan.md",
357
368
  tasksWrite: "Write tasks.md",
369
+ docsInitialCommit: "Initial docs commit",
358
370
  docsCommitPlanning: "Commit docs (sync)",
359
371
  issueCreate: "Create GitHub Issue",
360
372
  branchCreate: "Create branch",
361
373
  tasksExecute: "Execute tasks",
374
+ docsCommitSync: "Commit docs (sync)",
362
375
  prCreate: "Create PR",
363
376
  codeReview: "Code review",
364
377
  featureDone: "Feature done"
@@ -375,6 +388,7 @@ var I18N = {
375
388
  docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} planning docs"',
376
389
  issueCreateAndWrite: "Create a GitHub Issue, fill the issue number in spec.md/tasks.md, then prepare a docs commit. (See skills/create-issue.md)",
377
390
  docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
391
+ docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} docs update"',
378
392
  standaloneNeedsProjectRoot: "Standalone mode requires projectRoot. (npx lee-spec-kit config --project-root ...)",
379
393
  createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
380
394
  tasksAllDoneButNoChecklist: 'All tasks are DONE, but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
@@ -983,6 +997,7 @@ async function runFeature(name, options) {
983
997
  process.exit(1);
984
998
  }
985
999
  const { docsDir, projectType, lang } = config;
1000
+ const projectName = config.projectName;
986
1001
  assertValid(validateSafeName(name), "\uAE30\uB2A5 \uC774\uB984");
987
1002
  let repo = options.repo;
988
1003
  if (projectType === "fullstack" && !repo) {
@@ -1043,6 +1058,7 @@ async function runFeature(name, options) {
1043
1058
  const idNumber = featureId.replace("F", "");
1044
1059
  const repoName = projectType === "fullstack" && repo ? `{{projectName}}-${repo}` : "{{projectName}}";
1045
1060
  const replacements = {
1061
+ "{{projectName}}": projectName ?? "{{projectName}}",
1046
1062
  // ko placeholders
1047
1063
  "{\uAE30\uB2A5\uBA85}": name,
1048
1064
  "{\uBC88\uD638}": idNumber,
@@ -1224,7 +1240,7 @@ function getStepDefinitions(lang) {
1224
1240
  },
1225
1241
  {
1226
1242
  step: 7,
1227
- name: tr(lang, "steps", "docsCommitPlanning"),
1243
+ name: tr(lang, "steps", "docsInitialCommit"),
1228
1244
  checklist: {
1229
1245
  done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && f.git.docsEverCommitted
1230
1246
  },
@@ -1356,6 +1372,26 @@ function getStepDefinitions(lang) {
1356
1372
  ];
1357
1373
  }
1358
1374
  if (f.nextTodoTask) {
1375
+ if (f.git.docsHasUncommittedChanges) {
1376
+ return [
1377
+ {
1378
+ type: "command",
1379
+ requiresUserOk: true,
1380
+ scope: "docs",
1381
+ cwd: f.git.docsGitCwd,
1382
+ cmd: f.issueNumber ? tr(lang, "messages", "docsCommitIssueUpdate", {
1383
+ docsGitCwd: f.git.docsGitCwd,
1384
+ featurePath: f.docs.featurePathFromDocs,
1385
+ issueNumber: f.issueNumber,
1386
+ folderName: f.folderName
1387
+ }) : tr(lang, "messages", "docsCommitUpdate", {
1388
+ docsGitCwd: f.git.docsGitCwd,
1389
+ featurePath: f.docs.featurePathFromDocs,
1390
+ folderName: f.folderName
1391
+ })
1392
+ }
1393
+ ];
1394
+ }
1359
1395
  return [
1360
1396
  {
1361
1397
  type: "instruction",
@@ -1383,6 +1419,34 @@ function getStepDefinitions(lang) {
1383
1419
  },
1384
1420
  {
1385
1421
  step: 11,
1422
+ name: tr(lang, "steps", "docsCommitSync"),
1423
+ checklist: {
1424
+ done: (f) => !f.git.docsHasUncommittedChanges
1425
+ },
1426
+ current: {
1427
+ when: (f) => isImplementationDone(f) && f.git.docsHasUncommittedChanges,
1428
+ actions: (f) => [
1429
+ {
1430
+ type: "command",
1431
+ requiresUserOk: true,
1432
+ scope: "docs",
1433
+ cwd: f.git.docsGitCwd,
1434
+ cmd: f.issueNumber ? tr(lang, "messages", "docsCommitIssueUpdate", {
1435
+ docsGitCwd: f.git.docsGitCwd,
1436
+ featurePath: f.docs.featurePathFromDocs,
1437
+ issueNumber: f.issueNumber,
1438
+ folderName: f.folderName
1439
+ }) : tr(lang, "messages", "docsCommitUpdate", {
1440
+ docsGitCwd: f.git.docsGitCwd,
1441
+ featurePath: f.docs.featurePathFromDocs,
1442
+ folderName: f.folderName
1443
+ })
1444
+ }
1445
+ ]
1446
+ }
1447
+ },
1448
+ {
1449
+ step: 12,
1386
1450
  name: tr(lang, "steps", "prCreate"),
1387
1451
  checklist: { done: (f) => isPrMetadataConfigured(f) && !!f.pr.link },
1388
1452
  current: {
@@ -1408,7 +1472,7 @@ function getStepDefinitions(lang) {
1408
1472
  }
1409
1473
  },
1410
1474
  {
1411
- step: 12,
1475
+ step: 13,
1412
1476
  name: tr(lang, "steps", "codeReview"),
1413
1477
  checklist: {
1414
1478
  done: (f) => isPrMetadataConfigured(f) && f.pr.status === "Approved"
@@ -1443,7 +1507,7 @@ function getStepDefinitions(lang) {
1443
1507
  }
1444
1508
  },
1445
1509
  {
1446
- step: 13,
1510
+ step: 14,
1447
1511
  name: tr(lang, "steps", "featureDone"),
1448
1512
  checklist: { done: (f) => isFeatureDone(f) },
1449
1513
  current: {
@@ -2065,18 +2129,28 @@ async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderNa
2065
2129
  return fallbackSlug || fallbackFolderName;
2066
2130
  }
2067
2131
  function updateCommand(program2) {
2068
- program2.command("update").description("Update docs templates to the latest version").option("--agents", "Update agents/ folder only").option("--skills", "Update agents/skills folder only").option("--templates", "Update feature-base/ folder only").option("-f, --force", "Force overwrite without confirmation").action(async (options) => {
2132
+ program2.command("update").description("Update docs templates to the latest version").option("--agents", "Update agents/ folder only").option("--skills", "Update agents/skills folder only").option("--templates", "Update feature-base/ folder only").option(
2133
+ "-f, --force",
2134
+ "Force overwrite even if docs has uncommitted changes"
2135
+ ).action(async (options) => {
2069
2136
  try {
2070
2137
  await runUpdate(options);
2071
2138
  } catch (error) {
2139
+ const config = await getConfig(process.cwd());
2140
+ const lang = config?.lang ?? DEFAULT_LANG;
2072
2141
  if (error instanceof Error && error.message === "canceled") {
2073
- const config = await getConfig(process.cwd());
2074
- const lang = config?.lang ?? DEFAULT_LANG;
2075
2142
  console.log(chalk6.yellow(`
2076
2143
  ${tr(lang, "cli", "common.canceled")}`));
2077
2144
  process.exit(0);
2078
2145
  }
2079
- console.error(chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")), error);
2146
+ if (error instanceof Error) {
2147
+ console.error(
2148
+ chalk6.red(tr(lang, "cli", "common.errorLabel")),
2149
+ chalk6.red(error.message)
2150
+ );
2151
+ } else {
2152
+ console.error(chalk6.red(tr(lang, "cli", "common.errorLabel")), error);
2153
+ }
2080
2154
  process.exit(1);
2081
2155
  }
2082
2156
  });
@@ -2096,6 +2170,7 @@ async function runUpdate(options) {
2096
2170
  const { docsDir, projectType, lang } = config;
2097
2171
  const templatesDir = getTemplatesDir();
2098
2172
  const sourceDir = path4.join(templatesDir, lang, projectType);
2173
+ const forceOverwrite = !!options.force || await isDocsWorktreeCleanOrThrow(docsDir, lang);
2099
2174
  const hasExplicitSelection = !!(options.agents || options.skills || options.templates);
2100
2175
  const updateAgents = options.agents || options.skills || !hasExplicitSelection;
2101
2176
  const updateTemplates = options.templates || !hasExplicitSelection;
@@ -2120,15 +2195,20 @@ async function runUpdate(options) {
2120
2195
  const typeAgents = agentsMode === "skills" ? path4.join(typeAgentsBase, "skills") : typeAgentsBase;
2121
2196
  const targetAgents = agentsMode === "skills" ? path4.join(targetAgentsBase, "skills") : targetAgentsBase;
2122
2197
  const featurePath = projectType === "fullstack" ? "docs/features/{be|fe}" : "docs/features";
2123
- const replacements = {
2198
+ const projectName = config.projectName ?? "{{projectName}}";
2199
+ const commonReplacements = {
2200
+ "{{projectName}}": projectName,
2124
2201
  "{{featurePath}}": featurePath
2125
2202
  };
2203
+ const typeReplacements = {
2204
+ "{{projectName}}": projectName
2205
+ };
2126
2206
  if (await fs8.pathExists(commonAgents)) {
2127
2207
  const count = await updateFolder(
2128
2208
  commonAgents,
2129
2209
  targetAgents,
2130
- options.force,
2131
- replacements,
2210
+ forceOverwrite,
2211
+ commonReplacements,
2132
2212
  lang
2133
2213
  );
2134
2214
  updatedCount += count;
@@ -2137,8 +2217,8 @@ async function runUpdate(options) {
2137
2217
  const count = await updateFolder(
2138
2218
  typeAgents,
2139
2219
  targetAgents,
2140
- options.force,
2141
- void 0,
2220
+ forceOverwrite,
2221
+ typeReplacements,
2142
2222
  lang
2143
2223
  );
2144
2224
  updatedCount += count;
@@ -2154,11 +2234,14 @@ async function runUpdate(options) {
2154
2234
  const sourceFeatureBase = path4.join(sourceDir, "features", "feature-base");
2155
2235
  const targetFeatureBase = path4.join(docsDir, "features", "feature-base");
2156
2236
  if (await fs8.pathExists(sourceFeatureBase)) {
2237
+ const replacements = {
2238
+ "{{projectName}}": config.projectName ?? "{{projectName}}"
2239
+ };
2157
2240
  const count = await updateFolder(
2158
2241
  sourceFeatureBase,
2159
2242
  targetFeatureBase,
2160
- options.force,
2161
- void 0,
2243
+ forceOverwrite,
2244
+ replacements,
2162
2245
  lang
2163
2246
  );
2164
2247
  updatedCount += count;
@@ -2191,9 +2274,7 @@ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DE
2191
2274
  }
2192
2275
  let sourceContent = await fs8.readFile(sourcePath, "utf-8");
2193
2276
  if (replacements) {
2194
- for (const [key, value] of Object.entries(replacements)) {
2195
- sourceContent = sourceContent.replaceAll(key, value);
2196
- }
2277
+ sourceContent = applyReplacements(sourceContent, replacements);
2197
2278
  }
2198
2279
  let shouldUpdate = true;
2199
2280
  if (await fs8.pathExists(targetPath)) {
@@ -2230,6 +2311,42 @@ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DE
2230
2311
  }
2231
2312
  return updatedCount;
2232
2313
  }
2314
+ function getGitTopLevel2(cwd) {
2315
+ try {
2316
+ const out = execFileSync("git", ["rev-parse", "--show-toplevel"], {
2317
+ cwd,
2318
+ encoding: "utf-8",
2319
+ stdio: ["ignore", "pipe", "ignore"]
2320
+ }).trim();
2321
+ return out || null;
2322
+ } catch {
2323
+ return null;
2324
+ }
2325
+ }
2326
+ function getDocsPorcelainStatus(docsDir) {
2327
+ const top = getGitTopLevel2(docsDir);
2328
+ if (!top) return null;
2329
+ const rel = path4.relative(top, docsDir) || ".";
2330
+ try {
2331
+ return execFileSync("git", ["status", "--porcelain=v1", "--", rel], {
2332
+ cwd: top,
2333
+ encoding: "utf-8",
2334
+ stdio: ["ignore", "pipe", "ignore"]
2335
+ });
2336
+ } catch {
2337
+ return null;
2338
+ }
2339
+ }
2340
+ async function isDocsWorktreeCleanOrThrow(docsDir, lang) {
2341
+ const status = getDocsPorcelainStatus(docsDir);
2342
+ if (status === null) {
2343
+ throw new Error(tr(lang, "cli", "update.gitStatusUnavailable"));
2344
+ }
2345
+ if (status.trim().length > 0) {
2346
+ throw new Error(tr(lang, "cli", "update.docsWorktreeDirty"));
2347
+ }
2348
+ return true;
2349
+ }
2233
2350
  function configCommand(program2) {
2234
2351
  program2.command("config").description("View or modify project configuration").option("--project-root <path>", "Set project root path").option("--repo <repo>", "Repository type for fullstack: fe | be").action(async (options) => {
2235
2352
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lee-spec-kit",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Project documentation structure generator for AI-assisted development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,14 +52,24 @@ For file links within the repo in PR body, **always use current branch name**:
52
52
  - Command: `{test command executed}`
53
53
  - Result: `{PASS/FAIL summary}`
54
54
 
55
- ## Screenshots (for UI changes)
55
+ ## Screenshots (Frontend / UI changes)
56
56
 
57
- {Attach if applicable}
57
+ > If you follow the Release assets upload flow in `skills/create-pr.md`, you can include images in the PR body without committing files to your branch.
58
+
59
+ {Screenshot markdown (e.g. ![](URL))}
60
+
61
+ ## Architecture Diagram (Backend / core structure changes)
62
+
63
+ ```mermaid
64
+ flowchart LR
65
+ A[Client] --> B[API]
66
+ B --> C[DB]
67
+ ```
58
68
 
59
69
  ## Related Documents
60
70
 
61
- - **Spec**: `docs/features/{be|fe}/F{number}-{feature-name}/spec.md`
62
- - **Tasks**: `docs/features/{be|fe}/F{number}-{feature-name}/tasks.md`
71
+ - **Spec**: `{{featurePath}}/F{number}-{feature-name}/spec.md`
72
+ - **Tasks**: `{{featurePath}}/F{number}-{feature-name}/tasks.md`
63
73
 
64
74
  Closes #{issue-number}
65
75
  ```
@@ -35,7 +35,54 @@ Guide for creating Pull Requests.
35
35
  3. Record **execution results** in the "Tests" section of PR body
36
36
  4. All checkboxes must be checked
37
37
 
38
- ### 3. Request User Approval
38
+ ### 3. Prepare Screenshots / Diagrams (Include in PR Body)
39
+
40
+ Include the artifacts in the PR body.
41
+
42
+ #### Frontend PR (UI changes)
43
+
44
+ - Use `agent-browser` to generate screenshots.
45
+ - Save files under a local temp folder (`/tmp/lee-spec-kit/pr-assets/`).
46
+ - Upload them as Release assets, then put the image URLs into the "Screenshots" section of the PR body.
47
+
48
+ ```bash
49
+ # (one-time) install agent-browser
50
+ npm i -g agent-browser
51
+ agent-browser install # install Playwright browsers
52
+
53
+ # Start a dev server: ports are often already taken, so prefer a free port.
54
+ # - If you already have a running dev server, you can just set PREVIEW_URL to that URL.
55
+ PORT=$(node -e "const net=require('net');const s=net.createServer();s.listen(0,'127.0.0.1',()=>{console.log(s.address().port);s.close();});")
56
+ # (example) Vite
57
+ pnpm dev --host 127.0.0.1 --port \"$PORT\"
58
+ PREVIEW_URL=\"http://127.0.0.1:${PORT}\"
59
+
60
+ # (example) capture from a preview URL
61
+ mkdir -p /tmp/lee-spec-kit/pr-assets
62
+ agent-browser open "$PREVIEW_URL"
63
+ agent-browser screenshot /tmp/lee-spec-kit/pr-assets/ui-1.png --full
64
+ agent-browser close
65
+ ```
66
+
67
+ ```bash
68
+ # Upload to Release assets and generate the URL to paste into the PR body
69
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
70
+ SAFE_BRANCH=$(git branch --show-current | tr '/' '-')
71
+ TAG="pr-assets/${SAFE_BRANCH}"
72
+
73
+ gh release view "$TAG" >/dev/null 2>&1 || \
74
+ gh release create "$TAG" --prerelease --title "pr-assets: ${SAFE_BRANCH}" --notes ""
75
+
76
+ gh release upload "$TAG" /tmp/lee-spec-kit/pr-assets/* --clobber
77
+
78
+ echo \"![](https://github.com/${REPO}/releases/download/${TAG}/ui-1.png)\"
79
+ ```
80
+
81
+ #### Backend PR (Core structure)
82
+
83
+ - Write a Mermaid diagram (flowchart/sequence/etc.) in the PR body (see the "Architecture Diagram" section in `pr-template.md`).
84
+
85
+ ### 4. Request User Approval
39
86
 
40
87
  > 🚨 **User Approval Required**
41
88
 
@@ -45,7 +92,7 @@ Before creating PR, share the following **in a code block** and wait for **expli
45
92
  - Full body (`pr-template.md` format)
46
93
  - Labels
47
94
 
48
- ### 4. Create PR
95
+ ### 5. Create PR
49
96
 
50
97
  ```bash
51
98
  gh pr create \
@@ -30,6 +30,12 @@ Keep `tasks.md` aligned with reality.
30
30
  - Do not mark `[DONE]` without actually completing the work and verifying criteria.
31
31
  - If you need to change a completed task, add a new task instead of rewriting history.
32
32
 
33
+ ### Step 3.25: Commit per task (important)
34
+
35
+ - Complete **only one task at a time** (do not batch-finish multiple tasks in one commit).
36
+ - After marking a task `[DONE]` (and updating any checklist items), create commits (code commit + docs commit) so each task has its own history.
37
+ - Once all tasks are `[DONE]`, share the "Completion Criteria" checklist with the user and get approval (OK), then check it (especially the **User approval (OK) received** item).
38
+
33
39
  ### Step 3.5: Record decisions (strongly recommended, effectively required)
34
40
 
35
41
  To avoid “why did we implement it like this?” losing context, **record any non-obvious or tradeoff-heavy implementation choice** in `decisions.md`.
@@ -37,6 +37,7 @@
37
37
 
38
38
  - [ ] All tasks are `[DONE]`, and each task's `Acceptance` is verified and `Checklist` is checked
39
39
  - [ ] Tests executed and passing (record command/result below)
40
+ - [ ] User approval (OK) received
40
41
 
41
42
  ### Test Run Log
42
43
 
@@ -37,6 +37,7 @@
37
37
 
38
38
  - [ ] All tasks are `[DONE]`, and each task's `Acceptance` is verified and `Checklist` is checked
39
39
  - [ ] Tests executed and passing (record command/result below)
40
+ - [ ] User approval (OK) received
40
41
 
41
42
  ### Test Run Log
42
43
 
@@ -50,14 +50,24 @@ PR 본문에서 레포 내 파일 링크는 **반드시 현재 브랜치명을
50
50
  - 명령어: `{실행한 테스트 명령어}`
51
51
  - 결과: `{PASS/FAIL 요약}`
52
52
 
53
- ## 스크린샷 (UI 변경 시)
53
+ ## 스크린샷 (프론트엔드 / UI 변경 시)
54
54
 
55
- {있으면 첨부}
55
+ > `skills/create-pr.md`의 Release assets 업로드 절차를 사용하면 브랜치에 파일을 커밋하지 않고도 이미지를 본문에 포함할 수 있습니다.
56
+
57
+ {스크린샷 마크다운 (예: ![](URL))}
58
+
59
+ ## 아키텍처 다이어그램 (백엔드 / 핵심 구조 변경 시)
60
+
61
+ ```mermaid
62
+ flowchart LR
63
+ A[Client] --> B[API]
64
+ B --> C[DB]
65
+ ```
56
66
 
57
67
  ## 관련 문서
58
68
 
59
- - **Spec**: `docs/features/{be|fe}/F{번호}-{기능명}/spec.md`
60
- - **Tasks**: `docs/features/{be|fe}/F{번호}-{기능명}/tasks.md`
69
+ - **Spec**: `{{featurePath}}/F{번호}-{기능명}/spec.md`
70
+ - **Tasks**: `{{featurePath}}/F{번호}-{기능명}/tasks.md`
61
71
 
62
72
  Closes #{이슈번호}
63
73
  ```
@@ -35,7 +35,54 @@ Pull Request를 생성할 때 따르는 가이드입니다.
35
35
  3. PR 본문 "테스트" 섹션에 **실행 결과** 기록
36
36
  4. 모든 체크박스가 체크되어야 함
37
37
 
38
- ### 3. 사용자 확인 요청
38
+ ### 3. 스크린샷/다이어그램 작성 (PR 본문에 포함)
39
+
40
+ PR 본문에 결과물을 포함합니다.
41
+
42
+ #### 프론트엔드 PR (UI 변경)
43
+
44
+ - `agent-browser`로 스크린샷을 생성합니다.
45
+ - 스크린샷 파일은 로컬 임시 폴더(`/tmp/lee-spec-kit/pr-assets/`)에 저장합니다.
46
+ - 릴리스 자산(Release assets)으로 업로드한 뒤, 생성된 이미지 URL을 PR 본문 "스크린샷" 섹션에 넣습니다.
47
+
48
+ ```bash
49
+ # (최초 1회) agent-browser 설치
50
+ npm i -g agent-browser
51
+ agent-browser install # Playwright 브라우저 설치
52
+
53
+ # 개발 서버 실행: 이미 사용 중인 포트가 많으므로 "빈 포트"를 권장합니다.
54
+ # - 이미 떠있는 개발 서버가 있다면 그 URL을 PREVIEW_URL로 지정해도 됩니다.
55
+ PORT=$(node -e "const net=require('net');const s=net.createServer();s.listen(0,'127.0.0.1',()=>{console.log(s.address().port);s.close();});")
56
+ # (예시) Vite
57
+ pnpm dev --host 127.0.0.1 --port \"$PORT\"
58
+ PREVIEW_URL=\"http://127.0.0.1:${PORT}\"
59
+
60
+ # (예시) 미리보기 URL을 정해 스크린샷 생성
61
+ mkdir -p /tmp/lee-spec-kit/pr-assets
62
+ agent-browser open "$PREVIEW_URL"
63
+ agent-browser screenshot /tmp/lee-spec-kit/pr-assets/ui-1.png --full
64
+ agent-browser close
65
+ ```
66
+
67
+ ```bash
68
+ # 스크린샷을 Release assets로 업로드하고, PR 본문에 넣을 URL 만들기
69
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
70
+ SAFE_BRANCH=$(git branch --show-current | tr '/' '-')
71
+ TAG="pr-assets/${SAFE_BRANCH}"
72
+
73
+ gh release view "$TAG" >/dev/null 2>&1 || \
74
+ gh release create "$TAG" --prerelease --title "pr-assets: ${SAFE_BRANCH}" --notes ""
75
+
76
+ gh release upload "$TAG" /tmp/lee-spec-kit/pr-assets/* --clobber
77
+
78
+ echo \"![](https://github.com/${REPO}/releases/download/${TAG}/ui-1.png)\"
79
+ ```
80
+
81
+ #### 백엔드 PR (핵심 구조)
82
+
83
+ - PR 본문에 Mermaid 다이어그램(예: flowchart/sequence)을 작성합니다. (`pr-template.md`의 "아키텍처 다이어그램" 섹션 참고)
84
+
85
+ ### 4. 사용자 확인 요청
39
86
 
40
87
  > 🚨 **사용자 확인 필수**
41
88
 
@@ -45,7 +92,7 @@ PR 생성 전 다음 내용을 **코드블록으로** 사용자에게 공유하
45
92
  - 본문 전체 (`pr-template.md` 형식)
46
93
  - 라벨
47
94
 
48
- ### 4. PR 생성
95
+ ### 5. PR 생성
49
96
 
50
97
  ```bash
51
98
  gh pr create \
@@ -47,8 +47,10 @@ CLI가 가리키는 **Active Task** 또는 **Next Action**을 수행합니다.
47
47
  #### 3-2) 태스크/체크리스트 업데이트 + 커밋
48
48
 
49
49
  1. 작업이 끝나면 해당 태스크의 상태를 `[DONE]`으로 변경하고, `Acceptance/Checklist` 항목을 `[x]`로 체크합니다.
50
- 2. 커밋을 생성합니다 (코드 커밋 + 문서 커밋).
51
- 3. **즉시 1단계로 돌아가** 다음 일을 CLI에게 물어봅니다.
50
+ 2. **한 번에 하나의 태스크만** `[DONE]` 처리합니다. (태스크 2개 이상을 번에 완료/커밋으로 묶지 않기)
51
+ 3. 커밋을 생성합니다 (코드 커밋 + 문서 커밋). 태스크 단위로 커밋이 남아야 합니다.
52
+ 4. 모든 태스크가 `[DONE]`가 되면, "완료 조건" 체크리스트를 사용자에게 공유하고 승인(OK)을 받은 뒤 체크합니다. (특히 **사용자 승인(OK) 완료** 항목)
53
+ 5. **즉시 1단계로 돌아가** 다음 할 일을 CLI에게 물어봅니다.
52
54
 
53
55
  ---
54
56
 
@@ -37,6 +37,7 @@
37
37
 
38
38
  - [ ] 모든 태스크가 `[DONE]`이며, 각 태스크의 `Acceptance` 검증 및 `Checklist` 체크 완료
39
39
  - [ ] 테스트 실행 및 통과 (아래에 명령어/결과 기록)
40
+ - [ ] 사용자 승인(OK) 완료
40
41
 
41
42
  ### 테스트 실행 기록
42
43
 
@@ -37,6 +37,7 @@
37
37
 
38
38
  - [ ] 모든 태스크가 `[DONE]`이며, 각 태스크의 `Acceptance` 검증 및 `Checklist` 체크 완료
39
39
  - [ ] 테스트 실행 및 통과 (아래에 명령어/결과 기록)
40
+ - [ ] 사용자 승인(OK) 완료
40
41
 
41
42
  ### 테스트 실행 기록
42
43