lee-spec-kit 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/index.js +461 -0
  4. package/package.json +60 -0
  5. package/templates/en/fullstack/README.md +12 -0
  6. package/templates/en/fullstack/agents/agents.md +88 -0
  7. package/templates/en/fullstack/agents/constitution.md +75 -0
  8. package/templates/en/fullstack/agents/git-workflow.md +112 -0
  9. package/templates/en/fullstack/agents/issue-template.md +58 -0
  10. package/templates/en/fullstack/agents/pr-template.md +57 -0
  11. package/templates/en/fullstack/features/README.md +76 -0
  12. package/templates/en/fullstack/features/be/README.md +5 -0
  13. package/templates/en/fullstack/features/fe/README.md +5 -0
  14. package/templates/en/fullstack/features/feature-base/decisions.md +15 -0
  15. package/templates/en/fullstack/features/feature-base/plan.md +49 -0
  16. package/templates/en/fullstack/features/feature-base/spec.md +55 -0
  17. package/templates/en/fullstack/features/feature-base/tasks.md +33 -0
  18. package/templates/en/fullstack/prd/README.md +15 -0
  19. package/templates/en/single/README.md +11 -0
  20. package/templates/en/single/agents/agents.md +74 -0
  21. package/templates/en/single/agents/constitution.md +75 -0
  22. package/templates/en/single/agents/git-workflow.md +157 -0
  23. package/templates/en/single/agents/issue-template.md +60 -0
  24. package/templates/en/single/agents/pr-template.md +71 -0
  25. package/templates/en/single/features/README.md +56 -0
  26. package/templates/en/single/features/feature-base/decisions.md +15 -0
  27. package/templates/en/single/features/feature-base/plan.md +48 -0
  28. package/templates/en/single/features/feature-base/spec.md +55 -0
  29. package/templates/en/single/features/feature-base/tasks.md +33 -0
  30. package/templates/en/single/prd/README.md +15 -0
  31. package/templates/ko/fullstack/README.md +12 -0
  32. package/templates/ko/fullstack/agents/agents.md +125 -0
  33. package/templates/ko/fullstack/agents/constitution.md +75 -0
  34. package/templates/ko/fullstack/agents/git-workflow.md +157 -0
  35. package/templates/ko/fullstack/agents/issue-template.md +60 -0
  36. package/templates/ko/fullstack/agents/pr-template.md +71 -0
  37. package/templates/ko/fullstack/features/README.md +98 -0
  38. package/templates/ko/fullstack/features/be/README.md +5 -0
  39. package/templates/ko/fullstack/features/fe/README.md +5 -0
  40. package/templates/ko/fullstack/features/feature-base/decisions.md +15 -0
  41. package/templates/ko/fullstack/features/feature-base/plan.md +49 -0
  42. package/templates/ko/fullstack/features/feature-base/spec.md +55 -0
  43. package/templates/ko/fullstack/features/feature-base/tasks.md +33 -0
  44. package/templates/ko/fullstack/prd/README.md +15 -0
  45. package/templates/ko/single/README.md +11 -0
  46. package/templates/ko/single/agents/agents.md +87 -0
  47. package/templates/ko/single/agents/constitution.md +75 -0
  48. package/templates/ko/single/agents/git-workflow.md +157 -0
  49. package/templates/ko/single/agents/issue-template.md +60 -0
  50. package/templates/ko/single/agents/pr-template.md +71 -0
  51. package/templates/ko/single/features/README.md +56 -0
  52. package/templates/ko/single/features/feature-base/decisions.md +15 -0
  53. package/templates/ko/single/features/feature-base/plan.md +48 -0
  54. package/templates/ko/single/features/feature-base/spec.md +55 -0
  55. package/templates/ko/single/features/feature-base/tasks.md +33 -0
  56. package/templates/ko/single/prd/README.md +15 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lee Yoonsu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # lee-spec-kit
2
+
3
+ 프로젝트 문서 구조 생성기 - AI 에이전트 기반 개발을 위한 docs 템플릿 CLI
4
+
5
+ [![npm version](https://img.shields.io/npm/v/lee-spec-kit.svg)](https://www.npmjs.com/package/lee-spec-kit)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## 설치
9
+
10
+ ```bash
11
+ # npx로 바로 사용
12
+ npx lee-spec-kit init
13
+
14
+ # 또는 전역 설치
15
+ npm install -g lee-spec-kit
16
+ ```
17
+
18
+ ## 사용법
19
+
20
+ ### 프로젝트 초기화
21
+
22
+ ```bash
23
+ # 대화형 모드
24
+ npx lee-spec-kit init
25
+
26
+ # 옵션 지정
27
+ npx lee-spec-kit init --name my-project --type fullstack --lang ko
28
+ ```
29
+
30
+ **옵션:**
31
+
32
+ | 옵션 | 설명 | 기본값 |
33
+ | ------------------- | ------------------------------ | ----------- |
34
+ | `-n, --name <name>` | 프로젝트 이름 | 현재 폴더명 |
35
+ | `-t, --type <type>` | `single` 또는 `fullstack` | 대화형 선택 |
36
+ | `-l, --lang <lang>` | `ko` (한국어) 또는 `en` (영어) | `ko` |
37
+ | `-d, --dir <dir>` | 설치 디렉토리 | `./docs` |
38
+ | `-y, --yes` | 대화형 프롬프트 스킵 | - |
39
+
40
+ ### 새 기능 생성
41
+
42
+ ```bash
43
+ # Single 프로젝트
44
+ lee-spec-kit feature user-auth
45
+
46
+ # Fullstack 프로젝트
47
+ lee-spec-kit feature --repo be user-auth
48
+ lee-spec-kit feature --repo fe user-profile
49
+ ```
50
+
51
+ ### 상태 확인
52
+
53
+ ```bash
54
+ # 터미널에 출력
55
+ lee-spec-kit status
56
+
57
+ # 파일로 저장
58
+ lee-spec-kit status --write
59
+ ```
60
+
61
+ ## 생성되는 구조
62
+
63
+ ### Fullstack (FE/BE 분리)
64
+
65
+ ```
66
+ docs/
67
+ ├── README.md
68
+ ├── agents/
69
+ │ ├── agents.md # 에이전트 운영 규칙
70
+ │ ├── constitution.md # 프로젝트 원칙
71
+ │ ├── git-workflow.md # Git 자동화 규칙
72
+ │ ├── issue-template.md
73
+ │ └── pr-template.md
74
+ ├── prd/
75
+ │ └── README.md
76
+ └── features/
77
+ ├── README.md
78
+ ├── feature-base/ # 공용 템플릿
79
+ ├── be/ # Backend Features
80
+ └── fe/ # Frontend Features
81
+ ```
82
+
83
+ ### Single (단일 레포)
84
+
85
+ ```
86
+ docs/
87
+ ├── README.md
88
+ ├── agents/
89
+ ├── prd/
90
+ └── features/
91
+ ├── feature-base/
92
+ └── F001-feature/ # 개별 기능
93
+ ```
94
+
95
+ ## 프로젝트 타입
96
+
97
+ | 타입 | 설명 |
98
+ | ----------- | -------------------------------------------- |
99
+ | `single` | 단일 레포 프로젝트 (모노레포 또는 단일 스택) |
100
+ | `fullstack` | FE/BE 분리 프로젝트 |
101
+
102
+ ## Feature 워크플로우
103
+
104
+ 1. `spec.md` 작성 - **무엇을, 왜** 만드는지
105
+ 2. 사용자 리뷰 요청
106
+ 3. `plan.md` 작성 - **어떻게** 만드는지 (기술 스택)
107
+ 4. `tasks.md` 작성 - 태스크 분해
108
+ 5. `decisions.md` - 기술 결정 기록 (ADR)
109
+
110
+ ## 라이선스
111
+
112
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import prompts from 'prompts';
4
+ import chalk from 'chalk';
5
+ import path4 from 'path';
6
+ import fs5 from 'fs-extra';
7
+ import { glob } from 'glob';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ async function copyTemplates(src, dest) {
11
+ await fs5.copy(src, dest, {
12
+ overwrite: true,
13
+ errorOnExist: false
14
+ });
15
+ }
16
+ async function replaceInFiles(dir, replacements) {
17
+ const files = await glob("**/*.md", { cwd: dir, absolute: true });
18
+ for (const file of files) {
19
+ let content = await fs5.readFile(file, "utf-8");
20
+ for (const [search, replace] of Object.entries(replacements)) {
21
+ content = content.replaceAll(search, replace);
22
+ }
23
+ await fs5.writeFile(file, content, "utf-8");
24
+ }
25
+ const shFiles = await glob("**/*.sh", { cwd: dir, absolute: true });
26
+ for (const file of shFiles) {
27
+ let content = await fs5.readFile(file, "utf-8");
28
+ for (const [search, replace] of Object.entries(replacements)) {
29
+ content = content.replaceAll(search, replace);
30
+ }
31
+ await fs5.writeFile(file, content, "utf-8");
32
+ }
33
+ }
34
+ var __filename2 = fileURLToPath(import.meta.url);
35
+ var __dirname2 = path4.dirname(__filename2);
36
+ function getTemplatesDir() {
37
+ const rootDir = path4.resolve(__dirname2, "..");
38
+ return path4.join(rootDir, "templates");
39
+ }
40
+
41
+ // src/commands/init.ts
42
+ function initCommand(program2) {
43
+ program2.command("init").description("Initialize project documentation structure").option("-n, --name <name>", "Project name (default: current folder name)").option("-t, --type <type>", "Project type: single | fullstack").option("-l, --lang <lang>", "Language: ko | en (default: ko)").option("-d, --dir <dir>", "Target directory (default: ./docs)", "./docs").option("-y, --yes", "Skip prompts and use defaults").action(async (options) => {
44
+ try {
45
+ await runInit(options);
46
+ } catch (error) {
47
+ if (error instanceof Error && error.message === "canceled") {
48
+ console.log(chalk.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
49
+ process.exit(0);
50
+ }
51
+ console.error(chalk.red("\uC624\uB958:"), error);
52
+ process.exit(1);
53
+ }
54
+ });
55
+ }
56
+ async function runInit(options) {
57
+ const cwd = process.cwd();
58
+ const defaultName = path4.basename(cwd);
59
+ let projectName = options.name || defaultName;
60
+ let projectType = options.type;
61
+ let lang = options.lang || "ko";
62
+ const targetDir = path4.resolve(cwd, options.dir || "./docs");
63
+ if (!options.yes) {
64
+ const response = await prompts(
65
+ [
66
+ {
67
+ type: options.name ? null : "text",
68
+ name: "projectName",
69
+ message: "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694:",
70
+ initial: defaultName
71
+ },
72
+ {
73
+ type: options.type ? null : "select",
74
+ name: "projectType",
75
+ message: "\uD504\uB85C\uC81D\uD2B8 \uD0C0\uC785\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
76
+ choices: [
77
+ {
78
+ title: "Single - \uB2E8\uC77C \uB808\uD3EC \uD504\uB85C\uC81D\uD2B8",
79
+ value: "single",
80
+ description: "features/ \uD3F4\uB354 \uD558\uB098\uB85C \uAD00\uB9AC"
81
+ },
82
+ {
83
+ title: "Fullstack - FE/BE \uBD84\uB9AC \uD504\uB85C\uC81D\uD2B8",
84
+ value: "fullstack",
85
+ description: "features/be/, features/fe/ \uBD84\uB9AC \uAD00\uB9AC"
86
+ }
87
+ ],
88
+ initial: 0
89
+ },
90
+ {
91
+ type: options.lang ? null : "select",
92
+ name: "lang",
93
+ message: "\uBB38\uC11C \uC5B8\uC5B4\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
94
+ choices: [
95
+ { title: "\uD55C\uAD6D\uC5B4 (ko)", value: "ko" },
96
+ { title: "English (en)", value: "en" }
97
+ ],
98
+ initial: 0
99
+ }
100
+ ],
101
+ {
102
+ onCancel: () => {
103
+ throw new Error("canceled");
104
+ }
105
+ }
106
+ );
107
+ projectName = response.projectName || projectName;
108
+ projectType = response.projectType || projectType;
109
+ lang = response.lang || lang;
110
+ }
111
+ if (!projectType) {
112
+ projectType = "single";
113
+ }
114
+ if (await fs5.pathExists(targetDir)) {
115
+ const files = await fs5.readdir(targetDir);
116
+ if (files.length > 0) {
117
+ const { overwrite } = await prompts({
118
+ type: "confirm",
119
+ name: "overwrite",
120
+ message: `${targetDir} \uD3F4\uB354\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uB36E\uC5B4\uC4F0\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`,
121
+ initial: false
122
+ });
123
+ if (!overwrite) {
124
+ console.log(chalk.yellow("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
125
+ return;
126
+ }
127
+ }
128
+ }
129
+ console.log();
130
+ console.log(chalk.blue("\u{1F4C1} docs \uAD6C\uC870 \uC0DD\uC131 \uC911..."));
131
+ console.log(chalk.gray(` \uD504\uB85C\uC81D\uD2B8: ${projectName}`));
132
+ console.log(chalk.gray(` \uD0C0\uC785: ${projectType}`));
133
+ console.log(chalk.gray(` \uC5B8\uC5B4: ${lang}`));
134
+ console.log(chalk.gray(` \uACBD\uB85C: ${targetDir}`));
135
+ console.log();
136
+ const templatesDir = getTemplatesDir();
137
+ const templatePath = path4.join(templatesDir, lang, projectType);
138
+ if (!await fs5.pathExists(templatePath)) {
139
+ throw new Error(`\uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${templatePath}`);
140
+ }
141
+ await copyTemplates(templatePath, targetDir);
142
+ const replacements = {
143
+ "{{projectName}}": projectName,
144
+ "{{date}}": (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
145
+ };
146
+ await replaceInFiles(targetDir, replacements);
147
+ console.log(chalk.green("\u2705 docs \uAD6C\uC870 \uC0DD\uC131 \uC644\uB8CC!"));
148
+ console.log();
149
+ console.log(chalk.blue("\uB2E4\uC74C \uB2E8\uACC4:"));
150
+ console.log(chalk.gray(` 1. ${targetDir}/prd/README.md \uC791\uC131`));
151
+ console.log(chalk.gray(" 2. lee-spec-kit feature <name> \uC73C\uB85C \uAE30\uB2A5 \uCD94\uAC00"));
152
+ console.log();
153
+ }
154
+ async function getConfig(cwd) {
155
+ const possibleDirs = [
156
+ path4.join(cwd, "docs"),
157
+ cwd
158
+ // 이미 docs 폴더 안에 있을 수 있음
159
+ ];
160
+ for (const docsDir of possibleDirs) {
161
+ const agentsPath = path4.join(docsDir, "agents");
162
+ const featuresPath = path4.join(docsDir, "features");
163
+ if (await fs5.pathExists(agentsPath) && await fs5.pathExists(featuresPath)) {
164
+ const bePath = path4.join(featuresPath, "be");
165
+ const fePath = path4.join(featuresPath, "fe");
166
+ const projectType = await fs5.pathExists(bePath) || await fs5.pathExists(fePath) ? "fullstack" : "single";
167
+ const agentsMdPath = path4.join(agentsPath, "agents.md");
168
+ let lang = "ko";
169
+ if (await fs5.pathExists(agentsMdPath)) {
170
+ const content = await fs5.readFile(agentsMdPath, "utf-8");
171
+ if (!/[가-힣]/.test(content)) {
172
+ lang = "en";
173
+ }
174
+ }
175
+ return { docsDir, projectType, lang };
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+
181
+ // src/commands/feature.ts
182
+ function featureCommand(program2) {
183
+ program2.command("feature <name>").description("Create a new feature folder").option("-r, --repo <repo>", "Repository type: be | fe (fullstack only)").option("--id <id>", "Feature ID (default: auto)").action(async (name, options) => {
184
+ try {
185
+ await runFeature(name, options);
186
+ } catch (error) {
187
+ if (error instanceof Error && error.message === "canceled") {
188
+ console.log(chalk.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
189
+ process.exit(0);
190
+ }
191
+ console.error(chalk.red("\uC624\uB958:"), error);
192
+ process.exit(1);
193
+ }
194
+ });
195
+ }
196
+ async function runFeature(name, options) {
197
+ const cwd = process.cwd();
198
+ const config = await getConfig(cwd);
199
+ if (!config) {
200
+ console.error(
201
+ chalk.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
202
+ );
203
+ process.exit(1);
204
+ }
205
+ const { docsDir, projectType, lang } = config;
206
+ let repo = options.repo;
207
+ if (projectType === "fullstack" && !repo) {
208
+ const response = await prompts(
209
+ {
210
+ type: "select",
211
+ name: "repo",
212
+ message: "\uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
213
+ choices: [
214
+ { title: "Backend (be)", value: "be" },
215
+ { title: "Frontend (fe)", value: "fe" }
216
+ ]
217
+ },
218
+ {
219
+ onCancel: () => {
220
+ throw new Error("canceled");
221
+ }
222
+ }
223
+ );
224
+ repo = response.repo;
225
+ }
226
+ const featureId = options.id || await getNextFeatureId(docsDir, projectType);
227
+ let featuresDir;
228
+ if (projectType === "fullstack" && repo) {
229
+ featuresDir = path4.join(docsDir, "features", repo);
230
+ } else {
231
+ featuresDir = path4.join(docsDir, "features");
232
+ }
233
+ const featureFolderName = `${featureId}-${name}`;
234
+ const featureDir = path4.join(featuresDir, featureFolderName);
235
+ if (await fs5.pathExists(featureDir)) {
236
+ console.error(chalk.red(`\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uD3F4\uB354\uC785\uB2C8\uB2E4: ${featureDir}`));
237
+ process.exit(1);
238
+ }
239
+ const featureBasePath = path4.join(docsDir, "features", "feature-base");
240
+ if (!await fs5.pathExists(featureBasePath)) {
241
+ console.error(chalk.red("feature-base \uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
242
+ process.exit(1);
243
+ }
244
+ await fs5.copy(featureBasePath, featureDir);
245
+ const idNumber = featureId.replace("F", "");
246
+ const repoName = projectType === "fullstack" && repo ? `{{projectName}}-${repo}` : "{{projectName}}";
247
+ const replacements = {
248
+ "{\uAE30\uB2A5\uBA85}": name,
249
+ "{\uBC88\uD638}": idNumber,
250
+ "YYYY-MM-DD": (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
251
+ "{be|fe}": repo || "",
252
+ "git-dungeon-{be|fe}": repoName,
253
+ "{\uC774\uC288\uBC88\uD638}": ""
254
+ };
255
+ if (lang === "en") {
256
+ replacements["\uAE30\uB2A5 ID"] = "Feature ID";
257
+ replacements["\uAE30\uB2A5\uBA85"] = "Feature Name";
258
+ replacements["\uB300\uC0C1 \uB808\uD3EC"] = "Target Repo";
259
+ replacements["\uC774\uC288 \uBC88\uD638"] = "Issue Number";
260
+ replacements["\uC791\uC131\uC77C"] = "Created";
261
+ replacements["\uC0C1\uD0DC"] = "Status";
262
+ }
263
+ await replaceInFiles(featureDir, replacements);
264
+ console.log();
265
+ console.log(chalk.green(`\u2705 Feature \uD3F4\uB354 \uC0DD\uC131 \uC644\uB8CC: ${featureDir}`));
266
+ console.log();
267
+ console.log(chalk.blue("\uB2E4\uC74C \uB2E8\uACC4:"));
268
+ console.log(chalk.gray(` 1. ${featureDir}/spec.md \uC791\uC131`));
269
+ console.log(chalk.gray(" 2. \uC0AC\uC6A9\uC790 \uB9AC\uBDF0 \uC694\uCCAD"));
270
+ console.log(chalk.gray(" 3. \uC2B9\uC778 \uD6C4 plan.md \uC791\uC131"));
271
+ console.log();
272
+ }
273
+ async function getNextFeatureId(docsDir, projectType) {
274
+ const featuresDir = path4.join(docsDir, "features");
275
+ let max = 0;
276
+ const scanDirs = [];
277
+ if (projectType === "fullstack") {
278
+ scanDirs.push(path4.join(featuresDir, "be"));
279
+ scanDirs.push(path4.join(featuresDir, "fe"));
280
+ } else {
281
+ scanDirs.push(featuresDir);
282
+ }
283
+ for (const dir of scanDirs) {
284
+ if (!await fs5.pathExists(dir)) continue;
285
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
286
+ for (const entry of entries) {
287
+ if (!entry.isDirectory()) continue;
288
+ const match = entry.name.match(/^F(\d+)-/);
289
+ if (match) {
290
+ const num = parseInt(match[1], 10);
291
+ if (num > max) max = num;
292
+ }
293
+ }
294
+ }
295
+ const next = max + 1;
296
+ const width = Math.max(3, String(next).length);
297
+ return `F${String(next).padStart(width, "0")}`;
298
+ }
299
+ function statusCommand(program2) {
300
+ program2.command("status").description("Show feature status").option("-w, --write", "Write status.md file").option("-s, --strict", "Fail on missing/duplicate feature IDs").action(async (options) => {
301
+ try {
302
+ await runStatus(options);
303
+ } catch (error) {
304
+ console.error(chalk.red("\uC624\uB958:"), error);
305
+ process.exit(1);
306
+ }
307
+ });
308
+ }
309
+ async function runStatus(options) {
310
+ const cwd = process.cwd();
311
+ const config = await getConfig(cwd);
312
+ if (!config) {
313
+ console.error(
314
+ chalk.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
315
+ );
316
+ process.exit(1);
317
+ }
318
+ const { docsDir, projectType } = config;
319
+ const featuresDir = path4.join(docsDir, "features");
320
+ const features = [];
321
+ const idMap = /* @__PURE__ */ new Map();
322
+ const scopes = projectType === "fullstack" ? ["be", "fe"] : [""];
323
+ for (const scope of scopes) {
324
+ const scanDir = scope ? path4.join(featuresDir, scope) : featuresDir;
325
+ if (!await fs5.pathExists(scanDir)) continue;
326
+ const entries = await fs5.readdir(scanDir, { withFileTypes: true });
327
+ for (const entry of entries) {
328
+ if (!entry.isDirectory()) continue;
329
+ if (entry.name === "feature-base") continue;
330
+ const featureDir = path4.join(scanDir, entry.name);
331
+ const specPath = path4.join(featureDir, "spec.md");
332
+ const tasksPath = path4.join(featureDir, "tasks.md");
333
+ if (!await fs5.pathExists(specPath)) continue;
334
+ if (!await fs5.pathExists(tasksPath)) continue;
335
+ const specContent = await fs5.readFile(specPath, "utf-8");
336
+ const tasksContent = await fs5.readFile(tasksPath, "utf-8");
337
+ const id = extractSpecValue(specContent, "\uAE30\uB2A5 ID") || extractSpecValue(specContent, "Feature ID") || "UNKNOWN";
338
+ const name = extractSpecValue(specContent, "\uAE30\uB2A5\uBA85") || extractSpecValue(specContent, "Feature Name") || entry.name;
339
+ const repo = extractSpecValue(specContent, "\uB300\uC0C1 \uB808\uD3EC") || extractSpecValue(specContent, "Target Repo") || (scope ? `{{projectName}}-${scope}` : "{{projectName}}");
340
+ const issue = extractSpecValue(specContent, "\uC774\uC288 \uBC88\uD638") || extractSpecValue(specContent, "Issue Number") || "-";
341
+ const relPath = path4.relative(docsDir, featureDir);
342
+ if (!idMap.has(id)) {
343
+ idMap.set(id, []);
344
+ }
345
+ idMap.get(id).push(relPath);
346
+ const { total, done, doing, todo } = countTasks(tasksContent);
347
+ let status = "TODO";
348
+ if (total > 0 && done === total) {
349
+ status = "DONE";
350
+ } else if (doing > 0) {
351
+ status = "DOING";
352
+ } else if (todo > 0) {
353
+ status = "TODO";
354
+ } else if (total === 0) {
355
+ status = "NO_TASKS";
356
+ }
357
+ features.push({
358
+ id,
359
+ name,
360
+ repo,
361
+ issue,
362
+ status,
363
+ progress: `${done}/${total}`,
364
+ path: relPath
365
+ });
366
+ }
367
+ }
368
+ if (features.length === 0) {
369
+ console.log(chalk.yellow("Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
370
+ return;
371
+ }
372
+ if (options.strict) {
373
+ const duplicates = [...idMap.entries()].filter(
374
+ ([, paths]) => paths.length > 1
375
+ );
376
+ if (duplicates.length > 0) {
377
+ console.error(chalk.red("\uC911\uBCF5 Feature ID \uBC1C\uACAC:"));
378
+ for (const [id, paths] of duplicates) {
379
+ console.error(chalk.red(` ${id}:`));
380
+ for (const p of paths) {
381
+ console.error(chalk.red(` - ${p}`));
382
+ }
383
+ }
384
+ process.exit(1);
385
+ }
386
+ const unknowns = [...idMap.entries()].filter(([id]) => id === "UNKNOWN");
387
+ if (unknowns.length > 0) {
388
+ console.error(chalk.red("Feature ID\uAC00 \uC5C6\uB294 \uD56D\uBAA9:"));
389
+ for (const [, paths] of unknowns) {
390
+ for (const p of paths) {
391
+ console.error(chalk.red(` - ${p}`));
392
+ }
393
+ }
394
+ process.exit(1);
395
+ }
396
+ }
397
+ features.sort((a, b) => a.id.localeCompare(b.id));
398
+ const header = "| ID | Name | Repo | Issue | Status | Progress | Path |";
399
+ const separator = "| --- | --- | --- | --- | --- | --- | --- |";
400
+ console.log();
401
+ console.log(header);
402
+ console.log(separator);
403
+ for (const f of features) {
404
+ const statusColor = f.status === "DONE" ? chalk.green : f.status === "DOING" ? chalk.yellow : chalk.gray;
405
+ console.log(
406
+ `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${statusColor(f.status)} | ${f.progress} | ${f.path} |`
407
+ );
408
+ }
409
+ console.log();
410
+ if (options.write) {
411
+ const outputPath = path4.join(featuresDir, "status.md");
412
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
413
+ const content = [
414
+ "# Feature Status",
415
+ "",
416
+ `- Generated: ${date}`,
417
+ "- Source: `tasks.md`, `spec.md`",
418
+ "",
419
+ header,
420
+ separator,
421
+ ...features.map(
422
+ (f) => `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${f.status} | ${f.progress} | ${f.path} |`
423
+ ),
424
+ ""
425
+ ].join("\n");
426
+ await fs5.writeFile(outputPath, content, "utf-8");
427
+ console.log(chalk.green(`\u2705 ${outputPath} \uC0DD\uC131 \uC644\uB8CC`));
428
+ }
429
+ }
430
+ function extractSpecValue(content, key) {
431
+ const regex = new RegExp(`^- \\*\\*${key}\\*\\*:\\s*(.*)$`, "m");
432
+ const match = content.match(regex);
433
+ return match ? match[1].trim() : "";
434
+ }
435
+ function countTasks(content) {
436
+ let total = 0;
437
+ let done = 0;
438
+ let doing = 0;
439
+ let todo = 0;
440
+ const lines = content.split("\n");
441
+ for (const line of lines) {
442
+ const match = line.match(/^- \[([A-Z]+)\]/);
443
+ if (match) {
444
+ total++;
445
+ const status = match[1];
446
+ if (status === "DONE") done++;
447
+ else if (status === "DOING" || status === "REVIEW") doing++;
448
+ else if (status === "TODO") todo++;
449
+ }
450
+ }
451
+ return { total, done, doing, todo };
452
+ }
453
+
454
+ // src/index.ts
455
+ program.name("lee-spec-kit").description(
456
+ "Project documentation structure generator for AI-assisted development"
457
+ ).version("0.1.0");
458
+ initCommand(program);
459
+ featureCommand(program);
460
+ statusCommand(program);
461
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "lee-spec-kit",
3
+ "version": "0.1.0",
4
+ "description": "Project documentation structure generator for AI-assisted development",
5
+ "type": "module",
6
+ "bin": {
7
+ "lee-spec-kit": "./dist/index.js",
8
+ "lsk": "./dist/index.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "files": [
12
+ "dist",
13
+ "templates"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "lint": "eslint src",
19
+ "format": "prettier --write .",
20
+ "prepublishOnly": "pnpm build"
21
+ },
22
+ "keywords": [
23
+ "docs",
24
+ "template",
25
+ "cli",
26
+ "ai",
27
+ "agent",
28
+ "specification",
29
+ "documentation"
30
+ ],
31
+ "author": "Lee Yoonsu",
32
+ "license": "MIT",
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/leeyoonsu/lee-spec-kit"
39
+ },
40
+ "dependencies": {
41
+ "chalk": "^5.6.2",
42
+ "commander": "^14.0.2",
43
+ "fs-extra": "^11.3.3",
44
+ "glob": "^13.0.0",
45
+ "prompts": "^2.4.2"
46
+ },
47
+ "devDependencies": {
48
+ "@types/fs-extra": "^11.0.4",
49
+ "@types/node": "^25.0.3",
50
+ "@types/prompts": "^2.4.9",
51
+ "@typescript-eslint/eslint-plugin": "^8.51.0",
52
+ "@typescript-eslint/parser": "^8.51.0",
53
+ "eslint": "^9.39.2",
54
+ "eslint-config-prettier": "^10.1.8",
55
+ "prettier": "^3.7.4",
56
+ "tsup": "^8.5.1",
57
+ "typescript": "^5.9.3"
58
+ },
59
+ "packageManager": "pnpm@10.7.0"
60
+ }
@@ -0,0 +1,12 @@
1
+ # {{projectName}} Documentation Guide
2
+
3
+ This documentation is organized by feature to help agents quickly understand the project.
4
+
5
+ ## Directory Structure
6
+
7
+ | Path | Purpose | Key Documents |
8
+ | ------------------- | --------------------- | ------------------------------------------------------------- |
9
+ | `docs/agents/` | Agent operating rules | `agents.md`, `constitution.md`, `git-workflow.md` |
10
+ | `docs/prd/` | Product requirements | Project-specific |
11
+ | `docs/features/be/` | Backend features | `{feature-id}/spec.md`, `plan.md`, `tasks.md`, `decisions.md` |
12
+ | `docs/features/fe/` | Frontend features | `{feature-id}/spec.md`, `plan.md`, `tasks.md`, `decisions.md` |