gencow 0.1.121 → 0.1.122
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 +486 -548
- 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 +13 -10
- package/server/index.js.map +2 -2
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,20 +1030,20 @@ 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
|
},
|
|
@@ -1059,9 +1059,9 @@ ${hasPrompt ? `
|
|
|
1059
1059
|
|
|
1060
1060
|
if (!isLocal) {
|
|
1061
1061
|
// Cloud reset은 미지원 — 안내 메시지
|
|
1062
|
-
error("db:reset
|
|
1063
|
-
info(`${DIM}Cloud DB
|
|
1064
|
-
info(`${DIM}
|
|
1062
|
+
error("db:reset requires --local flag.");
|
|
1063
|
+
info(`${DIM}Cloud DB reset is only available via Dashboard (data protection).${RESET}`);
|
|
1064
|
+
info(`${DIM}To reset local DB: gencow db:reset --local${RESET}`);
|
|
1065
1065
|
log("");
|
|
1066
1066
|
return;
|
|
1067
1067
|
}
|
|
@@ -1096,14 +1096,14 @@ ${hasPrompt ? `
|
|
|
1096
1096
|
return;
|
|
1097
1097
|
}
|
|
1098
1098
|
|
|
1099
|
-
success(`TRUNCATE
|
|
1099
|
+
success(`TRUNCATE done: ${data.truncated?.length || 0} tables`);
|
|
1100
1100
|
if (data.truncated?.length > 0) {
|
|
1101
1101
|
info(` ${DIM}${data.truncated.join(", ")}${RESET}`);
|
|
1102
1102
|
}
|
|
1103
1103
|
if (data.seeded) {
|
|
1104
|
-
success(`Seed
|
|
1104
|
+
success(`Seed completed ✓`);
|
|
1105
1105
|
} else {
|
|
1106
|
-
info(`${DIM}seed.ts
|
|
1106
|
+
info(`${DIM}No seed.ts found — skipping${RESET}`);
|
|
1107
1107
|
}
|
|
1108
1108
|
} catch (e) {
|
|
1109
1109
|
error(`Server connection failed: ${e.message}`);
|
|
@@ -1125,9 +1125,9 @@ ${hasPrompt ? `
|
|
|
1125
1125
|
|
|
1126
1126
|
// 3. Delete DB
|
|
1127
1127
|
rmSync(dbPath, { recursive: true, force: true });
|
|
1128
|
-
success("DB deleted — gencow dev
|
|
1128
|
+
success("DB deleted — restart with gencow dev to create a new one");
|
|
1129
1129
|
} else {
|
|
1130
|
-
info("DB
|
|
1130
|
+
info("DB does not exist yet. Run gencow dev to get started.");
|
|
1131
1131
|
}
|
|
1132
1132
|
}
|
|
1133
1133
|
log("");
|
|
@@ -1152,7 +1152,7 @@ ${hasPrompt ? `
|
|
|
1152
1152
|
if (!statusRes.ok) throw new Error("Server not ready");
|
|
1153
1153
|
} catch {
|
|
1154
1154
|
error(`Server not running on :${port}`);
|
|
1155
|
-
info(`${DIM}gencow dev --local
|
|
1155
|
+
info(`${DIM}Start server first: gencow dev --local${RESET}`);
|
|
1156
1156
|
log("");
|
|
1157
1157
|
return;
|
|
1158
1158
|
}
|
|
@@ -1176,7 +1176,7 @@ ${hasPrompt ? `
|
|
|
1176
1176
|
} else if (!res.ok) {
|
|
1177
1177
|
error(`Seed failed: ${data.error || "Unknown error"}`);
|
|
1178
1178
|
} else {
|
|
1179
|
-
success("Seed
|
|
1179
|
+
success("Seed completed ✓");
|
|
1180
1180
|
}
|
|
1181
1181
|
} catch (e) {
|
|
1182
1182
|
error(`Server connection failed: ${e.message}`);
|
|
@@ -1186,6 +1186,7 @@ ${hasPrompt ? `
|
|
|
1186
1186
|
}
|
|
1187
1187
|
|
|
1188
1188
|
// ── Cloud 모드 (기본) ──
|
|
1189
|
+
const isProd = args.includes("--prod");
|
|
1189
1190
|
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
1190
1191
|
let appId = null;
|
|
1191
1192
|
let seedPlatformUrl = null;
|
|
@@ -1194,18 +1195,28 @@ ${hasPrompt ? `
|
|
|
1194
1195
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
1195
1196
|
appId = gencowJson.appId || gencowJson.appName;
|
|
1196
1197
|
seedPlatformUrl = gencowJson.platformUrl || null;
|
|
1198
|
+
// --prod: use prod app
|
|
1199
|
+
if (isProd && gencowJson.prodApp) {
|
|
1200
|
+
appId = gencowJson.prodApp;
|
|
1201
|
+
}
|
|
1197
1202
|
} catch { /* ignore parse error */ }
|
|
1198
1203
|
}
|
|
1199
1204
|
|
|
1205
|
+
if (isProd && appId && !appId.endsWith("-prod")) {
|
|
1206
|
+
error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1200
1210
|
if (!appId) {
|
|
1201
|
-
error("Cloud app not found
|
|
1202
|
-
info(`${DIM}
|
|
1203
|
-
info(`${DIM}
|
|
1211
|
+
error("Cloud app not found. Make sure gencow.json exists with an appId.");
|
|
1212
|
+
info(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
|
|
1213
|
+
info(`${DIM}To seed local DB: gencow db:seed --local${RESET}`);
|
|
1204
1214
|
log("");
|
|
1205
1215
|
return;
|
|
1206
1216
|
}
|
|
1207
1217
|
|
|
1208
|
-
|
|
1218
|
+
const envLabel = isProd ? "prod" : "dev";
|
|
1219
|
+
log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(${envLabel}: ${appId})${RESET}\n`);
|
|
1209
1220
|
info("Running seed.ts on cloud app...");
|
|
1210
1221
|
|
|
1211
1222
|
const seedAppBaseUrl = getAppUrl(appId, seedPlatformUrl || "https://gencow.app");
|
|
@@ -1219,8 +1230,8 @@ ${hasPrompt ? `
|
|
|
1219
1230
|
|
|
1220
1231
|
if (!statusRes || !statusRes.ok) {
|
|
1221
1232
|
error(`Cloud app "${appId}" is not running`);
|
|
1222
|
-
info(`${DIM}Dashboard → Apps → ${appId} → Resume
|
|
1223
|
-
info(`${DIM}
|
|
1233
|
+
info(`${DIM}Dashboard → Apps → ${appId} → Resume to start the app.${RESET}`);
|
|
1234
|
+
info(`${DIM}Or: run gencow dev to create and deploy.${RESET}`);
|
|
1224
1235
|
log("");
|
|
1225
1236
|
return;
|
|
1226
1237
|
}
|
|
@@ -1238,15 +1249,15 @@ ${hasPrompt ? `
|
|
|
1238
1249
|
log(` ${DIM}export default async function seed(ctx) {${RESET}`);
|
|
1239
1250
|
log(` ${DIM} await ctx.db.insert(tasks).values([...]);${RESET}`);
|
|
1240
1251
|
log(` ${DIM}};${RESET}\n`);
|
|
1241
|
-
info(`${DIM}After creating seed.ts: gencow dev
|
|
1252
|
+
info(`${DIM}After creating seed.ts: run gencow dev then gencow db:seed${RESET}`);
|
|
1242
1253
|
} else if (!res.ok) {
|
|
1243
1254
|
error(`Cloud seed failed: ${data.error || "Unknown error"}`);
|
|
1244
1255
|
} else {
|
|
1245
|
-
success("Cloud seed
|
|
1256
|
+
success("Cloud seed completed ✓");
|
|
1246
1257
|
}
|
|
1247
1258
|
} catch (e) {
|
|
1248
1259
|
error(`Cloud app connection failed: ${e.message}`);
|
|
1249
|
-
info(`${DIM}
|
|
1260
|
+
info(`${DIM}Check if the app is running: ${seedAppBaseUrl}${RESET}`);
|
|
1250
1261
|
}
|
|
1251
1262
|
log("");
|
|
1252
1263
|
},
|
|
@@ -1261,7 +1272,7 @@ ${hasPrompt ? `
|
|
|
1261
1272
|
log(`\n${BOLD}${CYAN}Gencow DB Restore${RESET}\n`);
|
|
1262
1273
|
|
|
1263
1274
|
if (!existsSync(backupDir)) {
|
|
1264
|
-
warn("
|
|
1275
|
+
warn("No backups found.");
|
|
1265
1276
|
log("");
|
|
1266
1277
|
return;
|
|
1267
1278
|
}
|
|
@@ -1274,7 +1285,7 @@ ${hasPrompt ? `
|
|
|
1274
1285
|
.reverse();
|
|
1275
1286
|
|
|
1276
1287
|
if (backups.length === 0) {
|
|
1277
|
-
warn("
|
|
1288
|
+
warn("No backups found.");
|
|
1278
1289
|
log("");
|
|
1279
1290
|
return;
|
|
1280
1291
|
}
|
|
@@ -1295,7 +1306,7 @@ ${hasPrompt ? `
|
|
|
1295
1306
|
}
|
|
1296
1307
|
cpSync(latestPath, dbPath, { recursive: true });
|
|
1297
1308
|
success(`Restored from: .gencow/backups/${latest}`);
|
|
1298
|
-
info("gencow dev
|
|
1309
|
+
info("Restart with gencow dev.");
|
|
1299
1310
|
log("");
|
|
1300
1311
|
},
|
|
1301
1312
|
|
|
@@ -1545,28 +1556,53 @@ ${hasPrompt ? `
|
|
|
1545
1556
|
return;
|
|
1546
1557
|
}
|
|
1547
1558
|
|
|
1548
|
-
// ── Cloud
|
|
1559
|
+
// ── Cloud mode (default) ──
|
|
1560
|
+
const isProd = args.includes("--prod");
|
|
1549
1561
|
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
1550
1562
|
let appId = null;
|
|
1551
1563
|
if (existsSync(gencowJsonPath)) {
|
|
1552
1564
|
try {
|
|
1553
1565
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
1554
1566
|
appId = gencowJson.appId || gencowJson.appName;
|
|
1567
|
+
// --prod: use prod app
|
|
1568
|
+
if (isProd && gencowJson.prodApp) {
|
|
1569
|
+
appId = gencowJson.prodApp;
|
|
1570
|
+
}
|
|
1555
1571
|
} catch { /* ignore parse error */ }
|
|
1556
1572
|
}
|
|
1557
1573
|
|
|
1574
|
+
if (isProd && appId && !appId.endsWith("-prod")) {
|
|
1575
|
+
error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1558
1579
|
if (!appId) {
|
|
1559
|
-
error("Cloud app not found
|
|
1560
|
-
info(`${DIM}
|
|
1561
|
-
info(`${DIM}
|
|
1580
|
+
error("Cloud app not found. Make sure gencow.json exists with an appId.");
|
|
1581
|
+
info(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
|
|
1582
|
+
info(`${DIM}To push to local DB: gencow db:push --local${RESET}`);
|
|
1562
1583
|
log("");
|
|
1563
1584
|
return;
|
|
1564
1585
|
}
|
|
1565
1586
|
|
|
1566
|
-
// ──
|
|
1587
|
+
// ── Prod safety confirmation ──
|
|
1588
|
+
if (isProd) {
|
|
1589
|
+
const { createInterface } = await import("readline");
|
|
1590
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1591
|
+
const answer = await new Promise(res =>
|
|
1592
|
+
rl.question(`\n ${RED}!${RESET} Push schema to ${BOLD}PRODUCTION${RESET} database (${appId})?\n Type "yes" to confirm: `, res)
|
|
1593
|
+
);
|
|
1594
|
+
rl.close();
|
|
1595
|
+
if (answer.trim().toLowerCase() !== "yes") {
|
|
1596
|
+
warn("Cancelled.");
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// ── Cloud mode: push schema via Platform API ──
|
|
1567
1602
|
const creds = requireCreds();
|
|
1568
|
-
|
|
1569
|
-
|
|
1603
|
+
const envLabel = isProd ? "prod" : "dev";
|
|
1604
|
+
log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(${envLabel}: ${appId})${RESET}\n`);
|
|
1605
|
+
warn("Make sure the latest code is deployed before running this command.");
|
|
1570
1606
|
log("");
|
|
1571
1607
|
info("Pushing schema.ts → cloud database...");
|
|
1572
1608
|
|
|
@@ -1586,7 +1622,7 @@ ${hasPrompt ? `
|
|
|
1586
1622
|
}
|
|
1587
1623
|
|
|
1588
1624
|
// 🆕 로컬 drizzle-kit generate 실행 (프롬프트 패스스루)
|
|
1589
|
-
info("
|
|
1625
|
+
info("Generating schema migrations...");
|
|
1590
1626
|
try {
|
|
1591
1627
|
const dk = _drizzleKitCmd("generate");
|
|
1592
1628
|
execSync(dk.cmd, {
|
|
@@ -1594,14 +1630,14 @@ ${hasPrompt ? `
|
|
|
1594
1630
|
env: { ...process.env, ...dk.env },
|
|
1595
1631
|
stdio: "inherit",
|
|
1596
1632
|
});
|
|
1597
|
-
success("
|
|
1633
|
+
success("Migrations generated");
|
|
1598
1634
|
} catch (e) {
|
|
1599
1635
|
const msg = e.stderr?.toString() || e.message || "";
|
|
1600
1636
|
if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
|
|
1601
|
-
log(`${DIM}
|
|
1637
|
+
log(`${DIM} No schema changes — using existing migrations${RESET}`);
|
|
1602
1638
|
} else {
|
|
1603
|
-
warn(
|
|
1604
|
-
info("
|
|
1639
|
+
warn(`Migration generation failed: ${msg.split("\n")[0]}`);
|
|
1640
|
+
info("Will attempt schema push via server.");
|
|
1605
1641
|
}
|
|
1606
1642
|
}
|
|
1607
1643
|
|
|
@@ -1638,8 +1674,8 @@ ${hasPrompt ? `
|
|
|
1638
1674
|
success("Schema pushed to cloud DB!");
|
|
1639
1675
|
log(` ${DIM}Tables are in sync with schema.ts${RESET}\n`);
|
|
1640
1676
|
} catch (e) {
|
|
1641
|
-
error(`Platform
|
|
1642
|
-
info("
|
|
1677
|
+
error(`Platform connection failed: ${e.message}`);
|
|
1678
|
+
info("Check if the server is running.");
|
|
1643
1679
|
process.exit(1);
|
|
1644
1680
|
} finally {
|
|
1645
1681
|
// 임시 번들 정리
|
|
@@ -1696,81 +1732,6 @@ ${hasPrompt ? `
|
|
|
1696
1732
|
await open(url);
|
|
1697
1733
|
},
|
|
1698
1734
|
|
|
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
1735
|
|
|
1775
1736
|
// ── help ─────────────────────────────────────────────
|
|
1776
1737
|
help() {
|
|
@@ -1792,9 +1753,11 @@ ${BOLD}Dev commands:${RESET}
|
|
|
1792
1753
|
${GREEN}dashboard${RESET} Open the Gencow admin dashboard in browser
|
|
1793
1754
|
${GREEN}db:push${RESET} Sync schema.ts → cloud DB ${DIM}(default: cloud)${RESET}
|
|
1794
1755
|
${DIM}--local Push to local DB instead of cloud${RESET}
|
|
1756
|
+
${DIM}--prod Push to production DB (confirmation required)${RESET}
|
|
1795
1757
|
${GREEN}db:generate${RESET} Generate SQL migration files from schema.ts
|
|
1796
1758
|
${GREEN}db:seed${RESET} Run gencow/seed.ts on cloud app ${DIM}(default: cloud)${RESET}
|
|
1797
1759
|
${DIM}--local Seed local DB instead of cloud${RESET}
|
|
1760
|
+
${DIM}--prod Seed production app${RESET}
|
|
1798
1761
|
${GREEN}db:reset${RESET} TRUNCATE all data + run seed.ts ${DIM}(local only)${RESET}
|
|
1799
1762
|
${DIM}--local Required — local DB reset only (cloud: use Dashboard)${RESET}
|
|
1800
1763
|
${GREEN}db:studio${RESET} Open Drizzle Studio ${DIM}(visual DB browser)${RESET}
|
|
@@ -1829,9 +1792,14 @@ ${BOLD}BaaS commands (login required):${RESET}
|
|
|
1829
1792
|
${GREEN}domain status${RESET} Check domain DNS/TLS status
|
|
1830
1793
|
${GREEN}domain remove${RESET} Disconnect custom domain
|
|
1831
1794
|
|
|
1832
|
-
${BOLD}
|
|
1833
|
-
${GREEN}
|
|
1834
|
-
${GREEN}
|
|
1795
|
+
${BOLD}App management:${RESET}
|
|
1796
|
+
${GREEN}app list${RESET} List your apps
|
|
1797
|
+
${GREEN}app create${RESET} Create a new app
|
|
1798
|
+
${GREEN}app delete${RESET} Delete an app ${DIM}(confirmation required)${RESET}
|
|
1799
|
+
${GREEN}app status${RESET} Show app status
|
|
1800
|
+
|
|
1801
|
+
${DIM}Tip: Most commands support --prod to target the production app.${RESET}
|
|
1802
|
+
${DIM} e.g. gencow env list --prod, gencow db:seed --prod${RESET}
|
|
1835
1803
|
|
|
1836
1804
|
${BOLD}Examples:${RESET}
|
|
1837
1805
|
${DIM}# New project:${RESET}
|
|
@@ -1840,10 +1808,9 @@ ${BOLD}Examples:${RESET}
|
|
|
1840
1808
|
${DIM}# Initialize in current directory:${RESET}
|
|
1841
1809
|
gencow init . --force
|
|
1842
1810
|
|
|
1843
|
-
${DIM}#
|
|
1811
|
+
${DIM}# Deploy to production (Pro+ only):${RESET}
|
|
1844
1812
|
gencow login
|
|
1845
|
-
gencow
|
|
1846
|
-
gencow remote:deploy
|
|
1813
|
+
gencow deploy
|
|
1847
1814
|
`);
|
|
1848
1815
|
},
|
|
1849
1816
|
|
|
@@ -1855,7 +1822,7 @@ ${BOLD}Examples:${RESET}
|
|
|
1855
1822
|
info(`Platform: ${platformUrl}`);
|
|
1856
1823
|
|
|
1857
1824
|
// 1. auth-start → 인증 코드 생성
|
|
1858
|
-
info("
|
|
1825
|
+
info("Requesting auth code...");
|
|
1859
1826
|
let authData;
|
|
1860
1827
|
try {
|
|
1861
1828
|
const res = await fetch(`${platformUrl}/platform/cli/auth-start`, {
|
|
@@ -1865,16 +1832,16 @@ ${BOLD}Examples:${RESET}
|
|
|
1865
1832
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1866
1833
|
authData = await res.json();
|
|
1867
1834
|
} catch (e) {
|
|
1868
|
-
error(`Platform
|
|
1835
|
+
error(`Platform connection failed: ${e.message}`);
|
|
1869
1836
|
error(`Platform URL: ${platformUrl}`);
|
|
1870
|
-
error(
|
|
1837
|
+
error(`Check if the server is running.`);
|
|
1871
1838
|
process.exit(1);
|
|
1872
1839
|
}
|
|
1873
1840
|
|
|
1874
1841
|
log("");
|
|
1875
|
-
log(` ${BOLD}
|
|
1842
|
+
log(` ${BOLD}Auth code: ${CYAN}${authData.code}${RESET}`);
|
|
1876
1843
|
log("");
|
|
1877
|
-
info(
|
|
1844
|
+
info(`Login in your browser:`);
|
|
1878
1845
|
log(` ${CYAN}${authData.url}${RESET}`);
|
|
1879
1846
|
log("");
|
|
1880
1847
|
|
|
@@ -1885,11 +1852,11 @@ ${BOLD}Examples:${RESET}
|
|
|
1885
1852
|
process.platform === "win32" ? "start" : "xdg-open";
|
|
1886
1853
|
exec(`${openCmd} "${authData.url}"`);
|
|
1887
1854
|
} catch {
|
|
1888
|
-
warn("
|
|
1855
|
+
warn("Could not open browser automatically. Open the URL above manually.");
|
|
1889
1856
|
}
|
|
1890
1857
|
|
|
1891
1858
|
// 3. 폴링 — 2초마다 인증 완료 확인
|
|
1892
|
-
info("
|
|
1859
|
+
info("Waiting for login...");
|
|
1893
1860
|
const POLL_INTERVAL = 2000;
|
|
1894
1861
|
const MAX_POLLS = 150; // 10분 내 5분 = 150회
|
|
1895
1862
|
|
|
@@ -1914,14 +1881,14 @@ ${BOLD}Examples:${RESET}
|
|
|
1914
1881
|
});
|
|
1915
1882
|
|
|
1916
1883
|
log("");
|
|
1917
|
-
success(
|
|
1918
|
-
info(
|
|
1884
|
+
success(`Login successful!`);
|
|
1885
|
+
info(`Token saved: ${DIM}~/.gencow/credentials.json${RESET}`);
|
|
1919
1886
|
log("");
|
|
1920
1887
|
return;
|
|
1921
1888
|
}
|
|
1922
1889
|
|
|
1923
1890
|
if (data.status === "expired") {
|
|
1924
|
-
error("
|
|
1891
|
+
error("Auth code expired. Try again: gencow login");
|
|
1925
1892
|
process.exit(1);
|
|
1926
1893
|
}
|
|
1927
1894
|
|
|
@@ -1933,7 +1900,7 @@ ${BOLD}Examples:${RESET}
|
|
|
1933
1900
|
}
|
|
1934
1901
|
}
|
|
1935
1902
|
|
|
1936
|
-
error("\
|
|
1903
|
+
error("\nTimed out. Try again: gencow login");
|
|
1937
1904
|
process.exit(1);
|
|
1938
1905
|
},
|
|
1939
1906
|
|
|
@@ -1941,9 +1908,9 @@ ${BOLD}Examples:${RESET}
|
|
|
1941
1908
|
async logout() {
|
|
1942
1909
|
try {
|
|
1943
1910
|
unlinkSync(CREDS_PATH);
|
|
1944
|
-
success("
|
|
1911
|
+
success("Logged out. Credentials cleared.");
|
|
1945
1912
|
} catch {
|
|
1946
|
-
info("
|
|
1913
|
+
info("Already logged out.");
|
|
1947
1914
|
}
|
|
1948
1915
|
},
|
|
1949
1916
|
|
|
@@ -1951,8 +1918,8 @@ ${BOLD}Examples:${RESET}
|
|
|
1951
1918
|
async whoami() {
|
|
1952
1919
|
const creds = loadCreds();
|
|
1953
1920
|
if (!creds?.apiKey) {
|
|
1954
|
-
error("
|
|
1955
|
-
info("
|
|
1921
|
+
error("Not logged in.");
|
|
1922
|
+
info("Run: gencow login");
|
|
1956
1923
|
return;
|
|
1957
1924
|
}
|
|
1958
1925
|
|
|
@@ -1962,10 +1929,10 @@ ${BOLD}Examples:${RESET}
|
|
|
1962
1929
|
if (!res.ok) {
|
|
1963
1930
|
const data = await res.json().catch(() => ({}));
|
|
1964
1931
|
const msg = data.error || `HTTP ${res.status}`;
|
|
1965
|
-
error(
|
|
1932
|
+
error(`Auth failed: ${msg}`);
|
|
1966
1933
|
if (res.status === 401) {
|
|
1967
|
-
info("
|
|
1968
|
-
info("
|
|
1934
|
+
info("Token is expired or invalid.");
|
|
1935
|
+
info("Run: gencow login");
|
|
1969
1936
|
}
|
|
1970
1937
|
return;
|
|
1971
1938
|
}
|
|
@@ -1978,13 +1945,13 @@ ${BOLD}Examples:${RESET}
|
|
|
1978
1945
|
if (data.expiresAt) {
|
|
1979
1946
|
const expiresDate = new Date(data.expiresAt);
|
|
1980
1947
|
const daysLeft = Math.ceil((expiresDate.getTime() - Date.now()) / (24 * 60 * 60 * 1000));
|
|
1981
|
-
info(`Expires: ${expiresDate.toLocaleString()} (${daysLeft}
|
|
1948
|
+
info(`Expires: ${expiresDate.toLocaleString()} (${daysLeft} days left)`);
|
|
1982
1949
|
}
|
|
1983
1950
|
log("");
|
|
1984
1951
|
} catch (e) {
|
|
1985
|
-
error(
|
|
1952
|
+
error(`Connection failed: ${e.message}`);
|
|
1986
1953
|
info(`Platform URL: ${creds.platformUrl}`);
|
|
1987
|
-
info("
|
|
1954
|
+
info("Check if the server is running.");
|
|
1988
1955
|
}
|
|
1989
1956
|
},
|
|
1990
1957
|
|
|
@@ -2055,17 +2022,17 @@ ${BOLD}Examples:${RESET}
|
|
|
2055
2022
|
appId = gencowJson.appId || gencowJson.appName;
|
|
2056
2023
|
}
|
|
2057
2024
|
if (!appId) {
|
|
2058
|
-
error("
|
|
2025
|
+
error("App not found. Run from the project root with gencow.json.");
|
|
2059
2026
|
process.exit(1);
|
|
2060
2027
|
}
|
|
2061
2028
|
|
|
2062
2029
|
if (subcmd === "logs") {
|
|
2063
2030
|
const lines = deployArgs.includes("-n") ? Number(deployArgs[deployArgs.indexOf("-n") + 1]) || 100 : 100;
|
|
2064
|
-
info(`"${appId}"
|
|
2031
|
+
info(`Fetching logs for "${appId}"... (last ${lines} lines)`);
|
|
2065
2032
|
const res = await rpcQuery(creds, "apps.logs", { name: appId, lines });
|
|
2066
2033
|
if (!res.ok) {
|
|
2067
2034
|
const errData = await res.json().catch(() => ({}));
|
|
2068
|
-
error(
|
|
2035
|
+
error(`Failed to fetch logs: ${errData.error || res.statusText}`);
|
|
2069
2036
|
process.exit(1);
|
|
2070
2037
|
}
|
|
2071
2038
|
const data = await res.json();
|
|
@@ -2075,29 +2042,29 @@ ${BOLD}Examples:${RESET}
|
|
|
2075
2042
|
log(` ${DIM}${line}${RESET}`);
|
|
2076
2043
|
}
|
|
2077
2044
|
} else {
|
|
2078
|
-
info("
|
|
2045
|
+
info("No logs found.");
|
|
2079
2046
|
}
|
|
2080
2047
|
return;
|
|
2081
2048
|
}
|
|
2082
2049
|
|
|
2083
2050
|
if (subcmd === "status") {
|
|
2084
|
-
info(`"${appId}"
|
|
2051
|
+
info(`Checking "${appId}" status...`);
|
|
2085
2052
|
const res = await rpcQuery(creds, "apps.get", { name: appId });
|
|
2086
2053
|
if (!res.ok) {
|
|
2087
2054
|
const errData = await res.json().catch(() => ({}));
|
|
2088
|
-
error(
|
|
2055
|
+
error(`Failed to fetch status: ${errData.error || res.statusText}`);
|
|
2089
2056
|
process.exit(1);
|
|
2090
2057
|
}
|
|
2091
2058
|
const data = await res.json();
|
|
2092
2059
|
const app = data.result || data;
|
|
2093
2060
|
log("");
|
|
2094
|
-
log(` ${BOLD}
|
|
2061
|
+
log(` ${BOLD}App Status${RESET}`);
|
|
2095
2062
|
log(` ──────────────────────`);
|
|
2096
|
-
info(
|
|
2097
|
-
info(
|
|
2063
|
+
info(`Name: ${app.name}`);
|
|
2064
|
+
info(`Status: ${app.status === "running" ? `${GREEN}● running${RESET}` : app.status === "crashed" ? `${RED}● crashed${RESET}` : `${YELLOW}● ${app.status}${RESET}`}`);
|
|
2098
2065
|
info(`URL: ${app.url || "N/A"}`);
|
|
2099
|
-
if (app.lastDeployedAt) info(
|
|
2100
|
-
if (app.running !== undefined) info(
|
|
2066
|
+
if (app.lastDeployedAt) info(`Deployed: ${new Date(app.lastDeployedAt).toLocaleString()}`);
|
|
2067
|
+
if (app.running !== undefined) info(`Process: ${app.running ? `${GREEN}alive${RESET}` : `${RED}dead${RESET}`}`);
|
|
2101
2068
|
log("");
|
|
2102
2069
|
return;
|
|
2103
2070
|
}
|
|
@@ -2107,8 +2074,8 @@ ${BOLD}Examples:${RESET}
|
|
|
2107
2074
|
const a = deployArgs[i];
|
|
2108
2075
|
if (a === "--prod") {
|
|
2109
2076
|
// deprecated: deploy는 이미 prod 기본값
|
|
2110
|
-
warn(`${YELLOW}--prod
|
|
2111
|
-
info(`gencow deploy
|
|
2077
|
+
warn(`${YELLOW}--prod flag is no longer needed.${RESET}`);
|
|
2078
|
+
info(`gencow deploy targets production by default.`);
|
|
2112
2079
|
envTarget = "prod";
|
|
2113
2080
|
}
|
|
2114
2081
|
else if (a === "--force" || a === "-f") forceDeploy = true;
|
|
@@ -2126,19 +2093,19 @@ ${BOLD}Examples:${RESET}
|
|
|
2126
2093
|
}
|
|
2127
2094
|
else if (!a.startsWith("-")) {
|
|
2128
2095
|
// 미인식 subcommand — 에러 출력
|
|
2129
|
-
error(
|
|
2096
|
+
error(`Unknown deploy argument: "${a}"`);
|
|
2130
2097
|
log("");
|
|
2131
|
-
info(
|
|
2132
|
-
info(` gencow deploy
|
|
2133
|
-
info(` gencow deploy --static
|
|
2134
|
-
info(` gencow deploy --rollback
|
|
2135
|
-
info(` gencow deploy logs
|
|
2136
|
-
info(` gencow deploy status
|
|
2137
|
-
info(` gencow deploy --force
|
|
2098
|
+
info(`Usage: gencow deploy [options]`);
|
|
2099
|
+
info(` gencow deploy Production deploy (Pro+)`);
|
|
2100
|
+
info(` gencow deploy --static Production static deploy (Pro+)`);
|
|
2101
|
+
info(` gencow deploy --rollback Rollback to previous version`);
|
|
2102
|
+
info(` gencow deploy logs View server logs`);
|
|
2103
|
+
info(` gencow deploy status Check app status`);
|
|
2104
|
+
info(` gencow deploy --force Skip dependency audit`);
|
|
2138
2105
|
log("");
|
|
2139
|
-
info(`💡
|
|
2140
|
-
info(` gencow dev
|
|
2141
|
-
info(` gencow static [dir]
|
|
2106
|
+
info(`💡 Dev environment:`);
|
|
2107
|
+
info(` gencow dev Real-time backend deploy`);
|
|
2108
|
+
info(` gencow static [dir] Static file deploy`);
|
|
2142
2109
|
process.exit(1);
|
|
2143
2110
|
}
|
|
2144
2111
|
}
|
|
@@ -2169,12 +2136,12 @@ ${BOLD}Examples:${RESET}
|
|
|
2169
2136
|
// prodApp이 있으면 prod 대상 롤백, 없으면 dev 대상 롤백
|
|
2170
2137
|
const rollbackTarget = prodAppId || appId;
|
|
2171
2138
|
if (!rollbackTarget) {
|
|
2172
|
-
error("
|
|
2139
|
+
error("App not found. Run from the project root with gencow.json.");
|
|
2173
2140
|
process.exit(1);
|
|
2174
2141
|
}
|
|
2175
2142
|
|
|
2176
2143
|
log(`\n${BOLD}${CYAN}Gencow Rollback${RESET}\n`);
|
|
2177
|
-
info(
|
|
2144
|
+
info(`App: ${rollbackTarget}${prodAppId ? " (prod)" : ""}`);
|
|
2178
2145
|
log("");
|
|
2179
2146
|
|
|
2180
2147
|
const rollbackStartTime = Date.now();
|
|
@@ -2182,7 +2149,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2182
2149
|
let spinnerIdx = 0;
|
|
2183
2150
|
const spinner = setInterval(() => {
|
|
2184
2151
|
const elapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(0);
|
|
2185
|
-
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET}
|
|
2152
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Rolling back... ${DIM}(${elapsed}s)${RESET} `);
|
|
2186
2153
|
}, 120);
|
|
2187
2154
|
|
|
2188
2155
|
const rollbackRes = await platformFetch(creds, `/platform/apps/${rollbackTarget}/rollback`, {
|
|
@@ -2195,7 +2162,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2195
2162
|
|
|
2196
2163
|
if (!rollbackRes.ok) {
|
|
2197
2164
|
const errData = await rollbackRes.json().catch(() => ({}));
|
|
2198
|
-
error(
|
|
2165
|
+
error(`Rollback failed: ${errData.error || rollbackRes.statusText}`);
|
|
2199
2166
|
process.exit(1);
|
|
2200
2167
|
}
|
|
2201
2168
|
|
|
@@ -2203,12 +2170,12 @@ ${BOLD}Examples:${RESET}
|
|
|
2203
2170
|
const rollbackElapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(1);
|
|
2204
2171
|
|
|
2205
2172
|
log("");
|
|
2206
|
-
success(`🔄
|
|
2207
|
-
info(
|
|
2208
|
-
info(
|
|
2209
|
-
info(`URL:
|
|
2173
|
+
success(`🔄 Rollback complete! (${rollbackElapsed}s)`);
|
|
2174
|
+
info(`Rolled back: #${rollbackData.rolledBackFrom} → #${rollbackData.rolledBackTo}`);
|
|
2175
|
+
info(`Bundle: ${rollbackData.bundleHash}`);
|
|
2176
|
+
info(`URL: ${rollbackData.url}`);
|
|
2210
2177
|
log("");
|
|
2211
|
-
warn(`ℹ️
|
|
2178
|
+
warn(`ℹ️ Only code was rolled back. Database was not changed.`);
|
|
2212
2179
|
log("");
|
|
2213
2180
|
return;
|
|
2214
2181
|
}
|
|
@@ -2237,12 +2204,12 @@ ${BOLD}Examples:${RESET}
|
|
|
2237
2204
|
});
|
|
2238
2205
|
rl.close();
|
|
2239
2206
|
if (answer.toLowerCase() !== "y") {
|
|
2240
|
-
info("
|
|
2207
|
+
info("Deploy cancelled.");
|
|
2241
2208
|
return;
|
|
2242
2209
|
}
|
|
2243
2210
|
}
|
|
2244
2211
|
|
|
2245
|
-
info("
|
|
2212
|
+
info("Creating prod app...");
|
|
2246
2213
|
const createProdRes = await platformFetch(creds, `/platform/apps/${appId}/create-prod`, {
|
|
2247
2214
|
method: "POST",
|
|
2248
2215
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2252,19 +2219,19 @@ ${BOLD}Examples:${RESET}
|
|
|
2252
2219
|
const errData = await createProdRes.json().catch(() => ({}));
|
|
2253
2220
|
if (createProdRes.status === 403) {
|
|
2254
2221
|
log("");
|
|
2255
|
-
log(` ${RED}⛔ Production Deploy — Pro
|
|
2222
|
+
log(` ${RED}⛔ Production Deploy — Pro plan or higher required${RESET}`);
|
|
2256
2223
|
log("");
|
|
2257
|
-
log(` ${DIM}gencow deploy
|
|
2224
|
+
log(` ${DIM}gencow deploy is for production deployment.${RESET}`);
|
|
2258
2225
|
log("");
|
|
2259
|
-
log(` ${BOLD}💡
|
|
2260
|
-
log(` ${GREEN}gencow dev${RESET} ${DIM}←
|
|
2261
|
-
log(` ${GREEN}gencow static dist/${RESET} ${DIM}←
|
|
2226
|
+
log(` ${BOLD}💡 Dev environment deploy:${RESET}`);
|
|
2227
|
+
log(` ${GREEN}gencow dev${RESET} ${DIM}← Real-time backend + live logs${RESET}`);
|
|
2228
|
+
log(` ${GREEN}gencow static dist/${RESET} ${DIM}← Static file (dist/) deploy${RESET}`);
|
|
2262
2229
|
log("");
|
|
2263
|
-
log(` ${BOLD}🚀
|
|
2264
|
-
log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Pro
|
|
2230
|
+
log(` ${BOLD}🚀 Unlock production deploy:${RESET}`);
|
|
2231
|
+
log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Pro plan${RESET}`);
|
|
2265
2232
|
log("");
|
|
2266
2233
|
} else {
|
|
2267
|
-
error(`Prod
|
|
2234
|
+
error(`Prod app creation failed: ${errData.error || createProdRes.statusText}`);
|
|
2268
2235
|
}
|
|
2269
2236
|
process.exit(1);
|
|
2270
2237
|
}
|
|
@@ -2280,9 +2247,9 @@ ${BOLD}Examples:${RESET}
|
|
|
2280
2247
|
writeFileSync(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
|
|
2281
2248
|
|
|
2282
2249
|
if (createProdData.alreadyExists) {
|
|
2283
|
-
info(`Prod
|
|
2250
|
+
info(`Prod app verified: ${prodAppId}`);
|
|
2284
2251
|
} else {
|
|
2285
|
-
success(`Prod
|
|
2252
|
+
success(`Prod app created: ${prodAppId}`);
|
|
2286
2253
|
info(`URL: ${createProdData.url}`);
|
|
2287
2254
|
// 프로비저닝 대기
|
|
2288
2255
|
await new Promise(r => setTimeout(r, 3000));
|
|
@@ -2291,23 +2258,23 @@ ${BOLD}Examples:${RESET}
|
|
|
2291
2258
|
|
|
2292
2259
|
// 배포 대상을 prod 앱으로 전환
|
|
2293
2260
|
appId = prodAppId;
|
|
2294
|
-
info(
|
|
2261
|
+
info(`Deploy target: ${CYAN}${appId}${RESET} (production)`);
|
|
2295
2262
|
}
|
|
2296
2263
|
|
|
2297
2264
|
log(`\n${BOLD}${CYAN}Gencow Deploy${RESET}\n`);
|
|
2298
2265
|
if (appId) {
|
|
2299
|
-
info(
|
|
2266
|
+
info(`App: ${appId}`);
|
|
2300
2267
|
} else {
|
|
2301
|
-
info(
|
|
2268
|
+
info(`App: new (auto-generated ID)`);
|
|
2302
2269
|
}
|
|
2303
|
-
info(
|
|
2304
|
-
info(
|
|
2270
|
+
info(`Env: ${envTarget}`);
|
|
2271
|
+
info(`Format: tar.gz`);
|
|
2305
2272
|
log("");
|
|
2306
2273
|
|
|
2307
2274
|
// 0-1. drizzle-kit generate 자동 실행 (번들링 전 migrations/ 최신화)
|
|
2308
2275
|
{
|
|
2309
2276
|
const { execSync: execGen } = await import("child_process");
|
|
2310
|
-
info("
|
|
2277
|
+
info("Generating schema migrations...");
|
|
2311
2278
|
try {
|
|
2312
2279
|
if (isMonorepo() && !isStandaloneProject()) {
|
|
2313
2280
|
// 모노레포: package.json scripts.db:generate 사용
|
|
@@ -2332,18 +2299,18 @@ ${BOLD}Examples:${RESET}
|
|
|
2332
2299
|
msg.includes("nothing to migrate") ||
|
|
2333
2300
|
msg.includes("No changes detected")
|
|
2334
2301
|
) {
|
|
2335
|
-
log(`${DIM}
|
|
2302
|
+
log(`${DIM} No schema changes — keeping existing migrations${RESET}`);
|
|
2336
2303
|
} else {
|
|
2337
|
-
warn(
|
|
2338
|
-
warn("gencow/migrations/
|
|
2339
|
-
info("
|
|
2304
|
+
warn(`Migration generation warning: ${msg.split("\n")[0] || "unknown"}`);
|
|
2305
|
+
warn("gencow/migrations/ missing or outdated — platform may skip schema apply.");
|
|
2306
|
+
info("Manual run: gencow db:generate");
|
|
2340
2307
|
}
|
|
2341
2308
|
}
|
|
2342
2309
|
}
|
|
2343
2310
|
log("");
|
|
2344
2311
|
|
|
2345
2312
|
// 1. tar.gz 패키징
|
|
2346
|
-
info("
|
|
2313
|
+
info("Packaging project...");
|
|
2347
2314
|
const { execSync: exec } = await import("child_process");
|
|
2348
2315
|
const tmpBundle = resolve(process.cwd(), ".gencow", "deploy-bundle.tar.gz");
|
|
2349
2316
|
mkdirSync(dirname(tmpBundle), { recursive: true });
|
|
@@ -2356,7 +2323,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2356
2323
|
|
|
2357
2324
|
// gencow/ 디렉토리 존재 확인
|
|
2358
2325
|
if (!existsSync(resolve(process.cwd(), "gencow"))) {
|
|
2359
|
-
error("gencow/
|
|
2326
|
+
error("gencow/ directory not found. Run from Gencow project root.");
|
|
2360
2327
|
process.exit(1);
|
|
2361
2328
|
}
|
|
2362
2329
|
|
|
@@ -2373,7 +2340,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2373
2340
|
if (auditMsg) log(auditMsg);
|
|
2374
2341
|
}
|
|
2375
2342
|
} catch (auditErr) {
|
|
2376
|
-
warn(
|
|
2343
|
+
warn(`Skipping dependency audit: ${auditErr.message}`);
|
|
2377
2344
|
}
|
|
2378
2345
|
}
|
|
2379
2346
|
|
|
@@ -2395,7 +2362,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2395
2362
|
const totalDeps = Object.keys(allDeps).length;
|
|
2396
2363
|
const runtimeCount = Object.keys(runtimeDeps).length;
|
|
2397
2364
|
if (totalDeps > runtimeCount) {
|
|
2398
|
-
info(`${DIM}package.json
|
|
2365
|
+
info(`${DIM}package.json filtered: ${runtimeCount}/${totalDeps} packages will be installed on server.${RESET}`);
|
|
2399
2366
|
}
|
|
2400
2367
|
} catch {
|
|
2401
2368
|
// Fallback: use original package.json
|
|
@@ -2425,7 +2392,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2425
2392
|
exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
|
|
2426
2393
|
}
|
|
2427
2394
|
} catch (e) {
|
|
2428
|
-
error(
|
|
2395
|
+
error(`Packaging failed: ${e.message}`);
|
|
2429
2396
|
process.exit(1);
|
|
2430
2397
|
}
|
|
2431
2398
|
|
|
@@ -2433,7 +2400,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2433
2400
|
try { if (useFilteredPkg) unlinkSync(filteredPkgPath); } catch {}
|
|
2434
2401
|
|
|
2435
2402
|
const bundleSize = statSync(tmpBundle).size;
|
|
2436
|
-
success(
|
|
2403
|
+
success(`Bundle created: ${(bundleSize / 1024).toFixed(1)} KB`);
|
|
2437
2404
|
|
|
2438
2405
|
// 2-0. 번들 크기 경고 (Ph2: 리소스 미터링 — 차단 아님, 안내만)
|
|
2439
2406
|
try {
|
|
@@ -2449,13 +2416,13 @@ ${BOLD}Examples:${RESET}
|
|
|
2449
2416
|
|
|
2450
2417
|
// 2. 앱이 없으면 생성 (appId가 없는 경우)
|
|
2451
2418
|
if (!appId) {
|
|
2452
|
-
info("
|
|
2419
|
+
info("Creating app (auto-generating ID)...");
|
|
2453
2420
|
|
|
2454
2421
|
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
2455
2422
|
|
|
2456
2423
|
if (!createRes.ok) {
|
|
2457
2424
|
const createErr = await createRes.json().catch(() => ({}));
|
|
2458
|
-
error(
|
|
2425
|
+
error(`App creation failed: ${createErr.error || createRes.statusText}`);
|
|
2459
2426
|
process.exit(1);
|
|
2460
2427
|
}
|
|
2461
2428
|
|
|
@@ -2463,11 +2430,11 @@ ${BOLD}Examples:${RESET}
|
|
|
2463
2430
|
appId = createData.appId || createData.name;
|
|
2464
2431
|
|
|
2465
2432
|
if (!appId) {
|
|
2466
|
-
error("
|
|
2433
|
+
error("App creation response missing appId.");
|
|
2467
2434
|
process.exit(1);
|
|
2468
2435
|
}
|
|
2469
2436
|
|
|
2470
|
-
success(
|
|
2437
|
+
success(`App "${appId}" created. Waiting for provisioning...`);
|
|
2471
2438
|
|
|
2472
2439
|
// gencow.json 저장 (즉시)
|
|
2473
2440
|
writeFileSync(gencowJsonPath, JSON.stringify({
|
|
@@ -2475,7 +2442,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2475
2442
|
displayName,
|
|
2476
2443
|
platformUrl: creds.platformUrl,
|
|
2477
2444
|
}, null, 2));
|
|
2478
|
-
info(`${DIM}gencow.json
|
|
2445
|
+
info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
|
|
2479
2446
|
|
|
2480
2447
|
await new Promise(r => setTimeout(r, 3000));
|
|
2481
2448
|
}
|
|
@@ -2489,7 +2456,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2489
2456
|
let spinnerIdx = 0;
|
|
2490
2457
|
const spinner = setInterval(() => {
|
|
2491
2458
|
const elapsed = ((Date.now() - deployStartTime) / 1000).toFixed(0);
|
|
2492
|
-
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET}
|
|
2459
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Deploying... ${DIM}(${elapsed}s)${RESET} `);
|
|
2493
2460
|
}, 120);
|
|
2494
2461
|
|
|
2495
2462
|
const deployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
|
|
@@ -2512,19 +2479,19 @@ ${BOLD}Examples:${RESET}
|
|
|
2512
2479
|
|
|
2513
2480
|
// 앱이 없으면 (appId가 stale한 경우) 새로 생성 후 재시도
|
|
2514
2481
|
if (deployRes.status === 404) {
|
|
2515
|
-
warn(
|
|
2482
|
+
warn(`App "${appId}" not found. Creating new one...`);
|
|
2516
2483
|
|
|
2517
2484
|
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
2518
2485
|
|
|
2519
2486
|
if (!createRes.ok) {
|
|
2520
2487
|
const createErr = await createRes.json().catch(() => ({}));
|
|
2521
|
-
error(
|
|
2488
|
+
error(`App creation failed: ${createErr.error || createRes.statusText}`);
|
|
2522
2489
|
process.exit(1);
|
|
2523
2490
|
}
|
|
2524
2491
|
|
|
2525
2492
|
const createData = await createRes.json();
|
|
2526
2493
|
appId = createData.appId || createData.name;
|
|
2527
|
-
success(
|
|
2494
|
+
success(`App "${appId}" created. Provisioning...`);
|
|
2528
2495
|
|
|
2529
2496
|
// gencow.json 업데이트
|
|
2530
2497
|
writeFileSync(gencowJsonPath, JSON.stringify({
|
|
@@ -2536,7 +2503,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2536
2503
|
await new Promise(r => setTimeout(r, 3000));
|
|
2537
2504
|
|
|
2538
2505
|
// 재배포
|
|
2539
|
-
info("
|
|
2506
|
+
info("Retrying deploy...");
|
|
2540
2507
|
exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
|
|
2541
2508
|
const retryBuffer = readFileSync(tmpBundle);
|
|
2542
2509
|
|
|
@@ -2544,7 +2511,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2544
2511
|
let retrySpinnerIdx = 0;
|
|
2545
2512
|
const retrySpinner = setInterval(() => {
|
|
2546
2513
|
const elapsed = ((Date.now() - retryStartTime) / 1000).toFixed(0);
|
|
2547
|
-
process.stdout.write(`\r ${CYAN}${spinnerFrames[retrySpinnerIdx++ % spinnerFrames.length]}${RESET}
|
|
2514
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[retrySpinnerIdx++ % spinnerFrames.length]}${RESET} Deploying... ${DIM}(${elapsed}s)${RESET} `);
|
|
2548
2515
|
}, 120);
|
|
2549
2516
|
|
|
2550
2517
|
const retryRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
|
|
@@ -2562,10 +2529,10 @@ ${BOLD}Examples:${RESET}
|
|
|
2562
2529
|
|
|
2563
2530
|
if (!retryRes.ok) {
|
|
2564
2531
|
const retryErr = await retryRes.json().catch(() => ({}));
|
|
2565
|
-
error(
|
|
2532
|
+
error(`Deploy failed: ${retryErr.error || retryRes.statusText}`);
|
|
2566
2533
|
if (retryErr.crashLogs?.length) {
|
|
2567
2534
|
log("");
|
|
2568
|
-
|
|
2535
|
+
warn("Server startup failure:");
|
|
2569
2536
|
for (const line of retryErr.crashLogs) log(` ${DIM}${line}${RESET}`);
|
|
2570
2537
|
}
|
|
2571
2538
|
process.exit(1);
|
|
@@ -2574,14 +2541,14 @@ ${BOLD}Examples:${RESET}
|
|
|
2574
2541
|
const retryData = await retryRes.json();
|
|
2575
2542
|
const retryElapsed = ((Date.now() - retryStartTime) / 1000).toFixed(1);
|
|
2576
2543
|
log("");
|
|
2577
|
-
success(
|
|
2544
|
+
success(`Server build complete! (${retryElapsed}s)`);
|
|
2578
2545
|
|
|
2579
2546
|
// Health check — 앱 URL로 실제 응답 확인
|
|
2580
2547
|
if (retryData.url) {
|
|
2581
2548
|
await this._verifyAppReady(retryData.url, appId);
|
|
2582
2549
|
}
|
|
2583
2550
|
|
|
2584
|
-
info(
|
|
2551
|
+
info(`App ID: ${appId}`);
|
|
2585
2552
|
info(`URL: ${retryData.url}`);
|
|
2586
2553
|
info(`Hash: ${retryData.bundleHash}`);
|
|
2587
2554
|
updateEnvLocalUrl(retryData.url);
|
|
@@ -2589,10 +2556,10 @@ ${BOLD}Examples:${RESET}
|
|
|
2589
2556
|
return;
|
|
2590
2557
|
}
|
|
2591
2558
|
|
|
2592
|
-
error(
|
|
2559
|
+
error(`Deploy failed: ${errData.error || deployRes.statusText}`);
|
|
2593
2560
|
if (errData.crashLogs?.length) {
|
|
2594
2561
|
log("");
|
|
2595
|
-
warn("
|
|
2562
|
+
warn("Server startup failure cause:");
|
|
2596
2563
|
for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
|
|
2597
2564
|
}
|
|
2598
2565
|
process.exit(1);
|
|
@@ -2602,14 +2569,14 @@ ${BOLD}Examples:${RESET}
|
|
|
2602
2569
|
const deployElapsed = ((Date.now() - deployStartTime) / 1000).toFixed(1);
|
|
2603
2570
|
|
|
2604
2571
|
log("");
|
|
2605
|
-
success(
|
|
2572
|
+
success(`Server build complete! (${deployElapsed}s)`);
|
|
2606
2573
|
|
|
2607
2574
|
// Health check — 앱 URL로 실제 응답 확인 (Blue-Green은 서버에서 완료됐지만, edge case 방어)
|
|
2608
2575
|
if (deployData.url) {
|
|
2609
2576
|
await this._verifyAppReady(deployData.url, appId);
|
|
2610
2577
|
}
|
|
2611
2578
|
|
|
2612
|
-
info(
|
|
2579
|
+
info(`App ID: ${appId}`);
|
|
2613
2580
|
info(`URL: ${deployData.url}`);
|
|
2614
2581
|
info(`Hash: ${deployData.bundleHash}`);
|
|
2615
2582
|
if (deployData.deployId) info(`ID: ${deployData.deployId}`);
|
|
@@ -2623,7 +2590,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2623
2590
|
displayName,
|
|
2624
2591
|
platformUrl: creds.platformUrl,
|
|
2625
2592
|
}, null, 2));
|
|
2626
|
-
info(`${DIM}gencow.json
|
|
2593
|
+
info(`${DIM}gencow.json created${RESET}`);
|
|
2627
2594
|
}
|
|
2628
2595
|
},
|
|
2629
2596
|
|
|
@@ -2643,7 +2610,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2643
2610
|
// health check 스피너
|
|
2644
2611
|
const healthSpinner = setInterval(() => {
|
|
2645
2612
|
const elapsed = ((Date.now() - start) / 1000).toFixed(0);
|
|
2646
|
-
process.stdout.write(`\r ${CYAN}${spinnerFrames[idx++ % spinnerFrames.length]}${RESET}
|
|
2613
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[idx++ % spinnerFrames.length]}${RESET} Checking app readiness... ${DIM}(${elapsed}s)${RESET} `);
|
|
2647
2614
|
}, 120);
|
|
2648
2615
|
|
|
2649
2616
|
let healthy = false;
|
|
@@ -2669,10 +2636,10 @@ ${BOLD}Examples:${RESET}
|
|
|
2669
2636
|
|
|
2670
2637
|
const healthElapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
2671
2638
|
if (healthy) {
|
|
2672
|
-
success(
|
|
2639
|
+
success(`App Ready! (${healthElapsed}s)`);
|
|
2673
2640
|
} else {
|
|
2674
|
-
warn(
|
|
2675
|
-
info(
|
|
2641
|
+
warn(`App response check failed (${healthElapsed}s). Server may still be starting.`);
|
|
2642
|
+
info(`Manual check: ${CYAN}${appUrl}${RESET}`);
|
|
2676
2643
|
}
|
|
2677
2644
|
},
|
|
2678
2645
|
|
|
@@ -2695,9 +2662,9 @@ ${BOLD}Examples:${RESET}
|
|
|
2695
2662
|
}
|
|
2696
2663
|
|
|
2697
2664
|
if (!targetDir || !existsSync(resolve(process.cwd(), targetDir))) {
|
|
2698
|
-
error(
|
|
2699
|
-
info(
|
|
2700
|
-
info(
|
|
2665
|
+
error(`Static files directory not found.`);
|
|
2666
|
+
info(`Auto-detect order: ${AUTO_DETECT.join(", ")}`);
|
|
2667
|
+
info(`Specify manually: gencow static <dir>`);
|
|
2701
2668
|
process.exit(1);
|
|
2702
2669
|
}
|
|
2703
2670
|
|
|
@@ -2748,22 +2715,22 @@ ${BOLD}Examples:${RESET}
|
|
|
2748
2715
|
const shouldDeployBackend = detectedBackend && !noBackend && !isBackendEmpty;
|
|
2749
2716
|
|
|
2750
2717
|
if (isBackendEmpty && detectedBackend) {
|
|
2751
|
-
info(`${DIM}gencow/
|
|
2752
|
-
info(`${DIM}💡
|
|
2718
|
+
info(`${DIM}gencow/ detected — no API functions (query/mutation) found → skipping backend deploy${RESET}`);
|
|
2719
|
+
info(`${DIM}💡 If you need a backend, define query() or mutation() in gencow/*.ts files.${RESET}`);
|
|
2753
2720
|
}
|
|
2754
2721
|
|
|
2755
2722
|
if (shouldDeployBackend) {
|
|
2756
2723
|
log(`\n${BOLD}${CYAN}Gencow Deploy (Fullstack)${RESET}\n`);
|
|
2757
|
-
info(
|
|
2758
|
-
info(
|
|
2724
|
+
info(`Backend detected: ${backendRoot === process.cwd() ? "gencow/" : resolve(backendRoot, "gencow/")} (auto-deploy)`);
|
|
2725
|
+
info(`Frontend: ${targetDir}/`);
|
|
2759
2726
|
} else {
|
|
2760
2727
|
log(`\n${BOLD}${CYAN}Gencow Static Deploy${RESET}\n`);
|
|
2761
|
-
info(
|
|
2728
|
+
info(`Directory: ${targetDir}/`);
|
|
2762
2729
|
}
|
|
2763
2730
|
if (appId) {
|
|
2764
|
-
info(
|
|
2731
|
+
info(`App ID: ${appId}`);
|
|
2765
2732
|
} else {
|
|
2766
|
-
info(
|
|
2733
|
+
info(`App: new (ID auto-generated)`);
|
|
2767
2734
|
}
|
|
2768
2735
|
log("");
|
|
2769
2736
|
|
|
@@ -2795,23 +2762,23 @@ ${BOLD}Examples:${RESET}
|
|
|
2795
2762
|
}
|
|
2796
2763
|
if (apiRefFiles.length > 0) {
|
|
2797
2764
|
log("");
|
|
2798
|
-
warn(
|
|
2765
|
+
warn(`API references found in build files:`);
|
|
2799
2766
|
for (const f of apiRefFiles.slice(0, 5)) log(` ${DIM}- ${f}${RESET}`);
|
|
2800
|
-
if (apiRefFiles.length > 5) log(` ${DIM}...
|
|
2767
|
+
if (apiRefFiles.length > 5) log(` ${DIM}... and ${apiRefFiles.length - 5} more${RESET}`);
|
|
2801
2768
|
log("");
|
|
2802
|
-
warn(
|
|
2803
|
-
info(`💡
|
|
2804
|
-
info(`💡
|
|
2769
|
+
warn(`Static hosting has no API server — these will return 404.`);
|
|
2770
|
+
info(`💡 If you need a backend: ${CYAN}VITE_API_URL=https://{backend}.{domain} npm run build${RESET} then deploy`);
|
|
2771
|
+
info(`💡 If using a separate backend: set ${CYAN}VITE_API_URL${RESET} env var before building.`);
|
|
2805
2772
|
log("");
|
|
2806
2773
|
|
|
2807
2774
|
const { createInterface } = await import("readline");
|
|
2808
2775
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2809
2776
|
const answer = await new Promise(resolve => {
|
|
2810
|
-
rl.question(` ${YELLOW}⚠${RESET}
|
|
2777
|
+
rl.question(` ${YELLOW}⚠${RESET} Proceed with static deploy anyway? (y/N) `, resolve);
|
|
2811
2778
|
});
|
|
2812
2779
|
rl.close();
|
|
2813
2780
|
if (answer.toLowerCase() !== "y") {
|
|
2814
|
-
info("
|
|
2781
|
+
info("Deploy cancelled.");
|
|
2815
2782
|
return;
|
|
2816
2783
|
}
|
|
2817
2784
|
log("");
|
|
@@ -2821,7 +2788,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2821
2788
|
|
|
2822
2789
|
// ── 백엔드 자동 배포 (감지된 경우) ────────────────────
|
|
2823
2790
|
if (shouldDeployBackend) {
|
|
2824
|
-
log(` ${BOLD}──
|
|
2791
|
+
log(` ${BOLD}── Backend Deploy ──────────────────────${RESET}\n`);
|
|
2825
2792
|
|
|
2826
2793
|
// 1-0. Pre-deploy dependency audit + Phase B: filter package.json
|
|
2827
2794
|
let auditResult = null;
|
|
@@ -2835,7 +2802,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2835
2802
|
if (auditMsg) log(auditMsg);
|
|
2836
2803
|
}
|
|
2837
2804
|
} catch (auditErr) {
|
|
2838
|
-
warn(
|
|
2805
|
+
warn(`Dependency audit skipped: ${auditErr.message}`);
|
|
2839
2806
|
}
|
|
2840
2807
|
}
|
|
2841
2808
|
|
|
@@ -2843,7 +2810,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2843
2810
|
// 서버에서 generate 시 stdin이 없어 rename 프롬프트에 막히던 문제 해결
|
|
2844
2811
|
const schemaPath = resolve(backendRoot, "gencow", "schema.ts");
|
|
2845
2812
|
if (existsSync(schemaPath)) {
|
|
2846
|
-
info("
|
|
2813
|
+
info("Generating schema migrations...");
|
|
2847
2814
|
try {
|
|
2848
2815
|
const dk = _drizzleKitCmd("generate");
|
|
2849
2816
|
execSync(dk.cmd, {
|
|
@@ -2851,20 +2818,20 @@ ${BOLD}Examples:${RESET}
|
|
|
2851
2818
|
env: { ...process.env, ...dk.env },
|
|
2852
2819
|
stdio: "inherit", // ← 프롬프트 패스스루!
|
|
2853
2820
|
});
|
|
2854
|
-
success("
|
|
2821
|
+
success("Migrations generated");
|
|
2855
2822
|
} catch (e) {
|
|
2856
2823
|
const msg = e.stderr?.toString() || e.message || "";
|
|
2857
2824
|
if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
|
|
2858
|
-
log(`${DIM}
|
|
2825
|
+
log(`${DIM} No schema changes — using existing migrations${RESET}`);
|
|
2859
2826
|
} else {
|
|
2860
|
-
warn(
|
|
2861
|
-
info("
|
|
2827
|
+
warn(`Migration generation failed: ${msg.split("\\n")[0]}`);
|
|
2828
|
+
info("Will attempt schema push via server.");
|
|
2862
2829
|
}
|
|
2863
2830
|
}
|
|
2864
2831
|
}
|
|
2865
2832
|
|
|
2866
2833
|
// 1. tar.gz 패키징 (백엔드)
|
|
2867
|
-
info("
|
|
2834
|
+
info("Packaging backend...");
|
|
2868
2835
|
const { execSync: exec } = await import("child_process");
|
|
2869
2836
|
const tmpBackendBundle = resolve(backendRoot, ".gencow", "deploy-bundle.tar.gz");
|
|
2870
2837
|
mkdirSync(dirname(tmpBackendBundle), { recursive: true });
|
|
@@ -2892,7 +2859,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2892
2859
|
const totalDeps = Object.keys(allDeps).length;
|
|
2893
2860
|
const runtimeCount = Object.keys(runtimeDeps).length;
|
|
2894
2861
|
if (totalDeps > runtimeCount) {
|
|
2895
|
-
info(`${DIM}package.json
|
|
2862
|
+
info(`${DIM}package.json filtered: ${runtimeCount}/${totalDeps} packages will be installed on server.${RESET}`);
|
|
2896
2863
|
}
|
|
2897
2864
|
} catch {
|
|
2898
2865
|
useFilteredPkg = false;
|
|
@@ -2921,34 +2888,34 @@ ${BOLD}Examples:${RESET}
|
|
|
2921
2888
|
exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBackendBundle}" ${backendFiles.join(" ")}`, { cwd: backendRoot });
|
|
2922
2889
|
}
|
|
2923
2890
|
} catch (e) {
|
|
2924
|
-
error(
|
|
2891
|
+
error(`Backend packaging failed: ${e.message}`);
|
|
2925
2892
|
process.exit(1);
|
|
2926
2893
|
}
|
|
2927
2894
|
try { if (useFilteredPkg) unlinkSync(filteredPkgPath); } catch {}
|
|
2928
2895
|
|
|
2929
2896
|
const backendBundleSize = statSync(tmpBackendBundle).size;
|
|
2930
|
-
success(
|
|
2897
|
+
success(`Backend bundle created: ${(backendBundleSize / 1024).toFixed(1)} KB`);
|
|
2931
2898
|
|
|
2932
2899
|
// 2. 앱이 없으면 생성
|
|
2933
2900
|
if (!appId) {
|
|
2934
|
-
info("
|
|
2901
|
+
info("Creating app (auto-generating ID)...");
|
|
2935
2902
|
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
2936
2903
|
if (!createRes.ok) {
|
|
2937
2904
|
const createErr = await createRes.json().catch(() => ({}));
|
|
2938
|
-
error(
|
|
2905
|
+
error(`App creation failed: ${createErr.error || createRes.statusText}`);
|
|
2939
2906
|
process.exit(1);
|
|
2940
2907
|
}
|
|
2941
2908
|
const createData = await createRes.json();
|
|
2942
2909
|
appId = createData.appId || createData.name;
|
|
2943
2910
|
if (!appId) {
|
|
2944
|
-
error("
|
|
2911
|
+
error("App creation response missing appId.");
|
|
2945
2912
|
process.exit(1);
|
|
2946
2913
|
}
|
|
2947
|
-
success(
|
|
2914
|
+
success(`App "${appId}" created. Provisioning...`);
|
|
2948
2915
|
writeFileSync(gencowJsonPath, JSON.stringify({
|
|
2949
2916
|
appId, displayName, platformUrl: creds.platformUrl,
|
|
2950
2917
|
}, null, 2));
|
|
2951
|
-
info(`${DIM}gencow.json
|
|
2918
|
+
info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
|
|
2952
2919
|
await new Promise(r => setTimeout(r, 3000));
|
|
2953
2920
|
}
|
|
2954
2921
|
|
|
@@ -2961,7 +2928,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2961
2928
|
let spinnerIdx = 0;
|
|
2962
2929
|
const backendSpinner = setInterval(() => {
|
|
2963
2930
|
const elapsed = ((Date.now() - backendStartTime) / 1000).toFixed(0);
|
|
2964
|
-
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET}
|
|
2931
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} Deploying backend... ${DIM}(${elapsed}s)${RESET} `);
|
|
2965
2932
|
}, 120);
|
|
2966
2933
|
|
|
2967
2934
|
const backendDeployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
|
|
@@ -2976,24 +2943,24 @@ ${BOLD}Examples:${RESET}
|
|
|
2976
2943
|
|
|
2977
2944
|
if (!backendDeployRes.ok) {
|
|
2978
2945
|
const errData = await backendDeployRes.json().catch(() => ({}));
|
|
2979
|
-
error(
|
|
2946
|
+
error(`Backend deploy failed: ${errData.error || backendDeployRes.statusText}`);
|
|
2980
2947
|
if (errData.crashLogs?.length) {
|
|
2981
2948
|
log("");
|
|
2982
|
-
warn("
|
|
2949
|
+
warn("Server startup failure:");
|
|
2983
2950
|
for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
|
|
2984
2951
|
}
|
|
2985
2952
|
if (targetDir) {
|
|
2986
2953
|
// --static 모드: 백엔드 실패해도 프론트엔드 배포는 계속 진행
|
|
2987
|
-
warn("
|
|
2954
|
+
warn("Backend deploy failed — continuing with static deploy.");
|
|
2988
2955
|
log("");
|
|
2989
2956
|
} else {
|
|
2990
|
-
error("
|
|
2957
|
+
error("Backend deploy failed, skipping frontend deploy.");
|
|
2991
2958
|
process.exit(1);
|
|
2992
2959
|
}
|
|
2993
2960
|
} else {
|
|
2994
2961
|
const backendData = await backendDeployRes.json();
|
|
2995
2962
|
const backendElapsed = ((Date.now() - backendStartTime) / 1000).toFixed(1);
|
|
2996
|
-
success(
|
|
2963
|
+
success(`Backend build complete! (${backendElapsed}s)`);
|
|
2997
2964
|
|
|
2998
2965
|
// Health check — 백엔드 앱 URL로 실제 응답 확인
|
|
2999
2966
|
if (backendData.url) {
|
|
@@ -3005,11 +2972,11 @@ ${BOLD}Examples:${RESET}
|
|
|
3005
2972
|
updateEnvLocalUrl(backendData.url);
|
|
3006
2973
|
log("");
|
|
3007
2974
|
}
|
|
3008
|
-
log(` ${BOLD}──
|
|
2975
|
+
log(` ${BOLD}── Frontend Deploy ──────────────────${RESET}\n`);
|
|
3009
2976
|
}
|
|
3010
2977
|
|
|
3011
2978
|
// 1. tar.gz 패키징
|
|
3012
|
-
info("
|
|
2979
|
+
info("Packaging static files...");
|
|
3013
2980
|
const { execSync: exec } = await import("child_process");
|
|
3014
2981
|
const tmpBundle = resolve(process.cwd(), ".gencow", "static-bundle.tar.gz");
|
|
3015
2982
|
mkdirSync(dirname(tmpBundle), { recursive: true });
|
|
@@ -3017,40 +2984,40 @@ ${BOLD}Examples:${RESET}
|
|
|
3017
2984
|
try {
|
|
3018
2985
|
exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" -C "${resolve(process.cwd(), targetDir)}" .`, { cwd: process.cwd() });
|
|
3019
2986
|
} catch (e) {
|
|
3020
|
-
error(
|
|
2987
|
+
error(`Packaging failed: ${e.message}`);
|
|
3021
2988
|
process.exit(1);
|
|
3022
2989
|
}
|
|
3023
2990
|
|
|
3024
2991
|
const bundleSize = statSync(tmpBundle).size;
|
|
3025
2992
|
const MAX_SIZE = 100 * 1024 * 1024;
|
|
3026
2993
|
if (bundleSize > MAX_SIZE) {
|
|
3027
|
-
error(
|
|
2994
|
+
error(`Bundle too large: ${(bundleSize / (1024 * 1024)).toFixed(1)} MB (max 100 MB)`);
|
|
3028
2995
|
try { unlinkSync(tmpBundle); } catch { }
|
|
3029
2996
|
process.exit(1);
|
|
3030
2997
|
}
|
|
3031
|
-
success(
|
|
2998
|
+
success(`Bundle created: ${(bundleSize / 1024).toFixed(1)} KB`);
|
|
3032
2999
|
|
|
3033
3000
|
// 2. 앱이 없으면 생성
|
|
3034
3001
|
if (!appId) {
|
|
3035
|
-
info("
|
|
3002
|
+
info("Creating app (auto-generating ID)...");
|
|
3036
3003
|
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
3037
3004
|
if (!createRes.ok) {
|
|
3038
3005
|
const createErr = await createRes.json().catch(() => ({}));
|
|
3039
|
-
error(
|
|
3006
|
+
error(`App creation failed: ${createErr.error || createRes.statusText}`);
|
|
3040
3007
|
process.exit(1);
|
|
3041
3008
|
}
|
|
3042
3009
|
const createData = await createRes.json();
|
|
3043
3010
|
appId = createData.appId || createData.name;
|
|
3044
|
-
success(
|
|
3011
|
+
success(`App "${appId}" created. Provisioning...`);
|
|
3045
3012
|
writeFileSync(gencowJsonPath, JSON.stringify({
|
|
3046
3013
|
appId, displayName, platformUrl: creds.platformUrl,
|
|
3047
3014
|
}, null, 2));
|
|
3048
|
-
info(`${DIM}gencow.json
|
|
3015
|
+
info(`${DIM}gencow.json created (appId: ${appId})${RESET}`);
|
|
3049
3016
|
await new Promise(r => setTimeout(r, 3000));
|
|
3050
3017
|
}
|
|
3051
3018
|
|
|
3052
3019
|
// 3. 업로드
|
|
3053
|
-
info("
|
|
3020
|
+
info("Deploying static files...");
|
|
3054
3021
|
const bundleBuffer = readFileSync(tmpBundle);
|
|
3055
3022
|
|
|
3056
3023
|
const deployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy-static`, {
|
|
@@ -3063,22 +3030,22 @@ ${BOLD}Examples:${RESET}
|
|
|
3063
3030
|
|
|
3064
3031
|
if (!deployRes.ok) {
|
|
3065
3032
|
const errData = await deployRes.json().catch(() => ({}));
|
|
3066
|
-
error(
|
|
3033
|
+
error(`Static deploy failed: ${errData.error || deployRes.statusText}`);
|
|
3067
3034
|
process.exit(1);
|
|
3068
3035
|
}
|
|
3069
3036
|
|
|
3070
3037
|
const data = await deployRes.json();
|
|
3071
3038
|
log("");
|
|
3072
|
-
success(
|
|
3073
|
-
info(
|
|
3039
|
+
success(`Static deploy complete!`);
|
|
3040
|
+
info(`App ID: ${appId}`);
|
|
3074
3041
|
info(`URL: ${data.url}`);
|
|
3075
|
-
info(
|
|
3042
|
+
info(`Files: ${data.files} (${(data.size / 1024).toFixed(1)} KB)`);
|
|
3076
3043
|
info(`Hash: ${data.bundleHash}`);
|
|
3077
3044
|
if (data.deployId) info(`ID: ${data.deployId}`);
|
|
3078
3045
|
|
|
3079
3046
|
// 이미지 최적화 결과 표시
|
|
3080
3047
|
if (data.optimizedImages) {
|
|
3081
|
-
success(`🖼 ${data.optimizedImages}
|
|
3048
|
+
success(`🖼 ${data.optimizedImages} images optimized to WebP (saved: ${(data.savedBytes / 1024).toFixed(1)} KB)`);
|
|
3082
3049
|
}
|
|
3083
3050
|
|
|
3084
3051
|
// skipped 파일 경고 표시
|
|
@@ -3131,12 +3098,12 @@ ${BOLD}Examples:${RESET}
|
|
|
3131
3098
|
|
|
3132
3099
|
// --prod인데 prod 앱이 없는 경우
|
|
3133
3100
|
if (envTarget === "prod" && appId && !appId.endsWith("-prod")) {
|
|
3134
|
-
error("
|
|
3101
|
+
error("No prod app yet. Run gencow deploy first.");
|
|
3135
3102
|
return;
|
|
3136
3103
|
}
|
|
3137
3104
|
|
|
3138
3105
|
if (!appId) {
|
|
3139
|
-
error("
|
|
3106
|
+
error("App ID not found. Run gencow deploy first.");
|
|
3140
3107
|
return;
|
|
3141
3108
|
}
|
|
3142
3109
|
|
|
@@ -3148,13 +3115,13 @@ ${BOLD}Examples:${RESET}
|
|
|
3148
3115
|
{ headers: { "Authorization": `Bearer ${creds.apiKey}` } }
|
|
3149
3116
|
);
|
|
3150
3117
|
if (!res.ok) {
|
|
3151
|
-
error(
|
|
3118
|
+
error(`Failed to list env vars: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
|
|
3152
3119
|
return;
|
|
3153
3120
|
}
|
|
3154
3121
|
const vars = await res.json();
|
|
3155
|
-
log(`\n${BOLD}${CYAN}
|
|
3122
|
+
log(`\n${BOLD}${CYAN}Environment Variables${RESET} — ${appId} (${envTarget})\n`);
|
|
3156
3123
|
if (vars.length === 0) {
|
|
3157
|
-
info("
|
|
3124
|
+
info("No environment variables configured.");
|
|
3158
3125
|
} else {
|
|
3159
3126
|
for (const v of vars) {
|
|
3160
3127
|
log(` ${v.key}`);
|
|
@@ -3168,7 +3135,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3168
3135
|
// gencow env set KEY=VALUE [KEY2=VALUE2...]
|
|
3169
3136
|
const kvPairs = restArgs.filter(a => a.includes("=") && !a.startsWith("-"));
|
|
3170
3137
|
if (kvPairs.length === 0) {
|
|
3171
|
-
error("
|
|
3138
|
+
error("Usage: gencow env set KEY=VALUE [KEY2=VALUE2...]");
|
|
3172
3139
|
return;
|
|
3173
3140
|
}
|
|
3174
3141
|
|
|
@@ -3189,9 +3156,9 @@ ${BOLD}Examples:${RESET}
|
|
|
3189
3156
|
);
|
|
3190
3157
|
|
|
3191
3158
|
if (res.ok) {
|
|
3192
|
-
success(`${key}
|
|
3159
|
+
success(`${key} configured (${envTarget})`);
|
|
3193
3160
|
} else {
|
|
3194
|
-
error(`${key}
|
|
3161
|
+
error(`${key} configuration failed`);
|
|
3195
3162
|
}
|
|
3196
3163
|
}
|
|
3197
3164
|
break;
|
|
@@ -3201,7 +3168,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3201
3168
|
case "remove": {
|
|
3202
3169
|
const keys = restArgs.filter(a => !a.startsWith("-"));
|
|
3203
3170
|
if (keys.length === 0) {
|
|
3204
|
-
error("
|
|
3171
|
+
error("Usage: gencow env unset KEY [KEY2...]");
|
|
3205
3172
|
return;
|
|
3206
3173
|
}
|
|
3207
3174
|
|
|
@@ -3215,9 +3182,9 @@ ${BOLD}Examples:${RESET}
|
|
|
3215
3182
|
);
|
|
3216
3183
|
|
|
3217
3184
|
if (res.ok) {
|
|
3218
|
-
success(`${key}
|
|
3185
|
+
success(`${key} removed`);
|
|
3219
3186
|
} else {
|
|
3220
|
-
error(`${key}
|
|
3187
|
+
error(`${key} removal failed`);
|
|
3221
3188
|
}
|
|
3222
3189
|
}
|
|
3223
3190
|
break;
|
|
@@ -3227,7 +3194,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3227
3194
|
// 로컬 .env 파일 → 리모트 일괄 push
|
|
3228
3195
|
const envFile = resolve(process.cwd(), envTarget === "prod" ? ".env.production" : ".env");
|
|
3229
3196
|
if (!existsSync(envFile)) {
|
|
3230
|
-
error(`${envFile}
|
|
3197
|
+
error(`${envFile} not found.`);
|
|
3231
3198
|
return;
|
|
3232
3199
|
}
|
|
3233
3200
|
|
|
@@ -3241,7 +3208,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3241
3208
|
}
|
|
3242
3209
|
|
|
3243
3210
|
const count = Object.keys(vars).length;
|
|
3244
|
-
info(
|
|
3211
|
+
info(`Pushing ${count} env vars to ${appId} (${envTarget})...`);
|
|
3245
3212
|
|
|
3246
3213
|
const res = await fetch(
|
|
3247
3214
|
`${creds.platformUrl}/platform/apps/${appId}/env/bulk`,
|
|
@@ -3256,16 +3223,16 @@ ${BOLD}Examples:${RESET}
|
|
|
3256
3223
|
);
|
|
3257
3224
|
|
|
3258
3225
|
if (res.ok) {
|
|
3259
|
-
success(`${count}
|
|
3226
|
+
success(`${count} env vars pushed`);
|
|
3260
3227
|
} else {
|
|
3261
|
-
error(`
|
|
3228
|
+
error(`Push failed: ${(await res.json().catch(() => ({}))).error}`);
|
|
3262
3229
|
}
|
|
3263
3230
|
break;
|
|
3264
3231
|
}
|
|
3265
3232
|
|
|
3266
3233
|
default:
|
|
3267
|
-
error(
|
|
3268
|
-
info("
|
|
3234
|
+
error(`Unknown subcommand: ${subCmd}`);
|
|
3235
|
+
info("Usage: gencow env [list|set|unset|push] ...");
|
|
3269
3236
|
}
|
|
3270
3237
|
},
|
|
3271
3238
|
|
|
@@ -3275,8 +3242,9 @@ ${BOLD}Examples:${RESET}
|
|
|
3275
3242
|
const subCmd = configArgs[0] || "help";
|
|
3276
3243
|
const restArgs = configArgs.slice(1);
|
|
3277
3244
|
|
|
3278
|
-
//
|
|
3245
|
+
// Resolve appId + --prod support
|
|
3279
3246
|
let appId = null;
|
|
3247
|
+
const isProd = restArgs.includes("--prod");
|
|
3280
3248
|
for (let i = 0; i < restArgs.length; i++) {
|
|
3281
3249
|
if (restArgs[i] === "--app" || restArgs[i] === "-a") appId = restArgs[++i];
|
|
3282
3250
|
}
|
|
@@ -3286,22 +3254,31 @@ ${BOLD}Examples:${RESET}
|
|
|
3286
3254
|
if (existsSync(gencowJsonPath)) {
|
|
3287
3255
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
3288
3256
|
appId = gencowJson.appId || gencowJson.appName;
|
|
3257
|
+
if (isProd && gencowJson.prodApp) {
|
|
3258
|
+
appId = gencowJson.prodApp;
|
|
3259
|
+
}
|
|
3289
3260
|
}
|
|
3290
3261
|
}
|
|
3291
3262
|
|
|
3292
3263
|
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
|
|
3264
|
+
log(`\n${BOLD}${CYAN}gencow config${RESET} — App configuration\n`);
|
|
3265
|
+
log(` ${CYAN}set${RESET} image.maxWidth <value> Auto WebP max width (px)`);
|
|
3266
|
+
log(` ${CYAN}set${RESET} image.quality <value> Auto WebP quality (1-100)`);
|
|
3267
|
+
log(` ${CYAN}get${RESET} image Show current image config`);
|
|
3268
|
+
log(` ${CYAN}reset${RESET} image Reset to tier defaults\n`);
|
|
3269
|
+
log(` ${DIM}Options:${RESET}`);
|
|
3270
|
+
log(` ${DIM}--app, -a <name> Target app (default: from gencow.json)${RESET}`);
|
|
3271
|
+
log(` ${DIM}--prod Target production app${RESET}\n`);
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
if (isProd && appId && !appId.endsWith("-prod")) {
|
|
3276
|
+
error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
|
|
3300
3277
|
return;
|
|
3301
3278
|
}
|
|
3302
3279
|
|
|
3303
3280
|
if (!appId) {
|
|
3304
|
-
error("
|
|
3281
|
+
error("App not found. Run 'gencow dev' first.");
|
|
3305
3282
|
return;
|
|
3306
3283
|
}
|
|
3307
3284
|
|
|
@@ -3311,7 +3288,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3311
3288
|
const val = restArgs[restArgs.indexOf(key) + 1];
|
|
3312
3289
|
|
|
3313
3290
|
if (!key || val === undefined) {
|
|
3314
|
-
error("
|
|
3291
|
+
error("Usage: gencow config set image.maxWidth <value>");
|
|
3315
3292
|
return;
|
|
3316
3293
|
}
|
|
3317
3294
|
|
|
@@ -3319,34 +3296,34 @@ ${BOLD}Examples:${RESET}
|
|
|
3319
3296
|
if (key === "image.maxWidth" || key === "image.max-width") {
|
|
3320
3297
|
const v = parseInt(val);
|
|
3321
3298
|
if (isNaN(v) || v < 0 || v > 10000) {
|
|
3322
|
-
error("maxWidth
|
|
3299
|
+
error("maxWidth must be between 0 and 10000 (0 = reset)");
|
|
3323
3300
|
return;
|
|
3324
3301
|
}
|
|
3325
3302
|
imageConfig.autoMaxWidth = v;
|
|
3326
3303
|
} else if (key === "image.quality") {
|
|
3327
3304
|
const v = parseInt(val);
|
|
3328
3305
|
if (isNaN(v) || v < 0 || v > 100) {
|
|
3329
|
-
error("quality
|
|
3306
|
+
error("quality must be between 0 and 100 (0 = reset)");
|
|
3330
3307
|
return;
|
|
3331
3308
|
}
|
|
3332
3309
|
imageConfig.autoQuality = v;
|
|
3333
3310
|
} else {
|
|
3334
|
-
error(
|
|
3335
|
-
info("
|
|
3311
|
+
error(`Unknown config key: ${key}`);
|
|
3312
|
+
info("Available: image.maxWidth, image.quality");
|
|
3336
3313
|
return;
|
|
3337
3314
|
}
|
|
3338
3315
|
|
|
3339
3316
|
const res = await rpcMutation(creds, "apps.updateImageConfig", { name: appId, imageConfig });
|
|
3340
3317
|
if (res.ok) {
|
|
3341
3318
|
const data = await res.json();
|
|
3342
|
-
success(`${key} = ${val}
|
|
3319
|
+
success(`${key} = ${val} configured`);
|
|
3343
3320
|
if (data.imageConfig) {
|
|
3344
|
-
info(
|
|
3321
|
+
info(`Current config: ${JSON.stringify(data.imageConfig)}`);
|
|
3345
3322
|
}
|
|
3346
|
-
info(`${DIM}
|
|
3323
|
+
info(`${DIM}Takes effect on next image request. Existing cached images use separate keys.${RESET}`);
|
|
3347
3324
|
} else {
|
|
3348
3325
|
const errData = await res.json().catch(() => ({}));
|
|
3349
|
-
error(
|
|
3326
|
+
error(`Config update failed: ${errData.error || res.statusText}`);
|
|
3350
3327
|
}
|
|
3351
3328
|
break;
|
|
3352
3329
|
}
|
|
@@ -3354,24 +3331,24 @@ ${BOLD}Examples:${RESET}
|
|
|
3354
3331
|
case "get": {
|
|
3355
3332
|
const key = restArgs.find(a => !a.startsWith("-"));
|
|
3356
3333
|
if (key && key !== "image") {
|
|
3357
|
-
error(
|
|
3358
|
-
info("
|
|
3334
|
+
error(`Unknown config key: ${key}`);
|
|
3335
|
+
info("Available: image");
|
|
3359
3336
|
return;
|
|
3360
3337
|
}
|
|
3361
3338
|
|
|
3362
3339
|
const res = await rpcQuery(creds, "apps.getImageConfig", { name: appId });
|
|
3363
3340
|
if (res.ok) {
|
|
3364
3341
|
const config = await res.json();
|
|
3365
|
-
log(`\n${BOLD}${CYAN}
|
|
3342
|
+
log(`\n${BOLD}${CYAN}Image Config${RESET} — ${appId}\n`);
|
|
3366
3343
|
if (Object.keys(config).length === 0) {
|
|
3367
|
-
info("
|
|
3344
|
+
info("No custom config (using tier defaults)");
|
|
3368
3345
|
} else {
|
|
3369
3346
|
if (config.autoMaxWidth) log(` ${GREEN}autoMaxWidth${RESET} ${config.autoMaxWidth} px`);
|
|
3370
3347
|
if (config.autoQuality) log(` ${GREEN}autoQuality${RESET} ${config.autoQuality}`);
|
|
3371
3348
|
}
|
|
3372
3349
|
log("");
|
|
3373
3350
|
} else {
|
|
3374
|
-
error("
|
|
3351
|
+
error("Failed to get config");
|
|
3375
3352
|
}
|
|
3376
3353
|
break;
|
|
3377
3354
|
}
|
|
@@ -3379,7 +3356,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3379
3356
|
case "reset": {
|
|
3380
3357
|
const key = restArgs.find(a => !a.startsWith("-"));
|
|
3381
3358
|
if (key && key !== "image") {
|
|
3382
|
-
error(
|
|
3359
|
+
error(`Unknown config key: ${key}`);
|
|
3383
3360
|
return;
|
|
3384
3361
|
}
|
|
3385
3362
|
|
|
@@ -3389,16 +3366,16 @@ ${BOLD}Examples:${RESET}
|
|
|
3389
3366
|
imageConfig: { autoMaxWidth: 0, autoQuality: 0 },
|
|
3390
3367
|
});
|
|
3391
3368
|
if (res.ok) {
|
|
3392
|
-
success("
|
|
3369
|
+
success("Image config reset (using tier defaults)");
|
|
3393
3370
|
} else {
|
|
3394
|
-
error("
|
|
3371
|
+
error("Reset failed");
|
|
3395
3372
|
}
|
|
3396
3373
|
break;
|
|
3397
3374
|
}
|
|
3398
3375
|
|
|
3399
3376
|
default:
|
|
3400
|
-
error(
|
|
3401
|
-
info("
|
|
3377
|
+
error(`Unknown subcommand: ${subCmd}`);
|
|
3378
|
+
info("Usage: gencow config [set|get|reset] ...");
|
|
3402
3379
|
}
|
|
3403
3380
|
},
|
|
3404
3381
|
|
|
@@ -3407,29 +3384,32 @@ ${BOLD}Examples:${RESET}
|
|
|
3407
3384
|
const subCmd = filesArgs[0] || "help";
|
|
3408
3385
|
const restArgs = filesArgs.slice(1);
|
|
3409
3386
|
|
|
3410
|
-
// help
|
|
3387
|
+
// help
|
|
3411
3388
|
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
|
|
3389
|
+
log(`\n${BOLD}${CYAN}gencow files${RESET} — File management\n`);
|
|
3390
|
+
log(` ${CYAN}upload${RESET} <path...> [--recursive|-r] Upload files`);
|
|
3391
|
+
log(` ${CYAN}list${RESET} List uploaded files`);
|
|
3392
|
+
log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y] Delete a file`);
|
|
3393
|
+
log(` ${CYAN}url${RESET} <storage_id> Get serving URL`);
|
|
3394
|
+
log(`\n ${DIM}Options:${RESET}`);
|
|
3395
|
+
log(` ${DIM}--app, -a <name> Target app (default: from gencow.json)${RESET}`);
|
|
3396
|
+
log(` ${DIM}--prod Target production app${RESET}\n`);
|
|
3419
3397
|
return;
|
|
3420
3398
|
}
|
|
3421
3399
|
|
|
3422
3400
|
const creds = requireCreds();
|
|
3423
3401
|
|
|
3424
|
-
//
|
|
3402
|
+
// Resolve appId + track --app/--prod flag positions
|
|
3425
3403
|
let appId = null;
|
|
3426
|
-
const
|
|
3404
|
+
const isProd = restArgs.includes("--prod");
|
|
3405
|
+
const flagIndices = new Set();
|
|
3427
3406
|
for (let i = 0; i < restArgs.length; i++) {
|
|
3428
3407
|
if (restArgs[i] === "--app" || restArgs[i] === "-a") {
|
|
3429
3408
|
flagIndices.add(i);
|
|
3430
3409
|
flagIndices.add(i + 1);
|
|
3431
3410
|
appId = restArgs[++i];
|
|
3432
3411
|
}
|
|
3412
|
+
if (restArgs[i] === "--prod") flagIndices.add(i);
|
|
3433
3413
|
}
|
|
3434
3414
|
|
|
3435
3415
|
if (!appId) {
|
|
@@ -3437,11 +3417,19 @@ ${BOLD}Examples:${RESET}
|
|
|
3437
3417
|
if (existsSync(gencowJsonPath)) {
|
|
3438
3418
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
3439
3419
|
appId = gencowJson.appId || gencowJson.appName;
|
|
3420
|
+
if (isProd && gencowJson.prodApp) {
|
|
3421
|
+
appId = gencowJson.prodApp;
|
|
3422
|
+
}
|
|
3440
3423
|
}
|
|
3441
3424
|
}
|
|
3442
3425
|
|
|
3426
|
+
if (isProd && appId && !appId.endsWith("-prod")) {
|
|
3427
|
+
error("Prod app not found. Run 'gencow deploy' first to create a prod app.");
|
|
3428
|
+
return;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3443
3431
|
if (!appId) {
|
|
3444
|
-
error("
|
|
3432
|
+
error("App not found. Run 'gencow dev' first.");
|
|
3445
3433
|
return;
|
|
3446
3434
|
}
|
|
3447
3435
|
|
|
@@ -3472,11 +3460,11 @@ ${BOLD}Examples:${RESET}
|
|
|
3472
3460
|
|
|
3473
3461
|
// 로컬 크기 검증 (서버 왕복 방지)
|
|
3474
3462
|
if (stat.size > MAX_FILE_SIZE) {
|
|
3475
|
-
error(`${fileName}: ${fmtFileSize(stat.size)} — 50MB
|
|
3463
|
+
error(`${fileName}: ${fmtFileSize(stat.size)} — exceeds 50MB limit`);
|
|
3476
3464
|
return null;
|
|
3477
3465
|
}
|
|
3478
3466
|
|
|
3479
|
-
info(
|
|
3467
|
+
info(`Uploading ${fileName} (${fmtFileSize(stat.size)})...`);
|
|
3480
3468
|
|
|
3481
3469
|
const fileBuffer = readFileSync(filePath);
|
|
3482
3470
|
const blob = new Blob([fileBuffer]);
|
|
@@ -3533,7 +3521,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3533
3521
|
const paths = restArgs.filter((a, i) => !a.startsWith("-") && !flagIndices.has(i));
|
|
3534
3522
|
|
|
3535
3523
|
if (paths.length === 0) {
|
|
3536
|
-
error("
|
|
3524
|
+
error("Usage: gencow files upload <file|dir path...> [--recursive|-r]");
|
|
3537
3525
|
return;
|
|
3538
3526
|
}
|
|
3539
3527
|
|
|
@@ -3541,13 +3529,13 @@ ${BOLD}Examples:${RESET}
|
|
|
3541
3529
|
for (const p of paths) {
|
|
3542
3530
|
const resolved = resolve(process.cwd(), p);
|
|
3543
3531
|
if (!existsSync(resolved)) {
|
|
3544
|
-
error(
|
|
3532
|
+
error(`File not found: ${p}`);
|
|
3545
3533
|
continue;
|
|
3546
3534
|
}
|
|
3547
3535
|
const stat = statSync(resolved);
|
|
3548
3536
|
if (stat.isDirectory()) {
|
|
3549
3537
|
if (!recursive) {
|
|
3550
|
-
error(`${p}
|
|
3538
|
+
error(`${p} is a directory. Use --recursive (-r) flag.`);
|
|
3551
3539
|
continue;
|
|
3552
3540
|
}
|
|
3553
3541
|
filesToUpload.push(...collectFiles(resolved));
|
|
@@ -3557,12 +3545,12 @@ ${BOLD}Examples:${RESET}
|
|
|
3557
3545
|
}
|
|
3558
3546
|
|
|
3559
3547
|
if (filesToUpload.length === 0) {
|
|
3560
|
-
error("
|
|
3548
|
+
error("No files to upload.");
|
|
3561
3549
|
return;
|
|
3562
3550
|
}
|
|
3563
3551
|
|
|
3564
|
-
log(`\n${BOLD}${CYAN}
|
|
3565
|
-
info(
|
|
3552
|
+
log(`\n${BOLD}${CYAN}File Upload${RESET} — ${appId}\n`);
|
|
3553
|
+
info(`Uploading ${filesToUpload.length} files...\n`);
|
|
3566
3554
|
|
|
3567
3555
|
let uploaded = 0;
|
|
3568
3556
|
let failed = 0;
|
|
@@ -3573,14 +3561,14 @@ ${BOLD}Examples:${RESET}
|
|
|
3573
3561
|
}
|
|
3574
3562
|
|
|
3575
3563
|
log("");
|
|
3576
|
-
if (uploaded > 0) success(`${uploaded}
|
|
3577
|
-
if (failed > 0) warn(`${failed}
|
|
3564
|
+
if (uploaded > 0) success(`${uploaded} files uploaded`);
|
|
3565
|
+
if (failed > 0) warn(`${failed} files failed`);
|
|
3578
3566
|
break;
|
|
3579
3567
|
}
|
|
3580
3568
|
|
|
3581
3569
|
case "list":
|
|
3582
3570
|
case "ls": {
|
|
3583
|
-
log(`\n${BOLD}${CYAN}
|
|
3571
|
+
log(`\n${BOLD}${CYAN}File List${RESET} — ${appId}\n`);
|
|
3584
3572
|
|
|
3585
3573
|
const res = await fetch(
|
|
3586
3574
|
`${creds.platformUrl}/platform/files/list`,
|
|
@@ -3596,13 +3584,13 @@ ${BOLD}Examples:${RESET}
|
|
|
3596
3584
|
|
|
3597
3585
|
const data = await res.json().catch(() => []);
|
|
3598
3586
|
if (!res.ok) {
|
|
3599
|
-
error(data.error || "
|
|
3587
|
+
error(data.error || "Failed to list files");
|
|
3600
3588
|
return;
|
|
3601
3589
|
}
|
|
3602
3590
|
|
|
3603
3591
|
const files = Array.isArray(data) ? data : [];
|
|
3604
3592
|
if (files.length === 0) {
|
|
3605
|
-
info("
|
|
3593
|
+
info("No files stored.");
|
|
3606
3594
|
log("");
|
|
3607
3595
|
return;
|
|
3608
3596
|
}
|
|
@@ -3611,7 +3599,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3611
3599
|
const idWidth = Math.max(12, ...files.map(f => (f.storage_id || "").length));
|
|
3612
3600
|
const nameWidth = Math.max(8, ...files.map(f => (f.name || "").length).map(l => Math.min(l, 40)));
|
|
3613
3601
|
|
|
3614
|
-
log(` ${DIM}${"ID".padEnd(idWidth)} ${"
|
|
3602
|
+
log(` ${DIM}${"ID".padEnd(idWidth)} ${"Name".padEnd(nameWidth)} ${"Size".padStart(10)} ${"Source".padEnd(10)} Uploaded${RESET}`);
|
|
3615
3603
|
log(` ${DIM}${"─".repeat(idWidth)} ${"─".repeat(nameWidth)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(16)}${RESET}`);
|
|
3616
3604
|
|
|
3617
3605
|
for (const f of files) {
|
|
@@ -3621,7 +3609,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3621
3609
|
const date = f.created_at ? fmtDate(f.created_at) : "-";
|
|
3622
3610
|
log(` ${(f.storage_id || "").padEnd(idWidth)} ${name.padEnd(nameWidth)} ${size} ${source} ${date}`);
|
|
3623
3611
|
}
|
|
3624
|
-
log(`\n ${DIM}
|
|
3612
|
+
log(`\n ${DIM}Total: ${files.length} files${RESET}\n`);
|
|
3625
3613
|
break;
|
|
3626
3614
|
}
|
|
3627
3615
|
|
|
@@ -3629,7 +3617,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3629
3617
|
case "rm": {
|
|
3630
3618
|
const storageId = restArgs.find((a, i) => !a.startsWith("-") && !flagIndices.has(i));
|
|
3631
3619
|
if (!storageId) {
|
|
3632
|
-
error("
|
|
3620
|
+
error("Usage: gencow files delete <storage_id>");
|
|
3633
3621
|
return;
|
|
3634
3622
|
}
|
|
3635
3623
|
|
|
@@ -3645,7 +3633,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3645
3633
|
});
|
|
3646
3634
|
});
|
|
3647
3635
|
if (answer !== "y" && answer !== "yes") {
|
|
3648
|
-
info("
|
|
3636
|
+
info("Delete cancelled.");
|
|
3649
3637
|
return;
|
|
3650
3638
|
}
|
|
3651
3639
|
}
|
|
@@ -3664,9 +3652,9 @@ ${BOLD}Examples:${RESET}
|
|
|
3664
3652
|
|
|
3665
3653
|
const data = await res.json().catch(() => ({}));
|
|
3666
3654
|
if (res.ok && data.success) {
|
|
3667
|
-
success(
|
|
3655
|
+
success(`File deleted: ${storageId}`);
|
|
3668
3656
|
} else {
|
|
3669
|
-
error(
|
|
3657
|
+
error(`Delete failed: ${data.error || res.statusText}`);
|
|
3670
3658
|
}
|
|
3671
3659
|
break;
|
|
3672
3660
|
}
|
|
@@ -3674,7 +3662,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3674
3662
|
case "url": {
|
|
3675
3663
|
const storageId = restArgs.find((a, i) => !a.startsWith("-") && !flagIndices.has(i));
|
|
3676
3664
|
if (!storageId) {
|
|
3677
|
-
error("
|
|
3665
|
+
error("Usage: gencow files url <storage_id>");
|
|
3678
3666
|
return;
|
|
3679
3667
|
}
|
|
3680
3668
|
log(getFileUrl(storageId));
|
|
@@ -3682,14 +3670,14 @@ ${BOLD}Examples:${RESET}
|
|
|
3682
3670
|
}
|
|
3683
3671
|
|
|
3684
3672
|
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
|
|
3673
|
+
if (subCmd !== "help") error(`Unknown subcommand: ${subCmd}`);
|
|
3674
|
+
log(`\n${BOLD}${CYAN}gencow files${RESET} — File management\n`);
|
|
3675
|
+
log(` ${CYAN}upload${RESET} <path...> [--recursive|-r] Upload files`);
|
|
3676
|
+
log(` ${CYAN}list${RESET} List files`);
|
|
3677
|
+
log(` ${CYAN}delete${RESET} <storage_id> [--yes|-y] Delete file`);
|
|
3678
|
+
log(` ${CYAN}url${RESET} <storage_id> Get serving URL`);
|
|
3679
|
+
log(`\n ${DIM}Options:${RESET}`);
|
|
3680
|
+
log(` ${DIM}--app, -a <name> Target app (default: gencow.json)${RESET}\n`);
|
|
3693
3681
|
}
|
|
3694
3682
|
},
|
|
3695
3683
|
|
|
@@ -3714,7 +3702,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3714
3702
|
}
|
|
3715
3703
|
|
|
3716
3704
|
if (!appId) {
|
|
3717
|
-
error("
|
|
3705
|
+
error("App not found. Run gencow deploy first.");
|
|
3718
3706
|
return;
|
|
3719
3707
|
}
|
|
3720
3708
|
|
|
@@ -3736,7 +3724,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3736
3724
|
case "ls": {
|
|
3737
3725
|
const res = await rpcQuery(creds, "backup.list", { appName: appId });
|
|
3738
3726
|
if (!res.ok) {
|
|
3739
|
-
error(
|
|
3727
|
+
error(`Failed to fetch backup list: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
|
|
3740
3728
|
return;
|
|
3741
3729
|
}
|
|
3742
3730
|
const data = await res.json();
|
|
@@ -3746,8 +3734,8 @@ ${BOLD}Examples:${RESET}
|
|
|
3746
3734
|
log(`\n${BOLD}${CYAN}Database Backups${RESET} — ${appId} ${DIM}(Plan: ${data.plan || "free"})${RESET}\n`);
|
|
3747
3735
|
|
|
3748
3736
|
if (backups.length === 0) {
|
|
3749
|
-
info("
|
|
3750
|
-
info(`${GREEN}gencow backup create${RESET} —
|
|
3737
|
+
info("No backups found.");
|
|
3738
|
+
info(`${GREEN}gencow backup create${RESET} — create a manual backup`);
|
|
3751
3739
|
} else {
|
|
3752
3740
|
for (const b of backups) {
|
|
3753
3741
|
const statusColor = b.status === "completed" ? GREEN : b.status === "failed" ? RED : YELLOW;
|
|
@@ -3765,19 +3753,19 @@ ${BOLD}Examples:${RESET}
|
|
|
3765
3753
|
|
|
3766
3754
|
const res = await rpcMutation(creds, "backup.create", { appName: appId, note });
|
|
3767
3755
|
if (!res.ok) {
|
|
3768
|
-
error(
|
|
3756
|
+
error(`Backup creation failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
|
|
3769
3757
|
return;
|
|
3770
3758
|
}
|
|
3771
3759
|
const backup = await res.json();
|
|
3772
|
-
success(
|
|
3760
|
+
success(`Backup created #${backup.id} ${fmtSize(backup.fileSize || 0)}`);
|
|
3773
3761
|
break;
|
|
3774
3762
|
}
|
|
3775
3763
|
|
|
3776
3764
|
case "restore": {
|
|
3777
3765
|
const backupId = parseInt(restArgs[0]);
|
|
3778
3766
|
if (!backupId || isNaN(backupId)) {
|
|
3779
|
-
error("
|
|
3780
|
-
info("gencow backup list
|
|
3767
|
+
error("Specify backup ID: gencow backup restore <id>");
|
|
3768
|
+
info("Run gencow backup list to check IDs.");
|
|
3781
3769
|
return;
|
|
3782
3770
|
}
|
|
3783
3771
|
|
|
@@ -3792,7 +3780,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3792
3780
|
});
|
|
3793
3781
|
|
|
3794
3782
|
if (answer !== "y" && answer !== "yes") {
|
|
3795
|
-
info("
|
|
3783
|
+
info("Restore cancelled.");
|
|
3796
3784
|
return;
|
|
3797
3785
|
}
|
|
3798
3786
|
|
|
@@ -3800,10 +3788,10 @@ ${BOLD}Examples:${RESET}
|
|
|
3800
3788
|
|
|
3801
3789
|
const res = await rpcMutation(creds, "backup.restore", { appName: appId, backupId });
|
|
3802
3790
|
if (!res.ok) {
|
|
3803
|
-
error(
|
|
3791
|
+
error(`Restore failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
|
|
3804
3792
|
return;
|
|
3805
3793
|
}
|
|
3806
|
-
success("
|
|
3794
|
+
success("Database restored. App restarted.");
|
|
3807
3795
|
break;
|
|
3808
3796
|
}
|
|
3809
3797
|
|
|
@@ -3811,23 +3799,23 @@ ${BOLD}Examples:${RESET}
|
|
|
3811
3799
|
case "rm": {
|
|
3812
3800
|
const backupId = parseInt(restArgs[0]);
|
|
3813
3801
|
if (!backupId || isNaN(backupId)) {
|
|
3814
|
-
error("
|
|
3802
|
+
error("Specify backup ID: gencow backup delete <id>");
|
|
3815
3803
|
return;
|
|
3816
3804
|
}
|
|
3817
3805
|
|
|
3818
3806
|
const res = await rpcMutation(creds, "backup.delete", { appName: appId, backupId });
|
|
3819
3807
|
if (!res.ok) {
|
|
3820
|
-
error(
|
|
3808
|
+
error(`Delete failed: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
|
|
3821
3809
|
return;
|
|
3822
3810
|
}
|
|
3823
|
-
success(
|
|
3811
|
+
success(`Backup #${backupId} deleted`);
|
|
3824
3812
|
break;
|
|
3825
3813
|
}
|
|
3826
3814
|
|
|
3827
3815
|
case "download": {
|
|
3828
3816
|
const backupId = parseInt(restArgs[0]);
|
|
3829
3817
|
if (!backupId || isNaN(backupId)) {
|
|
3830
|
-
error("
|
|
3818
|
+
error("Specify backup ID: gencow backup download <id>");
|
|
3831
3819
|
return;
|
|
3832
3820
|
}
|
|
3833
3821
|
|
|
@@ -3837,7 +3825,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3837
3825
|
`/platform/apps/${appId}/backups/${backupId}/download`
|
|
3838
3826
|
);
|
|
3839
3827
|
if (!res.ok) {
|
|
3840
|
-
error(
|
|
3828
|
+
error(`Download failed: ${(await res.json?.().catch(() => ({}))).error || res.statusText}`);
|
|
3841
3829
|
return;
|
|
3842
3830
|
}
|
|
3843
3831
|
|
|
@@ -3845,12 +3833,12 @@ ${BOLD}Examples:${RESET}
|
|
|
3845
3833
|
const { writeFile } = await import("fs/promises");
|
|
3846
3834
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
3847
3835
|
await writeFile(filename, buffer);
|
|
3848
|
-
success(
|
|
3836
|
+
success(`Saved: ${filename} (${fmtSize(buffer.length)})`);
|
|
3849
3837
|
break;
|
|
3850
3838
|
}
|
|
3851
3839
|
|
|
3852
3840
|
default:
|
|
3853
|
-
error(
|
|
3841
|
+
error(`Unknown sub-command: ${subCmd}`);
|
|
3854
3842
|
log(`\n ${BOLD}Usage:${RESET}`);
|
|
3855
3843
|
log(` gencow backup list List all backups`);
|
|
3856
3844
|
log(` gencow backup create [note] Create a manual backup`);
|
|
@@ -3881,7 +3869,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3881
3869
|
}
|
|
3882
3870
|
|
|
3883
3871
|
if (!appId) {
|
|
3884
|
-
error("
|
|
3872
|
+
error("App not found. Run gencow deploy first.");
|
|
3885
3873
|
return;
|
|
3886
3874
|
}
|
|
3887
3875
|
|
|
@@ -3889,8 +3877,8 @@ ${BOLD}Examples:${RESET}
|
|
|
3889
3877
|
case "set": {
|
|
3890
3878
|
const domain = restArgs.find(a => !a.startsWith("-") && a.includes("."));
|
|
3891
3879
|
if (!domain) {
|
|
3892
|
-
error("
|
|
3893
|
-
info("
|
|
3880
|
+
error("Usage: gencow domain set <domain>");
|
|
3881
|
+
info(" Example: gencow domain set myapp.com");
|
|
3894
3882
|
return;
|
|
3895
3883
|
}
|
|
3896
3884
|
|
|
@@ -3902,32 +3890,32 @@ ${BOLD}Examples:${RESET}
|
|
|
3902
3890
|
|
|
3903
3891
|
const data = await res.json();
|
|
3904
3892
|
if (!res.ok) {
|
|
3905
|
-
error(
|
|
3893
|
+
error(`Domain registration failed: ${data.error}`);
|
|
3906
3894
|
return;
|
|
3907
3895
|
}
|
|
3908
3896
|
|
|
3909
|
-
log(`\n${BOLD}${CYAN}🌐
|
|
3897
|
+
log(`\n${BOLD}${CYAN}🌐 Custom Domain Registered${RESET}\n`);
|
|
3910
3898
|
success(`${domain} → ${appId}`);
|
|
3911
3899
|
log("");
|
|
3912
3900
|
|
|
3913
3901
|
if (data.status === "active") {
|
|
3914
3902
|
info(`DNS: ${GREEN}✅ ${data.dns.detail}${RESET}`);
|
|
3915
|
-
info(`TLS:
|
|
3903
|
+
info(`TLS: Certificate auto-provisioned on first request`);
|
|
3916
3904
|
} else {
|
|
3917
|
-
warn(`DNS: ⏳
|
|
3905
|
+
warn(`DNS: ⏳ Not yet verified.`);
|
|
3918
3906
|
log("");
|
|
3919
|
-
log(`
|
|
3907
|
+
log(` Set up the following DNS record:`);
|
|
3920
3908
|
log(` ┌──────────────────────────────────────────┐`);
|
|
3921
3909
|
log(` │ ${BOLD}Type: A${RESET} │`);
|
|
3922
3910
|
log(` │ Name: ${domain.padEnd(33)}│`);
|
|
3923
3911
|
log(` │ Value: ${data.serverIp.padEnd(33)}│`);
|
|
3924
3912
|
log(` │ │`);
|
|
3925
|
-
log(` │ ${DIM}
|
|
3913
|
+
log(` │ ${DIM}Or CNAME:${RESET} │`);
|
|
3926
3914
|
log(` │ Value: ${data.cname.padEnd(33)}│`);
|
|
3927
3915
|
log(` └──────────────────────────────────────────┘`);
|
|
3928
3916
|
}
|
|
3929
3917
|
log("");
|
|
3930
|
-
info(`DNS
|
|
3918
|
+
info(`After DNS setup: ${DIM}gencow domain status${RESET}`);
|
|
3931
3919
|
log("");
|
|
3932
3920
|
break;
|
|
3933
3921
|
}
|
|
@@ -3942,14 +3930,14 @@ ${BOLD}Examples:${RESET}
|
|
|
3942
3930
|
|
|
3943
3931
|
const data = await res.json();
|
|
3944
3932
|
if (!res.ok) {
|
|
3945
|
-
error(
|
|
3933
|
+
error(`Domain removal failed: ${data.error}`);
|
|
3946
3934
|
return;
|
|
3947
3935
|
}
|
|
3948
3936
|
|
|
3949
3937
|
if (data.domain) {
|
|
3950
|
-
success(
|
|
3938
|
+
success(`Domain ${data.domain} disconnected`);
|
|
3951
3939
|
} else {
|
|
3952
|
-
info("
|
|
3940
|
+
info("No custom domain configured.");
|
|
3953
3941
|
}
|
|
3954
3942
|
break;
|
|
3955
3943
|
}
|
|
@@ -3962,26 +3950,26 @@ ${BOLD}Examples:${RESET}
|
|
|
3962
3950
|
|
|
3963
3951
|
const data = await res.json();
|
|
3964
3952
|
if (!res.ok) {
|
|
3965
|
-
error(
|
|
3953
|
+
error(`Domain status check failed: ${data.error}`);
|
|
3966
3954
|
return;
|
|
3967
3955
|
}
|
|
3968
3956
|
|
|
3969
|
-
log(`\n${BOLD}${CYAN}🌐
|
|
3957
|
+
log(`\n${BOLD}${CYAN}🌐 Custom Domain Status${RESET} — ${appId}\n`);
|
|
3970
3958
|
|
|
3971
3959
|
if (!data.domain) {
|
|
3972
|
-
info("
|
|
3973
|
-
info(
|
|
3960
|
+
info("No custom domain configured.");
|
|
3961
|
+
info(`Set up: ${DIM}gencow domain set <domain>${RESET}`);
|
|
3974
3962
|
} else {
|
|
3975
|
-
info(
|
|
3963
|
+
info(`Domain: ${BOLD}${data.domain}${RESET}`);
|
|
3976
3964
|
|
|
3977
3965
|
if (data.status === "active") {
|
|
3978
3966
|
info(`DNS: ${GREEN}✅ ${data.dns.detail}${RESET}`);
|
|
3979
|
-
info(`TLS: ${GREEN}✅ Let's Encrypt
|
|
3967
|
+
info(`TLS: ${GREEN}✅ Let's Encrypt auto-provisioned${RESET}`);
|
|
3980
3968
|
} else if (data.status === "pending") {
|
|
3981
|
-
warn(`DNS: ⏳
|
|
3982
|
-
info(`TLS:
|
|
3969
|
+
warn(`DNS: ⏳ Verification pending`);
|
|
3970
|
+
info(`TLS: Auto-provisioned after DNS verification`);
|
|
3983
3971
|
} else {
|
|
3984
|
-
error(
|
|
3972
|
+
error(`Status: ${data.status}`);
|
|
3985
3973
|
}
|
|
3986
3974
|
}
|
|
3987
3975
|
log("");
|
|
@@ -4055,7 +4043,7 @@ console.log("GENCOW_API_JSON=" + JSON.stringify({
|
|
|
4055
4043
|
}));
|
|
4056
4044
|
process.exit(0);
|
|
4057
4045
|
`;
|
|
4058
|
-
info("
|
|
4046
|
+
info("Extracting function list...");
|
|
4059
4047
|
try {
|
|
4060
4048
|
writeFileSync(extractTsPath, script);
|
|
4061
4049
|
let outStr;
|
|
@@ -4119,10 +4107,10 @@ process.exit(0);
|
|
|
4119
4107
|
log(` ${DIM}Queries: ${queries.join(", ")}${RESET}`);
|
|
4120
4108
|
log(` ${DIM}Mutations: ${mutations.join(", ")}${RESET}`);
|
|
4121
4109
|
log("");
|
|
4122
|
-
info(
|
|
4110
|
+
info(`Usage: import { api } from "@/gencow/api";`);
|
|
4123
4111
|
} catch (e) {
|
|
4124
4112
|
try { unlinkSync(extractTsPath); } catch { }
|
|
4125
|
-
error(`Codegen
|
|
4113
|
+
error(`Codegen failed: ${e.message}`);
|
|
4126
4114
|
process.exit(1);
|
|
4127
4115
|
}
|
|
4128
4116
|
},
|
|
@@ -4278,57 +4266,7 @@ process.exit(0);
|
|
|
4278
4266
|
log(` Usage: gencow app [list|create|delete|status]`);
|
|
4279
4267
|
},
|
|
4280
4268
|
|
|
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
|
-
},
|
|
4269
|
+
// ── remote:deploy — REMOVED (legacy, use `gencow deploy` instead) ──
|
|
4332
4270
|
|
|
4333
4271
|
// ── logs ───────────────────────────────────────────
|
|
4334
4272
|
async logs(...args) {
|
|
@@ -4427,13 +4365,13 @@ process.exit(0);
|
|
|
4427
4365
|
}
|
|
4428
4366
|
followFailures++;
|
|
4429
4367
|
if (followFailures > FOLLOW_MAX_FAILURES) {
|
|
4430
|
-
error(
|
|
4431
|
-
info(
|
|
4368
|
+
error(`Connection failed — check app status.`);
|
|
4369
|
+
info(`Check if the server is running: gencow dev`);
|
|
4432
4370
|
process.exit(1);
|
|
4433
4371
|
}
|
|
4434
4372
|
const delay = Math.min(FOLLOW_BASE_RECONNECT_MS * Math.pow(2, followFailures - 1), FOLLOW_MAX_RECONNECT_MS);
|
|
4435
4373
|
const delaySec = (delay / 1000).toFixed(0);
|
|
4436
|
-
warn(
|
|
4374
|
+
warn(`Disconnected — reconnecting in ${delaySec}s... (${followFailures}/${FOLLOW_MAX_FAILURES})`);
|
|
4437
4375
|
scheduleFollowReconnect(delay);
|
|
4438
4376
|
});
|
|
4439
4377
|
}
|
|
@@ -4675,7 +4613,7 @@ process.exit(0);
|
|
|
4675
4613
|
|
|
4676
4614
|
// 앱이 없으면 자동 생성 (deploy의 auto-create 패턴 재사용)
|
|
4677
4615
|
if (!appName) {
|
|
4678
|
-
info("
|
|
4616
|
+
info("No app found. Creating...");
|
|
4679
4617
|
|
|
4680
4618
|
// displayName: gencow.config.ts의 name 또는 프로젝트 폴더명
|
|
4681
4619
|
let displayName = null;
|
|
@@ -4690,7 +4628,7 @@ process.exit(0);
|
|
|
4690
4628
|
const createRes = await rpcMutation(creds, "apps.create", { name: displayName });
|
|
4691
4629
|
if (!createRes.ok) {
|
|
4692
4630
|
const createErr = await createRes.json().catch(() => ({}));
|
|
4693
|
-
error(
|
|
4631
|
+
error(`App creation failed: ${createErr.error || createRes.statusText}`);
|
|
4694
4632
|
process.exit(1);
|
|
4695
4633
|
}
|
|
4696
4634
|
|
|
@@ -4698,11 +4636,11 @@ process.exit(0);
|
|
|
4698
4636
|
appName = createData.appId || createData.name;
|
|
4699
4637
|
|
|
4700
4638
|
if (!appName) {
|
|
4701
|
-
error("
|
|
4639
|
+
error("App creation response missing appId.");
|
|
4702
4640
|
process.exit(1);
|
|
4703
4641
|
}
|
|
4704
4642
|
|
|
4705
|
-
success(
|
|
4643
|
+
success(`App "${appName}" created`);
|
|
4706
4644
|
|
|
4707
4645
|
// gencow.json 저장 (deploy와 동일 패턴)
|
|
4708
4646
|
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
@@ -4711,13 +4649,13 @@ process.exit(0);
|
|
|
4711
4649
|
displayName,
|
|
4712
4650
|
platformUrl: creds.platformUrl,
|
|
4713
4651
|
}, null, 2));
|
|
4714
|
-
info(`${DIM}gencow.json
|
|
4652
|
+
info(`${DIM}gencow.json created (appId: ${appName})${RESET}`);
|
|
4715
4653
|
|
|
4716
4654
|
// .env에 VITE_API_URL 자동 설정
|
|
4717
4655
|
updateEnvLocalUrl(getAppUrl(appName, creds.platformUrl));
|
|
4718
4656
|
|
|
4719
4657
|
// 프로비저닝 대기
|
|
4720
|
-
info("
|
|
4658
|
+
info("Waiting for provisioning...");
|
|
4721
4659
|
await new Promise(r => setTimeout(r, 3000));
|
|
4722
4660
|
}
|
|
4723
4661
|
|
|
@@ -4736,9 +4674,9 @@ process.exit(0);
|
|
|
4736
4674
|
const isSchema = reason !== "initial" && /schema.*\.ts$/i.test(reason);
|
|
4737
4675
|
|
|
4738
4676
|
if (reason === "initial") {
|
|
4739
|
-
log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET}
|
|
4677
|
+
log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} Initial deploy...`);
|
|
4740
4678
|
} else {
|
|
4741
|
-
log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} ${reason}
|
|
4679
|
+
log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} ${reason} change detected → deploying...`);
|
|
4742
4680
|
}
|
|
4743
4681
|
|
|
4744
4682
|
// 스키마 변경 또는 초기 배포 시 마이그레이션 생성
|
|
@@ -4746,7 +4684,7 @@ process.exit(0);
|
|
|
4746
4684
|
const schemaExists = existsSync(resolve(process.cwd(), functionsDir, "schema.ts"));
|
|
4747
4685
|
if (schemaExists && (isSchema || reason === "initial")) {
|
|
4748
4686
|
if (isSchema) {
|
|
4749
|
-
log(`${DIM}${ts}${RESET} ${YELLOW}[migrate]${RESET}
|
|
4687
|
+
log(`${DIM}${ts}${RESET} ${YELLOW}[migrate]${RESET} Schema change detected → generating migrations...`);
|
|
4750
4688
|
}
|
|
4751
4689
|
try {
|
|
4752
4690
|
const { execSync } = await import("child_process");
|
|
@@ -4756,13 +4694,13 @@ process.exit(0);
|
|
|
4756
4694
|
env: { ...process.env, ...dk.env },
|
|
4757
4695
|
stdio: "inherit",
|
|
4758
4696
|
});
|
|
4759
|
-
log(`${DIM}${ts}${RESET} ${GREEN}[migrate]${RESET} ✔
|
|
4697
|
+
log(`${DIM}${ts}${RESET} ${GREEN}[migrate]${RESET} ✔ Migrations generated`);
|
|
4760
4698
|
} catch (e) {
|
|
4761
4699
|
const msg = e.stderr?.toString() || e.message || "";
|
|
4762
4700
|
if (msg.includes("No schema changes") || msg.includes("nothing to migrate")) {
|
|
4763
4701
|
// 스키마 변경 없음 — 정상
|
|
4764
4702
|
} else {
|
|
4765
|
-
warn(`[migrate]
|
|
4703
|
+
warn(`[migrate] Migration generation failed (non-critical): ${msg.split("\n")[0]}`);
|
|
4766
4704
|
}
|
|
4767
4705
|
}
|
|
4768
4706
|
}
|
|
@@ -4801,7 +4739,7 @@ process.exit(0);
|
|
|
4801
4739
|
|
|
4802
4740
|
if (!res.ok) {
|
|
4803
4741
|
const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
4804
|
-
log(`${DIM}${ts2}${RESET} ${RED}[deploy]${RESET} ✗
|
|
4742
|
+
log(`${DIM}${ts2}${RESET} ${RED}[deploy]${RESET} ✗ Deploy failed!`);
|
|
4805
4743
|
|
|
4806
4744
|
// 에러 메시지에서 파일:라인 정보 추출
|
|
4807
4745
|
const errMsg = data.error || "Unknown error";
|
|
@@ -4818,13 +4756,13 @@ process.exit(0);
|
|
|
4818
4756
|
|
|
4819
4757
|
// 스키마 관련 에러 힌트
|
|
4820
4758
|
if (errMsg.includes("does not exist") || errMsg.includes("relation")) {
|
|
4821
|
-
log(` ${YELLOW}💡
|
|
4759
|
+
log(` ${YELLOW}💡 Schema changed? Migrations run automatically when schema.ts changes.${RESET}`);
|
|
4822
4760
|
}
|
|
4823
4761
|
return;
|
|
4824
4762
|
}
|
|
4825
4763
|
|
|
4826
4764
|
const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
4827
|
-
log(`${DIM}${ts2}${RESET} ${GREEN}[deploy]${RESET} ✔
|
|
4765
|
+
log(`${DIM}${ts2}${RESET} ${GREEN}[deploy]${RESET} ✔ Deploy complete ${DIM}(${sizeKB}KB, ${elapsed}s)${RESET}`);
|
|
4828
4766
|
}
|
|
4829
4767
|
|
|
4830
4768
|
// ── 로그 포맷팅 함수 ────────────────────────────────
|
|
@@ -4857,7 +4795,7 @@ process.exit(0);
|
|
|
4857
4795
|
try {
|
|
4858
4796
|
logWs = new WS(wsUrl);
|
|
4859
4797
|
} catch (e) {
|
|
4860
|
-
warn(`[log] WebSocket
|
|
4798
|
+
warn(`[log] WebSocket connection failed: ${e.message}`);
|
|
4861
4799
|
scheduleReconnect();
|
|
4862
4800
|
return;
|
|
4863
4801
|
}
|
|
@@ -4865,7 +4803,7 @@ process.exit(0);
|
|
|
4865
4803
|
logWs.on("open", () => {
|
|
4866
4804
|
// 카운터 리셋은 close 이벤트에서 안정 연결 판정 후 수행
|
|
4867
4805
|
logWsOpenedAt = Date.now();
|
|
4868
|
-
log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${GREEN}[log]${RESET}
|
|
4806
|
+
log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${GREEN}[log]${RESET} Log streaming connected`);
|
|
4869
4807
|
logWs.send(JSON.stringify({ type: "log:subscribe" }));
|
|
4870
4808
|
});
|
|
4871
4809
|
|
|
@@ -4889,13 +4827,13 @@ process.exit(0);
|
|
|
4889
4827
|
}
|
|
4890
4828
|
reconnectFailures++;
|
|
4891
4829
|
if (reconnectFailures > MAX_RECONNECT_FAILURES) {
|
|
4892
|
-
warn(`[log]
|
|
4893
|
-
info(
|
|
4830
|
+
warn(`[log] Log streaming failed ${MAX_RECONNECT_FAILURES} times. Giving up.`);
|
|
4831
|
+
info(`Check manually: gencow deploy logs`);
|
|
4894
4832
|
return;
|
|
4895
4833
|
}
|
|
4896
4834
|
const delay = Math.min(BASE_RECONNECT_MS * Math.pow(2, reconnectFailures - 1), MAX_RECONNECT_MS);
|
|
4897
4835
|
const delaySec = (delay / 1000).toFixed(0);
|
|
4898
|
-
log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${YELLOW}[log]${RESET}
|
|
4836
|
+
log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${YELLOW}[log]${RESET} Disconnected — reconnecting in ${delaySec}s... (${reconnectFailures}/${MAX_RECONNECT_FAILURES})`);
|
|
4899
4837
|
scheduleReconnect(delay);
|
|
4900
4838
|
});
|
|
4901
4839
|
}
|
|
@@ -5178,7 +5116,7 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
|
|
|
5178
5116
|
// ── 모든 컴포넌트 설치 후 README 1회 업데이트 ───────────
|
|
5179
5117
|
await updateReadme(loadConfig());
|
|
5180
5118
|
|
|
5181
|
-
log(`\n${GREEN}✔${RESET} ${BOLD}
|
|
5119
|
+
log(`\n${GREEN}✔${RESET} ${BOLD}Done!${RESET} ${DIM}Run gencow dev to get started.${RESET}`);
|
|
5182
5120
|
|
|
5183
5121
|
// ── 컴포넌트별 가이드 메시지 ─────────────────────────────
|
|
5184
5122
|
for (const key of ordered) {
|
|
@@ -5200,11 +5138,11 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
|
|
|
5200
5138
|
const label = config.label || name.toUpperCase();
|
|
5201
5139
|
|
|
5202
5140
|
if (config.notReady) {
|
|
5203
|
-
log(` ${YELLOW}⚠${RESET} ${BOLD}[${label}]${RESET} ${DIM}
|
|
5141
|
+
log(` ${YELLOW}⚠${RESET} ${BOLD}[${label}]${RESET} ${DIM}Coming Soon${RESET}`);
|
|
5204
5142
|
return;
|
|
5205
5143
|
}
|
|
5206
5144
|
|
|
5207
|
-
log(` ${CYAN}▸${RESET} ${BOLD}[${label}]${RESET}
|
|
5145
|
+
log(` ${CYAN}▸${RESET} ${BOLD}[${label}]${RESET} Installing...`);
|
|
5208
5146
|
|
|
5209
5147
|
// 1. npm 의존성 설치 (패키지 매니저 자동 감지)
|
|
5210
5148
|
if (config.deps && config.deps.length > 0) {
|
|
@@ -5221,9 +5159,9 @@ if (config.deps && config.deps.length > 0) {
|
|
|
5221
5159
|
stdio: "pipe",
|
|
5222
5160
|
cwd: process.cwd(),
|
|
5223
5161
|
});
|
|
5224
|
-
log(` ${GREEN}✓${RESET} ${config.deps.join(", ")}
|
|
5162
|
+
log(` ${GREEN}✓${RESET} ${config.deps.join(", ")} packages installed`);
|
|
5225
5163
|
} catch (e) {
|
|
5226
|
-
log(` ${YELLOW}⚠${RESET}
|
|
5164
|
+
log(` ${YELLOW}⚠${RESET} Package install failed — ${DIM}run bun add ${config.deps.join(" ")} manually${RESET}`);
|
|
5227
5165
|
}
|
|
5228
5166
|
}
|
|
5229
5167
|
|
|
@@ -5247,10 +5185,10 @@ for (const file of config.files) {
|
|
|
5247
5185
|
}
|
|
5248
5186
|
|
|
5249
5187
|
if (fs.existsSync(destPath)) {
|
|
5250
|
-
log(` ${YELLOW}⚠${RESET} ${file.dest}
|
|
5188
|
+
log(` ${YELLOW}⚠${RESET} ${file.dest} already exists, skipping`);
|
|
5251
5189
|
} else {
|
|
5252
5190
|
fs.copyFileSync(srcPath, destPath);
|
|
5253
|
-
log(` ${GREEN}✓${RESET} ${file.dest}
|
|
5191
|
+
log(` ${GREEN}✓${RESET} ${file.dest} created`);
|
|
5254
5192
|
}
|
|
5255
5193
|
}
|
|
5256
5194
|
|
|
@@ -5267,7 +5205,7 @@ if (config.env && Object.keys(config.env).length > 0) {
|
|
|
5267
5205
|
if (!envContent.includes(key)) {
|
|
5268
5206
|
envContent += `\n${key}=${value}`;
|
|
5269
5207
|
added = true;
|
|
5270
|
-
log(` ${GREEN}✓${RESET} .env
|
|
5208
|
+
log(` ${GREEN}✓${RESET} .env — added ${key}=`);
|
|
5271
5209
|
}
|
|
5272
5210
|
}
|
|
5273
5211
|
|
|
@@ -5316,15 +5254,15 @@ const safe = await ai.withRetry(
|
|
|
5316
5254
|
{ maxRetries: 3 }
|
|
5317
5255
|
);
|
|
5318
5256
|
\`\`\``,
|
|
5319
|
-
vibePrompt: `AI
|
|
5257
|
+
vibePrompt: `AI Engine API:
|
|
5320
5258
|
import { ai } from "@/gencow/ai";
|
|
5321
|
-
ai.chat({ messages }) //
|
|
5322
|
-
ai.stream({ messages }) //
|
|
5323
|
-
ai.embed("
|
|
5324
|
-
ai.embedMany(["a","b"]) //
|
|
5259
|
+
ai.chat({ messages }) // text generation
|
|
5260
|
+
ai.stream({ messages }) // streaming
|
|
5261
|
+
ai.embed("text") // embedding
|
|
5262
|
+
ai.embedMany(["a","b"]) // batch embedding
|
|
5325
5263
|
ai.agent({ messages, tools, maxSteps }) // Agent Loop
|
|
5326
|
-
ai.withRetry(fn, { maxRetries: 3 }) //
|
|
5327
|
-
ai.estimateTokens(text) //
|
|
5264
|
+
ai.withRetry(fn, { maxRetries: 3 }) // retry+fallback
|
|
5265
|
+
ai.estimateTokens(text) // token estimation
|
|
5328
5266
|
ai.trimMessages(msgs, { maxTokens: 4000 }) // 토큰 예산`,
|
|
5329
5267
|
},
|
|
5330
5268
|
"rag.ts": {
|
|
@@ -5352,7 +5290,7 @@ const { answer, sources } = await rag.ask(ctx, "환불 정책이 뭔가요?");
|
|
|
5352
5290
|
// 문서 삭제
|
|
5353
5291
|
await rag.delete(ctx, "old-manual.pdf");
|
|
5354
5292
|
\`\`\``,
|
|
5355
|
-
vibePrompt: `RAG
|
|
5293
|
+
vibePrompt: `RAG Engine API:
|
|
5356
5294
|
import { rag } from "@/gencow/rag";
|
|
5357
5295
|
rag.ingest(ctx, source, text) // 문서 인제스트 (스마트 청킹)
|
|
5358
5296
|
rag.search(ctx, query, { filter }) // 시맨틱 검색
|
|
@@ -5380,7 +5318,7 @@ const tools = defineTools(ctx, {
|
|
|
5380
5318
|
|
|
5381
5319
|
const reply = await ai.chat({ messages, tools });
|
|
5382
5320
|
\`\`\``,
|
|
5383
|
-
vibePrompt: `Tool Calling
|
|
5321
|
+
vibePrompt: `Tool Calling API:
|
|
5384
5322
|
import { defineTools } from "@/gencow/tools";
|
|
5385
5323
|
const tools = defineTools(ctx, { ... }) // AI 도구 정의
|
|
5386
5324
|
ai.chat({ messages, tools }) // 도구 연동 응답`,
|
|
@@ -5401,7 +5339,7 @@ const reply = await ai.chat({
|
|
|
5401
5339
|
messages: [...memCtx.recentMessages, { role: "user", content: query }],
|
|
5402
5340
|
});
|
|
5403
5341
|
\`\`\``,
|
|
5404
|
-
vibePrompt: `Agent Memory
|
|
5342
|
+
vibePrompt: `Agent Memory API:
|
|
5405
5343
|
import { memory } from "@/gencow/memory";
|
|
5406
5344
|
memory.buildContext(ctx, userId, sessionId, query) // 메모리 컨텍스트
|
|
5407
5345
|
memory.extract(ctx, userId, conversation) // 사실 추출`,
|
|
@@ -5435,7 +5373,7 @@ const reranked = await reranker.rerank(query, results, { topK: 5 });
|
|
|
5435
5373
|
// 한 번에 (search + rerank)
|
|
5436
5374
|
const best = await reranker.searchAndRerank(ctx, rag, query);
|
|
5437
5375
|
\`\`\``,
|
|
5438
|
-
vibePrompt: `Reranker
|
|
5376
|
+
vibePrompt: `Reranker API:
|
|
5439
5377
|
import { reranker } from "@/gencow/reranker";
|
|
5440
5378
|
reranker.rerank(query, docs, { topK: 5 }) // LLM 재정렬
|
|
5441
5379
|
reranker.searchAndRerank(ctx, rag, query) // 통합 파이프라인`,
|
|
@@ -5465,7 +5403,7 @@ const result = await guardrails.wrap(
|
|
|
5465
5403
|
{ maxLength: 2000 }
|
|
5466
5404
|
);
|
|
5467
5405
|
\`\`\``,
|
|
5468
|
-
vibePrompt: `Guardrails
|
|
5406
|
+
vibePrompt: `Guardrails API:
|
|
5469
5407
|
import { guardrails } from "@/gencow/guardrails";
|
|
5470
5408
|
guardrails.validateInput(text, { maskPII: true }) // PII 마스킹
|
|
5471
5409
|
guardrails.validateInput(text, { blockTopics }) // 주제 차단
|
|
@@ -5492,7 +5430,7 @@ const myPrompt = definePrompt({
|
|
|
5492
5430
|
});
|
|
5493
5431
|
const prompt = myPrompt({ role: "전문가", task: "코드 리뷰" });
|
|
5494
5432
|
\`\`\``,
|
|
5495
|
-
vibePrompt: `Prompt Templates
|
|
5433
|
+
vibePrompt: `Prompt Templates API:
|
|
5496
5434
|
import { definePrompt, ragQAPrompt } from "@/gencow/prompts";
|
|
5497
5435
|
definePrompt({ template, defaults }) // 재사용 프롬프트 정의
|
|
5498
5436
|
ragQAPrompt({ question, context }) // RAG Q&A 프롬프트`,
|
|
@@ -5515,7 +5453,7 @@ await rag.ingest(ctx, "manual.pdf", text);
|
|
|
5515
5453
|
// 자동 감지
|
|
5516
5454
|
const content = await parsers.auto("report.html", htmlString);
|
|
5517
5455
|
\`\`\``,
|
|
5518
|
-
vibePrompt: `File Parsers
|
|
5456
|
+
vibePrompt: `File Parsers API:
|
|
5519
5457
|
import { parsers } from "@/gencow/parsers";
|
|
5520
5458
|
parsers.pdf(buffer) // PDF → 텍스트
|
|
5521
5459
|
parsers.html(htmlString) // HTML → 텍스트
|
|
@@ -5559,18 +5497,18 @@ async function updateReadme(config) {
|
|
|
5559
5497
|
for (const comp of installed) {
|
|
5560
5498
|
componentSection += `### ${comp.title}\n\n`;
|
|
5561
5499
|
componentSection += `${comp.table}\n\n`;
|
|
5562
|
-
componentSection += `####
|
|
5500
|
+
componentSection += `#### Usage\n\n`;
|
|
5563
5501
|
componentSection += `${comp.usage}\n\n`;
|
|
5564
5502
|
}
|
|
5565
5503
|
|
|
5566
5504
|
// 바이브코딩 프롬프트 섹션
|
|
5567
5505
|
componentSection += `### 🤖 AI Vibe-Coding Prompt\n\n`;
|
|
5568
5506
|
componentSection += "```\n";
|
|
5569
|
-
componentSection += "
|
|
5507
|
+
componentSection += "The following AI components are installed in this Gencow backend:\n\n";
|
|
5570
5508
|
for (const comp of installed) {
|
|
5571
5509
|
componentSection += `${comp.vibePrompt}\n\n`;
|
|
5572
5510
|
}
|
|
5573
|
-
componentSection += "
|
|
5511
|
+
componentSection += "Use the APIs above to build your desired features.\n";
|
|
5574
5512
|
componentSection += "```\n\n";
|
|
5575
5513
|
componentSection += `${END_MARKER}`;
|
|
5576
5514
|
|
|
@@ -5584,7 +5522,7 @@ async function updateReadme(config) {
|
|
|
5584
5522
|
}
|
|
5585
5523
|
|
|
5586
5524
|
fs.writeFileSync(readmePath, readme);
|
|
5587
|
-
log(` ${GREEN}✓${RESET} README.md
|
|
5525
|
+
log(` ${GREEN}✓${RESET} README.md component docs updated`);
|
|
5588
5526
|
}
|
|
5589
5527
|
|
|
5590
5528
|
const [, , cmd = "help", ...args] = process.argv;
|