tthr 0.0.59 → 0.0.61

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,538 @@
1
+ // src/commands/generate.ts
2
+ import chalk2 from "chalk";
3
+ import ora from "ora";
4
+ import fs3 from "fs-extra";
5
+ import path3 from "path";
6
+
7
+ // src/utils/auth.ts
8
+ import chalk from "chalk";
9
+ import fs from "fs-extra";
10
+ import path from "path";
11
+ var CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
12
+ var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
13
+ var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
14
+ var API_URL = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
15
+ var REFRESH_THRESHOLD_DAYS = 7;
16
+ async function getCredentials() {
17
+ try {
18
+ if (await fs.pathExists(CREDENTIALS_FILE)) {
19
+ return await fs.readJSON(CREDENTIALS_FILE);
20
+ }
21
+ } catch {
22
+ }
23
+ return null;
24
+ }
25
+ async function refreshSession(credentials) {
26
+ try {
27
+ const response = await fetch(`${API_URL}/auth/session/refresh`, {
28
+ method: "POST",
29
+ headers: {
30
+ "Content-Type": "application/json",
31
+ "Authorization": `Bearer ${credentials.accessToken}`
32
+ }
33
+ });
34
+ if (!response.ok) {
35
+ return null;
36
+ }
37
+ const data = await response.json();
38
+ const updated = {
39
+ ...credentials,
40
+ expiresAt: data.expiresAt
41
+ };
42
+ await saveCredentials(updated);
43
+ return updated;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ async function requireAuth() {
49
+ const credentials = await getCredentials();
50
+ if (!credentials) {
51
+ console.error(chalk.red("\n\u2717 Not logged in\n"));
52
+ console.log(chalk.dim("Run `tthr login` to authenticate\n"));
53
+ process.exit(1);
54
+ }
55
+ if (credentials.expiresAt) {
56
+ const expiresAt = new Date(credentials.expiresAt);
57
+ const now = /* @__PURE__ */ new Date();
58
+ if (now > expiresAt) {
59
+ await clearCredentials();
60
+ console.error(chalk.red("\n\u2717 Session expired \u2014 you have been signed out\n"));
61
+ console.log(chalk.dim("Run `tthr login` to authenticate\n"));
62
+ process.exit(1);
63
+ }
64
+ const daysUntilExpiry = (expiresAt.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24);
65
+ if (daysUntilExpiry <= REFRESH_THRESHOLD_DAYS) {
66
+ const refreshed = await refreshSession(credentials);
67
+ if (refreshed) {
68
+ return refreshed;
69
+ }
70
+ }
71
+ }
72
+ return credentials;
73
+ }
74
+ async function saveCredentials(credentials) {
75
+ await fs.ensureDir(CONFIG_DIR);
76
+ await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
77
+ }
78
+ async function clearCredentials() {
79
+ try {
80
+ await fs.remove(CREDENTIALS_FILE);
81
+ } catch {
82
+ }
83
+ }
84
+
85
+ // src/utils/config.ts
86
+ import fs2 from "fs-extra";
87
+ import path2 from "path";
88
+ var DEFAULT_CONFIG = {
89
+ schema: "./tether/schema.ts",
90
+ functions: "./tether/functions",
91
+ output: "./tether/_generated",
92
+ dev: {
93
+ port: 3001,
94
+ host: "localhost"
95
+ },
96
+ database: {
97
+ walMode: true
98
+ }
99
+ };
100
+ async function loadConfig(cwd = process.cwd()) {
101
+ const configPath = path2.resolve(cwd, "tether.config.ts");
102
+ if (!await fs2.pathExists(configPath)) {
103
+ return DEFAULT_CONFIG;
104
+ }
105
+ const configSource = await fs2.readFile(configPath, "utf-8");
106
+ const config = {};
107
+ const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
108
+ if (schemaMatch) {
109
+ config.schema = schemaMatch[1];
110
+ }
111
+ const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
112
+ if (functionsMatch) {
113
+ config.functions = functionsMatch[1];
114
+ }
115
+ const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
116
+ if (outputMatch) {
117
+ config.output = outputMatch[1];
118
+ }
119
+ const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
120
+ if (envMatch) {
121
+ config.environment = envMatch[1];
122
+ }
123
+ const portMatch = configSource.match(/port\s*:\s*(\d+)/);
124
+ if (portMatch) {
125
+ config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
126
+ }
127
+ const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
128
+ if (hostMatch) {
129
+ config.dev = { ...config.dev, host: hostMatch[1] };
130
+ }
131
+ return {
132
+ ...DEFAULT_CONFIG,
133
+ ...config,
134
+ dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
135
+ database: { ...DEFAULT_CONFIG.database, ...config.database }
136
+ };
137
+ }
138
+ function resolvePath(configPath, cwd = process.cwd()) {
139
+ const normalised = configPath.replace(/^\.\//, "");
140
+ return path2.resolve(cwd, normalised);
141
+ }
142
+ async function detectFramework(cwd = process.cwd()) {
143
+ const packageJsonPath = path2.resolve(cwd, "package.json");
144
+ if (!await fs2.pathExists(packageJsonPath)) {
145
+ return "unknown";
146
+ }
147
+ try {
148
+ const packageJson = await fs2.readJson(packageJsonPath);
149
+ const deps = {
150
+ ...packageJson.dependencies,
151
+ ...packageJson.devDependencies
152
+ };
153
+ if (deps.nuxt || deps["@nuxt/kit"]) {
154
+ return "nuxt";
155
+ }
156
+ if (deps.next) {
157
+ return "next";
158
+ }
159
+ if (deps["@sveltejs/kit"]) {
160
+ return "sveltekit";
161
+ }
162
+ if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
163
+ return "vite";
164
+ }
165
+ return "vanilla";
166
+ } catch {
167
+ return "unknown";
168
+ }
169
+ }
170
+ function getFrameworkDevCommand(framework) {
171
+ switch (framework) {
172
+ case "nuxt":
173
+ return "nuxt dev";
174
+ case "next":
175
+ return "next dev";
176
+ case "sveltekit":
177
+ return "vite dev";
178
+ case "vite":
179
+ return "vite dev";
180
+ case "vanilla":
181
+ return null;
182
+ // No default dev server for vanilla
183
+ default:
184
+ return null;
185
+ }
186
+ }
187
+
188
+ // src/commands/generate.ts
189
+ function parseSchemaFile(source) {
190
+ const tables = [];
191
+ const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
192
+ if (!schemaMatch) return tables;
193
+ const schemaContent = schemaMatch[1];
194
+ const tableStartRegex = /(\w+)\s*:\s*\{/g;
195
+ let match;
196
+ while ((match = tableStartRegex.exec(schemaContent)) !== null) {
197
+ const tableName = match[1];
198
+ const startOffset = match.index + match[0].length;
199
+ let braceCount = 1;
200
+ let endOffset = startOffset;
201
+ for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
202
+ const char = schemaContent[i];
203
+ if (char === "{") braceCount++;
204
+ else if (char === "}") braceCount--;
205
+ endOffset = i;
206
+ }
207
+ const columnsContent = schemaContent.slice(startOffset, endOffset);
208
+ const columns = parseColumns(columnsContent);
209
+ tables.push({ name: tableName, columns });
210
+ }
211
+ return tables;
212
+ }
213
+ function parseColumns(content) {
214
+ const columns = [];
215
+ const columnRegex = /(\w+)\s*:\s*(\w+)(?:<([^>]+)>)?\s*\(\s*\)((?:\[.*?\]|[^,\n}])*)/g;
216
+ let match;
217
+ while ((match = columnRegex.exec(content)) !== null) {
218
+ const name = match[1];
219
+ const schemaType = match[2];
220
+ const genericType = match[3];
221
+ const modifiers = match[4] || "";
222
+ const nullable = !modifiers.includes(".notNull()");
223
+ const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
224
+ let oneOf;
225
+ if (oneOfMatch) {
226
+ oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
227
+ }
228
+ columns.push({
229
+ name,
230
+ type: schemaTypeToTS(schemaType),
231
+ nullable,
232
+ jsonType: genericType,
233
+ // Store the generic type for json columns
234
+ oneOf
235
+ });
236
+ }
237
+ return columns;
238
+ }
239
+ function schemaTypeToTS(schemaType) {
240
+ switch (schemaType) {
241
+ case "text":
242
+ return "string";
243
+ case "integer":
244
+ return "number";
245
+ case "real":
246
+ return "number";
247
+ case "boolean":
248
+ return "boolean";
249
+ case "timestamp":
250
+ return "string";
251
+ case "json":
252
+ return "unknown";
253
+ case "blob":
254
+ return "Uint8Array";
255
+ case "asset":
256
+ return "TetherAsset";
257
+ // Asset object returned by API
258
+ default:
259
+ return "unknown";
260
+ }
261
+ }
262
+ function tableNameToInterface(tableName) {
263
+ let name = tableName;
264
+ if (name.endsWith("ies")) {
265
+ name = name.slice(0, -3) + "y";
266
+ } else if (name.endsWith("s") && !name.endsWith("ss")) {
267
+ name = name.slice(0, -1);
268
+ }
269
+ return name.charAt(0).toUpperCase() + name.slice(1);
270
+ }
271
+ function generateDbFile(tables) {
272
+ const jsonTypes = /* @__PURE__ */ new Set();
273
+ for (const table of tables) {
274
+ for (const col of table.columns) {
275
+ if (col.jsonType) {
276
+ jsonTypes.add(col.jsonType.replace(/\[\]$/, ""));
277
+ }
278
+ }
279
+ }
280
+ const lines = [
281
+ "// Auto-generated by Tether CLI - do not edit manually",
282
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
283
+ "",
284
+ "import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
285
+ "import {",
286
+ " query as baseQuery,",
287
+ " mutation as baseMutation,",
288
+ " type QueryDefinition,",
289
+ " type MutationDefinition,",
290
+ " z,",
291
+ "} from '@tthr/server';"
292
+ ];
293
+ if (jsonTypes.size > 0) {
294
+ const typeImports = Array.from(jsonTypes).sort().join(", ");
295
+ lines.push(`import type { ${typeImports} } from '../schema';`);
296
+ }
297
+ lines.push("");
298
+ lines.push("// Asset type returned by the API for asset columns");
299
+ lines.push("export interface TetherAsset {");
300
+ lines.push(" id: string;");
301
+ lines.push(" filename: string;");
302
+ lines.push(" contentType: string;");
303
+ lines.push(" size: number;");
304
+ lines.push(" url: string;");
305
+ lines.push(" createdAt: string;");
306
+ lines.push("}");
307
+ lines.push("");
308
+ for (const table of tables) {
309
+ const interfaceName = tableNameToInterface(table.name);
310
+ lines.push(`export interface ${interfaceName} {`);
311
+ lines.push(" _id: string;");
312
+ for (const col of table.columns) {
313
+ let colType;
314
+ if (col.oneOf && col.oneOf.length > 0) {
315
+ colType = col.oneOf.map((v) => `'${v}'`).join(" | ");
316
+ } else {
317
+ colType = col.jsonType || col.type;
318
+ }
319
+ const typeStr = col.nullable ? `${colType} | null` : colType;
320
+ const optional = col.nullable ? "?" : "";
321
+ lines.push(` ${col.name}${optional}: ${typeStr};`);
322
+ }
323
+ lines.push(" _createdAt: string;");
324
+ lines.push(" _updatedAt?: string | null;");
325
+ lines.push(" _deletedAt?: string | null;");
326
+ lines.push("}");
327
+ lines.push("");
328
+ }
329
+ lines.push("export interface Schema {");
330
+ for (const table of tables) {
331
+ const interfaceName = tableNameToInterface(table.name);
332
+ lines.push(` ${table.name}: ${interfaceName};`);
333
+ }
334
+ lines.push("}");
335
+ lines.push("");
336
+ lines.push("// Database client with typed tables");
337
+ lines.push("// This is a proxy that will be populated by the Tether runtime");
338
+ lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
339
+ lines.push("");
340
+ lines.push("// ============================================================================");
341
+ lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
342
+ lines.push("// This ensures the `db` parameter in handlers is properly typed with Schema");
343
+ lines.push("// ============================================================================");
344
+ lines.push("");
345
+ lines.push("/**");
346
+ lines.push(" * Define a query function with typed database access.");
347
+ lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
348
+ lines.push(" */");
349
+ lines.push("export function query<TArgs = void, TResult = unknown>(");
350
+ lines.push(" definition: QueryDefinition<TArgs, TResult, Schema>");
351
+ lines.push("): QueryDefinition<TArgs, TResult, Schema> {");
352
+ lines.push(" return baseQuery(definition);");
353
+ lines.push("}");
354
+ lines.push("");
355
+ lines.push("/**");
356
+ lines.push(" * Define a mutation function with typed database access.");
357
+ lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
358
+ lines.push(" */");
359
+ lines.push("export function mutation<TArgs = void, TResult = unknown>(");
360
+ lines.push(" definition: MutationDefinition<TArgs, TResult, Schema>");
361
+ lines.push("): MutationDefinition<TArgs, TResult, Schema> {");
362
+ lines.push(" return baseMutation(definition);");
363
+ lines.push("}");
364
+ lines.push("");
365
+ lines.push("// Re-export z for convenience");
366
+ lines.push("export { z };");
367
+ if (jsonTypes.size > 0) {
368
+ lines.push("");
369
+ lines.push("// Re-export JSON schema types");
370
+ const typeExports = Array.from(jsonTypes).sort().join(", ");
371
+ lines.push(`export type { ${typeExports} } from '../schema';`);
372
+ }
373
+ lines.push("");
374
+ return lines.join("\n");
375
+ }
376
+ async function parseFunctionsDir(functionsDir) {
377
+ const functions = [];
378
+ if (!await fs3.pathExists(functionsDir)) {
379
+ return functions;
380
+ }
381
+ const files = await fs3.readdir(functionsDir);
382
+ for (const file of files) {
383
+ if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
384
+ const filePath = path3.join(functionsDir, file);
385
+ const stat = await fs3.stat(filePath);
386
+ if (!stat.isFile()) continue;
387
+ const moduleName = file.replace(/\.(ts|js)$/, "");
388
+ const source = await fs3.readFile(filePath, "utf-8");
389
+ const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation)\s*\(/g;
390
+ let match;
391
+ while ((match = exportRegex.exec(source)) !== null) {
392
+ functions.push({
393
+ name: match[1],
394
+ moduleName
395
+ });
396
+ }
397
+ }
398
+ return functions;
399
+ }
400
+ async function generateApiFile(functionsDir) {
401
+ const functions = await parseFunctionsDir(functionsDir);
402
+ const moduleMap = /* @__PURE__ */ new Map();
403
+ for (const fn of functions) {
404
+ if (!moduleMap.has(fn.moduleName)) {
405
+ moduleMap.set(fn.moduleName, []);
406
+ }
407
+ moduleMap.get(fn.moduleName).push(fn.name);
408
+ }
409
+ const lines = [
410
+ "// Auto-generated by Tether CLI - do not edit manually",
411
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
412
+ "",
413
+ "import { createApiProxy } from '@tthr/client';",
414
+ "",
415
+ "/**",
416
+ " * API function reference type for useQuery/useMutation.",
417
+ ' * The _name property contains the function path (e.g., "users.list").',
418
+ " */",
419
+ "export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
420
+ " _name: string;",
421
+ " _args?: TArgs;",
422
+ " _result?: TResult;",
423
+ "}",
424
+ ""
425
+ ];
426
+ if (moduleMap.size > 0) {
427
+ for (const [moduleName, fnNames] of moduleMap) {
428
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
429
+ lines.push(`export interface ${interfaceName} {`);
430
+ for (const fnName of fnNames) {
431
+ lines.push(` ${fnName}: ApiFunction;`);
432
+ }
433
+ lines.push("}");
434
+ lines.push("");
435
+ }
436
+ lines.push("export interface Api {");
437
+ for (const moduleName of moduleMap.keys()) {
438
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
439
+ lines.push(` ${moduleName}: ${interfaceName};`);
440
+ }
441
+ lines.push("}");
442
+ } else {
443
+ lines.push("/**");
444
+ lines.push(" * Flexible API type that allows access to any function path.");
445
+ lines.push(" * Functions are accessed as api.moduleName.functionName");
446
+ lines.push(" * The actual types depend on your function definitions.");
447
+ lines.push(" */");
448
+ lines.push("export type Api = {");
449
+ lines.push(" [module: string]: {");
450
+ lines.push(" [fn: string]: ApiFunction;");
451
+ lines.push(" };");
452
+ lines.push("};");
453
+ }
454
+ lines.push("");
455
+ lines.push("// API client proxy - provides typed access to your functions");
456
+ lines.push("// On the client: returns { _name } references for useQuery/useMutation");
457
+ lines.push("// In Tether functions: calls the actual function implementation");
458
+ lines.push("export const api = createApiProxy<Api>();");
459
+ lines.push("");
460
+ return lines.join("\n");
461
+ }
462
+ async function generateTypes(options = {}) {
463
+ const config = await loadConfig();
464
+ const schemaPath = resolvePath(config.schema);
465
+ const outputDir = resolvePath(config.output);
466
+ const functionsDir = resolvePath(config.functions);
467
+ if (!await fs3.pathExists(schemaPath)) {
468
+ throw new Error(`Schema file not found: ${schemaPath}`);
469
+ }
470
+ const schemaSource = await fs3.readFile(schemaPath, "utf-8");
471
+ const tables = parseSchemaFile(schemaSource);
472
+ await fs3.ensureDir(outputDir);
473
+ await fs3.writeFile(
474
+ path3.join(outputDir, "db.ts"),
475
+ generateDbFile(tables)
476
+ );
477
+ await fs3.writeFile(
478
+ path3.join(outputDir, "api.ts"),
479
+ await generateApiFile(functionsDir)
480
+ );
481
+ await fs3.writeFile(
482
+ path3.join(outputDir, "index.ts"),
483
+ `// Auto-generated by Tether CLI - do not edit manually
484
+ export * from './db';
485
+ export * from './api';
486
+ `
487
+ );
488
+ return { tables, outputDir };
489
+ }
490
+ async function generateCommand() {
491
+ await requireAuth();
492
+ const configPath = path3.resolve(process.cwd(), "tether.config.ts");
493
+ if (!await fs3.pathExists(configPath)) {
494
+ console.log(chalk2.red("\nError: Not a Tether project"));
495
+ console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
496
+ process.exit(1);
497
+ }
498
+ console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
499
+ const spinner = ora("Reading schema...").start();
500
+ try {
501
+ spinner.text = "Generating types...";
502
+ const { tables, outputDir } = await generateTypes();
503
+ if (tables.length === 0) {
504
+ spinner.warn("No tables found in schema");
505
+ console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
506
+ return;
507
+ }
508
+ spinner.succeed(`Types generated for ${tables.length} table(s)`);
509
+ console.log("\n" + chalk2.green("\u2713") + " Tables:");
510
+ for (const table of tables) {
511
+ console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
512
+ }
513
+ const relativeOutput = path3.relative(process.cwd(), outputDir);
514
+ console.log("\n" + chalk2.green("\u2713") + " Generated files:");
515
+ console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
516
+ console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
517
+ console.log(chalk2.dim(` ${relativeOutput}/index.ts
518
+ `));
519
+ } catch (error) {
520
+ spinner.fail("Failed to generate types");
521
+ console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
522
+ process.exit(1);
523
+ }
524
+ }
525
+
526
+ export {
527
+ API_URL,
528
+ getCredentials,
529
+ requireAuth,
530
+ saveCredentials,
531
+ clearCredentials,
532
+ loadConfig,
533
+ resolvePath,
534
+ detectFramework,
535
+ getFrameworkDevCommand,
536
+ generateTypes,
537
+ generateCommand
538
+ };
@@ -0,0 +1,8 @@
1
+ import {
2
+ generateCommand,
3
+ generateTypes
4
+ } from "./chunk-NDEYB3IH.js";
5
+ export {
6
+ generateCommand,
7
+ generateTypes
8
+ };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ API_URL,
3
4
  clearCredentials,
4
5
  detectFramework,
5
6
  generateCommand,
@@ -10,7 +11,7 @@ import {
10
11
  requireAuth,
11
12
  resolvePath,
12
13
  saveCredentials
13
- } from "./chunk-GKELUQ44.js";
14
+ } from "./chunk-NDEYB3IH.js";
14
15
 
15
16
  // src/index.ts
16
17
  import { Command } from "commander";
@@ -29,7 +30,7 @@ import ora from "ora";
29
30
  import fs from "fs-extra";
30
31
  import path from "path";
31
32
  var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
32
- var API_URL = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
33
+ var API_URL2 = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
33
34
  async function deployCommand(options) {
34
35
  const credentials = await requireAuth();
35
36
  const configPath = path.resolve(process.cwd(), "tether.config.ts");
@@ -54,7 +55,7 @@ async function deployCommand(options) {
54
55
  console.log(chalk.bold("\n\u26A1 Deploying to Tether\n"));
55
56
  console.log(chalk.dim(` Project: ${projectId}`));
56
57
  console.log(chalk.dim(` Environment: ${environment}`));
57
- console.log(chalk.dim(` API: ${API_URL}
58
+ console.log(chalk.dim(` API: ${API_URL2}
58
59
  `));
59
60
  console.log(chalk.dim(" Generating types..."));
60
61
  try {
@@ -106,7 +107,7 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
106
107
  return;
107
108
  }
108
109
  spinner.text = "Deploying schema...";
109
- const schemaUrl = `${API_URL}/projects/${projectId}/env/${environment}/deploy/schema`;
110
+ const schemaUrl = `${API_URL2}/projects/${projectId}/env/${environment}/deploy/schema`;
110
111
  console.log(chalk.dim(`
111
112
  URL: ${schemaUrl}`));
112
113
  const requestBody = {
@@ -186,7 +187,7 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
186
187
  return;
187
188
  }
188
189
  spinner.text = "Deploying functions...";
189
- const functionsUrl = `${API_URL}/projects/${projectId}/env/${environment}/deploy/functions`;
190
+ const functionsUrl = `${API_URL2}/projects/${projectId}/env/${environment}/deploy/functions`;
190
191
  console.log(chalk.dim(`
191
192
  URL: ${functionsUrl}`));
192
193
  const response = await fetch(functionsUrl, {
@@ -301,6 +302,7 @@ function parseSchema(source) {
301
302
  unique: modifiers.includes(".unique()"),
302
303
  hasDefault: modifiers.includes(".default("),
303
304
  references: modifiers.match(/\.references\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1],
305
+ onDelete: modifiers.match(/\.onDelete\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1],
304
306
  oneOf
305
307
  };
306
308
  }
@@ -342,6 +344,9 @@ function buildColumnSql(colName, def, forAlterTable = false) {
342
344
  if (def.references) {
343
345
  const [refTable, refCol] = def.references.split(".");
344
346
  colSql += ` REFERENCES "${refTable}"("${refCol || "_id"}")`;
347
+ if (def.onDelete) {
348
+ colSql += ` ON DELETE ${def.onDelete.toUpperCase()}`;
349
+ }
345
350
  }
346
351
  if (def.oneOf && def.oneOf.length > 0) {
347
352
  const values = def.oneOf.map((v) => `'${v}'`).join(", ");
@@ -353,6 +358,10 @@ function generateSchemaSQL(tables) {
353
358
  const statements = [];
354
359
  for (const table of tables) {
355
360
  const columnDefs = [];
361
+ columnDefs.push('"_id" TEXT PRIMARY KEY');
362
+ columnDefs.push(`"_createdAt" TEXT DEFAULT (datetime('now'))`);
363
+ columnDefs.push('"_updatedAt" TEXT');
364
+ columnDefs.push('"_deletedAt" TEXT');
356
365
  for (const [colName, colDef] of Object.entries(table.columns)) {
357
366
  const def = colDef;
358
367
  columnDefs.push(buildColumnSql(colName, def));
@@ -427,7 +436,7 @@ function parseFunctions(moduleName, source) {
427
436
 
428
437
  // src/commands/init.ts
429
438
  var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
430
- var API_URL2 = isDev2 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
439
+ var API_URL3 = isDev2 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
431
440
  async function getLatestVersion(packageName) {
432
441
  try {
433
442
  const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
@@ -529,7 +538,7 @@ ${packageManager} is not installed on your system.`));
529
538
  await scaffoldVanillaProject(projectName, projectPath, spinner);
530
539
  }
531
540
  spinner.text = "Creating project on Tether...";
532
- const response = await fetch(`${API_URL2}/projects`, {
541
+ const response = await fetch(`${API_URL3}/projects`, {
533
542
  method: "POST",
534
543
  headers: {
535
544
  "Content-Type": "application/json",
@@ -1544,7 +1553,7 @@ async function runGenerate(spinner) {
1544
1553
  }
1545
1554
  isGenerating = true;
1546
1555
  try {
1547
- const { generateTypes: generateTypes2 } = await import("./generate-5AY6JNCG.js");
1556
+ const { generateTypes: generateTypes2 } = await import("./generate-Q4MUBHHO.js");
1548
1557
  spinner.text = "Regenerating types...";
1549
1558
  await generateTypes2({ silent: true });
1550
1559
  spinner.succeed("Types regenerated");
@@ -1729,7 +1738,7 @@ async function devCommand(options) {
1729
1738
  }
1730
1739
  }
1731
1740
  spinner.text = "Generating types...";
1732
- const { generateTypes: generateTypes2 } = await import("./generate-5AY6JNCG.js");
1741
+ const { generateTypes: generateTypes2 } = await import("./generate-Q4MUBHHO.js");
1733
1742
  await generateTypes2({ silent: true });
1734
1743
  spinner.succeed("Types generated");
1735
1744
  spinner.start("Setting up file watchers...");
@@ -1800,7 +1809,7 @@ import os from "os";
1800
1809
  import { exec } from "child_process";
1801
1810
  import readline from "readline";
1802
1811
  var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
1803
- var API_URL3 = isDev3 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
1812
+ var API_URL4 = isDev3 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
1804
1813
  var AUTH_URL = isDev3 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
1805
1814
  async function loginCommand() {
1806
1815
  console.log(chalk4.bold("\u26A1 Login to Tether\n"));
@@ -1890,7 +1899,7 @@ async function requestDeviceCode() {
1890
1899
  const userCode = generateUserCode();
1891
1900
  const deviceCode = crypto.randomUUID();
1892
1901
  const deviceName = os.hostname();
1893
- const response = await fetch(`${API_URL3}/auth/device`, {
1902
+ const response = await fetch(`${API_URL4}/auth/device`, {
1894
1903
  method: "POST",
1895
1904
  headers: { "Content-Type": "application/json" },
1896
1905
  body: JSON.stringify({ userCode, deviceCode, deviceName })
@@ -1924,7 +1933,7 @@ async function pollForApproval(deviceCode, interval, expiresIn) {
1924
1933
  while (Date.now() < expiresAt) {
1925
1934
  await sleep(interval * 1e3);
1926
1935
  spinner.text = `Checking... ${chalk4.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
1927
- const response = await fetch(`${API_URL3}/auth/device/${deviceCode}`, {
1936
+ const response = await fetch(`${API_URL4}/auth/device/${deviceCode}`, {
1928
1937
  method: "GET"
1929
1938
  }).catch(() => null);
1930
1939
  updateCountdown();
@@ -2137,7 +2146,7 @@ import ora6 from "ora";
2137
2146
  import fs5 from "fs-extra";
2138
2147
  import path5 from "path";
2139
2148
  var isDev4 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
2140
- var API_URL4 = isDev4 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
2149
+ var API_URL5 = isDev4 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
2141
2150
  async function execCommand(sql) {
2142
2151
  if (!sql || sql.trim() === "") {
2143
2152
  console.log(chalk6.red("\nError: SQL query required"));
@@ -2159,7 +2168,7 @@ async function execCommand(sql) {
2159
2168
  }
2160
2169
  const spinner = ora6("Executing query...").start();
2161
2170
  try {
2162
- const response = await fetch(`${API_URL4}/projects/${projectId}/exec`, {
2171
+ const response = await fetch(`${API_URL5}/projects/${projectId}/exec`, {
2163
2172
  method: "POST",
2164
2173
  headers: {
2165
2174
  "Content-Type": "application/json",
@@ -2210,7 +2219,7 @@ import ora7 from "ora";
2210
2219
  import fs6 from "fs-extra";
2211
2220
  import path6 from "path";
2212
2221
  var isDev5 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
2213
- var API_URL5 = isDev5 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
2222
+ var API_URL6 = isDev5 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
2214
2223
  async function getProjectId() {
2215
2224
  const envPath = path6.resolve(process.cwd(), ".env");
2216
2225
  let projectId;
@@ -2231,7 +2240,7 @@ async function envListCommand() {
2231
2240
  const projectId = await getProjectId();
2232
2241
  const spinner = ora7("Fetching environments...").start();
2233
2242
  try {
2234
- const response = await fetch(`${API_URL5}/projects/${projectId}/environments`, {
2243
+ const response = await fetch(`${API_URL6}/projects/${projectId}/environments`, {
2235
2244
  headers: {
2236
2245
  "Authorization": `Bearer ${credentials.accessToken}`
2237
2246
  }
@@ -2274,7 +2283,7 @@ async function envCreateCommand(name, options) {
2274
2283
  body.cloneFrom = options.from;
2275
2284
  spinner.text = `Creating environment '${normalizedName}' from '${options.from}'...`;
2276
2285
  }
2277
- const response = await fetch(`${API_URL5}/projects/${projectId}/environments`, {
2286
+ const response = await fetch(`${API_URL6}/projects/${projectId}/environments`, {
2278
2287
  method: "POST",
2279
2288
  headers: {
2280
2289
  "Content-Type": "application/json",
@@ -2305,7 +2314,7 @@ async function envDeleteCommand(name) {
2305
2314
  const projectId = await getProjectId();
2306
2315
  const spinner = ora7(`Deleting environment '${name}'...`).start();
2307
2316
  try {
2308
- const response = await fetch(`${API_URL5}/projects/${projectId}/environments/${name}`, {
2317
+ const response = await fetch(`${API_URL6}/projects/${projectId}/environments/${name}`, {
2309
2318
  method: "DELETE",
2310
2319
  headers: {
2311
2320
  "Authorization": `Bearer ${credentials.accessToken}`
@@ -2327,7 +2336,7 @@ async function envDefaultCommand(name) {
2327
2336
  const projectId = await getProjectId();
2328
2337
  const spinner = ora7(`Setting '${name}' as default environment...`).start();
2329
2338
  try {
2330
- const response = await fetch(`${API_URL5}/projects/${projectId}/environments/default`, {
2339
+ const response = await fetch(`${API_URL6}/projects/${projectId}/environments/default`, {
2331
2340
  method: "PATCH",
2332
2341
  headers: {
2333
2342
  "Content-Type": "application/json",
@@ -2365,6 +2374,152 @@ function maskApiKey(key) {
2365
2374
  return `${visibleStart}...${visibleEnd}`;
2366
2375
  }
2367
2376
 
2377
+ // src/commands/migrate.ts
2378
+ import chalk8 from "chalk";
2379
+ import ora8 from "ora";
2380
+ import fs7 from "fs-extra";
2381
+ import path7 from "path";
2382
+ import readline2 from "readline";
2383
+ async function migrateSystemColumnsCommand(options) {
2384
+ const credentials = await requireAuth();
2385
+ const envPath = path7.resolve(process.cwd(), ".env");
2386
+ let projectId;
2387
+ if (await fs7.pathExists(envPath)) {
2388
+ const envContent = await fs7.readFile(envPath, "utf-8");
2389
+ const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
2390
+ projectId = match?.[1]?.trim();
2391
+ }
2392
+ if (!projectId) {
2393
+ console.log(chalk8.red("\nError: Project ID not found"));
2394
+ console.log(chalk8.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
2395
+ process.exit(1);
2396
+ }
2397
+ const environment = options.env || "development";
2398
+ console.log(chalk8.bold("\n\u26A1 Migrate Legacy IDs to System Columns\n"));
2399
+ console.log(chalk8.dim(` Project: ${projectId}`));
2400
+ console.log(chalk8.dim(` Environment: ${environment}
2401
+ `));
2402
+ const spinner = ora8("Analysing database tables...").start();
2403
+ try {
2404
+ const baseUrl = `${API_URL}/projects/${projectId}/env/${environment}/migrate/system-columns`;
2405
+ const dryRunResponse = await fetch(`${baseUrl}?dry_run=true`, {
2406
+ method: "POST",
2407
+ headers: {
2408
+ "Content-Type": "application/json",
2409
+ "Authorization": `Bearer ${credentials.accessToken}`
2410
+ }
2411
+ });
2412
+ if (!dryRunResponse.ok) {
2413
+ const text = await dryRunResponse.text();
2414
+ let errorMessage;
2415
+ try {
2416
+ const error = JSON.parse(text);
2417
+ errorMessage = error.error || `HTTP ${dryRunResponse.status}`;
2418
+ } catch {
2419
+ errorMessage = text || `HTTP ${dryRunResponse.status}: ${dryRunResponse.statusText}`;
2420
+ }
2421
+ throw new Error(errorMessage);
2422
+ }
2423
+ const preview = await dryRunResponse.json();
2424
+ spinner.stop();
2425
+ const tablesToMigrate = preview.tables.filter((t) => t.status === "migrated");
2426
+ const tablesSkipped = preview.tables.filter((t) => t.status === "skipped");
2427
+ if (tablesToMigrate.length === 0) {
2428
+ console.log(chalk8.green("\u2713") + " All tables already have system columns \u2014 nothing to migrate\n");
2429
+ if (tablesSkipped.length > 0) {
2430
+ console.log(chalk8.dim(` ${tablesSkipped.length} table(s) already up to date:`));
2431
+ for (const table of tablesSkipped) {
2432
+ console.log(chalk8.dim(` - ${table.tableName}`));
2433
+ }
2434
+ console.log();
2435
+ }
2436
+ return;
2437
+ }
2438
+ console.log(chalk8.yellow(` ${tablesToMigrate.length} table(s) to migrate:
2439
+ `));
2440
+ for (const table of tablesToMigrate) {
2441
+ console.log(` ${chalk8.cyan(table.tableName)} ${chalk8.dim(`(${table.rowsMigrated} rows)`)}`);
2442
+ if (table.columnsAdded.length > 0) {
2443
+ console.log(chalk8.dim(` Adding columns: ${table.columnsAdded.join(", ")}`));
2444
+ }
2445
+ if (table.columnMappings.length > 0) {
2446
+ for (const mapping of table.columnMappings) {
2447
+ console.log(chalk8.dim(` Mapping: ${mapping.from} \u2192 ${mapping.to}`));
2448
+ }
2449
+ }
2450
+ console.log();
2451
+ }
2452
+ if (tablesSkipped.length > 0) {
2453
+ console.log(chalk8.dim(` ${tablesSkipped.length} table(s) already up to date`));
2454
+ console.log();
2455
+ }
2456
+ if (options.dryRun) {
2457
+ console.log(chalk8.dim(" Dry run complete \u2014 no changes were made\n"));
2458
+ return;
2459
+ }
2460
+ console.log(chalk8.yellow(" \u26A0 This will modify your database tables."));
2461
+ console.log(chalk8.yellow(" Existing columns will be kept. Old primary key values"));
2462
+ console.log(chalk8.yellow(" will be copied to the new system columns where possible.\n"));
2463
+ const confirmed = await askConfirmation(" Proceed with migration?");
2464
+ if (!confirmed) {
2465
+ console.log(chalk8.dim("\n Migration cancelled\n"));
2466
+ return;
2467
+ }
2468
+ console.log();
2469
+ const migrateSpinner = ora8("Migrating tables...").start();
2470
+ const migrateResponse = await fetch(baseUrl, {
2471
+ method: "POST",
2472
+ headers: {
2473
+ "Content-Type": "application/json",
2474
+ "Authorization": `Bearer ${credentials.accessToken}`
2475
+ }
2476
+ });
2477
+ if (!migrateResponse.ok) {
2478
+ const text = await migrateResponse.text();
2479
+ let errorMessage;
2480
+ try {
2481
+ const error = JSON.parse(text);
2482
+ errorMessage = error.error || `HTTP ${migrateResponse.status}`;
2483
+ } catch {
2484
+ errorMessage = text || `HTTP ${migrateResponse.status}: ${migrateResponse.statusText}`;
2485
+ }
2486
+ throw new Error(errorMessage);
2487
+ }
2488
+ const result = await migrateResponse.json();
2489
+ migrateSpinner.succeed("Migration complete");
2490
+ console.log();
2491
+ const migrated = result.tables.filter((t) => t.status === "migrated");
2492
+ for (const table of migrated) {
2493
+ console.log(` ${chalk8.green("\u2713")} ${chalk8.cyan(table.tableName)} \u2014 ${table.rowsMigrated} rows migrated`);
2494
+ for (const mapping of table.columnMappings) {
2495
+ console.log(chalk8.dim(` ${mapping.from} \u2192 ${mapping.to}`));
2496
+ }
2497
+ }
2498
+ console.log();
2499
+ console.log(chalk8.dim(` ${result.summary}`));
2500
+ console.log();
2501
+ console.log(chalk8.dim(" Old columns have been preserved. You can remove them manually"));
2502
+ console.log(chalk8.dim(" once you have updated your application code.\n"));
2503
+ } catch (error) {
2504
+ spinner.fail("Migration failed");
2505
+ console.error(chalk8.red(error instanceof Error ? error.message : "Unknown error"));
2506
+ console.log();
2507
+ process.exit(1);
2508
+ }
2509
+ }
2510
+ function askConfirmation(prompt) {
2511
+ return new Promise((resolve) => {
2512
+ const rl = readline2.createInterface({
2513
+ input: process.stdin,
2514
+ output: process.stdout
2515
+ });
2516
+ rl.question(`${prompt} ${chalk8.dim("(y/N)")} `, (answer) => {
2517
+ rl.close();
2518
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
2519
+ });
2520
+ });
2521
+ }
2522
+
2368
2523
  // src/index.ts
2369
2524
  var program = new Command();
2370
2525
  program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version("0.0.1").enablePositionalOptions();
@@ -2380,6 +2535,8 @@ program.command("logout").description("Sign out of Tether").action(logoutCommand
2380
2535
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
2381
2536
  program.command("update").description("Update all Tether packages to the latest version").option("--dry-run", "Show what would be updated without updating").action(updateCommand);
2382
2537
  program.command("exec <sql>").description("Execute a SQL query against the project database").action(execCommand);
2538
+ var migrate = program.command("migrate").description("Database migration utilities");
2539
+ migrate.command("system-columns").description("Migrate legacy tables to use system-managed _id, _createdAt, _updatedAt, _deletedAt columns").option("-e, --env <environment>", "Target environment (default: development)").option("--dry-run", "Show what would be migrated without making changes").action(migrateSystemColumnsCommand);
2383
2540
  var env = program.command("env").description("Manage project environments");
2384
2541
  env.command("list").description("List all environments").action(envListCommand);
2385
2542
  env.command("create <name>").description("Create a new environment").option("--from <env>", "Clone schema and functions from an existing environment").action(envCreateCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tthr",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "description": "Tether CLI - project scaffolding and deployment",
5
5
  "type": "module",
6
6
  "bin": {