lee-spec-kit 0.6.8 → 0.6.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 +14 -16
- package/README.md +20 -22
- package/dist/index.js +196 -123
- package/package.json +1 -1
- package/templates/en/common/agents/issue-template.md +3 -3
- package/templates/en/common/features/README.md +1 -1
- package/templates/en/common/features/feature-base/plan.md +1 -1
- package/templates/en/common/features/feature-base/spec.md +1 -1
- package/templates/en/common/features/feature-base/tasks.md +1 -1
- package/templates/en/common/ideas/README.md +1 -1
- package/templates/ko/common/agents/issue-template.md +3 -3
- package/templates/ko/common/features/README.md +1 -1
- package/templates/ko/common/features/feature-base/plan.md +1 -1
- package/templates/ko/common/features/feature-base/spec.md +1 -1
- package/templates/ko/common/features/feature-base/tasks.md +1 -1
- package/templates/ko/common/ideas/README.md +1 -1
package/README.en.md
CHANGED
|
@@ -67,7 +67,7 @@ npx lee-spec-kit doctor
|
|
|
67
67
|
### 🚀 Feature creation
|
|
68
68
|
|
|
69
69
|
- Generates `spec.md`, `plan.md`, `tasks.md`, `decisions.md`
|
|
70
|
-
- Multi mode supports flexible component separation (e.g.
|
|
70
|
+
- Multi mode supports flexible component separation (e.g. app/api/worker)
|
|
71
71
|
- Integrates Issue/PR templates (docs side)
|
|
72
72
|
|
|
73
73
|
### 📊 Status management
|
|
@@ -109,16 +109,14 @@ npx lee-spec-kit init --name my-project --type fullstack # alias
|
|
|
109
109
|
| Option | Description | Default |
|
|
110
110
|
| ------------------- | ------------------------------------------------------------------------------------------- | ------------------------------- |
|
|
111
111
|
| `-n, --name <name>` | Project name | current folder |
|
|
112
|
-
| `-t, --type <type>` | `single` or `multi` (`fullstack` alias supported) | interactive (`
|
|
113
|
-
| `--components <list>` | multi component list (comma-separated, e.g. `
|
|
112
|
+
| `-t, --type <type>` | `single` or `multi` (`fullstack` alias supported) | interactive (`multi` with `--yes`/`--non-interactive`) |
|
|
113
|
+
| `--components <list>` | multi component list (comma-separated, e.g. `app,api,worker`) | `app` |
|
|
114
114
|
| `-l, --lang <lang>` | `ko` or `en` | `en` |
|
|
115
115
|
| `--workflow <mode>` | Workflow mode: `github` (issue/PR/review) or `local` (local-first) | `github` |
|
|
116
116
|
| `-d, --dir <dir>` | Install directory | `./docs` |
|
|
117
117
|
| `--docs-repo <mode>` | docs repo mode (`embedded` or `standalone`) | `embedded` |
|
|
118
|
-
| `--project-root <path>` | standalone(single) project repo path or standalone(multi) JSON map (`{"
|
|
119
|
-
| `--
|
|
120
|
-
| `--be-project-root <path>` | standalone(multi) backend repo path | - |
|
|
121
|
-
| `--component-project-roots <pairs>` | standalone(multi) component roots (`fe=/path/fe,be=/path/be,worker=/path/worker`) | - |
|
|
118
|
+
| `--project-root <path>` | standalone(single) project repo path or standalone(multi) JSON map (`{"app":"/path/app","api":"/path/api"}`) | - |
|
|
119
|
+
| `--component-project-roots <pairs>` | standalone(multi) component roots (`app=/path/app,api=/path/api,worker=/path/worker`) | - |
|
|
122
120
|
| `--push-docs` | enable standalone docs push (use with `--docs-remote`) | `false` |
|
|
123
121
|
| `--docs-remote <url>` | standalone docs remote URL (used with `--push-docs`) | - |
|
|
124
122
|
| `-y, --yes` | Skip most interactive inputs (overwrite confirmation still appears if target dir is not empty) | - |
|
|
@@ -149,8 +147,8 @@ The `--json` payload includes `isLeeSpecKitProject`, `reasonCode` (`PROJECT_DETE
|
|
|
149
147
|
npx lee-spec-kit feature user-auth
|
|
150
148
|
|
|
151
149
|
# Multi
|
|
152
|
-
npx lee-spec-kit feature --component
|
|
153
|
-
npx lee-spec-kit feature --component
|
|
150
|
+
npx lee-spec-kit feature --component api user-auth
|
|
151
|
+
npx lee-spec-kit feature --component app user-profile
|
|
154
152
|
npx lee-spec-kit feature --component worker queue-jobs
|
|
155
153
|
|
|
156
154
|
# Specify Feature ID/description
|
|
@@ -196,7 +194,7 @@ Use advanced selectors (`--component`, `--all`, `--done`) only when you need mul
|
|
|
196
194
|
| Option | Description |
|
|
197
195
|
| -------------- | ----------------------------------------------- |
|
|
198
196
|
| `--json` | JSON output for agents |
|
|
199
|
-
| `--component <id>` | Select target component in multi mode (e.g. `
|
|
197
|
+
| `--component <id>` | Select target component in multi mode (e.g. `app`, `api`, `worker`) |
|
|
200
198
|
| `--all` | Include completed features when auto-detecting |
|
|
201
199
|
| `--done` | Show completed (workflow-done) features only |
|
|
202
200
|
| `--approve <reply>` | Approve one labeled option using any reply that includes a label token (e.g. `A`, `A OK`, `A proceed`) |
|
|
@@ -266,7 +264,7 @@ npx lee-spec-kit view --json
|
|
|
266
264
|
| Option | Description |
|
|
267
265
|
| -------------- | ----------------------------------------------- |
|
|
268
266
|
| `--json` | JSON output for agents |
|
|
269
|
-
| `--component <id>` | Select target component in multi mode (e.g. `
|
|
267
|
+
| `--component <id>` | Select target component in multi mode (e.g. `app`, `api`, `worker`) |
|
|
270
268
|
| `--all` | Include completed features when auto-detecting |
|
|
271
269
|
| `--done` | Show completed (workflow-done) features only |
|
|
272
270
|
|
|
@@ -291,7 +289,7 @@ npx lee-spec-kit flow --strict
|
|
|
291
289
|
| Option | Description |
|
|
292
290
|
| ----------------- | ----------- |
|
|
293
291
|
| `--json` | JSON output for agents |
|
|
294
|
-
| `--component <id>`| Select target component in multi mode (e.g. `
|
|
292
|
+
| `--component <id>`| Select target component in multi mode (e.g. `app`, `api`, `worker`) |
|
|
295
293
|
| `--all` | Include completed features when auto-detecting |
|
|
296
294
|
| `--done` | Show completed (workflow-done) features only |
|
|
297
295
|
| `--approve <reply>` | Pass through context label approval (e.g. `A`, `A OK`, `A proceed`) |
|
|
@@ -419,7 +417,7 @@ Running `init` creates `.lee-spec-kit.json` in your docs root (default: `docs/`)
|
|
|
419
417
|
| ------------- | ------------------------------------------------ |
|
|
420
418
|
| `projectName` | Project name |
|
|
421
419
|
| `projectType` | `single` or `multi` (`fullstack` alias supported) |
|
|
422
|
-
| `components` | (multi only) component list (e.g. `["
|
|
420
|
+
| `components` | (multi only) component list (e.g. `["app","api","worker"]`) |
|
|
423
421
|
| `lang` | `ko` or `en` |
|
|
424
422
|
| `createdAt` | Creation date |
|
|
425
423
|
| `docsRepo` | `embedded` or `standalone` |
|
|
@@ -544,12 +542,12 @@ npx lee-spec-kit config --project-root /new/path
|
|
|
544
542
|
npx lee-spec-kit config --dir ./docs2 --project-root /new/path
|
|
545
543
|
|
|
546
544
|
# update projectRoot (multi)
|
|
547
|
-
npx lee-spec-kit config --project-root /new/
|
|
548
|
-
npx lee-spec-kit config --project-root /new/
|
|
545
|
+
npx lee-spec-kit config --project-root /new/app/path --component app
|
|
546
|
+
npx lee-spec-kit config --project-root /new/api/path --component api
|
|
549
547
|
npx lee-spec-kit config --project-root /new/worker/path --component worker
|
|
550
548
|
|
|
551
549
|
# non-interactive mode (fails immediately if required input is missing)
|
|
552
|
-
npx lee-spec-kit config --project-root /new/
|
|
550
|
+
npx lee-spec-kit config --project-root /new/app/path --component app --non-interactive
|
|
553
551
|
```
|
|
554
552
|
|
|
555
553
|
**Options:**
|
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ npx lee-spec-kit doctor
|
|
|
80
80
|
### 🚀 Feature 생성
|
|
81
81
|
|
|
82
82
|
- spec.md, plan.md, tasks.md, decisions.md 자동 생성
|
|
83
|
-
-
|
|
83
|
+
- Multi 프로젝트에서 임의 컴포넌트(app/api/worker 등) 분리 지원
|
|
84
84
|
- GitHub Issue/PR 템플릿 연동
|
|
85
85
|
|
|
86
86
|
### 📊 상태 관리
|
|
@@ -125,16 +125,14 @@ npx lee-spec-kit init --name my-project --type fullstack # alias
|
|
|
125
125
|
| 옵션 | 설명 | 기본값 |
|
|
126
126
|
| ------------------- | -------------------------------------------------------------------- | ------------------------- |
|
|
127
127
|
| `-n, --name <name>` | 프로젝트 이름 | 현재 폴더명 |
|
|
128
|
-
| `-t, --type <type>` | `single` 또는 `multi` (`fullstack` alias 지원) | 대화형 선택 (`--yes`/`--non-interactive`면 `
|
|
129
|
-
| `--components <list>` | multi 컴포넌트 목록 (쉼표 구분, 예: `
|
|
128
|
+
| `-t, --type <type>` | `single` 또는 `multi` (`fullstack` alias 지원) | 대화형 선택 (`--yes`/`--non-interactive`면 `multi`) |
|
|
129
|
+
| `--components <list>` | multi 컴포넌트 목록 (쉼표 구분, 예: `app,api,worker`) | `app` |
|
|
130
130
|
| `-l, --lang <lang>` | `ko` (한국어) 또는 `en` (영어) | `en` |
|
|
131
131
|
| `--workflow <mode>` | 워크플로우 모드: `github`(issue/PR/review 포함) 또는 `local`(로컬 중심) | `github` |
|
|
132
132
|
| `-d, --dir <dir>` | 설치 디렉토리 | `./docs` |
|
|
133
133
|
| `--docs-repo <mode>` | docs 레포 모드 (`embedded` 또는 `standalone`) | `embedded` |
|
|
134
|
-
| `--project-root <path>` | standalone(single) 프로젝트 레포 경로 또는 standalone(multi) JSON 매핑 (`{"
|
|
135
|
-
| `--
|
|
136
|
-
| `--be-project-root <path>` | standalone(multi) BE 레포 경로 | - |
|
|
137
|
-
| `--component-project-roots <pairs>` | standalone(multi) 컴포넌트별 레포 경로 (`fe=/path/fe,be=/path/be,worker=/path/worker`) | - |
|
|
134
|
+
| `--project-root <path>` | standalone(single) 프로젝트 레포 경로 또는 standalone(multi) JSON 매핑 (`{"app":"/path/app","api":"/path/api"}`) | - |
|
|
135
|
+
| `--component-project-roots <pairs>` | standalone(multi) 컴포넌트별 레포 경로 (`app=/path/app,api=/path/api,worker=/path/worker`) | - |
|
|
138
136
|
| `--push-docs` | standalone docs 원격 push 사용 (`--docs-remote`와 함께 사용) | `false` |
|
|
139
137
|
| `--docs-remote <url>` | standalone docs 원격 URL (`--push-docs`와 함께 사용) | - |
|
|
140
138
|
| `-y, --yes` | 대화형 입력을 대부분 스킵 (단, 대상 디렉토리가 비어있지 않으면 덮어쓰기 확인은 표시) | - |
|
|
@@ -165,8 +163,8 @@ npx lee-spec-kit detect --dir /path/to/workspace
|
|
|
165
163
|
npx lee-spec-kit feature user-auth
|
|
166
164
|
|
|
167
165
|
# Multi 프로젝트
|
|
168
|
-
npx lee-spec-kit feature --component
|
|
169
|
-
npx lee-spec-kit feature --component
|
|
166
|
+
npx lee-spec-kit feature --component api user-auth
|
|
167
|
+
npx lee-spec-kit feature --component app user-profile
|
|
170
168
|
npx lee-spec-kit feature --component worker queue-jobs
|
|
171
169
|
|
|
172
170
|
# Feature ID/설명 지정
|
|
@@ -213,7 +211,7 @@ npx lee-spec-kit context F001 --approve A --execute --ticket <TICKET> --execute-
|
|
|
213
211
|
| 옵션 | 설명 |
|
|
214
212
|
| --------------- | ----------------------------------------------- |
|
|
215
213
|
| `--json` | 에이전트용 JSON 출력 |
|
|
216
|
-
| `--component <id>` | multi에서 대상 컴포넌트 지정 (예: `
|
|
214
|
+
| `--component <id>` | multi에서 대상 컴포넌트 지정 (예: `app`, `api`, `worker`) |
|
|
217
215
|
| `--all` | 자동 감지 실패 시 완료된 Feature까지 포함해서 표시 |
|
|
218
216
|
| `--done` | 완료(workflow-done) Feature만 표시 |
|
|
219
217
|
| `--approve <reply>` | 라벨 포함 승인 응답으로 단일 옵션 선택 (예: `A`, `A OK`, `A 진행해`) |
|
|
@@ -283,7 +281,7 @@ npx lee-spec-kit view --json
|
|
|
283
281
|
| 옵션 | 설명 |
|
|
284
282
|
| --------------- | ----------------------------------------------- |
|
|
285
283
|
| `--json` | 에이전트용 JSON 출력 |
|
|
286
|
-
| `--component <id>` | multi에서 대상 컴포넌트 지정 (예: `
|
|
284
|
+
| `--component <id>` | multi에서 대상 컴포넌트 지정 (예: `app`, `api`, `worker`) |
|
|
287
285
|
| `--all` | 자동 감지 실패 시 완료된 Feature까지 포함해서 표시 |
|
|
288
286
|
| `--done` | 완료(workflow-done) Feature만 표시 |
|
|
289
287
|
|
|
@@ -308,7 +306,7 @@ npx lee-spec-kit flow --strict
|
|
|
308
306
|
| 옵션 | 설명 |
|
|
309
307
|
| ------------------ | ---- |
|
|
310
308
|
| `--json` | 에이전트용 JSON 출력 |
|
|
311
|
-
| `--component <id>` | multi에서 대상 컴포넌트 지정 (예: `
|
|
309
|
+
| `--component <id>` | multi에서 대상 컴포넌트 지정 (예: `app`, `api`, `worker`) |
|
|
312
310
|
| `--all` | 자동 감지 실패 시 완료된 Feature까지 포함해서 표시 |
|
|
313
311
|
| `--done` | 완료(workflow-done) Feature만 표시 |
|
|
314
312
|
| `--approve <reply>`| context 라벨 승인 응답 전달 (예: `A`, `A OK`, `A 진행해`) |
|
|
@@ -466,7 +464,7 @@ npx lee-spec-kit update --force
|
|
|
466
464
|
| ------------- | --------------------------------------- |
|
|
467
465
|
| `projectName` | 프로젝트 이름 |
|
|
468
466
|
| `projectType` | `single` 또는 `multi` (`fullstack` alias 지원) |
|
|
469
|
-
| `components` | (multi만) 컴포넌트 목록 (예: `["
|
|
467
|
+
| `components` | (multi만) 컴포넌트 목록 (예: `["app","api","worker"]`) |
|
|
470
468
|
| `lang` | `ko` 또는 `en` |
|
|
471
469
|
| `createdAt` | 생성 날짜 |
|
|
472
470
|
| `docsRepo` | `embedded` 또는 `standalone` |
|
|
@@ -603,8 +601,8 @@ npx lee-spec-kit update --force
|
|
|
603
601
|
"docsRepo": "standalone",
|
|
604
602
|
"pushDocs": false,
|
|
605
603
|
"projectRoot": {
|
|
606
|
-
"
|
|
607
|
-
"
|
|
604
|
+
"app": "/path/to/app",
|
|
605
|
+
"api": "/path/to/api"
|
|
608
606
|
}
|
|
609
607
|
}
|
|
610
608
|
```
|
|
@@ -623,12 +621,12 @@ npx lee-spec-kit config --project-root /new/path
|
|
|
623
621
|
npx lee-spec-kit config --dir ./docs2 --project-root /new/path
|
|
624
622
|
|
|
625
623
|
# projectRoot 수정 (Multi)
|
|
626
|
-
npx lee-spec-kit config --project-root /new/
|
|
627
|
-
npx lee-spec-kit config --project-root /new/
|
|
624
|
+
npx lee-spec-kit config --project-root /new/app/path --component app
|
|
625
|
+
npx lee-spec-kit config --project-root /new/api/path --component api
|
|
628
626
|
npx lee-spec-kit config --project-root /new/worker/path --component worker
|
|
629
627
|
|
|
630
628
|
# 비대화형 모드 (필수 옵션 누락 시 즉시 실패)
|
|
631
|
-
npx lee-spec-kit config --project-root /new/
|
|
629
|
+
npx lee-spec-kit config --project-root /new/app/path --component app --non-interactive
|
|
632
630
|
```
|
|
633
631
|
|
|
634
632
|
**옵션:**
|
|
@@ -659,7 +657,7 @@ npx lee-spec-kit config --project-root /new/fe/path --component fe --non-interac
|
|
|
659
657
|
|
|
660
658
|
## 생성되는 구조
|
|
661
659
|
|
|
662
|
-
###
|
|
660
|
+
### Multi (컴포넌트 분리)
|
|
663
661
|
|
|
664
662
|
```
|
|
665
663
|
docs/
|
|
@@ -675,8 +673,8 @@ docs/
|
|
|
675
673
|
│ └── README.md
|
|
676
674
|
└── features/
|
|
677
675
|
├── README.md
|
|
678
|
-
├──
|
|
679
|
-
└──
|
|
676
|
+
├── app/ # App Features
|
|
677
|
+
└── api/ # API Features
|
|
680
678
|
```
|
|
681
679
|
|
|
682
680
|
### Single (단일 레포)
|
|
@@ -715,7 +713,7 @@ flowchart LR
|
|
|
715
713
|
| 프로젝트 타입 | 설명 |
|
|
716
714
|
| ------------- | -------------------------------------------- |
|
|
717
715
|
| `single` | 단일 레포 프로젝트 (모노레포 또는 단일 스택) |
|
|
718
|
-
| `multi` | 멀티 컴포넌트 프로젝트 (예:
|
|
716
|
+
| `multi` | 멀티 컴포넌트 프로젝트 (예: app/api/worker) |
|
|
719
717
|
| `fullstack` | `multi`의 하위호환 alias |
|
|
720
718
|
|
|
721
719
|
## 문제 해결
|
package/dist/index.js
CHANGED
|
@@ -164,14 +164,12 @@ var I18N = {
|
|
|
164
164
|
"init.choice.projectType.single.title": "Single - \uB2E8\uC77C \uB808\uD3EC \uD504\uB85C\uC81D\uD2B8",
|
|
165
165
|
"init.choice.projectType.single.desc": "features/ \uD3F4\uB354 \uD558\uB098\uB85C \uAD00\uB9AC",
|
|
166
166
|
"init.choice.projectType.fullstack.title": "Multi - \uBA40\uD2F0 \uCEF4\uD3EC\uB10C\uD2B8 \uD504\uB85C\uC81D\uD2B8",
|
|
167
|
-
"init.choice.projectType.fullstack.desc": "Multi \uCEF4\uD3EC\uB10C\uD2B8 \uD504\uB85C\uC81D\uD2B8 (\uAE30\uBCF8: features/
|
|
167
|
+
"init.choice.projectType.fullstack.desc": "Multi \uCEF4\uD3EC\uB10C\uD2B8 \uD504\uB85C\uC81D\uD2B8 (\uAE30\uBCF8: features/{component}/)",
|
|
168
168
|
"init.prompt.docsMode": "Docs \uAD00\uB9AC \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
|
|
169
169
|
"init.choice.docsRepo.embedded.title": "embedded - \uD504\uB85C\uC81D\uD2B8 \uB0B4 \uD3EC\uD568 (./docs)",
|
|
170
170
|
"init.choice.docsRepo.embedded.desc": "\uD504\uB85C\uC81D\uD2B8\uC640 \uD568\uAED8 push\uB429\uB2C8\uB2E4",
|
|
171
171
|
"init.choice.docsRepo.standalone.title": "standalone - \uBCC4\uB3C4 \uB3C5\uB9BD \uB808\uD3EC",
|
|
172
172
|
"init.choice.docsRepo.standalone.desc": "push \uC5EC\uBD80\uB97C \uBCC4\uB3C4\uB85C \uC124\uC815\uD569\uB2C8\uB2E4",
|
|
173
|
-
"init.prompt.feRepoPath": "Frontend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
|
|
174
|
-
"init.prompt.beRepoPath": "Backend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
|
|
175
173
|
"init.prompt.componentRepoPath": "{component} \uCEF4\uD3EC\uB10C\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
|
|
176
174
|
"init.prompt.projectRepoPath": "\uD504\uB85C\uC81D\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
|
|
177
175
|
"init.validation.enterPath": "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694",
|
|
@@ -317,7 +315,7 @@ var I18N = {
|
|
|
317
315
|
"cliError.configOrDocs.runFromDocsDir": "docs/\uAC00 \uC788\uB294 \uB514\uB809\uD130\uB9AC\uC5D0\uC11C \uBA85\uB839\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
|
|
318
316
|
"cliError.lock.retryLater": "\uC7A0\uC2DC \uAE30\uB2E4\uB9B0 \uB4A4 \uAC19\uC740 \uBA85\uB839\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
|
|
319
317
|
"cliError.lock.checkOtherProcess": "\uB2E4\uB978 lee-spec-kit \uD504\uB85C\uC138\uC2A4\uAC00 \uC2E4\uD589 \uC911\uC778\uC9C0 \uD655\uC778\uD558\uC138\uC694.",
|
|
320
|
-
"cliError.lock.inspectLockFiles": "\
|
|
318
|
+
"cliError.lock.inspectLockFiles": "\uB7F0\uD0C0\uC784 lock \uD30C\uC77C(\uD504\uB85C\uC81D\uD2B8 `.git/lee-spec-kit.runtime/locks` \uB610\uB294 OS temp)\uC744 \uD655\uC778\uD558\uC138\uC694.",
|
|
321
319
|
"cliError.invalidArg.reviewUsage": "\uBA85\uB839 \uC0AC\uC6A9\uBC95\uACFC \uC720\uD6A8\uD55C \uD50C\uB798\uADF8\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
322
320
|
"cliError.invalidArg.fixValues": "\uC798\uBABB\uB41C \uAC12\uC744 \uC218\uC815\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
|
|
323
321
|
"cliError.invalidArg.validateBeforeAutomation": "\uC790\uB3D9\uD654 \uD658\uACBD\uC774\uB77C\uBA74 CLI \uD638\uCD9C \uC804\uC5D0 \uC778\uC790\uB97C \uAC80\uC99D\uD558\uC138\uC694.",
|
|
@@ -353,7 +351,7 @@ var I18N = {
|
|
|
353
351
|
"cliError.unknown.runDoctor": "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC0C1\uD0DC\uB97C \uC9C4\uB2E8\uD558\uC138\uC694.",
|
|
354
352
|
"cliError.unknown.reportReasonCode": "reasonCode\uC640 \uB85C\uADF8\uB97C \uC720\uC9C0\uBCF4\uC218\uC790\uC5D0\uAC8C \uC804\uB2EC\uD558\uC138\uC694.",
|
|
355
353
|
"context.git.standaloneProjectRootMissing": "standalone \uBAA8\uB4DC\uC785\uB2C8\uB2E4. projectRoot\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58 \uD655\uC778\uC774 \uBD88\uAC00\uB2A5\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
|
|
356
|
-
"context.git.multiProjectRootShapeInvalid": 'multi standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: { "
|
|
354
|
+
"context.git.multiProjectRootShapeInvalid": 'multi standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: { "app": "...", "api": "...", "worker": "..." })',
|
|
357
355
|
"context.git.multiProjectRootRepoMissing": "projectRoot.{repo}\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ... --component {repo})",
|
|
358
356
|
"context.git.singleProjectRootShapeInvalid": 'single standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: "/path/to/project")',
|
|
359
357
|
"validation.nameEmpty": "\uC774\uB984\uC740 \uBE44\uC5B4\uC788\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
@@ -562,14 +560,12 @@ var I18N = {
|
|
|
562
560
|
"init.choice.projectType.single.title": "Single - single repo project",
|
|
563
561
|
"init.choice.projectType.single.desc": "Manage with a single features/ folder",
|
|
564
562
|
"init.choice.projectType.fullstack.title": "Multi - multi-component project",
|
|
565
|
-
"init.choice.projectType.fullstack.desc": "Default structure uses features/
|
|
563
|
+
"init.choice.projectType.fullstack.desc": "Default structure uses features/{component}/",
|
|
566
564
|
"init.prompt.docsMode": "Select docs mode:",
|
|
567
565
|
"init.choice.docsRepo.embedded.title": "embedded - inside the project (./docs)",
|
|
568
566
|
"init.choice.docsRepo.embedded.desc": "Pushed together with the project",
|
|
569
567
|
"init.choice.docsRepo.standalone.title": "standalone - separate docs repo",
|
|
570
568
|
"init.choice.docsRepo.standalone.desc": "Configure push settings separately",
|
|
571
|
-
"init.prompt.feRepoPath": "Enter frontend repository path:",
|
|
572
|
-
"init.prompt.beRepoPath": "Enter backend repository path:",
|
|
573
569
|
"init.prompt.componentRepoPath": 'Enter repository path for component "{component}":',
|
|
574
570
|
"init.prompt.projectRepoPath": "Enter project repository path:",
|
|
575
571
|
"init.validation.enterPath": "Please enter a path",
|
|
@@ -715,7 +711,7 @@ var I18N = {
|
|
|
715
711
|
"cliError.configOrDocs.runFromDocsDir": "Run command from the directory that contains docs/.",
|
|
716
712
|
"cliError.lock.retryLater": "Wait briefly, then retry the same command.",
|
|
717
713
|
"cliError.lock.checkOtherProcess": "Check whether another lee-spec-kit process is still running.",
|
|
718
|
-
"cliError.lock.inspectLockFiles": "Inspect lock files (
|
|
714
|
+
"cliError.lock.inspectLockFiles": "Inspect runtime lock files (project `.git/lee-spec-kit.runtime/locks` or OS temp).",
|
|
719
715
|
"cliError.invalidArg.reviewUsage": "Review command usage and valid flags.",
|
|
720
716
|
"cliError.invalidArg.fixValues": "Fix invalid value(s) and retry.",
|
|
721
717
|
"cliError.invalidArg.validateBeforeAutomation": "If using automation, validate arguments before invoking CLI.",
|
|
@@ -751,7 +747,7 @@ var I18N = {
|
|
|
751
747
|
"cliError.unknown.runDoctor": "Run diagnostics for workspace state.",
|
|
752
748
|
"cliError.unknown.reportReasonCode": "Report the reasonCode and logs to maintainers.",
|
|
753
749
|
"context.git.standaloneProjectRootMissing": "Standalone mode is enabled, but projectRoot is missing. Cannot resolve project branch. (npx lee-spec-kit config --project-root ...)",
|
|
754
|
-
"context.git.multiProjectRootShapeInvalid": 'Multi standalone mode requires projectRoot as an object. (Example: { "
|
|
750
|
+
"context.git.multiProjectRootShapeInvalid": 'Multi standalone mode requires projectRoot as an object. (Example: { "app": "...", "api": "...", "worker": "..." })',
|
|
755
751
|
"context.git.multiProjectRootRepoMissing": "projectRoot.{repo} is empty. (npx lee-spec-kit config --project-root ... --component {repo})",
|
|
756
752
|
"context.git.singleProjectRootShapeInvalid": 'Single standalone mode requires projectRoot as a string path. (Example: "/path/to/project")',
|
|
757
753
|
"validation.nameEmpty": "Name cannot be empty.",
|
|
@@ -1077,7 +1073,7 @@ function printCliErrorSuggestions(suggestions, lang = DEFAULT_LANG) {
|
|
|
1077
1073
|
}
|
|
1078
1074
|
|
|
1079
1075
|
// src/utils/components.ts
|
|
1080
|
-
var
|
|
1076
|
+
var DEFAULT_MULTI_COMPONENTS = ["app"];
|
|
1081
1077
|
function unique(values) {
|
|
1082
1078
|
const seen = /* @__PURE__ */ new Set();
|
|
1083
1079
|
const out = [];
|
|
@@ -1114,7 +1110,7 @@ function resolveProjectComponents(projectType, configured) {
|
|
|
1114
1110
|
if (projectType === "single") return [];
|
|
1115
1111
|
const normalized = normalizeComponentList(configured);
|
|
1116
1112
|
if (normalized.length > 0) return normalized;
|
|
1117
|
-
return [...
|
|
1113
|
+
return [...DEFAULT_MULTI_COMPONENTS];
|
|
1118
1114
|
}
|
|
1119
1115
|
function assertAllowedComponent(component, allowed) {
|
|
1120
1116
|
if (!allowed.includes(component)) {
|
|
@@ -1124,11 +1120,6 @@ function assertAllowedComponent(component, allowed) {
|
|
|
1124
1120
|
);
|
|
1125
1121
|
}
|
|
1126
1122
|
}
|
|
1127
|
-
function isDefaultFullstackComponents(components) {
|
|
1128
|
-
if (components.length !== 2) return false;
|
|
1129
|
-
const set = new Set(components);
|
|
1130
|
-
return set.has("fe") && set.has("be");
|
|
1131
|
-
}
|
|
1132
1123
|
|
|
1133
1124
|
// src/utils/project-type.ts
|
|
1134
1125
|
function normalizeProjectType(input) {
|
|
@@ -1240,39 +1231,62 @@ function assertValid(result, context, lang = DEFAULT_LANG) {
|
|
|
1240
1231
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1241
1232
|
var DEFAULT_POLL_MS = 150;
|
|
1242
1233
|
var DEFAULT_STALE_MS = 2 * 6e4;
|
|
1234
|
+
var RUNTIME_GIT_DIRNAME = "lee-spec-kit.runtime";
|
|
1235
|
+
var RUNTIME_TEMP_DIRNAME = "lee-spec-kit-runtime";
|
|
1243
1236
|
function sleep(ms) {
|
|
1244
1237
|
return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
|
|
1245
1238
|
}
|
|
1246
|
-
function
|
|
1247
|
-
return path18.
|
|
1248
|
-
}
|
|
1249
|
-
function getInitLockPath(targetDir) {
|
|
1250
|
-
return path18.join(
|
|
1251
|
-
path18.dirname(targetDir),
|
|
1252
|
-
`.lee-spec-kit.${path18.basename(targetDir)}.lock`
|
|
1253
|
-
);
|
|
1239
|
+
function toScopeKey(value) {
|
|
1240
|
+
return createHash("sha1").update(path18.resolve(value)).digest("hex").slice(0, 16);
|
|
1254
1241
|
}
|
|
1255
|
-
function
|
|
1256
|
-
|
|
1257
|
-
return path18.join(os.tmpdir(), "lee-spec-kit-locks", `${key}.project.lock`);
|
|
1242
|
+
function getTempRuntimeDir(scopePath) {
|
|
1243
|
+
return path18.join(os.tmpdir(), RUNTIME_TEMP_DIRNAME, toScopeKey(scopePath));
|
|
1258
1244
|
}
|
|
1259
|
-
function
|
|
1245
|
+
function resolveGitRuntimeDir(cwd) {
|
|
1260
1246
|
try {
|
|
1261
1247
|
const out = execFileSync(
|
|
1262
1248
|
"git",
|
|
1263
|
-
["rev-parse", "--git-path",
|
|
1249
|
+
["rev-parse", "--git-path", RUNTIME_GIT_DIRNAME],
|
|
1264
1250
|
{
|
|
1265
1251
|
cwd,
|
|
1266
1252
|
encoding: "utf-8",
|
|
1267
1253
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1268
1254
|
}
|
|
1269
1255
|
).trim();
|
|
1270
|
-
if (!out) return
|
|
1256
|
+
if (!out) return null;
|
|
1271
1257
|
return path18.isAbsolute(out) ? out : path18.resolve(cwd, out);
|
|
1272
1258
|
} catch {
|
|
1273
|
-
return
|
|
1259
|
+
return null;
|
|
1274
1260
|
}
|
|
1275
1261
|
}
|
|
1262
|
+
function getRuntimeStateDir(cwd) {
|
|
1263
|
+
const resolved = path18.resolve(cwd);
|
|
1264
|
+
return resolveGitRuntimeDir(resolved) ?? getTempRuntimeDir(resolved);
|
|
1265
|
+
}
|
|
1266
|
+
function getDocsLockPath(docsDir) {
|
|
1267
|
+
return path18.join(
|
|
1268
|
+
getRuntimeStateDir(docsDir),
|
|
1269
|
+
"locks",
|
|
1270
|
+
`docs-${toScopeKey(docsDir)}.lock`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
function getInitLockPath(targetDir) {
|
|
1274
|
+
return path18.join(
|
|
1275
|
+
getRuntimeStateDir(path18.dirname(path18.resolve(targetDir))),
|
|
1276
|
+
"locks",
|
|
1277
|
+
`init-${toScopeKey(targetDir)}.lock`
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
function getApprovalTicketStorePath(docsDir) {
|
|
1281
|
+
return path18.join(
|
|
1282
|
+
getRuntimeStateDir(docsDir),
|
|
1283
|
+
"tickets",
|
|
1284
|
+
`approval-${toScopeKey(docsDir)}.json`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
function getProjectExecutionLockPath(cwd) {
|
|
1288
|
+
return path18.join(getRuntimeStateDir(cwd), "locks", "project.lock");
|
|
1289
|
+
}
|
|
1276
1290
|
async function isStaleLock(lockPath, staleMs) {
|
|
1277
1291
|
try {
|
|
1278
1292
|
const stat = await fs15.stat(lockPath);
|
|
@@ -1436,7 +1450,7 @@ function parseStandaloneMultiProjectRootJson(raw) {
|
|
|
1436
1450
|
} catch {
|
|
1437
1451
|
throw createCliError(
|
|
1438
1452
|
"INVALID_ARGUMENT",
|
|
1439
|
-
'`--project-root` for standalone multi must be a JSON object. Example: {"
|
|
1453
|
+
'`--project-root` for standalone multi must be a JSON object. Example: {"app":"/path/app","api":"/path/api"}'
|
|
1440
1454
|
);
|
|
1441
1455
|
}
|
|
1442
1456
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -1520,19 +1534,13 @@ function getComponentFeaturesReadme(lang, component) {
|
|
|
1520
1534
|
function initCommand(program2) {
|
|
1521
1535
|
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 | multi (fullstack alias)").option(
|
|
1522
1536
|
"--components <list>",
|
|
1523
|
-
"Component list for multi (comma-separated, e.g.
|
|
1537
|
+
"Component list for multi (comma-separated, e.g. app,api,worker)"
|
|
1524
1538
|
).option("-l, --lang <lang>", "Language: ko | en (default: en)").option("--workflow <mode>", "Workflow mode: github | local").option("-d, --dir <dir>", "Target directory (default: ./docs)", "./docs").option("--docs-repo <mode>", "Docs repository mode: embedded | standalone").option(
|
|
1525
1539
|
"--project-root <path>",
|
|
1526
1540
|
"Project root path (standalone single) or JSON map for standalone multi"
|
|
1527
|
-
).option(
|
|
1528
|
-
"--fe-project-root <path>",
|
|
1529
|
-
"Frontend repository path (standalone multi)"
|
|
1530
|
-
).option(
|
|
1531
|
-
"--be-project-root <path>",
|
|
1532
|
-
"Backend repository path (standalone multi)"
|
|
1533
1541
|
).option(
|
|
1534
1542
|
"--component-project-roots <pairs>",
|
|
1535
|
-
"Component roots for standalone multi (comma-separated, e.g.
|
|
1543
|
+
"Component roots for standalone multi (comma-separated, e.g. app=/path/app,api=/path/api,worker=/path/worker)"
|
|
1536
1544
|
).option("--push-docs", "Push standalone docs to remote").option("--docs-remote <url>", "Remote URL for standalone docs repository").option("-y, --yes", "Skip prompts and use defaults").option("-f, --force", "Overwrite target directory if not empty").option("--non-interactive", "Fail instead of prompting for input").action(async (options) => {
|
|
1537
1545
|
try {
|
|
1538
1546
|
await runInit(options);
|
|
@@ -1670,7 +1678,7 @@ async function runInit(options) {
|
|
|
1670
1678
|
description: tr(lang, "cli", "init.choice.projectType.fullstack.desc")
|
|
1671
1679
|
}
|
|
1672
1680
|
],
|
|
1673
|
-
initial:
|
|
1681
|
+
initial: 1
|
|
1674
1682
|
},
|
|
1675
1683
|
{
|
|
1676
1684
|
type: options.docsRepo ? null : "select",
|
|
@@ -1702,10 +1710,10 @@ async function runInit(options) {
|
|
|
1702
1710
|
docsRepo = response.docsRepo || "embedded";
|
|
1703
1711
|
if (docsRepo === "standalone") {
|
|
1704
1712
|
const resolvedType = normalizeProjectType(
|
|
1705
|
-
String(projectType || response.projectType || "
|
|
1713
|
+
String(projectType || response.projectType || "multi")
|
|
1706
1714
|
);
|
|
1707
1715
|
if (resolvedType === "multi") {
|
|
1708
|
-
const promptComponents = components.length > 0 ? components : ["
|
|
1716
|
+
const promptComponents = components.length > 0 ? components : ["app"];
|
|
1709
1717
|
const rootMap = {};
|
|
1710
1718
|
if (typeof projectRoot === "string" && projectRoot.trim()) {
|
|
1711
1719
|
Object.assign(rootMap, parseStandaloneMultiProjectRootJson(projectRoot));
|
|
@@ -1717,8 +1725,6 @@ async function runInit(options) {
|
|
|
1717
1725
|
rootMap[normalized] = normalizedRoot;
|
|
1718
1726
|
}
|
|
1719
1727
|
}
|
|
1720
|
-
if (options.feProjectRoot?.trim()) rootMap.fe = options.feProjectRoot.trim();
|
|
1721
|
-
if (options.beProjectRoot?.trim()) rootMap.be = options.beProjectRoot.trim();
|
|
1722
1728
|
Object.assign(rootMap, componentProjectRoots);
|
|
1723
1729
|
for (const component of promptComponents) {
|
|
1724
1730
|
const seeded = (rootMap[component] || "").trim();
|
|
@@ -1726,7 +1732,9 @@ async function runInit(options) {
|
|
|
1726
1732
|
rootMap[component] = seeded;
|
|
1727
1733
|
continue;
|
|
1728
1734
|
}
|
|
1729
|
-
const message =
|
|
1735
|
+
const message = tr(lang, "cli", "init.prompt.componentRepoPath", {
|
|
1736
|
+
component
|
|
1737
|
+
});
|
|
1730
1738
|
const response2 = await prompts(
|
|
1731
1739
|
[
|
|
1732
1740
|
{
|
|
@@ -1810,7 +1818,7 @@ async function runInit(options) {
|
|
|
1810
1818
|
}
|
|
1811
1819
|
}
|
|
1812
1820
|
if (!projectType) {
|
|
1813
|
-
projectType = "
|
|
1821
|
+
projectType = "multi";
|
|
1814
1822
|
}
|
|
1815
1823
|
assertValid(
|
|
1816
1824
|
validateSafeNameWithLang(projectName, lang),
|
|
@@ -1842,12 +1850,12 @@ async function runInit(options) {
|
|
|
1842
1850
|
}
|
|
1843
1851
|
} else {
|
|
1844
1852
|
if (components.length === 0) {
|
|
1845
|
-
components = ["
|
|
1853
|
+
components = ["app"];
|
|
1846
1854
|
}
|
|
1847
1855
|
components.forEach(assertValidComponentId);
|
|
1848
1856
|
}
|
|
1849
1857
|
if (docsRepo !== "standalone") {
|
|
1850
|
-
if (options.projectRoot || options.
|
|
1858
|
+
if (options.projectRoot || options.componentProjectRoots || typeof options.pushDocs === "boolean" || options.docsRemote) {
|
|
1851
1859
|
throw createCliError(
|
|
1852
1860
|
"INVALID_ARGUMENT",
|
|
1853
1861
|
"Standalone-only options require `--docs-repo standalone`."
|
|
@@ -1869,8 +1877,6 @@ async function runInit(options) {
|
|
|
1869
1877
|
multiRoot[normalized] = normalizedRoot;
|
|
1870
1878
|
}
|
|
1871
1879
|
}
|
|
1872
|
-
if (options.feProjectRoot?.trim()) multiRoot.fe = options.feProjectRoot.trim();
|
|
1873
|
-
if (options.beProjectRoot?.trim()) multiRoot.be = options.beProjectRoot.trim();
|
|
1874
1880
|
Object.assign(multiRoot, componentProjectRoots);
|
|
1875
1881
|
const unknownComponents = Object.keys(multiRoot).filter(
|
|
1876
1882
|
(component) => !components.includes(component)
|
|
@@ -1900,12 +1906,6 @@ async function runInit(options) {
|
|
|
1900
1906
|
"`--component-project-roots` can only be used when `--type multi`."
|
|
1901
1907
|
);
|
|
1902
1908
|
}
|
|
1903
|
-
if (options.feProjectRoot || options.beProjectRoot) {
|
|
1904
|
-
throw createCliError(
|
|
1905
|
-
"INVALID_ARGUMENT",
|
|
1906
|
-
"`--fe-project-root` and `--be-project-root` can only be used when `--type multi`."
|
|
1907
|
-
);
|
|
1908
|
-
}
|
|
1909
1909
|
const singleRoot = typeof projectRoot === "string" ? projectRoot.trim() : "";
|
|
1910
1910
|
if (!singleRoot) {
|
|
1911
1911
|
throw createCliError(
|
|
@@ -1991,7 +1991,7 @@ async function runInit(options) {
|
|
|
1991
1991
|
}
|
|
1992
1992
|
}
|
|
1993
1993
|
}
|
|
1994
|
-
const featurePath = projectType === "multi" ?
|
|
1994
|
+
const featurePath = projectType === "multi" ? "docs/features/{component}" : "docs/features";
|
|
1995
1995
|
const replacements = {
|
|
1996
1996
|
"{{projectName}}": projectName,
|
|
1997
1997
|
"{{projectType}}": projectType,
|
|
@@ -2177,6 +2177,20 @@ function getSearchBaseDirs(cwd) {
|
|
|
2177
2177
|
}
|
|
2178
2178
|
return ancestors.slice(0, boundaryIndex + 1);
|
|
2179
2179
|
}
|
|
2180
|
+
var FEATURE_FOLDER_PATTERN = /^F\d{3,}-/i;
|
|
2181
|
+
function normalizeComponentKeys(value) {
|
|
2182
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
2183
|
+
return Object.keys(value).map((key) => key.trim().toLowerCase()).filter(Boolean);
|
|
2184
|
+
}
|
|
2185
|
+
async function inferComponentsFromFeaturesDir(docsDir) {
|
|
2186
|
+
const featuresPath = path18.join(docsDir, "features");
|
|
2187
|
+
if (!await fs15.pathExists(featuresPath)) return [];
|
|
2188
|
+
const entries = await fs15.readdir(featuresPath, { withFileTypes: true });
|
|
2189
|
+
const inferred = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name.trim().toLowerCase()).filter(
|
|
2190
|
+
(name) => !!name && name !== "feature-base" && !FEATURE_FOLDER_PATTERN.test(name)
|
|
2191
|
+
);
|
|
2192
|
+
return [...new Set(inferred)];
|
|
2193
|
+
}
|
|
2180
2194
|
async function getConfig(cwd) {
|
|
2181
2195
|
const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
|
|
2182
2196
|
const baseDirs = [
|
|
@@ -2199,9 +2213,13 @@ async function getConfig(cwd) {
|
|
|
2199
2213
|
try {
|
|
2200
2214
|
const configFile = await fs15.readJson(configPath);
|
|
2201
2215
|
const projectType = normalizeProjectType(configFile.projectType);
|
|
2216
|
+
const inferredComponents = [
|
|
2217
|
+
...normalizeComponentKeys(configFile.projectRoot),
|
|
2218
|
+
...await inferComponentsFromFeaturesDir(resolvedDocsDir)
|
|
2219
|
+
];
|
|
2202
2220
|
const components = resolveProjectComponents(
|
|
2203
2221
|
projectType,
|
|
2204
|
-
configFile.components
|
|
2222
|
+
Array.isArray(configFile.components) && configFile.components.length > 0 ? configFile.components : inferredComponents
|
|
2205
2223
|
);
|
|
2206
2224
|
return {
|
|
2207
2225
|
docsDir: resolvedDocsDir,
|
|
@@ -2223,10 +2241,9 @@ async function getConfig(cwd) {
|
|
|
2223
2241
|
const agentsPath = path18.join(resolvedDocsDir, "agents");
|
|
2224
2242
|
const featuresPath = path18.join(resolvedDocsDir, "features");
|
|
2225
2243
|
if (await fs15.pathExists(agentsPath) && await fs15.pathExists(featuresPath)) {
|
|
2226
|
-
const
|
|
2227
|
-
const
|
|
2228
|
-
const
|
|
2229
|
-
const components = projectType === "multi" ? resolveProjectComponents("multi", ["fe", "be"]) : void 0;
|
|
2244
|
+
const inferredComponents = await inferComponentsFromFeaturesDir(resolvedDocsDir);
|
|
2245
|
+
const projectType = inferredComponents.length > 0 ? "multi" : "single";
|
|
2246
|
+
const components = projectType === "multi" ? resolveProjectComponents("multi", inferredComponents) : void 0;
|
|
2230
2247
|
const langProbeCandidates = [
|
|
2231
2248
|
path18.join(agentsPath, "custom.md"),
|
|
2232
2249
|
path18.join(agentsPath, "constitution.md"),
|
|
@@ -2388,29 +2405,32 @@ async function runFeature(name, options) {
|
|
|
2388
2405
|
);
|
|
2389
2406
|
}
|
|
2390
2407
|
if (projectType === "multi" && !component) {
|
|
2391
|
-
if (
|
|
2408
|
+
if (configuredComponents.length === 1) {
|
|
2409
|
+
component = configuredComponents[0];
|
|
2410
|
+
} else if (options.nonInteractive) {
|
|
2392
2411
|
throw createCliError(
|
|
2393
2412
|
"PROMPT_BLOCKED",
|
|
2394
2413
|
"`--component` is required in multi mode when using `--non-interactive`."
|
|
2395
2414
|
);
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2415
|
+
} else {
|
|
2416
|
+
const response = await prompts(
|
|
2417
|
+
{
|
|
2418
|
+
type: "select",
|
|
2419
|
+
name: "component",
|
|
2420
|
+
message: tr(lang, "cli", "feature.selectRepo"),
|
|
2421
|
+
choices: configuredComponents.map((value) => ({
|
|
2422
|
+
title: value.toUpperCase(),
|
|
2423
|
+
value
|
|
2424
|
+
}))
|
|
2425
|
+
},
|
|
2426
|
+
{
|
|
2427
|
+
onCancel: () => {
|
|
2428
|
+
throw new Error("canceled");
|
|
2429
|
+
}
|
|
2410
2430
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2431
|
+
);
|
|
2432
|
+
component = response.component;
|
|
2433
|
+
}
|
|
2414
2434
|
}
|
|
2415
2435
|
if (projectType === "multi") {
|
|
2416
2436
|
assertAllowedComponent(component, configuredComponents);
|
|
@@ -2464,6 +2484,8 @@ async function runFeature(name, options) {
|
|
|
2464
2484
|
"{\uBC88\uD638}": idNumber,
|
|
2465
2485
|
"{\uACB0\uC815 \uC81C\uBAA9}": `${name} \uACB0\uC815`,
|
|
2466
2486
|
"{YYYY-MM-DD}": getLocalDateString(),
|
|
2487
|
+
"{component}": component || "",
|
|
2488
|
+
"{{projectName}}-{component}": repoName,
|
|
2467
2489
|
"{be|fe}": component || "",
|
|
2468
2490
|
"{\uC774\uC288\uBC88\uD638}": "",
|
|
2469
2491
|
"{{description}}": options.desc || "",
|
|
@@ -4477,7 +4499,7 @@ async function runUpdate(options) {
|
|
|
4477
4499
|
const targetAgentsBase = path18.join(docsDir, "agents");
|
|
4478
4500
|
const commonAgents = agentsMode === "skills" ? path18.join(commonAgentsBase, "skills") : commonAgentsBase;
|
|
4479
4501
|
const targetAgents = agentsMode === "skills" ? path18.join(targetAgentsBase, "skills") : targetAgentsBase;
|
|
4480
|
-
const featurePath = projectType === "multi" ?
|
|
4502
|
+
const featurePath = projectType === "multi" ? "docs/features/{component}" : "docs/features";
|
|
4481
4503
|
const projectName = config.projectName ?? "{{projectName}}";
|
|
4482
4504
|
const commonReplacements = {
|
|
4483
4505
|
"{{projectName}}": projectName,
|
|
@@ -4821,31 +4843,34 @@ async function runConfig(options) {
|
|
|
4821
4843
|
const components = resolveProjectComponents(projectType, configFile.components);
|
|
4822
4844
|
let targetComponent = targetFromOptions;
|
|
4823
4845
|
if (!targetComponent) {
|
|
4824
|
-
if (
|
|
4846
|
+
if (components.length === 1) {
|
|
4847
|
+
targetComponent = components[0];
|
|
4848
|
+
} else if (options.nonInteractive) {
|
|
4825
4849
|
throw createCliError(
|
|
4826
4850
|
"PROMPT_BLOCKED",
|
|
4827
4851
|
"`--component` is required for multi projectRoot update when using `--non-interactive`."
|
|
4828
4852
|
);
|
|
4829
|
-
}
|
|
4830
|
-
|
|
4831
|
-
|
|
4853
|
+
} else {
|
|
4854
|
+
const response = await prompts(
|
|
4855
|
+
[
|
|
4856
|
+
{
|
|
4857
|
+
type: "select",
|
|
4858
|
+
name: "component",
|
|
4859
|
+
message: tr(config.lang, "cli", "config.selectRepoToUpdate"),
|
|
4860
|
+
choices: components.map((value) => ({
|
|
4861
|
+
title: value.toUpperCase(),
|
|
4862
|
+
value
|
|
4863
|
+
}))
|
|
4864
|
+
}
|
|
4865
|
+
],
|
|
4832
4866
|
{
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
choices: components.map((value) => ({
|
|
4837
|
-
title: value.toUpperCase(),
|
|
4838
|
-
value
|
|
4839
|
-
}))
|
|
4840
|
-
}
|
|
4841
|
-
],
|
|
4842
|
-
{
|
|
4843
|
-
onCancel: () => {
|
|
4844
|
-
throw new Error("canceled");
|
|
4867
|
+
onCancel: () => {
|
|
4868
|
+
throw new Error("canceled");
|
|
4869
|
+
}
|
|
4845
4870
|
}
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4871
|
+
);
|
|
4872
|
+
targetComponent = response.component;
|
|
4873
|
+
}
|
|
4849
4874
|
}
|
|
4850
4875
|
if (!targetComponent) {
|
|
4851
4876
|
throw createCliError(
|
|
@@ -5274,7 +5299,7 @@ async function getBuiltinDoc(docId, projectType, lang) {
|
|
|
5274
5299
|
}
|
|
5275
5300
|
|
|
5276
5301
|
// src/commands/context.ts
|
|
5277
|
-
var
|
|
5302
|
+
var LEGACY_APPROVAL_TICKET_FILENAME = ".lee-spec-kit.approval-tickets.json";
|
|
5278
5303
|
var APPROVAL_TICKET_TTL_MS = 5 * 60 * 1e3;
|
|
5279
5304
|
async function resolveContextState(config, featureName, options) {
|
|
5280
5305
|
if (!config) {
|
|
@@ -5300,8 +5325,11 @@ function parseApprovalLabel(input, validLabels) {
|
|
|
5300
5325
|
}
|
|
5301
5326
|
return null;
|
|
5302
5327
|
}
|
|
5303
|
-
function
|
|
5304
|
-
return
|
|
5328
|
+
function getApprovalTicketPaths(config) {
|
|
5329
|
+
return {
|
|
5330
|
+
runtimePath: getApprovalTicketStorePath(config.docsDir),
|
|
5331
|
+
legacyPath: path18.join(config.docsDir, LEGACY_APPROVAL_TICKET_FILENAME)
|
|
5332
|
+
};
|
|
5305
5333
|
}
|
|
5306
5334
|
function getApprovalSessionId() {
|
|
5307
5335
|
const explicit = (process.env.LEE_SPEC_KIT_SESSION_ID || "").trim();
|
|
@@ -5327,6 +5355,41 @@ async function loadApprovalTicketStore(storePath) {
|
|
|
5327
5355
|
return { tickets: [] };
|
|
5328
5356
|
}
|
|
5329
5357
|
}
|
|
5358
|
+
async function saveApprovalTicketStore(storePath, payload) {
|
|
5359
|
+
await fs15.ensureDir(path18.dirname(storePath));
|
|
5360
|
+
await fs15.writeJson(storePath, payload, { spaces: 2 });
|
|
5361
|
+
}
|
|
5362
|
+
async function resolveApprovalTicketStoreAndPath(config, nowMs) {
|
|
5363
|
+
const { runtimePath, legacyPath } = getApprovalTicketPaths(config);
|
|
5364
|
+
if (await fs15.pathExists(runtimePath)) {
|
|
5365
|
+
return {
|
|
5366
|
+
storePath: runtimePath,
|
|
5367
|
+
store: await loadApprovalTicketStore(runtimePath)
|
|
5368
|
+
};
|
|
5369
|
+
}
|
|
5370
|
+
if (!await fs15.pathExists(legacyPath)) {
|
|
5371
|
+
return {
|
|
5372
|
+
storePath: runtimePath,
|
|
5373
|
+
store: { tickets: [] }
|
|
5374
|
+
};
|
|
5375
|
+
}
|
|
5376
|
+
const legacyStore = await loadApprovalTicketStore(legacyPath);
|
|
5377
|
+
const migrated = pruneApprovalTickets(legacyStore.tickets, nowMs);
|
|
5378
|
+
await saveApprovalTicketStore(
|
|
5379
|
+
runtimePath,
|
|
5380
|
+
{
|
|
5381
|
+
tickets: migrated,
|
|
5382
|
+
updatedAt: new Date(nowMs).toISOString(),
|
|
5383
|
+
migratedFrom: legacyPath
|
|
5384
|
+
}
|
|
5385
|
+
);
|
|
5386
|
+
await fs15.remove(legacyPath).catch(() => {
|
|
5387
|
+
});
|
|
5388
|
+
return {
|
|
5389
|
+
storePath: runtimePath,
|
|
5390
|
+
store: { tickets: migrated }
|
|
5391
|
+
};
|
|
5392
|
+
}
|
|
5330
5393
|
function pruneApprovalTickets(tickets, nowMs) {
|
|
5331
5394
|
return tickets.filter((ticket) => {
|
|
5332
5395
|
if (ticket.usedAt) return false;
|
|
@@ -5348,21 +5411,22 @@ async function issueApprovalTicket(config, payload) {
|
|
|
5348
5411
|
createdAt: new Date(nowMs).toISOString(),
|
|
5349
5412
|
expiresAt: new Date(nowMs + APPROVAL_TICKET_TTL_MS).toISOString()
|
|
5350
5413
|
};
|
|
5351
|
-
const storePath = getApprovalTicketPath(config);
|
|
5352
5414
|
const lockPath = getDocsLockPath(config.docsDir);
|
|
5353
5415
|
return withFileLock(
|
|
5354
5416
|
lockPath,
|
|
5355
5417
|
async () => {
|
|
5356
|
-
const store = await
|
|
5418
|
+
const { storePath, store } = await resolveApprovalTicketStoreAndPath(
|
|
5419
|
+
config,
|
|
5420
|
+
nowMs
|
|
5421
|
+
);
|
|
5357
5422
|
const nextTickets = pruneApprovalTickets(store.tickets, nowMs);
|
|
5358
5423
|
nextTickets.push(record);
|
|
5359
|
-
await
|
|
5424
|
+
await saveApprovalTicketStore(
|
|
5360
5425
|
storePath,
|
|
5361
5426
|
{
|
|
5362
5427
|
tickets: nextTickets,
|
|
5363
5428
|
updatedAt: new Date(nowMs).toISOString()
|
|
5364
|
-
}
|
|
5365
|
-
{ spaces: 2 }
|
|
5429
|
+
}
|
|
5366
5430
|
);
|
|
5367
5431
|
return record;
|
|
5368
5432
|
},
|
|
@@ -5377,21 +5441,22 @@ async function consumeApprovalTicket(config, token, expected) {
|
|
|
5377
5441
|
"Execution requires an approval ticket. Run `context --approve <reply> --json` first and pass `--ticket <token>`."
|
|
5378
5442
|
);
|
|
5379
5443
|
}
|
|
5380
|
-
const storePath = getApprovalTicketPath(config);
|
|
5381
5444
|
const lockPath = getDocsLockPath(config.docsDir);
|
|
5382
5445
|
const sessionId = getApprovalSessionId();
|
|
5383
5446
|
const nowMs = Date.now();
|
|
5384
5447
|
return withFileLock(
|
|
5385
5448
|
lockPath,
|
|
5386
5449
|
async () => {
|
|
5387
|
-
const store = await
|
|
5450
|
+
const { storePath, store } = await resolveApprovalTicketStoreAndPath(
|
|
5451
|
+
config,
|
|
5452
|
+
nowMs
|
|
5453
|
+
);
|
|
5388
5454
|
const cleaned = pruneApprovalTickets(store.tickets, nowMs);
|
|
5389
5455
|
const index = cleaned.findIndex((entry) => entry.token === normalizedToken);
|
|
5390
5456
|
if (index < 0) {
|
|
5391
|
-
await
|
|
5457
|
+
await saveApprovalTicketStore(
|
|
5392
5458
|
storePath,
|
|
5393
|
-
{ tickets: cleaned, updatedAt: new Date(nowMs).toISOString() }
|
|
5394
|
-
{ spaces: 2 }
|
|
5459
|
+
{ tickets: cleaned, updatedAt: new Date(nowMs).toISOString() }
|
|
5395
5460
|
);
|
|
5396
5461
|
throw createCliError(
|
|
5397
5462
|
"INVALID_APPROVAL",
|
|
@@ -5402,10 +5467,9 @@ async function consumeApprovalTicket(config, token, expected) {
|
|
|
5402
5467
|
const expiresAtMs = Date.parse(record.expiresAt || "");
|
|
5403
5468
|
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) {
|
|
5404
5469
|
cleaned.splice(index, 1);
|
|
5405
|
-
await
|
|
5470
|
+
await saveApprovalTicketStore(
|
|
5406
5471
|
storePath,
|
|
5407
|
-
{ tickets: cleaned, updatedAt: new Date(nowMs).toISOString() }
|
|
5408
|
-
{ spaces: 2 }
|
|
5472
|
+
{ tickets: cleaned, updatedAt: new Date(nowMs).toISOString() }
|
|
5409
5473
|
);
|
|
5410
5474
|
throw createCliError(
|
|
5411
5475
|
"CONTEXT_STALE",
|
|
@@ -5443,13 +5507,12 @@ async function consumeApprovalTicket(config, token, expected) {
|
|
|
5443
5507
|
);
|
|
5444
5508
|
}
|
|
5445
5509
|
cleaned.splice(index, 1);
|
|
5446
|
-
await
|
|
5510
|
+
await saveApprovalTicketStore(
|
|
5447
5511
|
storePath,
|
|
5448
5512
|
{
|
|
5449
5513
|
tickets: cleaned,
|
|
5450
5514
|
updatedAt: new Date(nowMs).toISOString()
|
|
5451
|
-
}
|
|
5452
|
-
{ spaces: 2 }
|
|
5515
|
+
}
|
|
5453
5516
|
);
|
|
5454
5517
|
return record;
|
|
5455
5518
|
},
|
|
@@ -6278,6 +6341,7 @@ function detectPlaceholders(content) {
|
|
|
6278
6341
|
{ key: "{feature-name}", re: /\{feature-name\}/g },
|
|
6279
6342
|
{ key: "{number}", re: /\{number\}/g },
|
|
6280
6343
|
{ key: "{issue-number}", re: /\{issue-number\}/g },
|
|
6344
|
+
{ key: "{component}", re: /\{component\}/g },
|
|
6281
6345
|
{ key: "{be|fe}", re: /\{be\|fe\}/g },
|
|
6282
6346
|
{ key: "{Story Title}", re: /\{Story Title\}/g },
|
|
6283
6347
|
{ key: "{user type}", re: /\{user type\}/g },
|
|
@@ -6358,8 +6422,12 @@ ${line}
|
|
|
6358
6422
|
}
|
|
6359
6423
|
function applyPlaceholderFixes(content, context, lang) {
|
|
6360
6424
|
const date = getLocalDateString();
|
|
6425
|
+
const projectName = context.projectName || "project";
|
|
6426
|
+
const repoName = context.repoType === "single" ? projectName : `${projectName}-${context.repoType}`;
|
|
6361
6427
|
const replacements = {
|
|
6362
|
-
"{{projectName}}":
|
|
6428
|
+
"{{projectName}}": projectName,
|
|
6429
|
+
"{{projectName}}-{component}": repoName,
|
|
6430
|
+
"{{projectName}}-{be|fe}": repoName,
|
|
6363
6431
|
"{{date}}": date,
|
|
6364
6432
|
"{{featurePath}}": context.featurePath,
|
|
6365
6433
|
"{{description}}": `${context.featureName} feature`,
|
|
@@ -6369,6 +6437,7 @@ function applyPlaceholderFixes(content, context, lang) {
|
|
|
6369
6437
|
"{\uBC88\uD638}": context.featureNumber,
|
|
6370
6438
|
"{issue-number}": "-",
|
|
6371
6439
|
"{\uC774\uC288\uBC88\uD638}": "-",
|
|
6440
|
+
"{component}": context.repoType === "single" ? "" : context.repoType,
|
|
6372
6441
|
"{be|fe}": context.repoType === "single" ? "" : context.repoType,
|
|
6373
6442
|
"YYYY-MM-DD": date,
|
|
6374
6443
|
"{Story Title}": `${context.featureName} user flow`,
|
|
@@ -7448,6 +7517,10 @@ function parsePrArtifactMode(raw, kind, lang) {
|
|
|
7448
7517
|
tg(lang, "artifactModeInvalid", { kind, value })
|
|
7449
7518
|
);
|
|
7450
7519
|
}
|
|
7520
|
+
function isMermaidPreferredComponent(component) {
|
|
7521
|
+
const normalized = (component || "").trim().toLowerCase();
|
|
7522
|
+
return ["be", "backend", "api", "server", "core"].includes(normalized);
|
|
7523
|
+
}
|
|
7451
7524
|
function resolvePrArtifactPolicy(config, feature, options) {
|
|
7452
7525
|
const screenshotsMode = parsePrArtifactMode(
|
|
7453
7526
|
options.screenshots,
|
|
@@ -7460,7 +7533,7 @@ function resolvePrArtifactPolicy(config, feature, options) {
|
|
|
7460
7533
|
config.lang
|
|
7461
7534
|
);
|
|
7462
7535
|
const includeScreenshots = screenshotsMode === "on" ? true : screenshotsMode === "off" ? false : config.pr?.screenshots?.upload ?? false;
|
|
7463
|
-
const includeMermaid = mermaidMode === "on" ? true : mermaidMode === "off" ? false : feature.type
|
|
7536
|
+
const includeMermaid = mermaidMode === "on" ? true : mermaidMode === "off" ? false : isMermaidPreferredComponent(feature.type);
|
|
7464
7537
|
return {
|
|
7465
7538
|
includeScreenshots,
|
|
7466
7539
|
includeMermaid
|
package/package.json
CHANGED
|
@@ -58,8 +58,8 @@ In GitHub Issues, use different link formats **based on file location**:
|
|
|
58
58
|
> Format: `- **{Label}**: \`{path}\``
|
|
59
59
|
|
|
60
60
|
```markdown
|
|
61
|
-
- **Spec**: `docs/features/{
|
|
62
|
-
- **Tasks**: `docs/features/{
|
|
61
|
+
- **Spec**: `docs/features/{component}/F001-feature-name/spec.md`
|
|
62
|
+
- **Tasks**: `docs/features/{component}/F001-feature-name/tasks.md`
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
> ⚠️ Local documents are not clickable on GitHub, so use **bold label + code block path** format instead of markdown links.
|
|
@@ -85,7 +85,7 @@ In GitHub Issues, use different link formats **based on file location**:
|
|
|
85
85
|
|
|
86
86
|
## Related Documents
|
|
87
87
|
|
|
88
|
-
- **Spec**: `docs/features/{
|
|
88
|
+
- **Spec**: `docs/features/{component}/F{number}-{feature-name}/spec.md`
|
|
89
89
|
|
|
90
90
|
## Labels
|
|
91
91
|
|
|
@@ -27,7 +27,7 @@ features/
|
|
|
27
27
|
npx lee-spec-kit feature user-auth
|
|
28
28
|
|
|
29
29
|
# Multi project
|
|
30
|
-
npx lee-spec-kit feature --component
|
|
30
|
+
npx lee-spec-kit feature --component app user-profile
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
> 💡 CLI copies templates from `feature-base/` and auto-assigns IDs.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
- **Feature ID**: F{number}
|
|
10
10
|
- **Feature Name**: {feature-name}
|
|
11
|
-
- **Target Repo**: {{projectName}}-{
|
|
11
|
+
- **Target Repo**: {{projectName}}-{component}
|
|
12
12
|
- **Issue Number**: #{issue-number}
|
|
13
13
|
- **Created**: {YYYY-MM-DD}
|
|
14
14
|
- **Status**: Review | Approved
|
|
@@ -14,7 +14,7 @@ Core rule: once an idea becomes a Feature, the SSOT moves to `docs/features/`.
|
|
|
14
14
|
- Put at least these at the top:
|
|
15
15
|
- Goal / context
|
|
16
16
|
- Rough scope (what’s in/out)
|
|
17
|
-
- Target
|
|
17
|
+
- Target component (optional): `api` / `app` / `worker` / `all`
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -42,8 +42,8 @@ GitHub Issue에서 링크는 **파일 위치에 따라** 다르게 작성:
|
|
|
42
42
|
> 형식: `- **{레이블}**: \`{경로}\``
|
|
43
43
|
|
|
44
44
|
```markdown
|
|
45
|
-
- **Spec**: `docs/features/{
|
|
46
|
-
- **Tasks**: `docs/features/{
|
|
45
|
+
- **Spec**: `docs/features/{component}/F001-feature-name/spec.md`
|
|
46
|
+
- **Tasks**: `docs/features/{component}/F001-feature-name/tasks.md`
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
> ⚠️ 로컬 문서는 GitHub에서 클릭되지 않으므로, 마크다운 링크 대신 **볼드 레이블 + 코드블록 경로** 형식을 사용합니다.
|
|
@@ -69,7 +69,7 @@ GitHub Issue에서 링크는 **파일 위치에 따라** 다르게 작성:
|
|
|
69
69
|
|
|
70
70
|
## 관련 문서
|
|
71
71
|
|
|
72
|
-
- **Spec**: `docs/features/{
|
|
72
|
+
- **Spec**: `docs/features/{component}/F{번호}-{기능명}/spec.md`
|
|
73
73
|
|
|
74
74
|
## 라벨
|
|
75
75
|
|