better-convex 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/aggregate/index.d.ts +388 -0
  2. package/dist/aggregate/index.js +37 -0
  3. package/dist/{auth-client → auth/client}/index.js +1 -1
  4. package/dist/auth/http/index.d.ts +63 -0
  5. package/dist/auth/http/index.js +429 -0
  6. package/dist/auth/index.d.ts +19001 -185
  7. package/dist/auth/index.js +373 -686
  8. package/dist/{auth-nextjs → auth/nextjs}/index.d.ts +3 -4
  9. package/dist/{auth-nextjs → auth/nextjs}/index.js +3 -5
  10. package/dist/{caller-factory-B1FvYSKr.js → caller-factory-Dmgv8MLS.js} +15 -12
  11. package/dist/cli.mjs +2601 -13
  12. package/dist/codegen-Cz1idI3-.mjs +969 -0
  13. package/dist/{create-schema-orm-DplxTtYj.js → create-schema-orm-69VF4CFV.js} +4 -3
  14. package/dist/crpc/index.d.ts +2 -2
  15. package/dist/crpc/index.js +3 -3
  16. package/dist/{http-types-BRLY10NX.d.ts → http-types-BCf2wCgp.d.ts} +25 -25
  17. package/dist/meta-utils-DDVYp9Xf.js +117 -0
  18. package/dist/orm/index.d.ts +4 -3012
  19. package/dist/orm/index.js +9631 -2
  20. package/dist/{index-BQkhP2ny.d.ts → procedure-caller-CcjtUFvL.d.ts} +211 -74
  21. package/dist/query-context-BDSis9rT.js +1518 -0
  22. package/dist/query-context-DGExXZIV.d.ts +42 -0
  23. package/dist/react/index.d.ts +31 -35
  24. package/dist/react/index.js +145 -58
  25. package/dist/rsc/index.d.ts +4 -7
  26. package/dist/rsc/index.js +14 -10
  27. package/dist/runtime-B9xQFY8W.js +2280 -0
  28. package/dist/server/index.d.ts +3 -4
  29. package/dist/server/index.js +384 -10
  30. package/dist/{types-o-5rYcTr.d.ts → types-CIBGEYXq.d.ts} +4 -3
  31. package/dist/types-DgwvxKbT.d.ts +4 -0
  32. package/dist/watcher.mjs +8 -8
  33. package/dist/where-clause-compiler-CRP-i1Qa.d.ts +3463 -0
  34. package/package.json +14 -10
  35. package/dist/codegen-DkpPBVPn.mjs +0 -189
  36. package/dist/context-utils-DSuX99Da.d.ts +0 -17
  37. package/dist/meta-utils-DCpLSBWB.js +0 -41
  38. package/dist/orm-CleikBIV.js +0 -8820
  39. /package/dist/{auth-client → auth/client}/index.d.ts +0 -0
  40. /package/dist/{auth-config → auth/config}/index.d.ts +0 -0
  41. /package/dist/{auth-config → auth/config}/index.js +0 -0
  42. /package/dist/{create-schema-DhWXOhnU.js → create-schema-BdZOL6ns.js} +0 -0
  43. /package/dist/{customFunctions-C1okqCzL.js → customFunctions-CZnCwoR3.js} +0 -0
  44. /package/dist/{error-BZUhlhYz.js → error-Be4OcwwD.js} +0 -0
  45. /package/dist/{query-options-BL1Q0X7q.js → query-options-B0c1b6pZ.js} +0 -0
  46. /package/dist/{transformer-CTNSPjwp.js → transformer-Dh0w2py0.js} +0 -0
  47. /package/dist/{types-jftzhhuc.d.ts → types-DwGkkq2s.d.ts} +0 -0
package/dist/cli.mjs CHANGED
@@ -1,13 +1,1909 @@
1
1
  #!/usr/bin/env node
2
- import { t as generateMeta } from "./codegen-DkpPBVPn.mjs";
2
+ import { n as getConvexConfig, t as generateMeta } from "./codegen-Cz1idI3-.mjs";
3
3
  import { createRequire } from "node:module";
4
+ import { createHash } from "node:crypto";
4
5
  import fs from "node:fs";
5
6
  import path, { dirname, join, resolve } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { execa } from "execa";
9
+ import { createJiti } from "jiti";
10
+ import { v } from "convex/values";
11
+ import { createInterface } from "node:readline/promises";
12
+ import { build } from "esbuild";
8
13
  import * as childProcess from "node:child_process";
9
14
  import { parse } from "dotenv";
10
15
 
16
+ //#region src/orm/builders/column-builder.ts
17
+ /**
18
+ * entityKind symbol for runtime type checking
19
+ * Following Drizzle's pattern for type guards
20
+ */
21
+ const entityKind = Symbol.for("better-convex:entityKind");
22
+ /**
23
+ * Base ColumnBuilder abstract class
24
+ *
25
+ * All column builders inherit from this class.
26
+ * Implements chaining methods and stores runtime config.
27
+ */
28
+ var ColumnBuilder = class {
29
+ static [entityKind] = "ColumnBuilder";
30
+ [entityKind] = "ColumnBuilder";
31
+ /**
32
+ * Runtime configuration - actual mutable state
33
+ */
34
+ config;
35
+ constructor(name, dataType, columnType) {
36
+ this.config = {
37
+ name,
38
+ notNull: false,
39
+ default: void 0,
40
+ hasDefault: false,
41
+ primaryKey: false,
42
+ isUnique: false,
43
+ uniqueName: void 0,
44
+ uniqueNulls: void 0,
45
+ foreignKeyConfigs: [],
46
+ dataType,
47
+ columnType
48
+ };
49
+ }
50
+ /**
51
+ * Mark column as NOT NULL
52
+ * Returns type-branded instance with notNull: true
53
+ */
54
+ notNull() {
55
+ this.config.notNull = true;
56
+ return this;
57
+ }
58
+ /**
59
+ * Override the TypeScript type for this column.
60
+ * Mirrors Drizzle's $type() (type-only, no runtime validation changes).
61
+ */
62
+ $type() {
63
+ return this;
64
+ }
65
+ /**
66
+ * Set default value for column
67
+ * Makes field optional on insert
68
+ */
69
+ default(value) {
70
+ this.config.default = value;
71
+ this.config.hasDefault = true;
72
+ return this;
73
+ }
74
+ /**
75
+ * Set default function for column (runtime evaluated on insert).
76
+ * Mirrors Drizzle's $defaultFn() / $default().
77
+ */
78
+ $defaultFn(fn) {
79
+ this.config.defaultFn = fn;
80
+ return this;
81
+ }
82
+ /**
83
+ * Alias of $defaultFn for Drizzle parity.
84
+ */
85
+ $default(fn) {
86
+ return this.$defaultFn(fn);
87
+ }
88
+ /**
89
+ * Set on-update function for column (runtime evaluated on update).
90
+ * Mirrors Drizzle's $onUpdateFn() / $onUpdate().
91
+ */
92
+ $onUpdateFn(fn) {
93
+ this.config.onUpdateFn = fn;
94
+ return this;
95
+ }
96
+ /**
97
+ * Alias of $onUpdateFn for Drizzle parity.
98
+ */
99
+ $onUpdate(fn) {
100
+ return this.$onUpdateFn(fn);
101
+ }
102
+ /**
103
+ * Mark column as primary key
104
+ * Implies NOT NULL
105
+ */
106
+ primaryKey() {
107
+ this.config.primaryKey = true;
108
+ this.config.notNull = true;
109
+ return this;
110
+ }
111
+ /**
112
+ * Mark column as UNIQUE
113
+ * Mirrors Drizzle column unique API
114
+ */
115
+ unique(name, config) {
116
+ this.config.isUnique = true;
117
+ this.config.uniqueName = name;
118
+ this.config.uniqueNulls = config?.nulls;
119
+ return this;
120
+ }
121
+ /**
122
+ * Define a foreign key reference
123
+ * Mirrors Drizzle column references() API
124
+ */
125
+ references(ref, config = {}) {
126
+ this.config.foreignKeyConfigs.push({
127
+ ref,
128
+ config
129
+ });
130
+ return this;
131
+ }
132
+ };
133
+
134
+ //#endregion
135
+ //#region src/orm/builders/system-fields.ts
136
+ /**
137
+ * System Fields - Convex-provided fields available on all documents
138
+ *
139
+ * id: Document ID (string, backed by internal Convex _id)
140
+ * createdAt: Creation timestamp alias (backed by internal Convex _creationTime)
141
+ *
142
+ * These are automatically added to every Convex table.
143
+ */
144
+ var ConvexSystemIdBuilder = class extends ColumnBuilder {
145
+ static [entityKind] = "ConvexSystemIdBuilder";
146
+ [entityKind] = "ConvexSystemIdBuilder";
147
+ constructor() {
148
+ super("_id", "string", "ConvexSystemId");
149
+ this.config.notNull = true;
150
+ }
151
+ build() {
152
+ return v.string();
153
+ }
154
+ /**
155
+ * Convex validator - runtime access
156
+ * System fields use v.string() for _id
157
+ */
158
+ get convexValidator() {
159
+ return this.build();
160
+ }
161
+ };
162
+ var ConvexSystemCreationTimeBuilder = class extends ColumnBuilder {
163
+ static [entityKind] = "ConvexSystemCreationTimeBuilder";
164
+ [entityKind] = "ConvexSystemCreationTimeBuilder";
165
+ constructor() {
166
+ super("_creationTime", "number", "ConvexSystemCreationTime");
167
+ this.config.notNull = true;
168
+ }
169
+ build() {
170
+ return v.number();
171
+ }
172
+ /**
173
+ * Convex validator - runtime access
174
+ * System fields use v.number() for _creationTime
175
+ */
176
+ get convexValidator() {
177
+ return this.build();
178
+ }
179
+ };
180
+ var ConvexSystemCreatedAtBuilder = class extends ColumnBuilder {
181
+ static [entityKind] = "ConvexSystemCreatedAtBuilder";
182
+ [entityKind] = "ConvexSystemCreatedAtBuilder";
183
+ constructor() {
184
+ super("_creationTime", "number", "ConvexSystemCreatedAt");
185
+ this.config.notNull = true;
186
+ }
187
+ build() {
188
+ return v.number();
189
+ }
190
+ get convexValidator() {
191
+ return this.build();
192
+ }
193
+ };
194
+ function createSystemFields(tableName) {
195
+ const id = new ConvexSystemIdBuilder();
196
+ const creationTime = new ConvexSystemCreationTimeBuilder();
197
+ const createdAt = new ConvexSystemCreatedAtBuilder();
198
+ id.config.tableName = tableName;
199
+ creationTime.config.tableName = tableName;
200
+ createdAt.config.tableName = tableName;
201
+ return {
202
+ id,
203
+ _creationTime: creationTime,
204
+ createdAt
205
+ };
206
+ }
207
+
208
+ //#endregion
209
+ //#region src/orm/index-utils.ts
210
+ function getIndexes(table) {
211
+ const indexes = table.getIndexes?.();
212
+ return Array.isArray(indexes) ? indexes : [];
213
+ }
214
+ function getAggregateIndexes(table) {
215
+ const indexes = table.getAggregateIndexes?.();
216
+ return Array.isArray(indexes) ? indexes : [];
217
+ }
218
+ function getRankIndexes(table) {
219
+ const indexes = table.getRankIndexes?.();
220
+ return Array.isArray(indexes) ? indexes : [];
221
+ }
222
+
223
+ //#endregion
224
+ //#region src/orm/symbols.ts
225
+ const TableName = Symbol.for("better-convex:TableName");
226
+ const Columns = Symbol.for("better-convex:Columns");
227
+ const Brand = Symbol.for("better-convex:Brand");
228
+ const Relations = Symbol.for("better-convex:Relations");
229
+ const OrmContext = Symbol.for("better-convex:OrmContext");
230
+ const RlsPolicies = Symbol.for("better-convex:RlsPolicies");
231
+ const EnableRLS = Symbol.for("better-convex:EnableRLS");
232
+ const TableDeleteConfig = Symbol.for("better-convex:TableDeleteConfig");
233
+ const OrmSchemaOptions = Symbol.for("better-convex:OrmSchemaOptions");
234
+ const OrmSchemaDefinition = Symbol.for("better-convex:OrmSchemaDefinition");
235
+
236
+ //#endregion
237
+ //#region src/orm/mutation-utils.ts
238
+ const UTF8_ENCODER = new TextEncoder();
239
+ function getTableName(table) {
240
+ const name = table.tableName ?? table[TableName] ?? table?._?.name;
241
+ if (!name) throw new Error("Table is missing a name");
242
+ return name;
243
+ }
244
+ function getUniqueIndexes(table) {
245
+ const fromMethod = table.getUniqueIndexes?.();
246
+ if (Array.isArray(fromMethod)) return fromMethod;
247
+ const fromField = table.uniqueIndexes;
248
+ return Array.isArray(fromField) ? fromField : [];
249
+ }
250
+ function getChecks(table) {
251
+ const fromMethod = table.getChecks?.();
252
+ if (Array.isArray(fromMethod)) return fromMethod;
253
+ const fromField = table.checks;
254
+ return Array.isArray(fromField) ? fromField : [];
255
+ }
256
+ function getForeignKeys(table) {
257
+ const fromMethod = table.getForeignKeys?.();
258
+ if (Array.isArray(fromMethod)) return fromMethod;
259
+ const fromField = table.foreignKeys;
260
+ return Array.isArray(fromField) ? fromField : [];
261
+ }
262
+
263
+ //#endregion
264
+ //#region src/orm/introspection.ts
265
+ function getSystemFields(table) {
266
+ if (table.id && table._creationTime) return {
267
+ id: table.id,
268
+ createdAt: table._creationTime ?? table.createdAt
269
+ };
270
+ const system = createSystemFields(getTableName(table));
271
+ for (const builder of Object.values(system)) builder.config.table = table;
272
+ return {
273
+ id: system.id,
274
+ createdAt: system.createdAt
275
+ };
276
+ }
277
+ function getTableColumns(table) {
278
+ return {
279
+ ...table[Columns] ?? {},
280
+ ...getSystemFields(table)
281
+ };
282
+ }
283
+ function getTableConfig(table) {
284
+ const policies = table.getRlsPolicies?.() ?? table[RlsPolicies] ?? [];
285
+ const enabled = table.isRlsEnabled?.() ?? table[EnableRLS] ?? false;
286
+ return {
287
+ name: getTableName(table),
288
+ columns: getTableColumns(table),
289
+ indexes: getIndexes(table),
290
+ aggregateIndexes: getAggregateIndexes(table),
291
+ rankIndexes: getRankIndexes(table),
292
+ uniqueIndexes: getUniqueIndexes(table),
293
+ foreignKeys: getForeignKeys(table),
294
+ checks: getChecks(table),
295
+ rls: {
296
+ enabled,
297
+ policies
298
+ }
299
+ };
300
+ }
301
+
302
+ //#endregion
303
+ //#region src/cli/analyze.ts
304
+ const MB = 1024 * 1024;
305
+ const DEFAULT_WARNING_MB = 6;
306
+ const DEFAULT_DANGER_MB = 8;
307
+ const DEFAULT_TOP_INPUTS = 12;
308
+ const DEFAULT_TOP_PACKAGES = 12;
309
+ const DEFAULT_DETAIL_ENTRIES = 20;
310
+ const DEFAULT_OUTPUT_WIDTH = 120;
311
+ const SMALL_INPUT_MIN_BYTES = 8 * 1024;
312
+ const SMALL_INPUT_MIN_SHARE = .002;
313
+ const WHITESPACE_SPLIT_REGEX = /\s+/;
314
+ const NEWLINE_SPLIT_REGEX = /\r?\n/;
315
+ const ENTRY_POINT_EXTENSIONS = new Set([
316
+ ".ts",
317
+ ".tsx",
318
+ ".mts",
319
+ ".cts",
320
+ ".js",
321
+ ".jsx",
322
+ ".mjs",
323
+ ".cjs"
324
+ ]);
325
+ const SCHEMA_RESOLVE_FILTER = /^\.{1,2}\/schema(\.ts|\.js)?$/;
326
+ const USE_NODE_DIRECTIVE_REGEX = /^\s*("|')use node\1;?\s*$/;
327
+ const VALID_IDENTIFIER_REGEX = /^[a-zA-Z_$][\w$]*$/;
328
+ const EXPORTED_CONST_CAPTURE_REGEX = /export\s+const\s+([a-zA-Z_$][\w$]*)\s*=/g;
329
+ const CHAINED_PROCEDURE_CAPTURE_REGEX = /\.\s*(?:query|mutation|action)\s*\(/;
330
+ const EXPORTED_NATIVE_HANDLER_CAPTURE_REGEX = /export\s+const\s+([a-zA-Z_$][\w$]*)\s*=\s*(?:[\w$]+\.)?(?:query|mutation|action|internalQuery|internalMutation|internalAction)\s*\(/g;
331
+ const EXPORTED_ORM_API_DESTRUCTURE_CAPTURE_REGEX = /export\s+const\s*\{([^}]+)\}\s*=\s*orm\.api\s*\(\s*\)\s*;?/g;
332
+ const supportsColor = process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
333
+ const isInteractiveTerminal = process.stdin.isTTY && process.stdout.isTTY;
334
+ let colorEnabled = supportsColor;
335
+ let outputWidth = process.stdout.columns ?? DEFAULT_OUTPUT_WIDTH;
336
+ const ANSI = {
337
+ reset: "\x1B[0m",
338
+ bold: "\x1B[1m",
339
+ dim: "\x1B[2m",
340
+ red: "\x1B[31m",
341
+ yellow: "\x1B[33m",
342
+ green: "\x1B[32m",
343
+ cyan: "\x1B[36m",
344
+ magenta: "\x1B[35m",
345
+ gray: "\x1B[90m"
346
+ };
347
+ const dedupe = (values) => Array.from(new Set(values));
348
+ const formatBytes = (bytes) => `${(bytes / MB).toFixed(2)} MB`;
349
+ const toMB = (bytes) => (bytes / MB).toFixed(2);
350
+ const truncate = (value, maxWidth) => {
351
+ if (value.length <= maxWidth) return value;
352
+ if (maxWidth <= 3) return value.slice(0, maxWidth);
353
+ return `${value.slice(0, maxWidth - 3)}...`;
354
+ };
355
+ const pad = (value, width, align = "left") => {
356
+ if (align === "right") return value.padStart(width, " ");
357
+ return value.padEnd(width, " ");
358
+ };
359
+ const colorize = (value, color) => {
360
+ if (!colorEnabled) return value;
361
+ return `${color}${value}${ANSI.reset}`;
362
+ };
363
+ const bold = (value) => colorize(value, ANSI.bold);
364
+ const dim = (value) => colorize(value, ANSI.dim);
365
+ const shareColor = (sharePercent) => {
366
+ if (sharePercent >= 25) return ANSI.red;
367
+ if (sharePercent >= 10) return ANSI.yellow;
368
+ if (sharePercent >= 3) return ANSI.cyan;
369
+ return ANSI.gray;
370
+ };
371
+ const severityColor = (severity) => {
372
+ if (severity === "DANGER") return ANSI.red;
373
+ if (severity === "WARN") return ANSI.yellow;
374
+ return ANSI.green;
375
+ };
376
+ const makeShareBar = (sharePercent, width = 16) => {
377
+ const clamped = Math.max(0, Math.min(100, sharePercent));
378
+ const filled = Math.round(clamped / 100 * width);
379
+ return colorize(`${"#".repeat(filled)}${".".repeat(Math.max(0, width - filled))}`, shareColor(sharePercent));
380
+ };
381
+ const colorizePadded = (value, width, align, color) => colorize(pad(value, width, align), color);
382
+ const ANSI_PATTERN = new RegExp(`\\x1b\\[[0-9;]*m`, "g");
383
+ const visibleLength = (value) => value.replace(ANSI_PATTERN, "").length;
384
+ const wrapPlain = (text, width) => {
385
+ const maxWidth = Math.max(16, width);
386
+ const words = text.split(WHITESPACE_SPLIT_REGEX).filter(Boolean);
387
+ if (words.length === 0) return [""];
388
+ const lines = [];
389
+ let current = "";
390
+ for (const word of words) {
391
+ if (!current) {
392
+ current = word;
393
+ continue;
394
+ }
395
+ if (current.length + 1 + word.length <= maxWidth) {
396
+ current += ` ${word}`;
397
+ continue;
398
+ }
399
+ lines.push(current);
400
+ current = word;
401
+ }
402
+ if (current) lines.push(current);
403
+ return lines;
404
+ };
405
+ const printWrapped = ({ indent = 0, prefix = "", text, color = null }) => {
406
+ const indentStr = " ".repeat(Math.max(0, indent));
407
+ const prefixLen = visibleLength(prefix);
408
+ wrapPlain(text, Math.max(16, outputWidth - indent - prefixLen)).forEach((line, index) => {
409
+ const leader = index === 0 ? prefix : " ".repeat(prefixLen);
410
+ const body = color ? colorize(line, color) : line;
411
+ console.log(`${indentStr}${leader}${body}`);
412
+ });
413
+ };
414
+ const firstLine = (value) => value.split("\n").at(0) ?? value;
415
+ const normalizeOutputPath = (filePath) => filePath.split(path.sep).join("/");
416
+ const pathHasMultipleDots = (base) => (base.match(/\./g) ?? []).length > 1;
417
+ const shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
418
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
419
+ const walkDeployEntryPoints = (dir, options) => {
420
+ const files = [];
421
+ const includeMultiDot = options?.includeMultiDot ?? false;
422
+ const includeGeneratedDir = options?.includeGeneratedDir ?? false;
423
+ const visit = (currentDir) => {
424
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
425
+ const fullPath = path.join(currentDir, entry.name);
426
+ if (entry.isDirectory()) {
427
+ if (entry.name === "_generated") continue;
428
+ if (entry.name === "generated" && !includeGeneratedDir) continue;
429
+ visit(fullPath);
430
+ continue;
431
+ }
432
+ if (!entry.isFile()) continue;
433
+ const normalizedRelPath = normalizeOutputPath(path.relative(dir, fullPath));
434
+ const parsed = path.parse(fullPath);
435
+ const base = parsed.base;
436
+ const ext = parsed.ext.toLowerCase();
437
+ if (normalizedRelPath.startsWith("_deps/")) continue;
438
+ if (!ENTRY_POINT_EXTENSIONS.has(ext)) continue;
439
+ if (base.startsWith(".") || base.startsWith("#")) continue;
440
+ if (base === "schema.ts" || base === "schema.js") continue;
441
+ if (!includeMultiDot && pathHasMultipleDots(base)) continue;
442
+ if (normalizedRelPath.includes(" ")) continue;
443
+ files.push(fullPath);
444
+ }
445
+ };
446
+ visit(dir);
447
+ return files;
448
+ };
449
+ const detectProjectRoots = () => {
450
+ const projectRoot = process.cwd();
451
+ const preferredFunctionsRoot = path.join(projectRoot, "convex", "functions");
452
+ const fallbackFunctionsRoot = path.join(projectRoot, "convex");
453
+ if (fs.existsSync(preferredFunctionsRoot)) return {
454
+ projectRoot,
455
+ functionsRoot: preferredFunctionsRoot
456
+ };
457
+ if (fs.existsSync(fallbackFunctionsRoot)) return {
458
+ projectRoot,
459
+ functionsRoot: fallbackFunctionsRoot
460
+ };
461
+ throw new Error(`Missing Convex functions directory. Expected one of:\n- ${preferredFunctionsRoot}\n- ${fallbackFunctionsRoot}`);
462
+ };
463
+ const hasUseNodeDirective = (source) => {
464
+ if (!source.includes("use node")) return false;
465
+ const lines = source.split(NEWLINE_SPLIT_REGEX);
466
+ for (const line of lines) {
467
+ const trimmed = line.trim();
468
+ if (!trimmed) continue;
469
+ if (trimmed.startsWith("//")) continue;
470
+ if (trimmed.startsWith("/*")) continue;
471
+ return USE_NODE_DIRECTIVE_REGEX.test(trimmed);
472
+ }
473
+ return false;
474
+ };
475
+ const isNodeEntryPoint = (entryPoint, functionsRoot) => {
476
+ if (normalizeOutputPath(path.relative(functionsRoot, entryPoint)).startsWith("actions/")) return true;
477
+ return hasUseNodeDirective(fs.readFileSync(entryPoint, "utf8"));
478
+ };
479
+ const hasRuntimeProcedureType = (value) => value === "query" || value === "mutation" || value === "action";
480
+ const getNativeHandlerExportNames = (source) => {
481
+ const exportNames = new Set(Array.from(source.matchAll(EXPORTED_NATIVE_HANDLER_CAPTURE_REGEX)).map((match) => match[1]).filter((name) => !!name));
482
+ const exportConstMatches = Array.from(source.matchAll(EXPORTED_CONST_CAPTURE_REGEX));
483
+ for (const [index, match] of exportConstMatches.entries()) {
484
+ const exportName = match[1];
485
+ if (!exportName) continue;
486
+ const start = (match.index ?? 0) + match[0].length;
487
+ const end = exportConstMatches[index + 1]?.index ?? source.length;
488
+ const initializerSlice = source.slice(start, end);
489
+ if (CHAINED_PROCEDURE_CAPTURE_REGEX.test(initializerSlice)) exportNames.add(exportName);
490
+ }
491
+ for (const match of source.matchAll(EXPORTED_ORM_API_DESTRUCTURE_CAPTURE_REGEX)) {
492
+ const bindings = match[1];
493
+ for (const binding of bindings.split(",")) {
494
+ const trimmed = binding.trim();
495
+ if (!trimmed || trimmed.startsWith("...")) continue;
496
+ const withoutDefault = trimmed.split("=")[0]?.trim() ?? "";
497
+ const localBinding = withoutDefault.split(":")[1]?.trim() ?? withoutDefault;
498
+ if (VALID_IDENTIFIER_REGEX.test(localBinding)) exportNames.add(localBinding);
499
+ }
500
+ }
501
+ return Array.from(exportNames);
502
+ };
503
+ const listConvexHandlerExports = async (entryPoint, jitiInstance) => {
504
+ const exportNames = /* @__PURE__ */ new Set();
505
+ const source = fs.readFileSync(entryPoint, "utf8");
506
+ for (const exportName of getNativeHandlerExportNames(source)) exportNames.add(exportName);
507
+ try {
508
+ const module = await jitiInstance.import(entryPoint);
509
+ if (module && typeof module === "object") for (const [name, value] of Object.entries(module)) {
510
+ if (name.startsWith("_")) continue;
511
+ const meta = value._crpcMeta;
512
+ if (hasRuntimeProcedureType(meta?.type)) exportNames.add(name);
513
+ }
514
+ } catch {}
515
+ return Array.from(exportNames).sort((a, b) => a.localeCompare(b));
516
+ };
517
+ const scanHandlerExportsByEntry = async (entryPoints) => {
518
+ const jitiInstance = createJiti(process.cwd(), {
519
+ interopDefault: true,
520
+ moduleCache: false
521
+ });
522
+ const results = await Promise.all(entryPoints.map(async (entryPoint) => ({
523
+ entryPoint,
524
+ exportNames: await listConvexHandlerExports(entryPoint, jitiInstance)
525
+ })));
526
+ const byEntry = /* @__PURE__ */ new Map();
527
+ for (const result of results) if (result.exportNames.length > 0) byEntry.set(result.entryPoint, result.exportNames);
528
+ return byEntry;
529
+ };
530
+ const parseArgs$1 = (argv) => {
531
+ const options = {
532
+ mode: "hotspot",
533
+ entryPattern: null,
534
+ details: false,
535
+ showInputs: false,
536
+ interactive: "never",
537
+ includeGenerated: false,
538
+ showSmall: false,
539
+ width: null,
540
+ topInputs: DEFAULT_TOP_INPUTS,
541
+ topPackages: DEFAULT_TOP_PACKAGES,
542
+ detailEntries: DEFAULT_DETAIL_ENTRIES,
543
+ warningMb: DEFAULT_WARNING_MB,
544
+ dangerMb: DEFAULT_DANGER_MB,
545
+ failMb: null
546
+ };
547
+ for (let i = 0; i < argv.length; i += 1) {
548
+ const arg = argv[i];
549
+ const next = argv[i + 1];
550
+ if (arg === "--hotspot") throw new Error("`--hotspot` was removed. Hotspot is the default mode, so just run `better-convex analyze`.");
551
+ if (arg === "--deploy") {
552
+ options.mode = "deploy";
553
+ continue;
554
+ }
555
+ if (arg === "--entry" || arg.startsWith("--entry=")) throw new Error("`--entry` was removed. Pass the entry regex as the first positional argument, e.g. `better-convex analyze polar.*`.");
556
+ if (arg === "--detail-entries" || arg.startsWith("--detail-entries=") || arg === "--top" || arg.startsWith("--top=")) throw new Error("`--top` and `--detail-entries` were removed. The analyzer now uses built-in detail defaults.");
557
+ if (arg === "--details") {
558
+ options.details = true;
559
+ continue;
560
+ }
561
+ if (arg === "--input") {
562
+ options.showInputs = true;
563
+ continue;
564
+ }
565
+ if (arg === "--interactive" || arg === "-i") {
566
+ options.interactive = "always";
567
+ continue;
568
+ }
569
+ if (arg === "--no-interactive" || arg === "-I") throw new Error("`--no-interactive` was removed. Non-interactive is already the default.");
570
+ if (arg === "--all" || arg === "-a") {
571
+ options.includeGenerated = true;
572
+ continue;
573
+ }
574
+ if (!arg.startsWith("-")) {
575
+ if (options.entryPattern !== null) throw new Error(`Only one positional entry regex is allowed. Received "${options.entryPattern}" and "${arg}".`);
576
+ options.entryPattern = arg;
577
+ continue;
578
+ }
579
+ if (arg === "--show-small") {
580
+ options.showSmall = true;
581
+ continue;
582
+ }
583
+ if (arg === "--width" && next) {
584
+ const parsed = Number.parseInt(next, 10);
585
+ if (Number.isFinite(parsed) && parsed >= 60) options.width = parsed;
586
+ i += 1;
587
+ continue;
588
+ }
589
+ if (arg === "--top-inputs" && next) {
590
+ const parsed = Number.parseInt(next, 10);
591
+ if (Number.isFinite(parsed) && parsed > 0) options.topInputs = parsed;
592
+ i += 1;
593
+ continue;
594
+ }
595
+ if (arg === "--top-packages" && next) {
596
+ const parsed = Number.parseInt(next, 10);
597
+ if (Number.isFinite(parsed) && parsed > 0) options.topPackages = parsed;
598
+ i += 1;
599
+ continue;
600
+ }
601
+ if (arg === "--warn-mb" && next) {
602
+ const parsed = Number.parseFloat(next);
603
+ if (Number.isFinite(parsed) && parsed > 0) options.warningMb = parsed;
604
+ i += 1;
605
+ continue;
606
+ }
607
+ if (arg === "--danger-mb" && next) {
608
+ const parsed = Number.parseFloat(next);
609
+ if (Number.isFinite(parsed) && parsed > 0) options.dangerMb = parsed;
610
+ i += 1;
611
+ continue;
612
+ }
613
+ if (arg === "--fail-mb" && next) {
614
+ const parsed = Number.parseFloat(next);
615
+ if (Number.isFinite(parsed) && parsed > 0) options.failMb = parsed;
616
+ i += 1;
617
+ continue;
618
+ }
619
+ if (arg === "--help" || arg === "-h") {
620
+ console.log(`Usage:
621
+ better-convex analyze [entryRegex]
622
+ better-convex analyze --deploy
623
+ better-convex analyze '^convex/functions/auth\\.ts$' --details
624
+
625
+ Modes:
626
+ (default) Hotspot analysis (per-function isolate ranking)
627
+ --deploy Deploy analysis (single isolate bundle, Convex-like)
628
+
629
+ Flags:
630
+ --details Show extra per-entry/package details
631
+ --input Include top internal inputs in detail output
632
+ --interactive, -i Enable interactive hotspot UI (default: off, TTY only)
633
+ --all, -a Include all Convex-ignored entries (multi-dot files + generated/, even without handlers)
634
+ --show-small Include tiny dependencies (hidden by default)
635
+ --width <n> Force output width (min 60)
636
+ --top-inputs <n> Rows for input tables (default ${DEFAULT_TOP_INPUTS})
637
+ --top-packages <n> Rows for package tables (default ${DEFAULT_TOP_PACKAGES})
638
+ --warn-mb <n> WARN threshold per entry/chunk (default ${DEFAULT_WARNING_MB})
639
+ --danger-mb <n> DANGER threshold per entry/chunk (default ${DEFAULT_DANGER_MB})
640
+ --fail-mb <n> Exit 1 if largest entry/chunk >= n MB`);
641
+ process.exit(0);
642
+ }
643
+ }
644
+ if (options.mode === "deploy" && options.interactive === "always") throw new Error("`--interactive` is hotspot-only. Remove it when using `--deploy`.");
645
+ return options;
646
+ };
647
+ const schemaExternalFallbackPlugin = {
648
+ name: "schema-external-fallback",
649
+ setup(buildCtx) {
650
+ buildCtx.onResolve({ filter: SCHEMA_RESOLVE_FILTER }, (args) => ({
651
+ path: args.path,
652
+ external: true
653
+ }));
654
+ }
655
+ };
656
+ const shouldRetryWithSchemaExternalized = (error) => {
657
+ const message = error instanceof Error ? error.message : String(error);
658
+ return message.includes("No matching export") && (message.includes("schema.ts") || message.includes("/schema"));
659
+ };
660
+ const severityForBytes = (bytes, options) => {
661
+ const outputMb = bytes / MB;
662
+ if (outputMb >= options.dangerMb) return "DANGER";
663
+ if (outputMb >= options.warningMb) return "WARN";
664
+ return "OK";
665
+ };
666
+ const smallInputThreshold = (outputBytes) => Math.max(SMALL_INPUT_MIN_BYTES, Math.floor(outputBytes * SMALL_INPUT_MIN_SHARE));
667
+ const isSmallInput = (bytesInOutput, outputBytes, showSmall) => !showSmall && bytesInOutput < smallInputThreshold(outputBytes);
668
+ const shortPath = (inputPath) => {
669
+ if (inputPath.startsWith("convex/functions/")) return `fn/${inputPath.slice(17)}`;
670
+ if (inputPath.startsWith("convex/lib/")) return `lib/${inputPath.slice(11)}`;
671
+ if (inputPath.startsWith("../packages/better-convex/dist/")) return `bcx/${inputPath.slice(31)}`;
672
+ if (inputPath.startsWith("../node_modules/")) return `nm/${inputPath.slice(16)}`;
673
+ if (inputPath.startsWith("example/convex/")) return `convex/${inputPath.slice(15)}`;
674
+ return inputPath;
675
+ };
676
+ const compactPath = (inputPath, maxWidth) => truncate(shortPath(inputPath), Math.max(16, maxWidth));
677
+ const isNodeModulesInputPath = (inputPath) => inputPath.includes("node_modules/");
678
+ const packageFromInputPath = (inputPath) => {
679
+ const idx = inputPath.lastIndexOf("node_modules/");
680
+ if (idx >= 0) {
681
+ const [first, second] = inputPath.slice(idx + 13).split("/");
682
+ if (!first) return "(node_modules)";
683
+ if (first.startsWith("@") && second) return `${first}/${second}`;
684
+ return first;
685
+ }
686
+ if (inputPath.startsWith("convex/")) return "workspace:convex";
687
+ if (inputPath.startsWith("../packages/")) {
688
+ const parts = inputPath.replace("../", "").split("/");
689
+ return parts.length >= 2 ? `workspace:${parts[0]}/${parts[1]}` : "workspace:packages";
690
+ }
691
+ if (inputPath.startsWith("packages/")) {
692
+ const parts = inputPath.split("/");
693
+ return parts.length >= 2 ? `workspace:${parts[0]}/${parts[1]}` : "workspace:packages";
694
+ }
695
+ return "(other)";
696
+ };
697
+ const buildHotspotEntry = (entryPoint, externalizeSchema) => build({
698
+ bundle: true,
699
+ entryPoints: [entryPoint],
700
+ external: ["convex", "convex/*"],
701
+ format: "esm",
702
+ logLevel: "silent",
703
+ metafile: true,
704
+ platform: "browser",
705
+ target: ["esnext"],
706
+ conditions: ["convex", "module"],
707
+ minifySyntax: true,
708
+ minifyIdentifiers: true,
709
+ minifyWhitespace: false,
710
+ define: { "process.env.NODE_ENV": "\"production\"" },
711
+ write: false,
712
+ plugins: externalizeSchema ? [schemaExternalFallbackPlugin] : []
713
+ });
714
+ const analyzeHotspotEntry = async (entryPoint, projectRoot, includeDeepData) => {
715
+ let result;
716
+ let schemaExternalized = false;
717
+ try {
718
+ result = await buildHotspotEntry(entryPoint, false);
719
+ } catch (error) {
720
+ if (!shouldRetryWithSchemaExternalized(error)) throw error;
721
+ result = await buildHotspotEntry(entryPoint, true);
722
+ schemaExternalized = true;
723
+ }
724
+ const meta = result.metafile;
725
+ if (!meta) throw new Error(`No metafile generated for ${entryPoint}`);
726
+ const output = Object.values(meta.outputs).at(0);
727
+ if (!output) throw new Error(`No output generated for ${entryPoint}`);
728
+ const inputEntries = Object.entries(meta.inputs);
729
+ const totalInputBytes = inputEntries.reduce((sum, [, value]) => sum + (value.bytes ?? 0), 0);
730
+ const isLocalInput = (inputPath) => inputPath.startsWith("convex/") || inputPath.startsWith("example/convex/") || inputPath.includes("/example/convex/");
731
+ const localInputBytes = inputEntries.filter(([inputPath]) => isLocalInput(inputPath)).reduce((sum, [, value]) => sum + (value.bytes ?? 0), 0);
732
+ const row = {
733
+ entry: path.relative(projectRoot, entryPoint),
734
+ inputCount: inputEntries.length,
735
+ localInputBytes,
736
+ dependencyInputBytes: Math.max(0, totalInputBytes - localInputBytes),
737
+ totalInputBytes,
738
+ outputBytes: output.bytes,
739
+ schemaExternalized
740
+ };
741
+ if (!includeDeepData) return row;
742
+ const bytesByInput = new Map(inputEntries.map(([inputPath, value]) => [inputPath, value.bytes ?? 0]));
743
+ const outputInputs = Object.entries(output.inputs ?? {}).map(([inputPath, value]) => ({
744
+ path: inputPath,
745
+ bytesInOutput: value.bytesInOutput ?? 0,
746
+ sourceBytes: bytesByInput.get(inputPath) ?? 0
747
+ })).sort((a, b) => b.bytesInOutput - a.bytesInOutput);
748
+ const inputSet = new Set(inputEntries.map(([inputPath]) => inputPath));
749
+ const importsByInput = Object.fromEntries(inputEntries.map(([inputPath, value]) => [inputPath, Array.from(new Set((value.imports ?? []).map((entry) => entry.path).filter((importPath) => typeof importPath === "string" && inputSet.has(importPath))))]));
750
+ return {
751
+ ...row,
752
+ deep: {
753
+ importsByInput,
754
+ outputInputs
755
+ }
756
+ };
757
+ };
758
+ const printHotspotTopInputs = (row, options) => {
759
+ if (!row.deep) return;
760
+ const internalInputs = row.deep.outputInputs.filter((input) => input.bytesInOutput > 0 && !isNodeModulesInputPath(input.path));
761
+ const externalBytes = row.deep.outputInputs.filter((input) => input.bytesInOutput > 0 && isNodeModulesInputPath(input.path)).reduce((sum, input) => sum + input.bytesInOutput, 0);
762
+ const topInputs = internalInputs.filter((input) => !isSmallInput(input.bytesInOutput, row.outputBytes, options.showSmall)).slice(0, options.topInputs);
763
+ const hiddenInputs = internalInputs.filter((input) => isSmallInput(input.bytesInOutput, row.outputBytes, options.showSmall));
764
+ const hiddenBytes = hiddenInputs.reduce((sum, input) => sum + input.bytesInOutput, 0);
765
+ if (topInputs.length === 0) {
766
+ if (!options.showSmall && hiddenInputs.length > 0) {
767
+ console.log("");
768
+ console.log(bold(`Top internal inputs: ${row.entry}`));
769
+ console.log(dim(`(all visible internal inputs were small; hidden ${hiddenInputs.length} inputs / ${toMB(hiddenBytes)} MB)`));
770
+ if (externalBytes > 0) console.log(dim(`External deps are summarized in Top packages (${toMB(externalBytes)} MB).`));
771
+ }
772
+ return;
773
+ }
774
+ console.log("");
775
+ console.log(bold(`Top internal inputs: ${row.entry}`));
776
+ const inputPathWidth = Math.max(18, outputWidth - 57);
777
+ console.log(dim(`bytesInOutput sourceBytes share impact ${pad("path", inputPathWidth)}`));
778
+ console.log(dim(`------------- ---------- ---------- ---------------- ${"-".repeat(inputPathWidth)}`));
779
+ for (const input of topInputs) {
780
+ const share = row.outputBytes > 0 ? input.bytesInOutput / row.outputBytes * 100 : 0;
781
+ const bytesInOutput = input.bytesInOutput.toString().padStart(13);
782
+ const sourceBytes = input.sourceBytes.toString().padStart(10);
783
+ const sharePct = colorize(`${share.toFixed(2)}%`.padStart(10), shareColor(share));
784
+ const bar = makeShareBar(share);
785
+ console.log(`${bytesInOutput} ${sourceBytes} ${sharePct} ${bar} ${compactPath(input.path, inputPathWidth)}`);
786
+ }
787
+ if (!options.showSmall && hiddenInputs.length > 0) {
788
+ const share = row.outputBytes > 0 ? hiddenBytes / row.outputBytes * 100 : 0;
789
+ const hiddenLabel = dim(`(small: ${hiddenInputs.length} hidden inputs)`);
790
+ console.log(`${hiddenBytes.toString().padStart(13)} ${"".padStart(10)} ${colorize(`${share.toFixed(2)}%`.padStart(10), ANSI.gray)} ${makeShareBar(share)} ${hiddenLabel}`);
791
+ }
792
+ if (externalBytes > 0) {
793
+ const share = row.outputBytes > 0 ? externalBytes / row.outputBytes * 100 : 0;
794
+ console.log(dim(`External deps: ${toMB(externalBytes)} MB (${share.toFixed(2)}%), see Top packages.`));
795
+ }
796
+ };
797
+ const printHotspotPackages = (row, options) => {
798
+ if (!row.deep) return;
799
+ const packageRows = buildPackageImportGraphRows(row, Number.POSITIVE_INFINITY);
800
+ const visibleRows = packageRows.filter((item) => !isSmallInput(item.bytesInOutput, row.outputBytes, options.showSmall));
801
+ const hiddenRows = packageRows.filter((item) => isSmallInput(item.bytesInOutput, row.outputBytes, options.showSmall));
802
+ const hiddenBytes = hiddenRows.reduce((sum, item) => sum + item.bytesInOutput, 0);
803
+ const topRows = visibleRows.slice(0, options.topPackages);
804
+ console.log("");
805
+ console.log(bold(`Package graph: ${row.entry}`));
806
+ if (topRows.length === 0) {
807
+ console.log(" (no packages above small-dependency threshold)");
808
+ return;
809
+ }
810
+ const packageColWidth = Math.max(16, Math.min(30, outputWidth - 58));
811
+ const barWidth = Math.max(8, Math.min(16, outputWidth - (packageColWidth + 52)));
812
+ for (const [index, item] of topRows.entries()) {
813
+ const share = row.outputBytes > 0 ? item.bytesInOutput / row.outputBytes * 100 : 0;
814
+ const shareStr = colorize(`${share.toFixed(2)}%`, shareColor(share));
815
+ const sizeStr = colorize(`${toMB(item.bytesInOutput)} MB`, shareColor(share));
816
+ const bar = makeShareBar(share, barWidth);
817
+ const packageLabel = truncate(item.packageName, packageColWidth);
818
+ const targetPrefix = dim(" imports -> ");
819
+ const maxTargetWidth = Math.max(18, outputWidth - visibleLength(targetPrefix) - 2);
820
+ console.log(`${bold(`${index + 1}.`)} ${pad(packageLabel, packageColWidth)} ${pad(sizeStr, 18)} ${pad(shareStr, 10)} ${bar}`);
821
+ if (item.topTargets.length > 0) {
822
+ const targetLabel = item.topTargets.join(", ");
823
+ console.log(`${targetPrefix}${truncate(targetLabel, maxTargetWidth)}`);
824
+ }
825
+ }
826
+ if (!options.showSmall && hiddenRows.length > 0) {
827
+ const share = row.outputBytes > 0 ? hiddenBytes / row.outputBytes * 100 : 0;
828
+ console.log(` ${dim(`(small packages hidden: ${hiddenRows.length}, ${toMB(hiddenBytes)} MB, ${share.toFixed(2)}%)`)}`);
829
+ }
830
+ };
831
+ const shouldPromptForHotspotPick = (options) => {
832
+ if (options.interactive === "never") return false;
833
+ if (!isInteractiveTerminal) return false;
834
+ if (options.details || options.entryPattern) return false;
835
+ return options.interactive === "always";
836
+ };
837
+ const HOTSPOT_SORT_ORDER = [
838
+ "out",
839
+ "dep",
840
+ "fns"
841
+ ];
842
+ const HOTSPOT_DETAIL_ORDER = [
843
+ "handlers",
844
+ "packages",
845
+ "inputs"
846
+ ];
847
+ const HOTSPOT_MIN_SPLIT_COLUMNS = 120;
848
+ const HOTSPOT_LEFT_MIN_WIDTH = 30;
849
+ const HOTSPOT_LEFT_MAX_WIDTH = 56;
850
+ const HOTSPOT_HEADER_LINES = 2;
851
+ const HOTSPOT_BOTTOM_LINES = 2;
852
+ const clampNumber = (value, min, max) => Math.min(Math.max(value, min), max);
853
+ const cycleHotspotSort = (sortKey) => {
854
+ return HOTSPOT_SORT_ORDER[(HOTSPOT_SORT_ORDER.indexOf(sortKey) + 1) % HOTSPOT_SORT_ORDER.length];
855
+ };
856
+ const cycleHotspotDetailPane = (detailPane) => {
857
+ return HOTSPOT_DETAIL_ORDER[(HOTSPOT_DETAIL_ORDER.indexOf(detailPane) + 1) % HOTSPOT_DETAIL_ORDER.length];
858
+ };
859
+ const cycleHotspotDetailPaneBackward = (detailPane) => {
860
+ return HOTSPOT_DETAIL_ORDER[(HOTSPOT_DETAIL_ORDER.indexOf(detailPane) - 1 + HOTSPOT_DETAIL_ORDER.length) % HOTSPOT_DETAIL_ORDER.length];
861
+ };
862
+ const fitListViewport = (totalRows, selectedIndex, viewportHeight, topIndex) => {
863
+ if (totalRows <= 0 || viewportHeight <= 0) return 0;
864
+ const maxTop = Math.max(0, totalRows - viewportHeight);
865
+ const clampedSelected = clampNumber(selectedIndex, 0, totalRows - 1);
866
+ let nextTop = clampNumber(topIndex, 0, maxTop);
867
+ if (clampedSelected < nextTop) nextTop = clampedSelected;
868
+ else if (clampedSelected >= nextTop + viewportHeight) nextTop = clampedSelected - viewportHeight + 1;
869
+ return clampNumber(nextTop, 0, maxTop);
870
+ };
871
+ const pickSelectedIndex = (rows, preferredEntry, fallbackIndex) => {
872
+ if (rows.length === 0) return 0;
873
+ if (preferredEntry) {
874
+ const preferredIndex = rows.findIndex((row) => row.entry === preferredEntry);
875
+ if (preferredIndex >= 0) return preferredIndex;
876
+ }
877
+ return clampNumber(fallbackIndex, 0, rows.length - 1);
878
+ };
879
+ const resolveInteractiveLayout = (columns, rows) => {
880
+ const safeColumns = Math.max(60, columns);
881
+ const safeRows = Math.max(12, rows);
882
+ const bodyHeight = Math.max(6, safeRows - HOTSPOT_HEADER_LINES - HOTSPOT_BOTTOM_LINES);
883
+ if (safeColumns >= HOTSPOT_MIN_SPLIT_COLUMNS) {
884
+ const leftWidth = clampNumber(Math.floor(safeColumns * .33), HOTSPOT_LEFT_MIN_WIDTH, HOTSPOT_LEFT_MAX_WIDTH);
885
+ const rightWidth = Math.max(24, safeColumns - leftWidth - 3);
886
+ const viewportHeight = Math.max(1, bodyHeight - 1);
887
+ return {
888
+ mode: "split",
889
+ columns: safeColumns,
890
+ rows: safeRows,
891
+ bodyHeight,
892
+ leftWidth,
893
+ rightWidth,
894
+ listViewportHeight: viewportHeight,
895
+ detailViewportHeight: viewportHeight
896
+ };
897
+ }
898
+ const stackBody = Math.max(5, bodyHeight);
899
+ const listHeight = Math.max(2, Math.floor((stackBody - 1) * .45));
900
+ let detailHeight = Math.max(2, stackBody - listHeight - 1);
901
+ if (listHeight + detailHeight + 1 > stackBody) detailHeight = Math.max(2, stackBody - listHeight - 1);
902
+ return {
903
+ mode: "stacked",
904
+ columns: safeColumns,
905
+ rows: safeRows,
906
+ bodyHeight: stackBody,
907
+ listHeight,
908
+ detailHeight,
909
+ listViewportHeight: Math.max(1, listHeight - 1),
910
+ detailViewportHeight: Math.max(1, detailHeight - 1)
911
+ };
912
+ };
913
+ const reduceInteractiveState = (state, action) => {
914
+ switch (action.type) {
915
+ case "moveSelection": {
916
+ if (action.rowCount <= 0) return {
917
+ ...state,
918
+ selectedIndex: 0,
919
+ topIndex: 0
920
+ };
921
+ const nextIndex = clampNumber(state.selectedIndex + action.delta, 0, action.rowCount - 1);
922
+ return {
923
+ ...state,
924
+ selectedIndex: nextIndex
925
+ };
926
+ }
927
+ case "setFilter": return {
928
+ ...state,
929
+ filterQuery: action.query
930
+ };
931
+ case "cycleSort": return {
932
+ ...state,
933
+ sortKey: cycleHotspotSort(state.sortKey)
934
+ };
935
+ case "cyclePane": return {
936
+ ...state,
937
+ detailPane: action.direction === 1 ? cycleHotspotDetailPane(state.detailPane) : cycleHotspotDetailPaneBackward(state.detailPane)
938
+ };
939
+ case "toggleGenerated": return {
940
+ ...state,
941
+ includeGenerated: !state.includeGenerated
942
+ };
943
+ case "toggleWatch": return {
944
+ ...state,
945
+ watchEnabled: !state.watchEnabled
946
+ };
947
+ case "toggleHelp": return {
948
+ ...state,
949
+ showHelp: !state.showHelp
950
+ };
951
+ case "requestRefresh": return {
952
+ ...state,
953
+ statusMessage: "Refreshing analysis..."
954
+ };
955
+ case "setStatus": return {
956
+ ...state,
957
+ statusMessage: action.message
958
+ };
959
+ case "setTopIndex": return {
960
+ ...state,
961
+ topIndex: action.topIndex
962
+ };
963
+ default: return state;
964
+ }
965
+ };
966
+ const sortHotspotRows = (rows, sortKey) => {
967
+ const clone = [...rows];
968
+ if (sortKey === "dep") {
969
+ clone.sort((a, b) => b.dependencyInputBytes - a.dependencyInputBytes);
970
+ return clone;
971
+ }
972
+ if (sortKey === "fns") {
973
+ clone.sort((a, b) => b.handlerExports.length - a.handlerExports.length);
974
+ return clone;
975
+ }
976
+ clone.sort((a, b) => b.outputBytes - a.outputBytes);
977
+ return clone;
978
+ };
979
+ const filterHotspotRows = (rows, query) => {
980
+ const trimmed = query.trim().toLowerCase();
981
+ if (!trimmed) return rows;
982
+ return rows.filter((row) => {
983
+ if (row.entry.toLowerCase().includes(trimmed)) return true;
984
+ return row.handlerExports.some((name) => name.toLowerCase().includes(trimmed));
985
+ });
986
+ };
987
+ const sortLabel = (sortKey) => {
988
+ if (sortKey === "dep") return "DepMB";
989
+ if (sortKey === "fns") return "Fns";
990
+ return "OutMB";
991
+ };
992
+ const selectHotspotEntryPoints = (params) => {
993
+ const { baseCandidateEntries, allCandidateEntries, handlerExportsByEntry, includeGenerated } = params;
994
+ const isolateEntries = baseCandidateEntries.filter((entryPoint) => handlerExportsByEntry.has(entryPoint));
995
+ const baseEntrySet = new Set(baseCandidateEntries);
996
+ const ignoredEntries = allCandidateEntries.filter((entryPoint) => !baseEntrySet.has(entryPoint));
997
+ const generatedEntries = includeGenerated ? ignoredEntries : ignoredEntries.filter((entryPoint) => handlerExportsByEntry.has(entryPoint));
998
+ return {
999
+ isolateEntries,
1000
+ generatedEntries,
1001
+ entryPoints: dedupe([...isolateEntries, ...generatedEntries])
1002
+ };
1003
+ };
1004
+ const filterEntryPointsByPattern = (entryPoints, roots, entryPattern) => {
1005
+ if (!entryPattern) return entryPoints;
1006
+ let regex;
1007
+ try {
1008
+ regex = new RegExp(entryPattern, "i");
1009
+ } catch (error) {
1010
+ const reason = error instanceof Error ? firstLine(error.message) : "invalid regex";
1011
+ throw new Error(`Invalid entry regex "${entryPattern}": ${reason}`);
1012
+ }
1013
+ return entryPoints.filter((entryPoint) => regex.test(path.relative(roots.projectRoot, entryPoint)));
1014
+ };
1015
+ const collectAnalyzeEntrySelection = async (roots, options) => {
1016
+ const allCandidateWithNode = walkDeployEntryPoints(roots.functionsRoot, {
1017
+ includeMultiDot: true,
1018
+ includeGeneratedDir: true
1019
+ });
1020
+ const nodeEntryPoints = allCandidateWithNode.filter((entryPoint) => isNodeEntryPoint(entryPoint, roots.functionsRoot));
1021
+ const nodeEntrySet = new Set(nodeEntryPoints);
1022
+ const allCandidateEntries = allCandidateWithNode.filter((entryPoint) => !nodeEntrySet.has(entryPoint));
1023
+ const baseCandidateEntries = walkDeployEntryPoints(roots.functionsRoot).filter((entryPoint) => !nodeEntrySet.has(entryPoint));
1024
+ const handlerExportsByEntry = await scanHandlerExportsByEntry(allCandidateEntries);
1025
+ const { isolateEntries, generatedEntries, entryPoints } = selectHotspotEntryPoints({
1026
+ baseCandidateEntries,
1027
+ allCandidateEntries,
1028
+ handlerExportsByEntry,
1029
+ includeGenerated: options.includeGenerated
1030
+ });
1031
+ return {
1032
+ nodeEntryPoints,
1033
+ isolateEntries,
1034
+ generatedEntries,
1035
+ entryPoints: filterEntryPointsByPattern(entryPoints, roots, options.entryPattern),
1036
+ handlerExportsByEntry
1037
+ };
1038
+ };
1039
+ const collectHotspotRows = async (roots, options, includeDeepData) => {
1040
+ const { isolateEntries, generatedEntries, entryPoints, handlerExportsByEntry } = await collectAnalyzeEntrySelection(roots, options);
1041
+ const rows = [];
1042
+ for (const entryPoint of entryPoints) try {
1043
+ rows.push({
1044
+ ...await analyzeHotspotEntry(entryPoint, roots.projectRoot, includeDeepData),
1045
+ handlerExports: handlerExportsByEntry.get(entryPoint) ?? []
1046
+ });
1047
+ } catch (error) {
1048
+ rows.push({
1049
+ entry: path.relative(roots.projectRoot, entryPoint),
1050
+ error: error instanceof Error ? error.message : String(error)
1051
+ });
1052
+ }
1053
+ return {
1054
+ isolateEntries,
1055
+ generatedEntries,
1056
+ entryPoints,
1057
+ successRows: rows.filter((row) => !("error" in row)).sort((a, b) => b.outputBytes - a.outputBytes),
1058
+ failedRows: rows.filter((row) => "error" in row),
1059
+ handlerExportsByEntry
1060
+ };
1061
+ };
1062
+ const buildPackageImportGraphRows = (row, limit = 12) => {
1063
+ if (!row.deep) return [];
1064
+ const packageBytes = /* @__PURE__ */ new Map();
1065
+ const packageTargets = /* @__PURE__ */ new Map();
1066
+ for (const input of row.deep.outputInputs) {
1067
+ if (input.bytesInOutput <= 0) continue;
1068
+ const sourcePackage = packageFromInputPath(input.path);
1069
+ packageBytes.set(sourcePackage, (packageBytes.get(sourcePackage) ?? 0) + input.bytesInOutput);
1070
+ const targets = row.deep.importsByInput[input.path] ?? [];
1071
+ if (targets.length === 0) continue;
1072
+ const targetCounts = packageTargets.get(sourcePackage) ?? /* @__PURE__ */ new Map();
1073
+ for (const targetPath of targets) {
1074
+ const targetPackage = packageFromInputPath(targetPath);
1075
+ if (targetPackage === sourcePackage) continue;
1076
+ targetCounts.set(targetPackage, (targetCounts.get(targetPackage) ?? 0) + 1);
1077
+ }
1078
+ packageTargets.set(sourcePackage, targetCounts);
1079
+ }
1080
+ const sorted = Array.from(packageBytes.entries()).map(([packageName, bytesInOutput]) => {
1081
+ const targetMap = packageTargets.get(packageName) ?? /* @__PURE__ */ new Map();
1082
+ return {
1083
+ packageName,
1084
+ bytesInOutput,
1085
+ topTargets: Array.from(targetMap.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 3).map(([target]) => target)
1086
+ };
1087
+ }).sort((a, b) => b.bytesInOutput - a.bytesInOutput);
1088
+ if (!Number.isFinite(limit)) return sorted;
1089
+ return sorted.slice(0, limit);
1090
+ };
1091
+ const padVisible = (value, width) => {
1092
+ const missing = width - visibleLength(value);
1093
+ if (missing <= 0) return value;
1094
+ return `${value}${" ".repeat(missing)}`;
1095
+ };
1096
+ const fillPane = (lines, height) => {
1097
+ const next = [...lines];
1098
+ while (next.length < height) next.push("");
1099
+ return next.slice(0, height);
1100
+ };
1101
+ const buildHandlersPaneLines = (row, width) => {
1102
+ if (!row) return [dim("No selected entry.")];
1103
+ if (row.handlerExports.length === 0) return [dim("No handler exports detected.")];
1104
+ return row.handlerExports.map((name) => `- ${truncate(name, width - 2)}`);
1105
+ };
1106
+ const buildPackagesPaneLines = (row, options, width) => {
1107
+ if (!row) return [dim("No selected entry.")];
1108
+ if (!row.deep) return [dim("No package data loaded.")];
1109
+ const packageRows = buildPackageImportGraphRows(row, Number.POSITIVE_INFINITY);
1110
+ const visibleRows = packageRows.filter((item) => !isSmallInput(item.bytesInOutput, row.outputBytes, options.showSmall));
1111
+ const hiddenCount = packageRows.length - visibleRows.length;
1112
+ const lines = [];
1113
+ if (visibleRows.length === 0) {
1114
+ lines.push(dim("No package rows above small-input threshold."));
1115
+ return lines;
1116
+ }
1117
+ const topRows = visibleRows.slice(0, Math.min(12, options.topPackages));
1118
+ const packageWidth = Math.max(10, width - 26);
1119
+ for (const [index, pkg] of topRows.entries()) {
1120
+ const share = row.outputBytes > 0 ? pkg.bytesInOutput / row.outputBytes * 100 : 0;
1121
+ lines.push(`${pad(String(index + 1), 2, "right")} ${pad(truncate(pkg.packageName, packageWidth), packageWidth)} ${pad(`${toMB(pkg.bytesInOutput)}MB`, 9, "right")} ${pad(`${share.toFixed(2)}%`, 8, "right")}`);
1122
+ if (pkg.topTargets.length > 0) {
1123
+ const targetLabel = pkg.topTargets.join(", ");
1124
+ lines.push(dim(` -> ${truncate(targetLabel, Math.max(12, width - 7))}`));
1125
+ }
1126
+ }
1127
+ if (!options.showSmall && hiddenCount > 0) lines.push(dim(`hidden small packages: ${hiddenCount}`));
1128
+ return lines;
1129
+ };
1130
+ const buildInputsPaneLines = (row, options, width) => {
1131
+ if (!row) return [dim("No selected entry.")];
1132
+ if (!row.deep) return [dim("No input data loaded.")];
1133
+ const internal = row.deep.outputInputs.filter((input) => input.bytesInOutput > 0 && !isNodeModulesInputPath(input.path));
1134
+ const externalBytes = row.deep.outputInputs.filter((input) => input.bytesInOutput > 0 && isNodeModulesInputPath(input.path)).reduce((sum, input) => sum + input.bytesInOutput, 0);
1135
+ const visibleInputs = internal.filter((input) => !isSmallInput(input.bytesInOutput, row.outputBytes, options.showSmall)).slice(0, Math.min(12, options.topInputs));
1136
+ if (visibleInputs.length === 0) {
1137
+ const lines = [dim("No internal inputs above small-input threshold.")];
1138
+ if (externalBytes > 0) {
1139
+ const share = row.outputBytes > 0 ? externalBytes / row.outputBytes * 100 : 0;
1140
+ lines.push(dim(`external deps: ${toMB(externalBytes)}MB (${share.toFixed(2)}%)`));
1141
+ }
1142
+ return lines;
1143
+ }
1144
+ const pathWidth = Math.max(12, width - 24);
1145
+ const lines = visibleInputs.map((input) => {
1146
+ const share = row.outputBytes > 0 ? input.bytesInOutput / row.outputBytes * 100 : 0;
1147
+ return `${pad(`${toMB(input.bytesInOutput)}MB`, 9, "right")} ${pad(`${share.toFixed(2)}%`, 8, "right")} ${truncate(shortPath(input.path), pathWidth)}`;
1148
+ });
1149
+ if (externalBytes > 0) {
1150
+ const share = row.outputBytes > 0 ? externalBytes / row.outputBytes * 100 : 0;
1151
+ lines.push(dim(`external deps: ${toMB(externalBytes)}MB (${share.toFixed(2)}%)`));
1152
+ }
1153
+ return lines;
1154
+ };
1155
+ const buildHelpPaneLines = () => [
1156
+ "j/k move selection",
1157
+ "left/right arrow cycle detail pane",
1158
+ "/ filter entries",
1159
+ "s sort cycle (OutMB, DepMB, Fns)",
1160
+ "g toggle all entries",
1161
+ "r refresh analysis",
1162
+ "w toggle watch mode",
1163
+ "? toggle help overlay",
1164
+ "q quit"
1165
+ ];
1166
+ const runHotspotInteractive = async (roots, options) => {
1167
+ const stdin = process.stdin;
1168
+ const stdout = process.stdout;
1169
+ let state = {
1170
+ selectedIndex: 0,
1171
+ topIndex: 0,
1172
+ filterQuery: "",
1173
+ sortKey: "out",
1174
+ detailPane: "packages",
1175
+ includeGenerated: options.includeGenerated,
1176
+ watchEnabled: false,
1177
+ showHelp: false,
1178
+ statusMessage: ""
1179
+ };
1180
+ let snapshot = await collectHotspotRows(roots, {
1181
+ ...options,
1182
+ includeGenerated: state.includeGenerated,
1183
+ entryPattern: null
1184
+ }, false);
1185
+ let visibleRows = sortHotspotRows(filterHotspotRows(snapshot.successRows, state.filterQuery), state.sortKey);
1186
+ let watcher = null;
1187
+ let watchTimer = null;
1188
+ let refreshing = false;
1189
+ let refreshQueued = false;
1190
+ let shouldQuit = false;
1191
+ let listViewportHeight = 10;
1192
+ const deepRowCache = /* @__PURE__ */ new Map();
1193
+ const getSelectedEntry = () => visibleRows.at(state.selectedIndex)?.entry ?? null;
1194
+ const findRowByEntry = (entry) => {
1195
+ if (!entry) return null;
1196
+ return snapshot.successRows.find((row) => row.entry === entry) ?? null;
1197
+ };
1198
+ const syncVisibleRows = (preferredEntry) => {
1199
+ visibleRows = sortHotspotRows(filterHotspotRows(snapshot.successRows, state.filterQuery), state.sortKey);
1200
+ const nextSelectedIndex = pickSelectedIndex(visibleRows, preferredEntry, state.selectedIndex);
1201
+ const nextTopIndex = fitListViewport(visibleRows.length, nextSelectedIndex, listViewportHeight, state.topIndex);
1202
+ state = reduceInteractiveState(state, {
1203
+ type: "setTopIndex",
1204
+ topIndex: nextTopIndex
1205
+ });
1206
+ state = {
1207
+ ...state,
1208
+ selectedIndex: nextSelectedIndex
1209
+ };
1210
+ };
1211
+ const refreshSnapshot = async (reason) => {
1212
+ if (refreshing) {
1213
+ refreshQueued = true;
1214
+ return;
1215
+ }
1216
+ refreshing = true;
1217
+ state = reduceInteractiveState(state, {
1218
+ type: "setStatus",
1219
+ message: reason
1220
+ });
1221
+ const preferredEntry = getSelectedEntry();
1222
+ try {
1223
+ snapshot = await collectHotspotRows(roots, {
1224
+ ...options,
1225
+ includeGenerated: state.includeGenerated,
1226
+ entryPattern: null
1227
+ }, false);
1228
+ deepRowCache.clear();
1229
+ syncVisibleRows(preferredEntry);
1230
+ state = reduceInteractiveState(state, {
1231
+ type: "setStatus",
1232
+ message: `Refreshed (${snapshot.successRows.length} entries).`
1233
+ });
1234
+ } catch (error) {
1235
+ state = reduceInteractiveState(state, {
1236
+ type: "setStatus",
1237
+ message: `Refresh failed: ${firstLine(error instanceof Error ? error.message : String(error))}`
1238
+ });
1239
+ } finally {
1240
+ refreshing = false;
1241
+ if (refreshQueued) {
1242
+ refreshQueued = false;
1243
+ await refreshSnapshot("Refreshing (queued)...");
1244
+ }
1245
+ }
1246
+ };
1247
+ const stopWatcher = () => {
1248
+ if (watcher) {
1249
+ watcher.close();
1250
+ watcher = null;
1251
+ }
1252
+ if (watchTimer) {
1253
+ clearTimeout(watchTimer);
1254
+ watchTimer = null;
1255
+ }
1256
+ };
1257
+ const applyWatchMode = () => {
1258
+ if (!state.watchEnabled) {
1259
+ stopWatcher();
1260
+ state = reduceInteractiveState(state, {
1261
+ type: "setStatus",
1262
+ message: "Watch mode disabled."
1263
+ });
1264
+ return;
1265
+ }
1266
+ try {
1267
+ watcher = fs.watch(roots.functionsRoot, { recursive: true }, () => {
1268
+ if (watchTimer) clearTimeout(watchTimer);
1269
+ watchTimer = setTimeout(() => {
1270
+ refreshSnapshot("Auto-refresh (file change)...");
1271
+ }, 200);
1272
+ });
1273
+ state = reduceInteractiveState(state, {
1274
+ type: "setStatus",
1275
+ message: "Watch mode enabled."
1276
+ });
1277
+ } catch (error) {
1278
+ state = reduceInteractiveState(state, { type: "toggleWatch" });
1279
+ state = reduceInteractiveState(state, {
1280
+ type: "setStatus",
1281
+ message: `Watch unavailable: ${firstLine(error instanceof Error ? error.message : String(error))}`
1282
+ });
1283
+ }
1284
+ };
1285
+ const promptFilter = async () => {
1286
+ if (typeof stdin.setRawMode === "function") stdin.setRawMode(false);
1287
+ const rl = createInterface({
1288
+ input: stdin,
1289
+ output: stdout
1290
+ });
1291
+ try {
1292
+ const value = await rl.question("Filter entries (empty clears): ");
1293
+ const preferredEntry = getSelectedEntry();
1294
+ state = reduceInteractiveState(state, {
1295
+ type: "setFilter",
1296
+ query: value.trim()
1297
+ });
1298
+ syncVisibleRows(preferredEntry);
1299
+ state = reduceInteractiveState(state, {
1300
+ type: "setStatus",
1301
+ message: state.filterQuery ? `Filter applied: "${state.filterQuery}"` : "Filter cleared."
1302
+ });
1303
+ } finally {
1304
+ rl.close();
1305
+ if (typeof stdin.setRawMode === "function") stdin.setRawMode(true);
1306
+ stdin.resume();
1307
+ stdin.setEncoding("utf8");
1308
+ }
1309
+ };
1310
+ const ensureDeepRow = async (entry) => {
1311
+ if (deepRowCache.has(entry)) return deepRowCache.get(entry) ?? null;
1312
+ const entryPoint = path.join(roots.projectRoot, entry);
1313
+ try {
1314
+ const analyzed = await analyzeHotspotEntry(entryPoint, roots.projectRoot, true);
1315
+ const fallbackHandlers = findRowByEntry(entry)?.handlerExports ?? [];
1316
+ const mergedRow = {
1317
+ ...analyzed,
1318
+ handlerExports: snapshot.handlerExportsByEntry.get(entryPoint) ?? fallbackHandlers
1319
+ };
1320
+ deepRowCache.set(entry, mergedRow);
1321
+ return mergedRow;
1322
+ } catch (error) {
1323
+ deepRowCache.set(entry, null);
1324
+ state = reduceInteractiveState(state, {
1325
+ type: "setStatus",
1326
+ message: `Detail load failed for ${entry}: ${firstLine(error instanceof Error ? error.message : String(error))}`
1327
+ });
1328
+ return null;
1329
+ }
1330
+ };
1331
+ const renderInteractive = async () => {
1332
+ const layout = resolveInteractiveLayout(stdout.columns ?? outputWidth, stdout.rows ?? 30);
1333
+ listViewportHeight = layout.listViewportHeight;
1334
+ state = reduceInteractiveState(state, {
1335
+ type: "setTopIndex",
1336
+ topIndex: fitListViewport(visibleRows.length, state.selectedIndex, layout.listViewportHeight, state.topIndex)
1337
+ });
1338
+ const activeEntry = (visibleRows.at(state.selectedIndex) ?? null)?.entry ?? null;
1339
+ let detailRow = findRowByEntry(activeEntry);
1340
+ if (!state.showHelp && activeEntry && state.detailPane !== "handlers") detailRow = await ensureDeepRow(activeEntry);
1341
+ const statusLine = state.statusMessage || "Ready";
1342
+ const headerLine = `entries=${visibleRows.length}/${snapshot.successRows.length} sort=${sortLabel(state.sortKey)} filter=${state.filterQuery || "∅"} all=${state.includeGenerated ? "on" : "off"} pane=${state.detailPane} watch=${state.watchEnabled ? "on" : "off"}`;
1343
+ const listLines = (() => {
1344
+ const lines = [bold(truncate(`Entries (${visibleRows.length})${state.filterQuery ? ` · filter="${state.filterQuery}"` : ""}`, layout.mode === "split" ? layout.leftWidth : layout.columns))];
1345
+ if (visibleRows.length === 0) lines.push(dim("No entries. Adjust filter or refresh."));
1346
+ else {
1347
+ const start = state.topIndex;
1348
+ const end = Math.min(visibleRows.length, start + layout.listViewportHeight);
1349
+ const labelWidth = (layout.mode === "split" ? layout.leftWidth : layout.columns) - 4;
1350
+ for (let i = start; i < end; i += 1) {
1351
+ const row = visibleRows[i];
1352
+ const marker = i === state.selectedIndex ? colorize("›", ANSI.cyan) : " ";
1353
+ const label = truncate(`${row.entry} · ${toMB(row.outputBytes)}MB · ${row.handlerExports.length} fn`, Math.max(16, labelWidth));
1354
+ lines.push(`${marker} ${label}`);
1355
+ }
1356
+ }
1357
+ return lines;
1358
+ })();
1359
+ const detailTitle = `Detail (${state.detailPane}) · ${state.showHelp ? "help" : activeEntry ? activeEntry : "none"}`;
1360
+ const detailBodyWidth = (layout.mode === "split" ? layout.rightWidth : layout.columns) - 1;
1361
+ const detailBody = state.showHelp ? buildHelpPaneLines() : state.detailPane === "handlers" ? buildHandlersPaneLines(detailRow, detailBodyWidth) : state.detailPane === "packages" ? buildPackagesPaneLines(detailRow, options, detailBodyWidth) : buildInputsPaneLines(detailRow, options, detailBodyWidth);
1362
+ const detailLines = [bold(truncate(detailTitle, detailBodyWidth)), ...detailBody];
1363
+ const keyHints = "j/k move ←/→ pane / filter s sort g all r refresh w watch ? help q quit";
1364
+ stdout.write("\x1B[2J\x1B[H");
1365
+ console.log(bold("better-convex analyze · interactive"));
1366
+ console.log(dim(truncate(headerLine, layout.columns)));
1367
+ if (layout.mode === "split") {
1368
+ const leftPane = fillPane(listLines, layout.bodyHeight);
1369
+ const rightPane = fillPane(detailLines, layout.bodyHeight);
1370
+ for (let i = 0; i < layout.bodyHeight; i += 1) {
1371
+ const left = padVisible(truncate(leftPane[i] ?? "", layout.leftWidth), layout.leftWidth);
1372
+ const right = truncate(rightPane[i] ?? "", layout.rightWidth);
1373
+ console.log(`${left} │ ${right}`);
1374
+ }
1375
+ } else {
1376
+ const listPane = fillPane(listLines, layout.listHeight);
1377
+ const detailPane = fillPane(detailLines, layout.detailHeight);
1378
+ for (const line of listPane) console.log(truncate(line, layout.columns));
1379
+ console.log(dim("-".repeat(layout.columns)));
1380
+ for (const line of detailPane) console.log(truncate(line, layout.columns));
1381
+ }
1382
+ console.log(dim(truncate(keyHints, layout.columns)));
1383
+ console.log(dim(truncate(statusLine, layout.columns)));
1384
+ };
1385
+ const readKey = () => new Promise((resolve) => {
1386
+ stdin.once("data", (data) => resolve(String(data)));
1387
+ });
1388
+ syncVisibleRows(null);
1389
+ if (typeof stdin.setRawMode === "function") stdin.setRawMode(true);
1390
+ stdin.resume();
1391
+ stdin.setEncoding("utf8");
1392
+ try {
1393
+ while (!shouldQuit) {
1394
+ await renderInteractive();
1395
+ const key = await readKey();
1396
+ if (key === "" || key === "q") {
1397
+ shouldQuit = true;
1398
+ continue;
1399
+ }
1400
+ if (key === "j" || key === "\x1B[B") {
1401
+ state = reduceInteractiveState(state, {
1402
+ type: "moveSelection",
1403
+ delta: 1,
1404
+ rowCount: visibleRows.length
1405
+ });
1406
+ continue;
1407
+ }
1408
+ if (key === "k" || key === "\x1B[A") {
1409
+ state = reduceInteractiveState(state, {
1410
+ type: "moveSelection",
1411
+ delta: -1,
1412
+ rowCount: visibleRows.length
1413
+ });
1414
+ continue;
1415
+ }
1416
+ if (key === "/") {
1417
+ await promptFilter();
1418
+ continue;
1419
+ }
1420
+ if (key === "s") {
1421
+ const preferredEntry = getSelectedEntry();
1422
+ state = reduceInteractiveState(state, { type: "cycleSort" });
1423
+ syncVisibleRows(preferredEntry);
1424
+ state = reduceInteractiveState(state, {
1425
+ type: "setStatus",
1426
+ message: `Sort: ${sortLabel(state.sortKey)}`
1427
+ });
1428
+ continue;
1429
+ }
1430
+ if (key === "g") {
1431
+ state = reduceInteractiveState(state, { type: "toggleGenerated" });
1432
+ await refreshSnapshot(state.includeGenerated ? "Refreshing with all entries..." : "Refreshing with function entries only...");
1433
+ continue;
1434
+ }
1435
+ if (key === "\x1B[C") {
1436
+ state = reduceInteractiveState(state, {
1437
+ type: "cyclePane",
1438
+ direction: 1
1439
+ });
1440
+ state = reduceInteractiveState(state, {
1441
+ type: "setStatus",
1442
+ message: `Detail pane: ${state.detailPane}`
1443
+ });
1444
+ continue;
1445
+ }
1446
+ if (key === "\x1B[D") {
1447
+ state = reduceInteractiveState(state, {
1448
+ type: "cyclePane",
1449
+ direction: -1
1450
+ });
1451
+ state = reduceInteractiveState(state, {
1452
+ type: "setStatus",
1453
+ message: `Detail pane: ${state.detailPane}`
1454
+ });
1455
+ continue;
1456
+ }
1457
+ if (key === "r") {
1458
+ state = reduceInteractiveState(state, { type: "requestRefresh" });
1459
+ await refreshSnapshot(state.statusMessage);
1460
+ continue;
1461
+ }
1462
+ if (key === "w") {
1463
+ state = reduceInteractiveState(state, { type: "toggleWatch" });
1464
+ applyWatchMode();
1465
+ continue;
1466
+ }
1467
+ if (key === "?") state = reduceInteractiveState(state, { type: "toggleHelp" });
1468
+ }
1469
+ } finally {
1470
+ stopWatcher();
1471
+ if (typeof stdin.setRawMode === "function") stdin.setRawMode(false);
1472
+ stdin.pause();
1473
+ stdout.write("\x1B[2J\x1B[H");
1474
+ }
1475
+ return 0;
1476
+ };
1477
+ const runHotspotAnalysis = async (roots, options) => {
1478
+ if (shouldPromptForHotspotPick(options)) return runHotspotInteractive(roots, options);
1479
+ const includeDeepData = options.details;
1480
+ const { isolateEntries, generatedEntries, entryPoints, successRows, failedRows } = await collectHotspotRows(roots, options, includeDeepData);
1481
+ if (entryPoints.length === 0) {
1482
+ if (options.includeGenerated) console.log("No matching entries found for the provided regex.");
1483
+ else console.log("No matching Convex handler entries found (files exporting query/mutation/action, including internal variants).");
1484
+ return 0;
1485
+ }
1486
+ const outputTotal = successRows.reduce((sum, row) => sum + row.outputBytes, 0);
1487
+ const outputAverage = successRows.length > 0 ? outputTotal / successRows.length : 0;
1488
+ const totalHandlers = successRows.reduce((sum, row) => sum + row.handlerExports.length, 0);
1489
+ const largest = successRows.at(0);
1490
+ const buildHotspotDetailCommand = (entry, mode) => {
1491
+ const parts = [
1492
+ "better-convex",
1493
+ "analyze",
1494
+ shellQuote(`^${escapeRegex(entry)}$`),
1495
+ "--details"
1496
+ ];
1497
+ if (options.showInputs) parts.push("--input");
1498
+ if (options.includeGenerated) parts.push("--all");
1499
+ if (options.detailEntries !== DEFAULT_DETAIL_ENTRIES) parts.push("--top", String(options.detailEntries));
1500
+ if (mode === "agent") parts.push("--top-inputs", "30", "--top-packages", "20");
1501
+ return parts.join(" ");
1502
+ };
1503
+ const actionRows = successRows;
1504
+ console.log(bold("Runtime hotspot analysis (least optimized functions)"));
1505
+ console.log(`isolateEntries=${isolateEntries.length} handlers=${totalHandlers} extraAll=${generatedEntries.length} selected=${entryPoints.length} ok=${successRows.length} failed=${failedRows.length} avg=${formatBytes(outputAverage)}${largest ? ` largest=${largest.entry} (${formatBytes(largest.outputBytes)})` : ""}`);
1506
+ console.log("");
1507
+ if (actionRows.length > 0) {
1508
+ console.log(bold("Agent queue (run top-down):"));
1509
+ for (const [index, row] of actionRows.entries()) console.log(`${index + 1}. ${row.entry} (${toMB(row.outputBytes)} MB, ${row.handlerExports.length} handlers) -> ${buildHotspotDetailCommand(row.entry, "agent")}`);
1510
+ if (options.interactive === "always" && !isInteractiveTerminal) {
1511
+ console.log("");
1512
+ console.log(dim("Interactive picker requires a TTY. Falling back to command list."));
1513
+ }
1514
+ console.log("");
1515
+ }
1516
+ if (successRows.length > 0) {
1517
+ const maxEntryWidth = Math.max(16, outputWidth - 42 - 14);
1518
+ const widths = {
1519
+ rank: 4,
1520
+ sev: 7,
1521
+ output: 7,
1522
+ deps: 7,
1523
+ local: 7,
1524
+ inputCount: 6,
1525
+ handlerCount: 4,
1526
+ entry: Math.max(16, Math.min(maxEntryWidth, Math.max(...successRows.map((row) => row.entry.length), 5)))
1527
+ };
1528
+ const header = [
1529
+ pad("Rank", widths.rank, "right"),
1530
+ pad("Level", widths.sev),
1531
+ pad("OutMB", widths.output, "right"),
1532
+ pad("DepMB", widths.deps, "right"),
1533
+ pad("LocMB", widths.local, "right"),
1534
+ pad("Files", widths.inputCount, "right"),
1535
+ pad("Fns", widths.handlerCount, "right"),
1536
+ pad("Entry", widths.entry)
1537
+ ].join(" ");
1538
+ const divider = [
1539
+ "-".repeat(widths.rank),
1540
+ "-".repeat(widths.sev),
1541
+ "-".repeat(widths.output),
1542
+ "-".repeat(widths.deps),
1543
+ "-".repeat(widths.local),
1544
+ "-".repeat(widths.inputCount),
1545
+ "-".repeat(widths.handlerCount),
1546
+ "-".repeat(widths.entry)
1547
+ ].join(" ");
1548
+ console.log(dim(header));
1549
+ console.log(dim(divider));
1550
+ for (const [index, row] of successRows.entries()) {
1551
+ const severity = severityForBytes(row.outputBytes, options);
1552
+ const outputMbValue = Number.parseFloat(toMB(row.outputBytes));
1553
+ console.log([
1554
+ pad(String(index + 1), widths.rank, "right"),
1555
+ colorizePadded(severity, widths.sev, "left", severityColor(severity)),
1556
+ colorizePadded(toMB(row.outputBytes), widths.output, "right", shareColor(outputMbValue / options.dangerMb * 100)),
1557
+ pad(toMB(row.dependencyInputBytes), widths.deps, "right"),
1558
+ pad(toMB(row.localInputBytes), widths.local, "right"),
1559
+ pad(String(row.inputCount), widths.inputCount, "right"),
1560
+ pad(String(row.handlerExports.length), widths.handlerCount, "right"),
1561
+ pad(truncate(row.entry, widths.entry), widths.entry)
1562
+ ].join(" "));
1563
+ }
1564
+ }
1565
+ if (failedRows.length > 0) {
1566
+ console.log(`\n${colorize("Failed entries:", ANSI.red)}`);
1567
+ for (const row of failedRows) {
1568
+ const errorLine = firstLine(row.error);
1569
+ const available = Math.max(24, outputWidth - row.entry.length - 4);
1570
+ console.log(`- ${row.entry}: ${truncate(errorLine, available)}`);
1571
+ }
1572
+ }
1573
+ if (includeDeepData) {
1574
+ const detailRows = options.entryPattern ? successRows : successRows.slice(0, options.detailEntries);
1575
+ if (!options.entryPattern && successRows.length > detailRows.length) {
1576
+ console.log("");
1577
+ console.log(dim(`Expanded details for top ${detailRows.length} entries. Pass an entry regex as first arg for a specific module.`));
1578
+ }
1579
+ for (const row of detailRows) {
1580
+ if (row.handlerExports.length > 0) {
1581
+ console.log("");
1582
+ printWrapped({
1583
+ prefix: `${bold("Handlers:")} `,
1584
+ text: row.handlerExports.join(", ")
1585
+ });
1586
+ }
1587
+ printHotspotPackages(row, options);
1588
+ if (options.showInputs) printHotspotTopInputs(row, options);
1589
+ }
1590
+ }
1591
+ let exitCode = 0;
1592
+ if (failedRows.length > 0) exitCode = 1;
1593
+ if (options.failMb !== null && largest && largest.outputBytes / MB >= options.failMb) {
1594
+ console.log("");
1595
+ console.log(colorize(`Fail threshold reached: largest output is ${toMB(largest.outputBytes)} MB (>= ${options.failMb.toFixed(2)} MB).`, ANSI.red));
1596
+ exitCode = 1;
1597
+ }
1598
+ return exitCode;
1599
+ };
1600
+ const buildDeployBundle = async (entryPoints, externalizeSchema) => build({
1601
+ bundle: true,
1602
+ entryPoints,
1603
+ format: "esm",
1604
+ platform: "browser",
1605
+ target: ["esnext"],
1606
+ write: false,
1607
+ metafile: true,
1608
+ logLevel: "silent",
1609
+ outdir: "out",
1610
+ splitting: true,
1611
+ jsx: "automatic",
1612
+ conditions: ["convex", "module"],
1613
+ minifySyntax: true,
1614
+ minifyIdentifiers: true,
1615
+ minifyWhitespace: false,
1616
+ define: { "process.env.NODE_ENV": "\"production\"" },
1617
+ plugins: externalizeSchema ? [schemaExternalFallbackPlugin] : []
1618
+ });
1619
+ const printDeployTopInputs = (outputBytes, aggregateInputBytes, options) => {
1620
+ const topInputs = aggregateInputBytes.filter((input) => !isSmallInput(input.bytes, outputBytes, options.showSmall)).slice(0, options.topInputs);
1621
+ if (topInputs.length === 0) return;
1622
+ console.log("");
1623
+ console.log(bold("Top inputs (deploy bundle):"));
1624
+ const inputPathWidth = Math.max(18, outputWidth - 46);
1625
+ console.log(dim(`bytesInOutput share impact ${pad("path", inputPathWidth)}`));
1626
+ console.log(dim(`------------- ---------- ---------------- ${"-".repeat(inputPathWidth)}`));
1627
+ for (const input of topInputs) {
1628
+ const share = outputBytes > 0 ? input.bytes / outputBytes * 100 : 0;
1629
+ const sharePct = colorize(`${share.toFixed(2)}%`.padStart(10), shareColor(share));
1630
+ const bar = makeShareBar(share);
1631
+ console.log(`${input.bytes.toString().padStart(13)} ${sharePct} ${bar} ${compactPath(input.inputPath, inputPathWidth)}`);
1632
+ }
1633
+ };
1634
+ const printDeployTopPackages = (outputBytes, aggregateInputBytes, options) => {
1635
+ const packageMap = /* @__PURE__ */ new Map();
1636
+ for (const item of aggregateInputBytes) {
1637
+ const packageName = packageFromInputPath(item.inputPath);
1638
+ const current = packageMap.get(packageName) ?? { bytes: 0 };
1639
+ current.bytes += item.bytes;
1640
+ packageMap.set(packageName, current);
1641
+ }
1642
+ const topRows = Array.from(packageMap.entries()).map(([packageName, value]) => ({
1643
+ packageName,
1644
+ bytes: value.bytes
1645
+ })).sort((a, b) => b.bytes - a.bytes).filter((item) => !isSmallInput(item.bytes, outputBytes, options.showSmall)).slice(0, options.topPackages);
1646
+ if (topRows.length === 0) return;
1647
+ console.log("");
1648
+ console.log(bold("Top packages (deploy bundle):"));
1649
+ const packageColWidth = Math.max(16, Math.min(30, outputWidth - 58));
1650
+ const barWidth = Math.max(8, Math.min(16, outputWidth - (packageColWidth + 52)));
1651
+ for (const [index, item] of topRows.entries()) {
1652
+ const share = outputBytes > 0 ? item.bytes / outputBytes * 100 : 0;
1653
+ const shareStr = colorize(`${share.toFixed(2)}%`, shareColor(share));
1654
+ const sizeStr = colorize(`${toMB(item.bytes)} MB`, shareColor(share));
1655
+ const bar = makeShareBar(share, barWidth);
1656
+ const packageLabel = truncate(item.packageName, packageColWidth);
1657
+ console.log(`${bold(`${index + 1}.`)} ${pad(packageLabel, packageColWidth)} ${pad(sizeStr, 18)} ${pad(shareStr, 10)} ${bar}`);
1658
+ }
1659
+ };
1660
+ const runDeployAnalysis = async (roots, options) => {
1661
+ const { entryPoints, nodeEntryPoints } = await collectAnalyzeEntrySelection(roots, options);
1662
+ if (entryPoints.length === 0) {
1663
+ console.log("No Convex handler entries found to analyze (files exporting query/mutation/action, including internal variants).");
1664
+ return 0;
1665
+ }
1666
+ let result;
1667
+ let schemaExternalized = false;
1668
+ try {
1669
+ result = await buildDeployBundle(entryPoints, false);
1670
+ } catch (error) {
1671
+ if (!shouldRetryWithSchemaExternalized(error)) throw error;
1672
+ result = await buildDeployBundle(entryPoints, true);
1673
+ schemaExternalized = true;
1674
+ }
1675
+ const meta = result.metafile;
1676
+ if (!meta) throw new Error("No metafile generated for deploy analysis.");
1677
+ const jsOutputs = Object.entries(meta.outputs).filter(([outputPath]) => !outputPath.endsWith(".map")).map(([outputPath, value]) => ({
1678
+ outputPath,
1679
+ ...value
1680
+ })).filter((output) => output.outputPath.endsWith(".js"));
1681
+ const totalOutputBytes = jsOutputs.reduce((sum, output) => sum + output.bytes, 0);
1682
+ const entryOutputs = jsOutputs.filter((output) => !!output.entryPoint).map((output) => ({
1683
+ outputPath: output.outputPath,
1684
+ entryPoint: output.entryPoint,
1685
+ bytes: output.bytes,
1686
+ inputCount: Object.keys(output.inputs ?? {}).length
1687
+ })).sort((a, b) => b.bytes - a.bytes);
1688
+ const sharedChunks = jsOutputs.filter((output) => !output.entryPoint).map((output) => ({
1689
+ outputPath: output.outputPath,
1690
+ bytes: output.bytes
1691
+ })).sort((a, b) => b.bytes - a.bytes);
1692
+ const aggregateInputs = /* @__PURE__ */ new Map();
1693
+ for (const output of jsOutputs) for (const [inputPath, value] of Object.entries(output.inputs ?? {})) {
1694
+ const bytesInOutput = value.bytesInOutput ?? 0;
1695
+ aggregateInputs.set(inputPath, (aggregateInputs.get(inputPath) ?? 0) + bytesInOutput);
1696
+ }
1697
+ const aggregateInputRows = Array.from(aggregateInputs.entries()).map(([inputPath, bytes]) => ({
1698
+ inputPath,
1699
+ bytes
1700
+ })).sort((a, b) => b.bytes - a.bytes);
1701
+ const largestEntry = entryOutputs.at(0);
1702
+ const largestChunk = sharedChunks.at(0);
1703
+ console.log(bold("Runtime deploy analysis (isolate bundle)"));
1704
+ console.log(`entries=${entryPoints.length} nodeEntriesSkipped=${nodeEntryPoints.length} outputs=${jsOutputs.length} total=${formatBytes(totalOutputBytes)}`);
1705
+ if (schemaExternalized) console.log(colorize("note: schema imports were externalized after a build error; dependency sizes are approximate.", ANSI.yellow));
1706
+ if (largestEntry) console.log(`largestEntry=${shortPath(largestEntry.entryPoint)} (${formatBytes(largestEntry.bytes)})`);
1707
+ if (largestChunk) console.log(`largestSharedChunk=${shortPath(largestChunk.outputPath)} (${formatBytes(largestChunk.bytes)})`);
1708
+ console.log("");
1709
+ const topEntries = entryOutputs.slice(0, 15);
1710
+ if (topEntries.length > 0) {
1711
+ const widths = {
1712
+ rank: 4,
1713
+ sev: 7,
1714
+ output: 7,
1715
+ inputs: 6,
1716
+ entry: Math.max(24, Math.min(72, outputWidth - 34))
1717
+ };
1718
+ console.log(dim(`${pad("Rank", widths.rank, "right")} ${pad("Level", widths.sev)} ${pad("OutMB", widths.output, "right")} ${pad("In", widths.inputs, "right")} ${pad("Entry", widths.entry)}`));
1719
+ console.log(dim(`${"-".repeat(widths.rank)} ${"-".repeat(widths.sev)} ${"-".repeat(widths.output)} ${"-".repeat(widths.inputs)} ${"-".repeat(widths.entry)}`));
1720
+ for (const [index, row] of topEntries.entries()) {
1721
+ const severity = severityForBytes(row.bytes, options);
1722
+ console.log(`${pad(String(index + 1), widths.rank, "right")} ${colorizePadded(severity, widths.sev, "left", severityColor(severity))} ${colorizePadded(toMB(row.bytes), widths.output, "right", shareColor(row.bytes / MB / options.dangerMb * 100))} ${pad(String(row.inputCount), widths.inputs, "right")} ${pad(truncate(shortPath(row.entryPoint), widths.entry), widths.entry)}`);
1723
+ }
1724
+ }
1725
+ const topChunks = sharedChunks.slice(0, 8);
1726
+ if (topChunks.length > 0) {
1727
+ console.log("");
1728
+ console.log(bold("Top shared chunks:"));
1729
+ for (const chunk of topChunks) {
1730
+ const share = totalOutputBytes > 0 ? chunk.bytes / totalOutputBytes * 100 : 0;
1731
+ const severity = severityForBytes(chunk.bytes, options);
1732
+ console.log(`${colorize(`[${severity}]`, severityColor(severity))} ${pad(`${toMB(chunk.bytes)} MB`, 10, "right")} ${pad(`${share.toFixed(2)}%`, 8, "right")} ${shortPath(chunk.outputPath)}`);
1733
+ }
1734
+ }
1735
+ if (options.details) {
1736
+ printDeployTopPackages(totalOutputBytes, aggregateInputRows, options);
1737
+ printDeployTopInputs(totalOutputBytes, aggregateInputRows, options);
1738
+ }
1739
+ const largestBytes = Math.max(largestEntry?.bytes ?? 0, largestChunk?.bytes ?? 0);
1740
+ if (options.failMb !== null && largestBytes / MB >= options.failMb) {
1741
+ console.log("");
1742
+ printWrapped({
1743
+ text: `Fail threshold reached: largest output is ${toMB(largestBytes)} MB (>= ${options.failMb.toFixed(2)} MB).`,
1744
+ color: ANSI.red
1745
+ });
1746
+ return 1;
1747
+ }
1748
+ return 0;
1749
+ };
1750
+ async function runAnalyze(argv) {
1751
+ const options = parseArgs$1(argv);
1752
+ colorEnabled = supportsColor;
1753
+ outputWidth = options.width ?? process.stdout.columns ?? DEFAULT_OUTPUT_WIDTH;
1754
+ const roots = detectProjectRoots();
1755
+ if (options.mode === "hotspot") return runHotspotAnalysis(roots, options);
1756
+ return runDeployAnalysis(roots, options);
1757
+ }
1758
+
1759
+ //#endregion
1760
+ //#region src/cli/config.ts
1761
+ const DEFAULT_CONFIG_PATH = "better-convex.json";
1762
+ const CODEGEN_SCOPES = new Set([
1763
+ "all",
1764
+ "auth",
1765
+ "orm"
1766
+ ]);
1767
+ const BACKFILL_ENABLED_VALUES = new Set([
1768
+ "auto",
1769
+ "on",
1770
+ "off"
1771
+ ]);
1772
+ function createDefaultConfig() {
1773
+ return {
1774
+ api: true,
1775
+ auth: true,
1776
+ outputDir: "convex/shared",
1777
+ dev: {
1778
+ debug: false,
1779
+ convexArgs: [],
1780
+ aggregateBackfill: {
1781
+ enabled: "auto",
1782
+ wait: true,
1783
+ batchSize: 1e3,
1784
+ pollIntervalMs: 1e3,
1785
+ timeoutMs: 9e5,
1786
+ strict: false
1787
+ }
1788
+ },
1789
+ codegen: {
1790
+ debug: false,
1791
+ convexArgs: []
1792
+ },
1793
+ deploy: {
1794
+ convexArgs: [],
1795
+ aggregateBackfill: {
1796
+ enabled: "auto",
1797
+ wait: true,
1798
+ batchSize: 1e3,
1799
+ pollIntervalMs: 1e3,
1800
+ timeoutMs: 9e5,
1801
+ strict: true
1802
+ }
1803
+ }
1804
+ };
1805
+ }
1806
+ function isRecord(value) {
1807
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1808
+ }
1809
+ function parseBoolean(value, fieldName, configPath) {
1810
+ if (typeof value === "boolean") return value;
1811
+ throw new Error(`Invalid ${fieldName} in ${configPath}: expected boolean, got ${typeof value}.`);
1812
+ }
1813
+ function parseString(value, fieldName, configPath) {
1814
+ if (typeof value === "string" && value.length > 0) return value;
1815
+ throw new Error(`Invalid ${fieldName} in ${configPath}: expected non-empty string.`);
1816
+ }
1817
+ function parseStringArray(value, fieldName, configPath) {
1818
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new Error(`Invalid ${fieldName} in ${configPath}: expected string array.`);
1819
+ return [...value];
1820
+ }
1821
+ function parsePositiveInteger(value, fieldName, configPath) {
1822
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
1823
+ throw new Error(`Invalid ${fieldName} in ${configPath}: expected a positive integer.`);
1824
+ }
1825
+ function parseScope(value, fieldName, configPath) {
1826
+ if (typeof value === "string" && CODEGEN_SCOPES.has(value)) return value;
1827
+ throw new Error(`Invalid ${fieldName} in ${configPath}: expected one of all, auth, orm.`);
1828
+ }
1829
+ function parseBackfillEnabled(value, fieldName, configPath) {
1830
+ if (value === true) return "on";
1831
+ if (value === false) return "off";
1832
+ if (typeof value === "string" && BACKFILL_ENABLED_VALUES.has(value)) return value;
1833
+ throw new Error(`Invalid ${fieldName} in ${configPath}: expected boolean or one of auto, on, off.`);
1834
+ }
1835
+ function parseAggregateBackfillConfig(value, fieldName, configPath) {
1836
+ if (!isRecord(value)) throw new Error(`Invalid ${fieldName} in ${configPath}: expected object.`);
1837
+ const parsed = {};
1838
+ if ("enabled" in value) parsed.enabled = parseBackfillEnabled(value.enabled, `${fieldName}.enabled`, configPath);
1839
+ if ("wait" in value) parsed.wait = parseBoolean(value.wait, `${fieldName}.wait`, configPath);
1840
+ if ("batchSize" in value) parsed.batchSize = parsePositiveInteger(value.batchSize, `${fieldName}.batchSize`, configPath);
1841
+ if ("pollIntervalMs" in value) parsed.pollIntervalMs = parsePositiveInteger(value.pollIntervalMs, `${fieldName}.pollIntervalMs`, configPath);
1842
+ if ("timeoutMs" in value) parsed.timeoutMs = parsePositiveInteger(value.timeoutMs, `${fieldName}.timeoutMs`, configPath);
1843
+ if ("strict" in value) parsed.strict = parseBoolean(value.strict, `${fieldName}.strict`, configPath);
1844
+ return parsed;
1845
+ }
1846
+ function parseCommandConfig(value, fieldName, configPath) {
1847
+ if (!isRecord(value)) throw new Error(`Invalid ${fieldName} in ${configPath}: expected object.`);
1848
+ const parsed = {};
1849
+ if ("debug" in value) parsed.debug = parseBoolean(value.debug, `${fieldName}.debug`, configPath);
1850
+ if ("convexArgs" in value) parsed.convexArgs = parseStringArray(value.convexArgs, `${fieldName}.convexArgs`, configPath);
1851
+ if (fieldName === "codegen" && "scope" in value && value.scope !== void 0) parsed.scope = parseScope(value.scope, `${fieldName}.scope`, configPath);
1852
+ if (fieldName === "dev" && "aggregateBackfill" in value && value.aggregateBackfill !== void 0) parsed.aggregateBackfill = parseAggregateBackfillConfig(value.aggregateBackfill, `${fieldName}.aggregateBackfill`, configPath);
1853
+ return parsed;
1854
+ }
1855
+ function parseDeployConfig(value, configPath) {
1856
+ if (!isRecord(value)) throw new Error(`Invalid deploy in ${configPath}: expected object.`);
1857
+ const parsed = {};
1858
+ if ("convexArgs" in value) parsed.convexArgs = parseStringArray(value.convexArgs, "deploy.convexArgs", configPath);
1859
+ if ("aggregateBackfill" in value && value.aggregateBackfill !== void 0) parsed.aggregateBackfill = parseAggregateBackfillConfig(value.aggregateBackfill, "deploy.aggregateBackfill", configPath);
1860
+ return parsed;
1861
+ }
1862
+ function loadBetterConvexConfig(configPathArg) {
1863
+ const resolvedConfigPath = path.resolve(process.cwd(), configPathArg ?? DEFAULT_CONFIG_PATH);
1864
+ const hasExplicitConfigPath = typeof configPathArg === "string";
1865
+ if (!fs.existsSync(resolvedConfigPath)) {
1866
+ if (hasExplicitConfigPath) throw new Error(`Config file not found: ${resolvedConfigPath}`);
1867
+ return createDefaultConfig();
1868
+ }
1869
+ let rawConfig;
1870
+ try {
1871
+ rawConfig = JSON.parse(fs.readFileSync(resolvedConfigPath, "utf-8"));
1872
+ } catch (error) {
1873
+ throw new Error(`Failed to parse config file ${resolvedConfigPath}: ${error.message}`);
1874
+ }
1875
+ if (!isRecord(rawConfig)) throw new Error(`Invalid config file ${resolvedConfigPath}: expected top-level object.`);
1876
+ const config = createDefaultConfig();
1877
+ if ("api" in rawConfig) config.api = parseBoolean(rawConfig.api, "api", resolvedConfigPath);
1878
+ if ("auth" in rawConfig) config.auth = parseBoolean(rawConfig.auth, "auth", resolvedConfigPath);
1879
+ if ("outputDir" in rawConfig) config.outputDir = parseString(rawConfig.outputDir, "outputDir", resolvedConfigPath);
1880
+ if ("dev" in rawConfig) {
1881
+ const parsed = parseCommandConfig(rawConfig.dev, "dev", resolvedConfigPath);
1882
+ if (parsed.debug !== void 0) config.dev.debug = parsed.debug;
1883
+ if (parsed.convexArgs !== void 0) config.dev.convexArgs = parsed.convexArgs;
1884
+ if (parsed.aggregateBackfill !== void 0) config.dev.aggregateBackfill = {
1885
+ ...config.dev.aggregateBackfill,
1886
+ ...parsed.aggregateBackfill
1887
+ };
1888
+ }
1889
+ if ("codegen" in rawConfig) {
1890
+ const parsed = parseCommandConfig(rawConfig.codegen, "codegen", resolvedConfigPath);
1891
+ if (parsed.debug !== void 0) config.codegen.debug = parsed.debug;
1892
+ if (parsed.convexArgs !== void 0) config.codegen.convexArgs = parsed.convexArgs;
1893
+ if (parsed.scope !== void 0) config.codegen.scope = parsed.scope;
1894
+ }
1895
+ if ("deploy" in rawConfig) {
1896
+ const parsed = parseDeployConfig(rawConfig.deploy, resolvedConfigPath);
1897
+ if (parsed.convexArgs !== void 0) config.deploy.convexArgs = parsed.convexArgs;
1898
+ if (parsed.aggregateBackfill !== void 0) config.deploy.aggregateBackfill = {
1899
+ ...config.deploy.aggregateBackfill,
1900
+ ...parsed.aggregateBackfill
1901
+ };
1902
+ }
1903
+ return config;
1904
+ }
1905
+
1906
+ //#endregion
11
1907
  //#region src/cli/env.ts
12
1908
  async function syncEnv(options = {}) {
13
1909
  const { auth = false, force = false, prod = false } = options;
@@ -111,9 +2007,20 @@ async function syncEnv(options = {}) {
111
2007
  const __filename = fileURLToPath(import.meta.url);
112
2008
  const __dirname = dirname(__filename);
113
2009
  const realConvex = join(dirname(createRequire(import.meta.url).resolve("convex/package.json")), "bin/main.js");
2010
+ const MISSING_BACKFILL_FUNCTION_RE = /could not find function|function .* was not found|unknown function/i;
2011
+ const GITIGNORE_CONVEX_ENTRY_RE = /(^|\r?\n)\.convex\/?\s*(\r?\n|$)/m;
2012
+ const AGGREGATE_STATE_RELATIVE_PATH = join(".convex", "better-convex", "aggregate-backfill-state.json");
2013
+ const AGGREGATE_STATE_VERSION = 1;
2014
+ const VALID_SCOPES = new Set([
2015
+ "all",
2016
+ "auth",
2017
+ "orm"
2018
+ ]);
114
2019
  function parseArgs(argv) {
115
2020
  let debug = false;
116
2021
  let outputDir;
2022
+ let scope;
2023
+ let configPath;
117
2024
  const filtered = [];
118
2025
  for (let i = 0; i < argv.length; i++) {
119
2026
  const a = argv[i];
@@ -121,8 +2028,24 @@ function parseArgs(argv) {
121
2028
  debug = true;
122
2029
  continue;
123
2030
  }
124
- if (a === "--meta") {
125
- outputDir = argv[i + 1];
2031
+ if (a === "--api") {
2032
+ const value = argv[i + 1];
2033
+ if (!value) throw new Error("Missing value for --api.");
2034
+ outputDir = value;
2035
+ i += 1;
2036
+ continue;
2037
+ }
2038
+ if (a === "--scope") {
2039
+ const value = argv[i + 1];
2040
+ if (!value || !VALID_SCOPES.has(value)) throw new Error(`Invalid --scope value "${value ?? ""}". Expected one of: all, auth, orm.`);
2041
+ scope = value;
2042
+ i += 1;
2043
+ continue;
2044
+ }
2045
+ if (a === "--config") {
2046
+ const value = argv[i + 1];
2047
+ if (!value) throw new Error("Missing value for --config.");
2048
+ configPath = value;
126
2049
  i += 1;
127
2050
  continue;
128
2051
  }
@@ -135,24 +2058,519 @@ function parseArgs(argv) {
135
2058
  restArgs,
136
2059
  convexArgs: restArgs,
137
2060
  debug,
138
- outputDir
2061
+ outputDir,
2062
+ scope,
2063
+ configPath
139
2064
  };
140
2065
  }
2066
+ const DEFAULT_AGGREGATE_FINGERPRINT_STATE = {
2067
+ version: AGGREGATE_STATE_VERSION,
2068
+ entries: {}
2069
+ };
2070
+ function normalizeStringList(values) {
2071
+ if (!Array.isArray(values)) return [];
2072
+ return [...new Set(values.filter((value) => typeof value === "string"))].sort();
2073
+ }
2074
+ function readOptionalCliFlagValue(args, flag) {
2075
+ for (let i = 0; i < args.length; i += 1) {
2076
+ const arg = args[i];
2077
+ if (arg === flag) {
2078
+ const value = args[i + 1];
2079
+ if (value) return value;
2080
+ continue;
2081
+ }
2082
+ const withEquals = `${flag}=`;
2083
+ if (arg.startsWith(withEquals)) {
2084
+ const value = arg.slice(withEquals.length);
2085
+ if (value) return value;
2086
+ }
2087
+ }
2088
+ }
2089
+ function collectSchemaTables(schemaModule) {
2090
+ const allTables = /* @__PURE__ */ new Set();
2091
+ const relations = schemaModule.relations;
2092
+ if (relations && typeof relations === "object" && !Array.isArray(relations)) for (const relationConfig of Object.values(relations)) {
2093
+ const table = relationConfig?.table;
2094
+ if (table) allTables.add(table);
2095
+ }
2096
+ const tables = schemaModule.tables;
2097
+ if (tables && typeof tables === "object" && !Array.isArray(tables)) {
2098
+ for (const table of Object.values(tables)) if (table) allTables.add(table);
2099
+ }
2100
+ return [...allTables];
2101
+ }
2102
+ function buildAggregateFingerprintPayload(tables) {
2103
+ return tables.map((table) => getTableConfig(table)).map((tableConfig) => ({
2104
+ tableName: tableConfig.name,
2105
+ aggregateIndexes: tableConfig.aggregateIndexes.map((index) => ({
2106
+ name: index.name,
2107
+ fields: normalizeStringList(index.fields),
2108
+ countFields: normalizeStringList(index.countFields),
2109
+ sumFields: normalizeStringList(index.sumFields),
2110
+ avgFields: normalizeStringList(index.avgFields),
2111
+ minFields: normalizeStringList(index.minFields),
2112
+ maxFields: normalizeStringList(index.maxFields)
2113
+ })).sort((a, b) => a.name.localeCompare(b.name))
2114
+ })).sort((a, b) => a.tableName.localeCompare(b.tableName));
2115
+ }
2116
+ async function computeAggregateIndexFingerprint(functionsDir) {
2117
+ const schemaPath = join(functionsDir, "schema.ts");
2118
+ if (!fs.existsSync(schemaPath)) return null;
2119
+ const schemaModule = await createJiti(process.cwd(), {
2120
+ interopDefault: true,
2121
+ moduleCache: false
2122
+ }).import(schemaPath);
2123
+ if (!schemaModule || typeof schemaModule !== "object") return null;
2124
+ const tables = collectSchemaTables(schemaModule);
2125
+ if (tables.length === 0) return null;
2126
+ const payload = buildAggregateFingerprintPayload(tables);
2127
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
2128
+ }
2129
+ function getDevAggregateBackfillStatePath(cwd = process.cwd()) {
2130
+ return join(cwd, AGGREGATE_STATE_RELATIVE_PATH);
2131
+ }
2132
+ function readAggregateFingerprintState(statePath) {
2133
+ if (!fs.existsSync(statePath)) return {
2134
+ ...DEFAULT_AGGREGATE_FINGERPRINT_STATE,
2135
+ entries: {}
2136
+ };
2137
+ try {
2138
+ const raw = fs.readFileSync(statePath, "utf8");
2139
+ const parsed = JSON.parse(raw);
2140
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.entries !== "object" || parsed.entries === null) return {
2141
+ ...DEFAULT_AGGREGATE_FINGERPRINT_STATE,
2142
+ entries: {}
2143
+ };
2144
+ return {
2145
+ version: AGGREGATE_STATE_VERSION,
2146
+ entries: Object.fromEntries(Object.entries(parsed.entries).filter(([, value]) => typeof value === "object" && value !== null && typeof value.fingerprint === "string"))
2147
+ };
2148
+ } catch {
2149
+ return {
2150
+ ...DEFAULT_AGGREGATE_FINGERPRINT_STATE,
2151
+ entries: {}
2152
+ };
2153
+ }
2154
+ }
2155
+ function writeAggregateFingerprintState(statePath, state) {
2156
+ fs.mkdirSync(dirname(statePath), { recursive: true });
2157
+ const tmpPath = `${statePath}.tmp`;
2158
+ fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
2159
+ fs.renameSync(tmpPath, statePath);
2160
+ }
2161
+ function getAggregateBackfillDeploymentKey(args) {
2162
+ if (args.includes("--prod")) return "prod";
2163
+ const deploymentName = readOptionalCliFlagValue(args, "--deployment-name");
2164
+ if (deploymentName) return `deployment:${deploymentName}`;
2165
+ const previewName = readOptionalCliFlagValue(args, "--preview-name");
2166
+ if (previewName) return `preview:${previewName}`;
2167
+ return "local";
2168
+ }
2169
+ function ensureConvexGitignoreEntry(cwd = process.cwd()) {
2170
+ let currentDir = resolve(cwd);
2171
+ let gitRoot = null;
2172
+ while (true) {
2173
+ if (fs.existsSync(join(currentDir, ".git"))) {
2174
+ gitRoot = currentDir;
2175
+ break;
2176
+ }
2177
+ const parent = dirname(currentDir);
2178
+ if (parent === currentDir) break;
2179
+ currentDir = parent;
2180
+ }
2181
+ if (!gitRoot) return;
2182
+ const gitignorePath = join(gitRoot, ".gitignore");
2183
+ const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
2184
+ if (GITIGNORE_CONVEX_ENTRY_RE.test(existing)) return;
2185
+ const normalized = existing.endsWith("\n") || existing.length === 0 ? existing : `${existing}\n`;
2186
+ fs.writeFileSync(gitignorePath, `${normalized}.convex/\n`);
2187
+ }
141
2188
  const processes = [];
142
2189
  function cleanup() {
143
2190
  for (const proc of processes) if (proc && !proc.killed) proc.kill("SIGTERM");
144
2191
  }
2192
+ function deriveScopeFromToggles(api, auth) {
2193
+ if (api && auth) return "all";
2194
+ if (!api && auth) return "auth";
2195
+ if (!api && !auth) return "orm";
2196
+ return null;
2197
+ }
2198
+ const VALID_BACKFILL_ENABLED = new Set([
2199
+ "auto",
2200
+ "on",
2201
+ "off"
2202
+ ]);
2203
+ function parsePositiveIntegerArg(flag, raw) {
2204
+ const parsed = Number(raw);
2205
+ if (!Number.isInteger(parsed) || parsed < 1) throw new Error(`${flag} expects a positive integer.`);
2206
+ return parsed;
2207
+ }
2208
+ function readFlagValue(args, index, flag) {
2209
+ const value = args[index + 1];
2210
+ if (!value) throw new Error(`Missing value for ${flag}.`);
2211
+ return {
2212
+ value,
2213
+ nextIndex: index + 1
2214
+ };
2215
+ }
2216
+ function extractBackfillCliOptions(args) {
2217
+ const remainingArgs = [];
2218
+ const overrides = {};
2219
+ for (let i = 0; i < args.length; i += 1) {
2220
+ const arg = args[i];
2221
+ if (arg === "--force") continue;
2222
+ if (arg === "--backfill") {
2223
+ const { value, nextIndex } = readFlagValue(args, i, "--backfill");
2224
+ if (!VALID_BACKFILL_ENABLED.has(value)) throw new Error("Invalid --backfill value. Expected auto, on, or off.");
2225
+ overrides.enabled = value;
2226
+ i = nextIndex;
2227
+ continue;
2228
+ }
2229
+ if (arg.startsWith("--backfill=")) {
2230
+ const value = arg.slice(11);
2231
+ if (!VALID_BACKFILL_ENABLED.has(value)) throw new Error("Invalid --backfill value. Expected auto, on, or off.");
2232
+ overrides.enabled = value;
2233
+ continue;
2234
+ }
2235
+ if (arg === "--backfill-mode") {
2236
+ readFlagValue(args, i, "--backfill-mode");
2237
+ throw new Error("`--backfill-mode` was removed. Use `better-convex aggregate rebuild`.");
2238
+ }
2239
+ if (arg.startsWith("--backfill-mode=")) throw new Error("`--backfill-mode` was removed. Use `better-convex aggregate rebuild`.");
2240
+ if (arg === "--backfill-wait") {
2241
+ overrides.wait = true;
2242
+ continue;
2243
+ }
2244
+ if (arg === "--no-backfill-wait") {
2245
+ overrides.wait = false;
2246
+ continue;
2247
+ }
2248
+ if (arg === "--backfill-strict") {
2249
+ overrides.strict = true;
2250
+ continue;
2251
+ }
2252
+ if (arg === "--no-backfill-strict") {
2253
+ overrides.strict = false;
2254
+ continue;
2255
+ }
2256
+ if (arg === "--backfill-batch-size") {
2257
+ const { value, nextIndex } = readFlagValue(args, i, "--backfill-batch-size");
2258
+ overrides.batchSize = parsePositiveIntegerArg("--backfill-batch-size", value);
2259
+ i = nextIndex;
2260
+ continue;
2261
+ }
2262
+ if (arg.startsWith("--backfill-batch-size=")) {
2263
+ overrides.batchSize = parsePositiveIntegerArg("--backfill-batch-size", arg.slice(22));
2264
+ continue;
2265
+ }
2266
+ if (arg === "--backfill-timeout-ms") {
2267
+ const { value, nextIndex } = readFlagValue(args, i, "--backfill-timeout-ms");
2268
+ overrides.timeoutMs = parsePositiveIntegerArg("--backfill-timeout-ms", value);
2269
+ i = nextIndex;
2270
+ continue;
2271
+ }
2272
+ if (arg.startsWith("--backfill-timeout-ms=")) {
2273
+ overrides.timeoutMs = parsePositiveIntegerArg("--backfill-timeout-ms", arg.slice(22));
2274
+ continue;
2275
+ }
2276
+ if (arg === "--backfill-poll-ms") {
2277
+ const { value, nextIndex } = readFlagValue(args, i, "--backfill-poll-ms");
2278
+ overrides.pollIntervalMs = parsePositiveIntegerArg("--backfill-poll-ms", value);
2279
+ i = nextIndex;
2280
+ continue;
2281
+ }
2282
+ if (arg.startsWith("--backfill-poll-ms=")) {
2283
+ overrides.pollIntervalMs = parsePositiveIntegerArg("--backfill-poll-ms", arg.slice(19));
2284
+ continue;
2285
+ }
2286
+ remainingArgs.push(arg);
2287
+ }
2288
+ return {
2289
+ remainingArgs,
2290
+ overrides
2291
+ };
2292
+ }
2293
+ function extractResetCliOptions(args) {
2294
+ const remainingArgs = [];
2295
+ let confirmed = false;
2296
+ let beforeHook;
2297
+ let afterHook;
2298
+ const isBackfillFlag = (arg) => arg === "--backfill" || arg.startsWith("--backfill=") || arg.startsWith("--backfill-") || arg.startsWith("--no-backfill-");
2299
+ for (let i = 0; i < args.length; i += 1) {
2300
+ const arg = args[i];
2301
+ if (isBackfillFlag(arg)) throw new Error("`better-convex reset` does not accept backfill flags. It always runs aggregateBackfill in resume mode.");
2302
+ if (arg === "--yes") {
2303
+ confirmed = true;
2304
+ continue;
2305
+ }
2306
+ if (arg === "--before") {
2307
+ const { value, nextIndex } = readFlagValue(args, i, "--before");
2308
+ beforeHook = value;
2309
+ i = nextIndex;
2310
+ continue;
2311
+ }
2312
+ if (arg.startsWith("--before=")) {
2313
+ const value = arg.slice(9);
2314
+ if (!value) throw new Error("Missing value for --before.");
2315
+ beforeHook = value;
2316
+ continue;
2317
+ }
2318
+ if (arg === "--after") {
2319
+ const { value, nextIndex } = readFlagValue(args, i, "--after");
2320
+ afterHook = value;
2321
+ i = nextIndex;
2322
+ continue;
2323
+ }
2324
+ if (arg.startsWith("--after=")) {
2325
+ const value = arg.slice(8);
2326
+ if (!value) throw new Error("Missing value for --after.");
2327
+ afterHook = value;
2328
+ continue;
2329
+ }
2330
+ remainingArgs.push(arg);
2331
+ }
2332
+ return {
2333
+ confirmed,
2334
+ beforeHook,
2335
+ afterHook,
2336
+ remainingArgs
2337
+ };
2338
+ }
2339
+ function resolveBackfillConfig(base, overrides) {
2340
+ const resolvedBase = base ?? {
2341
+ enabled: "auto",
2342
+ wait: true,
2343
+ batchSize: 1e3,
2344
+ timeoutMs: 9e5,
2345
+ pollIntervalMs: 1e3,
2346
+ strict: false
2347
+ };
2348
+ return {
2349
+ ...resolvedBase,
2350
+ enabled: overrides.enabled ?? resolvedBase.enabled,
2351
+ wait: overrides.wait ?? resolvedBase.wait,
2352
+ batchSize: overrides.batchSize ?? resolvedBase.batchSize,
2353
+ timeoutMs: overrides.timeoutMs ?? resolvedBase.timeoutMs,
2354
+ pollIntervalMs: overrides.pollIntervalMs ?? resolvedBase.pollIntervalMs,
2355
+ strict: overrides.strict ?? resolvedBase.strict
2356
+ };
2357
+ }
2358
+ function extractRunDeploymentArgs(args) {
2359
+ const deploymentArgs = [];
2360
+ for (let i = 0; i < args.length; i += 1) {
2361
+ const arg = args[i];
2362
+ if (arg === "--prod") {
2363
+ deploymentArgs.push(arg);
2364
+ continue;
2365
+ }
2366
+ if (arg === "--preview-name" || arg === "--deployment-name" || arg === "--env-file" || arg === "--component") {
2367
+ const value = args[i + 1];
2368
+ if (!value) throw new Error(`Missing value for ${arg}.`);
2369
+ deploymentArgs.push(arg, value);
2370
+ i += 1;
2371
+ continue;
2372
+ }
2373
+ if (arg.startsWith("--preview-name=") || arg.startsWith("--deployment-name=") || arg.startsWith("--env-file=") || arg.startsWith("--component=")) deploymentArgs.push(arg);
2374
+ }
2375
+ return deploymentArgs;
2376
+ }
2377
+ function isMissingBackfillFunctionOutput(output) {
2378
+ return MISSING_BACKFILL_FUNCTION_RE.test(output);
2379
+ }
2380
+ function parseConvexRunJson(stdout) {
2381
+ const trimmed = stdout.trim();
2382
+ if (trimmed.length === 0) return [];
2383
+ try {
2384
+ return JSON.parse(trimmed);
2385
+ } catch {
2386
+ const lines = trimmed.split("\n").map((line) => line.trim()).filter(Boolean);
2387
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
2388
+ const line = lines[i];
2389
+ try {
2390
+ return JSON.parse(line);
2391
+ } catch {}
2392
+ }
2393
+ }
2394
+ throw new Error(`Failed to parse convex run output as JSON.\nOutput:\n${stdout.trim()}`);
2395
+ }
2396
+ function sleep(ms, signal) {
2397
+ if (signal?.aborted) return Promise.resolve();
2398
+ return new Promise((resolve) => {
2399
+ const timer = setTimeout(() => {
2400
+ signal?.removeEventListener("abort", onAbort);
2401
+ resolve();
2402
+ }, ms);
2403
+ const onAbort = () => {
2404
+ clearTimeout(timer);
2405
+ signal?.removeEventListener("abort", onAbort);
2406
+ resolve();
2407
+ };
2408
+ signal?.addEventListener("abort", onAbort);
2409
+ });
2410
+ }
2411
+ async function runConvexFunction(execaFn, realConvexPath, functionName, args, deploymentArgs, options) {
2412
+ const result = await execaFn("node", [
2413
+ realConvexPath,
2414
+ "run",
2415
+ ...deploymentArgs,
2416
+ functionName,
2417
+ JSON.stringify(args)
2418
+ ], {
2419
+ cwd: process.cwd(),
2420
+ reject: false,
2421
+ stdio: "pipe"
2422
+ });
2423
+ const stdout = typeof result.stdout === "string" ? result.stdout : "";
2424
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
2425
+ if (options?.echoOutput !== false) {
2426
+ if (stdout) process.stdout.write(stdout.endsWith("\n") ? stdout : `${stdout}\n`);
2427
+ if (stderr) process.stderr.write(stderr.endsWith("\n") ? stderr : `${stderr}\n`);
2428
+ }
2429
+ return {
2430
+ exitCode: result.exitCode ?? 1,
2431
+ stdout,
2432
+ stderr
2433
+ };
2434
+ }
2435
+ async function runAggregateBackfillFlow(params) {
2436
+ const { execaFn, realConvexPath, backfillConfig, mode, deploymentArgs, signal, context } = params;
2437
+ if (signal?.aborted) return 0;
2438
+ if (backfillConfig.enabled === "off") return 0;
2439
+ const kickoff = await runConvexFunction(execaFn, realConvexPath, "generated/server:aggregateBackfill", {
2440
+ mode,
2441
+ batchSize: backfillConfig.batchSize
2442
+ }, deploymentArgs, { echoOutput: false });
2443
+ if (kickoff.exitCode !== 0) {
2444
+ const combinedOutput = `${kickoff.stdout}\n${kickoff.stderr}`;
2445
+ if (backfillConfig.enabled === "auto" && isMissingBackfillFunctionOutput(combinedOutput)) {
2446
+ if (context === "deploy") console.info("ℹ️ aggregateBackfill not found in this deployment; skipping post-deploy backfill (auto mode).");
2447
+ return 0;
2448
+ }
2449
+ return kickoff.exitCode;
2450
+ }
2451
+ const kickoffPayload = parseConvexRunJson(kickoff.stdout);
2452
+ const needsRebuild = typeof kickoffPayload === "object" && kickoffPayload !== null && !Array.isArray(kickoffPayload) && typeof kickoffPayload.needsRebuild === "number" ? kickoffPayload.needsRebuild : 0;
2453
+ const scheduled = typeof kickoffPayload === "object" && kickoffPayload !== null && !Array.isArray(kickoffPayload) && typeof kickoffPayload.scheduled === "number" ? kickoffPayload.scheduled : 0;
2454
+ const targets = typeof kickoffPayload === "object" && kickoffPayload !== null && !Array.isArray(kickoffPayload) && typeof kickoffPayload.targets === "number" ? kickoffPayload.targets : 0;
2455
+ const pruned = typeof kickoffPayload === "object" && kickoffPayload !== null && !Array.isArray(kickoffPayload) && typeof kickoffPayload.pruned === "number" ? kickoffPayload.pruned : 0;
2456
+ if (pruned > 0) console.info(`ℹ️ aggregateBackfill pruned ${pruned} removed indexes`);
2457
+ if (mode === "resume" && needsRebuild > 0) {
2458
+ const message = `Aggregate backfill found ${needsRebuild} index definitions that require rebuild. Run \`better-convex aggregate rebuild\` for this deployment.`;
2459
+ if (backfillConfig.strict) {
2460
+ console.error(`❌ ${message}`);
2461
+ return 1;
2462
+ }
2463
+ console.warn(`⚠️ ${message}`);
2464
+ } else if (scheduled > 0) console.info(`ℹ️ aggregateBackfill scheduled ${scheduled}/${targets} target indexes`);
2465
+ if (!backfillConfig.wait || signal?.aborted) return 0;
2466
+ const deadline = Date.now() + backfillConfig.timeoutMs;
2467
+ let lastProgress = "";
2468
+ while (!signal?.aborted) {
2469
+ const statusResult = await runConvexFunction(execaFn, realConvexPath, "generated/server:aggregateBackfillStatus", {}, deploymentArgs, { echoOutput: false });
2470
+ if (statusResult.exitCode !== 0) return statusResult.exitCode;
2471
+ const statuses = parseConvexRunJson(statusResult.stdout);
2472
+ const failed = statuses.find((entry) => Boolean(entry.lastError));
2473
+ if (failed) {
2474
+ console.error(`❌ Aggregate backfill failed for ${failed.tableName}.${failed.indexName}: ${failed.lastError}`);
2475
+ return backfillConfig.strict ? 1 : 0;
2476
+ }
2477
+ const total = statuses.length;
2478
+ const ready = statuses.filter((entry) => entry.status === "READY").length;
2479
+ const progress = `${ready}/${total}`;
2480
+ if (progress !== lastProgress) {
2481
+ lastProgress = progress;
2482
+ if (total > 0) console.info(`ℹ️ aggregateBackfill progress ${ready}/${total} READY`);
2483
+ }
2484
+ if (total === 0 || ready === total) return 0;
2485
+ if (Date.now() > deadline) {
2486
+ const timeoutMessage = `Aggregate backfill timed out after ${backfillConfig.timeoutMs}ms (${ready}/${total} READY).`;
2487
+ if (backfillConfig.strict) {
2488
+ console.error(`❌ ${timeoutMessage}`);
2489
+ return 1;
2490
+ }
2491
+ console.warn(`⚠️ ${timeoutMessage}`);
2492
+ return 0;
2493
+ }
2494
+ await sleep(backfillConfig.pollIntervalMs, signal);
2495
+ }
2496
+ return 0;
2497
+ }
2498
+ async function runAggregatePruneFlow(params) {
2499
+ const { execaFn, realConvexPath, deploymentArgs } = params;
2500
+ const result = await runConvexFunction(execaFn, realConvexPath, "generated/server:aggregateBackfill", { mode: "prune" }, deploymentArgs, { echoOutput: false });
2501
+ if (result.exitCode !== 0) return result.exitCode;
2502
+ const payload = parseConvexRunJson(result.stdout);
2503
+ const pruned = typeof payload === "object" && payload !== null && !Array.isArray(payload) && typeof payload.pruned === "number" ? payload.pruned : 0;
2504
+ if (pruned > 0) console.info(`ℹ️ aggregateBackfill pruned ${pruned} removed indexes`);
2505
+ else console.info("ℹ️ aggregateBackfill prune no-op");
2506
+ return 0;
2507
+ }
2508
+ async function runDevSchemaBackfillIfNeeded(params) {
2509
+ const { execaFn, realConvexPath, backfillConfig, functionsDir, deploymentArgs, signal } = params;
2510
+ const fingerprint = await computeAggregateIndexFingerprint(functionsDir);
2511
+ if (!fingerprint) return 0;
2512
+ const deploymentKey = getAggregateBackfillDeploymentKey(deploymentArgs);
2513
+ const statePath = getDevAggregateBackfillStatePath();
2514
+ const state = readAggregateFingerprintState(statePath);
2515
+ if (state.entries[deploymentKey]?.fingerprint === fingerprint) return 0;
2516
+ console.info(`ℹ️ aggregateBackfill resume (${deploymentKey} schema change)`);
2517
+ const exitCode = await runAggregateBackfillFlow({
2518
+ execaFn,
2519
+ realConvexPath,
2520
+ backfillConfig: {
2521
+ ...backfillConfig,
2522
+ enabled: "on"
2523
+ },
2524
+ mode: "resume",
2525
+ deploymentArgs,
2526
+ signal,
2527
+ context: "dev"
2528
+ });
2529
+ if (exitCode !== 0 || signal.aborted) return exitCode;
2530
+ state.entries[deploymentKey] = {
2531
+ fingerprint,
2532
+ updatedAt: Date.now()
2533
+ };
2534
+ writeAggregateFingerprintState(statePath, state);
2535
+ return 0;
2536
+ }
145
2537
  async function run(argv, deps) {
146
- const { execa: execaFn, generateMeta: generateMetaFn, syncEnv: syncEnvFn, realConvex: realConvexPath } = {
2538
+ const { execa: execaFn, runAnalyze: runAnalyzeFn, generateMeta: generateMetaFn, getConvexConfig: getConvexConfigFn, syncEnv: syncEnvFn, loadBetterConvexConfig: loadBetterConvexConfigFn, ensureConvexGitignoreEntry: ensureConvexGitignoreEntryFn, enableDevSchemaWatch, realConvex: realConvexPath } = {
147
2539
  execa,
2540
+ runAnalyze,
148
2541
  generateMeta,
2542
+ getConvexConfig,
149
2543
  syncEnv,
2544
+ loadBetterConvexConfig,
2545
+ ensureConvexGitignoreEntry,
2546
+ enableDevSchemaWatch: !deps,
150
2547
  realConvex,
151
2548
  ...deps
152
2549
  };
153
- const { command, restArgs, convexArgs, debug, outputDir } = parseArgs(argv);
2550
+ const { command, restArgs, convexArgs, debug: cliDebug, outputDir: cliOutputDir, scope: cliScope, configPath } = parseArgs(argv);
154
2551
  if (command === "dev") {
155
- await generateMetaFn(outputDir, { debug });
2552
+ if (cliScope) throw new Error("`--scope` is not supported for `better-convex dev`. Use `better-convex codegen --scope <all|auth|orm>` for scoped generation.");
2553
+ const config = loadBetterConvexConfigFn(configPath);
2554
+ const { remainingArgs: devCommandArgs, overrides: devBackfillOverrides } = extractBackfillCliOptions(convexArgs);
2555
+ const outputDir = cliOutputDir ?? config.outputDir;
2556
+ const debug = cliDebug || config.dev.debug;
2557
+ const generateApi = config.api;
2558
+ const generateAuth = config.auth;
2559
+ const convexDevArgs = [...config.dev.convexArgs, ...devCommandArgs];
2560
+ const devBackfillConfig = resolveBackfillConfig(config.dev.aggregateBackfill, devBackfillOverrides);
2561
+ const { functionsDir } = getConvexConfigFn(outputDir);
2562
+ const schemaPath = join(functionsDir, "schema.ts");
2563
+ const deploymentArgs = extractRunDeploymentArgs(convexDevArgs);
2564
+ if (!deps) try {
2565
+ ensureConvexGitignoreEntryFn(process.cwd());
2566
+ } catch (error) {
2567
+ console.warn(`⚠️ Failed to ensure .convex/ is ignored in .gitignore: ${error.message}`);
2568
+ }
2569
+ await generateMetaFn(outputDir, {
2570
+ debug,
2571
+ api: generateApi,
2572
+ auth: generateAuth
2573
+ });
156
2574
  const isTs = __filename.endsWith(".ts");
157
2575
  const watcherPath = isTs ? join(__dirname, "watcher.ts") : join(__dirname, "watcher.mjs");
158
2576
  const watcherProcess = execaFn(isTs ? "bun" : process.execPath, [watcherPath], {
@@ -160,40 +2578,131 @@ async function run(argv, deps) {
160
2578
  cwd: process.cwd(),
161
2579
  env: {
162
2580
  ...process.env,
163
- BETTER_CONVEX_OUTPUT_DIR: outputDir || "",
164
- BETTER_CONVEX_DEBUG: debug ? "1" : ""
2581
+ BETTER_CONVEX_API_OUTPUT_DIR: outputDir || "",
2582
+ BETTER_CONVEX_DEBUG: debug ? "1" : "",
2583
+ BETTER_CONVEX_GENERATE_API: generateApi ? "1" : "0",
2584
+ BETTER_CONVEX_GENERATE_AUTH: generateAuth ? "1" : "0"
165
2585
  }
166
2586
  });
167
2587
  processes.push(watcherProcess);
168
2588
  const convexProcess = execaFn("node", [
169
2589
  realConvexPath,
170
2590
  "dev",
171
- ...convexArgs
2591
+ ...convexDevArgs
172
2592
  ], {
173
2593
  stdio: "inherit",
174
2594
  cwd: process.cwd(),
175
2595
  reject: false
176
2596
  });
177
2597
  processes.push(convexProcess);
2598
+ const backfillAbortController = new AbortController();
2599
+ let schemaWatcher = null;
2600
+ let schemaDebounceTimer = null;
2601
+ let schemaBackfillInFlight = null;
2602
+ let schemaBackfillQueued = false;
2603
+ const maybeRunSchemaBackfill = async () => {
2604
+ try {
2605
+ if (await runDevSchemaBackfillIfNeeded({
2606
+ execaFn,
2607
+ realConvexPath,
2608
+ backfillConfig: devBackfillConfig,
2609
+ functionsDir,
2610
+ deploymentArgs,
2611
+ signal: backfillAbortController.signal
2612
+ }) !== 0 && !backfillAbortController.signal.aborted) console.warn("⚠️ aggregateBackfill on schema update failed in dev (continuing without blocking).");
2613
+ } catch (error) {
2614
+ if (!backfillAbortController.signal.aborted) console.warn(`⚠️ aggregateBackfill on schema update errored in dev: ${error.message}`);
2615
+ }
2616
+ };
2617
+ const queueSchemaBackfill = () => {
2618
+ if (backfillAbortController.signal.aborted) return;
2619
+ schemaBackfillQueued = true;
2620
+ if (schemaBackfillInFlight) return;
2621
+ schemaBackfillInFlight = (async () => {
2622
+ while (schemaBackfillQueued && !backfillAbortController.signal.aborted) {
2623
+ schemaBackfillQueued = false;
2624
+ await maybeRunSchemaBackfill();
2625
+ }
2626
+ })().finally(() => {
2627
+ schemaBackfillInFlight = null;
2628
+ });
2629
+ };
2630
+ if (devBackfillConfig.enabled !== "off") (async () => {
2631
+ try {
2632
+ if (await runAggregateBackfillFlow({
2633
+ execaFn,
2634
+ realConvexPath,
2635
+ backfillConfig: devBackfillConfig,
2636
+ mode: "resume",
2637
+ deploymentArgs,
2638
+ signal: backfillAbortController.signal,
2639
+ context: "dev"
2640
+ }) !== 0 && !backfillAbortController.signal.aborted) console.warn("⚠️ aggregateBackfill kickoff failed in dev (continuing without blocking).");
2641
+ } catch (error) {
2642
+ if (!backfillAbortController.signal.aborted) console.warn(`⚠️ aggregateBackfill kickoff errored in dev: ${error.message}`);
2643
+ }
2644
+ })();
2645
+ if (enableDevSchemaWatch && devBackfillConfig.enabled !== "off" && fs.existsSync(schemaPath)) {
2646
+ const { watch } = await import("chokidar");
2647
+ const watchedSchema = watch(schemaPath, { ignoreInitial: true });
2648
+ schemaWatcher = watchedSchema;
2649
+ watchedSchema.on("change", () => {
2650
+ if (schemaDebounceTimer) clearTimeout(schemaDebounceTimer);
2651
+ schemaDebounceTimer = setTimeout(() => {
2652
+ queueSchemaBackfill();
2653
+ }, 200);
2654
+ }).on("error", (error) => {
2655
+ if (!backfillAbortController.signal.aborted) console.warn(`⚠️ schema watch error (aggregate backfill): ${error.message}`);
2656
+ });
2657
+ }
178
2658
  process.on("exit", cleanup);
179
2659
  process.on("SIGINT", () => {
2660
+ backfillAbortController.abort();
2661
+ if (schemaDebounceTimer) clearTimeout(schemaDebounceTimer);
2662
+ schemaWatcher?.close();
180
2663
  cleanup();
181
2664
  process.exit(0);
182
2665
  });
183
2666
  process.on("SIGTERM", () => {
2667
+ backfillAbortController.abort();
2668
+ if (schemaDebounceTimer) clearTimeout(schemaDebounceTimer);
2669
+ schemaWatcher?.close();
184
2670
  cleanup();
185
2671
  process.exit(0);
186
2672
  });
187
2673
  const result = await Promise.race([watcherProcess.catch(() => ({ exitCode: 1 })), convexProcess]);
2674
+ backfillAbortController.abort();
2675
+ if (schemaDebounceTimer) clearTimeout(schemaDebounceTimer);
2676
+ await schemaWatcher?.close();
188
2677
  cleanup();
189
2678
  return result.exitCode ?? 0;
190
2679
  }
191
2680
  if (command === "codegen") {
192
- await generateMetaFn(outputDir, { debug });
2681
+ const config = loadBetterConvexConfigFn(configPath);
2682
+ const outputDir = cliOutputDir ?? config.outputDir;
2683
+ const debug = cliDebug || config.codegen.debug;
2684
+ const convexCodegenArgs = [...config.codegen.convexArgs, ...convexArgs];
2685
+ const scope = cliScope ?? config.codegen.scope;
2686
+ if (scope) await generateMetaFn(outputDir, {
2687
+ debug,
2688
+ scope
2689
+ });
2690
+ else {
2691
+ const derivedScope = deriveScopeFromToggles(config.api, config.auth);
2692
+ if (derivedScope) await generateMetaFn(outputDir, {
2693
+ debug,
2694
+ scope: derivedScope
2695
+ });
2696
+ else await generateMetaFn(outputDir, {
2697
+ debug,
2698
+ api: config.api,
2699
+ auth: config.auth
2700
+ });
2701
+ }
193
2702
  return (await execaFn("node", [
194
2703
  realConvexPath,
195
2704
  "codegen",
196
- ...convexArgs
2705
+ ...convexCodegenArgs
197
2706
  ], {
198
2707
  stdio: "inherit",
199
2708
  cwd: process.cwd()
@@ -218,6 +2727,85 @@ async function run(argv, deps) {
218
2727
  reject: false
219
2728
  })).exitCode ?? 0;
220
2729
  }
2730
+ if (command === "analyze") return runAnalyzeFn(restArgs);
2731
+ if (command === "reset") {
2732
+ const { confirmed, beforeHook, afterHook, remainingArgs: resetCommandArgs } = extractResetCliOptions(convexArgs);
2733
+ if (!confirmed) throw new Error("`better-convex reset` is destructive. Re-run with `--yes`.");
2734
+ const deploymentArgs = extractRunDeploymentArgs([...loadBetterConvexConfigFn(configPath).deploy.convexArgs, ...resetCommandArgs]);
2735
+ const runOptionalHook = async (functionName) => {
2736
+ if (!functionName) return 0;
2737
+ return (await runConvexFunction(execaFn, realConvexPath, functionName, {}, deploymentArgs)).exitCode;
2738
+ };
2739
+ const beforeExitCode = await runOptionalHook(beforeHook);
2740
+ if (beforeExitCode !== 0) return beforeExitCode;
2741
+ const resetResult = await runConvexFunction(execaFn, realConvexPath, "generated/server:reset", {}, deploymentArgs);
2742
+ if (resetResult.exitCode !== 0) return resetResult.exitCode;
2743
+ const backfillExitCode = await runAggregateBackfillFlow({
2744
+ execaFn,
2745
+ realConvexPath,
2746
+ backfillConfig: {
2747
+ enabled: "on",
2748
+ wait: true,
2749
+ batchSize: 1e3,
2750
+ pollIntervalMs: 1e3,
2751
+ timeoutMs: 9e5,
2752
+ strict: false
2753
+ },
2754
+ mode: "resume",
2755
+ deploymentArgs,
2756
+ context: "aggregate"
2757
+ });
2758
+ if (backfillExitCode !== 0) return backfillExitCode;
2759
+ return runOptionalHook(afterHook);
2760
+ }
2761
+ if (command === "deploy") {
2762
+ const config = loadBetterConvexConfigFn(configPath);
2763
+ const { remainingArgs: deployCommandArgs, overrides: deployBackfillOverrides } = extractBackfillCliOptions(convexArgs);
2764
+ const deployArgs = [...config.deploy.convexArgs, ...deployCommandArgs];
2765
+ const deployResult = await execaFn("node", [
2766
+ realConvexPath,
2767
+ "deploy",
2768
+ ...deployArgs
2769
+ ], {
2770
+ stdio: "inherit",
2771
+ cwd: process.cwd(),
2772
+ reject: false
2773
+ });
2774
+ if ((deployResult.exitCode ?? 1) !== 0) return deployResult.exitCode ?? 1;
2775
+ return runAggregateBackfillFlow({
2776
+ execaFn,
2777
+ realConvexPath,
2778
+ backfillConfig: resolveBackfillConfig(config.deploy.aggregateBackfill, deployBackfillOverrides),
2779
+ mode: "resume",
2780
+ deploymentArgs: extractRunDeploymentArgs(deployArgs),
2781
+ context: "deploy"
2782
+ });
2783
+ }
2784
+ if (command === "aggregate") {
2785
+ const subcommand = restArgs[0];
2786
+ if (subcommand !== "rebuild" && subcommand !== "backfill" && subcommand !== "prune") throw new Error("Unknown aggregate command. Use: `better-convex aggregate backfill`, `better-convex aggregate rebuild`, or `better-convex aggregate prune`.");
2787
+ const config = loadBetterConvexConfigFn(configPath);
2788
+ const { remainingArgs: aggregateCommandArgs, overrides: aggregateBackfillOverrides } = extractBackfillCliOptions(restArgs.slice(1));
2789
+ const aggregateArgs = [...config.deploy.convexArgs, ...aggregateCommandArgs];
2790
+ const backfillConfig = {
2791
+ ...resolveBackfillConfig(config.deploy.aggregateBackfill, aggregateBackfillOverrides),
2792
+ enabled: "on"
2793
+ };
2794
+ const deploymentArgs = extractRunDeploymentArgs(aggregateArgs);
2795
+ if (subcommand === "prune") return runAggregatePruneFlow({
2796
+ execaFn,
2797
+ realConvexPath,
2798
+ deploymentArgs
2799
+ });
2800
+ return runAggregateBackfillFlow({
2801
+ execaFn,
2802
+ realConvexPath,
2803
+ backfillConfig,
2804
+ mode: subcommand === "rebuild" ? "rebuild" : "resume",
2805
+ deploymentArgs,
2806
+ context: "aggregate"
2807
+ });
2808
+ }
221
2809
  return (await execaFn("node", [
222
2810
  realConvexPath,
223
2811
  command,
@@ -243,4 +2831,4 @@ if (isEntryPoint(process.argv[1], __filename)) run(process.argv.slice(2)).then((
243
2831
  });
244
2832
 
245
2833
  //#endregion
246
- export { isEntryPoint, parseArgs, run };
2834
+ export { ensureConvexGitignoreEntry, getAggregateBackfillDeploymentKey, getDevAggregateBackfillStatePath, isEntryPoint, parseArgs, run };