gencow 0.1.120 → 0.1.122

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/bin/gencow.mjs CHANGED
@@ -105,7 +105,7 @@ function validatePlatformUrl(url) {
105
105
  // 그 외는 반드시 https://
106
106
  if (!url.startsWith("https://")) {
107
107
  error(`platformUrl must use https:// (got: ${url})`);
108
- info("시크릿이 평문으로 전송될 있습니다. GENCOW_PLATFORM_URL을 확인하세요.");
108
+ info("Secrets may be transmitted in plaintext. Check GENCOW_PLATFORM_URL.");
109
109
  process.exit(1);
110
110
  }
111
111
  return url;
@@ -275,7 +275,7 @@ function _ensureEsbuildConsistency() {
275
275
  if (hostVersion === binaryVersion) return (_esbuildEnvCache = {});
276
276
 
277
277
  // ── 불일치 감지 → 2단계 수정 ──
278
- warn(`esbuild 버전 불일치 감지: host=${hostVersion}, binary=${binaryVersion}`);
278
+ warn(`esbuild version mismatch detected: host=${hostVersion}, binary=${binaryVersion}`);
279
279
 
280
280
  const env = {};
281
281
 
@@ -290,7 +290,7 @@ function _ensureEsbuildConsistency() {
290
290
  const binary = resolve(platformPkgDir, "bin/esbuild");
291
291
  if (existsSync(binary)) {
292
292
  env.ESBUILD_BINARY_PATH = binary;
293
- info(`ESBUILD_BINARY_PATH → esbuild@${esbuildVer} 바이너리 사용`);
293
+ info(`ESBUILD_BINARY_PATH → using esbuild@${esbuildVer} binary`);
294
294
  }
295
295
  } catch { /* platform binary resolve 실패 — fallback 없이 계속 */ }
296
296
  } catch { /* drizzle-kit esbuild resolve 실패 */ }
@@ -326,8 +326,8 @@ function _patchNpmrcForEsbuild(cwd) {
326
326
  "",
327
327
  ].join("\n");
328
328
  appendFileSync(pnpmrcPath, patch);
329
- info(".pnpmrc에 esbuild hoisting 차단 규칙을 추가했습니다.");
330
- info("다음 pnpm install 영구 적용됩니다.");
329
+ info("Added esbuild hoisting exclusion rule to .pnpmrc.");
330
+ info("Permanent fix will apply on next pnpm install.");
331
331
  }
332
332
  return;
333
333
  }
@@ -362,7 +362,7 @@ function findServerRoot() {
362
362
  const siblingServer = resolve(__dirname, "../../server");
363
363
  if (existsSync(siblingServer)) return siblingServer;
364
364
 
365
- error("서버 런타임을 찾을 없습니다. bun install을 실행하세요.");
365
+ error("Server runtime not found. Run 'bun install'.");
366
366
  process.exit(1);
367
367
  }
368
368
 
@@ -472,7 +472,7 @@ process.exit(0);
472
472
  const stderr = execError.stderr?.toString() || "";
473
473
  const stdout = execError.stdout?.toString() || "";
474
474
 
475
- error("API Codegen failed — gencow/index.ts를 파싱할 수 없습니다.");
475
+ error("API Codegen failed — cannot parse gencow/index.ts.");
476
476
  log("");
477
477
 
478
478
  // stderr에서 핵심 에러 줄 추출 (타입 에러, import 에러 등)
@@ -486,7 +486,7 @@ process.exit(0);
486
486
  ).slice(0, 5); // 최대 5줄
487
487
 
488
488
  if (errorLines.length > 0) {
489
- error("원인:");
489
+ error("Cause:");
490
490
  for (const line of errorLines) {
491
491
  info(` ${RED}${line.trim()}${RESET}`);
492
492
  }
@@ -499,16 +499,16 @@ process.exit(0);
499
499
  }
500
500
 
501
501
  log("");
502
- info(`${YELLOW}해결 방법:${RESET}`);
503
- info(" 1. gencow/index.ts에서 TypeScript 에러를 수정하세요");
504
- info(" 2. import 경로가 올바른지 확인하세요");
505
- info(" 3. @gencow/core 패키지가 설치되어 있는지 확인하세요");
502
+ info(`${YELLOW}How to fix:${RESET}`);
503
+ info(" 1. Fix TypeScript errors in gencow/index.ts");
504
+ info(" 2. Verify import paths are correct");
505
+ info(" 3. Ensure @gencow/core is installed");
506
506
  log("");
507
507
 
508
508
  // 빈 스텁 api.ts 생성 — IDE가 즉시 에러를 잡을 수 있도록
509
- const stubContent = `/**\n * ⚠️ API Codegen Failed\n *\n * gencow/index.ts에서 에러가 발생하여 api.ts 생성할 없었습니다.\n * 콘솔의 에러 메시지를 확인하고 수정한 gencow dev를 재시작하세요.\n *\n * 파일은 자동 생성됩니다직접 수정하지 마세요.\n */\nexport const api = {} as const;\n`;
509
+ const stubContent = `/**\n * ⚠️ API Codegen Failed\n *\n * An error occurred in gencow/index.ts, so api.ts could not be generated.\n * Check the console for error details, fix the issue, and restart gencow dev.\n *\n * This file is auto-generateddo not edit manually.\n */\nexport const api = {} as const;\n`;
510
510
  writeFileSync(apiTsPath, stubContent);
511
- warn(`빈 스텁 ${config.functionsDir}/api.ts 생성됨 (에러 수정 후 자동 재생성)`);
511
+ warn(`Empty stub ${config.functionsDir}/api.ts created (auto-regenerated after fix)`);
512
512
  return; // 에러를 throw하지 않고 조용히 반환 — dev 서버는 계속 실행
513
513
  }
514
514
 
@@ -595,9 +595,9 @@ process.exit(0);
595
595
  } catch (e) {
596
596
  error(`API Codegen failed: ${e.message}`);
597
597
  // 빈 스텁 api.ts 생성 — 예기치 않은 에러에서도 IDE가 즉시 에러를 잡을 수 있도록
598
- const stubContent = `/**\n * ⚠️ API Codegen Failed: ${e.message.replace(/\*/g, "")}\n *\n * 에러를 수정한 gencow dev를 재시작하세요.\n * 파일은 자동 생성됩니다직접 수정하지 마세요.\n */\nexport const api = {} as const;\n`;
598
+ const stubContent = `/**\n * ⚠️ API Codegen Failed: ${e.message.replace(/\*/g, "")}\n *\n * Fix the error and restart gencow dev.\n * This file is auto-generateddo not edit manually.\n */\nexport const api = {} as const;\n`;
599
599
  try { writeFileSync(apiTsPath, stubContent); } catch { /* ignore */ }
600
- warn(`빈 스텁 ${config.functionsDir}/api.ts 생성됨`);
600
+ warn(`Empty stub ${config.functionsDir}/api.ts created`);
601
601
  } finally {
602
602
  try { unlinkSync(extractTsPath); } catch { }
603
603
  }
@@ -654,11 +654,11 @@ function updateEnvLocalUrl(deployUrl) {
654
654
  }
655
655
 
656
656
  writeFileSync(envPath, content);
657
- success(`.env ${envKey} 자동 설정됨`);
658
- info(` ${DIM}배포 플랫폼(Vercel ) 환경변수에도 추가하세요:${RESET}`);
657
+ success(`.env — auto-configured ${envKey}`);
658
+ info(` ${DIM}Also add to your deploy platform (Vercel, etc.) env vars:${RESET}`);
659
659
  info(` ${envKey}=${deployUrl}`);
660
660
  } catch (e) {
661
- warn(`.env 업데이트 실패: ${e.message}`);
661
+ warn(`.env update failed: ${e.message}`);
662
662
  }
663
663
  }
664
664
 
@@ -710,13 +710,13 @@ const commands = {
710
710
  const { createInterface } = await import("readline");
711
711
  const rl = createInterface({ input: process.stdin, output: process.stdout });
712
712
  name = await new Promise(resolve => {
713
- rl.question(`\n ${DIM}프로젝트 이름을 입력하세요 (현재 폴더: .):${RESET} `, answer => {
713
+ rl.question(`\n ${DIM}Enter project name (current folder: .):${RESET} `, answer => {
714
714
  rl.close();
715
715
  resolve(answer.trim());
716
716
  });
717
717
  });
718
718
  if (!name) {
719
- error("프로젝트 이름이 필요합니다.");
719
+ error("Project name is required.");
720
720
  process.exit(1);
721
721
  }
722
722
  }
@@ -733,14 +733,14 @@ const commands = {
733
733
  if (!templateId) {
734
734
  templateId = await (async () => {
735
735
  const tpls = commands._templates;
736
- log(` ${DIM}템플릿을 선택하세요:${RESET}\n`);
736
+ log(` ${DIM}Choose a template:${RESET}\n`);
737
737
  tpls.forEach((t, i) =>
738
738
  log(` ${CYAN}${i + 1}.${RESET} ${t.id.padEnd(12)} ${DIM}— ${t.label}${RESET}`)
739
739
  );
740
740
  const { createInterface } = await import("readline");
741
741
  const rl = createInterface({ input: process.stdin, output: process.stdout });
742
742
  return new Promise(resolve => {
743
- rl.question(`\n 번호를 입력하세요 (1-${tpls.length}): `, answer => {
743
+ rl.question(`\n Enter number (1-${tpls.length}): `, answer => {
744
744
  rl.close();
745
745
  const idx = parseInt(answer) - 1;
746
746
  resolve(tpls[idx]?.id || "default");
@@ -756,7 +756,7 @@ const commands = {
756
756
  process.exit(1);
757
757
  }
758
758
 
759
- log(`\n ${DIM}▸ 템플릿:${RESET} ${CYAN}${template.id}${RESET} — ${template.label}\n`);
759
+ log(`\n ${DIM}▸ Template:${RESET} ${CYAN}${template.id}${RESET} — ${template.label}\n`);
760
760
 
761
761
  const projectDir = isCwd ? process.cwd() : resolve(process.cwd(), name);
762
762
 
@@ -768,7 +768,7 @@ const commands = {
768
768
  process.exit(1);
769
769
  }
770
770
  if (entries.length > 0 && force) {
771
- warn(`기존 파일 보존, gencow 파일만 생성/덮어쓰기합니다.`);
771
+ warn(`Existing files preserved, only gencow files created/overwritten.`);
772
772
  }
773
773
  }
774
774
 
@@ -800,7 +800,7 @@ const commands = {
800
800
  writeFileSync(pkgJsonPath, JSON.stringify(existing, null, 2) + "\n");
801
801
  success("Updated package.json (dependencies merged)");
802
802
  } catch {
803
- warn("package.json 파싱 실패 새로 생성합니다.");
803
+ warn("Failed to parse package.json creating new one.");
804
804
  writeFileSync(pkgJsonPath, JSON.stringify({
805
805
  name,
806
806
  version: "0.1.0",
@@ -847,7 +847,7 @@ const commands = {
847
847
  if (file === "prompt.md") {
848
848
  // prompt.md goes to project root
849
849
  cpSync(src, resolve(projectDir, "prompt.md"));
850
- success("Created prompt.md (바이브코딩 프롬프트)");
850
+ success("Created prompt.md (vibe coding prompt)");
851
851
  } else {
852
852
  // Everything else goes to gencow/
853
853
  cpSync(src, resolve(gencowDir, file));
@@ -866,7 +866,7 @@ const commands = {
866
866
  const securityMdSource = resolve(__dirname, "..", "templates", "SECURITY.md");
867
867
  if (existsSync(securityMdSource)) {
868
868
  cpSync(securityMdSource, resolve(gencowDir, "SECURITY.md"));
869
- success("Created gencow/SECURITY.md (보안 가이드)");
869
+ success("Created gencow/SECURITY.md (security guide)");
870
870
  }
871
871
 
872
872
  // 3.7. crons.ts scaffold (크론 스케줄러 — 빈 템플릿)
@@ -899,7 +899,7 @@ const crons = cronJobs();
899
899
 
900
900
  export default crons;
901
901
  `);
902
- success("Created gencow/crons.ts (크론 스케줄러 템플릿)");
902
+ success("Created gencow/crons.ts (cron scheduler template)");
903
903
  }
904
904
 
905
905
  // 3.7. auth.ts (auth 설정 — defineAuth 기반, shadcn 패턴으로 사용자 소유)
@@ -943,7 +943,7 @@ export default defineConfig({
943
943
  // 5. .env (preserve existing)
944
944
  const envPath = resolve(projectDir, ".env");
945
945
  if (existsSync(envPath) && force) {
946
- info(".env 이미 존재건너뜁니다.");
946
+ info(".env already existsskipping.");
947
947
  } else {
948
948
  const envContent = `# Gencow Environment Variables\n# Add your environment variables here\n\n# AI 기능 사용 시 필수 (ctx.ai.chat, ai.chat 등)\n# https://platform.openai.com/api-keys 에서 발급\nOPENAI_API_KEY=sk-your-key-here\n`;
949
949
  writeFileSync(envPath, envContent);
@@ -959,9 +959,9 @@ export default defineConfig({
959
959
  if (linesToAdd.length > 0) {
960
960
  const append = (existing.endsWith("\n") ? "" : "\n") + "# Gencow\n" + linesToAdd.join("\n") + "\n";
961
961
  writeFileSync(gitignorePath, existing + append);
962
- success("Updated .gitignore (gencow 항목 추가)");
962
+ success("Updated .gitignore (added gencow entries)");
963
963
  } else {
964
- info(".gitignore 이미 gencow 항목 포함 건너뜁니다.");
964
+ info(".gitignore already has gencow entriesskipping.");
965
965
  }
966
966
  } else {
967
967
  writeFileSync(gitignorePath, gencowGitignoreLines.join("\n") + "\n");
@@ -971,7 +971,7 @@ export default defineConfig({
971
971
  // 7. tsconfig.json (preserve existing)
972
972
  const tsconfigPath = resolve(projectDir, "tsconfig.json");
973
973
  if (existsSync(tsconfigPath) && force) {
974
- info("tsconfig.json 이미 존재건너뜁니다.");
974
+ info("tsconfig.json already existsskipping.");
975
975
  } else {
976
976
  writeFileSync(tsconfigPath, JSON.stringify({
977
977
  compilerOptions: {
@@ -1017,12 +1017,12 @@ export default defineConfig({
1017
1017
  success("Linked @gencow/core + dependencies");
1018
1018
 
1019
1019
  // 9. Auto install npm dependencies (drizzle-orm, ai, etc.)
1020
- info("의존성 설치 중...");
1020
+ info("Installing dependencies...");
1021
1021
  try {
1022
1022
  execSync("bun install", { cwd: projectDir, stdio: ["ignore", "pipe", "pipe"] });
1023
- success("의존성 설치 완료");
1023
+ success("Dependencies installed");
1024
1024
  } catch {
1025
- warn("패키지 설치 실패 — " + (isCwd ? "bun install" : "cd " + name + " && bun install") + " 직접 실행하세요");
1025
+ warn("Package install failed — " + (isCwd ? "bun install" : "cd " + name + " && bun install") + " to install manually");
1026
1026
  }
1027
1027
 
1028
1028
  // Build next steps message based on template
@@ -1030,20 +1030,20 @@ export default defineConfig({
1030
1030
  const hasPrompt = template.id !== "default";
1031
1031
 
1032
1032
  log(`
1033
- ${BOLD}${GREEN} ✅ 프로젝트 생성 완료!${RESET} ${DIM}(${template.id})${RESET}
1033
+ ${BOLD}${GREEN} ✅ Project created!${RESET} ${DIM}(${template.id})${RESET}
1034
1034
 
1035
- ${DIM}다음 단계:${RESET}
1035
+ ${DIM}Next steps:${RESET}
1036
1036
  ${isCwd ? "" : `
1037
1037
  ${CYAN}cd ${name}${RESET}`}
1038
- ${CYAN}gencow dev${RESET} ${DIM}← 개발 서버 시작${RESET}
1038
+ ${CYAN}gencow dev${RESET} ${DIM}← Start dev server${RESET}
1039
1039
  ${hasPrompt ? `
1040
- ${DIM}프론트엔드 바이브코딩:${RESET}
1041
- ${CYAN}prompt.md${RESET} ${DIM}파일을 AI에게 주세요 (Cursor, Copilot )${RESET}
1040
+ ${DIM}Frontend vibe coding:${RESET}
1041
+ ${CYAN}prompt.md${RESET} ${DIM}Feed this file to your AI (Cursor, Copilot, etc.)${RESET}
1042
1042
  ` : `
1043
- ${DIM}추가 기능:${RESET}
1044
- ${CYAN}gencow add AI${RESET} ${DIM}← AI 컴포넌트 추가${RESET}
1045
- ${CYAN}gencow add RAG${RESET} ${DIM}← RAG 컴포넌트 추가${RESET}
1046
- ${CYAN}gencow add Memory${RESET} ${DIM}← Agent Memory 추가${RESET}
1043
+ ${DIM}Add more:${RESET}
1044
+ ${CYAN}gencow add AI${RESET} ${DIM}← Add AI component${RESET}
1045
+ ${CYAN}gencow add RAG${RESET} ${DIM}← Add RAG component${RESET}
1046
+ ${CYAN}gencow add Memory${RESET} ${DIM}← Add Agent Memory${RESET}
1047
1047
  `}
1048
1048
  `);
1049
1049
  },
@@ -1059,9 +1059,9 @@ ${hasPrompt ? `
1059
1059
 
1060
1060
  if (!isLocal) {
1061
1061
  // Cloud reset은 미지원 — 안내 메시지
1062
- error("db:reset --local 플래그가 필요합니다.");
1063
- info(`${DIM}Cloud DB 초기화는 Dashboard에서만 가능합니다 (데이터 보호).${RESET}`);
1064
- info(`${DIM}로컬 DB 초기화하려면: gencow db:reset --local${RESET}`);
1062
+ error("db:reset requires --local flag.");
1063
+ info(`${DIM}Cloud DB reset is only available via Dashboard (data protection).${RESET}`);
1064
+ info(`${DIM}To reset local DB: gencow db:reset --local${RESET}`);
1065
1065
  log("");
1066
1066
  return;
1067
1067
  }
@@ -1096,14 +1096,14 @@ ${hasPrompt ? `
1096
1096
  return;
1097
1097
  }
1098
1098
 
1099
- success(`TRUNCATE 완료: ${data.truncated?.length || 0} tables`);
1099
+ success(`TRUNCATE done: ${data.truncated?.length || 0} tables`);
1100
1100
  if (data.truncated?.length > 0) {
1101
1101
  info(` ${DIM}${data.truncated.join(", ")}${RESET}`);
1102
1102
  }
1103
1103
  if (data.seeded) {
1104
- success(`Seed 실행 완료 ✓`);
1104
+ success(`Seed completed ✓`);
1105
1105
  } else {
1106
- info(`${DIM}seed.ts 없음건너뜀${RESET}`);
1106
+ info(`${DIM}No seed.ts foundskipping${RESET}`);
1107
1107
  }
1108
1108
  } catch (e) {
1109
1109
  error(`Server connection failed: ${e.message}`);
@@ -1125,9 +1125,9 @@ ${hasPrompt ? `
1125
1125
 
1126
1126
  // 3. Delete DB
1127
1127
  rmSync(dbPath, { recursive: true, force: true });
1128
- success("DB deleted — gencow dev 재시작하면 새로 생성됩니다");
1128
+ success("DB deleted — restart with gencow dev to create a new one");
1129
1129
  } else {
1130
- info("DB 아직 없습니다. gencow dev 시작하세요.");
1130
+ info("DB does not exist yet. Run gencow dev to get started.");
1131
1131
  }
1132
1132
  }
1133
1133
  log("");
@@ -1152,7 +1152,7 @@ ${hasPrompt ? `
1152
1152
  if (!statusRes.ok) throw new Error("Server not ready");
1153
1153
  } catch {
1154
1154
  error(`Server not running on :${port}`);
1155
- info(`${DIM}gencow dev --local 로 서버를 먼저 시작하세요.${RESET}`);
1155
+ info(`${DIM}Start server first: gencow dev --local${RESET}`);
1156
1156
  log("");
1157
1157
  return;
1158
1158
  }
@@ -1176,7 +1176,7 @@ ${hasPrompt ? `
1176
1176
  } else if (!res.ok) {
1177
1177
  error(`Seed failed: ${data.error || "Unknown error"}`);
1178
1178
  } else {
1179
- success("Seed 실행 완료 ✓");
1179
+ success("Seed completed ✓");
1180
1180
  }
1181
1181
  } catch (e) {
1182
1182
  error(`Server connection failed: ${e.message}`);
@@ -1186,6 +1186,7 @@ ${hasPrompt ? `
1186
1186
  }
1187
1187
 
1188
1188
  // ── Cloud 모드 (기본) ──
1189
+ const isProd = args.includes("--prod");
1189
1190
  const gencowJsonPath = resolve(process.cwd(), "gencow.json");
1190
1191
  let appId = null;
1191
1192
  let seedPlatformUrl = null;
@@ -1194,18 +1195,28 @@ ${hasPrompt ? `
1194
1195
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
1195
1196
  appId = gencowJson.appId || gencowJson.appName;
1196
1197
  seedPlatformUrl = gencowJson.platformUrl || null;
1198
+ // --prod: use prod app
1199
+ if (isProd && gencowJson.prodApp) {
1200
+ appId = gencowJson.prodApp;
1201
+ }
1197
1202
  } catch { /* ignore parse error */ }
1198
1203
  }
1199
1204
 
1205
+ if (isProd && appId && !appId.endsWith("-prod")) {
1206
+ error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
1207
+ return;
1208
+ }
1209
+
1200
1210
  if (!appId) {
1201
- error("Cloud app not found gencow.json 없거나 appId가 설정되지 않았습니다.");
1202
- info(`${DIM}먼저 gencow dev 실행하세요.${RESET}`);
1203
- info(`${DIM}로컬 서버에 seed하려면: gencow db:seed --local${RESET}`);
1211
+ error("Cloud app not found. Make sure gencow.json exists with an appId.");
1212
+ info(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
1213
+ info(`${DIM}To seed local DB: gencow db:seed --local${RESET}`);
1204
1214
  log("");
1205
1215
  return;
1206
1216
  }
1207
1217
 
1208
- log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
1218
+ const envLabel = isProd ? "prod" : "dev";
1219
+ log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(${envLabel}: ${appId})${RESET}\n`);
1209
1220
  info("Running seed.ts on cloud app...");
1210
1221
 
1211
1222
  const seedAppBaseUrl = getAppUrl(appId, seedPlatformUrl || "https://gencow.app");
@@ -1219,8 +1230,8 @@ ${hasPrompt ? `
1219
1230
 
1220
1231
  if (!statusRes || !statusRes.ok) {
1221
1232
  error(`Cloud app "${appId}" is not running`);
1222
- info(`${DIM}Dashboard → Apps → ${appId} → Resume 으로 앱을 시작하세요.${RESET}`);
1223
- info(`${DIM}또는: gencow dev 생성 후 시도하세요.${RESET}`);
1233
+ info(`${DIM}Dashboard → Apps → ${appId} → Resume to start the app.${RESET}`);
1234
+ info(`${DIM}Or: run gencow dev to create and deploy.${RESET}`);
1224
1235
  log("");
1225
1236
  return;
1226
1237
  }
@@ -1238,15 +1249,15 @@ ${hasPrompt ? `
1238
1249
  log(` ${DIM}export default async function seed(ctx) {${RESET}`);
1239
1250
  log(` ${DIM} await ctx.db.insert(tasks).values([...]);${RESET}`);
1240
1251
  log(` ${DIM}};${RESET}\n`);
1241
- info(`${DIM}After creating seed.ts: gencow dev 실행 gencow db:seed${RESET}`);
1252
+ info(`${DIM}After creating seed.ts: run gencow dev then gencow db:seed${RESET}`);
1242
1253
  } else if (!res.ok) {
1243
1254
  error(`Cloud seed failed: ${data.error || "Unknown error"}`);
1244
1255
  } else {
1245
- success("Cloud seed 실행 완료 ✓");
1256
+ success("Cloud seed completed ✓");
1246
1257
  }
1247
1258
  } catch (e) {
1248
1259
  error(`Cloud app connection failed: ${e.message}`);
1249
- info(`${DIM}앱이 실행 중인지 확인하세요: ${seedAppBaseUrl}${RESET}`);
1260
+ info(`${DIM}Check if the app is running: ${seedAppBaseUrl}${RESET}`);
1250
1261
  }
1251
1262
  log("");
1252
1263
  },
@@ -1261,7 +1272,7 @@ ${hasPrompt ? `
1261
1272
  log(`\n${BOLD}${CYAN}Gencow DB Restore${RESET}\n`);
1262
1273
 
1263
1274
  if (!existsSync(backupDir)) {
1264
- warn("백업이 없습니다.");
1275
+ warn("No backups found.");
1265
1276
  log("");
1266
1277
  return;
1267
1278
  }
@@ -1274,7 +1285,7 @@ ${hasPrompt ? `
1274
1285
  .reverse();
1275
1286
 
1276
1287
  if (backups.length === 0) {
1277
- warn("백업이 없습니다.");
1288
+ warn("No backups found.");
1278
1289
  log("");
1279
1290
  return;
1280
1291
  }
@@ -1295,7 +1306,7 @@ ${hasPrompt ? `
1295
1306
  }
1296
1307
  cpSync(latestPath, dbPath, { recursive: true });
1297
1308
  success(`Restored from: .gencow/backups/${latest}`);
1298
- info("gencow dev 로 재시작하세요.");
1309
+ info("Restart with gencow dev.");
1299
1310
  log("");
1300
1311
  },
1301
1312
 
@@ -1545,28 +1556,53 @@ ${hasPrompt ? `
1545
1556
  return;
1546
1557
  }
1547
1558
 
1548
- // ── Cloud 모드 (기본) ──
1559
+ // ── Cloud mode (default) ──
1560
+ const isProd = args.includes("--prod");
1549
1561
  const gencowJsonPath = resolve(process.cwd(), "gencow.json");
1550
1562
  let appId = null;
1551
1563
  if (existsSync(gencowJsonPath)) {
1552
1564
  try {
1553
1565
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
1554
1566
  appId = gencowJson.appId || gencowJson.appName;
1567
+ // --prod: use prod app
1568
+ if (isProd && gencowJson.prodApp) {
1569
+ appId = gencowJson.prodApp;
1570
+ }
1555
1571
  } catch { /* ignore parse error */ }
1556
1572
  }
1557
1573
 
1574
+ if (isProd && appId && !appId.endsWith("-prod")) {
1575
+ error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
1576
+ return;
1577
+ }
1578
+
1558
1579
  if (!appId) {
1559
- error("Cloud app not found gencow.json 없거나 appId가 설정되지 않았습니다.");
1560
- info(`${DIM}먼저 gencow dev 실행하세요.${RESET}`);
1561
- info(`${DIM}로컬 DB push하려면: gencow db:push --local${RESET}`);
1580
+ error("Cloud app not found. Make sure gencow.json exists with an appId.");
1581
+ info(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
1582
+ info(`${DIM}To push to local DB: gencow db:push --local${RESET}`);
1562
1583
  log("");
1563
1584
  return;
1564
1585
  }
1565
1586
 
1566
- // ── Cloud 모드: Platform API를 통해 Cloud DB에 schema push ──
1587
+ // ── Prod safety confirmation ──
1588
+ if (isProd) {
1589
+ const { createInterface } = await import("readline");
1590
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1591
+ const answer = await new Promise(res =>
1592
+ rl.question(`\n ${RED}!${RESET} Push schema to ${BOLD}PRODUCTION${RESET} database (${appId})?\n Type "yes" to confirm: `, res)
1593
+ );
1594
+ rl.close();
1595
+ if (answer.trim().toLowerCase() !== "yes") {
1596
+ warn("Cancelled.");
1597
+ return;
1598
+ }
1599
+ }
1600
+
1601
+ // ── Cloud mode: push schema via Platform API ──
1567
1602
  const creds = requireCreds();
1568
- log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
1569
- warn("명령어 실행 먼저 gencow dev 를 통해 최신 코드가 배포되어 있어야 합니다.");
1603
+ const envLabel = isProd ? "prod" : "dev";
1604
+ log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(${envLabel}: ${appId})${RESET}\n`);
1605
+ warn("Make sure the latest code is deployed before running this command.");
1570
1606
  log("");
1571
1607
  info("Pushing schema.ts → cloud database...");
1572
1608
 
@@ -1586,7 +1622,7 @@ ${hasPrompt ? `
1586
1622
  }
1587
1623
 
1588
1624
  // 🆕 로컬 drizzle-kit generate 실행 (프롬프트 패스스루)
1589
- info("스키마 마이그레이션 생성 중...");
1625
+ info("Generating schema migrations...");
1590
1626
  try {
1591
1627
  const dk = _drizzleKitCmd("generate");
1592
1628
  execSync(dk.cmd, {
@@ -1594,14 +1630,14 @@ ${hasPrompt ? `
1594
1630
  env: { ...process.env, ...dk.env },
1595
1631
  stdio: "inherit",
1596
1632
  });
1597
- success("마이그레이션 생성 완료");
1633
+ success("Migrations generated");
1598
1634
  } catch (e) {
1599
1635
  const msg = e.stderr?.toString() || e.message || "";
1600
1636
  if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
1601
- log(`${DIM} 스키마 변경 없음기존 마이그레이션 사용${RESET}`);
1637
+ log(`${DIM} No schema changesusing existing migrations${RESET}`);
1602
1638
  } else {
1603
- warn(`마이그레이션 생성 실패: ${msg.split("\n")[0]}`);
1604
- info("서버에서 기존 방식으로 스키마 적용을 시도합니다.");
1639
+ warn(`Migration generation failed: ${msg.split("\n")[0]}`);
1640
+ info("Will attempt schema push via server.");
1605
1641
  }
1606
1642
  }
1607
1643
 
@@ -1638,8 +1674,8 @@ ${hasPrompt ? `
1638
1674
  success("Schema pushed to cloud DB!");
1639
1675
  log(` ${DIM}Tables are in sync with schema.ts${RESET}\n`);
1640
1676
  } catch (e) {
1641
- error(`Platform 연결 실패: ${e.message}`);
1642
- info("서버가 실행 중인지 확인하세요.");
1677
+ error(`Platform connection failed: ${e.message}`);
1678
+ info("Check if the server is running.");
1643
1679
  process.exit(1);
1644
1680
  } finally {
1645
1681
  // 임시 번들 정리
@@ -1696,81 +1732,6 @@ ${hasPrompt ? `
1696
1732
  await open(url);
1697
1733
  },
1698
1734
 
1699
- // ── deploy ───────────────────────────────────────────
1700
- async deploy(subcmd) {
1701
- const config = loadConfig();
1702
- const cwd = process.cwd();
1703
-
1704
- // docker-compose file location
1705
- const composeFile = resolve(cwd, "docker-compose.prod.yml");
1706
- if (!existsSync(composeFile)) {
1707
- error("docker-compose.prod.yml not found — run from project root");
1708
- process.exit(1);
1709
- }
1710
-
1711
- // subcommands: stop, logs, status
1712
- if (subcmd === "stop") {
1713
- log(`\n${BOLD}${CYAN}Gencow Deploy — Stop${RESET}\n`);
1714
- info("Stopping production containers...");
1715
- execSync(`docker compose -f ${composeFile} down`, { cwd, stdio: "inherit" });
1716
- success("Stopped");
1717
- return;
1718
- }
1719
- if (subcmd === "logs") {
1720
- execSync(`docker compose -f ${composeFile} logs -f`, { cwd, stdio: "inherit" });
1721
- return;
1722
- }
1723
- if (subcmd === "status") {
1724
- execSync(`docker compose -f ${composeFile} ps`, { cwd, stdio: "inherit" });
1725
- return;
1726
- }
1727
-
1728
- // Default: build + up
1729
- log(`\n${BOLD}${CYAN}Gencow Deploy${RESET} ${DIM}— Production${RESET}\n`);
1730
- info(`Functions: ${DIM}${config.functionsDir}${RESET}`);
1731
- info(`DB: ${DIM}${config.db.url}${RESET}`);
1732
- info(`Storage: ${DIM}${config.storage}${RESET}`);
1733
- log("");
1734
-
1735
- // 1. Generate migrations so they're bundled in the image
1736
- info("Generating migrations from schema.ts...");
1737
- try {
1738
- runInServer("pnpm db:generate", buildEnv(config));
1739
- success("Migrations generated");
1740
- } catch {
1741
- warn("db:generate failed — using existing migrations");
1742
- }
1743
-
1744
- // 2. Check Docker is available
1745
- try {
1746
- execSync("docker info", { stdio: "ignore" });
1747
- } catch {
1748
- error("Docker is not running — start Docker Desktop and try again");
1749
- process.exit(1);
1750
- }
1751
-
1752
- // 3. Build Docker image
1753
- log("");
1754
- info("Building Docker image...");
1755
- execSync(`docker compose -f ${composeFile} build`, { cwd, stdio: "inherit" });
1756
- success("Image built");
1757
-
1758
- // 4. Start services
1759
- log("");
1760
- info("Starting production services...");
1761
- execSync(`docker compose -f ${composeFile} up -d`, { cwd, stdio: "inherit" });
1762
- log("");
1763
- success("Deployment complete!");
1764
- log(`
1765
- ${BOLD}Services:${RESET}
1766
- ${GREEN}▸${RESET} Server: http://localhost:${config.port}
1767
- ${GREEN}▸${RESET} Dashboard: http://localhost:3000/_admin
1768
-
1769
- ${DIM}gencow deploy logs — follow logs${RESET}
1770
- ${DIM}gencow deploy status — container status${RESET}
1771
- ${DIM}gencow deploy stop — stop all services${RESET}
1772
- `);
1773
- },
1774
1735
 
1775
1736
  // ── help ─────────────────────────────────────────────
1776
1737
  help() {
@@ -1792,9 +1753,11 @@ ${BOLD}Dev commands:${RESET}
1792
1753
  ${GREEN}dashboard${RESET} Open the Gencow admin dashboard in browser
1793
1754
  ${GREEN}db:push${RESET} Sync schema.ts → cloud DB ${DIM}(default: cloud)${RESET}
1794
1755
  ${DIM}--local Push to local DB instead of cloud${RESET}
1756
+ ${DIM}--prod Push to production DB (confirmation required)${RESET}
1795
1757
  ${GREEN}db:generate${RESET} Generate SQL migration files from schema.ts
1796
1758
  ${GREEN}db:seed${RESET} Run gencow/seed.ts on cloud app ${DIM}(default: cloud)${RESET}
1797
1759
  ${DIM}--local Seed local DB instead of cloud${RESET}
1760
+ ${DIM}--prod Seed production app${RESET}
1798
1761
  ${GREEN}db:reset${RESET} TRUNCATE all data + run seed.ts ${DIM}(local only)${RESET}
1799
1762
  ${DIM}--local Required — local DB reset only (cloud: use Dashboard)${RESET}
1800
1763
  ${GREEN}db:studio${RESET} Open Drizzle Studio ${DIM}(visual DB browser)${RESET}
@@ -1829,9 +1792,14 @@ ${BOLD}BaaS commands (login required):${RESET}
1829
1792
  ${GREEN}domain status${RESET} Check domain DNS/TLS status
1830
1793
  ${GREEN}domain remove${RESET} Disconnect custom domain
1831
1794
 
1832
- ${BOLD}Production:${RESET}
1833
- ${GREEN}deploy${RESET} Build Docker image + start production stack
1834
- ${GREEN}deploy stop${RESET} Stop production containers
1795
+ ${BOLD}App management:${RESET}
1796
+ ${GREEN}app list${RESET} List your apps
1797
+ ${GREEN}app create${RESET} Create a new app
1798
+ ${GREEN}app delete${RESET} Delete an app ${DIM}(confirmation required)${RESET}
1799
+ ${GREEN}app status${RESET} Show app status
1800
+
1801
+ ${DIM}Tip: Most commands support --prod to target the production app.${RESET}
1802
+ ${DIM} e.g. gencow env list --prod, gencow db:seed --prod${RESET}
1835
1803
 
1836
1804
  ${BOLD}Examples:${RESET}
1837
1805
  ${DIM}# New project:${RESET}
@@ -1840,10 +1808,9 @@ ${BOLD}Examples:${RESET}
1840
1808
  ${DIM}# Initialize in current directory:${RESET}
1841
1809
  gencow init . --force
1842
1810
 
1843
- ${DIM}# BaaS (deploy to platform):${RESET}
1811
+ ${DIM}# Deploy to production (Pro+ only):${RESET}
1844
1812
  gencow login
1845
- gencow app create my-app
1846
- gencow remote:deploy
1813
+ gencow deploy
1847
1814
  `);
1848
1815
  },
1849
1816
 
@@ -1855,7 +1822,7 @@ ${BOLD}Examples:${RESET}
1855
1822
  info(`Platform: ${platformUrl}`);
1856
1823
 
1857
1824
  // 1. auth-start → 인증 코드 생성
1858
- info("인증 코드 요청 중...");
1825
+ info("Requesting auth code...");
1859
1826
  let authData;
1860
1827
  try {
1861
1828
  const res = await fetch(`${platformUrl}/platform/cli/auth-start`, {
@@ -1865,16 +1832,16 @@ ${BOLD}Examples:${RESET}
1865
1832
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1866
1833
  authData = await res.json();
1867
1834
  } catch (e) {
1868
- error(`Platform 서버 연결 실패: ${e.message}`);
1835
+ error(`Platform connection failed: ${e.message}`);
1869
1836
  error(`Platform URL: ${platformUrl}`);
1870
- error(`서버가 실행 중인지 확인하세요.`);
1837
+ error(`Check if the server is running.`);
1871
1838
  process.exit(1);
1872
1839
  }
1873
1840
 
1874
1841
  log("");
1875
- log(` ${BOLD}인증 코드: ${CYAN}${authData.code}${RESET}`);
1842
+ log(` ${BOLD}Auth code: ${CYAN}${authData.code}${RESET}`);
1876
1843
  log("");
1877
- info(`브라우저에서 로그인하세요:`);
1844
+ info(`Login in your browser:`);
1878
1845
  log(` ${CYAN}${authData.url}${RESET}`);
1879
1846
  log("");
1880
1847
 
@@ -1885,11 +1852,11 @@ ${BOLD}Examples:${RESET}
1885
1852
  process.platform === "win32" ? "start" : "xdg-open";
1886
1853
  exec(`${openCmd} "${authData.url}"`);
1887
1854
  } catch {
1888
- warn("브라우저를 자동으로 없습니다. URL 직접 열어주세요.");
1855
+ warn("Could not open browser automatically. Open the URL above manually.");
1889
1856
  }
1890
1857
 
1891
1858
  // 3. 폴링 — 2초마다 인증 완료 확인
1892
- info("로그인 대기 중...");
1859
+ info("Waiting for login...");
1893
1860
  const POLL_INTERVAL = 2000;
1894
1861
  const MAX_POLLS = 150; // 10분 내 5분 = 150회
1895
1862
 
@@ -1914,14 +1881,14 @@ ${BOLD}Examples:${RESET}
1914
1881
  });
1915
1882
 
1916
1883
  log("");
1917
- success(`로그인 완료!`);
1918
- info(`토큰 저장됨: ${DIM}~/.gencow/credentials.json${RESET}`);
1884
+ success(`Login successful!`);
1885
+ info(`Token saved: ${DIM}~/.gencow/credentials.json${RESET}`);
1919
1886
  log("");
1920
1887
  return;
1921
1888
  }
1922
1889
 
1923
1890
  if (data.status === "expired") {
1924
- error("인증 코드가 만료되었습니다. 다시 시도하세요: gencow login");
1891
+ error("Auth code expired. Try again: gencow login");
1925
1892
  process.exit(1);
1926
1893
  }
1927
1894
 
@@ -1933,7 +1900,7 @@ ${BOLD}Examples:${RESET}
1933
1900
  }
1934
1901
  }
1935
1902
 
1936
- error("\n타임아웃. 다시 시도하세요: gencow login");
1903
+ error("\nTimed out. Try again: gencow login");
1937
1904
  process.exit(1);
1938
1905
  },
1939
1906
 
@@ -1941,9 +1908,9 @@ ${BOLD}Examples:${RESET}
1941
1908
  async logout() {
1942
1909
  try {
1943
1910
  unlinkSync(CREDS_PATH);
1944
- success("로그아웃 완료. 자격 증명이 삭제되었습니다.");
1911
+ success("Logged out. Credentials cleared.");
1945
1912
  } catch {
1946
- info("이미 로그아웃 상태입니다.");
1913
+ info("Already logged out.");
1947
1914
  }
1948
1915
  },
1949
1916
 
@@ -1951,8 +1918,8 @@ ${BOLD}Examples:${RESET}
1951
1918
  async whoami() {
1952
1919
  const creds = loadCreds();
1953
1920
  if (!creds?.apiKey) {
1954
- error("로그인하지 않았습니다.");
1955
- info("실행: gencow login");
1921
+ error("Not logged in.");
1922
+ info("Run: gencow login");
1956
1923
  return;
1957
1924
  }
1958
1925
 
@@ -1962,10 +1929,10 @@ ${BOLD}Examples:${RESET}
1962
1929
  if (!res.ok) {
1963
1930
  const data = await res.json().catch(() => ({}));
1964
1931
  const msg = data.error || `HTTP ${res.status}`;
1965
- error(`인증 실패: ${msg}`);
1932
+ error(`Auth failed: ${msg}`);
1966
1933
  if (res.status === 401) {
1967
- info("토큰이 만료되었거나 유효하지 않습니다.");
1968
- info("실행: gencow login");
1934
+ info("Token is expired or invalid.");
1935
+ info("Run: gencow login");
1969
1936
  }
1970
1937
  return;
1971
1938
  }
@@ -1978,13 +1945,13 @@ ${BOLD}Examples:${RESET}
1978
1945
  if (data.expiresAt) {
1979
1946
  const expiresDate = new Date(data.expiresAt);
1980
1947
  const daysLeft = Math.ceil((expiresDate.getTime() - Date.now()) / (24 * 60 * 60 * 1000));
1981
- info(`Expires: ${expiresDate.toLocaleString()} (${daysLeft} 남음)`);
1948
+ info(`Expires: ${expiresDate.toLocaleString()} (${daysLeft} days left)`);
1982
1949
  }
1983
1950
  log("");
1984
1951
  } catch (e) {
1985
- error(`서버 연결 실패: ${e.message}`);
1952
+ error(`Connection failed: ${e.message}`);
1986
1953
  info(`Platform URL: ${creds.platformUrl}`);
1987
- info("서버가 실행 중인지 확인하세요.");
1954
+ info("Check if the server is running.");
1988
1955
  }
1989
1956
  },
1990
1957
 
@@ -2055,17 +2022,17 @@ ${BOLD}Examples:${RESET}
2055
2022
  appId = gencowJson.appId || gencowJson.appName;
2056
2023
  }
2057
2024
  if (!appId) {
2058
- error(" ID를 찾을 없습니다. gencow.json이 있는 프로젝트 루트에서 실행하세요.");
2025
+ error("App not found. Run from the project root with gencow.json.");
2059
2026
  process.exit(1);
2060
2027
  }
2061
2028
 
2062
2029
  if (subcmd === "logs") {
2063
2030
  const lines = deployArgs.includes("-n") ? Number(deployArgs[deployArgs.indexOf("-n") + 1]) || 100 : 100;
2064
- info(`"${appId}" 로그 조회 중... (최근 ${lines})`);
2031
+ info(`Fetching logs for "${appId}"... (last ${lines} lines)`);
2065
2032
  const res = await rpcQuery(creds, "apps.logs", { name: appId, lines });
2066
2033
  if (!res.ok) {
2067
2034
  const errData = await res.json().catch(() => ({}));
2068
- error(`로그 조회 실패: ${errData.error || res.statusText}`);
2035
+ error(`Failed to fetch logs: ${errData.error || res.statusText}`);
2069
2036
  process.exit(1);
2070
2037
  }
2071
2038
  const data = await res.json();
@@ -2075,29 +2042,29 @@ ${BOLD}Examples:${RESET}
2075
2042
  log(` ${DIM}${line}${RESET}`);
2076
2043
  }
2077
2044
  } else {
2078
- info("로그가 없습니다.");
2045
+ info("No logs found.");
2079
2046
  }
2080
2047
  return;
2081
2048
  }
2082
2049
 
2083
2050
  if (subcmd === "status") {
2084
- info(`"${appId}" 상태 확인 중...`);
2051
+ info(`Checking "${appId}" status...`);
2085
2052
  const res = await rpcQuery(creds, "apps.get", { name: appId });
2086
2053
  if (!res.ok) {
2087
2054
  const errData = await res.json().catch(() => ({}));
2088
- error(`상태 조회 실패: ${errData.error || res.statusText}`);
2055
+ error(`Failed to fetch status: ${errData.error || res.statusText}`);
2089
2056
  process.exit(1);
2090
2057
  }
2091
2058
  const data = await res.json();
2092
2059
  const app = data.result || data;
2093
2060
  log("");
2094
- log(` ${BOLD} 상태${RESET}`);
2061
+ log(` ${BOLD}App Status${RESET}`);
2095
2062
  log(` ──────────────────────`);
2096
- info(`이름: ${app.name}`);
2097
- info(`상태: ${app.status === "running" ? `${GREEN}● running${RESET}` : app.status === "crashed" ? `${RED}● crashed${RESET}` : `${YELLOW}● ${app.status}${RESET}`}`);
2063
+ info(`Name: ${app.name}`);
2064
+ info(`Status: ${app.status === "running" ? `${GREEN}● running${RESET}` : app.status === "crashed" ? `${RED}● crashed${RESET}` : `${YELLOW}● ${app.status}${RESET}`}`);
2098
2065
  info(`URL: ${app.url || "N/A"}`);
2099
- if (app.lastDeployedAt) info(`마지막 배포: ${new Date(app.lastDeployedAt).toLocaleString()}`);
2100
- if (app.running !== undefined) info(`프로세스: ${app.running ? `${GREEN}alive${RESET}` : `${RED}dead${RESET}`}`);
2066
+ if (app.lastDeployedAt) info(`Deployed: ${new Date(app.lastDeployedAt).toLocaleString()}`);
2067
+ if (app.running !== undefined) info(`Process: ${app.running ? `${GREEN}alive${RESET}` : `${RED}dead${RESET}`}`);
2101
2068
  log("");
2102
2069
  return;
2103
2070
  }
@@ -2107,8 +2074,8 @@ ${BOLD}Examples:${RESET}
2107
2074
  const a = deployArgs[i];
2108
2075
  if (a === "--prod") {
2109
2076
  // deprecated: deploy는 이미 prod 기본값
2110
- warn(`${YELLOW}--prod 플래그는 이상 필요하지 않습니다.${RESET}`);
2111
- info(`gencow deploy 기본적으로 프로덕션에 배포합니다.`);
2077
+ warn(`${YELLOW}--prod flag is no longer needed.${RESET}`);
2078
+ info(`gencow deploy targets production by default.`);
2112
2079
  envTarget = "prod";
2113
2080
  }
2114
2081
  else if (a === "--force" || a === "-f") forceDeploy = true;
@@ -2126,19 +2093,19 @@ ${BOLD}Examples:${RESET}
2126
2093
  }
2127
2094
  else if (!a.startsWith("-")) {
2128
2095
  // 미인식 subcommand — 에러 출력
2129
- error(`알 수 없는 deploy 인자: "${a}"`);
2096
+ error(`Unknown deploy argument: "${a}"`);
2130
2097
  log("");
2131
- info(`사용법: gencow deploy [옵션]`);
2132
- info(` gencow deploy 프로덕션 배포 (Pro+)`);
2133
- info(` gencow deploy --static 프로덕션 정적 배포 (Pro+)`);
2134
- info(` gencow deploy --rollback 이전 버전으로 롤백`);
2135
- info(` gencow deploy logs 서버 로그 조회`);
2136
- info(` gencow deploy status 상태 확인`);
2137
- info(` gencow deploy --force 의존성 감사 스킵`);
2098
+ info(`Usage: gencow deploy [options]`);
2099
+ info(` gencow deploy Production deploy (Pro+)`);
2100
+ info(` gencow deploy --static Production static deploy (Pro+)`);
2101
+ info(` gencow deploy --rollback Rollback to previous version`);
2102
+ info(` gencow deploy logs View server logs`);
2103
+ info(` gencow deploy status Check app status`);
2104
+ info(` gencow deploy --force Skip dependency audit`);
2138
2105
  log("");
2139
- info(`💡 개발 환경 배포:`);
2140
- info(` gencow dev 백엔드 실시간 배포`);
2141
- info(` gencow static [dir] 정적 파일 배포`);
2106
+ info(`💡 Dev environment:`);
2107
+ info(` gencow dev Real-time backend deploy`);
2108
+ info(` gencow static [dir] Static file deploy`);
2142
2109
  process.exit(1);
2143
2110
  }
2144
2111
  }
@@ -2169,12 +2136,12 @@ ${BOLD}Examples:${RESET}
2169
2136
  // prodApp이 있으면 prod 대상 롤백, 없으면 dev 대상 롤백
2170
2137
  const rollbackTarget = prodAppId || appId;
2171
2138
  if (!rollbackTarget) {
2172
- error(" ID를 찾을 없습니다. gencow.json이 있는 프로젝트 루트에서 실행하세요.");
2139
+ error("App not found. Run from the project root with gencow.json.");
2173
2140
  process.exit(1);
2174
2141
  }
2175
2142
 
2176
2143
  log(`\n${BOLD}${CYAN}Gencow Rollback${RESET}\n`);
2177
- info(`앱: ${rollbackTarget}${prodAppId ? " (prod)" : ""}`);
2144
+ info(`App: ${rollbackTarget}${prodAppId ? " (prod)" : ""}`);
2178
2145
  log("");
2179
2146
 
2180
2147
  const rollbackStartTime = Date.now();
@@ -2182,7 +2149,7 @@ ${BOLD}Examples:${RESET}
2182
2149
  let spinnerIdx = 0;
2183
2150
  const spinner = setInterval(() => {
2184
2151
  const elapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(0);
2185
- process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} 롤백 중... ${DIM}(${elapsed}s)${RESET} `);
2152
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Rolling back... ${DIM}(${elapsed}s)${RESET} `);
2186
2153
  }, 120);
2187
2154
 
2188
2155
  const rollbackRes = await platformFetch(creds, `/platform/apps/${rollbackTarget}/rollback`, {
@@ -2195,7 +2162,7 @@ ${BOLD}Examples:${RESET}
2195
2162
 
2196
2163
  if (!rollbackRes.ok) {
2197
2164
  const errData = await rollbackRes.json().catch(() => ({}));
2198
- error(`롤백 실패: ${errData.error || rollbackRes.statusText}`);
2165
+ error(`Rollback failed: ${errData.error || rollbackRes.statusText}`);
2199
2166
  process.exit(1);
2200
2167
  }
2201
2168
 
@@ -2203,12 +2170,12 @@ ${BOLD}Examples:${RESET}
2203
2170
  const rollbackElapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(1);
2204
2171
 
2205
2172
  log("");
2206
- success(`🔄 롤백 완료! (${rollbackElapsed}s)`);
2207
- info(`롤백: #${rollbackData.rolledBackFrom} → #${rollbackData.rolledBackTo}`);
2208
- info(`번들: ${rollbackData.bundleHash}`);
2209
- info(`URL: ${rollbackData.url}`);
2173
+ success(`🔄 Rollback complete! (${rollbackElapsed}s)`);
2174
+ info(`Rolled back: #${rollbackData.rolledBackFrom} → #${rollbackData.rolledBackTo}`);
2175
+ info(`Bundle: ${rollbackData.bundleHash}`);
2176
+ info(`URL: ${rollbackData.url}`);
2210
2177
  log("");
2211
- warn(`ℹ️ 코드만 롤백되었습니다. 데이터베이스는 변경되지 않았습니다.`);
2178
+ warn(`ℹ️ Only code was rolled back. Database was not changed.`);
2212
2179
  log("");
2213
2180
  return;
2214
2181
  }
@@ -2237,12 +2204,12 @@ ${BOLD}Examples:${RESET}
2237
2204
  });
2238
2205
  rl.close();
2239
2206
  if (answer.toLowerCase() !== "y") {
2240
- info("배포 취소됨.");
2207
+ info("Deploy cancelled.");
2241
2208
  return;
2242
2209
  }
2243
2210
  }
2244
2211
 
2245
- info("Prod 생성 중...");
2212
+ info("Creating prod app...");
2246
2213
  const createProdRes = await platformFetch(creds, `/platform/apps/${appId}/create-prod`, {
2247
2214
  method: "POST",
2248
2215
  headers: { "Content-Type": "application/json" },
@@ -2252,19 +2219,19 @@ ${BOLD}Examples:${RESET}
2252
2219
  const errData = await createProdRes.json().catch(() => ({}));
2253
2220
  if (createProdRes.status === 403) {
2254
2221
  log("");
2255
- log(` ${RED}⛔ Production Deploy — Pro 플랜 이상 필요${RESET}`);
2222
+ log(` ${RED}⛔ Production Deploy — Pro plan or higher required${RESET}`);
2256
2223
  log("");
2257
- log(` ${DIM}gencow deploy 프로덕션 환경에 배포하는 명령어입니다.${RESET}`);
2224
+ log(` ${DIM}gencow deploy is for production deployment.${RESET}`);
2258
2225
  log("");
2259
- log(` ${BOLD}💡 개발 환경 배포:${RESET}`);
2260
- log(` ${GREEN}gencow dev${RESET} ${DIM}← 백엔드 실시간 배포 + 라이브 로그${RESET}`);
2261
- log(` ${GREEN}gencow static dist/${RESET} ${DIM}← 정적 파일(dist/) 배포${RESET}`);
2226
+ log(` ${BOLD}💡 Dev environment deploy:${RESET}`);
2227
+ log(` ${GREEN}gencow dev${RESET} ${DIM}← Real-time backend + live logs${RESET}`);
2228
+ log(` ${GREEN}gencow static dist/${RESET} ${DIM}← Static file (dist/) deploy${RESET}`);
2262
2229
  log("");
2263
- log(` ${BOLD}🚀 프로덕션 배포 잠금 해제:${RESET}`);
2264
- log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Pro 플랜으로 업그레이드${RESET}`);
2230
+ log(` ${BOLD}🚀 Unlock production deploy:${RESET}`);
2231
+ log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Pro plan${RESET}`);
2265
2232
  log("");
2266
2233
  } else {
2267
- error(`Prod 생성 실패: ${errData.error || createProdRes.statusText}`);
2234
+ error(`Prod app creation failed: ${errData.error || createProdRes.statusText}`);
2268
2235
  }
2269
2236
  process.exit(1);
2270
2237
  }
@@ -2280,9 +2247,9 @@ ${BOLD}Examples:${RESET}
2280
2247
  writeFileSync(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
2281
2248
 
2282
2249
  if (createProdData.alreadyExists) {
2283
- info(`Prod 확인: ${prodAppId}`);
2250
+ info(`Prod app verified: ${prodAppId}`);
2284
2251
  } else {
2285
- success(`Prod 생성 완료: ${prodAppId}`);
2252
+ success(`Prod app created: ${prodAppId}`);
2286
2253
  info(`URL: ${createProdData.url}`);
2287
2254
  // 프로비저닝 대기
2288
2255
  await new Promise(r => setTimeout(r, 3000));
@@ -2291,23 +2258,23 @@ ${BOLD}Examples:${RESET}
2291
2258
 
2292
2259
  // 배포 대상을 prod 앱으로 전환
2293
2260
  appId = prodAppId;
2294
- info(`배포 대상: ${CYAN}${appId}${RESET} (production)`);
2261
+ info(`Deploy target: ${CYAN}${appId}${RESET} (production)`);
2295
2262
  }
2296
2263
 
2297
2264
  log(`\n${BOLD}${CYAN}Gencow Deploy${RESET}\n`);
2298
2265
  if (appId) {
2299
- info(`앱 ID: ${appId}`);
2266
+ info(`App: ${appId}`);
2300
2267
  } else {
2301
- info(`앱: 신규 (ID 자동 생성)`);
2268
+ info(`App: new (auto-generated ID)`);
2302
2269
  }
2303
- info(`환경: ${envTarget}`);
2304
- info(`포맷: tar.gz`);
2270
+ info(`Env: ${envTarget}`);
2271
+ info(`Format: tar.gz`);
2305
2272
  log("");
2306
2273
 
2307
2274
  // 0-1. drizzle-kit generate 자동 실행 (번들링 전 migrations/ 최신화)
2308
2275
  {
2309
2276
  const { execSync: execGen } = await import("child_process");
2310
- info("스키마 마이그레이션 생성 중...");
2277
+ info("Generating schema migrations...");
2311
2278
  try {
2312
2279
  if (isMonorepo() && !isStandaloneProject()) {
2313
2280
  // 모노레포: package.json scripts.db:generate 사용
@@ -2332,18 +2299,18 @@ ${BOLD}Examples:${RESET}
2332
2299
  msg.includes("nothing to migrate") ||
2333
2300
  msg.includes("No changes detected")
2334
2301
  ) {
2335
- log(`${DIM} 스키마 변경 없음기존 마이그레이션 유지${RESET}`);
2302
+ log(`${DIM} No schema changeskeeping existing migrations${RESET}`);
2336
2303
  } else {
2337
- warn(`마이그레이션 생성 경고: ${msg.split("\n")[0] || "unknown"}`);
2338
- warn("gencow/migrations/ 없거나 오래된 경우 플랫폼에서 스키마 적용이 스킵될 수 있습니다.");
2339
- info("수동 실행: gencow db:generate");
2304
+ warn(`Migration generation warning: ${msg.split("\n")[0] || "unknown"}`);
2305
+ warn("gencow/migrations/ missing or outdated platform may skip schema apply.");
2306
+ info("Manual run: gencow db:generate");
2340
2307
  }
2341
2308
  }
2342
2309
  }
2343
2310
  log("");
2344
2311
 
2345
2312
  // 1. tar.gz 패키징
2346
- info("프로젝트 패키징 중...");
2313
+ info("Packaging project...");
2347
2314
  const { execSync: exec } = await import("child_process");
2348
2315
  const tmpBundle = resolve(process.cwd(), ".gencow", "deploy-bundle.tar.gz");
2349
2316
  mkdirSync(dirname(tmpBundle), { recursive: true });
@@ -2356,7 +2323,7 @@ ${BOLD}Examples:${RESET}
2356
2323
 
2357
2324
  // gencow/ 디렉토리 존재 확인
2358
2325
  if (!existsSync(resolve(process.cwd(), "gencow"))) {
2359
- error("gencow/ 디렉토리가 없습니다. Gencow 프로젝트 루트에서 실행하세요.");
2326
+ error("gencow/ directory not found. Run from Gencow project root.");
2360
2327
  process.exit(1);
2361
2328
  }
2362
2329
 
@@ -2373,7 +2340,7 @@ ${BOLD}Examples:${RESET}
2373
2340
  if (auditMsg) log(auditMsg);
2374
2341
  }
2375
2342
  } catch (auditErr) {
2376
- warn(`의존성 검사 스킵: ${auditErr.message}`);
2343
+ warn(`Skipping dependency audit: ${auditErr.message}`);
2377
2344
  }
2378
2345
  }
2379
2346
 
@@ -2395,7 +2362,7 @@ ${BOLD}Examples:${RESET}
2395
2362
  const totalDeps = Object.keys(allDeps).length;
2396
2363
  const runtimeCount = Object.keys(runtimeDeps).length;
2397
2364
  if (totalDeps > runtimeCount) {
2398
- info(`${DIM}package.json 필터링: ${runtimeCount}/${totalDeps} 패키지만 서버에 설치됩니다.${RESET}`);
2365
+ info(`${DIM}package.json filtered: ${runtimeCount}/${totalDeps} packages will be installed on server.${RESET}`);
2399
2366
  }
2400
2367
  } catch {
2401
2368
  // Fallback: use original package.json
@@ -2425,7 +2392,7 @@ ${BOLD}Examples:${RESET}
2425
2392
  exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
2426
2393
  }
2427
2394
  } catch (e) {
2428
- error(`패키징 실패: ${e.message}`);
2395
+ error(`Packaging failed: ${e.message}`);
2429
2396
  process.exit(1);
2430
2397
  }
2431
2398
 
@@ -2433,7 +2400,7 @@ ${BOLD}Examples:${RESET}
2433
2400
  try { if (useFilteredPkg) unlinkSync(filteredPkgPath); } catch {}
2434
2401
 
2435
2402
  const bundleSize = statSync(tmpBundle).size;
2436
- success(`번들 생성: ${(bundleSize / 1024).toFixed(1)} KB`);
2403
+ success(`Bundle created: ${(bundleSize / 1024).toFixed(1)} KB`);
2437
2404
 
2438
2405
  // 2-0. 번들 크기 경고 (Ph2: 리소스 미터링 — 차단 아님, 안내만)
2439
2406
  try {
@@ -2449,13 +2416,13 @@ ${BOLD}Examples:${RESET}
2449
2416
 
2450
2417
  // 2. 앱이 없으면 생성 (appId가 없는 경우)
2451
2418
  if (!appId) {
2452
- info(" 생성 (ID 자동 생성)...");
2419
+ info("Creating app (auto-generating ID)...");
2453
2420
 
2454
2421
  const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
2455
2422
 
2456
2423
  if (!createRes.ok) {
2457
2424
  const createErr = await createRes.json().catch(() => ({}));
2458
- error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
2425
+ error(`App creation failed: ${createErr.error || createRes.statusText}`);
2459
2426
  process.exit(1);
2460
2427
  }
2461
2428
 
@@ -2463,11 +2430,11 @@ ${BOLD}Examples:${RESET}
2463
2430
  appId = createData.appId || createData.name;
2464
2431
 
2465
2432
  if (!appId) {
2466
- error(" 생성 응답에 appId가 없습니다.");
2433
+ error("App creation response missing appId.");
2467
2434
  process.exit(1);
2468
2435
  }
2469
2436
 
2470
- success(`앱 "${appId}" 생성 완료. 프로비저닝 대기 중...`);
2437
+ success(`App "${appId}" created. Waiting for provisioning...`);
2471
2438
 
2472
2439
  // gencow.json 저장 (즉시)
2473
2440
  writeFileSync(gencowJsonPath, JSON.stringify({
@@ -2475,7 +2442,7 @@ ${BOLD}Examples:${RESET}
2475
2442
  displayName,
2476
2443
  platformUrl: creds.platformUrl,
2477
2444
  }, null, 2));
2478
- info(`${DIM}gencow.json 생성됨 (appId: ${appId})${RESET}`);
2445
+ info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
2479
2446
 
2480
2447
  await new Promise(r => setTimeout(r, 3000));
2481
2448
  }
@@ -2489,7 +2456,7 @@ ${BOLD}Examples:${RESET}
2489
2456
  let spinnerIdx = 0;
2490
2457
  const spinner = setInterval(() => {
2491
2458
  const elapsed = ((Date.now() - deployStartTime) / 1000).toFixed(0);
2492
- process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} 배포 중... ${DIM}(${elapsed}s)${RESET} `);
2459
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Deploying... ${DIM}(${elapsed}s)${RESET} `);
2493
2460
  }, 120);
2494
2461
 
2495
2462
  const deployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
@@ -2512,19 +2479,19 @@ ${BOLD}Examples:${RESET}
2512
2479
 
2513
2480
  // 앱이 없으면 (appId가 stale한 경우) 새로 생성 후 재시도
2514
2481
  if (deployRes.status === 404) {
2515
- warn(`앱 "${appId}" 찾을 없습니다. 새로 생성합니다...`);
2482
+ warn(`App "${appId}" not found. Creating new one...`);
2516
2483
 
2517
2484
  const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
2518
2485
 
2519
2486
  if (!createRes.ok) {
2520
2487
  const createErr = await createRes.json().catch(() => ({}));
2521
- error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
2488
+ error(`App creation failed: ${createErr.error || createRes.statusText}`);
2522
2489
  process.exit(1);
2523
2490
  }
2524
2491
 
2525
2492
  const createData = await createRes.json();
2526
2493
  appId = createData.appId || createData.name;
2527
- success(`앱 "${appId}" 생성 완료. 프로비저닝 대기 중...`);
2494
+ success(`App "${appId}" created. Provisioning...`);
2528
2495
 
2529
2496
  // gencow.json 업데이트
2530
2497
  writeFileSync(gencowJsonPath, JSON.stringify({
@@ -2536,7 +2503,7 @@ ${BOLD}Examples:${RESET}
2536
2503
  await new Promise(r => setTimeout(r, 3000));
2537
2504
 
2538
2505
  // 재배포
2539
- info("재배포 시도...");
2506
+ info("Retrying deploy...");
2540
2507
  exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
2541
2508
  const retryBuffer = readFileSync(tmpBundle);
2542
2509
 
@@ -2544,7 +2511,7 @@ ${BOLD}Examples:${RESET}
2544
2511
  let retrySpinnerIdx = 0;
2545
2512
  const retrySpinner = setInterval(() => {
2546
2513
  const elapsed = ((Date.now() - retryStartTime) / 1000).toFixed(0);
2547
- process.stdout.write(`\r ${CYAN}${spinnerFrames[retrySpinnerIdx++ % spinnerFrames.length]}${RESET} 배포 중... ${DIM}(${elapsed}s)${RESET} `);
2514
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[retrySpinnerIdx++ % spinnerFrames.length]}${RESET} Deploying... ${DIM}(${elapsed}s)${RESET} `);
2548
2515
  }, 120);
2549
2516
 
2550
2517
  const retryRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
@@ -2562,10 +2529,10 @@ ${BOLD}Examples:${RESET}
2562
2529
 
2563
2530
  if (!retryRes.ok) {
2564
2531
  const retryErr = await retryRes.json().catch(() => ({}));
2565
- error(`배포 실패: ${retryErr.error || retryRes.statusText}`);
2532
+ error(`Deploy failed: ${retryErr.error || retryRes.statusText}`);
2566
2533
  if (retryErr.crashLogs?.length) {
2567
2534
  log("");
2568
- warn("서버 시작 실패 원인:");
2535
+ warn("Server startup failure:");
2569
2536
  for (const line of retryErr.crashLogs) log(` ${DIM}${line}${RESET}`);
2570
2537
  }
2571
2538
  process.exit(1);
@@ -2574,14 +2541,14 @@ ${BOLD}Examples:${RESET}
2574
2541
  const retryData = await retryRes.json();
2575
2542
  const retryElapsed = ((Date.now() - retryStartTime) / 1000).toFixed(1);
2576
2543
  log("");
2577
- success(`서버 빌드 완료! (${retryElapsed}s)`);
2544
+ success(`Server build complete! (${retryElapsed}s)`);
2578
2545
 
2579
2546
  // Health check — 앱 URL로 실제 응답 확인
2580
2547
  if (retryData.url) {
2581
2548
  await this._verifyAppReady(retryData.url, appId);
2582
2549
  }
2583
2550
 
2584
- info(`앱 ID: ${appId}`);
2551
+ info(`App ID: ${appId}`);
2585
2552
  info(`URL: ${retryData.url}`);
2586
2553
  info(`Hash: ${retryData.bundleHash}`);
2587
2554
  updateEnvLocalUrl(retryData.url);
@@ -2589,10 +2556,10 @@ ${BOLD}Examples:${RESET}
2589
2556
  return;
2590
2557
  }
2591
2558
 
2592
- error(`배포 실패: ${errData.error || deployRes.statusText}`);
2559
+ error(`Deploy failed: ${errData.error || deployRes.statusText}`);
2593
2560
  if (errData.crashLogs?.length) {
2594
2561
  log("");
2595
- warn("서버 시작 실패 원인:");
2562
+ warn("Server startup failure cause:");
2596
2563
  for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
2597
2564
  }
2598
2565
  process.exit(1);
@@ -2602,14 +2569,14 @@ ${BOLD}Examples:${RESET}
2602
2569
  const deployElapsed = ((Date.now() - deployStartTime) / 1000).toFixed(1);
2603
2570
 
2604
2571
  log("");
2605
- success(`서버 빌드 완료! (${deployElapsed}s)`);
2572
+ success(`Server build complete! (${deployElapsed}s)`);
2606
2573
 
2607
2574
  // Health check — 앱 URL로 실제 응답 확인 (Blue-Green은 서버에서 완료됐지만, edge case 방어)
2608
2575
  if (deployData.url) {
2609
2576
  await this._verifyAppReady(deployData.url, appId);
2610
2577
  }
2611
2578
 
2612
- info(`앱 ID: ${appId}`);
2579
+ info(`App ID: ${appId}`);
2613
2580
  info(`URL: ${deployData.url}`);
2614
2581
  info(`Hash: ${deployData.bundleHash}`);
2615
2582
  if (deployData.deployId) info(`ID: ${deployData.deployId}`);
@@ -2623,7 +2590,7 @@ ${BOLD}Examples:${RESET}
2623
2590
  displayName,
2624
2591
  platformUrl: creds.platformUrl,
2625
2592
  }, null, 2));
2626
- info(`${DIM}gencow.json 생성됨${RESET}`);
2593
+ info(`${DIM}gencow.json created${RESET}`);
2627
2594
  }
2628
2595
  },
2629
2596
 
@@ -2643,7 +2610,7 @@ ${BOLD}Examples:${RESET}
2643
2610
  // health check 스피너
2644
2611
  const healthSpinner = setInterval(() => {
2645
2612
  const elapsed = ((Date.now() - start) / 1000).toFixed(0);
2646
- process.stdout.write(`\r ${CYAN}${spinnerFrames[idx++ % spinnerFrames.length]}${RESET} 준비 확인 중... ${DIM}(${elapsed}s)${RESET} `);
2613
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[idx++ % spinnerFrames.length]}${RESET} Checking app readiness... ${DIM}(${elapsed}s)${RESET} `);
2647
2614
  }, 120);
2648
2615
 
2649
2616
  let healthy = false;
@@ -2669,10 +2636,10 @@ ${BOLD}Examples:${RESET}
2669
2636
 
2670
2637
  const healthElapsed = ((Date.now() - start) / 1000).toFixed(1);
2671
2638
  if (healthy) {
2672
- success(`앱 Ready! (${healthElapsed}s)`);
2639
+ success(`App Ready! (${healthElapsed}s)`);
2673
2640
  } else {
2674
- warn(`앱 응답 확인 실패 (${healthElapsed}s). 서버가 시작 중일 있습니다.`);
2675
- info(`수동 확인: ${CYAN}${appUrl}${RESET}`);
2641
+ warn(`App response check failed (${healthElapsed}s). Server may still be starting.`);
2642
+ info(`Manual check: ${CYAN}${appUrl}${RESET}`);
2676
2643
  }
2677
2644
  },
2678
2645
 
@@ -2695,9 +2662,9 @@ ${BOLD}Examples:${RESET}
2695
2662
  }
2696
2663
 
2697
2664
  if (!targetDir || !existsSync(resolve(process.cwd(), targetDir))) {
2698
- error(`정적 파일 폴더를 찾을 수 없습니다.`);
2699
- info(`자동 감지 순서: ${AUTO_DETECT.join(", ")}`);
2700
- info(`수동 지정: gencow static <dir>`);
2665
+ error(`Static files directory not found.`);
2666
+ info(`Auto-detect order: ${AUTO_DETECT.join(", ")}`);
2667
+ info(`Specify manually: gencow static <dir>`);
2701
2668
  process.exit(1);
2702
2669
  }
2703
2670
 
@@ -2748,22 +2715,22 @@ ${BOLD}Examples:${RESET}
2748
2715
  const shouldDeployBackend = detectedBackend && !noBackend && !isBackendEmpty;
2749
2716
 
2750
2717
  if (isBackendEmpty && detectedBackend) {
2751
- info(`${DIM}gencow/ 감지됨 — API 함수(query/mutation) 없음백엔드 배포 건너뜀${RESET}`);
2752
- info(`${DIM}💡 백엔드가 필요하면 gencow/ .ts 파일에 query() 또는 mutation() 정의하세요.${RESET}`);
2718
+ info(`${DIM}gencow/ detectedno API functions (query/mutation) foundskipping backend deploy${RESET}`);
2719
+ info(`${DIM}💡 If you need a backend, define query() or mutation() in gencow/*.ts files.${RESET}`);
2753
2720
  }
2754
2721
 
2755
2722
  if (shouldDeployBackend) {
2756
2723
  log(`\n${BOLD}${CYAN}Gencow Deploy (Fullstack)${RESET}\n`);
2757
- info(`백엔드 감지: ${backendRoot === process.cwd() ? "gencow/" : resolve(backendRoot, "gencow/")} (자동 배포)`);
2758
- info(`프론트엔드: ${targetDir}/`);
2724
+ info(`Backend detected: ${backendRoot === process.cwd() ? "gencow/" : resolve(backendRoot, "gencow/")} (auto-deploy)`);
2725
+ info(`Frontend: ${targetDir}/`);
2759
2726
  } else {
2760
2727
  log(`\n${BOLD}${CYAN}Gencow Static Deploy${RESET}\n`);
2761
- info(`폴더: ${targetDir}/`);
2728
+ info(`Directory: ${targetDir}/`);
2762
2729
  }
2763
2730
  if (appId) {
2764
- info(`앱 ID: ${appId}`);
2731
+ info(`App ID: ${appId}`);
2765
2732
  } else {
2766
- info(`앱: 신규 (ID 자동 생성)`);
2733
+ info(`App: new (ID auto-generated)`);
2767
2734
  }
2768
2735
  log("");
2769
2736
 
@@ -2795,23 +2762,23 @@ ${BOLD}Examples:${RESET}
2795
2762
  }
2796
2763
  if (apiRefFiles.length > 0) {
2797
2764
  log("");
2798
- warn(`빌드 파일에서 API 참조가 발견되었습니다:`);
2765
+ warn(`API references found in build files:`);
2799
2766
  for (const f of apiRefFiles.slice(0, 5)) log(` ${DIM}- ${f}${RESET}`);
2800
- if (apiRefFiles.length > 5) log(` ${DIM}... ${apiRefFiles.length - 5}개${RESET}`);
2767
+ if (apiRefFiles.length > 5) log(` ${DIM}... and ${apiRefFiles.length - 5} more${RESET}`);
2801
2768
  log("");
2802
- warn(`정적 호스팅에는 API 서버가 없어 404가 발생합니다.`);
2803
- info(`💡 백엔드가 필요하다면: ${CYAN}VITE_API_URL=https://{backend}.{도메인} npm run build${RESET} 배포`);
2804
- info(`💡 별도 백엔드를 사용한다면: ${CYAN}VITE_API_URL${RESET} 환경변수로 빌드 대상을 지정하세요.`);
2769
+ warn(`Static hosting has no API server these will return 404.`);
2770
+ info(`💡 If you need a backend: ${CYAN}VITE_API_URL=https://{backend}.{domain} npm run build${RESET} then deploy`);
2771
+ info(`💡 If using a separate backend: set ${CYAN}VITE_API_URL${RESET} env var before building.`);
2805
2772
  log("");
2806
2773
 
2807
2774
  const { createInterface } = await import("readline");
2808
2775
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2809
2776
  const answer = await new Promise(resolve => {
2810
- rl.question(` ${YELLOW}⚠${RESET} 그래도 정적 배포를 계속하시겠습니까? (y/N) `, resolve);
2777
+ rl.question(` ${YELLOW}⚠${RESET} Proceed with static deploy anyway? (y/N) `, resolve);
2811
2778
  });
2812
2779
  rl.close();
2813
2780
  if (answer.toLowerCase() !== "y") {
2814
- info("배포 취소됨.");
2781
+ info("Deploy cancelled.");
2815
2782
  return;
2816
2783
  }
2817
2784
  log("");
@@ -2821,7 +2788,7 @@ ${BOLD}Examples:${RESET}
2821
2788
 
2822
2789
  // ── 백엔드 자동 배포 (감지된 경우) ────────────────────
2823
2790
  if (shouldDeployBackend) {
2824
- log(` ${BOLD}── 백엔드 배포 ──────────────────────${RESET}\n`);
2791
+ log(` ${BOLD}── Backend Deploy ──────────────────────${RESET}\n`);
2825
2792
 
2826
2793
  // 1-0. Pre-deploy dependency audit + Phase B: filter package.json
2827
2794
  let auditResult = null;
@@ -2835,7 +2802,7 @@ ${BOLD}Examples:${RESET}
2835
2802
  if (auditMsg) log(auditMsg);
2836
2803
  }
2837
2804
  } catch (auditErr) {
2838
- warn(`의존성 검사 스킵: ${auditErr.message}`);
2805
+ warn(`Dependency audit skipped: ${auditErr.message}`);
2839
2806
  }
2840
2807
  }
2841
2808
 
@@ -2843,7 +2810,7 @@ ${BOLD}Examples:${RESET}
2843
2810
  // 서버에서 generate 시 stdin이 없어 rename 프롬프트에 막히던 문제 해결
2844
2811
  const schemaPath = resolve(backendRoot, "gencow", "schema.ts");
2845
2812
  if (existsSync(schemaPath)) {
2846
- info("스키마 마이그레이션 생성 중...");
2813
+ info("Generating schema migrations...");
2847
2814
  try {
2848
2815
  const dk = _drizzleKitCmd("generate");
2849
2816
  execSync(dk.cmd, {
@@ -2851,20 +2818,20 @@ ${BOLD}Examples:${RESET}
2851
2818
  env: { ...process.env, ...dk.env },
2852
2819
  stdio: "inherit", // ← 프롬프트 패스스루!
2853
2820
  });
2854
- success("마이그레이션 생성 완료");
2821
+ success("Migrations generated");
2855
2822
  } catch (e) {
2856
2823
  const msg = e.stderr?.toString() || e.message || "";
2857
2824
  if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
2858
- log(`${DIM} 스키마 변경 없음기존 마이그레이션 사용${RESET}`);
2825
+ log(`${DIM} No schema changesusing existing migrations${RESET}`);
2859
2826
  } else {
2860
- warn(`마이그레이션 생성 실패: ${msg.split("\\n")[0]}`);
2861
- info("서버에서 기존 방식으로 스키마 적용을 시도합니다.");
2827
+ warn(`Migration generation failed: ${msg.split("\\n")[0]}`);
2828
+ info("Will attempt schema push via server.");
2862
2829
  }
2863
2830
  }
2864
2831
  }
2865
2832
 
2866
2833
  // 1. tar.gz 패키징 (백엔드)
2867
- info("백엔드 패키징 중...");
2834
+ info("Packaging backend...");
2868
2835
  const { execSync: exec } = await import("child_process");
2869
2836
  const tmpBackendBundle = resolve(backendRoot, ".gencow", "deploy-bundle.tar.gz");
2870
2837
  mkdirSync(dirname(tmpBackendBundle), { recursive: true });
@@ -2892,7 +2859,7 @@ ${BOLD}Examples:${RESET}
2892
2859
  const totalDeps = Object.keys(allDeps).length;
2893
2860
  const runtimeCount = Object.keys(runtimeDeps).length;
2894
2861
  if (totalDeps > runtimeCount) {
2895
- info(`${DIM}package.json 필터링: ${runtimeCount}/${totalDeps} 패키지만 서버에 설치됩니다.${RESET}`);
2862
+ info(`${DIM}package.json filtered: ${runtimeCount}/${totalDeps} packages will be installed on server.${RESET}`);
2896
2863
  }
2897
2864
  } catch {
2898
2865
  useFilteredPkg = false;
@@ -2921,34 +2888,34 @@ ${BOLD}Examples:${RESET}
2921
2888
  exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBackendBundle}" ${backendFiles.join(" ")}`, { cwd: backendRoot });
2922
2889
  }
2923
2890
  } catch (e) {
2924
- error(`백엔드 패키징 실패: ${e.message}`);
2891
+ error(`Backend packaging failed: ${e.message}`);
2925
2892
  process.exit(1);
2926
2893
  }
2927
2894
  try { if (useFilteredPkg) unlinkSync(filteredPkgPath); } catch {}
2928
2895
 
2929
2896
  const backendBundleSize = statSync(tmpBackendBundle).size;
2930
- success(`백엔드 번들 생성: ${(backendBundleSize / 1024).toFixed(1)} KB`);
2897
+ success(`Backend bundle created: ${(backendBundleSize / 1024).toFixed(1)} KB`);
2931
2898
 
2932
2899
  // 2. 앱이 없으면 생성
2933
2900
  if (!appId) {
2934
- info(" 생성 (ID 자동 생성)...");
2901
+ info("Creating app (auto-generating ID)...");
2935
2902
  const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
2936
2903
  if (!createRes.ok) {
2937
2904
  const createErr = await createRes.json().catch(() => ({}));
2938
- error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
2905
+ error(`App creation failed: ${createErr.error || createRes.statusText}`);
2939
2906
  process.exit(1);
2940
2907
  }
2941
2908
  const createData = await createRes.json();
2942
2909
  appId = createData.appId || createData.name;
2943
2910
  if (!appId) {
2944
- error(" 생성 응답에 appId가 없습니다.");
2911
+ error("App creation response missing appId.");
2945
2912
  process.exit(1);
2946
2913
  }
2947
- success(`앱 "${appId}" 생성 완료. 프로비저닝 대기 중...`);
2914
+ success(`App "${appId}" created. Provisioning...`);
2948
2915
  writeFileSync(gencowJsonPath, JSON.stringify({
2949
2916
  appId, displayName, platformUrl: creds.platformUrl,
2950
2917
  }, null, 2));
2951
- info(`${DIM}gencow.json 생성됨 (appId: ${appId})${RESET}`);
2918
+ info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
2952
2919
  await new Promise(r => setTimeout(r, 3000));
2953
2920
  }
2954
2921
 
@@ -2961,7 +2928,7 @@ ${BOLD}Examples:${RESET}
2961
2928
  let spinnerIdx = 0;
2962
2929
  const backendSpinner = setInterval(() => {
2963
2930
  const elapsed = ((Date.now() - backendStartTime) / 1000).toFixed(0);
2964
- process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} 백엔드 배포 중... ${DIM}(${elapsed}s)${RESET} `);
2931
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Deploying backend... ${DIM}(${elapsed}s)${RESET} `);
2965
2932
  }, 120);
2966
2933
 
2967
2934
  const backendDeployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
@@ -2976,24 +2943,24 @@ ${BOLD}Examples:${RESET}
2976
2943
 
2977
2944
  if (!backendDeployRes.ok) {
2978
2945
  const errData = await backendDeployRes.json().catch(() => ({}));
2979
- error(`백엔드 배포 실패: ${errData.error || backendDeployRes.statusText}`);
2946
+ error(`Backend deploy failed: ${errData.error || backendDeployRes.statusText}`);
2980
2947
  if (errData.crashLogs?.length) {
2981
2948
  log("");
2982
- warn("서버 시작 실패 원인:");
2949
+ warn("Server startup failure:");
2983
2950
  for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
2984
2951
  }
2985
2952
  if (targetDir) {
2986
2953
  // --static 모드: 백엔드 실패해도 프론트엔드 배포는 계속 진행
2987
- warn("백엔드 배포 실패정적 파일 배포는 계속 진행합니다.");
2954
+ warn("Backend deploy failedcontinuing with static deploy.");
2988
2955
  log("");
2989
2956
  } else {
2990
- error("백엔드 배포 실패로 프론트엔드 배포를 건너뜁니다.");
2957
+ error("Backend deploy failed, skipping frontend deploy.");
2991
2958
  process.exit(1);
2992
2959
  }
2993
2960
  } else {
2994
2961
  const backendData = await backendDeployRes.json();
2995
2962
  const backendElapsed = ((Date.now() - backendStartTime) / 1000).toFixed(1);
2996
- success(`백엔드 빌드 완료! (${backendElapsed}s)`);
2963
+ success(`Backend build complete! (${backendElapsed}s)`);
2997
2964
 
2998
2965
  // Health check — 백엔드 앱 URL로 실제 응답 확인
2999
2966
  if (backendData.url) {
@@ -3005,11 +2972,11 @@ ${BOLD}Examples:${RESET}
3005
2972
  updateEnvLocalUrl(backendData.url);
3006
2973
  log("");
3007
2974
  }
3008
- log(` ${BOLD}── 프론트엔드 배포 ──────────────────${RESET}\n`);
2975
+ log(` ${BOLD}── Frontend Deploy ──────────────────${RESET}\n`);
3009
2976
  }
3010
2977
 
3011
2978
  // 1. tar.gz 패키징
3012
- info("정적 파일 패키징 중...");
2979
+ info("Packaging static files...");
3013
2980
  const { execSync: exec } = await import("child_process");
3014
2981
  const tmpBundle = resolve(process.cwd(), ".gencow", "static-bundle.tar.gz");
3015
2982
  mkdirSync(dirname(tmpBundle), { recursive: true });
@@ -3017,40 +2984,40 @@ ${BOLD}Examples:${RESET}
3017
2984
  try {
3018
2985
  exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" -C "${resolve(process.cwd(), targetDir)}" .`, { cwd: process.cwd() });
3019
2986
  } catch (e) {
3020
- error(`패키징 실패: ${e.message}`);
2987
+ error(`Packaging failed: ${e.message}`);
3021
2988
  process.exit(1);
3022
2989
  }
3023
2990
 
3024
2991
  const bundleSize = statSync(tmpBundle).size;
3025
2992
  const MAX_SIZE = 100 * 1024 * 1024;
3026
2993
  if (bundleSize > MAX_SIZE) {
3027
- error(`번들이 너무 큽니다: ${(bundleSize / (1024 * 1024)).toFixed(1)} MB (최대 100 MB)`);
2994
+ error(`Bundle too large: ${(bundleSize / (1024 * 1024)).toFixed(1)} MB (max 100 MB)`);
3028
2995
  try { unlinkSync(tmpBundle); } catch { }
3029
2996
  process.exit(1);
3030
2997
  }
3031
- success(`번들 생성: ${(bundleSize / 1024).toFixed(1)} KB`);
2998
+ success(`Bundle created: ${(bundleSize / 1024).toFixed(1)} KB`);
3032
2999
 
3033
3000
  // 2. 앱이 없으면 생성
3034
3001
  if (!appId) {
3035
- info(" 생성 (ID 자동 생성)...");
3002
+ info("Creating app (auto-generating ID)...");
3036
3003
  const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
3037
3004
  if (!createRes.ok) {
3038
3005
  const createErr = await createRes.json().catch(() => ({}));
3039
- error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
3006
+ error(`App creation failed: ${createErr.error || createRes.statusText}`);
3040
3007
  process.exit(1);
3041
3008
  }
3042
3009
  const createData = await createRes.json();
3043
3010
  appId = createData.appId || createData.name;
3044
- success(`앱 "${appId}" 생성 완료. 프로비저닝 대기 중...`);
3011
+ success(`App "${appId}" created. Provisioning...`);
3045
3012
  writeFileSync(gencowJsonPath, JSON.stringify({
3046
3013
  appId, displayName, platformUrl: creds.platformUrl,
3047
3014
  }, null, 2));
3048
- info(`${DIM}gencow.json 생성됨 (appId: ${appId})${RESET}`);
3015
+ info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
3049
3016
  await new Promise(r => setTimeout(r, 3000));
3050
3017
  }
3051
3018
 
3052
3019
  // 3. 업로드
3053
- info("정적 파일 배포 중...");
3020
+ info("Deploying static files...");
3054
3021
  const bundleBuffer = readFileSync(tmpBundle);
3055
3022
 
3056
3023
  const deployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy-static`, {
@@ -3063,22 +3030,22 @@ ${BOLD}Examples:${RESET}
3063
3030
 
3064
3031
  if (!deployRes.ok) {
3065
3032
  const errData = await deployRes.json().catch(() => ({}));
3066
- error(`정적 배포 실패: ${errData.error || deployRes.statusText}`);
3033
+ error(`Static deploy failed: ${errData.error || deployRes.statusText}`);
3067
3034
  process.exit(1);
3068
3035
  }
3069
3036
 
3070
3037
  const data = await deployRes.json();
3071
3038
  log("");
3072
- success(`정적 배포 완료!`);
3073
- info(`앱 ID: ${appId}`);
3039
+ success(`Static deploy complete!`);
3040
+ info(`App ID: ${appId}`);
3074
3041
  info(`URL: ${data.url}`);
3075
- info(`파일: ${data.files} (${(data.size / 1024).toFixed(1)} KB)`);
3042
+ info(`Files: ${data.files} (${(data.size / 1024).toFixed(1)} KB)`);
3076
3043
  info(`Hash: ${data.bundleHash}`);
3077
3044
  if (data.deployId) info(`ID: ${data.deployId}`);
3078
3045
 
3079
3046
  // 이미지 최적화 결과 표시
3080
3047
  if (data.optimizedImages) {
3081
- success(`🖼 ${data.optimizedImages} 이미지 WebP 최적화 (절약: ${(data.savedBytes / 1024).toFixed(1)} KB)`);
3048
+ success(`🖼 ${data.optimizedImages} images optimized to WebP (saved: ${(data.savedBytes / 1024).toFixed(1)} KB)`);
3082
3049
  }
3083
3050
 
3084
3051
  // skipped 파일 경고 표시
@@ -3131,12 +3098,12 @@ ${BOLD}Examples:${RESET}
3131
3098
 
3132
3099
  // --prod인데 prod 앱이 없는 경우
3133
3100
  if (envTarget === "prod" && appId && !appId.endsWith("-prod")) {
3134
- error("Prod 앱이 아직 없습니다. gencow deploy 먼저 실행하세요.");
3101
+ error("No prod app yet. Run gencow deploy first.");
3135
3102
  return;
3136
3103
  }
3137
3104
 
3138
3105
  if (!appId) {
3139
- error(" ID 찾을 없습니다. gencow deploy 먼저 실행하세요.");
3106
+ error("App ID not found. Run gencow deploy first.");
3140
3107
  return;
3141
3108
  }
3142
3109
 
@@ -3148,13 +3115,13 @@ ${BOLD}Examples:${RESET}
3148
3115
  { headers: { "Authorization": `Bearer ${creds.apiKey}` } }
3149
3116
  );
3150
3117
  if (!res.ok) {
3151
- error(`환경변수 조회 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3118
+ error(`Failed to list env vars: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3152
3119
  return;
3153
3120
  }
3154
3121
  const vars = await res.json();
3155
- log(`\n${BOLD}${CYAN}환경변수${RESET} — ${appId} (${envTarget})\n`);
3122
+ log(`\n${BOLD}${CYAN}Environment Variables${RESET} — ${appId} (${envTarget})\n`);
3156
3123
  if (vars.length === 0) {
3157
- info("설정된 환경변수가 없습니다.");
3124
+ info("No environment variables configured.");
3158
3125
  } else {
3159
3126
  for (const v of vars) {
3160
3127
  log(` ${v.key}`);
@@ -3168,7 +3135,7 @@ ${BOLD}Examples:${RESET}
3168
3135
  // gencow env set KEY=VALUE [KEY2=VALUE2...]
3169
3136
  const kvPairs = restArgs.filter(a => a.includes("=") && !a.startsWith("-"));
3170
3137
  if (kvPairs.length === 0) {
3171
- error("사용법: gencow env set KEY=VALUE [KEY2=VALUE2...]");
3138
+ error("Usage: gencow env set KEY=VALUE [KEY2=VALUE2...]");
3172
3139
  return;
3173
3140
  }
3174
3141
 
@@ -3189,9 +3156,9 @@ ${BOLD}Examples:${RESET}
3189
3156
  );
3190
3157
 
3191
3158
  if (res.ok) {
3192
- success(`${key} 설정 완료 (${envTarget})`);
3159
+ success(`${key} configured (${envTarget})`);
3193
3160
  } else {
3194
- error(`${key} 설정 실패`);
3161
+ error(`${key} configuration failed`);
3195
3162
  }
3196
3163
  }
3197
3164
  break;
@@ -3201,7 +3168,7 @@ ${BOLD}Examples:${RESET}
3201
3168
  case "remove": {
3202
3169
  const keys = restArgs.filter(a => !a.startsWith("-"));
3203
3170
  if (keys.length === 0) {
3204
- error("사용법: gencow env unset KEY [KEY2...]");
3171
+ error("Usage: gencow env unset KEY [KEY2...]");
3205
3172
  return;
3206
3173
  }
3207
3174
 
@@ -3215,9 +3182,9 @@ ${BOLD}Examples:${RESET}
3215
3182
  );
3216
3183
 
3217
3184
  if (res.ok) {
3218
- success(`${key} 삭제 완료`);
3185
+ success(`${key} removed`);
3219
3186
  } else {
3220
- error(`${key} 삭제 실패`);
3187
+ error(`${key} removal failed`);
3221
3188
  }
3222
3189
  }
3223
3190
  break;
@@ -3227,7 +3194,7 @@ ${BOLD}Examples:${RESET}
3227
3194
  // 로컬 .env 파일 → 리모트 일괄 push
3228
3195
  const envFile = resolve(process.cwd(), envTarget === "prod" ? ".env.production" : ".env");
3229
3196
  if (!existsSync(envFile)) {
3230
- error(`${envFile} 파일을 찾을 수 없습니다.`);
3197
+ error(`${envFile} not found.`);
3231
3198
  return;
3232
3199
  }
3233
3200
 
@@ -3241,7 +3208,7 @@ ${BOLD}Examples:${RESET}
3241
3208
  }
3242
3209
 
3243
3210
  const count = Object.keys(vars).length;
3244
- info(`${count} 환경변수를 ${appId} (${envTarget})에 push합니다...`);
3211
+ info(`Pushing ${count} env vars to ${appId} (${envTarget})...`);
3245
3212
 
3246
3213
  const res = await fetch(
3247
3214
  `${creds.platformUrl}/platform/apps/${appId}/env/bulk`,
@@ -3256,16 +3223,16 @@ ${BOLD}Examples:${RESET}
3256
3223
  );
3257
3224
 
3258
3225
  if (res.ok) {
3259
- success(`${count} 환경변수 push 완료`);
3226
+ success(`${count} env vars pushed`);
3260
3227
  } else {
3261
- error(`push 실패: ${(await res.json().catch(() => ({}))).error}`);
3228
+ error(`Push failed: ${(await res.json().catch(() => ({}))).error}`);
3262
3229
  }
3263
3230
  break;
3264
3231
  }
3265
3232
 
3266
3233
  default:
3267
- error(`알 없는 하위 명령: ${subCmd}`);
3268
- info("사용법: gencow env [list|set|unset|push] ...");
3234
+ error(`Unknown subcommand: ${subCmd}`);
3235
+ info("Usage: gencow env [list|set|unset|push] ...");
3269
3236
  }
3270
3237
  },
3271
3238
 
@@ -3275,8 +3242,9 @@ ${BOLD}Examples:${RESET}
3275
3242
  const subCmd = configArgs[0] || "help";
3276
3243
  const restArgs = configArgs.slice(1);
3277
3244
 
3278
- // ID 결정
3245
+ // Resolve appId + --prod support
3279
3246
  let appId = null;
3247
+ const isProd = restArgs.includes("--prod");
3280
3248
  for (let i = 0; i < restArgs.length; i++) {
3281
3249
  if (restArgs[i] === "--app" || restArgs[i] === "-a") appId = restArgs[++i];
3282
3250
  }
@@ -3286,22 +3254,31 @@ ${BOLD}Examples:${RESET}
3286
3254
  if (existsSync(gencowJsonPath)) {
3287
3255
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
3288
3256
  appId = gencowJson.appId || gencowJson.appName;
3257
+ if (isProd && gencowJson.prodApp) {
3258
+ appId = gencowJson.prodApp;
3259
+ }
3289
3260
  }
3290
3261
  }
3291
3262
 
3292
3263
  if (subCmd === "help" || subCmd === "--help" || subCmd === "-h") {
3293
- log(`\n${BOLD}${CYAN}gencow config${RESET} — 설정 관리\n`);
3294
- log(` ${CYAN}set${RESET} image.maxWidth <값> Auto WebP 최대 (px)`);
3295
- log(` ${CYAN}set${RESET} image.quality <값> Auto WebP 품질 (1-100)`);
3296
- log(` ${CYAN}get${RESET} image 현재 이미지 설정 조회`);
3297
- log(` ${CYAN}reset${RESET} image 이미지 설정 초기화 (Tier 기본값)\n`);
3298
- log(` ${DIM}옵션:${RESET}`);
3299
- log(` ${DIM}--app, -a <앱이름> 대상 지정 (기본: gencow.json)${RESET}\n`);
3264
+ log(`\n${BOLD}${CYAN}gencow config${RESET} — App configuration\n`);
3265
+ log(` ${CYAN}set${RESET} image.maxWidth <value> Auto WebP max width (px)`);
3266
+ log(` ${CYAN}set${RESET} image.quality <value> Auto WebP quality (1-100)`);
3267
+ log(` ${CYAN}get${RESET} image Show current image config`);
3268
+ log(` ${CYAN}reset${RESET} image Reset to tier defaults\n`);
3269
+ log(` ${DIM}Options:${RESET}`);
3270
+ log(` ${DIM}--app, -a <name> Target app (default: from gencow.json)${RESET}`);
3271
+ log(` ${DIM}--prod Target production app${RESET}\n`);
3272
+ return;
3273
+ }
3274
+
3275
+ if (isProd && appId && !appId.endsWith("-prod")) {
3276
+ error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
3300
3277
  return;
3301
3278
  }
3302
3279
 
3303
3280
  if (!appId) {
3304
- error(" ID를 찾을 없습니다. gencow deploy를 먼저 실행하세요.");
3281
+ error("App not found. Run 'gencow dev' first.");
3305
3282
  return;
3306
3283
  }
3307
3284
 
@@ -3311,7 +3288,7 @@ ${BOLD}Examples:${RESET}
3311
3288
  const val = restArgs[restArgs.indexOf(key) + 1];
3312
3289
 
3313
3290
  if (!key || val === undefined) {
3314
- error("사용법: gencow config set image.maxWidth <값>");
3291
+ error("Usage: gencow config set image.maxWidth <value>");
3315
3292
  return;
3316
3293
  }
3317
3294
 
@@ -3319,34 +3296,34 @@ ${BOLD}Examples:${RESET}
3319
3296
  if (key === "image.maxWidth" || key === "image.max-width") {
3320
3297
  const v = parseInt(val);
3321
3298
  if (isNaN(v) || v < 0 || v > 10000) {
3322
- error("maxWidth 0~10000 사이의 값이어야 합니다. (0 = 리셋)");
3299
+ error("maxWidth must be between 0 and 10000 (0 = reset)");
3323
3300
  return;
3324
3301
  }
3325
3302
  imageConfig.autoMaxWidth = v;
3326
3303
  } else if (key === "image.quality") {
3327
3304
  const v = parseInt(val);
3328
3305
  if (isNaN(v) || v < 0 || v > 100) {
3329
- error("quality 0~100 사이의 값이어야 합니다. (0 = 리셋)");
3306
+ error("quality must be between 0 and 100 (0 = reset)");
3330
3307
  return;
3331
3308
  }
3332
3309
  imageConfig.autoQuality = v;
3333
3310
  } else {
3334
- error(`알 없는 설정 키: ${key}`);
3335
- info("사용 가능: image.maxWidth, image.quality");
3311
+ error(`Unknown config key: ${key}`);
3312
+ info("Available: image.maxWidth, image.quality");
3336
3313
  return;
3337
3314
  }
3338
3315
 
3339
3316
  const res = await rpcMutation(creds, "apps.updateImageConfig", { name: appId, imageConfig });
3340
3317
  if (res.ok) {
3341
3318
  const data = await res.json();
3342
- success(`${key} = ${val} 설정 완료`);
3319
+ success(`${key} = ${val} configured`);
3343
3320
  if (data.imageConfig) {
3344
- info(`현재 설정: ${JSON.stringify(data.imageConfig)}`);
3321
+ info(`Current config: ${JSON.stringify(data.imageConfig)}`);
3345
3322
  }
3346
- info(`${DIM} 다음 이미지 요청부터 적용됩니다. 기존 캐시는 별도 키로 저장됩니다.${RESET}`);
3323
+ info(`${DIM}Takes effect on next image request. Existing cached images use separate keys.${RESET}`);
3347
3324
  } else {
3348
3325
  const errData = await res.json().catch(() => ({}));
3349
- error(`설정 실패: ${errData.error || res.statusText}`);
3326
+ error(`Config update failed: ${errData.error || res.statusText}`);
3350
3327
  }
3351
3328
  break;
3352
3329
  }
@@ -3354,24 +3331,24 @@ ${BOLD}Examples:${RESET}
3354
3331
  case "get": {
3355
3332
  const key = restArgs.find(a => !a.startsWith("-"));
3356
3333
  if (key && key !== "image") {
3357
- error(`알 없는 설정 키: ${key}`);
3358
- info("사용 가능: image");
3334
+ error(`Unknown config key: ${key}`);
3335
+ info("Available: image");
3359
3336
  return;
3360
3337
  }
3361
3338
 
3362
3339
  const res = await rpcQuery(creds, "apps.getImageConfig", { name: appId });
3363
3340
  if (res.ok) {
3364
3341
  const config = await res.json();
3365
- log(`\n${BOLD}${CYAN}이미지 설정${RESET} — ${appId}\n`);
3342
+ log(`\n${BOLD}${CYAN}Image Config${RESET} — ${appId}\n`);
3366
3343
  if (Object.keys(config).length === 0) {
3367
- info("커스텀 설정 없음 (Tier 기본값 사용)");
3344
+ info("No custom config (using tier defaults)");
3368
3345
  } else {
3369
3346
  if (config.autoMaxWidth) log(` ${GREEN}autoMaxWidth${RESET} ${config.autoMaxWidth} px`);
3370
3347
  if (config.autoQuality) log(` ${GREEN}autoQuality${RESET} ${config.autoQuality}`);
3371
3348
  }
3372
3349
  log("");
3373
3350
  } else {
3374
- error("설정 조회 실패");
3351
+ error("Failed to get config");
3375
3352
  }
3376
3353
  break;
3377
3354
  }
@@ -3379,7 +3356,7 @@ ${BOLD}Examples:${RESET}
3379
3356
  case "reset": {
3380
3357
  const key = restArgs.find(a => !a.startsWith("-"));
3381
3358
  if (key && key !== "image") {
3382
- error(`알 없는 설정 키: ${key}`);
3359
+ error(`Unknown config key: ${key}`);
3383
3360
  return;
3384
3361
  }
3385
3362
 
@@ -3389,16 +3366,16 @@ ${BOLD}Examples:${RESET}
3389
3366
  imageConfig: { autoMaxWidth: 0, autoQuality: 0 },
3390
3367
  });
3391
3368
  if (res.ok) {
3392
- success("이미지 설정 초기화 완료 (Tier 기본값 사용)");
3369
+ success("Image config reset (using tier defaults)");
3393
3370
  } else {
3394
- error("초기화 실패");
3371
+ error("Reset failed");
3395
3372
  }
3396
3373
  break;
3397
3374
  }
3398
3375
 
3399
3376
  default:
3400
- error(`알 없는 하위 명령: ${subCmd}`);
3401
- info("사용법: gencow config [set|get|reset] ...");
3377
+ error(`Unknown subcommand: ${subCmd}`);
3378
+ info("Usage: gencow config [set|get|reset] ...");
3402
3379
  }
3403
3380
  },
3404
3381
 
@@ -3407,29 +3384,32 @@ ${BOLD}Examples:${RESET}
3407
3384
  const subCmd = filesArgs[0] || "help";
3408
3385
  const restArgs = filesArgs.slice(1);
3409
3386
 
3410
- // help는 인증/앱 정보 없이도 표시
3387
+ // help
3411
3388
  if (subCmd === "help" || subCmd === "--help" || subCmd === "-h") {
3412
- log(`\n${BOLD}${CYAN}gencow files${RESET} — 파일 관리\n`);
3413
- log(` ${CYAN}upload${RESET} <경로...> [--recursive|-r] 파일 업로드`);
3414
- log(` ${CYAN}list${RESET} 파일 목록`);
3415
- log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y] 파일 삭제`);
3416
- log(` ${CYAN}url${RESET} <storage_id> 서빙 URL 출력`);
3417
- log(`\n ${DIM}옵션:${RESET}`);
3418
- log(` ${DIM}--app, -a <앱이름> 대상 지정 (기본: gencow.json)${RESET}\n`);
3389
+ log(`\n${BOLD}${CYAN}gencow files${RESET} — File management\n`);
3390
+ log(` ${CYAN}upload${RESET} <path...> [--recursive|-r] Upload files`);
3391
+ log(` ${CYAN}list${RESET} List uploaded files`);
3392
+ log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y] Delete a file`);
3393
+ log(` ${CYAN}url${RESET} <storage_id> Get serving URL`);
3394
+ log(`\n ${DIM}Options:${RESET}`);
3395
+ log(` ${DIM}--app, -a <name> Target app (default: from gencow.json)${RESET}`);
3396
+ log(` ${DIM}--prod Target production app${RESET}\n`);
3419
3397
  return;
3420
3398
  }
3421
3399
 
3422
3400
  const creds = requireCreds();
3423
3401
 
3424
- // ID 결정 + --app 플래그 위치 추적 (paths 필터링용)
3402
+ // Resolve appId + track --app/--prod flag positions
3425
3403
  let appId = null;
3426
- const flagIndices = new Set(); // --app와 그 값의 인덱스
3404
+ const isProd = restArgs.includes("--prod");
3405
+ const flagIndices = new Set();
3427
3406
  for (let i = 0; i < restArgs.length; i++) {
3428
3407
  if (restArgs[i] === "--app" || restArgs[i] === "-a") {
3429
3408
  flagIndices.add(i);
3430
3409
  flagIndices.add(i + 1);
3431
3410
  appId = restArgs[++i];
3432
3411
  }
3412
+ if (restArgs[i] === "--prod") flagIndices.add(i);
3433
3413
  }
3434
3414
 
3435
3415
  if (!appId) {
@@ -3437,11 +3417,19 @@ ${BOLD}Examples:${RESET}
3437
3417
  if (existsSync(gencowJsonPath)) {
3438
3418
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
3439
3419
  appId = gencowJson.appId || gencowJson.appName;
3420
+ if (isProd && gencowJson.prodApp) {
3421
+ appId = gencowJson.prodApp;
3422
+ }
3440
3423
  }
3441
3424
  }
3442
3425
 
3426
+ if (isProd && appId && !appId.endsWith("-prod")) {
3427
+ error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
3428
+ return;
3429
+ }
3430
+
3443
3431
  if (!appId) {
3444
- error(" ID를 찾을 없습니다. gencow deploy를 먼저 실행하세요.");
3432
+ error("App not found. Run 'gencow dev' first.");
3445
3433
  return;
3446
3434
  }
3447
3435
 
@@ -3472,11 +3460,11 @@ ${BOLD}Examples:${RESET}
3472
3460
 
3473
3461
  // 로컬 크기 검증 (서버 왕복 방지)
3474
3462
  if (stat.size > MAX_FILE_SIZE) {
3475
- error(`${fileName}: ${fmtFileSize(stat.size)} — 50MB 제한 초과`);
3463
+ error(`${fileName}: ${fmtFileSize(stat.size)} — exceeds 50MB limit`);
3476
3464
  return null;
3477
3465
  }
3478
3466
 
3479
- info(`${fileName} (${fmtFileSize(stat.size)}) 업로드 중...`);
3467
+ info(`Uploading ${fileName} (${fmtFileSize(stat.size)})...`);
3480
3468
 
3481
3469
  const fileBuffer = readFileSync(filePath);
3482
3470
  const blob = new Blob([fileBuffer]);
@@ -3533,7 +3521,7 @@ ${BOLD}Examples:${RESET}
3533
3521
  const paths = restArgs.filter((a, i) => !a.startsWith("-") && !flagIndices.has(i));
3534
3522
 
3535
3523
  if (paths.length === 0) {
3536
- error("사용법: gencow files upload <파일|폴더 경로...> [--recursive|-r]");
3524
+ error("Usage: gencow files upload <file|dir path...> [--recursive|-r]");
3537
3525
  return;
3538
3526
  }
3539
3527
 
@@ -3541,13 +3529,13 @@ ${BOLD}Examples:${RESET}
3541
3529
  for (const p of paths) {
3542
3530
  const resolved = resolve(process.cwd(), p);
3543
3531
  if (!existsSync(resolved)) {
3544
- error(`파일을 찾을 없습니다: ${p}`);
3532
+ error(`File not found: ${p}`);
3545
3533
  continue;
3546
3534
  }
3547
3535
  const stat = statSync(resolved);
3548
3536
  if (stat.isDirectory()) {
3549
3537
  if (!recursive) {
3550
- error(`${p} 디렉토리입니다. --recursive (-r) 옵션을 사용하세요.`);
3538
+ error(`${p} is a directory. Use --recursive (-r) flag.`);
3551
3539
  continue;
3552
3540
  }
3553
3541
  filesToUpload.push(...collectFiles(resolved));
@@ -3557,12 +3545,12 @@ ${BOLD}Examples:${RESET}
3557
3545
  }
3558
3546
 
3559
3547
  if (filesToUpload.length === 0) {
3560
- error("업로드할 파일이 없습니다.");
3548
+ error("No files to upload.");
3561
3549
  return;
3562
3550
  }
3563
3551
 
3564
- log(`\n${BOLD}${CYAN}파일 업로드${RESET} — ${appId}\n`);
3565
- info(`${filesToUpload.length} 파일 업로드 시작...\n`);
3552
+ log(`\n${BOLD}${CYAN}File Upload${RESET} — ${appId}\n`);
3553
+ info(`Uploading ${filesToUpload.length} files...\n`);
3566
3554
 
3567
3555
  let uploaded = 0;
3568
3556
  let failed = 0;
@@ -3573,14 +3561,14 @@ ${BOLD}Examples:${RESET}
3573
3561
  }
3574
3562
 
3575
3563
  log("");
3576
- if (uploaded > 0) success(`${uploaded} 파일 업로드 완료`);
3577
- if (failed > 0) warn(`${failed} 파일 실패`);
3564
+ if (uploaded > 0) success(`${uploaded} files uploaded`);
3565
+ if (failed > 0) warn(`${failed} files failed`);
3578
3566
  break;
3579
3567
  }
3580
3568
 
3581
3569
  case "list":
3582
3570
  case "ls": {
3583
- log(`\n${BOLD}${CYAN}파일 목록${RESET} — ${appId}\n`);
3571
+ log(`\n${BOLD}${CYAN}File List${RESET} — ${appId}\n`);
3584
3572
 
3585
3573
  const res = await fetch(
3586
3574
  `${creds.platformUrl}/platform/files/list`,
@@ -3596,13 +3584,13 @@ ${BOLD}Examples:${RESET}
3596
3584
 
3597
3585
  const data = await res.json().catch(() => []);
3598
3586
  if (!res.ok) {
3599
- error(data.error || "파일 목록 조회 실패");
3587
+ error(data.error || "Failed to list files");
3600
3588
  return;
3601
3589
  }
3602
3590
 
3603
3591
  const files = Array.isArray(data) ? data : [];
3604
3592
  if (files.length === 0) {
3605
- info("저장된 파일이 없습니다.");
3593
+ info("No files stored.");
3606
3594
  log("");
3607
3595
  return;
3608
3596
  }
@@ -3611,7 +3599,7 @@ ${BOLD}Examples:${RESET}
3611
3599
  const idWidth = Math.max(12, ...files.map(f => (f.storage_id || "").length));
3612
3600
  const nameWidth = Math.max(8, ...files.map(f => (f.name || "").length).map(l => Math.min(l, 40)));
3613
3601
 
3614
- log(` ${DIM}${"ID".padEnd(idWidth)} ${"이름".padEnd(nameWidth)} ${"크기".padStart(10)} ${"소스".padEnd(10)} 업로드 시각${RESET}`);
3602
+ log(` ${DIM}${"ID".padEnd(idWidth)} ${"Name".padEnd(nameWidth)} ${"Size".padStart(10)} ${"Source".padEnd(10)} Uploaded${RESET}`);
3615
3603
  log(` ${DIM}${"─".repeat(idWidth)} ${"─".repeat(nameWidth)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(16)}${RESET}`);
3616
3604
 
3617
3605
  for (const f of files) {
@@ -3621,7 +3609,7 @@ ${BOLD}Examples:${RESET}
3621
3609
  const date = f.created_at ? fmtDate(f.created_at) : "-";
3622
3610
  log(` ${(f.storage_id || "").padEnd(idWidth)} ${name.padEnd(nameWidth)} ${size} ${source} ${date}`);
3623
3611
  }
3624
- log(`\n ${DIM} ${files.length} 파일${RESET}\n`);
3612
+ log(`\n ${DIM}Total: ${files.length} files${RESET}\n`);
3625
3613
  break;
3626
3614
  }
3627
3615
 
@@ -3629,7 +3617,7 @@ ${BOLD}Examples:${RESET}
3629
3617
  case "rm": {
3630
3618
  const storageId = restArgs.find((a, i) => !a.startsWith("-") && !flagIndices.has(i));
3631
3619
  if (!storageId) {
3632
- error("사용법: gencow files delete <storage_id>");
3620
+ error("Usage: gencow files delete <storage_id>");
3633
3621
  return;
3634
3622
  }
3635
3623
 
@@ -3645,7 +3633,7 @@ ${BOLD}Examples:${RESET}
3645
3633
  });
3646
3634
  });
3647
3635
  if (answer !== "y" && answer !== "yes") {
3648
- info("삭제를 취소했습니다.");
3636
+ info("Delete cancelled.");
3649
3637
  return;
3650
3638
  }
3651
3639
  }
@@ -3664,9 +3652,9 @@ ${BOLD}Examples:${RESET}
3664
3652
 
3665
3653
  const data = await res.json().catch(() => ({}));
3666
3654
  if (res.ok && data.success) {
3667
- success(`파일 삭제 완료: ${storageId}`);
3655
+ success(`File deleted: ${storageId}`);
3668
3656
  } else {
3669
- error(`삭제 실패: ${data.error || res.statusText}`);
3657
+ error(`Delete failed: ${data.error || res.statusText}`);
3670
3658
  }
3671
3659
  break;
3672
3660
  }
@@ -3674,7 +3662,7 @@ ${BOLD}Examples:${RESET}
3674
3662
  case "url": {
3675
3663
  const storageId = restArgs.find((a, i) => !a.startsWith("-") && !flagIndices.has(i));
3676
3664
  if (!storageId) {
3677
- error("사용법: gencow files url <storage_id>");
3665
+ error("Usage: gencow files url <storage_id>");
3678
3666
  return;
3679
3667
  }
3680
3668
  log(getFileUrl(storageId));
@@ -3682,14 +3670,14 @@ ${BOLD}Examples:${RESET}
3682
3670
  }
3683
3671
 
3684
3672
  default:
3685
- if (subCmd !== "help") error(`알 없는 하위 명령: ${subCmd}`);
3686
- log(`\n${BOLD}${CYAN}gencow files${RESET} — 파일 관리\n`);
3687
- log(` ${CYAN}upload${RESET} <경로...> [--recursive|-r] 파일 업로드`);
3688
- log(` ${CYAN}list${RESET} 파일 목록`);
3689
- log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y] 파일 삭제`);
3690
- log(` ${CYAN}url${RESET} <storage_id> 서빙 URL 출력`);
3691
- log(`\n ${DIM}옵션:${RESET}`);
3692
- log(` ${DIM}--app, -a <앱이름> 대상 지정 (기본: gencow.json)${RESET}\n`);
3673
+ if (subCmd !== "help") error(`Unknown subcommand: ${subCmd}`);
3674
+ log(`\n${BOLD}${CYAN}gencow files${RESET} — File management\n`);
3675
+ log(` ${CYAN}upload${RESET} <path...> [--recursive|-r] Upload files`);
3676
+ log(` ${CYAN}list${RESET} List files`);
3677
+ log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y] Delete file`);
3678
+ log(` ${CYAN}url${RESET} <storage_id> Get serving URL`);
3679
+ log(`\n ${DIM}Options:${RESET}`);
3680
+ log(` ${DIM}--app, -a <name> Target app (default: gencow.json)${RESET}\n`);
3693
3681
  }
3694
3682
  },
3695
3683
 
@@ -3714,7 +3702,7 @@ ${BOLD}Examples:${RESET}
3714
3702
  }
3715
3703
 
3716
3704
  if (!appId) {
3717
- error(" ID를 찾을 없습니다. gencow deploy 먼저 실행하세요.");
3705
+ error("App not found. Run gencow deploy first.");
3718
3706
  return;
3719
3707
  }
3720
3708
 
@@ -3736,7 +3724,7 @@ ${BOLD}Examples:${RESET}
3736
3724
  case "ls": {
3737
3725
  const res = await rpcQuery(creds, "backup.list", { appName: appId });
3738
3726
  if (!res.ok) {
3739
- error(`백업 목록 조회 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3727
+ error(`Failed to fetch backup list: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3740
3728
  return;
3741
3729
  }
3742
3730
  const data = await res.json();
@@ -3746,8 +3734,8 @@ ${BOLD}Examples:${RESET}
3746
3734
  log(`\n${BOLD}${CYAN}Database Backups${RESET} — ${appId} ${DIM}(Plan: ${data.plan || "free"})${RESET}\n`);
3747
3735
 
3748
3736
  if (backups.length === 0) {
3749
- info("백업이 없습니다.");
3750
- info(`${GREEN}gencow backup create${RESET} — 수동 백업 생성`);
3737
+ info("No backups found.");
3738
+ info(`${GREEN}gencow backup create${RESET} — create a manual backup`);
3751
3739
  } else {
3752
3740
  for (const b of backups) {
3753
3741
  const statusColor = b.status === "completed" ? GREEN : b.status === "failed" ? RED : YELLOW;
@@ -3765,19 +3753,19 @@ ${BOLD}Examples:${RESET}
3765
3753
 
3766
3754
  const res = await rpcMutation(creds, "backup.create", { appName: appId, note });
3767
3755
  if (!res.ok) {
3768
- error(`백업 생성 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3756
+ error(`Backup creation failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3769
3757
  return;
3770
3758
  }
3771
3759
  const backup = await res.json();
3772
- success(`백업 생성 완료 #${backup.id} ${fmtSize(backup.fileSize || 0)}`);
3760
+ success(`Backup created #${backup.id} ${fmtSize(backup.fileSize || 0)}`);
3773
3761
  break;
3774
3762
  }
3775
3763
 
3776
3764
  case "restore": {
3777
3765
  const backupId = parseInt(restArgs[0]);
3778
3766
  if (!backupId || isNaN(backupId)) {
3779
- error("백업 ID 지정하세요: gencow backup restore <id>");
3780
- info("gencow backup list ID를 확인하세요.");
3767
+ error("Specify backup ID: gencow backup restore <id>");
3768
+ info("Run gencow backup list to check IDs.");
3781
3769
  return;
3782
3770
  }
3783
3771
 
@@ -3792,7 +3780,7 @@ ${BOLD}Examples:${RESET}
3792
3780
  });
3793
3781
 
3794
3782
  if (answer !== "y" && answer !== "yes") {
3795
- info("복원이 취소되었습니다.");
3783
+ info("Restore cancelled.");
3796
3784
  return;
3797
3785
  }
3798
3786
 
@@ -3800,10 +3788,10 @@ ${BOLD}Examples:${RESET}
3800
3788
 
3801
3789
  const res = await rpcMutation(creds, "backup.restore", { appName: appId, backupId });
3802
3790
  if (!res.ok) {
3803
- error(`복원 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3791
+ error(`Restore failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3804
3792
  return;
3805
3793
  }
3806
- success("데이터베이스 복원 완료. 앱이 재시작되었습니다.");
3794
+ success("Database restored. App restarted.");
3807
3795
  break;
3808
3796
  }
3809
3797
 
@@ -3811,23 +3799,23 @@ ${BOLD}Examples:${RESET}
3811
3799
  case "rm": {
3812
3800
  const backupId = parseInt(restArgs[0]);
3813
3801
  if (!backupId || isNaN(backupId)) {
3814
- error("백업 ID 지정하세요: gencow backup delete <id>");
3802
+ error("Specify backup ID: gencow backup delete <id>");
3815
3803
  return;
3816
3804
  }
3817
3805
 
3818
3806
  const res = await rpcMutation(creds, "backup.delete", { appName: appId, backupId });
3819
3807
  if (!res.ok) {
3820
- error(`삭제 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3808
+ error(`Delete failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3821
3809
  return;
3822
3810
  }
3823
- success(`백업 #${backupId} 삭제 완료`);
3811
+ success(`Backup #${backupId} deleted`);
3824
3812
  break;
3825
3813
  }
3826
3814
 
3827
3815
  case "download": {
3828
3816
  const backupId = parseInt(restArgs[0]);
3829
3817
  if (!backupId || isNaN(backupId)) {
3830
- error("백업 ID 지정하세요: gencow backup download <id>");
3818
+ error("Specify backup ID: gencow backup download <id>");
3831
3819
  return;
3832
3820
  }
3833
3821
 
@@ -3837,7 +3825,7 @@ ${BOLD}Examples:${RESET}
3837
3825
  `/platform/apps/${appId}/backups/${backupId}/download`
3838
3826
  );
3839
3827
  if (!res.ok) {
3840
- error(`다운로드 실패: ${(await res.json?.().catch(() => ({}))).error || res.statusText}`);
3828
+ error(`Download failed: ${(await res.json?.().catch(() => ({}))).error || res.statusText}`);
3841
3829
  return;
3842
3830
  }
3843
3831
 
@@ -3845,12 +3833,12 @@ ${BOLD}Examples:${RESET}
3845
3833
  const { writeFile } = await import("fs/promises");
3846
3834
  const buffer = Buffer.from(await res.arrayBuffer());
3847
3835
  await writeFile(filename, buffer);
3848
- success(`저장 완료: ${filename} (${fmtSize(buffer.length)})`);
3836
+ success(`Saved: ${filename} (${fmtSize(buffer.length)})`);
3849
3837
  break;
3850
3838
  }
3851
3839
 
3852
3840
  default:
3853
- error(`알 없는 하위 명령: ${subCmd}`);
3841
+ error(`Unknown sub-command: ${subCmd}`);
3854
3842
  log(`\n ${BOLD}Usage:${RESET}`);
3855
3843
  log(` gencow backup list List all backups`);
3856
3844
  log(` gencow backup create [note] Create a manual backup`);
@@ -3881,7 +3869,7 @@ ${BOLD}Examples:${RESET}
3881
3869
  }
3882
3870
 
3883
3871
  if (!appId) {
3884
- error(" ID를 찾을 없습니다. gencow deploy 먼저 실행하세요.");
3872
+ error("App not found. Run gencow deploy first.");
3885
3873
  return;
3886
3874
  }
3887
3875
 
@@ -3889,8 +3877,8 @@ ${BOLD}Examples:${RESET}
3889
3877
  case "set": {
3890
3878
  const domain = restArgs.find(a => !a.startsWith("-") && a.includes("."));
3891
3879
  if (!domain) {
3892
- error("사용법: gencow domain set <domain>");
3893
- info(" 예: gencow domain set myapp.com");
3880
+ error("Usage: gencow domain set <domain>");
3881
+ info(" Example: gencow domain set myapp.com");
3894
3882
  return;
3895
3883
  }
3896
3884
 
@@ -3902,32 +3890,32 @@ ${BOLD}Examples:${RESET}
3902
3890
 
3903
3891
  const data = await res.json();
3904
3892
  if (!res.ok) {
3905
- error(`도메인 등록 실패: ${data.error}`);
3893
+ error(`Domain registration failed: ${data.error}`);
3906
3894
  return;
3907
3895
  }
3908
3896
 
3909
- log(`\n${BOLD}${CYAN}🌐 커스텀 도메인 등록${RESET}\n`);
3897
+ log(`\n${BOLD}${CYAN}🌐 Custom Domain Registered${RESET}\n`);
3910
3898
  success(`${domain} → ${appId}`);
3911
3899
  log("");
3912
3900
 
3913
3901
  if (data.status === "active") {
3914
3902
  info(`DNS: ${GREEN}✅ ${data.dns.detail}${RESET}`);
3915
- info(`TLS: 인증서 자동 발급 대기 (첫 접속 시)`);
3903
+ info(`TLS: Certificate auto-provisioned on first request`);
3916
3904
  } else {
3917
- warn(`DNS: ⏳ 아직 확인되지 않았습니다.`);
3905
+ warn(`DNS: ⏳ Not yet verified.`);
3918
3906
  log("");
3919
- log(` 아래 DNS 레코드를 설정하세요:`);
3907
+ log(` Set up the following DNS record:`);
3920
3908
  log(` ┌──────────────────────────────────────────┐`);
3921
3909
  log(` │ ${BOLD}Type: A${RESET} │`);
3922
3910
  log(` │ Name: ${domain.padEnd(33)}│`);
3923
3911
  log(` │ Value: ${data.serverIp.padEnd(33)}│`);
3924
3912
  log(` │ │`);
3925
- log(` │ ${DIM}또는 CNAME:${RESET} │`);
3913
+ log(` │ ${DIM}Or CNAME:${RESET} │`);
3926
3914
  log(` │ Value: ${data.cname.padEnd(33)}│`);
3927
3915
  log(` └──────────────────────────────────────────┘`);
3928
3916
  }
3929
3917
  log("");
3930
- info(`DNS 설정 후 확인: ${DIM}gencow domain status${RESET}`);
3918
+ info(`After DNS setup: ${DIM}gencow domain status${RESET}`);
3931
3919
  log("");
3932
3920
  break;
3933
3921
  }
@@ -3942,14 +3930,14 @@ ${BOLD}Examples:${RESET}
3942
3930
 
3943
3931
  const data = await res.json();
3944
3932
  if (!res.ok) {
3945
- error(`도메인 해제 실패: ${data.error}`);
3933
+ error(`Domain removal failed: ${data.error}`);
3946
3934
  return;
3947
3935
  }
3948
3936
 
3949
3937
  if (data.domain) {
3950
- success(`도메인 ${data.domain} 해제 완료`);
3938
+ success(`Domain ${data.domain} disconnected`);
3951
3939
  } else {
3952
- info("설정된 커스텀 도메인이 없습니다.");
3940
+ info("No custom domain configured.");
3953
3941
  }
3954
3942
  break;
3955
3943
  }
@@ -3962,26 +3950,26 @@ ${BOLD}Examples:${RESET}
3962
3950
 
3963
3951
  const data = await res.json();
3964
3952
  if (!res.ok) {
3965
- error(`도메인 상태 조회 실패: ${data.error}`);
3953
+ error(`Domain status check failed: ${data.error}`);
3966
3954
  return;
3967
3955
  }
3968
3956
 
3969
- log(`\n${BOLD}${CYAN}🌐 커스텀 도메인 상태${RESET} — ${appId}\n`);
3957
+ log(`\n${BOLD}${CYAN}🌐 Custom Domain Status${RESET} — ${appId}\n`);
3970
3958
 
3971
3959
  if (!data.domain) {
3972
- info("커스텀 도메인이 설정되지 않았습니다.");
3973
- info(`설정: ${DIM}gencow domain set <domain>${RESET}`);
3960
+ info("No custom domain configured.");
3961
+ info(`Set up: ${DIM}gencow domain set <domain>${RESET}`);
3974
3962
  } else {
3975
- info(`도메인: ${BOLD}${data.domain}${RESET}`);
3963
+ info(`Domain: ${BOLD}${data.domain}${RESET}`);
3976
3964
 
3977
3965
  if (data.status === "active") {
3978
3966
  info(`DNS: ${GREEN}✅ ${data.dns.detail}${RESET}`);
3979
- info(`TLS: ${GREEN}✅ Let's Encrypt 자동 발급${RESET}`);
3967
+ info(`TLS: ${GREEN}✅ Let's Encrypt auto-provisioned${RESET}`);
3980
3968
  } else if (data.status === "pending") {
3981
- warn(`DNS: ⏳ 확인 대기 중`);
3982
- info(`TLS: DNS 확인 자동 발급`);
3969
+ warn(`DNS: ⏳ Verification pending`);
3970
+ info(`TLS: Auto-provisioned after DNS verification`);
3983
3971
  } else {
3984
- error(`상태: ${data.status}`);
3972
+ error(`Status: ${data.status}`);
3985
3973
  }
3986
3974
  }
3987
3975
  log("");
@@ -4055,7 +4043,7 @@ console.log("GENCOW_API_JSON=" + JSON.stringify({
4055
4043
  }));
4056
4044
  process.exit(0);
4057
4045
  `;
4058
- info("함수 목록 추출 중...");
4046
+ info("Extracting function list...");
4059
4047
  try {
4060
4048
  writeFileSync(extractTsPath, script);
4061
4049
  let outStr;
@@ -4119,10 +4107,10 @@ process.exit(0);
4119
4107
  log(` ${DIM}Queries: ${queries.join(", ")}${RESET}`);
4120
4108
  log(` ${DIM}Mutations: ${mutations.join(", ")}${RESET}`);
4121
4109
  log("");
4122
- info(`사용법: import { api } from "@/gencow/api";`);
4110
+ info(`Usage: import { api } from "@/gencow/api";`);
4123
4111
  } catch (e) {
4124
4112
  try { unlinkSync(extractTsPath); } catch { }
4125
- error(`Codegen 실패: ${e.message}`);
4113
+ error(`Codegen failed: ${e.message}`);
4126
4114
  process.exit(1);
4127
4115
  }
4128
4116
  },
@@ -4278,57 +4266,7 @@ process.exit(0);
4278
4266
  log(` Usage: gencow app [list|create|delete|status]`);
4279
4267
  },
4280
4268
 
4281
- // ── deploy (remote mode) ──────────────────────────
4282
- async "remote:deploy"() {
4283
- const creds = requireCreds();
4284
- const config = loadConfig();
4285
- const appName = config.deploy?.app || creds.currentApp;
4286
- if (!appName) {
4287
- error("No app specified. Run: gencow app create <name>");
4288
- process.exit(1);
4289
- }
4290
-
4291
- log(`\n${BOLD}${CYAN}Gencow Deploy → ${appName}${RESET}\n`);
4292
-
4293
- const { functionsDir } = config;
4294
- const absoluteFunctions = resolve(process.cwd(), functionsDir);
4295
- if (!existsSync(absoluteFunctions)) {
4296
- error(`Functions dir not found: ${absoluteFunctions}`);
4297
- process.exit(1);
4298
- }
4299
-
4300
- // 1. Bundle gencow/ folder to tar.gz
4301
- info(`Bundling ${functionsDir}...`);
4302
- const { create: tarCreate } = await import("tar");
4303
- const chunks = [];
4304
- await new Promise((resolve, reject) => {
4305
- tarCreate({ cwd: absoluteFunctions, gzip: true }, ["."])
4306
- .on("data", c => chunks.push(c))
4307
- .on("end", resolve)
4308
- .on("error", reject);
4309
- });
4310
- const bundle = Buffer.concat(chunks);
4311
- success(`Bundle ready (${(bundle.length / 1024).toFixed(1)} KB)`);
4312
-
4313
-
4314
- // 2. Upload to platform
4315
- info(`Uploading to ${creds.platformUrl}...`);
4316
- const form = new FormData();
4317
- form.append("bundle", new Blob([bundle], { type: "application/gzip" }), "bundle.tar.gz");
4318
- const res = await platformFetch(creds, `/platform/apps/${appName}/deploy`, {
4319
- method: "POST",
4320
- body: form,
4321
- });
4322
- const data = await res.json();
4323
- if (!res.ok) { error(data.error); process.exit(1); }
4324
-
4325
- success("Deployed!");
4326
- log(`
4327
- ${GREEN}▸${RESET} URL: ${data.url}
4328
- ${GREEN}▸${RESET} Bundle: ${data.bundleHash}
4329
- ${DIM}gencow logs — follow logs${RESET}
4330
- `);
4331
- },
4269
+ // ── remote:deploy — REMOVED (legacy, use `gencow deploy` instead) ──
4332
4270
 
4333
4271
  // ── logs ───────────────────────────────────────────
4334
4272
  async logs(...args) {
@@ -4427,13 +4365,13 @@ process.exit(0);
4427
4365
  }
4428
4366
  followFailures++;
4429
4367
  if (followFailures > FOLLOW_MAX_FAILURES) {
4430
- error(`연결 불가 상태를 확인하세요.`);
4431
- info(`서버가 실행 중인지 확인: gencow dev`);
4368
+ error(`Connection failedcheck app status.`);
4369
+ info(`Check if the server is running: gencow dev`);
4432
4370
  process.exit(1);
4433
4371
  }
4434
4372
  const delay = Math.min(FOLLOW_BASE_RECONNECT_MS * Math.pow(2, followFailures - 1), FOLLOW_MAX_RECONNECT_MS);
4435
4373
  const delaySec = (delay / 1000).toFixed(0);
4436
- warn(`연결 끊김 — ${delaySec} 후 재연결... (${followFailures}/${FOLLOW_MAX_FAILURES})`);
4374
+ warn(`Disconnectedreconnecting in ${delaySec}s... (${followFailures}/${FOLLOW_MAX_FAILURES})`);
4437
4375
  scheduleFollowReconnect(delay);
4438
4376
  });
4439
4377
  }
@@ -4675,7 +4613,7 @@ process.exit(0);
4675
4613
 
4676
4614
  // 앱이 없으면 자동 생성 (deploy의 auto-create 패턴 재사용)
4677
4615
  if (!appName) {
4678
- info("앱이 없습니다. 자동 생성 중...");
4616
+ info("No app found. Creating...");
4679
4617
 
4680
4618
  // displayName: gencow.config.ts의 name 또는 프로젝트 폴더명
4681
4619
  let displayName = null;
@@ -4690,7 +4628,7 @@ process.exit(0);
4690
4628
  const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
4691
4629
  if (!createRes.ok) {
4692
4630
  const createErr = await createRes.json().catch(() => ({}));
4693
- error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
4631
+ error(`App creation failed: ${createErr.error || createRes.statusText}`);
4694
4632
  process.exit(1);
4695
4633
  }
4696
4634
 
@@ -4698,11 +4636,11 @@ process.exit(0);
4698
4636
  appName = createData.appId || createData.name;
4699
4637
 
4700
4638
  if (!appName) {
4701
- error(" 생성 응답에 appId가 없습니다.");
4639
+ error("App creation response missing appId.");
4702
4640
  process.exit(1);
4703
4641
  }
4704
4642
 
4705
- success(`앱 "${appName}" 생성 완료`);
4643
+ success(`App "${appName}" created`);
4706
4644
 
4707
4645
  // gencow.json 저장 (deploy와 동일 패턴)
4708
4646
  const gencowJsonPath = resolve(process.cwd(), "gencow.json");
@@ -4711,13 +4649,13 @@ process.exit(0);
4711
4649
  displayName,
4712
4650
  platformUrl: creds.platformUrl,
4713
4651
  }, null, 2));
4714
- info(`${DIM}gencow.json 생성됨 (appId: ${appName})${RESET}`);
4652
+ info(`${DIM}gencow.json created (appId: ${appName})${RESET}`);
4715
4653
 
4716
4654
  // .env에 VITE_API_URL 자동 설정
4717
4655
  updateEnvLocalUrl(getAppUrl(appName, creds.platformUrl));
4718
4656
 
4719
4657
  // 프로비저닝 대기
4720
- info("프로비저닝 대기 중...");
4658
+ info("Waiting for provisioning...");
4721
4659
  await new Promise(r => setTimeout(r, 3000));
4722
4660
  }
4723
4661
 
@@ -4736,9 +4674,9 @@ process.exit(0);
4736
4674
  const isSchema = reason !== "initial" && /schema.*\.ts$/i.test(reason);
4737
4675
 
4738
4676
  if (reason === "initial") {
4739
- log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} 초기 배포 중...`);
4677
+ log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} Initial deploy...`);
4740
4678
  } else {
4741
- log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} ${reason} 변경 감지배포 중...`);
4679
+ log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} ${reason} change detecteddeploying...`);
4742
4680
  }
4743
4681
 
4744
4682
  // 스키마 변경 또는 초기 배포 시 마이그레이션 생성
@@ -4746,7 +4684,7 @@ process.exit(0);
4746
4684
  const schemaExists = existsSync(resolve(process.cwd(), functionsDir, "schema.ts"));
4747
4685
  if (schemaExists && (isSchema || reason === "initial")) {
4748
4686
  if (isSchema) {
4749
- log(`${DIM}${ts}${RESET} ${YELLOW}[migrate]${RESET} 스키마 변경 감지마이그레이션 생성 중...`);
4687
+ log(`${DIM}${ts}${RESET} ${YELLOW}[migrate]${RESET} Schema change detectedgenerating migrations...`);
4750
4688
  }
4751
4689
  try {
4752
4690
  const { execSync } = await import("child_process");
@@ -4756,13 +4694,13 @@ process.exit(0);
4756
4694
  env: { ...process.env, ...dk.env },
4757
4695
  stdio: "inherit",
4758
4696
  });
4759
- log(`${DIM}${ts}${RESET} ${GREEN}[migrate]${RESET} ✔ 마이그레이션 생성 완료`);
4697
+ log(`${DIM}${ts}${RESET} ${GREEN}[migrate]${RESET} ✔ Migrations generated`);
4760
4698
  } catch (e) {
4761
4699
  const msg = e.stderr?.toString() || e.message || "";
4762
4700
  if (msg.includes("No schema changes") || msg.includes("nothing to migrate")) {
4763
4701
  // 스키마 변경 없음 — 정상
4764
4702
  } else {
4765
- warn(`[migrate] 마이그레이션 생성 실패 (무시 가능): ${msg.split("\n")[0]}`);
4703
+ warn(`[migrate] Migration generation failed (non-critical): ${msg.split("\n")[0]}`);
4766
4704
  }
4767
4705
  }
4768
4706
  }
@@ -4801,7 +4739,7 @@ process.exit(0);
4801
4739
 
4802
4740
  if (!res.ok) {
4803
4741
  const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
4804
- log(`${DIM}${ts2}${RESET} ${RED}[deploy]${RESET} ✗ 배포 실패!`);
4742
+ log(`${DIM}${ts2}${RESET} ${RED}[deploy]${RESET} ✗ Deploy failed!`);
4805
4743
 
4806
4744
  // 에러 메시지에서 파일:라인 정보 추출
4807
4745
  const errMsg = data.error || "Unknown error";
@@ -4818,13 +4756,13 @@ process.exit(0);
4818
4756
 
4819
4757
  // 스키마 관련 에러 힌트
4820
4758
  if (errMsg.includes("does not exist") || errMsg.includes("relation")) {
4821
- log(` ${YELLOW}💡 스키마가 변경되었나요? schema.ts 수정하면 자동으로 마이그레이션이 실행됩니다.${RESET}`);
4759
+ log(` ${YELLOW}💡 Schema changed? Migrations run automatically when schema.ts changes.${RESET}`);
4822
4760
  }
4823
4761
  return;
4824
4762
  }
4825
4763
 
4826
4764
  const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
4827
- log(`${DIM}${ts2}${RESET} ${GREEN}[deploy]${RESET} ✔ 배포 완료 ${DIM}(${sizeKB}KB, ${elapsed}s)${RESET}`);
4765
+ log(`${DIM}${ts2}${RESET} ${GREEN}[deploy]${RESET} ✔ Deploy complete ${DIM}(${sizeKB}KB, ${elapsed}s)${RESET}`);
4828
4766
  }
4829
4767
 
4830
4768
  // ── 로그 포맷팅 함수 ────────────────────────────────
@@ -4857,7 +4795,7 @@ process.exit(0);
4857
4795
  try {
4858
4796
  logWs = new WS(wsUrl);
4859
4797
  } catch (e) {
4860
- warn(`[log] WebSocket 연결 실패: ${e.message}`);
4798
+ warn(`[log] WebSocket connection failed: ${e.message}`);
4861
4799
  scheduleReconnect();
4862
4800
  return;
4863
4801
  }
@@ -4865,7 +4803,7 @@ process.exit(0);
4865
4803
  logWs.on("open", () => {
4866
4804
  // 카운터 리셋은 close 이벤트에서 안정 연결 판정 후 수행
4867
4805
  logWsOpenedAt = Date.now();
4868
- log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${GREEN}[log]${RESET} 로그 스트리밍 연결됨`);
4806
+ log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${GREEN}[log]${RESET} Log streaming connected`);
4869
4807
  logWs.send(JSON.stringify({ type: "log:subscribe" }));
4870
4808
  });
4871
4809
 
@@ -4889,13 +4827,13 @@ process.exit(0);
4889
4827
  }
4890
4828
  reconnectFailures++;
4891
4829
  if (reconnectFailures > MAX_RECONNECT_FAILURES) {
4892
- warn(`[log] 로그 스트리밍 ${MAX_RECONNECT_FAILURES} 연속 실패. 재연결 중단.`);
4893
- info(`수동 확인: gencow deploy logs`);
4830
+ warn(`[log] Log streaming failed ${MAX_RECONNECT_FAILURES} times. Giving up.`);
4831
+ info(`Check manually: gencow deploy logs`);
4894
4832
  return;
4895
4833
  }
4896
4834
  const delay = Math.min(BASE_RECONNECT_MS * Math.pow(2, reconnectFailures - 1), MAX_RECONNECT_MS);
4897
4835
  const delaySec = (delay / 1000).toFixed(0);
4898
- log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${YELLOW}[log]${RESET} 연결 끊김 — ${delaySec} 후 재연결... (${reconnectFailures}/${MAX_RECONNECT_FAILURES})`);
4836
+ log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${YELLOW}[log]${RESET} Disconnectedreconnecting in ${delaySec}s... (${reconnectFailures}/${MAX_RECONNECT_FAILURES})`);
4899
4837
  scheduleReconnect(delay);
4900
4838
  });
4901
4839
  }
@@ -5178,7 +5116,7 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
5178
5116
  // ── 모든 컴포넌트 설치 후 README 1회 업데이트 ───────────
5179
5117
  await updateReadme(loadConfig());
5180
5118
 
5181
- log(`\n${GREEN}✔${RESET} ${BOLD}완료!${RESET} ${DIM}gencow dev 실행하면 바로 사용할 수 있습니다.${RESET}`);
5119
+ log(`\n${GREEN}✔${RESET} ${BOLD}Done!${RESET} ${DIM}Run gencow dev to get started.${RESET}`);
5182
5120
 
5183
5121
  // ── 컴포넌트별 가이드 메시지 ─────────────────────────────
5184
5122
  for (const key of ordered) {
@@ -5200,11 +5138,11 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
5200
5138
  const label = config.label || name.toUpperCase();
5201
5139
 
5202
5140
  if (config.notReady) {
5203
- log(` ${YELLOW}⚠${RESET} ${BOLD}[${label}]${RESET} ${DIM}아직 준비 중입니다 (Coming Soon)${RESET}`);
5141
+ log(` ${YELLOW}⚠${RESET} ${BOLD}[${label}]${RESET} ${DIM}Coming Soon${RESET}`);
5204
5142
  return;
5205
5143
  }
5206
5144
 
5207
- log(` ${CYAN}▸${RESET} ${BOLD}[${label}]${RESET} 설치 중...`);
5145
+ log(` ${CYAN}▸${RESET} ${BOLD}[${label}]${RESET} Installing...`);
5208
5146
 
5209
5147
  // 1. npm 의존성 설치 (패키지 매니저 자동 감지)
5210
5148
  if (config.deps && config.deps.length > 0) {
@@ -5221,9 +5159,9 @@ if (config.deps && config.deps.length > 0) {
5221
5159
  stdio: "pipe",
5222
5160
  cwd: process.cwd(),
5223
5161
  });
5224
- log(` ${GREEN}✓${RESET} ${config.deps.join(", ")} 패키지 설치`);
5162
+ log(` ${GREEN}✓${RESET} ${config.deps.join(", ")} packages installed`);
5225
5163
  } catch (e) {
5226
- log(` ${YELLOW}⚠${RESET} 패키지 설치 실패 — ${DIM}bun add ${config.deps.join(" ")} 를 직접 실행하세요${RESET}`);
5164
+ log(` ${YELLOW}⚠${RESET} Package install failed — ${DIM}run bun add ${config.deps.join(" ")} manually${RESET}`);
5227
5165
  }
5228
5166
  }
5229
5167
 
@@ -5247,10 +5185,10 @@ for (const file of config.files) {
5247
5185
  }
5248
5186
 
5249
5187
  if (fs.existsSync(destPath)) {
5250
- log(` ${YELLOW}⚠${RESET} ${file.dest} 이미 존재, 건너뛰`);
5188
+ log(` ${YELLOW}⚠${RESET} ${file.dest} already exists, skipping`);
5251
5189
  } else {
5252
5190
  fs.copyFileSync(srcPath, destPath);
5253
- log(` ${GREEN}✓${RESET} ${file.dest} 생성`);
5191
+ log(` ${GREEN}✓${RESET} ${file.dest} created`);
5254
5192
  }
5255
5193
  }
5256
5194
 
@@ -5267,7 +5205,7 @@ if (config.env && Object.keys(config.env).length > 0) {
5267
5205
  if (!envContent.includes(key)) {
5268
5206
  envContent += `\n${key}=${value}`;
5269
5207
  added = true;
5270
- log(` ${GREEN}✓${RESET} .env ${key}= 추가`);
5208
+ log(` ${GREEN}✓${RESET} .env added ${key}=`);
5271
5209
  }
5272
5210
  }
5273
5211
 
@@ -5316,15 +5254,15 @@ const safe = await ai.withRetry(
5316
5254
  { maxRetries: 3 }
5317
5255
  );
5318
5256
  \`\`\``,
5319
- vibePrompt: `AI 엔진 사용법:
5257
+ vibePrompt: `AI Engine API:
5320
5258
  import { ai } from "@/gencow/ai";
5321
- ai.chat({ messages }) // 텍스트 생성
5322
- ai.stream({ messages }) // 스트리밍
5323
- ai.embed("텍스트") // 임베딩
5324
- ai.embedMany(["a","b"]) // 배치 임베딩
5259
+ ai.chat({ messages }) // text generation
5260
+ ai.stream({ messages }) // streaming
5261
+ ai.embed("text") // embedding
5262
+ ai.embedMany(["a","b"]) // batch embedding
5325
5263
  ai.agent({ messages, tools, maxSteps }) // Agent Loop
5326
- ai.withRetry(fn, { maxRetries: 3 }) // 재시도+폴백
5327
- ai.estimateTokens(text) // 토큰 추정
5264
+ ai.withRetry(fn, { maxRetries: 3 }) // retry+fallback
5265
+ ai.estimateTokens(text) // token estimation
5328
5266
  ai.trimMessages(msgs, { maxTokens: 4000 }) // 토큰 예산`,
5329
5267
  },
5330
5268
  "rag.ts": {
@@ -5352,7 +5290,7 @@ const { answer, sources } = await rag.ask(ctx, "환불 정책이 뭔가요?");
5352
5290
  // 문서 삭제
5353
5291
  await rag.delete(ctx, "old-manual.pdf");
5354
5292
  \`\`\``,
5355
- vibePrompt: `RAG 엔진 사용법:
5293
+ vibePrompt: `RAG Engine API:
5356
5294
  import { rag } from "@/gencow/rag";
5357
5295
  rag.ingest(ctx, source, text) // 문서 인제스트 (스마트 청킹)
5358
5296
  rag.search(ctx, query, { filter }) // 시맨틱 검색
@@ -5380,7 +5318,7 @@ const tools = defineTools(ctx, {
5380
5318
 
5381
5319
  const reply = await ai.chat({ messages, tools });
5382
5320
  \`\`\``,
5383
- vibePrompt: `Tool Calling 사용법:
5321
+ vibePrompt: `Tool Calling API:
5384
5322
  import { defineTools } from "@/gencow/tools";
5385
5323
  const tools = defineTools(ctx, { ... }) // AI 도구 정의
5386
5324
  ai.chat({ messages, tools }) // 도구 연동 응답`,
@@ -5401,7 +5339,7 @@ const reply = await ai.chat({
5401
5339
  messages: [...memCtx.recentMessages, { role: "user", content: query }],
5402
5340
  });
5403
5341
  \`\`\``,
5404
- vibePrompt: `Agent Memory 사용법:
5342
+ vibePrompt: `Agent Memory API:
5405
5343
  import { memory } from "@/gencow/memory";
5406
5344
  memory.buildContext(ctx, userId, sessionId, query) // 메모리 컨텍스트
5407
5345
  memory.extract(ctx, userId, conversation) // 사실 추출`,
@@ -5435,7 +5373,7 @@ const reranked = await reranker.rerank(query, results, { topK: 5 });
5435
5373
  // 한 번에 (search + rerank)
5436
5374
  const best = await reranker.searchAndRerank(ctx, rag, query);
5437
5375
  \`\`\``,
5438
- vibePrompt: `Reranker 사용법:
5376
+ vibePrompt: `Reranker API:
5439
5377
  import { reranker } from "@/gencow/reranker";
5440
5378
  reranker.rerank(query, docs, { topK: 5 }) // LLM 재정렬
5441
5379
  reranker.searchAndRerank(ctx, rag, query) // 통합 파이프라인`,
@@ -5465,7 +5403,7 @@ const result = await guardrails.wrap(
5465
5403
  { maxLength: 2000 }
5466
5404
  );
5467
5405
  \`\`\``,
5468
- vibePrompt: `Guardrails 사용법:
5406
+ vibePrompt: `Guardrails API:
5469
5407
  import { guardrails } from "@/gencow/guardrails";
5470
5408
  guardrails.validateInput(text, { maskPII: true }) // PII 마스킹
5471
5409
  guardrails.validateInput(text, { blockTopics }) // 주제 차단
@@ -5492,7 +5430,7 @@ const myPrompt = definePrompt({
5492
5430
  });
5493
5431
  const prompt = myPrompt({ role: "전문가", task: "코드 리뷰" });
5494
5432
  \`\`\``,
5495
- vibePrompt: `Prompt Templates 사용법:
5433
+ vibePrompt: `Prompt Templates API:
5496
5434
  import { definePrompt, ragQAPrompt } from "@/gencow/prompts";
5497
5435
  definePrompt({ template, defaults }) // 재사용 프롬프트 정의
5498
5436
  ragQAPrompt({ question, context }) // RAG Q&A 프롬프트`,
@@ -5515,7 +5453,7 @@ await rag.ingest(ctx, "manual.pdf", text);
5515
5453
  // 자동 감지
5516
5454
  const content = await parsers.auto("report.html", htmlString);
5517
5455
  \`\`\``,
5518
- vibePrompt: `File Parsers 사용법:
5456
+ vibePrompt: `File Parsers API:
5519
5457
  import { parsers } from "@/gencow/parsers";
5520
5458
  parsers.pdf(buffer) // PDF → 텍스트
5521
5459
  parsers.html(htmlString) // HTML → 텍스트
@@ -5559,18 +5497,18 @@ async function updateReadme(config) {
5559
5497
  for (const comp of installed) {
5560
5498
  componentSection += `### ${comp.title}\n\n`;
5561
5499
  componentSection += `${comp.table}\n\n`;
5562
- componentSection += `#### 사용법\n\n`;
5500
+ componentSection += `#### Usage\n\n`;
5563
5501
  componentSection += `${comp.usage}\n\n`;
5564
5502
  }
5565
5503
 
5566
5504
  // 바이브코딩 프롬프트 섹션
5567
5505
  componentSection += `### 🤖 AI Vibe-Coding Prompt\n\n`;
5568
5506
  componentSection += "```\n";
5569
- componentSection += "다음은 현재 Gencow 백엔드에 설치된 AI 컴포넌트야:\n\n";
5507
+ componentSection += "The following AI components are installed in this Gencow backend:\n\n";
5570
5508
  for (const comp of installed) {
5571
5509
  componentSection += `${comp.vibePrompt}\n\n`;
5572
5510
  }
5573
- componentSection += " API를 활용해서 원하는 기능을 구현해줘.\n";
5511
+ componentSection += "Use the APIs above to build your desired features.\n";
5574
5512
  componentSection += "```\n\n";
5575
5513
  componentSection += `${END_MARKER}`;
5576
5514
 
@@ -5584,7 +5522,7 @@ async function updateReadme(config) {
5584
5522
  }
5585
5523
 
5586
5524
  fs.writeFileSync(readmePath, readme);
5587
- log(` ${GREEN}✓${RESET} README.md 컴포넌트 문서 업데이트`);
5525
+ log(` ${GREEN}✓${RESET} README.md component docs updated`);
5588
5526
  }
5589
5527
 
5590
5528
  const [, , cmd = "help", ...args] = process.argv;