tthr 0.0.45 → 0.0.47

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,465 @@
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
+ async function getCredentials() {
14
+ try {
15
+ if (await fs.pathExists(CREDENTIALS_FILE)) {
16
+ return await fs.readJSON(CREDENTIALS_FILE);
17
+ }
18
+ } catch {
19
+ }
20
+ return null;
21
+ }
22
+ async function requireAuth() {
23
+ const credentials = await getCredentials();
24
+ if (!credentials) {
25
+ console.error(chalk.red("\n\u2717 Not logged in\n"));
26
+ console.log(chalk.dim("Run `tthr login` to authenticate\n"));
27
+ process.exit(1);
28
+ }
29
+ if (credentials.expiresAt) {
30
+ const expiresAt = new Date(credentials.expiresAt);
31
+ if (/* @__PURE__ */ new Date() > expiresAt) {
32
+ console.error(chalk.red("\n\u2717 Session expired\n"));
33
+ console.log(chalk.dim("Run `tthr login` to authenticate\n"));
34
+ process.exit(1);
35
+ }
36
+ }
37
+ return credentials;
38
+ }
39
+ async function saveCredentials(credentials) {
40
+ await fs.ensureDir(CONFIG_DIR);
41
+ await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
42
+ }
43
+ async function clearCredentials() {
44
+ try {
45
+ await fs.remove(CREDENTIALS_FILE);
46
+ } catch {
47
+ }
48
+ }
49
+
50
+ // src/utils/config.ts
51
+ import fs2 from "fs-extra";
52
+ import path2 from "path";
53
+ var DEFAULT_CONFIG = {
54
+ schema: "./tether/schema.ts",
55
+ functions: "./tether/functions",
56
+ output: "./tether/_generated",
57
+ dev: {
58
+ port: 3001,
59
+ host: "localhost"
60
+ },
61
+ database: {
62
+ walMode: true
63
+ }
64
+ };
65
+ async function loadConfig(cwd = process.cwd()) {
66
+ const configPath = path2.resolve(cwd, "tether.config.ts");
67
+ if (!await fs2.pathExists(configPath)) {
68
+ return DEFAULT_CONFIG;
69
+ }
70
+ const configSource = await fs2.readFile(configPath, "utf-8");
71
+ const config = {};
72
+ const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
73
+ if (schemaMatch) {
74
+ config.schema = schemaMatch[1];
75
+ }
76
+ const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
77
+ if (functionsMatch) {
78
+ config.functions = functionsMatch[1];
79
+ }
80
+ const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
81
+ if (outputMatch) {
82
+ config.output = outputMatch[1];
83
+ }
84
+ const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
85
+ if (envMatch) {
86
+ config.environment = envMatch[1];
87
+ }
88
+ const portMatch = configSource.match(/port\s*:\s*(\d+)/);
89
+ if (portMatch) {
90
+ config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
91
+ }
92
+ const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
93
+ if (hostMatch) {
94
+ config.dev = { ...config.dev, host: hostMatch[1] };
95
+ }
96
+ return {
97
+ ...DEFAULT_CONFIG,
98
+ ...config,
99
+ dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
100
+ database: { ...DEFAULT_CONFIG.database, ...config.database }
101
+ };
102
+ }
103
+ function resolvePath(configPath, cwd = process.cwd()) {
104
+ const normalised = configPath.replace(/^\.\//, "");
105
+ return path2.resolve(cwd, normalised);
106
+ }
107
+ async function detectFramework(cwd = process.cwd()) {
108
+ const packageJsonPath = path2.resolve(cwd, "package.json");
109
+ if (!await fs2.pathExists(packageJsonPath)) {
110
+ return "unknown";
111
+ }
112
+ try {
113
+ const packageJson = await fs2.readJson(packageJsonPath);
114
+ const deps = {
115
+ ...packageJson.dependencies,
116
+ ...packageJson.devDependencies
117
+ };
118
+ if (deps.nuxt || deps["@nuxt/kit"]) {
119
+ return "nuxt";
120
+ }
121
+ if (deps.next) {
122
+ return "next";
123
+ }
124
+ if (deps["@sveltejs/kit"]) {
125
+ return "sveltekit";
126
+ }
127
+ if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
128
+ return "vite";
129
+ }
130
+ return "vanilla";
131
+ } catch {
132
+ return "unknown";
133
+ }
134
+ }
135
+ function getFrameworkDevCommand(framework) {
136
+ switch (framework) {
137
+ case "nuxt":
138
+ return "nuxt dev";
139
+ case "next":
140
+ return "next dev";
141
+ case "sveltekit":
142
+ return "vite dev";
143
+ case "vite":
144
+ return "vite dev";
145
+ case "vanilla":
146
+ return null;
147
+ // No default dev server for vanilla
148
+ default:
149
+ return null;
150
+ }
151
+ }
152
+
153
+ // src/commands/generate.ts
154
+ function parseSchemaFile(source) {
155
+ const tables = [];
156
+ const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
157
+ if (!schemaMatch) return tables;
158
+ const schemaContent = schemaMatch[1];
159
+ const tableStartRegex = /(\w+)\s*:\s*\{/g;
160
+ let match;
161
+ while ((match = tableStartRegex.exec(schemaContent)) !== null) {
162
+ const tableName = match[1];
163
+ const startOffset = match.index + match[0].length;
164
+ let braceCount = 1;
165
+ let endOffset = startOffset;
166
+ for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
167
+ const char = schemaContent[i];
168
+ if (char === "{") braceCount++;
169
+ else if (char === "}") braceCount--;
170
+ endOffset = i;
171
+ }
172
+ const columnsContent = schemaContent.slice(startOffset, endOffset);
173
+ const columns = parseColumns(columnsContent);
174
+ tables.push({ name: tableName, columns });
175
+ }
176
+ return tables;
177
+ }
178
+ function parseColumns(content) {
179
+ const columns = [];
180
+ const columnRegex = /(\w+)\s*:\s*(\w+)\s*\(\s*\)([^,\n}]*)/g;
181
+ let match;
182
+ while ((match = columnRegex.exec(content)) !== null) {
183
+ const name = match[1];
184
+ const schemaType = match[2];
185
+ const modifiers = match[3] || "";
186
+ const nullable = !modifiers.includes(".notNull()");
187
+ columns.push({
188
+ name,
189
+ type: schemaTypeToTS(schemaType),
190
+ nullable
191
+ });
192
+ }
193
+ return columns;
194
+ }
195
+ function schemaTypeToTS(schemaType) {
196
+ switch (schemaType) {
197
+ case "text":
198
+ return "string";
199
+ case "integer":
200
+ return "number";
201
+ case "real":
202
+ return "number";
203
+ case "boolean":
204
+ return "boolean";
205
+ case "timestamp":
206
+ return "string";
207
+ case "json":
208
+ return "unknown";
209
+ case "blob":
210
+ return "Uint8Array";
211
+ default:
212
+ return "unknown";
213
+ }
214
+ }
215
+ function tableNameToInterface(tableName) {
216
+ let name = tableName;
217
+ if (name.endsWith("ies")) {
218
+ name = name.slice(0, -3) + "y";
219
+ } else if (name.endsWith("s") && !name.endsWith("ss")) {
220
+ name = name.slice(0, -1);
221
+ }
222
+ return name.charAt(0).toUpperCase() + name.slice(1);
223
+ }
224
+ function generateDbFile(tables) {
225
+ const lines = [
226
+ "// Auto-generated by Tether CLI - do not edit manually",
227
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
228
+ "",
229
+ "import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
230
+ "import {",
231
+ " query as baseQuery,",
232
+ " mutation as baseMutation,",
233
+ " action as baseAction,",
234
+ " type QueryDefinition,",
235
+ " type MutationDefinition,",
236
+ " type ActionDefinition,",
237
+ " z,",
238
+ "} from '@tthr/server';",
239
+ ""
240
+ ];
241
+ for (const table of tables) {
242
+ const interfaceName = tableNameToInterface(table.name);
243
+ lines.push(`export interface ${interfaceName} {`);
244
+ lines.push(" _id: string;");
245
+ for (const col of table.columns) {
246
+ const typeStr = col.nullable ? `${col.type} | null` : col.type;
247
+ lines.push(` ${col.name}: ${typeStr};`);
248
+ }
249
+ lines.push(" _createdAt: string;");
250
+ lines.push("}");
251
+ lines.push("");
252
+ }
253
+ lines.push("export interface Schema {");
254
+ for (const table of tables) {
255
+ const interfaceName = tableNameToInterface(table.name);
256
+ lines.push(` ${table.name}: ${interfaceName};`);
257
+ }
258
+ lines.push("}");
259
+ lines.push("");
260
+ lines.push("// Database client with typed tables");
261
+ lines.push("// This is a proxy that will be populated by the Tether runtime");
262
+ lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
263
+ lines.push("");
264
+ lines.push("// ============================================================================");
265
+ lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
266
+ lines.push("// This ensures the `db` parameter in handlers is properly typed with Schema");
267
+ lines.push("// ============================================================================");
268
+ lines.push("");
269
+ lines.push("/**");
270
+ lines.push(" * Define a query function with typed database access.");
271
+ lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
272
+ lines.push(" */");
273
+ lines.push("export function query<TArgs = void, TResult = unknown>(");
274
+ lines.push(" definition: QueryDefinition<TArgs, TResult, Schema>");
275
+ lines.push("): QueryDefinition<TArgs, TResult, Schema> {");
276
+ lines.push(" return baseQuery(definition);");
277
+ lines.push("}");
278
+ lines.push("");
279
+ lines.push("/**");
280
+ lines.push(" * Define a mutation function with typed database access.");
281
+ lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
282
+ lines.push(" */");
283
+ lines.push("export function mutation<TArgs = void, TResult = unknown>(");
284
+ lines.push(" definition: MutationDefinition<TArgs, TResult, Schema>");
285
+ lines.push("): MutationDefinition<TArgs, TResult, Schema> {");
286
+ lines.push(" return baseMutation(definition);");
287
+ lines.push("}");
288
+ lines.push("");
289
+ lines.push("/**");
290
+ lines.push(" * Define an action function with typed database access.");
291
+ lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
292
+ lines.push(" */");
293
+ lines.push("export function action<TArgs = void, TResult = unknown>(");
294
+ lines.push(" definition: ActionDefinition<TArgs, TResult, Schema>");
295
+ lines.push("): ActionDefinition<TArgs, TResult, Schema> {");
296
+ lines.push(" return baseAction(definition);");
297
+ lines.push("}");
298
+ lines.push("");
299
+ lines.push("// Re-export z for convenience");
300
+ lines.push("export { z };");
301
+ lines.push("");
302
+ return lines.join("\n");
303
+ }
304
+ async function parseFunctionsDir(functionsDir) {
305
+ const functions = [];
306
+ if (!await fs3.pathExists(functionsDir)) {
307
+ return functions;
308
+ }
309
+ const files = await fs3.readdir(functionsDir);
310
+ for (const file of files) {
311
+ if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
312
+ const filePath = path3.join(functionsDir, file);
313
+ const stat = await fs3.stat(filePath);
314
+ if (!stat.isFile()) continue;
315
+ const moduleName = file.replace(/\.(ts|js)$/, "");
316
+ const source = await fs3.readFile(filePath, "utf-8");
317
+ const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action)\s*\(/g;
318
+ let match;
319
+ while ((match = exportRegex.exec(source)) !== null) {
320
+ functions.push({
321
+ name: match[1],
322
+ moduleName
323
+ });
324
+ }
325
+ }
326
+ return functions;
327
+ }
328
+ async function generateApiFile(functionsDir) {
329
+ const functions = await parseFunctionsDir(functionsDir);
330
+ const moduleMap = /* @__PURE__ */ new Map();
331
+ for (const fn of functions) {
332
+ if (!moduleMap.has(fn.moduleName)) {
333
+ moduleMap.set(fn.moduleName, []);
334
+ }
335
+ moduleMap.get(fn.moduleName).push(fn.name);
336
+ }
337
+ const lines = [
338
+ "// Auto-generated by Tether CLI - do not edit manually",
339
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
340
+ "",
341
+ "import { createApiProxy } from '@tthr/client';",
342
+ "",
343
+ "/**",
344
+ " * API function reference type for useQuery/useMutation.",
345
+ ' * The _name property contains the function path (e.g., "users.list").',
346
+ " */",
347
+ "export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
348
+ " _name: string;",
349
+ " _args?: TArgs;",
350
+ " _result?: TResult;",
351
+ "}",
352
+ ""
353
+ ];
354
+ if (moduleMap.size > 0) {
355
+ for (const [moduleName, fnNames] of moduleMap) {
356
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
357
+ lines.push(`export interface ${interfaceName} {`);
358
+ for (const fnName of fnNames) {
359
+ lines.push(` ${fnName}: ApiFunction;`);
360
+ }
361
+ lines.push("}");
362
+ lines.push("");
363
+ }
364
+ lines.push("export interface Api {");
365
+ for (const moduleName of moduleMap.keys()) {
366
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
367
+ lines.push(` ${moduleName}: ${interfaceName};`);
368
+ }
369
+ lines.push("}");
370
+ } else {
371
+ lines.push("/**");
372
+ lines.push(" * Flexible API type that allows access to any function path.");
373
+ lines.push(" * Functions are accessed as api.moduleName.functionName");
374
+ lines.push(" * The actual types depend on your function definitions.");
375
+ lines.push(" */");
376
+ lines.push("export type Api = {");
377
+ lines.push(" [module: string]: {");
378
+ lines.push(" [fn: string]: ApiFunction;");
379
+ lines.push(" };");
380
+ lines.push("};");
381
+ }
382
+ lines.push("");
383
+ lines.push("// API client proxy - provides typed access to your functions");
384
+ lines.push("// On the client: returns { _name } references for useQuery/useMutation");
385
+ lines.push("// In Tether functions: calls the actual function implementation");
386
+ lines.push("export const api = createApiProxy<Api>();");
387
+ lines.push("");
388
+ return lines.join("\n");
389
+ }
390
+ async function generateTypes(options = {}) {
391
+ const config = await loadConfig();
392
+ const schemaPath = resolvePath(config.schema);
393
+ const outputDir = resolvePath(config.output);
394
+ const functionsDir = resolvePath(config.functions);
395
+ if (!await fs3.pathExists(schemaPath)) {
396
+ throw new Error(`Schema file not found: ${schemaPath}`);
397
+ }
398
+ const schemaSource = await fs3.readFile(schemaPath, "utf-8");
399
+ const tables = parseSchemaFile(schemaSource);
400
+ await fs3.ensureDir(outputDir);
401
+ await fs3.writeFile(
402
+ path3.join(outputDir, "db.ts"),
403
+ generateDbFile(tables)
404
+ );
405
+ await fs3.writeFile(
406
+ path3.join(outputDir, "api.ts"),
407
+ await generateApiFile(functionsDir)
408
+ );
409
+ await fs3.writeFile(
410
+ path3.join(outputDir, "index.ts"),
411
+ `// Auto-generated by Tether CLI - do not edit manually
412
+ export * from './db';
413
+ export * from './api';
414
+ `
415
+ );
416
+ return { tables, outputDir };
417
+ }
418
+ async function generateCommand() {
419
+ await requireAuth();
420
+ const configPath = path3.resolve(process.cwd(), "tether.config.ts");
421
+ if (!await fs3.pathExists(configPath)) {
422
+ console.log(chalk2.red("\nError: Not a Tether project"));
423
+ console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
424
+ process.exit(1);
425
+ }
426
+ console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
427
+ const spinner = ora("Reading schema...").start();
428
+ try {
429
+ spinner.text = "Generating types...";
430
+ const { tables, outputDir } = await generateTypes();
431
+ if (tables.length === 0) {
432
+ spinner.warn("No tables found in schema");
433
+ console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
434
+ return;
435
+ }
436
+ spinner.succeed(`Types generated for ${tables.length} table(s)`);
437
+ console.log("\n" + chalk2.green("\u2713") + " Tables:");
438
+ for (const table of tables) {
439
+ console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
440
+ }
441
+ const relativeOutput = path3.relative(process.cwd(), outputDir);
442
+ console.log("\n" + chalk2.green("\u2713") + " Generated files:");
443
+ console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
444
+ console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
445
+ console.log(chalk2.dim(` ${relativeOutput}/index.ts
446
+ `));
447
+ } catch (error) {
448
+ spinner.fail("Failed to generate types");
449
+ console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
450
+ process.exit(1);
451
+ }
452
+ }
453
+
454
+ export {
455
+ getCredentials,
456
+ requireAuth,
457
+ saveCredentials,
458
+ clearCredentials,
459
+ loadConfig,
460
+ resolvePath,
461
+ detectFramework,
462
+ getFrameworkDevCommand,
463
+ generateTypes,
464
+ generateCommand
465
+ };
@@ -0,0 +1,8 @@
1
+ import {
2
+ generateCommand,
3
+ generateTypes
4
+ } from "./chunk-E5LJW52C.js";
5
+ export {
6
+ generateCommand,
7
+ generateTypes
8
+ };
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  requireAuth,
10
10
  resolvePath,
11
11
  saveCredentials
12
- } from "./chunk-TU5LOAQ5.js";
12
+ } from "./chunk-E5LJW52C.js";
13
13
 
14
14
  // src/index.ts
15
15
  import { Command } from "commander";
@@ -1510,7 +1510,7 @@ async function runGenerate(spinner) {
1510
1510
  }
1511
1511
  isGenerating = true;
1512
1512
  try {
1513
- const { generateTypes } = await import("./generate-HRIVNCH3.js");
1513
+ const { generateTypes } = await import("./generate-IJY2CQM4.js");
1514
1514
  spinner.text = "Regenerating types...";
1515
1515
  await generateTypes({ silent: true });
1516
1516
  spinner.succeed("Types regenerated");
@@ -1582,11 +1582,12 @@ async function validateFunctions(functionsDir) {
1582
1582
  }
1583
1583
  return { valid: errors.length === 0, errors };
1584
1584
  }
1585
- function startFrameworkDev(command, port, spinner) {
1585
+ function startFrameworkDev(command, port, spinner, extraArgs = []) {
1586
1586
  const [cmd, ...args] = command.split(" ");
1587
1587
  const portArgs = ["--port", port];
1588
- spinner.text = `Starting ${command}...`;
1589
- const child = spawn(cmd, [...args, ...portArgs], {
1588
+ const allArgs = [...args, ...portArgs, ...extraArgs];
1589
+ spinner.text = `Starting ${command}${extraArgs.length ? ` with extra args: ${extraArgs.join(" ")}` : ""}...`;
1590
+ const child = spawn(cmd, allArgs, {
1590
1591
  cwd: process.cwd(),
1591
1592
  stdio: ["inherit", "pipe", "pipe"],
1592
1593
  shell: true,
@@ -1664,6 +1665,9 @@ async function devCommand(options) {
1664
1665
  console.log(chalk3.dim(` Schema: ${path3.relative(process.cwd(), schemaPath)}`));
1665
1666
  console.log(chalk3.dim(` Functions: ${path3.relative(process.cwd(), functionsDir)}`));
1666
1667
  console.log(chalk3.dim(` Output: ${path3.relative(process.cwd(), outputDir)}`));
1668
+ if (options.extraArgs && options.extraArgs.length > 0) {
1669
+ console.log(chalk3.dim(` Extra args: ${options.extraArgs.join(" ")}`));
1670
+ }
1667
1671
  console.log("");
1668
1672
  const spinner = ora3("Initialising...").start();
1669
1673
  process.on("SIGINT", () => {
@@ -1691,7 +1695,7 @@ async function devCommand(options) {
1691
1695
  }
1692
1696
  }
1693
1697
  spinner.text = "Generating types...";
1694
- const { generateTypes } = await import("./generate-HRIVNCH3.js");
1698
+ const { generateTypes } = await import("./generate-IJY2CQM4.js");
1695
1699
  await generateTypes({ silent: true });
1696
1700
  spinner.succeed("Types generated");
1697
1701
  spinner.start("Setting up file watchers...");
@@ -1737,7 +1741,7 @@ async function devCommand(options) {
1737
1741
  spinner.succeed("File watchers ready");
1738
1742
  if (devCmd && !options.skipFramework) {
1739
1743
  spinner.start(`Starting ${framework} dev server...`);
1740
- frameworkProcess = startFrameworkDev(devCmd, port, spinner);
1744
+ frameworkProcess = startFrameworkDev(devCmd, port, spinner, options.extraArgs || []);
1741
1745
  } else {
1742
1746
  spinner.start("Watching for changes...");
1743
1747
  console.log("\n" + chalk3.cyan(` Tether dev ready`));
@@ -1759,6 +1763,8 @@ async function devCommand(options) {
1759
1763
  import chalk4 from "chalk";
1760
1764
  import ora4 from "ora";
1761
1765
  import os from "os";
1766
+ import { exec } from "child_process";
1767
+ import readline from "readline";
1762
1768
  var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
1763
1769
  var API_URL3 = isDev3 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
1764
1770
  var AUTH_URL = isDev3 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
@@ -1775,11 +1781,14 @@ async function loginCommand() {
1775
1781
  const deviceCode = await requestDeviceCode();
1776
1782
  spinner.stop();
1777
1783
  const authUrl = `${AUTH_URL}/${deviceCode.userCode}`;
1778
- console.log(chalk4.dim("Open this URL in your browser to authenticate:\n"));
1784
+ console.log(chalk4.dim("Authenticate in your browser:\n"));
1779
1785
  console.log(chalk4.cyan.bold(` ${authUrl}
1780
1786
  `));
1781
1787
  console.log(chalk4.dim(`Your code: ${chalk4.white.bold(deviceCode.userCode)}
1782
1788
  `));
1789
+ await waitForEnter(chalk4.dim("Press Enter to open in browser..."));
1790
+ openBrowser(authUrl);
1791
+ console.log();
1783
1792
  const credentials = await pollForApproval(deviceCode.deviceCode, deviceCode.interval, deviceCode.expiresIn);
1784
1793
  await saveCredentials(credentials);
1785
1794
  console.log(chalk4.green("\u2713") + ` Logged in as ${chalk4.cyan(credentials.email)}`);
@@ -1911,6 +1920,33 @@ async function pollForApproval(deviceCode, interval, expiresIn) {
1911
1920
  function sleep(ms) {
1912
1921
  return new Promise((resolve) => setTimeout(resolve, ms));
1913
1922
  }
1923
+ function openBrowser(url) {
1924
+ const platform = process.platform;
1925
+ let command;
1926
+ if (platform === "darwin") {
1927
+ command = `open "${url}"`;
1928
+ } else if (platform === "win32") {
1929
+ command = `start "" "${url}"`;
1930
+ } else {
1931
+ command = `xdg-open "${url}"`;
1932
+ }
1933
+ exec(command, (error) => {
1934
+ if (error) {
1935
+ }
1936
+ });
1937
+ }
1938
+ function waitForEnter(prompt) {
1939
+ return new Promise((resolve) => {
1940
+ const rl = readline.createInterface({
1941
+ input: process.stdin,
1942
+ output: process.stdout
1943
+ });
1944
+ rl.question(prompt, () => {
1945
+ rl.close();
1946
+ resolve();
1947
+ });
1948
+ });
1949
+ }
1914
1950
 
1915
1951
  // src/commands/update.ts
1916
1952
  import chalk5 from "chalk";
@@ -2286,9 +2322,12 @@ function maskApiKey(key) {
2286
2322
 
2287
2323
  // src/index.ts
2288
2324
  var program = new Command();
2289
- program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version("0.0.1");
2325
+ program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version("0.0.1").enablePositionalOptions();
2290
2326
  program.command("init [name]").description("Create a new Tether project").option("-t, --template <template>", "Project template (vue, svelte, react, vanilla)", "vue").action(initCommand);
2291
- program.command("dev").description("Start local development server with hot reload").option("-p, --port <port>", "Port to run on").option("-e, --env <environment>", "Target environment (default: development)").option("--local", "Run fully local (do not connect to cloud)").option("--skip-framework", "Only run Tether watchers, skip framework dev server").action(devCommand);
2327
+ program.command("dev").description("Start local development server with hot reload").option("-p, --port <port>", "Port to run on").option("-e, --env <environment>", "Target environment (default: development)").option("--local", "Run fully local (do not connect to cloud)").option("--skip-framework", "Only run Tether watchers, skip framework dev server").allowUnknownOption().passThroughOptions().action((options, command) => {
2328
+ const extraArgs = command.args || [];
2329
+ devCommand({ ...options, extraArgs });
2330
+ });
2292
2331
  program.command("generate").alias("gen").description("Generate types from schema").action(generateCommand);
2293
2332
  program.command("deploy").description("Deploy schema and functions to Tether").option("-s, --schema", "Deploy schema only").option("-f, --functions", "Deploy functions only").option("-e, --env <environment>", "Target environment (default: development)").option("--dry-run", "Show what would be deployed without deploying").action(deployCommand);
2294
2333
  program.command("login").description("Authenticate with Tether").action(loginCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tthr",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "description": "Tether CLI - project scaffolding and deployment",
5
5
  "type": "module",
6
6
  "bin": {