tthr 0.0.18 → 0.0.20

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 +331 -181
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ import { Command } from "commander";
7
7
  import chalk3 from "chalk";
8
8
  import ora2 from "ora";
9
9
  import prompts from "prompts";
10
- import fs3 from "fs-extra";
11
- import path3 from "path";
10
+ import fs4 from "fs-extra";
11
+ import path4 from "path";
12
12
  import { execSync } from "child_process";
13
13
 
14
14
  // src/utils/auth.ts
@@ -57,22 +57,78 @@ async function clearCredentials() {
57
57
  // src/commands/deploy.ts
58
58
  import chalk2 from "chalk";
59
59
  import ora from "ora";
60
+ import fs3 from "fs-extra";
61
+ import path3 from "path";
62
+
63
+ // src/utils/config.ts
60
64
  import fs2 from "fs-extra";
61
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
62
118
  var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
63
119
  var API_URL = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
64
120
  async function deployCommand(options) {
65
121
  const credentials = await requireAuth();
66
- const configPath = path2.resolve(process.cwd(), "tether.config.ts");
67
- if (!await fs2.pathExists(configPath)) {
122
+ const configPath = path3.resolve(process.cwd(), "tether.config.ts");
123
+ if (!await fs3.pathExists(configPath)) {
68
124
  console.log(chalk2.red("\nError: Not a Tether project"));
69
125
  console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
70
126
  process.exit(1);
71
127
  }
72
- const envPath = path2.resolve(process.cwd(), ".env");
128
+ const envPath = path3.resolve(process.cwd(), ".env");
73
129
  let projectId;
74
- if (await fs2.pathExists(envPath)) {
75
- const envContent = await fs2.readFile(envPath, "utf-8");
130
+ if (await fs3.pathExists(envPath)) {
131
+ const envContent = await fs3.readFile(envPath, "utf-8");
76
132
  const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
77
133
  projectId = match?.[1]?.trim();
78
134
  }
@@ -85,26 +141,30 @@ async function deployCommand(options) {
85
141
  console.log(chalk2.dim(` Project: ${projectId}`));
86
142
  console.log(chalk2.dim(` API: ${API_URL}
87
143
  `));
144
+ const config = await loadConfig();
88
145
  const deploySchema = options.schema || !options.schema && !options.functions;
89
146
  const deployFunctions = options.functions || !options.schema && !options.functions;
90
147
  if (deploySchema) {
91
- await deploySchemaToServer(projectId, credentials.accessToken, options.dryRun);
148
+ const schemaPath = resolvePath(config.schema);
149
+ await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, options.dryRun);
92
150
  }
93
151
  if (deployFunctions) {
94
- await deployFunctionsToServer(projectId, credentials.accessToken, options.dryRun);
152
+ const functionsDir = resolvePath(config.functions);
153
+ await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, options.dryRun);
95
154
  }
96
155
  console.log(chalk2.green("\n\u2713 Deployment complete\n"));
97
156
  }
98
- async function deploySchemaToServer(projectId, token, dryRun) {
157
+ async function deploySchemaToServer(projectId, token, schemaPath, dryRun) {
99
158
  const spinner = ora("Reading schema...").start();
100
159
  try {
101
- const schemaPath = path2.resolve(process.cwd(), "tether", "schema.ts");
102
- if (!await fs2.pathExists(schemaPath)) {
160
+ if (!await fs3.pathExists(schemaPath)) {
103
161
  spinner.warn("No schema file found");
104
- console.log(chalk2.dim(" Create tether/schema.ts to define your database schema\n"));
162
+ const relativePath = path3.relative(process.cwd(), schemaPath);
163
+ console.log(chalk2.dim(` Create ${relativePath} to define your database schema
164
+ `));
105
165
  return;
106
166
  }
107
- const schemaSource = await fs2.readFile(schemaPath, "utf-8");
167
+ const schemaSource = await fs3.readFile(schemaPath, "utf-8");
108
168
  const tables = parseSchema(schemaSource);
109
169
  spinner.text = `Found ${tables.length} table(s)`;
110
170
  if (dryRun) {
@@ -140,16 +200,17 @@ async function deploySchemaToServer(projectId, token, dryRun) {
140
200
  console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
141
201
  }
142
202
  }
143
- async function deployFunctionsToServer(projectId, token, dryRun) {
203
+ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
144
204
  const spinner = ora("Reading functions...").start();
145
205
  try {
146
- const functionsDir = path2.resolve(process.cwd(), "tether", "functions");
147
- if (!await fs2.pathExists(functionsDir)) {
206
+ if (!await fs3.pathExists(functionsDir)) {
148
207
  spinner.warn("No functions directory found");
149
- console.log(chalk2.dim(" Create tether/functions/ to define your API functions\n"));
208
+ const relativePath = path3.relative(process.cwd(), functionsDir);
209
+ console.log(chalk2.dim(` Create ${relativePath}/ to define your API functions
210
+ `));
150
211
  return;
151
212
  }
152
- const files = await fs2.readdir(functionsDir);
213
+ const files = await fs3.readdir(functionsDir);
153
214
  const tsFiles = files.filter((f) => f.endsWith(".ts"));
154
215
  if (tsFiles.length === 0) {
155
216
  spinner.info("No function files found");
@@ -157,8 +218,8 @@ async function deployFunctionsToServer(projectId, token, dryRun) {
157
218
  }
158
219
  const functions = [];
159
220
  for (const file of tsFiles) {
160
- const filePath = path2.join(functionsDir, file);
161
- const source = await fs2.readFile(filePath, "utf-8");
221
+ const filePath = path3.join(functionsDir, file);
222
+ const source = await fs3.readFile(filePath, "utf-8");
162
223
  const moduleName = file.replace(".ts", "");
163
224
  const parsedFunctions = parseFunctions(moduleName, source);
164
225
  functions.push(...parsedFunctions);
@@ -429,8 +490,8 @@ ${packageManager} is not installed on your system.`));
429
490
  });
430
491
  template = response.template || "nuxt";
431
492
  }
432
- const projectPath = path3.resolve(process.cwd(), projectName);
433
- if (await fs3.pathExists(projectPath)) {
493
+ const projectPath = path4.resolve(process.cwd(), projectName);
494
+ if (await fs4.pathExists(projectPath)) {
434
495
  const { overwrite } = await prompts({
435
496
  type: "confirm",
436
497
  name: "overwrite",
@@ -441,7 +502,7 @@ ${packageManager} is not installed on your system.`));
441
502
  console.log(chalk3.yellow("Cancelled"));
442
503
  process.exit(0);
443
504
  }
444
- await fs3.remove(projectPath);
505
+ await fs4.remove(projectPath);
445
506
  }
446
507
  const spinner = ora2(`Scaffolding ${template} project...`).start();
447
508
  let projectId;
@@ -516,7 +577,7 @@ function getPackageRunnerCommand(pm) {
516
577
  }
517
578
  }
518
579
  async function scaffoldNuxtProject(projectName, projectPath, spinner, packageManager) {
519
- const parentDir = path3.dirname(projectPath);
580
+ const parentDir = path4.dirname(projectPath);
520
581
  const runner = getPackageRunnerCommand(packageManager);
521
582
  spinner.stop();
522
583
  console.log(chalk3.dim("\nRunning nuxi init...\n"));
@@ -525,7 +586,7 @@ async function scaffoldNuxtProject(projectName, projectPath, spinner, packageMan
525
586
  cwd: parentDir,
526
587
  stdio: "inherit"
527
588
  });
528
- if (!await fs3.pathExists(path3.join(projectPath, "package.json"))) {
589
+ if (!await fs4.pathExists(path4.join(projectPath, "package.json"))) {
529
590
  throw new Error("Nuxt project was not created successfully - package.json missing");
530
591
  }
531
592
  spinner.start();
@@ -538,7 +599,7 @@ async function scaffoldNuxtProject(projectName, projectPath, spinner, packageMan
538
599
  }
539
600
  }
540
601
  async function scaffoldNextProject(projectName, projectPath, spinner, packageManager) {
541
- const parentDir = path3.dirname(projectPath);
602
+ const parentDir = path4.dirname(projectPath);
542
603
  const runner = getPackageRunnerCommand(packageManager);
543
604
  const pmFlag = packageManager === "npm" ? "--use-npm" : packageManager === "pnpm" ? "--use-pnpm" : packageManager === "yarn" ? "--use-yarn" : "--use-bun";
544
605
  spinner.stop();
@@ -548,7 +609,7 @@ async function scaffoldNextProject(projectName, projectPath, spinner, packageMan
548
609
  cwd: parentDir,
549
610
  stdio: "inherit"
550
611
  });
551
- if (!await fs3.pathExists(path3.join(projectPath, "package.json"))) {
612
+ if (!await fs4.pathExists(path4.join(projectPath, "package.json"))) {
552
613
  throw new Error("Next.js project was not created successfully - package.json missing");
553
614
  }
554
615
  spinner.start();
@@ -561,7 +622,7 @@ async function scaffoldNextProject(projectName, projectPath, spinner, packageMan
561
622
  }
562
623
  }
563
624
  async function scaffoldSvelteKitProject(projectName, projectPath, spinner, packageManager) {
564
- const parentDir = path3.dirname(projectPath);
625
+ const parentDir = path4.dirname(projectPath);
565
626
  const runner = getPackageRunnerCommand(packageManager);
566
627
  spinner.stop();
567
628
  console.log(chalk3.dim("\nRunning sv create...\n"));
@@ -570,7 +631,7 @@ async function scaffoldSvelteKitProject(projectName, projectPath, spinner, packa
570
631
  cwd: parentDir,
571
632
  stdio: "inherit"
572
633
  });
573
- if (!await fs3.pathExists(path3.join(projectPath, "package.json"))) {
634
+ if (!await fs4.pathExists(path4.join(projectPath, "package.json"))) {
574
635
  throw new Error("SvelteKit project was not created successfully - package.json missing");
575
636
  }
576
637
  spinner.start();
@@ -589,8 +650,8 @@ async function scaffoldSvelteKitProject(projectName, projectPath, spinner, packa
589
650
  }
590
651
  async function scaffoldVanillaProject(projectName, projectPath, spinner) {
591
652
  spinner.text = "Creating vanilla TypeScript project...";
592
- await fs3.ensureDir(projectPath);
593
- await fs3.ensureDir(path3.join(projectPath, "src"));
653
+ await fs4.ensureDir(projectPath);
654
+ await fs4.ensureDir(path4.join(projectPath, "src"));
594
655
  const packageJson = {
595
656
  name: projectName,
596
657
  version: "0.0.1",
@@ -607,9 +668,9 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
607
668
  "@types/node": "latest"
608
669
  }
609
670
  };
610
- await fs3.writeJSON(path3.join(projectPath, "package.json"), packageJson, { spaces: 2 });
611
- await fs3.writeJSON(
612
- path3.join(projectPath, "tsconfig.json"),
671
+ await fs4.writeJSON(path4.join(projectPath, "package.json"), packageJson, { spaces: 2 });
672
+ await fs4.writeJSON(
673
+ path4.join(projectPath, "tsconfig.json"),
613
674
  {
614
675
  compilerOptions: {
615
676
  target: "ES2022",
@@ -630,8 +691,8 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
630
691
  },
631
692
  { spaces: 2 }
632
693
  );
633
- await fs3.writeFile(
634
- path3.join(projectPath, "src", "index.ts"),
694
+ await fs4.writeFile(
695
+ path4.join(projectPath, "src", "index.ts"),
635
696
  `import { createClient } from '@tthr/client';
636
697
 
637
698
  const tether = createClient({
@@ -650,8 +711,8 @@ async function main() {
650
711
  main().catch(console.error);
651
712
  `
652
713
  );
653
- await fs3.writeFile(
654
- path3.join(projectPath, ".gitignore"),
714
+ await fs4.writeFile(
715
+ path4.join(projectPath, ".gitignore"),
655
716
  `node_modules/
656
717
  dist/
657
718
  .env
@@ -661,11 +722,11 @@ dist/
661
722
  );
662
723
  }
663
724
  async function addTetherFiles(projectPath, projectId, apiKey, template) {
664
- await fs3.ensureDir(path3.join(projectPath, "tether"));
665
- await fs3.ensureDir(path3.join(projectPath, "tether", "functions"));
725
+ await fs4.ensureDir(path4.join(projectPath, "tether"));
726
+ await fs4.ensureDir(path4.join(projectPath, "tether", "functions"));
666
727
  const configPackage = template === "nuxt" ? "@tthr/vue" : template === "next" ? "@tthr/react" : template === "sveltekit" ? "@tthr/svelte" : "@tthr/client";
667
- await fs3.writeFile(
668
- path3.join(projectPath, "tether.config.ts"),
728
+ await fs4.writeFile(
729
+ path4.join(projectPath, "tether.config.ts"),
669
730
  `import { defineConfig } from '${configPackage}';
670
731
 
671
732
  export default defineConfig({
@@ -682,12 +743,12 @@ export default defineConfig({
682
743
  functions: './tether/functions',
683
744
 
684
745
  // Generated types output
685
- output: './_generated',
746
+ output: './tether/_generated',
686
747
  });
687
748
  `
688
749
  );
689
- await fs3.writeFile(
690
- path3.join(projectPath, "tether", "schema.ts"),
750
+ await fs4.writeFile(
751
+ path4.join(projectPath, "tether", "schema.ts"),
691
752
  `import { defineSchema, text, integer, timestamp } from '@tthr/schema';
692
753
 
693
754
  export default defineSchema({
@@ -712,8 +773,8 @@ export default defineSchema({
712
773
  });
713
774
  `
714
775
  );
715
- await fs3.writeFile(
716
- path3.join(projectPath, "tether", "functions", "posts.ts"),
776
+ await fs4.writeFile(
777
+ path4.join(projectPath, "tether", "functions", "posts.ts"),
717
778
  `import { query, mutation, z } from '@tthr/server';
718
779
 
719
780
  // List all posts
@@ -798,8 +859,8 @@ export const remove = mutation({
798
859
  });
799
860
  `
800
861
  );
801
- await fs3.writeFile(
802
- path3.join(projectPath, "tether", "functions", "comments.ts"),
862
+ await fs4.writeFile(
863
+ path4.join(projectPath, "tether", "functions", "comments.ts"),
803
864
  `import { query, mutation, z } from '@tthr/server';
804
865
 
805
866
  // List all comments
@@ -871,27 +932,27 @@ export const remove = mutation({
871
932
  TETHER_PROJECT_ID=${projectId}
872
933
  TETHER_API_KEY=${apiKey}
873
934
  `;
874
- const envPath = path3.join(projectPath, ".env");
875
- if (await fs3.pathExists(envPath)) {
876
- const existing = await fs3.readFile(envPath, "utf-8");
877
- await fs3.writeFile(envPath, existing + "\n" + envContent);
935
+ const envPath = path4.join(projectPath, ".env");
936
+ if (await fs4.pathExists(envPath)) {
937
+ const existing = await fs4.readFile(envPath, "utf-8");
938
+ await fs4.writeFile(envPath, existing + "\n" + envContent);
878
939
  } else {
879
- await fs3.writeFile(envPath, envContent);
940
+ await fs4.writeFile(envPath, envContent);
880
941
  }
881
- const gitignorePath = path3.join(projectPath, ".gitignore");
942
+ const gitignorePath = path4.join(projectPath, ".gitignore");
882
943
  const tetherGitignore = `
883
944
  # Tether
884
945
  _generated/
885
946
  .env
886
947
  .env.local
887
948
  `;
888
- if (await fs3.pathExists(gitignorePath)) {
889
- const existing = await fs3.readFile(gitignorePath, "utf-8");
949
+ if (await fs4.pathExists(gitignorePath)) {
950
+ const existing = await fs4.readFile(gitignorePath, "utf-8");
890
951
  if (!existing.includes("_generated/")) {
891
- await fs3.writeFile(gitignorePath, existing + tetherGitignore);
952
+ await fs4.writeFile(gitignorePath, existing + tetherGitignore);
892
953
  }
893
954
  } else {
894
- await fs3.writeFile(gitignorePath, tetherGitignore.trim());
955
+ await fs4.writeFile(gitignorePath, tetherGitignore.trim());
895
956
  }
896
957
  }
897
958
  async function configureFramework(projectPath, template) {
@@ -904,9 +965,9 @@ async function configureFramework(projectPath, template) {
904
965
  }
905
966
  }
906
967
  async function configureNuxt(projectPath) {
907
- const configPath = path3.join(projectPath, "nuxt.config.ts");
908
- if (!await fs3.pathExists(configPath)) {
909
- await fs3.writeFile(
968
+ const configPath = path4.join(projectPath, "nuxt.config.ts");
969
+ if (!await fs4.pathExists(configPath)) {
970
+ await fs4.writeFile(
910
971
  configPath,
911
972
  `// https://nuxt.com/docs/api/configuration/nuxt-config
912
973
  export default defineNuxtConfig({
@@ -924,7 +985,7 @@ export default defineNuxtConfig({
924
985
  );
925
986
  return;
926
987
  }
927
- let config = await fs3.readFile(configPath, "utf-8");
988
+ let config = await fs4.readFile(configPath, "utf-8");
928
989
  if (config.includes("modules:")) {
929
990
  config = config.replace(
930
991
  /modules:\s*\[/,
@@ -951,11 +1012,11 @@ export default defineNuxtConfig({
951
1012
  `
952
1013
  );
953
1014
  }
954
- await fs3.writeFile(configPath, config);
1015
+ await fs4.writeFile(configPath, config);
955
1016
  }
956
1017
  async function configureNext(projectPath) {
957
- const providersPath = path3.join(projectPath, "src", "app", "providers.tsx");
958
- await fs3.writeFile(
1018
+ const providersPath = path4.join(projectPath, "src", "app", "providers.tsx");
1019
+ await fs4.writeFile(
959
1020
  providersPath,
960
1021
  `'use client';
961
1022
 
@@ -973,9 +1034,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
973
1034
  }
974
1035
  `
975
1036
  );
976
- const layoutPath = path3.join(projectPath, "src", "app", "layout.tsx");
977
- if (await fs3.pathExists(layoutPath)) {
978
- let layout = await fs3.readFile(layoutPath, "utf-8");
1037
+ const layoutPath = path4.join(projectPath, "src", "app", "layout.tsx");
1038
+ if (await fs4.pathExists(layoutPath)) {
1039
+ let layout = await fs4.readFile(layoutPath, "utf-8");
979
1040
  if (!layout.includes("import { Providers }")) {
980
1041
  layout = layout.replace(
981
1042
  /^(import.*\n)+/m,
@@ -988,24 +1049,24 @@ export function Providers({ children }: { children: React.ReactNode }) {
988
1049
  "$1<Providers>$2</Providers>$3"
989
1050
  );
990
1051
  }
991
- await fs3.writeFile(layoutPath, layout);
1052
+ await fs4.writeFile(layoutPath, layout);
992
1053
  }
993
- const envLocalPath = path3.join(projectPath, ".env.local");
1054
+ const envLocalPath = path4.join(projectPath, ".env.local");
994
1055
  const nextEnvContent = `# Tether Configuration (client-side)
995
1056
  NEXT_PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
996
1057
  `;
997
- if (await fs3.pathExists(envLocalPath)) {
998
- const existing = await fs3.readFile(envLocalPath, "utf-8");
999
- await fs3.writeFile(envLocalPath, existing + "\n" + nextEnvContent);
1058
+ if (await fs4.pathExists(envLocalPath)) {
1059
+ const existing = await fs4.readFile(envLocalPath, "utf-8");
1060
+ await fs4.writeFile(envLocalPath, existing + "\n" + nextEnvContent);
1000
1061
  } else {
1001
- await fs3.writeFile(envLocalPath, nextEnvContent);
1062
+ await fs4.writeFile(envLocalPath, nextEnvContent);
1002
1063
  }
1003
1064
  }
1004
1065
  async function configureSvelteKit(projectPath) {
1005
- const libPath = path3.join(projectPath, "src", "lib");
1006
- await fs3.ensureDir(libPath);
1007
- await fs3.writeFile(
1008
- path3.join(libPath, "tether.ts"),
1066
+ const libPath = path4.join(projectPath, "src", "lib");
1067
+ await fs4.ensureDir(libPath);
1068
+ await fs4.writeFile(
1069
+ path4.join(libPath, "tether.ts"),
1009
1070
  `import { createClient } from '@tthr/svelte';
1010
1071
  import { PUBLIC_TETHER_PROJECT_ID, PUBLIC_TETHER_URL } from '$env/static/public';
1011
1072
 
@@ -1015,14 +1076,14 @@ export const tether = createClient({
1015
1076
  });
1016
1077
  `
1017
1078
  );
1018
- const envPath = path3.join(projectPath, ".env");
1079
+ const envPath = path4.join(projectPath, ".env");
1019
1080
  const svelteEnvContent = `# Tether Configuration (public)
1020
1081
  PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
1021
1082
  `;
1022
- if (await fs3.pathExists(envPath)) {
1023
- const existing = await fs3.readFile(envPath, "utf-8");
1083
+ if (await fs4.pathExists(envPath)) {
1084
+ const existing = await fs4.readFile(envPath, "utf-8");
1024
1085
  if (!existing.includes("PUBLIC_TETHER_")) {
1025
- await fs3.writeFile(envPath, existing + "\n" + svelteEnvContent);
1086
+ await fs4.writeFile(envPath, existing + "\n" + svelteEnvContent);
1026
1087
  }
1027
1088
  }
1028
1089
  }
@@ -1077,10 +1138,10 @@ async function installTetherPackages(projectPath, template, packageManager) {
1077
1138
  }
1078
1139
  async function createDemoPage(projectPath, template) {
1079
1140
  if (template === "nuxt") {
1080
- const nuxt4AppVuePath = path3.join(projectPath, "app", "app.vue");
1081
- const nuxt3AppVuePath = path3.join(projectPath, "app.vue");
1082
- const appVuePath = await fs3.pathExists(path3.join(projectPath, "app")) ? nuxt4AppVuePath : nuxt3AppVuePath;
1083
- await fs3.writeFile(
1141
+ const nuxt4AppVuePath = path4.join(projectPath, "app", "app.vue");
1142
+ const nuxt3AppVuePath = path4.join(projectPath, "app.vue");
1143
+ const appVuePath = await fs4.pathExists(path4.join(projectPath, "app")) ? nuxt4AppVuePath : nuxt3AppVuePath;
1144
+ await fs4.writeFile(
1084
1145
  appVuePath,
1085
1146
  `<template>
1086
1147
  <TetherWelcome />
@@ -1088,8 +1149,8 @@ async function createDemoPage(projectPath, template) {
1088
1149
  `
1089
1150
  );
1090
1151
  } else if (template === "next") {
1091
- const pagePath = path3.join(projectPath, "src", "app", "page.tsx");
1092
- await fs3.writeFile(
1152
+ const pagePath = path4.join(projectPath, "src", "app", "page.tsx");
1153
+ await fs4.writeFile(
1093
1154
  pagePath,
1094
1155
  `'use client';
1095
1156
 
@@ -1199,8 +1260,8 @@ export default function Home() {
1199
1260
  `
1200
1261
  );
1201
1262
  } else if (template === "sveltekit") {
1202
- const pagePath = path3.join(projectPath, "src", "routes", "+page.svelte");
1203
- await fs3.writeFile(
1263
+ const pagePath = path4.join(projectPath, "src", "routes", "+page.svelte");
1264
+ await fs4.writeFile(
1204
1265
  pagePath,
1205
1266
  `<script lang="ts">
1206
1267
  import { onMount } from 'svelte';
@@ -1468,22 +1529,24 @@ export default function Home() {
1468
1529
  // src/commands/dev.ts
1469
1530
  import chalk4 from "chalk";
1470
1531
  import ora3 from "ora";
1471
- import fs4 from "fs-extra";
1472
- import path4 from "path";
1532
+ import fs5 from "fs-extra";
1533
+ import path5 from "path";
1473
1534
  async function devCommand(options) {
1474
1535
  await requireAuth();
1475
- const configPath = path4.resolve(process.cwd(), "tether.config.ts");
1476
- if (!await fs4.pathExists(configPath)) {
1536
+ const configPath = path5.resolve(process.cwd(), "tether.config.ts");
1537
+ if (!await fs5.pathExists(configPath)) {
1477
1538
  console.log(chalk4.red("\nError: Not a Tether project"));
1478
1539
  console.log(chalk4.dim("Run `tthr init` to create a new project\n"));
1479
1540
  process.exit(1);
1480
1541
  }
1542
+ const config = await loadConfig();
1543
+ const port = options.port || String(config.dev?.port || 3001);
1481
1544
  console.log(chalk4.bold("\n\u26A1 Starting Tether development server\n"));
1482
1545
  const spinner = ora3("Starting server...").start();
1483
1546
  try {
1484
- spinner.succeed(`Development server running on port ${options.port}`);
1485
- console.log("\n" + chalk4.cyan(` Local: http://localhost:${options.port}`));
1486
- console.log(chalk4.cyan(` WebSocket: ws://localhost:${options.port}/ws
1547
+ spinner.succeed(`Development server running on port ${port}`);
1548
+ console.log("\n" + chalk4.cyan(` Local: http://localhost:${port}`));
1549
+ console.log(chalk4.cyan(` WebSocket: ws://localhost:${port}/ws
1487
1550
  `));
1488
1551
  console.log(chalk4.dim(" Press Ctrl+C to stop\n"));
1489
1552
  process.on("SIGINT", () => {
@@ -1502,12 +1565,133 @@ async function devCommand(options) {
1502
1565
  // src/commands/generate.ts
1503
1566
  import chalk5 from "chalk";
1504
1567
  import ora4 from "ora";
1505
- import fs5 from "fs-extra";
1506
- import path5 from "path";
1568
+ import fs6 from "fs-extra";
1569
+ import path6 from "path";
1570
+ function parseSchemaFile(source) {
1571
+ const tables = [];
1572
+ const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
1573
+ if (!schemaMatch) return tables;
1574
+ const schemaContent = schemaMatch[1];
1575
+ const tableStartRegex = /(\w+)\s*:\s*\{/g;
1576
+ let match;
1577
+ while ((match = tableStartRegex.exec(schemaContent)) !== null) {
1578
+ const tableName = match[1];
1579
+ const startOffset = match.index + match[0].length;
1580
+ let braceCount = 1;
1581
+ let endOffset = startOffset;
1582
+ for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
1583
+ const char = schemaContent[i];
1584
+ if (char === "{") braceCount++;
1585
+ else if (char === "}") braceCount--;
1586
+ endOffset = i;
1587
+ }
1588
+ const columnsContent = schemaContent.slice(startOffset, endOffset);
1589
+ const columns = parseColumns(columnsContent);
1590
+ tables.push({ name: tableName, columns });
1591
+ }
1592
+ return tables;
1593
+ }
1594
+ function parseColumns(content) {
1595
+ const columns = [];
1596
+ const columnRegex = /(\w+)\s*:\s*(\w+)\s*\(\s*\)([^,\n}]*)/g;
1597
+ let match;
1598
+ while ((match = columnRegex.exec(content)) !== null) {
1599
+ const name = match[1];
1600
+ const schemaType = match[2];
1601
+ const modifiers = match[3] || "";
1602
+ const nullable = !modifiers.includes(".notNull()") && !modifiers.includes(".primaryKey()");
1603
+ const primaryKey = modifiers.includes(".primaryKey()");
1604
+ columns.push({
1605
+ name,
1606
+ type: schemaTypeToTS(schemaType),
1607
+ nullable,
1608
+ primaryKey
1609
+ });
1610
+ }
1611
+ return columns;
1612
+ }
1613
+ function schemaTypeToTS(schemaType) {
1614
+ switch (schemaType) {
1615
+ case "text":
1616
+ return "string";
1617
+ case "integer":
1618
+ return "number";
1619
+ case "real":
1620
+ return "number";
1621
+ case "boolean":
1622
+ return "boolean";
1623
+ case "timestamp":
1624
+ return "string";
1625
+ case "json":
1626
+ return "unknown";
1627
+ case "blob":
1628
+ return "Uint8Array";
1629
+ default:
1630
+ return "unknown";
1631
+ }
1632
+ }
1633
+ function tableNameToInterface(tableName) {
1634
+ let name = tableName;
1635
+ if (name.endsWith("ies")) {
1636
+ name = name.slice(0, -3) + "y";
1637
+ } else if (name.endsWith("s") && !name.endsWith("ss")) {
1638
+ name = name.slice(0, -1);
1639
+ }
1640
+ return name.charAt(0).toUpperCase() + name.slice(1);
1641
+ }
1642
+ function generateDbFile(tables) {
1643
+ const lines = [
1644
+ "// Auto-generated by Tether CLI - do not edit manually",
1645
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1646
+ "",
1647
+ "import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
1648
+ ""
1649
+ ];
1650
+ for (const table of tables) {
1651
+ const interfaceName = tableNameToInterface(table.name);
1652
+ lines.push(`export interface ${interfaceName} {`);
1653
+ for (const col of table.columns) {
1654
+ const typeStr = col.nullable ? `${col.type} | null` : col.type;
1655
+ lines.push(` ${col.name}: ${typeStr};`);
1656
+ }
1657
+ lines.push("}");
1658
+ lines.push("");
1659
+ }
1660
+ lines.push("export interface Schema {");
1661
+ for (const table of tables) {
1662
+ const interfaceName = tableNameToInterface(table.name);
1663
+ lines.push(` ${table.name}: ${interfaceName};`);
1664
+ }
1665
+ lines.push("}");
1666
+ lines.push("");
1667
+ lines.push("// Database client with typed tables");
1668
+ lines.push("// This is a proxy that will be populated by the Tether runtime");
1669
+ lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
1670
+ lines.push("");
1671
+ return lines.join("\n");
1672
+ }
1673
+ function generateApiFile(functionsDir, tables) {
1674
+ const lines = [
1675
+ "// Auto-generated by Tether CLI - do not edit manually",
1676
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1677
+ "",
1678
+ "import { createApiProxy } from '@tthr/client';",
1679
+ "import type { Schema } from './db';",
1680
+ "",
1681
+ "// API type placeholder - will be populated based on your functions",
1682
+ "// eslint-disable-next-line @typescript-eslint/no-empty-object-type",
1683
+ "export interface Api {}",
1684
+ "",
1685
+ "// API client proxy - will be populated by the Tether runtime",
1686
+ "export const api: Api = createApiProxy<Api>();",
1687
+ ""
1688
+ ];
1689
+ return lines.join("\n");
1690
+ }
1507
1691
  async function generateCommand() {
1508
1692
  await requireAuth();
1509
- const configPath = path5.resolve(process.cwd(), "tether.config.ts");
1510
- if (!await fs5.pathExists(configPath)) {
1693
+ const configPath = path6.resolve(process.cwd(), "tether.config.ts");
1694
+ if (!await fs6.pathExists(configPath)) {
1511
1695
  console.log(chalk5.red("\nError: Not a Tether project"));
1512
1696
  console.log(chalk5.dim("Run `tthr init` to create a new project\n"));
1513
1697
  process.exit(1);
@@ -1515,85 +1699,51 @@ async function generateCommand() {
1515
1699
  console.log(chalk5.bold("\n\u26A1 Generating types from schema\n"));
1516
1700
  const spinner = ora4("Reading schema...").start();
1517
1701
  try {
1518
- const schemaPath = path5.resolve(process.cwd(), "tether", "schema.ts");
1519
- const outputDir = path5.resolve(process.cwd(), "_generated");
1520
- if (!await fs5.pathExists(schemaPath)) {
1702
+ const config = await loadConfig();
1703
+ const schemaPath = resolvePath(config.schema);
1704
+ const outputDir = resolvePath(config.output);
1705
+ const functionsDir = resolvePath(config.functions);
1706
+ if (!await fs6.pathExists(schemaPath)) {
1521
1707
  spinner.fail("Schema file not found");
1522
1708
  console.log(chalk5.dim(`Expected: ${schemaPath}
1523
1709
  `));
1524
1710
  process.exit(1);
1525
1711
  }
1526
- spinner.text = "Generating types...";
1527
- await fs5.ensureDir(outputDir);
1528
- await fs5.writeFile(
1529
- path5.join(outputDir, "db.ts"),
1530
- `// Auto-generated by Tether CLI - do not edit manually
1531
- // Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
1532
-
1533
- import type { TetherDatabase } from '@tthr/client';
1534
-
1535
- export interface Post {
1536
- id: string;
1537
- title: string;
1538
- content: string | null;
1539
- authorId: string;
1540
- createdAt: string;
1541
- updatedAt: string;
1542
- }
1543
-
1544
- export interface Comment {
1545
- id: string;
1546
- postId: string;
1547
- content: string;
1548
- authorId: string;
1549
- createdAt: string;
1550
- }
1551
-
1552
- export interface Schema {
1553
- posts: Post;
1554
- comments: Comment;
1555
- }
1556
-
1557
- // Database client with typed tables
1558
- export declare const db: TetherDatabase<Schema>;
1559
- `
1712
+ const schemaSource = await fs6.readFile(schemaPath, "utf-8");
1713
+ const tables = parseSchemaFile(schemaSource);
1714
+ if (tables.length === 0) {
1715
+ spinner.warn("No tables found in schema");
1716
+ console.log(chalk5.dim(" Make sure your schema uses defineSchema({ ... })\n"));
1717
+ return;
1718
+ }
1719
+ spinner.text = `Generating types for ${tables.length} table(s)...`;
1720
+ await fs6.ensureDir(outputDir);
1721
+ await fs6.writeFile(
1722
+ path6.join(outputDir, "db.ts"),
1723
+ generateDbFile(tables)
1560
1724
  );
1561
- await fs5.writeFile(
1562
- path5.join(outputDir, "api.ts"),
1563
- `// Auto-generated by Tether CLI - do not edit manually
1564
- // Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
1565
-
1566
- import type { TetherClient } from '@tthr/client';
1567
- import type { Schema } from './db';
1568
-
1569
- export interface PostsApi {
1570
- list: (args?: { limit?: number }) => Promise<Schema['posts'][]>;
1571
- get: (args: { id: string }) => Promise<Schema['posts'] | null>;
1572
- create: (args: { title: string; content?: string }) => Promise<{ id: string }>;
1573
- update: (args: { id: string; title?: string; content?: string }) => Promise<void>;
1574
- remove: (args: { id: string }) => Promise<void>;
1575
- }
1576
-
1577
- export interface Api {
1578
- posts: PostsApi;
1579
- }
1580
-
1581
- // Typed Tether client
1582
- export declare const tether: TetherClient<Api>;
1583
- `
1725
+ await fs6.writeFile(
1726
+ path6.join(outputDir, "api.ts"),
1727
+ generateApiFile(functionsDir, tables)
1584
1728
  );
1585
- await fs5.writeFile(
1586
- path5.join(outputDir, "index.ts"),
1729
+ await fs6.writeFile(
1730
+ path6.join(outputDir, "index.ts"),
1587
1731
  `// Auto-generated by Tether CLI - do not edit manually
1588
1732
  export * from './db';
1589
1733
  export * from './api';
1590
1734
  `
1591
1735
  );
1592
- spinner.succeed("Types generated");
1736
+ spinner.succeed(`Types generated for ${tables.length} table(s)`);
1737
+ console.log("\n" + chalk5.green("\u2713") + " Tables:");
1738
+ for (const table of tables) {
1739
+ console.log(chalk5.dim(` - ${table.name} (${table.columns.length} columns)`));
1740
+ }
1741
+ const relativeOutput = path6.relative(process.cwd(), outputDir);
1593
1742
  console.log("\n" + chalk5.green("\u2713") + " Generated files:");
1594
- console.log(chalk5.dim(" _generated/db.ts"));
1595
- console.log(chalk5.dim(" _generated/api.ts"));
1596
- console.log(chalk5.dim(" _generated/index.ts\n"));
1743
+ console.log(chalk5.dim(` ${relativeOutput}/db.ts`));
1744
+ console.log(chalk5.dim(` ${relativeOutput}/api.ts`));
1745
+ console.log(chalk5.dim(` ${relativeOutput}/index.ts
1746
+ `));
1597
1747
  } catch (error) {
1598
1748
  spinner.fail("Failed to generate types");
1599
1749
  console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
@@ -1604,18 +1754,18 @@ export * from './api';
1604
1754
  // src/commands/migrate.ts
1605
1755
  import chalk6 from "chalk";
1606
1756
  import ora5 from "ora";
1607
- import fs6 from "fs-extra";
1608
- import path6 from "path";
1757
+ import fs7 from "fs-extra";
1758
+ import path7 from "path";
1609
1759
  import prompts2 from "prompts";
1610
1760
  async function migrateCommand(action, options) {
1611
1761
  await requireAuth();
1612
- const configPath = path6.resolve(process.cwd(), "tether.config.ts");
1613
- if (!await fs6.pathExists(configPath)) {
1762
+ const configPath = path7.resolve(process.cwd(), "tether.config.ts");
1763
+ if (!await fs7.pathExists(configPath)) {
1614
1764
  console.log(chalk6.red("\nError: Not a Tether project"));
1615
1765
  console.log(chalk6.dim("Run `tthr init` to create a new project\n"));
1616
1766
  process.exit(1);
1617
1767
  }
1618
- const migrationsDir = path6.resolve(process.cwd(), "tether", "migrations");
1768
+ const migrationsDir = path7.resolve(process.cwd(), "tether", "migrations");
1619
1769
  switch (action) {
1620
1770
  case "create":
1621
1771
  await createMigration(migrationsDir, options.name);
@@ -1650,11 +1800,11 @@ async function createMigration(migrationsDir, name) {
1650
1800
  }
1651
1801
  const spinner = ora5("Creating migration...").start();
1652
1802
  try {
1653
- await fs6.ensureDir(migrationsDir);
1803
+ await fs7.ensureDir(migrationsDir);
1654
1804
  const timestamp = Date.now();
1655
1805
  const filename = `${timestamp}_${migrationName}.sql`;
1656
- const filepath = path6.join(migrationsDir, filename);
1657
- await fs6.writeFile(
1806
+ const filepath = path7.join(migrationsDir, filename);
1807
+ await fs7.writeFile(
1658
1808
  filepath,
1659
1809
  `-- Migration: ${migrationName}
1660
1810
  -- Created at: ${(/* @__PURE__ */ new Date()).toISOString()}
@@ -1690,12 +1840,12 @@ async function runMigrations(migrationsDir, direction) {
1690
1840
  `));
1691
1841
  const spinner = ora5("Checking migrations...").start();
1692
1842
  try {
1693
- if (!await fs6.pathExists(migrationsDir)) {
1843
+ if (!await fs7.pathExists(migrationsDir)) {
1694
1844
  spinner.info("No migrations directory found");
1695
1845
  console.log(chalk6.dim("\nRun `tthr migrate create` to create your first migration\n"));
1696
1846
  return;
1697
1847
  }
1698
- const files = await fs6.readdir(migrationsDir);
1848
+ const files = await fs7.readdir(migrationsDir);
1699
1849
  const migrations = files.filter((f) => f.endsWith(".sql")).sort((a, b) => direction === "up" ? a.localeCompare(b) : b.localeCompare(a));
1700
1850
  if (migrations.length === 0) {
1701
1851
  spinner.info("No migrations found");
@@ -1718,12 +1868,12 @@ async function runMigrations(migrationsDir, direction) {
1718
1868
  async function showStatus(migrationsDir) {
1719
1869
  console.log(chalk6.bold("\n\u26A1 Migration status\n"));
1720
1870
  try {
1721
- if (!await fs6.pathExists(migrationsDir)) {
1871
+ if (!await fs7.pathExists(migrationsDir)) {
1722
1872
  console.log(chalk6.dim("No migrations directory found"));
1723
1873
  console.log(chalk6.dim("\nRun `tthr migrate create` to create your first migration\n"));
1724
1874
  return;
1725
1875
  }
1726
- const files = await fs6.readdir(migrationsDir);
1876
+ const files = await fs7.readdir(migrationsDir);
1727
1877
  const migrations = files.filter((f) => f.endsWith(".sql")).sort();
1728
1878
  if (migrations.length === 0) {
1729
1879
  console.log(chalk6.dim("No migrations found"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tthr",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Tether CLI - project scaffolding, migrations, and deployment",
5
5
  "type": "module",
6
6
  "bin": {