gencow 0.1.118 → 0.1.120
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 +357 -39
- package/core/index.js +111 -18
- package/dashboard/assets/index-Dv-kFCfN.js +372 -0
- package/dashboard/assets/index-treFoOsZ.css +1 -0
- package/dashboard/index.html +2 -2
- package/lib/__tests__/readme-codegen.test.ts +2 -1
- package/lib/readme-codegen.mjs +24 -14
- package/package.json +1 -1
- package/server/index.js +508 -43
- package/server/index.js.map +3 -3
- package/dashboard/assets/index-BYHvURCl.css +0 -1
- package/dashboard/assets/index-C26b7MIs.js +0 -372
package/bin/gencow.mjs
CHANGED
|
@@ -303,7 +303,8 @@ function _ensureEsbuildConsistency() {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
/**
|
|
306
|
-
* 모노레포 루트의 .
|
|
306
|
+
* 모노레포 루트의 .pnpmrc에 esbuild hoisting 차단 규칙을 추가.
|
|
307
|
+
* (.npmrc가 아닌 .pnpmrc — hoist-pattern은 pnpm 전용이므로 npm 경고 방지)
|
|
307
308
|
* 이미 존재하면 스킵.
|
|
308
309
|
*/
|
|
309
310
|
function _patchNpmrcForEsbuild(cwd) {
|
|
@@ -312,8 +313,9 @@ function _patchNpmrcForEsbuild(cwd) {
|
|
|
312
313
|
let dir = cwd;
|
|
313
314
|
for (let i = 0; i < 10; i++) {
|
|
314
315
|
if (existsSync(resolve(dir, "pnpm-workspace.yaml"))) {
|
|
315
|
-
|
|
316
|
-
const
|
|
316
|
+
// .pnpmrc 대상 (pnpm 전용 설정 → npm "Unknown project config" 경고 방지)
|
|
317
|
+
const pnpmrcPath = resolve(dir, ".pnpmrc");
|
|
318
|
+
const content = existsSync(pnpmrcPath) ? readFileSync(pnpmrcPath, "utf8") : "";
|
|
317
319
|
if (!content.includes("hoist-pattern[]=!@esbuild/*")) {
|
|
318
320
|
const patch = [
|
|
319
321
|
"",
|
|
@@ -323,8 +325,8 @@ function _patchNpmrcForEsbuild(cwd) {
|
|
|
323
325
|
"hoist-pattern[]=!esbuild",
|
|
324
326
|
"",
|
|
325
327
|
].join("\n");
|
|
326
|
-
appendFileSync(
|
|
327
|
-
info(".
|
|
328
|
+
appendFileSync(pnpmrcPath, patch);
|
|
329
|
+
info(".pnpmrc에 esbuild hoisting 차단 규칙을 추가했습니다.");
|
|
328
330
|
info("다음 pnpm install 시 영구 적용됩니다.");
|
|
329
331
|
}
|
|
330
332
|
return;
|
|
@@ -333,7 +335,7 @@ function _patchNpmrcForEsbuild(cwd) {
|
|
|
333
335
|
if (parent === dir) break;
|
|
334
336
|
dir = parent;
|
|
335
337
|
}
|
|
336
|
-
} catch { /* .
|
|
338
|
+
} catch { /* .pnpmrc 수정 실패 — 무시 (즉시 우회로 이번 실행은 문제 없음) */ }
|
|
337
339
|
}
|
|
338
340
|
|
|
339
341
|
function findServerRoot() {
|
|
@@ -1197,7 +1199,7 @@ ${hasPrompt ? `
|
|
|
1197
1199
|
|
|
1198
1200
|
if (!appId) {
|
|
1199
1201
|
error("Cloud app not found — gencow.json이 없거나 appId가 설정되지 않았습니다.");
|
|
1200
|
-
info(`${DIM}먼저 gencow
|
|
1202
|
+
info(`${DIM}먼저 gencow dev 를 실행하세요.${RESET}`);
|
|
1201
1203
|
info(`${DIM}로컬 서버에 seed하려면: gencow db:seed --local${RESET}`);
|
|
1202
1204
|
log("");
|
|
1203
1205
|
return;
|
|
@@ -1218,7 +1220,7 @@ ${hasPrompt ? `
|
|
|
1218
1220
|
if (!statusRes || !statusRes.ok) {
|
|
1219
1221
|
error(`Cloud app "${appId}" is not running`);
|
|
1220
1222
|
info(`${DIM}Dashboard → Apps → ${appId} → Resume 으로 앱을 시작하세요.${RESET}`);
|
|
1221
|
-
info(`${DIM}또는: gencow
|
|
1223
|
+
info(`${DIM}또는: gencow dev 로 앱 생성 후 시도하세요.${RESET}`);
|
|
1222
1224
|
log("");
|
|
1223
1225
|
return;
|
|
1224
1226
|
}
|
|
@@ -1236,7 +1238,7 @@ ${hasPrompt ? `
|
|
|
1236
1238
|
log(` ${DIM}export default async function seed(ctx) {${RESET}`);
|
|
1237
1239
|
log(` ${DIM} await ctx.db.insert(tasks).values([...]);${RESET}`);
|
|
1238
1240
|
log(` ${DIM}};${RESET}\n`);
|
|
1239
|
-
info(`${DIM}After creating seed.ts: gencow
|
|
1241
|
+
info(`${DIM}After creating seed.ts: gencow dev 실행 후 gencow db:seed${RESET}`);
|
|
1240
1242
|
} else if (!res.ok) {
|
|
1241
1243
|
error(`Cloud seed failed: ${data.error || "Unknown error"}`);
|
|
1242
1244
|
} else {
|
|
@@ -1555,7 +1557,7 @@ ${hasPrompt ? `
|
|
|
1555
1557
|
|
|
1556
1558
|
if (!appId) {
|
|
1557
1559
|
error("Cloud app not found — gencow.json이 없거나 appId가 설정되지 않았습니다.");
|
|
1558
|
-
info(`${DIM}먼저 gencow
|
|
1560
|
+
info(`${DIM}먼저 gencow dev 를 실행하세요.${RESET}`);
|
|
1559
1561
|
info(`${DIM}로컬 DB에 push하려면: gencow db:push --local${RESET}`);
|
|
1560
1562
|
log("");
|
|
1561
1563
|
return;
|
|
@@ -1564,7 +1566,7 @@ ${hasPrompt ? `
|
|
|
1564
1566
|
// ── Cloud 모드: Platform API를 통해 Cloud DB에 schema push ──
|
|
1565
1567
|
const creds = requireCreds();
|
|
1566
1568
|
log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
|
|
1567
|
-
warn("명령어 실행 전 먼저 gencow
|
|
1569
|
+
warn("명령어 실행 전 먼저 gencow dev 를 통해 최신 코드가 배포되어 있어야 합니다.");
|
|
1568
1570
|
log("");
|
|
1569
1571
|
info("Pushing schema.ts → cloud database...");
|
|
1570
1572
|
|
|
@@ -1805,18 +1807,24 @@ ${BOLD}BaaS commands (login required):${RESET}
|
|
|
1805
1807
|
${GREEN}login${RESET} Login to Gencow Platform ${DIM}(browser → token)${RESET}
|
|
1806
1808
|
${GREEN}logout${RESET} Clear credentials
|
|
1807
1809
|
${GREEN}whoami${RESET} Show current user info
|
|
1808
|
-
${GREEN}
|
|
1809
|
-
${DIM}--
|
|
1810
|
-
${DIM}--no-backend Skip backend auto-deploy in --static mode${RESET}
|
|
1810
|
+
${GREEN}static [dir]${RESET} Deploy static files to dev ${DIM}(dist/, out/, build/)${RESET}
|
|
1811
|
+
${DIM}--no-backend Skip backend auto-deploy${RESET}
|
|
1811
1812
|
${DIM}--force, -f Skip dependency audit${RESET}
|
|
1812
|
-
${GREEN}
|
|
1813
|
+
${GREEN}deploy${RESET} Deploy to production ${DIM}(Pro+ only)${RESET}
|
|
1814
|
+
${DIM}--static [dir] Deploy static files to production${RESET}
|
|
1815
|
+
${DIM}--rollback Rollback to previous deployment${RESET}
|
|
1816
|
+
${DIM}--force, -f Skip dependency audit${RESET}
|
|
1817
|
+
${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
|
|
1813
1818
|
${GREEN}env set K=V${RESET} Set cloud env var ${DIM}(hot-reload, no restart)${RESET}
|
|
1814
1819
|
${GREEN}env unset KEY${RESET} Remove cloud env var
|
|
1815
|
-
${GREEN}env push${RESET} Push .env to cloud
|
|
1820
|
+
${GREEN}env push${RESET} Push .env to cloud ${DIM}(--prod reads .env.production)${RESET}
|
|
1816
1821
|
${GREEN}files upload${RESET} Upload files to app storage ${DIM}(--recursive for dirs)${RESET}
|
|
1817
1822
|
${GREEN}files list${RESET} List uploaded files
|
|
1818
1823
|
${GREEN}files delete${RESET} Delete uploaded file ${DIM}(--yes to skip confirm)${RESET}
|
|
1819
1824
|
${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
|
|
1820
1828
|
${GREEN}domain set${RESET} Connect custom domain ${DIM}(myapp.com)${RESET}
|
|
1821
1829
|
${GREEN}domain status${RESET} Check domain DNS/TLS status
|
|
1822
1830
|
${GREEN}domain remove${RESET} Disconnect custom domain
|
|
@@ -1980,18 +1988,60 @@ ${BOLD}Examples:${RESET}
|
|
|
1980
1988
|
}
|
|
1981
1989
|
},
|
|
1982
1990
|
|
|
1983
|
-
// ──
|
|
1991
|
+
// ── static — dev 정적 파일 배포 (독립 명령어) ─────────
|
|
1992
|
+
async static(...staticArgs) {
|
|
1993
|
+
const creds = requireCreds();
|
|
1994
|
+
|
|
1995
|
+
let staticDir = null;
|
|
1996
|
+
let forceDeploy = false;
|
|
1997
|
+
let noBackend = false;
|
|
1998
|
+
let appId = null;
|
|
1999
|
+
|
|
2000
|
+
for (let i = 0; i < staticArgs.length; i++) {
|
|
2001
|
+
const a = staticArgs[i];
|
|
2002
|
+
if (a === "--force" || a === "-f") forceDeploy = true;
|
|
2003
|
+
else if (a === "--no-backend") noBackend = true;
|
|
2004
|
+
else if (a === "--app" || a === "-a") appId = staticArgs[++i];
|
|
2005
|
+
else if (!a.startsWith("-")) {
|
|
2006
|
+
staticDir = a;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// gencow.json에서 appId 로드
|
|
2011
|
+
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
2012
|
+
if (!appId && existsSync(gencowJsonPath)) {
|
|
2013
|
+
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
2014
|
+
appId = gencowJson.appId || gencowJson.appName;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// displayName
|
|
2018
|
+
let displayName = null;
|
|
2019
|
+
const configPath = resolve(process.cwd(), "gencow.config.ts");
|
|
2020
|
+
if (existsSync(configPath)) {
|
|
2021
|
+
const content = readFileSync(configPath, "utf8");
|
|
2022
|
+
const match = content.match(/name:\s*["']([^"']+)["']/);
|
|
2023
|
+
if (match) displayName = match[1];
|
|
2024
|
+
}
|
|
2025
|
+
if (!displayName) displayName = basename(process.cwd());
|
|
2026
|
+
|
|
2027
|
+
return await this._deployStatic(creds, appId, displayName, staticDir, gencowJsonPath, {
|
|
2028
|
+
forceDeploy, noBackend, envTarget: "dev",
|
|
2029
|
+
});
|
|
2030
|
+
},
|
|
2031
|
+
|
|
2032
|
+
// ── deploy — 프로덕션 배포 (Pro+ only) ───────────────
|
|
1984
2033
|
async deploy(...deployArgs) {
|
|
1985
2034
|
const creds = requireCreds();
|
|
1986
2035
|
|
|
1987
2036
|
// gencow.json에서 앱 ID 확인 (자동 생성된 유니크 ID)
|
|
1988
2037
|
let appId = null;
|
|
1989
2038
|
let displayName = null;
|
|
1990
|
-
let envTarget = "
|
|
2039
|
+
let envTarget = "prod"; // v0.1.120: deploy = prod 기본값
|
|
1991
2040
|
let staticDir = null; // --static 옵션
|
|
1992
2041
|
let isStatic = false;
|
|
1993
2042
|
let forceDeploy = false; // --force: skip dependency audit
|
|
1994
2043
|
let noBackend = false; // --no-backend: skip backend auto-deploy in --static mode
|
|
2044
|
+
let isRollback = false; // --rollback: rollback to previous deploy
|
|
1995
2045
|
|
|
1996
2046
|
// ── subcommand 분기 (logs / status) ───────────────────
|
|
1997
2047
|
const knownSubcmds = new Set(["logs", "status"]);
|
|
@@ -2055,9 +2105,15 @@ ${BOLD}Examples:${RESET}
|
|
|
2055
2105
|
|
|
2056
2106
|
for (let i = 0; i < deployArgs.length; i++) {
|
|
2057
2107
|
const a = deployArgs[i];
|
|
2058
|
-
if (a === "--prod")
|
|
2108
|
+
if (a === "--prod") {
|
|
2109
|
+
// deprecated: deploy는 이미 prod 기본값
|
|
2110
|
+
warn(`${YELLOW}--prod 플래그는 더 이상 필요하지 않습니다.${RESET}`);
|
|
2111
|
+
info(`gencow deploy는 기본적으로 프로덕션에 배포합니다.`);
|
|
2112
|
+
envTarget = "prod";
|
|
2113
|
+
}
|
|
2059
2114
|
else if (a === "--force" || a === "-f") forceDeploy = true;
|
|
2060
2115
|
else if (a === "--no-backend") noBackend = true;
|
|
2116
|
+
else if (a === "--rollback") isRollback = true;
|
|
2061
2117
|
else if (a === "--app" || a === "-a") appId = deployArgs[++i];
|
|
2062
2118
|
else if (a === "--static") {
|
|
2063
2119
|
isStatic = true;
|
|
@@ -2073,21 +2129,30 @@ ${BOLD}Examples:${RESET}
|
|
|
2073
2129
|
error(`알 수 없는 deploy 인자: "${a}"`);
|
|
2074
2130
|
log("");
|
|
2075
2131
|
info(`사용법: gencow deploy [옵션]`);
|
|
2076
|
-
info(` gencow deploy
|
|
2077
|
-
info(` gencow deploy --static 정적
|
|
2078
|
-
info(` gencow deploy --
|
|
2132
|
+
info(` gencow deploy 프로덕션 배포 (Pro+)`);
|
|
2133
|
+
info(` gencow deploy --static 프로덕션 정적 배포 (Pro+)`);
|
|
2134
|
+
info(` gencow deploy --rollback 이전 버전으로 롤백`);
|
|
2079
2135
|
info(` gencow deploy logs 서버 로그 조회`);
|
|
2080
2136
|
info(` gencow deploy status 앱 상태 확인`);
|
|
2081
2137
|
info(` gencow deploy --force 의존성 감사 스킵`);
|
|
2138
|
+
log("");
|
|
2139
|
+
info(`💡 개발 환경 배포:`);
|
|
2140
|
+
info(` gencow dev 백엔드 실시간 배포`);
|
|
2141
|
+
info(` gencow static [dir] 정적 파일 배포`);
|
|
2082
2142
|
process.exit(1);
|
|
2083
2143
|
}
|
|
2084
2144
|
}
|
|
2085
2145
|
|
|
2086
|
-
// gencow.json에서 appId 로드
|
|
2146
|
+
// gencow.json에서 appId + prodApp 로드
|
|
2087
2147
|
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
2148
|
+
let prodAppId = null;
|
|
2088
2149
|
if (!appId && existsSync(gencowJsonPath)) {
|
|
2089
2150
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
2090
2151
|
appId = gencowJson.appId || gencowJson.appName; // 하위 호환
|
|
2152
|
+
prodAppId = gencowJson.prodApp || null;
|
|
2153
|
+
} else if (existsSync(gencowJsonPath)) {
|
|
2154
|
+
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
2155
|
+
prodAppId = gencowJson.prodApp || null;
|
|
2091
2156
|
}
|
|
2092
2157
|
|
|
2093
2158
|
// 프로젝트명은 display용으로만 사용 (appId가 아님)
|
|
@@ -2099,9 +2164,134 @@ ${BOLD}Examples:${RESET}
|
|
|
2099
2164
|
}
|
|
2100
2165
|
if (!displayName) displayName = basename(process.cwd());
|
|
2101
2166
|
|
|
2167
|
+
// ── Rollback 분기 ─────────────────────────────────────
|
|
2168
|
+
if (isRollback) {
|
|
2169
|
+
// prodApp이 있으면 prod 대상 롤백, 없으면 dev 대상 롤백
|
|
2170
|
+
const rollbackTarget = prodAppId || appId;
|
|
2171
|
+
if (!rollbackTarget) {
|
|
2172
|
+
error("앱 ID를 찾을 수 없습니다. gencow.json이 있는 프로젝트 루트에서 실행하세요.");
|
|
2173
|
+
process.exit(1);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
log(`\n${BOLD}${CYAN}Gencow Rollback${RESET}\n`);
|
|
2177
|
+
info(`앱: ${rollbackTarget}${prodAppId ? " (prod)" : ""}`);
|
|
2178
|
+
log("");
|
|
2179
|
+
|
|
2180
|
+
const rollbackStartTime = Date.now();
|
|
2181
|
+
const spinnerFrames = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
2182
|
+
let spinnerIdx = 0;
|
|
2183
|
+
const spinner = setInterval(() => {
|
|
2184
|
+
const elapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(0);
|
|
2185
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} 롤백 중... ${DIM}(${elapsed}s)${RESET} `);
|
|
2186
|
+
}, 120);
|
|
2187
|
+
|
|
2188
|
+
const rollbackRes = await platformFetch(creds, `/platform/apps/${rollbackTarget}/rollback`, {
|
|
2189
|
+
method: "POST",
|
|
2190
|
+
headers: { "Content-Type": "application/json" },
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
clearInterval(spinner);
|
|
2194
|
+
process.stdout.write("\r" + " ".repeat(50) + "\r");
|
|
2195
|
+
|
|
2196
|
+
if (!rollbackRes.ok) {
|
|
2197
|
+
const errData = await rollbackRes.json().catch(() => ({}));
|
|
2198
|
+
error(`롤백 실패: ${errData.error || rollbackRes.statusText}`);
|
|
2199
|
+
process.exit(1);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
const rollbackData = await rollbackRes.json();
|
|
2203
|
+
const rollbackElapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(1);
|
|
2204
|
+
|
|
2205
|
+
log("");
|
|
2206
|
+
success(`🔄 롤백 완료! (${rollbackElapsed}s)`);
|
|
2207
|
+
info(`롤백: #${rollbackData.rolledBackFrom} → #${rollbackData.rolledBackTo}`);
|
|
2208
|
+
info(`번들: ${rollbackData.bundleHash}`);
|
|
2209
|
+
info(`URL: ${rollbackData.url}`);
|
|
2210
|
+
log("");
|
|
2211
|
+
warn(`ℹ️ 코드만 롤백되었습니다. 데이터베이스는 변경되지 않았습니다.`);
|
|
2212
|
+
log("");
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2102
2216
|
// ── Static Deploy 분기 ────────────────────────────────
|
|
2103
2217
|
if (isStatic) {
|
|
2104
|
-
|
|
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
|
+
|
|
2223
|
+
// ── Prod 앱 자동 생성 + 배포 대상 전환 (Pro+) ──────────
|
|
2224
|
+
if (envTarget === "prod" && appId) {
|
|
2225
|
+
if (!prodAppId) {
|
|
2226
|
+
// prod 앱이 없음 — 첫 프로덕션 배포
|
|
2227
|
+
const ciMode = process.env.CI === "true" || deployArgs.includes("--yes");
|
|
2228
|
+
if (!ciMode) {
|
|
2229
|
+
const { createInterface } = await import("readline");
|
|
2230
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2231
|
+
log("");
|
|
2232
|
+
log(` ${BOLD}🚀 First production deployment!${RESET}`);
|
|
2233
|
+
log(` This will create a production app: ${CYAN}${appId}-prod${RESET}`);
|
|
2234
|
+
log("");
|
|
2235
|
+
const answer = await new Promise(resolve => {
|
|
2236
|
+
rl.question(` ${YELLOW}⚠${RESET} Proceed? (y/N) `, resolve);
|
|
2237
|
+
});
|
|
2238
|
+
rl.close();
|
|
2239
|
+
if (answer.toLowerCase() !== "y") {
|
|
2240
|
+
info("배포 취소됨.");
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
info("Prod 앱 생성 중...");
|
|
2246
|
+
const createProdRes = await platformFetch(creds, `/platform/apps/${appId}/create-prod`, {
|
|
2247
|
+
method: "POST",
|
|
2248
|
+
headers: { "Content-Type": "application/json" },
|
|
2249
|
+
});
|
|
2250
|
+
|
|
2251
|
+
if (!createProdRes.ok) {
|
|
2252
|
+
const errData = await createProdRes.json().catch(() => ({}));
|
|
2253
|
+
if (createProdRes.status === 403) {
|
|
2254
|
+
log("");
|
|
2255
|
+
log(` ${RED}⛔ Production Deploy — Pro 플랜 이상 필요${RESET}`);
|
|
2256
|
+
log("");
|
|
2257
|
+
log(` ${DIM}gencow deploy는 프로덕션 환경에 배포하는 명령어입니다.${RESET}`);
|
|
2258
|
+
log("");
|
|
2259
|
+
log(` ${BOLD}💡 개발 환경 배포:${RESET}`);
|
|
2260
|
+
log(` ${GREEN}gencow dev${RESET} ${DIM}← 백엔드 실시간 배포 + 라이브 로그${RESET}`);
|
|
2261
|
+
log(` ${GREEN}gencow static dist/${RESET} ${DIM}← 정적 파일(dist/) 배포${RESET}`);
|
|
2262
|
+
log("");
|
|
2263
|
+
log(` ${BOLD}🚀 프로덕션 배포 잠금 해제:${RESET}`);
|
|
2264
|
+
log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Pro 플랜으로 업그레이드${RESET}`);
|
|
2265
|
+
log("");
|
|
2266
|
+
} else {
|
|
2267
|
+
error(`Prod 앱 생성 실패: ${errData.error || createProdRes.statusText}`);
|
|
2268
|
+
}
|
|
2269
|
+
process.exit(1);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
const createProdData = await createProdRes.json();
|
|
2273
|
+
prodAppId = createProdData.prodApp;
|
|
2274
|
+
|
|
2275
|
+
// gencow.json 업데이트
|
|
2276
|
+
const gencowJson = existsSync(gencowJsonPath)
|
|
2277
|
+
? JSON.parse(readFileSync(gencowJsonPath, "utf8"))
|
|
2278
|
+
: {};
|
|
2279
|
+
gencowJson.prodApp = prodAppId;
|
|
2280
|
+
writeFileSync(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
|
|
2281
|
+
|
|
2282
|
+
if (createProdData.alreadyExists) {
|
|
2283
|
+
info(`Prod 앱 확인: ${prodAppId}`);
|
|
2284
|
+
} else {
|
|
2285
|
+
success(`Prod 앱 생성 완료: ${prodAppId}`);
|
|
2286
|
+
info(`URL: ${createProdData.url}`);
|
|
2287
|
+
// 프로비저닝 대기
|
|
2288
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// 배포 대상을 prod 앱으로 전환
|
|
2293
|
+
appId = prodAppId;
|
|
2294
|
+
info(`배포 대상: ${CYAN}${appId}${RESET} (production)`);
|
|
2105
2295
|
}
|
|
2106
2296
|
|
|
2107
2297
|
log(`\n${BOLD}${CYAN}Gencow Deploy${RESET}\n`);
|
|
@@ -2114,20 +2304,6 @@ ${BOLD}Examples:${RESET}
|
|
|
2114
2304
|
info(`포맷: tar.gz`);
|
|
2115
2305
|
log("");
|
|
2116
2306
|
|
|
2117
|
-
// 프로덕션 배포 확인
|
|
2118
|
-
if (envTarget === "prod") {
|
|
2119
|
-
const { createInterface } = await import("readline");
|
|
2120
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2121
|
-
const answer = await new Promise(resolve => {
|
|
2122
|
-
rl.question(` ${YELLOW}⚠${RESET} 프로덕션 배포를 진행하시겠습니까? (y/N) `, resolve);
|
|
2123
|
-
});
|
|
2124
|
-
rl.close();
|
|
2125
|
-
if (answer.toLowerCase() !== "y") {
|
|
2126
|
-
info("배포 취소됨.");
|
|
2127
|
-
return;
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
2307
|
// 0-1. drizzle-kit generate 자동 실행 (번들링 전 migrations/ 최신화)
|
|
2132
2308
|
{
|
|
2133
2309
|
const { execSync: execGen } = await import("child_process");
|
|
@@ -2521,7 +2697,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2521
2697
|
if (!targetDir || !existsSync(resolve(process.cwd(), targetDir))) {
|
|
2522
2698
|
error(`정적 파일 폴더를 찾을 수 없습니다.`);
|
|
2523
2699
|
info(`자동 감지 순서: ${AUTO_DETECT.join(", ")}`);
|
|
2524
|
-
info(`수동 지정: gencow
|
|
2700
|
+
info(`수동 지정: gencow static <dir>`);
|
|
2525
2701
|
process.exit(1);
|
|
2526
2702
|
}
|
|
2527
2703
|
|
|
@@ -2946,9 +3122,19 @@ ${BOLD}Examples:${RESET}
|
|
|
2946
3122
|
if (existsSync(gencowJsonPath)) {
|
|
2947
3123
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
2948
3124
|
appId = gencowJson.appId || gencowJson.appName; // 하위 호환
|
|
3125
|
+
// --prod 시 prod 앱으로 대상 전환
|
|
3126
|
+
if (envTarget === "prod" && gencowJson.prodApp) {
|
|
3127
|
+
appId = gencowJson.prodApp;
|
|
3128
|
+
}
|
|
2949
3129
|
}
|
|
2950
3130
|
}
|
|
2951
3131
|
|
|
3132
|
+
// --prod인데 prod 앱이 없는 경우
|
|
3133
|
+
if (envTarget === "prod" && appId && !appId.endsWith("-prod")) {
|
|
3134
|
+
error("Prod 앱이 아직 없습니다. gencow deploy를 먼저 실행하세요.");
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
|
|
2952
3138
|
if (!appId) {
|
|
2953
3139
|
error("앱 ID를 찾을 수 없습니다. gencow deploy를 먼저 실행하세요.");
|
|
2954
3140
|
return;
|
|
@@ -3083,6 +3269,138 @@ ${BOLD}Examples:${RESET}
|
|
|
3083
3269
|
}
|
|
3084
3270
|
},
|
|
3085
3271
|
|
|
3272
|
+
// ── config ────────────────────────────────────────────
|
|
3273
|
+
async config(...configArgs) {
|
|
3274
|
+
const creds = requireCreds();
|
|
3275
|
+
const subCmd = configArgs[0] || "help";
|
|
3276
|
+
const restArgs = configArgs.slice(1);
|
|
3277
|
+
|
|
3278
|
+
// 앱 ID 결정
|
|
3279
|
+
let appId = null;
|
|
3280
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
3281
|
+
if (restArgs[i] === "--app" || restArgs[i] === "-a") appId = restArgs[++i];
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
if (!appId) {
|
|
3285
|
+
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
3286
|
+
if (existsSync(gencowJsonPath)) {
|
|
3287
|
+
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
3288
|
+
appId = gencowJson.appId || gencowJson.appName;
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
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`);
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
if (!appId) {
|
|
3304
|
+
error("앱 ID를 찾을 수 없습니다. gencow deploy를 먼저 실행하세요.");
|
|
3305
|
+
return;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
switch (subCmd) {
|
|
3309
|
+
case "set": {
|
|
3310
|
+
const key = restArgs.find((a, i) => !a.startsWith("-") && !(restArgs[i - 1] === "--app" || restArgs[i - 1] === "-a"));
|
|
3311
|
+
const val = restArgs[restArgs.indexOf(key) + 1];
|
|
3312
|
+
|
|
3313
|
+
if (!key || val === undefined) {
|
|
3314
|
+
error("사용법: gencow config set image.maxWidth <값>");
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
const imageConfig = {};
|
|
3319
|
+
if (key === "image.maxWidth" || key === "image.max-width") {
|
|
3320
|
+
const v = parseInt(val);
|
|
3321
|
+
if (isNaN(v) || v < 0 || v > 10000) {
|
|
3322
|
+
error("maxWidth는 0~10000 사이의 값이어야 합니다. (0 = 리셋)");
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
imageConfig.autoMaxWidth = v;
|
|
3326
|
+
} else if (key === "image.quality") {
|
|
3327
|
+
const v = parseInt(val);
|
|
3328
|
+
if (isNaN(v) || v < 0 || v > 100) {
|
|
3329
|
+
error("quality는 0~100 사이의 값이어야 합니다. (0 = 리셋)");
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
imageConfig.autoQuality = v;
|
|
3333
|
+
} else {
|
|
3334
|
+
error(`알 수 없는 설정 키: ${key}`);
|
|
3335
|
+
info("사용 가능: image.maxWidth, image.quality");
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
const res = await rpcMutation(creds, "apps.updateImageConfig", { name: appId, imageConfig });
|
|
3340
|
+
if (res.ok) {
|
|
3341
|
+
const data = await res.json();
|
|
3342
|
+
success(`${key} = ${val} 설정 완료`);
|
|
3343
|
+
if (data.imageConfig) {
|
|
3344
|
+
info(`현재 설정: ${JSON.stringify(data.imageConfig)}`);
|
|
3345
|
+
}
|
|
3346
|
+
info(`${DIM}※ 다음 이미지 요청부터 적용됩니다. 기존 캐시는 별도 키로 저장됩니다.${RESET}`);
|
|
3347
|
+
} else {
|
|
3348
|
+
const errData = await res.json().catch(() => ({}));
|
|
3349
|
+
error(`설정 실패: ${errData.error || res.statusText}`);
|
|
3350
|
+
}
|
|
3351
|
+
break;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
case "get": {
|
|
3355
|
+
const key = restArgs.find(a => !a.startsWith("-"));
|
|
3356
|
+
if (key && key !== "image") {
|
|
3357
|
+
error(`알 수 없는 설정 키: ${key}`);
|
|
3358
|
+
info("사용 가능: image");
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
const res = await rpcQuery(creds, "apps.getImageConfig", { name: appId });
|
|
3363
|
+
if (res.ok) {
|
|
3364
|
+
const config = await res.json();
|
|
3365
|
+
log(`\n${BOLD}${CYAN}이미지 설정${RESET} — ${appId}\n`);
|
|
3366
|
+
if (Object.keys(config).length === 0) {
|
|
3367
|
+
info("커스텀 설정 없음 (Tier 기본값 사용)");
|
|
3368
|
+
} else {
|
|
3369
|
+
if (config.autoMaxWidth) log(` ${GREEN}autoMaxWidth${RESET} ${config.autoMaxWidth} px`);
|
|
3370
|
+
if (config.autoQuality) log(` ${GREEN}autoQuality${RESET} ${config.autoQuality}`);
|
|
3371
|
+
}
|
|
3372
|
+
log("");
|
|
3373
|
+
} else {
|
|
3374
|
+
error("설정 조회 실패");
|
|
3375
|
+
}
|
|
3376
|
+
break;
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
case "reset": {
|
|
3380
|
+
const key = restArgs.find(a => !a.startsWith("-"));
|
|
3381
|
+
if (key && key !== "image") {
|
|
3382
|
+
error(`알 수 없는 설정 키: ${key}`);
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
// autoMaxWidth=0 + autoQuality=0 → 둘 다 리셋
|
|
3387
|
+
const res = await rpcMutation(creds, "apps.updateImageConfig", {
|
|
3388
|
+
name: appId,
|
|
3389
|
+
imageConfig: { autoMaxWidth: 0, autoQuality: 0 },
|
|
3390
|
+
});
|
|
3391
|
+
if (res.ok) {
|
|
3392
|
+
success("이미지 설정 초기화 완료 (Tier 기본값 사용)");
|
|
3393
|
+
} else {
|
|
3394
|
+
error("초기화 실패");
|
|
3395
|
+
}
|
|
3396
|
+
break;
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
default:
|
|
3400
|
+
error(`알 수 없는 하위 명령: ${subCmd}`);
|
|
3401
|
+
info("사용법: gencow config [set|get|reset] ...");
|
|
3402
|
+
}
|
|
3403
|
+
},
|
|
3086
3404
|
|
|
3087
3405
|
// ── files ─────────────────────────────────────────────
|
|
3088
3406
|
async files(...filesArgs) {
|