gencow 0.1.138 → 0.1.140

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
@@ -306,7 +306,7 @@ ${BOLD}Commands (login required):${RESET}
306
306
  ${DIM}--prod Seed production app${RESET}
307
307
  ${GREEN}static [dir]${RESET} Deploy static files ${DIM}(dist/, out/, build/)${RESET}
308
308
  ${DIM}--prod Deploy to production app${RESET}
309
- ${DIM}--force, -f Skip dependency audit${RESET}
309
+ ${DIM}--force, -f Skip optional dependency scan${RESET}
310
310
  ${GREEN}templates publish${RESET} Publish current project as a marketplace template
311
311
  ${DIM}--title, --slug, --price, --private, --unlisted${RESET}
312
312
  ${GREEN}templates list${RESET} Browse public templates
@@ -314,7 +314,7 @@ ${BOLD}Commands (login required):${RESET}
314
314
  ${GREEN}deploy${RESET} Deploy backend to cloud ${DIM}(dev by default)${RESET}
315
315
  ${DIM}--prod Deploy to production (Pro+ only)${RESET}
316
316
  ${DIM}--rollback Rollback to previous deployment${RESET}
317
- ${DIM}--force, -f Skip dependency audit${RESET}
317
+ ${DIM}--force, -f Skip optional dependency scan${RESET}
318
318
  ${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
319
319
  ${GREEN}env set K=V${RESET} Set cloud env var ${DIM}(hot-reload, no restart)${RESET}
320
320
  ${GREEN}env unset KEY${RESET} Remove cloud env var
package/core/index.js CHANGED
@@ -1391,6 +1391,103 @@ function buildDocumentCacheKey(input) {
1391
1391
  ].join(":");
1392
1392
  }
1393
1393
 
1394
+ // ../core/src/wake-app-result.ts
1395
+ var DEFAULT_WAKE_RETRY_AFTER_SEC = 30;
1396
+ function buildWakeAppSuccessResult(status, port) {
1397
+ return { ok: true, status, port };
1398
+ }
1399
+ function buildWakeAppBootFailedResult(error) {
1400
+ const message = error instanceof Error ? error.message : String(error);
1401
+ return { ok: false, status: "boot_failed", error: message };
1402
+ }
1403
+ function isWakeAppDeferredResult(result) {
1404
+ return !result.ok && (result.status === "capacity_rejected" || result.status === "queue_timeout");
1405
+ }
1406
+
1407
+ // ../core/src/platform-capacity-profile.ts
1408
+ var PLATFORM_CAPACITY_PROFILE_ENV = "GENCOW_CAPACITY_PROFILE";
1409
+ var PLATFORM_CAPACITY_ENV_KEYS = [
1410
+ PLATFORM_CAPACITY_PROFILE_ENV,
1411
+ "MAX_CONCURRENT_RUNNING",
1412
+ "MAX_CONCURRENT_WAKE",
1413
+ "MAX_WAKE_QUEUE_MS",
1414
+ "MIN_AVAILABLE_RAM_MB",
1415
+ "WAKE_RETRY_AFTER_SEC",
1416
+ "EVICTION_THRESHOLD_MB",
1417
+ "GENCOW_SLEEP_TIMEOUT_MINUTES",
1418
+ "GENCOW_SLEEP_MAX_PER_CYCLE",
1419
+ "WARM_POOL_MIN_IDLE",
1420
+ "WARM_POOL_MAX",
1421
+ "DEPLOY_CANDIDATE_RESERVE",
1422
+ "GENCOW_APP_PORT_RANGE",
1423
+ "COWBOX_WARM_POOL_PORT_RANGE"
1424
+ ];
1425
+ var PLATFORM_CAPACITY_PRESETS = Object.freeze({
1426
+ prod: Object.freeze({
1427
+ profile: "prod",
1428
+ maxConcurrentRunning: 600,
1429
+ maxConcurrentWake: 20,
1430
+ maxWakeQueueMs: 5e3,
1431
+ minAvailableRamMB: 8192,
1432
+ evictionThresholdMB: 16384,
1433
+ sleepTimeoutMinutes: 15,
1434
+ sleepMaxPerCycle: 50,
1435
+ warmPoolMinIdle: 10,
1436
+ warmPoolMax: 650,
1437
+ deployCandidateReserve: 40
1438
+ }),
1439
+ dev: Object.freeze({
1440
+ profile: "dev",
1441
+ maxConcurrentRunning: 60,
1442
+ maxConcurrentWake: 5,
1443
+ maxWakeQueueMs: 3e3,
1444
+ minAvailableRamMB: 2048,
1445
+ evictionThresholdMB: 4096,
1446
+ sleepTimeoutMinutes: 10,
1447
+ sleepMaxPerCycle: 20,
1448
+ warmPoolMinIdle: 3,
1449
+ warmPoolMax: 70,
1450
+ deployCandidateReserve: 5
1451
+ }),
1452
+ local: Object.freeze({
1453
+ profile: "local",
1454
+ maxConcurrentRunning: null,
1455
+ maxConcurrentWake: null,
1456
+ maxWakeQueueMs: 5e3,
1457
+ minAvailableRamMB: null,
1458
+ evictionThresholdMB: null,
1459
+ sleepTimeoutMinutes: 30,
1460
+ sleepMaxPerCycle: 10,
1461
+ warmPoolMinIdle: 3,
1462
+ warmPoolMax: 10,
1463
+ deployCandidateReserve: 0
1464
+ })
1465
+ });
1466
+ function normalizeDomain(value) {
1467
+ return (value || "").trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
1468
+ }
1469
+ function detectPlatformCapacityProfile(env = process.env) {
1470
+ const explicit = env[PLATFORM_CAPACITY_PROFILE_ENV]?.trim().toLowerCase();
1471
+ if (explicit) {
1472
+ if (["prod", "production", "128gb"].includes(explicit)) return "prod";
1473
+ if (["dev", "development", "16gb"].includes(explicit)) return "dev";
1474
+ if (["local", "test", "disabled"].includes(explicit)) return "local";
1475
+ throw new Error(`Invalid ${PLATFORM_CAPACITY_PROFILE_ENV}: ${explicit}. Expected prod, dev, or local.`);
1476
+ }
1477
+ const baseDomain = normalizeDomain(env.BASE_DOMAIN);
1478
+ if (baseDomain === "gencow.dev") return "dev";
1479
+ if (baseDomain === "gencow.app") return "prod";
1480
+ const platformUrl = normalizeDomain(env.GENCOW_PLATFORM_URL);
1481
+ if (platformUrl === "gencow.dev") return "dev";
1482
+ if (platformUrl === "gencow.app") return "prod";
1483
+ if (env.NODE_ENV === "production" && env.IS_PLATFORM === "true") return "prod";
1484
+ return "local";
1485
+ }
1486
+ function resolvePlatformCapacityPreset(env = process.env) {
1487
+ const profile = detectPlatformCapacityProfile(env);
1488
+ return { ...PLATFORM_CAPACITY_PRESETS[profile] };
1489
+ }
1490
+
1394
1491
  // ../core/src/grounded-answer-types.ts
1395
1492
  var DEFAULT_GROUNDING_BUDGET = {
1396
1493
  maxVerifyLoops: 2,
@@ -1499,6 +1596,34 @@ var mutationRegistry = globalThis.__gencow_mutationRegistry;
1499
1596
  var httpActionRegistry = globalThis.__gencow_httpActionRegistry;
1500
1597
  var subscribers = globalThis.__gencow_subscribers;
1501
1598
  var connectedClients = globalThis.__gencow_connectedClients;
1599
+ var SUBSCRIPTION_KEY_SEPARATOR = "::";
1600
+ function normalizeForStableJson(value) {
1601
+ if (value === void 0) return void 0;
1602
+ if (value === null) return null;
1603
+ if (value instanceof Date) return value.toISOString();
1604
+ if (Array.isArray(value)) return value.map((item) => normalizeForStableJson(item));
1605
+ if (typeof value === "object") {
1606
+ const source = value;
1607
+ const sorted = {};
1608
+ for (const key of Object.keys(source).sort()) {
1609
+ const normalized = normalizeForStableJson(source[key]);
1610
+ if (normalized !== void 0) sorted[key] = normalized;
1611
+ }
1612
+ return sorted;
1613
+ }
1614
+ return value;
1615
+ }
1616
+ function isEmptyPlainObject(value) {
1617
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
1618
+ }
1619
+ function buildQuerySubscriptionKey(queryKey, args) {
1620
+ const normalized = normalizeForStableJson(args);
1621
+ if (normalized === void 0 || isEmptyPlainObject(normalized)) return queryKey;
1622
+ return `${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}${JSON.stringify(normalized)}`;
1623
+ }
1624
+ function subscriptionKeyMatchesQueryKey(subscriptionKey, queryKey) {
1625
+ return subscriptionKey === queryKey || subscriptionKey.startsWith(`${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}`);
1626
+ }
1502
1627
  function query(key, handlerOrDef) {
1503
1628
  let handler;
1504
1629
  let argsSchema;
@@ -1584,6 +1709,25 @@ function deregisterClient(ws) {
1584
1709
  clients.delete(ws);
1585
1710
  }
1586
1711
  }
1712
+ function sendInvalidateToLocalSubscribers(queryKeys) {
1713
+ const targets = /* @__PURE__ */ new Map();
1714
+ for (const queryKey of queryKeys) {
1715
+ for (const [subscriptionKey, clients] of subscribers) {
1716
+ if (!subscriptionKeyMatchesQueryKey(subscriptionKey, queryKey)) continue;
1717
+ for (const ws of clients) {
1718
+ if (!targets.has(ws)) targets.set(ws, /* @__PURE__ */ new Set());
1719
+ targets.get(ws).add(subscriptionKey);
1720
+ }
1721
+ }
1722
+ }
1723
+ for (const [ws, keys] of targets) {
1724
+ try {
1725
+ ws.send(JSON.stringify({ type: "invalidate", queries: [...keys] }));
1726
+ } catch {
1727
+ deregisterClient(ws);
1728
+ }
1729
+ }
1730
+ }
1587
1731
  function buildRealtimeCtx(options) {
1588
1732
  const pendingEmits = /* @__PURE__ */ new Map();
1589
1733
  const _pendingRefresh = [];
@@ -1616,6 +1760,15 @@ function buildRealtimeCtx(options) {
1616
1760
  }, 50);
1617
1761
  pendingEmits.set(queryKey, { data, timer });
1618
1762
  },
1763
+ invalidate(queryKey) {
1764
+ _hasEmitted = true;
1765
+ const queryKeys = Array.isArray(queryKey) ? [...new Set(queryKey)] : [queryKey];
1766
+ if (options?.httpCallback) {
1767
+ options.httpCallback({ type: "invalidate", queryKeys });
1768
+ return;
1769
+ }
1770
+ sendInvalidateToLocalSubscribers(queryKeys);
1771
+ },
1619
1772
  refresh(queryKey) {
1620
1773
  _hasEmitted = true;
1621
1774
  if (!_pendingRefresh.includes(queryKey)) {
@@ -2529,7 +2682,7 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = /* @__PURE__ */ new Set([
2529
2682
  "MIMALLOC_PURGE_DELAY",
2530
2683
  "NODE_PATH"
2531
2684
  ]);
2532
- var RESERVED_TENANT_RUNTIME_ENV_PREFIXES = ["__GENCOW_", "GENCOW_DOCUMENT_", "GENCOW_WARM_"];
2685
+ var RESERVED_TENANT_RUNTIME_ENV_PREFIXES = ["__GENCOW_", "GENCOW_DOCUMENT_", "GENCOW_TEMPLATE_", "GENCOW_WARM_"];
2533
2686
  function isReservedTenantRuntimeEnvKey(key) {
2534
2687
  const normalized = key.trim();
2535
2688
  return RESERVED_TENANT_RUNTIME_ENV_KEYS.has(normalized) || RESERVED_TENANT_RUNTIME_ENV_PREFIXES.some((prefix) => normalized.startsWith(prefix));
@@ -2596,6 +2749,11 @@ function defineAuth(config) {
2596
2749
  return config;
2597
2750
  }
2598
2751
 
2752
+ // ../core/src/config.ts
2753
+ function defineConfig(config) {
2754
+ return config;
2755
+ }
2756
+
2599
2757
  // ../core/src/rls.ts
2600
2758
  import { sql as sql3 } from "drizzle-orm";
2601
2759
  import { pgPolicy } from "drizzle-orm/pg-core";
@@ -3037,17 +3195,11 @@ function crud(table, options) {
3037
3195
  }
3038
3196
  return await fn(db);
3039
3197
  }
3040
- async function fetchListWithTotal(db, whereClause, userId) {
3041
- let effectiveWhere = whereClause;
3042
- if (ownerMeta && userId && !ownerMeta.readPublic) {
3043
- const ownerFilter = eq(ownerMeta.column, userId);
3044
- effectiveWhere = effectiveWhere ? and(effectiveWhere, ownerFilter) : ownerFilter;
3045
- }
3046
- return await inRlsOrPlainTx(db, async (tx) => {
3047
- const data = await tx.select().from(anyTable).where(effectiveWhere).orderBy(desc(defaultOrderCol));
3048
- const countResult = await tx.select({ count: drizzleCount() }).from(anyTable).where(effectiveWhere);
3049
- return { data, total: Number(countResult[0]?.count ?? 0) };
3050
- });
3198
+ function invalidateListRealtime(ctx) {
3199
+ ctx.realtime.invalidate(`${prefix}.list`);
3200
+ }
3201
+ function invalidateGetRealtime(ctx, id) {
3202
+ ctx.realtime.invalidate(buildQuerySubscriptionKey(`${prefix}.get`, { id }));
3051
3203
  }
3052
3204
  const enabledMethods = new Set(options?.methods ?? ["list", "get", "create", "update", "remove"]);
3053
3205
  const enabledMethodsList = Array.from(enabledMethods);
@@ -3141,9 +3293,7 @@ function crud(table, options) {
3141
3293
  async (tx) => tx.insert(anyTable).values(insertData).returning()
3142
3294
  );
3143
3295
  if (useRealtime && enabledMethods.has("list")) {
3144
- const currentUserId = user?.id;
3145
- const listResult = await fetchListWithTotal(ctx.db, void 0, currentUserId);
3146
- ctx.realtime.emit(`${prefix}.list`, listResult);
3296
+ invalidateListRealtime(ctx);
3147
3297
  }
3148
3298
  return result;
3149
3299
  }
@@ -3173,13 +3323,13 @@ function crud(table, options) {
3173
3323
  async (tx) => tx.update(anyTable).set(updateData).where(updateWhere).returning()
3174
3324
  );
3175
3325
  if (useRealtime) {
3176
- const currentUserId = ownerMeta ? user?.id : void 0;
3177
3326
  if (enabledMethods.has("list")) {
3178
- const listResult = await fetchListWithTotal(ctx.db, void 0, currentUserId);
3179
- ctx.realtime.emit(`${prefix}.list`, listResult);
3327
+ invalidateListRealtime(ctx);
3180
3328
  }
3181
3329
  if (enabledMethods.has("get")) {
3182
- ctx.realtime.emit(`${prefix}.get`, result);
3330
+ const getKey = buildQuerySubscriptionKey(`${prefix}.get`, { id });
3331
+ if (isPublic && !ownerMeta) ctx.realtime.emit(getKey, result);
3332
+ else invalidateGetRealtime(ctx, id);
3183
3333
  }
3184
3334
  }
3185
3335
  return result;
@@ -3202,9 +3352,7 @@ function crud(table, options) {
3202
3352
  }
3203
3353
  });
3204
3354
  if (useRealtime && enabledMethods.has("list")) {
3205
- const currentUserId = ownerMeta ? user?.id : void 0;
3206
- const listResult = await fetchListWithTotal(ctx.db, void 0, currentUserId);
3207
- ctx.realtime.emit(`${prefix}.list`, listResult);
3355
+ invalidateListRealtime(ctx);
3208
3356
  }
3209
3357
  return { success: true };
3210
3358
  }
@@ -3219,23 +3367,32 @@ function crud(table, options) {
3219
3367
  }
3220
3368
  export {
3221
3369
  DEFAULT_GROUNDING_BUDGET,
3370
+ DEFAULT_WAKE_RETRY_AFTER_SEC,
3222
3371
  DEFAULT_WORKFLOW_MAX_DURATION_MS,
3223
3372
  DEFAULT_WORKFLOW_MAX_RETRIES,
3224
3373
  GencowValidationError,
3374
+ PLATFORM_CAPACITY_ENV_KEYS,
3375
+ PLATFORM_CAPACITY_PRESETS,
3376
+ PLATFORM_CAPACITY_PROFILE_ENV,
3225
3377
  WORKFLOW_REALTIME_KEY_PREFIX,
3226
3378
  WORKFLOW_RESUME_ACTION_PREFIX,
3227
3379
  applyFilterOp,
3228
3380
  buildDocumentCacheKey,
3381
+ buildQuerySubscriptionKey,
3229
3382
  buildRealtimeCtx,
3383
+ buildWakeAppBootFailedResult,
3384
+ buildWakeAppSuccessResult,
3230
3385
  createRlsDb,
3231
3386
  createScheduler,
3232
3387
  createWorkflowRealtimeToken,
3233
3388
  cronJobs,
3234
3389
  crud,
3235
3390
  defineAuth,
3391
+ defineConfig,
3236
3392
  deregisterClient,
3237
3393
  deriveWorkflowStatus,
3238
3394
  deserializeWorkflowValue,
3395
+ detectPlatformCapacityProfile,
3239
3396
  filterTenantRuntimeEnvVars,
3240
3397
  crud as gencowCrud,
3241
3398
  getOwnerRlsMeta,
@@ -3254,6 +3411,7 @@ export {
3254
3411
  handleWsMessage,
3255
3412
  httpAction,
3256
3413
  isReservedTenantRuntimeEnvKey,
3414
+ isWakeAppDeferredResult,
3257
3415
  loadWorkflowSnapshot,
3258
3416
  mutation,
3259
3417
  ownerRls,
@@ -3269,8 +3427,10 @@ export {
3269
3427
  ragSources,
3270
3428
  registerClient,
3271
3429
  registerOwnerRls,
3430
+ resolvePlatformCapacityPreset,
3272
3431
  serializeWorkflowValue,
3273
3432
  subscribe,
3433
+ subscriptionKeyMatchesQueryKey,
3274
3434
  unsubscribe,
3275
3435
  v,
3276
3436
  withRetry,
@@ -15,6 +15,17 @@ export function formatAppDeployedAgo(dateStr, now = Date.now()) {
15
15
  return `${days}d ago`;
16
16
  }
17
17
 
18
+ export function addDeployAppToConfigSource(src, appName) {
19
+ if (src.includes("deploy:")) return src;
20
+
21
+ const deployBlock = ` deploy: {\n app: "${appName}",\n },\n`;
22
+ const defineConfigSource = src.replace(/}\s*\)\s*;?\s*$/, `${deployBlock}});\n`);
23
+ if (defineConfigSource !== src) return defineConfigSource;
24
+
25
+ const objectExportSource = src.replace(/}\s*;?\s*$/, `${deployBlock}};\n`);
26
+ return objectExportSource;
27
+ }
28
+
18
29
  async function confirmDelete(name) {
19
30
  const { createInterface } = await import("readline");
20
31
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -59,7 +70,9 @@ export function createAppCommand({ loadConfig }) {
59
70
  log(` ${"NAME".padEnd(22)} ${"STATUS".padEnd(12)} ${"URL".padEnd(38)} DEPLOYED`);
60
71
  log(` ${"-".repeat(85)}`);
61
72
  for (const appRow of apps) {
62
- const status = appRow.running ? `${GREEN}running${RESET}` : `${DIM}${appRow.status || "stopped"}${RESET}`;
73
+ const status = appRow.running
74
+ ? `${GREEN}running${RESET}`
75
+ : `${DIM}${appRow.status || "stopped"}${RESET}`;
63
76
  const url = appRow.url || `${DIM}—${RESET}`;
64
77
  const deployed = formatAppDeployedAgo(appRow.lastDeployedAt);
65
78
  log(
@@ -116,10 +129,10 @@ export function createAppCommand({ loadConfig }) {
116
129
 
117
130
  const configPath = resolve(cwd, "gencow.config.ts");
118
131
  if (existsSync(configPath)) {
119
- let src = readFileSync(configPath, "utf8");
120
- if (!src.includes("deploy:")) {
121
- src = src.replace(/}\s*;?\s*$/, ` deploy: {\n app: "${name}",\n },\n};\n`);
122
- writeFileSync(configPath, src);
132
+ const src = readFileSync(configPath, "utf8");
133
+ const nextSrc = addDeployAppToConfigSource(src, name);
134
+ if (nextSrc !== src) {
135
+ writeFileSync(configPath, nextSrc);
123
136
  success(`gencow.config.ts — added deploy.app = "${name}"`);
124
137
  }
125
138
  }
@@ -18,6 +18,13 @@ import { build } from "esbuild";
18
18
  import { existsSync, readdirSync, readFileSync } from "fs";
19
19
  import { basename, dirname, join, relative } from "path";
20
20
 
21
+ export {
22
+ DEPLOY_DEPENDENCY_AUDIT_MANIFEST,
23
+ auditDependencyVersions,
24
+ formatDependencyVersionAuditError,
25
+ getDependencyCompatibilityMatrix,
26
+ } from "./deploy-dependency-compat.mjs";
27
+
21
28
  // ── Platform packages (available in cloud runtime NODE_PATH) ──
22
29
  // These are installed in the platform's node_modules and accessible
23
30
  // via NODE_PATH at runtime. Keep in sync with server NODE_PATH setup.
@@ -7,7 +7,7 @@ export function renderDeployHelp() {
7
7
  log(` ${BOLD}Options:${RESET}`);
8
8
  log(` ${DIM}--prod${RESET} Deploy to production (Pro+ only)`);
9
9
  log(` ${DIM}--rollback${RESET} Rollback to previous deployment`);
10
- log(` ${DIM}--force, -f${RESET} Skip dependency audit`);
10
+ log(` ${DIM}--force, -f${RESET} Skip optional dependency scan`);
11
11
  log(` ${DIM}--app, -a${RESET} Target specific app\n`);
12
12
  log(` ${BOLD}Examples:${RESET}`);
13
13
  log(` gencow deploy`);
@@ -0,0 +1,191 @@
1
+ export const DEPLOY_DEPENDENCY_AUDIT_MANIFEST = ".gencow/deploy-dependency-audit.json";
2
+ export const DEPLOY_DEPENDENCY_MATRIX_VERSION = "2026-04-27";
3
+
4
+ export const DEPENDENCY_COMPAT_MATRIX = Object.freeze([
5
+ {
6
+ packageName: "@gencow/core",
7
+ expectedRange: "latest || workspace:* || ^0.1.28",
8
+ installRange: "@gencow/core@latest",
9
+ policy: "block",
10
+ },
11
+ {
12
+ packageName: "drizzle-orm",
13
+ expectedRange: "workspace:* || ^1.0.0-beta || ^1.0.0",
14
+ installRange: "drizzle-orm@^1.0.0",
15
+ policy: "block",
16
+ },
17
+ {
18
+ packageName: "hono",
19
+ expectedRange: "workspace:* || ^4.12.0",
20
+ installRange: "hono@^4.12.0",
21
+ policy: "block",
22
+ },
23
+ {
24
+ packageName: "better-auth",
25
+ expectedRange: "workspace:* || ^1.5.1",
26
+ installRange: "better-auth@^1.5.1",
27
+ policy: "block",
28
+ },
29
+ ]);
30
+
31
+ export function getDependencyCompatibilityMatrix() {
32
+ return DEPENDENCY_COMPAT_MATRIX.map((rule) => ({ ...rule }));
33
+ }
34
+
35
+ export function auditDependencyVersions(packageJson) {
36
+ const dependencies = collectHostRuntimeDependencies(packageJson);
37
+ const issues = [];
38
+
39
+ for (const rule of DEPENDENCY_COMPAT_MATRIX) {
40
+ const currentRange = dependencies[rule.packageName];
41
+ if (!currentRange) continue;
42
+ if (!isRangeCompatible(currentRange, rule.expectedRange)) {
43
+ issues.push({
44
+ packageName: rule.packageName,
45
+ currentRange,
46
+ expectedRange: rule.expectedRange,
47
+ installRange: rule.installRange,
48
+ policy: rule.policy,
49
+ });
50
+ }
51
+ }
52
+
53
+ return {
54
+ passed: issues.length === 0,
55
+ issues,
56
+ manifest: buildDependencyAuditManifest(dependencies),
57
+ };
58
+ }
59
+
60
+ export function buildDependencyAuditManifest(dependencies) {
61
+ return {
62
+ schemaVersion: 1,
63
+ generatedAt: new Date().toISOString(),
64
+ matrixVersion: DEPLOY_DEPENDENCY_MATRIX_VERSION,
65
+ dependencies: pickMatrixDependencies(dependencies),
66
+ };
67
+ }
68
+
69
+ export function formatDependencyVersionAuditError(result) {
70
+ if (result.passed) return "";
71
+
72
+ const installTargets = [...new Set(result.issues.map((issue) => issue.installRange))];
73
+ const lines = [
74
+ "",
75
+ "╔══════════════════════════════════════════════════════════════╗",
76
+ "║ ⛔ DEPLOY BLOCKED — Runtime dependency version mismatch ║",
77
+ "╚══════════════════════════════════════════════════════════════╝",
78
+ "",
79
+ " 앱의 핵심 런타임 dependency가 Gencow host runtime contract와 맞지 않습니다.",
80
+ " 배포 후 Warm Pool/runtime crash를 막기 위해 배포를 차단합니다.",
81
+ "",
82
+ ];
83
+
84
+ for (const issue of result.issues) {
85
+ lines.push(` ✗ ${issue.packageName}: ${issue.currentRange}`);
86
+ lines.push(` expected: ${issue.expectedRange}`);
87
+ }
88
+
89
+ lines.push("");
90
+ lines.push(" Fix:");
91
+ lines.push(` pnpm add ${installTargets.join(" ")}`);
92
+ lines.push(" npx gencow@latest deploy");
93
+ lines.push("");
94
+
95
+ return lines.join("\n");
96
+ }
97
+
98
+ function collectHostRuntimeDependencies(packageJson) {
99
+ const allDeps = {
100
+ ...(packageJson?.peerDependencies || {}),
101
+ ...(packageJson?.devDependencies || {}),
102
+ ...(packageJson?.dependencies || {}),
103
+ };
104
+ return pickMatrixDependencies(allDeps);
105
+ }
106
+
107
+ function pickMatrixDependencies(dependencies) {
108
+ const picked = {};
109
+ for (const rule of DEPENDENCY_COMPAT_MATRIX) {
110
+ const value = dependencies?.[rule.packageName];
111
+ if (typeof value === "string" && value.trim()) {
112
+ picked[rule.packageName] = value.trim();
113
+ }
114
+ }
115
+ return picked;
116
+ }
117
+
118
+ function isRangeCompatible(currentRange, expectedRange) {
119
+ const currentClauses = splitRange(currentRange);
120
+ const expectedClauses = splitRange(expectedRange);
121
+
122
+ for (const current of currentClauses) {
123
+ for (const expected of expectedClauses) {
124
+ if (clausesCompatible(current, expected)) return true;
125
+ }
126
+ }
127
+ return false;
128
+ }
129
+
130
+ function splitRange(range) {
131
+ return String(range)
132
+ .split("||")
133
+ .map((part) => part.trim())
134
+ .filter(Boolean);
135
+ }
136
+
137
+ function clausesCompatible(current, expected) {
138
+ if (isSpecialClause(current) || isSpecialClause(expected)) {
139
+ return current === expected || (current.startsWith("workspace:") && expected === "workspace:*");
140
+ }
141
+
142
+ const currentVersion = parseVersionClause(current);
143
+ const expectedVersion = parseVersionClause(expected);
144
+ if (!currentVersion || !expectedVersion) return false;
145
+
146
+ if (expectedVersion.major === 0) {
147
+ if (currentVersion.major !== 0 || currentVersion.minor !== expectedVersion.minor) return false;
148
+ if (currentVersion.operator === "exact") {
149
+ return compareVersions(currentVersion, expectedVersion) >= 0;
150
+ }
151
+ return true;
152
+ }
153
+
154
+ if (currentVersion.major !== expectedVersion.major) return false;
155
+ return compareVersions(currentVersion, expectedVersion) >= 0;
156
+ }
157
+
158
+ function isSpecialClause(value) {
159
+ return value === "latest" || value.startsWith("workspace:");
160
+ }
161
+
162
+ function parseVersionClause(clause) {
163
+ const trimmed = clause.trim();
164
+ const operator = trimmed.startsWith("^")
165
+ ? "^"
166
+ : trimmed.startsWith("~")
167
+ ? "~"
168
+ : trimmed.startsWith(">=")
169
+ ? ">="
170
+ : "exact";
171
+ const match = trimmed.match(/(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
172
+ if (!match) return null;
173
+ return {
174
+ operator,
175
+ major: Number(match[1]),
176
+ minor: Number(match[2]),
177
+ patch: Number(match[3]),
178
+ prerelease: match[4] || "",
179
+ };
180
+ }
181
+
182
+ function compareVersions(a, b) {
183
+ for (const key of ["major", "minor", "patch"]) {
184
+ if (a[key] > b[key]) return 1;
185
+ if (a[key] < b[key]) return -1;
186
+ }
187
+ if (a.prerelease === b.prerelease) return 0;
188
+ if (!a.prerelease) return 1;
189
+ if (!b.prerelease) return -1;
190
+ return a.prerelease > b.prerelease ? 1 : -1;
191
+ }
@@ -119,6 +119,41 @@ export function createDeployPackageRuntime({
119
119
 
120
120
  await runWorkflowShadowingAudit();
121
121
 
122
+ let dependencyAuditManifestPath = null;
123
+ let dependencyAuditManifest = null;
124
+ let dependencyVersionMessage = "";
125
+ try {
126
+ const {
127
+ DEPLOY_DEPENDENCY_AUDIT_MANIFEST,
128
+ auditDependencyVersions,
129
+ formatDependencyVersionAuditError,
130
+ } = await import("./deploy-auditor.mjs");
131
+ const projectPkg = JSON.parse(readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8"));
132
+ const dependencyVersionResult = auditDependencyVersions(projectPkg);
133
+ dependencyVersionMessage = formatDependencyVersionAuditError(dependencyVersionResult);
134
+ dependencyAuditManifestPath = resolvePathImpl(cwd, DEPLOY_DEPENDENCY_AUDIT_MANIFEST);
135
+ dependencyAuditManifest = dependencyVersionResult.manifest;
136
+ } catch (caught) {
137
+ errorImpl(`Dependency version audit failed: ${caught.message}`);
138
+ exitImpl(1);
139
+ return null;
140
+ }
141
+
142
+ if (dependencyVersionMessage) {
143
+ errorImpl(dependencyVersionMessage);
144
+ exitImpl(1);
145
+ return null;
146
+ }
147
+
148
+ try {
149
+ mkdirSyncImpl(dirname(dependencyAuditManifestPath), { recursive: true });
150
+ writeFileSyncImpl(dependencyAuditManifestPath, JSON.stringify(dependencyAuditManifest, null, 2));
151
+ } catch (caught) {
152
+ errorImpl(`Dependency audit manifest write failed: ${caught.message}`);
153
+ exitImpl(1);
154
+ return null;
155
+ }
156
+
122
157
  let auditResult = null;
123
158
  if (!forceDeploy) {
124
159
  try {
@@ -136,6 +171,7 @@ export function createDeployPackageRuntime({
136
171
 
137
172
  let useFilteredPkg = false;
138
173
  const filesToPack = ["gencow/", "package.json"];
174
+ if (dependencyAuditManifestPath) filesToPack.push(".gencow/deploy-dependency-audit.json");
139
175
  if (existsSyncImpl(resolvePathImpl(cwd, "bun.lockb"))) filesToPack.push("bun.lockb");
140
176
  if (existsSyncImpl(resolvePathImpl(cwd, "package-lock.json"))) filesToPack.push("package-lock.json");
141
177
  if (existsSyncImpl(resolvePathImpl(cwd, "tsconfig.json"))) filesToPack.push("tsconfig.json");
@@ -176,6 +212,7 @@ export function createDeployPackageRuntime({
176
212
  if (file.endsWith("/")) {
177
213
  execSyncImpl(`cp -r "${src}" "${dst}"`, { cwd });
178
214
  } else if (existsSyncImpl(src)) {
215
+ mkdirSyncImpl(dirname(dst), { recursive: true });
179
216
  execSyncImpl(`cp "${src}" "${dst}"`, { cwd });
180
217
  }
181
218
  }
@@ -69,7 +69,7 @@ function showUnknownDeployArgument(arg, { errorImpl = error, infoImpl = info, lo
69
69
  infoImpl(" gencow deploy --rollback Rollback to previous version");
70
70
  infoImpl(" gencow deploy logs View server logs");
71
71
  infoImpl(" gencow deploy status Check app status");
72
- infoImpl(" gencow deploy --force Skip dependency audit");
72
+ infoImpl(" gencow deploy --force Skip optional dependency scan");
73
73
  logImpl("");
74
74
  infoImpl("Static files:");
75
75
  infoImpl(" gencow static [dir] Deploy static files (dev)");
@@ -80,16 +80,18 @@ const crons = cronJobs();
80
80
  export default crons;
81
81
  `;
82
82
 
83
- const GENCOW_CONFIG_TEMPLATE = `export default {
83
+ const GENCOW_CONFIG_TEMPLATE = `import { defineConfig } from "@gencow/core";
84
+
85
+ export default defineConfig({
84
86
  functionsDir: "./gencow",
85
87
  schema: "./gencow/schema.ts",
86
88
  // Optional: where generated frontend codegen artifacts are written.
87
- // If omitted, CLI uses src/gencow when src/ exists, otherwise functionsDir.
89
+ // Default: "./src/gencow".
88
90
  codegenOutDir: "./src/gencow",
89
91
  storage: "./.gencow/uploads",
90
92
  db: { url: "./.gencow/data" },
91
93
  port: 5456,
92
- };
94
+ });
93
95
  `;
94
96
 
95
97
  const DRIZZLE_CONFIG_TEMPLATE = `export default {