gencow 0.1.102 → 0.1.104

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 CHANGED
@@ -1378,7 +1378,13 @@ ${hasPrompt ? `
1378
1378
 
1379
1379
  if (isLocal) {
1380
1380
  // ── 로컬 모드: 기존 동작 유지 ──
1381
- log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(local)${RESET}\n`);
1381
+ let hasDb = !!process.env.DATABASE_URL;
1382
+ if (!hasDb) {
1383
+ const envPath = resolve(process.cwd(), ".env");
1384
+ if (existsSync(envPath)) hasDb = readFileSync(envPath, "utf8").includes("DATABASE_URL=");
1385
+ }
1386
+ const targetDb = hasDb ? "local PG" : "local fallback: PGlite";
1387
+ log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(${targetDb})${RESET}\n`);
1382
1388
  info("Pushing schema.ts → database (no migration files)...");
1383
1389
  runInServer("pnpm db:push --force", buildEnv(config));
1384
1390
  success("Schema pushed!");
@@ -1407,6 +1413,8 @@ ${hasPrompt ? `
1407
1413
  // ── Cloud 모드: Platform API를 통해 Cloud DB에 schema push ──
1408
1414
  const creds = requireCreds();
1409
1415
  log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
1416
+ warn("명령어 실행 전 먼저 gencow deploy 를 통해 최신 코드가 배포되어 있어야 합니다.");
1417
+ log("");
1410
1418
  info("Pushing schema.ts → cloud database...");
1411
1419
 
1412
1420
  // gencow/ 폴더를 tar.gz로 패키징
@@ -1434,8 +1442,8 @@ ${hasPrompt ? `
1434
1442
  success("마이그레이션 생성 완료");
1435
1443
  } catch (e) {
1436
1444
  const msg = e.stderr?.toString() || e.message || "";
1437
- if (msg.includes("No schema changes") || msg.includes("nothing to migrate")) {
1438
- info("스키마 변경 없음 — 기존 마이그레이션 사용");
1445
+ if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
1446
+ log(`${DIM} 스키마 변경 없음 — 기존 마이그레이션 사용${RESET}`);
1439
1447
  } else {
1440
1448
  warn(`마이그레이션 생성 실패: ${msg.split("\n")[0]}`);
1441
1449
  info("서버에서 기존 방식으로 스키마 적용을 시도합니다.");
@@ -1988,7 +1996,7 @@ ${BOLD}Examples:${RESET}
1988
1996
  msg.includes("nothing to migrate") ||
1989
1997
  msg.includes("No changes detected")
1990
1998
  ) {
1991
- info("스키마 변경 없음 — 기존 마이그레이션 유지");
1999
+ log(`${DIM} 스키마 변경 없음 — 기존 마이그레이션 유지${RESET}`);
1992
2000
  } else {
1993
2001
  warn(`마이그레이션 생성 경고: ${msg.split("\n")[0] || "unknown"}`);
1994
2002
  warn("gencow/migrations/ 가 없거나 오래된 경우 플랫폼에서 스키마 적용이 스킵될 수 있습니다.");
@@ -2140,6 +2148,7 @@ ${BOLD}Examples:${RESET}
2140
2148
  method: "POST",
2141
2149
  headers: {
2142
2150
  "Content-Type": "application/octet-stream",
2151
+ "X-Deploy-Local-Dir": process.cwd(),
2143
2152
  },
2144
2153
  body: bundleBuffer,
2145
2154
  });
@@ -2194,6 +2203,7 @@ ${BOLD}Examples:${RESET}
2194
2203
  method: "POST",
2195
2204
  headers: {
2196
2205
  "Content-Type": "application/octet-stream",
2206
+ "X-Deploy-Local-Dir": process.cwd(),
2197
2207
  },
2198
2208
  body: retryBuffer,
2199
2209
  });
@@ -2490,8 +2500,8 @@ ${BOLD}Examples:${RESET}
2490
2500
  success("마이그레이션 생성 완료");
2491
2501
  } catch (e) {
2492
2502
  const msg = e.stderr?.toString() || e.message || "";
2493
- if (msg.includes("No schema changes") || msg.includes("nothing to migrate")) {
2494
- info("스키마 변경 없음 — 기존 마이그레이션 사용");
2503
+ if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
2504
+ log(`${DIM} 스키마 변경 없음 — 기존 마이그레이션 사용${RESET}`);
2495
2505
  } else {
2496
2506
  warn(`마이그레이션 생성 실패: ${msg.split("\\n")[0]}`);
2497
2507
  info("서버에서 기존 방식으로 스키마 적용을 시도합니다.");
@@ -2602,7 +2612,7 @@ ${BOLD}Examples:${RESET}
2602
2612
 
2603
2613
  const backendDeployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
2604
2614
  method: "POST",
2605
- headers: { "Content-Type": "application/octet-stream" },
2615
+ headers: { "Content-Type": "application/octet-stream", "X-Deploy-Local-Dir": process.cwd() },
2606
2616
  body: backendBuffer,
2607
2617
  });
2608
2618
 
@@ -2685,7 +2695,7 @@ ${BOLD}Examples:${RESET}
2685
2695
 
2686
2696
  const deployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy-static`, {
2687
2697
  method: "POST",
2688
- headers: { "Content-Type": "application/octet-stream" },
2698
+ headers: { "Content-Type": "application/octet-stream", "X-Deploy-Local-Dir": process.cwd() },
2689
2699
  body: bundleBuffer,
2690
2700
  });
2691
2701
 
package/core/index.js CHANGED
@@ -1665,7 +1665,12 @@ function createScheduler() {
1665
1665
  actions.set(name, handler);
1666
1666
  },
1667
1667
  async executeAction(name, args) {
1668
- await executeAction(name, args);
1668
+ try {
1669
+ await executeAction(name, args);
1670
+ } catch (error) {
1671
+ const msg = error instanceof Error ? error.message : String(error);
1672
+ console.error(`[scheduler] executeAction("${name}") failed: ${msg}`);
1673
+ }
1669
1674
  }
1670
1675
  };
1671
1676
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
package/server/index.js CHANGED
@@ -1673,7 +1673,12 @@ function createScheduler() {
1673
1673
  actions.set(name21, handler);
1674
1674
  },
1675
1675
  async executeAction(name21, args) {
1676
- await executeAction(name21, args);
1676
+ try {
1677
+ await executeAction(name21, args);
1678
+ } catch (error95) {
1679
+ const msg = error95 instanceof Error ? error95.message : String(error95);
1680
+ console.error(`[scheduler] executeAction("${name21}") failed: ${msg}`);
1681
+ }
1677
1682
  }
1678
1683
  };
1679
1684
  }
@@ -82259,10 +82264,6 @@ if (IS_BAAS) {
82259
82264
  const contentLength = c.res.headers.get("content-length");
82260
82265
  if (contentLength) {
82261
82266
  runtimeBuffer.add("bandwidth_bytes", parseInt(contentLength, 10));
82262
- } else if (c.res.body) {
82263
- const cloned = c.res.clone();
82264
- const buf = await cloned.arrayBuffer();
82265
- runtimeBuffer.add("bandwidth_bytes", buf.byteLength);
82266
82267
  }
82267
82268
  } catch {
82268
82269
  }
@@ -82637,25 +82638,19 @@ async function main() {
82637
82638
  // @ts-ignore
82638
82639
  duplex: "half"
82639
82640
  });
82641
+ const buf = await proxyRes.arrayBuffer();
82640
82642
  if (appName) {
82641
- try {
82642
- const cl = proxyRes.headers.get("content-length");
82643
- if (cl) {
82644
- addProxyMetric(appName, parseInt(cl, 10));
82645
- } else if (proxyRes.body) {
82646
- const buf = await proxyRes.arrayBuffer();
82647
- addProxyMetric(appName, buf.byteLength);
82648
- return new Response(buf, {
82649
- status: proxyRes.status,
82650
- headers: proxyRes.headers
82651
- });
82652
- }
82653
- } catch {
82654
- }
82643
+ addProxyMetric(appName, buf.byteLength);
82655
82644
  }
82656
- return new Response(proxyRes.body, {
82645
+ const resHeaders = new Headers(proxyRes.headers);
82646
+ if (buf.byteLength > 0) {
82647
+ resHeaders.set("content-length", String(buf.byteLength));
82648
+ } else {
82649
+ resHeaders.delete("content-length");
82650
+ }
82651
+ return new Response(buf, {
82657
82652
  status: proxyRes.status,
82658
- headers: proxyRes.headers
82653
+ headers: resHeaders
82659
82654
  });
82660
82655
  }
82661
82656
  async function serveStaticFile(c, dataDir, pathname, appName) {
@@ -82771,6 +82766,22 @@ async function main() {
82771
82766
  if (sleepingApps.length > 0) {
82772
82767
  console.log(`[platform] Registered ${sleepingApps.length} sleeping app(s) into memory for future wake \u2713`);
82773
82768
  }
82769
+ const allAppsForCronSync = [...runningApps, ...sleepingApps];
82770
+ if (allAppsForCronSync.length > 0) {
82771
+ const { syncCronJobs: _syncCronJobs } = await import(resolve5(functionsPath, "../src/provisioner.ts"));
82772
+ let cronSyncCount = 0;
82773
+ for (const row of allAppsForCronSync) {
82774
+ try {
82775
+ await _syncCronJobs(row.id, row.name, row.dataDir);
82776
+ cronSyncCount++;
82777
+ } catch (e) {
82778
+ console.warn(`[cron] ${row.name}: boot-time cron sync failed:`, e.message);
82779
+ }
82780
+ }
82781
+ if (cronSyncCount > 0) {
82782
+ console.log(`[platform] Boot-time cron sync complete for ${cronSyncCount}/${allAppsForCronSync.length} app(s) \u2713`);
82783
+ }
82784
+ }
82774
82785
  } catch (e) {
82775
82786
  const msg = e instanceof Error ? e.message : String(e);
82776
82787
  console.error("[platform] Auto-recovery failed:", msg);
@@ -82808,15 +82819,27 @@ async function main() {
82808
82819
  try {
82809
82820
  const { canSleep: _canSleep, sleepApp: _sleepApp, getLastAccess: _getLastAccess, getAppMeta: _getAppMeta } = await import(resolve5(functionsPath, "../src/provisioner.ts"));
82810
82821
  const metaMap = _getAppMeta();
82822
+ const allApps = await db.select({ name: appsTable.name, status: appsTable.status }).from(appsTable);
82823
+ const dbStatusMap = new Map(allApps.map((a) => [a.name, a.status]));
82811
82824
  for (const [appName] of metaMap) {
82812
82825
  if (sleepCount >= MAX_SLEEP_PER_CYCLE) break;
82826
+ const dbStatus = dbStatusMap.get(appName);
82827
+ if (dbStatus === "sleeping") {
82828
+ console.log(`[sleep] SWEEP: ${appName} is "sleeping" in DB but active in memory \u2014 enforcing sleep...`);
82829
+ try {
82830
+ await _sleepApp(appName);
82831
+ sleepCount++;
82832
+ } catch (e) {
82833
+ console.error(`[sleep] Failed to sweep ghost app ${appName}:`, e);
82834
+ }
82835
+ continue;
82836
+ }
82813
82837
  const last = _getLastAccess(appName) || 0;
82814
82838
  const canSlp = _canSleep(appName);
82815
82839
  if (!canSlp) continue;
82816
82840
  const idleMin = Math.floor((Date.now() - last) / 6e4);
82817
82841
  try {
82818
82842
  await _sleepApp(appName);
82819
- await db.update(appsTable).set({ status: "sleeping" }).where(eqOp(appsTable.name, appName));
82820
82843
  console.log(`[sleep] ${appName}: idle ${idleMin}m \u2014 sleeping \u2713`);
82821
82844
  sleepCount++;
82822
82845
  } catch (e) {
@@ -82909,6 +82932,8 @@ async function main() {
82909
82932
  console.error(`[cron] ${label}: failed (${elapsed}s) \u2014 ${errMsg}`);
82910
82933
  }
82911
82934
  }
82935
+ let _cronEmptyCount = 0;
82936
+ const CRON_EMPTY_LOG_INTERVAL = 5;
82912
82937
  setInterval(async () => {
82913
82938
  try {
82914
82939
  const dueJobs = await rawSql(
@@ -82921,15 +82946,31 @@ async function main() {
82921
82946
  ORDER BY cj.next_run_at ASC
82922
82947
  LIMIT 50`
82923
82948
  );
82924
- if (dueJobs.length === 0) return;
82925
- console.log(`[cron] ${dueJobs.length} due cron job(s) found`);
82926
- for (let i = 0; i < dueJobs.length; i += CRON_CONCURRENCY) {
82927
- const batch = dueJobs.slice(i, i + CRON_CONCURRENCY);
82928
- await Promise.allSettled(batch.map((job) => executeCronJob(job)));
82929
- if (i + CRON_CONCURRENCY < dueJobs.length) {
82930
- await new Promise((r) => setTimeout(r, 1e3));
82949
+ if (dueJobs.length === 0) {
82950
+ _cronEmptyCount++;
82951
+ if (_cronEmptyCount % CRON_EMPTY_LOG_INTERVAL === 0) {
82952
+ console.log(`[cron] No due jobs (${_cronEmptyCount} consecutive empty polls)`);
82931
82953
  }
82954
+ return;
82932
82955
  }
82956
+ _cronEmptyCount = 0;
82957
+ console.log(`[cron] ${dueJobs.length} due cron job(s) found`);
82958
+ for (const job of dueJobs) {
82959
+ const nextRun = cronCalculateNextRun2(job.schedule);
82960
+ await rawSql(
82961
+ `UPDATE cron_jobs SET next_run_at = $1 WHERE id = $2`,
82962
+ [nextRun, job.id]
82963
+ );
82964
+ }
82965
+ void (async () => {
82966
+ for (let i = 0; i < dueJobs.length; i += CRON_CONCURRENCY) {
82967
+ const batch = dueJobs.slice(i, i + CRON_CONCURRENCY);
82968
+ await Promise.allSettled(batch.map((job) => executeCronJob(job)));
82969
+ if (i + CRON_CONCURRENCY < dueJobs.length) {
82970
+ await new Promise((r) => setTimeout(r, 1e3));
82971
+ }
82972
+ }
82973
+ })();
82933
82974
  } catch (err) {
82934
82975
  console.error(`[cron] Scheduler error:`, err.message);
82935
82976
  }
@@ -83253,7 +83294,7 @@ async function main() {
83253
83294
  if (!IS_BAAS && Array.isArray(result) && result.length >= 2) {
83254
83295
  checkMixedUserIds(name21, result);
83255
83296
  }
83256
- return c.json(result);
83297
+ return c.json(result ?? null);
83257
83298
  } catch (err) {
83258
83299
  const status = err?.code === "FUNCTION_TIMEOUT" ? 408 : err instanceof GencowValidationError ? 400 : 500;
83259
83300
  return c.json({ error: err.message, ...err?.code ? { code: err.code } : {} }, status);
@@ -83288,7 +83329,7 @@ async function main() {
83288
83329
  name21
83289
83330
  );
83290
83331
  await invalidateQueries(mut.invalidates, ctx);
83291
- return c.json(result, 201);
83332
+ return c.json(result ?? null, 201);
83292
83333
  } catch (err) {
83293
83334
  const status = err?.code === "FUNCTION_TIMEOUT" ? 408 : err instanceof GencowValidationError ? 400 : 500;
83294
83335
  return c.json({ error: err.message, ...err?.code ? { code: err.code } : {} }, status);