@tthr/cli 0.0.3 → 0.0.5

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.
Files changed (2) hide show
  1. package/dist/index.js +481 -239
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,11 +4,11 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
- import chalk2 from "chalk";
8
- import ora from "ora";
7
+ import chalk3 from "chalk";
8
+ import ora2 from "ora";
9
9
  import prompts from "prompts";
10
- import fs2 from "fs-extra";
11
- import path2 from "path";
10
+ import fs3 from "fs-extra";
11
+ import path3 from "path";
12
12
  import { execSync } from "child_process";
13
13
 
14
14
  // src/utils/auth.ts
@@ -54,9 +54,284 @@ async function clearCredentials() {
54
54
  }
55
55
  }
56
56
 
57
- // src/commands/init.ts
57
+ // src/commands/deploy.ts
58
+ import chalk2 from "chalk";
59
+ import ora from "ora";
60
+ import fs2 from "fs-extra";
61
+ import path2 from "path";
58
62
  var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
59
63
  var API_URL = isDev ? "http://localhost:3001/api/v1" : "https://tthr.io/api/v1";
64
+ async function deployCommand(options) {
65
+ const credentials = await requireAuth();
66
+ const configPath = path2.resolve(process.cwd(), "tether.config.ts");
67
+ if (!await fs2.pathExists(configPath)) {
68
+ console.log(chalk2.red("\nError: Not a Tether project"));
69
+ console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
70
+ process.exit(1);
71
+ }
72
+ const envPath = path2.resolve(process.cwd(), ".env");
73
+ let projectId;
74
+ if (await fs2.pathExists(envPath)) {
75
+ const envContent = await fs2.readFile(envPath, "utf-8");
76
+ const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
77
+ projectId = match?.[1]?.trim();
78
+ }
79
+ if (!projectId) {
80
+ console.log(chalk2.red("\nError: Project ID not found"));
81
+ console.log(chalk2.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
82
+ process.exit(1);
83
+ }
84
+ console.log(chalk2.bold("\n\u26A1 Deploying to Tether\n"));
85
+ console.log(chalk2.dim(` Project: ${projectId}`));
86
+ console.log(chalk2.dim(` API: ${API_URL}
87
+ `));
88
+ const deploySchema = options.schema || !options.schema && !options.functions;
89
+ const deployFunctions = options.functions || !options.schema && !options.functions;
90
+ if (deploySchema) {
91
+ await deploySchemaToServer(projectId, credentials.accessToken, options.dryRun);
92
+ }
93
+ if (deployFunctions) {
94
+ await deployFunctionsToServer(projectId, credentials.accessToken, options.dryRun);
95
+ }
96
+ console.log(chalk2.green("\n\u2713 Deployment complete\n"));
97
+ }
98
+ async function deploySchemaToServer(projectId, token, dryRun) {
99
+ const spinner = ora("Reading schema...").start();
100
+ try {
101
+ const schemaPath = path2.resolve(process.cwd(), "tether", "schema.ts");
102
+ if (!await fs2.pathExists(schemaPath)) {
103
+ spinner.warn("No schema file found");
104
+ console.log(chalk2.dim(" Create tether/schema.ts to define your database schema\n"));
105
+ return;
106
+ }
107
+ const schemaSource = await fs2.readFile(schemaPath, "utf-8");
108
+ const tables = parseSchema(schemaSource);
109
+ spinner.text = `Found ${tables.length} table(s)`;
110
+ if (dryRun) {
111
+ spinner.info("Dry run - would deploy schema:");
112
+ for (const table of tables) {
113
+ console.log(chalk2.dim(` - ${table.name}`));
114
+ }
115
+ return;
116
+ }
117
+ const sql = generateSchemaSQL(tables);
118
+ spinner.text = "Deploying schema...";
119
+ const response = await fetch(`${API_URL}/${projectId}/deploy/schema`, {
120
+ method: "POST",
121
+ headers: {
122
+ "Content-Type": "application/json",
123
+ "Authorization": `Bearer ${token}`
124
+ },
125
+ body: JSON.stringify({
126
+ sql,
127
+ tables: tables.map((t) => t.name)
128
+ })
129
+ });
130
+ if (!response.ok) {
131
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
132
+ throw new Error(error.error || `HTTP ${response.status}`);
133
+ }
134
+ spinner.succeed(`Schema deployed (${tables.length} table(s))`);
135
+ } catch (error) {
136
+ spinner.fail("Failed to deploy schema");
137
+ console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
138
+ }
139
+ }
140
+ async function deployFunctionsToServer(projectId, token, dryRun) {
141
+ const spinner = ora("Reading functions...").start();
142
+ try {
143
+ const functionsDir = path2.resolve(process.cwd(), "tether", "functions");
144
+ if (!await fs2.pathExists(functionsDir)) {
145
+ spinner.warn("No functions directory found");
146
+ console.log(chalk2.dim(" Create tether/functions/ to define your API functions\n"));
147
+ return;
148
+ }
149
+ const files = await fs2.readdir(functionsDir);
150
+ const tsFiles = files.filter((f) => f.endsWith(".ts"));
151
+ if (tsFiles.length === 0) {
152
+ spinner.info("No function files found");
153
+ return;
154
+ }
155
+ const functions = [];
156
+ for (const file of tsFiles) {
157
+ const filePath = path2.join(functionsDir, file);
158
+ const source = await fs2.readFile(filePath, "utf-8");
159
+ const moduleName = file.replace(".ts", "");
160
+ const parsedFunctions = parseFunctions(moduleName, source);
161
+ functions.push(...parsedFunctions);
162
+ }
163
+ spinner.text = `Found ${functions.length} function(s)`;
164
+ if (dryRun) {
165
+ spinner.info("Dry run - would deploy functions:");
166
+ for (const fn of functions) {
167
+ const icon = fn.type === "query" ? "\u{1F50D}" : fn.type === "mutation" ? "\u270F\uFE0F" : "\u26A1";
168
+ console.log(chalk2.dim(` ${icon} ${fn.name} (${fn.type})`));
169
+ }
170
+ return;
171
+ }
172
+ spinner.text = "Deploying functions...";
173
+ const response = await fetch(`${API_URL}/${projectId}/deploy/functions`, {
174
+ method: "POST",
175
+ headers: {
176
+ "Content-Type": "application/json",
177
+ "Authorization": `Bearer ${token}`
178
+ },
179
+ body: JSON.stringify({
180
+ functions: functions.map((fn) => ({
181
+ name: fn.name,
182
+ type: fn.type,
183
+ source: fn.source
184
+ }))
185
+ })
186
+ });
187
+ if (!response.ok) {
188
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
189
+ throw new Error(error.error || `HTTP ${response.status}`);
190
+ }
191
+ spinner.succeed(`Functions deployed (${functions.length} function(s))`);
192
+ const queries = functions.filter((f) => f.type === "query");
193
+ const mutations = functions.filter((f) => f.type === "mutation");
194
+ const actions = functions.filter((f) => f.type === "action");
195
+ if (queries.length > 0) {
196
+ console.log(chalk2.dim(`
197
+ Queries:`));
198
+ for (const fn of queries) {
199
+ console.log(chalk2.dim(` - ${fn.name}`));
200
+ }
201
+ }
202
+ if (mutations.length > 0) {
203
+ console.log(chalk2.dim(`
204
+ Mutations:`));
205
+ for (const fn of mutations) {
206
+ console.log(chalk2.dim(` - ${fn.name}`));
207
+ }
208
+ }
209
+ if (actions.length > 0) {
210
+ console.log(chalk2.dim(`
211
+ Actions:`));
212
+ for (const fn of actions) {
213
+ console.log(chalk2.dim(` - ${fn.name}`));
214
+ }
215
+ }
216
+ } catch (error) {
217
+ spinner.fail("Failed to deploy functions");
218
+ console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
219
+ }
220
+ }
221
+ function parseSchema(source) {
222
+ const tables = [];
223
+ const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
224
+ if (!schemaMatch) return tables;
225
+ const schemaContent = schemaMatch[1];
226
+ const tableRegex = /(\w+)\s*:\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
227
+ let match;
228
+ while ((match = tableRegex.exec(schemaContent)) !== null) {
229
+ const tableName = match[1];
230
+ const columnsContent = match[2];
231
+ const columns = {};
232
+ const columnRegex = /(\w+)\s*:\s*(\w+)\s*\(\s*\)([^,\n]*)/g;
233
+ let colMatch;
234
+ while ((colMatch = columnRegex.exec(columnsContent)) !== null) {
235
+ const colName = colMatch[1];
236
+ const colType = colMatch[2];
237
+ const modifiers = colMatch[3];
238
+ columns[colName] = {
239
+ type: colType,
240
+ primaryKey: modifiers.includes(".primaryKey()"),
241
+ notNull: modifiers.includes(".notNull()"),
242
+ unique: modifiers.includes(".unique()"),
243
+ hasDefault: modifiers.includes(".default("),
244
+ references: modifiers.match(/\.references\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1]
245
+ };
246
+ }
247
+ tables.push({ name: tableName, columns });
248
+ }
249
+ return tables;
250
+ }
251
+ function generateSchemaSQL(tables) {
252
+ const statements = [];
253
+ for (const table of tables) {
254
+ const columnDefs = [];
255
+ for (const [colName, colDef] of Object.entries(table.columns)) {
256
+ const def = colDef;
257
+ let sqlType = "TEXT";
258
+ switch (def.type) {
259
+ case "text":
260
+ sqlType = "TEXT";
261
+ break;
262
+ case "integer":
263
+ sqlType = "INTEGER";
264
+ break;
265
+ case "real":
266
+ sqlType = "REAL";
267
+ break;
268
+ case "blob":
269
+ sqlType = "BLOB";
270
+ break;
271
+ case "timestamp":
272
+ sqlType = "TEXT";
273
+ break;
274
+ case "boolean":
275
+ sqlType = "INTEGER";
276
+ break;
277
+ case "json":
278
+ sqlType = "TEXT";
279
+ break;
280
+ }
281
+ let colSql = `${colName} ${sqlType}`;
282
+ if (def.primaryKey) colSql += " PRIMARY KEY";
283
+ if (def.notNull) colSql += " NOT NULL";
284
+ if (def.unique) colSql += " UNIQUE";
285
+ if (def.hasDefault && def.type === "timestamp") {
286
+ colSql += " DEFAULT (datetime('now'))";
287
+ }
288
+ if (def.references) {
289
+ const [refTable, refCol] = def.references.split(".");
290
+ colSql += ` REFERENCES ${refTable}(${refCol})`;
291
+ }
292
+ columnDefs.push(colSql);
293
+ }
294
+ statements.push(
295
+ `CREATE TABLE IF NOT EXISTS ${table.name} (
296
+ ${columnDefs.join(",\n ")}
297
+ );`
298
+ );
299
+ }
300
+ return statements.join("\n\n");
301
+ }
302
+ function parseFunctions(moduleName, source) {
303
+ const functions = [];
304
+ const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action)\s*\(/g;
305
+ let match;
306
+ while ((match = exportRegex.exec(source)) !== null) {
307
+ const fnName = match[1];
308
+ const fnType = match[2];
309
+ const startIndex = match.index + match[0].length;
310
+ let depth = 1;
311
+ let endIndex = startIndex;
312
+ for (let i = startIndex; i < source.length && depth > 0; i++) {
313
+ const char = source[i];
314
+ if (char === "(" || char === "{") {
315
+ depth++;
316
+ } else if (char === ")" || char === "}") {
317
+ depth--;
318
+ }
319
+ endIndex = i;
320
+ }
321
+ const fnBody = source.slice(startIndex, endIndex);
322
+ functions.push({
323
+ name: `${moduleName}.${fnName}`,
324
+ type: fnType,
325
+ file: `${moduleName}.ts`,
326
+ source: `${fnType}(${fnBody})`
327
+ });
328
+ }
329
+ return functions;
330
+ }
331
+
332
+ // src/commands/init.ts
333
+ var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
334
+ var API_URL2 = isDev2 ? "http://localhost:3001/api/v1" : "https://tthr.io/api/v1";
60
335
  async function getLatestVersion(packageName) {
61
336
  try {
62
337
  const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
@@ -71,7 +346,7 @@ async function getLatestVersion(packageName) {
71
346
  }
72
347
  async function initCommand(name, options) {
73
348
  const credentials = await requireAuth();
74
- console.log(chalk2.bold("\n\u26A1 Create a new Tether project\n"));
349
+ console.log(chalk3.bold("\n\u26A1 Create a new Tether project\n"));
75
350
  let projectName = name;
76
351
  if (!projectName) {
77
352
  const response = await prompts({
@@ -82,7 +357,7 @@ async function initCommand(name, options) {
82
357
  });
83
358
  projectName = response.name;
84
359
  if (!projectName) {
85
- console.log(chalk2.red("Project name is required"));
360
+ console.log(chalk3.red("Project name is required"));
86
361
  process.exit(1);
87
362
  }
88
363
  }
@@ -102,8 +377,8 @@ async function initCommand(name, options) {
102
377
  });
103
378
  template = response.template || "nuxt";
104
379
  }
105
- const projectPath = path2.resolve(process.cwd(), projectName);
106
- if (await fs2.pathExists(projectPath)) {
380
+ const projectPath = path3.resolve(process.cwd(), projectName);
381
+ if (await fs3.pathExists(projectPath)) {
107
382
  const { overwrite } = await prompts({
108
383
  type: "confirm",
109
384
  name: "overwrite",
@@ -111,16 +386,16 @@ async function initCommand(name, options) {
111
386
  initial: false
112
387
  });
113
388
  if (!overwrite) {
114
- console.log(chalk2.yellow("Cancelled"));
389
+ console.log(chalk3.yellow("Cancelled"));
115
390
  process.exit(0);
116
391
  }
117
- await fs2.remove(projectPath);
392
+ await fs3.remove(projectPath);
118
393
  }
119
- const spinner = ora("Creating project on Tether...").start();
394
+ const spinner = ora2("Creating project on Tether...").start();
120
395
  let projectId;
121
396
  let apiKey;
122
397
  try {
123
- const response = await fetch(`${API_URL}/projects`, {
398
+ const response = await fetch(`${API_URL2}/projects`, {
124
399
  method: "POST",
125
400
  headers: {
126
401
  "Content-Type": "application/json",
@@ -153,33 +428,39 @@ async function initCommand(name, options) {
153
428
  await installTetherPackages(projectPath, template);
154
429
  spinner.text = "Creating demo page...";
155
430
  await createDemoPage(projectPath, template);
156
- spinner.text = "Running initial migration...";
157
- await runInitialMigration(projectPath, projectId, apiKey);
158
431
  spinner.succeed("Project created successfully!");
159
- console.log("\n" + chalk2.green("\u2713") + " Project created successfully!\n");
432
+ console.log("");
433
+ const originalCwd = process.cwd();
434
+ process.chdir(projectPath);
435
+ try {
436
+ await deployCommand({});
437
+ } finally {
438
+ process.chdir(originalCwd);
439
+ }
440
+ console.log("\n" + chalk3.green("\u2713") + " Project created successfully!\n");
160
441
  console.log("Next steps:\n");
161
- console.log(chalk2.cyan(` cd ${projectName}`));
442
+ console.log(chalk3.cyan(` cd ${projectName}`));
162
443
  if (template === "vanilla") {
163
- console.log(chalk2.cyan(" npm install"));
444
+ console.log(chalk3.cyan(" npm install"));
164
445
  }
165
- console.log(chalk2.cyan(" npm run dev"));
166
- console.log("\n" + chalk2.dim("For more information, visit https://tthr.io/docs\n"));
446
+ console.log(chalk3.cyan(" npm run dev"));
447
+ console.log("\n" + chalk3.dim("For more information, visit https://tthr.io/docs\n"));
167
448
  } catch (error) {
168
449
  spinner.fail("Failed to create project");
169
- console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
450
+ console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
170
451
  process.exit(1);
171
452
  }
172
453
  }
173
454
  async function scaffoldNuxtProject(projectName, projectPath, spinner) {
174
- const parentDir = path2.dirname(projectPath);
455
+ const parentDir = path3.dirname(projectPath);
175
456
  spinner.stop();
176
- console.log(chalk2.dim("\nRunning nuxi init...\n"));
457
+ console.log(chalk3.dim("\nRunning nuxi init...\n"));
177
458
  try {
178
459
  execSync(`npx nuxi@latest init ${projectName} --packageManager npm --gitInit false`, {
179
460
  cwd: parentDir,
180
461
  stdio: "inherit"
181
462
  });
182
- if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
463
+ if (!await fs3.pathExists(path3.join(projectPath, "package.json"))) {
183
464
  throw new Error("Nuxt project was not created successfully - package.json missing");
184
465
  }
185
466
  spinner.start();
@@ -192,15 +473,15 @@ async function scaffoldNuxtProject(projectName, projectPath, spinner) {
192
473
  }
193
474
  }
194
475
  async function scaffoldNextProject(projectName, projectPath, spinner) {
195
- const parentDir = path2.dirname(projectPath);
476
+ const parentDir = path3.dirname(projectPath);
196
477
  spinner.stop();
197
- console.log(chalk2.dim("\nRunning create-next-app...\n"));
478
+ console.log(chalk3.dim("\nRunning create-next-app...\n"));
198
479
  try {
199
480
  execSync(`npx create-next-app@latest ${projectName} --typescript --eslint --tailwind --src-dir --app --import-alias "@/*" --use-npm`, {
200
481
  cwd: parentDir,
201
482
  stdio: "inherit"
202
483
  });
203
- if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
484
+ if (!await fs3.pathExists(path3.join(projectPath, "package.json"))) {
204
485
  throw new Error("Next.js project was not created successfully - package.json missing");
205
486
  }
206
487
  spinner.start();
@@ -213,15 +494,15 @@ async function scaffoldNextProject(projectName, projectPath, spinner) {
213
494
  }
214
495
  }
215
496
  async function scaffoldSvelteKitProject(projectName, projectPath, spinner) {
216
- const parentDir = path2.dirname(projectPath);
497
+ const parentDir = path3.dirname(projectPath);
217
498
  spinner.stop();
218
- console.log(chalk2.dim("\nRunning sv create...\n"));
499
+ console.log(chalk3.dim("\nRunning sv create...\n"));
219
500
  try {
220
501
  execSync(`npx sv create ${projectName} --template minimal --types ts --no-add-ons --no-install`, {
221
502
  cwd: parentDir,
222
503
  stdio: "inherit"
223
504
  });
224
- if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
505
+ if (!await fs3.pathExists(path3.join(projectPath, "package.json"))) {
225
506
  throw new Error("SvelteKit project was not created successfully - package.json missing");
226
507
  }
227
508
  spinner.start();
@@ -235,8 +516,8 @@ async function scaffoldSvelteKitProject(projectName, projectPath, spinner) {
235
516
  }
236
517
  async function scaffoldVanillaProject(projectName, projectPath, spinner) {
237
518
  spinner.text = "Creating vanilla TypeScript project...";
238
- await fs2.ensureDir(projectPath);
239
- await fs2.ensureDir(path2.join(projectPath, "src"));
519
+ await fs3.ensureDir(projectPath);
520
+ await fs3.ensureDir(path3.join(projectPath, "src"));
240
521
  const packageJson = {
241
522
  name: projectName,
242
523
  version: "0.0.1",
@@ -253,9 +534,9 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
253
534
  "@types/node": "latest"
254
535
  }
255
536
  };
256
- await fs2.writeJSON(path2.join(projectPath, "package.json"), packageJson, { spaces: 2 });
257
- await fs2.writeJSON(
258
- path2.join(projectPath, "tsconfig.json"),
537
+ await fs3.writeJSON(path3.join(projectPath, "package.json"), packageJson, { spaces: 2 });
538
+ await fs3.writeJSON(
539
+ path3.join(projectPath, "tsconfig.json"),
259
540
  {
260
541
  compilerOptions: {
261
542
  target: "ES2022",
@@ -276,8 +557,8 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
276
557
  },
277
558
  { spaces: 2 }
278
559
  );
279
- await fs2.writeFile(
280
- path2.join(projectPath, "src", "index.ts"),
560
+ await fs3.writeFile(
561
+ path3.join(projectPath, "src", "index.ts"),
281
562
  `import { createClient } from '@tthr/client';
282
563
 
283
564
  const tether = createClient({
@@ -296,8 +577,8 @@ async function main() {
296
577
  main().catch(console.error);
297
578
  `
298
579
  );
299
- await fs2.writeFile(
300
- path2.join(projectPath, ".gitignore"),
580
+ await fs3.writeFile(
581
+ path3.join(projectPath, ".gitignore"),
301
582
  `node_modules/
302
583
  dist/
303
584
  .env
@@ -307,11 +588,11 @@ dist/
307
588
  );
308
589
  }
309
590
  async function addTetherFiles(projectPath, projectId, apiKey, template) {
310
- await fs2.ensureDir(path2.join(projectPath, "tether"));
311
- await fs2.ensureDir(path2.join(projectPath, "tether", "functions"));
591
+ await fs3.ensureDir(path3.join(projectPath, "tether"));
592
+ await fs3.ensureDir(path3.join(projectPath, "tether", "functions"));
312
593
  const configPackage = template === "nuxt" ? "@tthr/vue" : template === "next" ? "@tthr/react" : template === "sveltekit" ? "@tthr/svelte" : "@tthr/client";
313
- await fs2.writeFile(
314
- path2.join(projectPath, "tether.config.ts"),
594
+ await fs3.writeFile(
595
+ path3.join(projectPath, "tether.config.ts"),
315
596
  `import { defineConfig } from '${configPackage}';
316
597
 
317
598
  export default defineConfig({
@@ -332,8 +613,8 @@ export default defineConfig({
332
613
  });
333
614
  `
334
615
  );
335
- await fs2.writeFile(
336
- path2.join(projectPath, "tether", "schema.ts"),
616
+ await fs3.writeFile(
617
+ path3.join(projectPath, "tether", "schema.ts"),
337
618
  `import { defineSchema, text, integer, timestamp } from '@tthr/schema';
338
619
 
339
620
  export default defineSchema({
@@ -358,8 +639,8 @@ export default defineSchema({
358
639
  });
359
640
  `
360
641
  );
361
- await fs2.writeFile(
362
- path2.join(projectPath, "tether", "functions", "posts.ts"),
642
+ await fs3.writeFile(
643
+ path3.join(projectPath, "tether", "functions", "posts.ts"),
363
644
  `import { query, mutation, z } from '@tthr/server';
364
645
 
365
646
  // List all posts
@@ -446,30 +727,30 @@ export const remove = mutation({
446
727
  );
447
728
  const envContent = `# Tether Configuration
448
729
  TETHER_PROJECT_ID=${projectId}
449
- TETHER_URL=${isDev ? "http://localhost:3001" : "https://tthr.io"}
730
+ TETHER_URL=${isDev2 ? "http://localhost:3001" : "https://tthr.io"}
450
731
  TETHER_API_KEY=${apiKey}
451
732
  `;
452
- const envPath = path2.join(projectPath, ".env");
453
- if (await fs2.pathExists(envPath)) {
454
- const existing = await fs2.readFile(envPath, "utf-8");
455
- await fs2.writeFile(envPath, existing + "\n" + envContent);
733
+ const envPath = path3.join(projectPath, ".env");
734
+ if (await fs3.pathExists(envPath)) {
735
+ const existing = await fs3.readFile(envPath, "utf-8");
736
+ await fs3.writeFile(envPath, existing + "\n" + envContent);
456
737
  } else {
457
- await fs2.writeFile(envPath, envContent);
738
+ await fs3.writeFile(envPath, envContent);
458
739
  }
459
- const gitignorePath = path2.join(projectPath, ".gitignore");
740
+ const gitignorePath = path3.join(projectPath, ".gitignore");
460
741
  const tetherGitignore = `
461
742
  # Tether
462
743
  _generated/
463
744
  .env
464
745
  .env.local
465
746
  `;
466
- if (await fs2.pathExists(gitignorePath)) {
467
- const existing = await fs2.readFile(gitignorePath, "utf-8");
747
+ if (await fs3.pathExists(gitignorePath)) {
748
+ const existing = await fs3.readFile(gitignorePath, "utf-8");
468
749
  if (!existing.includes("_generated/")) {
469
- await fs2.writeFile(gitignorePath, existing + tetherGitignore);
750
+ await fs3.writeFile(gitignorePath, existing + tetherGitignore);
470
751
  }
471
752
  } else {
472
- await fs2.writeFile(gitignorePath, tetherGitignore.trim());
753
+ await fs3.writeFile(gitignorePath, tetherGitignore.trim());
473
754
  }
474
755
  }
475
756
  async function configureFramework(projectPath, template) {
@@ -482,9 +763,9 @@ async function configureFramework(projectPath, template) {
482
763
  }
483
764
  }
484
765
  async function configureNuxt(projectPath) {
485
- const configPath = path2.join(projectPath, "nuxt.config.ts");
486
- if (!await fs2.pathExists(configPath)) {
487
- await fs2.writeFile(
766
+ const configPath = path3.join(projectPath, "nuxt.config.ts");
767
+ if (!await fs3.pathExists(configPath)) {
768
+ await fs3.writeFile(
488
769
  configPath,
489
770
  `// https://nuxt.com/docs/api/configuration/nuxt-config
490
771
  export default defineNuxtConfig({
@@ -502,7 +783,7 @@ export default defineNuxtConfig({
502
783
  );
503
784
  return;
504
785
  }
505
- let config = await fs2.readFile(configPath, "utf-8");
786
+ let config = await fs3.readFile(configPath, "utf-8");
506
787
  if (config.includes("modules:")) {
507
788
  config = config.replace(
508
789
  /modules:\s*\[/,
@@ -529,11 +810,11 @@ export default defineNuxtConfig({
529
810
  `
530
811
  );
531
812
  }
532
- await fs2.writeFile(configPath, config);
813
+ await fs3.writeFile(configPath, config);
533
814
  }
534
815
  async function configureNext(projectPath) {
535
- const providersPath = path2.join(projectPath, "src", "app", "providers.tsx");
536
- await fs2.writeFile(
816
+ const providersPath = path3.join(projectPath, "src", "app", "providers.tsx");
817
+ await fs3.writeFile(
537
818
  providersPath,
538
819
  `'use client';
539
820
 
@@ -551,9 +832,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
551
832
  }
552
833
  `
553
834
  );
554
- const layoutPath = path2.join(projectPath, "src", "app", "layout.tsx");
555
- if (await fs2.pathExists(layoutPath)) {
556
- let layout = await fs2.readFile(layoutPath, "utf-8");
835
+ const layoutPath = path3.join(projectPath, "src", "app", "layout.tsx");
836
+ if (await fs3.pathExists(layoutPath)) {
837
+ let layout = await fs3.readFile(layoutPath, "utf-8");
557
838
  if (!layout.includes("import { Providers }")) {
558
839
  layout = layout.replace(
559
840
  /^(import.*\n)+/m,
@@ -566,25 +847,25 @@ export function Providers({ children }: { children: React.ReactNode }) {
566
847
  "$1<Providers>$2</Providers>$3"
567
848
  );
568
849
  }
569
- await fs2.writeFile(layoutPath, layout);
850
+ await fs3.writeFile(layoutPath, layout);
570
851
  }
571
- const envLocalPath = path2.join(projectPath, ".env.local");
852
+ const envLocalPath = path3.join(projectPath, ".env.local");
572
853
  const nextEnvContent = `# Tether Configuration (client-side)
573
854
  NEXT_PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
574
855
  NEXT_PUBLIC_TETHER_URL=\${TETHER_URL}
575
856
  `;
576
- if (await fs2.pathExists(envLocalPath)) {
577
- const existing = await fs2.readFile(envLocalPath, "utf-8");
578
- await fs2.writeFile(envLocalPath, existing + "\n" + nextEnvContent);
857
+ if (await fs3.pathExists(envLocalPath)) {
858
+ const existing = await fs3.readFile(envLocalPath, "utf-8");
859
+ await fs3.writeFile(envLocalPath, existing + "\n" + nextEnvContent);
579
860
  } else {
580
- await fs2.writeFile(envLocalPath, nextEnvContent);
861
+ await fs3.writeFile(envLocalPath, nextEnvContent);
581
862
  }
582
863
  }
583
864
  async function configureSvelteKit(projectPath) {
584
- const libPath = path2.join(projectPath, "src", "lib");
585
- await fs2.ensureDir(libPath);
586
- await fs2.writeFile(
587
- path2.join(libPath, "tether.ts"),
865
+ const libPath = path3.join(projectPath, "src", "lib");
866
+ await fs3.ensureDir(libPath);
867
+ await fs3.writeFile(
868
+ path3.join(libPath, "tether.ts"),
588
869
  `import { createClient } from '@tthr/svelte';
589
870
  import { PUBLIC_TETHER_PROJECT_ID, PUBLIC_TETHER_URL } from '$env/static/public';
590
871
 
@@ -594,15 +875,15 @@ export const tether = createClient({
594
875
  });
595
876
  `
596
877
  );
597
- const envPath = path2.join(projectPath, ".env");
878
+ const envPath = path3.join(projectPath, ".env");
598
879
  const svelteEnvContent = `# Tether Configuration (public)
599
880
  PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
600
881
  PUBLIC_TETHER_URL=\${TETHER_URL}
601
882
  `;
602
- if (await fs2.pathExists(envPath)) {
603
- const existing = await fs2.readFile(envPath, "utf-8");
883
+ if (await fs3.pathExists(envPath)) {
884
+ const existing = await fs3.readFile(envPath, "utf-8");
604
885
  if (!existing.includes("PUBLIC_TETHER_")) {
605
- await fs2.writeFile(envPath, existing + "\n" + svelteEnvContent);
886
+ await fs3.writeFile(envPath, existing + "\n" + svelteEnvContent);
606
887
  }
607
888
  }
608
889
  }
@@ -646,15 +927,15 @@ async function installTetherPackages(projectPath, template) {
646
927
  stdio: "pipe"
647
928
  });
648
929
  } catch (error) {
649
- console.warn(chalk2.yellow("\nNote: Some Tether packages may not be published yet."));
930
+ console.warn(chalk3.yellow("\nNote: Some Tether packages may not be published yet."));
650
931
  }
651
932
  }
652
933
  async function createDemoPage(projectPath, template) {
653
934
  if (template === "nuxt") {
654
- const nuxt4AppVuePath = path2.join(projectPath, "app", "app.vue");
655
- const nuxt3AppVuePath = path2.join(projectPath, "app.vue");
656
- const appVuePath = await fs2.pathExists(path2.join(projectPath, "app")) ? nuxt4AppVuePath : nuxt3AppVuePath;
657
- await fs2.writeFile(
935
+ const nuxt4AppVuePath = path3.join(projectPath, "app", "app.vue");
936
+ const nuxt3AppVuePath = path3.join(projectPath, "app.vue");
937
+ const appVuePath = await fs3.pathExists(path3.join(projectPath, "app")) ? nuxt4AppVuePath : nuxt3AppVuePath;
938
+ await fs3.writeFile(
658
939
  appVuePath,
659
940
  `<template>
660
941
  <TetherWelcome />
@@ -662,8 +943,8 @@ async function createDemoPage(projectPath, template) {
662
943
  `
663
944
  );
664
945
  } else if (template === "next") {
665
- const pagePath = path2.join(projectPath, "src", "app", "page.tsx");
666
- await fs2.writeFile(
946
+ const pagePath = path3.join(projectPath, "src", "app", "page.tsx");
947
+ await fs3.writeFile(
667
948
  pagePath,
668
949
  `'use client';
669
950
 
@@ -773,8 +1054,8 @@ export default function Home() {
773
1054
  `
774
1055
  );
775
1056
  } else if (template === "sveltekit") {
776
- const pagePath = path2.join(projectPath, "src", "routes", "+page.svelte");
777
- await fs2.writeFile(
1057
+ const pagePath = path3.join(projectPath, "src", "routes", "+page.svelte");
1058
+ await fs3.writeFile(
778
1059
  pagePath,
779
1060
  `<script lang="ts">
780
1061
  import { onMount } from 'svelte';
@@ -1038,109 +1319,69 @@ export default function Home() {
1038
1319
  );
1039
1320
  }
1040
1321
  }
1041
- async function runInitialMigration(projectPath, projectId, apiKey) {
1042
- const migrationSql = `
1043
- CREATE TABLE IF NOT EXISTS posts (
1044
- id TEXT PRIMARY KEY,
1045
- title TEXT NOT NULL,
1046
- content TEXT,
1047
- authorId TEXT NOT NULL,
1048
- createdAt TEXT NOT NULL DEFAULT (datetime('now')),
1049
- updatedAt TEXT NOT NULL DEFAULT (datetime('now'))
1050
- );
1051
-
1052
- CREATE TABLE IF NOT EXISTS comments (
1053
- id TEXT PRIMARY KEY,
1054
- postId TEXT NOT NULL REFERENCES posts(id),
1055
- content TEXT NOT NULL,
1056
- authorId TEXT NOT NULL,
1057
- createdAt TEXT NOT NULL DEFAULT (datetime('now'))
1058
- );
1059
-
1060
- CREATE INDEX IF NOT EXISTS idx_posts_authorId ON posts(authorId);
1061
- CREATE INDEX IF NOT EXISTS idx_posts_createdAt ON posts(createdAt);
1062
- CREATE INDEX IF NOT EXISTS idx_comments_postId ON comments(postId);
1063
- `;
1064
- try {
1065
- const response = await fetch(`${API_URL}/${projectId}/mutation`, {
1066
- method: "POST",
1067
- headers: {
1068
- "Content-Type": "application/json",
1069
- "Authorization": `Bearer ${apiKey}`
1070
- },
1071
- body: JSON.stringify({
1072
- function: "_migrate",
1073
- args: { sql: migrationSql }
1074
- })
1075
- });
1076
- if (!response.ok) {
1077
- }
1078
- } catch {
1079
- }
1080
- }
1081
1322
 
1082
1323
  // src/commands/dev.ts
1083
- import chalk3 from "chalk";
1084
- import ora2 from "ora";
1085
- import fs3 from "fs-extra";
1086
- import path3 from "path";
1324
+ import chalk4 from "chalk";
1325
+ import ora3 from "ora";
1326
+ import fs4 from "fs-extra";
1327
+ import path4 from "path";
1087
1328
  async function devCommand(options) {
1088
1329
  await requireAuth();
1089
- const configPath = path3.resolve(process.cwd(), "tether.config.ts");
1090
- if (!await fs3.pathExists(configPath)) {
1091
- console.log(chalk3.red("\nError: Not a Tether project"));
1092
- console.log(chalk3.dim("Run `tthr init` to create a new project\n"));
1330
+ const configPath = path4.resolve(process.cwd(), "tether.config.ts");
1331
+ if (!await fs4.pathExists(configPath)) {
1332
+ console.log(chalk4.red("\nError: Not a Tether project"));
1333
+ console.log(chalk4.dim("Run `tthr init` to create a new project\n"));
1093
1334
  process.exit(1);
1094
1335
  }
1095
- console.log(chalk3.bold("\n\u26A1 Starting Tether development server\n"));
1096
- const spinner = ora2("Starting server...").start();
1336
+ console.log(chalk4.bold("\n\u26A1 Starting Tether development server\n"));
1337
+ const spinner = ora3("Starting server...").start();
1097
1338
  try {
1098
1339
  spinner.succeed(`Development server running on port ${options.port}`);
1099
- console.log("\n" + chalk3.cyan(` Local: http://localhost:${options.port}`));
1100
- console.log(chalk3.cyan(` WebSocket: ws://localhost:${options.port}/ws
1340
+ console.log("\n" + chalk4.cyan(` Local: http://localhost:${options.port}`));
1341
+ console.log(chalk4.cyan(` WebSocket: ws://localhost:${options.port}/ws
1101
1342
  `));
1102
- console.log(chalk3.dim(" Press Ctrl+C to stop\n"));
1343
+ console.log(chalk4.dim(" Press Ctrl+C to stop\n"));
1103
1344
  process.on("SIGINT", () => {
1104
- console.log(chalk3.yellow("\n\nShutting down...\n"));
1345
+ console.log(chalk4.yellow("\n\nShutting down...\n"));
1105
1346
  process.exit(0);
1106
1347
  });
1107
1348
  await new Promise(() => {
1108
1349
  });
1109
1350
  } catch (error) {
1110
1351
  spinner.fail("Failed to start server");
1111
- console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
1352
+ console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
1112
1353
  process.exit(1);
1113
1354
  }
1114
1355
  }
1115
1356
 
1116
1357
  // src/commands/generate.ts
1117
- import chalk4 from "chalk";
1118
- import ora3 from "ora";
1119
- import fs4 from "fs-extra";
1120
- import path4 from "path";
1358
+ import chalk5 from "chalk";
1359
+ import ora4 from "ora";
1360
+ import fs5 from "fs-extra";
1361
+ import path5 from "path";
1121
1362
  async function generateCommand() {
1122
1363
  await requireAuth();
1123
- const configPath = path4.resolve(process.cwd(), "tether.config.ts");
1124
- if (!await fs4.pathExists(configPath)) {
1125
- console.log(chalk4.red("\nError: Not a Tether project"));
1126
- console.log(chalk4.dim("Run `tthr init` to create a new project\n"));
1364
+ const configPath = path5.resolve(process.cwd(), "tether.config.ts");
1365
+ if (!await fs5.pathExists(configPath)) {
1366
+ console.log(chalk5.red("\nError: Not a Tether project"));
1367
+ console.log(chalk5.dim("Run `tthr init` to create a new project\n"));
1127
1368
  process.exit(1);
1128
1369
  }
1129
- console.log(chalk4.bold("\n\u26A1 Generating types from schema\n"));
1130
- const spinner = ora3("Reading schema...").start();
1370
+ console.log(chalk5.bold("\n\u26A1 Generating types from schema\n"));
1371
+ const spinner = ora4("Reading schema...").start();
1131
1372
  try {
1132
- const schemaPath = path4.resolve(process.cwd(), "tether", "schema.ts");
1133
- const outputDir = path4.resolve(process.cwd(), "_generated");
1134
- if (!await fs4.pathExists(schemaPath)) {
1373
+ const schemaPath = path5.resolve(process.cwd(), "tether", "schema.ts");
1374
+ const outputDir = path5.resolve(process.cwd(), "_generated");
1375
+ if (!await fs5.pathExists(schemaPath)) {
1135
1376
  spinner.fail("Schema file not found");
1136
- console.log(chalk4.dim(`Expected: ${schemaPath}
1377
+ console.log(chalk5.dim(`Expected: ${schemaPath}
1137
1378
  `));
1138
1379
  process.exit(1);
1139
1380
  }
1140
1381
  spinner.text = "Generating types...";
1141
- await fs4.ensureDir(outputDir);
1142
- await fs4.writeFile(
1143
- path4.join(outputDir, "db.ts"),
1382
+ await fs5.ensureDir(outputDir);
1383
+ await fs5.writeFile(
1384
+ path5.join(outputDir, "db.ts"),
1144
1385
  `// Auto-generated by Tether CLI - do not edit manually
1145
1386
  // Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
1146
1387
 
@@ -1172,8 +1413,8 @@ export interface Schema {
1172
1413
  export declare const db: TetherDatabase<Schema>;
1173
1414
  `
1174
1415
  );
1175
- await fs4.writeFile(
1176
- path4.join(outputDir, "api.ts"),
1416
+ await fs5.writeFile(
1417
+ path5.join(outputDir, "api.ts"),
1177
1418
  `// Auto-generated by Tether CLI - do not edit manually
1178
1419
  // Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
1179
1420
 
@@ -1196,40 +1437,40 @@ export interface Api {
1196
1437
  export declare const tether: TetherClient<Api>;
1197
1438
  `
1198
1439
  );
1199
- await fs4.writeFile(
1200
- path4.join(outputDir, "index.ts"),
1440
+ await fs5.writeFile(
1441
+ path5.join(outputDir, "index.ts"),
1201
1442
  `// Auto-generated by Tether CLI - do not edit manually
1202
1443
  export * from './db';
1203
1444
  export * from './api';
1204
1445
  `
1205
1446
  );
1206
1447
  spinner.succeed("Types generated");
1207
- console.log("\n" + chalk4.green("\u2713") + " Generated files:");
1208
- console.log(chalk4.dim(" _generated/db.ts"));
1209
- console.log(chalk4.dim(" _generated/api.ts"));
1210
- console.log(chalk4.dim(" _generated/index.ts\n"));
1448
+ console.log("\n" + chalk5.green("\u2713") + " Generated files:");
1449
+ console.log(chalk5.dim(" _generated/db.ts"));
1450
+ console.log(chalk5.dim(" _generated/api.ts"));
1451
+ console.log(chalk5.dim(" _generated/index.ts\n"));
1211
1452
  } catch (error) {
1212
1453
  spinner.fail("Failed to generate types");
1213
- console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
1454
+ console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
1214
1455
  process.exit(1);
1215
1456
  }
1216
1457
  }
1217
1458
 
1218
1459
  // src/commands/migrate.ts
1219
- import chalk5 from "chalk";
1220
- import ora4 from "ora";
1221
- import fs5 from "fs-extra";
1222
- import path5 from "path";
1460
+ import chalk6 from "chalk";
1461
+ import ora5 from "ora";
1462
+ import fs6 from "fs-extra";
1463
+ import path6 from "path";
1223
1464
  import prompts2 from "prompts";
1224
1465
  async function migrateCommand(action, options) {
1225
1466
  await requireAuth();
1226
- const configPath = path5.resolve(process.cwd(), "tether.config.ts");
1227
- if (!await fs5.pathExists(configPath)) {
1228
- console.log(chalk5.red("\nError: Not a Tether project"));
1229
- console.log(chalk5.dim("Run `tthr init` to create a new project\n"));
1467
+ const configPath = path6.resolve(process.cwd(), "tether.config.ts");
1468
+ if (!await fs6.pathExists(configPath)) {
1469
+ console.log(chalk6.red("\nError: Not a Tether project"));
1470
+ console.log(chalk6.dim("Run `tthr init` to create a new project\n"));
1230
1471
  process.exit(1);
1231
1472
  }
1232
- const migrationsDir = path5.resolve(process.cwd(), "tether", "migrations");
1473
+ const migrationsDir = path6.resolve(process.cwd(), "tether", "migrations");
1233
1474
  switch (action) {
1234
1475
  case "create":
1235
1476
  await createMigration(migrationsDir, options.name);
@@ -1247,7 +1488,7 @@ async function migrateCommand(action, options) {
1247
1488
  }
1248
1489
  }
1249
1490
  async function createMigration(migrationsDir, name) {
1250
- console.log(chalk5.bold("\n\u26A1 Create migration\n"));
1491
+ console.log(chalk6.bold("\n\u26A1 Create migration\n"));
1251
1492
  let migrationName = name;
1252
1493
  if (!migrationName) {
1253
1494
  const response = await prompts2({
@@ -1258,17 +1499,17 @@ async function createMigration(migrationsDir, name) {
1258
1499
  });
1259
1500
  migrationName = response.name;
1260
1501
  if (!migrationName) {
1261
- console.log(chalk5.red("Migration name is required"));
1502
+ console.log(chalk6.red("Migration name is required"));
1262
1503
  process.exit(1);
1263
1504
  }
1264
1505
  }
1265
- const spinner = ora4("Creating migration...").start();
1506
+ const spinner = ora5("Creating migration...").start();
1266
1507
  try {
1267
- await fs5.ensureDir(migrationsDir);
1508
+ await fs6.ensureDir(migrationsDir);
1268
1509
  const timestamp = Date.now();
1269
1510
  const filename = `${timestamp}_${migrationName}.sql`;
1270
- const filepath = path5.join(migrationsDir, filename);
1271
- await fs5.writeFile(
1511
+ const filepath = path6.join(migrationsDir, filename);
1512
+ await fs6.writeFile(
1272
1513
  filepath,
1273
1514
  `-- Migration: ${migrationName}
1274
1515
  -- Created at: ${(/* @__PURE__ */ new Date()).toISOString()}
@@ -1289,143 +1530,143 @@ async function createMigration(migrationsDir, name) {
1289
1530
  `
1290
1531
  );
1291
1532
  spinner.succeed("Migration created");
1292
- console.log(chalk5.dim(`
1533
+ console.log(chalk6.dim(`
1293
1534
  ${filepath}
1294
1535
  `));
1295
1536
  } catch (error) {
1296
1537
  spinner.fail("Failed to create migration");
1297
- console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
1538
+ console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1298
1539
  process.exit(1);
1299
1540
  }
1300
1541
  }
1301
1542
  async function runMigrations(migrationsDir, direction) {
1302
- console.log(chalk5.bold(`
1543
+ console.log(chalk6.bold(`
1303
1544
  \u26A1 Running migrations (${direction})
1304
1545
  `));
1305
- const spinner = ora4("Checking migrations...").start();
1546
+ const spinner = ora5("Checking migrations...").start();
1306
1547
  try {
1307
- if (!await fs5.pathExists(migrationsDir)) {
1548
+ if (!await fs6.pathExists(migrationsDir)) {
1308
1549
  spinner.info("No migrations directory found");
1309
- console.log(chalk5.dim("\nRun `tthr migrate create` to create your first migration\n"));
1550
+ console.log(chalk6.dim("\nRun `tthr migrate create` to create your first migration\n"));
1310
1551
  return;
1311
1552
  }
1312
- const files = await fs5.readdir(migrationsDir);
1553
+ const files = await fs6.readdir(migrationsDir);
1313
1554
  const migrations = files.filter((f) => f.endsWith(".sql")).sort((a, b) => direction === "up" ? a.localeCompare(b) : b.localeCompare(a));
1314
1555
  if (migrations.length === 0) {
1315
1556
  spinner.info("No migrations found");
1316
- console.log(chalk5.dim("\nRun `tthr migrate create` to create your first migration\n"));
1557
+ console.log(chalk6.dim("\nRun `tthr migrate create` to create your first migration\n"));
1317
1558
  return;
1318
1559
  }
1319
1560
  spinner.text = `Found ${migrations.length} migration(s)`;
1320
1561
  spinner.succeed(`Would run ${migrations.length} migration(s)`);
1321
1562
  console.log("\nMigrations to run:");
1322
1563
  for (const migration of migrations) {
1323
- console.log(chalk5.dim(` ${migration}`));
1564
+ console.log(chalk6.dim(` ${migration}`));
1324
1565
  }
1325
- console.log(chalk5.yellow("\nNote: Migration execution not yet implemented\n"));
1566
+ console.log(chalk6.yellow("\nNote: Migration execution not yet implemented\n"));
1326
1567
  } catch (error) {
1327
1568
  spinner.fail("Failed to run migrations");
1328
- console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
1569
+ console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1329
1570
  process.exit(1);
1330
1571
  }
1331
1572
  }
1332
1573
  async function showStatus(migrationsDir) {
1333
- console.log(chalk5.bold("\n\u26A1 Migration status\n"));
1574
+ console.log(chalk6.bold("\n\u26A1 Migration status\n"));
1334
1575
  try {
1335
- if (!await fs5.pathExists(migrationsDir)) {
1336
- console.log(chalk5.dim("No migrations directory found"));
1337
- console.log(chalk5.dim("\nRun `tthr migrate create` to create your first migration\n"));
1576
+ if (!await fs6.pathExists(migrationsDir)) {
1577
+ console.log(chalk6.dim("No migrations directory found"));
1578
+ console.log(chalk6.dim("\nRun `tthr migrate create` to create your first migration\n"));
1338
1579
  return;
1339
1580
  }
1340
- const files = await fs5.readdir(migrationsDir);
1581
+ const files = await fs6.readdir(migrationsDir);
1341
1582
  const migrations = files.filter((f) => f.endsWith(".sql")).sort();
1342
1583
  if (migrations.length === 0) {
1343
- console.log(chalk5.dim("No migrations found"));
1344
- console.log(chalk5.dim("\nRun `tthr migrate create` to create your first migration\n"));
1584
+ console.log(chalk6.dim("No migrations found"));
1585
+ console.log(chalk6.dim("\nRun `tthr migrate create` to create your first migration\n"));
1345
1586
  return;
1346
1587
  }
1347
1588
  console.log(`Found ${migrations.length} migration(s):
1348
1589
  `);
1349
1590
  for (const migration of migrations) {
1350
- console.log(chalk5.yellow(" \u25CB") + ` ${migration} ${chalk5.dim("(pending)")}`);
1591
+ console.log(chalk6.yellow(" \u25CB") + ` ${migration} ${chalk6.dim("(pending)")}`);
1351
1592
  }
1352
1593
  console.log();
1353
1594
  } catch (error) {
1354
- console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
1595
+ console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1355
1596
  process.exit(1);
1356
1597
  }
1357
1598
  }
1358
1599
 
1359
1600
  // src/commands/login.ts
1360
- import chalk6 from "chalk";
1361
- import ora5 from "ora";
1362
- var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
1363
- var API_URL2 = isDev2 ? "http://localhost:3001/api/v1" : "https://tthr.io/api/v1";
1364
- var AUTH_URL = isDev2 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
1601
+ import chalk7 from "chalk";
1602
+ import ora6 from "ora";
1603
+ var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
1604
+ var API_URL3 = isDev3 ? "http://localhost:3001/api/v1" : "https://tthr.io/api/v1";
1605
+ var AUTH_URL = isDev3 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
1365
1606
  async function loginCommand() {
1366
- console.log(chalk6.bold("\u26A1 Login to Tether\n"));
1607
+ console.log(chalk7.bold("\u26A1 Login to Tether\n"));
1367
1608
  const existing = await getCredentials();
1368
1609
  if (existing) {
1369
- console.log(chalk6.green("\u2713") + ` Already logged in as ${chalk6.cyan(existing.email)}`);
1370
- console.log(chalk6.dim("\nRun `tthr logout` to sign out\n"));
1610
+ console.log(chalk7.green("\u2713") + ` Already logged in as ${chalk7.cyan(existing.email)}`);
1611
+ console.log(chalk7.dim("\nRun `tthr logout` to sign out\n"));
1371
1612
  return;
1372
1613
  }
1373
- const spinner = ora5("Generating authentication code...").start();
1614
+ const spinner = ora6("Generating authentication code...").start();
1374
1615
  try {
1375
1616
  const deviceCode = await requestDeviceCode();
1376
1617
  spinner.stop();
1377
1618
  const authUrl = `${AUTH_URL}/${deviceCode.userCode}`;
1378
- console.log(chalk6.dim("Open this URL in your browser to authenticate:\n"));
1379
- console.log(chalk6.cyan.bold(` ${authUrl}
1619
+ console.log(chalk7.dim("Open this URL in your browser to authenticate:\n"));
1620
+ console.log(chalk7.cyan.bold(` ${authUrl}
1380
1621
  `));
1381
- console.log(chalk6.dim(`Your code: ${chalk6.white.bold(deviceCode.userCode)}
1622
+ console.log(chalk7.dim(`Your code: ${chalk7.white.bold(deviceCode.userCode)}
1382
1623
  `));
1383
1624
  const credentials = await pollForApproval(deviceCode.deviceCode, deviceCode.interval, deviceCode.expiresIn);
1384
1625
  await saveCredentials(credentials);
1385
- console.log(chalk6.green("\u2713") + ` Logged in as ${chalk6.cyan(credentials.email)}`);
1626
+ console.log(chalk7.green("\u2713") + ` Logged in as ${chalk7.cyan(credentials.email)}`);
1386
1627
  console.log();
1387
1628
  } catch (error) {
1388
1629
  spinner.fail("Authentication failed");
1389
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1390
- console.log(chalk6.dim("\nTry again or visit https://tthr.io/docs/cli for help\n"));
1630
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1631
+ console.log(chalk7.dim("\nTry again or visit https://tthr.io/docs/cli for help\n"));
1391
1632
  process.exit(1);
1392
1633
  }
1393
1634
  }
1394
1635
  async function logoutCommand() {
1395
- console.log(chalk6.bold("\n\u26A1 Logout from Tether\n"));
1636
+ console.log(chalk7.bold("\n\u26A1 Logout from Tether\n"));
1396
1637
  const credentials = await getCredentials();
1397
1638
  if (!credentials) {
1398
- console.log(chalk6.dim("Not logged in\n"));
1639
+ console.log(chalk7.dim("Not logged in\n"));
1399
1640
  return;
1400
1641
  }
1401
- const spinner = ora5("Logging out...").start();
1642
+ const spinner = ora6("Logging out...").start();
1402
1643
  try {
1403
1644
  await clearCredentials();
1404
- spinner.succeed(`Logged out from ${chalk6.cyan(credentials.email)}`);
1645
+ spinner.succeed(`Logged out from ${chalk7.cyan(credentials.email)}`);
1405
1646
  console.log();
1406
1647
  } catch (error) {
1407
1648
  spinner.fail("Logout failed");
1408
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1649
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1409
1650
  process.exit(1);
1410
1651
  }
1411
1652
  }
1412
1653
  async function whoamiCommand() {
1413
1654
  const credentials = await getCredentials();
1414
1655
  if (!credentials) {
1415
- console.log(chalk6.dim("\nNot logged in"));
1416
- console.log(chalk6.dim("Run `tthr login` to authenticate\n"));
1656
+ console.log(chalk7.dim("\nNot logged in"));
1657
+ console.log(chalk7.dim("Run `tthr login` to authenticate\n"));
1417
1658
  return;
1418
1659
  }
1419
- console.log(chalk6.bold("\n\u26A1 Current user\n"));
1420
- console.log(` Email: ${chalk6.cyan(credentials.email)}`);
1421
- console.log(` User ID: ${chalk6.dim(credentials.userId)}`);
1660
+ console.log(chalk7.bold("\n\u26A1 Current user\n"));
1661
+ console.log(` Email: ${chalk7.cyan(credentials.email)}`);
1662
+ console.log(` User ID: ${chalk7.dim(credentials.userId)}`);
1422
1663
  if (credentials.expiresAt) {
1423
1664
  const expiresAt = new Date(credentials.expiresAt);
1424
1665
  const now = /* @__PURE__ */ new Date();
1425
1666
  if (expiresAt > now) {
1426
- console.log(` Session: ${chalk6.green("Active")}`);
1667
+ console.log(` Session: ${chalk7.green("Active")}`);
1427
1668
  } else {
1428
- console.log(` Session: ${chalk6.yellow("Expired")}`);
1669
+ console.log(` Session: ${chalk7.yellow("Expired")}`);
1429
1670
  }
1430
1671
  }
1431
1672
  console.log();
@@ -1446,7 +1687,7 @@ function generateUserCode() {
1446
1687
  async function requestDeviceCode() {
1447
1688
  const userCode = generateUserCode();
1448
1689
  const deviceCode = crypto.randomUUID();
1449
- const response = await fetch(`${API_URL2}/auth/device`, {
1690
+ const response = await fetch(`${API_URL3}/auth/device`, {
1450
1691
  method: "POST",
1451
1692
  headers: { "Content-Type": "application/json" },
1452
1693
  body: JSON.stringify({ userCode, deviceCode })
@@ -1466,21 +1707,21 @@ async function requestDeviceCode() {
1466
1707
  async function pollForApproval(deviceCode, interval, expiresIn) {
1467
1708
  const startTime = Date.now();
1468
1709
  const expiresAt = startTime + expiresIn * 1e3;
1469
- const spinner = ora5("").start();
1710
+ const spinner = ora6("").start();
1470
1711
  const updateCountdown = () => {
1471
1712
  const remaining = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1e3));
1472
1713
  const mins = Math.floor(remaining / 60);
1473
1714
  const secs = remaining % 60;
1474
1715
  const timeStr = mins > 0 ? `${mins}:${secs.toString().padStart(2, "0")}` : `${secs}s`;
1475
- spinner.text = `Waiting for approval... ${chalk6.dim(`(${timeStr} remaining)`)}`;
1716
+ spinner.text = `Waiting for approval... ${chalk7.dim(`(${timeStr} remaining)`)}`;
1476
1717
  };
1477
1718
  updateCountdown();
1478
1719
  const countdownInterval = setInterval(updateCountdown, 1e3);
1479
1720
  try {
1480
1721
  while (Date.now() < expiresAt) {
1481
1722
  await sleep(interval * 1e3);
1482
- spinner.text = `Checking... ${chalk6.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
1483
- const response = await fetch(`${API_URL2}/auth/device/${deviceCode}`, {
1723
+ spinner.text = `Checking... ${chalk7.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
1724
+ const response = await fetch(`${API_URL3}/auth/device/${deviceCode}`, {
1484
1725
  method: "GET"
1485
1726
  }).catch(() => null);
1486
1727
  updateCountdown();
@@ -1518,6 +1759,7 @@ program.command("init [name]").description("Create a new Tether project").option
1518
1759
  program.command("dev").description("Start local development server with hot reload").option("-p, --port <port>", "Port to run on", "3001").action(devCommand);
1519
1760
  program.command("generate").alias("gen").description("Generate types from schema").action(generateCommand);
1520
1761
  program.command("migrate").description("Database migrations").argument("[action]", "Migration action: create, up, down, status", "status").option("-n, --name <name>", "Migration name (for create)").action(migrateCommand);
1762
+ 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);
1521
1763
  program.command("login").description("Authenticate with Tether").action(loginCommand);
1522
1764
  program.command("logout").description("Sign out of Tether").action(logoutCommand);
1523
1765
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tthr/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Tether CLI - project scaffolding, migrations, and deployment",
5
5
  "type": "module",
6
6
  "bin": {