tthr 0.0.61 → 0.2.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.
@@ -0,0 +1,546 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/commands/generate.ts
9
+ import chalk2 from "chalk";
10
+ import ora from "ora";
11
+ import fs3 from "fs-extra";
12
+ import path3 from "path";
13
+
14
+ // src/utils/auth.ts
15
+ import chalk from "chalk";
16
+ import fs from "fs-extra";
17
+ import path from "path";
18
+ var CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
19
+ var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
20
+ var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
21
+ var API_URL = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
22
+ var REFRESH_THRESHOLD_DAYS = 7;
23
+ async function getCredentials() {
24
+ try {
25
+ if (await fs.pathExists(CREDENTIALS_FILE)) {
26
+ return await fs.readJSON(CREDENTIALS_FILE);
27
+ }
28
+ } catch {
29
+ }
30
+ return null;
31
+ }
32
+ async function refreshSession(credentials) {
33
+ try {
34
+ const response = await fetch(`${API_URL}/auth/session/refresh`, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ "Authorization": `Bearer ${credentials.accessToken}`
39
+ }
40
+ });
41
+ if (!response.ok) {
42
+ return null;
43
+ }
44
+ const data = await response.json();
45
+ const updated = {
46
+ ...credentials,
47
+ expiresAt: data.expiresAt
48
+ };
49
+ await saveCredentials(updated);
50
+ return updated;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function requireAuth() {
56
+ const credentials = await getCredentials();
57
+ if (!credentials) {
58
+ console.error(chalk.red("\n\u2717 Not logged in\n"));
59
+ console.log(chalk.dim("Run `tthr login` to authenticate\n"));
60
+ process.exit(1);
61
+ }
62
+ if (credentials.expiresAt) {
63
+ const expiresAt = new Date(credentials.expiresAt);
64
+ const now = /* @__PURE__ */ new Date();
65
+ if (now > expiresAt) {
66
+ await clearCredentials();
67
+ console.error(chalk.red("\n\u2717 Session expired \u2014 you have been signed out\n"));
68
+ console.log(chalk.dim("Run `tthr login` to authenticate\n"));
69
+ process.exit(1);
70
+ }
71
+ const daysUntilExpiry = (expiresAt.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24);
72
+ if (daysUntilExpiry <= REFRESH_THRESHOLD_DAYS) {
73
+ const refreshed = await refreshSession(credentials);
74
+ if (refreshed) {
75
+ return refreshed;
76
+ }
77
+ }
78
+ }
79
+ return credentials;
80
+ }
81
+ async function saveCredentials(credentials) {
82
+ await fs.ensureDir(CONFIG_DIR);
83
+ await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
84
+ }
85
+ async function clearCredentials() {
86
+ try {
87
+ await fs.remove(CREDENTIALS_FILE);
88
+ } catch {
89
+ }
90
+ }
91
+
92
+ // src/utils/config.ts
93
+ import fs2 from "fs-extra";
94
+ import path2 from "path";
95
+ var DEFAULT_CONFIG = {
96
+ schema: "./tether/schema.ts",
97
+ functions: "./tether/functions",
98
+ output: "./tether/_generated",
99
+ dev: {
100
+ port: 3001,
101
+ host: "localhost"
102
+ },
103
+ database: {
104
+ walMode: true
105
+ }
106
+ };
107
+ async function loadConfig(cwd = process.cwd()) {
108
+ const configPath = path2.resolve(cwd, "tether.config.ts");
109
+ if (!await fs2.pathExists(configPath)) {
110
+ return DEFAULT_CONFIG;
111
+ }
112
+ const configSource = await fs2.readFile(configPath, "utf-8");
113
+ const config = {};
114
+ const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
115
+ if (schemaMatch) {
116
+ config.schema = schemaMatch[1];
117
+ }
118
+ const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
119
+ if (functionsMatch) {
120
+ config.functions = functionsMatch[1];
121
+ }
122
+ const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
123
+ if (outputMatch) {
124
+ config.output = outputMatch[1];
125
+ }
126
+ const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
127
+ if (envMatch) {
128
+ config.environment = envMatch[1];
129
+ }
130
+ const portMatch = configSource.match(/port\s*:\s*(\d+)/);
131
+ if (portMatch) {
132
+ config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
133
+ }
134
+ const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
135
+ if (hostMatch) {
136
+ config.dev = { ...config.dev, host: hostMatch[1] };
137
+ }
138
+ return {
139
+ ...DEFAULT_CONFIG,
140
+ ...config,
141
+ dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
142
+ database: { ...DEFAULT_CONFIG.database, ...config.database }
143
+ };
144
+ }
145
+ function resolvePath(configPath, cwd = process.cwd()) {
146
+ const normalised = configPath.replace(/^\.\//, "");
147
+ return path2.resolve(cwd, normalised);
148
+ }
149
+ async function detectFramework(cwd = process.cwd()) {
150
+ const packageJsonPath = path2.resolve(cwd, "package.json");
151
+ if (!await fs2.pathExists(packageJsonPath)) {
152
+ return "unknown";
153
+ }
154
+ try {
155
+ const packageJson = await fs2.readJson(packageJsonPath);
156
+ const deps = {
157
+ ...packageJson.dependencies,
158
+ ...packageJson.devDependencies
159
+ };
160
+ if (deps.nuxt || deps["@nuxt/kit"]) {
161
+ return "nuxt";
162
+ }
163
+ if (deps.next) {
164
+ return "next";
165
+ }
166
+ if (deps["@sveltejs/kit"]) {
167
+ return "sveltekit";
168
+ }
169
+ if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
170
+ return "vite";
171
+ }
172
+ return "vanilla";
173
+ } catch {
174
+ return "unknown";
175
+ }
176
+ }
177
+ function getFrameworkDevCommand(framework) {
178
+ switch (framework) {
179
+ case "nuxt":
180
+ return "nuxt dev";
181
+ case "next":
182
+ return "next dev";
183
+ case "sveltekit":
184
+ return "vite dev";
185
+ case "vite":
186
+ return "vite dev";
187
+ case "vanilla":
188
+ return null;
189
+ // No default dev server for vanilla
190
+ default:
191
+ return null;
192
+ }
193
+ }
194
+
195
+ // src/commands/generate.ts
196
+ function parseSchemaFile(source) {
197
+ const tables = [];
198
+ const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
199
+ if (!schemaMatch) return tables;
200
+ const schemaContent = schemaMatch[1];
201
+ const tableStartRegex = /(\w+)\s*:\s*\{/g;
202
+ let match;
203
+ while ((match = tableStartRegex.exec(schemaContent)) !== null) {
204
+ const tableName = match[1];
205
+ const startOffset = match.index + match[0].length;
206
+ let braceCount = 1;
207
+ let endOffset = startOffset;
208
+ for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
209
+ const char = schemaContent[i];
210
+ if (char === "{") braceCount++;
211
+ else if (char === "}") braceCount--;
212
+ endOffset = i;
213
+ }
214
+ const columnsContent = schemaContent.slice(startOffset, endOffset);
215
+ const columns = parseColumns(columnsContent);
216
+ tables.push({ name: tableName, columns });
217
+ }
218
+ return tables;
219
+ }
220
+ function parseColumns(content) {
221
+ const columns = [];
222
+ const columnRegex = /(\w+)\s*:\s*(\w+)(?:<([^>]+)>)?\s*\(\s*\)((?:\[.*?\]|[^,\n}])*)/g;
223
+ let match;
224
+ while ((match = columnRegex.exec(content)) !== null) {
225
+ const name = match[1];
226
+ const schemaType = match[2];
227
+ const genericType = match[3];
228
+ const modifiers = match[4] || "";
229
+ const nullable = !modifiers.includes(".notNull()");
230
+ const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
231
+ let oneOf;
232
+ if (oneOfMatch) {
233
+ oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
234
+ }
235
+ columns.push({
236
+ name,
237
+ type: schemaTypeToTS(schemaType),
238
+ nullable,
239
+ jsonType: genericType,
240
+ // Store the generic type for json columns
241
+ oneOf
242
+ });
243
+ }
244
+ return columns;
245
+ }
246
+ function schemaTypeToTS(schemaType) {
247
+ switch (schemaType) {
248
+ case "text":
249
+ return "string";
250
+ case "integer":
251
+ return "number";
252
+ case "real":
253
+ return "number";
254
+ case "boolean":
255
+ return "boolean";
256
+ case "timestamp":
257
+ return "string";
258
+ case "json":
259
+ return "unknown";
260
+ case "blob":
261
+ return "Uint8Array";
262
+ case "asset":
263
+ return "TetherAsset";
264
+ // Asset object returned by API
265
+ default:
266
+ return "unknown";
267
+ }
268
+ }
269
+ function tableNameToInterface(tableName) {
270
+ let name = tableName;
271
+ if (name.endsWith("ies")) {
272
+ name = name.slice(0, -3) + "y";
273
+ } else if (name.endsWith("s") && !name.endsWith("ss")) {
274
+ name = name.slice(0, -1);
275
+ }
276
+ return name.charAt(0).toUpperCase() + name.slice(1);
277
+ }
278
+ function generateDbFile(tables) {
279
+ const jsonTypes = /* @__PURE__ */ new Set();
280
+ for (const table of tables) {
281
+ for (const col of table.columns) {
282
+ if (col.jsonType) {
283
+ jsonTypes.add(col.jsonType.replace(/\[\]$/, ""));
284
+ }
285
+ }
286
+ }
287
+ const lines = [
288
+ "// Auto-generated by Tether CLI - do not edit manually",
289
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
290
+ "",
291
+ "import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
292
+ "import {",
293
+ " query as baseQuery,",
294
+ " mutation as baseMutation,",
295
+ " type QueryDefinition,",
296
+ " type MutationDefinition,",
297
+ " z,",
298
+ "} from '@tthr/server';"
299
+ ];
300
+ if (jsonTypes.size > 0) {
301
+ const typeImports = Array.from(jsonTypes).sort().join(", ");
302
+ lines.push(`import type { ${typeImports} } from '../schema';`);
303
+ }
304
+ lines.push("");
305
+ lines.push("// Asset type returned by the API for asset columns");
306
+ lines.push("export interface TetherAsset {");
307
+ lines.push(" id: string;");
308
+ lines.push(" filename: string;");
309
+ lines.push(" contentType: string;");
310
+ lines.push(" size: number;");
311
+ lines.push(" url: string;");
312
+ lines.push(" createdAt: string;");
313
+ lines.push("}");
314
+ lines.push("");
315
+ for (const table of tables) {
316
+ const interfaceName = tableNameToInterface(table.name);
317
+ lines.push(`export interface ${interfaceName} {`);
318
+ lines.push(" _id: string;");
319
+ for (const col of table.columns) {
320
+ let colType;
321
+ if (col.oneOf && col.oneOf.length > 0) {
322
+ colType = col.oneOf.map((v) => `'${v}'`).join(" | ");
323
+ } else {
324
+ colType = col.jsonType || col.type;
325
+ }
326
+ const typeStr = col.nullable ? `${colType} | null` : colType;
327
+ const optional = col.nullable ? "?" : "";
328
+ lines.push(` ${col.name}${optional}: ${typeStr};`);
329
+ }
330
+ lines.push(" _createdAt: string;");
331
+ lines.push(" _updatedAt?: string | null;");
332
+ lines.push(" _deletedAt?: string | null;");
333
+ lines.push("}");
334
+ lines.push("");
335
+ }
336
+ lines.push("export interface Schema {");
337
+ for (const table of tables) {
338
+ const interfaceName = tableNameToInterface(table.name);
339
+ lines.push(` ${table.name}: ${interfaceName};`);
340
+ }
341
+ lines.push("}");
342
+ lines.push("");
343
+ lines.push("// Database client with typed tables");
344
+ lines.push("// This is a proxy that will be populated by the Tether runtime");
345
+ lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
346
+ lines.push("");
347
+ lines.push("// ============================================================================");
348
+ lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
349
+ lines.push("// This ensures the `db` parameter in handlers is properly typed with Schema");
350
+ lines.push("// ============================================================================");
351
+ lines.push("");
352
+ lines.push("/**");
353
+ lines.push(" * Define a query function with typed database access.");
354
+ lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
355
+ lines.push(" */");
356
+ lines.push("export function query<TArgs = void, TResult = unknown>(");
357
+ lines.push(" definition: QueryDefinition<TArgs, TResult, Schema>");
358
+ lines.push("): QueryDefinition<TArgs, TResult, Schema> {");
359
+ lines.push(" return baseQuery(definition);");
360
+ lines.push("}");
361
+ lines.push("");
362
+ lines.push("/**");
363
+ lines.push(" * Define a mutation function with typed database access.");
364
+ lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
365
+ lines.push(" */");
366
+ lines.push("export function mutation<TArgs = void, TResult = unknown>(");
367
+ lines.push(" definition: MutationDefinition<TArgs, TResult, Schema>");
368
+ lines.push("): MutationDefinition<TArgs, TResult, Schema> {");
369
+ lines.push(" return baseMutation(definition);");
370
+ lines.push("}");
371
+ lines.push("");
372
+ lines.push("// Re-export z for convenience");
373
+ lines.push("export { z };");
374
+ if (jsonTypes.size > 0) {
375
+ lines.push("");
376
+ lines.push("// Re-export JSON schema types");
377
+ const typeExports = Array.from(jsonTypes).sort().join(", ");
378
+ lines.push(`export type { ${typeExports} } from '../schema';`);
379
+ }
380
+ lines.push("");
381
+ return lines.join("\n");
382
+ }
383
+ async function parseFunctionsDir(functionsDir) {
384
+ const functions = [];
385
+ if (!await fs3.pathExists(functionsDir)) {
386
+ return functions;
387
+ }
388
+ const files = await fs3.readdir(functionsDir);
389
+ for (const file of files) {
390
+ if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
391
+ const filePath = path3.join(functionsDir, file);
392
+ const stat = await fs3.stat(filePath);
393
+ if (!stat.isFile()) continue;
394
+ const moduleName = file.replace(/\.(ts|js)$/, "");
395
+ const source = await fs3.readFile(filePath, "utf-8");
396
+ const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation)\s*\(/g;
397
+ let match;
398
+ while ((match = exportRegex.exec(source)) !== null) {
399
+ functions.push({
400
+ name: match[1],
401
+ moduleName
402
+ });
403
+ }
404
+ }
405
+ return functions;
406
+ }
407
+ async function generateApiFile(functionsDir) {
408
+ const functions = await parseFunctionsDir(functionsDir);
409
+ const moduleMap = /* @__PURE__ */ new Map();
410
+ for (const fn of functions) {
411
+ if (!moduleMap.has(fn.moduleName)) {
412
+ moduleMap.set(fn.moduleName, []);
413
+ }
414
+ moduleMap.get(fn.moduleName).push(fn.name);
415
+ }
416
+ const lines = [
417
+ "// Auto-generated by Tether CLI - do not edit manually",
418
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
419
+ "",
420
+ "import { createApiProxy } from '@tthr/client';",
421
+ "",
422
+ "/**",
423
+ " * API function reference type for useQuery/useMutation.",
424
+ ' * The _name property contains the function path (e.g., "users.list").',
425
+ " */",
426
+ "export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
427
+ " _name: string;",
428
+ " _args?: TArgs;",
429
+ " _result?: TResult;",
430
+ "}",
431
+ ""
432
+ ];
433
+ if (moduleMap.size > 0) {
434
+ for (const [moduleName, fnNames] of moduleMap) {
435
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
436
+ lines.push(`export interface ${interfaceName} {`);
437
+ for (const fnName of fnNames) {
438
+ lines.push(` ${fnName}: ApiFunction;`);
439
+ }
440
+ lines.push("}");
441
+ lines.push("");
442
+ }
443
+ lines.push("export interface Api {");
444
+ for (const moduleName of moduleMap.keys()) {
445
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
446
+ lines.push(` ${moduleName}: ${interfaceName};`);
447
+ }
448
+ lines.push("}");
449
+ } else {
450
+ lines.push("/**");
451
+ lines.push(" * Flexible API type that allows access to any function path.");
452
+ lines.push(" * Functions are accessed as api.moduleName.functionName");
453
+ lines.push(" * The actual types depend on your function definitions.");
454
+ lines.push(" */");
455
+ lines.push("export type Api = {");
456
+ lines.push(" [module: string]: {");
457
+ lines.push(" [fn: string]: ApiFunction;");
458
+ lines.push(" };");
459
+ lines.push("};");
460
+ }
461
+ lines.push("");
462
+ lines.push("// API client proxy - provides typed access to your functions");
463
+ lines.push("// On the client: returns { _name } references for useQuery/useMutation");
464
+ lines.push("// In Tether functions: calls the actual function implementation");
465
+ lines.push("export const api = createApiProxy<Api>();");
466
+ lines.push("");
467
+ return lines.join("\n");
468
+ }
469
+ async function generateTypes(options = {}) {
470
+ const config = await loadConfig();
471
+ const schemaPath = resolvePath(config.schema);
472
+ const outputDir = resolvePath(config.output);
473
+ const functionsDir = resolvePath(config.functions);
474
+ if (!await fs3.pathExists(schemaPath)) {
475
+ throw new Error(`Schema file not found: ${schemaPath}`);
476
+ }
477
+ const schemaSource = await fs3.readFile(schemaPath, "utf-8");
478
+ const tables = parseSchemaFile(schemaSource);
479
+ await fs3.ensureDir(outputDir);
480
+ await fs3.writeFile(
481
+ path3.join(outputDir, "db.ts"),
482
+ generateDbFile(tables)
483
+ );
484
+ await fs3.writeFile(
485
+ path3.join(outputDir, "api.ts"),
486
+ await generateApiFile(functionsDir)
487
+ );
488
+ await fs3.writeFile(
489
+ path3.join(outputDir, "index.ts"),
490
+ `// Auto-generated by Tether CLI - do not edit manually
491
+ export * from './db';
492
+ export * from './api';
493
+ `
494
+ );
495
+ return { tables, outputDir };
496
+ }
497
+ async function generateCommand() {
498
+ await requireAuth();
499
+ const configPath = path3.resolve(process.cwd(), "tether.config.ts");
500
+ if (!await fs3.pathExists(configPath)) {
501
+ console.log(chalk2.red("\nError: Not a Tether project"));
502
+ console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
503
+ process.exit(1);
504
+ }
505
+ console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
506
+ const spinner = ora("Reading schema...").start();
507
+ try {
508
+ spinner.text = "Generating types...";
509
+ const { tables, outputDir } = await generateTypes();
510
+ if (tables.length === 0) {
511
+ spinner.warn("No tables found in schema");
512
+ console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
513
+ return;
514
+ }
515
+ spinner.succeed(`Types generated for ${tables.length} table(s)`);
516
+ console.log("\n" + chalk2.green("\u2713") + " Tables:");
517
+ for (const table of tables) {
518
+ console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
519
+ }
520
+ const relativeOutput = path3.relative(process.cwd(), outputDir);
521
+ console.log("\n" + chalk2.green("\u2713") + " Generated files:");
522
+ console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
523
+ console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
524
+ console.log(chalk2.dim(` ${relativeOutput}/index.ts
525
+ `));
526
+ } catch (error) {
527
+ spinner.fail("Failed to generate types");
528
+ console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
529
+ process.exit(1);
530
+ }
531
+ }
532
+
533
+ export {
534
+ __require,
535
+ API_URL,
536
+ getCredentials,
537
+ requireAuth,
538
+ saveCredentials,
539
+ clearCredentials,
540
+ loadConfig,
541
+ resolvePath,
542
+ detectFramework,
543
+ getFrameworkDevCommand,
544
+ generateTypes,
545
+ generateCommand
546
+ };
@@ -0,0 +1,8 @@
1
+ import {
2
+ generateCommand,
3
+ generateTypes
4
+ } from "./chunk-FBTKIMH3.js";
5
+ export {
6
+ generateCommand,
7
+ generateTypes
8
+ };
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ import chalk from "chalk";
29
29
  import ora from "ora";
30
30
  import fs from "fs-extra";
31
31
  import path from "path";
32
+ import * as esbuild from "esbuild";
32
33
  var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
33
34
  var API_URL2 = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
34
35
  async function deployCommand(options) {
@@ -153,6 +154,57 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
153
154
  }
154
155
  }
155
156
  }
157
+ async function bundleFunctionFile(filePath) {
158
+ const result = await esbuild.build({
159
+ entryPoints: [filePath],
160
+ bundle: true,
161
+ format: "esm",
162
+ platform: "neutral",
163
+ write: false,
164
+ // Target modern JS — the Deno runtime handles it
165
+ target: "es2022",
166
+ // Provide shims for the Tether packages — the runtime injects the real implementations
167
+ plugins: [{
168
+ name: "tether-shims",
169
+ setup(build2) {
170
+ build2.onResolve({ filter: /^@tthr\/server$/ }, () => ({
171
+ path: "@tthr/server",
172
+ namespace: "tether-shim"
173
+ }));
174
+ build2.onLoad({ filter: /.*/, namespace: "tether-shim" }, () => ({
175
+ contents: `
176
+ export function query(def) { return def; }
177
+ export function mutation(def) { return def; }
178
+ export { z } from "zod";
179
+ `,
180
+ loader: "js",
181
+ resolveDir: path.dirname(filePath)
182
+ }));
183
+ build2.onResolve({ filter: /^@tthr\/client$/ }, () => ({
184
+ path: "@tthr/client",
185
+ namespace: "tether-client-shim"
186
+ }));
187
+ build2.onLoad({ filter: /.*/, namespace: "tether-client-shim" }, () => ({
188
+ contents: `
189
+ export function createDatabaseProxy() { return {}; }
190
+ `,
191
+ loader: "js"
192
+ }));
193
+ build2.onResolve({ filter: /^zod$/ }, () => ({
194
+ path: "https://esm.sh/zod@3.24.4",
195
+ external: true
196
+ }));
197
+ }
198
+ }],
199
+ logLevel: "silent"
200
+ });
201
+ if (result.errors.length > 0) {
202
+ const messages = await esbuild.formatMessages(result.errors, { kind: "error" });
203
+ throw new Error(`Bundle failed for ${filePath}:
204
+ ${messages.join("\n")}`);
205
+ }
206
+ return result.outputFiles[0].text;
207
+ }
156
208
  async function deployFunctionsToServer(projectId, token, functionsDir, environment, dryRun) {
157
209
  const spinner = ora("Reading functions...").start();
158
210
  try {
@@ -164,7 +216,7 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
164
216
  return;
165
217
  }
166
218
  const files = await fs.readdir(functionsDir);
167
- const tsFiles = files.filter((f) => f.endsWith(".ts"));
219
+ const tsFiles = files.filter((f) => f.endsWith(".ts") && !f.startsWith("_"));
168
220
  if (tsFiles.length === 0) {
169
221
  spinner.info("No function files found");
170
222
  return;
@@ -175,6 +227,12 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
175
227
  const source = await fs.readFile(filePath, "utf-8");
176
228
  const moduleName = file.replace(".ts", "");
177
229
  const parsedFunctions = parseFunctions(moduleName, source);
230
+ if (parsedFunctions.length === 0) continue;
231
+ spinner.text = `Bundling ${file}...`;
232
+ const bundledSource = await bundleFunctionFile(filePath);
233
+ for (const fn of parsedFunctions) {
234
+ fn.source = bundledSource;
235
+ }
178
236
  functions.push(...parsedFunctions);
179
237
  }
180
238
  spinner.text = `Found ${functions.length} function(s)`;
@@ -407,14 +465,14 @@ function parseFunctions(moduleName, source) {
407
465
  if (argsStartMatch && argsStartMatch.index !== void 0) {
408
466
  const argsStartIndex = argsStartMatch.index + argsStartMatch[0].length - 1;
409
467
  let braceCount = 1;
410
- let endIndex2 = argsStartIndex + 1;
468
+ let braceEndIndex = argsStartIndex + 1;
411
469
  for (let i = argsStartIndex + 1; i < fnBody.length && braceCount > 0; i++) {
412
470
  const char = fnBody[i];
413
471
  if (char === "{") braceCount++;
414
472
  else if (char === "}") braceCount--;
415
- endIndex2 = i;
473
+ braceEndIndex = i;
416
474
  }
417
- let parenEndIndex = endIndex2 + 1;
475
+ let parenEndIndex = braceEndIndex + 1;
418
476
  while (parenEndIndex < fnBody.length && fnBody[parenEndIndex] !== ")") {
419
477
  parenEndIndex++;
420
478
  }
@@ -427,7 +485,8 @@ function parseFunctions(moduleName, source) {
427
485
  name: `${moduleName}.${fnName}`,
428
486
  type: fnType,
429
487
  file: `${moduleName}.ts`,
430
- source: `${fnType}(${fnBody})`,
488
+ source: "",
489
+ // Set by caller from bundled output
431
490
  args
432
491
  });
433
492
  }
@@ -1546,6 +1605,8 @@ var schemaWatcher = null;
1546
1605
  var functionsWatcher = null;
1547
1606
  var isGenerating = false;
1548
1607
  var pendingGenerate = false;
1608
+ var isDeploying = false;
1609
+ var pendingDeploy = null;
1549
1610
  async function runGenerate(spinner) {
1550
1611
  if (isGenerating) {
1551
1612
  pendingGenerate = true;
@@ -1572,6 +1633,41 @@ async function runGenerate(spinner) {
1572
1633
  }
1573
1634
  }
1574
1635
  }
1636
+ async function runDeploy(what, projectId, token, schemaPath, functionsDir, environment, spinner) {
1637
+ if (isDeploying) {
1638
+ if (pendingDeploy === null) pendingDeploy = what;
1639
+ else if (pendingDeploy !== what) pendingDeploy = "all";
1640
+ return;
1641
+ }
1642
+ isDeploying = true;
1643
+ try {
1644
+ if (what === "schema" || what === "all") {
1645
+ spinner.text = "Deploying schema to development...";
1646
+ try {
1647
+ await deploySchemaToServer(projectId, token, schemaPath, environment);
1648
+ } catch (e) {
1649
+ console.log(chalk3.yellow(` Schema deploy failed: ${e instanceof Error ? e.message : e}`));
1650
+ }
1651
+ }
1652
+ if (what === "functions" || what === "all") {
1653
+ spinner.text = "Deploying functions to development...";
1654
+ try {
1655
+ await deployFunctionsToServer(projectId, token, functionsDir, environment);
1656
+ } catch (e) {
1657
+ console.log(chalk3.yellow(` Functions deploy failed: ${e instanceof Error ? e.message : e}`));
1658
+ }
1659
+ }
1660
+ spinner.succeed(`Deployed ${what} to ${environment}`);
1661
+ spinner.start("Watching for changes...");
1662
+ } finally {
1663
+ isDeploying = false;
1664
+ if (pendingDeploy) {
1665
+ const next = pendingDeploy;
1666
+ pendingDeploy = null;
1667
+ setTimeout(() => runDeploy(next, projectId, token, schemaPath, functionsDir, environment, spinner), 100);
1668
+ }
1669
+ }
1670
+ }
1575
1671
  async function validateSchema(schemaPath) {
1576
1672
  try {
1577
1673
  const content = await fs3.readFile(schemaPath, "utf-8");
@@ -1681,7 +1777,7 @@ function cleanup() {
1681
1777
  }
1682
1778
  }
1683
1779
  async function devCommand(options) {
1684
- await requireAuth();
1780
+ const credentials = await requireAuth();
1685
1781
  const configPath = path3.resolve(process.cwd(), "tether.config.ts");
1686
1782
  if (!await fs3.pathExists(configPath)) {
1687
1783
  console.log(chalk3.red("\nError: Not a Tether project"));
@@ -1741,6 +1837,12 @@ async function devCommand(options) {
1741
1837
  const { generateTypes: generateTypes2 } = await import("./generate-Q4MUBHHO.js");
1742
1838
  await generateTypes2({ silent: true });
1743
1839
  spinner.succeed("Types generated");
1840
+ if (config.projectId) {
1841
+ spinner.start("Deploying to development environment...");
1842
+ await runDeploy("all", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner);
1843
+ } else {
1844
+ spinner.warn("No project ID configured \u2014 skipping auto-deploy");
1845
+ }
1744
1846
  spinner.start("Setting up file watchers...");
1745
1847
  if (await fs3.pathExists(schemaPath)) {
1746
1848
  schemaWatcher = watch(schemaPath, {
@@ -1758,6 +1860,9 @@ async function devCommand(options) {
1758
1860
  return;
1759
1861
  }
1760
1862
  await runGenerate(spinner);
1863
+ if (config.projectId) {
1864
+ await runDeploy("schema", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner);
1865
+ }
1761
1866
  });
1762
1867
  }
1763
1868
  if (await fs3.pathExists(functionsDir)) {
@@ -1779,6 +1884,9 @@ async function devCommand(options) {
1779
1884
  }
1780
1885
  }
1781
1886
  await runGenerate(spinner);
1887
+ if (config.projectId) {
1888
+ await runDeploy("functions", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner);
1889
+ }
1782
1890
  });
1783
1891
  }
1784
1892
  spinner.succeed("File watchers ready");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tthr",
3
- "version": "0.0.61",
3
+ "version": "0.2.0",
4
4
  "description": "Tether CLI - project scaffolding and deployment",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,7 @@
21
21
  "chalk": "^5.4.1",
22
22
  "chokidar": "^5.0.0",
23
23
  "commander": "^13.0.0",
24
+ "esbuild": "^0.27.3",
24
25
  "fs-extra": "^11.2.0",
25
26
  "ora": "^8.1.1",
26
27
  "prompts": "^2.4.2"