gencow 0.1.139 → 0.1.141
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 +2 -2
- package/core/index.js +182 -22
- package/lib/app-command.mjs +18 -5
- package/lib/deploy-auditor.mjs +7 -0
- package/lib/deploy-command.mjs +1 -1
- package/lib/deploy-dependency-compat.mjs +191 -0
- package/lib/deploy-package-runtime.mjs +37 -0
- package/lib/deploy-runtime.mjs +1 -1
- package/lib/init-command.mjs +5 -3
- package/lib/static-command.mjs +1 -1
- package/lib/static-deploy-command.mjs +2 -1
- package/package.json +3 -1
- package/server/index.js +1042 -395
- package/server/index.js.map +4 -4
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
|
|
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
|
|
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
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3179
|
-
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
3327
|
+
invalidateListRealtime(ctx);
|
|
3180
3328
|
}
|
|
3181
3329
|
if (enabledMethods.has("get")) {
|
|
3182
|
-
|
|
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
|
-
|
|
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,
|
package/lib/app-command.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
writeFileSync(configPath,
|
|
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
|
}
|
package/lib/deploy-auditor.mjs
CHANGED
|
@@ -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.
|
package/lib/deploy-command.mjs
CHANGED
|
@@ -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
|
|
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
|
}
|
package/lib/deploy-runtime.mjs
CHANGED
|
@@ -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
|
|
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)");
|
package/lib/init-command.mjs
CHANGED
|
@@ -80,16 +80,18 @@ const crons = cronJobs();
|
|
|
80
80
|
export default crons;
|
|
81
81
|
`;
|
|
82
82
|
|
|
83
|
-
const GENCOW_CONFIG_TEMPLATE = `
|
|
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
|
-
//
|
|
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 {
|