@gonvex/cli 0.1.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 (32) hide show
  1. package/LICENSE +201 -0
  2. package/dist/browser.d.ts +1 -0
  3. package/dist/browser.js +2 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.js +986 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/react.d.ts +1 -0
  9. package/dist/react.js +2 -0
  10. package/dist/react.js.map +1 -0
  11. package/dist/templates/vite-react/.env.example +7 -0
  12. package/dist/templates/vite-react/README.md +34 -0
  13. package/dist/templates/vite-react/gonvex/_generated/api.ts +8 -0
  14. package/dist/templates/vite-react/gonvex/_generated/client.ts +2 -0
  15. package/dist/templates/vite-react/gonvex/_generated/manifest.json +52 -0
  16. package/dist/templates/vite-react/gonvex/_generated/react.ts +2 -0
  17. package/dist/templates/vite-react/gonvex/_generated/schema.ts +2 -0
  18. package/dist/templates/vite-react/gonvex/_generated/types.ts +2 -0
  19. package/dist/templates/vite-react/gonvex/messages.go +38 -0
  20. package/dist/templates/vite-react/gonvex/schema.go +14 -0
  21. package/dist/templates/vite-react/gonvex.json +7 -0
  22. package/dist/templates/vite-react/index.html +12 -0
  23. package/dist/templates/vite-react/package.json +30 -0
  24. package/dist/templates/vite-react/src/App.tsx +66 -0
  25. package/dist/templates/vite-react/src/main.tsx +17 -0
  26. package/dist/templates/vite-react/src/styles.css +198 -0
  27. package/dist/templates/vite-react/src/vite-env.d.ts +1 -0
  28. package/dist/templates/vite-react/tsconfig.app.json +22 -0
  29. package/dist/templates/vite-react/tsconfig.json +7 -0
  30. package/dist/templates/vite-react/tsconfig.node.json +15 -0
  31. package/dist/templates/vite-react/vite.config.ts +6 -0
  32. package/package.json +40 -0
package/dist/index.js ADDED
@@ -0,0 +1,986 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
5
+ import { mkdir, readFile, readdir, stat, writeFile, copyFile } from "node:fs/promises";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { dirname, join, relative, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ const defaultRuntimeURL = "http://localhost:8080";
10
+ const runtimeSyncRetryMs = 5000;
11
+ const runtimeStateCheckMs = 2500;
12
+ const color = {
13
+ green: (value) => `\x1b[32m${value}\x1b[0m`,
14
+ red: (value) => `\x1b[31m${value}\x1b[0m`,
15
+ yellow: (value) => `\x1b[33m${value}\x1b[0m`,
16
+ };
17
+ export async function main(argv = process.argv.slice(2)) {
18
+ const command = argv[0];
19
+ if (!command || command === "help" || command === "--help") {
20
+ printHelp();
21
+ return;
22
+ }
23
+ if (command === "dev") {
24
+ await runDev(argv.slice(1));
25
+ return;
26
+ }
27
+ if (command === "init") {
28
+ await runInit(argv.slice(1));
29
+ return;
30
+ }
31
+ if (command === "create") {
32
+ await runCreate(argv.slice(1));
33
+ return;
34
+ }
35
+ if (command === "env") {
36
+ await runEnv(argv.slice(1));
37
+ return;
38
+ }
39
+ printHelp();
40
+ throw new Error(`unknown command ${command}`);
41
+ }
42
+ export async function runCreate(argv) {
43
+ const target = argv.find((arg) => !arg.startsWith("-")) ?? "my-gonvex-app";
44
+ const appName = basename(target);
45
+ const template = valueFor(argv, "--template") ?? "vite-react";
46
+ const root = resolve(target);
47
+ if (existsSync(root))
48
+ throw new Error(`${target} already exists`);
49
+ await copyTemplate(template, root);
50
+ await rewritePackageName(root, appName);
51
+ await rewriteGonvexConfig(root, appName, defaultRuntimeURL);
52
+ await writeEnvLocal(root, appName, defaultRuntimeURL);
53
+ console.log(`[gonvex] created ${target} from ${template} template`);
54
+ console.log(`[gonvex] next: cd ${target} && npm install && npm run dev`);
55
+ }
56
+ async function runInit(argv) {
57
+ const template = valueFor(argv, "--template") ?? "vite-react";
58
+ const project = valueFor(argv, "--project") ?? basename(process.cwd());
59
+ const runtime = valueFor(argv, "--runtime") ?? defaultRuntimeURL;
60
+ await copyTemplate(template, process.cwd(), { overwrite: false });
61
+ await writeEnvLocal(process.cwd(), project, runtime);
62
+ console.log(`[gonvex] initialized ${project}`);
63
+ }
64
+ async function runDev(argv) {
65
+ const split = argv.indexOf("--");
66
+ const flagArgs = split === -1 ? argv : argv.slice(0, split);
67
+ const childCommand = split === -1 ? [] : argv.slice(split + 1);
68
+ const projectRoot = resolve(valueFor(flagArgs, "--project") ?? ".");
69
+ const once = flagArgs.includes("--once");
70
+ if (once && childCommand.length > 0)
71
+ throw new Error("--once cannot be used with a child command");
72
+ let settings = await loadSettings(projectRoot, {
73
+ runtimeURL: valueFor(flagArgs, "--runtime-url"),
74
+ projectID: valueFor(flagArgs, "--project-id"),
75
+ key: valueFor(flagArgs, "--key"),
76
+ });
77
+ settings = await ensureProjectSettings(projectRoot, settings, {
78
+ keyWasExplicit: Boolean(valueFor(flagArgs, "--key")),
79
+ runtimeWasExplicit: Boolean(valueFor(flagArgs, "--runtime-url")),
80
+ });
81
+ if (childCommand.length > 0) {
82
+ const initialState = await watchProject(projectRoot, settings, true);
83
+ const controller = new AbortController();
84
+ const watcher = watchProject(projectRoot, settings, false, controller.signal, initialState);
85
+ const child = spawn(childCommand[0], childCommand.slice(1), {
86
+ cwd: projectRoot,
87
+ stdio: "inherit",
88
+ signal: controller.signal,
89
+ shell: process.platform === "win32",
90
+ });
91
+ const code = await new Promise((resolve) => child.on("exit", resolve));
92
+ controller.abort();
93
+ await watcher.catch((error) => {
94
+ if (error?.name !== "AbortError")
95
+ throw error;
96
+ });
97
+ process.exitCode = code ?? 0;
98
+ return;
99
+ }
100
+ await watchProject(projectRoot, settings, once);
101
+ }
102
+ async function runEnv(argv) {
103
+ const parsedArgs = parseEnvCommandArgs(argv);
104
+ const action = parsedArgs.action;
105
+ if (!action || action === "help" || action === "--help") {
106
+ printEnvHelp();
107
+ return;
108
+ }
109
+ const projectRoot = resolve(parsedArgs.options["--project"] ?? ".");
110
+ const settings = await loadSettings(projectRoot, {
111
+ runtimeURL: parsedArgs.options["--runtime-url"],
112
+ projectID: parsedArgs.options["--project-id"],
113
+ key: parsedArgs.options["--key"],
114
+ });
115
+ if (!settings.key)
116
+ throw new Error("GONVEX_PROJECT_KEY is required for project env commands");
117
+ if (action === "list" || action === "ls") {
118
+ const variables = await fetchProjectEnv(settings);
119
+ if (variables.length === 0) {
120
+ console.log(`[gonvex] no env vars set for ${settings.projectID}`);
121
+ return;
122
+ }
123
+ for (const variable of variables) {
124
+ console.log(`${variable.name}=${variable.sensitive ? variable.masked : variable.value ?? variable.masked}`);
125
+ }
126
+ return;
127
+ }
128
+ if (action === "get") {
129
+ const name = parsedArgs.positional[0];
130
+ if (!name)
131
+ throw new Error("usage: gonvex env get NAME");
132
+ const variables = await fetchProjectEnv(settings);
133
+ const variable = variables.find((item) => item.name === name);
134
+ if (!variable)
135
+ throw new Error(`${name} is not set for ${settings.projectID}`);
136
+ console.log(variable.sensitive ? variable.masked : variable.value ?? variable.masked);
137
+ return;
138
+ }
139
+ if (action === "set") {
140
+ const parsed = parseEnvSetArgs(parsedArgs.positional);
141
+ await saveProjectEnv(settings, parsed.name, parsed.value);
142
+ console.log(`[gonvex] saved ${parsed.name} for ${settings.projectID}`);
143
+ return;
144
+ }
145
+ if (action === "remove" || action === "rm" || action === "unset" || action === "delete") {
146
+ const name = parsedArgs.positional[0];
147
+ if (!name)
148
+ throw new Error("usage: gonvex env remove NAME");
149
+ await deleteProjectEnv(settings, name);
150
+ console.log(`[gonvex] removed ${name} from ${settings.projectID}`);
151
+ return;
152
+ }
153
+ printEnvHelp();
154
+ throw new Error(`unknown env command ${action}`);
155
+ }
156
+ async function watchProject(root, settings, once, signal, initialState) {
157
+ const backendDir = join(root, "gonvex");
158
+ await mkdir(backendDir, { recursive: true });
159
+ let lastFingerprint = initialState?.lastFingerprint ?? "";
160
+ let lastManifest = initialState?.lastManifest ?? null;
161
+ let lastSyncAttempt = initialState?.lastSyncAttempt ?? 0;
162
+ let lastSyncSucceeded = initialState?.lastSyncSucceeded ?? false;
163
+ let lastRuntimeCheck = initialState?.lastRuntimeCheck ?? 0;
164
+ while (!signal?.aborted) {
165
+ const files = await goFiles(backendDir);
166
+ const fingerprint = await filesFingerprint(files);
167
+ const now = Date.now();
168
+ const shouldBuild = fingerprint !== lastFingerprint;
169
+ const shouldRetryRuntimeSync = !once && !lastSyncSucceeded && lastManifest !== null && now - lastSyncAttempt > runtimeSyncRetryMs;
170
+ const shouldVerifyRuntimeState = !once && lastSyncSucceeded && lastManifest !== null && now - lastRuntimeCheck > runtimeStateCheckMs;
171
+ if (shouldBuild || shouldRetryRuntimeSync) {
172
+ lastFingerprint = fingerprint;
173
+ let manifest;
174
+ if (shouldBuild) {
175
+ manifest = await buildManifest(root, files, settings.projectID);
176
+ }
177
+ else {
178
+ manifest = lastManifest;
179
+ }
180
+ const isInitialBuild = lastManifest === null;
181
+ const previousManifest = lastManifest ?? await readGeneratedManifest(root);
182
+ if (shouldBuild) {
183
+ lastManifest = manifest;
184
+ const writeResult = await writeBindings(root, manifest);
185
+ logFunctionDiff(previousManifest, manifest, writeResult, isInitialBuild);
186
+ }
187
+ lastSyncAttempt = now;
188
+ try {
189
+ await syncRuntime(settings, manifest);
190
+ lastSyncSucceeded = true;
191
+ lastRuntimeCheck = now;
192
+ console.log(`[gonvex] synced project ${settings.projectID || "(key-inferred)"} to ${settings.runtimeURL}`);
193
+ }
194
+ catch (error) {
195
+ lastSyncSucceeded = false;
196
+ console.error(`[gonvex] runtime sync failed: ${error instanceof Error ? error.message : String(error)}`);
197
+ }
198
+ }
199
+ else if (shouldVerifyRuntimeState) {
200
+ const manifest = lastManifest;
201
+ if (manifest === null)
202
+ continue;
203
+ lastRuntimeCheck = now;
204
+ try {
205
+ const inSync = await runtimeHasManifest(settings, manifest);
206
+ if (!inSync) {
207
+ lastSyncAttempt = now;
208
+ await syncRuntime(settings, manifest);
209
+ lastSyncSucceeded = true;
210
+ console.log(`[gonvex] runtime state was missing; re-synced project ${settings.projectID || "(key-inferred)"}`);
211
+ }
212
+ }
213
+ catch {
214
+ lastSyncSucceeded = false;
215
+ }
216
+ }
217
+ if (once) {
218
+ return { lastFingerprint, lastManifest, lastSyncAttempt, lastSyncSucceeded, lastRuntimeCheck };
219
+ }
220
+ await sleep(500, signal);
221
+ }
222
+ return { lastFingerprint, lastManifest, lastSyncAttempt, lastSyncSucceeded, lastRuntimeCheck };
223
+ }
224
+ async function buildManifest(root, files, projectID) {
225
+ const functions = {};
226
+ const schema = emptySchemaDefinition();
227
+ let packageName = "app";
228
+ for (const file of files) {
229
+ Object.assign(functions, await parseRegistrations(root, file));
230
+ mergeSchemaDefinition(schema, await parseSchema(file));
231
+ if (packageName === "app") {
232
+ packageName = await detectPackageName(file);
233
+ }
234
+ }
235
+ const bundle = await buildSourceBundle(root, files, projectID, packageName);
236
+ return {
237
+ project: projectID,
238
+ generatedAt: new Date().toISOString(),
239
+ functions,
240
+ schema,
241
+ bundle,
242
+ };
243
+ }
244
+ async function buildSourceBundle(root, files, projectID, packageName) {
245
+ const backendDir = join(root, "gonvex");
246
+ const encodedFiles = {};
247
+ for (const file of files) {
248
+ const source = await readFile(file);
249
+ const rel = relative(backendDir, file).replace(/\\/g, "/");
250
+ encodedFiles[`app/${rel}`] = Buffer.from(source).toString("base64");
251
+ }
252
+ const hash = createHash("sha256");
253
+ for (const path of Object.keys(encodedFiles).sort()) {
254
+ hash.update(`${path}:${encodedFiles[path]};`);
255
+ }
256
+ return {
257
+ hash: hash.digest("hex"),
258
+ modulePath: `gonvexapp/${sanitizeProjectID(projectID)}`,
259
+ packageName,
260
+ files: encodedFiles,
261
+ };
262
+ }
263
+ async function detectPackageName(file) {
264
+ const source = await readFile(file, "utf8");
265
+ const match = source.match(/^package\s+([A-Za-z_][A-Za-z0-9_]*)/m);
266
+ return match?.[1] ?? "app";
267
+ }
268
+ function sanitizeProjectID(projectID) {
269
+ const trimmed = projectID.trim();
270
+ if (!trimmed)
271
+ return "project";
272
+ return trimmed.replace(/[^a-zA-Z0-9._-]+/g, "-");
273
+ }
274
+ async function parseRegistrations(root, file) {
275
+ const source = await readFile(file, "utf8");
276
+ const pattern = /app\.(Query|Mutation|Action|HTTP|InternalMutation|LiveGrid)\(\s*"([^"]+)"\s*,\s*([A-Za-z_][A-Za-z0-9_]*)/g;
277
+ const entries = {};
278
+ for (const match of source.matchAll(pattern)) {
279
+ entries[match[2]] = {
280
+ kind: functionKind(match[1]),
281
+ handler: match[3],
282
+ file: relative(root, file),
283
+ };
284
+ }
285
+ return entries;
286
+ }
287
+ async function parseSchema(file) {
288
+ const source = await readFile(file, "utf8");
289
+ const tablePattern = /s\.(Table|TenantTable|LandlordTable)\(\s*"([^"]+)"\s*,\s*func\([^)]*\)\s*\{([\s\S]*?)\n\s*\}\s*\)/g;
290
+ const columnPattern = /t\.(ID|String|Text|Int|Int64|Float64|Bool|Time|JSON)\(\s*"([^"]+)"([^)]*)\)/g;
291
+ const indexPattern = /t\.(Index|UniqueIndex|TrigramIndex)\(\s*"([^"]+)"([^)]*)\)/g;
292
+ const schema = emptySchemaDefinition();
293
+ for (const tableMatch of source.matchAll(tablePattern)) {
294
+ const table = { columns: {}, indexes: {} };
295
+ const scope = tableMatch[1];
296
+ const name = tableMatch[2];
297
+ const body = tableMatch[3];
298
+ for (const columnMatch of body.matchAll(columnPattern)) {
299
+ const kind = columnMatch[1];
300
+ table.columns[columnMatch[2]] = {
301
+ type: columnType(kind),
302
+ nullable: columnMatch[3].includes("gonvex.Nullable"),
303
+ primaryKey: kind === "ID",
304
+ };
305
+ }
306
+ for (const indexMatch of body.matchAll(indexPattern)) {
307
+ table.indexes[indexMatch[2]] = {
308
+ columns: stringArgs(indexMatch[3]),
309
+ unique: indexMatch[1] === "UniqueIndex",
310
+ ...(indexMatch[1] === "TrigramIndex" ? { kind: "trigram" } : {}),
311
+ };
312
+ }
313
+ if (scope === "LandlordTable") {
314
+ schema.landlordTables[name] = table;
315
+ }
316
+ else {
317
+ schema.tenantTables[name] = table;
318
+ schema.tables[name] = table;
319
+ }
320
+ }
321
+ return schema;
322
+ }
323
+ async function writeBindings(root, manifest) {
324
+ const dir = join(root, "gonvex", "_generated");
325
+ await mkdir(dir, { recursive: true });
326
+ let changedFiles = 0;
327
+ if (await writeManifestIfChanged(join(dir, "manifest.json"), manifest))
328
+ changedFiles += 1;
329
+ const outputs = {
330
+ "api.ts": renderAPI(manifest),
331
+ "client.ts": '// Generated by gonvex dev. Do not edit.\nexport { GonvexClient, ConvexReactClient } from "@gonvex/client";\n',
332
+ "react.ts": '// Generated by gonvex dev. Do not edit.\nexport { ConvexProvider, ConvexProviderWithAuth, ConvexReactClient, GonvexProvider, useAction, useConvex, useConvexAuth, useConvexConnectionState, useMutation, usePaginatedQuery, useQuery } from "@gonvex/react";\n',
333
+ "types.ts": "// Generated by gonvex dev. Do not edit.\nexport type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };\n",
334
+ "schema.ts": renderSchemaIndex(manifest),
335
+ "landlord/schema.ts": renderScopedSchemaModule("landlord", manifest.schema.landlordTables),
336
+ "landlord/tables.ts": renderScopedTablesModule("landlord", manifest.schema.landlordTables),
337
+ "tenant/schema.ts": renderScopedSchemaModule("tenant", manifest.schema.tenantTables),
338
+ "tenant/tables.ts": renderScopedTablesModule("tenant", manifest.schema.tenantTables),
339
+ };
340
+ for (const [name, contents] of Object.entries(outputs)) {
341
+ if (await writeFileIfChanged(join(dir, name), contents))
342
+ changedFiles += 1;
343
+ }
344
+ return { changedFiles };
345
+ }
346
+ async function writeManifestIfChanged(path, manifest) {
347
+ try {
348
+ const existing = JSON.parse(await readFile(path, "utf8"));
349
+ if (JSON.stringify(manifestWithoutGeneratedAt(existing)) === JSON.stringify(manifestWithoutGeneratedAt(manifest))) {
350
+ return false;
351
+ }
352
+ }
353
+ catch {
354
+ // Missing, stale, or invalid generated manifests are repaired by writing them.
355
+ }
356
+ await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`);
357
+ return true;
358
+ }
359
+ async function writeFileIfChanged(path, contents) {
360
+ try {
361
+ if ((await readFile(path, "utf8")) === contents)
362
+ return false;
363
+ }
364
+ catch {
365
+ // Missing or unreadable generated files are repaired by writing them.
366
+ }
367
+ await mkdir(dirname(path), { recursive: true });
368
+ await writeFile(path, contents);
369
+ return true;
370
+ }
371
+ async function readGeneratedManifest(root) {
372
+ try {
373
+ return JSON.parse(await readFile(join(root, "gonvex", "_generated", "manifest.json"), "utf8"));
374
+ }
375
+ catch {
376
+ return null;
377
+ }
378
+ }
379
+ function manifestWithoutGeneratedAt(manifest) {
380
+ const { generatedAt: _generatedAt, ...rest } = manifest;
381
+ return rest;
382
+ }
383
+ function logFunctionDiff(previous, current, writeResult, isInitialBuild) {
384
+ const functionCount = Object.keys(current.functions).length;
385
+ if (!previous) {
386
+ console.log(`[gonvex] uploaded ${functionCount} runtime function(s)`);
387
+ return;
388
+ }
389
+ const diff = diffFunctions(previous, current);
390
+ if (diff.added.length === 0 && diff.removed.length === 0 && diff.edited.length === 0) {
391
+ if (isInitialBuild && writeResult.changedFiles === 0) {
392
+ return;
393
+ }
394
+ if (isInitialBuild) {
395
+ return;
396
+ }
397
+ console.log("[gonvex] ~ uploaded runtime source changes (no function API changes)");
398
+ return;
399
+ }
400
+ for (const path of diff.added)
401
+ console.log(color.green(`[gonvex] + added function ${path}`));
402
+ for (const path of diff.edited)
403
+ console.log(color.yellow(`[gonvex] ~ edited function ${path}`));
404
+ for (const path of diff.removed)
405
+ console.log(color.red(`[gonvex] - removed function ${path}`));
406
+ }
407
+ function diffFunctions(previous, current) {
408
+ const previousPaths = new Set(Object.keys(previous.functions));
409
+ const currentPaths = new Set(Object.keys(current.functions));
410
+ const added = [...currentPaths].filter((path) => !previousPaths.has(path)).sort();
411
+ const removed = [...previousPaths].filter((path) => !currentPaths.has(path)).sort();
412
+ const edited = [...currentPaths]
413
+ .filter((path) => previousPaths.has(path))
414
+ .filter((path) => functionEntryChanged(previous, current, path))
415
+ .sort();
416
+ return { added, removed, edited };
417
+ }
418
+ function functionEntryChanged(previous, current, path) {
419
+ const previousEntry = previous.functions[path];
420
+ const currentEntry = current.functions[path];
421
+ if (JSON.stringify(previousEntry) !== JSON.stringify(currentEntry))
422
+ return true;
423
+ const previousBundleFile = bundleFileForFunction(previousEntry);
424
+ const currentBundleFile = bundleFileForFunction(currentEntry);
425
+ return previous.bundle?.files[previousBundleFile] !== current.bundle?.files[currentBundleFile];
426
+ }
427
+ function bundleFileForFunction(entry) {
428
+ const normalized = entry.file.replace(/\\/g, "/").replace(/^\.?\//, "");
429
+ const withoutGonvexPrefix = normalized.startsWith("gonvex/") ? normalized.slice("gonvex/".length) : normalized;
430
+ return `app/${withoutGonvexPrefix}`;
431
+ }
432
+ function renderAPI(manifest) {
433
+ const root = {};
434
+ for (const [path, entry] of Object.entries(manifest.functions).sort(([a], [b]) => a.localeCompare(b))) {
435
+ const parts = path.split(".").filter(Boolean);
436
+ if (parts.length === 0)
437
+ continue;
438
+ let target = root;
439
+ for (const part of parts.slice(0, -1)) {
440
+ target = target[part] ??= {};
441
+ }
442
+ target[parts[parts.length - 1]] = { kind: entry.kind, path };
443
+ }
444
+ const lines = [
445
+ "// Generated by gonvex dev. Do not edit.",
446
+ "",
447
+ `export const api = ${renderObject(root, 0)} as const;`,
448
+ "",
449
+ "export const internal = api;",
450
+ "export type Api = typeof api;",
451
+ "",
452
+ ];
453
+ return lines.join("\n");
454
+ }
455
+ function renderSchemaIndex(manifest) {
456
+ const landlord = renderSchemaObject("landlord", manifest.schema.landlordTables, 0);
457
+ const tenant = renderSchemaObject("tenant", manifest.schema.tenantTables, 0);
458
+ return [
459
+ "// Generated by gonvex dev. Do not edit.",
460
+ "",
461
+ `export const landlord = ${landlord} as const;`,
462
+ "",
463
+ `export const tenant = ${tenant} as const;`,
464
+ "",
465
+ "export const tables = tenant.tables;",
466
+ "",
467
+ "export const schema = {",
468
+ " landlord,",
469
+ " tenant,",
470
+ " tables,",
471
+ "} as const;",
472
+ "",
473
+ "export type LandlordTableName = keyof typeof landlord.tables;",
474
+ "export type TenantTableName = keyof typeof tenant.tables;",
475
+ "export type TableName = TenantTableName;",
476
+ "",
477
+ ].join("\n");
478
+ }
479
+ function renderScopedSchemaModule(scope, tables) {
480
+ return [
481
+ "// Generated by gonvex dev. Do not edit.",
482
+ "",
483
+ `export const schema = ${renderSchemaObject(scope, tables, 0)} as const;`,
484
+ "",
485
+ "export const tables = schema.tables;",
486
+ "",
487
+ "export type TableName = keyof typeof tables;",
488
+ "",
489
+ ].join("\n");
490
+ }
491
+ function renderScopedTablesModule(scope, tables) {
492
+ return [
493
+ "// Generated by gonvex dev. Do not edit.",
494
+ "",
495
+ `export const scope = ${JSON.stringify(scope)} as const;`,
496
+ "",
497
+ `export const tables = ${renderObject(tables, 0)} as const;`,
498
+ "",
499
+ "export type TableName = keyof typeof tables;",
500
+ "",
501
+ ].join("\n");
502
+ }
503
+ function renderSchemaObject(scope, tables, depth) {
504
+ return renderObject({ scope, tables }, depth);
505
+ }
506
+ function renderObject(value, depth) {
507
+ if (!value || typeof value !== "object" || Array.isArray(value))
508
+ return JSON.stringify(value);
509
+ const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
510
+ if (isFunctionRef(value)) {
511
+ return `{ kind: ${JSON.stringify(value.kind)}, path: ${JSON.stringify(value.path)} }`;
512
+ }
513
+ const indent = " ".repeat(depth);
514
+ const childIndent = " ".repeat(depth + 1);
515
+ const lines = ["{"];
516
+ for (const [key, child] of entries) {
517
+ lines.push(`${childIndent}${propertyKey(key)}: ${renderObject(child, depth + 1)},`);
518
+ }
519
+ lines.push(`${indent}}`);
520
+ return lines.join("\n");
521
+ }
522
+ function isFunctionRef(value) {
523
+ return typeof value.kind === "string" && typeof value.path === "string" && Object.keys(value).length === 2;
524
+ }
525
+ function propertyKey(key) {
526
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
527
+ }
528
+ function emptySchemaDefinition() {
529
+ return {
530
+ tables: {},
531
+ landlordTables: {},
532
+ tenantTables: {},
533
+ };
534
+ }
535
+ function mergeSchemaDefinition(target, source) {
536
+ Object.assign(target.landlordTables, source.landlordTables);
537
+ Object.assign(target.tenantTables, source.tenantTables);
538
+ target.tables = target.tenantTables;
539
+ }
540
+ async function syncRuntime(settings, manifest) {
541
+ const response = await fetch(`${settings.runtimeURL.replace(/\/$/, "")}/dev/sync`, {
542
+ method: "POST",
543
+ headers: {
544
+ "content-type": "application/json",
545
+ ...(settings.projectID ? { "x-gonvex-project-id": settings.projectID } : {}),
546
+ ...(settings.key ? { authorization: `Bearer ${settings.key}`, "x-gonvex-key": settings.key } : {}),
547
+ },
548
+ body: JSON.stringify(manifest),
549
+ });
550
+ if (!response.ok)
551
+ throw new Error(`runtime returned ${response.status} ${response.statusText}: ${await response.text()}`);
552
+ }
553
+ async function fetchProjectEnv(settings) {
554
+ const response = await fetch(projectEnvURL(settings), {
555
+ headers: projectAuthHeaders(settings),
556
+ });
557
+ if (!response.ok)
558
+ throw new Error(`runtime returned ${response.status} ${response.statusText}: ${await response.text()}`);
559
+ const payload = await response.json();
560
+ return payload.variables ?? [];
561
+ }
562
+ async function saveProjectEnv(settings, name, value) {
563
+ const response = await fetch(projectEnvURL(settings), {
564
+ method: "POST",
565
+ headers: {
566
+ "content-type": "application/json",
567
+ ...projectAuthHeaders(settings),
568
+ },
569
+ body: JSON.stringify({ name, value }),
570
+ });
571
+ if (!response.ok)
572
+ throw new Error(`runtime returned ${response.status} ${response.statusText}: ${await response.text()}`);
573
+ }
574
+ async function deleteProjectEnv(settings, name) {
575
+ const response = await fetch(projectEnvURL(settings), {
576
+ method: "DELETE",
577
+ headers: {
578
+ "content-type": "application/json",
579
+ ...projectAuthHeaders(settings),
580
+ },
581
+ body: JSON.stringify({ name }),
582
+ });
583
+ if (!response.ok)
584
+ throw new Error(`runtime returned ${response.status} ${response.statusText}: ${await response.text()}`);
585
+ }
586
+ function projectEnvURL(settings) {
587
+ return `${settings.runtimeURL.replace(/\/$/, "")}/dev/projects/${encodeURIComponent(settings.projectID)}/env`;
588
+ }
589
+ function projectAuthHeaders(settings) {
590
+ return settings.key ? { authorization: `Bearer ${settings.key}`, "x-gonvex-key": settings.key } : {};
591
+ }
592
+ function parseEnvSetArgs(positional) {
593
+ const first = positional[0];
594
+ if (!first)
595
+ throw new Error("usage: gonvex env set NAME VALUE");
596
+ const equalsIndex = first.indexOf("=");
597
+ if (equalsIndex > 0 && positional.length === 1) {
598
+ return { name: first.slice(0, equalsIndex), value: first.slice(equalsIndex + 1) };
599
+ }
600
+ const value = positional.slice(1).join(" ");
601
+ if (!value)
602
+ throw new Error("usage: gonvex env set NAME VALUE");
603
+ return { name: first, value };
604
+ }
605
+ function parseEnvCommandArgs(argv) {
606
+ const options = {};
607
+ const positional = [];
608
+ let action = "";
609
+ for (let index = 0; index < argv.length; index += 1) {
610
+ const arg = argv[index];
611
+ if (["--project", "--runtime-url", "--project-id", "--key"].includes(arg)) {
612
+ const value = argv[index + 1];
613
+ if (value === undefined)
614
+ throw new Error(`${arg} requires a value`);
615
+ options[arg] = value;
616
+ index += 1;
617
+ continue;
618
+ }
619
+ if (!action && !arg.startsWith("-")) {
620
+ action = arg;
621
+ continue;
622
+ }
623
+ positional.push(arg);
624
+ }
625
+ return { action, options, positional };
626
+ }
627
+ async function runtimeHasManifest(settings, manifest) {
628
+ if (!manifest.project)
629
+ return true;
630
+ const url = new URL(`${settings.runtimeURL.replace(/\/$/, "")}/dev/manifest`);
631
+ url.searchParams.set("project", manifest.project);
632
+ const response = await fetch(url, {
633
+ headers: {
634
+ ...(settings.projectID ? { "x-gonvex-project-id": settings.projectID } : {}),
635
+ ...(settings.key ? { authorization: `Bearer ${settings.key}`, "x-gonvex-key": settings.key } : {}),
636
+ },
637
+ });
638
+ if (!response.ok)
639
+ return false;
640
+ const current = await response.json();
641
+ return current.project === manifest.project
642
+ && current.bundle?.hash === manifest.bundle?.hash
643
+ && Object.keys(current.functions ?? {}).length === Object.keys(manifest.functions ?? {}).length;
644
+ }
645
+ async function ensureProjectSettings(root, settings, options) {
646
+ if (settings.key)
647
+ return settings;
648
+ if (options.keyWasExplicit)
649
+ return settings;
650
+ const configuredProject = await findRuntimeProject(settings.runtimeURL, settings.projectID).catch(() => null);
651
+ if (configuredProject) {
652
+ await writeProjectEnv(root, settings.runtimeURL, configuredProject.id, settings.key);
653
+ console.log(`[gonvex] configured ${configuredProject.id} in .env.local`);
654
+ console.warn("[gonvex] GONVEX_PROJECT_KEY is not configured; runtime sync will fail if the runtime requires project keys.");
655
+ return { ...settings, projectID: configuredProject.id };
656
+ }
657
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
658
+ console.warn("[gonvex] GONVEX_PROJECT_KEY is not configured; runtime sync will fail if the runtime requires project keys.");
659
+ return settings;
660
+ }
661
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
662
+ try {
663
+ console.log("[gonvex] No Gonvex project is configured for this app.");
664
+ const runtimeURL = await promptDefault(rl, "Runtime URL", settings.runtimeURL || defaultRuntimeURL);
665
+ settings = { ...settings, runtimeURL };
666
+ const projects = await fetchRuntimeProjects(runtimeURL);
667
+ let project;
668
+ if (projects.length > 0) {
669
+ console.log("[gonvex] Choose a project:");
670
+ projects.forEach((item, index) => {
671
+ console.log(` ${index + 1}. ${item.name} (${item.id})`);
672
+ });
673
+ console.log(` ${projects.length + 1}. Create a new project`);
674
+ const choice = await promptDefault(rl, "Project", "1");
675
+ const index = Number.parseInt(choice, 10);
676
+ if (Number.isFinite(index) && index >= 1 && index <= projects.length) {
677
+ project = projects[index - 1];
678
+ const projectKey = await promptDefault(rl, "Project key from dashboard", "");
679
+ if (!projectKey)
680
+ throw new Error("Gonvex project key is required for existing projects");
681
+ const next = { ...settings, projectID: project.id, key: projectKey };
682
+ await writeProjectEnv(root, runtimeURL, project.id, projectKey);
683
+ console.log(`[gonvex] configured ${project.id} in .env.local`);
684
+ return next;
685
+ }
686
+ else {
687
+ const created = await createRuntimeProject(runtimeURL, await promptDefault(rl, "New project name", basename(root)));
688
+ project = created.project;
689
+ const next = { ...settings, projectID: project.id, key: created.projectKey };
690
+ await writeProjectEnv(root, runtimeURL, project.id, created.projectKey);
691
+ console.log(`[gonvex] configured ${project.id} in .env.local`);
692
+ return next;
693
+ }
694
+ }
695
+ else {
696
+ const created = await createRuntimeProject(runtimeURL, await promptDefault(rl, "New project name", basename(root)));
697
+ project = created.project;
698
+ const next = { ...settings, projectID: project.id, key: created.projectKey };
699
+ await writeProjectEnv(root, runtimeURL, project.id, created.projectKey);
700
+ console.log(`[gonvex] configured ${project.id} in .env.local`);
701
+ return next;
702
+ }
703
+ }
704
+ finally {
705
+ rl.close();
706
+ }
707
+ }
708
+ async function findRuntimeProject(runtimeURL, projectID) {
709
+ const wanted = projectID.trim();
710
+ if (!wanted)
711
+ return null;
712
+ const projects = await fetchRuntimeProjects(runtimeURL);
713
+ return projects.find((project) => project.id === wanted) ?? null;
714
+ }
715
+ async function fetchRuntimeProjects(runtimeURL) {
716
+ const response = await fetch(`${runtimeURL.replace(/\/$/, "")}/dev/projects`);
717
+ if (!response.ok)
718
+ throw new Error(`runtime returned ${response.status} ${response.statusText}: ${await response.text()}`);
719
+ const payload = await response.json();
720
+ return payload.projects ?? [];
721
+ }
722
+ async function createRuntimeProject(runtimeURL, name) {
723
+ const response = await fetch(`${runtimeURL.replace(/\/$/, "")}/dev/projects`, {
724
+ method: "POST",
725
+ headers: { "content-type": "application/json" },
726
+ body: JSON.stringify({ name }),
727
+ });
728
+ if (!response.ok)
729
+ throw new Error(`runtime returned ${response.status} ${response.statusText}: ${await response.text()}`);
730
+ const payload = await response.json();
731
+ if (!payload.projectKey)
732
+ throw new Error("runtime did not return a project key");
733
+ return { project: payload.project, projectKey: payload.projectKey };
734
+ }
735
+ async function promptDefault(rl, label, fallback) {
736
+ const answer = (await rl.question(`${label} (${fallback}): `)).trim();
737
+ return answer || fallback;
738
+ }
739
+ async function writeProjectEnv(root, runtimeURL, projectID, projectKey) {
740
+ await upsertEnvLocal(root, {
741
+ GONVEX_PROJECT_ID: projectID,
742
+ GONVEX_RUNTIME_URL: runtimeURL,
743
+ GONVEX_PROJECT_KEY: projectKey,
744
+ VITE_GONVEX_PROJECT_ID: projectID,
745
+ VITE_GONVEX_URL: runtimeURL,
746
+ VITE_GONVEX_WS_URL: webSocketURL(runtimeURL),
747
+ });
748
+ }
749
+ async function upsertEnvLocal(root, values) {
750
+ const envPath = join(root, ".env.local");
751
+ const existing = existsSync(envPath) ? await readFile(envPath, "utf8") : "";
752
+ const seen = new Set();
753
+ const lines = existing.split(/\r?\n/).filter((line, index, array) => index < array.length - 1 || line !== "");
754
+ const next = lines.flatMap((line) => {
755
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
756
+ if (!match)
757
+ return [line];
758
+ const key = match[1];
759
+ if (!(key in values))
760
+ return [line];
761
+ if (seen.has(key))
762
+ return [];
763
+ seen.add(key);
764
+ if (envLineValue(line) !== "")
765
+ return [line];
766
+ return [`${key}=${values[key]}`];
767
+ });
768
+ for (const [key, value] of Object.entries(values)) {
769
+ if (!seen.has(key))
770
+ next.push(`${key}=${value}`);
771
+ }
772
+ await writeFile(envPath, `${next.join("\n")}\n`);
773
+ }
774
+ function envLineValue(line) {
775
+ const index = line.indexOf("=");
776
+ if (index === -1)
777
+ return "";
778
+ return line.slice(index + 1).trim().replace(/^['"]|['"]$/g, "");
779
+ }
780
+ function webSocketURL(runtimeURL) {
781
+ return runtimeURL.replace(/^http:/, "ws:").replace(/^https:/, "wss:").replace(/\/$/, "") + "/ws";
782
+ }
783
+ async function loadSettings(root, overrides) {
784
+ loadDotEnv(join(root, ".env.local"));
785
+ loadDotEnv(join(root, ".env"));
786
+ const config = await loadConfig(root);
787
+ const key = overrides.key ?? process.env.GONVEX_PROJECT_KEY ?? process.env.GONVEX_DEPLOY_KEY ?? process.env.GONVEX_KEY ?? "";
788
+ const explicitProjectID = overrides.projectID ?? process.env.GONVEX_PROJECT_ID ?? process.env.GONVEX_PROJECT ?? config.project;
789
+ return {
790
+ projectID: overrides.projectID ?? projectIDFromKey(key) ?? explicitProjectID ?? basename(root),
791
+ runtimeURL: overrides.runtimeURL ?? process.env.GONVEX_RUNTIME_URL ?? config.runtime ?? defaultRuntimeURL,
792
+ key,
793
+ };
794
+ }
795
+ function projectIDFromKey(key) {
796
+ const trimmed = key.trim();
797
+ if (!trimmed.startsWith("gvx_"))
798
+ return undefined;
799
+ const payload = trimmed.slice("gvx_".length);
800
+ let encodedProject = payload.split(".", 1)[0];
801
+ if (encodedProject === payload) {
802
+ const parts = trimmed.split("_");
803
+ if (parts.length !== 3 || parts[0] !== "gvx" || !parts[1])
804
+ return undefined;
805
+ encodedProject = parts[1];
806
+ }
807
+ try {
808
+ return Buffer.from(encodedProject, "base64url").toString("utf8").trim() || undefined;
809
+ }
810
+ catch {
811
+ return undefined;
812
+ }
813
+ }
814
+ async function loadConfig(root) {
815
+ try {
816
+ return JSON.parse(await readFile(join(root, "gonvex.json"), "utf8"));
817
+ }
818
+ catch {
819
+ return {};
820
+ }
821
+ }
822
+ function loadDotEnv(path) {
823
+ if (!existsSync(path))
824
+ return;
825
+ const content = readFileSyncText(path);
826
+ for (const rawLine of content.split(/\r?\n/)) {
827
+ const line = rawLine.trim();
828
+ if (!line || line.startsWith("#"))
829
+ continue;
830
+ const index = line.indexOf("=");
831
+ if (index === -1)
832
+ continue;
833
+ const key = line.slice(0, index).trim();
834
+ if (!key || process.env[key] !== undefined)
835
+ continue;
836
+ process.env[key] = line.slice(index + 1).trim().replace(/^['"]|['"]$/g, "");
837
+ }
838
+ }
839
+ function readFileSyncText(path) {
840
+ return existsSync(path) ? readFileSync(path, "utf8") : "";
841
+ }
842
+ async function goFiles(root) {
843
+ if (!existsSync(root))
844
+ return [];
845
+ const entries = await readdir(root, { withFileTypes: true });
846
+ const files = [];
847
+ for (const entry of entries) {
848
+ const path = join(root, entry.name);
849
+ if (entry.isDirectory()) {
850
+ if (entry.name === "_generated")
851
+ continue;
852
+ files.push(...await goFiles(path));
853
+ }
854
+ else if (entry.isFile() && entry.name.endsWith(".go")) {
855
+ files.push(path);
856
+ }
857
+ }
858
+ return files.sort();
859
+ }
860
+ async function filesFingerprint(files) {
861
+ const hash = createHash("sha256");
862
+ for (const file of files) {
863
+ const info = await stat(file);
864
+ hash.update(`${file}:${info.mtimeMs}:${info.size};`);
865
+ }
866
+ return hash.digest("hex");
867
+ }
868
+ async function copyTemplate(template, target, options = {}) {
869
+ const source = templateDir(template);
870
+ if (!existsSync(source))
871
+ throw new Error(`unknown template ${template}`);
872
+ await copyDir(source, target, options.overwrite ?? true);
873
+ }
874
+ async function copyDir(source, target, overwrite) {
875
+ await mkdir(target, { recursive: true });
876
+ for (const entry of await readdir(source, { withFileTypes: true })) {
877
+ const sourcePath = join(source, entry.name);
878
+ const targetPath = join(target, entry.name);
879
+ if (entry.isDirectory()) {
880
+ await copyDir(sourcePath, targetPath, overwrite);
881
+ }
882
+ else if (overwrite || !existsSync(targetPath)) {
883
+ await mkdir(dirname(targetPath), { recursive: true });
884
+ await copyFile(sourcePath, targetPath);
885
+ }
886
+ }
887
+ }
888
+ async function rewritePackageName(root, name) {
889
+ const packagePath = join(root, "package.json");
890
+ const packageJSON = JSON.parse(await readFile(packagePath, "utf8"));
891
+ packageJSON.name = name;
892
+ await writeFile(packagePath, `${JSON.stringify(packageJSON, null, 2)}\n`);
893
+ }
894
+ async function rewriteGonvexConfig(root, project, runtime) {
895
+ const configPath = join(root, "gonvex.json");
896
+ if (!existsSync(configPath))
897
+ return;
898
+ const config = JSON.parse(await readFile(configPath, "utf8"));
899
+ config.project = project;
900
+ config.runtime = runtime;
901
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
902
+ }
903
+ async function writeEnvLocal(root, project, runtime) {
904
+ const envPath = join(root, ".env.local");
905
+ if (existsSync(envPath))
906
+ return;
907
+ const wsURL = runtime.replace(/^http:/, "ws:").replace(/^https:/, "wss:").replace(/\/$/, "") + "/ws";
908
+ await writeFile(envPath, `GONVEX_PROJECT_ID=${project}\nGONVEX_RUNTIME_URL=${runtime}\nGONVEX_PROJECT_KEY=\nVITE_GONVEX_WS_URL=${wsURL}\n`);
909
+ }
910
+ function templateDir(template) {
911
+ const packageTemplate = resolve(dirname(fileURLToPath(import.meta.url)), "templates", template);
912
+ if (existsSync(packageTemplate))
913
+ return packageTemplate;
914
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "templates", template);
915
+ }
916
+ function functionKind(raw) {
917
+ if (raw === "InternalMutation")
918
+ return "internalMutation";
919
+ if (raw === "LiveGrid")
920
+ return "liveGrid";
921
+ return raw.toLowerCase();
922
+ }
923
+ function columnType(kind) {
924
+ if (kind === "ID")
925
+ return "id";
926
+ if (kind === "Int64")
927
+ return "int64";
928
+ if (kind === "Float64")
929
+ return "float64";
930
+ return kind.toLowerCase();
931
+ }
932
+ function stringArgs(input) {
933
+ return [...input.matchAll(/"([^"]+)"/g)].map((match) => match[1]);
934
+ }
935
+ function valueFor(args, key) {
936
+ const index = args.indexOf(key);
937
+ if (index === -1)
938
+ return undefined;
939
+ return args[index + 1];
940
+ }
941
+ function basename(path) {
942
+ const parts = path.split(/[\\/]/).filter(Boolean);
943
+ return parts.at(-1) ?? "app";
944
+ }
945
+ function sleep(ms, signal) {
946
+ return new Promise((resolve, reject) => {
947
+ const timeout = setTimeout(resolve, ms);
948
+ signal?.addEventListener("abort", () => {
949
+ clearTimeout(timeout);
950
+ reject(new DOMException("aborted", "AbortError"));
951
+ }, { once: true });
952
+ });
953
+ }
954
+ function printHelp() {
955
+ console.log("Usage: gonvex <dev|init|create|env> [options]");
956
+ console.log(" gonvex dev [--project <path>] [--runtime-url <url>] [--project-id <id>] [--key <key>] [--once] [-- <command>]");
957
+ console.log(" gonvex init [--template vite-react] [--project <id>] [--runtime <url>]");
958
+ console.log(" gonvex create <app-name> [--template vite-react]");
959
+ console.log(" gonvex env <list|get|set|remove> [--project <path>] [--runtime-url <url>] [--project-id <id>] [--key <key>]");
960
+ }
961
+ function printEnvHelp() {
962
+ console.log("Usage: gonvex env <command> [options]");
963
+ console.log(" gonvex env list");
964
+ console.log(" gonvex env get NAME");
965
+ console.log(" gonvex env set NAME VALUE");
966
+ console.log(" gonvex env set NAME=VALUE");
967
+ console.log(" gonvex env remove NAME");
968
+ }
969
+ function isCliEntrypoint() {
970
+ const invokedPath = process.argv[1];
971
+ if (!invokedPath)
972
+ return false;
973
+ try {
974
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve(invokedPath));
975
+ }
976
+ catch {
977
+ return fileURLToPath(import.meta.url) === resolve(invokedPath);
978
+ }
979
+ }
980
+ if (isCliEntrypoint()) {
981
+ main().catch((error) => {
982
+ console.error(error instanceof Error ? error.message : String(error));
983
+ process.exit(1);
984
+ });
985
+ }
986
+ //# sourceMappingURL=index.js.map