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 +390 -33
- package/core/index.js +111 -18
- package/package.json +1 -1
- package/server/index.js +10007 -377
- package/server/index.js.map +4 -4
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
|
-
*
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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} ✔ 마이그레이션 생성 완료`);
|