gencow 0.1.48 → 0.1.50
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 +172 -32
- package/package.json +1 -1
package/bin/gencow.mjs
CHANGED
|
@@ -989,7 +989,12 @@ ${hasPrompt ? `
|
|
|
989
989
|
},
|
|
990
990
|
|
|
991
991
|
// ── dev ──────────────────────────────────────────────
|
|
992
|
-
async dev() {
|
|
992
|
+
async dev(...devArgs) {
|
|
993
|
+
// ── --cloud 모드: 클라우드 앱에 watch + auto-deploy + 실시간 로그 ──
|
|
994
|
+
if (devArgs.includes("--cloud")) {
|
|
995
|
+
return commands["dev:cloud"](...devArgs);
|
|
996
|
+
}
|
|
997
|
+
|
|
993
998
|
const config = loadConfig();
|
|
994
999
|
log(`\n${BOLD}${CYAN}Gencow Dev${RESET}\n`);
|
|
995
1000
|
info(`Functions: ${DIM}${config.functionsDir}${RESET}`);
|
|
@@ -2296,7 +2301,7 @@ process.exit(0);
|
|
|
2296
2301
|
${GREEN}▸${RESET} Dashboard: ${data.dashboard}
|
|
2297
2302
|
${GREEN}▸${RESET} .env: VITE_API_URL set
|
|
2298
2303
|
|
|
2299
|
-
${DIM}pnpm gencow dev
|
|
2304
|
+
${DIM}pnpm gencow dev --cloud — watch & auto-deploy to cloud + live logs${RESET}
|
|
2300
2305
|
`);
|
|
2301
2306
|
return;
|
|
2302
2307
|
}
|
|
@@ -2656,13 +2661,22 @@ process.exit(0);
|
|
|
2656
2661
|
process.on("SIGINT", () => proc.kill());
|
|
2657
2662
|
},
|
|
2658
2663
|
|
|
2659
|
-
// ── dev:
|
|
2660
|
-
async "dev:
|
|
2664
|
+
// ── dev:cloud (watch + auto-deploy + live log streaming) ──
|
|
2665
|
+
async "dev:cloud"(...cloudArgs) {
|
|
2661
2666
|
const creds = requireCreds();
|
|
2662
2667
|
const config = loadConfig();
|
|
2663
|
-
|
|
2668
|
+
|
|
2669
|
+
// ── --app <name> 옵션 파싱 ──
|
|
2670
|
+
let appName = null;
|
|
2671
|
+
const appIdx = cloudArgs.indexOf("--app");
|
|
2672
|
+
if (appIdx !== -1 && cloudArgs[appIdx + 1]) {
|
|
2673
|
+
appName = cloudArgs[appIdx + 1];
|
|
2674
|
+
}
|
|
2675
|
+
appName = appName || config.deploy?.app || creds.currentApp;
|
|
2676
|
+
|
|
2664
2677
|
if (!appName) {
|
|
2665
|
-
error("No
|
|
2678
|
+
error("No app specified.");
|
|
2679
|
+
info("Use --app <name> or run: gencow app create <name> first");
|
|
2666
2680
|
process.exit(1);
|
|
2667
2681
|
}
|
|
2668
2682
|
|
|
@@ -2673,17 +2687,41 @@ process.exit(0);
|
|
|
2673
2687
|
process.exit(1);
|
|
2674
2688
|
}
|
|
2675
2689
|
|
|
2676
|
-
|
|
2690
|
+
const appUrl = `https://${appName}.gencow.app`;
|
|
2691
|
+
|
|
2692
|
+
// ── 배포 함수 (번들 크기 + 소요 시간 표시) ────────────
|
|
2677
2693
|
async function deploy(reason = "initial") {
|
|
2678
|
-
const ts = new Date().toLocaleTimeString();
|
|
2679
|
-
|
|
2694
|
+
const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
2695
|
+
const isSchema = reason !== "initial" && /schema.*\.ts$/i.test(reason);
|
|
2696
|
+
|
|
2697
|
+
if (reason === "initial") {
|
|
2698
|
+
log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} 초기 배포 중...`);
|
|
2699
|
+
} else {
|
|
2700
|
+
log(`${DIM}${ts}${RESET} ${CYAN}[deploy]${RESET} ${reason} 변경 감지 → 배포 중...`);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// 스키마 변경 시 push 자동 실행 (마이그레이션 생성)
|
|
2704
|
+
if (isSchema) {
|
|
2705
|
+
log(`${DIM}${ts}${RESET} ${YELLOW}[migrate]${RESET} 스키마 변경 감지 → 마이그레이션 생성 중...`);
|
|
2706
|
+
try {
|
|
2707
|
+
const { execSync } = await import("child_process");
|
|
2708
|
+
execSync("npx drizzle-kit generate 2>&1", {
|
|
2709
|
+
cwd: process.cwd(),
|
|
2710
|
+
stdio: "pipe",
|
|
2711
|
+
});
|
|
2712
|
+
log(`${DIM}${ts}${RESET} ${GREEN}[migrate]${RESET} ✔ 마이그레이션 생성 완료`);
|
|
2713
|
+
} catch (e) {
|
|
2714
|
+
warn(`[migrate] 마이그레이션 생성 실패 (무시 가능): ${e.message?.split("\n")[0]}`);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2680
2717
|
|
|
2681
2718
|
try {
|
|
2682
2719
|
generateApiTs(config);
|
|
2683
2720
|
} catch (e) {
|
|
2684
|
-
|
|
2721
|
+
// skip silently
|
|
2685
2722
|
}
|
|
2686
2723
|
|
|
2724
|
+
const startMs = Date.now();
|
|
2687
2725
|
const { create: tarCreate } = await import("tar");
|
|
2688
2726
|
const chunks = [];
|
|
2689
2727
|
await new Promise((res, rej) => {
|
|
@@ -2701,24 +2739,125 @@ process.exit(0);
|
|
|
2701
2739
|
method: "POST", body: form,
|
|
2702
2740
|
});
|
|
2703
2741
|
const data = await res.json();
|
|
2704
|
-
|
|
2705
|
-
|
|
2742
|
+
const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
|
|
2743
|
+
const sizeKB = (bundle.length / 1024).toFixed(1);
|
|
2744
|
+
|
|
2745
|
+
if (!res.ok) {
|
|
2746
|
+
const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
2747
|
+
log(`${DIM}${ts2}${RESET} ${RED}[deploy]${RESET} ✗ 배포 실패!`);
|
|
2748
|
+
|
|
2749
|
+
// 에러 메시지에서 파일:라인 정보 추출
|
|
2750
|
+
const errMsg = data.error || "Unknown error";
|
|
2751
|
+
log(` ${RED}${errMsg}${RESET}`);
|
|
2752
|
+
|
|
2753
|
+
// 스키마 관련 에러 힌트
|
|
2754
|
+
if (errMsg.includes("does not exist") || errMsg.includes("relation")) {
|
|
2755
|
+
log(` ${YELLOW}💡 스키마가 변경되었나요? schema.ts를 수정하면 자동으로 마이그레이션이 실행됩니다.${RESET}`);
|
|
2756
|
+
}
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
2761
|
+
log(`${DIM}${ts2}${RESET} ${GREEN}[deploy]${RESET} ✔ 배포 완료 ${DIM}(${sizeKB}KB, ${elapsed}s)${RESET}`);
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
// ── 로그 포맷팅 함수 ────────────────────────────────
|
|
2765
|
+
function formatLogEntry(msg) {
|
|
2766
|
+
if (msg.type === "log:subscribed") return null;
|
|
2767
|
+
if (msg.type !== "log:entry") return null;
|
|
2768
|
+
|
|
2769
|
+
const ts = msg.timestamp?.slice(11, 19) || "";
|
|
2770
|
+
const lvl = msg.level === "error" ? `${RED}ERR${RESET}`
|
|
2771
|
+
: msg.level === "warn" ? `${YELLOW}WRN${RESET}`
|
|
2772
|
+
: `${DIM}INF${RESET}`;
|
|
2773
|
+
const src = msg.source ? ` ${DIM}[${msg.source}]${RESET}` : "";
|
|
2774
|
+
|
|
2775
|
+
// 에러 시 전체 메시지 (스택 트레이스 포함)
|
|
2776
|
+
if (msg.level === "error") {
|
|
2777
|
+
return `${DIM}${ts}${RESET} ${lvl}${src} ${RED}${msg.message}${RESET}`;
|
|
2778
|
+
}
|
|
2779
|
+
return `${DIM}${ts}${RESET} ${lvl}${src} ${msg.message}`;
|
|
2706
2780
|
}
|
|
2707
2781
|
|
|
2708
|
-
// ──
|
|
2782
|
+
// ── WebSocket 실시간 로그 스트리밍 ─────────────────
|
|
2783
|
+
let logWs = null;
|
|
2784
|
+
let reconnectTimer = null;
|
|
2785
|
+
|
|
2786
|
+
async function connectLogStream() {
|
|
2787
|
+
const { WebSocket: WS } = await import("ws");
|
|
2788
|
+
const wsUrl = `wss://${appName}.gencow.app/_admin/ws`;
|
|
2789
|
+
|
|
2790
|
+
try {
|
|
2791
|
+
logWs = new WS(wsUrl);
|
|
2792
|
+
} catch (e) {
|
|
2793
|
+
warn(`[log] WebSocket 연결 실패: ${e.message}`);
|
|
2794
|
+
scheduleReconnect();
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
logWs.on("open", () => {
|
|
2799
|
+
log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${GREEN}[log]${RESET} 로그 스트리밍 연결됨`);
|
|
2800
|
+
logWs.send(JSON.stringify({ type: "log:subscribe" }));
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2803
|
+
logWs.on("message", (raw) => {
|
|
2804
|
+
try {
|
|
2805
|
+
const msg = JSON.parse(raw.toString());
|
|
2806
|
+
const formatted = formatLogEntry(msg);
|
|
2807
|
+
if (formatted) log(formatted);
|
|
2808
|
+
} catch { /* non-JSON 무시 */ }
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
logWs.on("error", () => {
|
|
2812
|
+
// 에러는 close 이벤트로 처리
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
logWs.on("close", () => {
|
|
2816
|
+
log(`${DIM}${new Date().toLocaleTimeString("en-US", { hour12: false })}${RESET} ${YELLOW}[log]${RESET} 연결 끊김 — 3초 후 재연결...`);
|
|
2817
|
+
scheduleReconnect();
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function scheduleReconnect() {
|
|
2822
|
+
if (reconnectTimer) return;
|
|
2823
|
+
reconnectTimer = setTimeout(() => {
|
|
2824
|
+
reconnectTimer = null;
|
|
2825
|
+
connectLogStream().catch(() => {});
|
|
2826
|
+
}, 3000);
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// ── 배너 출력 ────────────────────────────────────
|
|
2830
|
+
// 감시 대상 파일 목록
|
|
2831
|
+
const watchedFiles = [];
|
|
2832
|
+
try {
|
|
2833
|
+
const { readdirSync } = await import("fs");
|
|
2834
|
+
const files = readdirSync(absoluteFunctions)
|
|
2835
|
+
.filter(f => f.endsWith(".ts") && !f.startsWith(".") && f !== "api.ts");
|
|
2836
|
+
watchedFiles.push(...files);
|
|
2837
|
+
} catch { /* ignore */ }
|
|
2838
|
+
|
|
2709
2839
|
log(`
|
|
2710
|
-
${BOLD}${CYAN}Gencow Dev
|
|
2840
|
+
${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
|
|
2711
2841
|
|
|
2712
|
-
${GREEN}▸${RESET} App:
|
|
2713
|
-
${GREEN}▸${RESET}
|
|
2714
|
-
${GREEN}▸${RESET}
|
|
2842
|
+
${GREEN}▸${RESET} App: ${BOLD}${appName}${RESET}
|
|
2843
|
+
${GREEN}▸${RESET} URL: ${DIM}${appUrl}${RESET}
|
|
2844
|
+
${GREEN}▸${RESET} Watching: ${DIM}${functionsDir}/ (${watchedFiles.join(", ") || "*.ts"})${RESET}
|
|
2845
|
+
${GREEN}▸${RESET} Mode: ${DIM}Cloud (PostgreSQL)${RESET}
|
|
2715
2846
|
|
|
2716
2847
|
${DIM}Ctrl+C to stop${RESET}
|
|
2848
|
+
|
|
2849
|
+
──── Live ─────────────────────────────────────────
|
|
2717
2850
|
`);
|
|
2718
2851
|
|
|
2719
|
-
// ──
|
|
2852
|
+
// ── 초기 배포 ────────────────────────────────────
|
|
2720
2853
|
await deploy("initial");
|
|
2721
2854
|
|
|
2855
|
+
// ── 로그 스트리밍 시작 (배포 후 연결) ──────────────
|
|
2856
|
+
// 약간의 딜레이 후 연결 (앱 재시작 대기)
|
|
2857
|
+
setTimeout(() => {
|
|
2858
|
+
connectLogStream().catch(() => {});
|
|
2859
|
+
}, 2000);
|
|
2860
|
+
|
|
2722
2861
|
// ── 파일 감시 (fs.watch recursive) ───────────────
|
|
2723
2862
|
const { watch } = await import("fs");
|
|
2724
2863
|
let debounceTimer = null;
|
|
@@ -2731,30 +2870,31 @@ ${BOLD}${CYAN}Gencow Dev Remote${RESET}
|
|
|
2731
2870
|
debounceTimer = setTimeout(async () => {
|
|
2732
2871
|
try {
|
|
2733
2872
|
await deploy(pendingFile);
|
|
2873
|
+
// 배포 후 로그 재연결 (앱 재시작으로 WS 끊길 수 있음)
|
|
2734
2874
|
} catch (e) {
|
|
2735
|
-
|
|
2875
|
+
const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
2876
|
+
log(`${DIM}${ts}${RESET} ${RED}[error]${RESET} ${e.message}`);
|
|
2736
2877
|
}
|
|
2737
|
-
},
|
|
2878
|
+
}, 500); // 500ms debounce
|
|
2738
2879
|
});
|
|
2739
2880
|
|
|
2740
|
-
// ── 주기적 상태 확인 (30s) ───────────────────────
|
|
2741
|
-
setInterval(async () => {
|
|
2742
|
-
try {
|
|
2743
|
-
const res = await platformFetch(creds, `/platform/apps/${appName}`);
|
|
2744
|
-
const data = await res.json();
|
|
2745
|
-
const ts = new Date().toLocaleTimeString();
|
|
2746
|
-
const status = data.running ? `${GREEN}running${RESET}` : `${YELLOW}${data.status}${RESET}`;
|
|
2747
|
-
log(`${DIM}${ts} [heartbeat] ${appName}: ${status}${RESET}`);
|
|
2748
|
-
} catch { /* ignore */ }
|
|
2749
|
-
}, 30_000);
|
|
2750
|
-
|
|
2751
2881
|
// ── SIGINT 처리 ───────────────────────────────────
|
|
2752
2882
|
process.on("SIGINT", () => {
|
|
2753
|
-
|
|
2883
|
+
if (logWs) {
|
|
2884
|
+
try { logWs.close(); } catch {}
|
|
2885
|
+
}
|
|
2886
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
2887
|
+
log(`\n\n${DIM} Stopped. App remains running at ${appUrl}${RESET}\n`);
|
|
2754
2888
|
process.exit(0);
|
|
2755
2889
|
});
|
|
2756
2890
|
},
|
|
2757
2891
|
|
|
2892
|
+
// ── dev:remote (deprecated → dev:cloud 위임) ──
|
|
2893
|
+
async "dev:remote"(...args) {
|
|
2894
|
+
warn("dev:remote is deprecated. Use: gencow dev --cloud");
|
|
2895
|
+
return commands["dev:cloud"](...args);
|
|
2896
|
+
},
|
|
2897
|
+
|
|
2758
2898
|
// ── add <component...> ──────────────────────────────
|
|
2759
2899
|
async add(...args) {
|
|
2760
2900
|
const components = args.filter(a => !a.startsWith("-"));
|