lee-spec-kit 0.4.8 → 0.4.9

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.",
@@ -266,6 +272,8 @@ var I18N = {
266
272
  "update.updatedTotal": "Updated {count} files!",
267
273
  "update.changeDetected": "changes detected (use --force to overwrite)",
268
274
  "update.fileUpdated": "{file} updated",
275
+ "update.gitStatusUnavailable": "Cannot determine git status (not a git repo or git unavailable). Use --force to overwrite.",
276
+ "update.docsWorktreeDirty": "Docs working tree has changes. Commit/stash your changes, or run with --force to overwrite.",
269
277
  "doctor.title": "\u{1F50E} Docs Doctor",
270
278
  "doctor.envWarnings": "\u26A0\uFE0F Environment warnings:",
271
279
  "doctor.noIssues": "\u2705 No issues found.",
@@ -983,6 +991,7 @@ async function runFeature(name, options) {
983
991
  process.exit(1);
984
992
  }
985
993
  const { docsDir, projectType, lang } = config;
994
+ const projectName = config.projectName;
986
995
  assertValid(validateSafeName(name), "\uAE30\uB2A5 \uC774\uB984");
987
996
  let repo = options.repo;
988
997
  if (projectType === "fullstack" && !repo) {
@@ -1043,6 +1052,7 @@ async function runFeature(name, options) {
1043
1052
  const idNumber = featureId.replace("F", "");
1044
1053
  const repoName = projectType === "fullstack" && repo ? `{{projectName}}-${repo}` : "{{projectName}}";
1045
1054
  const replacements = {
1055
+ "{{projectName}}": projectName ?? "{{projectName}}",
1046
1056
  // ko placeholders
1047
1057
  "{\uAE30\uB2A5\uBA85}": name,
1048
1058
  "{\uBC88\uD638}": idNumber,
@@ -2065,18 +2075,28 @@ async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderNa
2065
2075
  return fallbackSlug || fallbackFolderName;
2066
2076
  }
2067
2077
  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) => {
2078
+ 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(
2079
+ "-f, --force",
2080
+ "Force overwrite even if docs has uncommitted changes"
2081
+ ).action(async (options) => {
2069
2082
  try {
2070
2083
  await runUpdate(options);
2071
2084
  } catch (error) {
2085
+ const config = await getConfig(process.cwd());
2086
+ const lang = config?.lang ?? DEFAULT_LANG;
2072
2087
  if (error instanceof Error && error.message === "canceled") {
2073
- const config = await getConfig(process.cwd());
2074
- const lang = config?.lang ?? DEFAULT_LANG;
2075
2088
  console.log(chalk6.yellow(`
2076
2089
  ${tr(lang, "cli", "common.canceled")}`));
2077
2090
  process.exit(0);
2078
2091
  }
2079
- console.error(chalk6.red(tr(DEFAULT_LANG, "cli", "common.errorLabel")), error);
2092
+ if (error instanceof Error) {
2093
+ console.error(
2094
+ chalk6.red(tr(lang, "cli", "common.errorLabel")),
2095
+ chalk6.red(error.message)
2096
+ );
2097
+ } else {
2098
+ console.error(chalk6.red(tr(lang, "cli", "common.errorLabel")), error);
2099
+ }
2080
2100
  process.exit(1);
2081
2101
  }
2082
2102
  });
@@ -2096,6 +2116,7 @@ async function runUpdate(options) {
2096
2116
  const { docsDir, projectType, lang } = config;
2097
2117
  const templatesDir = getTemplatesDir();
2098
2118
  const sourceDir = path4.join(templatesDir, lang, projectType);
2119
+ const forceOverwrite = !!options.force || await isDocsWorktreeCleanOrThrow(docsDir, lang);
2099
2120
  const hasExplicitSelection = !!(options.agents || options.skills || options.templates);
2100
2121
  const updateAgents = options.agents || options.skills || !hasExplicitSelection;
2101
2122
  const updateTemplates = options.templates || !hasExplicitSelection;
@@ -2120,15 +2141,20 @@ async function runUpdate(options) {
2120
2141
  const typeAgents = agentsMode === "skills" ? path4.join(typeAgentsBase, "skills") : typeAgentsBase;
2121
2142
  const targetAgents = agentsMode === "skills" ? path4.join(targetAgentsBase, "skills") : targetAgentsBase;
2122
2143
  const featurePath = projectType === "fullstack" ? "docs/features/{be|fe}" : "docs/features";
2123
- const replacements = {
2144
+ const projectName = config.projectName ?? "{{projectName}}";
2145
+ const commonReplacements = {
2146
+ "{{projectName}}": projectName,
2124
2147
  "{{featurePath}}": featurePath
2125
2148
  };
2149
+ const typeReplacements = {
2150
+ "{{projectName}}": projectName
2151
+ };
2126
2152
  if (await fs8.pathExists(commonAgents)) {
2127
2153
  const count = await updateFolder(
2128
2154
  commonAgents,
2129
2155
  targetAgents,
2130
- options.force,
2131
- replacements,
2156
+ forceOverwrite,
2157
+ commonReplacements,
2132
2158
  lang
2133
2159
  );
2134
2160
  updatedCount += count;
@@ -2137,8 +2163,8 @@ async function runUpdate(options) {
2137
2163
  const count = await updateFolder(
2138
2164
  typeAgents,
2139
2165
  targetAgents,
2140
- options.force,
2141
- void 0,
2166
+ forceOverwrite,
2167
+ typeReplacements,
2142
2168
  lang
2143
2169
  );
2144
2170
  updatedCount += count;
@@ -2154,11 +2180,14 @@ async function runUpdate(options) {
2154
2180
  const sourceFeatureBase = path4.join(sourceDir, "features", "feature-base");
2155
2181
  const targetFeatureBase = path4.join(docsDir, "features", "feature-base");
2156
2182
  if (await fs8.pathExists(sourceFeatureBase)) {
2183
+ const replacements = {
2184
+ "{{projectName}}": config.projectName ?? "{{projectName}}"
2185
+ };
2157
2186
  const count = await updateFolder(
2158
2187
  sourceFeatureBase,
2159
2188
  targetFeatureBase,
2160
- options.force,
2161
- void 0,
2189
+ forceOverwrite,
2190
+ replacements,
2162
2191
  lang
2163
2192
  );
2164
2193
  updatedCount += count;
@@ -2191,9 +2220,7 @@ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DE
2191
2220
  }
2192
2221
  let sourceContent = await fs8.readFile(sourcePath, "utf-8");
2193
2222
  if (replacements) {
2194
- for (const [key, value] of Object.entries(replacements)) {
2195
- sourceContent = sourceContent.replaceAll(key, value);
2196
- }
2223
+ sourceContent = applyReplacements(sourceContent, replacements);
2197
2224
  }
2198
2225
  let shouldUpdate = true;
2199
2226
  if (await fs8.pathExists(targetPath)) {
@@ -2230,6 +2257,42 @@ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DE
2230
2257
  }
2231
2258
  return updatedCount;
2232
2259
  }
2260
+ function getGitTopLevel2(cwd) {
2261
+ try {
2262
+ const out = execFileSync("git", ["rev-parse", "--show-toplevel"], {
2263
+ cwd,
2264
+ encoding: "utf-8",
2265
+ stdio: ["ignore", "pipe", "ignore"]
2266
+ }).trim();
2267
+ return out || null;
2268
+ } catch {
2269
+ return null;
2270
+ }
2271
+ }
2272
+ function getDocsPorcelainStatus(docsDir) {
2273
+ const top = getGitTopLevel2(docsDir);
2274
+ if (!top) return null;
2275
+ const rel = path4.relative(top, docsDir) || ".";
2276
+ try {
2277
+ return execFileSync("git", ["status", "--porcelain=v1", "--", rel], {
2278
+ cwd: top,
2279
+ encoding: "utf-8",
2280
+ stdio: ["ignore", "pipe", "ignore"]
2281
+ });
2282
+ } catch {
2283
+ return null;
2284
+ }
2285
+ }
2286
+ async function isDocsWorktreeCleanOrThrow(docsDir, lang) {
2287
+ const status = getDocsPorcelainStatus(docsDir);
2288
+ if (status === null) {
2289
+ throw new Error(tr(lang, "cli", "update.gitStatusUnavailable"));
2290
+ }
2291
+ if (status.trim().length > 0) {
2292
+ throw new Error(tr(lang, "cli", "update.docsWorktreeDirty"));
2293
+ }
2294
+ return true;
2295
+ }
2233
2296
  function configCommand(program2) {
2234
2297
  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
2298
  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.9",
4
4
  "description": "Project documentation structure generator for AI-assisted development",
5
5
  "type": "module",
6
6
  "bin": {