tthr 0.0.60 → 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.
@@ -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, {
@@ -435,7 +436,7 @@ function parseFunctions(moduleName, source) {
435
436
 
436
437
  // src/commands/init.ts
437
438
  var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
438
- 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";
439
440
  async function getLatestVersion(packageName) {
440
441
  try {
441
442
  const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
@@ -537,7 +538,7 @@ ${packageManager} is not installed on your system.`));
537
538
  await scaffoldVanillaProject(projectName, projectPath, spinner);
538
539
  }
539
540
  spinner.text = "Creating project on Tether...";
540
- const response = await fetch(`${API_URL2}/projects`, {
541
+ const response = await fetch(`${API_URL3}/projects`, {
541
542
  method: "POST",
542
543
  headers: {
543
544
  "Content-Type": "application/json",
@@ -1552,7 +1553,7 @@ async function runGenerate(spinner) {
1552
1553
  }
1553
1554
  isGenerating = true;
1554
1555
  try {
1555
- const { generateTypes: generateTypes2 } = await import("./generate-5AY6JNCG.js");
1556
+ const { generateTypes: generateTypes2 } = await import("./generate-Q4MUBHHO.js");
1556
1557
  spinner.text = "Regenerating types...";
1557
1558
  await generateTypes2({ silent: true });
1558
1559
  spinner.succeed("Types regenerated");
@@ -1737,7 +1738,7 @@ async function devCommand(options) {
1737
1738
  }
1738
1739
  }
1739
1740
  spinner.text = "Generating types...";
1740
- const { generateTypes: generateTypes2 } = await import("./generate-5AY6JNCG.js");
1741
+ const { generateTypes: generateTypes2 } = await import("./generate-Q4MUBHHO.js");
1741
1742
  await generateTypes2({ silent: true });
1742
1743
  spinner.succeed("Types generated");
1743
1744
  spinner.start("Setting up file watchers...");
@@ -1808,7 +1809,7 @@ import os from "os";
1808
1809
  import { exec } from "child_process";
1809
1810
  import readline from "readline";
1810
1811
  var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
1811
- 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";
1812
1813
  var AUTH_URL = isDev3 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
1813
1814
  async function loginCommand() {
1814
1815
  console.log(chalk4.bold("\u26A1 Login to Tether\n"));
@@ -1898,7 +1899,7 @@ async function requestDeviceCode() {
1898
1899
  const userCode = generateUserCode();
1899
1900
  const deviceCode = crypto.randomUUID();
1900
1901
  const deviceName = os.hostname();
1901
- const response = await fetch(`${API_URL3}/auth/device`, {
1902
+ const response = await fetch(`${API_URL4}/auth/device`, {
1902
1903
  method: "POST",
1903
1904
  headers: { "Content-Type": "application/json" },
1904
1905
  body: JSON.stringify({ userCode, deviceCode, deviceName })
@@ -1932,7 +1933,7 @@ async function pollForApproval(deviceCode, interval, expiresIn) {
1932
1933
  while (Date.now() < expiresAt) {
1933
1934
  await sleep(interval * 1e3);
1934
1935
  spinner.text = `Checking... ${chalk4.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
1935
- const response = await fetch(`${API_URL3}/auth/device/${deviceCode}`, {
1936
+ const response = await fetch(`${API_URL4}/auth/device/${deviceCode}`, {
1936
1937
  method: "GET"
1937
1938
  }).catch(() => null);
1938
1939
  updateCountdown();
@@ -2145,7 +2146,7 @@ import ora6 from "ora";
2145
2146
  import fs5 from "fs-extra";
2146
2147
  import path5 from "path";
2147
2148
  var isDev4 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
2148
- 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";
2149
2150
  async function execCommand(sql) {
2150
2151
  if (!sql || sql.trim() === "") {
2151
2152
  console.log(chalk6.red("\nError: SQL query required"));
@@ -2167,7 +2168,7 @@ async function execCommand(sql) {
2167
2168
  }
2168
2169
  const spinner = ora6("Executing query...").start();
2169
2170
  try {
2170
- const response = await fetch(`${API_URL4}/projects/${projectId}/exec`, {
2171
+ const response = await fetch(`${API_URL5}/projects/${projectId}/exec`, {
2171
2172
  method: "POST",
2172
2173
  headers: {
2173
2174
  "Content-Type": "application/json",
@@ -2218,7 +2219,7 @@ import ora7 from "ora";
2218
2219
  import fs6 from "fs-extra";
2219
2220
  import path6 from "path";
2220
2221
  var isDev5 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
2221
- 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";
2222
2223
  async function getProjectId() {
2223
2224
  const envPath = path6.resolve(process.cwd(), ".env");
2224
2225
  let projectId;
@@ -2239,7 +2240,7 @@ async function envListCommand() {
2239
2240
  const projectId = await getProjectId();
2240
2241
  const spinner = ora7("Fetching environments...").start();
2241
2242
  try {
2242
- const response = await fetch(`${API_URL5}/projects/${projectId}/environments`, {
2243
+ const response = await fetch(`${API_URL6}/projects/${projectId}/environments`, {
2243
2244
  headers: {
2244
2245
  "Authorization": `Bearer ${credentials.accessToken}`
2245
2246
  }
@@ -2282,7 +2283,7 @@ async function envCreateCommand(name, options) {
2282
2283
  body.cloneFrom = options.from;
2283
2284
  spinner.text = `Creating environment '${normalizedName}' from '${options.from}'...`;
2284
2285
  }
2285
- const response = await fetch(`${API_URL5}/projects/${projectId}/environments`, {
2286
+ const response = await fetch(`${API_URL6}/projects/${projectId}/environments`, {
2286
2287
  method: "POST",
2287
2288
  headers: {
2288
2289
  "Content-Type": "application/json",
@@ -2313,7 +2314,7 @@ async function envDeleteCommand(name) {
2313
2314
  const projectId = await getProjectId();
2314
2315
  const spinner = ora7(`Deleting environment '${name}'...`).start();
2315
2316
  try {
2316
- const response = await fetch(`${API_URL5}/projects/${projectId}/environments/${name}`, {
2317
+ const response = await fetch(`${API_URL6}/projects/${projectId}/environments/${name}`, {
2317
2318
  method: "DELETE",
2318
2319
  headers: {
2319
2320
  "Authorization": `Bearer ${credentials.accessToken}`
@@ -2335,7 +2336,7 @@ async function envDefaultCommand(name) {
2335
2336
  const projectId = await getProjectId();
2336
2337
  const spinner = ora7(`Setting '${name}' as default environment...`).start();
2337
2338
  try {
2338
- const response = await fetch(`${API_URL5}/projects/${projectId}/environments/default`, {
2339
+ const response = await fetch(`${API_URL6}/projects/${projectId}/environments/default`, {
2339
2340
  method: "PATCH",
2340
2341
  headers: {
2341
2342
  "Content-Type": "application/json",
@@ -2373,6 +2374,152 @@ function maskApiKey(key) {
2373
2374
  return `${visibleStart}...${visibleEnd}`;
2374
2375
  }
2375
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
+
2376
2523
  // src/index.ts
2377
2524
  var program = new Command();
2378
2525
  program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version("0.0.1").enablePositionalOptions();
@@ -2388,6 +2535,8 @@ program.command("logout").description("Sign out of Tether").action(logoutCommand
2388
2535
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
2389
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);
2390
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);
2391
2540
  var env = program.command("env").description("Manage project environments");
2392
2541
  env.command("list").description("List all environments").action(envListCommand);
2393
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.60",
3
+ "version": "0.1.0",
4
4
  "description": "Tether CLI - project scaffolding and deployment",
5
5
  "type": "module",
6
6
  "bin": {