gencow 0.1.146 → 0.1.148

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
@@ -39,6 +39,7 @@ import {
39
39
  updateEnvLocalUrl,
40
40
  } from "../lib/cli-project-runtime.mjs";
41
41
  import { createCodegenCommand } from "../lib/codegen-command.mjs";
42
+ import { maybeCheckCliVersion } from "../lib/cli-version-check.mjs";
42
43
  import { updateComponentReadme } from "../lib/component-readme.mjs";
43
44
  import { createConfigCommand } from "../lib/config-command.mjs";
44
45
  import { createCorsCommand } from "../lib/cors-command.mjs";
@@ -81,6 +82,8 @@ import {
81
82
  import { platformFetch, requireCreds, rpcMutation, rpcQuery } from "../lib/platform-client.mjs";
82
83
 
83
84
  const __dirname = dirname(fileURLToPath(import.meta.url));
85
+ const CLI_PACKAGE_JSON = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf8"));
86
+ const CLI_VERSION = CLI_PACKAGE_JSON.version || "0.0.0";
84
87
  const _drizzleKitCmd = (subcmd, options = {}) => buildDrizzleKitCommand(subcmd, options);
85
88
  const generateApiTs = createApiCodegenRuntime({});
86
89
  const runAddCommand = createAddCommand({
@@ -282,6 +285,7 @@ const commands = {
282
285
  help() {
283
286
  log(`
284
287
  ${BOLD}${CYAN}Gencow CLI${RESET} ${DIM}— AI Backend Engine${RESET}
288
+ ${DIM}Version: ${CLI_VERSION}${RESET}
285
289
 
286
290
  ${BOLD}Usage:${RESET}
287
291
  gencow <command>
@@ -382,6 +386,10 @@ ${BOLD}Examples:${RESET}
382
386
  `);
383
387
  },
384
388
 
389
+ version() {
390
+ log(CLI_VERSION);
391
+ },
392
+
385
393
  // ── login/logout/whoami ───────────────────────────────
386
394
  login: runLoginCommand,
387
395
  logout: runLogoutCommand,
@@ -455,7 +463,15 @@ ${BOLD}Examples:${RESET}
455
463
 
456
464
  // ─── updateReadme: gencow/ 폴더 스캔 → README에 컴포넌트 문서 자동 추가 ────
457
465
 
458
- const [, , cmd = "help", ...args] = process.argv;
466
+ const [, , rawCmd = "help", ...args] = process.argv;
467
+ const cmd = rawCmd === "--help" || rawCmd === "-h" ? "help" : rawCmd === "--version" || rawCmd === "-v" ? "version" : rawCmd;
468
+ maybeCheckCliVersion({
469
+ command: cmd,
470
+ currentVersion: CLI_VERSION,
471
+ errorImpl: error,
472
+ processEnv: process.env,
473
+ warnImpl: warn,
474
+ });
459
475
 
460
476
  if (commands[cmd]) {
461
477
  Promise.resolve(commands[cmd](...args)).catch((e) => {
package/core/index.js CHANGED
@@ -3258,49 +3258,57 @@ async function withRlsConnection(session, rls, reuseOuterConnection, fn) {
3258
3258
  }
3259
3259
  function wrapPreparedQuery(pq, session, rls, reuseOuterConnection) {
3260
3260
  const origExecute = pq.execute.bind(pq);
3261
- const origAll = pq.all.bind(pq);
3261
+ const origAll = typeof pq.all === "function" ? pq.all.bind(pq) : null;
3262
+ const swapClient = (client) => {
3263
+ const prevPq = pq.client;
3264
+ const prevSession = session.client;
3265
+ pq.client = client;
3266
+ session.client = client;
3267
+ return () => {
3268
+ pq.client = prevPq;
3269
+ session.client = prevSession;
3270
+ };
3271
+ };
3262
3272
  pq.execute = async (placeholderValues) => {
3263
3273
  const active = rlsExecClient.getStore();
3264
3274
  if (active) {
3265
- const prev = pq.client;
3266
- pq.client = active;
3275
+ const restore = swapClient(active);
3267
3276
  try {
3268
3277
  return await origExecute(placeholderValues);
3269
3278
  } finally {
3270
- pq.client = prev;
3279
+ restore();
3271
3280
  }
3272
3281
  }
3273
3282
  return withRlsConnection(session, rls, reuseOuterConnection, async (client) => {
3274
- const prev = pq.client;
3275
- pq.client = client;
3283
+ const restore = swapClient(client);
3276
3284
  try {
3277
3285
  return await origExecute(placeholderValues);
3278
3286
  } finally {
3279
- pq.client = prev;
3287
+ restore();
3280
3288
  }
3281
3289
  });
3282
3290
  };
3283
- pq.all = async (placeholderValues) => {
3284
- const active = rlsExecClient.getStore();
3285
- if (active) {
3286
- const prev = pq.client;
3287
- pq.client = active;
3288
- try {
3289
- return await origAll(placeholderValues);
3290
- } finally {
3291
- pq.client = prev;
3292
- }
3293
- }
3294
- return withRlsConnection(session, rls, reuseOuterConnection, async (client) => {
3295
- const prev = pq.client;
3296
- pq.client = client;
3297
- try {
3298
- return await origAll(placeholderValues);
3299
- } finally {
3300
- pq.client = prev;
3291
+ if (origAll) {
3292
+ pq.all = async (placeholderValues) => {
3293
+ const active = rlsExecClient.getStore();
3294
+ if (active) {
3295
+ const restore = swapClient(active);
3296
+ try {
3297
+ return await origAll(placeholderValues);
3298
+ } finally {
3299
+ restore();
3300
+ }
3301
3301
  }
3302
- });
3303
- };
3302
+ return withRlsConnection(session, rls, reuseOuterConnection, async (client) => {
3303
+ const restore = swapClient(client);
3304
+ try {
3305
+ return await origAll(placeholderValues);
3306
+ } finally {
3307
+ restore();
3308
+ }
3309
+ });
3310
+ };
3311
+ }
3304
3312
  }
3305
3313
  function wrapSession(session, rls, reuseOuterConnection) {
3306
3314
  return new Proxy(session, {
@@ -5,7 +5,7 @@ import { dirname, relative, resolve } from "path";
5
5
  export const ADD_COMPONENTS = {
6
6
  ai: {
7
7
  label: "AI Engine",
8
- deps: ["ai", "@ai-sdk/openai"],
8
+ deps: ["ai", "@ai-sdk/openai", "@ai-sdk/provider-utils"],
9
9
  files: [{ src: "ai.ts", dest: "gencow/ai.ts" }],
10
10
  env: { OPENAI_API_KEY: "" },
11
11
  requires: [],
@@ -0,0 +1,70 @@
1
+ export const REQUIRED_CLI_TARBALL_FILES = Object.freeze([
2
+ "bin/gencow.mjs",
3
+ "bin/gencow-mcp.mjs",
4
+ "lib/readme-codegen.mjs",
5
+ "lib/deploy-auditor.mjs",
6
+ "lib/cron-manifest.mjs",
7
+ "lib/dev-cloud-bundle.mjs",
8
+ "lib/codegen/index.mjs",
9
+ "server/index.js",
10
+ "core/index.js",
11
+ "dashboard/index.html",
12
+ ]);
13
+
14
+ const FORBIDDEN_CODEGEN_CORE_IMPORTS = Object.freeze([
15
+ {
16
+ id: "server-only-procedure-route-collector",
17
+ pattern: /(?:import\s*{[^}]*\bcollectProcedureRouteMaps\b[^}]*}\s*from\s*["']@gencow\/core["']|collectProcedureRouteMaps[\s\S]{0,240}from\s*["']@gencow\/core["'])/,
18
+ message: "@gencow/core에서 server-only procedure route collector를 import합니다",
19
+ },
20
+ ]);
21
+
22
+ export function validateCliTarballFiles(files, requiredFiles = REQUIRED_CLI_TARBALL_FILES) {
23
+ const fileSet = new Set(files);
24
+ const errors = [];
25
+ const forbiddenSourceFiles = files.filter((path) => path === "src" || path.startsWith("src/"));
26
+
27
+ for (const path of forbiddenSourceFiles) {
28
+ errors.push({
29
+ code: "forbidden-source-file",
30
+ path,
31
+ message: "packages/cli/src 는 npm tarball에 포함되면 안 됩니다",
32
+ });
33
+ }
34
+
35
+ for (const path of requiredFiles) {
36
+ if (!fileSet.has(path)) {
37
+ errors.push({
38
+ code: "missing-required-file",
39
+ path,
40
+ message: "필수 CLI artifact가 npm tarball에 없습니다",
41
+ });
42
+ }
43
+ }
44
+
45
+ return {
46
+ ok: errors.length === 0,
47
+ errors,
48
+ };
49
+ }
50
+
51
+ export function validateGeneratedCodegenBundle(source) {
52
+ const errors = [];
53
+ for (const rule of FORBIDDEN_CODEGEN_CORE_IMPORTS) {
54
+ if (rule.pattern.test(source)) {
55
+ errors.push({
56
+ code: rule.id,
57
+ path: "lib/codegen/index.mjs",
58
+ message: rule.message,
59
+ });
60
+ }
61
+ }
62
+ return {
63
+ ok: errors.length === 0,
64
+ errors,
65
+ };
66
+ }
67
+
68
+ export function formatArtifactGuardErrors(errors) {
69
+ return errors.map((error) => ` ❌ ${error.path} — ${error.message}`).join("\n");
70
+ }
@@ -8,7 +8,6 @@ import {
8
8
  resolveCodegenConfig,
9
9
  resolveCodegenPublishDir,
10
10
  } from "./codegen-command.mjs";
11
- import { generateAuthSchema } from "./codegen/index.mjs";
12
11
  import { updateComponentReadme } from "./component-readme.mjs";
13
12
  import { buildReadmeMarkdown, extractComponentsBlock } from "./readme-codegen.mjs";
14
13
  import { CYAN, DIM, RED, RESET, YELLOW, error, info, log, success, warn } from "./output.mjs";
@@ -146,7 +145,7 @@ export function createApiCodegenRuntime(options = {}) {
146
145
  resolvePathImpl = resolve,
147
146
  successImpl = success,
148
147
  updateComponentReadmeImpl = updateComponentReadme,
149
- generateAuthSchemaImpl = generateAuthSchema,
148
+ generateAuthSchemaImpl,
150
149
  warnImpl = warn,
151
150
  writeFileSyncImpl = writeFileSync,
152
151
  } = options;
@@ -176,6 +175,7 @@ export function createApiCodegenRuntime(options = {}) {
176
175
  if (typeof bundled.createNodeProcessRunner !== "function") {
177
176
  throw new Error("Invalid codegen bundle: createNodeProcessRunner export not found");
178
177
  }
178
+ const generateAuthSchema = generateAuthSchemaImpl ?? bundled.generateAuthSchema;
179
179
 
180
180
  const result = await bundled.runCodegen({
181
181
  projectRoot: cwd,
@@ -185,7 +185,7 @@ export function createApiCodegenRuntime(options = {}) {
185
185
  finalOutDir: publishDir,
186
186
  authSchema: {
187
187
  enabled: resolveCodegenConfig(config).authSchema,
188
- generate: generateAuthSchemaImpl,
188
+ generate: generateAuthSchema,
189
189
  },
190
190
  });
191
191
  if (result.authSchema?.changed) {
@@ -0,0 +1,146 @@
1
+ import { execFileSync } from "child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { dirname, resolve } from "path";
5
+
6
+ const DEFAULT_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
7
+ const DEFAULT_NPM_TIMEOUT_MS = 1500;
8
+
9
+ function isEnabled(value) {
10
+ return value === "1" || value === "true" || value === "yes";
11
+ }
12
+
13
+ export function parseCliSemver(version) {
14
+ const match = String(version ?? "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
15
+ if (!match) return null;
16
+ return {
17
+ major: Number(match[1]),
18
+ minor: Number(match[2]),
19
+ patch: Number(match[3]),
20
+ };
21
+ }
22
+
23
+ export function compareCliSemver(left, right) {
24
+ const a = typeof left === "string" ? parseCliSemver(left) : left;
25
+ const b = typeof right === "string" ? parseCliSemver(right) : right;
26
+ if (!a || !b) return 0;
27
+ for (const key of ["major", "minor", "patch"]) {
28
+ if (a[key] > b[key]) return 1;
29
+ if (a[key] < b[key]) return -1;
30
+ }
31
+ return 0;
32
+ }
33
+
34
+ export function shouldWarnForStaleCli({ currentVersion, latestVersion }) {
35
+ return compareCliSemver(latestVersion, currentVersion) > 0;
36
+ }
37
+
38
+ export function resolveVersionCheckCachePath({ homeDir = homedir(), processEnv = process.env } = {}) {
39
+ return processEnv.GENCOW_VERSION_CHECK_CACHE || resolve(homeDir, ".gencow", "version-check.json");
40
+ }
41
+
42
+ function readCache(cachePath, { existsSyncImpl = existsSync, readFileSyncImpl = readFileSync } = {}) {
43
+ try {
44
+ if (!existsSyncImpl(cachePath)) return {};
45
+ return JSON.parse(readFileSyncImpl(cachePath, "utf8"));
46
+ } catch {
47
+ return {};
48
+ }
49
+ }
50
+
51
+ function writeCache(cachePath, cache, { mkdirSyncImpl = mkdirSync, writeFileSyncImpl = writeFileSync } = {}) {
52
+ try {
53
+ mkdirSyncImpl(dirname(cachePath), { recursive: true });
54
+ writeFileSyncImpl(cachePath, JSON.stringify(cache, null, 2) + "\n");
55
+ } catch {
56
+ /* version check cache must never break CLI commands */
57
+ }
58
+ }
59
+
60
+ function isFresh(timestamp, now, maxAgeMs) {
61
+ return typeof timestamp === "number" && now - timestamp >= 0 && now - timestamp < maxAgeMs;
62
+ }
63
+
64
+ function fetchLatestVersion({ execFileSyncImpl = execFileSync, timeoutMs = DEFAULT_NPM_TIMEOUT_MS } = {}) {
65
+ return String(
66
+ execFileSyncImpl("npm", ["view", "gencow", "version", "--silent"], {
67
+ encoding: "utf8",
68
+ stdio: ["ignore", "pipe", "ignore"],
69
+ timeout: timeoutMs,
70
+ }),
71
+ ).trim();
72
+ }
73
+
74
+ export function formatStaleCliWarning({ currentVersion, latestVersion, command }) {
75
+ const suffix = command ? ` ${command}` : "";
76
+ return [
77
+ `Gencow CLI is older than npm latest (current ${currentVersion}, latest ${latestVersion}).`,
78
+ `Use project-local/latest CLI for verification: bunx gencow@latest${suffix}`,
79
+ "If this came from a global install, check: which gencow && gencow --version",
80
+ "Skip this check with GENCOW_SKIP_VERSION_CHECK=true.",
81
+ ].join("\n");
82
+ }
83
+
84
+ export function maybeCheckCliVersion({
85
+ cacheMaxAgeMs = DEFAULT_CACHE_MAX_AGE_MS,
86
+ command,
87
+ currentVersion,
88
+ errorImpl = console.error,
89
+ execFileSyncImpl = execFileSync,
90
+ exitImpl = (code) => process.exit(code),
91
+ existsSyncImpl = existsSync,
92
+ homeDirImpl = homedir,
93
+ mkdirSyncImpl = mkdirSync,
94
+ nowImpl = () => Date.now(),
95
+ processEnv = process.env,
96
+ readFileSyncImpl = readFileSync,
97
+ warnImpl = console.warn,
98
+ writeFileSyncImpl = writeFileSync,
99
+ } = {}) {
100
+ if (isEnabled(processEnv.GENCOW_SKIP_VERSION_CHECK)) {
101
+ return { status: "skipped" };
102
+ }
103
+ if (command === "help" || command === "version") {
104
+ return { status: "skipped" };
105
+ }
106
+
107
+ const requireLatest = isEnabled(processEnv.GENCOW_REQUIRE_LATEST_CLI);
108
+ const now = nowImpl();
109
+ const cachePath = resolveVersionCheckCachePath({ homeDir: homeDirImpl(), processEnv });
110
+ const cache = readCache(cachePath, { existsSyncImpl, readFileSyncImpl });
111
+ let latestVersion = cache.latestVersion;
112
+
113
+ if (!latestVersion || !isFresh(cache.checkedAt, now, cacheMaxAgeMs)) {
114
+ try {
115
+ latestVersion = fetchLatestVersion({ execFileSyncImpl });
116
+ cache.latestVersion = latestVersion;
117
+ cache.checkedAt = now;
118
+ writeCache(cachePath, cache, { mkdirSyncImpl, writeFileSyncImpl });
119
+ } catch (caught) {
120
+ return { status: "unverified", error: caught };
121
+ }
122
+ }
123
+
124
+ if (!shouldWarnForStaleCli({ currentVersion, latestVersion })) {
125
+ return { status: "fresh", latestVersion };
126
+ }
127
+
128
+ const message = formatStaleCliWarning({ currentVersion, latestVersion, command });
129
+ if (requireLatest) {
130
+ errorImpl(message);
131
+ exitImpl(1);
132
+ return { status: "blocked", latestVersion };
133
+ }
134
+
135
+ const warnedRecently =
136
+ cache.warnedForVersion === latestVersion && isFresh(cache.warnedAt, now, cacheMaxAgeMs);
137
+ if (!warnedRecently) {
138
+ warnImpl(message);
139
+ cache.warnedAt = now;
140
+ cache.warnedForVersion = latestVersion;
141
+ writeCache(cachePath, cache, { mkdirSyncImpl, writeFileSyncImpl });
142
+ return { status: "warned", latestVersion };
143
+ }
144
+
145
+ return { status: "stale-suppressed", latestVersion };
146
+ }
@@ -6557,49 +6557,57 @@ async function withRlsConnection(session, rls, reuseOuterConnection, fn) {
6557
6557
  }
6558
6558
  function wrapPreparedQuery(pq, session, rls, reuseOuterConnection) {
6559
6559
  const origExecute = pq.execute.bind(pq);
6560
- const origAll = pq.all.bind(pq);
6560
+ const origAll = typeof pq.all === "function" ? pq.all.bind(pq) : null;
6561
+ const swapClient = (client) => {
6562
+ const prevPq = pq.client;
6563
+ const prevSession = session.client;
6564
+ pq.client = client;
6565
+ session.client = client;
6566
+ return () => {
6567
+ pq.client = prevPq;
6568
+ session.client = prevSession;
6569
+ };
6570
+ };
6561
6571
  pq.execute = async (placeholderValues) => {
6562
6572
  const active = rlsExecClient.getStore();
6563
6573
  if (active) {
6564
- const prev = pq.client;
6565
- pq.client = active;
6574
+ const restore = swapClient(active);
6566
6575
  try {
6567
6576
  return await origExecute(placeholderValues);
6568
6577
  } finally {
6569
- pq.client = prev;
6578
+ restore();
6570
6579
  }
6571
6580
  }
6572
6581
  return withRlsConnection(session, rls, reuseOuterConnection, async (client) => {
6573
- const prev = pq.client;
6574
- pq.client = client;
6582
+ const restore = swapClient(client);
6575
6583
  try {
6576
6584
  return await origExecute(placeholderValues);
6577
6585
  } finally {
6578
- pq.client = prev;
6586
+ restore();
6579
6587
  }
6580
6588
  });
6581
6589
  };
6582
- pq.all = async (placeholderValues) => {
6583
- const active = rlsExecClient.getStore();
6584
- if (active) {
6585
- const prev = pq.client;
6586
- pq.client = active;
6587
- try {
6588
- return await origAll(placeholderValues);
6589
- } finally {
6590
- pq.client = prev;
6591
- }
6592
- }
6593
- return withRlsConnection(session, rls, reuseOuterConnection, async (client) => {
6594
- const prev = pq.client;
6595
- pq.client = client;
6596
- try {
6597
- return await origAll(placeholderValues);
6598
- } finally {
6599
- pq.client = prev;
6590
+ if (origAll) {
6591
+ pq.all = async (placeholderValues) => {
6592
+ const active = rlsExecClient.getStore();
6593
+ if (active) {
6594
+ const restore = swapClient(active);
6595
+ try {
6596
+ return await origAll(placeholderValues);
6597
+ } finally {
6598
+ restore();
6599
+ }
6600
6600
  }
6601
- });
6602
- };
6601
+ return withRlsConnection(session, rls, reuseOuterConnection, async (client) => {
6602
+ const restore = swapClient(client);
6603
+ try {
6604
+ return await origAll(placeholderValues);
6605
+ } finally {
6606
+ restore();
6607
+ }
6608
+ });
6609
+ };
6610
+ }
6603
6611
  }
6604
6612
  function wrapSession(session, rls, reuseOuterConnection) {
6605
6613
  return new Proxy(session, {
@@ -1,6 +1,5 @@
1
1
  import { existsSync } from "fs";
2
2
  import { resolve, relative } from "path";
3
- import { generateAuthSchema } from "./codegen/index.mjs";
4
3
 
5
4
  export function resolveCodegenConfig(config = {}) {
6
5
  const outDir =
@@ -85,6 +84,7 @@ export function createCodegenCommand(deps) {
85
84
  if (typeof bundled.runCodegen !== "function") {
86
85
  throw new Error("Invalid codegen bundle: runCodegen export not found");
87
86
  }
87
+ const generateAuthSchema = deps.generateAuthSchemaImpl ?? bundled.generateAuthSchema;
88
88
  const result = await bundled.runCodegen({
89
89
  projectRoot: cwd,
90
90
  functionsDir: config.functionsDir,
@@ -93,7 +93,7 @@ export function createCodegenCommand(deps) {
93
93
  finalOutDir: publishDir,
94
94
  authSchema: {
95
95
  enabled: codegen.authSchema,
96
- generate: deps.generateAuthSchemaImpl ?? generateAuthSchema,
96
+ generate: generateAuthSchema,
97
97
  },
98
98
  });
99
99
  if (result.authSchema?.changed) {
@@ -0,0 +1,142 @@
1
+ import { createHash } from "crypto";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
3
+ import { dirname, resolve } from "path";
4
+ import { pathToFileURL } from "url";
5
+
6
+ export const CRON_MANIFEST_RELATIVE_PATH = ".gencow/cron-jobs.json";
7
+
8
+ function resolveCronDefinitionsFile({ cwd, functionsDir, existsSyncImpl, resolvePathImpl }) {
9
+ const cronsTs = resolvePathImpl(cwd, functionsDir, "crons.ts");
10
+ const cronsJs = resolvePathImpl(cwd, functionsDir, "crons.js");
11
+ if (existsSyncImpl(cronsTs)) return cronsTs;
12
+ if (existsSyncImpl(cronsJs)) return cronsJs;
13
+ return null;
14
+ }
15
+
16
+ function readNonEmptyString(value, field) {
17
+ if (typeof value !== "string" || value.trim() === "") {
18
+ throw new Error(`cron job ${field} must be a non-empty string`);
19
+ }
20
+ return value;
21
+ }
22
+
23
+ function normalizeCronJobs(rawJobs) {
24
+ if (!Array.isArray(rawJobs)) throw new Error("crons.getJobs() must return an array");
25
+
26
+ const jobs = [];
27
+ const skipped = [];
28
+ rawJobs.forEach((job, index) => {
29
+ if (!job || typeof job !== "object" || Array.isArray(job)) {
30
+ throw new Error(`cron job ${index} must be an object`);
31
+ }
32
+ if (typeof job.action !== "string") {
33
+ skipped.push({
34
+ name: typeof job.name === "string" && job.name.trim() ? job.name : `jobs[${index}]`,
35
+ reason: "inline action is not supported in cloud cron manifest",
36
+ });
37
+ return;
38
+ }
39
+ jobs.push({
40
+ name: readNonEmptyString(job.name, `${index}.name`),
41
+ pattern: readNonEmptyString(job.pattern, `${index}.pattern`),
42
+ action: readNonEmptyString(job.action, `${index}.action`),
43
+ });
44
+ });
45
+
46
+ return { jobs, skipped, skippedInlineCount: skipped.length };
47
+ }
48
+
49
+ function writeFallbackCoreShim(coreTarget, mkdirSyncImpl, writeFileSyncImpl) {
50
+ mkdirSyncImpl(coreTarget, { recursive: true });
51
+ writeFileSyncImpl(
52
+ resolve(coreTarget, "package.json"),
53
+ JSON.stringify({ name: "@gencow/core", type: "module", main: "index.js" }, null, 2),
54
+ );
55
+ writeFileSyncImpl(
56
+ resolve(coreTarget, "index.js"),
57
+ [
58
+ "export function cronJobs() {",
59
+ " const jobs = [];",
60
+ " const builder = {",
61
+ " interval(name, options, action) {",
62
+ " if (options?.seconds) jobs.push({ name, pattern: `*/${options.seconds} * * * * *`, action });",
63
+ " else if (options?.minutes) jobs.push({ name, pattern: `*/${options.minutes} * * * *`, action });",
64
+ " else if (options?.hours) jobs.push({ name, pattern: `0 */${options.hours} * * *`, action });",
65
+ " else throw new Error(\"interval cron requires seconds, minutes, or hours\");",
66
+ " return builder;",
67
+ " },",
68
+ " daily(name, options, action) { jobs.push({ name, pattern: `${options.minute ?? 0} ${options.hour} * * *`, action }); return builder; },",
69
+ " weekly(name, options, action) { jobs.push({ name, pattern: `${options.minute ?? 0} ${options.hour} * * ${options.dayOfWeek}`, action }); return builder; },",
70
+ " cron(name, pattern, action) { jobs.push({ name, pattern, action }); return builder; },",
71
+ " getJobs() { return [...jobs]; },",
72
+ " };",
73
+ " return builder;",
74
+ "}",
75
+ "",
76
+ ].join("\n"),
77
+ );
78
+ }
79
+
80
+ function installCronCoreShim({ workDir, mkdirSyncImpl, resolvePathImpl, writeFileSyncImpl }) {
81
+ const coreTarget = resolvePathImpl(workDir, "node_modules", "@gencow", "core");
82
+ mkdirSyncImpl(dirname(coreTarget), { recursive: true });
83
+ writeFallbackCoreShim(coreTarget, mkdirSyncImpl, writeFileSyncImpl);
84
+ }
85
+
86
+ export async function writeCronManifestForProject({
87
+ cwd,
88
+ functionsDir = "gencow",
89
+ cpSyncImpl = cpSync,
90
+ existsSyncImpl = existsSync,
91
+ mkdirSyncImpl = mkdirSync,
92
+ readFileSyncImpl = readFileSync,
93
+ resolvePathImpl = resolve,
94
+ rmSyncImpl = rmSync,
95
+ writeFileSyncImpl = writeFileSync,
96
+ } = {}) {
97
+ if (!cwd) throw new Error("cwd is required to write cron manifest");
98
+
99
+ const cronsFile = resolveCronDefinitionsFile({ cwd, functionsDir, existsSyncImpl, resolvePathImpl });
100
+ const manifestPath = resolvePathImpl(cwd, CRON_MANIFEST_RELATIVE_PATH);
101
+ if (!cronsFile) {
102
+ rmSyncImpl(manifestPath, { force: true });
103
+ return null;
104
+ }
105
+
106
+ const workDir = resolvePathImpl(cwd, ".gencow", "cron-manifest-workdir");
107
+ const stagedFunctionsDir = resolvePathImpl(workDir, functionsDir);
108
+ rmSyncImpl(workDir, { recursive: true, force: true });
109
+ mkdirSyncImpl(dirname(stagedFunctionsDir), { recursive: true });
110
+
111
+ try {
112
+ cpSyncImpl(resolvePathImpl(cwd, functionsDir), stagedFunctionsDir, { recursive: true });
113
+ installCronCoreShim({ workDir, mkdirSyncImpl, resolvePathImpl, writeFileSyncImpl });
114
+
115
+ const cronsFileName = cronsFile.endsWith(".js") ? "crons.js" : "crons.ts";
116
+ const stagedCronsFile = resolvePathImpl(stagedFunctionsDir, cronsFileName);
117
+ const importUrl = `${pathToFileURL(stagedCronsFile).href}?v=${Date.now()}-${Math.random()}`;
118
+ const module = await import(importUrl);
119
+ const builder = module.default ?? module.crons;
120
+ if (!builder || typeof builder.getJobs !== "function") {
121
+ throw new Error("crons.ts must export default cronJobs() builder");
122
+ }
123
+
124
+ const { jobs, skipped, skippedInlineCount } = normalizeCronJobs(builder.getJobs());
125
+ const sourceHash = createHash("sha256").update(readFileSyncImpl(cronsFile)).digest("hex");
126
+ const manifest = {
127
+ version: 1,
128
+ generatedAt: new Date().toISOString(),
129
+ source: `${functionsDir}/${cronsFileName}`,
130
+ sourceHash: `sha256:${sourceHash}`,
131
+ skipped,
132
+ skippedInlineCount,
133
+ jobs,
134
+ };
135
+
136
+ mkdirSyncImpl(dirname(manifestPath), { recursive: true });
137
+ writeFileSyncImpl(manifestPath, JSON.stringify(manifest, null, 2));
138
+ return { manifestPath, manifest };
139
+ } finally {
140
+ rmSyncImpl(workDir, { recursive: true, force: true });
141
+ }
142
+ }
@@ -10,7 +10,7 @@ export const DEPENDENCY_COMPAT_MATRIX = Object.freeze([
10
10
  },
11
11
  {
12
12
  packageName: "drizzle-orm",
13
- expectedRange: "workspace:* || ^1.0.0-beta || ^1.0.0",
13
+ expectedRange: "workspace:* || ^1.0.0-beta || ^1.0.0-rc || ^1.0.0",
14
14
  installRange: "drizzle-orm@^1.0.0",
15
15
  policy: "block",
16
16
  },
@@ -145,10 +145,7 @@ function clausesCompatible(current, expected) {
145
145
 
146
146
  if (expectedVersion.major === 0) {
147
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;
148
+ return compareVersions(currentVersion, expectedVersion) >= 0;
152
149
  }
153
150
 
154
151
  if (currentVersion.major !== expectedVersion.major) return false;