tthr 0.0.33 → 0.0.35

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.
package/dist/index.js CHANGED
@@ -1,184 +1,99 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ clearCredentials,
4
+ detectFramework,
5
+ generateCommand,
6
+ getCredentials,
7
+ getFrameworkDevCommand,
8
+ loadConfig,
9
+ requireAuth,
10
+ resolvePath,
11
+ saveCredentials
12
+ } from "./chunk-LXODXQ5V.js";
2
13
 
3
14
  // src/index.ts
4
15
  import { Command } from "commander";
5
16
 
6
17
  // src/commands/init.ts
7
- import chalk3 from "chalk";
18
+ import chalk2 from "chalk";
8
19
  import ora2 from "ora";
9
20
  import prompts from "prompts";
10
- import fs4 from "fs-extra";
11
- import path4 from "path";
21
+ import fs2 from "fs-extra";
22
+ import path2 from "path";
12
23
  import { execSync } from "child_process";
13
24
 
14
- // src/utils/auth.ts
25
+ // src/commands/deploy.ts
15
26
  import chalk from "chalk";
27
+ import ora from "ora";
16
28
  import fs from "fs-extra";
17
29
  import path from "path";
18
- var CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
19
- var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
20
- async function getCredentials() {
21
- try {
22
- if (await fs.pathExists(CREDENTIALS_FILE)) {
23
- return await fs.readJSON(CREDENTIALS_FILE);
24
- }
25
- } catch {
26
- }
27
- return null;
28
- }
29
- async function requireAuth() {
30
- const credentials = await getCredentials();
31
- if (!credentials) {
32
- console.error(chalk.red("\n\u2717 Not logged in\n"));
33
- console.log(chalk.dim("Run `tthr login` to authenticate\n"));
34
- process.exit(1);
35
- }
36
- if (credentials.expiresAt) {
37
- const expiresAt = new Date(credentials.expiresAt);
38
- if (/* @__PURE__ */ new Date() > expiresAt) {
39
- console.error(chalk.red("\n\u2717 Session expired\n"));
40
- console.log(chalk.dim("Run `tthr login` to authenticate\n"));
41
- process.exit(1);
42
- }
43
- }
44
- return credentials;
45
- }
46
- async function saveCredentials(credentials) {
47
- await fs.ensureDir(CONFIG_DIR);
48
- await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
49
- }
50
- async function clearCredentials() {
51
- try {
52
- await fs.remove(CREDENTIALS_FILE);
53
- } catch {
54
- }
55
- }
56
-
57
- // src/commands/deploy.ts
58
- import chalk2 from "chalk";
59
- import ora from "ora";
60
- import fs3 from "fs-extra";
61
- import path3 from "path";
62
-
63
- // src/utils/config.ts
64
- import fs2 from "fs-extra";
65
- import path2 from "path";
66
- var DEFAULT_CONFIG = {
67
- schema: "./tether/schema.ts",
68
- functions: "./tether/functions",
69
- output: "./tether/_generated",
70
- dev: {
71
- port: 3001,
72
- host: "localhost"
73
- },
74
- database: {
75
- walMode: true
76
- }
77
- };
78
- async function loadConfig(cwd = process.cwd()) {
79
- const configPath = path2.resolve(cwd, "tether.config.ts");
80
- if (!await fs2.pathExists(configPath)) {
81
- return DEFAULT_CONFIG;
82
- }
83
- const configSource = await fs2.readFile(configPath, "utf-8");
84
- const config = {};
85
- const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
86
- if (schemaMatch) {
87
- config.schema = schemaMatch[1];
88
- }
89
- const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
90
- if (functionsMatch) {
91
- config.functions = functionsMatch[1];
92
- }
93
- const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
94
- if (outputMatch) {
95
- config.output = outputMatch[1];
96
- }
97
- const portMatch = configSource.match(/port\s*:\s*(\d+)/);
98
- if (portMatch) {
99
- config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
100
- }
101
- const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
102
- if (hostMatch) {
103
- config.dev = { ...config.dev, host: hostMatch[1] };
104
- }
105
- return {
106
- ...DEFAULT_CONFIG,
107
- ...config,
108
- dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
109
- database: { ...DEFAULT_CONFIG.database, ...config.database }
110
- };
111
- }
112
- function resolvePath(configPath, cwd = process.cwd()) {
113
- const normalised = configPath.replace(/^\.\//, "");
114
- return path2.resolve(cwd, normalised);
115
- }
116
-
117
- // src/commands/deploy.ts
118
30
  var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
119
31
  var API_URL = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
120
32
  async function deployCommand(options) {
121
33
  const credentials = await requireAuth();
122
- const configPath = path3.resolve(process.cwd(), "tether.config.ts");
123
- if (!await fs3.pathExists(configPath)) {
124
- console.log(chalk2.red("\nError: Not a Tether project"));
125
- console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
34
+ const configPath = path.resolve(process.cwd(), "tether.config.ts");
35
+ if (!await fs.pathExists(configPath)) {
36
+ console.log(chalk.red("\nError: Not a Tether project"));
37
+ console.log(chalk.dim("Run `tthr init` to create a new project\n"));
126
38
  process.exit(1);
127
39
  }
128
- const envPath = path3.resolve(process.cwd(), ".env");
40
+ const envPath = path.resolve(process.cwd(), ".env");
129
41
  let projectId;
130
- if (await fs3.pathExists(envPath)) {
131
- const envContent = await fs3.readFile(envPath, "utf-8");
42
+ if (await fs.pathExists(envPath)) {
43
+ const envContent = await fs.readFile(envPath, "utf-8");
132
44
  const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
133
45
  projectId = match?.[1]?.trim();
134
46
  }
135
47
  if (!projectId) {
136
- console.log(chalk2.red("\nError: Project ID not found"));
137
- console.log(chalk2.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
48
+ console.log(chalk.red("\nError: Project ID not found"));
49
+ console.log(chalk.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
138
50
  process.exit(1);
139
51
  }
140
- console.log(chalk2.bold("\n\u26A1 Deploying to Tether\n"));
141
- console.log(chalk2.dim(` Project: ${projectId}`));
142
- console.log(chalk2.dim(` API: ${API_URL}
52
+ const environment = options.env || "development";
53
+ console.log(chalk.bold("\n\u26A1 Deploying to Tether\n"));
54
+ console.log(chalk.dim(` Project: ${projectId}`));
55
+ console.log(chalk.dim(` Environment: ${environment}`));
56
+ console.log(chalk.dim(` API: ${API_URL}
143
57
  `));
144
58
  const config = await loadConfig();
145
59
  const deploySchema = options.schema || !options.schema && !options.functions;
146
60
  const deployFunctions = options.functions || !options.schema && !options.functions;
147
61
  if (deploySchema) {
148
62
  const schemaPath = resolvePath(config.schema);
149
- await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, options.dryRun);
63
+ await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, environment, options.dryRun);
150
64
  }
151
65
  if (deployFunctions) {
152
66
  const functionsDir = resolvePath(config.functions);
153
- await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, options.dryRun);
67
+ await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, environment, options.dryRun);
154
68
  }
155
- console.log(chalk2.green("\n\u2713 Deployment complete\n"));
69
+ console.log(chalk.green("\n\u2713 Deployment complete\n"));
156
70
  }
157
- async function deploySchemaToServer(projectId, token, schemaPath, dryRun) {
71
+ async function deploySchemaToServer(projectId, token, schemaPath, environment, dryRun) {
158
72
  const spinner = ora("Reading schema...").start();
159
73
  try {
160
- if (!await fs3.pathExists(schemaPath)) {
74
+ if (!await fs.pathExists(schemaPath)) {
161
75
  spinner.warn("No schema file found");
162
- const relativePath = path3.relative(process.cwd(), schemaPath);
163
- console.log(chalk2.dim(` Create ${relativePath} to define your database schema
76
+ const relativePath = path.relative(process.cwd(), schemaPath);
77
+ console.log(chalk.dim(` Create ${relativePath} to define your database schema
164
78
  `));
165
79
  return;
166
80
  }
167
- const schemaSource = await fs3.readFile(schemaPath, "utf-8");
81
+ const schemaSource = await fs.readFile(schemaPath, "utf-8");
168
82
  const tables = parseSchema(schemaSource);
169
83
  spinner.text = `Found ${tables.length} table(s)`;
170
84
  const sql = generateSchemaSQL(tables);
171
85
  if (dryRun) {
172
86
  spinner.info("Dry run - would deploy schema:");
173
87
  for (const table of tables) {
174
- console.log(chalk2.dim(` - ${table.name} (${Object.keys(table.columns).length} columns)`));
88
+ console.log(chalk.dim(` - ${table.name} (${Object.keys(table.columns).length} columns)`));
175
89
  }
176
- console.log(chalk2.bold("\nGenerated SQL:\n"));
177
- console.log(chalk2.cyan(sql));
90
+ console.log(chalk.bold("\nGenerated SQL:\n"));
91
+ console.log(chalk.cyan(sql));
178
92
  return;
179
93
  }
180
94
  spinner.text = "Deploying schema...";
181
- const response = await fetch(`${API_URL}/projects/${projectId}/deploy/schema`, {
95
+ const envPath = environment !== "production" ? `/env/${environment}` : "";
96
+ const response = await fetch(`${API_URL}/projects/${projectId}${envPath}/deploy/schema`, {
182
97
  method: "POST",
183
98
  headers: {
184
99
  "Content-Type": "application/json",
@@ -200,20 +115,20 @@ async function deploySchemaToServer(projectId, token, schemaPath, dryRun) {
200
115
  spinner.succeed(`Schema deployed (${tables.length} table(s))`);
201
116
  } catch (error) {
202
117
  spinner.fail("Failed to deploy schema");
203
- console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
118
+ console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
204
119
  }
205
120
  }
206
- async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
121
+ async function deployFunctionsToServer(projectId, token, functionsDir, environment, dryRun) {
207
122
  const spinner = ora("Reading functions...").start();
208
123
  try {
209
- if (!await fs3.pathExists(functionsDir)) {
124
+ if (!await fs.pathExists(functionsDir)) {
210
125
  spinner.warn("No functions directory found");
211
- const relativePath = path3.relative(process.cwd(), functionsDir);
212
- console.log(chalk2.dim(` Create ${relativePath}/ to define your API functions
126
+ const relativePath = path.relative(process.cwd(), functionsDir);
127
+ console.log(chalk.dim(` Create ${relativePath}/ to define your API functions
213
128
  `));
214
129
  return;
215
130
  }
216
- const files = await fs3.readdir(functionsDir);
131
+ const files = await fs.readdir(functionsDir);
217
132
  const tsFiles = files.filter((f) => f.endsWith(".ts"));
218
133
  if (tsFiles.length === 0) {
219
134
  spinner.info("No function files found");
@@ -221,8 +136,8 @@ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
221
136
  }
222
137
  const functions = [];
223
138
  for (const file of tsFiles) {
224
- const filePath = path3.join(functionsDir, file);
225
- const source = await fs3.readFile(filePath, "utf-8");
139
+ const filePath = path.join(functionsDir, file);
140
+ const source = await fs.readFile(filePath, "utf-8");
226
141
  const moduleName = file.replace(".ts", "");
227
142
  const parsedFunctions = parseFunctions(moduleName, source);
228
143
  functions.push(...parsedFunctions);
@@ -232,12 +147,13 @@ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
232
147
  spinner.info("Dry run - would deploy functions:");
233
148
  for (const fn of functions) {
234
149
  const icon = fn.type === "query" ? "\u{1F50D}" : fn.type === "mutation" ? "\u270F\uFE0F" : "\u26A1";
235
- console.log(chalk2.dim(` ${icon} ${fn.name} (${fn.type})`));
150
+ console.log(chalk.dim(` ${icon} ${fn.name} (${fn.type})`));
236
151
  }
237
152
  return;
238
153
  }
239
154
  spinner.text = "Deploying functions...";
240
- const response = await fetch(`${API_URL}/projects/${projectId}/deploy/functions`, {
155
+ const envPath = environment !== "production" ? `/env/${environment}` : "";
156
+ const response = await fetch(`${API_URL}/projects/${projectId}${envPath}/deploy/functions`, {
241
157
  method: "POST",
242
158
  headers: {
243
159
  "Content-Type": "application/json",
@@ -260,29 +176,29 @@ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
260
176
  const mutations = functions.filter((f) => f.type === "mutation");
261
177
  const actions = functions.filter((f) => f.type === "action");
262
178
  if (queries.length > 0) {
263
- console.log(chalk2.dim(`
179
+ console.log(chalk.dim(`
264
180
  Queries:`));
265
181
  for (const fn of queries) {
266
- console.log(chalk2.dim(` - ${fn.name}`));
182
+ console.log(chalk.dim(` - ${fn.name}`));
267
183
  }
268
184
  }
269
185
  if (mutations.length > 0) {
270
- console.log(chalk2.dim(`
186
+ console.log(chalk.dim(`
271
187
  Mutations:`));
272
188
  for (const fn of mutations) {
273
- console.log(chalk2.dim(` - ${fn.name}`));
189
+ console.log(chalk.dim(` - ${fn.name}`));
274
190
  }
275
191
  }
276
192
  if (actions.length > 0) {
277
- console.log(chalk2.dim(`
193
+ console.log(chalk.dim(`
278
194
  Actions:`));
279
195
  for (const fn of actions) {
280
- console.log(chalk2.dim(` - ${fn.name}`));
196
+ console.log(chalk.dim(` - ${fn.name}`));
281
197
  }
282
198
  }
283
199
  } catch (error) {
284
200
  spinner.fail("Failed to deploy functions");
285
- console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
201
+ console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
286
202
  }
287
203
  }
288
204
  function parseSchema(source) {
@@ -323,15 +239,42 @@ function parseSchema(source) {
323
239
  const colName = colMatch[1];
324
240
  const colType = colMatch[2];
325
241
  const modifiers = colMatch[3];
242
+ if (colName === "_id" || colName === "_createdAt") {
243
+ console.warn(`Warning: Column '${colName}' is a reserved system column and will be ignored.`);
244
+ continue;
245
+ }
246
+ const userWantsPrimaryKey = modifiers.includes(".primaryKey()");
247
+ if (userWantsPrimaryKey) {
248
+ console.warn(`Warning: Column '${colName}' has .primaryKey() which will be ignored. Tether uses '_id' as the primary key.`);
249
+ }
326
250
  columns[colName] = {
327
251
  type: colType,
328
- primaryKey: modifiers.includes(".primaryKey()"),
252
+ primaryKey: false,
253
+ // _id is always the primary key, ignore user-defined primary keys
329
254
  notNull: modifiers.includes(".notNull()"),
330
- unique: modifiers.includes(".unique()"),
255
+ unique: modifiers.includes(".unique()") || userWantsPrimaryKey,
256
+ // Make it unique if they wanted PK
331
257
  hasDefault: modifiers.includes(".default("),
332
258
  references: modifiers.match(/\.references\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1]
333
259
  };
334
260
  }
261
+ columns["_id"] = {
262
+ type: "text",
263
+ primaryKey: true,
264
+ notNull: true,
265
+ unique: true,
266
+ // Also set unique so ALTER TABLE adds UNIQUE constraint
267
+ hasDefault: false,
268
+ isSystemColumn: true
269
+ };
270
+ columns["_createdAt"] = {
271
+ type: "timestamp",
272
+ primaryKey: false,
273
+ notNull: true,
274
+ unique: false,
275
+ hasDefault: true,
276
+ isSystemColumn: true
277
+ };
335
278
  tables.push({ name: tableName, columns, source: tableSource });
336
279
  }
337
280
  return tables;
@@ -359,9 +302,10 @@ function getColumnSqlType(type) {
359
302
  function buildColumnSql(colName, def, forAlterTable = false) {
360
303
  const sqlType = getColumnSqlType(def.type);
361
304
  let colSql = `${colName} ${sqlType}`;
362
- if (def.primaryKey) colSql += " PRIMARY KEY";
305
+ const addingPrimaryKey = def.primaryKey && !forAlterTable;
306
+ if (addingPrimaryKey) colSql += " PRIMARY KEY";
363
307
  if (def.notNull && !forAlterTable) colSql += " NOT NULL";
364
- if (def.unique) colSql += " UNIQUE";
308
+ if (def.unique && !addingPrimaryKey) colSql += " UNIQUE";
365
309
  if (def.hasDefault && def.type === "timestamp") {
366
310
  colSql += " DEFAULT (datetime('now'))";
367
311
  }
@@ -386,7 +330,6 @@ function generateSchemaSQL(tables) {
386
330
  );
387
331
  for (const [colName, colDef] of Object.entries(table.columns)) {
388
332
  const def = colDef;
389
- if (def.primaryKey) continue;
390
333
  const alterColSql = buildColumnSql(colName, def, true);
391
334
  statements.push(
392
335
  `-- Add column if missing (will error if exists, which is OK)
@@ -451,7 +394,7 @@ function isPackageManagerInstalled(pm) {
451
394
  }
452
395
  async function initCommand(name, options) {
453
396
  const credentials = await requireAuth();
454
- console.log(chalk3.bold("\n\u26A1 Create a new Tether project\n"));
397
+ console.log(chalk2.bold("\n\u26A1 Create a new Tether project\n"));
455
398
  let projectName = name;
456
399
  if (!projectName) {
457
400
  const response = await prompts({
@@ -462,7 +405,7 @@ async function initCommand(name, options) {
462
405
  });
463
406
  projectName = response.name;
464
407
  if (!projectName) {
465
- console.log(chalk3.red("Project name is required"));
408
+ console.log(chalk2.red("Project name is required"));
466
409
  process.exit(1);
467
410
  }
468
411
  }
@@ -480,9 +423,9 @@ async function initCommand(name, options) {
480
423
  });
481
424
  const packageManager = pmResponse.packageManager || "npm";
482
425
  if (!isPackageManagerInstalled(packageManager)) {
483
- console.log(chalk3.red(`
426
+ console.log(chalk2.red(`
484
427
  ${packageManager} is not installed on your system.`));
485
- console.log(chalk3.dim(`Please install ${packageManager} and try again.
428
+ console.log(chalk2.dim(`Please install ${packageManager} and try again.
486
429
  `));
487
430
  process.exit(1);
488
431
  }
@@ -502,8 +445,8 @@ ${packageManager} is not installed on your system.`));
502
445
  });
503
446
  template = response.template || "nuxt";
504
447
  }
505
- const projectPath = path4.resolve(process.cwd(), projectName);
506
- if (await fs4.pathExists(projectPath)) {
448
+ const projectPath = path2.resolve(process.cwd(), projectName);
449
+ if (await fs2.pathExists(projectPath)) {
507
450
  const { overwrite } = await prompts({
508
451
  type: "confirm",
509
452
  name: "overwrite",
@@ -511,10 +454,10 @@ ${packageManager} is not installed on your system.`));
511
454
  initial: false
512
455
  });
513
456
  if (!overwrite) {
514
- console.log(chalk3.yellow("Cancelled"));
457
+ console.log(chalk2.yellow("Cancelled"));
515
458
  process.exit(0);
516
459
  }
517
- await fs4.remove(projectPath);
460
+ await fs2.remove(projectPath);
518
461
  }
519
462
  const spinner = ora2(`Scaffolding ${template} project...`).start();
520
463
  let projectId;
@@ -562,17 +505,17 @@ ${packageManager} is not installed on your system.`));
562
505
  } finally {
563
506
  process.chdir(originalCwd);
564
507
  }
565
- console.log("\n" + chalk3.green("\u2713") + " Project created successfully!\n");
508
+ console.log("\n" + chalk2.green("\u2713") + " Project created successfully!\n");
566
509
  console.log("Next steps:\n");
567
- console.log(chalk3.cyan(` cd ${projectName}`));
510
+ console.log(chalk2.cyan(` cd ${projectName}`));
568
511
  if (template === "vanilla") {
569
- console.log(chalk3.cyan(` ${packageManager} install`));
512
+ console.log(chalk2.cyan(` ${packageManager} install`));
570
513
  }
571
- console.log(chalk3.cyan(` ${packageManager} run dev`));
572
- console.log("\n" + chalk3.dim("For more information, visit https://tthr.io/docs\n"));
514
+ console.log(chalk2.cyan(` ${packageManager} run dev`));
515
+ console.log("\n" + chalk2.dim("For more information, visit https://tthr.io/docs\n"));
573
516
  } catch (error) {
574
517
  spinner.fail("Failed to create project");
575
- console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
518
+ console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
576
519
  process.exit(1);
577
520
  }
578
521
  }
@@ -589,16 +532,16 @@ function getPackageRunnerCommand(pm) {
589
532
  }
590
533
  }
591
534
  async function scaffoldNuxtProject(projectName, projectPath, spinner, packageManager) {
592
- const parentDir = path4.dirname(projectPath);
535
+ const parentDir = path2.dirname(projectPath);
593
536
  const runner = getPackageRunnerCommand(packageManager);
594
537
  spinner.stop();
595
- console.log(chalk3.dim("\nRunning nuxi init...\n"));
538
+ console.log(chalk2.dim("\nRunning nuxi init...\n"));
596
539
  try {
597
540
  execSync(`${runner} nuxi@latest init ${projectName} -t minimal --packageManager ${packageManager} --gitInit false`, {
598
541
  cwd: parentDir,
599
542
  stdio: "inherit"
600
543
  });
601
- if (!await fs4.pathExists(path4.join(projectPath, "package.json"))) {
544
+ if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
602
545
  throw new Error("Nuxt project was not created successfully - package.json missing");
603
546
  }
604
547
  spinner.start();
@@ -611,17 +554,17 @@ async function scaffoldNuxtProject(projectName, projectPath, spinner, packageMan
611
554
  }
612
555
  }
613
556
  async function scaffoldNextProject(projectName, projectPath, spinner, packageManager) {
614
- const parentDir = path4.dirname(projectPath);
557
+ const parentDir = path2.dirname(projectPath);
615
558
  const runner = getPackageRunnerCommand(packageManager);
616
559
  const pmFlag = packageManager === "npm" ? "--use-npm" : packageManager === "pnpm" ? "--use-pnpm" : packageManager === "yarn" ? "--use-yarn" : "--use-bun";
617
560
  spinner.stop();
618
- console.log(chalk3.dim("\nRunning create-next-app...\n"));
561
+ console.log(chalk2.dim("\nRunning create-next-app...\n"));
619
562
  try {
620
563
  execSync(`${runner} create-next-app@latest ${projectName} --typescript --eslint --tailwind --src-dir --app --import-alias "@/*" ${pmFlag}`, {
621
564
  cwd: parentDir,
622
565
  stdio: "inherit"
623
566
  });
624
- if (!await fs4.pathExists(path4.join(projectPath, "package.json"))) {
567
+ if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
625
568
  throw new Error("Next.js project was not created successfully - package.json missing");
626
569
  }
627
570
  spinner.start();
@@ -634,16 +577,16 @@ async function scaffoldNextProject(projectName, projectPath, spinner, packageMan
634
577
  }
635
578
  }
636
579
  async function scaffoldSvelteKitProject(projectName, projectPath, spinner, packageManager) {
637
- const parentDir = path4.dirname(projectPath);
580
+ const parentDir = path2.dirname(projectPath);
638
581
  const runner = getPackageRunnerCommand(packageManager);
639
582
  spinner.stop();
640
- console.log(chalk3.dim("\nRunning sv create...\n"));
583
+ console.log(chalk2.dim("\nRunning sv create...\n"));
641
584
  try {
642
585
  execSync(`${runner} sv create ${projectName} --template minimal --types ts --no-add-ons --no-install`, {
643
586
  cwd: parentDir,
644
587
  stdio: "inherit"
645
588
  });
646
- if (!await fs4.pathExists(path4.join(projectPath, "package.json"))) {
589
+ if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
647
590
  throw new Error("SvelteKit project was not created successfully - package.json missing");
648
591
  }
649
592
  spinner.start();
@@ -662,8 +605,8 @@ async function scaffoldSvelteKitProject(projectName, projectPath, spinner, packa
662
605
  }
663
606
  async function scaffoldVanillaProject(projectName, projectPath, spinner) {
664
607
  spinner.text = "Creating vanilla TypeScript project...";
665
- await fs4.ensureDir(projectPath);
666
- await fs4.ensureDir(path4.join(projectPath, "src"));
608
+ await fs2.ensureDir(projectPath);
609
+ await fs2.ensureDir(path2.join(projectPath, "src"));
667
610
  const packageJson = {
668
611
  name: projectName,
669
612
  version: "0.0.1",
@@ -680,9 +623,9 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
680
623
  "@types/node": "latest"
681
624
  }
682
625
  };
683
- await fs4.writeJSON(path4.join(projectPath, "package.json"), packageJson, { spaces: 2 });
684
- await fs4.writeJSON(
685
- path4.join(projectPath, "tsconfig.json"),
626
+ await fs2.writeJSON(path2.join(projectPath, "package.json"), packageJson, { spaces: 2 });
627
+ await fs2.writeJSON(
628
+ path2.join(projectPath, "tsconfig.json"),
686
629
  {
687
630
  compilerOptions: {
688
631
  target: "ES2022",
@@ -703,8 +646,8 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
703
646
  },
704
647
  { spaces: 2 }
705
648
  );
706
- await fs4.writeFile(
707
- path4.join(projectPath, "src", "index.ts"),
649
+ await fs2.writeFile(
650
+ path2.join(projectPath, "src", "index.ts"),
708
651
  `import { createClient } from '@tthr/client';
709
652
 
710
653
  const tether = createClient({
@@ -723,8 +666,8 @@ async function main() {
723
666
  main().catch(console.error);
724
667
  `
725
668
  );
726
- await fs4.writeFile(
727
- path4.join(projectPath, ".gitignore"),
669
+ await fs2.writeFile(
670
+ path2.join(projectPath, ".gitignore"),
728
671
  `node_modules/
729
672
  dist/
730
673
  .env
@@ -734,11 +677,11 @@ dist/
734
677
  );
735
678
  }
736
679
  async function addTetherFiles(projectPath, projectId, apiKey, template) {
737
- await fs4.ensureDir(path4.join(projectPath, "tether"));
738
- await fs4.ensureDir(path4.join(projectPath, "tether", "functions"));
680
+ await fs2.ensureDir(path2.join(projectPath, "tether"));
681
+ await fs2.ensureDir(path2.join(projectPath, "tether", "functions"));
739
682
  const configPackage = template === "nuxt" ? "@tthr/vue" : template === "next" ? "@tthr/react" : template === "sveltekit" ? "@tthr/svelte" : "@tthr/client";
740
- await fs4.writeFile(
741
- path4.join(projectPath, "tether.config.ts"),
683
+ await fs2.writeFile(
684
+ path2.join(projectPath, "tether.config.ts"),
742
685
  `import { defineConfig } from '${configPackage}';
743
686
 
744
687
  export default defineConfig({
@@ -759,8 +702,8 @@ export default defineConfig({
759
702
  });
760
703
  `
761
704
  );
762
- await fs4.writeFile(
763
- path4.join(projectPath, "tether", "schema.ts"),
705
+ await fs2.writeFile(
706
+ path2.join(projectPath, "tether", "schema.ts"),
764
707
  `import { defineSchema, text, integer, timestamp } from '@tthr/schema';
765
708
 
766
709
  export default defineSchema({
@@ -785,8 +728,8 @@ export default defineSchema({
785
728
  });
786
729
  `
787
730
  );
788
- await fs4.writeFile(
789
- path4.join(projectPath, "tether", "functions", "posts.ts"),
731
+ await fs2.writeFile(
732
+ path2.join(projectPath, "tether", "functions", "posts.ts"),
790
733
  `import { query, mutation, z } from '@tthr/server';
791
734
 
792
735
  // List all posts
@@ -871,8 +814,8 @@ export const remove = mutation({
871
814
  });
872
815
  `
873
816
  );
874
- await fs4.writeFile(
875
- path4.join(projectPath, "tether", "functions", "index.ts"),
817
+ await fs2.writeFile(
818
+ path2.join(projectPath, "tether", "functions", "index.ts"),
876
819
  `// Re-export all function modules
877
820
  // The Tether SDK uses this file to discover custom functions
878
821
 
@@ -880,8 +823,8 @@ export * as posts from './posts';
880
823
  export * as comments from './comments';
881
824
  `
882
825
  );
883
- await fs4.writeFile(
884
- path4.join(projectPath, "tether", "functions", "comments.ts"),
826
+ await fs2.writeFile(
827
+ path2.join(projectPath, "tether", "functions", "comments.ts"),
885
828
  `import { query, mutation, z } from '@tthr/server';
886
829
 
887
830
  // List all comments
@@ -953,27 +896,27 @@ export const remove = mutation({
953
896
  TETHER_PROJECT_ID=${projectId}
954
897
  TETHER_API_KEY=${apiKey}
955
898
  `;
956
- const envPath = path4.join(projectPath, ".env");
957
- if (await fs4.pathExists(envPath)) {
958
- const existing = await fs4.readFile(envPath, "utf-8");
959
- await fs4.writeFile(envPath, existing + "\n" + envContent);
899
+ const envPath = path2.join(projectPath, ".env");
900
+ if (await fs2.pathExists(envPath)) {
901
+ const existing = await fs2.readFile(envPath, "utf-8");
902
+ await fs2.writeFile(envPath, existing + "\n" + envContent);
960
903
  } else {
961
- await fs4.writeFile(envPath, envContent);
904
+ await fs2.writeFile(envPath, envContent);
962
905
  }
963
- const gitignorePath = path4.join(projectPath, ".gitignore");
906
+ const gitignorePath = path2.join(projectPath, ".gitignore");
964
907
  const tetherGitignore = `
965
908
  # Tether
966
909
  _generated/
967
910
  .env
968
911
  .env.local
969
912
  `;
970
- if (await fs4.pathExists(gitignorePath)) {
971
- const existing = await fs4.readFile(gitignorePath, "utf-8");
913
+ if (await fs2.pathExists(gitignorePath)) {
914
+ const existing = await fs2.readFile(gitignorePath, "utf-8");
972
915
  if (!existing.includes("_generated/")) {
973
- await fs4.writeFile(gitignorePath, existing + tetherGitignore);
916
+ await fs2.writeFile(gitignorePath, existing + tetherGitignore);
974
917
  }
975
918
  } else {
976
- await fs4.writeFile(gitignorePath, tetherGitignore.trim());
919
+ await fs2.writeFile(gitignorePath, tetherGitignore.trim());
977
920
  }
978
921
  }
979
922
  async function configureFramework(projectPath, template) {
@@ -986,9 +929,9 @@ async function configureFramework(projectPath, template) {
986
929
  }
987
930
  }
988
931
  async function configureNuxt(projectPath) {
989
- const configPath = path4.join(projectPath, "nuxt.config.ts");
990
- if (!await fs4.pathExists(configPath)) {
991
- await fs4.writeFile(
932
+ const configPath = path2.join(projectPath, "nuxt.config.ts");
933
+ if (!await fs2.pathExists(configPath)) {
934
+ await fs2.writeFile(
992
935
  configPath,
993
936
  `// https://nuxt.com/docs/api/configuration/nuxt-config
994
937
  export default defineNuxtConfig({
@@ -1006,7 +949,7 @@ export default defineNuxtConfig({
1006
949
  );
1007
950
  return;
1008
951
  }
1009
- let config = await fs4.readFile(configPath, "utf-8");
952
+ let config = await fs2.readFile(configPath, "utf-8");
1010
953
  if (config.includes("modules:")) {
1011
954
  config = config.replace(
1012
955
  /modules:\s*\[/,
@@ -1033,11 +976,11 @@ export default defineNuxtConfig({
1033
976
  `
1034
977
  );
1035
978
  }
1036
- await fs4.writeFile(configPath, config);
979
+ await fs2.writeFile(configPath, config);
1037
980
  }
1038
981
  async function configureNext(projectPath) {
1039
- const providersPath = path4.join(projectPath, "src", "app", "providers.tsx");
1040
- await fs4.writeFile(
982
+ const providersPath = path2.join(projectPath, "src", "app", "providers.tsx");
983
+ await fs2.writeFile(
1041
984
  providersPath,
1042
985
  `'use client';
1043
986
 
@@ -1055,9 +998,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
1055
998
  }
1056
999
  `
1057
1000
  );
1058
- const layoutPath = path4.join(projectPath, "src", "app", "layout.tsx");
1059
- if (await fs4.pathExists(layoutPath)) {
1060
- let layout = await fs4.readFile(layoutPath, "utf-8");
1001
+ const layoutPath = path2.join(projectPath, "src", "app", "layout.tsx");
1002
+ if (await fs2.pathExists(layoutPath)) {
1003
+ let layout = await fs2.readFile(layoutPath, "utf-8");
1061
1004
  if (!layout.includes("import { Providers }")) {
1062
1005
  layout = layout.replace(
1063
1006
  /^(import.*\n)+/m,
@@ -1070,24 +1013,24 @@ export function Providers({ children }: { children: React.ReactNode }) {
1070
1013
  "$1<Providers>$2</Providers>$3"
1071
1014
  );
1072
1015
  }
1073
- await fs4.writeFile(layoutPath, layout);
1016
+ await fs2.writeFile(layoutPath, layout);
1074
1017
  }
1075
- const envLocalPath = path4.join(projectPath, ".env.local");
1018
+ const envLocalPath = path2.join(projectPath, ".env.local");
1076
1019
  const nextEnvContent = `# Tether Configuration (client-side)
1077
1020
  NEXT_PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
1078
1021
  `;
1079
- if (await fs4.pathExists(envLocalPath)) {
1080
- const existing = await fs4.readFile(envLocalPath, "utf-8");
1081
- await fs4.writeFile(envLocalPath, existing + "\n" + nextEnvContent);
1022
+ if (await fs2.pathExists(envLocalPath)) {
1023
+ const existing = await fs2.readFile(envLocalPath, "utf-8");
1024
+ await fs2.writeFile(envLocalPath, existing + "\n" + nextEnvContent);
1082
1025
  } else {
1083
- await fs4.writeFile(envLocalPath, nextEnvContent);
1026
+ await fs2.writeFile(envLocalPath, nextEnvContent);
1084
1027
  }
1085
1028
  }
1086
1029
  async function configureSvelteKit(projectPath) {
1087
- const libPath = path4.join(projectPath, "src", "lib");
1088
- await fs4.ensureDir(libPath);
1089
- await fs4.writeFile(
1090
- path4.join(libPath, "tether.ts"),
1030
+ const libPath = path2.join(projectPath, "src", "lib");
1031
+ await fs2.ensureDir(libPath);
1032
+ await fs2.writeFile(
1033
+ path2.join(libPath, "tether.ts"),
1091
1034
  `import { createClient } from '@tthr/svelte';
1092
1035
  import { PUBLIC_TETHER_PROJECT_ID, PUBLIC_TETHER_URL } from '$env/static/public';
1093
1036
 
@@ -1097,19 +1040,19 @@ export const tether = createClient({
1097
1040
  });
1098
1041
  `
1099
1042
  );
1100
- const envPath = path4.join(projectPath, ".env");
1043
+ const envPath = path2.join(projectPath, ".env");
1101
1044
  const svelteEnvContent = `# Tether Configuration (public)
1102
1045
  PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
1103
1046
  `;
1104
- if (await fs4.pathExists(envPath)) {
1105
- const existing = await fs4.readFile(envPath, "utf-8");
1047
+ if (await fs2.pathExists(envPath)) {
1048
+ const existing = await fs2.readFile(envPath, "utf-8");
1106
1049
  if (!existing.includes("PUBLIC_TETHER_")) {
1107
- await fs4.writeFile(envPath, existing + "\n" + svelteEnvContent);
1050
+ await fs2.writeFile(envPath, existing + "\n" + svelteEnvContent);
1108
1051
  }
1109
1052
  }
1110
1053
  }
1111
- function getInstallCommand(pm, isDev5 = false) {
1112
- const devFlag = isDev5 ? pm === "npm" ? "-D" : pm === "yarn" ? "-D" : pm === "pnpm" ? "-D" : "-d" : "";
1054
+ function getInstallCommand(pm, isDev6 = false) {
1055
+ const devFlag = isDev6 ? pm === "npm" ? "-D" : pm === "yarn" ? "-D" : pm === "pnpm" ? "-D" : "-d" : "";
1113
1056
  return `${pm} ${pm === "npm" ? "install" : "add"} ${devFlag}`.trim();
1114
1057
  }
1115
1058
  async function installTetherPackages(projectPath, template, packageManager) {
@@ -1154,15 +1097,15 @@ async function installTetherPackages(projectPath, template, packageManager) {
1154
1097
  stdio: "pipe"
1155
1098
  });
1156
1099
  } catch (error) {
1157
- console.warn(chalk3.yellow("\nNote: Some Tether packages may not be published yet."));
1100
+ console.warn(chalk2.yellow("\nNote: Some Tether packages may not be published yet."));
1158
1101
  }
1159
1102
  }
1160
1103
  async function createDemoPage(projectPath, template) {
1161
1104
  if (template === "nuxt") {
1162
- const nuxt4AppVuePath = path4.join(projectPath, "app", "app.vue");
1163
- const nuxt3AppVuePath = path4.join(projectPath, "app.vue");
1164
- const appVuePath = await fs4.pathExists(path4.join(projectPath, "app")) ? nuxt4AppVuePath : nuxt3AppVuePath;
1165
- await fs4.writeFile(
1105
+ const nuxt4AppVuePath = path2.join(projectPath, "app", "app.vue");
1106
+ const nuxt3AppVuePath = path2.join(projectPath, "app.vue");
1107
+ const appVuePath = await fs2.pathExists(path2.join(projectPath, "app")) ? nuxt4AppVuePath : nuxt3AppVuePath;
1108
+ await fs2.writeFile(
1166
1109
  appVuePath,
1167
1110
  `<template>
1168
1111
  <TetherWelcome />
@@ -1170,8 +1113,8 @@ async function createDemoPage(projectPath, template) {
1170
1113
  `
1171
1114
  );
1172
1115
  } else if (template === "next") {
1173
- const pagePath = path4.join(projectPath, "src", "app", "page.tsx");
1174
- await fs4.writeFile(
1116
+ const pagePath = path2.join(projectPath, "src", "app", "page.tsx");
1117
+ await fs2.writeFile(
1175
1118
  pagePath,
1176
1119
  `'use client';
1177
1120
 
@@ -1281,8 +1224,8 @@ export default function Home() {
1281
1224
  `
1282
1225
  );
1283
1226
  } else if (template === "sveltekit") {
1284
- const pagePath = path4.join(projectPath, "src", "routes", "+page.svelte");
1285
- await fs4.writeFile(
1227
+ const pagePath = path2.join(projectPath, "src", "routes", "+page.svelte");
1228
+ await fs2.writeFile(
1286
1229
  pagePath,
1287
1230
  `<script lang="ts">
1288
1231
  import { onMount } from 'svelte';
@@ -1548,369 +1491,340 @@ export default function Home() {
1548
1491
  }
1549
1492
 
1550
1493
  // src/commands/dev.ts
1551
- import chalk4 from "chalk";
1494
+ import chalk3 from "chalk";
1552
1495
  import ora3 from "ora";
1553
- import fs5 from "fs-extra";
1554
- import path5 from "path";
1555
- async function devCommand(options) {
1556
- await requireAuth();
1557
- const configPath = path5.resolve(process.cwd(), "tether.config.ts");
1558
- if (!await fs5.pathExists(configPath)) {
1559
- console.log(chalk4.red("\nError: Not a Tether project"));
1560
- console.log(chalk4.dim("Run `tthr init` to create a new project\n"));
1561
- process.exit(1);
1496
+ import fs3 from "fs-extra";
1497
+ import path3 from "path";
1498
+ import { spawn } from "child_process";
1499
+ import { watch } from "chokidar";
1500
+ var frameworkProcess = null;
1501
+ var schemaWatcher = null;
1502
+ var functionsWatcher = null;
1503
+ var isGenerating = false;
1504
+ var pendingGenerate = false;
1505
+ async function runGenerate(spinner) {
1506
+ if (isGenerating) {
1507
+ pendingGenerate = true;
1508
+ return true;
1562
1509
  }
1563
- const config = await loadConfig();
1564
- const port = options.port || String(config.dev?.port || 3001);
1565
- console.log(chalk4.bold("\n\u26A1 Starting Tether development server\n"));
1566
- const spinner = ora3("Starting server...").start();
1510
+ isGenerating = true;
1567
1511
  try {
1568
- spinner.succeed(`Development server running on port ${port}`);
1569
- console.log("\n" + chalk4.cyan(` Local: http://localhost:${port}`));
1570
- console.log(chalk4.cyan(` WebSocket: ws://localhost:${port}/ws
1571
- `));
1572
- console.log(chalk4.dim(" Press Ctrl+C to stop\n"));
1573
- process.on("SIGINT", () => {
1574
- console.log(chalk4.yellow("\n\nShutting down...\n"));
1575
- process.exit(0);
1576
- });
1577
- await new Promise(() => {
1578
- });
1512
+ const { generateTypes } = await import("./generate-YQ4QRPXF.js");
1513
+ spinner.text = "Regenerating types...";
1514
+ await generateTypes({ silent: true });
1515
+ spinner.succeed("Types regenerated");
1516
+ spinner.start("Watching for changes...");
1517
+ return true;
1579
1518
  } catch (error) {
1580
- spinner.fail("Failed to start server");
1581
- console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
1582
- process.exit(1);
1519
+ spinner.fail("Type generation failed");
1520
+ console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
1521
+ spinner.start("Watching for changes...");
1522
+ return false;
1523
+ } finally {
1524
+ isGenerating = false;
1525
+ if (pendingGenerate) {
1526
+ pendingGenerate = false;
1527
+ setTimeout(() => runGenerate(spinner), 100);
1528
+ }
1583
1529
  }
1584
1530
  }
1585
-
1586
- // src/commands/generate.ts
1587
- import chalk5 from "chalk";
1588
- import ora4 from "ora";
1589
- import fs6 from "fs-extra";
1590
- import path6 from "path";
1591
- function parseSchemaFile(source) {
1592
- const tables = [];
1593
- const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
1594
- if (!schemaMatch) return tables;
1595
- const schemaContent = schemaMatch[1];
1596
- const tableStartRegex = /(\w+)\s*:\s*\{/g;
1597
- let match;
1598
- while ((match = tableStartRegex.exec(schemaContent)) !== null) {
1599
- const tableName = match[1];
1600
- const startOffset = match.index + match[0].length;
1601
- let braceCount = 1;
1602
- let endOffset = startOffset;
1603
- for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
1604
- const char = schemaContent[i];
1531
+ async function validateSchema(schemaPath) {
1532
+ try {
1533
+ const content = await fs3.readFile(schemaPath, "utf-8");
1534
+ if (!content.includes("defineSchema")) {
1535
+ return { valid: false, error: "Missing defineSchema() call" };
1536
+ }
1537
+ let braceCount = 0;
1538
+ for (const char of content) {
1605
1539
  if (char === "{") braceCount++;
1606
1540
  else if (char === "}") braceCount--;
1607
- endOffset = i;
1541
+ if (braceCount < 0) {
1542
+ return { valid: false, error: "Unmatched closing brace" };
1543
+ }
1608
1544
  }
1609
- const columnsContent = schemaContent.slice(startOffset, endOffset);
1610
- const columns = parseColumns(columnsContent);
1611
- tables.push({ name: tableName, columns });
1612
- }
1613
- return tables;
1614
- }
1615
- function parseColumns(content) {
1616
- const columns = [];
1617
- const columnRegex = /(\w+)\s*:\s*(\w+)\s*\(\s*\)([^,\n}]*)/g;
1618
- let match;
1619
- while ((match = columnRegex.exec(content)) !== null) {
1620
- const name = match[1];
1621
- const schemaType = match[2];
1622
- const modifiers = match[3] || "";
1623
- const nullable = !modifiers.includes(".notNull()") && !modifiers.includes(".primaryKey()");
1624
- const primaryKey = modifiers.includes(".primaryKey()");
1625
- columns.push({
1626
- name,
1627
- type: schemaTypeToTS(schemaType),
1628
- nullable,
1629
- primaryKey
1630
- });
1631
- }
1632
- return columns;
1633
- }
1634
- function schemaTypeToTS(schemaType) {
1635
- switch (schemaType) {
1636
- case "text":
1637
- return "string";
1638
- case "integer":
1639
- return "number";
1640
- case "real":
1641
- return "number";
1642
- case "boolean":
1643
- return "boolean";
1644
- case "timestamp":
1645
- return "string";
1646
- case "json":
1647
- return "unknown";
1648
- case "blob":
1649
- return "Uint8Array";
1650
- default:
1651
- return "unknown";
1652
- }
1653
- }
1654
- function tableNameToInterface(tableName) {
1655
- let name = tableName;
1656
- if (name.endsWith("ies")) {
1657
- name = name.slice(0, -3) + "y";
1658
- } else if (name.endsWith("s") && !name.endsWith("ss")) {
1659
- name = name.slice(0, -1);
1660
- }
1661
- return name.charAt(0).toUpperCase() + name.slice(1);
1662
- }
1663
- function generateDbFile(tables) {
1664
- const lines = [
1665
- "// Auto-generated by Tether CLI - do not edit manually",
1666
- `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1667
- "",
1668
- "import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
1669
- ""
1670
- ];
1671
- for (const table of tables) {
1672
- const interfaceName = tableNameToInterface(table.name);
1673
- lines.push(`export interface ${interfaceName} {`);
1674
- for (const col of table.columns) {
1675
- const typeStr = col.nullable ? `${col.type} | null` : col.type;
1676
- lines.push(` ${col.name}: ${typeStr};`);
1545
+ if (braceCount !== 0) {
1546
+ return { valid: false, error: "Unmatched opening brace" };
1677
1547
  }
1678
- lines.push("}");
1679
- lines.push("");
1548
+ return { valid: true };
1549
+ } catch (error) {
1550
+ return { valid: false, error: error instanceof Error ? error.message : "Unknown error" };
1680
1551
  }
1681
- lines.push("export interface Schema {");
1682
- for (const table of tables) {
1683
- const interfaceName = tableNameToInterface(table.name);
1684
- lines.push(` ${table.name}: ${interfaceName};`);
1685
- }
1686
- lines.push("}");
1687
- lines.push("");
1688
- lines.push("// Database client with typed tables");
1689
- lines.push("// This is a proxy that will be populated by the Tether runtime");
1690
- lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
1691
- lines.push("");
1692
- return lines.join("\n");
1693
1552
  }
1694
- async function parseFunctionsDir(functionsDir) {
1695
- const functions = [];
1696
- if (!await fs6.pathExists(functionsDir)) {
1697
- return functions;
1553
+ async function validateFunctions(functionsDir) {
1554
+ const errors = [];
1555
+ if (!await fs3.pathExists(functionsDir)) {
1556
+ return { valid: true, errors: [] };
1698
1557
  }
1699
- const files = await fs6.readdir(functionsDir);
1558
+ const files = await fs3.readdir(functionsDir);
1700
1559
  for (const file of files) {
1701
1560
  if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
1702
- const filePath = path6.join(functionsDir, file);
1703
- const stat = await fs6.stat(filePath);
1561
+ const filePath = path3.join(functionsDir, file);
1562
+ const stat = await fs3.stat(filePath);
1704
1563
  if (!stat.isFile()) continue;
1705
- const moduleName = file.replace(/\.(ts|js)$/, "");
1706
- const source = await fs6.readFile(filePath, "utf-8");
1707
- const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action)\s*\(/g;
1708
- let match;
1709
- while ((match = exportRegex.exec(source)) !== null) {
1710
- functions.push({
1711
- name: match[1],
1712
- moduleName
1713
- });
1564
+ try {
1565
+ const content = await fs3.readFile(filePath, "utf-8");
1566
+ const hasExports = /export\s+const\s+\w+\s*=\s*(query|mutation|action)\s*\(/.test(content);
1567
+ if (!hasExports && !file.includes("index")) {
1568
+ errors.push(`${file}: No query, mutation, or action exports found`);
1569
+ }
1570
+ let braceCount = 0;
1571
+ for (const char of content) {
1572
+ if (char === "{") braceCount++;
1573
+ else if (char === "}") braceCount--;
1574
+ }
1575
+ if (braceCount !== 0) {
1576
+ errors.push(`${file}: Mismatched braces`);
1577
+ }
1578
+ } catch (error) {
1579
+ errors.push(`${file}: ${error instanceof Error ? error.message : "Read error"}`);
1714
1580
  }
1715
1581
  }
1716
- return functions;
1582
+ return { valid: errors.length === 0, errors };
1717
1583
  }
1718
- async function generateApiFile(functionsDir) {
1719
- const functions = await parseFunctionsDir(functionsDir);
1720
- const moduleMap = /* @__PURE__ */ new Map();
1721
- for (const fn of functions) {
1722
- if (!moduleMap.has(fn.moduleName)) {
1723
- moduleMap.set(fn.moduleName, []);
1724
- }
1725
- moduleMap.get(fn.moduleName).push(fn.name);
1726
- }
1727
- const lines = [
1728
- "// Auto-generated by Tether CLI - do not edit manually",
1729
- `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1730
- "",
1731
- "import { createApiProxy } from '@tthr/client';",
1732
- "",
1733
- "/**",
1734
- " * API function reference type for useQuery/useMutation.",
1735
- ' * The _name property contains the function path (e.g., "users.list").',
1736
- " */",
1737
- "export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
1738
- " _name: string;",
1739
- " _args?: TArgs;",
1740
- " _result?: TResult;",
1741
- "}",
1742
- ""
1743
- ];
1744
- if (moduleMap.size > 0) {
1745
- for (const [moduleName, fnNames] of moduleMap) {
1746
- const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
1747
- lines.push(`export interface ${interfaceName} {`);
1748
- for (const fnName of fnNames) {
1749
- lines.push(` ${fnName}: ApiFunction;`);
1750
- }
1751
- lines.push("}");
1752
- lines.push("");
1584
+ function startFrameworkDev(command, port, spinner) {
1585
+ const [cmd, ...args] = command.split(" ");
1586
+ const portArgs = ["--port", port];
1587
+ spinner.text = `Starting ${command}...`;
1588
+ const child = spawn(cmd, [...args, ...portArgs], {
1589
+ cwd: process.cwd(),
1590
+ stdio: ["inherit", "pipe", "pipe"],
1591
+ shell: true,
1592
+ env: {
1593
+ ...process.env,
1594
+ FORCE_COLOR: "1"
1753
1595
  }
1754
- lines.push("export interface Api {");
1755
- for (const moduleName of moduleMap.keys()) {
1756
- const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
1757
- lines.push(` ${moduleName}: ${interfaceName};`);
1596
+ });
1597
+ let started = false;
1598
+ child.stdout?.on("data", (data) => {
1599
+ const output = data.toString();
1600
+ process.stdout.write(output);
1601
+ if (!started && (output.includes("Local:") || output.includes("ready") || output.includes("listening"))) {
1602
+ started = true;
1603
+ spinner.succeed(`Framework dev server running`);
1604
+ spinner.start("Watching for changes...");
1758
1605
  }
1759
- lines.push("}");
1760
- } else {
1761
- lines.push("/**");
1762
- lines.push(" * Flexible API type that allows access to any function path.");
1763
- lines.push(" * Functions are accessed as api.moduleName.functionName");
1764
- lines.push(" * The actual types depend on your function definitions.");
1765
- lines.push(" */");
1766
- lines.push("export type Api = {");
1767
- lines.push(" [module: string]: {");
1768
- lines.push(" [fn: string]: ApiFunction;");
1769
- lines.push(" };");
1770
- lines.push("};");
1771
- }
1772
- lines.push("");
1773
- lines.push("// API client proxy - provides typed access to your functions");
1774
- lines.push("// On the client: returns { _name } references for useQuery/useMutation");
1775
- lines.push("// In Tether functions: calls the actual function implementation");
1776
- lines.push("export const api = createApiProxy<Api>();");
1777
- lines.push("");
1778
- return lines.join("\n");
1606
+ });
1607
+ child.stderr?.on("data", (data) => {
1608
+ const output = data.toString();
1609
+ if (!output.includes("ExperimentalWarning") && !output.includes("--trace-warnings")) {
1610
+ process.stderr.write(chalk3.dim(output));
1611
+ }
1612
+ });
1613
+ child.on("error", (error) => {
1614
+ spinner.fail(`Failed to start framework: ${error.message}`);
1615
+ });
1616
+ child.on("exit", (code) => {
1617
+ if (code !== 0 && code !== null) {
1618
+ console.log(chalk3.yellow(`
1619
+ Framework process exited with code ${code}`));
1620
+ }
1621
+ });
1622
+ return child;
1779
1623
  }
1780
- async function generateCommand() {
1624
+ function cleanup() {
1625
+ if (frameworkProcess) {
1626
+ frameworkProcess.kill("SIGTERM");
1627
+ frameworkProcess = null;
1628
+ }
1629
+ if (schemaWatcher) {
1630
+ schemaWatcher.close();
1631
+ schemaWatcher = null;
1632
+ }
1633
+ if (functionsWatcher) {
1634
+ functionsWatcher.close();
1635
+ functionsWatcher = null;
1636
+ }
1637
+ }
1638
+ async function devCommand(options) {
1781
1639
  await requireAuth();
1782
- const configPath = path6.resolve(process.cwd(), "tether.config.ts");
1783
- if (!await fs6.pathExists(configPath)) {
1784
- console.log(chalk5.red("\nError: Not a Tether project"));
1785
- console.log(chalk5.dim("Run `tthr init` to create a new project\n"));
1640
+ const configPath = path3.resolve(process.cwd(), "tether.config.ts");
1641
+ if (!await fs3.pathExists(configPath)) {
1642
+ console.log(chalk3.red("\nError: Not a Tether project"));
1643
+ console.log(chalk3.dim("Run `tthr init` to create a new project\n"));
1786
1644
  process.exit(1);
1787
1645
  }
1788
- console.log(chalk5.bold("\n\u26A1 Generating types from schema\n"));
1789
- const spinner = ora4("Reading schema...").start();
1646
+ const config = await loadConfig();
1647
+ const port = options.port || String(config.dev?.port || 3001);
1648
+ const schemaPath = resolvePath(config.schema);
1649
+ const functionsDir = resolvePath(config.functions);
1650
+ const outputDir = resolvePath(config.output);
1651
+ const framework = config.framework || await detectFramework();
1652
+ const devCmd = config.dev?.command || getFrameworkDevCommand(framework);
1653
+ const environment = options.env || config.environment || "development";
1654
+ const isLocal = options.local ?? false;
1655
+ console.log(chalk3.bold("\n\u26A1 Starting Tether development server\n"));
1656
+ if (framework !== "unknown") {
1657
+ console.log(chalk3.dim(` Framework: ${framework}`));
1658
+ }
1659
+ if (devCmd) {
1660
+ console.log(chalk3.dim(` Dev command: ${devCmd}`));
1661
+ }
1662
+ console.log(chalk3.dim(` Environment: ${environment}${isLocal ? " (local)" : " (cloud)"}`));
1663
+ console.log(chalk3.dim(` Schema: ${path3.relative(process.cwd(), schemaPath)}`));
1664
+ console.log(chalk3.dim(` Functions: ${path3.relative(process.cwd(), functionsDir)}`));
1665
+ console.log(chalk3.dim(` Output: ${path3.relative(process.cwd(), outputDir)}`));
1666
+ console.log("");
1667
+ const spinner = ora3("Initialising...").start();
1668
+ process.on("SIGINT", () => {
1669
+ spinner.stop();
1670
+ console.log(chalk3.yellow("\n\nShutting down...\n"));
1671
+ cleanup();
1672
+ process.exit(0);
1673
+ });
1674
+ process.on("SIGTERM", () => {
1675
+ cleanup();
1676
+ process.exit(0);
1677
+ });
1790
1678
  try {
1791
- const config = await loadConfig();
1792
- const schemaPath = resolvePath(config.schema);
1793
- const outputDir = resolvePath(config.output);
1794
- const functionsDir = resolvePath(config.functions);
1795
- if (!await fs6.pathExists(schemaPath)) {
1796
- spinner.fail("Schema file not found");
1797
- console.log(chalk5.dim(`Expected: ${schemaPath}
1798
- `));
1799
- process.exit(1);
1679
+ spinner.text = "Validating schema...";
1680
+ const schemaValidation = await validateSchema(schemaPath);
1681
+ if (!schemaValidation.valid) {
1682
+ spinner.warn(`Schema validation warning: ${schemaValidation.error}`);
1800
1683
  }
1801
- const schemaSource = await fs6.readFile(schemaPath, "utf-8");
1802
- const tables = parseSchemaFile(schemaSource);
1803
- if (tables.length === 0) {
1804
- spinner.warn("No tables found in schema");
1805
- console.log(chalk5.dim(" Make sure your schema uses defineSchema({ ... })\n"));
1806
- return;
1684
+ spinner.text = "Validating functions...";
1685
+ const functionsValidation = await validateFunctions(functionsDir);
1686
+ if (!functionsValidation.valid) {
1687
+ spinner.warn("Function validation warnings:");
1688
+ for (const error of functionsValidation.errors) {
1689
+ console.log(chalk3.yellow(` - ${error}`));
1690
+ }
1807
1691
  }
1808
- spinner.text = `Generating types for ${tables.length} table(s)...`;
1809
- await fs6.ensureDir(outputDir);
1810
- await fs6.writeFile(
1811
- path6.join(outputDir, "db.ts"),
1812
- generateDbFile(tables)
1813
- );
1814
- await fs6.writeFile(
1815
- path6.join(outputDir, "api.ts"),
1816
- await generateApiFile(functionsDir)
1817
- );
1818
- await fs6.writeFile(
1819
- path6.join(outputDir, "index.ts"),
1820
- `// Auto-generated by Tether CLI - do not edit manually
1821
- export * from './db';
1822
- export * from './api';
1823
- `
1824
- );
1825
- spinner.succeed(`Types generated for ${tables.length} table(s)`);
1826
- console.log("\n" + chalk5.green("\u2713") + " Tables:");
1827
- for (const table of tables) {
1828
- console.log(chalk5.dim(` - ${table.name} (${table.columns.length} columns)`));
1829
- }
1830
- const relativeOutput = path6.relative(process.cwd(), outputDir);
1831
- console.log("\n" + chalk5.green("\u2713") + " Generated files:");
1832
- console.log(chalk5.dim(` ${relativeOutput}/db.ts`));
1833
- console.log(chalk5.dim(` ${relativeOutput}/api.ts`));
1834
- console.log(chalk5.dim(` ${relativeOutput}/index.ts
1692
+ spinner.text = "Generating types...";
1693
+ const { generateTypes } = await import("./generate-YQ4QRPXF.js");
1694
+ await generateTypes({ silent: true });
1695
+ spinner.succeed("Types generated");
1696
+ spinner.start("Setting up file watchers...");
1697
+ if (await fs3.pathExists(schemaPath)) {
1698
+ schemaWatcher = watch(schemaPath, {
1699
+ ignoreInitial: true,
1700
+ awaitWriteFinish: {
1701
+ stabilityThreshold: 300,
1702
+ pollInterval: 100
1703
+ }
1704
+ });
1705
+ schemaWatcher.on("change", async () => {
1706
+ console.log(chalk3.cyan("\n Schema changed, validating..."));
1707
+ const validation = await validateSchema(schemaPath);
1708
+ if (!validation.valid) {
1709
+ console.log(chalk3.yellow(` Schema error: ${validation.error}`));
1710
+ return;
1711
+ }
1712
+ await runGenerate(spinner);
1713
+ });
1714
+ }
1715
+ if (await fs3.pathExists(functionsDir)) {
1716
+ functionsWatcher = watch(path3.join(functionsDir, "**/*.{ts,js}"), {
1717
+ ignoreInitial: true,
1718
+ awaitWriteFinish: {
1719
+ stabilityThreshold: 300,
1720
+ pollInterval: 100
1721
+ }
1722
+ });
1723
+ functionsWatcher.on("all", async (event, filePath) => {
1724
+ const relativePath = path3.relative(functionsDir, filePath);
1725
+ console.log(chalk3.cyan(`
1726
+ Function ${event}: ${relativePath}`));
1727
+ const validation = await validateFunctions(functionsDir);
1728
+ if (!validation.valid) {
1729
+ for (const error of validation.errors) {
1730
+ console.log(chalk3.yellow(` Warning: ${error}`));
1731
+ }
1732
+ }
1733
+ await runGenerate(spinner);
1734
+ });
1735
+ }
1736
+ spinner.succeed("File watchers ready");
1737
+ if (devCmd && !options.skipFramework) {
1738
+ spinner.start(`Starting ${framework} dev server...`);
1739
+ frameworkProcess = startFrameworkDev(devCmd, port, spinner);
1740
+ } else {
1741
+ spinner.start("Watching for changes...");
1742
+ console.log("\n" + chalk3.cyan(` Tether dev ready`));
1743
+ console.log(chalk3.dim(` Watching schema and functions for changes`));
1744
+ console.log(chalk3.dim(` Press Ctrl+C to stop
1835
1745
  `));
1746
+ }
1747
+ await new Promise(() => {
1748
+ });
1836
1749
  } catch (error) {
1837
- spinner.fail("Failed to generate types");
1838
- console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
1750
+ spinner.fail("Failed to start dev server");
1751
+ console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
1752
+ cleanup();
1839
1753
  process.exit(1);
1840
1754
  }
1841
1755
  }
1842
1756
 
1843
1757
  // src/commands/login.ts
1844
- import chalk6 from "chalk";
1845
- import ora5 from "ora";
1758
+ import chalk4 from "chalk";
1759
+ import ora4 from "ora";
1846
1760
  import os from "os";
1847
1761
  var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
1848
1762
  var API_URL3 = isDev3 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
1849
1763
  var AUTH_URL = isDev3 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
1850
1764
  async function loginCommand() {
1851
- console.log(chalk6.bold("\u26A1 Login to Tether\n"));
1765
+ console.log(chalk4.bold("\u26A1 Login to Tether\n"));
1852
1766
  const existing = await getCredentials();
1853
1767
  if (existing) {
1854
- console.log(chalk6.green("\u2713") + ` Already logged in as ${chalk6.cyan(existing.email)}`);
1855
- console.log(chalk6.dim("\nRun `tthr logout` to sign out\n"));
1768
+ console.log(chalk4.green("\u2713") + ` Already logged in as ${chalk4.cyan(existing.email)}`);
1769
+ console.log(chalk4.dim("\nRun `tthr logout` to sign out\n"));
1856
1770
  return;
1857
1771
  }
1858
- const spinner = ora5("Generating authentication code...").start();
1772
+ const spinner = ora4("Generating authentication code...").start();
1859
1773
  try {
1860
1774
  const deviceCode = await requestDeviceCode();
1861
1775
  spinner.stop();
1862
1776
  const authUrl = `${AUTH_URL}/${deviceCode.userCode}`;
1863
- console.log(chalk6.dim("Open this URL in your browser to authenticate:\n"));
1864
- console.log(chalk6.cyan.bold(` ${authUrl}
1777
+ console.log(chalk4.dim("Open this URL in your browser to authenticate:\n"));
1778
+ console.log(chalk4.cyan.bold(` ${authUrl}
1865
1779
  `));
1866
- console.log(chalk6.dim(`Your code: ${chalk6.white.bold(deviceCode.userCode)}
1780
+ console.log(chalk4.dim(`Your code: ${chalk4.white.bold(deviceCode.userCode)}
1867
1781
  `));
1868
1782
  const credentials = await pollForApproval(deviceCode.deviceCode, deviceCode.interval, deviceCode.expiresIn);
1869
1783
  await saveCredentials(credentials);
1870
- console.log(chalk6.green("\u2713") + ` Logged in as ${chalk6.cyan(credentials.email)}`);
1784
+ console.log(chalk4.green("\u2713") + ` Logged in as ${chalk4.cyan(credentials.email)}`);
1871
1785
  console.log();
1872
1786
  } catch (error) {
1873
1787
  spinner.fail("Authentication failed");
1874
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1875
- console.log(chalk6.dim("\nTry again or visit https://tthr.io/docs/cli for help\n"));
1788
+ console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
1789
+ console.log(chalk4.dim("\nTry again or visit https://tthr.io/docs/cli for help\n"));
1876
1790
  process.exit(1);
1877
1791
  }
1878
1792
  }
1879
1793
  async function logoutCommand() {
1880
- console.log(chalk6.bold("\n\u26A1 Logout from Tether\n"));
1794
+ console.log(chalk4.bold("\n\u26A1 Logout from Tether\n"));
1881
1795
  const credentials = await getCredentials();
1882
1796
  if (!credentials) {
1883
- console.log(chalk6.dim("Not logged in\n"));
1797
+ console.log(chalk4.dim("Not logged in\n"));
1884
1798
  return;
1885
1799
  }
1886
- const spinner = ora5("Logging out...").start();
1800
+ const spinner = ora4("Logging out...").start();
1887
1801
  try {
1888
1802
  await clearCredentials();
1889
- spinner.succeed(`Logged out from ${chalk6.cyan(credentials.email)}`);
1803
+ spinner.succeed(`Logged out from ${chalk4.cyan(credentials.email)}`);
1890
1804
  console.log();
1891
1805
  } catch (error) {
1892
1806
  spinner.fail("Logout failed");
1893
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1807
+ console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
1894
1808
  process.exit(1);
1895
1809
  }
1896
1810
  }
1897
1811
  async function whoamiCommand() {
1898
1812
  const credentials = await getCredentials();
1899
1813
  if (!credentials) {
1900
- console.log(chalk6.dim("\nNot logged in"));
1901
- console.log(chalk6.dim("Run `tthr login` to authenticate\n"));
1814
+ console.log(chalk4.dim("\nNot logged in"));
1815
+ console.log(chalk4.dim("Run `tthr login` to authenticate\n"));
1902
1816
  return;
1903
1817
  }
1904
- console.log(chalk6.bold("\n\u26A1 Current user\n"));
1905
- console.log(` Email: ${chalk6.cyan(credentials.email)}`);
1906
- console.log(` User ID: ${chalk6.dim(credentials.userId)}`);
1818
+ console.log(chalk4.bold("\n\u26A1 Current user\n"));
1819
+ console.log(` Email: ${chalk4.cyan(credentials.email)}`);
1820
+ console.log(` User ID: ${chalk4.dim(credentials.userId)}`);
1907
1821
  if (credentials.expiresAt) {
1908
1822
  const expiresAt = new Date(credentials.expiresAt);
1909
1823
  const now = /* @__PURE__ */ new Date();
1910
1824
  if (expiresAt > now) {
1911
- console.log(` Session: ${chalk6.green("Active")}`);
1825
+ console.log(` Session: ${chalk4.green("Active")}`);
1912
1826
  } else {
1913
- console.log(` Session: ${chalk6.yellow("Expired")}`);
1827
+ console.log(` Session: ${chalk4.yellow("Expired")}`);
1914
1828
  }
1915
1829
  }
1916
1830
  console.log();
@@ -1952,20 +1866,20 @@ async function requestDeviceCode() {
1952
1866
  async function pollForApproval(deviceCode, interval, expiresIn) {
1953
1867
  const startTime = Date.now();
1954
1868
  const expiresAt = startTime + expiresIn * 1e3;
1955
- const spinner = ora5("").start();
1869
+ const spinner = ora4("").start();
1956
1870
  const updateCountdown = () => {
1957
1871
  const remaining = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1e3));
1958
1872
  const mins = Math.floor(remaining / 60);
1959
1873
  const secs = remaining % 60;
1960
1874
  const timeStr = mins > 0 ? `${mins}:${secs.toString().padStart(2, "0")}` : `${secs}s`;
1961
- spinner.text = `Waiting for approval... ${chalk6.dim(`(${timeStr} remaining)`)}`;
1875
+ spinner.text = `Waiting for approval... ${chalk4.dim(`(${timeStr} remaining)`)}`;
1962
1876
  };
1963
1877
  updateCountdown();
1964
1878
  const countdownInterval = setInterval(updateCountdown, 1e3);
1965
1879
  try {
1966
1880
  while (Date.now() < expiresAt) {
1967
1881
  await sleep(interval * 1e3);
1968
- spinner.text = `Checking... ${chalk6.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
1882
+ spinner.text = `Checking... ${chalk4.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
1969
1883
  const response = await fetch(`${API_URL3}/auth/device/${deviceCode}`, {
1970
1884
  method: "GET"
1971
1885
  }).catch(() => null);
@@ -1998,10 +1912,10 @@ function sleep(ms) {
1998
1912
  }
1999
1913
 
2000
1914
  // src/commands/update.ts
2001
- import chalk7 from "chalk";
2002
- import ora6 from "ora";
2003
- import fs7 from "fs-extra";
2004
- import path7 from "path";
1915
+ import chalk5 from "chalk";
1916
+ import ora5 from "ora";
1917
+ import fs4 from "fs-extra";
1918
+ import path4 from "path";
2005
1919
  import { execSync as execSync2 } from "child_process";
2006
1920
  var TETHER_PACKAGES = [
2007
1921
  "@tthr/client",
@@ -2013,29 +1927,29 @@ var TETHER_PACKAGES = [
2013
1927
  ];
2014
1928
  function detectPackageManager() {
2015
1929
  const cwd = process.cwd();
2016
- if (fs7.existsSync(path7.join(cwd, "bun.lockb")) || fs7.existsSync(path7.join(cwd, "bun.lock"))) {
1930
+ if (fs4.existsSync(path4.join(cwd, "bun.lockb")) || fs4.existsSync(path4.join(cwd, "bun.lock"))) {
2017
1931
  return "bun";
2018
1932
  }
2019
- if (fs7.existsSync(path7.join(cwd, "pnpm-lock.yaml"))) {
1933
+ if (fs4.existsSync(path4.join(cwd, "pnpm-lock.yaml"))) {
2020
1934
  return "pnpm";
2021
1935
  }
2022
- if (fs7.existsSync(path7.join(cwd, "yarn.lock"))) {
1936
+ if (fs4.existsSync(path4.join(cwd, "yarn.lock"))) {
2023
1937
  return "yarn";
2024
1938
  }
2025
1939
  return "npm";
2026
1940
  }
2027
- function getInstallCommand2(pm, packages, isDev5) {
1941
+ function getInstallCommand2(pm, packages, isDev6) {
2028
1942
  const packagesStr = packages.join(" ");
2029
1943
  switch (pm) {
2030
1944
  case "bun":
2031
- return isDev5 ? `bun add -d ${packagesStr}` : `bun add ${packagesStr}`;
1945
+ return isDev6 ? `bun add -d ${packagesStr}` : `bun add ${packagesStr}`;
2032
1946
  case "pnpm":
2033
- return isDev5 ? `pnpm add -D ${packagesStr}` : `pnpm add ${packagesStr}`;
1947
+ return isDev6 ? `pnpm add -D ${packagesStr}` : `pnpm add ${packagesStr}`;
2034
1948
  case "yarn":
2035
- return isDev5 ? `yarn add -D ${packagesStr}` : `yarn add ${packagesStr}`;
1949
+ return isDev6 ? `yarn add -D ${packagesStr}` : `yarn add ${packagesStr}`;
2036
1950
  case "npm":
2037
1951
  default:
2038
- return isDev5 ? `npm install -D ${packagesStr}` : `npm install ${packagesStr}`;
1952
+ return isDev6 ? `npm install -D ${packagesStr}` : `npm install ${packagesStr}`;
2039
1953
  }
2040
1954
  }
2041
1955
  async function getLatestVersion2(packageName) {
@@ -2049,14 +1963,14 @@ async function getLatestVersion2(packageName) {
2049
1963
  }
2050
1964
  }
2051
1965
  async function updateCommand(options) {
2052
- const packageJsonPath = path7.resolve(process.cwd(), "package.json");
2053
- if (!await fs7.pathExists(packageJsonPath)) {
2054
- console.log(chalk7.red("\nError: No package.json found"));
2055
- console.log(chalk7.dim("Make sure you're in the root of your project\n"));
1966
+ const packageJsonPath = path4.resolve(process.cwd(), "package.json");
1967
+ if (!await fs4.pathExists(packageJsonPath)) {
1968
+ console.log(chalk5.red("\nError: No package.json found"));
1969
+ console.log(chalk5.dim("Make sure you're in the root of your project\n"));
2056
1970
  process.exit(1);
2057
1971
  }
2058
- console.log(chalk7.bold("\n\u26A1 Updating Tether packages\n"));
2059
- const packageJson = await fs7.readJson(packageJsonPath);
1972
+ console.log(chalk5.bold("\n\u26A1 Updating Tether packages\n"));
1973
+ const packageJson = await fs4.readJson(packageJsonPath);
2060
1974
  const deps = packageJson.dependencies || {};
2061
1975
  const devDeps = packageJson.devDependencies || {};
2062
1976
  const installedDeps = [];
@@ -2069,13 +1983,13 @@ async function updateCommand(options) {
2069
1983
  }
2070
1984
  }
2071
1985
  if (installedDeps.length === 0) {
2072
- console.log(chalk7.yellow(" No updatable Tether packages found in package.json"));
2073
- console.log(chalk7.dim(" Tether packages start with @tthr/ (workspace: dependencies are skipped)\n"));
1986
+ console.log(chalk5.yellow(" No updatable Tether packages found in package.json"));
1987
+ console.log(chalk5.dim(" Tether packages start with @tthr/ (workspace: dependencies are skipped)\n"));
2074
1988
  return;
2075
1989
  }
2076
- console.log(chalk7.dim(` Found ${installedDeps.length} Tether package(s):
1990
+ console.log(chalk5.dim(` Found ${installedDeps.length} Tether package(s):
2077
1991
  `));
2078
- const spinner = ora6("Checking for updates...").start();
1992
+ const spinner = ora5("Checking for updates...").start();
2079
1993
  const packagesToUpdate = [];
2080
1994
  for (const pkg of installedDeps) {
2081
1995
  const latestVersion = await getLatestVersion2(pkg.name);
@@ -2089,32 +2003,32 @@ async function updateCommand(options) {
2089
2003
  isDev: pkg.isDev
2090
2004
  });
2091
2005
  } else {
2092
- console.log(chalk7.dim(` ${pkg.name}@${currentClean} (up to date)`));
2006
+ console.log(chalk5.dim(` ${pkg.name}@${currentClean} (up to date)`));
2093
2007
  }
2094
2008
  }
2095
2009
  }
2096
2010
  spinner.stop();
2097
2011
  if (packagesToUpdate.length === 0) {
2098
- console.log(chalk7.green("\n\u2713 All Tether packages are up to date\n"));
2012
+ console.log(chalk5.green("\n\u2713 All Tether packages are up to date\n"));
2099
2013
  return;
2100
2014
  }
2101
- console.log(chalk7.cyan("\n Updates available:\n"));
2015
+ console.log(chalk5.cyan("\n Updates available:\n"));
2102
2016
  for (const pkg of packagesToUpdate) {
2103
2017
  console.log(
2104
- chalk7.white(` ${pkg.name}`) + chalk7.red(` ${pkg.current}`) + chalk7.dim(" \u2192 ") + chalk7.green(`${pkg.latest}`) + (pkg.isDev ? chalk7.dim(" (dev)") : "")
2018
+ chalk5.white(` ${pkg.name}`) + chalk5.red(` ${pkg.current}`) + chalk5.dim(" \u2192 ") + chalk5.green(`${pkg.latest}`) + (pkg.isDev ? chalk5.dim(" (dev)") : "")
2105
2019
  );
2106
2020
  }
2107
2021
  if (options.dryRun) {
2108
- console.log(chalk7.yellow("\n Dry run - no packages were updated\n"));
2022
+ console.log(chalk5.yellow("\n Dry run - no packages were updated\n"));
2109
2023
  return;
2110
2024
  }
2111
2025
  const pm = detectPackageManager();
2112
- console.log(chalk7.dim(`
2026
+ console.log(chalk5.dim(`
2113
2027
  Using ${pm} to update packages...
2114
2028
  `));
2115
2029
  const depsToUpdate = packagesToUpdate.filter((p) => !p.isDev).map((p) => `${p.name}@latest`);
2116
2030
  const devDepsToUpdate = packagesToUpdate.filter((p) => p.isDev).map((p) => `${p.name}@latest`);
2117
- const updateSpinner = ora6("Installing updates...").start();
2031
+ const updateSpinner = ora5("Installing updates...").start();
2118
2032
  try {
2119
2033
  if (depsToUpdate.length > 0) {
2120
2034
  const cmd = getInstallCommand2(pm, depsToUpdate, false);
@@ -2125,43 +2039,43 @@ async function updateCommand(options) {
2125
2039
  execSync2(cmd, { stdio: "pipe", cwd: process.cwd() });
2126
2040
  }
2127
2041
  updateSpinner.succeed("Packages updated successfully");
2128
- console.log(chalk7.green(`
2042
+ console.log(chalk5.green(`
2129
2043
  \u2713 Updated ${packagesToUpdate.length} package(s)
2130
2044
  `));
2131
2045
  } catch (error) {
2132
2046
  updateSpinner.fail("Failed to update packages");
2133
- console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
2047
+ console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
2134
2048
  process.exit(1);
2135
2049
  }
2136
2050
  }
2137
2051
 
2138
2052
  // src/commands/exec.ts
2139
- import chalk8 from "chalk";
2140
- import ora7 from "ora";
2141
- import fs8 from "fs-extra";
2142
- import path8 from "path";
2053
+ import chalk6 from "chalk";
2054
+ import ora6 from "ora";
2055
+ import fs5 from "fs-extra";
2056
+ import path5 from "path";
2143
2057
  var isDev4 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
2144
2058
  var API_URL4 = isDev4 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
2145
2059
  async function execCommand(sql) {
2146
2060
  if (!sql || sql.trim() === "") {
2147
- console.log(chalk8.red("\nError: SQL query required"));
2148
- console.log(chalk8.dim('Usage: tthr exec "SELECT * FROM table_name"\n'));
2061
+ console.log(chalk6.red("\nError: SQL query required"));
2062
+ console.log(chalk6.dim('Usage: tthr exec "SELECT * FROM table_name"\n'));
2149
2063
  process.exit(1);
2150
2064
  }
2151
2065
  const credentials = await requireAuth();
2152
- const envPath = path8.resolve(process.cwd(), ".env");
2066
+ const envPath = path5.resolve(process.cwd(), ".env");
2153
2067
  let projectId;
2154
- if (await fs8.pathExists(envPath)) {
2155
- const envContent = await fs8.readFile(envPath, "utf-8");
2068
+ if (await fs5.pathExists(envPath)) {
2069
+ const envContent = await fs5.readFile(envPath, "utf-8");
2156
2070
  const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
2157
2071
  projectId = match?.[1]?.trim();
2158
2072
  }
2159
2073
  if (!projectId) {
2160
- console.log(chalk8.red("\nError: Project ID not found"));
2161
- console.log(chalk8.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
2074
+ console.log(chalk6.red("\nError: Project ID not found"));
2075
+ console.log(chalk6.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
2162
2076
  process.exit(1);
2163
2077
  }
2164
- const spinner = ora7("Executing query...").start();
2078
+ const spinner = ora6("Executing query...").start();
2165
2079
  try {
2166
2080
  const response = await fetch(`${API_URL4}/projects/${projectId}/exec`, {
2167
2081
  method: "POST",
@@ -2181,24 +2095,24 @@ async function execCommand(sql) {
2181
2095
  if (result.columns && result.rows) {
2182
2096
  console.log();
2183
2097
  if (result.rows.length === 0) {
2184
- console.log(chalk8.dim(" No rows returned"));
2098
+ console.log(chalk6.dim(" No rows returned"));
2185
2099
  } else {
2186
- console.log(chalk8.bold(" " + result.columns.join(" ")));
2187
- console.log(chalk8.dim(" " + result.columns.map(() => "--------").join(" ")));
2100
+ console.log(chalk6.bold(" " + result.columns.join(" ")));
2101
+ console.log(chalk6.dim(" " + result.columns.map(() => "--------").join(" ")));
2188
2102
  for (const row of result.rows) {
2189
2103
  const values = result.columns.map((col) => {
2190
2104
  const val = row[col];
2191
- if (val === null) return chalk8.dim("NULL");
2105
+ if (val === null) return chalk6.dim("NULL");
2192
2106
  if (typeof val === "object") return JSON.stringify(val);
2193
2107
  return String(val);
2194
2108
  });
2195
2109
  console.log(" " + values.join(" "));
2196
2110
  }
2197
- console.log(chalk8.dim(`
2111
+ console.log(chalk6.dim(`
2198
2112
  ${result.rows.length} row(s)`));
2199
2113
  }
2200
2114
  } else if (result.rowsAffected !== void 0) {
2201
- console.log(chalk8.dim(`
2115
+ console.log(chalk6.dim(`
2202
2116
  ${result.rowsAffected} row(s) affected`));
2203
2117
  }
2204
2118
  console.log();
@@ -2208,16 +2122,182 @@ async function execCommand(sql) {
2208
2122
  }
2209
2123
  }
2210
2124
 
2125
+ // src/commands/env.ts
2126
+ import chalk7 from "chalk";
2127
+ import ora7 from "ora";
2128
+ import fs6 from "fs-extra";
2129
+ import path6 from "path";
2130
+ var isDev5 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
2131
+ var API_URL5 = isDev5 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
2132
+ async function getProjectId() {
2133
+ const envPath = path6.resolve(process.cwd(), ".env");
2134
+ let projectId;
2135
+ if (await fs6.pathExists(envPath)) {
2136
+ const envContent = await fs6.readFile(envPath, "utf-8");
2137
+ const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
2138
+ projectId = match?.[1]?.trim();
2139
+ }
2140
+ if (!projectId) {
2141
+ console.log(chalk7.red("\nError: Project ID not found"));
2142
+ console.log(chalk7.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
2143
+ process.exit(1);
2144
+ }
2145
+ return projectId;
2146
+ }
2147
+ async function envListCommand() {
2148
+ const credentials = await requireAuth();
2149
+ const projectId = await getProjectId();
2150
+ const spinner = ora7("Fetching environments...").start();
2151
+ try {
2152
+ const response = await fetch(`${API_URL5}/projects/${projectId}/environments`, {
2153
+ headers: {
2154
+ "Authorization": `Bearer ${credentials.accessToken}`
2155
+ }
2156
+ });
2157
+ if (!response.ok) {
2158
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
2159
+ throw new Error(error.error || `HTTP ${response.status}`);
2160
+ }
2161
+ const data = await response.json();
2162
+ spinner.succeed(`Found ${data.environments.length} environment(s)`);
2163
+ console.log();
2164
+ for (const env2 of data.environments) {
2165
+ const isDefault = env2.name === data.defaultEnvironment;
2166
+ const colour = getEnvColour(env2.name);
2167
+ const defaultBadge = isDefault ? chalk7.yellow(" \u2605 default") : "";
2168
+ console.log(` ${chalk7.hex(colour)("\u25CF")} ${chalk7.bold(env2.name)}${defaultBadge}`);
2169
+ console.log(chalk7.dim(` API Key: ${maskApiKey(env2.apiKey)}`));
2170
+ console.log(chalk7.dim(` Created: ${new Date(env2.createdAt).toLocaleDateString()}`));
2171
+ console.log();
2172
+ }
2173
+ } catch (error) {
2174
+ spinner.fail("Failed to fetch environments");
2175
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
2176
+ process.exit(1);
2177
+ }
2178
+ }
2179
+ async function envCreateCommand(name, options) {
2180
+ const credentials = await requireAuth();
2181
+ const projectId = await getProjectId();
2182
+ const normalizedName = name.toLowerCase();
2183
+ if (!/^[a-z][a-z0-9-_]*$/.test(normalizedName)) {
2184
+ console.log(chalk7.red("\nError: Invalid environment name"));
2185
+ console.log(chalk7.dim("Name must start with a letter and contain only letters, numbers, hyphens, and underscores\n"));
2186
+ process.exit(1);
2187
+ }
2188
+ const spinner = ora7(`Creating environment '${normalizedName}'...`).start();
2189
+ try {
2190
+ const body = { name: normalizedName };
2191
+ if (options.from) {
2192
+ body.cloneFrom = options.from;
2193
+ spinner.text = `Creating environment '${normalizedName}' from '${options.from}'...`;
2194
+ }
2195
+ const response = await fetch(`${API_URL5}/projects/${projectId}/environments`, {
2196
+ method: "POST",
2197
+ headers: {
2198
+ "Content-Type": "application/json",
2199
+ "Authorization": `Bearer ${credentials.accessToken}`
2200
+ },
2201
+ body: JSON.stringify(body)
2202
+ });
2203
+ if (!response.ok) {
2204
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
2205
+ throw new Error(error.error || `HTTP ${response.status}`);
2206
+ }
2207
+ const env2 = await response.json();
2208
+ spinner.succeed(`Environment '${normalizedName}' created`);
2209
+ console.log();
2210
+ console.log(chalk7.dim(` API Key: ${env2.apiKey}`));
2211
+ console.log();
2212
+ console.log(chalk7.dim("Deploy to this environment with:"));
2213
+ console.log(chalk7.cyan(` tthr deploy --env ${normalizedName}`));
2214
+ console.log();
2215
+ } catch (error) {
2216
+ spinner.fail("Failed to create environment");
2217
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
2218
+ process.exit(1);
2219
+ }
2220
+ }
2221
+ async function envDeleteCommand(name) {
2222
+ const credentials = await requireAuth();
2223
+ const projectId = await getProjectId();
2224
+ const spinner = ora7(`Deleting environment '${name}'...`).start();
2225
+ try {
2226
+ const response = await fetch(`${API_URL5}/projects/${projectId}/environments/${name}`, {
2227
+ method: "DELETE",
2228
+ headers: {
2229
+ "Authorization": `Bearer ${credentials.accessToken}`
2230
+ }
2231
+ });
2232
+ if (!response.ok) {
2233
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
2234
+ throw new Error(error.error || `HTTP ${response.status}`);
2235
+ }
2236
+ spinner.succeed(`Environment '${name}' deleted`);
2237
+ } catch (error) {
2238
+ spinner.fail("Failed to delete environment");
2239
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
2240
+ process.exit(1);
2241
+ }
2242
+ }
2243
+ async function envDefaultCommand(name) {
2244
+ const credentials = await requireAuth();
2245
+ const projectId = await getProjectId();
2246
+ const spinner = ora7(`Setting '${name}' as default environment...`).start();
2247
+ try {
2248
+ const response = await fetch(`${API_URL5}/projects/${projectId}/environments/default`, {
2249
+ method: "PATCH",
2250
+ headers: {
2251
+ "Content-Type": "application/json",
2252
+ "Authorization": `Bearer ${credentials.accessToken}`
2253
+ },
2254
+ body: JSON.stringify({ name })
2255
+ });
2256
+ if (!response.ok) {
2257
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
2258
+ throw new Error(error.error || `HTTP ${response.status}`);
2259
+ }
2260
+ spinner.succeed(`'${name}' is now the default environment`);
2261
+ } catch (error) {
2262
+ spinner.fail("Failed to set default environment");
2263
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
2264
+ process.exit(1);
2265
+ }
2266
+ }
2267
+ function getEnvColour(envName) {
2268
+ switch (envName) {
2269
+ case "production":
2270
+ return "#10b981";
2271
+ // green
2272
+ case "development":
2273
+ return "#f59e0b";
2274
+ // amber
2275
+ default:
2276
+ return "#3b82f6";
2277
+ }
2278
+ }
2279
+ function maskApiKey(key) {
2280
+ if (!key || key.length < 15) return key;
2281
+ const visibleStart = key.substring(0, 10);
2282
+ const visibleEnd = key.substring(key.length - 4);
2283
+ return `${visibleStart}...${visibleEnd}`;
2284
+ }
2285
+
2211
2286
  // src/index.ts
2212
2287
  var program = new Command();
2213
2288
  program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version("0.0.1");
2214
2289
  program.command("init [name]").description("Create a new Tether project").option("-t, --template <template>", "Project template (vue, svelte, react, vanilla)", "vue").action(initCommand);
2215
- program.command("dev").description("Start local development server with hot reload").option("-p, --port <port>", "Port to run on", "3001").action(devCommand);
2290
+ 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);
2216
2291
  program.command("generate").alias("gen").description("Generate types from schema").action(generateCommand);
2217
- program.command("deploy").description("Deploy schema and functions to Tether").option("-s, --schema", "Deploy schema only").option("-f, --functions", "Deploy functions only").option("--dry-run", "Show what would be deployed without deploying").action(deployCommand);
2292
+ 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);
2218
2293
  program.command("login").description("Authenticate with Tether").action(loginCommand);
2219
2294
  program.command("logout").description("Sign out of Tether").action(logoutCommand);
2220
2295
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
2221
2296
  program.command("update").description("Update all Tether packages to the latest version").option("--dry-run", "Show what would be updated without updating").action(updateCommand);
2222
2297
  program.command("exec <sql>").description("Execute a SQL query against the project database").action(execCommand);
2298
+ var env = program.command("env").description("Manage project environments");
2299
+ env.command("list").description("List all environments").action(envListCommand);
2300
+ env.command("create <name>").description("Create a new environment").option("--from <env>", "Clone schema and functions from an existing environment").action(envCreateCommand);
2301
+ env.command("delete <name>").description("Delete an environment").action(envDeleteCommand);
2302
+ env.command("default <name>").description("Set the default environment").action(envDefaultCommand);
2223
2303
  program.parse();