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.
Files changed (2) hide show
  1. package/bin/gencow.mjs +172 -32
  2. 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:remote — watch & auto-deploy${RESET}
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:remote (Convex-like watch + auto-deploy) ──
2660
- async "dev:remote"() {
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
- const appName = config.deploy?.app || creds.currentApp;
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 current app. Run: gencow app create <name> first");
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
- log(`\n${DIM}${ts}${RESET} ${CYAN}▸${RESET} ${reason === "initial" ? "Initial deploy" : `Changed: ${reason}`}`);
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
- warn(`Skipping API codegen due to error: ${e.message}`);
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
- if (!res.ok) { error(`Deploy failed: ${data.error}`); return; }
2705
- success(`${appName} deployed ${DIM}(${(bundle.length / 1024).toFixed(1)} KB, hash: ${data.bundleHash})${RESET}`);
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 Remote${RESET}
2840
+ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
2711
2841
 
2712
- ${GREEN}▸${RESET} App: ${BOLD}${appName}${RESET}
2713
- ${GREEN}▸${RESET} Watching: ${DIM}${functionsDir}${RESET}
2714
- ${GREEN}▸${RESET} Platform: ${DIM}${creds.platformUrl}${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
- error(`Deploy error: ${e.message}`);
2875
+ const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
2876
+ log(`${DIM}${ts}${RESET} ${RED}[error]${RESET} ${e.message}`);
2736
2877
  }
2737
- }, 300); // 300ms debounce — 여러 파일 동시 저장 대비
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
- log(`\n\n${DIM}Stopped watching. App remains running on platform.${RESET}\n`);
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("-"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {