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 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.45.1",
865
- "drizzle-kit": "^0.31.4",
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
- // ID 결정
3441
- let appId = null;
3442
- let envTarget = "dev";
3443
-
3444
- for (let i = 0; i < restArgs.length; i++) {
3445
- if (restArgs[i] === "--app" || restArgs[i] === "-a") appId = restArgs[++i];
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
- if (!appId) {
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
- // gencow.json에서 appId 읽기
5335
- const gencowJsonPath = resolve(process.cwd(), "gencow.json");
5336
- let appId = null;
5337
- if (existsSync(gencowJsonPath)) {
5338
- try {
5339
- const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
5340
- appId = gencowJson.appId || gencowJson.appName;
5341
- } catch {
5342
- /* ignore parse error */
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 = filteredArgs.slice(1);
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 = filteredArgs.slice(1);
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(creds, `/platform/apps/${appId}/env/${encodeURIComponent(key)}`, {
5448
- method: "DELETE",
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("No .env file found in current directory");
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("CORS_ORIGINS");
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
+ }
@@ -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 += `- 커스텀 도메인에서 API를 호출하려면 환경변수를 설정하세요:\n\n`;
561
+ md += `- 프론트엔드가 Gencow 바깥(Vercel, Netlify 등)에서 호스팅될 때만 추가 설정이 필요합니다.\n`;
562
+ md += `- 커스텀 도메인(\`gencow domain set\`)은 same-origin 이므로 CORS 설정이 필요 없습니다.\n\n`;
562
563
  md += `\`\`\`bash\n`;
563
- md += `gencow env set CORS_ORIGINS=https://myapp.com,https://www.myapp.com # 클라우드에 설정\n`;
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
  // 해싱/암호화 대안
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.132",
3
+ "version": "0.1.133",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {