@hua-labs/create-hua-ux 0.1.0-alpha.0.1

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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -0
  3. package/dist/bin/create-hua-ux.d.ts +9 -0
  4. package/dist/bin/create-hua-ux.d.ts.map +1 -0
  5. package/dist/bin/create-hua-ux.js +37 -0
  6. package/dist/constants/versions.d.ts +55 -0
  7. package/dist/constants/versions.d.ts.map +1 -0
  8. package/dist/constants/versions.js +57 -0
  9. package/dist/create-project.d.ts +18 -0
  10. package/dist/create-project.d.ts.map +1 -0
  11. package/dist/create-project.js +237 -0
  12. package/dist/doctor.d.ts +21 -0
  13. package/dist/doctor.d.ts.map +1 -0
  14. package/dist/doctor.js +259 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +177 -0
  18. package/dist/utils.d.ts +108 -0
  19. package/dist/utils.d.ts.map +1 -0
  20. package/dist/utils.js +896 -0
  21. package/dist/version.d.ts +9 -0
  22. package/dist/version.d.ts.map +1 -0
  23. package/dist/version.js +11 -0
  24. package/package.json +46 -0
  25. package/templates/nextjs/.claude/project-context.md +310 -0
  26. package/templates/nextjs/.claude/skills/hua-ux-framework/SKILL.md +187 -0
  27. package/templates/nextjs/.cursorrules +302 -0
  28. package/templates/nextjs/.eslintrc.json +1 -0
  29. package/templates/nextjs/README.md +431 -0
  30. package/templates/nextjs/ai-context.md +332 -0
  31. package/templates/nextjs/app/api/translations/[language]/[namespace]/route.ts +86 -0
  32. package/templates/nextjs/app/globals.css +24 -0
  33. package/templates/nextjs/app/layout-with-geo.example.tsx +106 -0
  34. package/templates/nextjs/app/layout.tsx +30 -0
  35. package/templates/nextjs/app/page-with-geo.example.tsx +80 -0
  36. package/templates/nextjs/app/page.tsx +28 -0
  37. package/templates/nextjs/components/I18nProviderWrapper.tsx +19 -0
  38. package/templates/nextjs/lib/i18n-setup.ts +11 -0
  39. package/templates/nextjs/middleware.ts.example +22 -0
  40. package/templates/nextjs/next.config.ts +36 -0
  41. package/templates/nextjs/postcss.config.js +6 -0
  42. package/templates/nextjs/store/useAppStore.ts +8 -0
  43. package/templates/nextjs/tailwind.config.js +8 -0
  44. package/templates/nextjs/translations/en/common.json +6 -0
  45. package/templates/nextjs/translations/ko/common.json +6 -0
  46. package/templates/nextjs/tsconfig.json +41 -0
package/dist/utils.js ADDED
@@ -0,0 +1,896 @@
1
+ "use strict";
2
+ /**
3
+ * create-hua-ux - Utilities
4
+ *
5
+ * Utility functions for project creation
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ var __importDefault = (this && this.__importDefault) || function (mod) {
41
+ return (mod && mod.__esModule) ? mod : { "default": mod };
42
+ };
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.promptProjectName = promptProjectName;
45
+ exports.promptAiContextOptions = promptAiContextOptions;
46
+ exports.copyTemplate = copyTemplate;
47
+ exports.generatePackageJson = generatePackageJson;
48
+ exports.generateConfig = generateConfig;
49
+ exports.generateAiContextFiles = generateAiContextFiles;
50
+ exports.checkPrerequisites = checkPrerequisites;
51
+ exports.validateTemplate = validateTemplate;
52
+ exports.validateGeneratedProject = validateGeneratedProject;
53
+ exports.validateTranslationFiles = validateTranslationFiles;
54
+ exports.generateSummary = generateSummary;
55
+ exports.displaySummary = displaySummary;
56
+ exports.displayNextSteps = displayNextSteps;
57
+ const fs = __importStar(require("fs-extra"));
58
+ const path = __importStar(require("path"));
59
+ const child_process_1 = require("child_process");
60
+ const inquirer_1 = __importDefault(require("inquirer"));
61
+ const chalk_1 = __importDefault(require("chalk"));
62
+ const version_1 = require("./version");
63
+ const versions_1 = require("./constants/versions");
64
+ // Resolve template directory
65
+ // When compiled, __dirname points to dist/, so we need to go up to templates/
66
+ // When running with tsx, __dirname points to src/, so we need to go up one level
67
+ const TEMPLATE_DIR = path.join(__dirname, '../templates/nextjs');
68
+ /**
69
+ * Check if English-only mode is enabled
70
+ */
71
+ function isEnglishOnly() {
72
+ return process.env.LANG === 'en' || process.env.CLI_LANG === 'en' || process.argv.includes('--english-only');
73
+ }
74
+ /**
75
+ * Get localized message
76
+ */
77
+ function t(key) {
78
+ if (isEnglishOnly()) {
79
+ const messages = {
80
+ projectNamePrompt: 'What is your project name?',
81
+ projectNameRequired: 'Project name is required',
82
+ selectAiContext: 'Select AI context files to generate:',
83
+ documentationLanguage: 'Documentation language:',
84
+ };
85
+ return messages[key] || key;
86
+ }
87
+ // Bilingual (Korean + English)
88
+ const messages = {
89
+ projectNamePrompt: 'What is your project name? / 프로젝트 이름을 입력하세요:',
90
+ projectNameRequired: 'Project name is required / 프로젝트 이름이 필요합니다',
91
+ selectAiContext: 'Select AI context files to generate / 생성할 AI 컨텍스트 파일을 선택하세요:',
92
+ documentationLanguage: 'Documentation language / 문서 언어:',
93
+ };
94
+ return messages[key] || key;
95
+ }
96
+ /**
97
+ * Prompt for project name
98
+ */
99
+ async function promptProjectName() {
100
+ // If not interactive, cannot prompt
101
+ if (!isInteractive()) {
102
+ throw new Error('Project name is required when running in non-interactive mode. Please provide it as an argument: npx tsx src/index.ts <project-name>');
103
+ }
104
+ const { projectName } = await inquirer_1.default.prompt([
105
+ {
106
+ type: 'input',
107
+ name: 'projectName',
108
+ message: t('projectNamePrompt'),
109
+ validate: (input) => {
110
+ if (!input.trim()) {
111
+ return t('projectNameRequired');
112
+ }
113
+ return true;
114
+ },
115
+ },
116
+ ]);
117
+ return projectName;
118
+ }
119
+ /**
120
+ * Check if running in interactive mode
121
+ *
122
+ * For PowerShell and other environments, we check:
123
+ * 1. stdin/stdout are TTY (if available)
124
+ * 2. Not in CI environment
125
+ * 3. Not explicitly set to non-interactive
126
+ * 4. stdin is readable (not piped)
127
+ *
128
+ * In PowerShell, isTTY might be undefined, so we use a more lenient check.
129
+ */
130
+ function isInteractive() {
131
+ // Explicitly non-interactive
132
+ if (process.env.CI || process.env.NON_INTERACTIVE) {
133
+ return false;
134
+ }
135
+ // Check if stdin is TTY (available in most terminals)
136
+ // In PowerShell, this might be undefined, so we check if it's explicitly false
137
+ // If undefined, we assume it might be interactive (PowerShell can be interactive)
138
+ const stdinTTY = process.stdin.isTTY;
139
+ const stdoutTTY = process.stdout.isTTY;
140
+ // If both are explicitly false, definitely not interactive
141
+ if (stdinTTY === false && stdoutTTY === false) {
142
+ return false;
143
+ }
144
+ // If either is true, or both are undefined (PowerShell case), assume interactive
145
+ // This allows inquirer to attempt to use prompts
146
+ // Inquirer will handle the actual TTY check internally
147
+ return stdinTTY !== false && stdoutTTY !== false;
148
+ }
149
+ /**
150
+ * Prompt for AI context generation options
151
+ */
152
+ async function promptAiContextOptions() {
153
+ // If not interactive, use defaults
154
+ if (!isInteractive()) {
155
+ console.log('Running in non-interactive mode, using default options...');
156
+ return {
157
+ cursorrules: true,
158
+ aiContext: true,
159
+ claudeContext: true,
160
+ claudeSkills: false,
161
+ language: 'both',
162
+ };
163
+ }
164
+ // Use inquirer with proper error handling
165
+ try {
166
+ const isEn = isEnglishOnly();
167
+ const answers = await inquirer_1.default.prompt([
168
+ {
169
+ type: 'checkbox',
170
+ name: 'options',
171
+ message: t('selectAiContext'),
172
+ choices: [
173
+ {
174
+ name: isEn ? '.cursorrules (Cursor IDE rules)' : '.cursorrules (Cursor IDE rules) / Cursor IDE 규칙',
175
+ value: 'cursorrules',
176
+ checked: true,
177
+ },
178
+ {
179
+ name: isEn ? 'ai-context.md (General AI context)' : 'ai-context.md (General AI context) / 범용 AI 컨텍스트',
180
+ value: 'aiContext',
181
+ checked: true,
182
+ },
183
+ {
184
+ name: isEn ? '.claude/project-context.md (Claude context)' : '.claude/project-context.md (Claude context) / Claude 컨텍스트',
185
+ value: 'claudeContext',
186
+ checked: true,
187
+ },
188
+ {
189
+ name: isEn ? '.claude/skills/ (Claude skills)' : '.claude/skills/ (Claude skills) / Claude 스킬',
190
+ value: 'claudeSkills',
191
+ checked: false,
192
+ },
193
+ ],
194
+ },
195
+ {
196
+ type: 'list',
197
+ name: 'language',
198
+ message: t('documentationLanguage'),
199
+ choices: [
200
+ { name: isEn ? 'Korean only' : 'Korean only / 한국어만', value: 'ko' },
201
+ { name: isEn ? 'English only' : 'English only / 영어만', value: 'en' },
202
+ { name: isEn ? 'Both Korean and English' : 'Both Korean and English / 한국어와 영어 모두', value: 'both' },
203
+ ],
204
+ default: 'both',
205
+ },
206
+ ]);
207
+ return {
208
+ cursorrules: answers.options.includes('cursorrules'),
209
+ aiContext: answers.options.includes('aiContext'),
210
+ claudeContext: answers.options.includes('claudeContext'),
211
+ claudeSkills: answers.options.includes('claudeSkills'),
212
+ language: answers.language || 'both',
213
+ };
214
+ }
215
+ catch (error) {
216
+ // If inquirer fails (e.g., in non-interactive environment), use defaults
217
+ console.warn('Failed to get interactive input, using default options...');
218
+ return {
219
+ cursorrules: true,
220
+ aiContext: true,
221
+ claudeContext: true,
222
+ claudeSkills: false,
223
+ language: 'both',
224
+ };
225
+ }
226
+ }
227
+ /**
228
+ * Copy template files to project directory
229
+ *
230
+ * @param projectPath - Target project directory
231
+ * @param options - Copy options
232
+ * @param options.skipAiContext - Skip AI context files (.cursorrules, ai-context.md, .claude/)
233
+ */
234
+ async function copyTemplate(projectPath, options) {
235
+ await fs.copy(TEMPLATE_DIR, projectPath, {
236
+ filter: (src) => {
237
+ // Skip node_modules and .git
238
+ if (src.includes('node_modules') || src.includes('.git')) {
239
+ return false;
240
+ }
241
+ // Conditionally skip AI context files
242
+ if (options?.skipAiContext) {
243
+ const relativePath = path.relative(TEMPLATE_DIR, src);
244
+ if (relativePath === '.cursorrules' ||
245
+ relativePath === 'ai-context.md' ||
246
+ relativePath.startsWith('.claude')) {
247
+ return false;
248
+ }
249
+ }
250
+ return true;
251
+ },
252
+ });
253
+ }
254
+ /**
255
+ * Get hua-ux package version
256
+ *
257
+ * 모노레포 내부에서는 workspace 버전을, 외부에서는 npm 버전을 사용
258
+ *
259
+ * 감지 우선순위:
260
+ * 1. 환경 변수 (HUA_UX_WORKSPACE_VERSION)
261
+ * 2. pnpm-workspace.yaml 파일 존재 여부 (더 견고한 방법)
262
+ * 3. 폴더 이름 기반 감지 (하위 호환성)
263
+ * 4. hua-ux 패키지의 package.json에서 버전 읽기 (자동화)
264
+ * 5. npm 버전 (기본값)
265
+ */
266
+ function getHuaUxVersion() {
267
+ // 1. 환경 변수 우선 확인
268
+ if (process.env.HUA_UX_WORKSPACE_VERSION === 'workspace') {
269
+ return 'workspace:*';
270
+ }
271
+ // 2. pnpm-workspace.yaml 파일 존재 여부로 모노레포 감지 (더 견고한 방법)
272
+ try {
273
+ const fs = require('fs');
274
+ const path = require('path');
275
+ let currentDir = process.cwd();
276
+ const maxDepth = 10; // 최대 10단계 상위 디렉토리까지 확인
277
+ for (let i = 0; i < maxDepth; i++) {
278
+ const workspaceFile = path.join(currentDir, 'pnpm-workspace.yaml');
279
+ if (fs.existsSync(workspaceFile)) {
280
+ return 'workspace:*';
281
+ }
282
+ const parentDir = path.dirname(currentDir);
283
+ if (parentDir === currentDir)
284
+ break; // 루트 도달
285
+ currentDir = parentDir;
286
+ }
287
+ }
288
+ catch (error) {
289
+ // fs 모듈을 사용할 수 없는 경우 (Edge Runtime 등) 무시
290
+ }
291
+ // 3. 하위 호환성: 폴더 이름 기반 감지 (기존 방식)
292
+ const cwd = process.cwd();
293
+ if (cwd.includes('hua-platform') && !cwd.includes('node_modules')) {
294
+ return 'workspace:*';
295
+ }
296
+ // 4. hua-ux 패키지의 package.json에서 버전 읽기 (자동화)
297
+ // create-hua-ux 패키지에서 hua-ux 패키지의 package.json을 읽어서 버전 추출
298
+ try {
299
+ const fs = require('fs');
300
+ const path = require('path');
301
+ // create-hua-ux의 위치에서 hua-ux 패키지 찾기
302
+ // __dirname은 dist/utils.js 또는 src/utils.ts의 위치
303
+ // dist/utils.js인 경우: packages/create-hua-ux/dist/utils.js
304
+ // src/utils.ts인 경우: packages/create-hua-ux/src/utils.ts
305
+ const currentFile = __dirname;
306
+ const createHuaUxRoot = path.resolve(currentFile, '../..');
307
+ const huaUxPackageJson = path.join(createHuaUxRoot, '../hua-ux/package.json');
308
+ if (fs.existsSync(huaUxPackageJson)) {
309
+ const huaUxPackage = JSON.parse(fs.readFileSync(huaUxPackageJson, 'utf-8'));
310
+ const version = huaUxPackage.version;
311
+ if (version) {
312
+ // 버전 앞에 ^ 추가 (예: 0.1.0 -> ^0.1.0)
313
+ return `^${version}`;
314
+ }
315
+ }
316
+ }
317
+ catch (error) {
318
+ // 파일을 읽을 수 없는 경우 무시하고 다음 단계로
319
+ }
320
+ // 5. 빌드 시점에 생성된 버전 상수 사용 (npm 배포 후)
321
+ // 빌드 스크립트에서 hua-ux 패키지의 버전을 읽어서 생성한 상수
322
+ return version_1.HUA_UX_VERSION;
323
+ }
324
+ /**
325
+ * Get hua-ux related package version
326
+ *
327
+ * hua-ux와 관련된 패키지들의 버전을 반환합니다.
328
+ * 모노레포 내부에서는 workspace 버전을, 외부에서는 npm 버전을 사용합니다.
329
+ */
330
+ function getHuaUxRelatedPackageVersion() {
331
+ return getHuaUxVersion();
332
+ }
333
+ /**
334
+ * Generate package.json
335
+ */
336
+ async function generatePackageJson(projectPath, projectName) {
337
+ const packageJsonPath = path.join(projectPath, 'package.json');
338
+ // 기존 package.json이 있다면 삭제 (템플릿에서 복사된 파일이 있을 수 있음)
339
+ if (await fs.pathExists(packageJsonPath)) {
340
+ await fs.remove(packageJsonPath);
341
+ }
342
+ const packageJson = {
343
+ name: projectName,
344
+ version: '0.1.0',
345
+ private: true,
346
+ scripts: {
347
+ dev: 'next dev --turbopack',
348
+ build: 'next build',
349
+ start: 'next start',
350
+ lint: "next lint",
351
+ 'lint:fix': 'next lint --fix',
352
+ },
353
+ dependencies: {
354
+ '@hua-labs/hua-ux': getHuaUxVersion(),
355
+ '@hua-labs/i18n-core-zustand': getHuaUxRelatedPackageVersion(),
356
+ '@hua-labs/state': getHuaUxRelatedPackageVersion(),
357
+ next: versions_1.NEXTJS_VERSION,
358
+ react: versions_1.REACT_VERSION,
359
+ 'react-dom': versions_1.REACT_DOM_VERSION,
360
+ zustand: versions_1.ZUSTAND_VERSION,
361
+ },
362
+ devDependencies: {
363
+ '@types/node': versions_1.TYPES_NODE_VERSION,
364
+ '@types/react': versions_1.TYPES_REACT_VERSION,
365
+ '@types/react-dom': versions_1.TYPES_REACT_DOM_VERSION,
366
+ '@tailwindcss/postcss': versions_1.TAILWIND_POSTCSS_VERSION,
367
+ autoprefixer: versions_1.AUTOPREFIXER_VERSION,
368
+ postcss: versions_1.POSTCSS_VERSION,
369
+ tailwindcss: versions_1.TAILWIND_VERSION,
370
+ typescript: versions_1.TYPESCRIPT_VERSION,
371
+ },
372
+ };
373
+ await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
374
+ }
375
+ /**
376
+ * Generate hua-ux.config.ts
377
+ */
378
+ async function generateConfig(projectPath) {
379
+ const configContent = `import { defineConfig } from '@hua-labs/hua-ux/framework';
380
+
381
+ /**
382
+ * hua-ux 프레임워크 설정
383
+ *
384
+ * Preset을 선택하면 대부분의 설정이 자동으로 적용됩니다.
385
+ * - 'product': 제품 페이지용 (전문적, 효율적)
386
+ * - 'marketing': 마케팅 페이지용 (화려함, 눈에 띄는)
387
+ *
388
+ * **바이브 모드 (간단)**: \`preset: 'product'\`
389
+ * **개발자 모드 (세부 설정)**: \`preset: { type: 'product', motion: {...} }\`
390
+ */
391
+ export default defineConfig({
392
+ /**
393
+ * 프리셋 선택
394
+ *
395
+ * Preset을 선택하면 motion, spacing, i18n 등이 자동 설정됩니다.
396
+ *
397
+ * 바이브 모드 (간단):
398
+ * preset: 'product'
399
+ *
400
+ * 개발자 모드 (세부 설정):
401
+ * preset: {
402
+ * type: 'product',
403
+ * motion: { duration: 300 },
404
+ * }
405
+ */
406
+ preset: 'product',
407
+
408
+ /**
409
+ * 다국어 설정
410
+ */
411
+ i18n: {
412
+ defaultLanguage: 'ko',
413
+ supportedLanguages: ['ko', 'en'],
414
+ namespaces: ['common'],
415
+ translationLoader: 'api',
416
+ translationApiPath: '/api/translations',
417
+ },
418
+
419
+ /**
420
+ * 모션/애니메이션 설정
421
+ *
422
+ * 바이브 코더용 (명사 중심):
423
+ * motion: { style: 'smooth' } // 'smooth' | 'dramatic' | 'minimal'
424
+ *
425
+ * 개발자용 (기술적):
426
+ * motion: {
427
+ * defaultPreset: 'product',
428
+ * enableAnimations: true,
429
+ * duration: 300,
430
+ * }
431
+ */
432
+ motion: {
433
+ defaultPreset: 'product',
434
+ enableAnimations: true,
435
+ // style: 'smooth', // 바이브 코더용: 'smooth' | 'dramatic' | 'minimal'
436
+ },
437
+
438
+ /**
439
+ * 상태 관리 설정
440
+ */
441
+ state: {
442
+ persist: true,
443
+ ssr: true,
444
+ },
445
+
446
+ /**
447
+ * 브랜딩 설정 (화이트 라벨링)
448
+ *
449
+ * 색상, 타이포그래피 등을 설정하면 모든 컴포넌트에 자동 적용됩니다.
450
+ *
451
+ * branding: {
452
+ * colors: {
453
+ * primary: '#3B82F6',
454
+ * secondary: '#8B5CF6',
455
+ * },
456
+ * }
457
+ */
458
+ // branding: {
459
+ // colors: {
460
+ // primary: '#3B82F6',
461
+ // },
462
+ // },
463
+
464
+ /**
465
+ * 라이선스 설정 (Pro/Enterprise 플러그인 사용 시)
466
+ *
467
+ * license: {
468
+ * apiKey: process.env.HUA_UX_LICENSE_KEY,
469
+ * }
470
+ */
471
+ // license: {
472
+ // apiKey: process.env.HUA_UX_LICENSE_KEY,
473
+ // },
474
+
475
+ /**
476
+ * 플러그인 설정 (Pro/Enterprise 기능)
477
+ *
478
+ * plugins: [
479
+ * motionProPlugin,
480
+ * i18nProPlugin,
481
+ * ]
482
+ */
483
+ // plugins: [],
484
+ });
485
+ `;
486
+ await fs.writeFile(path.join(projectPath, 'hua-ux.config.ts'), configContent);
487
+ }
488
+ /**
489
+ * Generate AI context files
490
+ *
491
+ * Cursor, Claude 등 다양한 AI 도구를 위한 컨텍스트 파일 생성
492
+ * 템플릿 파일을 복사한 후 프로젝트별 정보를 동적으로 추가합니다.
493
+ */
494
+ async function generateAiContextFiles(projectPath, projectName, options) {
495
+ const opts = options || {
496
+ cursorrules: true,
497
+ aiContext: true,
498
+ claudeContext: true,
499
+ claudeSkills: false,
500
+ language: 'both',
501
+ };
502
+ // 옵션에 따라 파일 삭제 (생성하지 않을 파일)
503
+ if (!opts.cursorrules) {
504
+ const cursorrulesPath = path.join(projectPath, '.cursorrules');
505
+ if (await fs.pathExists(cursorrulesPath)) {
506
+ await fs.remove(cursorrulesPath);
507
+ }
508
+ }
509
+ if (!opts.aiContext) {
510
+ const aiContextPath = path.join(projectPath, 'ai-context.md');
511
+ if (await fs.pathExists(aiContextPath)) {
512
+ await fs.remove(aiContextPath);
513
+ }
514
+ }
515
+ if (!opts.claudeContext) {
516
+ const claudeContextPath = path.join(projectPath, '.claude', 'project-context.md');
517
+ if (await fs.pathExists(claudeContextPath)) {
518
+ await fs.remove(claudeContextPath);
519
+ }
520
+ }
521
+ if (!opts.claudeSkills) {
522
+ const claudeSkillsPath = path.join(projectPath, '.claude', 'skills');
523
+ if (await fs.pathExists(claudeSkillsPath)) {
524
+ await fs.remove(claudeSkillsPath);
525
+ }
526
+ }
527
+ // 프로젝트별 커스터마이징
528
+ if (projectName) {
529
+ // ai-context.md에 프로젝트 이름 추가
530
+ if (opts.aiContext) {
531
+ const aiContextPath = path.join(projectPath, 'ai-context.md');
532
+ if (await fs.pathExists(aiContextPath)) {
533
+ let content = await fs.readFile(aiContextPath, 'utf-8');
534
+ // Add project name to document header
535
+ content = content.replace(/^# hua-ux Project AI Context/, `# ${projectName} - hua-ux Project AI Context\n\n**Project Name**: ${projectName}`);
536
+ await fs.writeFile(aiContextPath, content, 'utf-8');
537
+ }
538
+ }
539
+ // .claude/project-context.md에도 프로젝트 이름 추가
540
+ if (opts.claudeContext) {
541
+ const claudeContextPath = path.join(projectPath, '.claude', 'project-context.md');
542
+ if (await fs.pathExists(claudeContextPath)) {
543
+ let content = await fs.readFile(claudeContextPath, 'utf-8');
544
+ content = content.replace(/^# hua-ux Project Context/, `# ${projectName} - hua-ux Project Context\n\n**Project Name**: ${projectName}`);
545
+ await fs.writeFile(claudeContextPath, content, 'utf-8');
546
+ }
547
+ }
548
+ }
549
+ // package.json에서 실제 설치된 패키지 버전 정보 추출하여 컨텍스트에 추가
550
+ const packageJsonPath = path.join(projectPath, 'package.json');
551
+ if (await fs.pathExists(packageJsonPath)) {
552
+ try {
553
+ const packageJson = await fs.readJSON(packageJsonPath);
554
+ const dependencies = packageJson.dependencies || {};
555
+ const devDependencies = packageJson.devDependencies || {};
556
+ // 버전 정보를 ai-context.md에 추가
557
+ if (opts.aiContext) {
558
+ const aiContextPath = path.join(projectPath, 'ai-context.md');
559
+ if (await fs.pathExists(aiContextPath)) {
560
+ let content = await fs.readFile(aiContextPath, 'utf-8');
561
+ // 의존성 정보 섹션 추가
562
+ const depsSection = `
563
+ ## 설치된 패키지 버전 / Installed Package Versions
564
+
565
+ ### 핵심 의존성 / Core Dependencies
566
+ ${Object.entries(dependencies)
567
+ .filter(([name]) => name.startsWith('@hua-labs/') || name === 'next' || name === 'react')
568
+ .map(([name, version]) => `- \`${name}\`: ${version}`)
569
+ .join('\n')}
570
+
571
+ ### 개발 의존성 / Dev Dependencies
572
+ ${Object.entries(devDependencies)
573
+ .filter(([name]) => name.includes('typescript') || name.includes('tailwind') || name.includes('@types'))
574
+ .map(([name, version]) => `- \`${name}\`: ${version}`)
575
+ .join('\n')}
576
+ `;
577
+ // 참고 자료 섹션 앞에 추가
578
+ content = content.replace(/## 참고 자료/, `${depsSection}\n## 참고 자료`);
579
+ await fs.writeFile(aiContextPath, content, 'utf-8');
580
+ }
581
+ }
582
+ }
583
+ catch (error) {
584
+ // package.json 파싱 실패 시 무시 (선택적 기능)
585
+ console.warn('Failed to extract package versions for AI context:', error);
586
+ }
587
+ }
588
+ }
589
+ /**
590
+ * Check prerequisites before project creation
591
+ *
592
+ * Verifies Node.js version, pnpm installation, and template integrity
593
+ */
594
+ async function checkPrerequisites() {
595
+ const isEn = isEnglishOnly();
596
+ const errors = [];
597
+ const warnings = [];
598
+ // 1. Node.js version check
599
+ const nodeVersion = process.version;
600
+ const requiredVersion = '18.0.0';
601
+ // Simple version comparison (major.minor.patch)
602
+ const parseVersion = (v) => {
603
+ return v.replace(/^v/, '').split('.').map(Number);
604
+ };
605
+ const compareVersions = (v1, v2) => {
606
+ const v1Parts = parseVersion(v1);
607
+ const v2Parts = parseVersion(v2);
608
+ for (let i = 0; i < 3; i++) {
609
+ if (v1Parts[i] > v2Parts[i])
610
+ return 1;
611
+ if (v1Parts[i] < v2Parts[i])
612
+ return -1;
613
+ }
614
+ return 0;
615
+ };
616
+ if (compareVersions(nodeVersion, requiredVersion) < 0) {
617
+ errors.push(isEn
618
+ ? `Node.js ${requiredVersion}+ required. Current: ${nodeVersion}`
619
+ : `Node.js ${requiredVersion}+ 필요합니다. 현재: ${nodeVersion}`);
620
+ }
621
+ // 2. pnpm installation check
622
+ try {
623
+ (0, child_process_1.execSync)('pnpm --version', { stdio: 'ignore' });
624
+ }
625
+ catch {
626
+ errors.push(isEn
627
+ ? 'pnpm is required. Install: npm install -g pnpm'
628
+ : 'pnpm이 필요합니다. 설치: npm install -g pnpm');
629
+ }
630
+ // 3. Template validation
631
+ try {
632
+ await validateTemplate();
633
+ }
634
+ catch (error) {
635
+ errors.push(isEn
636
+ ? `Template validation failed: ${error instanceof Error ? error.message : String(error)}`
637
+ : `템플릿 검증 실패: ${error instanceof Error ? error.message : String(error)}`);
638
+ }
639
+ // Display warnings
640
+ if (warnings.length > 0) {
641
+ console.log(chalk_1.default.yellow('\n⚠️ Warnings:'));
642
+ warnings.forEach(w => console.log(chalk_1.default.yellow(` - ${w}`)));
643
+ }
644
+ // Throw error if prerequisites not met
645
+ if (errors.length > 0) {
646
+ const errorMessage = isEn
647
+ ? `Prerequisites check failed:\n${errors.map(e => ` ❌ ${e}`).join('\n')}\n\n💡 Tips:\n - Update Node.js: https://nodejs.org/\n - Install pnpm: npm install -g pnpm`
648
+ : `사전 검증 실패:\n${errors.map(e => ` ❌ ${e}`).join('\n')}\n\n💡 팁:\n - Node.js 업데이트: https://nodejs.org/\n - pnpm 설치: npm install -g pnpm`;
649
+ throw new Error(errorMessage);
650
+ }
651
+ }
652
+ /**
653
+ * Validate template files integrity
654
+ *
655
+ * Checks if all required template files exist before project creation
656
+ */
657
+ async function validateTemplate() {
658
+ // Check if template directory exists
659
+ if (!(await fs.pathExists(TEMPLATE_DIR))) {
660
+ const isEn = isEnglishOnly();
661
+ throw new Error(isEn
662
+ ? `Template directory not found: ${TEMPLATE_DIR}`
663
+ : `템플릿 디렉토리를 찾을 수 없습니다: ${TEMPLATE_DIR}`);
664
+ }
665
+ // Note: package.json is generated dynamically, not in template
666
+ const requiredFiles = [
667
+ 'tsconfig.json',
668
+ 'next.config.ts',
669
+ 'tailwind.config.js',
670
+ 'app/layout.tsx',
671
+ 'app/page.tsx',
672
+ 'app/globals.css',
673
+ 'lib/i18n-setup.ts',
674
+ 'store/useAppStore.ts',
675
+ 'translations/ko/common.json',
676
+ 'translations/en/common.json',
677
+ ];
678
+ const missingFiles = [];
679
+ for (const file of requiredFiles) {
680
+ const filePath = path.join(TEMPLATE_DIR, file);
681
+ if (!(await fs.pathExists(filePath))) {
682
+ missingFiles.push(file);
683
+ }
684
+ }
685
+ if (missingFiles.length > 0) {
686
+ const isEn = isEnglishOnly();
687
+ throw new Error(isEn
688
+ ? `Template files missing: ${missingFiles.join(', ')}`
689
+ : `템플릿 파일 누락: ${missingFiles.join(', ')}`);
690
+ }
691
+ }
692
+ /**
693
+ * Validate generated project
694
+ *
695
+ * 프로젝트 생성 후 필수 파일과 설정이 올바르게 생성되었는지 검증
696
+ */
697
+ async function validateGeneratedProject(projectPath) {
698
+ const errors = [];
699
+ // 1. package.json 검증
700
+ const packageJsonPath = path.join(projectPath, 'package.json');
701
+ if (!(await fs.pathExists(packageJsonPath))) {
702
+ const isEn = isEnglishOnly();
703
+ errors.push(isEn ? 'package.json file was not created' : 'package.json 파일이 생성되지 않았습니다.');
704
+ }
705
+ else {
706
+ try {
707
+ const packageJson = await fs.readJSON(packageJsonPath);
708
+ // lint 스크립트 검증
709
+ if (packageJson.scripts?.lint !== 'next lint') {
710
+ errors.push(`package.json의 lint 스크립트가 올바르지 않습니다. 예상: "next lint", 실제: "${packageJson.scripts?.lint}"`);
711
+ }
712
+ // 필수 의존성 검증
713
+ const requiredDeps = ['@hua-labs/hua-ux', 'next', 'react', 'react-dom'];
714
+ for (const dep of requiredDeps) {
715
+ if (!packageJson.dependencies?.[dep]) {
716
+ errors.push(`필수 의존성 ${dep}이 package.json에 없습니다.`);
717
+ }
718
+ }
719
+ }
720
+ catch (error) {
721
+ errors.push(`package.json 파싱 실패: ${error}`);
722
+ }
723
+ }
724
+ // 2. hua-ux.config.ts 검증
725
+ const configPath = path.join(projectPath, 'hua-ux.config.ts');
726
+ if (!(await fs.pathExists(configPath))) {
727
+ const isEn = isEnglishOnly();
728
+ errors.push(isEn ? 'hua-ux.config.ts file was not created' : 'hua-ux.config.ts 파일이 생성되지 않았습니다.');
729
+ }
730
+ // 3. 필수 디렉토리 검증
731
+ const requiredDirs = ['app', 'lib', 'store', 'translations'];
732
+ for (const dir of requiredDirs) {
733
+ const dirPath = path.join(projectPath, dir);
734
+ if (!(await fs.pathExists(dirPath))) {
735
+ const isEn = isEnglishOnly();
736
+ errors.push(isEn ? `Required directory ${dir} was not created` : `필수 디렉토리 ${dir}가 생성되지 않았습니다.`);
737
+ }
738
+ }
739
+ // 4. 필수 파일 검증
740
+ const requiredFiles = [
741
+ 'app/layout.tsx',
742
+ 'app/page.tsx',
743
+ 'tsconfig.json',
744
+ 'next.config.ts',
745
+ ];
746
+ for (const file of requiredFiles) {
747
+ const filePath = path.join(projectPath, file);
748
+ if (!(await fs.pathExists(filePath))) {
749
+ const isEn = isEnglishOnly();
750
+ errors.push(isEn ? `Required file ${file} was not created` : `필수 파일 ${file}이 생성되지 않았습니다.`);
751
+ }
752
+ }
753
+ // 에러가 있으면 예외 발생
754
+ if (errors.length > 0) {
755
+ const isEn = isEnglishOnly();
756
+ throw new Error(isEn
757
+ ? `Project validation failed:\n${errors.map(e => ` ❌ ${e}`).join('\n')}\n\n💡 Tips:\n - Check file permissions\n - Ensure disk space is available\n - Try running again`
758
+ : `프로젝트 검증 실패:\n${errors.map(e => ` ❌ ${e}`).join('\n')}\n\n💡 팁:\n - 파일 권한 확인\n - 디스크 공간 확인\n - 다시 실행해보세요`);
759
+ }
760
+ }
761
+ /**
762
+ * Validate translation files JSON syntax
763
+ */
764
+ async function validateTranslationFiles(projectPath) {
765
+ const translationFiles = [
766
+ 'translations/ko/common.json',
767
+ 'translations/en/common.json',
768
+ ];
769
+ const errors = [];
770
+ const isEn = isEnglishOnly();
771
+ for (const file of translationFiles) {
772
+ const filePath = path.join(projectPath, file);
773
+ if (await fs.pathExists(filePath)) {
774
+ try {
775
+ const content = await fs.readFile(filePath, 'utf-8');
776
+ JSON.parse(content);
777
+ }
778
+ catch (error) {
779
+ if (error instanceof SyntaxError) {
780
+ errors.push(isEn
781
+ ? `Invalid JSON in ${file}: ${error.message}`
782
+ : `${file}의 JSON 문법 오류: ${error.message}`);
783
+ }
784
+ else {
785
+ errors.push(isEn
786
+ ? `Failed to read ${file}: ${error instanceof Error ? error.message : String(error)}`
787
+ : `${file} 읽기 실패: ${error instanceof Error ? error.message : String(error)}`);
788
+ }
789
+ }
790
+ }
791
+ }
792
+ if (errors.length > 0) {
793
+ throw new Error(isEn
794
+ ? `Translation files validation failed:\n${errors.map(e => ` ❌ ${e}`).join('\n')}`
795
+ : `번역 파일 검증 실패:\n${errors.map(e => ` ❌ ${e}`).join('\n')}`);
796
+ }
797
+ }
798
+ /**
799
+ * Generate installation summary
800
+ */
801
+ async function generateSummary(projectPath, aiContextOptions) {
802
+ let directories = 0;
803
+ let files = 0;
804
+ const countItems = async (dirPath) => {
805
+ try {
806
+ const items = await fs.readdir(dirPath, { withFileTypes: true });
807
+ for (const item of items) {
808
+ // Skip hidden files and common ignore patterns
809
+ if (item.name.startsWith('.') && item.name !== '.cursorrules' && !item.name.startsWith('.claude')) {
810
+ continue;
811
+ }
812
+ if (item.name === 'node_modules' || item.name === '.git') {
813
+ continue;
814
+ }
815
+ const itemPath = path.join(dirPath, item.name);
816
+ if (item.isDirectory()) {
817
+ directories++;
818
+ await countItems(itemPath);
819
+ }
820
+ else {
821
+ files++;
822
+ }
823
+ }
824
+ }
825
+ catch (error) {
826
+ // Ignore permission errors or other issues
827
+ }
828
+ };
829
+ await countItems(projectPath);
830
+ const aiContextFiles = [];
831
+ if (aiContextOptions) {
832
+ if (aiContextOptions.cursorrules)
833
+ aiContextFiles.push('.cursorrules');
834
+ if (aiContextOptions.aiContext)
835
+ aiContextFiles.push('ai-context.md');
836
+ if (aiContextOptions.claudeContext)
837
+ aiContextFiles.push('.claude/project-context.md');
838
+ if (aiContextOptions.claudeSkills)
839
+ aiContextFiles.push('.claude/skills/');
840
+ }
841
+ const languages = [];
842
+ if (aiContextOptions?.language === 'ko' || aiContextOptions?.language === 'both') {
843
+ languages.push('ko');
844
+ }
845
+ if (aiContextOptions?.language === 'en' || aiContextOptions?.language === 'both') {
846
+ languages.push('en');
847
+ }
848
+ return {
849
+ directories,
850
+ files,
851
+ aiContextFiles,
852
+ languages,
853
+ };
854
+ }
855
+ /**
856
+ * Display installation summary
857
+ */
858
+ function displaySummary(summary) {
859
+ const isEn = isEnglishOnly();
860
+ console.log(chalk_1.default.cyan('\n📊 Summary:'));
861
+ console.log(chalk_1.default.white(` 📁 Directories: ${summary.directories}`));
862
+ console.log(chalk_1.default.white(` 📄 Files: ${summary.files}`));
863
+ if (summary.aiContextFiles.length > 0) {
864
+ console.log(chalk_1.default.white(` 🤖 AI Context: ${summary.aiContextFiles.join(', ')}`));
865
+ }
866
+ else {
867
+ console.log(chalk_1.default.gray(` 🤖 AI Context: None`));
868
+ }
869
+ if (summary.languages.length > 0) {
870
+ console.log(chalk_1.default.white(` 🌐 Languages: ${summary.languages.join(', ')}`));
871
+ }
872
+ }
873
+ /**
874
+ * Display next steps with customized guidance
875
+ */
876
+ function displayNextSteps(projectPath, aiContextOptions) {
877
+ const isEn = isEnglishOnly();
878
+ const relativePath = path.relative(process.cwd(), projectPath);
879
+ const displayPath = relativePath || path.basename(projectPath);
880
+ console.log(chalk_1.default.cyan(`\n📚 Next Steps:`));
881
+ console.log(chalk_1.default.white(` cd ${displayPath}`));
882
+ console.log(chalk_1.default.white(` pnpm install`));
883
+ console.log(chalk_1.default.white(` pnpm dev`));
884
+ if (aiContextOptions?.claudeSkills) {
885
+ console.log(chalk_1.default.cyan(`\n💡 Claude Skills enabled:`));
886
+ console.log(chalk_1.default.white(isEn
887
+ ? ' Check .claude/skills/ for framework usage guide'
888
+ : ' .claude/skills/에서 프레임워크 사용 가이드를 확인하세요'));
889
+ }
890
+ if (aiContextOptions?.language === 'both') {
891
+ console.log(chalk_1.default.cyan(`\n🌐 Bilingual mode:`));
892
+ console.log(chalk_1.default.white(isEn
893
+ ? ' Edit translations/ko/ and translations/en/ for your content'
894
+ : ' translations/ko/와 translations/en/에서 번역을 수정하세요'));
895
+ }
896
+ }