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 +269 -79
- package/dashboard/assets/{index-RH5HoiTX.js → index-CLmqVsl3.js} +57 -57
- package/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/server/index.js +25595 -5101
- package/server/index.js.map +4 -4
- package/templates/ai-chat/prompt.md +2 -0
- package/templates/fullstack/prompt.md +2 -0
- package/templates/task-app/prompt.md +2 -0
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
|
|
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\`로
|
|
598
|
-
md += `- .env 파일은 로컬 개발 전용이야.
|
|
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\` |
|
|
650
|
-
md += `| \`gencow env set KEY=VALUE\` | 환경변수 추가/수정 |\n`;
|
|
651
|
-
md += `| \`gencow env unset KEY\` | 환경변수 삭제 |\n`;
|
|
652
|
-
md += `| \`gencow env push\` |
|
|
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 파일은 로컬 전용.
|
|
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
|
|
1773
|
-
${GREEN}env set K=V${RESET} Set
|
|
1774
|
-
${GREEN}env unset KEY${RESET} Remove
|
|
1775
|
-
${GREEN}env push${RESET} Push
|
|
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
|
-
|
|
3241
|
-
const
|
|
3242
|
-
const
|
|
3243
|
-
const
|
|
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
|
-
// ──
|
|
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
|
|
3249
|
-
|
|
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.
|
|
3259
|
-
log(` ${DIM}${"NAME".padEnd(maxLen)}
|
|
3260
|
-
log(` ${DIM}${"─".repeat(maxLen)} ${"─".repeat(
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
3320
|
-
|
|
3321
|
-
|
|
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
|
|
3521
|
+
log(`\n ${DIM}⚠ Restart app to apply changes${RESET}\n`);
|
|
3327
3522
|
return;
|
|
3328
3523
|
}
|
|
3329
3524
|
|
|
3330
|
-
// ── env push — .env to
|
|
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
|
|
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)
|
|
3542
|
+
if (key && val) vars[key] = val;
|
|
3352
3543
|
}
|
|
3353
3544
|
|
|
3354
|
-
|
|
3355
|
-
|
|
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}(${
|
|
3548
|
+
log(`\n${BOLD}Pushing .env${RESET} ${DIM}(cloud: ${appId}, ${count} variables)${RESET}\n`);
|
|
3360
3549
|
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
});
|
|
3370
|
-
|
|
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(
|
|
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
|
|
3388
|
-
log(` gencow env set K=V
|
|
3389
|
-
log(` gencow env unset KEY
|
|
3390
|
-
log(` gencow env push
|
|
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() {
|