gencow 0.1.121 → 0.1.123
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 +559 -764
- package/dashboard/apple-touch-icon.png +0 -0
- package/dashboard/assets/index-D3c-3Tbj.js +372 -0
- package/dashboard/assets/index-treFoOsZ.css +1 -0
- 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 +1 -0
- package/dashboard/file.svg +1 -0
- package/dashboard/globe.svg +1 -0
- package/dashboard/index.html +23 -0
- package/dashboard/next.svg +1 -0
- package/dashboard/vercel.svg +1 -0
- package/dashboard/window.svg +1 -0
- package/package.json +32 -31
- package/server/index.js +23 -11
- package/server/index.js.map +2 -2
- package/templates/ai-chat/ai.ts +41 -0
- package/templates/ai.ts +41 -0
- package/templates/fullstack/ai.ts +41 -0
- package/templates/guardrails.ts +13 -13
- package/templates/memory.ts +29 -25
- package/templates/parsers.ts +55 -8
- package/templates/prompts.ts +13 -13
- package/templates/rag.ts +88 -56
- package/templates/reranker.ts +9 -9
- package/templates/schema-rag.ts +17 -10
package/bin/gencow.mjs
CHANGED
|
@@ -105,7 +105,7 @@ function validatePlatformUrl(url) {
|
|
|
105
105
|
// 그 외는 반드시 https://
|
|
106
106
|
if (!url.startsWith("https://")) {
|
|
107
107
|
error(`platformUrl must use https:// (got: ${url})`);
|
|
108
|
-
info("
|
|
108
|
+
info("Secrets may be transmitted in plaintext. Check GENCOW_PLATFORM_URL.");
|
|
109
109
|
process.exit(1);
|
|
110
110
|
}
|
|
111
111
|
return url;
|
|
@@ -275,7 +275,7 @@ function _ensureEsbuildConsistency() {
|
|
|
275
275
|
if (hostVersion === binaryVersion) return (_esbuildEnvCache = {});
|
|
276
276
|
|
|
277
277
|
// ── 불일치 감지 → 2단계 수정 ──
|
|
278
|
-
warn(`esbuild
|
|
278
|
+
warn(`esbuild version mismatch detected: host=${hostVersion}, binary=${binaryVersion}`);
|
|
279
279
|
|
|
280
280
|
const env = {};
|
|
281
281
|
|
|
@@ -290,7 +290,7 @@ function _ensureEsbuildConsistency() {
|
|
|
290
290
|
const binary = resolve(platformPkgDir, "bin/esbuild");
|
|
291
291
|
if (existsSync(binary)) {
|
|
292
292
|
env.ESBUILD_BINARY_PATH = binary;
|
|
293
|
-
info(`ESBUILD_BINARY_PATH → esbuild@${esbuildVer}
|
|
293
|
+
info(`ESBUILD_BINARY_PATH → using esbuild@${esbuildVer} binary`);
|
|
294
294
|
}
|
|
295
295
|
} catch { /* platform binary resolve 실패 — fallback 없이 계속 */ }
|
|
296
296
|
} catch { /* drizzle-kit esbuild resolve 실패 */ }
|
|
@@ -326,8 +326,8 @@ function _patchNpmrcForEsbuild(cwd) {
|
|
|
326
326
|
"",
|
|
327
327
|
].join("\n");
|
|
328
328
|
appendFileSync(pnpmrcPath, patch);
|
|
329
|
-
info("
|
|
330
|
-
info("
|
|
329
|
+
info("Added esbuild hoisting exclusion rule to .pnpmrc.");
|
|
330
|
+
info("Permanent fix will apply on next pnpm install.");
|
|
331
331
|
}
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
@@ -362,7 +362,7 @@ function findServerRoot() {
|
|
|
362
362
|
const siblingServer = resolve(__dirname, "../../server");
|
|
363
363
|
if (existsSync(siblingServer)) return siblingServer;
|
|
364
364
|
|
|
365
|
-
error("
|
|
365
|
+
error("Server runtime not found. Run 'bun install'.");
|
|
366
366
|
process.exit(1);
|
|
367
367
|
}
|
|
368
368
|
|
|
@@ -472,7 +472,7 @@ process.exit(0);
|
|
|
472
472
|
const stderr = execError.stderr?.toString() || "";
|
|
473
473
|
const stdout = execError.stdout?.toString() || "";
|
|
474
474
|
|
|
475
|
-
error("API Codegen failed — gencow/index.ts
|
|
475
|
+
error("API Codegen failed — cannot parse gencow/index.ts.");
|
|
476
476
|
log("");
|
|
477
477
|
|
|
478
478
|
// stderr에서 핵심 에러 줄 추출 (타입 에러, import 에러 등)
|
|
@@ -486,7 +486,7 @@ process.exit(0);
|
|
|
486
486
|
).slice(0, 5); // 최대 5줄
|
|
487
487
|
|
|
488
488
|
if (errorLines.length > 0) {
|
|
489
|
-
error("
|
|
489
|
+
error("Cause:");
|
|
490
490
|
for (const line of errorLines) {
|
|
491
491
|
info(` ${RED}${line.trim()}${RESET}`);
|
|
492
492
|
}
|
|
@@ -499,16 +499,16 @@ process.exit(0);
|
|
|
499
499
|
}
|
|
500
500
|
|
|
501
501
|
log("");
|
|
502
|
-
info(`${YELLOW}
|
|
503
|
-
info(" 1. gencow/index.ts
|
|
504
|
-
info(" 2. import
|
|
505
|
-
info(" 3. @gencow/core
|
|
502
|
+
info(`${YELLOW}How to fix:${RESET}`);
|
|
503
|
+
info(" 1. Fix TypeScript errors in gencow/index.ts");
|
|
504
|
+
info(" 2. Verify import paths are correct");
|
|
505
|
+
info(" 3. Ensure @gencow/core is installed");
|
|
506
506
|
log("");
|
|
507
507
|
|
|
508
508
|
// 빈 스텁 api.ts 생성 — IDE가 즉시 에러를 잡을 수 있도록
|
|
509
|
-
const stubContent = `/**\n * ⚠️ API Codegen Failed\n *\n * gencow/index.ts
|
|
509
|
+
const stubContent = `/**\n * ⚠️ API Codegen Failed\n *\n * An error occurred in gencow/index.ts, so api.ts could not be generated.\n * Check the console for error details, fix the issue, and restart gencow dev.\n *\n * This file is auto-generated — do not edit manually.\n */\nexport const api = {} as const;\n`;
|
|
510
510
|
writeFileSync(apiTsPath, stubContent);
|
|
511
|
-
warn(
|
|
511
|
+
warn(`Empty stub ${config.functionsDir}/api.ts created (auto-regenerated after fix)`);
|
|
512
512
|
return; // 에러를 throw하지 않고 조용히 반환 — dev 서버는 계속 실행
|
|
513
513
|
}
|
|
514
514
|
|
|
@@ -595,9 +595,9 @@ process.exit(0);
|
|
|
595
595
|
} catch (e) {
|
|
596
596
|
error(`API Codegen failed: ${e.message}`);
|
|
597
597
|
// 빈 스텁 api.ts 생성 — 예기치 않은 에러에서도 IDE가 즉시 에러를 잡을 수 있도록
|
|
598
|
-
const stubContent = `/**\n * ⚠️ API Codegen Failed: ${e.message.replace(/\*/g, "")}\n *\n *
|
|
598
|
+
const stubContent = `/**\n * ⚠️ API Codegen Failed: ${e.message.replace(/\*/g, "")}\n *\n * Fix the error and restart gencow dev.\n * This file is auto-generated — do not edit manually.\n */\nexport const api = {} as const;\n`;
|
|
599
599
|
try { writeFileSync(apiTsPath, stubContent); } catch { /* ignore */ }
|
|
600
|
-
warn(
|
|
600
|
+
warn(`Empty stub ${config.functionsDir}/api.ts created`);
|
|
601
601
|
} finally {
|
|
602
602
|
try { unlinkSync(extractTsPath); } catch { }
|
|
603
603
|
}
|
|
@@ -654,11 +654,11 @@ function updateEnvLocalUrl(deployUrl) {
|
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
writeFileSync(envPath, content);
|
|
657
|
-
success(`.env
|
|
658
|
-
info(` ${DIM}
|
|
657
|
+
success(`.env — auto-configured ${envKey}`);
|
|
658
|
+
info(` ${DIM}Also add to your deploy platform (Vercel, etc.) env vars:${RESET}`);
|
|
659
659
|
info(` ${envKey}=${deployUrl}`);
|
|
660
660
|
} catch (e) {
|
|
661
|
-
warn(`.env
|
|
661
|
+
warn(`.env update failed: ${e.message}`);
|
|
662
662
|
}
|
|
663
663
|
}
|
|
664
664
|
|
|
@@ -710,13 +710,13 @@ const commands = {
|
|
|
710
710
|
const { createInterface } = await import("readline");
|
|
711
711
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
712
712
|
name = await new Promise(resolve => {
|
|
713
|
-
rl.question(`\n ${DIM}
|
|
713
|
+
rl.question(`\n ${DIM}Enter project name (current folder: .):${RESET} `, answer => {
|
|
714
714
|
rl.close();
|
|
715
715
|
resolve(answer.trim());
|
|
716
716
|
});
|
|
717
717
|
});
|
|
718
718
|
if (!name) {
|
|
719
|
-
error("
|
|
719
|
+
error("Project name is required.");
|
|
720
720
|
process.exit(1);
|
|
721
721
|
}
|
|
722
722
|
}
|
|
@@ -733,14 +733,14 @@ const commands = {
|
|
|
733
733
|
if (!templateId) {
|
|
734
734
|
templateId = await (async () => {
|
|
735
735
|
const tpls = commands._templates;
|
|
736
|
-
log(` ${DIM}
|
|
736
|
+
log(` ${DIM}Choose a template:${RESET}\n`);
|
|
737
737
|
tpls.forEach((t, i) =>
|
|
738
738
|
log(` ${CYAN}${i + 1}.${RESET} ${t.id.padEnd(12)} ${DIM}— ${t.label}${RESET}`)
|
|
739
739
|
);
|
|
740
740
|
const { createInterface } = await import("readline");
|
|
741
741
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
742
742
|
return new Promise(resolve => {
|
|
743
|
-
rl.question(`\n
|
|
743
|
+
rl.question(`\n Enter number (1-${tpls.length}): `, answer => {
|
|
744
744
|
rl.close();
|
|
745
745
|
const idx = parseInt(answer) - 1;
|
|
746
746
|
resolve(tpls[idx]?.id || "default");
|
|
@@ -756,7 +756,7 @@ const commands = {
|
|
|
756
756
|
process.exit(1);
|
|
757
757
|
}
|
|
758
758
|
|
|
759
|
-
log(`\n ${DIM}▸
|
|
759
|
+
log(`\n ${DIM}▸ Template:${RESET} ${CYAN}${template.id}${RESET} — ${template.label}\n`);
|
|
760
760
|
|
|
761
761
|
const projectDir = isCwd ? process.cwd() : resolve(process.cwd(), name);
|
|
762
762
|
|
|
@@ -768,7 +768,7 @@ const commands = {
|
|
|
768
768
|
process.exit(1);
|
|
769
769
|
}
|
|
770
770
|
if (entries.length > 0 && force) {
|
|
771
|
-
warn(
|
|
771
|
+
warn(`Existing files preserved, only gencow files created/overwritten.`);
|
|
772
772
|
}
|
|
773
773
|
}
|
|
774
774
|
|
|
@@ -800,7 +800,7 @@ const commands = {
|
|
|
800
800
|
writeFileSync(pkgJsonPath, JSON.stringify(existing, null, 2) + "\n");
|
|
801
801
|
success("Updated package.json (dependencies merged)");
|
|
802
802
|
} catch {
|
|
803
|
-
warn("package.json
|
|
803
|
+
warn("Failed to parse package.json — creating new one.");
|
|
804
804
|
writeFileSync(pkgJsonPath, JSON.stringify({
|
|
805
805
|
name,
|
|
806
806
|
version: "0.1.0",
|
|
@@ -847,7 +847,7 @@ const commands = {
|
|
|
847
847
|
if (file === "prompt.md") {
|
|
848
848
|
// prompt.md goes to project root
|
|
849
849
|
cpSync(src, resolve(projectDir, "prompt.md"));
|
|
850
|
-
success("Created prompt.md (
|
|
850
|
+
success("Created prompt.md (vibe coding prompt)");
|
|
851
851
|
} else {
|
|
852
852
|
// Everything else goes to gencow/
|
|
853
853
|
cpSync(src, resolve(gencowDir, file));
|
|
@@ -866,7 +866,7 @@ const commands = {
|
|
|
866
866
|
const securityMdSource = resolve(__dirname, "..", "templates", "SECURITY.md");
|
|
867
867
|
if (existsSync(securityMdSource)) {
|
|
868
868
|
cpSync(securityMdSource, resolve(gencowDir, "SECURITY.md"));
|
|
869
|
-
success("Created gencow/SECURITY.md (
|
|
869
|
+
success("Created gencow/SECURITY.md (security guide)");
|
|
870
870
|
}
|
|
871
871
|
|
|
872
872
|
// 3.7. crons.ts scaffold (크론 스케줄러 — 빈 템플릿)
|
|
@@ -899,7 +899,7 @@ const crons = cronJobs();
|
|
|
899
899
|
|
|
900
900
|
export default crons;
|
|
901
901
|
`);
|
|
902
|
-
success("Created gencow/crons.ts (
|
|
902
|
+
success("Created gencow/crons.ts (cron scheduler template)");
|
|
903
903
|
}
|
|
904
904
|
|
|
905
905
|
// 3.7. auth.ts (auth 설정 — defineAuth 기반, shadcn 패턴으로 사용자 소유)
|
|
@@ -943,7 +943,7 @@ export default defineConfig({
|
|
|
943
943
|
// 5. .env (preserve existing)
|
|
944
944
|
const envPath = resolve(projectDir, ".env");
|
|
945
945
|
if (existsSync(envPath) && force) {
|
|
946
|
-
info(".env
|
|
946
|
+
info(".env already exists — skipping.");
|
|
947
947
|
} else {
|
|
948
948
|
const envContent = `# Gencow Environment Variables\n# Add your environment variables here\n\n# AI 기능 사용 시 필수 (ctx.ai.chat, ai.chat 등)\n# https://platform.openai.com/api-keys 에서 발급\nOPENAI_API_KEY=sk-your-key-here\n`;
|
|
949
949
|
writeFileSync(envPath, envContent);
|
|
@@ -959,9 +959,9 @@ export default defineConfig({
|
|
|
959
959
|
if (linesToAdd.length > 0) {
|
|
960
960
|
const append = (existing.endsWith("\n") ? "" : "\n") + "# Gencow\n" + linesToAdd.join("\n") + "\n";
|
|
961
961
|
writeFileSync(gitignorePath, existing + append);
|
|
962
|
-
success("Updated .gitignore (gencow
|
|
962
|
+
success("Updated .gitignore (added gencow entries)");
|
|
963
963
|
} else {
|
|
964
|
-
info(".gitignore
|
|
964
|
+
info(".gitignore already has gencow entries — skipping.");
|
|
965
965
|
}
|
|
966
966
|
} else {
|
|
967
967
|
writeFileSync(gitignorePath, gencowGitignoreLines.join("\n") + "\n");
|
|
@@ -971,7 +971,7 @@ export default defineConfig({
|
|
|
971
971
|
// 7. tsconfig.json (preserve existing)
|
|
972
972
|
const tsconfigPath = resolve(projectDir, "tsconfig.json");
|
|
973
973
|
if (existsSync(tsconfigPath) && force) {
|
|
974
|
-
info("tsconfig.json
|
|
974
|
+
info("tsconfig.json already exists — skipping.");
|
|
975
975
|
} else {
|
|
976
976
|
writeFileSync(tsconfigPath, JSON.stringify({
|
|
977
977
|
compilerOptions: {
|
|
@@ -1017,12 +1017,12 @@ export default defineConfig({
|
|
|
1017
1017
|
success("Linked @gencow/core + dependencies");
|
|
1018
1018
|
|
|
1019
1019
|
// 9. Auto install npm dependencies (drizzle-orm, ai, etc.)
|
|
1020
|
-
info("
|
|
1020
|
+
info("Installing dependencies...");
|
|
1021
1021
|
try {
|
|
1022
1022
|
execSync("bun install", { cwd: projectDir, stdio: ["ignore", "pipe", "pipe"] });
|
|
1023
|
-
success("
|
|
1023
|
+
success("Dependencies installed");
|
|
1024
1024
|
} catch {
|
|
1025
|
-
warn("
|
|
1025
|
+
warn("Package install failed — " + (isCwd ? "bun install" : "cd " + name + " && bun install") + " to install manually");
|
|
1026
1026
|
}
|
|
1027
1027
|
|
|
1028
1028
|
// Build next steps message based on template
|
|
@@ -1030,107 +1030,28 @@ export default defineConfig({
|
|
|
1030
1030
|
const hasPrompt = template.id !== "default";
|
|
1031
1031
|
|
|
1032
1032
|
log(`
|
|
1033
|
-
${BOLD}${GREEN} ✅
|
|
1033
|
+
${BOLD}${GREEN} ✅ Project created!${RESET} ${DIM}(${template.id})${RESET}
|
|
1034
1034
|
|
|
1035
|
-
${DIM}
|
|
1035
|
+
${DIM}Next steps:${RESET}
|
|
1036
1036
|
${isCwd ? "" : `
|
|
1037
1037
|
${CYAN}cd ${name}${RESET}`}
|
|
1038
|
-
${CYAN}gencow dev${RESET} ${DIM}←
|
|
1038
|
+
${CYAN}gencow dev${RESET} ${DIM}← Start dev server${RESET}
|
|
1039
1039
|
${hasPrompt ? `
|
|
1040
|
-
${DIM}
|
|
1041
|
-
${CYAN}prompt.md${RESET} ${DIM}
|
|
1040
|
+
${DIM}Frontend vibe coding:${RESET}
|
|
1041
|
+
${CYAN}prompt.md${RESET} ${DIM}Feed this file to your AI (Cursor, Copilot, etc.)${RESET}
|
|
1042
1042
|
` : `
|
|
1043
|
-
${DIM}
|
|
1044
|
-
${CYAN}gencow add AI${RESET} ${DIM}← AI
|
|
1045
|
-
${CYAN}gencow add RAG${RESET} ${DIM}← RAG
|
|
1046
|
-
${CYAN}gencow add Memory${RESET} ${DIM}← Agent Memory
|
|
1043
|
+
${DIM}Add more:${RESET}
|
|
1044
|
+
${CYAN}gencow add AI${RESET} ${DIM}← Add AI component${RESET}
|
|
1045
|
+
${CYAN}gencow add RAG${RESET} ${DIM}← Add RAG component${RESET}
|
|
1046
|
+
${CYAN}gencow add Memory${RESET} ${DIM}← Add Agent Memory${RESET}
|
|
1047
1047
|
`}
|
|
1048
1048
|
`);
|
|
1049
1049
|
},
|
|
1050
1050
|
|
|
1051
1051
|
// ── db:reset ─────────────────────────────────────────
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
async "db:reset"(...args) {
|
|
1056
|
-
const config = loadConfig();
|
|
1057
|
-
const cwd = process.cwd();
|
|
1058
|
-
const isLocal = args.includes("--local");
|
|
1059
|
-
|
|
1060
|
-
if (!isLocal) {
|
|
1061
|
-
// Cloud reset은 미지원 — 안내 메시지
|
|
1062
|
-
error("db:reset은 --local 플래그가 필요합니다.");
|
|
1063
|
-
info(`${DIM}Cloud DB 초기화는 Dashboard에서만 가능합니다 (데이터 보호).${RESET}`);
|
|
1064
|
-
info(`${DIM}로컬 DB를 초기화하려면: gencow db:reset --local${RESET}`);
|
|
1065
|
-
log("");
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
const port = process.env.PORT || config.port || 5456;
|
|
1070
|
-
|
|
1071
|
-
log(`\n${BOLD}${CYAN}Gencow DB Reset${RESET}\n`);
|
|
1072
|
-
|
|
1073
|
-
// 서버가 실행 중인지 확인
|
|
1074
|
-
let serverAlive = false;
|
|
1075
|
-
try {
|
|
1076
|
-
const res = await fetch(`http://localhost:${port}/_admin/status`, { signal: AbortSignal.timeout(2000) });
|
|
1077
|
-
serverAlive = res.ok;
|
|
1078
|
-
} catch { /* 서버 꺼져있음 */ }
|
|
1079
|
-
|
|
1080
|
-
if (serverAlive) {
|
|
1081
|
-
// ── 서버 실행 중: API 기반 TRUNCATE + seed ──
|
|
1082
|
-
info(`Server detected on :${port} — using API reset (TRUNCATE + seed)`);
|
|
1083
|
-
log("");
|
|
1084
|
-
|
|
1085
|
-
try {
|
|
1086
|
-
const res = await fetch(`http://localhost:${port}/_admin/reset`, {
|
|
1087
|
-
method: "POST",
|
|
1088
|
-
headers: { "Content-Type": "application/json" },
|
|
1089
|
-
body: JSON.stringify({ confirm: "delete all data" }),
|
|
1090
|
-
});
|
|
1091
|
-
const data = await res.json();
|
|
1092
|
-
|
|
1093
|
-
if (!res.ok) {
|
|
1094
|
-
error(`Reset failed: ${data.error || "Unknown error"}`);
|
|
1095
|
-
log("");
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
success(`TRUNCATE 완료: ${data.truncated?.length || 0} tables`);
|
|
1100
|
-
if (data.truncated?.length > 0) {
|
|
1101
|
-
info(` ${DIM}${data.truncated.join(", ")}${RESET}`);
|
|
1102
|
-
}
|
|
1103
|
-
if (data.seeded) {
|
|
1104
|
-
success(`Seed 실행 완료 ✓`);
|
|
1105
|
-
} else {
|
|
1106
|
-
info(`${DIM}seed.ts 없음 — 건너뜀${RESET}`);
|
|
1107
|
-
}
|
|
1108
|
-
} catch (e) {
|
|
1109
|
-
error(`Server connection failed: ${e.message}`);
|
|
1110
|
-
}
|
|
1111
|
-
} else {
|
|
1112
|
-
// ── 서버 꺼짐: 기존 PGlite 파일 삭제 ──
|
|
1113
|
-
const dbPath = resolve(cwd, config.db.url);
|
|
1114
|
-
if (existsSync(dbPath)) {
|
|
1115
|
-
// 1. Backup first
|
|
1116
|
-
const backupDir = resolve(cwd, ".gencow", "backups");
|
|
1117
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1118
|
-
const backupPath = resolve(backupDir, ts);
|
|
1119
|
-
mkdirSync(backupPath, { recursive: true });
|
|
1120
|
-
cpSync(dbPath, backupPath, { recursive: true });
|
|
1121
|
-
success(`Backup created: .gencow/backups/${ts}`);
|
|
1122
|
-
|
|
1123
|
-
// 2. Cleanup old backups (keep last 3)
|
|
1124
|
-
cleanupOldBackups(backupDir, 3);
|
|
1125
|
-
|
|
1126
|
-
// 3. Delete DB
|
|
1127
|
-
rmSync(dbPath, { recursive: true, force: true });
|
|
1128
|
-
success("DB deleted — gencow dev 로 재시작하면 새로 생성됩니다");
|
|
1129
|
-
} else {
|
|
1130
|
-
info("DB가 아직 없습니다. gencow dev 로 시작하세요.");
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
log("");
|
|
1052
|
+
async "db:reset"() {
|
|
1053
|
+
info(`db:reset is ${BOLD}coming soon${RESET}.`);
|
|
1054
|
+
info(`${DIM}Use Gencow Dashboard for cloud DB management.${RESET}\n`);
|
|
1134
1055
|
},
|
|
1135
1056
|
|
|
1136
1057
|
// ── db:seed ──────────────────────────────────────────
|
|
@@ -1141,51 +1062,13 @@ ${hasPrompt ? `
|
|
|
1141
1062
|
const isLocal = args.includes("--local");
|
|
1142
1063
|
|
|
1143
1064
|
if (isLocal) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(local)${RESET}\n`);
|
|
1148
|
-
|
|
1149
|
-
// 서버가 실행 중인지 확인
|
|
1150
|
-
try {
|
|
1151
|
-
const statusRes = await fetch(`http://localhost:${port}/_admin/status`, { signal: AbortSignal.timeout(2000) });
|
|
1152
|
-
if (!statusRes.ok) throw new Error("Server not ready");
|
|
1153
|
-
} catch {
|
|
1154
|
-
error(`Server not running on :${port}`);
|
|
1155
|
-
info(`${DIM}gencow dev --local 로 서버를 먼저 시작하세요.${RESET}`);
|
|
1156
|
-
log("");
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
try {
|
|
1161
|
-
const res = await fetch(`http://localhost:${port}/_admin/seed`, {
|
|
1162
|
-
method: "POST",
|
|
1163
|
-
headers: { "Content-Type": "application/json" },
|
|
1164
|
-
});
|
|
1165
|
-
const data = await res.json();
|
|
1166
|
-
|
|
1167
|
-
if (res.status === 404) {
|
|
1168
|
-
warn("gencow/seed.ts not found");
|
|
1169
|
-
info(`\n ${DIM}Create gencow/seed.ts:${RESET}\n`);
|
|
1170
|
-
log(` ${DIM}import { tasks } from "./schema";${RESET}`);
|
|
1171
|
-
log(` ${DIM}export default async function seed(ctx) {${RESET}`);
|
|
1172
|
-
log(` ${DIM} await ctx.db.insert(tasks).values([${RESET}`);
|
|
1173
|
-
log(` ${DIM} { title: "Hello World" },${RESET}`);
|
|
1174
|
-
log(` ${DIM} ]);${RESET}`);
|
|
1175
|
-
log(` ${DIM}};${RESET}`);
|
|
1176
|
-
} else if (!res.ok) {
|
|
1177
|
-
error(`Seed failed: ${data.error || "Unknown error"}`);
|
|
1178
|
-
} else {
|
|
1179
|
-
success("Seed 실행 완료 ✓");
|
|
1180
|
-
}
|
|
1181
|
-
} catch (e) {
|
|
1182
|
-
error(`Server connection failed: ${e.message}`);
|
|
1183
|
-
}
|
|
1184
|
-
log("");
|
|
1065
|
+
info(`Local db:seed is ${BOLD}coming soon${RESET}.`);
|
|
1066
|
+
info(`${DIM}Use cloud mode (default): gencow db:seed${RESET}\n`);
|
|
1185
1067
|
return;
|
|
1186
1068
|
}
|
|
1187
1069
|
|
|
1188
1070
|
// ── Cloud 모드 (기본) ──
|
|
1071
|
+
const isProd = args.includes("--prod");
|
|
1189
1072
|
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
1190
1073
|
let appId = null;
|
|
1191
1074
|
let seedPlatformUrl = null;
|
|
@@ -1194,18 +1077,27 @@ ${hasPrompt ? `
|
|
|
1194
1077
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
1195
1078
|
appId = gencowJson.appId || gencowJson.appName;
|
|
1196
1079
|
seedPlatformUrl = gencowJson.platformUrl || null;
|
|
1080
|
+
// --prod: use prod app
|
|
1081
|
+
if (isProd && gencowJson.prodApp) {
|
|
1082
|
+
appId = gencowJson.prodApp;
|
|
1083
|
+
}
|
|
1197
1084
|
} catch { /* ignore parse error */ }
|
|
1198
1085
|
}
|
|
1199
1086
|
|
|
1087
|
+
if (isProd && appId && !appId.endsWith("-prod")) {
|
|
1088
|
+
error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1200
1092
|
if (!appId) {
|
|
1201
|
-
error("Cloud app not found
|
|
1202
|
-
info(`${DIM}
|
|
1203
|
-
info(`${DIM}로컬 서버에 seed하려면: gencow db:seed --local${RESET}`);
|
|
1093
|
+
error("Cloud app not found. Make sure gencow.json exists with an appId.");
|
|
1094
|
+
info(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
|
|
1204
1095
|
log("");
|
|
1205
1096
|
return;
|
|
1206
1097
|
}
|
|
1207
1098
|
|
|
1208
|
-
|
|
1099
|
+
const envLabel = isProd ? "prod" : "dev";
|
|
1100
|
+
log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(${envLabel}: ${appId})${RESET}\n`);
|
|
1209
1101
|
info("Running seed.ts on cloud app...");
|
|
1210
1102
|
|
|
1211
1103
|
const seedAppBaseUrl = getAppUrl(appId, seedPlatformUrl || "https://gencow.app");
|
|
@@ -1219,8 +1111,8 @@ ${hasPrompt ? `
|
|
|
1219
1111
|
|
|
1220
1112
|
if (!statusRes || !statusRes.ok) {
|
|
1221
1113
|
error(`Cloud app "${appId}" is not running`);
|
|
1222
|
-
info(`${DIM}Dashboard → Apps → ${appId} → Resume
|
|
1223
|
-
info(`${DIM}
|
|
1114
|
+
info(`${DIM}Dashboard → Apps → ${appId} → Resume to start the app.${RESET}`);
|
|
1115
|
+
info(`${DIM}Or: run gencow dev to create and deploy.${RESET}`);
|
|
1224
1116
|
log("");
|
|
1225
1117
|
return;
|
|
1226
1118
|
}
|
|
@@ -1238,15 +1130,15 @@ ${hasPrompt ? `
|
|
|
1238
1130
|
log(` ${DIM}export default async function seed(ctx) {${RESET}`);
|
|
1239
1131
|
log(` ${DIM} await ctx.db.insert(tasks).values([...]);${RESET}`);
|
|
1240
1132
|
log(` ${DIM}};${RESET}\n`);
|
|
1241
|
-
info(`${DIM}After creating seed.ts: gencow dev
|
|
1133
|
+
info(`${DIM}After creating seed.ts: run gencow dev then gencow db:seed${RESET}`);
|
|
1242
1134
|
} else if (!res.ok) {
|
|
1243
1135
|
error(`Cloud seed failed: ${data.error || "Unknown error"}`);
|
|
1244
1136
|
} else {
|
|
1245
|
-
success("Cloud seed
|
|
1137
|
+
success("Cloud seed completed ✓");
|
|
1246
1138
|
}
|
|
1247
1139
|
} catch (e) {
|
|
1248
1140
|
error(`Cloud app connection failed: ${e.message}`);
|
|
1249
|
-
info(`${DIM}
|
|
1141
|
+
info(`${DIM}Check if the app is running: ${seedAppBaseUrl}${RESET}`);
|
|
1250
1142
|
}
|
|
1251
1143
|
log("");
|
|
1252
1144
|
},
|
|
@@ -1261,7 +1153,7 @@ ${hasPrompt ? `
|
|
|
1261
1153
|
log(`\n${BOLD}${CYAN}Gencow DB Restore${RESET}\n`);
|
|
1262
1154
|
|
|
1263
1155
|
if (!existsSync(backupDir)) {
|
|
1264
|
-
warn("
|
|
1156
|
+
warn("No backups found.");
|
|
1265
1157
|
log("");
|
|
1266
1158
|
return;
|
|
1267
1159
|
}
|
|
@@ -1274,7 +1166,7 @@ ${hasPrompt ? `
|
|
|
1274
1166
|
.reverse();
|
|
1275
1167
|
|
|
1276
1168
|
if (backups.length === 0) {
|
|
1277
|
-
warn("
|
|
1169
|
+
warn("No backups found.");
|
|
1278
1170
|
log("");
|
|
1279
1171
|
return;
|
|
1280
1172
|
}
|
|
@@ -1295,7 +1187,7 @@ ${hasPrompt ? `
|
|
|
1295
1187
|
}
|
|
1296
1188
|
cpSync(latestPath, dbPath, { recursive: true });
|
|
1297
1189
|
success(`Restored from: .gencow/backups/${latest}`);
|
|
1298
|
-
info("gencow dev
|
|
1190
|
+
info("Restart with gencow dev.");
|
|
1299
1191
|
log("");
|
|
1300
1192
|
},
|
|
1301
1193
|
|
|
@@ -1306,9 +1198,12 @@ ${hasPrompt ? `
|
|
|
1306
1198
|
if (devArgs.includes("--verbose")) {
|
|
1307
1199
|
process.env.GENCOW_VERBOSE = "true";
|
|
1308
1200
|
}
|
|
1309
|
-
// --local 플래그:
|
|
1201
|
+
// --local 플래그: coming soon
|
|
1310
1202
|
if (devArgs.includes("--local")) {
|
|
1311
|
-
|
|
1203
|
+
log(`\n${BOLD}${CYAN}Gencow Dev${RESET} ${DIM}(local)${RESET}\n`);
|
|
1204
|
+
info(`Local development mode is ${BOLD}coming soon${RESET}.`);
|
|
1205
|
+
info(`${DIM}Use cloud mode (default): gencow dev${RESET}\n`);
|
|
1206
|
+
return;
|
|
1312
1207
|
}
|
|
1313
1208
|
// --cloud 하위 호환 (이미 기본이므로 그대로 진행)
|
|
1314
1209
|
// 기본 = 클라우드 모드
|
|
@@ -1530,43 +1425,58 @@ ${hasPrompt ? `
|
|
|
1530
1425
|
const isLocal = args.includes("--local");
|
|
1531
1426
|
|
|
1532
1427
|
if (isLocal) {
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
if (!hasDb) {
|
|
1536
|
-
const envPath = resolve(process.cwd(), ".env");
|
|
1537
|
-
if (existsSync(envPath)) hasDb = readFileSync(envPath, "utf8").includes("DATABASE_URL=");
|
|
1538
|
-
}
|
|
1539
|
-
const targetDb = hasDb ? "local PG" : "local fallback: PGlite";
|
|
1540
|
-
log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(${targetDb})${RESET}\n`);
|
|
1541
|
-
info("Pushing schema.ts → database (no migration files)...");
|
|
1542
|
-
runInServer("pnpm db:push --force", buildEnv(config));
|
|
1543
|
-
success("Schema pushed!");
|
|
1544
|
-
log(` ${DIM}Tables are in sync with schema.ts${RESET}\n`);
|
|
1428
|
+
info(`Local db:push is ${BOLD}coming soon${RESET}.`);
|
|
1429
|
+
info(`${DIM}Use cloud mode (default): gencow db:push${RESET}\n`);
|
|
1545
1430
|
return;
|
|
1546
1431
|
}
|
|
1547
1432
|
|
|
1548
|
-
// ── Cloud
|
|
1433
|
+
// ── Cloud mode (default) ──
|
|
1434
|
+
const isProd = args.includes("--prod");
|
|
1549
1435
|
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
1550
1436
|
let appId = null;
|
|
1551
1437
|
if (existsSync(gencowJsonPath)) {
|
|
1552
1438
|
try {
|
|
1553
1439
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
1554
1440
|
appId = gencowJson.appId || gencowJson.appName;
|
|
1441
|
+
// --prod: use prod app
|
|
1442
|
+
if (isProd && gencowJson.prodApp) {
|
|
1443
|
+
appId = gencowJson.prodApp;
|
|
1444
|
+
}
|
|
1555
1445
|
} catch { /* ignore parse error */ }
|
|
1556
1446
|
}
|
|
1557
1447
|
|
|
1448
|
+
if (isProd && appId && !appId.endsWith("-prod")) {
|
|
1449
|
+
error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1558
1453
|
if (!appId) {
|
|
1559
|
-
error("Cloud app not found
|
|
1560
|
-
info(`${DIM}
|
|
1561
|
-
info(`${DIM}
|
|
1454
|
+
error("Cloud app not found. Make sure gencow.json exists with an appId.");
|
|
1455
|
+
info(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
|
|
1456
|
+
info(`${DIM}To push to local DB: gencow db:push --local${RESET}`);
|
|
1562
1457
|
log("");
|
|
1563
1458
|
return;
|
|
1564
1459
|
}
|
|
1565
1460
|
|
|
1566
|
-
// ──
|
|
1461
|
+
// ── Prod safety confirmation ──
|
|
1462
|
+
if (isProd) {
|
|
1463
|
+
const { createInterface } = await import("readline");
|
|
1464
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1465
|
+
const answer = await new Promise(res =>
|
|
1466
|
+
rl.question(`\n ${RED}!${RESET} Push schema to ${BOLD}PRODUCTION${RESET} database (${appId})?\n Type "yes" to confirm: `, res)
|
|
1467
|
+
);
|
|
1468
|
+
rl.close();
|
|
1469
|
+
if (answer.trim().toLowerCase() !== "yes") {
|
|
1470
|
+
warn("Cancelled.");
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// ── Cloud mode: push schema via Platform API ──
|
|
1567
1476
|
const creds = requireCreds();
|
|
1568
|
-
|
|
1569
|
-
|
|
1477
|
+
const envLabel = isProd ? "prod" : "dev";
|
|
1478
|
+
log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(${envLabel}: ${appId})${RESET}\n`);
|
|
1479
|
+
warn("Make sure the latest code is deployed before running this command.");
|
|
1570
1480
|
log("");
|
|
1571
1481
|
info("Pushing schema.ts → cloud database...");
|
|
1572
1482
|
|
|
@@ -1586,7 +1496,7 @@ ${hasPrompt ? `
|
|
|
1586
1496
|
}
|
|
1587
1497
|
|
|
1588
1498
|
// 🆕 로컬 drizzle-kit generate 실행 (프롬프트 패스스루)
|
|
1589
|
-
info("
|
|
1499
|
+
info("Generating schema migrations...");
|
|
1590
1500
|
try {
|
|
1591
1501
|
const dk = _drizzleKitCmd("generate");
|
|
1592
1502
|
execSync(dk.cmd, {
|
|
@@ -1594,14 +1504,14 @@ ${hasPrompt ? `
|
|
|
1594
1504
|
env: { ...process.env, ...dk.env },
|
|
1595
1505
|
stdio: "inherit",
|
|
1596
1506
|
});
|
|
1597
|
-
success("
|
|
1507
|
+
success("Migrations generated");
|
|
1598
1508
|
} catch (e) {
|
|
1599
1509
|
const msg = e.stderr?.toString() || e.message || "";
|
|
1600
1510
|
if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
|
|
1601
|
-
log(`${DIM}
|
|
1511
|
+
log(`${DIM} ✓ Migrations up-to-date — no new schema changes detected${RESET}`);
|
|
1602
1512
|
} else {
|
|
1603
|
-
warn(
|
|
1604
|
-
info("
|
|
1513
|
+
warn(`Migration generation failed: ${msg.split("\n")[0]}`);
|
|
1514
|
+
info("Will attempt schema push via server.");
|
|
1605
1515
|
}
|
|
1606
1516
|
}
|
|
1607
1517
|
|
|
@@ -1638,8 +1548,8 @@ ${hasPrompt ? `
|
|
|
1638
1548
|
success("Schema pushed to cloud DB!");
|
|
1639
1549
|
log(` ${DIM}Tables are in sync with schema.ts${RESET}\n`);
|
|
1640
1550
|
} catch (e) {
|
|
1641
|
-
error(`Platform
|
|
1642
|
-
info("
|
|
1551
|
+
error(`Platform connection failed: ${e.message}`);
|
|
1552
|
+
info("Check if the server is running.");
|
|
1643
1553
|
process.exit(1);
|
|
1644
1554
|
} finally {
|
|
1645
1555
|
// 임시 번들 정리
|
|
@@ -1679,98 +1589,16 @@ ${hasPrompt ? `
|
|
|
1679
1589
|
|
|
1680
1590
|
// ── db:studio ────────────────────────────────────────
|
|
1681
1591
|
async "db:studio"() {
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
info("Opening Drizzle Studio...");
|
|
1685
|
-
runInServer("pnpm db:studio", buildEnv(config));
|
|
1592
|
+
info(`Drizzle Studio is ${BOLD}coming soon${RESET}.`);
|
|
1593
|
+
info(`${DIM}Use Gencow Dashboard at https://gencow.app instead.${RESET}\n`);
|
|
1686
1594
|
},
|
|
1687
1595
|
|
|
1688
1596
|
// ── dashboard ────────────────────────────────────────
|
|
1689
1597
|
async dashboard() {
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
const url = `http://localhost:${port}/_admin`;
|
|
1693
|
-
log(`\n${BOLD}${CYAN}Gencow Dashboard${RESET}\n`);
|
|
1694
|
-
info(`Opening ${url}`);
|
|
1695
|
-
const { default: open } = await import("open");
|
|
1696
|
-
await open(url);
|
|
1598
|
+
info(`Local dashboard is ${BOLD}coming soon${RESET}.`);
|
|
1599
|
+
info(`${DIM}Use Gencow Dashboard at https://gencow.app${RESET}\n`);
|
|
1697
1600
|
},
|
|
1698
1601
|
|
|
1699
|
-
// ── deploy ───────────────────────────────────────────
|
|
1700
|
-
async deploy(subcmd) {
|
|
1701
|
-
const config = loadConfig();
|
|
1702
|
-
const cwd = process.cwd();
|
|
1703
|
-
|
|
1704
|
-
// docker-compose file location
|
|
1705
|
-
const composeFile = resolve(cwd, "docker-compose.prod.yml");
|
|
1706
|
-
if (!existsSync(composeFile)) {
|
|
1707
|
-
error("docker-compose.prod.yml not found — run from project root");
|
|
1708
|
-
process.exit(1);
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
// subcommands: stop, logs, status
|
|
1712
|
-
if (subcmd === "stop") {
|
|
1713
|
-
log(`\n${BOLD}${CYAN}Gencow Deploy — Stop${RESET}\n`);
|
|
1714
|
-
info("Stopping production containers...");
|
|
1715
|
-
execSync(`docker compose -f ${composeFile} down`, { cwd, stdio: "inherit" });
|
|
1716
|
-
success("Stopped");
|
|
1717
|
-
return;
|
|
1718
|
-
}
|
|
1719
|
-
if (subcmd === "logs") {
|
|
1720
|
-
execSync(`docker compose -f ${composeFile} logs -f`, { cwd, stdio: "inherit" });
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
if (subcmd === "status") {
|
|
1724
|
-
execSync(`docker compose -f ${composeFile} ps`, { cwd, stdio: "inherit" });
|
|
1725
|
-
return;
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
// Default: build + up
|
|
1729
|
-
log(`\n${BOLD}${CYAN}Gencow Deploy${RESET} ${DIM}— Production${RESET}\n`);
|
|
1730
|
-
info(`Functions: ${DIM}${config.functionsDir}${RESET}`);
|
|
1731
|
-
info(`DB: ${DIM}${config.db.url}${RESET}`);
|
|
1732
|
-
info(`Storage: ${DIM}${config.storage}${RESET}`);
|
|
1733
|
-
log("");
|
|
1734
|
-
|
|
1735
|
-
// 1. Generate migrations so they're bundled in the image
|
|
1736
|
-
info("Generating migrations from schema.ts...");
|
|
1737
|
-
try {
|
|
1738
|
-
runInServer("pnpm db:generate", buildEnv(config));
|
|
1739
|
-
success("Migrations generated");
|
|
1740
|
-
} catch {
|
|
1741
|
-
warn("db:generate failed — using existing migrations");
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// 2. Check Docker is available
|
|
1745
|
-
try {
|
|
1746
|
-
execSync("docker info", { stdio: "ignore" });
|
|
1747
|
-
} catch {
|
|
1748
|
-
error("Docker is not running — start Docker Desktop and try again");
|
|
1749
|
-
process.exit(1);
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
// 3. Build Docker image
|
|
1753
|
-
log("");
|
|
1754
|
-
info("Building Docker image...");
|
|
1755
|
-
execSync(`docker compose -f ${composeFile} build`, { cwd, stdio: "inherit" });
|
|
1756
|
-
success("Image built");
|
|
1757
|
-
|
|
1758
|
-
// 4. Start services
|
|
1759
|
-
log("");
|
|
1760
|
-
info("Starting production services...");
|
|
1761
|
-
execSync(`docker compose -f ${composeFile} up -d`, { cwd, stdio: "inherit" });
|
|
1762
|
-
log("");
|
|
1763
|
-
success("Deployment complete!");
|
|
1764
|
-
log(`
|
|
1765
|
-
${BOLD}Services:${RESET}
|
|
1766
|
-
${GREEN}▸${RESET} Server: http://localhost:${config.port}
|
|
1767
|
-
${GREEN}▸${RESET} Dashboard: http://localhost:3000/_admin
|
|
1768
|
-
|
|
1769
|
-
${DIM}gencow deploy logs — follow logs${RESET}
|
|
1770
|
-
${DIM}gencow deploy status — container status${RESET}
|
|
1771
|
-
${DIM}gencow deploy stop — stop all services${RESET}
|
|
1772
|
-
`);
|
|
1773
|
-
},
|
|
1774
1602
|
|
|
1775
1603
|
// ── help ─────────────────────────────────────────────
|
|
1776
1604
|
help() {
|
|
@@ -1784,34 +1612,25 @@ ${BOLD}Quick Start:${RESET}
|
|
|
1784
1612
|
${GREEN}init <name>${RESET} Create a new Gencow project
|
|
1785
1613
|
${DIM}--template, -t Select template (default, task-app, fullstack, ai-chat)${RESET}
|
|
1786
1614
|
${DIM}--force, -f Initialize in non-empty directory (preserves existing files)${RESET}
|
|
1787
|
-
|
|
1788
|
-
${
|
|
1789
|
-
${GREEN}dev${RESET} Watch + auto-deploy to cloud + live logs ${DIM}(default)${RESET}
|
|
1790
|
-
${DIM}--local Use local PGlite instead of cloud${RESET}
|
|
1791
|
-
${DIM}--verbose Show all HTTP logs (including admin/ws)${RESET}
|
|
1792
|
-
${GREEN}dashboard${RESET} Open the Gencow admin dashboard in browser
|
|
1793
|
-
${GREEN}db:push${RESET} Sync schema.ts → cloud DB ${DIM}(default: cloud)${RESET}
|
|
1794
|
-
${DIM}--local Push to local DB instead of cloud${RESET}
|
|
1795
|
-
${GREEN}db:generate${RESET} Generate SQL migration files from schema.ts
|
|
1796
|
-
${GREEN}db:seed${RESET} Run gencow/seed.ts on cloud app ${DIM}(default: cloud)${RESET}
|
|
1797
|
-
${DIM}--local Seed local DB instead of cloud${RESET}
|
|
1798
|
-
${GREEN}db:reset${RESET} TRUNCATE all data + run seed.ts ${DIM}(local only)${RESET}
|
|
1799
|
-
${DIM}--local Required — local DB reset only (cloud: use Dashboard)${RESET}
|
|
1800
|
-
${GREEN}db:studio${RESET} Open Drizzle Studio ${DIM}(visual DB browser)${RESET}
|
|
1801
|
-
${GREEN}add <comp...>${RESET} Add components ${DIM}(AI RAG Tools Memory Analytics)${RESET}
|
|
1802
|
-
${GREEN}mcp${RESET} Start MCP server for AI agent integration ${DIM}(stdio)${RESET}
|
|
1803
|
-
${GREEN}codegen${RESET} Generate frontend-independent api.ts
|
|
1615
|
+
${GREEN}add <comp...>${RESET} Add components ${DIM}(AI RAG Tools Memory)${RESET}
|
|
1616
|
+
${GREEN}codegen${RESET} Generate frontend api.ts from schema
|
|
1804
1617
|
${DIM}--outdir, -o Output directory (default: src/gencow/)${RESET}
|
|
1805
1618
|
|
|
1806
|
-
${BOLD}
|
|
1619
|
+
${BOLD}Commands (login required):${RESET}
|
|
1807
1620
|
${GREEN}login${RESET} Login to Gencow Platform ${DIM}(browser → token)${RESET}
|
|
1808
1621
|
${GREEN}logout${RESET} Clear credentials
|
|
1809
1622
|
${GREEN}whoami${RESET} Show current user info
|
|
1810
|
-
${GREEN}
|
|
1811
|
-
${DIM}--
|
|
1623
|
+
${GREEN}dev${RESET} Watch + auto-deploy to cloud + live logs
|
|
1624
|
+
${DIM}--verbose Show all HTTP logs (including admin/ws)${RESET}
|
|
1625
|
+
${GREEN}db:push${RESET} Sync schema.ts → cloud DB
|
|
1626
|
+
${DIM}--prod Push to production DB (confirmation required)${RESET}
|
|
1627
|
+
${GREEN}db:generate${RESET} Generate SQL migration files from schema.ts
|
|
1628
|
+
${GREEN}db:seed${RESET} Run gencow/seed.ts on cloud app
|
|
1629
|
+
${DIM}--prod Seed production app${RESET}
|
|
1630
|
+
${GREEN}static [dir]${RESET} Deploy static files ${DIM}(dist/, out/, build/)${RESET}
|
|
1631
|
+
${DIM}--prod Deploy to production app${RESET}
|
|
1812
1632
|
${DIM}--force, -f Skip dependency audit${RESET}
|
|
1813
|
-
${GREEN}deploy${RESET} Deploy to production ${DIM}(Pro+ only)${RESET}
|
|
1814
|
-
${DIM}--static [dir] Deploy static files to production${RESET}
|
|
1633
|
+
${GREEN}deploy${RESET} Deploy backend to production ${DIM}(Pro+ only)${RESET}
|
|
1815
1634
|
${DIM}--rollback Rollback to previous deployment${RESET}
|
|
1816
1635
|
${DIM}--force, -f Skip dependency audit${RESET}
|
|
1817
1636
|
${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
|
|
@@ -1822,16 +1641,22 @@ ${BOLD}BaaS commands (login required):${RESET}
|
|
|
1822
1641
|
${GREEN}files list${RESET} List uploaded files
|
|
1823
1642
|
${GREEN}files delete${RESET} Delete uploaded file ${DIM}(--yes to skip confirm)${RESET}
|
|
1824
1643
|
${GREEN}files url${RESET} Get serving URL for a file
|
|
1825
|
-
${GREEN}config set${RESET} Set
|
|
1826
|
-
|
|
1827
|
-
${GREEN}config
|
|
1644
|
+
${GREEN}config set${RESET} Set image config ${DIM}(config set image.maxWidth 1920)${RESET}
|
|
1645
|
+
${DIM}Keys: image.maxWidth (0-10000), image.quality (0-100)${RESET}
|
|
1646
|
+
${GREEN}config get${RESET} Show current image config
|
|
1647
|
+
${GREEN}config reset${RESET} Reset image config to tier defaults
|
|
1828
1648
|
${GREEN}domain set${RESET} Connect custom domain ${DIM}(myapp.com)${RESET}
|
|
1829
1649
|
${GREEN}domain status${RESET} Check domain DNS/TLS status
|
|
1830
1650
|
${GREEN}domain remove${RESET} Disconnect custom domain
|
|
1831
1651
|
|
|
1832
|
-
${BOLD}
|
|
1833
|
-
${GREEN}
|
|
1834
|
-
${GREEN}
|
|
1652
|
+
${BOLD}App management:${RESET}
|
|
1653
|
+
${GREEN}app list${RESET} List your apps
|
|
1654
|
+
${GREEN}app create${RESET} Create a new app
|
|
1655
|
+
${GREEN}app delete${RESET} Delete an app ${DIM}(confirmation required)${RESET}
|
|
1656
|
+
${GREEN}app status${RESET} Show app status
|
|
1657
|
+
|
|
1658
|
+
${DIM}Tip: Most commands support --prod to target the production app.${RESET}
|
|
1659
|
+
${DIM} e.g. gencow env list --prod, gencow db:seed --prod${RESET}
|
|
1835
1660
|
|
|
1836
1661
|
${BOLD}Examples:${RESET}
|
|
1837
1662
|
${DIM}# New project:${RESET}
|
|
@@ -1840,10 +1665,9 @@ ${BOLD}Examples:${RESET}
|
|
|
1840
1665
|
${DIM}# Initialize in current directory:${RESET}
|
|
1841
1666
|
gencow init . --force
|
|
1842
1667
|
|
|
1843
|
-
${DIM}#
|
|
1668
|
+
${DIM}# Deploy to production (Pro+ only):${RESET}
|
|
1844
1669
|
gencow login
|
|
1845
|
-
gencow
|
|
1846
|
-
gencow remote:deploy
|
|
1670
|
+
gencow deploy
|
|
1847
1671
|
`);
|
|
1848
1672
|
},
|
|
1849
1673
|
|
|
@@ -1855,7 +1679,7 @@ ${BOLD}Examples:${RESET}
|
|
|
1855
1679
|
info(`Platform: ${platformUrl}`);
|
|
1856
1680
|
|
|
1857
1681
|
// 1. auth-start → 인증 코드 생성
|
|
1858
|
-
info("
|
|
1682
|
+
info("Requesting auth code...");
|
|
1859
1683
|
let authData;
|
|
1860
1684
|
try {
|
|
1861
1685
|
const res = await fetch(`${platformUrl}/platform/cli/auth-start`, {
|
|
@@ -1865,16 +1689,16 @@ ${BOLD}Examples:${RESET}
|
|
|
1865
1689
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1866
1690
|
authData = await res.json();
|
|
1867
1691
|
} catch (e) {
|
|
1868
|
-
error(`Platform
|
|
1692
|
+
error(`Platform connection failed: ${e.message}`);
|
|
1869
1693
|
error(`Platform URL: ${platformUrl}`);
|
|
1870
|
-
error(
|
|
1694
|
+
error(`Check if the server is running.`);
|
|
1871
1695
|
process.exit(1);
|
|
1872
1696
|
}
|
|
1873
1697
|
|
|
1874
1698
|
log("");
|
|
1875
|
-
log(` ${BOLD}
|
|
1699
|
+
log(` ${BOLD}Auth code: ${CYAN}${authData.code}${RESET}`);
|
|
1876
1700
|
log("");
|
|
1877
|
-
info(
|
|
1701
|
+
info(`Login in your browser:`);
|
|
1878
1702
|
log(` ${CYAN}${authData.url}${RESET}`);
|
|
1879
1703
|
log("");
|
|
1880
1704
|
|
|
@@ -1885,11 +1709,11 @@ ${BOLD}Examples:${RESET}
|
|
|
1885
1709
|
process.platform === "win32" ? "start" : "xdg-open";
|
|
1886
1710
|
exec(`${openCmd} "${authData.url}"`);
|
|
1887
1711
|
} catch {
|
|
1888
|
-
warn("
|
|
1712
|
+
warn("Could not open browser automatically. Open the URL above manually.");
|
|
1889
1713
|
}
|
|
1890
1714
|
|
|
1891
1715
|
// 3. 폴링 — 2초마다 인증 완료 확인
|
|
1892
|
-
info("
|
|
1716
|
+
info("Waiting for login...");
|
|
1893
1717
|
const POLL_INTERVAL = 2000;
|
|
1894
1718
|
const MAX_POLLS = 150; // 10분 내 5분 = 150회
|
|
1895
1719
|
|
|
@@ -1914,14 +1738,14 @@ ${BOLD}Examples:${RESET}
|
|
|
1914
1738
|
});
|
|
1915
1739
|
|
|
1916
1740
|
log("");
|
|
1917
|
-
success(
|
|
1918
|
-
info(
|
|
1741
|
+
success(`Login successful!`);
|
|
1742
|
+
info(`Token saved: ${DIM}~/.gencow/credentials.json${RESET}`);
|
|
1919
1743
|
log("");
|
|
1920
1744
|
return;
|
|
1921
1745
|
}
|
|
1922
1746
|
|
|
1923
1747
|
if (data.status === "expired") {
|
|
1924
|
-
error("
|
|
1748
|
+
error("Auth code expired. Try again: gencow login");
|
|
1925
1749
|
process.exit(1);
|
|
1926
1750
|
}
|
|
1927
1751
|
|
|
@@ -1933,7 +1757,7 @@ ${BOLD}Examples:${RESET}
|
|
|
1933
1757
|
}
|
|
1934
1758
|
}
|
|
1935
1759
|
|
|
1936
|
-
error("\
|
|
1760
|
+
error("\nTimed out. Try again: gencow login");
|
|
1937
1761
|
process.exit(1);
|
|
1938
1762
|
},
|
|
1939
1763
|
|
|
@@ -1941,9 +1765,9 @@ ${BOLD}Examples:${RESET}
|
|
|
1941
1765
|
async logout() {
|
|
1942
1766
|
try {
|
|
1943
1767
|
unlinkSync(CREDS_PATH);
|
|
1944
|
-
success("
|
|
1768
|
+
success("Logged out. Credentials cleared.");
|
|
1945
1769
|
} catch {
|
|
1946
|
-
info("
|
|
1770
|
+
info("Already logged out.");
|
|
1947
1771
|
}
|
|
1948
1772
|
},
|
|
1949
1773
|
|
|
@@ -1951,8 +1775,8 @@ ${BOLD}Examples:${RESET}
|
|
|
1951
1775
|
async whoami() {
|
|
1952
1776
|
const creds = loadCreds();
|
|
1953
1777
|
if (!creds?.apiKey) {
|
|
1954
|
-
error("
|
|
1955
|
-
info("
|
|
1778
|
+
error("Not logged in.");
|
|
1779
|
+
info("Run: gencow login");
|
|
1956
1780
|
return;
|
|
1957
1781
|
}
|
|
1958
1782
|
|
|
@@ -1962,10 +1786,10 @@ ${BOLD}Examples:${RESET}
|
|
|
1962
1786
|
if (!res.ok) {
|
|
1963
1787
|
const data = await res.json().catch(() => ({}));
|
|
1964
1788
|
const msg = data.error || `HTTP ${res.status}`;
|
|
1965
|
-
error(
|
|
1789
|
+
error(`Auth failed: ${msg}`);
|
|
1966
1790
|
if (res.status === 401) {
|
|
1967
|
-
info("
|
|
1968
|
-
info("
|
|
1791
|
+
info("Token is expired or invalid.");
|
|
1792
|
+
info("Run: gencow login");
|
|
1969
1793
|
}
|
|
1970
1794
|
return;
|
|
1971
1795
|
}
|
|
@@ -1978,29 +1802,29 @@ ${BOLD}Examples:${RESET}
|
|
|
1978
1802
|
if (data.expiresAt) {
|
|
1979
1803
|
const expiresDate = new Date(data.expiresAt);
|
|
1980
1804
|
const daysLeft = Math.ceil((expiresDate.getTime() - Date.now()) / (24 * 60 * 60 * 1000));
|
|
1981
|
-
info(`Expires: ${expiresDate.toLocaleString()} (${daysLeft}
|
|
1805
|
+
info(`Expires: ${expiresDate.toLocaleString()} (${daysLeft} days left)`);
|
|
1982
1806
|
}
|
|
1983
1807
|
log("");
|
|
1984
1808
|
} catch (e) {
|
|
1985
|
-
error(
|
|
1809
|
+
error(`Connection failed: ${e.message}`);
|
|
1986
1810
|
info(`Platform URL: ${creds.platformUrl}`);
|
|
1987
|
-
info("
|
|
1811
|
+
info("Check if the server is running.");
|
|
1988
1812
|
}
|
|
1989
1813
|
},
|
|
1990
1814
|
|
|
1991
|
-
// ── static —
|
|
1815
|
+
// ── static — 정적 파일 배포 (dev 기본, --prod 로 프로덕션) ──
|
|
1992
1816
|
async static(...staticArgs) {
|
|
1993
1817
|
const creds = requireCreds();
|
|
1994
1818
|
|
|
1995
1819
|
let staticDir = null;
|
|
1996
1820
|
let forceDeploy = false;
|
|
1997
|
-
let noBackend = false;
|
|
1998
1821
|
let appId = null;
|
|
1822
|
+
let isProd = false;
|
|
1999
1823
|
|
|
2000
1824
|
for (let i = 0; i < staticArgs.length; i++) {
|
|
2001
1825
|
const a = staticArgs[i];
|
|
2002
1826
|
if (a === "--force" || a === "-f") forceDeploy = true;
|
|
2003
|
-
else if (a === "--
|
|
1827
|
+
else if (a === "--prod") isProd = true;
|
|
2004
1828
|
else if (a === "--app" || a === "-a") appId = staticArgs[++i];
|
|
2005
1829
|
else if (!a.startsWith("-")) {
|
|
2006
1830
|
staticDir = a;
|
|
@@ -2009,9 +1833,21 @@ ${BOLD}Examples:${RESET}
|
|
|
2009
1833
|
|
|
2010
1834
|
// gencow.json에서 appId 로드
|
|
2011
1835
|
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
2012
|
-
|
|
1836
|
+
let prodAppId = null;
|
|
1837
|
+
if (existsSync(gencowJsonPath)) {
|
|
2013
1838
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
2014
|
-
appId = gencowJson.appId || gencowJson.appName;
|
|
1839
|
+
if (!appId) appId = gencowJson.appId || gencowJson.appName;
|
|
1840
|
+
prodAppId = gencowJson.prodApp || null;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// --prod: prod app 대상
|
|
1844
|
+
const deployTarget = isProd ? (prodAppId || appId) : appId;
|
|
1845
|
+
const envTarget = isProd ? "prod" : "dev";
|
|
1846
|
+
|
|
1847
|
+
if (isProd && !prodAppId) {
|
|
1848
|
+
error("No production app found. Run 'gencow deploy' first to create one.");
|
|
1849
|
+
info(`${DIM}For dev static deploy, omit --prod: gencow static [dir]${RESET}\n`);
|
|
1850
|
+
return;
|
|
2015
1851
|
}
|
|
2016
1852
|
|
|
2017
1853
|
// displayName
|
|
@@ -2024,8 +1860,8 @@ ${BOLD}Examples:${RESET}
|
|
|
2024
1860
|
}
|
|
2025
1861
|
if (!displayName) displayName = basename(process.cwd());
|
|
2026
1862
|
|
|
2027
|
-
return await this._deployStatic(creds,
|
|
2028
|
-
forceDeploy, noBackend, envTarget
|
|
1863
|
+
return await this._deployStatic(creds, deployTarget, displayName, staticDir, gencowJsonPath, {
|
|
1864
|
+
forceDeploy, noBackend: true, envTarget,
|
|
2029
1865
|
});
|
|
2030
1866
|
},
|
|
2031
1867
|
|
|
@@ -2037,10 +1873,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2037
1873
|
let appId = null;
|
|
2038
1874
|
let displayName = null;
|
|
2039
1875
|
let envTarget = "prod"; // v0.1.120: deploy = prod 기본값
|
|
2040
|
-
let staticDir = null; // --static 옵션
|
|
2041
|
-
let isStatic = false;
|
|
2042
1876
|
let forceDeploy = false; // --force: skip dependency audit
|
|
2043
|
-
let noBackend = false; // --no-backend: skip backend auto-deploy in --static mode
|
|
2044
1877
|
let isRollback = false; // --rollback: rollback to previous deploy
|
|
2045
1878
|
|
|
2046
1879
|
// ── subcommand 분기 (logs / status) ───────────────────
|
|
@@ -2055,17 +1888,17 @@ ${BOLD}Examples:${RESET}
|
|
|
2055
1888
|
appId = gencowJson.appId || gencowJson.appName;
|
|
2056
1889
|
}
|
|
2057
1890
|
if (!appId) {
|
|
2058
|
-
error("
|
|
1891
|
+
error("App not found. Run from the project root with gencow.json.");
|
|
2059
1892
|
process.exit(1);
|
|
2060
1893
|
}
|
|
2061
1894
|
|
|
2062
1895
|
if (subcmd === "logs") {
|
|
2063
1896
|
const lines = deployArgs.includes("-n") ? Number(deployArgs[deployArgs.indexOf("-n") + 1]) || 100 : 100;
|
|
2064
|
-
info(`"${appId}"
|
|
1897
|
+
info(`Fetching logs for "${appId}"... (last ${lines} lines)`);
|
|
2065
1898
|
const res = await rpcQuery(creds, "apps.logs", { name: appId, lines });
|
|
2066
1899
|
if (!res.ok) {
|
|
2067
1900
|
const errData = await res.json().catch(() => ({}));
|
|
2068
|
-
error(
|
|
1901
|
+
error(`Failed to fetch logs: ${errData.error || res.statusText}`);
|
|
2069
1902
|
process.exit(1);
|
|
2070
1903
|
}
|
|
2071
1904
|
const data = await res.json();
|
|
@@ -2075,29 +1908,29 @@ ${BOLD}Examples:${RESET}
|
|
|
2075
1908
|
log(` ${DIM}${line}${RESET}`);
|
|
2076
1909
|
}
|
|
2077
1910
|
} else {
|
|
2078
|
-
info("
|
|
1911
|
+
info("No logs found.");
|
|
2079
1912
|
}
|
|
2080
1913
|
return;
|
|
2081
1914
|
}
|
|
2082
1915
|
|
|
2083
1916
|
if (subcmd === "status") {
|
|
2084
|
-
info(`"${appId}"
|
|
1917
|
+
info(`Checking "${appId}" status...`);
|
|
2085
1918
|
const res = await rpcQuery(creds, "apps.get", { name: appId });
|
|
2086
1919
|
if (!res.ok) {
|
|
2087
1920
|
const errData = await res.json().catch(() => ({}));
|
|
2088
|
-
error(
|
|
1921
|
+
error(`Failed to fetch status: ${errData.error || res.statusText}`);
|
|
2089
1922
|
process.exit(1);
|
|
2090
1923
|
}
|
|
2091
1924
|
const data = await res.json();
|
|
2092
1925
|
const app = data.result || data;
|
|
2093
1926
|
log("");
|
|
2094
|
-
log(` ${BOLD}
|
|
1927
|
+
log(` ${BOLD}App Status${RESET}`);
|
|
2095
1928
|
log(` ──────────────────────`);
|
|
2096
|
-
info(
|
|
2097
|
-
info(
|
|
1929
|
+
info(`Name: ${app.name}`);
|
|
1930
|
+
info(`Status: ${app.status === "running" ? `${GREEN}● running${RESET}` : app.status === "crashed" ? `${RED}● crashed${RESET}` : `${YELLOW}● ${app.status}${RESET}`}`);
|
|
2098
1931
|
info(`URL: ${app.url || "N/A"}`);
|
|
2099
|
-
if (app.lastDeployedAt) info(
|
|
2100
|
-
if (app.running !== undefined) info(
|
|
1932
|
+
if (app.lastDeployedAt) info(`Deployed: ${new Date(app.lastDeployedAt).toLocaleString()}`);
|
|
1933
|
+
if (app.running !== undefined) info(`Process: ${app.running ? `${GREEN}alive${RESET}` : `${RED}dead${RESET}`}`);
|
|
2101
1934
|
log("");
|
|
2102
1935
|
return;
|
|
2103
1936
|
}
|
|
@@ -2107,38 +1940,35 @@ ${BOLD}Examples:${RESET}
|
|
|
2107
1940
|
const a = deployArgs[i];
|
|
2108
1941
|
if (a === "--prod") {
|
|
2109
1942
|
// deprecated: deploy는 이미 prod 기본값
|
|
2110
|
-
warn(`${YELLOW}--prod
|
|
2111
|
-
info(`gencow deploy
|
|
1943
|
+
warn(`${YELLOW}--prod flag is no longer needed.${RESET}`);
|
|
1944
|
+
info(`gencow deploy targets production by default.`);
|
|
2112
1945
|
envTarget = "prod";
|
|
2113
1946
|
}
|
|
2114
1947
|
else if (a === "--force" || a === "-f") forceDeploy = true;
|
|
2115
|
-
else if (a === "--no-backend") noBackend = true;
|
|
2116
1948
|
else if (a === "--rollback") isRollback = true;
|
|
2117
1949
|
else if (a === "--app" || a === "-a") appId = deployArgs[++i];
|
|
2118
1950
|
else if (a === "--static") {
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
}
|
|
1951
|
+
// Deprecated: redirect to gencow static --prod
|
|
1952
|
+
warn(`${YELLOW}--static is deprecated. Use: gencow static --prod${RESET}`);
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
else if (a === "--no-backend") {
|
|
1956
|
+
// Deprecated: silently ignore
|
|
2126
1957
|
}
|
|
2127
1958
|
else if (!a.startsWith("-")) {
|
|
2128
1959
|
// 미인식 subcommand — 에러 출력
|
|
2129
|
-
error(
|
|
1960
|
+
error(`Unknown deploy argument: "${a}"`);
|
|
2130
1961
|
log("");
|
|
2131
|
-
info(
|
|
2132
|
-
info(` gencow deploy
|
|
2133
|
-
info(` gencow deploy --
|
|
2134
|
-
info(` gencow deploy
|
|
2135
|
-
info(` gencow deploy
|
|
2136
|
-
info(` gencow deploy
|
|
2137
|
-
info(` gencow deploy --force 의존성 감사 스킵`);
|
|
1962
|
+
info(`Usage: gencow deploy [options]`);
|
|
1963
|
+
info(` gencow deploy Production backend deploy (Pro+)`);
|
|
1964
|
+
info(` gencow deploy --rollback Rollback to previous version`);
|
|
1965
|
+
info(` gencow deploy logs View server logs`);
|
|
1966
|
+
info(` gencow deploy status Check app status`);
|
|
1967
|
+
info(` gencow deploy --force Skip dependency audit`);
|
|
2138
1968
|
log("");
|
|
2139
|
-
info(`💡
|
|
2140
|
-
info(` gencow
|
|
2141
|
-
info(` gencow static
|
|
1969
|
+
info(`💡 Static files:`);
|
|
1970
|
+
info(` gencow static [dir] Deploy static files (dev)`);
|
|
1971
|
+
info(` gencow static --prod Deploy static files (production)`);
|
|
2142
1972
|
process.exit(1);
|
|
2143
1973
|
}
|
|
2144
1974
|
}
|
|
@@ -2169,12 +1999,12 @@ ${BOLD}Examples:${RESET}
|
|
|
2169
1999
|
// prodApp이 있으면 prod 대상 롤백, 없으면 dev 대상 롤백
|
|
2170
2000
|
const rollbackTarget = prodAppId || appId;
|
|
2171
2001
|
if (!rollbackTarget) {
|
|
2172
|
-
error("
|
|
2002
|
+
error("App not found. Run from the project root with gencow.json.");
|
|
2173
2003
|
process.exit(1);
|
|
2174
2004
|
}
|
|
2175
2005
|
|
|
2176
2006
|
log(`\n${BOLD}${CYAN}Gencow Rollback${RESET}\n`);
|
|
2177
|
-
info(
|
|
2007
|
+
info(`App: ${rollbackTarget}${prodAppId ? " (prod)" : ""}`);
|
|
2178
2008
|
log("");
|
|
2179
2009
|
|
|
2180
2010
|
const rollbackStartTime = Date.now();
|
|
@@ -2182,7 +2012,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2182
2012
|
let spinnerIdx = 0;
|
|
2183
2013
|
const spinner = setInterval(() => {
|
|
2184
2014
|
const elapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(0);
|
|
2185
|
-
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET}
|
|
2015
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Rolling back... ${DIM}(${elapsed}s)${RESET} `);
|
|
2186
2016
|
}, 120);
|
|
2187
2017
|
|
|
2188
2018
|
const rollbackRes = await platformFetch(creds, `/platform/apps/${rollbackTarget}/rollback`, {
|
|
@@ -2195,7 +2025,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2195
2025
|
|
|
2196
2026
|
if (!rollbackRes.ok) {
|
|
2197
2027
|
const errData = await rollbackRes.json().catch(() => ({}));
|
|
2198
|
-
error(
|
|
2028
|
+
error(`Rollback failed: ${errData.error || rollbackRes.statusText}`);
|
|
2199
2029
|
process.exit(1);
|
|
2200
2030
|
}
|
|
2201
2031
|
|
|
@@ -2203,22 +2033,16 @@ ${BOLD}Examples:${RESET}
|
|
|
2203
2033
|
const rollbackElapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(1);
|
|
2204
2034
|
|
|
2205
2035
|
log("");
|
|
2206
|
-
success(`🔄
|
|
2207
|
-
info(
|
|
2208
|
-
info(
|
|
2209
|
-
info(`URL:
|
|
2036
|
+
success(`🔄 Rollback complete! (${rollbackElapsed}s)`);
|
|
2037
|
+
info(`Rolled back: #${rollbackData.rolledBackFrom} → #${rollbackData.rolledBackTo}`);
|
|
2038
|
+
info(`Bundle: ${rollbackData.bundleHash}`);
|
|
2039
|
+
info(`URL: ${rollbackData.url}`);
|
|
2210
2040
|
log("");
|
|
2211
|
-
warn(`ℹ️
|
|
2041
|
+
warn(`ℹ️ Only code was rolled back. Database was not changed.`);
|
|
2212
2042
|
log("");
|
|
2213
2043
|
return;
|
|
2214
2044
|
}
|
|
2215
2045
|
|
|
2216
|
-
// ── Static Deploy 분기 ────────────────────────────────
|
|
2217
|
-
if (isStatic) {
|
|
2218
|
-
// prod 모드면 prodApp 대상으로 정적 배포
|
|
2219
|
-
const staticTarget = (envTarget === "prod" && prodAppId) ? prodAppId : appId;
|
|
2220
|
-
return await this._deployStatic(creds, staticTarget, displayName, staticDir, gencowJsonPath, { forceDeploy, noBackend, envTarget });
|
|
2221
|
-
}
|
|
2222
2046
|
|
|
2223
2047
|
// ── Prod 앱 자동 생성 + 배포 대상 전환 (Pro+) ──────────
|
|
2224
2048
|
if (envTarget === "prod" && appId) {
|
|
@@ -2237,12 +2061,12 @@ ${BOLD}Examples:${RESET}
|
|
|
2237
2061
|
});
|
|
2238
2062
|
rl.close();
|
|
2239
2063
|
if (answer.toLowerCase() !== "y") {
|
|
2240
|
-
info("
|
|
2064
|
+
info("Deploy cancelled.");
|
|
2241
2065
|
return;
|
|
2242
2066
|
}
|
|
2243
2067
|
}
|
|
2244
2068
|
|
|
2245
|
-
info("
|
|
2069
|
+
info("Creating prod app...");
|
|
2246
2070
|
const createProdRes = await platformFetch(creds, `/platform/apps/${appId}/create-prod`, {
|
|
2247
2071
|
method: "POST",
|
|
2248
2072
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2252,19 +2076,19 @@ ${BOLD}Examples:${RESET}
|
|
|
2252
2076
|
const errData = await createProdRes.json().catch(() => ({}));
|
|
2253
2077
|
if (createProdRes.status === 403) {
|
|
2254
2078
|
log("");
|
|
2255
|
-
log(` ${RED}⛔ Production Deploy — Pro
|
|
2079
|
+
log(` ${RED}⛔ Production Deploy — Pro plan or higher required${RESET}`);
|
|
2256
2080
|
log("");
|
|
2257
|
-
log(` ${DIM}gencow deploy
|
|
2081
|
+
log(` ${DIM}gencow deploy is for production deployment.${RESET}`);
|
|
2258
2082
|
log("");
|
|
2259
|
-
log(` ${BOLD}💡
|
|
2260
|
-
log(` ${GREEN}gencow dev${RESET} ${DIM}←
|
|
2261
|
-
log(` ${GREEN}gencow static dist/${RESET} ${DIM}←
|
|
2083
|
+
log(` ${BOLD}💡 Dev environment deploy:${RESET}`);
|
|
2084
|
+
log(` ${GREEN}gencow dev${RESET} ${DIM}← Real-time backend + live logs${RESET}`);
|
|
2085
|
+
log(` ${GREEN}gencow static dist/${RESET} ${DIM}← Static file (dist/) deploy${RESET}`);
|
|
2262
2086
|
log("");
|
|
2263
|
-
log(` ${BOLD}🚀
|
|
2264
|
-
log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Pro
|
|
2087
|
+
log(` ${BOLD}🚀 Unlock production deploy:${RESET}`);
|
|
2088
|
+
log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Pro plan${RESET}`);
|
|
2265
2089
|
log("");
|
|
2266
2090
|
} else {
|
|
2267
|
-
error(`Prod
|
|
2091
|
+
error(`Prod app creation failed: ${errData.error || createProdRes.statusText}`);
|
|
2268
2092
|
}
|
|
2269
2093
|
process.exit(1);
|
|
2270
2094
|
}
|
|
@@ -2280,9 +2104,9 @@ ${BOLD}Examples:${RESET}
|
|
|
2280
2104
|
writeFileSync(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
|
|
2281
2105
|
|
|
2282
2106
|
if (createProdData.alreadyExists) {
|
|
2283
|
-
info(`Prod
|
|
2107
|
+
info(`Prod app verified: ${prodAppId}`);
|
|
2284
2108
|
} else {
|
|
2285
|
-
success(`Prod
|
|
2109
|
+
success(`Prod app created: ${prodAppId}`);
|
|
2286
2110
|
info(`URL: ${createProdData.url}`);
|
|
2287
2111
|
// 프로비저닝 대기
|
|
2288
2112
|
await new Promise(r => setTimeout(r, 3000));
|
|
@@ -2291,23 +2115,23 @@ ${BOLD}Examples:${RESET}
|
|
|
2291
2115
|
|
|
2292
2116
|
// 배포 대상을 prod 앱으로 전환
|
|
2293
2117
|
appId = prodAppId;
|
|
2294
|
-
info(
|
|
2118
|
+
info(`Deploy target: ${CYAN}${appId}${RESET} (production)`);
|
|
2295
2119
|
}
|
|
2296
2120
|
|
|
2297
2121
|
log(`\n${BOLD}${CYAN}Gencow Deploy${RESET}\n`);
|
|
2298
2122
|
if (appId) {
|
|
2299
|
-
info(
|
|
2123
|
+
info(`App: ${appId}`);
|
|
2300
2124
|
} else {
|
|
2301
|
-
info(
|
|
2125
|
+
info(`App: new (auto-generated ID)`);
|
|
2302
2126
|
}
|
|
2303
|
-
info(
|
|
2304
|
-
info(
|
|
2127
|
+
info(`Env: ${envTarget}`);
|
|
2128
|
+
info(`Format: tar.gz`);
|
|
2305
2129
|
log("");
|
|
2306
2130
|
|
|
2307
2131
|
// 0-1. drizzle-kit generate 자동 실행 (번들링 전 migrations/ 최신화)
|
|
2308
2132
|
{
|
|
2309
2133
|
const { execSync: execGen } = await import("child_process");
|
|
2310
|
-
info("
|
|
2134
|
+
info("Generating schema migrations...");
|
|
2311
2135
|
try {
|
|
2312
2136
|
if (isMonorepo() && !isStandaloneProject()) {
|
|
2313
2137
|
// 모노레포: package.json scripts.db:generate 사용
|
|
@@ -2332,18 +2156,18 @@ ${BOLD}Examples:${RESET}
|
|
|
2332
2156
|
msg.includes("nothing to migrate") ||
|
|
2333
2157
|
msg.includes("No changes detected")
|
|
2334
2158
|
) {
|
|
2335
|
-
log(`${DIM}
|
|
2159
|
+
log(`${DIM} ✓ Migrations up-to-date — no new schema changes detected${RESET}`);
|
|
2336
2160
|
} else {
|
|
2337
|
-
warn(
|
|
2338
|
-
warn("gencow/migrations/
|
|
2339
|
-
info("
|
|
2161
|
+
warn(`Migration generation warning: ${msg.split("\n")[0] || "unknown"}`);
|
|
2162
|
+
warn("gencow/migrations/ missing or outdated — platform may skip schema apply.");
|
|
2163
|
+
info("Manual run: gencow db:generate");
|
|
2340
2164
|
}
|
|
2341
2165
|
}
|
|
2342
2166
|
}
|
|
2343
2167
|
log("");
|
|
2344
2168
|
|
|
2345
2169
|
// 1. tar.gz 패키징
|
|
2346
|
-
info("
|
|
2170
|
+
info("Packaging project...");
|
|
2347
2171
|
const { execSync: exec } = await import("child_process");
|
|
2348
2172
|
const tmpBundle = resolve(process.cwd(), ".gencow", "deploy-bundle.tar.gz");
|
|
2349
2173
|
mkdirSync(dirname(tmpBundle), { recursive: true });
|
|
@@ -2356,7 +2180,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2356
2180
|
|
|
2357
2181
|
// gencow/ 디렉토리 존재 확인
|
|
2358
2182
|
if (!existsSync(resolve(process.cwd(), "gencow"))) {
|
|
2359
|
-
error("gencow/
|
|
2183
|
+
error("gencow/ directory not found. Run from Gencow project root.");
|
|
2360
2184
|
process.exit(1);
|
|
2361
2185
|
}
|
|
2362
2186
|
|
|
@@ -2373,7 +2197,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2373
2197
|
if (auditMsg) log(auditMsg);
|
|
2374
2198
|
}
|
|
2375
2199
|
} catch (auditErr) {
|
|
2376
|
-
warn(
|
|
2200
|
+
warn(`Skipping dependency audit: ${auditErr.message}`);
|
|
2377
2201
|
}
|
|
2378
2202
|
}
|
|
2379
2203
|
|
|
@@ -2395,7 +2219,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2395
2219
|
const totalDeps = Object.keys(allDeps).length;
|
|
2396
2220
|
const runtimeCount = Object.keys(runtimeDeps).length;
|
|
2397
2221
|
if (totalDeps > runtimeCount) {
|
|
2398
|
-
info(`${DIM}package.json
|
|
2222
|
+
info(`${DIM}package.json filtered: ${runtimeCount}/${totalDeps} packages will be installed on server.${RESET}`);
|
|
2399
2223
|
}
|
|
2400
2224
|
} catch {
|
|
2401
2225
|
// Fallback: use original package.json
|
|
@@ -2425,7 +2249,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2425
2249
|
exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
|
|
2426
2250
|
}
|
|
2427
2251
|
} catch (e) {
|
|
2428
|
-
error(
|
|
2252
|
+
error(`Packaging failed: ${e.message}`);
|
|
2429
2253
|
process.exit(1);
|
|
2430
2254
|
}
|
|
2431
2255
|
|
|
@@ -2433,7 +2257,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2433
2257
|
try { if (useFilteredPkg) unlinkSync(filteredPkgPath); } catch {}
|
|
2434
2258
|
|
|
2435
2259
|
const bundleSize = statSync(tmpBundle).size;
|
|
2436
|
-
success(
|
|
2260
|
+
success(`Bundle created: ${(bundleSize / 1024).toFixed(1)} KB`);
|
|
2437
2261
|
|
|
2438
2262
|
// 2-0. 번들 크기 경고 (Ph2: 리소스 미터링 — 차단 아님, 안내만)
|
|
2439
2263
|
try {
|
|
@@ -2449,13 +2273,13 @@ ${BOLD}Examples:${RESET}
|
|
|
2449
2273
|
|
|
2450
2274
|
// 2. 앱이 없으면 생성 (appId가 없는 경우)
|
|
2451
2275
|
if (!appId) {
|
|
2452
|
-
info("
|
|
2276
|
+
info("Creating app (auto-generating ID)...");
|
|
2453
2277
|
|
|
2454
2278
|
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
2455
2279
|
|
|
2456
2280
|
if (!createRes.ok) {
|
|
2457
2281
|
const createErr = await createRes.json().catch(() => ({}));
|
|
2458
|
-
error(
|
|
2282
|
+
error(`App creation failed: ${createErr.error || createRes.statusText}`);
|
|
2459
2283
|
process.exit(1);
|
|
2460
2284
|
}
|
|
2461
2285
|
|
|
@@ -2463,11 +2287,11 @@ ${BOLD}Examples:${RESET}
|
|
|
2463
2287
|
appId = createData.appId || createData.name;
|
|
2464
2288
|
|
|
2465
2289
|
if (!appId) {
|
|
2466
|
-
error("
|
|
2290
|
+
error("App creation response missing appId.");
|
|
2467
2291
|
process.exit(1);
|
|
2468
2292
|
}
|
|
2469
2293
|
|
|
2470
|
-
success(
|
|
2294
|
+
success(`App "${appId}" created. Waiting for provisioning...`);
|
|
2471
2295
|
|
|
2472
2296
|
// gencow.json 저장 (즉시)
|
|
2473
2297
|
writeFileSync(gencowJsonPath, JSON.stringify({
|
|
@@ -2475,7 +2299,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2475
2299
|
displayName,
|
|
2476
2300
|
platformUrl: creds.platformUrl,
|
|
2477
2301
|
}, null, 2));
|
|
2478
|
-
info(`${DIM}gencow.json
|
|
2302
|
+
info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
|
|
2479
2303
|
|
|
2480
2304
|
await new Promise(r => setTimeout(r, 3000));
|
|
2481
2305
|
}
|
|
@@ -2489,7 +2313,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2489
2313
|
let spinnerIdx = 0;
|
|
2490
2314
|
const spinner = setInterval(() => {
|
|
2491
2315
|
const elapsed = ((Date.now() - deployStartTime) / 1000).toFixed(0);
|
|
2492
|
-
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET}
|
|
2316
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Deploying... ${DIM}(${elapsed}s)${RESET} `);
|
|
2493
2317
|
}, 120);
|
|
2494
2318
|
|
|
2495
2319
|
const deployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
|
|
@@ -2512,19 +2336,19 @@ ${BOLD}Examples:${RESET}
|
|
|
2512
2336
|
|
|
2513
2337
|
// 앱이 없으면 (appId가 stale한 경우) 새로 생성 후 재시도
|
|
2514
2338
|
if (deployRes.status === 404) {
|
|
2515
|
-
warn(
|
|
2339
|
+
warn(`App "${appId}" not found. Creating new one...`);
|
|
2516
2340
|
|
|
2517
2341
|
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
2518
2342
|
|
|
2519
2343
|
if (!createRes.ok) {
|
|
2520
2344
|
const createErr = await createRes.json().catch(() => ({}));
|
|
2521
|
-
error(
|
|
2345
|
+
error(`App creation failed: ${createErr.error || createRes.statusText}`);
|
|
2522
2346
|
process.exit(1);
|
|
2523
2347
|
}
|
|
2524
2348
|
|
|
2525
2349
|
const createData = await createRes.json();
|
|
2526
2350
|
appId = createData.appId || createData.name;
|
|
2527
|
-
success(
|
|
2351
|
+
success(`App "${appId}" created. Provisioning...`);
|
|
2528
2352
|
|
|
2529
2353
|
// gencow.json 업데이트
|
|
2530
2354
|
writeFileSync(gencowJsonPath, JSON.stringify({
|
|
@@ -2536,7 +2360,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2536
2360
|
await new Promise(r => setTimeout(r, 3000));
|
|
2537
2361
|
|
|
2538
2362
|
// 재배포
|
|
2539
|
-
info("
|
|
2363
|
+
info("Retrying deploy...");
|
|
2540
2364
|
exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
|
|
2541
2365
|
const retryBuffer = readFileSync(tmpBundle);
|
|
2542
2366
|
|
|
@@ -2544,7 +2368,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2544
2368
|
let retrySpinnerIdx = 0;
|
|
2545
2369
|
const retrySpinner = setInterval(() => {
|
|
2546
2370
|
const elapsed = ((Date.now() - retryStartTime) / 1000).toFixed(0);
|
|
2547
|
-
process.stdout.write(`\r ${CYAN}${spinnerFrames[retrySpinnerIdx++ % spinnerFrames.length]}${RESET}
|
|
2371
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[retrySpinnerIdx++ % spinnerFrames.length]}${RESET} Deploying... ${DIM}(${elapsed}s)${RESET} `);
|
|
2548
2372
|
}, 120);
|
|
2549
2373
|
|
|
2550
2374
|
const retryRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
|
|
@@ -2562,10 +2386,10 @@ ${BOLD}Examples:${RESET}
|
|
|
2562
2386
|
|
|
2563
2387
|
if (!retryRes.ok) {
|
|
2564
2388
|
const retryErr = await retryRes.json().catch(() => ({}));
|
|
2565
|
-
error(
|
|
2389
|
+
error(`Deploy failed: ${retryErr.error || retryRes.statusText}`);
|
|
2566
2390
|
if (retryErr.crashLogs?.length) {
|
|
2567
2391
|
log("");
|
|
2568
|
-
|
|
2392
|
+
warn("Server startup failure:");
|
|
2569
2393
|
for (const line of retryErr.crashLogs) log(` ${DIM}${line}${RESET}`);
|
|
2570
2394
|
}
|
|
2571
2395
|
process.exit(1);
|
|
@@ -2574,14 +2398,14 @@ ${BOLD}Examples:${RESET}
|
|
|
2574
2398
|
const retryData = await retryRes.json();
|
|
2575
2399
|
const retryElapsed = ((Date.now() - retryStartTime) / 1000).toFixed(1);
|
|
2576
2400
|
log("");
|
|
2577
|
-
success(
|
|
2401
|
+
success(`Server build complete! (${retryElapsed}s)`);
|
|
2578
2402
|
|
|
2579
2403
|
// Health check — 앱 URL로 실제 응답 확인
|
|
2580
2404
|
if (retryData.url) {
|
|
2581
2405
|
await this._verifyAppReady(retryData.url, appId);
|
|
2582
2406
|
}
|
|
2583
2407
|
|
|
2584
|
-
info(
|
|
2408
|
+
info(`App ID: ${appId}`);
|
|
2585
2409
|
info(`URL: ${retryData.url}`);
|
|
2586
2410
|
info(`Hash: ${retryData.bundleHash}`);
|
|
2587
2411
|
updateEnvLocalUrl(retryData.url);
|
|
@@ -2589,10 +2413,10 @@ ${BOLD}Examples:${RESET}
|
|
|
2589
2413
|
return;
|
|
2590
2414
|
}
|
|
2591
2415
|
|
|
2592
|
-
error(
|
|
2416
|
+
error(`Deploy failed: ${errData.error || deployRes.statusText}`);
|
|
2593
2417
|
if (errData.crashLogs?.length) {
|
|
2594
2418
|
log("");
|
|
2595
|
-
warn("
|
|
2419
|
+
warn("Server startup failure cause:");
|
|
2596
2420
|
for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
|
|
2597
2421
|
}
|
|
2598
2422
|
process.exit(1);
|
|
@@ -2602,14 +2426,14 @@ ${BOLD}Examples:${RESET}
|
|
|
2602
2426
|
const deployElapsed = ((Date.now() - deployStartTime) / 1000).toFixed(1);
|
|
2603
2427
|
|
|
2604
2428
|
log("");
|
|
2605
|
-
success(
|
|
2429
|
+
success(`Server build complete! (${deployElapsed}s)`);
|
|
2606
2430
|
|
|
2607
2431
|
// Health check — 앱 URL로 실제 응답 확인 (Blue-Green은 서버에서 완료됐지만, edge case 방어)
|
|
2608
2432
|
if (deployData.url) {
|
|
2609
2433
|
await this._verifyAppReady(deployData.url, appId);
|
|
2610
2434
|
}
|
|
2611
2435
|
|
|
2612
|
-
info(
|
|
2436
|
+
info(`App ID: ${appId}`);
|
|
2613
2437
|
info(`URL: ${deployData.url}`);
|
|
2614
2438
|
info(`Hash: ${deployData.bundleHash}`);
|
|
2615
2439
|
if (deployData.deployId) info(`ID: ${deployData.deployId}`);
|
|
@@ -2623,7 +2447,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2623
2447
|
displayName,
|
|
2624
2448
|
platformUrl: creds.platformUrl,
|
|
2625
2449
|
}, null, 2));
|
|
2626
|
-
info(`${DIM}gencow.json
|
|
2450
|
+
info(`${DIM}gencow.json created${RESET}`);
|
|
2627
2451
|
}
|
|
2628
2452
|
},
|
|
2629
2453
|
|
|
@@ -2643,7 +2467,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2643
2467
|
// health check 스피너
|
|
2644
2468
|
const healthSpinner = setInterval(() => {
|
|
2645
2469
|
const elapsed = ((Date.now() - start) / 1000).toFixed(0);
|
|
2646
|
-
process.stdout.write(`\r ${CYAN}${spinnerFrames[idx++ % spinnerFrames.length]}${RESET}
|
|
2470
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[idx++ % spinnerFrames.length]}${RESET} Checking app readiness... ${DIM}(${elapsed}s)${RESET} `);
|
|
2647
2471
|
}, 120);
|
|
2648
2472
|
|
|
2649
2473
|
let healthy = false;
|
|
@@ -2669,10 +2493,10 @@ ${BOLD}Examples:${RESET}
|
|
|
2669
2493
|
|
|
2670
2494
|
const healthElapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
2671
2495
|
if (healthy) {
|
|
2672
|
-
success(
|
|
2496
|
+
success(`App Ready! (${healthElapsed}s)`);
|
|
2673
2497
|
} else {
|
|
2674
|
-
warn(
|
|
2675
|
-
info(
|
|
2498
|
+
warn(`App response check failed (${healthElapsed}s). Server may still be starting.`);
|
|
2499
|
+
info(`Manual check: ${CYAN}${appUrl}${RESET}`);
|
|
2676
2500
|
}
|
|
2677
2501
|
},
|
|
2678
2502
|
|
|
@@ -2695,9 +2519,9 @@ ${BOLD}Examples:${RESET}
|
|
|
2695
2519
|
}
|
|
2696
2520
|
|
|
2697
2521
|
if (!targetDir || !existsSync(resolve(process.cwd(), targetDir))) {
|
|
2698
|
-
error(
|
|
2699
|
-
info(
|
|
2700
|
-
info(
|
|
2522
|
+
error(`Static files directory not found.`);
|
|
2523
|
+
info(`Auto-detect order: ${AUTO_DETECT.join(", ")}`);
|
|
2524
|
+
info(`Specify manually: gencow static <dir>`);
|
|
2701
2525
|
process.exit(1);
|
|
2702
2526
|
}
|
|
2703
2527
|
|
|
@@ -2748,22 +2572,22 @@ ${BOLD}Examples:${RESET}
|
|
|
2748
2572
|
const shouldDeployBackend = detectedBackend && !noBackend && !isBackendEmpty;
|
|
2749
2573
|
|
|
2750
2574
|
if (isBackendEmpty && detectedBackend) {
|
|
2751
|
-
info(`${DIM}gencow/
|
|
2752
|
-
info(`${DIM}💡
|
|
2575
|
+
info(`${DIM}gencow/ detected — no API functions (query/mutation) found → skipping backend deploy${RESET}`);
|
|
2576
|
+
info(`${DIM}💡 If you need a backend, define query() or mutation() in gencow/*.ts files.${RESET}`);
|
|
2753
2577
|
}
|
|
2754
2578
|
|
|
2755
2579
|
if (shouldDeployBackend) {
|
|
2756
2580
|
log(`\n${BOLD}${CYAN}Gencow Deploy (Fullstack)${RESET}\n`);
|
|
2757
|
-
info(
|
|
2758
|
-
info(
|
|
2581
|
+
info(`Backend detected: ${backendRoot === process.cwd() ? "gencow/" : resolve(backendRoot, "gencow/")} (auto-deploy)`);
|
|
2582
|
+
info(`Frontend: ${targetDir}/`);
|
|
2759
2583
|
} else {
|
|
2760
2584
|
log(`\n${BOLD}${CYAN}Gencow Static Deploy${RESET}\n`);
|
|
2761
|
-
info(
|
|
2585
|
+
info(`Directory: ${targetDir}/`);
|
|
2762
2586
|
}
|
|
2763
2587
|
if (appId) {
|
|
2764
|
-
info(
|
|
2588
|
+
info(`App ID: ${appId}`);
|
|
2765
2589
|
} else {
|
|
2766
|
-
info(
|
|
2590
|
+
info(`App: new (ID auto-generated)`);
|
|
2767
2591
|
}
|
|
2768
2592
|
log("");
|
|
2769
2593
|
|
|
@@ -2795,23 +2619,23 @@ ${BOLD}Examples:${RESET}
|
|
|
2795
2619
|
}
|
|
2796
2620
|
if (apiRefFiles.length > 0) {
|
|
2797
2621
|
log("");
|
|
2798
|
-
warn(
|
|
2622
|
+
warn(`API references found in build files:`);
|
|
2799
2623
|
for (const f of apiRefFiles.slice(0, 5)) log(` ${DIM}- ${f}${RESET}`);
|
|
2800
|
-
if (apiRefFiles.length > 5) log(` ${DIM}...
|
|
2624
|
+
if (apiRefFiles.length > 5) log(` ${DIM}... and ${apiRefFiles.length - 5} more${RESET}`);
|
|
2801
2625
|
log("");
|
|
2802
|
-
warn(
|
|
2803
|
-
info(`💡
|
|
2804
|
-
info(`💡
|
|
2626
|
+
warn(`Static hosting has no API server — these will return 404.`);
|
|
2627
|
+
info(`💡 If you need a backend: ${CYAN}VITE_API_URL=https://{backend}.{domain} npm run build${RESET} then deploy`);
|
|
2628
|
+
info(`💡 If using a separate backend: set ${CYAN}VITE_API_URL${RESET} env var before building.`);
|
|
2805
2629
|
log("");
|
|
2806
2630
|
|
|
2807
2631
|
const { createInterface } = await import("readline");
|
|
2808
2632
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2809
2633
|
const answer = await new Promise(resolve => {
|
|
2810
|
-
rl.question(` ${YELLOW}⚠${RESET}
|
|
2634
|
+
rl.question(` ${YELLOW}⚠${RESET} Proceed with static deploy anyway? (y/N) `, resolve);
|
|
2811
2635
|
});
|
|
2812
2636
|
rl.close();
|
|
2813
2637
|
if (answer.toLowerCase() !== "y") {
|
|
2814
|
-
info("
|
|
2638
|
+
info("Deploy cancelled.");
|
|
2815
2639
|
return;
|
|
2816
2640
|
}
|
|
2817
2641
|
log("");
|
|
@@ -2821,7 +2645,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2821
2645
|
|
|
2822
2646
|
// ── 백엔드 자동 배포 (감지된 경우) ────────────────────
|
|
2823
2647
|
if (shouldDeployBackend) {
|
|
2824
|
-
log(` ${BOLD}──
|
|
2648
|
+
log(` ${BOLD}── Backend Deploy ──────────────────────${RESET}\n`);
|
|
2825
2649
|
|
|
2826
2650
|
// 1-0. Pre-deploy dependency audit + Phase B: filter package.json
|
|
2827
2651
|
let auditResult = null;
|
|
@@ -2835,7 +2659,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2835
2659
|
if (auditMsg) log(auditMsg);
|
|
2836
2660
|
}
|
|
2837
2661
|
} catch (auditErr) {
|
|
2838
|
-
warn(
|
|
2662
|
+
warn(`Dependency audit skipped: ${auditErr.message}`);
|
|
2839
2663
|
}
|
|
2840
2664
|
}
|
|
2841
2665
|
|
|
@@ -2843,7 +2667,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2843
2667
|
// 서버에서 generate 시 stdin이 없어 rename 프롬프트에 막히던 문제 해결
|
|
2844
2668
|
const schemaPath = resolve(backendRoot, "gencow", "schema.ts");
|
|
2845
2669
|
if (existsSync(schemaPath)) {
|
|
2846
|
-
info("
|
|
2670
|
+
info("Generating schema migrations...");
|
|
2847
2671
|
try {
|
|
2848
2672
|
const dk = _drizzleKitCmd("generate");
|
|
2849
2673
|
execSync(dk.cmd, {
|
|
@@ -2851,20 +2675,20 @@ ${BOLD}Examples:${RESET}
|
|
|
2851
2675
|
env: { ...process.env, ...dk.env },
|
|
2852
2676
|
stdio: "inherit", // ← 프롬프트 패스스루!
|
|
2853
2677
|
});
|
|
2854
|
-
success("
|
|
2678
|
+
success("Migrations generated");
|
|
2855
2679
|
} catch (e) {
|
|
2856
2680
|
const msg = e.stderr?.toString() || e.message || "";
|
|
2857
2681
|
if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
|
|
2858
|
-
log(`${DIM}
|
|
2682
|
+
log(`${DIM} ✓ Migrations up-to-date — no new schema changes detected${RESET}`);
|
|
2859
2683
|
} else {
|
|
2860
|
-
warn(
|
|
2861
|
-
info("
|
|
2684
|
+
warn(`Migration generation failed: ${msg.split("\\n")[0]}`);
|
|
2685
|
+
info("Will attempt schema push via server.");
|
|
2862
2686
|
}
|
|
2863
2687
|
}
|
|
2864
2688
|
}
|
|
2865
2689
|
|
|
2866
2690
|
// 1. tar.gz 패키징 (백엔드)
|
|
2867
|
-
info("
|
|
2691
|
+
info("Packaging backend...");
|
|
2868
2692
|
const { execSync: exec } = await import("child_process");
|
|
2869
2693
|
const tmpBackendBundle = resolve(backendRoot, ".gencow", "deploy-bundle.tar.gz");
|
|
2870
2694
|
mkdirSync(dirname(tmpBackendBundle), { recursive: true });
|
|
@@ -2892,7 +2716,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2892
2716
|
const totalDeps = Object.keys(allDeps).length;
|
|
2893
2717
|
const runtimeCount = Object.keys(runtimeDeps).length;
|
|
2894
2718
|
if (totalDeps > runtimeCount) {
|
|
2895
|
-
info(`${DIM}package.json
|
|
2719
|
+
info(`${DIM}package.json filtered: ${runtimeCount}/${totalDeps} packages will be installed on server.${RESET}`);
|
|
2896
2720
|
}
|
|
2897
2721
|
} catch {
|
|
2898
2722
|
useFilteredPkg = false;
|
|
@@ -2921,34 +2745,34 @@ ${BOLD}Examples:${RESET}
|
|
|
2921
2745
|
exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBackendBundle}" ${backendFiles.join(" ")}`, { cwd: backendRoot });
|
|
2922
2746
|
}
|
|
2923
2747
|
} catch (e) {
|
|
2924
|
-
error(
|
|
2748
|
+
error(`Backend packaging failed: ${e.message}`);
|
|
2925
2749
|
process.exit(1);
|
|
2926
2750
|
}
|
|
2927
2751
|
try { if (useFilteredPkg) unlinkSync(filteredPkgPath); } catch {}
|
|
2928
2752
|
|
|
2929
2753
|
const backendBundleSize = statSync(tmpBackendBundle).size;
|
|
2930
|
-
success(
|
|
2754
|
+
success(`Backend bundle created: ${(backendBundleSize / 1024).toFixed(1)} KB`);
|
|
2931
2755
|
|
|
2932
2756
|
// 2. 앱이 없으면 생성
|
|
2933
2757
|
if (!appId) {
|
|
2934
|
-
info("
|
|
2758
|
+
info("Creating app (auto-generating ID)...");
|
|
2935
2759
|
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
2936
2760
|
if (!createRes.ok) {
|
|
2937
2761
|
const createErr = await createRes.json().catch(() => ({}));
|
|
2938
|
-
error(
|
|
2762
|
+
error(`App creation failed: ${createErr.error || createRes.statusText}`);
|
|
2939
2763
|
process.exit(1);
|
|
2940
2764
|
}
|
|
2941
2765
|
const createData = await createRes.json();
|
|
2942
2766
|
appId = createData.appId || createData.name;
|
|
2943
2767
|
if (!appId) {
|
|
2944
|
-
error("
|
|
2768
|
+
error("App creation response missing appId.");
|
|
2945
2769
|
process.exit(1);
|
|
2946
2770
|
}
|
|
2947
|
-
success(
|
|
2771
|
+
success(`App "${appId}" created. Provisioning...`);
|
|
2948
2772
|
writeFileSync(gencowJsonPath, JSON.stringify({
|
|
2949
2773
|
appId, displayName, platformUrl: creds.platformUrl,
|
|
2950
2774
|
}, null, 2));
|
|
2951
|
-
info(`${DIM}gencow.json
|
|
2775
|
+
info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
|
|
2952
2776
|
await new Promise(r => setTimeout(r, 3000));
|
|
2953
2777
|
}
|
|
2954
2778
|
|
|
@@ -2961,7 +2785,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2961
2785
|
let spinnerIdx = 0;
|
|
2962
2786
|
const backendSpinner = setInterval(() => {
|
|
2963
2787
|
const elapsed = ((Date.now() - backendStartTime) / 1000).toFixed(0);
|
|
2964
|
-
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET}
|
|
2788
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Deploying backend... ${DIM}(${elapsed}s)${RESET} `);
|
|
2965
2789
|
}, 120);
|
|
2966
2790
|
|
|
2967
2791
|
const backendDeployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
|
|
@@ -2976,24 +2800,24 @@ ${BOLD}Examples:${RESET}
|
|
|
2976
2800
|
|
|
2977
2801
|
if (!backendDeployRes.ok) {
|
|
2978
2802
|
const errData = await backendDeployRes.json().catch(() => ({}));
|
|
2979
|
-
error(
|
|
2803
|
+
error(`Backend deploy failed: ${errData.error || backendDeployRes.statusText}`);
|
|
2980
2804
|
if (errData.crashLogs?.length) {
|
|
2981
2805
|
log("");
|
|
2982
|
-
warn("
|
|
2806
|
+
warn("Server startup failure:");
|
|
2983
2807
|
for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
|
|
2984
2808
|
}
|
|
2985
2809
|
if (targetDir) {
|
|
2986
2810
|
// --static 모드: 백엔드 실패해도 프론트엔드 배포는 계속 진행
|
|
2987
|
-
warn("
|
|
2811
|
+
warn("Backend deploy failed — continuing with static deploy.");
|
|
2988
2812
|
log("");
|
|
2989
2813
|
} else {
|
|
2990
|
-
error("
|
|
2814
|
+
error("Backend deploy failed, skipping frontend deploy.");
|
|
2991
2815
|
process.exit(1);
|
|
2992
2816
|
}
|
|
2993
2817
|
} else {
|
|
2994
2818
|
const backendData = await backendDeployRes.json();
|
|
2995
2819
|
const backendElapsed = ((Date.now() - backendStartTime) / 1000).toFixed(1);
|
|
2996
|
-
success(
|
|
2820
|
+
success(`Backend build complete! (${backendElapsed}s)`);
|
|
2997
2821
|
|
|
2998
2822
|
// Health check — 백엔드 앱 URL로 실제 응답 확인
|
|
2999
2823
|
if (backendData.url) {
|
|
@@ -3005,11 +2829,11 @@ ${BOLD}Examples:${RESET}
|
|
|
3005
2829
|
updateEnvLocalUrl(backendData.url);
|
|
3006
2830
|
log("");
|
|
3007
2831
|
}
|
|
3008
|
-
log(` ${BOLD}──
|
|
2832
|
+
log(` ${BOLD}── Frontend Deploy ──────────────────${RESET}\n`);
|
|
3009
2833
|
}
|
|
3010
2834
|
|
|
3011
2835
|
// 1. tar.gz 패키징
|
|
3012
|
-
info("
|
|
2836
|
+
info("Packaging static files...");
|
|
3013
2837
|
const { execSync: exec } = await import("child_process");
|
|
3014
2838
|
const tmpBundle = resolve(process.cwd(), ".gencow", "static-bundle.tar.gz");
|
|
3015
2839
|
mkdirSync(dirname(tmpBundle), { recursive: true });
|
|
@@ -3017,40 +2841,40 @@ ${BOLD}Examples:${RESET}
|
|
|
3017
2841
|
try {
|
|
3018
2842
|
exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" -C "${resolve(process.cwd(), targetDir)}" .`, { cwd: process.cwd() });
|
|
3019
2843
|
} catch (e) {
|
|
3020
|
-
error(
|
|
2844
|
+
error(`Packaging failed: ${e.message}`);
|
|
3021
2845
|
process.exit(1);
|
|
3022
2846
|
}
|
|
3023
2847
|
|
|
3024
2848
|
const bundleSize = statSync(tmpBundle).size;
|
|
3025
2849
|
const MAX_SIZE = 100 * 1024 * 1024;
|
|
3026
2850
|
if (bundleSize > MAX_SIZE) {
|
|
3027
|
-
error(
|
|
2851
|
+
error(`Bundle too large: ${(bundleSize / (1024 * 1024)).toFixed(1)} MB (max 100 MB)`);
|
|
3028
2852
|
try { unlinkSync(tmpBundle); } catch { }
|
|
3029
2853
|
process.exit(1);
|
|
3030
2854
|
}
|
|
3031
|
-
success(
|
|
2855
|
+
success(`Bundle created: ${(bundleSize / 1024).toFixed(1)} KB`);
|
|
3032
2856
|
|
|
3033
2857
|
// 2. 앱이 없으면 생성
|
|
3034
2858
|
if (!appId) {
|
|
3035
|
-
info("
|
|
2859
|
+
info("Creating app (auto-generating ID)...");
|
|
3036
2860
|
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
3037
2861
|
if (!createRes.ok) {
|
|
3038
2862
|
const createErr = await createRes.json().catch(() => ({}));
|
|
3039
|
-
error(
|
|
2863
|
+
error(`App creation failed: ${createErr.error || createRes.statusText}`);
|
|
3040
2864
|
process.exit(1);
|
|
3041
2865
|
}
|
|
3042
2866
|
const createData = await createRes.json();
|
|
3043
2867
|
appId = createData.appId || createData.name;
|
|
3044
|
-
success(
|
|
2868
|
+
success(`App "${appId}" created. Provisioning...`);
|
|
3045
2869
|
writeFileSync(gencowJsonPath, JSON.stringify({
|
|
3046
2870
|
appId, displayName, platformUrl: creds.platformUrl,
|
|
3047
2871
|
}, null, 2));
|
|
3048
|
-
info(`${DIM}gencow.json
|
|
2872
|
+
info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
|
|
3049
2873
|
await new Promise(r => setTimeout(r, 3000));
|
|
3050
2874
|
}
|
|
3051
2875
|
|
|
3052
2876
|
// 3. 업로드
|
|
3053
|
-
info("
|
|
2877
|
+
info("Deploying static files...");
|
|
3054
2878
|
const bundleBuffer = readFileSync(tmpBundle);
|
|
3055
2879
|
|
|
3056
2880
|
const deployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy-static`, {
|
|
@@ -3063,22 +2887,22 @@ ${BOLD}Examples:${RESET}
|
|
|
3063
2887
|
|
|
3064
2888
|
if (!deployRes.ok) {
|
|
3065
2889
|
const errData = await deployRes.json().catch(() => ({}));
|
|
3066
|
-
error(
|
|
2890
|
+
error(`Static deploy failed: ${errData.error || deployRes.statusText}`);
|
|
3067
2891
|
process.exit(1);
|
|
3068
2892
|
}
|
|
3069
2893
|
|
|
3070
2894
|
const data = await deployRes.json();
|
|
3071
2895
|
log("");
|
|
3072
|
-
success(
|
|
3073
|
-
info(
|
|
2896
|
+
success(`Static deploy complete!`);
|
|
2897
|
+
info(`App ID: ${appId}`);
|
|
3074
2898
|
info(`URL: ${data.url}`);
|
|
3075
|
-
info(
|
|
2899
|
+
info(`Files: ${data.files} (${(data.size / 1024).toFixed(1)} KB)`);
|
|
3076
2900
|
info(`Hash: ${data.bundleHash}`);
|
|
3077
2901
|
if (data.deployId) info(`ID: ${data.deployId}`);
|
|
3078
2902
|
|
|
3079
2903
|
// 이미지 최적화 결과 표시
|
|
3080
2904
|
if (data.optimizedImages) {
|
|
3081
|
-
success(`🖼 ${data.optimizedImages}
|
|
2905
|
+
success(`🖼 ${data.optimizedImages} images optimized to WebP (saved: ${(data.savedBytes / 1024).toFixed(1)} KB)`);
|
|
3082
2906
|
}
|
|
3083
2907
|
|
|
3084
2908
|
// skipped 파일 경고 표시
|
|
@@ -3131,12 +2955,12 @@ ${BOLD}Examples:${RESET}
|
|
|
3131
2955
|
|
|
3132
2956
|
// --prod인데 prod 앱이 없는 경우
|
|
3133
2957
|
if (envTarget === "prod" && appId && !appId.endsWith("-prod")) {
|
|
3134
|
-
error("
|
|
2958
|
+
error("No prod app yet. Run gencow deploy first.");
|
|
3135
2959
|
return;
|
|
3136
2960
|
}
|
|
3137
2961
|
|
|
3138
2962
|
if (!appId) {
|
|
3139
|
-
error("
|
|
2963
|
+
error("App ID not found. Run gencow deploy first.");
|
|
3140
2964
|
return;
|
|
3141
2965
|
}
|
|
3142
2966
|
|
|
@@ -3148,13 +2972,13 @@ ${BOLD}Examples:${RESET}
|
|
|
3148
2972
|
{ headers: { "Authorization": `Bearer ${creds.apiKey}` } }
|
|
3149
2973
|
);
|
|
3150
2974
|
if (!res.ok) {
|
|
3151
|
-
error(
|
|
2975
|
+
error(`Failed to list env vars: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
|
|
3152
2976
|
return;
|
|
3153
2977
|
}
|
|
3154
2978
|
const vars = await res.json();
|
|
3155
|
-
log(`\n${BOLD}${CYAN}
|
|
2979
|
+
log(`\n${BOLD}${CYAN}Environment Variables${RESET} — ${appId} (${envTarget})\n`);
|
|
3156
2980
|
if (vars.length === 0) {
|
|
3157
|
-
info("
|
|
2981
|
+
info("No environment variables configured.");
|
|
3158
2982
|
} else {
|
|
3159
2983
|
for (const v of vars) {
|
|
3160
2984
|
log(` ${v.key}`);
|
|
@@ -3168,7 +2992,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3168
2992
|
// gencow env set KEY=VALUE [KEY2=VALUE2...]
|
|
3169
2993
|
const kvPairs = restArgs.filter(a => a.includes("=") && !a.startsWith("-"));
|
|
3170
2994
|
if (kvPairs.length === 0) {
|
|
3171
|
-
error("
|
|
2995
|
+
error("Usage: gencow env set KEY=VALUE [KEY2=VALUE2...]");
|
|
3172
2996
|
return;
|
|
3173
2997
|
}
|
|
3174
2998
|
|
|
@@ -3189,9 +3013,9 @@ ${BOLD}Examples:${RESET}
|
|
|
3189
3013
|
);
|
|
3190
3014
|
|
|
3191
3015
|
if (res.ok) {
|
|
3192
|
-
success(`${key}
|
|
3016
|
+
success(`${key} configured (${envTarget})`);
|
|
3193
3017
|
} else {
|
|
3194
|
-
error(`${key}
|
|
3018
|
+
error(`${key} configuration failed`);
|
|
3195
3019
|
}
|
|
3196
3020
|
}
|
|
3197
3021
|
break;
|
|
@@ -3201,7 +3025,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3201
3025
|
case "remove": {
|
|
3202
3026
|
const keys = restArgs.filter(a => !a.startsWith("-"));
|
|
3203
3027
|
if (keys.length === 0) {
|
|
3204
|
-
error("
|
|
3028
|
+
error("Usage: gencow env unset KEY [KEY2...]");
|
|
3205
3029
|
return;
|
|
3206
3030
|
}
|
|
3207
3031
|
|
|
@@ -3215,9 +3039,9 @@ ${BOLD}Examples:${RESET}
|
|
|
3215
3039
|
);
|
|
3216
3040
|
|
|
3217
3041
|
if (res.ok) {
|
|
3218
|
-
success(`${key}
|
|
3042
|
+
success(`${key} removed`);
|
|
3219
3043
|
} else {
|
|
3220
|
-
error(`${key}
|
|
3044
|
+
error(`${key} removal failed`);
|
|
3221
3045
|
}
|
|
3222
3046
|
}
|
|
3223
3047
|
break;
|
|
@@ -3227,7 +3051,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3227
3051
|
// 로컬 .env 파일 → 리모트 일괄 push
|
|
3228
3052
|
const envFile = resolve(process.cwd(), envTarget === "prod" ? ".env.production" : ".env");
|
|
3229
3053
|
if (!existsSync(envFile)) {
|
|
3230
|
-
error(`${envFile}
|
|
3054
|
+
error(`${envFile} not found.`);
|
|
3231
3055
|
return;
|
|
3232
3056
|
}
|
|
3233
3057
|
|
|
@@ -3241,7 +3065,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3241
3065
|
}
|
|
3242
3066
|
|
|
3243
3067
|
const count = Object.keys(vars).length;
|
|
3244
|
-
info(
|
|
3068
|
+
info(`Pushing ${count} env vars to ${appId} (${envTarget})...`);
|
|
3245
3069
|
|
|
3246
3070
|
const res = await fetch(
|
|
3247
3071
|
`${creds.platformUrl}/platform/apps/${appId}/env/bulk`,
|
|
@@ -3256,16 +3080,16 @@ ${BOLD}Examples:${RESET}
|
|
|
3256
3080
|
);
|
|
3257
3081
|
|
|
3258
3082
|
if (res.ok) {
|
|
3259
|
-
success(`${count}
|
|
3083
|
+
success(`${count} env vars pushed`);
|
|
3260
3084
|
} else {
|
|
3261
|
-
error(`
|
|
3085
|
+
error(`Push failed: ${(await res.json().catch(() => ({}))).error}`);
|
|
3262
3086
|
}
|
|
3263
3087
|
break;
|
|
3264
3088
|
}
|
|
3265
3089
|
|
|
3266
3090
|
default:
|
|
3267
|
-
error(
|
|
3268
|
-
info("
|
|
3091
|
+
error(`Unknown subcommand: ${subCmd}`);
|
|
3092
|
+
info("Usage: gencow env [list|set|unset|push] ...");
|
|
3269
3093
|
}
|
|
3270
3094
|
},
|
|
3271
3095
|
|
|
@@ -3275,8 +3099,9 @@ ${BOLD}Examples:${RESET}
|
|
|
3275
3099
|
const subCmd = configArgs[0] || "help";
|
|
3276
3100
|
const restArgs = configArgs.slice(1);
|
|
3277
3101
|
|
|
3278
|
-
//
|
|
3102
|
+
// Resolve appId + --prod support
|
|
3279
3103
|
let appId = null;
|
|
3104
|
+
const isProd = restArgs.includes("--prod");
|
|
3280
3105
|
for (let i = 0; i < restArgs.length; i++) {
|
|
3281
3106
|
if (restArgs[i] === "--app" || restArgs[i] === "-a") appId = restArgs[++i];
|
|
3282
3107
|
}
|
|
@@ -3286,22 +3111,31 @@ ${BOLD}Examples:${RESET}
|
|
|
3286
3111
|
if (existsSync(gencowJsonPath)) {
|
|
3287
3112
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
3288
3113
|
appId = gencowJson.appId || gencowJson.appName;
|
|
3114
|
+
if (isProd && gencowJson.prodApp) {
|
|
3115
|
+
appId = gencowJson.prodApp;
|
|
3116
|
+
}
|
|
3289
3117
|
}
|
|
3290
3118
|
}
|
|
3291
3119
|
|
|
3292
3120
|
if (subCmd === "help" || subCmd === "--help" || subCmd === "-h") {
|
|
3293
|
-
log(`\n${BOLD}${CYAN}gencow config${RESET} —
|
|
3294
|
-
log(` ${CYAN}set${RESET} image.maxWidth
|
|
3295
|
-
log(` ${CYAN}set${RESET} image.quality
|
|
3296
|
-
log(` ${CYAN}get${RESET} image
|
|
3297
|
-
log(` ${CYAN}reset${RESET} image
|
|
3298
|
-
log(` ${DIM}
|
|
3299
|
-
log(` ${DIM}--app, -a
|
|
3121
|
+
log(`\n${BOLD}${CYAN}gencow config${RESET} — App configuration\n`);
|
|
3122
|
+
log(` ${CYAN}set${RESET} image.maxWidth <value> Auto WebP max width (px)`);
|
|
3123
|
+
log(` ${CYAN}set${RESET} image.quality <value> Auto WebP quality (1-100)`);
|
|
3124
|
+
log(` ${CYAN}get${RESET} image Show current image config`);
|
|
3125
|
+
log(` ${CYAN}reset${RESET} image Reset to tier defaults\n`);
|
|
3126
|
+
log(` ${DIM}Options:${RESET}`);
|
|
3127
|
+
log(` ${DIM}--app, -a <name> Target app (default: from gencow.json)${RESET}`);
|
|
3128
|
+
log(` ${DIM}--prod Target production app${RESET}\n`);
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
if (isProd && appId && !appId.endsWith("-prod")) {
|
|
3133
|
+
error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
|
|
3300
3134
|
return;
|
|
3301
3135
|
}
|
|
3302
3136
|
|
|
3303
3137
|
if (!appId) {
|
|
3304
|
-
error("
|
|
3138
|
+
error("App not found. Run 'gencow dev' first.");
|
|
3305
3139
|
return;
|
|
3306
3140
|
}
|
|
3307
3141
|
|
|
@@ -3311,7 +3145,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3311
3145
|
const val = restArgs[restArgs.indexOf(key) + 1];
|
|
3312
3146
|
|
|
3313
3147
|
if (!key || val === undefined) {
|
|
3314
|
-
error("
|
|
3148
|
+
error("Usage: gencow config set image.maxWidth <value>");
|
|
3315
3149
|
return;
|
|
3316
3150
|
}
|
|
3317
3151
|
|
|
@@ -3319,34 +3153,34 @@ ${BOLD}Examples:${RESET}
|
|
|
3319
3153
|
if (key === "image.maxWidth" || key === "image.max-width") {
|
|
3320
3154
|
const v = parseInt(val);
|
|
3321
3155
|
if (isNaN(v) || v < 0 || v > 10000) {
|
|
3322
|
-
error("maxWidth
|
|
3156
|
+
error("maxWidth must be between 0 and 10000 (0 = reset)");
|
|
3323
3157
|
return;
|
|
3324
3158
|
}
|
|
3325
3159
|
imageConfig.autoMaxWidth = v;
|
|
3326
3160
|
} else if (key === "image.quality") {
|
|
3327
3161
|
const v = parseInt(val);
|
|
3328
3162
|
if (isNaN(v) || v < 0 || v > 100) {
|
|
3329
|
-
error("quality
|
|
3163
|
+
error("quality must be between 0 and 100 (0 = reset)");
|
|
3330
3164
|
return;
|
|
3331
3165
|
}
|
|
3332
3166
|
imageConfig.autoQuality = v;
|
|
3333
3167
|
} else {
|
|
3334
|
-
error(
|
|
3335
|
-
info("
|
|
3168
|
+
error(`Unknown config key: ${key}`);
|
|
3169
|
+
info("Available: image.maxWidth, image.quality");
|
|
3336
3170
|
return;
|
|
3337
3171
|
}
|
|
3338
3172
|
|
|
3339
3173
|
const res = await rpcMutation(creds, "apps.updateImageConfig", { name: appId, imageConfig });
|
|
3340
3174
|
if (res.ok) {
|
|
3341
3175
|
const data = await res.json();
|
|
3342
|
-
success(`${key} = ${val}
|
|
3176
|
+
success(`${key} = ${val} configured`);
|
|
3343
3177
|
if (data.imageConfig) {
|
|
3344
|
-
info(
|
|
3178
|
+
info(`Current config: ${JSON.stringify(data.imageConfig)}`);
|
|
3345
3179
|
}
|
|
3346
|
-
info(`${DIM}
|
|
3180
|
+
info(`${DIM}Takes effect on next image request. Existing cached images use separate keys.${RESET}`);
|
|
3347
3181
|
} else {
|
|
3348
3182
|
const errData = await res.json().catch(() => ({}));
|
|
3349
|
-
error(
|
|
3183
|
+
error(`Config update failed: ${errData.error || res.statusText}`);
|
|
3350
3184
|
}
|
|
3351
3185
|
break;
|
|
3352
3186
|
}
|
|
@@ -3354,24 +3188,24 @@ ${BOLD}Examples:${RESET}
|
|
|
3354
3188
|
case "get": {
|
|
3355
3189
|
const key = restArgs.find(a => !a.startsWith("-"));
|
|
3356
3190
|
if (key && key !== "image") {
|
|
3357
|
-
error(
|
|
3358
|
-
info("
|
|
3191
|
+
error(`Unknown config key: ${key}`);
|
|
3192
|
+
info("Available: image");
|
|
3359
3193
|
return;
|
|
3360
3194
|
}
|
|
3361
3195
|
|
|
3362
3196
|
const res = await rpcQuery(creds, "apps.getImageConfig", { name: appId });
|
|
3363
3197
|
if (res.ok) {
|
|
3364
3198
|
const config = await res.json();
|
|
3365
|
-
log(`\n${BOLD}${CYAN}
|
|
3199
|
+
log(`\n${BOLD}${CYAN}Image Config${RESET} — ${appId}\n`);
|
|
3366
3200
|
if (Object.keys(config).length === 0) {
|
|
3367
|
-
info("
|
|
3201
|
+
info("No custom config (using tier defaults)");
|
|
3368
3202
|
} else {
|
|
3369
3203
|
if (config.autoMaxWidth) log(` ${GREEN}autoMaxWidth${RESET} ${config.autoMaxWidth} px`);
|
|
3370
3204
|
if (config.autoQuality) log(` ${GREEN}autoQuality${RESET} ${config.autoQuality}`);
|
|
3371
3205
|
}
|
|
3372
3206
|
log("");
|
|
3373
3207
|
} else {
|
|
3374
|
-
error("
|
|
3208
|
+
error("Failed to get config");
|
|
3375
3209
|
}
|
|
3376
3210
|
break;
|
|
3377
3211
|
}
|
|
@@ -3379,7 +3213,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3379
3213
|
case "reset": {
|
|
3380
3214
|
const key = restArgs.find(a => !a.startsWith("-"));
|
|
3381
3215
|
if (key && key !== "image") {
|
|
3382
|
-
error(
|
|
3216
|
+
error(`Unknown config key: ${key}`);
|
|
3383
3217
|
return;
|
|
3384
3218
|
}
|
|
3385
3219
|
|
|
@@ -3389,16 +3223,16 @@ ${BOLD}Examples:${RESET}
|
|
|
3389
3223
|
imageConfig: { autoMaxWidth: 0, autoQuality: 0 },
|
|
3390
3224
|
});
|
|
3391
3225
|
if (res.ok) {
|
|
3392
|
-
success("
|
|
3226
|
+
success("Image config reset (using tier defaults)");
|
|
3393
3227
|
} else {
|
|
3394
|
-
error("
|
|
3228
|
+
error("Reset failed");
|
|
3395
3229
|
}
|
|
3396
3230
|
break;
|
|
3397
3231
|
}
|
|
3398
3232
|
|
|
3399
3233
|
default:
|
|
3400
|
-
error(
|
|
3401
|
-
info("
|
|
3234
|
+
error(`Unknown subcommand: ${subCmd}`);
|
|
3235
|
+
info("Usage: gencow config [set|get|reset] ...");
|
|
3402
3236
|
}
|
|
3403
3237
|
},
|
|
3404
3238
|
|
|
@@ -3407,29 +3241,32 @@ ${BOLD}Examples:${RESET}
|
|
|
3407
3241
|
const subCmd = filesArgs[0] || "help";
|
|
3408
3242
|
const restArgs = filesArgs.slice(1);
|
|
3409
3243
|
|
|
3410
|
-
// help
|
|
3244
|
+
// help
|
|
3411
3245
|
if (subCmd === "help" || subCmd === "--help" || subCmd === "-h") {
|
|
3412
|
-
log(`\n${BOLD}${CYAN}gencow files${RESET} —
|
|
3413
|
-
log(` ${CYAN}upload${RESET}
|
|
3414
|
-
log(` ${CYAN}list${RESET}
|
|
3415
|
-
log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y]
|
|
3416
|
-
log(` ${CYAN}url${RESET} <storage_id>
|
|
3417
|
-
log(`\n ${DIM}
|
|
3418
|
-
log(` ${DIM}--app, -a
|
|
3246
|
+
log(`\n${BOLD}${CYAN}gencow files${RESET} — File management\n`);
|
|
3247
|
+
log(` ${CYAN}upload${RESET} <path...> [--recursive|-r] Upload files`);
|
|
3248
|
+
log(` ${CYAN}list${RESET} List uploaded files`);
|
|
3249
|
+
log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y] Delete a file`);
|
|
3250
|
+
log(` ${CYAN}url${RESET} <storage_id> Get serving URL`);
|
|
3251
|
+
log(`\n ${DIM}Options:${RESET}`);
|
|
3252
|
+
log(` ${DIM}--app, -a <name> Target app (default: from gencow.json)${RESET}`);
|
|
3253
|
+
log(` ${DIM}--prod Target production app${RESET}\n`);
|
|
3419
3254
|
return;
|
|
3420
3255
|
}
|
|
3421
3256
|
|
|
3422
3257
|
const creds = requireCreds();
|
|
3423
3258
|
|
|
3424
|
-
//
|
|
3259
|
+
// Resolve appId + track --app/--prod flag positions
|
|
3425
3260
|
let appId = null;
|
|
3426
|
-
const
|
|
3261
|
+
const isProd = restArgs.includes("--prod");
|
|
3262
|
+
const flagIndices = new Set();
|
|
3427
3263
|
for (let i = 0; i < restArgs.length; i++) {
|
|
3428
3264
|
if (restArgs[i] === "--app" || restArgs[i] === "-a") {
|
|
3429
3265
|
flagIndices.add(i);
|
|
3430
3266
|
flagIndices.add(i + 1);
|
|
3431
3267
|
appId = restArgs[++i];
|
|
3432
3268
|
}
|
|
3269
|
+
if (restArgs[i] === "--prod") flagIndices.add(i);
|
|
3433
3270
|
}
|
|
3434
3271
|
|
|
3435
3272
|
if (!appId) {
|
|
@@ -3437,11 +3274,19 @@ ${BOLD}Examples:${RESET}
|
|
|
3437
3274
|
if (existsSync(gencowJsonPath)) {
|
|
3438
3275
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
3439
3276
|
appId = gencowJson.appId || gencowJson.appName;
|
|
3277
|
+
if (isProd && gencowJson.prodApp) {
|
|
3278
|
+
appId = gencowJson.prodApp;
|
|
3279
|
+
}
|
|
3440
3280
|
}
|
|
3441
3281
|
}
|
|
3442
3282
|
|
|
3283
|
+
if (isProd && appId && !appId.endsWith("-prod")) {
|
|
3284
|
+
error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3443
3288
|
if (!appId) {
|
|
3444
|
-
error("
|
|
3289
|
+
error("App not found. Run 'gencow dev' first.");
|
|
3445
3290
|
return;
|
|
3446
3291
|
}
|
|
3447
3292
|
|
|
@@ -3472,11 +3317,11 @@ ${BOLD}Examples:${RESET}
|
|
|
3472
3317
|
|
|
3473
3318
|
// 로컬 크기 검증 (서버 왕복 방지)
|
|
3474
3319
|
if (stat.size > MAX_FILE_SIZE) {
|
|
3475
|
-
error(`${fileName}: ${fmtFileSize(stat.size)} — 50MB
|
|
3320
|
+
error(`${fileName}: ${fmtFileSize(stat.size)} — exceeds 50MB limit`);
|
|
3476
3321
|
return null;
|
|
3477
3322
|
}
|
|
3478
3323
|
|
|
3479
|
-
info(
|
|
3324
|
+
info(`Uploading ${fileName} (${fmtFileSize(stat.size)})...`);
|
|
3480
3325
|
|
|
3481
3326
|
const fileBuffer = readFileSync(filePath);
|
|
3482
3327
|
const blob = new Blob([fileBuffer]);
|
|
@@ -3533,7 +3378,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3533
3378
|
const paths = restArgs.filter((a, i) => !a.startsWith("-") && !flagIndices.has(i));
|
|
3534
3379
|
|
|
3535
3380
|
if (paths.length === 0) {
|
|
3536
|
-
error("
|
|
3381
|
+
error("Usage: gencow files upload <file|dir path...> [--recursive|-r]");
|
|
3537
3382
|
return;
|
|
3538
3383
|
}
|
|
3539
3384
|
|
|
@@ -3541,13 +3386,13 @@ ${BOLD}Examples:${RESET}
|
|
|
3541
3386
|
for (const p of paths) {
|
|
3542
3387
|
const resolved = resolve(process.cwd(), p);
|
|
3543
3388
|
if (!existsSync(resolved)) {
|
|
3544
|
-
error(
|
|
3389
|
+
error(`File not found: ${p}`);
|
|
3545
3390
|
continue;
|
|
3546
3391
|
}
|
|
3547
3392
|
const stat = statSync(resolved);
|
|
3548
3393
|
if (stat.isDirectory()) {
|
|
3549
3394
|
if (!recursive) {
|
|
3550
|
-
error(`${p}
|
|
3395
|
+
error(`${p} is a directory. Use --recursive (-r) flag.`);
|
|
3551
3396
|
continue;
|
|
3552
3397
|
}
|
|
3553
3398
|
filesToUpload.push(...collectFiles(resolved));
|
|
@@ -3557,12 +3402,12 @@ ${BOLD}Examples:${RESET}
|
|
|
3557
3402
|
}
|
|
3558
3403
|
|
|
3559
3404
|
if (filesToUpload.length === 0) {
|
|
3560
|
-
error("
|
|
3405
|
+
error("No files to upload.");
|
|
3561
3406
|
return;
|
|
3562
3407
|
}
|
|
3563
3408
|
|
|
3564
|
-
log(`\n${BOLD}${CYAN}
|
|
3565
|
-
info(
|
|
3409
|
+
log(`\n${BOLD}${CYAN}File Upload${RESET} — ${appId}\n`);
|
|
3410
|
+
info(`Uploading ${filesToUpload.length} files...\n`);
|
|
3566
3411
|
|
|
3567
3412
|
let uploaded = 0;
|
|
3568
3413
|
let failed = 0;
|
|
@@ -3573,14 +3418,14 @@ ${BOLD}Examples:${RESET}
|
|
|
3573
3418
|
}
|
|
3574
3419
|
|
|
3575
3420
|
log("");
|
|
3576
|
-
if (uploaded > 0) success(`${uploaded}
|
|
3577
|
-
if (failed > 0) warn(`${failed}
|
|
3421
|
+
if (uploaded > 0) success(`${uploaded} files uploaded`);
|
|
3422
|
+
if (failed > 0) warn(`${failed} files failed`);
|
|
3578
3423
|
break;
|
|
3579
3424
|
}
|
|
3580
3425
|
|
|
3581
3426
|
case "list":
|
|
3582
3427
|
case "ls": {
|
|
3583
|
-
log(`\n${BOLD}${CYAN}
|
|
3428
|
+
log(`\n${BOLD}${CYAN}File List${RESET} — ${appId}\n`);
|
|
3584
3429
|
|
|
3585
3430
|
const res = await fetch(
|
|
3586
3431
|
`${creds.platformUrl}/platform/files/list`,
|
|
@@ -3596,13 +3441,13 @@ ${BOLD}Examples:${RESET}
|
|
|
3596
3441
|
|
|
3597
3442
|
const data = await res.json().catch(() => []);
|
|
3598
3443
|
if (!res.ok) {
|
|
3599
|
-
error(data.error || "
|
|
3444
|
+
error(data.error || "Failed to list files");
|
|
3600
3445
|
return;
|
|
3601
3446
|
}
|
|
3602
3447
|
|
|
3603
3448
|
const files = Array.isArray(data) ? data : [];
|
|
3604
3449
|
if (files.length === 0) {
|
|
3605
|
-
info("
|
|
3450
|
+
info("No files stored.");
|
|
3606
3451
|
log("");
|
|
3607
3452
|
return;
|
|
3608
3453
|
}
|
|
@@ -3611,7 +3456,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3611
3456
|
const idWidth = Math.max(12, ...files.map(f => (f.storage_id || "").length));
|
|
3612
3457
|
const nameWidth = Math.max(8, ...files.map(f => (f.name || "").length).map(l => Math.min(l, 40)));
|
|
3613
3458
|
|
|
3614
|
-
log(` ${DIM}${"ID".padEnd(idWidth)} ${"
|
|
3459
|
+
log(` ${DIM}${"ID".padEnd(idWidth)} ${"Name".padEnd(nameWidth)} ${"Size".padStart(10)} ${"Source".padEnd(10)} Uploaded${RESET}`);
|
|
3615
3460
|
log(` ${DIM}${"─".repeat(idWidth)} ${"─".repeat(nameWidth)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(16)}${RESET}`);
|
|
3616
3461
|
|
|
3617
3462
|
for (const f of files) {
|
|
@@ -3621,7 +3466,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3621
3466
|
const date = f.created_at ? fmtDate(f.created_at) : "-";
|
|
3622
3467
|
log(` ${(f.storage_id || "").padEnd(idWidth)} ${name.padEnd(nameWidth)} ${size} ${source} ${date}`);
|
|
3623
3468
|
}
|
|
3624
|
-
log(`\n ${DIM}
|
|
3469
|
+
log(`\n ${DIM}Total: ${files.length} files${RESET}\n`);
|
|
3625
3470
|
break;
|
|
3626
3471
|
}
|
|
3627
3472
|
|
|
@@ -3629,7 +3474,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3629
3474
|
case "rm": {
|
|
3630
3475
|
const storageId = restArgs.find((a, i) => !a.startsWith("-") && !flagIndices.has(i));
|
|
3631
3476
|
if (!storageId) {
|
|
3632
|
-
error("
|
|
3477
|
+
error("Usage: gencow files delete <storage_id>");
|
|
3633
3478
|
return;
|
|
3634
3479
|
}
|
|
3635
3480
|
|
|
@@ -3645,7 +3490,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3645
3490
|
});
|
|
3646
3491
|
});
|
|
3647
3492
|
if (answer !== "y" && answer !== "yes") {
|
|
3648
|
-
info("
|
|
3493
|
+
info("Delete cancelled.");
|
|
3649
3494
|
return;
|
|
3650
3495
|
}
|
|
3651
3496
|
}
|
|
@@ -3664,9 +3509,9 @@ ${BOLD}Examples:${RESET}
|
|
|
3664
3509
|
|
|
3665
3510
|
const data = await res.json().catch(() => ({}));
|
|
3666
3511
|
if (res.ok && data.success) {
|
|
3667
|
-
success(
|
|
3512
|
+
success(`File deleted: ${storageId}`);
|
|
3668
3513
|
} else {
|
|
3669
|
-
error(
|
|
3514
|
+
error(`Delete failed: ${data.error || res.statusText}`);
|
|
3670
3515
|
}
|
|
3671
3516
|
break;
|
|
3672
3517
|
}
|
|
@@ -3674,7 +3519,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3674
3519
|
case "url": {
|
|
3675
3520
|
const storageId = restArgs.find((a, i) => !a.startsWith("-") && !flagIndices.has(i));
|
|
3676
3521
|
if (!storageId) {
|
|
3677
|
-
error("
|
|
3522
|
+
error("Usage: gencow files url <storage_id>");
|
|
3678
3523
|
return;
|
|
3679
3524
|
}
|
|
3680
3525
|
log(getFileUrl(storageId));
|
|
@@ -3682,14 +3527,14 @@ ${BOLD}Examples:${RESET}
|
|
|
3682
3527
|
}
|
|
3683
3528
|
|
|
3684
3529
|
default:
|
|
3685
|
-
if (subCmd !== "help") error(
|
|
3686
|
-
log(`\n${BOLD}${CYAN}gencow files${RESET} —
|
|
3687
|
-
log(` ${CYAN}upload${RESET}
|
|
3688
|
-
log(` ${CYAN}list${RESET}
|
|
3689
|
-
log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y]
|
|
3690
|
-
log(` ${CYAN}url${RESET} <storage_id>
|
|
3691
|
-
log(`\n ${DIM}
|
|
3692
|
-
log(` ${DIM}--app, -a
|
|
3530
|
+
if (subCmd !== "help") error(`Unknown subcommand: ${subCmd}`);
|
|
3531
|
+
log(`\n${BOLD}${CYAN}gencow files${RESET} — File management\n`);
|
|
3532
|
+
log(` ${CYAN}upload${RESET} <path...> [--recursive|-r] Upload files`);
|
|
3533
|
+
log(` ${CYAN}list${RESET} List files`);
|
|
3534
|
+
log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y] Delete file`);
|
|
3535
|
+
log(` ${CYAN}url${RESET} <storage_id> Get serving URL`);
|
|
3536
|
+
log(`\n ${DIM}Options:${RESET}`);
|
|
3537
|
+
log(` ${DIM}--app, -a <name> Target app (default: gencow.json)${RESET}\n`);
|
|
3693
3538
|
}
|
|
3694
3539
|
},
|
|
3695
3540
|
|
|
@@ -3714,7 +3559,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3714
3559
|
}
|
|
3715
3560
|
|
|
3716
3561
|
if (!appId) {
|
|
3717
|
-
error("
|
|
3562
|
+
error("App not found. Run gencow deploy first.");
|
|
3718
3563
|
return;
|
|
3719
3564
|
}
|
|
3720
3565
|
|
|
@@ -3736,7 +3581,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3736
3581
|
case "ls": {
|
|
3737
3582
|
const res = await rpcQuery(creds, "backup.list", { appName: appId });
|
|
3738
3583
|
if (!res.ok) {
|
|
3739
|
-
error(
|
|
3584
|
+
error(`Failed to fetch backup list: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
|
|
3740
3585
|
return;
|
|
3741
3586
|
}
|
|
3742
3587
|
const data = await res.json();
|
|
@@ -3746,8 +3591,8 @@ ${BOLD}Examples:${RESET}
|
|
|
3746
3591
|
log(`\n${BOLD}${CYAN}Database Backups${RESET} — ${appId} ${DIM}(Plan: ${data.plan || "free"})${RESET}\n`);
|
|
3747
3592
|
|
|
3748
3593
|
if (backups.length === 0) {
|
|
3749
|
-
info("
|
|
3750
|
-
info(`${GREEN}gencow backup create${RESET} —
|
|
3594
|
+
info("No backups found.");
|
|
3595
|
+
info(`${GREEN}gencow backup create${RESET} — create a manual backup`);
|
|
3751
3596
|
} else {
|
|
3752
3597
|
for (const b of backups) {
|
|
3753
3598
|
const statusColor = b.status === "completed" ? GREEN : b.status === "failed" ? RED : YELLOW;
|
|
@@ -3765,19 +3610,19 @@ ${BOLD}Examples:${RESET}
|
|
|
3765
3610
|
|
|
3766
3611
|
const res = await rpcMutation(creds, "backup.create", { appName: appId, note });
|
|
3767
3612
|
if (!res.ok) {
|
|
3768
|
-
error(
|
|
3613
|
+
error(`Backup creation failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
|
|
3769
3614
|
return;
|
|
3770
3615
|
}
|
|
3771
3616
|
const backup = await res.json();
|
|
3772
|
-
success(
|
|
3617
|
+
success(`Backup created #${backup.id} ${fmtSize(backup.fileSize || 0)}`);
|
|
3773
3618
|
break;
|
|
3774
3619
|
}
|
|
3775
3620
|
|
|
3776
3621
|
case "restore": {
|
|
3777
3622
|
const backupId = parseInt(restArgs[0]);
|
|
3778
3623
|
if (!backupId || isNaN(backupId)) {
|
|
3779
|
-
error("
|
|
3780
|
-
info("gencow backup list
|
|
3624
|
+
error("Specify backup ID: gencow backup restore <id>");
|
|
3625
|
+
info("Run gencow backup list to check IDs.");
|
|
3781
3626
|
return;
|
|
3782
3627
|
}
|
|
3783
3628
|
|
|
@@ -3792,7 +3637,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3792
3637
|
});
|
|
3793
3638
|
|
|
3794
3639
|
if (answer !== "y" && answer !== "yes") {
|
|
3795
|
-
info("
|
|
3640
|
+
info("Restore cancelled.");
|
|
3796
3641
|
return;
|
|
3797
3642
|
}
|
|
3798
3643
|
|
|
@@ -3800,10 +3645,10 @@ ${BOLD}Examples:${RESET}
|
|
|
3800
3645
|
|
|
3801
3646
|
const res = await rpcMutation(creds, "backup.restore", { appName: appId, backupId });
|
|
3802
3647
|
if (!res.ok) {
|
|
3803
|
-
error(
|
|
3648
|
+
error(`Restore failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
|
|
3804
3649
|
return;
|
|
3805
3650
|
}
|
|
3806
|
-
success("
|
|
3651
|
+
success("Database restored. App restarted.");
|
|
3807
3652
|
break;
|
|
3808
3653
|
}
|
|
3809
3654
|
|
|
@@ -3811,23 +3656,23 @@ ${BOLD}Examples:${RESET}
|
|
|
3811
3656
|
case "rm": {
|
|
3812
3657
|
const backupId = parseInt(restArgs[0]);
|
|
3813
3658
|
if (!backupId || isNaN(backupId)) {
|
|
3814
|
-
error("
|
|
3659
|
+
error("Specify backup ID: gencow backup delete <id>");
|
|
3815
3660
|
return;
|
|
3816
3661
|
}
|
|
3817
3662
|
|
|
3818
3663
|
const res = await rpcMutation(creds, "backup.delete", { appName: appId, backupId });
|
|
3819
3664
|
if (!res.ok) {
|
|
3820
|
-
error(
|
|
3665
|
+
error(`Delete failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
|
|
3821
3666
|
return;
|
|
3822
3667
|
}
|
|
3823
|
-
success(
|
|
3668
|
+
success(`Backup #${backupId} deleted`);
|
|
3824
3669
|
break;
|
|
3825
3670
|
}
|
|
3826
3671
|
|
|
3827
3672
|
case "download": {
|
|
3828
3673
|
const backupId = parseInt(restArgs[0]);
|
|
3829
3674
|
if (!backupId || isNaN(backupId)) {
|
|
3830
|
-
error("
|
|
3675
|
+
error("Specify backup ID: gencow backup download <id>");
|
|
3831
3676
|
return;
|
|
3832
3677
|
}
|
|
3833
3678
|
|
|
@@ -3837,7 +3682,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3837
3682
|
`/platform/apps/${appId}/backups/${backupId}/download`
|
|
3838
3683
|
);
|
|
3839
3684
|
if (!res.ok) {
|
|
3840
|
-
error(
|
|
3685
|
+
error(`Download failed: ${(await res.json?.().catch(() => ({}))).error || res.statusText}`);
|
|
3841
3686
|
return;
|
|
3842
3687
|
}
|
|
3843
3688
|
|
|
@@ -3845,12 +3690,12 @@ ${BOLD}Examples:${RESET}
|
|
|
3845
3690
|
const { writeFile } = await import("fs/promises");
|
|
3846
3691
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
3847
3692
|
await writeFile(filename, buffer);
|
|
3848
|
-
success(
|
|
3693
|
+
success(`Saved: ${filename} (${fmtSize(buffer.length)})`);
|
|
3849
3694
|
break;
|
|
3850
3695
|
}
|
|
3851
3696
|
|
|
3852
3697
|
default:
|
|
3853
|
-
error(
|
|
3698
|
+
error(`Unknown sub-command: ${subCmd}`);
|
|
3854
3699
|
log(`\n ${BOLD}Usage:${RESET}`);
|
|
3855
3700
|
log(` gencow backup list List all backups`);
|
|
3856
3701
|
log(` gencow backup create [note] Create a manual backup`);
|
|
@@ -3881,7 +3726,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3881
3726
|
}
|
|
3882
3727
|
|
|
3883
3728
|
if (!appId) {
|
|
3884
|
-
error("
|
|
3729
|
+
error("App not found. Run gencow deploy first.");
|
|
3885
3730
|
return;
|
|
3886
3731
|
}
|
|
3887
3732
|
|
|
@@ -3889,8 +3734,8 @@ ${BOLD}Examples:${RESET}
|
|
|
3889
3734
|
case "set": {
|
|
3890
3735
|
const domain = restArgs.find(a => !a.startsWith("-") && a.includes("."));
|
|
3891
3736
|
if (!domain) {
|
|
3892
|
-
error("
|
|
3893
|
-
info("
|
|
3737
|
+
error("Usage: gencow domain set <domain>");
|
|
3738
|
+
info(" Example: gencow domain set myapp.com");
|
|
3894
3739
|
return;
|
|
3895
3740
|
}
|
|
3896
3741
|
|
|
@@ -3902,32 +3747,32 @@ ${BOLD}Examples:${RESET}
|
|
|
3902
3747
|
|
|
3903
3748
|
const data = await res.json();
|
|
3904
3749
|
if (!res.ok) {
|
|
3905
|
-
error(
|
|
3750
|
+
error(`Domain registration failed: ${data.error}`);
|
|
3906
3751
|
return;
|
|
3907
3752
|
}
|
|
3908
3753
|
|
|
3909
|
-
log(`\n${BOLD}${CYAN}🌐
|
|
3754
|
+
log(`\n${BOLD}${CYAN}🌐 Custom Domain Registered${RESET}\n`);
|
|
3910
3755
|
success(`${domain} → ${appId}`);
|
|
3911
3756
|
log("");
|
|
3912
3757
|
|
|
3913
3758
|
if (data.status === "active") {
|
|
3914
3759
|
info(`DNS: ${GREEN}✅ ${data.dns.detail}${RESET}`);
|
|
3915
|
-
info(`TLS:
|
|
3760
|
+
info(`TLS: Certificate auto-provisioned on first request`);
|
|
3916
3761
|
} else {
|
|
3917
|
-
warn(`DNS: ⏳
|
|
3762
|
+
warn(`DNS: ⏳ Not yet verified.`);
|
|
3918
3763
|
log("");
|
|
3919
|
-
log(`
|
|
3764
|
+
log(` Set up the following DNS record:`);
|
|
3920
3765
|
log(` ┌──────────────────────────────────────────┐`);
|
|
3921
3766
|
log(` │ ${BOLD}Type: A${RESET} │`);
|
|
3922
3767
|
log(` │ Name: ${domain.padEnd(33)}│`);
|
|
3923
3768
|
log(` │ Value: ${data.serverIp.padEnd(33)}│`);
|
|
3924
3769
|
log(` │ │`);
|
|
3925
|
-
log(` │ ${DIM}
|
|
3770
|
+
log(` │ ${DIM}Or CNAME:${RESET} │`);
|
|
3926
3771
|
log(` │ Value: ${data.cname.padEnd(33)}│`);
|
|
3927
3772
|
log(` └──────────────────────────────────────────┘`);
|
|
3928
3773
|
}
|
|
3929
3774
|
log("");
|
|
3930
|
-
info(`DNS
|
|
3775
|
+
info(`After DNS setup: ${DIM}gencow domain status${RESET}`);
|
|
3931
3776
|
log("");
|
|
3932
3777
|
break;
|
|
3933
3778
|
}
|
|
@@ -3942,14 +3787,14 @@ ${BOLD}Examples:${RESET}
|
|
|
3942
3787
|
|
|
3943
3788
|
const data = await res.json();
|
|
3944
3789
|
if (!res.ok) {
|
|
3945
|
-
error(
|
|
3790
|
+
error(`Domain removal failed: ${data.error}`);
|
|
3946
3791
|
return;
|
|
3947
3792
|
}
|
|
3948
3793
|
|
|
3949
3794
|
if (data.domain) {
|
|
3950
|
-
success(
|
|
3795
|
+
success(`Domain ${data.domain} disconnected`);
|
|
3951
3796
|
} else {
|
|
3952
|
-
info("
|
|
3797
|
+
info("No custom domain configured.");
|
|
3953
3798
|
}
|
|
3954
3799
|
break;
|
|
3955
3800
|
}
|
|
@@ -3962,26 +3807,26 @@ ${BOLD}Examples:${RESET}
|
|
|
3962
3807
|
|
|
3963
3808
|
const data = await res.json();
|
|
3964
3809
|
if (!res.ok) {
|
|
3965
|
-
error(
|
|
3810
|
+
error(`Domain status check failed: ${data.error}`);
|
|
3966
3811
|
return;
|
|
3967
3812
|
}
|
|
3968
3813
|
|
|
3969
|
-
log(`\n${BOLD}${CYAN}🌐
|
|
3814
|
+
log(`\n${BOLD}${CYAN}🌐 Custom Domain Status${RESET} — ${appId}\n`);
|
|
3970
3815
|
|
|
3971
3816
|
if (!data.domain) {
|
|
3972
|
-
info("
|
|
3973
|
-
info(
|
|
3817
|
+
info("No custom domain configured.");
|
|
3818
|
+
info(`Set up: ${DIM}gencow domain set <domain>${RESET}`);
|
|
3974
3819
|
} else {
|
|
3975
|
-
info(
|
|
3820
|
+
info(`Domain: ${BOLD}${data.domain}${RESET}`);
|
|
3976
3821
|
|
|
3977
3822
|
if (data.status === "active") {
|
|
3978
3823
|
info(`DNS: ${GREEN}✅ ${data.dns.detail}${RESET}`);
|
|
3979
|
-
info(`TLS: ${GREEN}✅ Let's Encrypt
|
|
3824
|
+
info(`TLS: ${GREEN}✅ Let's Encrypt auto-provisioned${RESET}`);
|
|
3980
3825
|
} else if (data.status === "pending") {
|
|
3981
|
-
warn(`DNS: ⏳
|
|
3982
|
-
info(`TLS:
|
|
3826
|
+
warn(`DNS: ⏳ Verification pending`);
|
|
3827
|
+
info(`TLS: Auto-provisioned after DNS verification`);
|
|
3983
3828
|
} else {
|
|
3984
|
-
error(
|
|
3829
|
+
error(`Status: ${data.status}`);
|
|
3985
3830
|
}
|
|
3986
3831
|
}
|
|
3987
3832
|
log("");
|
|
@@ -3990,28 +3835,9 @@ ${BOLD}Examples:${RESET}
|
|
|
3990
3835
|
}
|
|
3991
3836
|
},
|
|
3992
3837
|
|
|
3993
|
-
async mcp(
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
if (mcpArgs[i] === "--port" || mcpArgs[i] === "-p") port = mcpArgs[++i];
|
|
3997
|
-
}
|
|
3998
|
-
|
|
3999
|
-
const mcpScript = resolve(fileURLToPath(import.meta.url), "..", "gencow-mcp.mjs");
|
|
4000
|
-
if (!existsSync(mcpScript)) {
|
|
4001
|
-
error("gencow-mcp.mjs not found. Ensure the CLI package is properly installed.");
|
|
4002
|
-
process.exit(1);
|
|
4003
|
-
}
|
|
4004
|
-
|
|
4005
|
-
// MCP stdio 서버를 직접 실행 (stdin/stdout 상속)
|
|
4006
|
-
const { execFileSync } = await import("child_process");
|
|
4007
|
-
try {
|
|
4008
|
-
execFileSync("node", [mcpScript], {
|
|
4009
|
-
stdio: "inherit",
|
|
4010
|
-
env: { ...process.env, GENCOW_PORT: port },
|
|
4011
|
-
});
|
|
4012
|
-
} catch {
|
|
4013
|
-
// MCP 서버가 종료될 때 정상적으로 종료
|
|
4014
|
-
}
|
|
3838
|
+
async mcp() {
|
|
3839
|
+
info(`MCP server is ${BOLD}coming soon${RESET}.`);
|
|
3840
|
+
info(`${DIM}MCP integration requires local dev server (coming soon).${RESET}\n`);
|
|
4015
3841
|
},
|
|
4016
3842
|
|
|
4017
3843
|
|
|
@@ -4046,8 +3872,16 @@ ${BOLD}Examples:${RESET}
|
|
|
4046
3872
|
let runtimeRoot;
|
|
4047
3873
|
try { runtimeRoot = findServerRoot(); } catch { runtimeRoot = process.cwd(); }
|
|
4048
3874
|
const extractTsPath = resolve(runtimeRoot, ".gencow-extract.ts");
|
|
3875
|
+
|
|
3876
|
+
// @gencow/core — 번들된 core 절대 경로 우선, 없으면 bare specifier
|
|
3877
|
+
// generateApiTs()와 동일 해석 전략: 글로벌 CLI에서도 resolve 보장
|
|
3878
|
+
const bundledCorePath = resolve(runtimeRoot, "../core/index.js");
|
|
3879
|
+
const coreImport = existsSync(bundledCorePath)
|
|
3880
|
+
? bundledCorePath.replace(/\\/g, "/")
|
|
3881
|
+
: "@gencow/core";
|
|
3882
|
+
|
|
4049
3883
|
const script = `
|
|
4050
|
-
import { getRegisteredQueries, getRegisteredMutations } from "
|
|
3884
|
+
import { getRegisteredQueries, getRegisteredMutations } from "${coreImport}";
|
|
4051
3885
|
import "${absoluteFunctions}/index.ts";
|
|
4052
3886
|
console.log("GENCOW_API_JSON=" + JSON.stringify({
|
|
4053
3887
|
queries: getRegisteredQueries(),
|
|
@@ -4055,14 +3889,23 @@ console.log("GENCOW_API_JSON=" + JSON.stringify({
|
|
|
4055
3889
|
}));
|
|
4056
3890
|
process.exit(0);
|
|
4057
3891
|
`;
|
|
4058
|
-
info("
|
|
3892
|
+
info("Extracting function list...");
|
|
3893
|
+
// NODE_PATH: 사용자 프로젝트 node_modules + CLI node_modules 포함
|
|
3894
|
+
// generateApiTs()와 동일 — 글로벌 설치 시 사용자 의존성 해석 보장
|
|
3895
|
+
const cwdNodeModules = resolve(process.cwd(), "node_modules");
|
|
3896
|
+
const cliNodeModules = resolve(__dirname, "..", "node_modules");
|
|
3897
|
+
const extractEnv = {
|
|
3898
|
+
...process.env,
|
|
3899
|
+
NODE_PATH: [cwdNodeModules, cliNodeModules, process.env.NODE_PATH || ""].filter(Boolean).join(":"),
|
|
3900
|
+
};
|
|
4059
3901
|
try {
|
|
4060
3902
|
writeFileSync(extractTsPath, script);
|
|
4061
3903
|
let outStr;
|
|
4062
3904
|
try {
|
|
4063
3905
|
outStr = execSync(`bun ${extractTsPath}`, {
|
|
4064
3906
|
cwd: runtimeRoot,
|
|
4065
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
3907
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3908
|
+
env: extractEnv,
|
|
4066
3909
|
}).toString();
|
|
4067
3910
|
} catch (execErr) {
|
|
4068
3911
|
const stderr = execErr.stderr?.toString() || "";
|
|
@@ -4119,10 +3962,10 @@ process.exit(0);
|
|
|
4119
3962
|
log(` ${DIM}Queries: ${queries.join(", ")}${RESET}`);
|
|
4120
3963
|
log(` ${DIM}Mutations: ${mutations.join(", ")}${RESET}`);
|
|
4121
3964
|
log("");
|
|
4122
|
-
info(
|
|
3965
|
+
info(`Usage: import { api } from "@/gencow/api";`);
|
|
4123
3966
|
} catch (e) {
|
|
4124
3967
|
try { unlinkSync(extractTsPath); } catch { }
|
|
4125
|
-
error(`Codegen
|
|
3968
|
+
error(`Codegen failed: ${e.message}`);
|
|
4126
3969
|
process.exit(1);
|
|
4127
3970
|
}
|
|
4128
3971
|
},
|
|
@@ -4278,57 +4121,7 @@ process.exit(0);
|
|
|
4278
4121
|
log(` Usage: gencow app [list|create|delete|status]`);
|
|
4279
4122
|
},
|
|
4280
4123
|
|
|
4281
|
-
// ── deploy (
|
|
4282
|
-
async "remote:deploy"() {
|
|
4283
|
-
const creds = requireCreds();
|
|
4284
|
-
const config = loadConfig();
|
|
4285
|
-
const appName = config.deploy?.app || creds.currentApp;
|
|
4286
|
-
if (!appName) {
|
|
4287
|
-
error("No app specified. Run: gencow app create <name>");
|
|
4288
|
-
process.exit(1);
|
|
4289
|
-
}
|
|
4290
|
-
|
|
4291
|
-
log(`\n${BOLD}${CYAN}Gencow Deploy → ${appName}${RESET}\n`);
|
|
4292
|
-
|
|
4293
|
-
const { functionsDir } = config;
|
|
4294
|
-
const absoluteFunctions = resolve(process.cwd(), functionsDir);
|
|
4295
|
-
if (!existsSync(absoluteFunctions)) {
|
|
4296
|
-
error(`Functions dir not found: ${absoluteFunctions}`);
|
|
4297
|
-
process.exit(1);
|
|
4298
|
-
}
|
|
4299
|
-
|
|
4300
|
-
// 1. Bundle gencow/ folder to tar.gz
|
|
4301
|
-
info(`Bundling ${functionsDir}...`);
|
|
4302
|
-
const { create: tarCreate } = await import("tar");
|
|
4303
|
-
const chunks = [];
|
|
4304
|
-
await new Promise((resolve, reject) => {
|
|
4305
|
-
tarCreate({ cwd: absoluteFunctions, gzip: true }, ["."])
|
|
4306
|
-
.on("data", c => chunks.push(c))
|
|
4307
|
-
.on("end", resolve)
|
|
4308
|
-
.on("error", reject);
|
|
4309
|
-
});
|
|
4310
|
-
const bundle = Buffer.concat(chunks);
|
|
4311
|
-
success(`Bundle ready (${(bundle.length / 1024).toFixed(1)} KB)`);
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
// 2. Upload to platform
|
|
4315
|
-
info(`Uploading to ${creds.platformUrl}...`);
|
|
4316
|
-
const form = new FormData();
|
|
4317
|
-
form.append("bundle", new Blob([bundle], { type: "application/gzip" }), "bundle.tar.gz");
|
|
4318
|
-
const res = await platformFetch(creds, `/platform/apps/${appName}/deploy`, {
|
|
4319
|
-
method: "POST",
|
|
4320
|
-
body: form,
|
|
4321
|
-
});
|
|
4322
|
-
const data = await res.json();
|
|
4323
|
-
if (!res.ok) { error(data.error); process.exit(1); }
|
|
4324
|
-
|
|
4325
|
-
success("Deployed!");
|
|
4326
|
-
log(`
|
|
4327
|
-
${GREEN}▸${RESET} URL: ${data.url}
|
|
4328
|
-
${GREEN}▸${RESET} Bundle: ${data.bundleHash}
|
|
4329
|
-
${DIM}gencow logs — follow logs${RESET}
|
|
4330
|
-
`);
|
|
4331
|
-
},
|
|
4124
|
+
// ── remote:deploy — REMOVED (legacy, use `gencow deploy` instead) ──
|
|
4332
4125
|
|
|
4333
4126
|
// ── logs ───────────────────────────────────────────
|
|
4334
4127
|
async logs(...args) {
|
|
@@ -4427,13 +4220,13 @@ process.exit(0);
|
|
|
4427
4220
|
}
|
|
4428
4221
|
followFailures++;
|
|
4429
4222
|
if (followFailures > FOLLOW_MAX_FAILURES) {
|
|
4430
|
-
error(
|
|
4431
|
-
info(
|
|
4223
|
+
error(`Connection failed — check app status.`);
|
|
4224
|
+
info(`Check if the server is running: gencow dev`);
|
|
4432
4225
|
process.exit(1);
|
|
4433
4226
|
}
|
|
4434
4227
|
const delay = Math.min(FOLLOW_BASE_RECONNECT_MS * Math.pow(2, followFailures - 1), FOLLOW_MAX_RECONNECT_MS);
|
|
4435
4228
|
const delaySec = (delay / 1000).toFixed(0);
|
|
4436
|
-
warn(
|
|
4229
|
+
warn(`Disconnected — reconnecting in ${delaySec}s... (${followFailures}/${FOLLOW_MAX_FAILURES})`);
|
|
4437
4230
|
scheduleFollowReconnect(delay);
|
|
4438
4231
|
});
|
|
4439
4232
|
}
|
|
@@ -4675,7 +4468,7 @@ process.exit(0);
|
|
|
4675
4468
|
|
|
4676
4469
|
// 앱이 없으면 자동 생성 (deploy의 auto-create 패턴 재사용)
|
|
4677
4470
|
if (!appName) {
|
|
4678
|
-
info("
|
|
4471
|
+
info("No app found. Creating...");
|
|
4679
4472
|
|
|
4680
4473
|
// displayName: gencow.config.ts의 name 또는 프로젝트 폴더명
|
|
4681
4474
|
let displayName = null;
|
|
@@ -4690,7 +4483,7 @@ process.exit(0);
|
|
|
4690
4483
|
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
4691
4484
|
if (!createRes.ok) {
|
|
4692
4485
|
const createErr = await createRes.json().catch(() => ({}));
|
|
4693
|
-
error(
|
|
4486
|
+
error(`App creation failed: ${createErr.error || createRes.statusText}`);
|
|
4694
4487
|
process.exit(1);
|
|
4695
4488
|
}
|
|
4696
4489
|
|
|
@@ -4698,11 +4491,11 @@ process.exit(0);
|
|
|
4698
4491
|
appName = createData.appId || createData.name;
|
|
4699
4492
|
|
|
4700
4493
|
if (!appName) {
|
|
4701
|
-
error("
|
|
4494
|
+
error("App creation response missing appId.");
|
|
4702
4495
|
process.exit(1);
|
|
4703
4496
|
}
|
|
4704
4497
|
|
|
4705
|
-
success(
|
|
4498
|
+
success(`App "${appName}" created`);
|
|
4706
4499
|
|
|
4707
4500
|
// gencow.json 저장 (deploy와 동일 패턴)
|
|
4708
4501
|
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
@@ -4711,13 +4504,13 @@ process.exit(0);
|
|
|
4711
4504
|
displayName,
|
|
4712
4505
|
platformUrl: creds.platformUrl,
|
|
4713
4506
|
}, null, 2));
|
|
4714
|
-
info(`${DIM}gencow.json
|
|
4507
|
+
info(`${DIM}gencow.json created (appId: ${appName})${RESET}`);
|
|
4715
4508
|
|
|
4716
4509
|
// .env에 VITE_API_URL 자동 설정
|
|
4717
4510
|
updateEnvLocalUrl(getAppUrl(appName, creds.platformUrl));
|
|
4718
4511
|
|
|
4719
4512
|
// 프로비저닝 대기
|
|
4720
|
-
info("
|
|
4513
|
+
info("Waiting for provisioning...");
|
|
4721
4514
|
await new Promise(r => setTimeout(r, 3000));
|
|
4722
4515
|
}
|
|
4723
4516
|
|
|
@@ -4736,9 +4529,9 @@ process.exit(0);
|
|
|
4736
4529
|
const isSchema = reason !== "initial" && /schema.*\.ts$/i.test(reason);
|
|
4737
4530
|
|
|
4738
4531
|
if (reason === "initial") {
|
|
4739
|
-
log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET}
|
|
4532
|
+
log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} Initial deploy...`);
|
|
4740
4533
|
} else {
|
|
4741
|
-
log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} ${reason}
|
|
4534
|
+
log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} ${reason} change detected → deploying...`);
|
|
4742
4535
|
}
|
|
4743
4536
|
|
|
4744
4537
|
// 스키마 변경 또는 초기 배포 시 마이그레이션 생성
|
|
@@ -4746,7 +4539,7 @@ process.exit(0);
|
|
|
4746
4539
|
const schemaExists = existsSync(resolve(process.cwd(), functionsDir, "schema.ts"));
|
|
4747
4540
|
if (schemaExists && (isSchema || reason === "initial")) {
|
|
4748
4541
|
if (isSchema) {
|
|
4749
|
-
log(`${DIM}${ts}${RESET} ${YELLOW}[migrate]${RESET}
|
|
4542
|
+
log(`${DIM}${ts}${RESET} ${YELLOW}[migrate]${RESET} Schema change detected → generating migrations...`);
|
|
4750
4543
|
}
|
|
4751
4544
|
try {
|
|
4752
4545
|
const { execSync } = await import("child_process");
|
|
@@ -4756,13 +4549,13 @@ process.exit(0);
|
|
|
4756
4549
|
env: { ...process.env, ...dk.env },
|
|
4757
4550
|
stdio: "inherit",
|
|
4758
4551
|
});
|
|
4759
|
-
log(`${DIM}${ts}${RESET} ${GREEN}[migrate]${RESET} ✔
|
|
4552
|
+
log(`${DIM}${ts}${RESET} ${GREEN}[migrate]${RESET} ✔ Migrations generated`);
|
|
4760
4553
|
} catch (e) {
|
|
4761
4554
|
const msg = e.stderr?.toString() || e.message || "";
|
|
4762
4555
|
if (msg.includes("No schema changes") || msg.includes("nothing to migrate")) {
|
|
4763
4556
|
// 스키마 변경 없음 — 정상
|
|
4764
4557
|
} else {
|
|
4765
|
-
warn(`[migrate]
|
|
4558
|
+
warn(`[migrate] Migration generation failed (non-critical): ${msg.split("\n")[0]}`);
|
|
4766
4559
|
}
|
|
4767
4560
|
}
|
|
4768
4561
|
}
|
|
@@ -4801,7 +4594,7 @@ process.exit(0);
|
|
|
4801
4594
|
|
|
4802
4595
|
if (!res.ok) {
|
|
4803
4596
|
const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
4804
|
-
log(`${DIM}${ts2}${RESET} ${RED}[deploy]${RESET} ✗
|
|
4597
|
+
log(`${DIM}${ts2}${RESET} ${RED}[deploy]${RESET} ✗ Deploy failed!`);
|
|
4805
4598
|
|
|
4806
4599
|
// 에러 메시지에서 파일:라인 정보 추출
|
|
4807
4600
|
const errMsg = data.error || "Unknown error";
|
|
@@ -4818,13 +4611,13 @@ process.exit(0);
|
|
|
4818
4611
|
|
|
4819
4612
|
// 스키마 관련 에러 힌트
|
|
4820
4613
|
if (errMsg.includes("does not exist") || errMsg.includes("relation")) {
|
|
4821
|
-
log(` ${YELLOW}💡
|
|
4614
|
+
log(` ${YELLOW}💡 Schema changed? Migrations run automatically when schema.ts changes.${RESET}`);
|
|
4822
4615
|
}
|
|
4823
4616
|
return;
|
|
4824
4617
|
}
|
|
4825
4618
|
|
|
4826
4619
|
const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
4827
|
-
log(`${DIM}${ts2}${RESET} ${GREEN}[deploy]${RESET} ✔
|
|
4620
|
+
log(`${DIM}${ts2}${RESET} ${GREEN}[deploy]${RESET} ✔ Deploy complete ${DIM}(${sizeKB}KB, ${elapsed}s)${RESET}`);
|
|
4828
4621
|
}
|
|
4829
4622
|
|
|
4830
4623
|
// ── 로그 포맷팅 함수 ────────────────────────────────
|
|
@@ -4857,7 +4650,7 @@ process.exit(0);
|
|
|
4857
4650
|
try {
|
|
4858
4651
|
logWs = new WS(wsUrl);
|
|
4859
4652
|
} catch (e) {
|
|
4860
|
-
warn(`[log] WebSocket
|
|
4653
|
+
warn(`[log] WebSocket connection failed: ${e.message}`);
|
|
4861
4654
|
scheduleReconnect();
|
|
4862
4655
|
return;
|
|
4863
4656
|
}
|
|
@@ -4865,7 +4658,7 @@ process.exit(0);
|
|
|
4865
4658
|
logWs.on("open", () => {
|
|
4866
4659
|
// 카운터 리셋은 close 이벤트에서 안정 연결 판정 후 수행
|
|
4867
4660
|
logWsOpenedAt = Date.now();
|
|
4868
|
-
log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${GREEN}[log]${RESET}
|
|
4661
|
+
log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${GREEN}[log]${RESET} Log streaming connected`);
|
|
4869
4662
|
logWs.send(JSON.stringify({ type: "log:subscribe" }));
|
|
4870
4663
|
});
|
|
4871
4664
|
|
|
@@ -4889,13 +4682,13 @@ process.exit(0);
|
|
|
4889
4682
|
}
|
|
4890
4683
|
reconnectFailures++;
|
|
4891
4684
|
if (reconnectFailures > MAX_RECONNECT_FAILURES) {
|
|
4892
|
-
warn(`[log]
|
|
4893
|
-
info(
|
|
4685
|
+
warn(`[log] Log streaming failed ${MAX_RECONNECT_FAILURES} times. Giving up.`);
|
|
4686
|
+
info(`Check manually: gencow deploy logs`);
|
|
4894
4687
|
return;
|
|
4895
4688
|
}
|
|
4896
4689
|
const delay = Math.min(BASE_RECONNECT_MS * Math.pow(2, reconnectFailures - 1), MAX_RECONNECT_MS);
|
|
4897
4690
|
const delaySec = (delay / 1000).toFixed(0);
|
|
4898
|
-
log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${YELLOW}[log]${RESET}
|
|
4691
|
+
log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${YELLOW}[log]${RESET} Disconnected — reconnecting in ${delaySec}s... (${reconnectFailures}/${MAX_RECONNECT_FAILURES})`);
|
|
4899
4692
|
scheduleReconnect(delay);
|
|
4900
4693
|
});
|
|
4901
4694
|
}
|
|
@@ -5003,7 +4796,8 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
|
|
|
5003
4796
|
log(` ${GREEN}Prompts${RESET} Reusable prompt templates`);
|
|
5004
4797
|
log(` ${GREEN}Parsers${RESET} PDF/HTML/CSV file parsing`);
|
|
5005
4798
|
log(` ${GREEN}Memory${RESET} Agent memory ${DIM}(episodic/semantic/procedural)${RESET}`);
|
|
5006
|
-
log(
|
|
4799
|
+
log(`\n${BOLD}Coming soon:${RESET}`);
|
|
4800
|
+
log(` ${DIM}Analytics${RESET} LLM call tracking`);
|
|
5007
4801
|
log(`\n${BOLD}Examples:${RESET}`);
|
|
5008
4802
|
log(` ${DIM}gencow add AI${RESET}`);
|
|
5009
4803
|
log(` ${DIM}gencow add AI RAG Reranker${RESET}`);
|
|
@@ -5050,10 +4844,11 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
|
|
|
5050
4844
|
env: {},
|
|
5051
4845
|
requires: ["ai"],
|
|
5052
4846
|
guide: [
|
|
5053
|
-
`gencow/rag.ts
|
|
5054
|
-
`
|
|
5055
|
-
`
|
|
5056
|
-
`
|
|
4847
|
+
`gencow/rag.ts — rag.ingest(), rag.search(), rag.ask()`,
|
|
4848
|
+
`Shared KB: await rag.ingest(ctx, "manual.pdf", text)`,
|
|
4849
|
+
`Personal: await rag.ingest(ctx, "notes.pdf", text, { userId: user.id })`,
|
|
4850
|
+
`Search: const results = await rag.search(ctx, "query", { userId: user.id })`,
|
|
4851
|
+
`Import schema-rag.ts in your schema.ts to create DB tables`,
|
|
5057
4852
|
],
|
|
5058
4853
|
},
|
|
5059
4854
|
memory: {
|
|
@@ -5178,7 +4973,7 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
|
|
|
5178
4973
|
// ── 모든 컴포넌트 설치 후 README 1회 업데이트 ───────────
|
|
5179
4974
|
await updateReadme(loadConfig());
|
|
5180
4975
|
|
|
5181
|
-
log(`\n${GREEN}✔${RESET} ${BOLD}
|
|
4976
|
+
log(`\n${GREEN}✔${RESET} ${BOLD}Done!${RESET} ${DIM}Run gencow dev to get started.${RESET}`);
|
|
5182
4977
|
|
|
5183
4978
|
// ── 컴포넌트별 가이드 메시지 ─────────────────────────────
|
|
5184
4979
|
for (const key of ordered) {
|
|
@@ -5200,11 +4995,11 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
|
|
|
5200
4995
|
const label = config.label || name.toUpperCase();
|
|
5201
4996
|
|
|
5202
4997
|
if (config.notReady) {
|
|
5203
|
-
log(` ${YELLOW}⚠${RESET} ${BOLD}[${label}]${RESET} ${DIM}
|
|
4998
|
+
log(` ${YELLOW}⚠${RESET} ${BOLD}[${label}]${RESET} ${DIM}Coming Soon${RESET}`);
|
|
5204
4999
|
return;
|
|
5205
5000
|
}
|
|
5206
5001
|
|
|
5207
|
-
log(` ${CYAN}▸${RESET} ${BOLD}[${label}]${RESET}
|
|
5002
|
+
log(` ${CYAN}▸${RESET} ${BOLD}[${label}]${RESET} Installing...`);
|
|
5208
5003
|
|
|
5209
5004
|
// 1. npm 의존성 설치 (패키지 매니저 자동 감지)
|
|
5210
5005
|
if (config.deps && config.deps.length > 0) {
|
|
@@ -5221,9 +5016,9 @@ if (config.deps && config.deps.length > 0) {
|
|
|
5221
5016
|
stdio: "pipe",
|
|
5222
5017
|
cwd: process.cwd(),
|
|
5223
5018
|
});
|
|
5224
|
-
log(` ${GREEN}✓${RESET} ${config.deps.join(", ")}
|
|
5019
|
+
log(` ${GREEN}✓${RESET} ${config.deps.join(", ")} packages installed`);
|
|
5225
5020
|
} catch (e) {
|
|
5226
|
-
log(` ${YELLOW}⚠${RESET}
|
|
5021
|
+
log(` ${YELLOW}⚠${RESET} Package install failed — ${DIM}run bun add ${config.deps.join(" ")} manually${RESET}`);
|
|
5227
5022
|
}
|
|
5228
5023
|
}
|
|
5229
5024
|
|
|
@@ -5247,10 +5042,10 @@ for (const file of config.files) {
|
|
|
5247
5042
|
}
|
|
5248
5043
|
|
|
5249
5044
|
if (fs.existsSync(destPath)) {
|
|
5250
|
-
log(` ${YELLOW}⚠${RESET} ${file.dest}
|
|
5045
|
+
log(` ${YELLOW}⚠${RESET} ${file.dest} already exists, skipping`);
|
|
5251
5046
|
} else {
|
|
5252
5047
|
fs.copyFileSync(srcPath, destPath);
|
|
5253
|
-
log(` ${GREEN}✓${RESET} ${file.dest}
|
|
5048
|
+
log(` ${GREEN}✓${RESET} ${file.dest} created`);
|
|
5254
5049
|
}
|
|
5255
5050
|
}
|
|
5256
5051
|
|
|
@@ -5267,7 +5062,7 @@ if (config.env && Object.keys(config.env).length > 0) {
|
|
|
5267
5062
|
if (!envContent.includes(key)) {
|
|
5268
5063
|
envContent += `\n${key}=${value}`;
|
|
5269
5064
|
added = true;
|
|
5270
|
-
log(` ${GREEN}✓${RESET} .env
|
|
5065
|
+
log(` ${GREEN}✓${RESET} .env — added ${key}=`);
|
|
5271
5066
|
}
|
|
5272
5067
|
}
|
|
5273
5068
|
|
|
@@ -5316,15 +5111,15 @@ const safe = await ai.withRetry(
|
|
|
5316
5111
|
{ maxRetries: 3 }
|
|
5317
5112
|
);
|
|
5318
5113
|
\`\`\``,
|
|
5319
|
-
vibePrompt: `AI
|
|
5114
|
+
vibePrompt: `AI Engine API:
|
|
5320
5115
|
import { ai } from "@/gencow/ai";
|
|
5321
|
-
ai.chat({ messages }) //
|
|
5322
|
-
ai.stream({ messages }) //
|
|
5323
|
-
ai.embed("
|
|
5324
|
-
ai.embedMany(["a","b"]) //
|
|
5116
|
+
ai.chat({ messages }) // text generation
|
|
5117
|
+
ai.stream({ messages }) // streaming
|
|
5118
|
+
ai.embed("text") // embedding
|
|
5119
|
+
ai.embedMany(["a","b"]) // batch embedding
|
|
5325
5120
|
ai.agent({ messages, tools, maxSteps }) // Agent Loop
|
|
5326
|
-
ai.withRetry(fn, { maxRetries: 3 }) //
|
|
5327
|
-
ai.estimateTokens(text) //
|
|
5121
|
+
ai.withRetry(fn, { maxRetries: 3 }) // retry+fallback
|
|
5122
|
+
ai.estimateTokens(text) // token estimation
|
|
5328
5123
|
ai.trimMessages(msgs, { maxTokens: 4000 }) // 토큰 예산`,
|
|
5329
5124
|
},
|
|
5330
5125
|
"rag.ts": {
|
|
@@ -5352,7 +5147,7 @@ const { answer, sources } = await rag.ask(ctx, "환불 정책이 뭔가요?");
|
|
|
5352
5147
|
// 문서 삭제
|
|
5353
5148
|
await rag.delete(ctx, "old-manual.pdf");
|
|
5354
5149
|
\`\`\``,
|
|
5355
|
-
vibePrompt: `RAG
|
|
5150
|
+
vibePrompt: `RAG Engine API:
|
|
5356
5151
|
import { rag } from "@/gencow/rag";
|
|
5357
5152
|
rag.ingest(ctx, source, text) // 문서 인제스트 (스마트 청킹)
|
|
5358
5153
|
rag.search(ctx, query, { filter }) // 시맨틱 검색
|
|
@@ -5380,7 +5175,7 @@ const tools = defineTools(ctx, {
|
|
|
5380
5175
|
|
|
5381
5176
|
const reply = await ai.chat({ messages, tools });
|
|
5382
5177
|
\`\`\``,
|
|
5383
|
-
vibePrompt: `Tool Calling
|
|
5178
|
+
vibePrompt: `Tool Calling API:
|
|
5384
5179
|
import { defineTools } from "@/gencow/tools";
|
|
5385
5180
|
const tools = defineTools(ctx, { ... }) // AI 도구 정의
|
|
5386
5181
|
ai.chat({ messages, tools }) // 도구 연동 응답`,
|
|
@@ -5401,7 +5196,7 @@ const reply = await ai.chat({
|
|
|
5401
5196
|
messages: [...memCtx.recentMessages, { role: "user", content: query }],
|
|
5402
5197
|
});
|
|
5403
5198
|
\`\`\``,
|
|
5404
|
-
vibePrompt: `Agent Memory
|
|
5199
|
+
vibePrompt: `Agent Memory API:
|
|
5405
5200
|
import { memory } from "@/gencow/memory";
|
|
5406
5201
|
memory.buildContext(ctx, userId, sessionId, query) // 메모리 컨텍스트
|
|
5407
5202
|
memory.extract(ctx, userId, conversation) // 사실 추출`,
|
|
@@ -5435,7 +5230,7 @@ const reranked = await reranker.rerank(query, results, { topK: 5 });
|
|
|
5435
5230
|
// 한 번에 (search + rerank)
|
|
5436
5231
|
const best = await reranker.searchAndRerank(ctx, rag, query);
|
|
5437
5232
|
\`\`\``,
|
|
5438
|
-
vibePrompt: `Reranker
|
|
5233
|
+
vibePrompt: `Reranker API:
|
|
5439
5234
|
import { reranker } from "@/gencow/reranker";
|
|
5440
5235
|
reranker.rerank(query, docs, { topK: 5 }) // LLM 재정렬
|
|
5441
5236
|
reranker.searchAndRerank(ctx, rag, query) // 통합 파이프라인`,
|
|
@@ -5465,7 +5260,7 @@ const result = await guardrails.wrap(
|
|
|
5465
5260
|
{ maxLength: 2000 }
|
|
5466
5261
|
);
|
|
5467
5262
|
\`\`\``,
|
|
5468
|
-
vibePrompt: `Guardrails
|
|
5263
|
+
vibePrompt: `Guardrails API:
|
|
5469
5264
|
import { guardrails } from "@/gencow/guardrails";
|
|
5470
5265
|
guardrails.validateInput(text, { maskPII: true }) // PII 마스킹
|
|
5471
5266
|
guardrails.validateInput(text, { blockTopics }) // 주제 차단
|
|
@@ -5492,7 +5287,7 @@ const myPrompt = definePrompt({
|
|
|
5492
5287
|
});
|
|
5493
5288
|
const prompt = myPrompt({ role: "전문가", task: "코드 리뷰" });
|
|
5494
5289
|
\`\`\``,
|
|
5495
|
-
vibePrompt: `Prompt Templates
|
|
5290
|
+
vibePrompt: `Prompt Templates API:
|
|
5496
5291
|
import { definePrompt, ragQAPrompt } from "@/gencow/prompts";
|
|
5497
5292
|
definePrompt({ template, defaults }) // 재사용 프롬프트 정의
|
|
5498
5293
|
ragQAPrompt({ question, context }) // RAG Q&A 프롬프트`,
|
|
@@ -5515,7 +5310,7 @@ await rag.ingest(ctx, "manual.pdf", text);
|
|
|
5515
5310
|
// 자동 감지
|
|
5516
5311
|
const content = await parsers.auto("report.html", htmlString);
|
|
5517
5312
|
\`\`\``,
|
|
5518
|
-
vibePrompt: `File Parsers
|
|
5313
|
+
vibePrompt: `File Parsers API:
|
|
5519
5314
|
import { parsers } from "@/gencow/parsers";
|
|
5520
5315
|
parsers.pdf(buffer) // PDF → 텍스트
|
|
5521
5316
|
parsers.html(htmlString) // HTML → 텍스트
|
|
@@ -5559,18 +5354,18 @@ async function updateReadme(config) {
|
|
|
5559
5354
|
for (const comp of installed) {
|
|
5560
5355
|
componentSection += `### ${comp.title}\n\n`;
|
|
5561
5356
|
componentSection += `${comp.table}\n\n`;
|
|
5562
|
-
componentSection += `####
|
|
5357
|
+
componentSection += `#### Usage\n\n`;
|
|
5563
5358
|
componentSection += `${comp.usage}\n\n`;
|
|
5564
5359
|
}
|
|
5565
5360
|
|
|
5566
5361
|
// 바이브코딩 프롬프트 섹션
|
|
5567
5362
|
componentSection += `### 🤖 AI Vibe-Coding Prompt\n\n`;
|
|
5568
5363
|
componentSection += "```\n";
|
|
5569
|
-
componentSection += "
|
|
5364
|
+
componentSection += "The following AI components are installed in this Gencow backend:\n\n";
|
|
5570
5365
|
for (const comp of installed) {
|
|
5571
5366
|
componentSection += `${comp.vibePrompt}\n\n`;
|
|
5572
5367
|
}
|
|
5573
|
-
componentSection += "
|
|
5368
|
+
componentSection += "Use the APIs above to build your desired features.\n";
|
|
5574
5369
|
componentSection += "```\n\n";
|
|
5575
5370
|
componentSection += `${END_MARKER}`;
|
|
5576
5371
|
|
|
@@ -5584,7 +5379,7 @@ async function updateReadme(config) {
|
|
|
5584
5379
|
}
|
|
5585
5380
|
|
|
5586
5381
|
fs.writeFileSync(readmePath, readme);
|
|
5587
|
-
log(` ${GREEN}✓${RESET} README.md
|
|
5382
|
+
log(` ${GREEN}✓${RESET} README.md component docs updated`);
|
|
5588
5383
|
}
|
|
5589
5384
|
|
|
5590
5385
|
const [, , cmd = "help", ...args] = process.argv;
|