gencow 0.1.121 → 0.1.123

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,107 +1030,28 @@ 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
  },
1050
1050
 
1051
1051
  // ── db:reset ─────────────────────────────────────────
1052
- // TRUNCATE all user tables + seed.ts 실행.
1053
- // 로컬 전용: Cloud DB reset 위험하므로 CLI에서 미지원 (Dashboard에서만 가능)
1054
- // 사용: gencow db:reset --local
1055
- async "db:reset"(...args) {
1056
- const config = loadConfig();
1057
- const cwd = process.cwd();
1058
- const isLocal = args.includes("--local");
1059
-
1060
- if (!isLocal) {
1061
- // Cloud reset은 미지원 — 안내 메시지
1062
- error("db:reset은 --local 플래그가 필요합니다.");
1063
- info(`${DIM}Cloud DB 초기화는 Dashboard에서만 가능합니다 (데이터 보호).${RESET}`);
1064
- info(`${DIM}로컬 DB를 초기화하려면: gencow db:reset --local${RESET}`);
1065
- log("");
1066
- return;
1067
- }
1068
-
1069
- const port = process.env.PORT || config.port || 5456;
1070
-
1071
- log(`\n${BOLD}${CYAN}Gencow DB Reset${RESET}\n`);
1072
-
1073
- // 서버가 실행 중인지 확인
1074
- let serverAlive = false;
1075
- try {
1076
- const res = await fetch(`http://localhost:${port}/_admin/status`, { signal: AbortSignal.timeout(2000) });
1077
- serverAlive = res.ok;
1078
- } catch { /* 서버 꺼져있음 */ }
1079
-
1080
- if (serverAlive) {
1081
- // ── 서버 실행 중: API 기반 TRUNCATE + seed ──
1082
- info(`Server detected on :${port} — using API reset (TRUNCATE + seed)`);
1083
- log("");
1084
-
1085
- try {
1086
- const res = await fetch(`http://localhost:${port}/_admin/reset`, {
1087
- method: "POST",
1088
- headers: { "Content-Type": "application/json" },
1089
- body: JSON.stringify({ confirm: "delete all data" }),
1090
- });
1091
- const data = await res.json();
1092
-
1093
- if (!res.ok) {
1094
- error(`Reset failed: ${data.error || "Unknown error"}`);
1095
- log("");
1096
- return;
1097
- }
1098
-
1099
- success(`TRUNCATE 완료: ${data.truncated?.length || 0} tables`);
1100
- if (data.truncated?.length > 0) {
1101
- info(` ${DIM}${data.truncated.join(", ")}${RESET}`);
1102
- }
1103
- if (data.seeded) {
1104
- success(`Seed 실행 완료 ✓`);
1105
- } else {
1106
- info(`${DIM}seed.ts 없음 — 건너뜀${RESET}`);
1107
- }
1108
- } catch (e) {
1109
- error(`Server connection failed: ${e.message}`);
1110
- }
1111
- } else {
1112
- // ── 서버 꺼짐: 기존 PGlite 파일 삭제 ──
1113
- const dbPath = resolve(cwd, config.db.url);
1114
- if (existsSync(dbPath)) {
1115
- // 1. Backup first
1116
- const backupDir = resolve(cwd, ".gencow", "backups");
1117
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
1118
- const backupPath = resolve(backupDir, ts);
1119
- mkdirSync(backupPath, { recursive: true });
1120
- cpSync(dbPath, backupPath, { recursive: true });
1121
- success(`Backup created: .gencow/backups/${ts}`);
1122
-
1123
- // 2. Cleanup old backups (keep last 3)
1124
- cleanupOldBackups(backupDir, 3);
1125
-
1126
- // 3. Delete DB
1127
- rmSync(dbPath, { recursive: true, force: true });
1128
- success("DB deleted — gencow dev 로 재시작하면 새로 생성됩니다");
1129
- } else {
1130
- info("DB가 아직 없습니다. gencow dev 로 시작하세요.");
1131
- }
1132
- }
1133
- log("");
1052
+ async "db:reset"() {
1053
+ info(`db:reset is ${BOLD}coming soon${RESET}.`);
1054
+ info(`${DIM}Use Gencow Dashboard for cloud DB management.${RESET}\n`);
1134
1055
  },
1135
1056
 
1136
1057
  // ── db:seed ──────────────────────────────────────────
@@ -1141,51 +1062,13 @@ ${hasPrompt ? `
1141
1062
  const isLocal = args.includes("--local");
1142
1063
 
1143
1064
  if (isLocal) {
1144
- // ── 로컬 모드: localhost 서버에 seed ──
1145
- const port = process.env.PORT || config.port || 5456;
1146
-
1147
- log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(local)${RESET}\n`);
1148
-
1149
- // 서버가 실행 중인지 확인
1150
- try {
1151
- const statusRes = await fetch(`http://localhost:${port}/_admin/status`, { signal: AbortSignal.timeout(2000) });
1152
- if (!statusRes.ok) throw new Error("Server not ready");
1153
- } catch {
1154
- error(`Server not running on :${port}`);
1155
- info(`${DIM}gencow dev --local 로 서버를 먼저 시작하세요.${RESET}`);
1156
- log("");
1157
- return;
1158
- }
1159
-
1160
- try {
1161
- const res = await fetch(`http://localhost:${port}/_admin/seed`, {
1162
- method: "POST",
1163
- headers: { "Content-Type": "application/json" },
1164
- });
1165
- const data = await res.json();
1166
-
1167
- if (res.status === 404) {
1168
- warn("gencow/seed.ts not found");
1169
- info(`\n ${DIM}Create gencow/seed.ts:${RESET}\n`);
1170
- log(` ${DIM}import { tasks } from "./schema";${RESET}`);
1171
- log(` ${DIM}export default async function seed(ctx) {${RESET}`);
1172
- log(` ${DIM} await ctx.db.insert(tasks).values([${RESET}`);
1173
- log(` ${DIM} { title: "Hello World" },${RESET}`);
1174
- log(` ${DIM} ]);${RESET}`);
1175
- log(` ${DIM}};${RESET}`);
1176
- } else if (!res.ok) {
1177
- error(`Seed failed: ${data.error || "Unknown error"}`);
1178
- } else {
1179
- success("Seed 실행 완료 ✓");
1180
- }
1181
- } catch (e) {
1182
- error(`Server connection failed: ${e.message}`);
1183
- }
1184
- log("");
1065
+ info(`Local db:seed is ${BOLD}coming soon${RESET}.`);
1066
+ info(`${DIM}Use cloud mode (default): gencow db:seed${RESET}\n`);
1185
1067
  return;
1186
1068
  }
1187
1069
 
1188
1070
  // ── Cloud 모드 (기본) ──
1071
+ const isProd = args.includes("--prod");
1189
1072
  const gencowJsonPath = resolve(process.cwd(), "gencow.json");
1190
1073
  let appId = null;
1191
1074
  let seedPlatformUrl = null;
@@ -1194,18 +1077,27 @@ ${hasPrompt ? `
1194
1077
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
1195
1078
  appId = gencowJson.appId || gencowJson.appName;
1196
1079
  seedPlatformUrl = gencowJson.platformUrl || null;
1080
+ // --prod: use prod app
1081
+ if (isProd && gencowJson.prodApp) {
1082
+ appId = gencowJson.prodApp;
1083
+ }
1197
1084
  } catch { /* ignore parse error */ }
1198
1085
  }
1199
1086
 
1087
+ if (isProd && appId && !appId.endsWith("-prod")) {
1088
+ error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
1089
+ return;
1090
+ }
1091
+
1200
1092
  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}`);
1093
+ error("Cloud app not found. Make sure gencow.json exists with an appId.");
1094
+ info(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
1204
1095
  log("");
1205
1096
  return;
1206
1097
  }
1207
1098
 
1208
- log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
1099
+ const envLabel = isProd ? "prod" : "dev";
1100
+ log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(${envLabel}: ${appId})${RESET}\n`);
1209
1101
  info("Running seed.ts on cloud app...");
1210
1102
 
1211
1103
  const seedAppBaseUrl = getAppUrl(appId, seedPlatformUrl || "https://gencow.app");
@@ -1219,8 +1111,8 @@ ${hasPrompt ? `
1219
1111
 
1220
1112
  if (!statusRes || !statusRes.ok) {
1221
1113
  error(`Cloud app "${appId}" is not running`);
1222
- info(`${DIM}Dashboard → Apps → ${appId} → Resume 으로 앱을 시작하세요.${RESET}`);
1223
- info(`${DIM}또는: gencow dev 생성 후 시도하세요.${RESET}`);
1114
+ info(`${DIM}Dashboard → Apps → ${appId} → Resume to start the app.${RESET}`);
1115
+ info(`${DIM}Or: run gencow dev to create and deploy.${RESET}`);
1224
1116
  log("");
1225
1117
  return;
1226
1118
  }
@@ -1238,15 +1130,15 @@ ${hasPrompt ? `
1238
1130
  log(` ${DIM}export default async function seed(ctx) {${RESET}`);
1239
1131
  log(` ${DIM} await ctx.db.insert(tasks).values([...]);${RESET}`);
1240
1132
  log(` ${DIM}};${RESET}\n`);
1241
- info(`${DIM}After creating seed.ts: gencow dev 실행 gencow db:seed${RESET}`);
1133
+ info(`${DIM}After creating seed.ts: run gencow dev then gencow db:seed${RESET}`);
1242
1134
  } else if (!res.ok) {
1243
1135
  error(`Cloud seed failed: ${data.error || "Unknown error"}`);
1244
1136
  } else {
1245
- success("Cloud seed 실행 완료 ✓");
1137
+ success("Cloud seed completed ✓");
1246
1138
  }
1247
1139
  } catch (e) {
1248
1140
  error(`Cloud app connection failed: ${e.message}`);
1249
- info(`${DIM}앱이 실행 중인지 확인하세요: ${seedAppBaseUrl}${RESET}`);
1141
+ info(`${DIM}Check if the app is running: ${seedAppBaseUrl}${RESET}`);
1250
1142
  }
1251
1143
  log("");
1252
1144
  },
@@ -1261,7 +1153,7 @@ ${hasPrompt ? `
1261
1153
  log(`\n${BOLD}${CYAN}Gencow DB Restore${RESET}\n`);
1262
1154
 
1263
1155
  if (!existsSync(backupDir)) {
1264
- warn("백업이 없습니다.");
1156
+ warn("No backups found.");
1265
1157
  log("");
1266
1158
  return;
1267
1159
  }
@@ -1274,7 +1166,7 @@ ${hasPrompt ? `
1274
1166
  .reverse();
1275
1167
 
1276
1168
  if (backups.length === 0) {
1277
- warn("백업이 없습니다.");
1169
+ warn("No backups found.");
1278
1170
  log("");
1279
1171
  return;
1280
1172
  }
@@ -1295,7 +1187,7 @@ ${hasPrompt ? `
1295
1187
  }
1296
1188
  cpSync(latestPath, dbPath, { recursive: true });
1297
1189
  success(`Restored from: .gencow/backups/${latest}`);
1298
- info("gencow dev 로 재시작하세요.");
1190
+ info("Restart with gencow dev.");
1299
1191
  log("");
1300
1192
  },
1301
1193
 
@@ -1306,9 +1198,12 @@ ${hasPrompt ? `
1306
1198
  if (devArgs.includes("--verbose")) {
1307
1199
  process.env.GENCOW_VERBOSE = "true";
1308
1200
  }
1309
- // --local 플래그: 로컬 PGlite 모드
1201
+ // --local 플래그: coming soon
1310
1202
  if (devArgs.includes("--local")) {
1311
- return commands["dev:local"](...devArgs);
1203
+ log(`\n${BOLD}${CYAN}Gencow Dev${RESET} ${DIM}(local)${RESET}\n`);
1204
+ info(`Local development mode is ${BOLD}coming soon${RESET}.`);
1205
+ info(`${DIM}Use cloud mode (default): gencow dev${RESET}\n`);
1206
+ return;
1312
1207
  }
1313
1208
  // --cloud 하위 호환 (이미 기본이므로 그대로 진행)
1314
1209
  // 기본 = 클라우드 모드
@@ -1530,43 +1425,58 @@ ${hasPrompt ? `
1530
1425
  const isLocal = args.includes("--local");
1531
1426
 
1532
1427
  if (isLocal) {
1533
- // ── 로컬 모드: 기존 동작 유지 ──
1534
- let hasDb = !!process.env.DATABASE_URL;
1535
- if (!hasDb) {
1536
- const envPath = resolve(process.cwd(), ".env");
1537
- if (existsSync(envPath)) hasDb = readFileSync(envPath, "utf8").includes("DATABASE_URL=");
1538
- }
1539
- const targetDb = hasDb ? "local PG" : "local fallback: PGlite";
1540
- log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(${targetDb})${RESET}\n`);
1541
- info("Pushing schema.ts → database (no migration files)...");
1542
- runInServer("pnpm db:push --force", buildEnv(config));
1543
- success("Schema pushed!");
1544
- log(` ${DIM}Tables are in sync with schema.ts${RESET}\n`);
1428
+ info(`Local db:push is ${BOLD}coming soon${RESET}.`);
1429
+ info(`${DIM}Use cloud mode (default): gencow db:push${RESET}\n`);
1545
1430
  return;
1546
1431
  }
1547
1432
 
1548
- // ── Cloud 모드 (기본) ──
1433
+ // ── Cloud mode (default) ──
1434
+ const isProd = args.includes("--prod");
1549
1435
  const gencowJsonPath = resolve(process.cwd(), "gencow.json");
1550
1436
  let appId = null;
1551
1437
  if (existsSync(gencowJsonPath)) {
1552
1438
  try {
1553
1439
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
1554
1440
  appId = gencowJson.appId || gencowJson.appName;
1441
+ // --prod: use prod app
1442
+ if (isProd && gencowJson.prodApp) {
1443
+ appId = gencowJson.prodApp;
1444
+ }
1555
1445
  } catch { /* ignore parse error */ }
1556
1446
  }
1557
1447
 
1448
+ if (isProd && appId && !appId.endsWith("-prod")) {
1449
+ error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
1450
+ return;
1451
+ }
1452
+
1558
1453
  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}`);
1454
+ error("Cloud app not found. Make sure gencow.json exists with an appId.");
1455
+ info(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
1456
+ info(`${DIM}To push to local DB: gencow db:push --local${RESET}`);
1562
1457
  log("");
1563
1458
  return;
1564
1459
  }
1565
1460
 
1566
- // ── Cloud 모드: Platform API를 통해 Cloud DB에 schema push ──
1461
+ // ── Prod safety confirmation ──
1462
+ if (isProd) {
1463
+ const { createInterface } = await import("readline");
1464
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1465
+ const answer = await new Promise(res =>
1466
+ rl.question(`\n ${RED}!${RESET} Push schema to ${BOLD}PRODUCTION${RESET} database (${appId})?\n Type "yes" to confirm: `, res)
1467
+ );
1468
+ rl.close();
1469
+ if (answer.trim().toLowerCase() !== "yes") {
1470
+ warn("Cancelled.");
1471
+ return;
1472
+ }
1473
+ }
1474
+
1475
+ // ── Cloud mode: push schema via Platform API ──
1567
1476
  const creds = requireCreds();
1568
- log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
1569
- warn("명령어 실행 먼저 gencow dev 를 통해 최신 코드가 배포되어 있어야 합니다.");
1477
+ const envLabel = isProd ? "prod" : "dev";
1478
+ log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(${envLabel}: ${appId})${RESET}\n`);
1479
+ warn("Make sure the latest code is deployed before running this command.");
1570
1480
  log("");
1571
1481
  info("Pushing schema.ts → cloud database...");
1572
1482
 
@@ -1586,7 +1496,7 @@ ${hasPrompt ? `
1586
1496
  }
1587
1497
 
1588
1498
  // 🆕 로컬 drizzle-kit generate 실행 (프롬프트 패스스루)
1589
- info("스키마 마이그레이션 생성 중...");
1499
+ info("Generating schema migrations...");
1590
1500
  try {
1591
1501
  const dk = _drizzleKitCmd("generate");
1592
1502
  execSync(dk.cmd, {
@@ -1594,14 +1504,14 @@ ${hasPrompt ? `
1594
1504
  env: { ...process.env, ...dk.env },
1595
1505
  stdio: "inherit",
1596
1506
  });
1597
- success("마이그레이션 생성 완료");
1507
+ success("Migrations generated");
1598
1508
  } catch (e) {
1599
1509
  const msg = e.stderr?.toString() || e.message || "";
1600
1510
  if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
1601
- log(`${DIM} 스키마 변경 없음기존 마이그레이션 사용${RESET}`);
1511
+ log(`${DIM} Migrations up-to-dateno new schema changes detected${RESET}`);
1602
1512
  } else {
1603
- warn(`마이그레이션 생성 실패: ${msg.split("\n")[0]}`);
1604
- info("서버에서 기존 방식으로 스키마 적용을 시도합니다.");
1513
+ warn(`Migration generation failed: ${msg.split("\n")[0]}`);
1514
+ info("Will attempt schema push via server.");
1605
1515
  }
1606
1516
  }
1607
1517
 
@@ -1638,8 +1548,8 @@ ${hasPrompt ? `
1638
1548
  success("Schema pushed to cloud DB!");
1639
1549
  log(` ${DIM}Tables are in sync with schema.ts${RESET}\n`);
1640
1550
  } catch (e) {
1641
- error(`Platform 연결 실패: ${e.message}`);
1642
- info("서버가 실행 중인지 확인하세요.");
1551
+ error(`Platform connection failed: ${e.message}`);
1552
+ info("Check if the server is running.");
1643
1553
  process.exit(1);
1644
1554
  } finally {
1645
1555
  // 임시 번들 정리
@@ -1679,98 +1589,16 @@ ${hasPrompt ? `
1679
1589
 
1680
1590
  // ── db:studio ────────────────────────────────────────
1681
1591
  async "db:studio"() {
1682
- const config = loadConfig();
1683
- log(`\n${BOLD}${CYAN}Gencow DB Studio${RESET}\n`);
1684
- info("Opening Drizzle Studio...");
1685
- runInServer("pnpm db:studio", buildEnv(config));
1592
+ info(`Drizzle Studio is ${BOLD}coming soon${RESET}.`);
1593
+ info(`${DIM}Use Gencow Dashboard at https://gencow.app instead.${RESET}\n`);
1686
1594
  },
1687
1595
 
1688
1596
  // ── dashboard ────────────────────────────────────────
1689
1597
  async dashboard() {
1690
- const config = loadConfig();
1691
- const port = process.env.PORT || config.port;
1692
- const url = `http://localhost:${port}/_admin`;
1693
- log(`\n${BOLD}${CYAN}Gencow Dashboard${RESET}\n`);
1694
- info(`Opening ${url}`);
1695
- const { default: open } = await import("open");
1696
- await open(url);
1598
+ info(`Local dashboard is ${BOLD}coming soon${RESET}.`);
1599
+ info(`${DIM}Use Gencow Dashboard at https://gencow.app${RESET}\n`);
1697
1600
  },
1698
1601
 
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
1602
 
1775
1603
  // ── help ─────────────────────────────────────────────
1776
1604
  help() {
@@ -1784,34 +1612,25 @@ ${BOLD}Quick Start:${RESET}
1784
1612
  ${GREEN}init <name>${RESET} Create a new Gencow project
1785
1613
  ${DIM}--template, -t Select template (default, task-app, fullstack, ai-chat)${RESET}
1786
1614
  ${DIM}--force, -f Initialize in non-empty directory (preserves existing files)${RESET}
1787
-
1788
- ${BOLD}Dev commands:${RESET}
1789
- ${GREEN}dev${RESET} Watch + auto-deploy to cloud + live logs ${DIM}(default)${RESET}
1790
- ${DIM}--local Use local PGlite instead of cloud${RESET}
1791
- ${DIM}--verbose Show all HTTP logs (including admin/ws)${RESET}
1792
- ${GREEN}dashboard${RESET} Open the Gencow admin dashboard in browser
1793
- ${GREEN}db:push${RESET} Sync schema.ts → cloud DB ${DIM}(default: cloud)${RESET}
1794
- ${DIM}--local Push to local DB instead of cloud${RESET}
1795
- ${GREEN}db:generate${RESET} Generate SQL migration files from schema.ts
1796
- ${GREEN}db:seed${RESET} Run gencow/seed.ts on cloud app ${DIM}(default: cloud)${RESET}
1797
- ${DIM}--local Seed local DB instead of cloud${RESET}
1798
- ${GREEN}db:reset${RESET} TRUNCATE all data + run seed.ts ${DIM}(local only)${RESET}
1799
- ${DIM}--local Required — local DB reset only (cloud: use Dashboard)${RESET}
1800
- ${GREEN}db:studio${RESET} Open Drizzle Studio ${DIM}(visual DB browser)${RESET}
1801
- ${GREEN}add <comp...>${RESET} Add components ${DIM}(AI RAG Tools Memory Analytics)${RESET}
1802
- ${GREEN}mcp${RESET} Start MCP server for AI agent integration ${DIM}(stdio)${RESET}
1803
- ${GREEN}codegen${RESET} Generate frontend-independent api.ts
1615
+ ${GREEN}add <comp...>${RESET} Add components ${DIM}(AI RAG Tools Memory)${RESET}
1616
+ ${GREEN}codegen${RESET} Generate frontend api.ts from schema
1804
1617
  ${DIM}--outdir, -o Output directory (default: src/gencow/)${RESET}
1805
1618
 
1806
- ${BOLD}BaaS commands (login required):${RESET}
1619
+ ${BOLD}Commands (login required):${RESET}
1807
1620
  ${GREEN}login${RESET} Login to Gencow Platform ${DIM}(browser → token)${RESET}
1808
1621
  ${GREEN}logout${RESET} Clear credentials
1809
1622
  ${GREEN}whoami${RESET} Show current user info
1810
- ${GREEN}static [dir]${RESET} Deploy static files to dev ${DIM}(dist/, out/, build/)${RESET}
1811
- ${DIM}--no-backend Skip backend auto-deploy${RESET}
1623
+ ${GREEN}dev${RESET} Watch + auto-deploy to cloud + live logs
1624
+ ${DIM}--verbose Show all HTTP logs (including admin/ws)${RESET}
1625
+ ${GREEN}db:push${RESET} Sync schema.ts → cloud DB
1626
+ ${DIM}--prod Push to production DB (confirmation required)${RESET}
1627
+ ${GREEN}db:generate${RESET} Generate SQL migration files from schema.ts
1628
+ ${GREEN}db:seed${RESET} Run gencow/seed.ts on cloud app
1629
+ ${DIM}--prod Seed production app${RESET}
1630
+ ${GREEN}static [dir]${RESET} Deploy static files ${DIM}(dist/, out/, build/)${RESET}
1631
+ ${DIM}--prod Deploy to production app${RESET}
1812
1632
  ${DIM}--force, -f Skip dependency audit${RESET}
1813
- ${GREEN}deploy${RESET} Deploy to production ${DIM}(Pro+ only)${RESET}
1814
- ${DIM}--static [dir] Deploy static files to production${RESET}
1633
+ ${GREEN}deploy${RESET} Deploy backend to production ${DIM}(Pro+ only)${RESET}
1815
1634
  ${DIM}--rollback Rollback to previous deployment${RESET}
1816
1635
  ${DIM}--force, -f Skip dependency audit${RESET}
1817
1636
  ${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
@@ -1822,16 +1641,22 @@ ${BOLD}BaaS commands (login required):${RESET}
1822
1641
  ${GREEN}files list${RESET} List uploaded files
1823
1642
  ${GREEN}files delete${RESET} Delete uploaded file ${DIM}(--yes to skip confirm)${RESET}
1824
1643
  ${GREEN}files url${RESET} Get serving URL for a file
1825
- ${GREEN}config set${RESET} Set app config ${DIM}(image.maxWidth, image.quality)${RESET}
1826
- ${GREEN}config get${RESET} Show current app config
1827
- ${GREEN}config reset${RESET} Reset app config to tier defaults
1644
+ ${GREEN}config set${RESET} Set image config ${DIM}(config set image.maxWidth 1920)${RESET}
1645
+ ${DIM}Keys: image.maxWidth (0-10000), image.quality (0-100)${RESET}
1646
+ ${GREEN}config get${RESET} Show current image config
1647
+ ${GREEN}config reset${RESET} Reset image config to tier defaults
1828
1648
  ${GREEN}domain set${RESET} Connect custom domain ${DIM}(myapp.com)${RESET}
1829
1649
  ${GREEN}domain status${RESET} Check domain DNS/TLS status
1830
1650
  ${GREEN}domain remove${RESET} Disconnect custom domain
1831
1651
 
1832
- ${BOLD}Production:${RESET}
1833
- ${GREEN}deploy${RESET} Build Docker image + start production stack
1834
- ${GREEN}deploy stop${RESET} Stop production containers
1652
+ ${BOLD}App management:${RESET}
1653
+ ${GREEN}app list${RESET} List your apps
1654
+ ${GREEN}app create${RESET} Create a new app
1655
+ ${GREEN}app delete${RESET} Delete an app ${DIM}(confirmation required)${RESET}
1656
+ ${GREEN}app status${RESET} Show app status
1657
+
1658
+ ${DIM}Tip: Most commands support --prod to target the production app.${RESET}
1659
+ ${DIM} e.g. gencow env list --prod, gencow db:seed --prod${RESET}
1835
1660
 
1836
1661
  ${BOLD}Examples:${RESET}
1837
1662
  ${DIM}# New project:${RESET}
@@ -1840,10 +1665,9 @@ ${BOLD}Examples:${RESET}
1840
1665
  ${DIM}# Initialize in current directory:${RESET}
1841
1666
  gencow init . --force
1842
1667
 
1843
- ${DIM}# BaaS (deploy to platform):${RESET}
1668
+ ${DIM}# Deploy to production (Pro+ only):${RESET}
1844
1669
  gencow login
1845
- gencow app create my-app
1846
- gencow remote:deploy
1670
+ gencow deploy
1847
1671
  `);
1848
1672
  },
1849
1673
 
@@ -1855,7 +1679,7 @@ ${BOLD}Examples:${RESET}
1855
1679
  info(`Platform: ${platformUrl}`);
1856
1680
 
1857
1681
  // 1. auth-start → 인증 코드 생성
1858
- info("인증 코드 요청 중...");
1682
+ info("Requesting auth code...");
1859
1683
  let authData;
1860
1684
  try {
1861
1685
  const res = await fetch(`${platformUrl}/platform/cli/auth-start`, {
@@ -1865,16 +1689,16 @@ ${BOLD}Examples:${RESET}
1865
1689
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1866
1690
  authData = await res.json();
1867
1691
  } catch (e) {
1868
- error(`Platform 서버 연결 실패: ${e.message}`);
1692
+ error(`Platform connection failed: ${e.message}`);
1869
1693
  error(`Platform URL: ${platformUrl}`);
1870
- error(`서버가 실행 중인지 확인하세요.`);
1694
+ error(`Check if the server is running.`);
1871
1695
  process.exit(1);
1872
1696
  }
1873
1697
 
1874
1698
  log("");
1875
- log(` ${BOLD}인증 코드: ${CYAN}${authData.code}${RESET}`);
1699
+ log(` ${BOLD}Auth code: ${CYAN}${authData.code}${RESET}`);
1876
1700
  log("");
1877
- info(`브라우저에서 로그인하세요:`);
1701
+ info(`Login in your browser:`);
1878
1702
  log(` ${CYAN}${authData.url}${RESET}`);
1879
1703
  log("");
1880
1704
 
@@ -1885,11 +1709,11 @@ ${BOLD}Examples:${RESET}
1885
1709
  process.platform === "win32" ? "start" : "xdg-open";
1886
1710
  exec(`${openCmd} "${authData.url}"`);
1887
1711
  } catch {
1888
- warn("브라우저를 자동으로 없습니다. URL 직접 열어주세요.");
1712
+ warn("Could not open browser automatically. Open the URL above manually.");
1889
1713
  }
1890
1714
 
1891
1715
  // 3. 폴링 — 2초마다 인증 완료 확인
1892
- info("로그인 대기 중...");
1716
+ info("Waiting for login...");
1893
1717
  const POLL_INTERVAL = 2000;
1894
1718
  const MAX_POLLS = 150; // 10분 내 5분 = 150회
1895
1719
 
@@ -1914,14 +1738,14 @@ ${BOLD}Examples:${RESET}
1914
1738
  });
1915
1739
 
1916
1740
  log("");
1917
- success(`로그인 완료!`);
1918
- info(`토큰 저장됨: ${DIM}~/.gencow/credentials.json${RESET}`);
1741
+ success(`Login successful!`);
1742
+ info(`Token saved: ${DIM}~/.gencow/credentials.json${RESET}`);
1919
1743
  log("");
1920
1744
  return;
1921
1745
  }
1922
1746
 
1923
1747
  if (data.status === "expired") {
1924
- error("인증 코드가 만료되었습니다. 다시 시도하세요: gencow login");
1748
+ error("Auth code expired. Try again: gencow login");
1925
1749
  process.exit(1);
1926
1750
  }
1927
1751
 
@@ -1933,7 +1757,7 @@ ${BOLD}Examples:${RESET}
1933
1757
  }
1934
1758
  }
1935
1759
 
1936
- error("\n타임아웃. 다시 시도하세요: gencow login");
1760
+ error("\nTimed out. Try again: gencow login");
1937
1761
  process.exit(1);
1938
1762
  },
1939
1763
 
@@ -1941,9 +1765,9 @@ ${BOLD}Examples:${RESET}
1941
1765
  async logout() {
1942
1766
  try {
1943
1767
  unlinkSync(CREDS_PATH);
1944
- success("로그아웃 완료. 자격 증명이 삭제되었습니다.");
1768
+ success("Logged out. Credentials cleared.");
1945
1769
  } catch {
1946
- info("이미 로그아웃 상태입니다.");
1770
+ info("Already logged out.");
1947
1771
  }
1948
1772
  },
1949
1773
 
@@ -1951,8 +1775,8 @@ ${BOLD}Examples:${RESET}
1951
1775
  async whoami() {
1952
1776
  const creds = loadCreds();
1953
1777
  if (!creds?.apiKey) {
1954
- error("로그인하지 않았습니다.");
1955
- info("실행: gencow login");
1778
+ error("Not logged in.");
1779
+ info("Run: gencow login");
1956
1780
  return;
1957
1781
  }
1958
1782
 
@@ -1962,10 +1786,10 @@ ${BOLD}Examples:${RESET}
1962
1786
  if (!res.ok) {
1963
1787
  const data = await res.json().catch(() => ({}));
1964
1788
  const msg = data.error || `HTTP ${res.status}`;
1965
- error(`인증 실패: ${msg}`);
1789
+ error(`Auth failed: ${msg}`);
1966
1790
  if (res.status === 401) {
1967
- info("토큰이 만료되었거나 유효하지 않습니다.");
1968
- info("실행: gencow login");
1791
+ info("Token is expired or invalid.");
1792
+ info("Run: gencow login");
1969
1793
  }
1970
1794
  return;
1971
1795
  }
@@ -1978,29 +1802,29 @@ ${BOLD}Examples:${RESET}
1978
1802
  if (data.expiresAt) {
1979
1803
  const expiresDate = new Date(data.expiresAt);
1980
1804
  const daysLeft = Math.ceil((expiresDate.getTime() - Date.now()) / (24 * 60 * 60 * 1000));
1981
- info(`Expires: ${expiresDate.toLocaleString()} (${daysLeft} 남음)`);
1805
+ info(`Expires: ${expiresDate.toLocaleString()} (${daysLeft} days left)`);
1982
1806
  }
1983
1807
  log("");
1984
1808
  } catch (e) {
1985
- error(`서버 연결 실패: ${e.message}`);
1809
+ error(`Connection failed: ${e.message}`);
1986
1810
  info(`Platform URL: ${creds.platformUrl}`);
1987
- info("서버가 실행 중인지 확인하세요.");
1811
+ info("Check if the server is running.");
1988
1812
  }
1989
1813
  },
1990
1814
 
1991
- // ── static — dev 정적 파일 배포 (독립 명령어) ─────────
1815
+ // ── static — 정적 파일 배포 (dev 기본, --prod 로 프로덕션) ──
1992
1816
  async static(...staticArgs) {
1993
1817
  const creds = requireCreds();
1994
1818
 
1995
1819
  let staticDir = null;
1996
1820
  let forceDeploy = false;
1997
- let noBackend = false;
1998
1821
  let appId = null;
1822
+ let isProd = false;
1999
1823
 
2000
1824
  for (let i = 0; i < staticArgs.length; i++) {
2001
1825
  const a = staticArgs[i];
2002
1826
  if (a === "--force" || a === "-f") forceDeploy = true;
2003
- else if (a === "--no-backend") noBackend = true;
1827
+ else if (a === "--prod") isProd = true;
2004
1828
  else if (a === "--app" || a === "-a") appId = staticArgs[++i];
2005
1829
  else if (!a.startsWith("-")) {
2006
1830
  staticDir = a;
@@ -2009,9 +1833,21 @@ ${BOLD}Examples:${RESET}
2009
1833
 
2010
1834
  // gencow.json에서 appId 로드
2011
1835
  const gencowJsonPath = resolve(process.cwd(), "gencow.json");
2012
- if (!appId && existsSync(gencowJsonPath)) {
1836
+ let prodAppId = null;
1837
+ if (existsSync(gencowJsonPath)) {
2013
1838
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
2014
- appId = gencowJson.appId || gencowJson.appName;
1839
+ if (!appId) appId = gencowJson.appId || gencowJson.appName;
1840
+ prodAppId = gencowJson.prodApp || null;
1841
+ }
1842
+
1843
+ // --prod: prod app 대상
1844
+ const deployTarget = isProd ? (prodAppId || appId) : appId;
1845
+ const envTarget = isProd ? "prod" : "dev";
1846
+
1847
+ if (isProd && !prodAppId) {
1848
+ error("No production app found. Run 'gencow deploy' first to create one.");
1849
+ info(`${DIM}For dev static deploy, omit --prod: gencow static [dir]${RESET}\n`);
1850
+ return;
2015
1851
  }
2016
1852
 
2017
1853
  // displayName
@@ -2024,8 +1860,8 @@ ${BOLD}Examples:${RESET}
2024
1860
  }
2025
1861
  if (!displayName) displayName = basename(process.cwd());
2026
1862
 
2027
- return await this._deployStatic(creds, appId, displayName, staticDir, gencowJsonPath, {
2028
- forceDeploy, noBackend, envTarget: "dev",
1863
+ return await this._deployStatic(creds, deployTarget, displayName, staticDir, gencowJsonPath, {
1864
+ forceDeploy, noBackend: true, envTarget,
2029
1865
  });
2030
1866
  },
2031
1867
 
@@ -2037,10 +1873,7 @@ ${BOLD}Examples:${RESET}
2037
1873
  let appId = null;
2038
1874
  let displayName = null;
2039
1875
  let envTarget = "prod"; // v0.1.120: deploy = prod 기본값
2040
- let staticDir = null; // --static 옵션
2041
- let isStatic = false;
2042
1876
  let forceDeploy = false; // --force: skip dependency audit
2043
- let noBackend = false; // --no-backend: skip backend auto-deploy in --static mode
2044
1877
  let isRollback = false; // --rollback: rollback to previous deploy
2045
1878
 
2046
1879
  // ── subcommand 분기 (logs / status) ───────────────────
@@ -2055,17 +1888,17 @@ ${BOLD}Examples:${RESET}
2055
1888
  appId = gencowJson.appId || gencowJson.appName;
2056
1889
  }
2057
1890
  if (!appId) {
2058
- error(" ID를 찾을 없습니다. gencow.json이 있는 프로젝트 루트에서 실행하세요.");
1891
+ error("App not found. Run from the project root with gencow.json.");
2059
1892
  process.exit(1);
2060
1893
  }
2061
1894
 
2062
1895
  if (subcmd === "logs") {
2063
1896
  const lines = deployArgs.includes("-n") ? Number(deployArgs[deployArgs.indexOf("-n") + 1]) || 100 : 100;
2064
- info(`"${appId}" 로그 조회 중... (최근 ${lines})`);
1897
+ info(`Fetching logs for "${appId}"... (last ${lines} lines)`);
2065
1898
  const res = await rpcQuery(creds, "apps.logs", { name: appId, lines });
2066
1899
  if (!res.ok) {
2067
1900
  const errData = await res.json().catch(() => ({}));
2068
- error(`로그 조회 실패: ${errData.error || res.statusText}`);
1901
+ error(`Failed to fetch logs: ${errData.error || res.statusText}`);
2069
1902
  process.exit(1);
2070
1903
  }
2071
1904
  const data = await res.json();
@@ -2075,29 +1908,29 @@ ${BOLD}Examples:${RESET}
2075
1908
  log(` ${DIM}${line}${RESET}`);
2076
1909
  }
2077
1910
  } else {
2078
- info("로그가 없습니다.");
1911
+ info("No logs found.");
2079
1912
  }
2080
1913
  return;
2081
1914
  }
2082
1915
 
2083
1916
  if (subcmd === "status") {
2084
- info(`"${appId}" 상태 확인 중...`);
1917
+ info(`Checking "${appId}" status...`);
2085
1918
  const res = await rpcQuery(creds, "apps.get", { name: appId });
2086
1919
  if (!res.ok) {
2087
1920
  const errData = await res.json().catch(() => ({}));
2088
- error(`상태 조회 실패: ${errData.error || res.statusText}`);
1921
+ error(`Failed to fetch status: ${errData.error || res.statusText}`);
2089
1922
  process.exit(1);
2090
1923
  }
2091
1924
  const data = await res.json();
2092
1925
  const app = data.result || data;
2093
1926
  log("");
2094
- log(` ${BOLD} 상태${RESET}`);
1927
+ log(` ${BOLD}App Status${RESET}`);
2095
1928
  log(` ──────────────────────`);
2096
- info(`이름: ${app.name}`);
2097
- info(`상태: ${app.status === "running" ? `${GREEN}● running${RESET}` : app.status === "crashed" ? `${RED}● crashed${RESET}` : `${YELLOW}● ${app.status}${RESET}`}`);
1929
+ info(`Name: ${app.name}`);
1930
+ info(`Status: ${app.status === "running" ? `${GREEN}● running${RESET}` : app.status === "crashed" ? `${RED}● crashed${RESET}` : `${YELLOW}● ${app.status}${RESET}`}`);
2098
1931
  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}`}`);
1932
+ if (app.lastDeployedAt) info(`Deployed: ${new Date(app.lastDeployedAt).toLocaleString()}`);
1933
+ if (app.running !== undefined) info(`Process: ${app.running ? `${GREEN}alive${RESET}` : `${RED}dead${RESET}`}`);
2101
1934
  log("");
2102
1935
  return;
2103
1936
  }
@@ -2107,38 +1940,35 @@ ${BOLD}Examples:${RESET}
2107
1940
  const a = deployArgs[i];
2108
1941
  if (a === "--prod") {
2109
1942
  // deprecated: deploy는 이미 prod 기본값
2110
- warn(`${YELLOW}--prod 플래그는 이상 필요하지 않습니다.${RESET}`);
2111
- info(`gencow deploy 기본적으로 프로덕션에 배포합니다.`);
1943
+ warn(`${YELLOW}--prod flag is no longer needed.${RESET}`);
1944
+ info(`gencow deploy targets production by default.`);
2112
1945
  envTarget = "prod";
2113
1946
  }
2114
1947
  else if (a === "--force" || a === "-f") forceDeploy = true;
2115
- else if (a === "--no-backend") noBackend = true;
2116
1948
  else if (a === "--rollback") isRollback = true;
2117
1949
  else if (a === "--app" || a === "-a") appId = deployArgs[++i];
2118
1950
  else if (a === "--static") {
2119
- isStatic = true;
2120
- // 다음 인자가 옵션이 아니면 디렉토리로 사용
2121
- const next = deployArgs[i + 1];
2122
- if (next && !next.startsWith("-")) {
2123
- staticDir = next;
2124
- i++;
2125
- }
1951
+ // Deprecated: redirect to gencow static --prod
1952
+ warn(`${YELLOW}--static is deprecated. Use: gencow static --prod${RESET}`);
1953
+ return;
1954
+ }
1955
+ else if (a === "--no-backend") {
1956
+ // Deprecated: silently ignore
2126
1957
  }
2127
1958
  else if (!a.startsWith("-")) {
2128
1959
  // 미인식 subcommand — 에러 출력
2129
- error(`알 수 없는 deploy 인자: "${a}"`);
1960
+ error(`Unknown deploy argument: "${a}"`);
2130
1961
  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 의존성 감사 스킵`);
1962
+ info(`Usage: gencow deploy [options]`);
1963
+ info(` gencow deploy Production backend deploy (Pro+)`);
1964
+ info(` gencow deploy --rollback Rollback to previous version`);
1965
+ info(` gencow deploy logs View server logs`);
1966
+ info(` gencow deploy status Check app status`);
1967
+ info(` gencow deploy --force Skip dependency audit`);
2138
1968
  log("");
2139
- info(`💡 개발 환경 배포:`);
2140
- info(` gencow dev 백엔드 실시간 배포`);
2141
- info(` gencow static [dir] 정적 파일 배포`);
1969
+ info(`💡 Static files:`);
1970
+ info(` gencow static [dir] Deploy static files (dev)`);
1971
+ info(` gencow static --prod Deploy static files (production)`);
2142
1972
  process.exit(1);
2143
1973
  }
2144
1974
  }
@@ -2169,12 +1999,12 @@ ${BOLD}Examples:${RESET}
2169
1999
  // prodApp이 있으면 prod 대상 롤백, 없으면 dev 대상 롤백
2170
2000
  const rollbackTarget = prodAppId || appId;
2171
2001
  if (!rollbackTarget) {
2172
- error(" ID를 찾을 없습니다. gencow.json이 있는 프로젝트 루트에서 실행하세요.");
2002
+ error("App not found. Run from the project root with gencow.json.");
2173
2003
  process.exit(1);
2174
2004
  }
2175
2005
 
2176
2006
  log(`\n${BOLD}${CYAN}Gencow Rollback${RESET}\n`);
2177
- info(`앱: ${rollbackTarget}${prodAppId ? " (prod)" : ""}`);
2007
+ info(`App: ${rollbackTarget}${prodAppId ? " (prod)" : ""}`);
2178
2008
  log("");
2179
2009
 
2180
2010
  const rollbackStartTime = Date.now();
@@ -2182,7 +2012,7 @@ ${BOLD}Examples:${RESET}
2182
2012
  let spinnerIdx = 0;
2183
2013
  const spinner = setInterval(() => {
2184
2014
  const elapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(0);
2185
- process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} 롤백 중... ${DIM}(${elapsed}s)${RESET} `);
2015
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Rolling back... ${DIM}(${elapsed}s)${RESET} `);
2186
2016
  }, 120);
2187
2017
 
2188
2018
  const rollbackRes = await platformFetch(creds, `/platform/apps/${rollbackTarget}/rollback`, {
@@ -2195,7 +2025,7 @@ ${BOLD}Examples:${RESET}
2195
2025
 
2196
2026
  if (!rollbackRes.ok) {
2197
2027
  const errData = await rollbackRes.json().catch(() => ({}));
2198
- error(`롤백 실패: ${errData.error || rollbackRes.statusText}`);
2028
+ error(`Rollback failed: ${errData.error || rollbackRes.statusText}`);
2199
2029
  process.exit(1);
2200
2030
  }
2201
2031
 
@@ -2203,22 +2033,16 @@ ${BOLD}Examples:${RESET}
2203
2033
  const rollbackElapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(1);
2204
2034
 
2205
2035
  log("");
2206
- success(`🔄 롤백 완료! (${rollbackElapsed}s)`);
2207
- info(`롤백: #${rollbackData.rolledBackFrom} → #${rollbackData.rolledBackTo}`);
2208
- info(`번들: ${rollbackData.bundleHash}`);
2209
- info(`URL: ${rollbackData.url}`);
2036
+ success(`🔄 Rollback complete! (${rollbackElapsed}s)`);
2037
+ info(`Rolled back: #${rollbackData.rolledBackFrom} → #${rollbackData.rolledBackTo}`);
2038
+ info(`Bundle: ${rollbackData.bundleHash}`);
2039
+ info(`URL: ${rollbackData.url}`);
2210
2040
  log("");
2211
- warn(`ℹ️ 코드만 롤백되었습니다. 데이터베이스는 변경되지 않았습니다.`);
2041
+ warn(`ℹ️ Only code was rolled back. Database was not changed.`);
2212
2042
  log("");
2213
2043
  return;
2214
2044
  }
2215
2045
 
2216
- // ── Static Deploy 분기 ────────────────────────────────
2217
- if (isStatic) {
2218
- // prod 모드면 prodApp 대상으로 정적 배포
2219
- const staticTarget = (envTarget === "prod" && prodAppId) ? prodAppId : appId;
2220
- return await this._deployStatic(creds, staticTarget, displayName, staticDir, gencowJsonPath, { forceDeploy, noBackend, envTarget });
2221
- }
2222
2046
 
2223
2047
  // ── Prod 앱 자동 생성 + 배포 대상 전환 (Pro+) ──────────
2224
2048
  if (envTarget === "prod" && appId) {
@@ -2237,12 +2061,12 @@ ${BOLD}Examples:${RESET}
2237
2061
  });
2238
2062
  rl.close();
2239
2063
  if (answer.toLowerCase() !== "y") {
2240
- info("배포 취소됨.");
2064
+ info("Deploy cancelled.");
2241
2065
  return;
2242
2066
  }
2243
2067
  }
2244
2068
 
2245
- info("Prod 생성 중...");
2069
+ info("Creating prod app...");
2246
2070
  const createProdRes = await platformFetch(creds, `/platform/apps/${appId}/create-prod`, {
2247
2071
  method: "POST",
2248
2072
  headers: { "Content-Type": "application/json" },
@@ -2252,19 +2076,19 @@ ${BOLD}Examples:${RESET}
2252
2076
  const errData = await createProdRes.json().catch(() => ({}));
2253
2077
  if (createProdRes.status === 403) {
2254
2078
  log("");
2255
- log(` ${RED}⛔ Production Deploy — Pro 플랜 이상 필요${RESET}`);
2079
+ log(` ${RED}⛔ Production Deploy — Pro plan or higher required${RESET}`);
2256
2080
  log("");
2257
- log(` ${DIM}gencow deploy 프로덕션 환경에 배포하는 명령어입니다.${RESET}`);
2081
+ log(` ${DIM}gencow deploy is for production deployment.${RESET}`);
2258
2082
  log("");
2259
- log(` ${BOLD}💡 개발 환경 배포:${RESET}`);
2260
- log(` ${GREEN}gencow dev${RESET} ${DIM}← 백엔드 실시간 배포 + 라이브 로그${RESET}`);
2261
- log(` ${GREEN}gencow static dist/${RESET} ${DIM}← 정적 파일(dist/) 배포${RESET}`);
2083
+ log(` ${BOLD}💡 Dev environment deploy:${RESET}`);
2084
+ log(` ${GREEN}gencow dev${RESET} ${DIM}← Real-time backend + live logs${RESET}`);
2085
+ log(` ${GREEN}gencow static dist/${RESET} ${DIM}← Static file (dist/) deploy${RESET}`);
2262
2086
  log("");
2263
- log(` ${BOLD}🚀 프로덕션 배포 잠금 해제:${RESET}`);
2264
- log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Pro 플랜으로 업그레이드${RESET}`);
2087
+ log(` ${BOLD}🚀 Unlock production deploy:${RESET}`);
2088
+ log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Pro plan${RESET}`);
2265
2089
  log("");
2266
2090
  } else {
2267
- error(`Prod 생성 실패: ${errData.error || createProdRes.statusText}`);
2091
+ error(`Prod app creation failed: ${errData.error || createProdRes.statusText}`);
2268
2092
  }
2269
2093
  process.exit(1);
2270
2094
  }
@@ -2280,9 +2104,9 @@ ${BOLD}Examples:${RESET}
2280
2104
  writeFileSync(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
2281
2105
 
2282
2106
  if (createProdData.alreadyExists) {
2283
- info(`Prod 확인: ${prodAppId}`);
2107
+ info(`Prod app verified: ${prodAppId}`);
2284
2108
  } else {
2285
- success(`Prod 생성 완료: ${prodAppId}`);
2109
+ success(`Prod app created: ${prodAppId}`);
2286
2110
  info(`URL: ${createProdData.url}`);
2287
2111
  // 프로비저닝 대기
2288
2112
  await new Promise(r => setTimeout(r, 3000));
@@ -2291,23 +2115,23 @@ ${BOLD}Examples:${RESET}
2291
2115
 
2292
2116
  // 배포 대상을 prod 앱으로 전환
2293
2117
  appId = prodAppId;
2294
- info(`배포 대상: ${CYAN}${appId}${RESET} (production)`);
2118
+ info(`Deploy target: ${CYAN}${appId}${RESET} (production)`);
2295
2119
  }
2296
2120
 
2297
2121
  log(`\n${BOLD}${CYAN}Gencow Deploy${RESET}\n`);
2298
2122
  if (appId) {
2299
- info(`앱 ID: ${appId}`);
2123
+ info(`App: ${appId}`);
2300
2124
  } else {
2301
- info(`앱: 신규 (ID 자동 생성)`);
2125
+ info(`App: new (auto-generated ID)`);
2302
2126
  }
2303
- info(`환경: ${envTarget}`);
2304
- info(`포맷: tar.gz`);
2127
+ info(`Env: ${envTarget}`);
2128
+ info(`Format: tar.gz`);
2305
2129
  log("");
2306
2130
 
2307
2131
  // 0-1. drizzle-kit generate 자동 실행 (번들링 전 migrations/ 최신화)
2308
2132
  {
2309
2133
  const { execSync: execGen } = await import("child_process");
2310
- info("스키마 마이그레이션 생성 중...");
2134
+ info("Generating schema migrations...");
2311
2135
  try {
2312
2136
  if (isMonorepo() && !isStandaloneProject()) {
2313
2137
  // 모노레포: package.json scripts.db:generate 사용
@@ -2332,18 +2156,18 @@ ${BOLD}Examples:${RESET}
2332
2156
  msg.includes("nothing to migrate") ||
2333
2157
  msg.includes("No changes detected")
2334
2158
  ) {
2335
- log(`${DIM} 스키마 변경 없음기존 마이그레이션 유지${RESET}`);
2159
+ log(`${DIM} Migrations up-to-dateno new schema changes detected${RESET}`);
2336
2160
  } else {
2337
- warn(`마이그레이션 생성 경고: ${msg.split("\n")[0] || "unknown"}`);
2338
- warn("gencow/migrations/ 없거나 오래된 경우 플랫폼에서 스키마 적용이 스킵될 수 있습니다.");
2339
- info("수동 실행: gencow db:generate");
2161
+ warn(`Migration generation warning: ${msg.split("\n")[0] || "unknown"}`);
2162
+ warn("gencow/migrations/ missing or outdated platform may skip schema apply.");
2163
+ info("Manual run: gencow db:generate");
2340
2164
  }
2341
2165
  }
2342
2166
  }
2343
2167
  log("");
2344
2168
 
2345
2169
  // 1. tar.gz 패키징
2346
- info("프로젝트 패키징 중...");
2170
+ info("Packaging project...");
2347
2171
  const { execSync: exec } = await import("child_process");
2348
2172
  const tmpBundle = resolve(process.cwd(), ".gencow", "deploy-bundle.tar.gz");
2349
2173
  mkdirSync(dirname(tmpBundle), { recursive: true });
@@ -2356,7 +2180,7 @@ ${BOLD}Examples:${RESET}
2356
2180
 
2357
2181
  // gencow/ 디렉토리 존재 확인
2358
2182
  if (!existsSync(resolve(process.cwd(), "gencow"))) {
2359
- error("gencow/ 디렉토리가 없습니다. Gencow 프로젝트 루트에서 실행하세요.");
2183
+ error("gencow/ directory not found. Run from Gencow project root.");
2360
2184
  process.exit(1);
2361
2185
  }
2362
2186
 
@@ -2373,7 +2197,7 @@ ${BOLD}Examples:${RESET}
2373
2197
  if (auditMsg) log(auditMsg);
2374
2198
  }
2375
2199
  } catch (auditErr) {
2376
- warn(`의존성 검사 스킵: ${auditErr.message}`);
2200
+ warn(`Skipping dependency audit: ${auditErr.message}`);
2377
2201
  }
2378
2202
  }
2379
2203
 
@@ -2395,7 +2219,7 @@ ${BOLD}Examples:${RESET}
2395
2219
  const totalDeps = Object.keys(allDeps).length;
2396
2220
  const runtimeCount = Object.keys(runtimeDeps).length;
2397
2221
  if (totalDeps > runtimeCount) {
2398
- info(`${DIM}package.json 필터링: ${runtimeCount}/${totalDeps} 패키지만 서버에 설치됩니다.${RESET}`);
2222
+ info(`${DIM}package.json filtered: ${runtimeCount}/${totalDeps} packages will be installed on server.${RESET}`);
2399
2223
  }
2400
2224
  } catch {
2401
2225
  // Fallback: use original package.json
@@ -2425,7 +2249,7 @@ ${BOLD}Examples:${RESET}
2425
2249
  exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
2426
2250
  }
2427
2251
  } catch (e) {
2428
- error(`패키징 실패: ${e.message}`);
2252
+ error(`Packaging failed: ${e.message}`);
2429
2253
  process.exit(1);
2430
2254
  }
2431
2255
 
@@ -2433,7 +2257,7 @@ ${BOLD}Examples:${RESET}
2433
2257
  try { if (useFilteredPkg) unlinkSync(filteredPkgPath); } catch {}
2434
2258
 
2435
2259
  const bundleSize = statSync(tmpBundle).size;
2436
- success(`번들 생성: ${(bundleSize / 1024).toFixed(1)} KB`);
2260
+ success(`Bundle created: ${(bundleSize / 1024).toFixed(1)} KB`);
2437
2261
 
2438
2262
  // 2-0. 번들 크기 경고 (Ph2: 리소스 미터링 — 차단 아님, 안내만)
2439
2263
  try {
@@ -2449,13 +2273,13 @@ ${BOLD}Examples:${RESET}
2449
2273
 
2450
2274
  // 2. 앱이 없으면 생성 (appId가 없는 경우)
2451
2275
  if (!appId) {
2452
- info(" 생성 (ID 자동 생성)...");
2276
+ info("Creating app (auto-generating ID)...");
2453
2277
 
2454
2278
  const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
2455
2279
 
2456
2280
  if (!createRes.ok) {
2457
2281
  const createErr = await createRes.json().catch(() => ({}));
2458
- error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
2282
+ error(`App creation failed: ${createErr.error || createRes.statusText}`);
2459
2283
  process.exit(1);
2460
2284
  }
2461
2285
 
@@ -2463,11 +2287,11 @@ ${BOLD}Examples:${RESET}
2463
2287
  appId = createData.appId || createData.name;
2464
2288
 
2465
2289
  if (!appId) {
2466
- error(" 생성 응답에 appId가 없습니다.");
2290
+ error("App creation response missing appId.");
2467
2291
  process.exit(1);
2468
2292
  }
2469
2293
 
2470
- success(`앱 "${appId}" 생성 완료. 프로비저닝 대기 중...`);
2294
+ success(`App "${appId}" created. Waiting for provisioning...`);
2471
2295
 
2472
2296
  // gencow.json 저장 (즉시)
2473
2297
  writeFileSync(gencowJsonPath, JSON.stringify({
@@ -2475,7 +2299,7 @@ ${BOLD}Examples:${RESET}
2475
2299
  displayName,
2476
2300
  platformUrl: creds.platformUrl,
2477
2301
  }, null, 2));
2478
- info(`${DIM}gencow.json 생성됨 (appId: ${appId})${RESET}`);
2302
+ info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
2479
2303
 
2480
2304
  await new Promise(r => setTimeout(r, 3000));
2481
2305
  }
@@ -2489,7 +2313,7 @@ ${BOLD}Examples:${RESET}
2489
2313
  let spinnerIdx = 0;
2490
2314
  const spinner = setInterval(() => {
2491
2315
  const elapsed = ((Date.now() - deployStartTime) / 1000).toFixed(0);
2492
- process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} 배포 중... ${DIM}(${elapsed}s)${RESET} `);
2316
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Deploying... ${DIM}(${elapsed}s)${RESET} `);
2493
2317
  }, 120);
2494
2318
 
2495
2319
  const deployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
@@ -2512,19 +2336,19 @@ ${BOLD}Examples:${RESET}
2512
2336
 
2513
2337
  // 앱이 없으면 (appId가 stale한 경우) 새로 생성 후 재시도
2514
2338
  if (deployRes.status === 404) {
2515
- warn(`앱 "${appId}" 찾을 없습니다. 새로 생성합니다...`);
2339
+ warn(`App "${appId}" not found. Creating new one...`);
2516
2340
 
2517
2341
  const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
2518
2342
 
2519
2343
  if (!createRes.ok) {
2520
2344
  const createErr = await createRes.json().catch(() => ({}));
2521
- error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
2345
+ error(`App creation failed: ${createErr.error || createRes.statusText}`);
2522
2346
  process.exit(1);
2523
2347
  }
2524
2348
 
2525
2349
  const createData = await createRes.json();
2526
2350
  appId = createData.appId || createData.name;
2527
- success(`앱 "${appId}" 생성 완료. 프로비저닝 대기 중...`);
2351
+ success(`App "${appId}" created. Provisioning...`);
2528
2352
 
2529
2353
  // gencow.json 업데이트
2530
2354
  writeFileSync(gencowJsonPath, JSON.stringify({
@@ -2536,7 +2360,7 @@ ${BOLD}Examples:${RESET}
2536
2360
  await new Promise(r => setTimeout(r, 3000));
2537
2361
 
2538
2362
  // 재배포
2539
- info("재배포 시도...");
2363
+ info("Retrying deploy...");
2540
2364
  exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
2541
2365
  const retryBuffer = readFileSync(tmpBundle);
2542
2366
 
@@ -2544,7 +2368,7 @@ ${BOLD}Examples:${RESET}
2544
2368
  let retrySpinnerIdx = 0;
2545
2369
  const retrySpinner = setInterval(() => {
2546
2370
  const elapsed = ((Date.now() - retryStartTime) / 1000).toFixed(0);
2547
- process.stdout.write(`\r ${CYAN}${spinnerFrames[retrySpinnerIdx++ % spinnerFrames.length]}${RESET} 배포 중... ${DIM}(${elapsed}s)${RESET} `);
2371
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[retrySpinnerIdx++ % spinnerFrames.length]}${RESET} Deploying... ${DIM}(${elapsed}s)${RESET} `);
2548
2372
  }, 120);
2549
2373
 
2550
2374
  const retryRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
@@ -2562,10 +2386,10 @@ ${BOLD}Examples:${RESET}
2562
2386
 
2563
2387
  if (!retryRes.ok) {
2564
2388
  const retryErr = await retryRes.json().catch(() => ({}));
2565
- error(`배포 실패: ${retryErr.error || retryRes.statusText}`);
2389
+ error(`Deploy failed: ${retryErr.error || retryRes.statusText}`);
2566
2390
  if (retryErr.crashLogs?.length) {
2567
2391
  log("");
2568
- warn("서버 시작 실패 원인:");
2392
+ warn("Server startup failure:");
2569
2393
  for (const line of retryErr.crashLogs) log(` ${DIM}${line}${RESET}`);
2570
2394
  }
2571
2395
  process.exit(1);
@@ -2574,14 +2398,14 @@ ${BOLD}Examples:${RESET}
2574
2398
  const retryData = await retryRes.json();
2575
2399
  const retryElapsed = ((Date.now() - retryStartTime) / 1000).toFixed(1);
2576
2400
  log("");
2577
- success(`서버 빌드 완료! (${retryElapsed}s)`);
2401
+ success(`Server build complete! (${retryElapsed}s)`);
2578
2402
 
2579
2403
  // Health check — 앱 URL로 실제 응답 확인
2580
2404
  if (retryData.url) {
2581
2405
  await this._verifyAppReady(retryData.url, appId);
2582
2406
  }
2583
2407
 
2584
- info(`앱 ID: ${appId}`);
2408
+ info(`App ID: ${appId}`);
2585
2409
  info(`URL: ${retryData.url}`);
2586
2410
  info(`Hash: ${retryData.bundleHash}`);
2587
2411
  updateEnvLocalUrl(retryData.url);
@@ -2589,10 +2413,10 @@ ${BOLD}Examples:${RESET}
2589
2413
  return;
2590
2414
  }
2591
2415
 
2592
- error(`배포 실패: ${errData.error || deployRes.statusText}`);
2416
+ error(`Deploy failed: ${errData.error || deployRes.statusText}`);
2593
2417
  if (errData.crashLogs?.length) {
2594
2418
  log("");
2595
- warn("서버 시작 실패 원인:");
2419
+ warn("Server startup failure cause:");
2596
2420
  for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
2597
2421
  }
2598
2422
  process.exit(1);
@@ -2602,14 +2426,14 @@ ${BOLD}Examples:${RESET}
2602
2426
  const deployElapsed = ((Date.now() - deployStartTime) / 1000).toFixed(1);
2603
2427
 
2604
2428
  log("");
2605
- success(`서버 빌드 완료! (${deployElapsed}s)`);
2429
+ success(`Server build complete! (${deployElapsed}s)`);
2606
2430
 
2607
2431
  // Health check — 앱 URL로 실제 응답 확인 (Blue-Green은 서버에서 완료됐지만, edge case 방어)
2608
2432
  if (deployData.url) {
2609
2433
  await this._verifyAppReady(deployData.url, appId);
2610
2434
  }
2611
2435
 
2612
- info(`앱 ID: ${appId}`);
2436
+ info(`App ID: ${appId}`);
2613
2437
  info(`URL: ${deployData.url}`);
2614
2438
  info(`Hash: ${deployData.bundleHash}`);
2615
2439
  if (deployData.deployId) info(`ID: ${deployData.deployId}`);
@@ -2623,7 +2447,7 @@ ${BOLD}Examples:${RESET}
2623
2447
  displayName,
2624
2448
  platformUrl: creds.platformUrl,
2625
2449
  }, null, 2));
2626
- info(`${DIM}gencow.json 생성됨${RESET}`);
2450
+ info(`${DIM}gencow.json created${RESET}`);
2627
2451
  }
2628
2452
  },
2629
2453
 
@@ -2643,7 +2467,7 @@ ${BOLD}Examples:${RESET}
2643
2467
  // health check 스피너
2644
2468
  const healthSpinner = setInterval(() => {
2645
2469
  const elapsed = ((Date.now() - start) / 1000).toFixed(0);
2646
- process.stdout.write(`\r ${CYAN}${spinnerFrames[idx++ % spinnerFrames.length]}${RESET} 준비 확인 중... ${DIM}(${elapsed}s)${RESET} `);
2470
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[idx++ % spinnerFrames.length]}${RESET} Checking app readiness... ${DIM}(${elapsed}s)${RESET} `);
2647
2471
  }, 120);
2648
2472
 
2649
2473
  let healthy = false;
@@ -2669,10 +2493,10 @@ ${BOLD}Examples:${RESET}
2669
2493
 
2670
2494
  const healthElapsed = ((Date.now() - start) / 1000).toFixed(1);
2671
2495
  if (healthy) {
2672
- success(`앱 Ready! (${healthElapsed}s)`);
2496
+ success(`App Ready! (${healthElapsed}s)`);
2673
2497
  } else {
2674
- warn(`앱 응답 확인 실패 (${healthElapsed}s). 서버가 시작 중일 있습니다.`);
2675
- info(`수동 확인: ${CYAN}${appUrl}${RESET}`);
2498
+ warn(`App response check failed (${healthElapsed}s). Server may still be starting.`);
2499
+ info(`Manual check: ${CYAN}${appUrl}${RESET}`);
2676
2500
  }
2677
2501
  },
2678
2502
 
@@ -2695,9 +2519,9 @@ ${BOLD}Examples:${RESET}
2695
2519
  }
2696
2520
 
2697
2521
  if (!targetDir || !existsSync(resolve(process.cwd(), targetDir))) {
2698
- error(`정적 파일 폴더를 찾을 수 없습니다.`);
2699
- info(`자동 감지 순서: ${AUTO_DETECT.join(", ")}`);
2700
- info(`수동 지정: gencow static <dir>`);
2522
+ error(`Static files directory not found.`);
2523
+ info(`Auto-detect order: ${AUTO_DETECT.join(", ")}`);
2524
+ info(`Specify manually: gencow static <dir>`);
2701
2525
  process.exit(1);
2702
2526
  }
2703
2527
 
@@ -2748,22 +2572,22 @@ ${BOLD}Examples:${RESET}
2748
2572
  const shouldDeployBackend = detectedBackend && !noBackend && !isBackendEmpty;
2749
2573
 
2750
2574
  if (isBackendEmpty && detectedBackend) {
2751
- info(`${DIM}gencow/ 감지됨 — API 함수(query/mutation) 없음백엔드 배포 건너뜀${RESET}`);
2752
- info(`${DIM}💡 백엔드가 필요하면 gencow/ .ts 파일에 query() 또는 mutation() 정의하세요.${RESET}`);
2575
+ info(`${DIM}gencow/ detectedno API functions (query/mutation) foundskipping backend deploy${RESET}`);
2576
+ info(`${DIM}💡 If you need a backend, define query() or mutation() in gencow/*.ts files.${RESET}`);
2753
2577
  }
2754
2578
 
2755
2579
  if (shouldDeployBackend) {
2756
2580
  log(`\n${BOLD}${CYAN}Gencow Deploy (Fullstack)${RESET}\n`);
2757
- info(`백엔드 감지: ${backendRoot === process.cwd() ? "gencow/" : resolve(backendRoot, "gencow/")} (자동 배포)`);
2758
- info(`프론트엔드: ${targetDir}/`);
2581
+ info(`Backend detected: ${backendRoot === process.cwd() ? "gencow/" : resolve(backendRoot, "gencow/")} (auto-deploy)`);
2582
+ info(`Frontend: ${targetDir}/`);
2759
2583
  } else {
2760
2584
  log(`\n${BOLD}${CYAN}Gencow Static Deploy${RESET}\n`);
2761
- info(`폴더: ${targetDir}/`);
2585
+ info(`Directory: ${targetDir}/`);
2762
2586
  }
2763
2587
  if (appId) {
2764
- info(`앱 ID: ${appId}`);
2588
+ info(`App ID: ${appId}`);
2765
2589
  } else {
2766
- info(`앱: 신규 (ID 자동 생성)`);
2590
+ info(`App: new (ID auto-generated)`);
2767
2591
  }
2768
2592
  log("");
2769
2593
 
@@ -2795,23 +2619,23 @@ ${BOLD}Examples:${RESET}
2795
2619
  }
2796
2620
  if (apiRefFiles.length > 0) {
2797
2621
  log("");
2798
- warn(`빌드 파일에서 API 참조가 발견되었습니다:`);
2622
+ warn(`API references found in build files:`);
2799
2623
  for (const f of apiRefFiles.slice(0, 5)) log(` ${DIM}- ${f}${RESET}`);
2800
- if (apiRefFiles.length > 5) log(` ${DIM}... ${apiRefFiles.length - 5}개${RESET}`);
2624
+ if (apiRefFiles.length > 5) log(` ${DIM}... and ${apiRefFiles.length - 5} more${RESET}`);
2801
2625
  log("");
2802
- warn(`정적 호스팅에는 API 서버가 없어 404가 발생합니다.`);
2803
- info(`💡 백엔드가 필요하다면: ${CYAN}VITE_API_URL=https://{backend}.{도메인} npm run build${RESET} 배포`);
2804
- info(`💡 별도 백엔드를 사용한다면: ${CYAN}VITE_API_URL${RESET} 환경변수로 빌드 대상을 지정하세요.`);
2626
+ warn(`Static hosting has no API server these will return 404.`);
2627
+ info(`💡 If you need a backend: ${CYAN}VITE_API_URL=https://{backend}.{domain} npm run build${RESET} then deploy`);
2628
+ info(`💡 If using a separate backend: set ${CYAN}VITE_API_URL${RESET} env var before building.`);
2805
2629
  log("");
2806
2630
 
2807
2631
  const { createInterface } = await import("readline");
2808
2632
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2809
2633
  const answer = await new Promise(resolve => {
2810
- rl.question(` ${YELLOW}⚠${RESET} 그래도 정적 배포를 계속하시겠습니까? (y/N) `, resolve);
2634
+ rl.question(` ${YELLOW}⚠${RESET} Proceed with static deploy anyway? (y/N) `, resolve);
2811
2635
  });
2812
2636
  rl.close();
2813
2637
  if (answer.toLowerCase() !== "y") {
2814
- info("배포 취소됨.");
2638
+ info("Deploy cancelled.");
2815
2639
  return;
2816
2640
  }
2817
2641
  log("");
@@ -2821,7 +2645,7 @@ ${BOLD}Examples:${RESET}
2821
2645
 
2822
2646
  // ── 백엔드 자동 배포 (감지된 경우) ────────────────────
2823
2647
  if (shouldDeployBackend) {
2824
- log(` ${BOLD}── 백엔드 배포 ──────────────────────${RESET}\n`);
2648
+ log(` ${BOLD}── Backend Deploy ──────────────────────${RESET}\n`);
2825
2649
 
2826
2650
  // 1-0. Pre-deploy dependency audit + Phase B: filter package.json
2827
2651
  let auditResult = null;
@@ -2835,7 +2659,7 @@ ${BOLD}Examples:${RESET}
2835
2659
  if (auditMsg) log(auditMsg);
2836
2660
  }
2837
2661
  } catch (auditErr) {
2838
- warn(`의존성 검사 스킵: ${auditErr.message}`);
2662
+ warn(`Dependency audit skipped: ${auditErr.message}`);
2839
2663
  }
2840
2664
  }
2841
2665
 
@@ -2843,7 +2667,7 @@ ${BOLD}Examples:${RESET}
2843
2667
  // 서버에서 generate 시 stdin이 없어 rename 프롬프트에 막히던 문제 해결
2844
2668
  const schemaPath = resolve(backendRoot, "gencow", "schema.ts");
2845
2669
  if (existsSync(schemaPath)) {
2846
- info("스키마 마이그레이션 생성 중...");
2670
+ info("Generating schema migrations...");
2847
2671
  try {
2848
2672
  const dk = _drizzleKitCmd("generate");
2849
2673
  execSync(dk.cmd, {
@@ -2851,20 +2675,20 @@ ${BOLD}Examples:${RESET}
2851
2675
  env: { ...process.env, ...dk.env },
2852
2676
  stdio: "inherit", // ← 프롬프트 패스스루!
2853
2677
  });
2854
- success("마이그레이션 생성 완료");
2678
+ success("Migrations generated");
2855
2679
  } catch (e) {
2856
2680
  const msg = e.stderr?.toString() || e.message || "";
2857
2681
  if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
2858
- log(`${DIM} 스키마 변경 없음기존 마이그레이션 사용${RESET}`);
2682
+ log(`${DIM} Migrations up-to-dateno new schema changes detected${RESET}`);
2859
2683
  } else {
2860
- warn(`마이그레이션 생성 실패: ${msg.split("\\n")[0]}`);
2861
- info("서버에서 기존 방식으로 스키마 적용을 시도합니다.");
2684
+ warn(`Migration generation failed: ${msg.split("\\n")[0]}`);
2685
+ info("Will attempt schema push via server.");
2862
2686
  }
2863
2687
  }
2864
2688
  }
2865
2689
 
2866
2690
  // 1. tar.gz 패키징 (백엔드)
2867
- info("백엔드 패키징 중...");
2691
+ info("Packaging backend...");
2868
2692
  const { execSync: exec } = await import("child_process");
2869
2693
  const tmpBackendBundle = resolve(backendRoot, ".gencow", "deploy-bundle.tar.gz");
2870
2694
  mkdirSync(dirname(tmpBackendBundle), { recursive: true });
@@ -2892,7 +2716,7 @@ ${BOLD}Examples:${RESET}
2892
2716
  const totalDeps = Object.keys(allDeps).length;
2893
2717
  const runtimeCount = Object.keys(runtimeDeps).length;
2894
2718
  if (totalDeps > runtimeCount) {
2895
- info(`${DIM}package.json 필터링: ${runtimeCount}/${totalDeps} 패키지만 서버에 설치됩니다.${RESET}`);
2719
+ info(`${DIM}package.json filtered: ${runtimeCount}/${totalDeps} packages will be installed on server.${RESET}`);
2896
2720
  }
2897
2721
  } catch {
2898
2722
  useFilteredPkg = false;
@@ -2921,34 +2745,34 @@ ${BOLD}Examples:${RESET}
2921
2745
  exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBackendBundle}" ${backendFiles.join(" ")}`, { cwd: backendRoot });
2922
2746
  }
2923
2747
  } catch (e) {
2924
- error(`백엔드 패키징 실패: ${e.message}`);
2748
+ error(`Backend packaging failed: ${e.message}`);
2925
2749
  process.exit(1);
2926
2750
  }
2927
2751
  try { if (useFilteredPkg) unlinkSync(filteredPkgPath); } catch {}
2928
2752
 
2929
2753
  const backendBundleSize = statSync(tmpBackendBundle).size;
2930
- success(`백엔드 번들 생성: ${(backendBundleSize / 1024).toFixed(1)} KB`);
2754
+ success(`Backend bundle created: ${(backendBundleSize / 1024).toFixed(1)} KB`);
2931
2755
 
2932
2756
  // 2. 앱이 없으면 생성
2933
2757
  if (!appId) {
2934
- info(" 생성 (ID 자동 생성)...");
2758
+ info("Creating app (auto-generating ID)...");
2935
2759
  const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
2936
2760
  if (!createRes.ok) {
2937
2761
  const createErr = await createRes.json().catch(() => ({}));
2938
- error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
2762
+ error(`App creation failed: ${createErr.error || createRes.statusText}`);
2939
2763
  process.exit(1);
2940
2764
  }
2941
2765
  const createData = await createRes.json();
2942
2766
  appId = createData.appId || createData.name;
2943
2767
  if (!appId) {
2944
- error(" 생성 응답에 appId가 없습니다.");
2768
+ error("App creation response missing appId.");
2945
2769
  process.exit(1);
2946
2770
  }
2947
- success(`앱 "${appId}" 생성 완료. 프로비저닝 대기 중...`);
2771
+ success(`App "${appId}" created. Provisioning...`);
2948
2772
  writeFileSync(gencowJsonPath, JSON.stringify({
2949
2773
  appId, displayName, platformUrl: creds.platformUrl,
2950
2774
  }, null, 2));
2951
- info(`${DIM}gencow.json 생성됨 (appId: ${appId})${RESET}`);
2775
+ info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
2952
2776
  await new Promise(r => setTimeout(r, 3000));
2953
2777
  }
2954
2778
 
@@ -2961,7 +2785,7 @@ ${BOLD}Examples:${RESET}
2961
2785
  let spinnerIdx = 0;
2962
2786
  const backendSpinner = setInterval(() => {
2963
2787
  const elapsed = ((Date.now() - backendStartTime) / 1000).toFixed(0);
2964
- process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} 백엔드 배포 중... ${DIM}(${elapsed}s)${RESET} `);
2788
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Deploying backend... ${DIM}(${elapsed}s)${RESET} `);
2965
2789
  }, 120);
2966
2790
 
2967
2791
  const backendDeployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
@@ -2976,24 +2800,24 @@ ${BOLD}Examples:${RESET}
2976
2800
 
2977
2801
  if (!backendDeployRes.ok) {
2978
2802
  const errData = await backendDeployRes.json().catch(() => ({}));
2979
- error(`백엔드 배포 실패: ${errData.error || backendDeployRes.statusText}`);
2803
+ error(`Backend deploy failed: ${errData.error || backendDeployRes.statusText}`);
2980
2804
  if (errData.crashLogs?.length) {
2981
2805
  log("");
2982
- warn("서버 시작 실패 원인:");
2806
+ warn("Server startup failure:");
2983
2807
  for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
2984
2808
  }
2985
2809
  if (targetDir) {
2986
2810
  // --static 모드: 백엔드 실패해도 프론트엔드 배포는 계속 진행
2987
- warn("백엔드 배포 실패정적 파일 배포는 계속 진행합니다.");
2811
+ warn("Backend deploy failedcontinuing with static deploy.");
2988
2812
  log("");
2989
2813
  } else {
2990
- error("백엔드 배포 실패로 프론트엔드 배포를 건너뜁니다.");
2814
+ error("Backend deploy failed, skipping frontend deploy.");
2991
2815
  process.exit(1);
2992
2816
  }
2993
2817
  } else {
2994
2818
  const backendData = await backendDeployRes.json();
2995
2819
  const backendElapsed = ((Date.now() - backendStartTime) / 1000).toFixed(1);
2996
- success(`백엔드 빌드 완료! (${backendElapsed}s)`);
2820
+ success(`Backend build complete! (${backendElapsed}s)`);
2997
2821
 
2998
2822
  // Health check — 백엔드 앱 URL로 실제 응답 확인
2999
2823
  if (backendData.url) {
@@ -3005,11 +2829,11 @@ ${BOLD}Examples:${RESET}
3005
2829
  updateEnvLocalUrl(backendData.url);
3006
2830
  log("");
3007
2831
  }
3008
- log(` ${BOLD}── 프론트엔드 배포 ──────────────────${RESET}\n`);
2832
+ log(` ${BOLD}── Frontend Deploy ──────────────────${RESET}\n`);
3009
2833
  }
3010
2834
 
3011
2835
  // 1. tar.gz 패키징
3012
- info("정적 파일 패키징 중...");
2836
+ info("Packaging static files...");
3013
2837
  const { execSync: exec } = await import("child_process");
3014
2838
  const tmpBundle = resolve(process.cwd(), ".gencow", "static-bundle.tar.gz");
3015
2839
  mkdirSync(dirname(tmpBundle), { recursive: true });
@@ -3017,40 +2841,40 @@ ${BOLD}Examples:${RESET}
3017
2841
  try {
3018
2842
  exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" -C "${resolve(process.cwd(), targetDir)}" .`, { cwd: process.cwd() });
3019
2843
  } catch (e) {
3020
- error(`패키징 실패: ${e.message}`);
2844
+ error(`Packaging failed: ${e.message}`);
3021
2845
  process.exit(1);
3022
2846
  }
3023
2847
 
3024
2848
  const bundleSize = statSync(tmpBundle).size;
3025
2849
  const MAX_SIZE = 100 * 1024 * 1024;
3026
2850
  if (bundleSize > MAX_SIZE) {
3027
- error(`번들이 너무 큽니다: ${(bundleSize / (1024 * 1024)).toFixed(1)} MB (최대 100 MB)`);
2851
+ error(`Bundle too large: ${(bundleSize / (1024 * 1024)).toFixed(1)} MB (max 100 MB)`);
3028
2852
  try { unlinkSync(tmpBundle); } catch { }
3029
2853
  process.exit(1);
3030
2854
  }
3031
- success(`번들 생성: ${(bundleSize / 1024).toFixed(1)} KB`);
2855
+ success(`Bundle created: ${(bundleSize / 1024).toFixed(1)} KB`);
3032
2856
 
3033
2857
  // 2. 앱이 없으면 생성
3034
2858
  if (!appId) {
3035
- info(" 생성 (ID 자동 생성)...");
2859
+ info("Creating app (auto-generating ID)...");
3036
2860
  const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
3037
2861
  if (!createRes.ok) {
3038
2862
  const createErr = await createRes.json().catch(() => ({}));
3039
- error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
2863
+ error(`App creation failed: ${createErr.error || createRes.statusText}`);
3040
2864
  process.exit(1);
3041
2865
  }
3042
2866
  const createData = await createRes.json();
3043
2867
  appId = createData.appId || createData.name;
3044
- success(`앱 "${appId}" 생성 완료. 프로비저닝 대기 중...`);
2868
+ success(`App "${appId}" created. Provisioning...`);
3045
2869
  writeFileSync(gencowJsonPath, JSON.stringify({
3046
2870
  appId, displayName, platformUrl: creds.platformUrl,
3047
2871
  }, null, 2));
3048
- info(`${DIM}gencow.json 생성됨 (appId: ${appId})${RESET}`);
2872
+ info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
3049
2873
  await new Promise(r => setTimeout(r, 3000));
3050
2874
  }
3051
2875
 
3052
2876
  // 3. 업로드
3053
- info("정적 파일 배포 중...");
2877
+ info("Deploying static files...");
3054
2878
  const bundleBuffer = readFileSync(tmpBundle);
3055
2879
 
3056
2880
  const deployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy-static`, {
@@ -3063,22 +2887,22 @@ ${BOLD}Examples:${RESET}
3063
2887
 
3064
2888
  if (!deployRes.ok) {
3065
2889
  const errData = await deployRes.json().catch(() => ({}));
3066
- error(`정적 배포 실패: ${errData.error || deployRes.statusText}`);
2890
+ error(`Static deploy failed: ${errData.error || deployRes.statusText}`);
3067
2891
  process.exit(1);
3068
2892
  }
3069
2893
 
3070
2894
  const data = await deployRes.json();
3071
2895
  log("");
3072
- success(`정적 배포 완료!`);
3073
- info(`앱 ID: ${appId}`);
2896
+ success(`Static deploy complete!`);
2897
+ info(`App ID: ${appId}`);
3074
2898
  info(`URL: ${data.url}`);
3075
- info(`파일: ${data.files} (${(data.size / 1024).toFixed(1)} KB)`);
2899
+ info(`Files: ${data.files} (${(data.size / 1024).toFixed(1)} KB)`);
3076
2900
  info(`Hash: ${data.bundleHash}`);
3077
2901
  if (data.deployId) info(`ID: ${data.deployId}`);
3078
2902
 
3079
2903
  // 이미지 최적화 결과 표시
3080
2904
  if (data.optimizedImages) {
3081
- success(`🖼 ${data.optimizedImages} 이미지 WebP 최적화 (절약: ${(data.savedBytes / 1024).toFixed(1)} KB)`);
2905
+ success(`🖼 ${data.optimizedImages} images optimized to WebP (saved: ${(data.savedBytes / 1024).toFixed(1)} KB)`);
3082
2906
  }
3083
2907
 
3084
2908
  // skipped 파일 경고 표시
@@ -3131,12 +2955,12 @@ ${BOLD}Examples:${RESET}
3131
2955
 
3132
2956
  // --prod인데 prod 앱이 없는 경우
3133
2957
  if (envTarget === "prod" && appId && !appId.endsWith("-prod")) {
3134
- error("Prod 앱이 아직 없습니다. gencow deploy 먼저 실행하세요.");
2958
+ error("No prod app yet. Run gencow deploy first.");
3135
2959
  return;
3136
2960
  }
3137
2961
 
3138
2962
  if (!appId) {
3139
- error(" ID 찾을 없습니다. gencow deploy 먼저 실행하세요.");
2963
+ error("App ID not found. Run gencow deploy first.");
3140
2964
  return;
3141
2965
  }
3142
2966
 
@@ -3148,13 +2972,13 @@ ${BOLD}Examples:${RESET}
3148
2972
  { headers: { "Authorization": `Bearer ${creds.apiKey}` } }
3149
2973
  );
3150
2974
  if (!res.ok) {
3151
- error(`환경변수 조회 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
2975
+ error(`Failed to list env vars: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3152
2976
  return;
3153
2977
  }
3154
2978
  const vars = await res.json();
3155
- log(`\n${BOLD}${CYAN}환경변수${RESET} — ${appId} (${envTarget})\n`);
2979
+ log(`\n${BOLD}${CYAN}Environment Variables${RESET} — ${appId} (${envTarget})\n`);
3156
2980
  if (vars.length === 0) {
3157
- info("설정된 환경변수가 없습니다.");
2981
+ info("No environment variables configured.");
3158
2982
  } else {
3159
2983
  for (const v of vars) {
3160
2984
  log(` ${v.key}`);
@@ -3168,7 +2992,7 @@ ${BOLD}Examples:${RESET}
3168
2992
  // gencow env set KEY=VALUE [KEY2=VALUE2...]
3169
2993
  const kvPairs = restArgs.filter(a => a.includes("=") && !a.startsWith("-"));
3170
2994
  if (kvPairs.length === 0) {
3171
- error("사용법: gencow env set KEY=VALUE [KEY2=VALUE2...]");
2995
+ error("Usage: gencow env set KEY=VALUE [KEY2=VALUE2...]");
3172
2996
  return;
3173
2997
  }
3174
2998
 
@@ -3189,9 +3013,9 @@ ${BOLD}Examples:${RESET}
3189
3013
  );
3190
3014
 
3191
3015
  if (res.ok) {
3192
- success(`${key} 설정 완료 (${envTarget})`);
3016
+ success(`${key} configured (${envTarget})`);
3193
3017
  } else {
3194
- error(`${key} 설정 실패`);
3018
+ error(`${key} configuration failed`);
3195
3019
  }
3196
3020
  }
3197
3021
  break;
@@ -3201,7 +3025,7 @@ ${BOLD}Examples:${RESET}
3201
3025
  case "remove": {
3202
3026
  const keys = restArgs.filter(a => !a.startsWith("-"));
3203
3027
  if (keys.length === 0) {
3204
- error("사용법: gencow env unset KEY [KEY2...]");
3028
+ error("Usage: gencow env unset KEY [KEY2...]");
3205
3029
  return;
3206
3030
  }
3207
3031
 
@@ -3215,9 +3039,9 @@ ${BOLD}Examples:${RESET}
3215
3039
  );
3216
3040
 
3217
3041
  if (res.ok) {
3218
- success(`${key} 삭제 완료`);
3042
+ success(`${key} removed`);
3219
3043
  } else {
3220
- error(`${key} 삭제 실패`);
3044
+ error(`${key} removal failed`);
3221
3045
  }
3222
3046
  }
3223
3047
  break;
@@ -3227,7 +3051,7 @@ ${BOLD}Examples:${RESET}
3227
3051
  // 로컬 .env 파일 → 리모트 일괄 push
3228
3052
  const envFile = resolve(process.cwd(), envTarget === "prod" ? ".env.production" : ".env");
3229
3053
  if (!existsSync(envFile)) {
3230
- error(`${envFile} 파일을 찾을 수 없습니다.`);
3054
+ error(`${envFile} not found.`);
3231
3055
  return;
3232
3056
  }
3233
3057
 
@@ -3241,7 +3065,7 @@ ${BOLD}Examples:${RESET}
3241
3065
  }
3242
3066
 
3243
3067
  const count = Object.keys(vars).length;
3244
- info(`${count} 환경변수를 ${appId} (${envTarget})에 push합니다...`);
3068
+ info(`Pushing ${count} env vars to ${appId} (${envTarget})...`);
3245
3069
 
3246
3070
  const res = await fetch(
3247
3071
  `${creds.platformUrl}/platform/apps/${appId}/env/bulk`,
@@ -3256,16 +3080,16 @@ ${BOLD}Examples:${RESET}
3256
3080
  );
3257
3081
 
3258
3082
  if (res.ok) {
3259
- success(`${count} 환경변수 push 완료`);
3083
+ success(`${count} env vars pushed`);
3260
3084
  } else {
3261
- error(`push 실패: ${(await res.json().catch(() => ({}))).error}`);
3085
+ error(`Push failed: ${(await res.json().catch(() => ({}))).error}`);
3262
3086
  }
3263
3087
  break;
3264
3088
  }
3265
3089
 
3266
3090
  default:
3267
- error(`알 없는 하위 명령: ${subCmd}`);
3268
- info("사용법: gencow env [list|set|unset|push] ...");
3091
+ error(`Unknown subcommand: ${subCmd}`);
3092
+ info("Usage: gencow env [list|set|unset|push] ...");
3269
3093
  }
3270
3094
  },
3271
3095
 
@@ -3275,8 +3099,9 @@ ${BOLD}Examples:${RESET}
3275
3099
  const subCmd = configArgs[0] || "help";
3276
3100
  const restArgs = configArgs.slice(1);
3277
3101
 
3278
- // ID 결정
3102
+ // Resolve appId + --prod support
3279
3103
  let appId = null;
3104
+ const isProd = restArgs.includes("--prod");
3280
3105
  for (let i = 0; i < restArgs.length; i++) {
3281
3106
  if (restArgs[i] === "--app" || restArgs[i] === "-a") appId = restArgs[++i];
3282
3107
  }
@@ -3286,22 +3111,31 @@ ${BOLD}Examples:${RESET}
3286
3111
  if (existsSync(gencowJsonPath)) {
3287
3112
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
3288
3113
  appId = gencowJson.appId || gencowJson.appName;
3114
+ if (isProd && gencowJson.prodApp) {
3115
+ appId = gencowJson.prodApp;
3116
+ }
3289
3117
  }
3290
3118
  }
3291
3119
 
3292
3120
  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`);
3121
+ log(`\n${BOLD}${CYAN}gencow config${RESET} — App configuration\n`);
3122
+ log(` ${CYAN}set${RESET} image.maxWidth <value> Auto WebP max width (px)`);
3123
+ log(` ${CYAN}set${RESET} image.quality <value> Auto WebP quality (1-100)`);
3124
+ log(` ${CYAN}get${RESET} image Show current image config`);
3125
+ log(` ${CYAN}reset${RESET} image Reset to tier defaults\n`);
3126
+ log(` ${DIM}Options:${RESET}`);
3127
+ log(` ${DIM}--app, -a <name> Target app (default: from gencow.json)${RESET}`);
3128
+ log(` ${DIM}--prod Target production app${RESET}\n`);
3129
+ return;
3130
+ }
3131
+
3132
+ if (isProd && appId && !appId.endsWith("-prod")) {
3133
+ error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
3300
3134
  return;
3301
3135
  }
3302
3136
 
3303
3137
  if (!appId) {
3304
- error(" ID를 찾을 없습니다. gencow deploy를 먼저 실행하세요.");
3138
+ error("App not found. Run 'gencow dev' first.");
3305
3139
  return;
3306
3140
  }
3307
3141
 
@@ -3311,7 +3145,7 @@ ${BOLD}Examples:${RESET}
3311
3145
  const val = restArgs[restArgs.indexOf(key) + 1];
3312
3146
 
3313
3147
  if (!key || val === undefined) {
3314
- error("사용법: gencow config set image.maxWidth <값>");
3148
+ error("Usage: gencow config set image.maxWidth <value>");
3315
3149
  return;
3316
3150
  }
3317
3151
 
@@ -3319,34 +3153,34 @@ ${BOLD}Examples:${RESET}
3319
3153
  if (key === "image.maxWidth" || key === "image.max-width") {
3320
3154
  const v = parseInt(val);
3321
3155
  if (isNaN(v) || v < 0 || v > 10000) {
3322
- error("maxWidth 0~10000 사이의 값이어야 합니다. (0 = 리셋)");
3156
+ error("maxWidth must be between 0 and 10000 (0 = reset)");
3323
3157
  return;
3324
3158
  }
3325
3159
  imageConfig.autoMaxWidth = v;
3326
3160
  } else if (key === "image.quality") {
3327
3161
  const v = parseInt(val);
3328
3162
  if (isNaN(v) || v < 0 || v > 100) {
3329
- error("quality 0~100 사이의 값이어야 합니다. (0 = 리셋)");
3163
+ error("quality must be between 0 and 100 (0 = reset)");
3330
3164
  return;
3331
3165
  }
3332
3166
  imageConfig.autoQuality = v;
3333
3167
  } else {
3334
- error(`알 없는 설정 키: ${key}`);
3335
- info("사용 가능: image.maxWidth, image.quality");
3168
+ error(`Unknown config key: ${key}`);
3169
+ info("Available: image.maxWidth, image.quality");
3336
3170
  return;
3337
3171
  }
3338
3172
 
3339
3173
  const res = await rpcMutation(creds, "apps.updateImageConfig", { name: appId, imageConfig });
3340
3174
  if (res.ok) {
3341
3175
  const data = await res.json();
3342
- success(`${key} = ${val} 설정 완료`);
3176
+ success(`${key} = ${val} configured`);
3343
3177
  if (data.imageConfig) {
3344
- info(`현재 설정: ${JSON.stringify(data.imageConfig)}`);
3178
+ info(`Current config: ${JSON.stringify(data.imageConfig)}`);
3345
3179
  }
3346
- info(`${DIM} 다음 이미지 요청부터 적용됩니다. 기존 캐시는 별도 키로 저장됩니다.${RESET}`);
3180
+ info(`${DIM}Takes effect on next image request. Existing cached images use separate keys.${RESET}`);
3347
3181
  } else {
3348
3182
  const errData = await res.json().catch(() => ({}));
3349
- error(`설정 실패: ${errData.error || res.statusText}`);
3183
+ error(`Config update failed: ${errData.error || res.statusText}`);
3350
3184
  }
3351
3185
  break;
3352
3186
  }
@@ -3354,24 +3188,24 @@ ${BOLD}Examples:${RESET}
3354
3188
  case "get": {
3355
3189
  const key = restArgs.find(a => !a.startsWith("-"));
3356
3190
  if (key && key !== "image") {
3357
- error(`알 없는 설정 키: ${key}`);
3358
- info("사용 가능: image");
3191
+ error(`Unknown config key: ${key}`);
3192
+ info("Available: image");
3359
3193
  return;
3360
3194
  }
3361
3195
 
3362
3196
  const res = await rpcQuery(creds, "apps.getImageConfig", { name: appId });
3363
3197
  if (res.ok) {
3364
3198
  const config = await res.json();
3365
- log(`\n${BOLD}${CYAN}이미지 설정${RESET} — ${appId}\n`);
3199
+ log(`\n${BOLD}${CYAN}Image Config${RESET} — ${appId}\n`);
3366
3200
  if (Object.keys(config).length === 0) {
3367
- info("커스텀 설정 없음 (Tier 기본값 사용)");
3201
+ info("No custom config (using tier defaults)");
3368
3202
  } else {
3369
3203
  if (config.autoMaxWidth) log(` ${GREEN}autoMaxWidth${RESET} ${config.autoMaxWidth} px`);
3370
3204
  if (config.autoQuality) log(` ${GREEN}autoQuality${RESET} ${config.autoQuality}`);
3371
3205
  }
3372
3206
  log("");
3373
3207
  } else {
3374
- error("설정 조회 실패");
3208
+ error("Failed to get config");
3375
3209
  }
3376
3210
  break;
3377
3211
  }
@@ -3379,7 +3213,7 @@ ${BOLD}Examples:${RESET}
3379
3213
  case "reset": {
3380
3214
  const key = restArgs.find(a => !a.startsWith("-"));
3381
3215
  if (key && key !== "image") {
3382
- error(`알 없는 설정 키: ${key}`);
3216
+ error(`Unknown config key: ${key}`);
3383
3217
  return;
3384
3218
  }
3385
3219
 
@@ -3389,16 +3223,16 @@ ${BOLD}Examples:${RESET}
3389
3223
  imageConfig: { autoMaxWidth: 0, autoQuality: 0 },
3390
3224
  });
3391
3225
  if (res.ok) {
3392
- success("이미지 설정 초기화 완료 (Tier 기본값 사용)");
3226
+ success("Image config reset (using tier defaults)");
3393
3227
  } else {
3394
- error("초기화 실패");
3228
+ error("Reset failed");
3395
3229
  }
3396
3230
  break;
3397
3231
  }
3398
3232
 
3399
3233
  default:
3400
- error(`알 없는 하위 명령: ${subCmd}`);
3401
- info("사용법: gencow config [set|get|reset] ...");
3234
+ error(`Unknown subcommand: ${subCmd}`);
3235
+ info("Usage: gencow config [set|get|reset] ...");
3402
3236
  }
3403
3237
  },
3404
3238
 
@@ -3407,29 +3241,32 @@ ${BOLD}Examples:${RESET}
3407
3241
  const subCmd = filesArgs[0] || "help";
3408
3242
  const restArgs = filesArgs.slice(1);
3409
3243
 
3410
- // help는 인증/앱 정보 없이도 표시
3244
+ // help
3411
3245
  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`);
3246
+ log(`\n${BOLD}${CYAN}gencow files${RESET} — File management\n`);
3247
+ log(` ${CYAN}upload${RESET} <path...> [--recursive|-r] Upload files`);
3248
+ log(` ${CYAN}list${RESET} List uploaded files`);
3249
+ log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y] Delete a file`);
3250
+ log(` ${CYAN}url${RESET} <storage_id> Get serving URL`);
3251
+ log(`\n ${DIM}Options:${RESET}`);
3252
+ log(` ${DIM}--app, -a <name> Target app (default: from gencow.json)${RESET}`);
3253
+ log(` ${DIM}--prod Target production app${RESET}\n`);
3419
3254
  return;
3420
3255
  }
3421
3256
 
3422
3257
  const creds = requireCreds();
3423
3258
 
3424
- // ID 결정 + --app 플래그 위치 추적 (paths 필터링용)
3259
+ // Resolve appId + track --app/--prod flag positions
3425
3260
  let appId = null;
3426
- const flagIndices = new Set(); // --app와 그 값의 인덱스
3261
+ const isProd = restArgs.includes("--prod");
3262
+ const flagIndices = new Set();
3427
3263
  for (let i = 0; i < restArgs.length; i++) {
3428
3264
  if (restArgs[i] === "--app" || restArgs[i] === "-a") {
3429
3265
  flagIndices.add(i);
3430
3266
  flagIndices.add(i + 1);
3431
3267
  appId = restArgs[++i];
3432
3268
  }
3269
+ if (restArgs[i] === "--prod") flagIndices.add(i);
3433
3270
  }
3434
3271
 
3435
3272
  if (!appId) {
@@ -3437,11 +3274,19 @@ ${BOLD}Examples:${RESET}
3437
3274
  if (existsSync(gencowJsonPath)) {
3438
3275
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
3439
3276
  appId = gencowJson.appId || gencowJson.appName;
3277
+ if (isProd && gencowJson.prodApp) {
3278
+ appId = gencowJson.prodApp;
3279
+ }
3440
3280
  }
3441
3281
  }
3442
3282
 
3283
+ if (isProd && appId && !appId.endsWith("-prod")) {
3284
+ error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
3285
+ return;
3286
+ }
3287
+
3443
3288
  if (!appId) {
3444
- error(" ID를 찾을 없습니다. gencow deploy를 먼저 실행하세요.");
3289
+ error("App not found. Run 'gencow dev' first.");
3445
3290
  return;
3446
3291
  }
3447
3292
 
@@ -3472,11 +3317,11 @@ ${BOLD}Examples:${RESET}
3472
3317
 
3473
3318
  // 로컬 크기 검증 (서버 왕복 방지)
3474
3319
  if (stat.size > MAX_FILE_SIZE) {
3475
- error(`${fileName}: ${fmtFileSize(stat.size)} — 50MB 제한 초과`);
3320
+ error(`${fileName}: ${fmtFileSize(stat.size)} — exceeds 50MB limit`);
3476
3321
  return null;
3477
3322
  }
3478
3323
 
3479
- info(`${fileName} (${fmtFileSize(stat.size)}) 업로드 중...`);
3324
+ info(`Uploading ${fileName} (${fmtFileSize(stat.size)})...`);
3480
3325
 
3481
3326
  const fileBuffer = readFileSync(filePath);
3482
3327
  const blob = new Blob([fileBuffer]);
@@ -3533,7 +3378,7 @@ ${BOLD}Examples:${RESET}
3533
3378
  const paths = restArgs.filter((a, i) => !a.startsWith("-") && !flagIndices.has(i));
3534
3379
 
3535
3380
  if (paths.length === 0) {
3536
- error("사용법: gencow files upload <파일|폴더 경로...> [--recursive|-r]");
3381
+ error("Usage: gencow files upload <file|dir path...> [--recursive|-r]");
3537
3382
  return;
3538
3383
  }
3539
3384
 
@@ -3541,13 +3386,13 @@ ${BOLD}Examples:${RESET}
3541
3386
  for (const p of paths) {
3542
3387
  const resolved = resolve(process.cwd(), p);
3543
3388
  if (!existsSync(resolved)) {
3544
- error(`파일을 찾을 없습니다: ${p}`);
3389
+ error(`File not found: ${p}`);
3545
3390
  continue;
3546
3391
  }
3547
3392
  const stat = statSync(resolved);
3548
3393
  if (stat.isDirectory()) {
3549
3394
  if (!recursive) {
3550
- error(`${p} 디렉토리입니다. --recursive (-r) 옵션을 사용하세요.`);
3395
+ error(`${p} is a directory. Use --recursive (-r) flag.`);
3551
3396
  continue;
3552
3397
  }
3553
3398
  filesToUpload.push(...collectFiles(resolved));
@@ -3557,12 +3402,12 @@ ${BOLD}Examples:${RESET}
3557
3402
  }
3558
3403
 
3559
3404
  if (filesToUpload.length === 0) {
3560
- error("업로드할 파일이 없습니다.");
3405
+ error("No files to upload.");
3561
3406
  return;
3562
3407
  }
3563
3408
 
3564
- log(`\n${BOLD}${CYAN}파일 업로드${RESET} — ${appId}\n`);
3565
- info(`${filesToUpload.length} 파일 업로드 시작...\n`);
3409
+ log(`\n${BOLD}${CYAN}File Upload${RESET} — ${appId}\n`);
3410
+ info(`Uploading ${filesToUpload.length} files...\n`);
3566
3411
 
3567
3412
  let uploaded = 0;
3568
3413
  let failed = 0;
@@ -3573,14 +3418,14 @@ ${BOLD}Examples:${RESET}
3573
3418
  }
3574
3419
 
3575
3420
  log("");
3576
- if (uploaded > 0) success(`${uploaded} 파일 업로드 완료`);
3577
- if (failed > 0) warn(`${failed} 파일 실패`);
3421
+ if (uploaded > 0) success(`${uploaded} files uploaded`);
3422
+ if (failed > 0) warn(`${failed} files failed`);
3578
3423
  break;
3579
3424
  }
3580
3425
 
3581
3426
  case "list":
3582
3427
  case "ls": {
3583
- log(`\n${BOLD}${CYAN}파일 목록${RESET} — ${appId}\n`);
3428
+ log(`\n${BOLD}${CYAN}File List${RESET} — ${appId}\n`);
3584
3429
 
3585
3430
  const res = await fetch(
3586
3431
  `${creds.platformUrl}/platform/files/list`,
@@ -3596,13 +3441,13 @@ ${BOLD}Examples:${RESET}
3596
3441
 
3597
3442
  const data = await res.json().catch(() => []);
3598
3443
  if (!res.ok) {
3599
- error(data.error || "파일 목록 조회 실패");
3444
+ error(data.error || "Failed to list files");
3600
3445
  return;
3601
3446
  }
3602
3447
 
3603
3448
  const files = Array.isArray(data) ? data : [];
3604
3449
  if (files.length === 0) {
3605
- info("저장된 파일이 없습니다.");
3450
+ info("No files stored.");
3606
3451
  log("");
3607
3452
  return;
3608
3453
  }
@@ -3611,7 +3456,7 @@ ${BOLD}Examples:${RESET}
3611
3456
  const idWidth = Math.max(12, ...files.map(f => (f.storage_id || "").length));
3612
3457
  const nameWidth = Math.max(8, ...files.map(f => (f.name || "").length).map(l => Math.min(l, 40)));
3613
3458
 
3614
- log(` ${DIM}${"ID".padEnd(idWidth)} ${"이름".padEnd(nameWidth)} ${"크기".padStart(10)} ${"소스".padEnd(10)} 업로드 시각${RESET}`);
3459
+ log(` ${DIM}${"ID".padEnd(idWidth)} ${"Name".padEnd(nameWidth)} ${"Size".padStart(10)} ${"Source".padEnd(10)} Uploaded${RESET}`);
3615
3460
  log(` ${DIM}${"─".repeat(idWidth)} ${"─".repeat(nameWidth)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(16)}${RESET}`);
3616
3461
 
3617
3462
  for (const f of files) {
@@ -3621,7 +3466,7 @@ ${BOLD}Examples:${RESET}
3621
3466
  const date = f.created_at ? fmtDate(f.created_at) : "-";
3622
3467
  log(` ${(f.storage_id || "").padEnd(idWidth)} ${name.padEnd(nameWidth)} ${size} ${source} ${date}`);
3623
3468
  }
3624
- log(`\n ${DIM} ${files.length} 파일${RESET}\n`);
3469
+ log(`\n ${DIM}Total: ${files.length} files${RESET}\n`);
3625
3470
  break;
3626
3471
  }
3627
3472
 
@@ -3629,7 +3474,7 @@ ${BOLD}Examples:${RESET}
3629
3474
  case "rm": {
3630
3475
  const storageId = restArgs.find((a, i) => !a.startsWith("-") && !flagIndices.has(i));
3631
3476
  if (!storageId) {
3632
- error("사용법: gencow files delete <storage_id>");
3477
+ error("Usage: gencow files delete <storage_id>");
3633
3478
  return;
3634
3479
  }
3635
3480
 
@@ -3645,7 +3490,7 @@ ${BOLD}Examples:${RESET}
3645
3490
  });
3646
3491
  });
3647
3492
  if (answer !== "y" && answer !== "yes") {
3648
- info("삭제를 취소했습니다.");
3493
+ info("Delete cancelled.");
3649
3494
  return;
3650
3495
  }
3651
3496
  }
@@ -3664,9 +3509,9 @@ ${BOLD}Examples:${RESET}
3664
3509
 
3665
3510
  const data = await res.json().catch(() => ({}));
3666
3511
  if (res.ok && data.success) {
3667
- success(`파일 삭제 완료: ${storageId}`);
3512
+ success(`File deleted: ${storageId}`);
3668
3513
  } else {
3669
- error(`삭제 실패: ${data.error || res.statusText}`);
3514
+ error(`Delete failed: ${data.error || res.statusText}`);
3670
3515
  }
3671
3516
  break;
3672
3517
  }
@@ -3674,7 +3519,7 @@ ${BOLD}Examples:${RESET}
3674
3519
  case "url": {
3675
3520
  const storageId = restArgs.find((a, i) => !a.startsWith("-") && !flagIndices.has(i));
3676
3521
  if (!storageId) {
3677
- error("사용법: gencow files url <storage_id>");
3522
+ error("Usage: gencow files url <storage_id>");
3678
3523
  return;
3679
3524
  }
3680
3525
  log(getFileUrl(storageId));
@@ -3682,14 +3527,14 @@ ${BOLD}Examples:${RESET}
3682
3527
  }
3683
3528
 
3684
3529
  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`);
3530
+ if (subCmd !== "help") error(`Unknown subcommand: ${subCmd}`);
3531
+ log(`\n${BOLD}${CYAN}gencow files${RESET} — File management\n`);
3532
+ log(` ${CYAN}upload${RESET} <path...> [--recursive|-r] Upload files`);
3533
+ log(` ${CYAN}list${RESET} List files`);
3534
+ log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y] Delete file`);
3535
+ log(` ${CYAN}url${RESET} <storage_id> Get serving URL`);
3536
+ log(`\n ${DIM}Options:${RESET}`);
3537
+ log(` ${DIM}--app, -a <name> Target app (default: gencow.json)${RESET}\n`);
3693
3538
  }
3694
3539
  },
3695
3540
 
@@ -3714,7 +3559,7 @@ ${BOLD}Examples:${RESET}
3714
3559
  }
3715
3560
 
3716
3561
  if (!appId) {
3717
- error(" ID를 찾을 없습니다. gencow deploy 먼저 실행하세요.");
3562
+ error("App not found. Run gencow deploy first.");
3718
3563
  return;
3719
3564
  }
3720
3565
 
@@ -3736,7 +3581,7 @@ ${BOLD}Examples:${RESET}
3736
3581
  case "ls": {
3737
3582
  const res = await rpcQuery(creds, "backup.list", { appName: appId });
3738
3583
  if (!res.ok) {
3739
- error(`백업 목록 조회 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3584
+ error(`Failed to fetch backup list: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3740
3585
  return;
3741
3586
  }
3742
3587
  const data = await res.json();
@@ -3746,8 +3591,8 @@ ${BOLD}Examples:${RESET}
3746
3591
  log(`\n${BOLD}${CYAN}Database Backups${RESET} — ${appId} ${DIM}(Plan: ${data.plan || "free"})${RESET}\n`);
3747
3592
 
3748
3593
  if (backups.length === 0) {
3749
- info("백업이 없습니다.");
3750
- info(`${GREEN}gencow backup create${RESET} — 수동 백업 생성`);
3594
+ info("No backups found.");
3595
+ info(`${GREEN}gencow backup create${RESET} — create a manual backup`);
3751
3596
  } else {
3752
3597
  for (const b of backups) {
3753
3598
  const statusColor = b.status === "completed" ? GREEN : b.status === "failed" ? RED : YELLOW;
@@ -3765,19 +3610,19 @@ ${BOLD}Examples:${RESET}
3765
3610
 
3766
3611
  const res = await rpcMutation(creds, "backup.create", { appName: appId, note });
3767
3612
  if (!res.ok) {
3768
- error(`백업 생성 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3613
+ error(`Backup creation failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3769
3614
  return;
3770
3615
  }
3771
3616
  const backup = await res.json();
3772
- success(`백업 생성 완료 #${backup.id} ${fmtSize(backup.fileSize || 0)}`);
3617
+ success(`Backup created #${backup.id} ${fmtSize(backup.fileSize || 0)}`);
3773
3618
  break;
3774
3619
  }
3775
3620
 
3776
3621
  case "restore": {
3777
3622
  const backupId = parseInt(restArgs[0]);
3778
3623
  if (!backupId || isNaN(backupId)) {
3779
- error("백업 ID 지정하세요: gencow backup restore <id>");
3780
- info("gencow backup list ID를 확인하세요.");
3624
+ error("Specify backup ID: gencow backup restore <id>");
3625
+ info("Run gencow backup list to check IDs.");
3781
3626
  return;
3782
3627
  }
3783
3628
 
@@ -3792,7 +3637,7 @@ ${BOLD}Examples:${RESET}
3792
3637
  });
3793
3638
 
3794
3639
  if (answer !== "y" && answer !== "yes") {
3795
- info("복원이 취소되었습니다.");
3640
+ info("Restore cancelled.");
3796
3641
  return;
3797
3642
  }
3798
3643
 
@@ -3800,10 +3645,10 @@ ${BOLD}Examples:${RESET}
3800
3645
 
3801
3646
  const res = await rpcMutation(creds, "backup.restore", { appName: appId, backupId });
3802
3647
  if (!res.ok) {
3803
- error(`복원 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3648
+ error(`Restore failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3804
3649
  return;
3805
3650
  }
3806
- success("데이터베이스 복원 완료. 앱이 재시작되었습니다.");
3651
+ success("Database restored. App restarted.");
3807
3652
  break;
3808
3653
  }
3809
3654
 
@@ -3811,23 +3656,23 @@ ${BOLD}Examples:${RESET}
3811
3656
  case "rm": {
3812
3657
  const backupId = parseInt(restArgs[0]);
3813
3658
  if (!backupId || isNaN(backupId)) {
3814
- error("백업 ID 지정하세요: gencow backup delete <id>");
3659
+ error("Specify backup ID: gencow backup delete <id>");
3815
3660
  return;
3816
3661
  }
3817
3662
 
3818
3663
  const res = await rpcMutation(creds, "backup.delete", { appName: appId, backupId });
3819
3664
  if (!res.ok) {
3820
- error(`삭제 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3665
+ error(`Delete failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
3821
3666
  return;
3822
3667
  }
3823
- success(`백업 #${backupId} 삭제 완료`);
3668
+ success(`Backup #${backupId} deleted`);
3824
3669
  break;
3825
3670
  }
3826
3671
 
3827
3672
  case "download": {
3828
3673
  const backupId = parseInt(restArgs[0]);
3829
3674
  if (!backupId || isNaN(backupId)) {
3830
- error("백업 ID 지정하세요: gencow backup download <id>");
3675
+ error("Specify backup ID: gencow backup download <id>");
3831
3676
  return;
3832
3677
  }
3833
3678
 
@@ -3837,7 +3682,7 @@ ${BOLD}Examples:${RESET}
3837
3682
  `/platform/apps/${appId}/backups/${backupId}/download`
3838
3683
  );
3839
3684
  if (!res.ok) {
3840
- error(`다운로드 실패: ${(await res.json?.().catch(() => ({}))).error || res.statusText}`);
3685
+ error(`Download failed: ${(await res.json?.().catch(() => ({}))).error || res.statusText}`);
3841
3686
  return;
3842
3687
  }
3843
3688
 
@@ -3845,12 +3690,12 @@ ${BOLD}Examples:${RESET}
3845
3690
  const { writeFile } = await import("fs/promises");
3846
3691
  const buffer = Buffer.from(await res.arrayBuffer());
3847
3692
  await writeFile(filename, buffer);
3848
- success(`저장 완료: ${filename} (${fmtSize(buffer.length)})`);
3693
+ success(`Saved: ${filename} (${fmtSize(buffer.length)})`);
3849
3694
  break;
3850
3695
  }
3851
3696
 
3852
3697
  default:
3853
- error(`알 없는 하위 명령: ${subCmd}`);
3698
+ error(`Unknown sub-command: ${subCmd}`);
3854
3699
  log(`\n ${BOLD}Usage:${RESET}`);
3855
3700
  log(` gencow backup list List all backups`);
3856
3701
  log(` gencow backup create [note] Create a manual backup`);
@@ -3881,7 +3726,7 @@ ${BOLD}Examples:${RESET}
3881
3726
  }
3882
3727
 
3883
3728
  if (!appId) {
3884
- error(" ID를 찾을 없습니다. gencow deploy 먼저 실행하세요.");
3729
+ error("App not found. Run gencow deploy first.");
3885
3730
  return;
3886
3731
  }
3887
3732
 
@@ -3889,8 +3734,8 @@ ${BOLD}Examples:${RESET}
3889
3734
  case "set": {
3890
3735
  const domain = restArgs.find(a => !a.startsWith("-") && a.includes("."));
3891
3736
  if (!domain) {
3892
- error("사용법: gencow domain set <domain>");
3893
- info(" 예: gencow domain set myapp.com");
3737
+ error("Usage: gencow domain set <domain>");
3738
+ info(" Example: gencow domain set myapp.com");
3894
3739
  return;
3895
3740
  }
3896
3741
 
@@ -3902,32 +3747,32 @@ ${BOLD}Examples:${RESET}
3902
3747
 
3903
3748
  const data = await res.json();
3904
3749
  if (!res.ok) {
3905
- error(`도메인 등록 실패: ${data.error}`);
3750
+ error(`Domain registration failed: ${data.error}`);
3906
3751
  return;
3907
3752
  }
3908
3753
 
3909
- log(`\n${BOLD}${CYAN}🌐 커스텀 도메인 등록${RESET}\n`);
3754
+ log(`\n${BOLD}${CYAN}🌐 Custom Domain Registered${RESET}\n`);
3910
3755
  success(`${domain} → ${appId}`);
3911
3756
  log("");
3912
3757
 
3913
3758
  if (data.status === "active") {
3914
3759
  info(`DNS: ${GREEN}✅ ${data.dns.detail}${RESET}`);
3915
- info(`TLS: 인증서 자동 발급 대기 (첫 접속 시)`);
3760
+ info(`TLS: Certificate auto-provisioned on first request`);
3916
3761
  } else {
3917
- warn(`DNS: ⏳ 아직 확인되지 않았습니다.`);
3762
+ warn(`DNS: ⏳ Not yet verified.`);
3918
3763
  log("");
3919
- log(` 아래 DNS 레코드를 설정하세요:`);
3764
+ log(` Set up the following DNS record:`);
3920
3765
  log(` ┌──────────────────────────────────────────┐`);
3921
3766
  log(` │ ${BOLD}Type: A${RESET} │`);
3922
3767
  log(` │ Name: ${domain.padEnd(33)}│`);
3923
3768
  log(` │ Value: ${data.serverIp.padEnd(33)}│`);
3924
3769
  log(` │ │`);
3925
- log(` │ ${DIM}또는 CNAME:${RESET} │`);
3770
+ log(` │ ${DIM}Or CNAME:${RESET} │`);
3926
3771
  log(` │ Value: ${data.cname.padEnd(33)}│`);
3927
3772
  log(` └──────────────────────────────────────────┘`);
3928
3773
  }
3929
3774
  log("");
3930
- info(`DNS 설정 후 확인: ${DIM}gencow domain status${RESET}`);
3775
+ info(`After DNS setup: ${DIM}gencow domain status${RESET}`);
3931
3776
  log("");
3932
3777
  break;
3933
3778
  }
@@ -3942,14 +3787,14 @@ ${BOLD}Examples:${RESET}
3942
3787
 
3943
3788
  const data = await res.json();
3944
3789
  if (!res.ok) {
3945
- error(`도메인 해제 실패: ${data.error}`);
3790
+ error(`Domain removal failed: ${data.error}`);
3946
3791
  return;
3947
3792
  }
3948
3793
 
3949
3794
  if (data.domain) {
3950
- success(`도메인 ${data.domain} 해제 완료`);
3795
+ success(`Domain ${data.domain} disconnected`);
3951
3796
  } else {
3952
- info("설정된 커스텀 도메인이 없습니다.");
3797
+ info("No custom domain configured.");
3953
3798
  }
3954
3799
  break;
3955
3800
  }
@@ -3962,26 +3807,26 @@ ${BOLD}Examples:${RESET}
3962
3807
 
3963
3808
  const data = await res.json();
3964
3809
  if (!res.ok) {
3965
- error(`도메인 상태 조회 실패: ${data.error}`);
3810
+ error(`Domain status check failed: ${data.error}`);
3966
3811
  return;
3967
3812
  }
3968
3813
 
3969
- log(`\n${BOLD}${CYAN}🌐 커스텀 도메인 상태${RESET} — ${appId}\n`);
3814
+ log(`\n${BOLD}${CYAN}🌐 Custom Domain Status${RESET} — ${appId}\n`);
3970
3815
 
3971
3816
  if (!data.domain) {
3972
- info("커스텀 도메인이 설정되지 않았습니다.");
3973
- info(`설정: ${DIM}gencow domain set <domain>${RESET}`);
3817
+ info("No custom domain configured.");
3818
+ info(`Set up: ${DIM}gencow domain set <domain>${RESET}`);
3974
3819
  } else {
3975
- info(`도메인: ${BOLD}${data.domain}${RESET}`);
3820
+ info(`Domain: ${BOLD}${data.domain}${RESET}`);
3976
3821
 
3977
3822
  if (data.status === "active") {
3978
3823
  info(`DNS: ${GREEN}✅ ${data.dns.detail}${RESET}`);
3979
- info(`TLS: ${GREEN}✅ Let's Encrypt 자동 발급${RESET}`);
3824
+ info(`TLS: ${GREEN}✅ Let's Encrypt auto-provisioned${RESET}`);
3980
3825
  } else if (data.status === "pending") {
3981
- warn(`DNS: ⏳ 확인 대기 중`);
3982
- info(`TLS: DNS 확인 자동 발급`);
3826
+ warn(`DNS: ⏳ Verification pending`);
3827
+ info(`TLS: Auto-provisioned after DNS verification`);
3983
3828
  } else {
3984
- error(`상태: ${data.status}`);
3829
+ error(`Status: ${data.status}`);
3985
3830
  }
3986
3831
  }
3987
3832
  log("");
@@ -3990,28 +3835,9 @@ ${BOLD}Examples:${RESET}
3990
3835
  }
3991
3836
  },
3992
3837
 
3993
- async mcp(...mcpArgs) {
3994
- let port = "5456";
3995
- for (let i = 0; i < mcpArgs.length; i++) {
3996
- if (mcpArgs[i] === "--port" || mcpArgs[i] === "-p") port = mcpArgs[++i];
3997
- }
3998
-
3999
- const mcpScript = resolve(fileURLToPath(import.meta.url), "..", "gencow-mcp.mjs");
4000
- if (!existsSync(mcpScript)) {
4001
- error("gencow-mcp.mjs not found. Ensure the CLI package is properly installed.");
4002
- process.exit(1);
4003
- }
4004
-
4005
- // MCP stdio 서버를 직접 실행 (stdin/stdout 상속)
4006
- const { execFileSync } = await import("child_process");
4007
- try {
4008
- execFileSync("node", [mcpScript], {
4009
- stdio: "inherit",
4010
- env: { ...process.env, GENCOW_PORT: port },
4011
- });
4012
- } catch {
4013
- // MCP 서버가 종료될 때 정상적으로 종료
4014
- }
3838
+ async mcp() {
3839
+ info(`MCP server is ${BOLD}coming soon${RESET}.`);
3840
+ info(`${DIM}MCP integration requires local dev server (coming soon).${RESET}\n`);
4015
3841
  },
4016
3842
 
4017
3843
 
@@ -4046,8 +3872,16 @@ ${BOLD}Examples:${RESET}
4046
3872
  let runtimeRoot;
4047
3873
  try { runtimeRoot = findServerRoot(); } catch { runtimeRoot = process.cwd(); }
4048
3874
  const extractTsPath = resolve(runtimeRoot, ".gencow-extract.ts");
3875
+
3876
+ // @gencow/core — 번들된 core 절대 경로 우선, 없으면 bare specifier
3877
+ // generateApiTs()와 동일 해석 전략: 글로벌 CLI에서도 resolve 보장
3878
+ const bundledCorePath = resolve(runtimeRoot, "../core/index.js");
3879
+ const coreImport = existsSync(bundledCorePath)
3880
+ ? bundledCorePath.replace(/\\/g, "/")
3881
+ : "@gencow/core";
3882
+
4049
3883
  const script = `
4050
- import { getRegisteredQueries, getRegisteredMutations } from "@gencow/core";
3884
+ import { getRegisteredQueries, getRegisteredMutations } from "${coreImport}";
4051
3885
  import "${absoluteFunctions}/index.ts";
4052
3886
  console.log("GENCOW_API_JSON=" + JSON.stringify({
4053
3887
  queries: getRegisteredQueries(),
@@ -4055,14 +3889,23 @@ console.log("GENCOW_API_JSON=" + JSON.stringify({
4055
3889
  }));
4056
3890
  process.exit(0);
4057
3891
  `;
4058
- info("함수 목록 추출 중...");
3892
+ info("Extracting function list...");
3893
+ // NODE_PATH: 사용자 프로젝트 node_modules + CLI node_modules 포함
3894
+ // generateApiTs()와 동일 — 글로벌 설치 시 사용자 의존성 해석 보장
3895
+ const cwdNodeModules = resolve(process.cwd(), "node_modules");
3896
+ const cliNodeModules = resolve(__dirname, "..", "node_modules");
3897
+ const extractEnv = {
3898
+ ...process.env,
3899
+ NODE_PATH: [cwdNodeModules, cliNodeModules, process.env.NODE_PATH || ""].filter(Boolean).join(":"),
3900
+ };
4059
3901
  try {
4060
3902
  writeFileSync(extractTsPath, script);
4061
3903
  let outStr;
4062
3904
  try {
4063
3905
  outStr = execSync(`bun ${extractTsPath}`, {
4064
3906
  cwd: runtimeRoot,
4065
- stdio: ["pipe", "pipe", "pipe"]
3907
+ stdio: ["pipe", "pipe", "pipe"],
3908
+ env: extractEnv,
4066
3909
  }).toString();
4067
3910
  } catch (execErr) {
4068
3911
  const stderr = execErr.stderr?.toString() || "";
@@ -4119,10 +3962,10 @@ process.exit(0);
4119
3962
  log(` ${DIM}Queries: ${queries.join(", ")}${RESET}`);
4120
3963
  log(` ${DIM}Mutations: ${mutations.join(", ")}${RESET}`);
4121
3964
  log("");
4122
- info(`사용법: import { api } from "@/gencow/api";`);
3965
+ info(`Usage: import { api } from "@/gencow/api";`);
4123
3966
  } catch (e) {
4124
3967
  try { unlinkSync(extractTsPath); } catch { }
4125
- error(`Codegen 실패: ${e.message}`);
3968
+ error(`Codegen failed: ${e.message}`);
4126
3969
  process.exit(1);
4127
3970
  }
4128
3971
  },
@@ -4278,57 +4121,7 @@ process.exit(0);
4278
4121
  log(` Usage: gencow app [list|create|delete|status]`);
4279
4122
  },
4280
4123
 
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
- },
4124
+ // ── remote:deploy — REMOVED (legacy, use `gencow deploy` instead) ──
4332
4125
 
4333
4126
  // ── logs ───────────────────────────────────────────
4334
4127
  async logs(...args) {
@@ -4427,13 +4220,13 @@ process.exit(0);
4427
4220
  }
4428
4221
  followFailures++;
4429
4222
  if (followFailures > FOLLOW_MAX_FAILURES) {
4430
- error(`연결 불가 상태를 확인하세요.`);
4431
- info(`서버가 실행 중인지 확인: gencow dev`);
4223
+ error(`Connection failedcheck app status.`);
4224
+ info(`Check if the server is running: gencow dev`);
4432
4225
  process.exit(1);
4433
4226
  }
4434
4227
  const delay = Math.min(FOLLOW_BASE_RECONNECT_MS * Math.pow(2, followFailures - 1), FOLLOW_MAX_RECONNECT_MS);
4435
4228
  const delaySec = (delay / 1000).toFixed(0);
4436
- warn(`연결 끊김 — ${delaySec} 후 재연결... (${followFailures}/${FOLLOW_MAX_FAILURES})`);
4229
+ warn(`Disconnectedreconnecting in ${delaySec}s... (${followFailures}/${FOLLOW_MAX_FAILURES})`);
4437
4230
  scheduleFollowReconnect(delay);
4438
4231
  });
4439
4232
  }
@@ -4675,7 +4468,7 @@ process.exit(0);
4675
4468
 
4676
4469
  // 앱이 없으면 자동 생성 (deploy의 auto-create 패턴 재사용)
4677
4470
  if (!appName) {
4678
- info("앱이 없습니다. 자동 생성 중...");
4471
+ info("No app found. Creating...");
4679
4472
 
4680
4473
  // displayName: gencow.config.ts의 name 또는 프로젝트 폴더명
4681
4474
  let displayName = null;
@@ -4690,7 +4483,7 @@ process.exit(0);
4690
4483
  const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
4691
4484
  if (!createRes.ok) {
4692
4485
  const createErr = await createRes.json().catch(() => ({}));
4693
- error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
4486
+ error(`App creation failed: ${createErr.error || createRes.statusText}`);
4694
4487
  process.exit(1);
4695
4488
  }
4696
4489
 
@@ -4698,11 +4491,11 @@ process.exit(0);
4698
4491
  appName = createData.appId || createData.name;
4699
4492
 
4700
4493
  if (!appName) {
4701
- error(" 생성 응답에 appId가 없습니다.");
4494
+ error("App creation response missing appId.");
4702
4495
  process.exit(1);
4703
4496
  }
4704
4497
 
4705
- success(`앱 "${appName}" 생성 완료`);
4498
+ success(`App "${appName}" created`);
4706
4499
 
4707
4500
  // gencow.json 저장 (deploy와 동일 패턴)
4708
4501
  const gencowJsonPath = resolve(process.cwd(), "gencow.json");
@@ -4711,13 +4504,13 @@ process.exit(0);
4711
4504
  displayName,
4712
4505
  platformUrl: creds.platformUrl,
4713
4506
  }, null, 2));
4714
- info(`${DIM}gencow.json 생성됨 (appId: ${appName})${RESET}`);
4507
+ info(`${DIM}gencow.json created (appId: ${appName})${RESET}`);
4715
4508
 
4716
4509
  // .env에 VITE_API_URL 자동 설정
4717
4510
  updateEnvLocalUrl(getAppUrl(appName, creds.platformUrl));
4718
4511
 
4719
4512
  // 프로비저닝 대기
4720
- info("프로비저닝 대기 중...");
4513
+ info("Waiting for provisioning...");
4721
4514
  await new Promise(r => setTimeout(r, 3000));
4722
4515
  }
4723
4516
 
@@ -4736,9 +4529,9 @@ process.exit(0);
4736
4529
  const isSchema = reason !== "initial" && /schema.*\.ts$/i.test(reason);
4737
4530
 
4738
4531
  if (reason === "initial") {
4739
- log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} 초기 배포 중...`);
4532
+ log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} Initial deploy...`);
4740
4533
  } else {
4741
- log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} ${reason} 변경 감지배포 중...`);
4534
+ log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} ${reason} change detecteddeploying...`);
4742
4535
  }
4743
4536
 
4744
4537
  // 스키마 변경 또는 초기 배포 시 마이그레이션 생성
@@ -4746,7 +4539,7 @@ process.exit(0);
4746
4539
  const schemaExists = existsSync(resolve(process.cwd(), functionsDir, "schema.ts"));
4747
4540
  if (schemaExists && (isSchema || reason === "initial")) {
4748
4541
  if (isSchema) {
4749
- log(`${DIM}${ts}${RESET} ${YELLOW}[migrate]${RESET} 스키마 변경 감지마이그레이션 생성 중...`);
4542
+ log(`${DIM}${ts}${RESET} ${YELLOW}[migrate]${RESET} Schema change detectedgenerating migrations...`);
4750
4543
  }
4751
4544
  try {
4752
4545
  const { execSync } = await import("child_process");
@@ -4756,13 +4549,13 @@ process.exit(0);
4756
4549
  env: { ...process.env, ...dk.env },
4757
4550
  stdio: "inherit",
4758
4551
  });
4759
- log(`${DIM}${ts}${RESET} ${GREEN}[migrate]${RESET} ✔ 마이그레이션 생성 완료`);
4552
+ log(`${DIM}${ts}${RESET} ${GREEN}[migrate]${RESET} ✔ Migrations generated`);
4760
4553
  } catch (e) {
4761
4554
  const msg = e.stderr?.toString() || e.message || "";
4762
4555
  if (msg.includes("No schema changes") || msg.includes("nothing to migrate")) {
4763
4556
  // 스키마 변경 없음 — 정상
4764
4557
  } else {
4765
- warn(`[migrate] 마이그레이션 생성 실패 (무시 가능): ${msg.split("\n")[0]}`);
4558
+ warn(`[migrate] Migration generation failed (non-critical): ${msg.split("\n")[0]}`);
4766
4559
  }
4767
4560
  }
4768
4561
  }
@@ -4801,7 +4594,7 @@ process.exit(0);
4801
4594
 
4802
4595
  if (!res.ok) {
4803
4596
  const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
4804
- log(`${DIM}${ts2}${RESET} ${RED}[deploy]${RESET} ✗ 배포 실패!`);
4597
+ log(`${DIM}${ts2}${RESET} ${RED}[deploy]${RESET} ✗ Deploy failed!`);
4805
4598
 
4806
4599
  // 에러 메시지에서 파일:라인 정보 추출
4807
4600
  const errMsg = data.error || "Unknown error";
@@ -4818,13 +4611,13 @@ process.exit(0);
4818
4611
 
4819
4612
  // 스키마 관련 에러 힌트
4820
4613
  if (errMsg.includes("does not exist") || errMsg.includes("relation")) {
4821
- log(` ${YELLOW}💡 스키마가 변경되었나요? schema.ts 수정하면 자동으로 마이그레이션이 실행됩니다.${RESET}`);
4614
+ log(` ${YELLOW}💡 Schema changed? Migrations run automatically when schema.ts changes.${RESET}`);
4822
4615
  }
4823
4616
  return;
4824
4617
  }
4825
4618
 
4826
4619
  const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
4827
- log(`${DIM}${ts2}${RESET} ${GREEN}[deploy]${RESET} ✔ 배포 완료 ${DIM}(${sizeKB}KB, ${elapsed}s)${RESET}`);
4620
+ log(`${DIM}${ts2}${RESET} ${GREEN}[deploy]${RESET} ✔ Deploy complete ${DIM}(${sizeKB}KB, ${elapsed}s)${RESET}`);
4828
4621
  }
4829
4622
 
4830
4623
  // ── 로그 포맷팅 함수 ────────────────────────────────
@@ -4857,7 +4650,7 @@ process.exit(0);
4857
4650
  try {
4858
4651
  logWs = new WS(wsUrl);
4859
4652
  } catch (e) {
4860
- warn(`[log] WebSocket 연결 실패: ${e.message}`);
4653
+ warn(`[log] WebSocket connection failed: ${e.message}`);
4861
4654
  scheduleReconnect();
4862
4655
  return;
4863
4656
  }
@@ -4865,7 +4658,7 @@ process.exit(0);
4865
4658
  logWs.on("open", () => {
4866
4659
  // 카운터 리셋은 close 이벤트에서 안정 연결 판정 후 수행
4867
4660
  logWsOpenedAt = Date.now();
4868
- log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${GREEN}[log]${RESET} 로그 스트리밍 연결됨`);
4661
+ log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${GREEN}[log]${RESET} Log streaming connected`);
4869
4662
  logWs.send(JSON.stringify({ type: "log:subscribe" }));
4870
4663
  });
4871
4664
 
@@ -4889,13 +4682,13 @@ process.exit(0);
4889
4682
  }
4890
4683
  reconnectFailures++;
4891
4684
  if (reconnectFailures > MAX_RECONNECT_FAILURES) {
4892
- warn(`[log] 로그 스트리밍 ${MAX_RECONNECT_FAILURES} 연속 실패. 재연결 중단.`);
4893
- info(`수동 확인: gencow deploy logs`);
4685
+ warn(`[log] Log streaming failed ${MAX_RECONNECT_FAILURES} times. Giving up.`);
4686
+ info(`Check manually: gencow deploy logs`);
4894
4687
  return;
4895
4688
  }
4896
4689
  const delay = Math.min(BASE_RECONNECT_MS * Math.pow(2, reconnectFailures - 1), MAX_RECONNECT_MS);
4897
4690
  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})`);
4691
+ log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${YELLOW}[log]${RESET} Disconnectedreconnecting in ${delaySec}s... (${reconnectFailures}/${MAX_RECONNECT_FAILURES})`);
4899
4692
  scheduleReconnect(delay);
4900
4693
  });
4901
4694
  }
@@ -5003,7 +4796,8 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
5003
4796
  log(` ${GREEN}Prompts${RESET} Reusable prompt templates`);
5004
4797
  log(` ${GREEN}Parsers${RESET} PDF/HTML/CSV file parsing`);
5005
4798
  log(` ${GREEN}Memory${RESET} Agent memory ${DIM}(episodic/semantic/procedural)${RESET}`);
5006
- log(` ${GREEN}Analytics${RESET} LLM call tracking ${DIM}(coming soon)${RESET}`);
4799
+ log(`\n${BOLD}Coming soon:${RESET}`);
4800
+ log(` ${DIM}Analytics${RESET} LLM call tracking`);
5007
4801
  log(`\n${BOLD}Examples:${RESET}`);
5008
4802
  log(` ${DIM}gencow add AI${RESET}`);
5009
4803
  log(` ${DIM}gencow add AI RAG Reranker${RESET}`);
@@ -5050,10 +4844,11 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
5050
4844
  env: {},
5051
4845
  requires: ["ai"],
5052
4846
  guide: [
5053
- `gencow/rag.ts 에서 rag.ingest(), rag.search() 사용 가능`,
5054
- `mutation: await rag.ingest(ctx, source, text)`,
5055
- `query: const results = await rag.search(ctx, "질문")`,
5056
- `schema-rag.ts schema.ts import 해야 DB 테이블이 생성됩니다`,
4847
+ `gencow/rag.ts rag.ingest(), rag.search(), rag.ask()`,
4848
+ `Shared KB: await rag.ingest(ctx, "manual.pdf", text)`,
4849
+ `Personal: await rag.ingest(ctx, "notes.pdf", text, { userId: user.id })`,
4850
+ `Search: const results = await rag.search(ctx, "query", { userId: user.id })`,
4851
+ `Import schema-rag.ts in your schema.ts to create DB tables`,
5057
4852
  ],
5058
4853
  },
5059
4854
  memory: {
@@ -5178,7 +4973,7 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
5178
4973
  // ── 모든 컴포넌트 설치 후 README 1회 업데이트 ───────────
5179
4974
  await updateReadme(loadConfig());
5180
4975
 
5181
- log(`\n${GREEN}✔${RESET} ${BOLD}완료!${RESET} ${DIM}gencow dev 실행하면 바로 사용할 수 있습니다.${RESET}`);
4976
+ log(`\n${GREEN}✔${RESET} ${BOLD}Done!${RESET} ${DIM}Run gencow dev to get started.${RESET}`);
5182
4977
 
5183
4978
  // ── 컴포넌트별 가이드 메시지 ─────────────────────────────
5184
4979
  for (const key of ordered) {
@@ -5200,11 +4995,11 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
5200
4995
  const label = config.label || name.toUpperCase();
5201
4996
 
5202
4997
  if (config.notReady) {
5203
- log(` ${YELLOW}⚠${RESET} ${BOLD}[${label}]${RESET} ${DIM}아직 준비 중입니다 (Coming Soon)${RESET}`);
4998
+ log(` ${YELLOW}⚠${RESET} ${BOLD}[${label}]${RESET} ${DIM}Coming Soon${RESET}`);
5204
4999
  return;
5205
5000
  }
5206
5001
 
5207
- log(` ${CYAN}▸${RESET} ${BOLD}[${label}]${RESET} 설치 중...`);
5002
+ log(` ${CYAN}▸${RESET} ${BOLD}[${label}]${RESET} Installing...`);
5208
5003
 
5209
5004
  // 1. npm 의존성 설치 (패키지 매니저 자동 감지)
5210
5005
  if (config.deps && config.deps.length > 0) {
@@ -5221,9 +5016,9 @@ if (config.deps && config.deps.length > 0) {
5221
5016
  stdio: "pipe",
5222
5017
  cwd: process.cwd(),
5223
5018
  });
5224
- log(` ${GREEN}✓${RESET} ${config.deps.join(", ")} 패키지 설치`);
5019
+ log(` ${GREEN}✓${RESET} ${config.deps.join(", ")} packages installed`);
5225
5020
  } catch (e) {
5226
- log(` ${YELLOW}⚠${RESET} 패키지 설치 실패 — ${DIM}bun add ${config.deps.join(" ")} 를 직접 실행하세요${RESET}`);
5021
+ log(` ${YELLOW}⚠${RESET} Package install failed — ${DIM}run bun add ${config.deps.join(" ")} manually${RESET}`);
5227
5022
  }
5228
5023
  }
5229
5024
 
@@ -5247,10 +5042,10 @@ for (const file of config.files) {
5247
5042
  }
5248
5043
 
5249
5044
  if (fs.existsSync(destPath)) {
5250
- log(` ${YELLOW}⚠${RESET} ${file.dest} 이미 존재, 건너뛰`);
5045
+ log(` ${YELLOW}⚠${RESET} ${file.dest} already exists, skipping`);
5251
5046
  } else {
5252
5047
  fs.copyFileSync(srcPath, destPath);
5253
- log(` ${GREEN}✓${RESET} ${file.dest} 생성`);
5048
+ log(` ${GREEN}✓${RESET} ${file.dest} created`);
5254
5049
  }
5255
5050
  }
5256
5051
 
@@ -5267,7 +5062,7 @@ if (config.env && Object.keys(config.env).length > 0) {
5267
5062
  if (!envContent.includes(key)) {
5268
5063
  envContent += `\n${key}=${value}`;
5269
5064
  added = true;
5270
- log(` ${GREEN}✓${RESET} .env ${key}= 추가`);
5065
+ log(` ${GREEN}✓${RESET} .env added ${key}=`);
5271
5066
  }
5272
5067
  }
5273
5068
 
@@ -5316,15 +5111,15 @@ const safe = await ai.withRetry(
5316
5111
  { maxRetries: 3 }
5317
5112
  );
5318
5113
  \`\`\``,
5319
- vibePrompt: `AI 엔진 사용법:
5114
+ vibePrompt: `AI Engine API:
5320
5115
  import { ai } from "@/gencow/ai";
5321
- ai.chat({ messages }) // 텍스트 생성
5322
- ai.stream({ messages }) // 스트리밍
5323
- ai.embed("텍스트") // 임베딩
5324
- ai.embedMany(["a","b"]) // 배치 임베딩
5116
+ ai.chat({ messages }) // text generation
5117
+ ai.stream({ messages }) // streaming
5118
+ ai.embed("text") // embedding
5119
+ ai.embedMany(["a","b"]) // batch embedding
5325
5120
  ai.agent({ messages, tools, maxSteps }) // Agent Loop
5326
- ai.withRetry(fn, { maxRetries: 3 }) // 재시도+폴백
5327
- ai.estimateTokens(text) // 토큰 추정
5121
+ ai.withRetry(fn, { maxRetries: 3 }) // retry+fallback
5122
+ ai.estimateTokens(text) // token estimation
5328
5123
  ai.trimMessages(msgs, { maxTokens: 4000 }) // 토큰 예산`,
5329
5124
  },
5330
5125
  "rag.ts": {
@@ -5352,7 +5147,7 @@ const { answer, sources } = await rag.ask(ctx, "환불 정책이 뭔가요?");
5352
5147
  // 문서 삭제
5353
5148
  await rag.delete(ctx, "old-manual.pdf");
5354
5149
  \`\`\``,
5355
- vibePrompt: `RAG 엔진 사용법:
5150
+ vibePrompt: `RAG Engine API:
5356
5151
  import { rag } from "@/gencow/rag";
5357
5152
  rag.ingest(ctx, source, text) // 문서 인제스트 (스마트 청킹)
5358
5153
  rag.search(ctx, query, { filter }) // 시맨틱 검색
@@ -5380,7 +5175,7 @@ const tools = defineTools(ctx, {
5380
5175
 
5381
5176
  const reply = await ai.chat({ messages, tools });
5382
5177
  \`\`\``,
5383
- vibePrompt: `Tool Calling 사용법:
5178
+ vibePrompt: `Tool Calling API:
5384
5179
  import { defineTools } from "@/gencow/tools";
5385
5180
  const tools = defineTools(ctx, { ... }) // AI 도구 정의
5386
5181
  ai.chat({ messages, tools }) // 도구 연동 응답`,
@@ -5401,7 +5196,7 @@ const reply = await ai.chat({
5401
5196
  messages: [...memCtx.recentMessages, { role: "user", content: query }],
5402
5197
  });
5403
5198
  \`\`\``,
5404
- vibePrompt: `Agent Memory 사용법:
5199
+ vibePrompt: `Agent Memory API:
5405
5200
  import { memory } from "@/gencow/memory";
5406
5201
  memory.buildContext(ctx, userId, sessionId, query) // 메모리 컨텍스트
5407
5202
  memory.extract(ctx, userId, conversation) // 사실 추출`,
@@ -5435,7 +5230,7 @@ const reranked = await reranker.rerank(query, results, { topK: 5 });
5435
5230
  // 한 번에 (search + rerank)
5436
5231
  const best = await reranker.searchAndRerank(ctx, rag, query);
5437
5232
  \`\`\``,
5438
- vibePrompt: `Reranker 사용법:
5233
+ vibePrompt: `Reranker API:
5439
5234
  import { reranker } from "@/gencow/reranker";
5440
5235
  reranker.rerank(query, docs, { topK: 5 }) // LLM 재정렬
5441
5236
  reranker.searchAndRerank(ctx, rag, query) // 통합 파이프라인`,
@@ -5465,7 +5260,7 @@ const result = await guardrails.wrap(
5465
5260
  { maxLength: 2000 }
5466
5261
  );
5467
5262
  \`\`\``,
5468
- vibePrompt: `Guardrails 사용법:
5263
+ vibePrompt: `Guardrails API:
5469
5264
  import { guardrails } from "@/gencow/guardrails";
5470
5265
  guardrails.validateInput(text, { maskPII: true }) // PII 마스킹
5471
5266
  guardrails.validateInput(text, { blockTopics }) // 주제 차단
@@ -5492,7 +5287,7 @@ const myPrompt = definePrompt({
5492
5287
  });
5493
5288
  const prompt = myPrompt({ role: "전문가", task: "코드 리뷰" });
5494
5289
  \`\`\``,
5495
- vibePrompt: `Prompt Templates 사용법:
5290
+ vibePrompt: `Prompt Templates API:
5496
5291
  import { definePrompt, ragQAPrompt } from "@/gencow/prompts";
5497
5292
  definePrompt({ template, defaults }) // 재사용 프롬프트 정의
5498
5293
  ragQAPrompt({ question, context }) // RAG Q&A 프롬프트`,
@@ -5515,7 +5310,7 @@ await rag.ingest(ctx, "manual.pdf", text);
5515
5310
  // 자동 감지
5516
5311
  const content = await parsers.auto("report.html", htmlString);
5517
5312
  \`\`\``,
5518
- vibePrompt: `File Parsers 사용법:
5313
+ vibePrompt: `File Parsers API:
5519
5314
  import { parsers } from "@/gencow/parsers";
5520
5315
  parsers.pdf(buffer) // PDF → 텍스트
5521
5316
  parsers.html(htmlString) // HTML → 텍스트
@@ -5559,18 +5354,18 @@ async function updateReadme(config) {
5559
5354
  for (const comp of installed) {
5560
5355
  componentSection += `### ${comp.title}\n\n`;
5561
5356
  componentSection += `${comp.table}\n\n`;
5562
- componentSection += `#### 사용법\n\n`;
5357
+ componentSection += `#### Usage\n\n`;
5563
5358
  componentSection += `${comp.usage}\n\n`;
5564
5359
  }
5565
5360
 
5566
5361
  // 바이브코딩 프롬프트 섹션
5567
5362
  componentSection += `### 🤖 AI Vibe-Coding Prompt\n\n`;
5568
5363
  componentSection += "```\n";
5569
- componentSection += "다음은 현재 Gencow 백엔드에 설치된 AI 컴포넌트야:\n\n";
5364
+ componentSection += "The following AI components are installed in this Gencow backend:\n\n";
5570
5365
  for (const comp of installed) {
5571
5366
  componentSection += `${comp.vibePrompt}\n\n`;
5572
5367
  }
5573
- componentSection += " API를 활용해서 원하는 기능을 구현해줘.\n";
5368
+ componentSection += "Use the APIs above to build your desired features.\n";
5574
5369
  componentSection += "```\n\n";
5575
5370
  componentSection += `${END_MARKER}`;
5576
5371
 
@@ -5584,7 +5379,7 @@ async function updateReadme(config) {
5584
5379
  }
5585
5380
 
5586
5381
  fs.writeFileSync(readmePath, readme);
5587
- log(` ${GREEN}✓${RESET} README.md 컴포넌트 문서 업데이트`);
5382
+ log(` ${GREEN}✓${RESET} README.md component docs updated`);
5588
5383
  }
5589
5384
 
5590
5385
  const [, , cmd = "help", ...args] = process.argv;