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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/index.js +461 -0
- package/package.json +60 -0
- package/templates/en/fullstack/README.md +12 -0
- package/templates/en/fullstack/agents/agents.md +88 -0
- package/templates/en/fullstack/agents/constitution.md +75 -0
- package/templates/en/fullstack/agents/git-workflow.md +112 -0
- package/templates/en/fullstack/agents/issue-template.md +58 -0
- package/templates/en/fullstack/agents/pr-template.md +57 -0
- package/templates/en/fullstack/features/README.md +76 -0
- package/templates/en/fullstack/features/be/README.md +5 -0
- package/templates/en/fullstack/features/fe/README.md +5 -0
- package/templates/en/fullstack/features/feature-base/decisions.md +15 -0
- package/templates/en/fullstack/features/feature-base/plan.md +49 -0
- package/templates/en/fullstack/features/feature-base/spec.md +55 -0
- package/templates/en/fullstack/features/feature-base/tasks.md +33 -0
- package/templates/en/fullstack/prd/README.md +15 -0
- package/templates/en/single/README.md +11 -0
- package/templates/en/single/agents/agents.md +74 -0
- package/templates/en/single/agents/constitution.md +75 -0
- package/templates/en/single/agents/git-workflow.md +157 -0
- package/templates/en/single/agents/issue-template.md +60 -0
- package/templates/en/single/agents/pr-template.md +71 -0
- package/templates/en/single/features/README.md +56 -0
- package/templates/en/single/features/feature-base/decisions.md +15 -0
- package/templates/en/single/features/feature-base/plan.md +48 -0
- package/templates/en/single/features/feature-base/spec.md +55 -0
- package/templates/en/single/features/feature-base/tasks.md +33 -0
- package/templates/en/single/prd/README.md +15 -0
- package/templates/ko/fullstack/README.md +12 -0
- package/templates/ko/fullstack/agents/agents.md +125 -0
- package/templates/ko/fullstack/agents/constitution.md +75 -0
- package/templates/ko/fullstack/agents/git-workflow.md +157 -0
- package/templates/ko/fullstack/agents/issue-template.md +60 -0
- package/templates/ko/fullstack/agents/pr-template.md +71 -0
- package/templates/ko/fullstack/features/README.md +98 -0
- package/templates/ko/fullstack/features/be/README.md +5 -0
- package/templates/ko/fullstack/features/fe/README.md +5 -0
- package/templates/ko/fullstack/features/feature-base/decisions.md +15 -0
- package/templates/ko/fullstack/features/feature-base/plan.md +49 -0
- package/templates/ko/fullstack/features/feature-base/spec.md +55 -0
- package/templates/ko/fullstack/features/feature-base/tasks.md +33 -0
- package/templates/ko/fullstack/prd/README.md +15 -0
- package/templates/ko/single/README.md +11 -0
- package/templates/ko/single/agents/agents.md +87 -0
- package/templates/ko/single/agents/constitution.md +75 -0
- package/templates/ko/single/agents/git-workflow.md +157 -0
- package/templates/ko/single/agents/issue-template.md +60 -0
- package/templates/ko/single/agents/pr-template.md +71 -0
- package/templates/ko/single/features/README.md +56 -0
- package/templates/ko/single/features/feature-base/decisions.md +15 -0
- package/templates/ko/single/features/feature-base/plan.md +48 -0
- package/templates/ko/single/features/feature-base/spec.md +55 -0
- package/templates/ko/single/features/feature-base/tasks.md +33 -0
- 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
|
+
[](https://www.npmjs.com/package/lee-spec-kit)
|
|
6
|
+
[](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` |
|