gencow 0.1.73 → 0.1.75

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
@@ -426,7 +426,8 @@ function generateReadmeMd(config, apiObj) {
426
426
 
427
427
  let md = `# Gencow API Guide\n`;
428
428
  md += `> ⚡ Auto-generated by Gencow CLI — do not edit manually.\n`;
429
- md += `> Last updated: ${now}\n\n`;
429
+ md += `> Last updated: ${now}\n`;
430
+ md += `> 📚 Gencow 공식 문서 (전체 참조): https://docs.gencow.com/llms-full.txt\n\n`;
430
431
  md += `---\n\n`;
431
432
 
432
433
  // ── 1. API Reference Table ─────────────────────────────
@@ -594,8 +595,9 @@ function generateReadmeMd(config, apiObj) {
594
595
  md += `- 백엔드: \`npx gencow deploy\` (gencow/ 폴더만 배포됨. 프론트엔드는 포함 안 됨)\n`;
595
596
  md += `- 프론트엔드: VITE_API_URL=https://{앱ID}.gencow.app npm run build 후 \`npx gencow deploy --static dist/\`\n`;
596
597
  md += `- gencow deploy는 프론트엔드를 포함하지 않아. 반드시 별도로 --static 배포해.\n`;
597
- md += `- 환경변수는 \`npx gencow env set KEY=VALUE\`로 관리해.\n`;
598
- md += `- .env 파일은 로컬 개발 전용이야. 서버에는 gencow env push로 올려.\n`;
598
+ md += `- 환경변수는 \`npx gencow env set KEY=VALUE\`로 클라우드에 설정해.\n`;
599
+ md += `- .env 파일은 로컬 개발 전용이야. 클라우드에는 gencow env push로 올려.\n`;
600
+ md += `- 로컬 dev 서버에 설정하려면 \`gencow env set --local KEY=VALUE\`를 써.\n`;
599
601
  md += `\`\`\`\n\n`;
600
602
 
601
603
  // ── 4. AI 사용법 ────────────────────────────────────
@@ -646,10 +648,11 @@ function generateReadmeMd(config, apiObj) {
646
648
  md += `### 환경변수 관리\n\n`;
647
649
  md += `| 명령어 | 설명 |\n`;
648
650
  md += `| :--- | :--- |\n`;
649
- md += `| \`gencow env list\` | 현재 환경변수 목록 |\n`;
650
- md += `| \`gencow env set KEY=VALUE\` | 환경변수 추가/수정 |\n`;
651
- md += `| \`gencow env unset KEY\` | 환경변수 삭제 |\n`;
652
- md += `| \`gencow env push\` | 로컬 .env를 서버에 일괄 업로드 |\n\n`;
651
+ md += `| \`gencow env list\` | 클라우드 환경변수 목록 |\n`;
652
+ md += `| \`gencow env set KEY=VALUE\` | 클라우드 환경변수 추가/수정 |\n`;
653
+ md += `| \`gencow env unset KEY\` | 클라우드 환경변수 삭제 |\n`;
654
+ md += `| \`gencow env push\` | .env를 클라우드에 일괄 업로드 |\n`;
655
+ md += `| \`--local\` 옵션 | 로컬 dev 서버 대상 (예: \`env set --local K=V\`) |\n\n`;
653
656
 
654
657
  // ── 6. Cron Jobs ──────────────────────────────────────
655
658
  md += `---\n\n## ⏰ Cron Jobs (예약 작업)\n\n`;
@@ -708,7 +711,7 @@ function generateReadmeMd(config, apiObj) {
708
711
  md += `- MCP 서버를 사용하면 AI가 이 구조를 자동으로 인식합니다.\n`;
709
712
  md += `- 로컬 개발: \`gencow dev\` → \`http://localhost:5456\`\n`;
710
713
  md += `- Self-fetch: \`process.env.GENCOW_INTERNAL_URL\` — cron/mutation에서 다른 함수 호출 시 사용\n`;
711
- md += `- .env 파일은 로컬 전용. 서버에는 \`gencow env push\`로 올리세요.\n`;
714
+ md += `- .env 파일은 로컬 전용. 클라우드에는 \`gencow env push\`로 올리세요. (로컬 서버는 \`--local\`)\n`;
712
715
 
713
716
  // ── 5. 기존 컴포넌트 섹션 보존 (gencow add로 추가된 부분) ──
714
717
  const COMP_START = "<!-- gencow-components-start -->";
@@ -1769,10 +1772,10 @@ ${BOLD}BaaS commands (login required):${RESET}
1769
1772
  ${DIM}--prod Deploy to production (confirmation required)${RESET}
1770
1773
  ${DIM}--name, -n Specify app name${RESET}
1771
1774
  ${DIM}--static [dir] Deploy static files (dist/, out/, build/)${RESET}
1772
- ${GREEN}env list${RESET} List remote env vars ${DIM}(--prod for production)${RESET}
1773
- ${GREEN}env set K=V${RESET} Set remote env var
1774
- ${GREEN}env unset KEY${RESET} Remove remote env var
1775
- ${GREEN}env push${RESET} Push local .env to remote
1775
+ ${GREEN}env list${RESET} List cloud env vars ${DIM}(--local for local dev server)${RESET}
1776
+ ${GREEN}env set K=V${RESET} Set cloud env var ${DIM}(--local for local)${RESET}
1777
+ ${GREEN}env unset KEY${RESET} Remove cloud env var ${DIM}(--local for local)${RESET}
1778
+ ${GREEN}env push${RESET} Push .env to cloud ${DIM}(--local for local)${RESET}
1776
1779
  ${GREEN}domain set${RESET} Connect custom domain ${DIM}(myapp.com)${RESET}
1777
1780
  ${GREEN}domain status${RESET} Check domain DNS/TLS status
1778
1781
  ${GREEN}domain remove${RESET} Disconnect custom domain
@@ -1946,10 +1949,12 @@ ${BOLD}Examples:${RESET}
1946
1949
  let envTarget = "dev";
1947
1950
  let staticDir = null; // --static 옵션
1948
1951
  let isStatic = false;
1952
+ let forceDeploy = false; // --force: skip dependency audit
1949
1953
 
1950
1954
  for (let i = 0; i < deployArgs.length; i++) {
1951
1955
  const a = deployArgs[i];
1952
1956
  if (a === "--prod") envTarget = "prod";
1957
+ else if (a === "--force" || a === "-f") forceDeploy = true;
1953
1958
  else if (a === "--app" || a === "-a") appId = deployArgs[++i];
1954
1959
  else if (a === "--static") {
1955
1960
  isStatic = true;
@@ -2025,6 +2030,23 @@ ${BOLD}Examples:${RESET}
2025
2030
  process.exit(1);
2026
2031
  }
2027
2032
 
2033
+ // 1-0. Pre-deploy dependency audit
2034
+ if (!forceDeploy) {
2035
+ try {
2036
+ const { auditDeployDependencies, formatAuditError } = await import("../lib/deploy-auditor.mjs");
2037
+ const entryPoint = resolve(process.cwd(), "gencow", "index.ts");
2038
+ if (existsSync(entryPoint)) {
2039
+ const auditResult = await auditDeployDependencies(entryPoint);
2040
+ if (!auditResult.passed) {
2041
+ log(formatAuditError(auditResult));
2042
+ process.exit(1);
2043
+ }
2044
+ }
2045
+ } catch (auditErr) {
2046
+ warn(`의존성 검사 스킵: ${auditErr.message}`);
2047
+ }
2048
+ }
2049
+
2028
2050
  try {
2029
2051
  exec(`tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
2030
2052
  } catch (e) {
@@ -2127,6 +2149,11 @@ ${BOLD}Examples:${RESET}
2127
2149
  if (!retryRes.ok) {
2128
2150
  const retryErr = await retryRes.json().catch(() => ({}));
2129
2151
  error(`배포 실패: ${retryErr.error || retryRes.statusText}`);
2152
+ if (retryErr.crashLogs?.length) {
2153
+ log("");
2154
+ warn("서버 시작 실패 원인:");
2155
+ for (const line of retryErr.crashLogs) log(` ${DIM}${line}${RESET}`);
2156
+ }
2130
2157
  process.exit(1);
2131
2158
  }
2132
2159
 
@@ -2142,6 +2169,11 @@ ${BOLD}Examples:${RESET}
2142
2169
  }
2143
2170
 
2144
2171
  error(`배포 실패: ${errData.error || deployRes.statusText}`);
2172
+ if (errData.crashLogs?.length) {
2173
+ log("");
2174
+ warn("서버 시작 실패 원인:");
2175
+ for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
2176
+ }
2145
2177
  process.exit(1);
2146
2178
  }
2147
2179
 
@@ -3235,108 +3267,268 @@ process.exit(0);
3235
3267
  });
3236
3268
  },
3237
3269
 
3238
- // ── env — 환경변수 관리 ────────────────────────────────
3270
+ // ── env — 환경변수 관리 (Cloud-First + --local) ─────────
3239
3271
  async env(...envArgs) {
3240
- const subcmd = envArgs[0] || "list";
3241
- const config = loadConfig();
3242
- const port = process.env.PORT || config.port || 5456;
3243
- const base = `http://localhost:${port}/_admin/settings/env-vars`;
3272
+ // ── --local 플래그 파싱 ──
3273
+ const isLocal = envArgs.includes("--local");
3274
+ const filteredArgs = envArgs.filter(a => a !== "--local");
3275
+ const subcmd = filteredArgs[0] || "list";
3276
+
3277
+ // ── 로컬 모드: 기존 localhost 로직 ──
3278
+ if (isLocal) {
3279
+ const config = loadConfig();
3280
+ const port = process.env.PORT || config.port || 5456;
3281
+ const base = `http://localhost:${port}/_admin/settings/env-vars`;
3282
+
3283
+ // ── local env list ──
3284
+ if (subcmd === "list") {
3285
+ try {
3286
+ const res = await fetch(base);
3287
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
3288
+ const vars = await res.json();
3289
+
3290
+ log(`\n${BOLD}Environment Variables${RESET} ${DIM}(local, ${vars.length} total)${RESET}\n`);
3291
+
3292
+ if (vars.length === 0) {
3293
+ info("No environment variables configured.");
3294
+ info(`Use ${GREEN}gencow env set --local KEY=VALUE${RESET} to add one.`);
3295
+ } else {
3296
+ const maxLen = Math.max(...vars.map(v => v.name.length), 4);
3297
+ log(` ${DIM}${"NAME".padEnd(maxLen)} VALUE${RESET}`);
3298
+ log(` ${DIM}${"─".repeat(maxLen)} ${"─".repeat(20)}${RESET}`);
3299
+ for (const v of vars) {
3300
+ log(` ${BOLD}${v.name.padEnd(maxLen)}${RESET} ${DIM}${v.maskedValue}${RESET}`);
3301
+ }
3302
+ }
3303
+ log("");
3304
+ } catch (e) {
3305
+ error(`Failed to fetch env vars: ${e.message}`);
3306
+ info(`Make sure the local dev server is running (gencow dev --local)`);
3307
+ }
3308
+ return;
3309
+ }
3310
+
3311
+ // ── local env set KEY=VALUE ──
3312
+ if (subcmd === "set") {
3313
+ const pairs = filteredArgs.slice(1);
3314
+ if (pairs.length === 0) {
3315
+ error("Usage: gencow env set --local KEY=VALUE [KEY2=VALUE2 ...]");
3316
+ return;
3317
+ }
3318
+ for (const pair of pairs) {
3319
+ const eqIdx = pair.indexOf("=");
3320
+ if (eqIdx < 1) { error(`Invalid format: ${pair}. Use KEY=VALUE`); continue; }
3321
+ const name = pair.slice(0, eqIdx).trim();
3322
+ const value = pair.slice(eqIdx + 1).trim();
3323
+ if (!name) { error("Key name cannot be empty"); continue; }
3324
+ if (!value) { error(`Value for ${name} cannot be empty`); continue; }
3325
+
3326
+ try {
3327
+ const res = await fetch(base, {
3328
+ method: "POST",
3329
+ headers: { "Content-Type": "application/json" },
3330
+ body: JSON.stringify({ name, value }),
3331
+ });
3332
+ if (!res.ok) {
3333
+ const body = await res.json().catch(() => ({}));
3334
+ throw new Error(body.error || `HTTP ${res.status}`);
3335
+ }
3336
+ success(`Set ${BOLD}${name}${RESET} ${DIM}(local)${RESET}`);
3337
+ } catch (e) {
3338
+ error(`Failed to set ${name}: ${e.message}`);
3339
+ }
3340
+ }
3341
+ log(`\n ${DIM}⚠ Restart local server to apply changes${RESET}\n`);
3342
+ return;
3343
+ }
3344
+
3345
+ // ── local env unset KEY ──
3346
+ if (subcmd === "unset" || subcmd === "remove" || subcmd === "delete") {
3347
+ const keys = filteredArgs.slice(1);
3348
+ if (keys.length === 0) { error("Usage: gencow env unset --local KEY [KEY2 ...]"); return; }
3349
+ for (const key of keys) {
3350
+ try {
3351
+ const res = await fetch(`${base}/${encodeURIComponent(key)}`, { method: "DELETE" });
3352
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
3353
+ success(`Removed ${BOLD}${key}${RESET} ${DIM}(local)${RESET}`);
3354
+ } catch (e) {
3355
+ error(`Failed to remove ${key}: ${e.message}`);
3356
+ }
3357
+ }
3358
+ log(`\n ${DIM}⚠ Restart local server to apply changes${RESET}\n`);
3359
+ return;
3360
+ }
3361
+
3362
+ // ── local env push ──
3363
+ if (subcmd === "push") {
3364
+ const envPath = resolve(process.cwd(), ".env");
3365
+ if (!existsSync(envPath)) { error("No .env file found in current directory"); return; }
3366
+
3367
+ const content = readFileSync(envPath, "utf-8");
3368
+ const pairs = [];
3369
+ for (const line of content.split("\n")) {
3370
+ const trimmed = line.trim();
3371
+ if (!trimmed || trimmed.startsWith("#")) continue;
3372
+ const eqIdx = trimmed.indexOf("=");
3373
+ if (eqIdx < 1) continue;
3374
+ const key = trimmed.slice(0, eqIdx).trim();
3375
+ let val = trimmed.slice(eqIdx + 1).trim();
3376
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
3377
+ val = val.slice(1, -1);
3378
+ }
3379
+ if (key && val) pairs.push({ key, val });
3380
+ }
3381
+
3382
+ if (pairs.length === 0) { warn("No valid KEY=VALUE pairs found in .env"); return; }
3383
+
3384
+ log(`\n${BOLD}Pushing .env${RESET} ${DIM}(local, ${pairs.length} variables)${RESET}\n`);
3385
+
3386
+ let ok = 0, fail = 0;
3387
+ for (const { key, val } of pairs) {
3388
+ try {
3389
+ const res = await fetch(base, {
3390
+ method: "POST",
3391
+ headers: { "Content-Type": "application/json" },
3392
+ body: JSON.stringify({ name: key, value: val }),
3393
+ });
3394
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
3395
+ success(`${key}`);
3396
+ ok++;
3397
+ } catch {
3398
+ error(`${key}`);
3399
+ fail++;
3400
+ }
3401
+ }
3402
+
3403
+ log(`\n ${GREEN}${ok} set${RESET}${fail > 0 ? `, ${RED}${fail} failed${RESET}` : ""}`);
3404
+ log(` ${DIM}⚠ Restart local server to apply changes${RESET}\n`);
3405
+ return;
3406
+ }
3407
+
3408
+ error(`Unknown env subcommand: ${subcmd}`);
3409
+ log(`\n ${BOLD}Usage (local):${RESET}`);
3410
+ log(` gencow env list --local List local env vars`);
3411
+ log(` gencow env set --local K=V Set local variable(s)`);
3412
+ log(` gencow env unset --local KEY Remove local variable(s)`);
3413
+ log(` gencow env push --local Push .env to local server\n`);
3414
+ return;
3415
+ }
3244
3416
 
3245
- // ── env list ──
3417
+ // ── 클라우드 모드 (기본) ──
3418
+ // gencow.json에서 appId 읽기
3419
+ const gencowJsonPath = resolve(process.cwd(), "gencow.json");
3420
+ let appId = null;
3421
+ if (existsSync(gencowJsonPath)) {
3422
+ try {
3423
+ const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
3424
+ appId = gencowJson.appId || gencowJson.appName;
3425
+ } catch { /* ignore parse error */ }
3426
+ }
3427
+
3428
+ if (!appId) {
3429
+ error("No gencow.json found. Cannot determine cloud app.");
3430
+ info(`Run ${GREEN}gencow deploy${RESET} first to create a cloud app.`);
3431
+ info(`Or use ${GREEN}gencow env set --local KEY=VALUE${RESET} for local dev server.`);
3432
+ return;
3433
+ }
3434
+
3435
+ const creds = requireCreds();
3436
+
3437
+ // ── cloud env list ──
3246
3438
  if (subcmd === "list") {
3247
3439
  try {
3248
- const res = await fetch(base);
3249
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
3440
+ const res = await platformFetch(creds, `/platform/apps/${appId}/env`, {
3441
+ method: "GET",
3442
+ });
3443
+ if (!res.ok) {
3444
+ const body = await res.json().catch(() => ({}));
3445
+ throw new Error(body.error || `HTTP ${res.status}`);
3446
+ }
3250
3447
  const vars = await res.json();
3251
3448
 
3252
- log(`\n${BOLD}Environment Variables${RESET} ${DIM}(${vars.length} total)${RESET}\n`);
3449
+ log(`\n${BOLD}Environment Variables${RESET} ${DIM}(cloud: ${appId}, ${vars.length} total)${RESET}\n`);
3253
3450
 
3254
3451
  if (vars.length === 0) {
3255
3452
  info("No environment variables configured.");
3256
3453
  info(`Use ${GREEN}gencow env set KEY=VALUE${RESET} to add one.`);
3257
3454
  } else {
3258
- const maxLen = Math.max(...vars.map(v => v.name.length), 4);
3259
- log(` ${DIM}${"NAME".padEnd(maxLen)} VALUE${RESET}`);
3260
- log(` ${DIM}${"─".repeat(maxLen)} ${"─".repeat(20)}${RESET}`);
3455
+ const maxLen = Math.max(...vars.map(v => v.key.length), 4);
3456
+ log(` ${DIM}${"NAME".padEnd(maxLen)} ENV${RESET}`);
3457
+ log(` ${DIM}${"─".repeat(maxLen)} ${"─".repeat(6)}${RESET}`);
3261
3458
  for (const v of vars) {
3262
- log(` ${BOLD}${v.name.padEnd(maxLen)}${RESET} ${DIM}${v.maskedValue}${RESET}`);
3459
+ log(` ${BOLD}${v.key.padEnd(maxLen)}${RESET} ${DIM}${v.env}${RESET}`);
3263
3460
  }
3264
3461
  }
3265
3462
  log("");
3266
3463
  } catch (e) {
3267
3464
  error(`Failed to fetch env vars: ${e.message}`);
3268
- info(`Make sure the server is running (gencow dev)`);
3269
3465
  }
3270
3466
  return;
3271
3467
  }
3272
3468
 
3273
- // ── env set KEY=VALUE ──
3469
+ // ── cloud env set KEY=VALUE ──
3274
3470
  if (subcmd === "set") {
3275
- const pairs = envArgs.slice(1);
3471
+ const pairs = filteredArgs.slice(1);
3276
3472
  if (pairs.length === 0) {
3277
3473
  error("Usage: gencow env set KEY=VALUE [KEY2=VALUE2 ...]");
3278
3474
  return;
3279
3475
  }
3280
3476
  for (const pair of pairs) {
3281
3477
  const eqIdx = pair.indexOf("=");
3282
- if (eqIdx < 1) {
3283
- error(`Invalid format: ${pair}. Use KEY=VALUE`);
3284
- continue;
3285
- }
3478
+ if (eqIdx < 1) { error(`Invalid format: ${pair}. Use KEY=VALUE`); continue; }
3286
3479
  const name = pair.slice(0, eqIdx).trim();
3287
3480
  const value = pair.slice(eqIdx + 1).trim();
3288
3481
  if (!name) { error("Key name cannot be empty"); continue; }
3289
3482
  if (!value) { error(`Value for ${name} cannot be empty`); continue; }
3290
3483
 
3291
3484
  try {
3292
- const res = await fetch(base, {
3485
+ const res = await platformFetch(creds, `/platform/apps/${appId}/env`, {
3293
3486
  method: "POST",
3294
3487
  headers: { "Content-Type": "application/json" },
3295
- body: JSON.stringify({ name, value }),
3488
+ body: JSON.stringify({ key: name, value }),
3296
3489
  });
3297
3490
  if (!res.ok) {
3298
3491
  const body = await res.json().catch(() => ({}));
3299
3492
  throw new Error(body.error || `HTTP ${res.status}`);
3300
3493
  }
3301
- success(`Set ${BOLD}${name}${RESET}`);
3494
+ success(`Set ${BOLD}${name}${RESET} ${DIM}(cloud: ${appId})${RESET}`);
3302
3495
  } catch (e) {
3303
3496
  error(`Failed to set ${name}: ${e.message}`);
3304
3497
  }
3305
3498
  }
3306
- log(`\n ${DIM}⚠ Restart server to apply changes${RESET}\n`);
3499
+ log(`\n ${DIM}⚠ Restart app to apply changes${RESET}\n`);
3307
3500
  return;
3308
3501
  }
3309
3502
 
3310
- // ── env unset KEY ──
3503
+ // ── cloud env unset KEY ──
3311
3504
  if (subcmd === "unset" || subcmd === "remove" || subcmd === "delete") {
3312
- const keys = envArgs.slice(1);
3313
- if (keys.length === 0) {
3314
- error("Usage: gencow env unset KEY [KEY2 ...]");
3315
- return;
3316
- }
3505
+ const keys = filteredArgs.slice(1);
3506
+ if (keys.length === 0) { error("Usage: gencow env unset KEY [KEY2 ...]"); return; }
3317
3507
  for (const key of keys) {
3318
3508
  try {
3319
- const res = await fetch(`${base}/${encodeURIComponent(key)}`, { method: "DELETE" });
3320
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
3321
- success(`Removed ${BOLD}${key}${RESET}`);
3509
+ const res = await platformFetch(creds, `/platform/apps/${appId}/env/${encodeURIComponent(key)}`, {
3510
+ method: "DELETE",
3511
+ });
3512
+ if (!res.ok) {
3513
+ const body = await res.json().catch(() => ({}));
3514
+ throw new Error(body.error || `HTTP ${res.status}`);
3515
+ }
3516
+ success(`Removed ${BOLD}${key}${RESET} ${DIM}(cloud: ${appId})${RESET}`);
3322
3517
  } catch (e) {
3323
3518
  error(`Failed to remove ${key}: ${e.message}`);
3324
3519
  }
3325
3520
  }
3326
- log(`\n ${DIM}⚠ Restart server to apply changes${RESET}\n`);
3521
+ log(`\n ${DIM}⚠ Restart app to apply changes${RESET}\n`);
3327
3522
  return;
3328
3523
  }
3329
3524
 
3330
- // ── env push — .env to server ──
3525
+ // ── cloud env push — .env to cloud ──
3331
3526
  if (subcmd === "push") {
3332
3527
  const envPath = resolve(process.cwd(), ".env");
3333
- if (!existsSync(envPath)) {
3334
- error("No .env file found in current directory");
3335
- return;
3336
- }
3528
+ if (!existsSync(envPath)) { error("No .env file found in current directory"); return; }
3337
3529
 
3338
3530
  const content = readFileSync(envPath, "utf-8");
3339
- const pairs = [];
3531
+ const vars = {};
3340
3532
  for (const line of content.split("\n")) {
3341
3533
  const trimmed = line.trim();
3342
3534
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -3344,50 +3536,48 @@ process.exit(0);
3344
3536
  if (eqIdx < 1) continue;
3345
3537
  const key = trimmed.slice(0, eqIdx).trim();
3346
3538
  let val = trimmed.slice(eqIdx + 1).trim();
3347
- // Remove surrounding quotes
3348
3539
  if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
3349
3540
  val = val.slice(1, -1);
3350
3541
  }
3351
- if (key && val) pairs.push({ key, val });
3542
+ if (key && val) vars[key] = val;
3352
3543
  }
3353
3544
 
3354
- if (pairs.length === 0) {
3355
- warn("No valid KEY=VALUE pairs found in .env");
3356
- return;
3357
- }
3545
+ const count = Object.keys(vars).length;
3546
+ if (count === 0) { warn("No valid KEY=VALUE pairs found in .env"); return; }
3358
3547
 
3359
- log(`\n${BOLD}Pushing .env${RESET} ${DIM}(${pairs.length} variables)${RESET}\n`);
3548
+ log(`\n${BOLD}Pushing .env${RESET} ${DIM}(cloud: ${appId}, ${count} variables)${RESET}\n`);
3360
3549
 
3361
- let ok = 0;
3362
- let fail = 0;
3363
- for (const { key, val } of pairs) {
3364
- try {
3365
- const res = await fetch(base, {
3366
- method: "POST",
3367
- headers: { "Content-Type": "application/json" },
3368
- body: JSON.stringify({ name: key, value: val }),
3369
- });
3370
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
3550
+ try {
3551
+ const res = await platformFetch(creds, `/platform/apps/${appId}/env/bulk`, {
3552
+ method: "PUT",
3553
+ headers: { "Content-Type": "application/json" },
3554
+ body: JSON.stringify({ vars }),
3555
+ });
3556
+ if (!res.ok) {
3557
+ const body = await res.json().catch(() => ({}));
3558
+ throw new Error(body.error || `HTTP ${res.status}`);
3559
+ }
3560
+ const data = await res.json();
3561
+ for (const key of Object.keys(vars)) {
3371
3562
  success(`${key}`);
3372
- ok++;
3373
- } catch {
3374
- error(`${key}`);
3375
- fail++;
3376
3563
  }
3564
+ log(`\n ${GREEN}${data.count || count} set${RESET} ${DIM}(cloud: ${appId})${RESET}`);
3565
+ } catch (e) {
3566
+ error(`Failed to push env vars: ${e.message}`);
3377
3567
  }
3378
3568
 
3379
- log(`\n ${GREEN}${ok} set${RESET}${fail > 0 ? `, ${RED}${fail} failed${RESET}` : ""}`);
3380
- log(` ${DIM}⚠ Restart server to apply changes${RESET}\n`);
3569
+ log(` ${DIM} Restart app to apply changes${RESET}\n`);
3381
3570
  return;
3382
3571
  }
3383
3572
 
3384
3573
  // Unknown subcmd
3385
3574
  error(`Unknown env subcommand: ${subcmd}`);
3386
3575
  log(`\n ${BOLD}Usage:${RESET}`);
3387
- log(` gencow env list List environment variables`);
3388
- log(` gencow env set K=V Set variable(s)`);
3389
- log(` gencow env unset KEY Remove variable(s)`);
3390
- log(` gencow env push Push local .env to server\n`);
3576
+ log(` gencow env list List cloud env vars`);
3577
+ log(` gencow env set K=V Set cloud variable(s)`);
3578
+ log(` gencow env unset KEY Remove cloud variable(s)`);
3579
+ log(` gencow env push Push .env to cloud`);
3580
+ log(` ${DIM}Add --local for local dev server${RESET}\n`);
3391
3581
  },
3392
3582
 
3393
3583
  async platform() {