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 +7 -2
- package/README.md +13 -2
- package/dist/index.js +83 -20
- package/package.json +1 -1
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
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2131
|
-
|
|
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
|
-
|
|
2141
|
-
|
|
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
|
-
|
|
2161
|
-
|
|
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
|
-
|
|
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 {
|