@axboot-mcp/mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CLAUDE.md +119 -0
  2. package/MCP_TOOL_PLAN.md +710 -0
  3. package/MCP_USAGE.md +914 -0
  4. package/README.md +168 -0
  5. package/REPOSITORY_CONVENTIONS.md +250 -0
  6. package/SEARCH_PARAMS_MCP_TOOL_COMPLETE_PLAN.md +646 -0
  7. package/SEARCH_PARAMS_PLAN.md +2570 -0
  8. package/STORE_PATTERNS.md +1178 -0
  9. package/debug-dto.js +72 -0
  10. package/generate-banner-store.js +62 -0
  11. package/generation-plan.json +2176 -0
  12. package/generation-results.json +1817 -0
  13. package/package.json +45 -0
  14. package/scripts/batch-generate-all.js +159 -0
  15. package/scripts/batch-generate-mcp.js +329 -0
  16. package/scripts/batch-generate-stores-v2.js +272 -0
  17. package/scripts/batch-generate-stores.js +179 -0
  18. package/scripts/batch-plan.json +3810 -0
  19. package/scripts/batch-process.py +90 -0
  20. package/scripts/batch-regenerate.js +356 -0
  21. package/scripts/direct-generate.js +227 -0
  22. package/scripts/execute-batches.js +1911 -0
  23. package/scripts/generate-all-stores.js +144 -0
  24. package/scripts/generate-stores-mcp.js +161 -0
  25. package/scripts/generate-stores-v2.js +450 -0
  26. package/scripts/generate-stores-v3.js +412 -0
  27. package/scripts/generate-stores-v4.js +521 -0
  28. package/scripts/generate-stores.js +382 -0
  29. package/scripts/repos-to-process.json +1899 -0
  30. package/src/config/nh-layout-patterns.ts +166 -0
  31. package/src/docs/HOOK_GENERATION_PLAN.md +2226 -0
  32. package/src/docs/NH_STORE_PATTERNS.md +297 -0
  33. package/src/docs/README.md +216 -0
  34. package/src/docs/index.ts +28 -0
  35. package/src/docs/loader.ts +568 -0
  36. package/src/docs/patterns.json +419 -0
  37. package/src/docs/practical-examples.md +732 -0
  38. package/src/docs/quick-start.md +257 -0
  39. package/src/docs/requirements-analysis-guide.md +364 -0
  40. package/src/docs/rules.json +321 -0
  41. package/src/docs/store-pattern-analysis.md +664 -0
  42. package/src/docs/store-patterns-rules.md +1168 -0
  43. package/src/docs/store-patterns-usage-guide.md +1835 -0
  44. package/src/docs/troubleshooting.md +544 -0
  45. package/src/docs/type-selection-guide.md +572 -0
  46. package/src/docs//354/202/254/354/232/251/353/262/225/AntD-/354/273/264/355/217/254/353/204/214/355/212/270-/354/202/254/354/232/251/353/262/225.md +1515 -0
  47. package/src/docs//354/202/254/354/232/251/353/262/225/DataGrid-/354/202/254/354/232/251/353/262/225.md +866 -0
  48. package/src/docs//354/202/254/354/232/251/353/262/225/FormItem-/354/202/254/354/232/251/353/262/225.md +903 -0
  49. package/src/docs//354/202/254/354/232/251/353/262/225/FormModal-/354/202/254/354/232/251/353/262/225.md +1155 -0
  50. package/src/docs//354/202/254/354/232/251/353/262/225/MCP-/353/260/224/354/235/264/353/270/214/354/275/224/353/224/251-/352/260/200/354/235/264/353/223/234.md +1133 -0
  51. package/src/docs//354/202/254/354/232/251/353/262/225/MSW-Mock-/353/215/260/354/235/264/355/204/260-/354/202/254/354/232/251/353/262/225.md +579 -0
  52. package/src/docs//354/202/254/354/232/251/353/262/225/Search-/354/273/264/355/217/254/353/204/214/355/212/270-/354/202/254/354/232/251/353/262/225.md +738 -0
  53. package/src/docs//354/202/254/354/232/251/353/262/225/Store-/355/214/250/355/204/264-/354/202/254/354/232/251/353/262/225.md +1135 -0
  54. package/src/docs//354/202/254/354/232/251/353/262/225//355/231/224/353/251/264/352/265/254/354/204/261-/355/203/200/354/236/205/353/263/204-/352/260/234/353/260/234/354/210/234/354/204/234.md +1805 -0
  55. package/src/docs//354/202/254/354/232/251/353/262/225//355/231/224/353/251/264/355/203/200/354/236/205/353/263/204-/352/260/234/353/260/234-/355/224/204/353/241/254/355/224/204/355/212/270-/352/260/200/354/235/264/353/223/234.md +946 -0
  56. package/src/docs//354/202/254/354/232/251/353/262/225//355/231/225/354/236/245/355/231/224/353/251/264/355/203/200/354/236/205/353/263/204-/354/203/201/354/204/270-/355/224/204/353/241/254/355/224/204/355/212/270/352/260/200/354/235/264/353/223/234.md +2422 -0
  57. package/src/features/store-features.ts +232 -0
  58. package/src/handlers/analyze-requirements.ts +403 -0
  59. package/src/handlers/analyze.ts +1373 -0
  60. package/src/handlers/generate-from-requirements.ts +250 -0
  61. package/src/handlers/generate-hook.ts +950 -0
  62. package/src/handlers/generate-interactive.ts +840 -0
  63. package/src/handlers/generate-listdatagrid.ts +521 -0
  64. package/src/handlers/generate-multi-stores.ts +577 -0
  65. package/src/handlers/generate-requirements-from-layout.ts +160 -0
  66. package/src/handlers/generate-search-params.ts +717 -0
  67. package/src/handlers/generate.ts +911 -0
  68. package/src/handlers/list-templates.ts +104 -0
  69. package/src/handlers/scan-metadata.ts +485 -0
  70. package/src/handlers/suggest-layout.ts +326 -0
  71. package/src/index.ts +959 -0
  72. package/src/prompts/search-params.md +793 -0
  73. package/src/templates/index.ts +107 -0
  74. package/src/templates/unified.ts +462 -0
  75. package/store-generation-error-patterns.md +225 -0
  76. package/test/useAgentStore.ts +136 -0
  77. package/test-server.js +78 -0
  78. package/tsconfig.json +20 -0
@@ -0,0 +1,911 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ import { spawn, execSync } from 'child_process';
4
+ import { analyzeInterface, InterfaceAnalysis, extractIdFieldFromRequest } from './analyze.js';
5
+ import { getTemplate, TemplateVariables } from '../templates/index.js';
6
+ import {
7
+ getInitialValues,
8
+ getStoreTypeFields,
9
+ getRequiredImports,
10
+ getNamingRules,
11
+ getImplementationRules,
12
+ getStoreTypeInfo,
13
+ getCachedDocs
14
+ } from '../docs/index.js';
15
+
16
+ /**
17
+ * 프로젝트 루트 찾기 (tsconfig.json이 있는 위치)
18
+ * @param startPath 시작 경로 (생성된 파일 경로)
19
+ * @returns 프로젝트 루트 경로
20
+ */
21
+ async function findProjectRoot(startPath: string): Promise<string> {
22
+ let currentDir = path.dirname(startPath);
23
+
24
+ // 최대 10단계 상위 폴더로 탐색
25
+ for (let i = 0; i < 10; i++) {
26
+ const tsconfigPath = path.join(currentDir, 'tsconfig.json');
27
+
28
+ try {
29
+ await fs.access(tsconfigPath);
30
+ return currentDir; // tsconfig.json을 찾으면 해당 경로가 프로젝트 루트
31
+ } catch {
32
+ // 파일이 없으면 상위 폴드로 이동
33
+ const parentDir = path.dirname(currentDir);
34
+ if (parentDir === currentDir) {
35
+ break; // 더 이상 상위 폴더가 없음
36
+ }
37
+ currentDir = parentDir;
38
+ }
39
+ }
40
+
41
+ // tsconfig.json을 찾지 못하면 기본값 (현재 작업 디렉토리)
42
+ return process.cwd();
43
+ }
44
+
45
+ /**
46
+ * package.json 확인하여 type-check 스크립트 확인
47
+ * @param projectRoot 프로젝트 루트 경로
48
+ * @returns type-check 스크립트 존재 여부
49
+ */
50
+ async function hasTypeCheckScript(projectRoot: string): Promise<boolean> {
51
+ try {
52
+ const packageJsonPath = path.join(projectRoot, 'package.json');
53
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
54
+ const packageJson = JSON.parse(content);
55
+
56
+ return !!(packageJson.scripts && packageJson.scripts['type-check']);
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 타입 체크 실행 (프로젝트 구조 자동 분석)
64
+ * @param filePath 타입 체크할 파일 경로
65
+ * @returns 성공 여부 및 오류 목록
66
+ */
67
+ async function typeCheck(filePath: string): Promise<{ success: boolean; errors?: string[] }> {
68
+ try {
69
+ // 1. 프로젝트 루트 찾기
70
+ const projectRoot = await findProjectRoot(filePath);
71
+
72
+ // 2. package.json에서 type-check 스크립트 확인
73
+ const hasTypeCheck = await hasTypeCheckScript(projectRoot);
74
+
75
+ let command: string;
76
+ let args: string[];
77
+
78
+ if (hasTypeCheck) {
79
+ // package.json에 type-check 스크립트가 있으면 그것 사용
80
+ command = 'npm';
81
+ args = ['run', 'type-check'];
82
+ } else {
83
+ // 없으면 tsc 직접 실행
84
+ command = 'npx';
85
+ args = ['tsc', '--noEmit', filePath];
86
+ }
87
+
88
+ // 3. 명령 실행 (백그라운드)
89
+ execSync(
90
+ `${command} ${args.join(' ')}`,
91
+ {
92
+ encoding: 'utf-8',
93
+ cwd: projectRoot,
94
+ stdio: ['ignore', 'ignore', 'pipe'],
95
+ timeout: 30000 // 30초 타임아웃
96
+ }
97
+ );
98
+
99
+ return { success: true };
100
+ } catch (error: any) {
101
+ const stderr = error.stderr || error.stdout || '';
102
+ const errors = stderr
103
+ .split('\n')
104
+ .filter((line: string) => line.includes('error TS'))
105
+ .map((line: string) => line.trim())
106
+ .filter((line: string) => line.length > 0);
107
+ return { success: false, errors };
108
+ }
109
+ }
110
+
111
+ export async function generateStore(args: {
112
+ interfacePath: string;
113
+ outputPath: string;
114
+ storeType?: number;
115
+ storeName?: string;
116
+ }) {
117
+ const { interfacePath, outputPath, storeType = 1, storeName = 'useGeneratedStore' } = args;
118
+
119
+ // 1. 인터페이스 분석
120
+ const analyzeResult = await analyzeInterface({ interfacePath });
121
+ const analyzeData = JSON.parse(analyzeResult.content[0].text);
122
+
123
+ if (!analyzeData.success) {
124
+ return {
125
+ content: [
126
+ {
127
+ type: 'text',
128
+ text: JSON.stringify({
129
+ success: false,
130
+ error: analyzeData.error || '인터페이스 분석 실패',
131
+ }),
132
+ },
133
+ ],
134
+ };
135
+ }
136
+
137
+ const analysis: InterfaceAnalysis = analyzeData;
138
+
139
+ // 유효성 검사: 메서드가 없는 경우 스토어 생성 불가
140
+ if (!analysis.methods || analysis.methods.length === 0) {
141
+ return {
142
+ content: [
143
+ {
144
+ type: 'text',
145
+ text: JSON.stringify({
146
+ success: false,
147
+ error: '인터페이스에 메서드가 없습니다. Repository 또는 Interface 파일에 메서드를 정의해주세요.',
148
+ }),
149
+ },
150
+ ],
151
+ };
152
+ }
153
+
154
+ // 유효성 검사: services/index.ts에 export되지 않은 Service는 건너뜀
155
+ // isServiceExported가 false이면 실제로 export되지 않은 것으로 간주
156
+ if (analysis.isServiceExported === false) {
157
+ return {
158
+ content: [
159
+ {
160
+ type: 'text',
161
+ text: JSON.stringify({
162
+ success: false,
163
+ error: `services/index.ts에 export되지 않은 Repository입니다. (${analysis.className})`,
164
+ skip: true, // 스킵 대상임을 표시
165
+ }),
166
+ },
167
+ ],
168
+ };
169
+ }
170
+
171
+ // 유효성 검사: 리스트 메서드가 없는 경우 스토어 생성 불가
172
+ const listMethods = analysis.methods.filter((m) => m.isListMethod);
173
+ if (listMethods.length === 0) {
174
+ return {
175
+ content: [
176
+ {
177
+ type: 'text',
178
+ text: JSON.stringify({
179
+ success: false,
180
+ error: `리스트 메서드를 찾을 수 없습니다. Repository에 목록 조회 메서드가 필요합니다. (${analysis.className})`,
181
+ }),
182
+ },
183
+ ],
184
+ };
185
+ }
186
+
187
+ // 2. 템플릿 로드 (통합 템플릿 사용)
188
+ const { unifiedTemplate } = await import('../templates/index.js');
189
+ const template = unifiedTemplate;
190
+
191
+ // 3. 템플릿 변수 빌드
192
+ const variables = buildTemplateVariables(analysis, storeType, storeName);
193
+
194
+ // 4. 템플릿 적용
195
+ const storeCode = applyTemplate(template, variables);
196
+
197
+ // 5. 파일 생성
198
+ try {
199
+ const outputDir = path.dirname(outputPath);
200
+ await fs.mkdir(outputDir, { recursive: true });
201
+ await fs.writeFile(outputPath, storeCode, 'utf-8');
202
+
203
+ // 6. 타입 체크 (최대 3회 시도)
204
+ const maxRetries = 3;
205
+ let typeErrors: string[] = [];
206
+ let aiFixed = false;
207
+
208
+ for (let i = 0; i < maxRetries; i++) {
209
+ const result = await typeCheck(outputPath);
210
+
211
+ if (result.success) {
212
+ break; // 타입 체크 통과
213
+ }
214
+
215
+ // 오류 수집
216
+ typeErrors = result.errors || [];
217
+
218
+ // 마지막 시도이면 종료 (사용자에게 위임)
219
+ if (i === maxRetries - 1) {
220
+ break;
221
+ }
222
+
223
+ // 1차 시도: AI 자동 수정
224
+ if (i === 0) {
225
+ console.log(`[AI 자동 수정] 타입 오류 ${typeErrors.length}건`);
226
+ // TODO: AI 기반 자동 수정 로직 (fixTypeErrors 함수)
227
+ // storeCode = await fixTypeErrors(storeCode, typeErrors, interfacePath);
228
+ // await fs.writeFile(outputPath, storeCode, 'utf-8');
229
+ aiFixed = true;
230
+ }
231
+ }
232
+
233
+ // 7. Lint 및 Prettier 실행 (비동기로 실행하고 실패해도 파일 생성은 성공으로 처리)
234
+ runLintAndPrettier(outputPath).catch((err) => {
235
+ console.warn(`Lint/Prettier 실행 중 경고: ${err}`);
236
+ });
237
+
238
+ return {
239
+ content: [
240
+ {
241
+ type: 'text',
242
+ text: JSON.stringify({
243
+ success: true,
244
+ message: typeErrors.length > 0
245
+ ? `Store 파일 생성 완료 (타입 오류 ${typeErrors.length}건${aiFixed ? ', AI 수정 후 실패' : ''})`
246
+ : 'Store 파일 생성 완료 (타입 체크 통과)',
247
+ outputPath,
248
+ analysis: {
249
+ className: analysis.className,
250
+ serviceName: analysis.serviceName,
251
+ methodCount: analysis.methods.length,
252
+ hasPage: analysis.hasPage,
253
+ },
254
+ typeErrors: typeErrors.length > 0 ? typeErrors : undefined, // 타입 오류가 있으면 결과에 포함
255
+ aiFixed: aiFixed, // AI 시도 여부 표시
256
+ }),
257
+ },
258
+ ],
259
+ };
260
+ } catch (error) {
261
+ return {
262
+ content: [
263
+ {
264
+ type: 'text',
265
+ text: JSON.stringify({
266
+ success: false,
267
+ error: error instanceof Error ? error.message : String(error),
268
+ }),
269
+ },
270
+ ],
271
+ };
272
+ }
273
+ }
274
+
275
+ /**
276
+ * 템플릿 변수 빌드
277
+ * docs 폴더의 패턴 정보를 활용하여 더 정확한 변수 생성
278
+ */
279
+ export function buildTemplateVariables(
280
+ analysis: InterfaceAnalysis,
281
+ storeType: number,
282
+ storeName: string
283
+ ): TemplateVariables {
284
+ // docs에서 패턴 정보 로드
285
+ const typeFields = getStoreTypeFields(storeType);
286
+ const initialValues = getInitialValues(storeType);
287
+ const typeInfo = getStoreTypeInfo(storeType);
288
+ const namingRules = getNamingRules();
289
+ const implementationRules = getImplementationRules();
290
+
291
+ // List 메서드 찾기 - 우선순위에 따라 선택
292
+ // 1. 엔티티명 + List (예: postProductListPrd)
293
+ // 2. list + 엔티티명 (예: postListProduct)
294
+ // 3. 그 외 list 포함 메서드
295
+ const className = analysis.className || '';
296
+ const entityName = className.replace('Repository', ''); // Product
297
+
298
+ const listMethods = analysis.methods.filter((m) => m.isListMethod);
299
+
300
+ // 우선순위별로 메서드 찾기
301
+ // 1순위: 페이징이 있는 메서드 우선 (hasPage === true)
302
+ let selectedMethod = listMethods.find((m) =>
303
+ m.name.toLowerCase().includes(`list${entityName.toLowerCase()}`) && m.hasPage === true
304
+ );
305
+
306
+ // 2순위: 엔티티명 포함 메서드
307
+ if (!selectedMethod) {
308
+ selectedMethod = listMethods.find((m) =>
309
+ m.name.toLowerCase().includes(`list${entityName.toLowerCase()}`)
310
+ );
311
+ }
312
+
313
+ // 3순위: 페이징이 있는 메서드
314
+ if (!selectedMethod) {
315
+ selectedMethod = listMethods.find((m) => m.hasPage === true);
316
+ }
317
+
318
+ // 4순위: 가장 짧은 이름 (구체적일수록 이름이 짧음)
319
+ if (!selectedMethod && listMethods.length > 0) {
320
+ selectedMethod = listMethods.reduce((prev, curr) =>
321
+ curr.name.length < prev.name.length ? curr : prev
322
+ );
323
+ }
324
+
325
+ // List 메서드가 없으면 빈 기본값 사용 (첫 번째 메서드를 강제로 사용하지 않음)
326
+ const defaultListMethod: any = { name: '', requestType: 'any', responseType: 'any', isListMethod: false, comment: '' };
327
+ selectedMethod = selectedMethod || defaultListMethod;
328
+
329
+ // List 메서드 주석 추출
330
+ const listMethodComment = selectedMethod?.comment || '';
331
+
332
+ // 삭제/저장/상세 메서드 찾기 - 우선 순위: 주석에 "엔티티명 + 저장/삭제" → 주석에 "저장/삭제" → 메서드명 키워드
333
+ const deleteMethod = findMethodByPriority(analysis.methods, entityName, 'delete');
334
+ const saveMethod = findMethodByPriority(analysis.methods, entityName, 'save');
335
+ // detail 메서드는 list 메서드와 중복되지 않아야 함
336
+ const detailMethodCandidate = findMethodByPriority(analysis.methods, entityName, 'detail');
337
+ const detailMethod = detailMethodCandidate?.name !== selectedMethod?.name ? detailMethodCandidate : null;
338
+
339
+ // 삭제/저장/상세/엑셀 메서드 주석 추출
340
+ const deleteMethodComment = deleteMethod?.comment || '';
341
+ const saveMethodComment = saveMethod?.comment || '';
342
+ const detailMethodComment = detailMethod?.comment || '';
343
+
344
+ // Store 이름 처리 (docs 네이밍 규칙 준수)
345
+ const storeNameCamel = storeName; // useMemberListStore
346
+ const storeNamePascal = toPascalCase(storeName.replace(/^use/, '')); // MemberListStore
347
+ const storeInterface = `${storeNamePascal.replace(/Store$/, '')}Store`; // memberListStore
348
+
349
+ // Service 이름
350
+ const serviceName = analysis.serviceName || 'UnknownService';
351
+
352
+ // TypeScript 기본 타입 키워드 목록 (import에서 제외할 타입)
353
+ const TS_KEYWORDS = new Set([
354
+ 'void', 'any', 'unknown', 'never', 'null', 'undefined',
355
+ 'boolean', 'number', 'string', 'object', 'symbol', 'bigint'
356
+ ]);
357
+
358
+ // Request/Response 타입
359
+ const rawListRequest = selectedMethod!.requestType || 'any';
360
+ const listResponse = selectedMethod!.responseType || 'any';
361
+
362
+ // 삭제/저장 Request 타입
363
+ const rawDeleteRequest = deleteMethod?.requestType || 'any';
364
+ const rawSaveRequest = saveMethod?.requestType || 'any';
365
+
366
+ // 디버깅: deleteMethod 확인
367
+ if (deleteMethod) {
368
+ console.log('[DEBUG] deleteMethod:', { name: deleteMethod.name, requestType: deleteMethod.requestType, isRequestArray: deleteMethod.isRequestArray });
369
+ }
370
+
371
+ // TypeScript 키워드인 경우 빈 문자열로 처리 (extends 없이 빈 interface 사용)
372
+ const listRequest = TS_KEYWORDS.has(rawListRequest) ? '' : rawListRequest;
373
+ const deleteRequest = TS_KEYWORDS.has(rawDeleteRequest) ? '' : rawDeleteRequest;
374
+ const saveRequest = TS_KEYWORDS.has(rawSaveRequest) ? '' : rawSaveRequest;
375
+
376
+ // DTO 타입 추출
377
+ const dtoItem = analysis.dtoTypes[0] || extractDtoFromResponse(listResponse);
378
+
379
+ // 메서드 이름 (camelCase)
380
+ const listMethodName = toCamelCase(selectedMethod!.name);
381
+ const deleteMethodName = deleteMethod ? toCamelCase(deleteMethod.name) : null;
382
+ const saveMethodName = saveMethod ? toCamelCase(saveMethod.name) : null;
383
+ const detailMethodName = detailMethod ? toCamelCase(detailMethod.name) : null;
384
+
385
+ // DTO 소문자 변환
386
+ const dtoItemLower = toCamelCase(dtoItem);
387
+
388
+ // 페이징 여부
389
+ const hasPage = analysis.hasPage || false;
390
+
391
+ // API 여부 플래그 - 주석 기반 판단을 우선 적용
392
+ // 주석에서 "엔티티명 삭제", "엔티티명 저장" 패턴이 있으면 무조건 true
393
+ const hasSave = analysis.hasSaveFromComment || saveMethodName !== null;
394
+ const hasDelete = analysis.hasDeleteFromComment || deleteMethodName !== null;
395
+ const hasDetail = analysis.hasDetailFromComment || detailMethodName !== null;
396
+ const hasList = selectedMethod!.isListMethod;
397
+
398
+ // 파라미터 타입과 인터페이스 필드 타입 간의 호환성 판단
399
+ // 파라미터: id: number | string (고정)
400
+ // 인터페이스 필드: Repository에서 추출한 타입 (예: string | undefined)
401
+ const idFieldType = analysis.idFieldType || 'string | number';
402
+ // idFieldType이 number 또는 number|undefined인 경우 Number() 변환, string 또는 string|undefined인 경우 String() 변환
403
+ const isNumberOnly = idFieldType === 'number' || idFieldType === 'number | undefined';
404
+ const isStringOnly = idFieldType === 'string' || idFieldType === 'string | undefined';
405
+ const idValue = isNumberOnly ? 'Number(id)' :
406
+ isStringOnly ? 'String(id)' : 'id';
407
+ const keyValue = isNumberOnly ? 'Number(key)' :
408
+ isStringOnly ? 'String(key)' : 'key';
409
+
410
+ // Save Request 배열 여부 확인
411
+ const isSaveRequestArray = saveMethod?.isRequestArray || false;
412
+ // Delete Request 배열 여부 확인
413
+ const isDeleteRequestArray = deleteMethod?.isRequestArray || false;
414
+ // 실제 Repository Request 타입 사용 (rawDeleteRequest는 이미 line 214에서 선언됨)
415
+ const hasDeleteRequestType = rawDeleteRequest && rawDeleteRequest !== 'any';
416
+ const deleteRequestType = hasDeleteRequestType ? rawDeleteRequest : null;
417
+ const deleteParamType = deleteRequestType
418
+ ? (isDeleteRequestArray ? rawDeleteRequest + '[]' : rawDeleteRequest)
419
+ : (isDeleteRequestArray ? 'React.Key[]' : 'number | string');
420
+ const deleteParamName = deleteRequestType ? 'request' : (isDeleteRequestArray ? 'ids' : 'id');
421
+ const callDeleteApiDeclaration = hasDelete ? ('callDeleteApi: (' + deleteParamName + ': ' + deleteParamType + ') => Promise<void>;') : '';
422
+
423
+ // API 호출 라인 생성 (메서드가 없으면 TODO 주석)
424
+ // 삭제 API는 Request 인터페이스의 필수 필드를 listRequestValue에서 가져와야 함
425
+ const deleteApiCall = deleteMethodName
426
+ ? (deleteRequestType
427
+ ? 'await ' + serviceName + '.' + deleteMethodName + '(' + deleteParamName + ');'
428
+ : (isDeleteRequestArray
429
+ ? 'await ' + serviceName + '.' + deleteMethodName + '(' + deleteParamName + '.map(id => ({ ...get().listRequestValue, ' + (analysis.idField || 'id') + ': ' + idValue.replace(/\bid\b/g, 'id') + ' })));'
430
+ : 'await ' + serviceName + '.' + deleteMethodName + '({ ...get().listRequestValue, ' + (analysis.idField || 'id') + ': ' + idValue + ' });'))
431
+ : '// TODO: 삭제 API 호출 구현 필요\n // await ' + serviceName + '.remove' + dtoItem + '({ id });';
432
+
433
+ const saveApiCall = saveMethodName
434
+ ? `${hasPage ? `request.__status__ = get().listSelectedRowKey ? ("U" as const) : ("C" as const);` : ``}
435
+ await ${serviceName}.${saveMethodName}(request);`
436
+ : `// TODO: 저장 API 호출 구현 필요\n // await ${serviceName}.save${dtoItem}(request);`;
437
+
438
+ // === dateRange 패턴 관련 ===
439
+ const hasDateRange = analysis.hasDateRange || false;
440
+ // dateRange가 있으면 dayjs import가 항상 필요함 (기본값에 dayjs() 사용)
441
+ const hasDayJs = hasDateRange || (analysis.hasDayJs || false);
442
+ const dateRangeFields = analysis.dateRangeFields;
443
+ const isBgnDtm = dateRangeFields?.searchType === 'bgnDtm';
444
+
445
+ // === docs 기반 추가 변수 ===
446
+
447
+ // MetaData 필드 (docs에서 가져온 순서대로)
448
+ const metadataRequired = typeFields.metadata.required || [];
449
+ const metadataOptional = typeFields.metadata.optional || [];
450
+ const allMetadataFields = [...metadataRequired, ...metadataOptional];
451
+
452
+ // States 필드
453
+ const statesRequired = typeFields.states.required || [];
454
+ const statesOptional = typeFields.states.optional || [];
455
+
456
+ // Actions
457
+ const actionsRequired = typeFields.actions.required || [];
458
+ const actionsOptional = typeFields.actions.optional || [];
459
+
460
+ // Subscribe selector
461
+ const subscribeSelector = typeFields.subscribeSelector || [];
462
+
463
+ // 초기값 생성 (docs의 initialValues 활용)
464
+ const createStateFields = generateCreateStateFields(initialValues, allMetadataFields, statesOptional);
465
+
466
+ // 구현 규칙 기반 callListApi 템플릿
467
+ const callListApiTemplate = implementationRules.callListApi?.template || '';
468
+
469
+ // 생성될 Store에 대한 문서 참조 주석
470
+ const docsReference = generateDocsReference(storeType, typeInfo);
471
+
472
+ // Request 필수 필드 기본값 생성 (requiredFields에서)
473
+ const requiredFields = analysis.requiredFields || [];
474
+ const requiredFieldsDefaults = generateRequiredFieldsDefaults(requiredFields);
475
+
476
+ // Imports - 실제로 사용하는 타입만 포함 (eslint no-unused-vars 방지)
477
+ // Response 타입은 템플릿에서 직접 사용하지 않으므로 import 제외
478
+ const requiredImports = new Set<string>();
479
+ requiredImports.add(serviceName); // Service 항상 추가
480
+ // ListRequest 타입 - TypeScript 키워드가 아니면 import
481
+ if (listRequest && !TS_KEYWORDS.has(rawListRequest)) {
482
+ requiredImports.add(listRequest);
483
+ }
484
+ // ListResponse는 API 호출 반환값으로 추론되므로 import 불필요
485
+ if (dtoItem && dtoItem !== 'Record<string, any>' && dtoItem !== listRequest && !TS_KEYWORDS.has(dtoItem)) {
486
+ requiredImports.add(dtoItem); // DTO 타입 (중복 제외)
487
+ }
488
+ // Delete/Save Request 타입 - 해당 API가 존재할 때만 import (TypeScript 키워드 제외)
489
+ if (hasDelete && deleteRequest && !TS_KEYWORDS.has(rawDeleteRequest) && deleteRequest !== listRequest && deleteRequest !== dtoItem) {
490
+ requiredImports.add(deleteRequest);
491
+ }
492
+ if (hasSave && saveRequest && !TS_KEYWORDS.has(rawSaveRequest) && saveRequest !== listRequest && saveRequest !== dtoItem) {
493
+ requiredImports.add(saveRequest);
494
+ }
495
+
496
+ // 정렬
497
+ const allImports = Array.from(requiredImports).sort();
498
+
499
+ // prettier 규칙: import가 길 경우 멀티 라인으로
500
+ // prettier는 import 문에 대해 더 관대하므로 매우 긴 경우에만 멀티 라인
501
+ let importsLine: string;
502
+ if (allImports.length > 0) {
503
+ const importsStr = allImports.join(', ');
504
+ // 전체 import 문 길이 (대략): importsStr + 30 characters
505
+ // prettier는 import가 120자 이상일 때만 멀티 라인 선호
506
+ const fullLength = importsStr.length + 30;
507
+ if (fullLength > 120) {
508
+ importsLine = `import {\n ${allImports.join(',\n ')},\n} from "services";`;
509
+ } else {
510
+ importsLine = `import { ${importsStr} } from "services";`;
511
+ }
512
+ } else {
513
+ importsLine = `import { ${serviceName} } from "services";`;
514
+ }
515
+
516
+ return {
517
+ // Store 이름
518
+ STORE_NAME: storeNameCamel,
519
+ STORE_NAME_PASCAL: storeNamePascal,
520
+ STORE_INTERFACE: storeInterface,
521
+
522
+ // Service
523
+ SERVICE_NAME: serviceName,
524
+ LIST_METHOD_NAME: listMethodName,
525
+ DELETE_METHOD_NAME: deleteMethodName,
526
+ SAVE_METHOD_NAME: saveMethodName,
527
+ DETAIL_METHOD_NAME: detailMethodName,
528
+ DELETE_API_CALL: deleteApiCall,
529
+ SAVE_API_CALL: saveApiCall,
530
+
531
+ // Request/Response
532
+ LIST_REQUEST: listRequest,
533
+ LIST_REQUEST_EXTENDS: listRequest ? `extends ${listRequest}` : '', // extends 또는 빈 문자열
534
+ LIST_RESPONSE: listResponse,
535
+ ACTIONS_EXTENDS: hasPage ? 'extends PageStoreActions<States> ' : 'extends Record<string, any> ', // Actions interface extends
536
+ DELETE_REQUEST: deleteRequest,
537
+ DELETE_REQUEST_EXTENDS: deleteRequest ? `extends ${deleteRequest}` : '',
538
+ SAVE_REQUEST: saveRequest,
539
+ SAVE_REQUEST_EXTENDS: saveRequest ? `extends ${saveRequest}` : '',
540
+ SAVE_PARAM_TYPE: saveRequest ? (isSaveRequestArray ? saveRequest + '[]' : saveRequest) : dtoItem, // Save 메서드 파라미터 타입 (SaveRequest 있으면 그것 사용, 없으면 DTO, 배열이면 [] 추가)
541
+ DTO_ITEM: dtoItem,
542
+ DTO_ITEM_LOWER: dtoItemLower,
543
+
544
+ // 페이징 여부
545
+ HAS_PAGE: hasPage,
546
+ HAS_SAVE: hasSave,
547
+ HAS_DELETE: hasDelete,
548
+ HAS_DETAIL: hasDetail,
549
+ HAS_LIST: hasList,
550
+ HAS_REORDER: storeType === 6, // Type 6 (재정렬 가능 리스트)만 재정렬 기능 활성화
551
+ HAS_LIST_REQUEST_PARAMS: !!listRequest, // ListRequest가 빈 문자열이 아니면 true (void가 아닌 경우)
552
+ HAS_LIST_RESPONSE: Boolean(listResponse && listResponse !== 'void' && listResponse !== 'any'), // 응답 타입이 void가 아니면 true (response.ds 접근용)
553
+ HAS_PAGE_IN_REQUEST: analysis.hasPageInRequest || false, // Request에 pageNumber/pageSize 필드가 있는지 (listRequestValue 초기값 결정용)
554
+ HAS_EXCEL: (analysis.hasExcel || false) && !!analysis.excelMethodName, // 엑셀 메서드가 있을 때만 true
555
+ EXCEL_METHOD_NAME: analysis.excelMethodName,
556
+ EXCEL_PARAM_TYPE: listRequest || 'ListRequest', // 엑셀 메서드 파라미터 타입 (ListRequest 또는 기본값)
557
+ IS_EXCEL_REQUEST_ARRAY: analysis.isExcelRequestArray || false, // 엑셀 Request 배열 여부
558
+ IS_DELETE_REQUEST_ARRAY: isDeleteRequestArray, // 삭제 Request 배열 여부
559
+ HAS_ARRAY_DELETE_API: analysis.hasArrayDeleteApi || false, // 다른 삭제 메서드 중 배열이 있는지 (callMultiDeleteApi 생성용)
560
+
561
+ // dateRange/dayjs 패턴
562
+ HAS_DATE_RANGE: hasDateRange,
563
+ HAS_DAY_JS: hasDayJs,
564
+ IS_BGNDTM: isBgnDtm,
565
+
566
+ // __status__ 패턴용 ID 필드
567
+ // idFieldType이 정확히 'number'인 경우 Number() 변환, 'string | number'인 경우 그대로 사용, string인 경우 String() 변환
568
+ ID_FIELD: analysis.idField || 'id',
569
+ CALL_DELETE_API_DECLARATION: callDeleteApiDeclaration, // callDeleteApi Actions interface 선언
570
+ DELETE_PARAM_NAME: deleteParamName,
571
+ DELETE_PARAM_TYPE: deleteParamType,
572
+ ID_FIELD_TYPE: analysis.idFieldType || 'string | number',
573
+ ID_FIELD_NEEDS_STRING_CONVERSION: !(analysis.idFieldType || 'string | number').includes('number'),
574
+ ID_FIELD_VALUE: idValue,
575
+ ID_FIELD_KEY_VALUE: keyValue,
576
+
577
+ // Request 필수 필드 기본값
578
+ REQUIRED_FIELDS_DEFAULTS: requiredFieldsDefaults,
579
+
580
+ // Imports
581
+ IMPORTS: importsLine,
582
+ LIST_METHOD_COMMENT: listMethodComment,
583
+ DELETE_METHOD_COMMENT: deleteMethodComment,
584
+ SAVE_METHOD_COMMENT: saveMethodComment,
585
+ DETAIL_METHOD_COMMENT: detailMethodComment,
586
+
587
+ // 응답 필드 정보
588
+ LIST_RESPONSE_FIELD: selectedMethod?.responseListField || 'ds',
589
+ DETAIL_RESPONSE_FIELD: detailMethod?.responseDetailField || 'rs',
590
+ IS_DIRECT_RESPONSE: detailMethod?.responseDetailField === null && detailMethod?.responseType !== undefined,
591
+
592
+ // 요청 필드 정보 (SearchParams 생성용)
593
+ REQUEST_FIELDS: analysis.requestFields ? JSON.stringify(analysis.requestFields) : '[]',
594
+
595
+ // === docs 기반 추가 변수 ===
596
+ METADATA_FIELDS: allMetadataFields.join(', '),
597
+ METADATA_REQUIRED: metadataRequired.join(', '),
598
+ METADATA_OPTIONAL: metadataOptional.join(', '),
599
+ STATES_REQUIRED: statesRequired.join(', '),
600
+ STATES_OPTIONAL: statesOptional.join(', '),
601
+ ACTIONS_REQUIRED: actionsRequired.join(', '),
602
+ ACTIONS_OPTIONAL: actionsOptional.join(', '),
603
+ SUBSCRIBE_SELECTOR: subscribeSelector.join(', '),
604
+ CREATE_STATE_FIELDS: createStateFields,
605
+ CALL_LIST_API_TEMPLATE: callListApiTemplate,
606
+ DOCS_REFERENCE: docsReference,
607
+ STORE_TYPE_NAME: typeInfo?.name || `Type ${storeType}`,
608
+ STORE_TYPE_DESCRIPTION: typeInfo?.description || '',
609
+ };
610
+ }
611
+
612
+ /**
613
+ * Response 타입에서 DTO 추출
614
+ */
615
+ function extractDtoFromResponse(responseType: string): string {
616
+ // PostMemberListMemberResponse -> MemberRes
617
+ const match = responseType.match(/Post\w+?(\w+)Response/);
618
+ if (match) {
619
+ const base = match[1];
620
+ // Member -> MemberRes, ListMember -> MemberRes
621
+ if (base.startsWith('List')) {
622
+ return base.replace('List', '') + 'Res';
623
+ }
624
+ return base + 'Res';
625
+ }
626
+ return 'unknown';
627
+ }
628
+
629
+ /**
630
+ * createState 필드 생성 (docs initialValues 활용)
631
+ */
632
+ function generateCreateStateFields(
633
+ initialValues: any,
634
+ metadataFields: string[],
635
+ statesOptional: string[]
636
+ ): string {
637
+ const lines: string[] = [];
638
+
639
+ // MetaData 필드 초기값
640
+ for (const field of metadataFields) {
641
+ if (initialValues[field] !== undefined) {
642
+ lines.push(` ${field}: ${JSON.stringify(initialValues[field])},`);
643
+ } else if (field === 'programFn') {
644
+ lines.push(` programFn: undefined,`);
645
+ } else if (field.includes('Value') || field.includes('Widths') || field.includes('Params') || field.includes('Keys')) {
646
+ lines.push(` ${field}: [],`);
647
+ } else if (field === 'flexGrow') {
648
+ lines.push(` flexGrow: 400,`);
649
+ } else if (field === 'formActive') {
650
+ lines.push(` formActive: false,`);
651
+ }
652
+ }
653
+
654
+ // States 필드 초기값
655
+ const defaultStates: Record<string, string> = {
656
+ routePath: 'undefined',
657
+ listSpinning: 'false',
658
+ listData: '[]',
659
+ listPage: '{ currentPage: 0, totalPages: 0 }',
660
+ selectedItem: 'undefined',
661
+ detail: 'undefined',
662
+ saveRequestValue: '{}',
663
+ detailLoading: 'false',
664
+ checkedRowKeys: '[]',
665
+ listSelectedRowKey: '""',
666
+ modalOpen: 'false',
667
+ };
668
+
669
+ for (const [field, defaultValue] of Object.entries(defaultStates)) {
670
+ if (!metadataFields.includes(field)) {
671
+ lines.push(` ${field}: ${defaultValue},`);
672
+ }
673
+ }
674
+
675
+ // 추가 States 옵션 필드
676
+ for (const field of statesOptional) {
677
+ if (!metadataFields.includes(field) && !defaultStates[field]) {
678
+ if (field.includes('Spinning') || field.includes('Loading') || field === 'isDirty') {
679
+ lines.push(` ${field}: false,`);
680
+ } else {
681
+ lines.push(` ${field}: undefined,`);
682
+ }
683
+ }
684
+ }
685
+
686
+ return lines.join('\n');
687
+ }
688
+
689
+ /**
690
+ * 문서 참조 주석 생성
691
+ */
692
+ function generateDocsReference(storeType: number, typeInfo: any): string {
693
+ const typeName = typeInfo?.name || `Type ${storeType}`;
694
+ const description = typeInfo?.description || '';
695
+ const examples = typeInfo?.examples || [];
696
+
697
+ const lines: string[] = [];
698
+ lines.push(`/**`);
699
+ lines.push(` * Store Pattern: ${typeName}`);
700
+ if (description) {
701
+ lines.push(` * ${description}`);
702
+ }
703
+ if (examples.length > 0) {
704
+ lines.push(` *`);
705
+ lines.push(` * Examples:`);
706
+ for (const example of examples) {
707
+ lines.push(` * - ${example}`);
708
+ }
709
+ }
710
+ lines.push(` *`);
711
+ lines.push(` * Generated based on NH-FE-B Store patterns`);
712
+ lines.push(` * See: src/docs/store-patterns-usage-guide.md`);
713
+ lines.push(` */`);
714
+
715
+ return lines.join('\n');
716
+ }
717
+
718
+ /**
719
+ * 템플릿 적용 - Mustache.js 사용
720
+ */
721
+ function applyTemplate(template: string, variables: TemplateVariables): string {
722
+ const Mustache = require('mustache');
723
+
724
+ // TypeScript 코드 생성이므로 HTML 이스케이프 비활성화
725
+ // 기본 escape 함수를 덮어써서 원본 그대로 반환
726
+ const originalEscape = Mustache.escape;
727
+ Mustache.escape = (text: any) => String(text);
728
+
729
+ try {
730
+ // Mustache.js를 사용하여 템플릿 렌더링
731
+ // {{#KEY}}...{{/KEY}}: 조건부 블록 (truthy일 때 내용 포함)
732
+ // {{^KEY}}...{{/KEY}}: 역조건부 블록 (falsy일 때 내용 포함)
733
+ // {{KEY}}: 변수 치환
734
+ let result = Mustache.render(template, variables);
735
+
736
+ // 후처리: 빈 줄 제거만 수행 (정규식 제거로 인한 함수 시그니처 손실 방지)
737
+ result = result
738
+ // 빈 줄 제거 (연속된 2개 이상의 줄바꿈을 하나로 줄임)
739
+ .replace(/\n\s*\n\s*\n/g, '\n\n')
740
+ // 빈 줄 정리
741
+ .replace(/\n\s*\n/g, '\n');
742
+
743
+ return result;
744
+ } finally {
745
+ // 원래 escape 함수 복원
746
+ Mustache.escape = originalEscape;
747
+ }
748
+ }
749
+
750
+ /**
751
+ * PascalCase 변환
752
+ */
753
+ function toPascalCase(str: string): string {
754
+ return str.replace(/(^|-)(\w)/g, (_, __, char) => char.toUpperCase());
755
+ }
756
+
757
+ /**
758
+ * camelCase 변환
759
+ */
760
+ function toCamelCase(str: string): string {
761
+ return str.charAt(0).toLowerCase() + str.slice(1);
762
+ }
763
+
764
+ /**
765
+ * Request 필수 필드 기본값 생성
766
+ * 필드 이름을 기반으로 적절한 기본값 추론
767
+ */
768
+ function generateRequiredFieldsDefaults(requiredFields: string[]): string {
769
+ const lines: string[] = [];
770
+
771
+ for (const field of requiredFields) {
772
+ // 필드 이름을 분석하여 기본값 결정
773
+ const lowerField = field.toLowerCase();
774
+
775
+ // Tpcd, tpCd, code, cd 등으로 끝나면 빈 문자열
776
+ if (/(Tpcd|tpCd|Code|cd|Type|type)$/.test(field)) {
777
+ lines.push(` ${field}: '',`);
778
+ }
779
+ // Yn으로 끝나면 빈 문자열
780
+ else if (/(Yn|yn)$/.test(field)) {
781
+ lines.push(` ${field}: '',`);
782
+ }
783
+ // No, Number로 끝나면 빈 문자열 또는 0
784
+ else if (/(No|Number)$/.test(field)) {
785
+ lines.push(` ${field}: '',`);
786
+ }
787
+ // Date, Dt, Dtm으로 끝나면 빈 문자열
788
+ else if (/(Date|Dt|Dtm)$/.test(field)) {
789
+ lines.push(` ${field}: '',`);
790
+ }
791
+ // 그 외에는 빈 문자열
792
+ else {
793
+ lines.push(` ${field}: '',`);
794
+ }
795
+ }
796
+
797
+ return lines.join('\n');
798
+ }
799
+
800
+ /**
801
+ * 메서드를 우선 순위별로 찾기
802
+ * 1순위: 주석에 "첫단어 + 공백 + 저장/삭제/상세" 정확 매칭 (예: "배너 저장")
803
+ * "배너 순서저장 저장"은 첫단어+공백+키워드 패턴이 아니므로 제외
804
+ * 2순위: 주석에 "저장/삭제/상세"만 있는 경우
805
+ * 3순위: 메서드명에 키워드 포함
806
+ */
807
+ function findMethodByPriority(
808
+ methods: any[],
809
+ entityName: string,
810
+ actionType: 'delete' | 'save' | 'detail'
811
+ ): any | undefined {
812
+ const keywords = {
813
+ delete: ['delete', 'remove'],
814
+ save: ['save', 'insert', 'create'],
815
+ detail: ['detail', 'info', 'get'],
816
+ };
817
+ const koreanKeywords = {
818
+ delete: '삭제',
819
+ save: '저장',
820
+ detail: '상세',
821
+ };
822
+
823
+ const actionKeywords = keywords[actionType];
824
+ const koreanKeyword = koreanKeywords[actionType];
825
+
826
+ // 1순위: 주석에서 "첫단어 + 공백 + 한글 키워드" 정확 매칭
827
+ // 주석: "/* 배너 저장 */" → 첫단어 "배너" + 공백 + "저장" → 매칭!
828
+ // 주석: "/* 배너 순서저장 저장 */" → 첫단어 "배너" + 공백 + "순서저장" → 매칭 실패
829
+ // detail의 경우: "/* 상세 조회 */", "/* 정보 조회 */", "/* 내역 조회 */"도 매칭
830
+ for (const m of methods) {
831
+ if (m.comment) {
832
+ // 주석 정제: /*, */, 앞뒤 공백 제거
833
+ const cleaned = m.comment.replace(/\/\*\*?|\*\//g, '').trim();
834
+ // 주의: \b (word boundary)는 한글에서 제대로 작동하지 않으므로 사용하지 않음
835
+ // 대신 공백으로 분리하여 두 번째 단어가 정확히 키워드인지 확인
836
+ const parts = cleaned.split(/\s+/);
837
+ if (parts.length >= 2) {
838
+ // 일반 패턴: 첫단어 + 공백 + 키워드 (예: "배너 저장")
839
+ if (parts[1] === koreanKeyword) {
840
+ return m;
841
+ }
842
+ // detail 추가 패턴: "상세 조회", "정보 조회", "내역 조회"
843
+ if (actionType === 'detail' && parts[1] === '조회' && ['상세', '정보', '내역'].includes(parts[0])) {
844
+ return m;
845
+ }
846
+ }
847
+ }
848
+ }
849
+
850
+ // 2순위: 주석에 한글 키워드만 있는 경우
851
+ // detail의 경우 "조회" 키워드도 포함
852
+ const koreanMatch = methods.find((m) => {
853
+ if (!m.comment) return false;
854
+ const hasKeyword = m.comment.includes(koreanKeyword);
855
+ const hasInquiry = actionType === 'detail' && m.comment.includes('조회');
856
+ return hasKeyword || hasInquiry;
857
+ });
858
+ if (koreanMatch) return koreanMatch;
859
+
860
+ // 3순위: 메서드명에 키워드 포함
861
+ const nameMatch = methods.find((m) =>
862
+ actionKeywords.some((keyword) => m.name.toLowerCase().includes(keyword))
863
+ );
864
+ return nameMatch;
865
+ }
866
+
867
+ /**
868
+ * Lint 및 Prettier 실행 (프로젝트 루트 기준)
869
+ */
870
+ async function runLintAndPrettier(filePath: string): Promise<void> {
871
+ // 프로젝트 루트 찾기
872
+ const projectRoot = await findProjectRoot(filePath);
873
+ const relativePath = path.relative(projectRoot, filePath);
874
+
875
+ // Prettier 실행
876
+ await runCommand('npx', ['prettier', '--write', relativePath], projectRoot);
877
+
878
+ // ESLint 실행
879
+ await runCommand('npx', ['eslint', '--fix', relativePath], projectRoot);
880
+ }
881
+
882
+ /**
883
+ * 명령 실행 헬퍼
884
+ */
885
+ function runCommand(command: string, args: string[], cwd: string): Promise<void> {
886
+ return new Promise((resolve, reject) => {
887
+ const proc = spawn(command, args, {
888
+ cwd,
889
+ stdio: ['ignore', 'pipe', 'pipe'],
890
+ shell: true,
891
+ });
892
+
893
+ let stderr = '';
894
+
895
+ proc.stderr?.on('data', (data) => {
896
+ stderr += data.toString();
897
+ });
898
+
899
+ proc.on('close', (code) => {
900
+ if (code === 0) {
901
+ resolve();
902
+ } else {
903
+ reject(new Error(`${command} ${args.join(' ')} failed with code ${code}: ${stderr}`));
904
+ }
905
+ });
906
+
907
+ proc.on('error', (err) => {
908
+ reject(err);
909
+ });
910
+ });
911
+ }