@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.
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/bin/create-hua-ux.d.ts +9 -0
- package/dist/bin/create-hua-ux.d.ts.map +1 -0
- package/dist/bin/create-hua-ux.js +37 -0
- package/dist/constants/versions.d.ts +55 -0
- package/dist/constants/versions.d.ts.map +1 -0
- package/dist/constants/versions.js +57 -0
- package/dist/create-project.d.ts +18 -0
- package/dist/create-project.d.ts.map +1 -0
- package/dist/create-project.js +237 -0
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +259 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +177 -0
- package/dist/utils.d.ts +108 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +896 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/package.json +46 -0
- package/templates/nextjs/.claude/project-context.md +310 -0
- package/templates/nextjs/.claude/skills/hua-ux-framework/SKILL.md +187 -0
- package/templates/nextjs/.cursorrules +302 -0
- package/templates/nextjs/.eslintrc.json +1 -0
- package/templates/nextjs/README.md +431 -0
- package/templates/nextjs/ai-context.md +332 -0
- package/templates/nextjs/app/api/translations/[language]/[namespace]/route.ts +86 -0
- package/templates/nextjs/app/globals.css +24 -0
- package/templates/nextjs/app/layout-with-geo.example.tsx +106 -0
- package/templates/nextjs/app/layout.tsx +30 -0
- package/templates/nextjs/app/page-with-geo.example.tsx +80 -0
- package/templates/nextjs/app/page.tsx +28 -0
- package/templates/nextjs/components/I18nProviderWrapper.tsx +19 -0
- package/templates/nextjs/lib/i18n-setup.ts +11 -0
- package/templates/nextjs/middleware.ts.example +22 -0
- package/templates/nextjs/next.config.ts +36 -0
- package/templates/nextjs/postcss.config.js +6 -0
- package/templates/nextjs/store/useAppStore.ts +8 -0
- package/templates/nextjs/tailwind.config.js +8 -0
- package/templates/nextjs/translations/en/common.json +6 -0
- package/templates/nextjs/translations/ko/common.json +6 -0
- 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
|
+
}
|