gencow 0.1.132 → 0.1.133
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 +269 -65
- package/lib/__tests__/cloud-targets.test.ts +47 -0
- package/lib/__tests__/readme-codegen.test.ts +2 -1
- package/lib/cloud-targets.mjs +65 -0
- package/lib/readme-codegen.mjs +5 -2
- package/package.json +1 -1
- package/server/index.js +377 -156
- package/server/index.js.map +4 -4
- package/dashboard/apple-touch-icon.png +0 -0
- package/dashboard/assets/index-CYN7QmGd.css +0 -1
- package/dashboard/assets/index-D2uOdJCM.js +0 -378
- package/dashboard/favicon-16.png +0 -0
- package/dashboard/favicon-192.png +0 -0
- package/dashboard/favicon-32.png +0 -0
- package/dashboard/favicon-512.png +0 -0
- package/dashboard/favicon.ico +0 -0
- package/dashboard/favicon.svg +0 -1
- package/dashboard/file.svg +0 -1
- package/dashboard/globe.svg +0 -1
- package/dashboard/index.html +0 -29
- package/dashboard/next.svg +0 -1
- package/dashboard/vercel.svg +0 -1
- package/dashboard/window.svg +0 -1
package/bin/gencow.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { fileURLToPath } from "url";
|
|
|
36
36
|
import { createRequire } from "module";
|
|
37
37
|
import { buildApiObject, buildGeneratedApiTs } from "../lib/api-codegen.mjs";
|
|
38
38
|
import { buildReadmeMarkdown, extractComponentsBlock } from "../lib/readme-codegen.mjs";
|
|
39
|
+
import { resolveCloudAppTarget } from "../lib/cloud-targets.mjs";
|
|
39
40
|
|
|
40
41
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
42
|
|
|
@@ -202,6 +203,68 @@ function parseDurationToMinutes(raw, label = "duration") {
|
|
|
202
203
|
return value * 60 * 24;
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
function normalizeCliCorsOrigin(raw) {
|
|
207
|
+
const trimmed = raw.trim();
|
|
208
|
+
if (!trimmed) {
|
|
209
|
+
throw new Error("Origin is required");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (trimmed.includes("*")) {
|
|
213
|
+
throw new Error("Wildcard origins are not supported");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let url;
|
|
217
|
+
try {
|
|
218
|
+
url = new URL(trimmed);
|
|
219
|
+
} catch {
|
|
220
|
+
throw new Error("Origin must be a valid URL");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (url.protocol !== "https:") {
|
|
224
|
+
throw new Error("Origin must start with https://");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (url.username || url.password) {
|
|
228
|
+
throw new Error("Origin cannot include username or password");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (url.pathname !== "/" || url.search || url.hash) {
|
|
232
|
+
throw new Error("Origin must be a bare origin without path, query, or hash");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return url.origin;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function printCorsStatus(appId, config) {
|
|
239
|
+
log(`\n${BOLD}${CYAN}CORS Origins${RESET} — ${appId} (${config.env})\n`);
|
|
240
|
+
|
|
241
|
+
log(` ${DIM}Automatically allowed:${RESET}`);
|
|
242
|
+
for (const entry of config.autoOrigins) {
|
|
243
|
+
log(` ${GREEN}${entry.label}${RESET} ${DIM}${entry.note}${RESET}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (config.customDomain) {
|
|
247
|
+
const statusLabel = config.customDomain.status ? ` (${config.customDomain.status})` : "";
|
|
248
|
+
log(
|
|
249
|
+
` ${GREEN}${config.customDomain.origin}${RESET} ${DIM}same-origin custom domain${statusLabel}${RESET}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (config.customOrigins.length === 0) {
|
|
254
|
+
log(`\n ${DIM}Custom origins:${RESET}`);
|
|
255
|
+
info("No custom origins configured.");
|
|
256
|
+
} else {
|
|
257
|
+
log(`\n ${DIM}Custom origins:${RESET}`);
|
|
258
|
+
for (const origin of config.customOrigins) {
|
|
259
|
+
log(` ${origin}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
log("");
|
|
264
|
+
info("Only needed when your frontend is hosted outside Gencow.");
|
|
265
|
+
log("");
|
|
266
|
+
}
|
|
267
|
+
|
|
205
268
|
// ─── gencow.config.ts parser ─────────────────────────────
|
|
206
269
|
//
|
|
207
270
|
// Reads gencow.config.ts from project root and returns the config object.
|
|
@@ -861,8 +924,8 @@ const commands = {
|
|
|
861
924
|
"@gencow/core": "latest",
|
|
862
925
|
"@electric-sql/pglite": "^0.3.15",
|
|
863
926
|
"better-auth": "^1.5.1",
|
|
864
|
-
"drizzle-orm": "^0.
|
|
865
|
-
"drizzle-kit": "^0.
|
|
927
|
+
"drizzle-orm": "^1.0.0-beta || ^1.0.0",
|
|
928
|
+
"drizzle-kit": "^1.0.0-beta || ^1.0.0",
|
|
866
929
|
hono: "^4.12.0",
|
|
867
930
|
postgres: "^3.4.8",
|
|
868
931
|
};
|
|
@@ -1955,6 +2018,9 @@ ${BOLD}Commands (login required):${RESET}
|
|
|
1955
2018
|
${GREEN}env set K=V${RESET} Set cloud env var ${DIM}(hot-reload, no restart)${RESET}
|
|
1956
2019
|
${GREEN}env unset KEY${RESET} Remove cloud env var
|
|
1957
2020
|
${GREEN}env push${RESET} Push .env to cloud ${DIM}(--prod reads .env.production)${RESET}
|
|
2021
|
+
${GREEN}cors list${RESET} Show allowed CORS origins ${DIM}(auto + custom)${RESET}
|
|
2022
|
+
${GREEN}cors add URL${RESET} Allow external frontend origin ${DIM}(HTTPS only)${RESET}
|
|
2023
|
+
${GREEN}cors remove URL${RESET} Remove custom CORS origin
|
|
1958
2024
|
${GREEN}files upload${RESET} Upload files to app storage ${DIM}(--recursive for dirs)${RESET}
|
|
1959
2025
|
${GREEN}files list${RESET} List uploaded files
|
|
1960
2026
|
${GREEN}files delete${RESET} Delete uploaded file ${DIM}(--yes to skip confirm)${RESET}
|
|
@@ -1983,7 +2049,7 @@ ${BOLD}Ops (Observability):${RESET}
|
|
|
1983
2049
|
${DIM}--prod Target production app${RESET}
|
|
1984
2050
|
|
|
1985
2051
|
${DIM}Tip: Most commands support --prod to target the production app.${RESET}
|
|
1986
|
-
${DIM} e.g. gencow deploy --prod, gencow env list --prod${RESET}
|
|
2052
|
+
${DIM} e.g. gencow deploy --prod, gencow env list --prod, gencow cors list --prod${RESET}
|
|
1987
2053
|
|
|
1988
2054
|
${BOLD}Examples:${RESET}
|
|
1989
2055
|
${DIM}# New project:${RESET}
|
|
@@ -3437,37 +3503,16 @@ ${BOLD}Examples:${RESET}
|
|
|
3437
3503
|
const subCmd = envArgs[0] || "list";
|
|
3438
3504
|
const restArgs = envArgs.slice(1);
|
|
3439
3505
|
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
if (restArgs[i] === "--prod") envTarget = "prod";
|
|
3447
|
-
}
|
|
3448
|
-
|
|
3449
|
-
if (!appId) {
|
|
3450
|
-
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
3451
|
-
if (existsSync(gencowJsonPath)) {
|
|
3452
|
-
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
3453
|
-
appId = gencowJson.appId || gencowJson.appName; // 하위 호환
|
|
3454
|
-
// --prod 시 prod 앱으로 대상 전환
|
|
3455
|
-
if (envTarget === "prod" && gencowJson.prodApp) {
|
|
3456
|
-
appId = gencowJson.prodApp;
|
|
3457
|
-
}
|
|
3458
|
-
}
|
|
3459
|
-
}
|
|
3460
|
-
|
|
3461
|
-
// --prod인데 prod 앱이 없는 경우
|
|
3462
|
-
if (envTarget === "prod" && appId && !appId.endsWith("-prod")) {
|
|
3463
|
-
error("No prod app yet. Run gencow deploy first.");
|
|
3506
|
+
const target = resolveCloudAppTarget(restArgs, {
|
|
3507
|
+
missingAppMessage: "App ID not found. Run gencow deploy first.",
|
|
3508
|
+
missingProdMessage: "No prod app yet. Run gencow deploy --prod first.",
|
|
3509
|
+
});
|
|
3510
|
+
if (target.error) {
|
|
3511
|
+
error(target.error);
|
|
3464
3512
|
return;
|
|
3465
3513
|
}
|
|
3466
3514
|
|
|
3467
|
-
|
|
3468
|
-
error("App ID not found. Run gencow deploy first.");
|
|
3469
|
-
return;
|
|
3470
|
-
}
|
|
3515
|
+
const { appId, envTarget } = target;
|
|
3471
3516
|
|
|
3472
3517
|
switch (subCmd) {
|
|
3473
3518
|
case "list":
|
|
@@ -3588,6 +3633,157 @@ ${BOLD}Examples:${RESET}
|
|
|
3588
3633
|
}
|
|
3589
3634
|
},
|
|
3590
3635
|
|
|
3636
|
+
// ── cors ──────────────────────────────────────────────
|
|
3637
|
+
async cors(...corsArgs) {
|
|
3638
|
+
if (corsArgs.includes("--help") || corsArgs.includes("-h")) {
|
|
3639
|
+
log(`\n${BOLD}${CYAN}gencow cors${RESET} — Manage allowed CORS origins\n`);
|
|
3640
|
+
log(` ${BOLD}Usage:${RESET} gencow cors <command> [options]\n`);
|
|
3641
|
+
log(` ${BOLD}Commands:${RESET}`);
|
|
3642
|
+
log(` ${CYAN}list${RESET} Show allowed origins`);
|
|
3643
|
+
log(` ${CYAN}add${RESET} <origin> Allow a new HTTPS origin`);
|
|
3644
|
+
log(` ${CYAN}remove${RESET} <origin> Remove a custom origin\n`);
|
|
3645
|
+
log(` ${BOLD}Options:${RESET}`);
|
|
3646
|
+
log(` ${DIM}--prod${RESET} Target production app`);
|
|
3647
|
+
log(` ${DIM}--app, -a${RESET} Target specific app\n`);
|
|
3648
|
+
log(` ${BOLD}Examples:${RESET}`);
|
|
3649
|
+
log(` gencow cors add https://myapp.vercel.app`);
|
|
3650
|
+
log(` gencow cors list --prod\n`);
|
|
3651
|
+
info("Custom domains connected with gencow domain set are same-origin, so they do not need CORS entries.");
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
const creds = requireCreds();
|
|
3656
|
+
const subCmd = corsArgs[0] || "list";
|
|
3657
|
+
const restArgs = corsArgs.slice(1);
|
|
3658
|
+
const target = resolveCloudAppTarget(restArgs, {
|
|
3659
|
+
missingAppMessage: "App ID not found. Run gencow deploy first.",
|
|
3660
|
+
missingProdMessage: "No prod app yet. Run gencow deploy --prod first.",
|
|
3661
|
+
});
|
|
3662
|
+
|
|
3663
|
+
if (target.error) {
|
|
3664
|
+
error(target.error);
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
const { appId, envTarget } = target;
|
|
3669
|
+
|
|
3670
|
+
async function fetchCorsConfig() {
|
|
3671
|
+
const res = await platformFetch(
|
|
3672
|
+
creds,
|
|
3673
|
+
`/platform/apps/${appId}/cors?env=${encodeURIComponent(envTarget)}`,
|
|
3674
|
+
{ method: "GET" },
|
|
3675
|
+
);
|
|
3676
|
+
const data = await res.json().catch(() => ({}));
|
|
3677
|
+
if (!res.ok) {
|
|
3678
|
+
throw new Error(data.error || res.statusText || "Failed to fetch CORS origins");
|
|
3679
|
+
}
|
|
3680
|
+
return data;
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
function resolveOriginArg() {
|
|
3684
|
+
const rawOrigin = restArgs.find((arg, index) => {
|
|
3685
|
+
if (arg.startsWith("-")) return false;
|
|
3686
|
+
const prev = restArgs[index - 1];
|
|
3687
|
+
return prev !== "--app" && prev !== "-a";
|
|
3688
|
+
});
|
|
3689
|
+
|
|
3690
|
+
if (!rawOrigin) {
|
|
3691
|
+
throw new Error("Origin is required");
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
return normalizeCliCorsOrigin(rawOrigin);
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
switch (subCmd) {
|
|
3698
|
+
case "list":
|
|
3699
|
+
case "ls": {
|
|
3700
|
+
try {
|
|
3701
|
+
printCorsStatus(appId, await fetchCorsConfig());
|
|
3702
|
+
} catch (e) {
|
|
3703
|
+
error(e.message);
|
|
3704
|
+
}
|
|
3705
|
+
break;
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
case "add": {
|
|
3709
|
+
let origin;
|
|
3710
|
+
try {
|
|
3711
|
+
origin = resolveOriginArg();
|
|
3712
|
+
} catch (e) {
|
|
3713
|
+
error(e.message);
|
|
3714
|
+
info("Usage: gencow cors add <origin>");
|
|
3715
|
+
info(" Example: gencow cors add https://myapp.com");
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
const res = await platformFetch(creds, `/platform/apps/${appId}/cors`, {
|
|
3720
|
+
method: "POST",
|
|
3721
|
+
headers: { "Content-Type": "application/json" },
|
|
3722
|
+
body: JSON.stringify({ origin, env: envTarget }),
|
|
3723
|
+
});
|
|
3724
|
+
const data = await res.json().catch(() => ({}));
|
|
3725
|
+
if (!res.ok) {
|
|
3726
|
+
error(data.error || res.statusText || "Failed to add origin");
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
if (data.status === "added") {
|
|
3731
|
+
success(`CORS origin added: ${data.origin}`);
|
|
3732
|
+
} else if (data.status === "already_exists") {
|
|
3733
|
+
info(`Already allowed: ${data.origin}`);
|
|
3734
|
+
} else if (data.status === "same_origin_domain") {
|
|
3735
|
+
info(`${data.origin} is your app's custom domain — same-origin, so no CORS entry is needed.`);
|
|
3736
|
+
} else {
|
|
3737
|
+
info(`${data.origin} is already auto-allowed by Gencow.`);
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
printCorsStatus(appId, data.config);
|
|
3741
|
+
break;
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
case "remove":
|
|
3745
|
+
case "rm": {
|
|
3746
|
+
let origin;
|
|
3747
|
+
try {
|
|
3748
|
+
origin = resolveOriginArg();
|
|
3749
|
+
} catch (e) {
|
|
3750
|
+
error(e.message);
|
|
3751
|
+
info("Usage: gencow cors remove <origin>");
|
|
3752
|
+
info(" Example: gencow cors remove https://myapp.com");
|
|
3753
|
+
return;
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
const res = await platformFetch(
|
|
3757
|
+
creds,
|
|
3758
|
+
`/platform/apps/${appId}/cors?env=${encodeURIComponent(envTarget)}&origin=${encodeURIComponent(origin)}`,
|
|
3759
|
+
{ method: "DELETE" },
|
|
3760
|
+
);
|
|
3761
|
+
const data = await res.json().catch(() => ({}));
|
|
3762
|
+
if (!res.ok) {
|
|
3763
|
+
error(data.error || res.statusText || "Failed to remove origin");
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
if (data.status === "removed") {
|
|
3768
|
+
success(`CORS origin removed: ${data.origin}`);
|
|
3769
|
+
} else if (data.status === "not_found") {
|
|
3770
|
+
info(`Not configured: ${data.origin}`);
|
|
3771
|
+
} else if (data.status === "same_origin_domain") {
|
|
3772
|
+
info(`${data.origin} is your app's custom domain — same-origin, so there is no CORS entry to remove.`);
|
|
3773
|
+
} else {
|
|
3774
|
+
info(`${data.origin} is auto-allowed by Gencow and is not stored as a custom origin.`);
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
printCorsStatus(appId, data.config);
|
|
3778
|
+
break;
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
default:
|
|
3782
|
+
error(`Unknown subcommand: ${subCmd}`);
|
|
3783
|
+
info("Usage: gencow cors [list|add|remove] ...");
|
|
3784
|
+
}
|
|
3785
|
+
},
|
|
3786
|
+
|
|
3591
3787
|
// ── config ────────────────────────────────────────────
|
|
3592
3788
|
async config(...configArgs) {
|
|
3593
3789
|
// --help handler
|
|
@@ -5320,7 +5516,6 @@ process.exit(0);
|
|
|
5320
5516
|
// ── env — 환경변수 관리 (Cloud only) ─────────
|
|
5321
5517
|
async env(...envArgs) {
|
|
5322
5518
|
const filteredArgs = envArgs.filter((a) => a !== "--local");
|
|
5323
|
-
const subcmd = filteredArgs[0] || "list";
|
|
5324
5519
|
|
|
5325
5520
|
// --local 사용 시 .env 파일 사용 안내
|
|
5326
5521
|
if (envArgs.includes("--local")) {
|
|
@@ -5330,32 +5525,37 @@ process.exit(0);
|
|
|
5330
5525
|
return;
|
|
5331
5526
|
}
|
|
5332
5527
|
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
}
|
|
5342
|
-
|
|
5343
|
-
}
|
|
5344
|
-
}
|
|
5345
|
-
|
|
5346
|
-
if (!appId) {
|
|
5347
|
-
error("No gencow.json found. Cannot determine cloud app.");
|
|
5348
|
-
info(`Run ${GREEN}gencow deploy${RESET} first to create a cloud app.`);
|
|
5349
|
-
info(`Or use ${GREEN}.env${RESET} file for local development.`);
|
|
5528
|
+
if (filteredArgs.includes("--help") || filteredArgs.includes("-h")) {
|
|
5529
|
+
log(`\n${BOLD}${CYAN}gencow env${RESET} — Environment variable management\n`);
|
|
5530
|
+
log(` ${BOLD}Usage:${RESET} gencow env <command> [options]\n`);
|
|
5531
|
+
log(` ${BOLD}Commands:${RESET}`);
|
|
5532
|
+
log(` ${CYAN}list${RESET} List cloud env vars`);
|
|
5533
|
+
log(` ${CYAN}set${RESET} KEY=VALUE Set cloud env var (hot-reload)`);
|
|
5534
|
+
log(` ${CYAN}unset${RESET} KEY Remove cloud env var`);
|
|
5535
|
+
log(` ${CYAN}push${RESET} Push .env to cloud\n`);
|
|
5536
|
+
log(` ${BOLD}Options:${RESET}`);
|
|
5537
|
+
log(` ${DIM}--prod${RESET} Target production app`);
|
|
5538
|
+
log(` ${DIM}--app, -a${RESET} Target specific app\n`);
|
|
5350
5539
|
return;
|
|
5351
5540
|
}
|
|
5352
5541
|
|
|
5542
|
+
const subcmd = filteredArgs[0] || "list";
|
|
5543
|
+
const restArgs = filteredArgs.slice(1);
|
|
5353
5544
|
const creds = requireCreds();
|
|
5545
|
+
const target = resolveCloudAppTarget(restArgs, {
|
|
5546
|
+
missingAppMessage: "App ID not found. Run gencow deploy first.",
|
|
5547
|
+
missingProdMessage: "No prod app yet. Run gencow deploy --prod first.",
|
|
5548
|
+
});
|
|
5549
|
+
if (target.error) {
|
|
5550
|
+
error(target.error);
|
|
5551
|
+
return;
|
|
5552
|
+
}
|
|
5553
|
+
const { appId, envTarget } = target;
|
|
5354
5554
|
|
|
5355
5555
|
// ── cloud env list ──
|
|
5356
|
-
if (subcmd === "list") {
|
|
5556
|
+
if (subcmd === "list" || subcmd === "ls") {
|
|
5357
5557
|
try {
|
|
5358
|
-
const res = await platformFetch(creds, `/platform/apps/${appId}/env`, {
|
|
5558
|
+
const res = await platformFetch(creds, `/platform/apps/${appId}/env?env=${envTarget}`, {
|
|
5359
5559
|
method: "GET",
|
|
5360
5560
|
});
|
|
5361
5561
|
if (!res.ok) {
|
|
@@ -5365,7 +5565,7 @@ process.exit(0);
|
|
|
5365
5565
|
const vars = await res.json();
|
|
5366
5566
|
|
|
5367
5567
|
log(
|
|
5368
|
-
`\n${BOLD}Environment Variables${RESET} ${DIM}(cloud: ${appId}, ${vars.length} total)${RESET}\n`,
|
|
5568
|
+
`\n${BOLD}Environment Variables${RESET} ${DIM}(cloud: ${appId}, ${envTarget}, ${vars.length} total)${RESET}\n`,
|
|
5369
5569
|
);
|
|
5370
5570
|
|
|
5371
5571
|
if (vars.length === 0) {
|
|
@@ -5388,7 +5588,7 @@ process.exit(0);
|
|
|
5388
5588
|
|
|
5389
5589
|
// ── cloud env set KEY=VALUE ──
|
|
5390
5590
|
if (subcmd === "set") {
|
|
5391
|
-
const pairs =
|
|
5591
|
+
const pairs = restArgs.filter((arg) => arg.includes("=") && !arg.startsWith("-"));
|
|
5392
5592
|
if (pairs.length === 0) {
|
|
5393
5593
|
error("Usage: gencow env set KEY=VALUE [KEY2=VALUE2 ...]");
|
|
5394
5594
|
return;
|
|
@@ -5418,13 +5618,13 @@ process.exit(0);
|
|
|
5418
5618
|
const res = await platformFetch(creds, `/platform/apps/${appId}/env`, {
|
|
5419
5619
|
method: "POST",
|
|
5420
5620
|
headers: { "Content-Type": "application/json" },
|
|
5421
|
-
body: JSON.stringify({ key: name, value }),
|
|
5621
|
+
body: JSON.stringify({ key: name, value, env: envTarget }),
|
|
5422
5622
|
});
|
|
5423
5623
|
if (!res.ok) {
|
|
5424
5624
|
const body = await res.json().catch(() => ({}));
|
|
5425
5625
|
throw new Error(body.error || `HTTP ${res.status}`);
|
|
5426
5626
|
}
|
|
5427
|
-
success(`Set ${BOLD}${name}${RESET} ${DIM}(cloud: ${appId})${RESET}`);
|
|
5627
|
+
success(`Set ${BOLD}${name}${RESET} ${DIM}(cloud: ${appId}, ${envTarget})${RESET}`);
|
|
5428
5628
|
} catch (e) {
|
|
5429
5629
|
error(`Failed to set ${name}: ${e.message}`);
|
|
5430
5630
|
}
|
|
@@ -5436,7 +5636,7 @@ process.exit(0);
|
|
|
5436
5636
|
|
|
5437
5637
|
// ── cloud env unset KEY ──
|
|
5438
5638
|
if (subcmd === "unset" || subcmd === "remove" || subcmd === "delete") {
|
|
5439
|
-
const keys =
|
|
5639
|
+
const keys = restArgs.filter((arg) => !arg.startsWith("-"));
|
|
5440
5640
|
if (keys.length === 0) {
|
|
5441
5641
|
error("Usage: gencow env unset KEY [KEY2 ...]");
|
|
5442
5642
|
return;
|
|
@@ -5444,14 +5644,18 @@ process.exit(0);
|
|
|
5444
5644
|
for (const rawKey of keys) {
|
|
5445
5645
|
const key = rawKey.toUpperCase().replace(/[^A-Z0-9_]/g, "");
|
|
5446
5646
|
try {
|
|
5447
|
-
const res = await platformFetch(
|
|
5448
|
-
|
|
5449
|
-
|
|
5647
|
+
const res = await platformFetch(
|
|
5648
|
+
creds,
|
|
5649
|
+
`/platform/apps/${appId}/env/${encodeURIComponent(key)}?env=${envTarget}`,
|
|
5650
|
+
{
|
|
5651
|
+
method: "DELETE",
|
|
5652
|
+
},
|
|
5653
|
+
);
|
|
5450
5654
|
if (!res.ok) {
|
|
5451
5655
|
const body = await res.json().catch(() => ({}));
|
|
5452
5656
|
throw new Error(body.error || `HTTP ${res.status}`);
|
|
5453
5657
|
}
|
|
5454
|
-
success(`Removed ${BOLD}${key}${RESET} ${DIM}(cloud: ${appId})${RESET}`);
|
|
5658
|
+
success(`Removed ${BOLD}${key}${RESET} ${DIM}(cloud: ${appId}, ${envTarget})${RESET}`);
|
|
5455
5659
|
} catch (e) {
|
|
5456
5660
|
error(`Failed to remove ${key}: ${e.message}`);
|
|
5457
5661
|
}
|
|
@@ -5462,9 +5666,9 @@ process.exit(0);
|
|
|
5462
5666
|
|
|
5463
5667
|
// ── cloud env push — .env to cloud ──
|
|
5464
5668
|
if (subcmd === "push") {
|
|
5465
|
-
const envPath = resolve(process.cwd(), ".env");
|
|
5669
|
+
const envPath = resolve(process.cwd(), envTarget === "prod" ? ".env.production" : ".env");
|
|
5466
5670
|
if (!existsSync(envPath)) {
|
|
5467
|
-
error(
|
|
5671
|
+
error(`${envPath} not found.`);
|
|
5468
5672
|
return;
|
|
5469
5673
|
}
|
|
5470
5674
|
|
|
@@ -5493,13 +5697,13 @@ process.exit(0);
|
|
|
5493
5697
|
return;
|
|
5494
5698
|
}
|
|
5495
5699
|
|
|
5496
|
-
log(`\n${BOLD}Pushing .env${RESET} ${DIM}(cloud: ${appId}, ${count} variables)${RESET}\n`);
|
|
5700
|
+
log(`\n${BOLD}Pushing .env${RESET} ${DIM}(cloud: ${appId}, ${envTarget}, ${count} variables)${RESET}\n`);
|
|
5497
5701
|
|
|
5498
5702
|
try {
|
|
5499
5703
|
const res = await platformFetch(creds, `/platform/apps/${appId}/env/bulk`, {
|
|
5500
5704
|
method: "PUT",
|
|
5501
5705
|
headers: { "Content-Type": "application/json" },
|
|
5502
|
-
body: JSON.stringify({ vars }),
|
|
5706
|
+
body: JSON.stringify({ vars, env: envTarget }),
|
|
5503
5707
|
});
|
|
5504
5708
|
if (!res.ok) {
|
|
5505
5709
|
const body = await res.json().catch(() => ({}));
|
|
@@ -5509,7 +5713,7 @@ process.exit(0);
|
|
|
5509
5713
|
for (const key of Object.keys(vars)) {
|
|
5510
5714
|
success(`${key}`);
|
|
5511
5715
|
}
|
|
5512
|
-
log(`\n ${GREEN}${data.count || count} set${RESET} ${DIM}(cloud: ${appId})${RESET}`);
|
|
5716
|
+
log(`\n ${GREEN}${data.count || count} set${RESET} ${DIM}(cloud: ${appId}, ${envTarget})${RESET}`);
|
|
5513
5717
|
} catch (e) {
|
|
5514
5718
|
error(`Failed to push env vars: ${e.message}`);
|
|
5515
5719
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
import { resolveCloudAppTarget } from "../cloud-targets.mjs";
|
|
6
|
+
|
|
7
|
+
const originalCwd = process.cwd();
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
process.chdir(originalCwd);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("resolveCloudAppTarget", () => {
|
|
14
|
+
it("uses the project prod app when --prod is provided", () => {
|
|
15
|
+
const cwd = mkdtempSync(resolve(tmpdir(), "gencow-cloud-targets-"));
|
|
16
|
+
writeFileSync(
|
|
17
|
+
resolve(cwd, "gencow.json"),
|
|
18
|
+
JSON.stringify({ appId: "demo-app", prodApp: "demo-app-prod" }, null, 2),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(resolveCloudAppTarget(["--prod"], { cwd })).toEqual({
|
|
22
|
+
appId: "demo-app-prod",
|
|
23
|
+
envTarget: "prod",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("rejects an explicit dev app when combined with --prod", () => {
|
|
30
|
+
const cwd = mkdtempSync(resolve(tmpdir(), "gencow-cloud-targets-"));
|
|
31
|
+
writeFileSync(
|
|
32
|
+
resolve(cwd, "gencow.json"),
|
|
33
|
+
JSON.stringify({ appId: "demo-app", prodApp: "demo-app-prod" }, null, 2),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(
|
|
37
|
+
resolveCloudAppTarget(["--app", "demo-app", "--prod"], {
|
|
38
|
+
cwd,
|
|
39
|
+
missingProdMessage: "No prod app yet. Run gencow deploy --prod first.",
|
|
40
|
+
}),
|
|
41
|
+
).toEqual({
|
|
42
|
+
error: "No prod app yet. Run gencow deploy --prod first.",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -318,7 +318,8 @@ describe("buildDeploySection — 배포 가이드", () => {
|
|
|
318
318
|
|
|
319
319
|
it("CORS 설정 안내", () => {
|
|
320
320
|
const md = buildDeploySection();
|
|
321
|
-
expect(md).toContain("
|
|
321
|
+
expect(md).toContain("gencow cors add");
|
|
322
|
+
expect(md).toContain("same-origin");
|
|
322
323
|
});
|
|
323
324
|
|
|
324
325
|
it("풀스택 통합 배포 (백엔드 자동 감지) 안내", () => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
|
|
4
|
+
export function readProjectAppTargets(cwd = process.cwd()) {
|
|
5
|
+
const gencowJsonPath = resolve(cwd, "gencow.json");
|
|
6
|
+
if (!existsSync(gencowJsonPath)) {
|
|
7
|
+
return { appId: null, prodApp: null };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
12
|
+
return {
|
|
13
|
+
appId: gencowJson.appId || gencowJson.appName || null,
|
|
14
|
+
prodApp: gencowJson.prodApp || null,
|
|
15
|
+
};
|
|
16
|
+
} catch {
|
|
17
|
+
return { appId: null, prodApp: null };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveCloudAppTarget(restArgs, options = {}) {
|
|
22
|
+
const {
|
|
23
|
+
cwd = process.cwd(),
|
|
24
|
+
missingAppMessage = "App ID not found. Run gencow deploy first.",
|
|
25
|
+
missingProdMessage = "No prod app yet. Run gencow deploy first.",
|
|
26
|
+
} = options;
|
|
27
|
+
|
|
28
|
+
let appId = null;
|
|
29
|
+
let envTarget = "dev";
|
|
30
|
+
let explicitApp = false;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
33
|
+
if (restArgs[i] === "--app" || restArgs[i] === "-a") {
|
|
34
|
+
appId = restArgs[++i];
|
|
35
|
+
explicitApp = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (restArgs[i] === "--prod") {
|
|
39
|
+
envTarget = "prod";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const projectTargets = readProjectAppTargets(cwd);
|
|
44
|
+
if (!appId && projectTargets.appId) {
|
|
45
|
+
appId = envTarget === "prod" && projectTargets.prodApp ? projectTargets.prodApp : projectTargets.appId;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (envTarget === "prod" && !explicitApp && projectTargets.prodApp) {
|
|
49
|
+
appId = projectTargets.prodApp;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (envTarget === "prod" && explicitApp && appId && !appId.endsWith("-prod")) {
|
|
53
|
+
return { error: missingProdMessage };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (envTarget === "prod" && !appId) {
|
|
57
|
+
return { error: missingProdMessage };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!appId) {
|
|
61
|
+
return { error: missingAppMessage };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { appId, envTarget };
|
|
65
|
+
}
|
package/lib/readme-codegen.mjs
CHANGED
|
@@ -558,9 +558,12 @@ export function buildDeploySection() {
|
|
|
558
558
|
md += `> ⛔ \`child_process\`, \`vm\`, \`os\`, \`cluster\`, \`worker_threads\` 모듈은 보안상 차단됩니다.\n\n`;
|
|
559
559
|
md += `### CORS 설정\n\n`;
|
|
560
560
|
md += `- \`*.{BASE_DOMAIN}\` 서브도메인 간 요청은 **자동 허용**됩니다.\n`;
|
|
561
|
-
md += `-
|
|
561
|
+
md += `- 프론트엔드가 Gencow 바깥(Vercel, Netlify 등)에서 호스팅될 때만 추가 설정이 필요합니다.\n`;
|
|
562
|
+
md += `- 커스텀 도메인(\`gencow domain set\`)은 same-origin 이므로 CORS 설정이 필요 없습니다.\n\n`;
|
|
562
563
|
md += `\`\`\`bash\n`;
|
|
563
|
-
md += `gencow
|
|
564
|
+
md += `gencow cors add https://myapp.com\n`;
|
|
565
|
+
md += `gencow cors add https://myapp.vercel.app\n`;
|
|
566
|
+
md += `gencow cors list\n`;
|
|
564
567
|
md += `\`\`\`\n\n`;
|
|
565
568
|
|
|
566
569
|
// 해싱/암호화 대안
|