gencow 0.1.117 → 0.1.119

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
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { execSync, spawn } from "child_process";
19
- import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, cpSync, readdirSync, rmSync, statSync, symlinkSync, copyFileSync } from "fs";
19
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, unlinkSync, cpSync, readdirSync, rmSync, statSync, symlinkSync, copyFileSync } from "fs";
20
20
  import { resolve, dirname, basename } from "path";
21
21
  import { homedir } from "os";
22
22
  import { fileURLToPath } from "url";
@@ -232,17 +232,108 @@ function isStandaloneProject() {
232
232
  }
233
233
 
234
234
  /**
235
- * drizzle-kit CLI 명령어 빌드 — 로컬 바이너리 우선 사용.
235
+ * drizzle-kit CLI 명령어 빌드 — 로컬 바이너리 우선 사용 + esbuild 정합성 체크.
236
236
  * pnpm 모노레포에서 npx가 esbuild 네이티브 바이너리를 잘못 resolve하여
237
237
  * "Host version X does not match binary version Y" 에러 방지.
238
- * 로컬 ./node_modules/.bin/drizzle-kit이 있으면 직접 실행, 없으면 npx fallback.
238
+ *
239
+ * 반환: { cmd: string, env: Record<string, string> }
240
+ * - cmd: 실행할 drizzle-kit 명령어
241
+ * - env: esbuild 불일치 시 ESBUILD_BINARY_PATH를 포함한 환경변수
239
242
  */
240
243
  function _drizzleKitCmd(subcmd) {
241
244
  const localBin = resolve(process.cwd(), "node_modules/.bin/drizzle-kit");
242
- if (existsSync(localBin)) {
243
- return `"${localBin}" ${subcmd}`;
244
- }
245
- return `npx drizzle-kit ${subcmd}`;
245
+ const cmd = existsSync(localBin) ? `"${localBin}" ${subcmd}` : `npx drizzle-kit ${subcmd}`;
246
+ const env = _ensureEsbuildConsistency();
247
+ return { cmd, env };
248
+ }
249
+
250
+ /**
251
+ * esbuild JS host와 네이티브 바이너리 버전 정합성 체크.
252
+ * 불일치 감지 시:
253
+ * 1) 즉시: ESBUILD_BINARY_PATH로 올바른 바이너리 지정 (이번 실행 성공)
254
+ * 2) 영구: .npmrc에 hoisting 차단 규칙 추가 (다음 pnpm install 시 완전 해결)
255
+ *
256
+ * 결과는 프로세스당 1회만 계산하여 캐싱 (watch 모드에서 반복 파일 I/O 방지).
257
+ *
258
+ * @returns {Record<string, string>} 환경변수 (정상이면 빈 객체)
259
+ */
260
+ let _esbuildEnvCache = null;
261
+ function _ensureEsbuildConsistency() {
262
+ if (_esbuildEnvCache !== null) return _esbuildEnvCache;
263
+ try {
264
+ const cwd = process.cwd();
265
+ const esbuildPkg = resolve(cwd, "node_modules/esbuild/package.json");
266
+ if (!existsSync(esbuildPkg)) return (_esbuildEnvCache = {});
267
+
268
+ const hostVersion = JSON.parse(readFileSync(esbuildPkg, "utf8")).version;
269
+ const platform = process.platform;
270
+ const arch = process.arch;
271
+ const binaryPkg = resolve(cwd, `node_modules/@esbuild/${platform}-${arch}/package.json`);
272
+ if (!existsSync(binaryPkg)) return (_esbuildEnvCache = {});
273
+
274
+ const binaryVersion = JSON.parse(readFileSync(binaryPkg, "utf8")).version;
275
+ if (hostVersion === binaryVersion) return (_esbuildEnvCache = {});
276
+
277
+ // ── 불일치 감지 → 2단계 수정 ──
278
+ warn(`esbuild 버전 불일치 감지: host=${hostVersion}, binary=${binaryVersion}`);
279
+
280
+ const env = {};
281
+
282
+ // 1) 즉시 우회: pnpm virtual store에서 올바른 바이너리 resolve
283
+ try {
284
+ const req = createRequire(resolve(cwd, "node_modules/drizzle-kit/package.json"));
285
+ const esbuildDir = dirname(req.resolve("esbuild/package.json"));
286
+ const esbuildVer = JSON.parse(readFileSync(resolve(esbuildDir, "package.json"), "utf8")).version;
287
+ const reqFromEsbuild = createRequire(resolve(esbuildDir, "package.json"));
288
+ try {
289
+ const platformPkgDir = dirname(reqFromEsbuild.resolve(`@esbuild/${platform}-${arch}/package.json`));
290
+ const binary = resolve(platformPkgDir, "bin/esbuild");
291
+ if (existsSync(binary)) {
292
+ env.ESBUILD_BINARY_PATH = binary;
293
+ info(`ESBUILD_BINARY_PATH → esbuild@${esbuildVer} 바이너리 사용`);
294
+ }
295
+ } catch { /* platform binary resolve 실패 — fallback 없이 계속 */ }
296
+ } catch { /* drizzle-kit esbuild resolve 실패 */ }
297
+
298
+ // 2) 영구 수정: .npmrc에 hoisting 차단 규칙 추가
299
+ _patchNpmrcForEsbuild(cwd);
300
+
301
+ return (_esbuildEnvCache = env);
302
+ } catch { return (_esbuildEnvCache = {}); }
303
+ }
304
+
305
+ /**
306
+ * 모노레포 루트의 .npmrc에 esbuild hoisting 차단 규칙을 추가.
307
+ * 이미 존재하면 스킵.
308
+ */
309
+ function _patchNpmrcForEsbuild(cwd) {
310
+ try {
311
+ // pnpm-workspace.yaml이 있는 디렉토리를 모노레포 루트로 판단
312
+ let dir = cwd;
313
+ for (let i = 0; i < 10; i++) {
314
+ if (existsSync(resolve(dir, "pnpm-workspace.yaml"))) {
315
+ const npmrcPath = resolve(dir, ".npmrc");
316
+ const content = existsSync(npmrcPath) ? readFileSync(npmrcPath, "utf8") : "";
317
+ if (!content.includes("hoist-pattern[]=!@esbuild/*")) {
318
+ const patch = [
319
+ "",
320
+ "# [gencow] esbuild 네이티브 바이너리 hoisting 충돌 방지 (자동 추가)",
321
+ "# 📄 docs/analysis/analysis-deploy-esbuild-version-mismatch.md",
322
+ "hoist-pattern[]=!@esbuild/*",
323
+ "hoist-pattern[]=!esbuild",
324
+ "",
325
+ ].join("\n");
326
+ appendFileSync(npmrcPath, patch);
327
+ info(".npmrc에 esbuild hoisting 차단 규칙을 추가했습니다.");
328
+ info("다음 pnpm install 시 영구 적용됩니다.");
329
+ }
330
+ return;
331
+ }
332
+ const parent = resolve(dir, "..");
333
+ if (parent === dir) break;
334
+ dir = parent;
335
+ }
336
+ } catch { /* .npmrc 수정 실패 — 무시 (즉시 우회로 이번 실행은 문제 없음) */ }
246
337
  }
247
338
 
248
339
  function findServerRoot() {
@@ -1310,9 +1401,10 @@ ${hasPrompt ? `
1310
1401
  });
1311
1402
  } else {
1312
1403
  // Standalone: use npx drizzle-kit directly
1313
- execSync(_drizzleKitCmd("generate"), {
1404
+ const dk = _drizzleKitCmd("generate");
1405
+ execSync(dk.cmd, {
1314
1406
  cwd: process.cwd(),
1315
- env: genEnv,
1407
+ env: { ...genEnv, ...dk.env },
1316
1408
  stdio: "inherit", // 프롬프트 패스스루 (rename 등)
1317
1409
  });
1318
1410
  }
@@ -1494,8 +1586,10 @@ ${hasPrompt ? `
1494
1586
  // 🆕 로컬 drizzle-kit generate 실행 (프롬프트 패스스루)
1495
1587
  info("스키마 마이그레이션 생성 중...");
1496
1588
  try {
1497
- execSync(_drizzleKitCmd("generate"), {
1589
+ const dk = _drizzleKitCmd("generate");
1590
+ execSync(dk.cmd, {
1498
1591
  cwd: process.cwd(),
1592
+ env: { ...process.env, ...dk.env },
1499
1593
  stdio: "inherit",
1500
1594
  });
1501
1595
  success("마이그레이션 생성 완료");
@@ -1562,9 +1656,10 @@ ${hasPrompt ? `
1562
1656
  runInServer("pnpm db:generate", buildEnv(config));
1563
1657
  } else {
1564
1658
  // Standalone: npx drizzle-kit generate (프롬프트 패스스루)
1565
- execSync(_drizzleKitCmd("generate"), {
1659
+ const dk = _drizzleKitCmd("generate");
1660
+ execSync(dk.cmd, {
1566
1661
  cwd: process.cwd(),
1567
- env: buildEnv(config),
1662
+ env: { ...buildEnv(config), ...dk.env },
1568
1663
  stdio: "inherit",
1569
1664
  });
1570
1665
  }
@@ -1713,15 +1808,20 @@ ${BOLD}BaaS commands (login required):${RESET}
1713
1808
  ${GREEN}deploy${RESET} Bundle gencow/ and deploy to platform
1714
1809
  ${DIM}--static [dir] Deploy static files (dist/, out/, build/)${RESET}
1715
1810
  ${DIM}--no-backend Skip backend auto-deploy in --static mode${RESET}
1811
+ ${DIM}--prod Deploy to production app (Pro+, auto-creates)${RESET}
1812
+ ${DIM}--rollback Rollback to previous deployment${RESET}
1716
1813
  ${DIM}--force, -f Skip dependency audit${RESET}
1717
- ${GREEN}env list${RESET} List cloud env vars
1814
+ ${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
1718
1815
  ${GREEN}env set K=V${RESET} Set cloud env var ${DIM}(hot-reload, no restart)${RESET}
1719
1816
  ${GREEN}env unset KEY${RESET} Remove cloud env var
1720
- ${GREEN}env push${RESET} Push .env to cloud
1817
+ ${GREEN}env push${RESET} Push .env to cloud ${DIM}(--prod reads .env.production)${RESET}
1721
1818
  ${GREEN}files upload${RESET} Upload files to app storage ${DIM}(--recursive for dirs)${RESET}
1722
1819
  ${GREEN}files list${RESET} List uploaded files
1723
1820
  ${GREEN}files delete${RESET} Delete uploaded file ${DIM}(--yes to skip confirm)${RESET}
1724
1821
  ${GREEN}files url${RESET} Get serving URL for a file
1822
+ ${GREEN}config set${RESET} Set app config ${DIM}(image.maxWidth, image.quality)${RESET}
1823
+ ${GREEN}config get${RESET} Show current app config
1824
+ ${GREEN}config reset${RESET} Reset app config to tier defaults
1725
1825
  ${GREEN}domain set${RESET} Connect custom domain ${DIM}(myapp.com)${RESET}
1726
1826
  ${GREEN}domain status${RESET} Check domain DNS/TLS status
1727
1827
  ${GREEN}domain remove${RESET} Disconnect custom domain
@@ -1897,6 +1997,7 @@ ${BOLD}Examples:${RESET}
1897
1997
  let isStatic = false;
1898
1998
  let forceDeploy = false; // --force: skip dependency audit
1899
1999
  let noBackend = false; // --no-backend: skip backend auto-deploy in --static mode
2000
+ let isRollback = false; // --rollback: rollback to previous deploy
1900
2001
 
1901
2002
  // ── subcommand 분기 (logs / status) ───────────────────
1902
2003
  const knownSubcmds = new Set(["logs", "status"]);
@@ -1963,6 +2064,7 @@ ${BOLD}Examples:${RESET}
1963
2064
  if (a === "--prod") envTarget = "prod";
1964
2065
  else if (a === "--force" || a === "-f") forceDeploy = true;
1965
2066
  else if (a === "--no-backend") noBackend = true;
2067
+ else if (a === "--rollback") isRollback = true;
1966
2068
  else if (a === "--app" || a === "-a") appId = deployArgs[++i];
1967
2069
  else if (a === "--static") {
1968
2070
  isStatic = true;
@@ -1980,6 +2082,7 @@ ${BOLD}Examples:${RESET}
1980
2082
  info(`사용법: gencow deploy [옵션]`);
1981
2083
  info(` gencow deploy 백엔드 배포`);
1982
2084
  info(` gencow deploy --static 정적 파일 배포`);
2085
+ info(` gencow deploy --rollback 이전 버전으로 롤백`);
1983
2086
  info(` gencow deploy --prod 프로덕션 배포`);
1984
2087
  info(` gencow deploy logs 서버 로그 조회`);
1985
2088
  info(` gencow deploy status 앱 상태 확인`);
@@ -1988,11 +2091,16 @@ ${BOLD}Examples:${RESET}
1988
2091
  }
1989
2092
  }
1990
2093
 
1991
- // gencow.json에서 appId 로드
2094
+ // gencow.json에서 appId + prodApp 로드
1992
2095
  const gencowJsonPath = resolve(process.cwd(), "gencow.json");
2096
+ let prodAppId = null;
1993
2097
  if (!appId && existsSync(gencowJsonPath)) {
1994
2098
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
1995
2099
  appId = gencowJson.appId || gencowJson.appName; // 하위 호환
2100
+ prodAppId = gencowJson.prodApp || null;
2101
+ } else if (existsSync(gencowJsonPath)) {
2102
+ const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
2103
+ prodAppId = gencowJson.prodApp || null;
1996
2104
  }
1997
2105
 
1998
2106
  // 프로젝트명은 display용으로만 사용 (appId가 아님)
@@ -2004,9 +2112,124 @@ ${BOLD}Examples:${RESET}
2004
2112
  }
2005
2113
  if (!displayName) displayName = basename(process.cwd());
2006
2114
 
2115
+ // ── Rollback 분기 ─────────────────────────────────────
2116
+ if (isRollback) {
2117
+ // prodApp이 있으면 prod 대상 롤백, 없으면 dev 대상 롤백
2118
+ const rollbackTarget = prodAppId || appId;
2119
+ if (!rollbackTarget) {
2120
+ error("앱 ID를 찾을 수 없습니다. gencow.json이 있는 프로젝트 루트에서 실행하세요.");
2121
+ process.exit(1);
2122
+ }
2123
+
2124
+ log(`\n${BOLD}${CYAN}Gencow Rollback${RESET}\n`);
2125
+ info(`앱: ${rollbackTarget}${prodAppId ? " (prod)" : ""}`);
2126
+ log("");
2127
+
2128
+ const rollbackStartTime = Date.now();
2129
+ const spinnerFrames = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
2130
+ let spinnerIdx = 0;
2131
+ const spinner = setInterval(() => {
2132
+ const elapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(0);
2133
+ process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} 롤백 중... ${DIM}(${elapsed}s)${RESET} `);
2134
+ }, 120);
2135
+
2136
+ const rollbackRes = await platformFetch(creds, `/platform/apps/${rollbackTarget}/rollback`, {
2137
+ method: "POST",
2138
+ headers: { "Content-Type": "application/json" },
2139
+ });
2140
+
2141
+ clearInterval(spinner);
2142
+ process.stdout.write("\r" + " ".repeat(50) + "\r");
2143
+
2144
+ if (!rollbackRes.ok) {
2145
+ const errData = await rollbackRes.json().catch(() => ({}));
2146
+ error(`롤백 실패: ${errData.error || rollbackRes.statusText}`);
2147
+ process.exit(1);
2148
+ }
2149
+
2150
+ const rollbackData = await rollbackRes.json();
2151
+ const rollbackElapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(1);
2152
+
2153
+ log("");
2154
+ success(`🔄 롤백 완료! (${rollbackElapsed}s)`);
2155
+ info(`롤백: #${rollbackData.rolledBackFrom} → #${rollbackData.rolledBackTo}`);
2156
+ info(`번들: ${rollbackData.bundleHash}`);
2157
+ info(`URL: ${rollbackData.url}`);
2158
+ log("");
2159
+ warn(`ℹ️ 코드만 롤백되었습니다. 데이터베이스는 변경되지 않았습니다.`);
2160
+ log("");
2161
+ return;
2162
+ }
2163
+
2007
2164
  // ── Static Deploy 분기 ────────────────────────────────
2008
2165
  if (isStatic) {
2009
- return await this._deployStatic(creds, appId, displayName, staticDir, gencowJsonPath, { forceDeploy, noBackend, envTarget });
2166
+ // prod 모드면 prodApp 대상으로 정적 배포
2167
+ const staticTarget = (envTarget === "prod" && prodAppId) ? prodAppId : appId;
2168
+ return await this._deployStatic(creds, staticTarget, displayName, staticDir, gencowJsonPath, { forceDeploy, noBackend, envTarget });
2169
+ }
2170
+
2171
+ // ── Prod 앱 자동 생성 + 배포 대상 전환 (Pro+) ──────────
2172
+ if (envTarget === "prod" && appId) {
2173
+ if (!prodAppId) {
2174
+ // prod 앱이 없음 — 첫 프로덕션 배포
2175
+ const ciMode = process.env.CI === "true" || deployArgs.includes("--yes");
2176
+ if (!ciMode) {
2177
+ const { createInterface } = await import("readline");
2178
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2179
+ log("");
2180
+ log(` ${BOLD}🚀 First production deployment!${RESET}`);
2181
+ log(` This will create a production app: ${CYAN}${appId}-prod${RESET}`);
2182
+ log("");
2183
+ const answer = await new Promise(resolve => {
2184
+ rl.question(` ${YELLOW}⚠${RESET} Proceed? (y/N) `, resolve);
2185
+ });
2186
+ rl.close();
2187
+ if (answer.toLowerCase() !== "y") {
2188
+ info("배포 취소됨.");
2189
+ return;
2190
+ }
2191
+ }
2192
+
2193
+ info("Prod 앱 생성 중...");
2194
+ const createProdRes = await platformFetch(creds, `/platform/apps/${appId}/create-prod`, {
2195
+ method: "POST",
2196
+ headers: { "Content-Type": "application/json" },
2197
+ });
2198
+
2199
+ if (!createProdRes.ok) {
2200
+ const errData = await createProdRes.json().catch(() => ({}));
2201
+ if (createProdRes.status === 403) {
2202
+ error(errData.error || "Production deploys require Pro plan or higher.");
2203
+ info("Run: gencow upgrade");
2204
+ } else {
2205
+ error(`Prod 앱 생성 실패: ${errData.error || createProdRes.statusText}`);
2206
+ }
2207
+ process.exit(1);
2208
+ }
2209
+
2210
+ const createProdData = await createProdRes.json();
2211
+ prodAppId = createProdData.prodApp;
2212
+
2213
+ // gencow.json 업데이트
2214
+ const gencowJson = existsSync(gencowJsonPath)
2215
+ ? JSON.parse(readFileSync(gencowJsonPath, "utf8"))
2216
+ : {};
2217
+ gencowJson.prodApp = prodAppId;
2218
+ writeFileSync(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
2219
+
2220
+ if (createProdData.alreadyExists) {
2221
+ info(`Prod 앱 확인: ${prodAppId}`);
2222
+ } else {
2223
+ success(`Prod 앱 생성 완료: ${prodAppId}`);
2224
+ info(`URL: ${createProdData.url}`);
2225
+ // 프로비저닝 대기
2226
+ await new Promise(r => setTimeout(r, 3000));
2227
+ }
2228
+ }
2229
+
2230
+ // 배포 대상을 prod 앱으로 전환
2231
+ appId = prodAppId;
2232
+ info(`배포 대상: ${CYAN}${appId}${RESET} (production)`);
2010
2233
  }
2011
2234
 
2012
2235
  log(`\n${BOLD}${CYAN}Gencow Deploy${RESET}\n`);
@@ -2019,20 +2242,6 @@ ${BOLD}Examples:${RESET}
2019
2242
  info(`포맷: tar.gz`);
2020
2243
  log("");
2021
2244
 
2022
- // 프로덕션 배포 확인
2023
- if (envTarget === "prod") {
2024
- const { createInterface } = await import("readline");
2025
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2026
- const answer = await new Promise(resolve => {
2027
- rl.question(` ${YELLOW}⚠${RESET} 프로덕션 배포를 진행하시겠습니까? (y/N) `, resolve);
2028
- });
2029
- rl.close();
2030
- if (answer.toLowerCase() !== "y") {
2031
- info("배포 취소됨.");
2032
- return;
2033
- }
2034
- }
2035
-
2036
2245
  // 0-1. drizzle-kit generate 자동 실행 (번들링 전 migrations/ 최신화)
2037
2246
  {
2038
2247
  const { execSync: execGen } = await import("child_process");
@@ -2046,8 +2255,10 @@ ${BOLD}Examples:${RESET}
2046
2255
  });
2047
2256
  } else {
2048
2257
  // Standalone: npx drizzle-kit generate (프롬프트 패스스루 — rename 감지 대화형 포함)
2049
- execGen(_drizzleKitCmd("generate"), {
2258
+ const dk = _drizzleKitCmd("generate");
2259
+ execGen(dk.cmd, {
2050
2260
  cwd: process.cwd(),
2261
+ env: { ...process.env, ...dk.env },
2051
2262
  stdio: "inherit",
2052
2263
  });
2053
2264
  }
@@ -2572,8 +2783,10 @@ ${BOLD}Examples:${RESET}
2572
2783
  if (existsSync(schemaPath)) {
2573
2784
  info("스키마 마이그레이션 생성 중...");
2574
2785
  try {
2575
- execSync(_drizzleKitCmd("generate"), {
2786
+ const dk = _drizzleKitCmd("generate");
2787
+ execSync(dk.cmd, {
2576
2788
  cwd: backendRoot,
2789
+ env: { ...process.env, ...dk.env },
2577
2790
  stdio: "inherit", // ← 프롬프트 패스스루!
2578
2791
  });
2579
2792
  success("마이그레이션 생성 완료");
@@ -2847,9 +3060,19 @@ ${BOLD}Examples:${RESET}
2847
3060
  if (existsSync(gencowJsonPath)) {
2848
3061
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
2849
3062
  appId = gencowJson.appId || gencowJson.appName; // 하위 호환
3063
+ // --prod 시 prod 앱으로 대상 전환
3064
+ if (envTarget === "prod" && gencowJson.prodApp) {
3065
+ appId = gencowJson.prodApp;
3066
+ }
2850
3067
  }
2851
3068
  }
2852
3069
 
3070
+ // --prod인데 prod 앱이 없는 경우
3071
+ if (envTarget === "prod" && appId && !appId.endsWith("-prod")) {
3072
+ error("Prod 앱이 아직 없습니다. gencow deploy --prod를 먼저 실행하세요.");
3073
+ return;
3074
+ }
3075
+
2853
3076
  if (!appId) {
2854
3077
  error("앱 ID를 찾을 수 없습니다. gencow deploy를 먼저 실행하세요.");
2855
3078
  return;
@@ -2984,6 +3207,138 @@ ${BOLD}Examples:${RESET}
2984
3207
  }
2985
3208
  },
2986
3209
 
3210
+ // ── config ────────────────────────────────────────────
3211
+ async config(...configArgs) {
3212
+ const creds = requireCreds();
3213
+ const subCmd = configArgs[0] || "help";
3214
+ const restArgs = configArgs.slice(1);
3215
+
3216
+ // 앱 ID 결정
3217
+ let appId = null;
3218
+ for (let i = 0; i < restArgs.length; i++) {
3219
+ if (restArgs[i] === "--app" || restArgs[i] === "-a") appId = restArgs[++i];
3220
+ }
3221
+
3222
+ if (!appId) {
3223
+ const gencowJsonPath = resolve(process.cwd(), "gencow.json");
3224
+ if (existsSync(gencowJsonPath)) {
3225
+ const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
3226
+ appId = gencowJson.appId || gencowJson.appName;
3227
+ }
3228
+ }
3229
+
3230
+ if (subCmd === "help" || subCmd === "--help" || subCmd === "-h") {
3231
+ log(`\n${BOLD}${CYAN}gencow config${RESET} — 앱 설정 관리\n`);
3232
+ log(` ${CYAN}set${RESET} image.maxWidth <값> Auto WebP 최대 폭 (px)`);
3233
+ log(` ${CYAN}set${RESET} image.quality <값> Auto WebP 품질 (1-100)`);
3234
+ log(` ${CYAN}get${RESET} image 현재 이미지 설정 조회`);
3235
+ log(` ${CYAN}reset${RESET} image 이미지 설정 초기화 (Tier 기본값)\n`);
3236
+ log(` ${DIM}옵션:${RESET}`);
3237
+ log(` ${DIM}--app, -a <앱이름> 대상 앱 지정 (기본: gencow.json)${RESET}\n`);
3238
+ return;
3239
+ }
3240
+
3241
+ if (!appId) {
3242
+ error("앱 ID를 찾을 수 없습니다. gencow deploy를 먼저 실행하세요.");
3243
+ return;
3244
+ }
3245
+
3246
+ switch (subCmd) {
3247
+ case "set": {
3248
+ const key = restArgs.find((a, i) => !a.startsWith("-") && !(restArgs[i - 1] === "--app" || restArgs[i - 1] === "-a"));
3249
+ const val = restArgs[restArgs.indexOf(key) + 1];
3250
+
3251
+ if (!key || val === undefined) {
3252
+ error("사용법: gencow config set image.maxWidth <값>");
3253
+ return;
3254
+ }
3255
+
3256
+ const imageConfig = {};
3257
+ if (key === "image.maxWidth" || key === "image.max-width") {
3258
+ const v = parseInt(val);
3259
+ if (isNaN(v) || v < 0 || v > 10000) {
3260
+ error("maxWidth는 0~10000 사이의 값이어야 합니다. (0 = 리셋)");
3261
+ return;
3262
+ }
3263
+ imageConfig.autoMaxWidth = v;
3264
+ } else if (key === "image.quality") {
3265
+ const v = parseInt(val);
3266
+ if (isNaN(v) || v < 0 || v > 100) {
3267
+ error("quality는 0~100 사이의 값이어야 합니다. (0 = 리셋)");
3268
+ return;
3269
+ }
3270
+ imageConfig.autoQuality = v;
3271
+ } else {
3272
+ error(`알 수 없는 설정 키: ${key}`);
3273
+ info("사용 가능: image.maxWidth, image.quality");
3274
+ return;
3275
+ }
3276
+
3277
+ const res = await rpcMutation(creds, "apps.updateImageConfig", { name: appId, imageConfig });
3278
+ if (res.ok) {
3279
+ const data = await res.json();
3280
+ success(`${key} = ${val} 설정 완료`);
3281
+ if (data.imageConfig) {
3282
+ info(`현재 설정: ${JSON.stringify(data.imageConfig)}`);
3283
+ }
3284
+ info(`${DIM}※ 다음 이미지 요청부터 적용됩니다. 기존 캐시는 별도 키로 저장됩니다.${RESET}`);
3285
+ } else {
3286
+ const errData = await res.json().catch(() => ({}));
3287
+ error(`설정 실패: ${errData.error || res.statusText}`);
3288
+ }
3289
+ break;
3290
+ }
3291
+
3292
+ case "get": {
3293
+ const key = restArgs.find(a => !a.startsWith("-"));
3294
+ if (key && key !== "image") {
3295
+ error(`알 수 없는 설정 키: ${key}`);
3296
+ info("사용 가능: image");
3297
+ return;
3298
+ }
3299
+
3300
+ const res = await rpcQuery(creds, "apps.getImageConfig", { name: appId });
3301
+ if (res.ok) {
3302
+ const config = await res.json();
3303
+ log(`\n${BOLD}${CYAN}이미지 설정${RESET} — ${appId}\n`);
3304
+ if (Object.keys(config).length === 0) {
3305
+ info("커스텀 설정 없음 (Tier 기본값 사용)");
3306
+ } else {
3307
+ if (config.autoMaxWidth) log(` ${GREEN}autoMaxWidth${RESET} ${config.autoMaxWidth} px`);
3308
+ if (config.autoQuality) log(` ${GREEN}autoQuality${RESET} ${config.autoQuality}`);
3309
+ }
3310
+ log("");
3311
+ } else {
3312
+ error("설정 조회 실패");
3313
+ }
3314
+ break;
3315
+ }
3316
+
3317
+ case "reset": {
3318
+ const key = restArgs.find(a => !a.startsWith("-"));
3319
+ if (key && key !== "image") {
3320
+ error(`알 수 없는 설정 키: ${key}`);
3321
+ return;
3322
+ }
3323
+
3324
+ // autoMaxWidth=0 + autoQuality=0 → 둘 다 리셋
3325
+ const res = await rpcMutation(creds, "apps.updateImageConfig", {
3326
+ name: appId,
3327
+ imageConfig: { autoMaxWidth: 0, autoQuality: 0 },
3328
+ });
3329
+ if (res.ok) {
3330
+ success("이미지 설정 초기화 완료 (Tier 기본값 사용)");
3331
+ } else {
3332
+ error("초기화 실패");
3333
+ }
3334
+ break;
3335
+ }
3336
+
3337
+ default:
3338
+ error(`알 수 없는 하위 명령: ${subCmd}`);
3339
+ info("사용법: gencow config [set|get|reset] ...");
3340
+ }
3341
+ },
2987
3342
 
2988
3343
  // ── files ─────────────────────────────────────────────
2989
3344
  async files(...filesArgs) {
@@ -4333,8 +4688,10 @@ process.exit(0);
4333
4688
  }
4334
4689
  try {
4335
4690
  const { execSync } = await import("child_process");
4336
- execSync(_drizzleKitCmd("generate"), {
4691
+ const dk = _drizzleKitCmd("generate");
4692
+ execSync(dk.cmd, {
4337
4693
  cwd: process.cwd(),
4694
+ env: { ...process.env, ...dk.env },
4338
4695
  stdio: "inherit",
4339
4696
  });
4340
4697
  log(`${DIM}${ts}${RESET} ${GREEN}[migrate]${RESET} ✔ 마이그레이션 생성 완료`);