codex-rule-maker 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,437 @@
1
+ """Framework profile definitions used by the document renderer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ class UnknownProfileError(ValueError):
9
+ """Raised when a stack profile is not supported."""
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class FrameworkProfile:
14
+ """Rules and notes for a supported framework or stack profile."""
15
+
16
+ key: str
17
+ display_name: str
18
+ philosophy_ko: str
19
+ philosophy_en: str
20
+ architecture_ko: tuple[str, ...]
21
+ architecture_en: tuple[str, ...]
22
+ framework_rules_ko: tuple[str, ...]
23
+ framework_rules_en: tuple[str, ...]
24
+ api_rules_ko: tuple[str, ...]
25
+ api_rules_en: tuple[str, ...]
26
+ test_rules_ko: tuple[str, ...]
27
+ test_rules_en: tuple[str, ...]
28
+ directories: tuple[str, ...]
29
+
30
+ def philosophy(self, language: str) -> str:
31
+ return self.philosophy_en if language == "en" else self.philosophy_ko
32
+
33
+ def architecture(self, language: str) -> tuple[str, ...]:
34
+ return self.architecture_en if language == "en" else self.architecture_ko
35
+
36
+ def framework_rules(self, language: str) -> tuple[str, ...]:
37
+ return self.framework_rules_en if language == "en" else self.framework_rules_ko
38
+
39
+ def api_rules(self, language: str) -> tuple[str, ...]:
40
+ return self.api_rules_en if language == "en" else self.api_rules_ko
41
+
42
+ def test_rules(self, language: str) -> tuple[str, ...]:
43
+ return self.test_rules_en if language == "en" else self.test_rules_ko
44
+
45
+
46
+ FASTAPI_PROFILE = FrameworkProfile(
47
+ key="fastapi",
48
+ display_name="FastAPI",
49
+ philosophy_ko="FastAPI의 가벼운 실행 모델은 유지하되, 코드는 Controller, Service, Repository, Schema, Entity 계층으로 분리한다.",
50
+ philosophy_en="Keep FastAPI's lightweight runtime model, but separate code into Controller, Service, Repository, Schema, and Entity layers.",
51
+ architecture_ko=(
52
+ "HTTP 엔드포인트는 controller/router 계층에 두고 비즈니스 판단은 service 계층으로 위임한다.",
53
+ "service 계층은 유스케이스 단위 메서드를 제공하고 repository 또는 외부 연동 client를 조합한다.",
54
+ "repository 계층은 DB 접근만 담당하며 HTTP 요청/응답 객체를 알지 못해야 한다.",
55
+ "Pydantic schema는 요청/응답 계약이고 ORM entity는 저장 모델이다. 두 모델을 혼용하지 않는다.",
56
+ "권장 흐름: Controller -> Service -> Repository -> DB.",
57
+ ),
58
+ architecture_en=(
59
+ "Place HTTP endpoints in the controller/router layer and delegate business decisions to services.",
60
+ "Services expose use-case methods and coordinate repositories or external clients.",
61
+ "Repositories only handle database access and must not know HTTP request/response objects.",
62
+ "Pydantic schemas are request/response contracts and ORM entities are persistence models. Do not mix them.",
63
+ "Recommended flow: Controller -> Service -> Repository -> DB.",
64
+ ),
65
+ framework_rules_ko=(
66
+ "APIRouter는 라우팅과 의존성 연결만 담당한다.",
67
+ "Service가 FastAPI Response, Depends, Request에 직접 의존하지 않게 한다.",
68
+ "Repository는 SQLAlchemy, SQLModel 등 실제 저장소 API를 감싸는 경계로 둔다.",
69
+ "예외는 도메인/서비스 예외로 먼저 표현하고 controller에서 HTTP 상태로 변환한다.",
70
+ "배경 작업, 외부 호출, 긴 작업은 service 하위의 명시적 adapter/client로 분리한다.",
71
+ ),
72
+ framework_rules_en=(
73
+ "APIRouter should only handle routing and dependency wiring.",
74
+ "Services should not directly depend on FastAPI Response, Depends, or Request.",
75
+ "Repositories wrap storage APIs such as SQLAlchemy or SQLModel.",
76
+ "Represent errors as domain/service exceptions first, then map them to HTTP statuses in controllers.",
77
+ "Move background jobs, external calls, and long-running work into explicit adapters/clients below services.",
78
+ ),
79
+ api_rules_ko=(
80
+ "응답 모델은 response_model 또는 명시적 schema로 고정한다.",
81
+ "입력 검증은 schema에서 시작하고 유스케이스 검증은 service에서 수행한다.",
82
+ "엔드포인트 경로는 리소스 명사 중심으로 작성한다.",
83
+ ),
84
+ api_rules_en=(
85
+ "Pin response contracts with response_model or explicit schemas.",
86
+ "Start input validation in schemas and perform use-case validation in services.",
87
+ "Use resource nouns in endpoint paths.",
88
+ ),
89
+ test_rules_ko=(
90
+ "Service 테스트는 repository/client를 대체 객체로 격리한다.",
91
+ "API 테스트는 TestClient 또는 httpx 기반으로 요청/응답 계약을 검증한다.",
92
+ "Repository 테스트는 DB 스키마와 쿼리 동작을 별도로 검증한다.",
93
+ ),
94
+ test_rules_en=(
95
+ "Isolate service tests with repository/client doubles.",
96
+ "Use TestClient or httpx-based API tests to verify request/response contracts.",
97
+ "Verify repository tests against database schema and query behavior separately.",
98
+ ),
99
+ directories=("app/controllers", "app/services", "app/repositories", "app/schemas", "app/entities", "app/core"),
100
+ )
101
+
102
+ SPRINGBOOT_PROFILE = FrameworkProfile(
103
+ key="springboot",
104
+ display_name="Spring Boot",
105
+ philosophy_ko="Spring Boot 프로젝트는 Controller, Service, Repository, Entity, DTO 계층과 명시적 의존성 주입을 기준으로 유지한다.",
106
+ philosophy_en="Spring Boot projects should keep explicit Controller, Service, Repository, Entity, and DTO layers with dependency injection.",
107
+ architecture_ko=(
108
+ "Controller는 HTTP 요청/응답과 인증 컨텍스트 연결만 담당한다.",
109
+ "Service는 트랜잭션 경계와 비즈니스 규칙의 중심이다.",
110
+ "Repository는 JPA 또는 데이터 접근 인터페이스로 제한한다.",
111
+ "Entity는 DB 영속성 모델이고 DTO는 API 계약이다. Entity를 외부 응답으로 직접 노출하지 않는다.",
112
+ "권장 흐름: Controller -> Service -> Repository -> DB.",
113
+ ),
114
+ architecture_en=(
115
+ "Controllers handle HTTP request/response mapping and security context wiring only.",
116
+ "Services own transaction boundaries and business rules.",
117
+ "Repositories are limited to JPA or data access interfaces.",
118
+ "Entities are persistence models and DTOs are API contracts. Never expose entities directly in external responses.",
119
+ "Recommended flow: Controller -> Service -> Repository -> DB.",
120
+ ),
121
+ framework_rules_ko=(
122
+ "생성자 주입을 기본으로 사용한다.",
123
+ "@Transactional은 비즈니스 유스케이스가 있는 service 계층에 둔다.",
124
+ "ControllerAdvice 또는 공통 예외 처리기로 오류 응답을 일관화한다.",
125
+ "Entity 변경은 migration과 DB 문서 변경을 함께 검토한다.",
126
+ ),
127
+ framework_rules_en=(
128
+ "Prefer constructor injection.",
129
+ "Place @Transactional on service-layer use cases.",
130
+ "Use ControllerAdvice or a shared exception handler for consistent error responses.",
131
+ "Review migrations and DB documentation together with entity changes.",
132
+ ),
133
+ api_rules_ko=(
134
+ "Request DTO와 Response DTO를 분리한다.",
135
+ "상태 코드는 Controller에서 명시적으로 결정한다.",
136
+ "Bean Validation을 DTO 입력 계약에 적용한다.",
137
+ ),
138
+ api_rules_en=(
139
+ "Separate request DTOs from response DTOs.",
140
+ "Choose HTTP status codes explicitly in controllers.",
141
+ "Apply Bean Validation to DTO input contracts.",
142
+ ),
143
+ test_rules_ko=(
144
+ "Service는 단위 테스트로 비즈니스 규칙을 검증한다.",
145
+ "Controller는 WebMvcTest 또는 통합 테스트로 API 계약을 검증한다.",
146
+ "Repository는 필요한 경우 DataJpaTest로 쿼리와 매핑을 검증한다.",
147
+ ),
148
+ test_rules_en=(
149
+ "Verify business rules with service unit tests.",
150
+ "Verify API contracts with WebMvcTest or integration tests.",
151
+ "Use DataJpaTest when repository queries and mappings need verification.",
152
+ ),
153
+ directories=("src/main/java/.../controller", "src/main/java/.../service", "src/main/java/.../repository", "src/main/java/.../entity", "src/main/java/.../dto"),
154
+ )
155
+
156
+ REACT_PROFILE = FrameworkProfile(
157
+ key="react",
158
+ display_name="React",
159
+ philosophy_ko="React 코드는 화면, 컴포넌트, 훅, 서비스, 상태 저장소를 분리하고 UI와 데이터 접근을 섞지 않는다.",
160
+ philosophy_en="React code should separate pages, components, hooks, services, and stores without mixing UI and data access.",
161
+ architecture_ko=(
162
+ "page는 라우팅 단위 화면 조립을 담당한다.",
163
+ "component는 재사용 가능한 UI와 표시 책임을 담당한다.",
164
+ "hook은 화면 상태, 비동기 흐름, UI 유스케이스를 캡슐화한다.",
165
+ "service는 API 호출과 외부 클라이언트 경계만 담당한다.",
166
+ "store는 전역 상태를 담당하고 서버 응답 원본을 무분별하게 복제하지 않는다.",
167
+ "권장 흐름: Page -> Hook/Store -> Service -> API.",
168
+ ),
169
+ architecture_en=(
170
+ "Pages compose route-level screens.",
171
+ "Components own reusable UI and presentation.",
172
+ "Hooks encapsulate screen state, async flow, and UI use cases.",
173
+ "Services own API calls and external client boundaries only.",
174
+ "Stores own global state and should not blindly duplicate raw server responses.",
175
+ "Recommended flow: Page -> Hook/Store -> Service -> API.",
176
+ ),
177
+ framework_rules_ko=(
178
+ "컴포넌트 안에서 fetch/axios 호출을 직접 수행하지 않는다.",
179
+ "복잡한 상태 전이는 hook 또는 store action으로 분리한다.",
180
+ "공통 UI 컴포넌트와 기능 전용 컴포넌트를 분리한다.",
181
+ "API 타입과 UI view model의 변환 위치를 명확히 둔다.",
182
+ ),
183
+ framework_rules_en=(
184
+ "Do not call fetch/axios directly inside components.",
185
+ "Move complex state transitions into hooks or store actions.",
186
+ "Separate shared UI components from feature-specific components.",
187
+ "Keep API types and UI view-model mapping in an explicit location.",
188
+ ),
189
+ api_rules_ko=(
190
+ "service 함수는 HTTP 세부사항을 감추고 typed result를 반환한다.",
191
+ "API 오류는 화면에서 직접 문자열 조립하지 말고 공통 오류 모델로 변환한다.",
192
+ "인증 토큰 저장, 갱신, 만료 처리는 한 계층에서 일관되게 관리한다.",
193
+ ),
194
+ api_rules_en=(
195
+ "Service functions hide HTTP details and return typed results.",
196
+ "Convert API errors to a shared error model instead of hand-building strings in screens.",
197
+ "Manage auth token storage, refresh, and expiration consistently in one layer.",
198
+ ),
199
+ test_rules_ko=(
200
+ "컴포넌트 테스트는 사용자 상호작용과 표시 결과를 검증한다.",
201
+ "hook 테스트는 상태 전이와 비동기 흐름을 검증한다.",
202
+ "service 테스트는 API client 경계와 오류 변환을 검증한다.",
203
+ ),
204
+ test_rules_en=(
205
+ "Component tests verify user interactions and rendered results.",
206
+ "Hook tests verify state transitions and async flow.",
207
+ "Service tests verify API client boundaries and error mapping.",
208
+ ),
209
+ directories=("src/pages", "src/components", "src/hooks", "src/services", "src/stores", "src/types"),
210
+ )
211
+
212
+ NEXTJS_PROFILE = FrameworkProfile(
213
+ key="nextjs",
214
+ display_name="Next.js",
215
+ philosophy_ko="Next.js는 App Router를 기본으로 하며 Server Component와 Client Component의 책임을 명확히 구분한다.",
216
+ philosophy_en="Next.js should use the App Router by default and clearly separate Server Component and Client Component responsibilities.",
217
+ architecture_ko=(
218
+ "app 디렉토리는 라우트, 레이아웃, 서버 데이터 로딩의 기준 위치로 사용한다.",
219
+ "Server Component는 데이터 조회와 정적/서버 렌더링 책임을 담당한다.",
220
+ "Client Component는 브라우저 상태, 이벤트, 인터랙션이 필요한 경우에만 사용한다.",
221
+ "route handler는 API boundary이며 복잡한 비즈니스 로직은 service 계층으로 분리한다.",
222
+ "권장 흐름: Route/Page -> Server Action or Service -> Repository/External API.",
223
+ ),
224
+ architecture_en=(
225
+ "Use the app directory as the place for routes, layouts, and server data loading.",
226
+ "Server Components own data reads and static/server rendering.",
227
+ "Client Components are only for browser state, events, and interactions.",
228
+ "Route handlers are API boundaries; move complex business logic into services.",
229
+ "Recommended flow: Route/Page -> Server Action or Service -> Repository/External API.",
230
+ ),
231
+ framework_rules_ko=(
232
+ "'use client'는 필요한 파일에만 선언한다.",
233
+ "서버 전용 비밀값과 브라우저 공개 환경 변수를 구분한다.",
234
+ "데이터 변경은 server action 또는 route handler로 경계를 명확히 한다.",
235
+ "캐시, revalidate, dynamic 설정은 데이터 신선도 요구사항과 함께 문서화한다.",
236
+ ),
237
+ framework_rules_en=(
238
+ "Declare 'use client' only in files that need it.",
239
+ "Separate server-only secrets from browser-exposed environment variables.",
240
+ "Keep data mutations behind server actions or route handlers.",
241
+ "Document cache, revalidate, and dynamic settings with freshness requirements.",
242
+ ),
243
+ api_rules_ko=(
244
+ "route handler 응답은 명시적 JSON 계약을 유지한다.",
245
+ "server action의 입력 검증과 권한 검사를 생략하지 않는다.",
246
+ "프론트엔드 내부 호출과 외부 공개 API를 구분한다.",
247
+ ),
248
+ api_rules_en=(
249
+ "Route handler responses should keep explicit JSON contracts.",
250
+ "Do not skip input validation or authorization checks in server actions.",
251
+ "Separate internal frontend calls from externally exposed APIs.",
252
+ ),
253
+ test_rules_ko=(
254
+ "서버 로직은 service 단위 테스트로 우선 검증한다.",
255
+ "클라이언트 컴포넌트는 사용자 상호작용 중심으로 검증한다.",
256
+ "라우팅과 인증 흐름은 통합 테스트 또는 E2E 테스트로 검증한다.",
257
+ ),
258
+ test_rules_en=(
259
+ "Verify server logic first with service unit tests.",
260
+ "Verify Client Components around user interactions.",
261
+ "Verify routing and auth flow with integration or E2E tests.",
262
+ ),
263
+ directories=("app", "components", "features", "services", "lib", "types"),
264
+ )
265
+
266
+ NODE_EXPRESS_PROFILE = FrameworkProfile(
267
+ key="node-express",
268
+ display_name="Node Express",
269
+ philosophy_ko="Express는 얇은 HTTP 계층으로 유지하고 route, controller, service, repository를 분리한다.",
270
+ philosophy_en="Keep Express as a thin HTTP layer and separate routes, controllers, services, and repositories.",
271
+ architecture_ko=(
272
+ "route는 URL과 middleware 연결만 담당한다.",
273
+ "controller는 요청 파싱, 응답 변환, service 호출만 담당한다.",
274
+ "service는 비즈니스 유스케이스와 트랜잭션 흐름을 담당한다.",
275
+ "repository는 DB 접근을 담당하고 Express 객체를 알지 못해야 한다.",
276
+ "권장 흐름: Route -> Controller -> Service -> Repository -> DB.",
277
+ ),
278
+ architecture_en=(
279
+ "Routes only connect URLs and middleware.",
280
+ "Controllers parse requests, map responses, and call services only.",
281
+ "Services own business use cases and transaction flow.",
282
+ "Repositories own database access and must not know Express objects.",
283
+ "Recommended flow: Route -> Controller -> Service -> Repository -> DB.",
284
+ ),
285
+ framework_rules_ko=(
286
+ "비동기 오류 처리는 공통 error middleware로 모은다.",
287
+ "req/res 객체를 service 또는 repository로 넘기지 않는다.",
288
+ "validation middleware와 service 검증의 책임을 구분한다.",
289
+ "외부 API 호출은 client/adapter 계층으로 분리한다.",
290
+ ),
291
+ framework_rules_en=(
292
+ "Centralize async error handling in shared error middleware.",
293
+ "Do not pass req/res objects into services or repositories.",
294
+ "Separate validation middleware responsibilities from service validation.",
295
+ "Move external API calls into client/adapter layers.",
296
+ ),
297
+ api_rules_ko=(
298
+ "JSON 응답 형식과 오류 응답 형식을 공통으로 유지한다.",
299
+ "라우트 경로는 리소스 명사 중심으로 작성한다.",
300
+ "입력 검증 실패, 인증 실패, 권한 실패를 구분한다.",
301
+ ),
302
+ api_rules_en=(
303
+ "Keep JSON response and error response formats consistent.",
304
+ "Use resource nouns for route paths.",
305
+ "Distinguish validation, authentication, and authorization failures.",
306
+ ),
307
+ test_rules_ko=(
308
+ "Service 테스트는 DB와 HTTP를 격리한다.",
309
+ "Controller/route 테스트는 supertest 등으로 HTTP 계약을 검증한다.",
310
+ "Repository 테스트는 실제 쿼리와 transaction 동작을 검증한다.",
311
+ ),
312
+ test_rules_en=(
313
+ "Service tests isolate database and HTTP dependencies.",
314
+ "Controller/route tests verify HTTP contracts with tools such as supertest.",
315
+ "Repository tests verify real query and transaction behavior.",
316
+ ),
317
+ directories=("src/routes", "src/controllers", "src/services", "src/repositories", "src/middlewares", "src/types"),
318
+ )
319
+
320
+ FULLSTACK_FASTAPI_REACT_PROFILE = FrameworkProfile(
321
+ key="fullstack-fastapi-react",
322
+ display_name="Fullstack FastAPI + React",
323
+ philosophy_ko="FastAPI 백엔드와 React 프론트엔드는 API 계약을 중심으로 분리하고, 각 영역의 계층 규칙을 동시에 지킨다.",
324
+ philosophy_en="Separate the FastAPI backend and React frontend around the API contract while keeping both layer rules.",
325
+ architecture_ko=(
326
+ "backend는 Controller -> Service -> Repository -> DB 흐름을 따른다.",
327
+ "frontend는 Page -> Hook/Store -> Service -> API 흐름을 따른다.",
328
+ "API_SPEC.md는 백엔드 response schema와 프론트엔드 service 타입의 공통 계약이다.",
329
+ "백엔드 Entity를 프론트엔드 타입으로 직접 취급하지 않는다.",
330
+ "인증, 오류, pagination, enum 값은 양쪽 구현과 문서를 함께 갱신한다.",
331
+ ),
332
+ architecture_en=(
333
+ "Backend follows Controller -> Service -> Repository -> DB.",
334
+ "Frontend follows Page -> Hook/Store -> Service -> API.",
335
+ "API_SPEC.md is the shared contract between backend response schemas and frontend service types.",
336
+ "Do not treat backend entities as frontend types directly.",
337
+ "Update auth, errors, pagination, and enum values in both implementations and docs.",
338
+ ),
339
+ framework_rules_ko=(
340
+ "backend와 frontend 디렉토리 경계를 분명히 유지한다.",
341
+ "API 변경은 backend schema, frontend service/type, REF_DOCS/API_SPEC.md를 함께 수정한다.",
342
+ "React 컴포넌트는 백엔드 엔드포인트를 직접 호출하지 않고 service 계층을 통한다.",
343
+ "FastAPI service는 프론트엔드 화면 구조를 알지 못해야 한다.",
344
+ "CORS, 인증 토큰, 오류 응답 형식은 공통 정책으로 문서화한다.",
345
+ ),
346
+ framework_rules_en=(
347
+ "Keep backend and frontend directory boundaries clear.",
348
+ "API changes must update backend schemas, frontend services/types, and REF_DOCS/API_SPEC.md together.",
349
+ "React components call backend endpoints through the service layer, not directly.",
350
+ "FastAPI services must not know frontend screen structure.",
351
+ "Document CORS, auth tokens, and error response format as shared policies.",
352
+ ),
353
+ api_rules_ko=(
354
+ "OpenAPI 또는 명시적 API_SPEC.md를 기준 계약으로 유지한다.",
355
+ "Request/Response 필드명은 백엔드와 프론트엔드에서 동일하게 관리한다.",
356
+ "인증이 필요한 API는 권한 요구사항과 실패 응답을 반드시 문서화한다.",
357
+ ),
358
+ api_rules_en=(
359
+ "Keep OpenAPI or explicit API_SPEC.md as the source contract.",
360
+ "Maintain identical request/response field names across backend and frontend.",
361
+ "Document authorization requirements and failure responses for protected APIs.",
362
+ ),
363
+ test_rules_ko=(
364
+ "백엔드 API 테스트와 프론트엔드 service 테스트가 같은 계약을 검증하게 한다.",
365
+ "주요 사용자 흐름은 E2E 또는 통합 테스트 대상으로 둔다.",
366
+ "API mock은 실제 API_SPEC.md와 불일치하지 않게 관리한다.",
367
+ ),
368
+ test_rules_en=(
369
+ "Backend API tests and frontend service tests should verify the same contract.",
370
+ "Cover major user flows with E2E or integration tests.",
371
+ "Keep API mocks aligned with API_SPEC.md.",
372
+ ),
373
+ directories=("backend/app/controllers", "backend/app/services", "backend/app/repositories", "frontend/src/pages", "frontend/src/components", "frontend/src/services", "frontend/src/stores"),
374
+ )
375
+
376
+ PROFILES: dict[str, FrameworkProfile] = {
377
+ profile.key: profile
378
+ for profile in (
379
+ FASTAPI_PROFILE,
380
+ SPRINGBOOT_PROFILE,
381
+ REACT_PROFILE,
382
+ NEXTJS_PROFILE,
383
+ NODE_EXPRESS_PROFILE,
384
+ FULLSTACK_FASTAPI_REACT_PROFILE,
385
+ )
386
+ }
387
+
388
+ PROFILE_ALIASES: dict[str, str] = {
389
+ "fast-api": "fastapi",
390
+ "spring": "springboot",
391
+ "spring-boot": "springboot",
392
+ "next": "nextjs",
393
+ "next.js": "nextjs",
394
+ "node": "node-express",
395
+ "express": "node-express",
396
+ "nodeexpress": "node-express",
397
+ "fullstack": "fullstack-fastapi-react",
398
+ "fastapi-react": "fullstack-fastapi-react",
399
+ }
400
+
401
+
402
+ def supported_profile_names() -> tuple[str, ...]:
403
+ """Return supported canonical profile names."""
404
+
405
+ return tuple(PROFILES)
406
+
407
+
408
+ def canonical_profile_name(name: str) -> str:
409
+ """Normalize a user-provided stack name to a supported profile key."""
410
+
411
+ normalized = name.strip().lower()
412
+ return PROFILE_ALIASES.get(normalized, normalized)
413
+
414
+
415
+ def resolve_profiles(stack: tuple[str, ...]) -> tuple[FrameworkProfile, ...]:
416
+ """Resolve stack names to framework profiles, preserving user order."""
417
+
418
+ profiles: list[FrameworkProfile] = []
419
+ unknown: list[str] = []
420
+ seen: set[str] = set()
421
+
422
+ for raw_name in stack:
423
+ key = canonical_profile_name(raw_name)
424
+ profile = PROFILES.get(key)
425
+ if profile is None:
426
+ unknown.append(raw_name)
427
+ continue
428
+ if key not in seen:
429
+ profiles.append(profile)
430
+ seen.add(key)
431
+
432
+ if unknown:
433
+ supported = ", ".join(supported_profile_names())
434
+ invalid = ", ".join(unknown)
435
+ raise UnknownProfileError(f"unsupported stack profile(s): {invalid}. Supported: {supported}")
436
+
437
+ return tuple(profiles)
@@ -0,0 +1,230 @@
1
+ """Interactive prompt flow for codex-init."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Callable, Optional, TextIO
9
+
10
+ from codex_builder.constants import (
11
+ CODEX_DIR_NAME,
12
+ DEFAULT_DOCS_LEVEL,
13
+ DEFAULT_LANGUAGE,
14
+ DEFAULT_STACK,
15
+ )
16
+ from codex_builder.models import ConfigError, ProjectConfig, parse_yes_no
17
+ from codex_builder.profiles import supported_profile_names
18
+ from codex_builder.validator import (
19
+ validate_docs_level,
20
+ validate_existing_directory,
21
+ validate_language,
22
+ validate_stack,
23
+ )
24
+
25
+
26
+ InputFunc = Callable[[str], str]
27
+
28
+
29
+ class PromptAbort(Exception):
30
+ """Raised when the user chooses not to generate .codex."""
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class PromptBuildRequest:
35
+ """Resolved request data needed by the CLI entrypoint."""
36
+
37
+ config: ProjectConfig
38
+ target_dir: Path
39
+ force: bool
40
+ backup_existing: bool
41
+
42
+
43
+ class PromptSession:
44
+ """Collect missing CLI values using plain input prompts."""
45
+
46
+ def __init__(self, *, input_func: Optional[InputFunc] = None, output: Optional[TextIO] = None) -> None:
47
+ self._input = input_func or input
48
+ self._output = output
49
+
50
+ def resolve_request(
51
+ self,
52
+ args: argparse.Namespace,
53
+ *,
54
+ prompt_for_missing: bool,
55
+ confirm_before_build: bool,
56
+ ) -> PromptBuildRequest:
57
+ config = self._resolve_config(args, prompt_for_missing=prompt_for_missing)
58
+ target_dir = self._resolve_target_dir(args.target_dir, prompt_for_missing=prompt_for_missing)
59
+ force, backup_existing, conflict_prompted = self._resolve_existing_codex_policy(args, target_dir)
60
+
61
+ if confirm_before_build or conflict_prompted:
62
+ if not self.confirm_generation(config, target_dir):
63
+ raise PromptAbort("cancelled by user")
64
+
65
+ return PromptBuildRequest(
66
+ config=config,
67
+ target_dir=target_dir,
68
+ force=force,
69
+ backup_existing=backup_existing,
70
+ )
71
+
72
+ def _resolve_config(self, args: argparse.Namespace, *, prompt_for_missing: bool) -> ProjectConfig:
73
+ cwd_name = Path.cwd().name
74
+
75
+ if prompt_for_missing:
76
+ project_name = args.name or self.prompt_text("프로젝트 이름", cwd_name)
77
+ description = args.description if args.description is not None else self.prompt_text("프로젝트 설명", "")
78
+ stack = validate_stack(args.stack) if args.stack else self.prompt_stack()
79
+ database = args.database if args.database else self.prompt_database()
80
+ auth_enabled = parse_yes_no(args.auth) if args.auth is not None else self.prompt_confirm("인증 기능을 사용하나요?", default=False)
81
+ external_api_enabled = (
82
+ parse_yes_no(args.external_api)
83
+ if args.external_api is not None
84
+ else self.prompt_confirm("외부 API 연동이 있나요?", default=False)
85
+ )
86
+ docs_level = validate_docs_level(args.docs_level) if args.docs_level else self.prompt_docs_level()
87
+ language = validate_language(args.language) if args.language else self.prompt_language()
88
+ else:
89
+ project_name = args.name or cwd_name
90
+ description = args.description or ""
91
+ stack = validate_stack(args.stack or DEFAULT_STACK)
92
+ database = args.database or None
93
+ auth_enabled = parse_yes_no(args.auth, default=False)
94
+ external_api_enabled = parse_yes_no(args.external_api, default=False)
95
+ docs_level = validate_docs_level(args.docs_level or DEFAULT_DOCS_LEVEL)
96
+ language = validate_language(args.language or DEFAULT_LANGUAGE)
97
+
98
+ return ProjectConfig(
99
+ project_name=project_name,
100
+ description=description,
101
+ stack=stack,
102
+ database=database or None,
103
+ auth_enabled=auth_enabled,
104
+ external_api_enabled=external_api_enabled,
105
+ docs_level=docs_level,
106
+ language=language,
107
+ )
108
+
109
+ def _resolve_target_dir(self, target_dir: Optional[Path], *, prompt_for_missing: bool) -> Path:
110
+ if target_dir is not None:
111
+ return validate_existing_directory(target_dir)
112
+
113
+ if prompt_for_missing:
114
+ raw_target = self.prompt_text("대상 디렉토리", str(Path.cwd()))
115
+ return validate_existing_directory(Path(raw_target))
116
+
117
+ return validate_existing_directory(Path.cwd())
118
+
119
+ def _resolve_existing_codex_policy(self, args: argparse.Namespace, target_dir: Path) -> tuple[bool, bool, bool]:
120
+ codex_dir = target_dir / CODEX_DIR_NAME
121
+ if args.force:
122
+ return True, not args.overwrite, False
123
+ if not codex_dir.exists():
124
+ return False, True, False
125
+
126
+ action = self.prompt_existing_codex_action()
127
+ if action == "abort":
128
+ raise PromptAbort("existing .codex kept unchanged")
129
+ if action == "backup":
130
+ return True, True, True
131
+ return True, False, True
132
+
133
+ def prompt_text(self, label: str, default: str) -> str:
134
+ suffix = f" [{default}]" if default else ""
135
+ value = self._input(f"{label}{suffix}: ").strip()
136
+ return value or default
137
+
138
+ def prompt_confirm(self, label: str, *, default: bool) -> bool:
139
+ suffix = "[Y/n]" if default else "[y/N]"
140
+ while True:
141
+ value = self._input(f"{label} {suffix}: ").strip().lower()
142
+ if not value:
143
+ return default
144
+ if value in {"y", "yes"}:
145
+ return True
146
+ if value in {"n", "no"}:
147
+ return False
148
+ self._write("y 또는 n으로 입력해 주세요.")
149
+
150
+ def prompt_stack(self) -> tuple[str, ...]:
151
+ self._write("지원 프로필:")
152
+ for profile_name in supported_profile_names():
153
+ self._write(f"- {profile_name}")
154
+
155
+ while True:
156
+ raw_stack = self.prompt_text("사용 스택", ",".join(DEFAULT_STACK))
157
+ try:
158
+ return validate_stack(raw_stack)
159
+ except ConfigError as exc:
160
+ self._write(f"잘못된 stack 값입니다. {exc}")
161
+
162
+ def prompt_database(self) -> Optional[str]:
163
+ if not self.prompt_confirm("DB를 사용하나요?", default=False):
164
+ return None
165
+
166
+ while True:
167
+ database = self.prompt_text("DB 종류", "mysql").strip().lower()
168
+ if database:
169
+ return database
170
+ self._write("DB 종류를 입력해 주세요.")
171
+
172
+ def prompt_docs_level(self) -> str:
173
+ return self._prompt_choice("문서화 수준", ("light", "standard", "strict"), DEFAULT_DOCS_LEVEL, validate_docs_level)
174
+
175
+ def prompt_language(self) -> str:
176
+ return self._prompt_choice("문서 언어", ("ko", "en"), DEFAULT_LANGUAGE, validate_language)
177
+
178
+ def prompt_existing_codex_action(self) -> str:
179
+ self._write("")
180
+ self._write("기존 .codex 폴더가 존재합니다.")
181
+ self._write("1. 중단")
182
+ self._write("2. 백업 후 재생성")
183
+ self._write("3. 삭제 후 재생성")
184
+
185
+ while True:
186
+ value = self.prompt_text("처리 방식 선택", "1").strip().lower()
187
+ if value in {"1", "abort", "stop", "중단"}:
188
+ return "abort"
189
+ if value in {"2", "backup", "백업"}:
190
+ return "backup"
191
+ if value in {"3", "overwrite", "delete", "삭제"}:
192
+ return "overwrite"
193
+ self._write("1, 2, 3 중 하나를 입력해 주세요.")
194
+
195
+ def confirm_generation(self, config: ProjectConfig, target_dir: Path) -> bool:
196
+ self._write("")
197
+ self._write("생성 설정 확인")
198
+ self._write("")
199
+ self._write(f"프로젝트 이름: {config.project_name}")
200
+ self._write(f"설명: {config.description or '-'}")
201
+ self._write(f"스택: {','.join(config.stack)}")
202
+ self._write(f"DB: {config.database or 'no'}")
203
+ self._write(f"인증: {self._yes_no(config.auth_enabled)}")
204
+ self._write(f"외부 API: {self._yes_no(config.external_api_enabled)}")
205
+ self._write(f"문서화 수준: {config.docs_level}")
206
+ self._write(f"언어: {config.language}")
207
+ self._write(f"대상 디렉토리: {target_dir}")
208
+ self._write("")
209
+ return self.prompt_confirm("이 설정으로 .codex를 생성할까요?", default=True)
210
+
211
+ def _prompt_choice(
212
+ self,
213
+ label: str,
214
+ choices: tuple[str, ...],
215
+ default: str,
216
+ validator: Callable[[str], str],
217
+ ) -> str:
218
+ choices_text = "/".join(choices)
219
+ while True:
220
+ value = self.prompt_text(f"{label} ({choices_text})", default)
221
+ try:
222
+ return validator(value)
223
+ except ConfigError as exc:
224
+ self._write(f"잘못된 입력입니다. {exc}")
225
+
226
+ def _write(self, message: str) -> None:
227
+ print(message, file=self._output)
228
+
229
+ def _yes_no(self, value: bool) -> str:
230
+ return "yes" if value else "no"