tthr 0.0.41 → 0.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,416 @@
1
+ // src/commands/generate.ts
2
+ import chalk2 from "chalk";
3
+ import ora from "ora";
4
+ import fs3 from "fs-extra";
5
+ import path3 from "path";
6
+
7
+ // src/utils/auth.ts
8
+ import chalk from "chalk";
9
+ import fs from "fs-extra";
10
+ import path from "path";
11
+ var CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
12
+ var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
13
+ async function getCredentials() {
14
+ try {
15
+ if (await fs.pathExists(CREDENTIALS_FILE)) {
16
+ return await fs.readJSON(CREDENTIALS_FILE);
17
+ }
18
+ } catch {
19
+ }
20
+ return null;
21
+ }
22
+ async function requireAuth() {
23
+ const credentials = await getCredentials();
24
+ if (!credentials) {
25
+ console.error(chalk.red("\n\u2717 Not logged in\n"));
26
+ console.log(chalk.dim("Run `tthr login` to authenticate\n"));
27
+ process.exit(1);
28
+ }
29
+ if (credentials.expiresAt) {
30
+ const expiresAt = new Date(credentials.expiresAt);
31
+ if (/* @__PURE__ */ new Date() > expiresAt) {
32
+ console.error(chalk.red("\n\u2717 Session expired\n"));
33
+ console.log(chalk.dim("Run `tthr login` to authenticate\n"));
34
+ process.exit(1);
35
+ }
36
+ }
37
+ return credentials;
38
+ }
39
+ async function saveCredentials(credentials) {
40
+ await fs.ensureDir(CONFIG_DIR);
41
+ await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
42
+ }
43
+ async function clearCredentials() {
44
+ try {
45
+ await fs.remove(CREDENTIALS_FILE);
46
+ } catch {
47
+ }
48
+ }
49
+
50
+ // src/utils/config.ts
51
+ import fs2 from "fs-extra";
52
+ import path2 from "path";
53
+ var DEFAULT_CONFIG = {
54
+ schema: "./tether/schema.ts",
55
+ functions: "./tether/functions",
56
+ output: "./tether/_generated",
57
+ dev: {
58
+ port: 3001,
59
+ host: "localhost"
60
+ },
61
+ database: {
62
+ walMode: true
63
+ }
64
+ };
65
+ async function loadConfig(cwd = process.cwd()) {
66
+ const configPath = path2.resolve(cwd, "tether.config.ts");
67
+ if (!await fs2.pathExists(configPath)) {
68
+ return DEFAULT_CONFIG;
69
+ }
70
+ const configSource = await fs2.readFile(configPath, "utf-8");
71
+ const config = {};
72
+ const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
73
+ if (schemaMatch) {
74
+ config.schema = schemaMatch[1];
75
+ }
76
+ const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
77
+ if (functionsMatch) {
78
+ config.functions = functionsMatch[1];
79
+ }
80
+ const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
81
+ if (outputMatch) {
82
+ config.output = outputMatch[1];
83
+ }
84
+ const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
85
+ if (envMatch) {
86
+ config.environment = envMatch[1];
87
+ }
88
+ const portMatch = configSource.match(/port\s*:\s*(\d+)/);
89
+ if (portMatch) {
90
+ config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
91
+ }
92
+ const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
93
+ if (hostMatch) {
94
+ config.dev = { ...config.dev, host: hostMatch[1] };
95
+ }
96
+ return {
97
+ ...DEFAULT_CONFIG,
98
+ ...config,
99
+ dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
100
+ database: { ...DEFAULT_CONFIG.database, ...config.database }
101
+ };
102
+ }
103
+ function resolvePath(configPath, cwd = process.cwd()) {
104
+ const normalised = configPath.replace(/^\.\//, "");
105
+ return path2.resolve(cwd, normalised);
106
+ }
107
+ async function detectFramework(cwd = process.cwd()) {
108
+ const packageJsonPath = path2.resolve(cwd, "package.json");
109
+ if (!await fs2.pathExists(packageJsonPath)) {
110
+ return "unknown";
111
+ }
112
+ try {
113
+ const packageJson = await fs2.readJson(packageJsonPath);
114
+ const deps = {
115
+ ...packageJson.dependencies,
116
+ ...packageJson.devDependencies
117
+ };
118
+ if (deps.nuxt || deps["@nuxt/kit"]) {
119
+ return "nuxt";
120
+ }
121
+ if (deps.next) {
122
+ return "next";
123
+ }
124
+ if (deps["@sveltejs/kit"]) {
125
+ return "sveltekit";
126
+ }
127
+ if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
128
+ return "vite";
129
+ }
130
+ return "vanilla";
131
+ } catch {
132
+ return "unknown";
133
+ }
134
+ }
135
+ function getFrameworkDevCommand(framework) {
136
+ switch (framework) {
137
+ case "nuxt":
138
+ return "nuxt dev";
139
+ case "next":
140
+ return "next dev";
141
+ case "sveltekit":
142
+ return "vite dev";
143
+ case "vite":
144
+ return "vite dev";
145
+ case "vanilla":
146
+ return null;
147
+ // No default dev server for vanilla
148
+ default:
149
+ return null;
150
+ }
151
+ }
152
+
153
+ // src/commands/generate.ts
154
+ function parseSchemaFile(source) {
155
+ const tables = [];
156
+ const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
157
+ if (!schemaMatch) return tables;
158
+ const schemaContent = schemaMatch[1];
159
+ const tableStartRegex = /(\w+)\s*:\s*\{/g;
160
+ let match;
161
+ while ((match = tableStartRegex.exec(schemaContent)) !== null) {
162
+ const tableName = match[1];
163
+ const startOffset = match.index + match[0].length;
164
+ let braceCount = 1;
165
+ let endOffset = startOffset;
166
+ for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
167
+ const char = schemaContent[i];
168
+ if (char === "{") braceCount++;
169
+ else if (char === "}") braceCount--;
170
+ endOffset = i;
171
+ }
172
+ const columnsContent = schemaContent.slice(startOffset, endOffset);
173
+ const columns = parseColumns(columnsContent);
174
+ tables.push({ name: tableName, columns });
175
+ }
176
+ return tables;
177
+ }
178
+ function parseColumns(content) {
179
+ const columns = [];
180
+ const columnRegex = /(\w+)\s*:\s*(\w+)\s*\(\s*\)([^,\n}]*)/g;
181
+ let match;
182
+ while ((match = columnRegex.exec(content)) !== null) {
183
+ const name = match[1];
184
+ const schemaType = match[2];
185
+ const modifiers = match[3] || "";
186
+ const nullable = !modifiers.includes(".notNull()");
187
+ columns.push({
188
+ name,
189
+ type: schemaTypeToTS(schemaType),
190
+ nullable
191
+ });
192
+ }
193
+ return columns;
194
+ }
195
+ function schemaTypeToTS(schemaType) {
196
+ switch (schemaType) {
197
+ case "text":
198
+ return "string";
199
+ case "integer":
200
+ return "number";
201
+ case "real":
202
+ return "number";
203
+ case "boolean":
204
+ return "boolean";
205
+ case "timestamp":
206
+ return "string";
207
+ case "json":
208
+ return "unknown";
209
+ case "blob":
210
+ return "Uint8Array";
211
+ default:
212
+ return "unknown";
213
+ }
214
+ }
215
+ function tableNameToInterface(tableName) {
216
+ let name = tableName;
217
+ if (name.endsWith("ies")) {
218
+ name = name.slice(0, -3) + "y";
219
+ } else if (name.endsWith("s") && !name.endsWith("ss")) {
220
+ name = name.slice(0, -1);
221
+ }
222
+ return name.charAt(0).toUpperCase() + name.slice(1);
223
+ }
224
+ function generateDbFile(tables) {
225
+ const lines = [
226
+ "// Auto-generated by Tether CLI - do not edit manually",
227
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
228
+ "",
229
+ "import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
230
+ ""
231
+ ];
232
+ for (const table of tables) {
233
+ const interfaceName = tableNameToInterface(table.name);
234
+ lines.push(`export interface ${interfaceName} {`);
235
+ for (const col of table.columns) {
236
+ const typeStr = col.nullable ? `${col.type} | null` : col.type;
237
+ lines.push(` ${col.name}: ${typeStr};`);
238
+ }
239
+ lines.push("}");
240
+ lines.push("");
241
+ }
242
+ lines.push("export interface Schema {");
243
+ for (const table of tables) {
244
+ const interfaceName = tableNameToInterface(table.name);
245
+ lines.push(` ${table.name}: ${interfaceName};`);
246
+ }
247
+ lines.push("}");
248
+ lines.push("");
249
+ lines.push("// Database client with typed tables");
250
+ lines.push("// This is a proxy that will be populated by the Tether runtime");
251
+ lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
252
+ lines.push("");
253
+ return lines.join("\n");
254
+ }
255
+ async function parseFunctionsDir(functionsDir) {
256
+ const functions = [];
257
+ if (!await fs3.pathExists(functionsDir)) {
258
+ return functions;
259
+ }
260
+ const files = await fs3.readdir(functionsDir);
261
+ for (const file of files) {
262
+ if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
263
+ const filePath = path3.join(functionsDir, file);
264
+ const stat = await fs3.stat(filePath);
265
+ if (!stat.isFile()) continue;
266
+ const moduleName = file.replace(/\.(ts|js)$/, "");
267
+ const source = await fs3.readFile(filePath, "utf-8");
268
+ const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action)\s*\(/g;
269
+ let match;
270
+ while ((match = exportRegex.exec(source)) !== null) {
271
+ functions.push({
272
+ name: match[1],
273
+ moduleName
274
+ });
275
+ }
276
+ }
277
+ return functions;
278
+ }
279
+ async function generateApiFile(functionsDir) {
280
+ const functions = await parseFunctionsDir(functionsDir);
281
+ const moduleMap = /* @__PURE__ */ new Map();
282
+ for (const fn of functions) {
283
+ if (!moduleMap.has(fn.moduleName)) {
284
+ moduleMap.set(fn.moduleName, []);
285
+ }
286
+ moduleMap.get(fn.moduleName).push(fn.name);
287
+ }
288
+ const lines = [
289
+ "// Auto-generated by Tether CLI - do not edit manually",
290
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
291
+ "",
292
+ "import { createApiProxy } from '@tthr/client';",
293
+ "",
294
+ "/**",
295
+ " * API function reference type for useQuery/useMutation.",
296
+ ' * The _name property contains the function path (e.g., "users.list").',
297
+ " */",
298
+ "export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
299
+ " _name: string;",
300
+ " _args?: TArgs;",
301
+ " _result?: TResult;",
302
+ "}",
303
+ ""
304
+ ];
305
+ if (moduleMap.size > 0) {
306
+ for (const [moduleName, fnNames] of moduleMap) {
307
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
308
+ lines.push(`export interface ${interfaceName} {`);
309
+ for (const fnName of fnNames) {
310
+ lines.push(` ${fnName}: ApiFunction;`);
311
+ }
312
+ lines.push("}");
313
+ lines.push("");
314
+ }
315
+ lines.push("export interface Api {");
316
+ for (const moduleName of moduleMap.keys()) {
317
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
318
+ lines.push(` ${moduleName}: ${interfaceName};`);
319
+ }
320
+ lines.push("}");
321
+ } else {
322
+ lines.push("/**");
323
+ lines.push(" * Flexible API type that allows access to any function path.");
324
+ lines.push(" * Functions are accessed as api.moduleName.functionName");
325
+ lines.push(" * The actual types depend on your function definitions.");
326
+ lines.push(" */");
327
+ lines.push("export type Api = {");
328
+ lines.push(" [module: string]: {");
329
+ lines.push(" [fn: string]: ApiFunction;");
330
+ lines.push(" };");
331
+ lines.push("};");
332
+ }
333
+ lines.push("");
334
+ lines.push("// API client proxy - provides typed access to your functions");
335
+ lines.push("// On the client: returns { _name } references for useQuery/useMutation");
336
+ lines.push("// In Tether functions: calls the actual function implementation");
337
+ lines.push("export const api = createApiProxy<Api>();");
338
+ lines.push("");
339
+ return lines.join("\n");
340
+ }
341
+ async function generateTypes(options = {}) {
342
+ const config = await loadConfig();
343
+ const schemaPath = resolvePath(config.schema);
344
+ const outputDir = resolvePath(config.output);
345
+ const functionsDir = resolvePath(config.functions);
346
+ if (!await fs3.pathExists(schemaPath)) {
347
+ throw new Error(`Schema file not found: ${schemaPath}`);
348
+ }
349
+ const schemaSource = await fs3.readFile(schemaPath, "utf-8");
350
+ const tables = parseSchemaFile(schemaSource);
351
+ await fs3.ensureDir(outputDir);
352
+ await fs3.writeFile(
353
+ path3.join(outputDir, "db.ts"),
354
+ generateDbFile(tables)
355
+ );
356
+ await fs3.writeFile(
357
+ path3.join(outputDir, "api.ts"),
358
+ await generateApiFile(functionsDir)
359
+ );
360
+ await fs3.writeFile(
361
+ path3.join(outputDir, "index.ts"),
362
+ `// Auto-generated by Tether CLI - do not edit manually
363
+ export * from './db';
364
+ export * from './api';
365
+ `
366
+ );
367
+ return { tables, outputDir };
368
+ }
369
+ async function generateCommand() {
370
+ await requireAuth();
371
+ const configPath = path3.resolve(process.cwd(), "tether.config.ts");
372
+ if (!await fs3.pathExists(configPath)) {
373
+ console.log(chalk2.red("\nError: Not a Tether project"));
374
+ console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
375
+ process.exit(1);
376
+ }
377
+ console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
378
+ const spinner = ora("Reading schema...").start();
379
+ try {
380
+ spinner.text = "Generating types...";
381
+ const { tables, outputDir } = await generateTypes();
382
+ if (tables.length === 0) {
383
+ spinner.warn("No tables found in schema");
384
+ console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
385
+ return;
386
+ }
387
+ spinner.succeed(`Types generated for ${tables.length} table(s)`);
388
+ console.log("\n" + chalk2.green("\u2713") + " Tables:");
389
+ for (const table of tables) {
390
+ console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
391
+ }
392
+ const relativeOutput = path3.relative(process.cwd(), outputDir);
393
+ console.log("\n" + chalk2.green("\u2713") + " Generated files:");
394
+ console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
395
+ console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
396
+ console.log(chalk2.dim(` ${relativeOutput}/index.ts
397
+ `));
398
+ } catch (error) {
399
+ spinner.fail("Failed to generate types");
400
+ console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
401
+ process.exit(1);
402
+ }
403
+ }
404
+
405
+ export {
406
+ getCredentials,
407
+ requireAuth,
408
+ saveCredentials,
409
+ clearCredentials,
410
+ loadConfig,
411
+ resolvePath,
412
+ detectFramework,
413
+ getFrameworkDevCommand,
414
+ generateTypes,
415
+ generateCommand
416
+ };
@@ -0,0 +1,418 @@
1
+ // src/commands/generate.ts
2
+ import chalk2 from "chalk";
3
+ import ora from "ora";
4
+ import fs3 from "fs-extra";
5
+ import path3 from "path";
6
+
7
+ // src/utils/auth.ts
8
+ import chalk from "chalk";
9
+ import fs from "fs-extra";
10
+ import path from "path";
11
+ var CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
12
+ var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
13
+ async function getCredentials() {
14
+ try {
15
+ if (await fs.pathExists(CREDENTIALS_FILE)) {
16
+ return await fs.readJSON(CREDENTIALS_FILE);
17
+ }
18
+ } catch {
19
+ }
20
+ return null;
21
+ }
22
+ async function requireAuth() {
23
+ const credentials = await getCredentials();
24
+ if (!credentials) {
25
+ console.error(chalk.red("\n\u2717 Not logged in\n"));
26
+ console.log(chalk.dim("Run `tthr login` to authenticate\n"));
27
+ process.exit(1);
28
+ }
29
+ if (credentials.expiresAt) {
30
+ const expiresAt = new Date(credentials.expiresAt);
31
+ if (/* @__PURE__ */ new Date() > expiresAt) {
32
+ console.error(chalk.red("\n\u2717 Session expired\n"));
33
+ console.log(chalk.dim("Run `tthr login` to authenticate\n"));
34
+ process.exit(1);
35
+ }
36
+ }
37
+ return credentials;
38
+ }
39
+ async function saveCredentials(credentials) {
40
+ await fs.ensureDir(CONFIG_DIR);
41
+ await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
42
+ }
43
+ async function clearCredentials() {
44
+ try {
45
+ await fs.remove(CREDENTIALS_FILE);
46
+ } catch {
47
+ }
48
+ }
49
+
50
+ // src/utils/config.ts
51
+ import fs2 from "fs-extra";
52
+ import path2 from "path";
53
+ var DEFAULT_CONFIG = {
54
+ schema: "./tether/schema.ts",
55
+ functions: "./tether/functions",
56
+ output: "./tether/_generated",
57
+ dev: {
58
+ port: 3001,
59
+ host: "localhost"
60
+ },
61
+ database: {
62
+ walMode: true
63
+ }
64
+ };
65
+ async function loadConfig(cwd = process.cwd()) {
66
+ const configPath = path2.resolve(cwd, "tether.config.ts");
67
+ if (!await fs2.pathExists(configPath)) {
68
+ return DEFAULT_CONFIG;
69
+ }
70
+ const configSource = await fs2.readFile(configPath, "utf-8");
71
+ const config = {};
72
+ const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
73
+ if (schemaMatch) {
74
+ config.schema = schemaMatch[1];
75
+ }
76
+ const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
77
+ if (functionsMatch) {
78
+ config.functions = functionsMatch[1];
79
+ }
80
+ const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
81
+ if (outputMatch) {
82
+ config.output = outputMatch[1];
83
+ }
84
+ const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
85
+ if (envMatch) {
86
+ config.environment = envMatch[1];
87
+ }
88
+ const portMatch = configSource.match(/port\s*:\s*(\d+)/);
89
+ if (portMatch) {
90
+ config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
91
+ }
92
+ const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
93
+ if (hostMatch) {
94
+ config.dev = { ...config.dev, host: hostMatch[1] };
95
+ }
96
+ return {
97
+ ...DEFAULT_CONFIG,
98
+ ...config,
99
+ dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
100
+ database: { ...DEFAULT_CONFIG.database, ...config.database }
101
+ };
102
+ }
103
+ function resolvePath(configPath, cwd = process.cwd()) {
104
+ const normalised = configPath.replace(/^\.\//, "");
105
+ return path2.resolve(cwd, normalised);
106
+ }
107
+ async function detectFramework(cwd = process.cwd()) {
108
+ const packageJsonPath = path2.resolve(cwd, "package.json");
109
+ if (!await fs2.pathExists(packageJsonPath)) {
110
+ return "unknown";
111
+ }
112
+ try {
113
+ const packageJson = await fs2.readJson(packageJsonPath);
114
+ const deps = {
115
+ ...packageJson.dependencies,
116
+ ...packageJson.devDependencies
117
+ };
118
+ if (deps.nuxt || deps["@nuxt/kit"]) {
119
+ return "nuxt";
120
+ }
121
+ if (deps.next) {
122
+ return "next";
123
+ }
124
+ if (deps["@sveltejs/kit"]) {
125
+ return "sveltekit";
126
+ }
127
+ if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
128
+ return "vite";
129
+ }
130
+ return "vanilla";
131
+ } catch {
132
+ return "unknown";
133
+ }
134
+ }
135
+ function getFrameworkDevCommand(framework) {
136
+ switch (framework) {
137
+ case "nuxt":
138
+ return "nuxt dev";
139
+ case "next":
140
+ return "next dev";
141
+ case "sveltekit":
142
+ return "vite dev";
143
+ case "vite":
144
+ return "vite dev";
145
+ case "vanilla":
146
+ return null;
147
+ // No default dev server for vanilla
148
+ default:
149
+ return null;
150
+ }
151
+ }
152
+
153
+ // src/commands/generate.ts
154
+ function parseSchemaFile(source) {
155
+ const tables = [];
156
+ const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
157
+ if (!schemaMatch) return tables;
158
+ const schemaContent = schemaMatch[1];
159
+ const tableStartRegex = /(\w+)\s*:\s*\{/g;
160
+ let match;
161
+ while ((match = tableStartRegex.exec(schemaContent)) !== null) {
162
+ const tableName = match[1];
163
+ const startOffset = match.index + match[0].length;
164
+ let braceCount = 1;
165
+ let endOffset = startOffset;
166
+ for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
167
+ const char = schemaContent[i];
168
+ if (char === "{") braceCount++;
169
+ else if (char === "}") braceCount--;
170
+ endOffset = i;
171
+ }
172
+ const columnsContent = schemaContent.slice(startOffset, endOffset);
173
+ const columns = parseColumns(columnsContent);
174
+ tables.push({ name: tableName, columns });
175
+ }
176
+ return tables;
177
+ }
178
+ function parseColumns(content) {
179
+ const columns = [];
180
+ const columnRegex = /(\w+)\s*:\s*(\w+)\s*\(\s*\)([^,\n}]*)/g;
181
+ let match;
182
+ while ((match = columnRegex.exec(content)) !== null) {
183
+ const name = match[1];
184
+ const schemaType = match[2];
185
+ const modifiers = match[3] || "";
186
+ const nullable = !modifiers.includes(".notNull()");
187
+ columns.push({
188
+ name,
189
+ type: schemaTypeToTS(schemaType),
190
+ nullable
191
+ });
192
+ }
193
+ return columns;
194
+ }
195
+ function schemaTypeToTS(schemaType) {
196
+ switch (schemaType) {
197
+ case "text":
198
+ return "string";
199
+ case "integer":
200
+ return "number";
201
+ case "real":
202
+ return "number";
203
+ case "boolean":
204
+ return "boolean";
205
+ case "timestamp":
206
+ return "string";
207
+ case "json":
208
+ return "unknown";
209
+ case "blob":
210
+ return "Uint8Array";
211
+ default:
212
+ return "unknown";
213
+ }
214
+ }
215
+ function tableNameToInterface(tableName) {
216
+ let name = tableName;
217
+ if (name.endsWith("ies")) {
218
+ name = name.slice(0, -3) + "y";
219
+ } else if (name.endsWith("s") && !name.endsWith("ss")) {
220
+ name = name.slice(0, -1);
221
+ }
222
+ return name.charAt(0).toUpperCase() + name.slice(1);
223
+ }
224
+ function generateDbFile(tables) {
225
+ const lines = [
226
+ "// Auto-generated by Tether CLI - do not edit manually",
227
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
228
+ "",
229
+ "import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
230
+ ""
231
+ ];
232
+ for (const table of tables) {
233
+ const interfaceName = tableNameToInterface(table.name);
234
+ lines.push(`export interface ${interfaceName} {`);
235
+ lines.push(" _id: string;");
236
+ for (const col of table.columns) {
237
+ const typeStr = col.nullable ? `${col.type} | null` : col.type;
238
+ lines.push(` ${col.name}: ${typeStr};`);
239
+ }
240
+ lines.push(" _createdAt: string;");
241
+ lines.push("}");
242
+ lines.push("");
243
+ }
244
+ lines.push("export interface Schema {");
245
+ for (const table of tables) {
246
+ const interfaceName = tableNameToInterface(table.name);
247
+ lines.push(` ${table.name}: ${interfaceName};`);
248
+ }
249
+ lines.push("}");
250
+ lines.push("");
251
+ lines.push("// Database client with typed tables");
252
+ lines.push("// This is a proxy that will be populated by the Tether runtime");
253
+ lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
254
+ lines.push("");
255
+ return lines.join("\n");
256
+ }
257
+ async function parseFunctionsDir(functionsDir) {
258
+ const functions = [];
259
+ if (!await fs3.pathExists(functionsDir)) {
260
+ return functions;
261
+ }
262
+ const files = await fs3.readdir(functionsDir);
263
+ for (const file of files) {
264
+ if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
265
+ const filePath = path3.join(functionsDir, file);
266
+ const stat = await fs3.stat(filePath);
267
+ if (!stat.isFile()) continue;
268
+ const moduleName = file.replace(/\.(ts|js)$/, "");
269
+ const source = await fs3.readFile(filePath, "utf-8");
270
+ const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action)\s*\(/g;
271
+ let match;
272
+ while ((match = exportRegex.exec(source)) !== null) {
273
+ functions.push({
274
+ name: match[1],
275
+ moduleName
276
+ });
277
+ }
278
+ }
279
+ return functions;
280
+ }
281
+ async function generateApiFile(functionsDir) {
282
+ const functions = await parseFunctionsDir(functionsDir);
283
+ const moduleMap = /* @__PURE__ */ new Map();
284
+ for (const fn of functions) {
285
+ if (!moduleMap.has(fn.moduleName)) {
286
+ moduleMap.set(fn.moduleName, []);
287
+ }
288
+ moduleMap.get(fn.moduleName).push(fn.name);
289
+ }
290
+ const lines = [
291
+ "// Auto-generated by Tether CLI - do not edit manually",
292
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
293
+ "",
294
+ "import { createApiProxy } from '@tthr/client';",
295
+ "",
296
+ "/**",
297
+ " * API function reference type for useQuery/useMutation.",
298
+ ' * The _name property contains the function path (e.g., "users.list").',
299
+ " */",
300
+ "export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
301
+ " _name: string;",
302
+ " _args?: TArgs;",
303
+ " _result?: TResult;",
304
+ "}",
305
+ ""
306
+ ];
307
+ if (moduleMap.size > 0) {
308
+ for (const [moduleName, fnNames] of moduleMap) {
309
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
310
+ lines.push(`export interface ${interfaceName} {`);
311
+ for (const fnName of fnNames) {
312
+ lines.push(` ${fnName}: ApiFunction;`);
313
+ }
314
+ lines.push("}");
315
+ lines.push("");
316
+ }
317
+ lines.push("export interface Api {");
318
+ for (const moduleName of moduleMap.keys()) {
319
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
320
+ lines.push(` ${moduleName}: ${interfaceName};`);
321
+ }
322
+ lines.push("}");
323
+ } else {
324
+ lines.push("/**");
325
+ lines.push(" * Flexible API type that allows access to any function path.");
326
+ lines.push(" * Functions are accessed as api.moduleName.functionName");
327
+ lines.push(" * The actual types depend on your function definitions.");
328
+ lines.push(" */");
329
+ lines.push("export type Api = {");
330
+ lines.push(" [module: string]: {");
331
+ lines.push(" [fn: string]: ApiFunction;");
332
+ lines.push(" };");
333
+ lines.push("};");
334
+ }
335
+ lines.push("");
336
+ lines.push("// API client proxy - provides typed access to your functions");
337
+ lines.push("// On the client: returns { _name } references for useQuery/useMutation");
338
+ lines.push("// In Tether functions: calls the actual function implementation");
339
+ lines.push("export const api = createApiProxy<Api>();");
340
+ lines.push("");
341
+ return lines.join("\n");
342
+ }
343
+ async function generateTypes(options = {}) {
344
+ const config = await loadConfig();
345
+ const schemaPath = resolvePath(config.schema);
346
+ const outputDir = resolvePath(config.output);
347
+ const functionsDir = resolvePath(config.functions);
348
+ if (!await fs3.pathExists(schemaPath)) {
349
+ throw new Error(`Schema file not found: ${schemaPath}`);
350
+ }
351
+ const schemaSource = await fs3.readFile(schemaPath, "utf-8");
352
+ const tables = parseSchemaFile(schemaSource);
353
+ await fs3.ensureDir(outputDir);
354
+ await fs3.writeFile(
355
+ path3.join(outputDir, "db.ts"),
356
+ generateDbFile(tables)
357
+ );
358
+ await fs3.writeFile(
359
+ path3.join(outputDir, "api.ts"),
360
+ await generateApiFile(functionsDir)
361
+ );
362
+ await fs3.writeFile(
363
+ path3.join(outputDir, "index.ts"),
364
+ `// Auto-generated by Tether CLI - do not edit manually
365
+ export * from './db';
366
+ export * from './api';
367
+ `
368
+ );
369
+ return { tables, outputDir };
370
+ }
371
+ async function generateCommand() {
372
+ await requireAuth();
373
+ const configPath = path3.resolve(process.cwd(), "tether.config.ts");
374
+ if (!await fs3.pathExists(configPath)) {
375
+ console.log(chalk2.red("\nError: Not a Tether project"));
376
+ console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
377
+ process.exit(1);
378
+ }
379
+ console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
380
+ const spinner = ora("Reading schema...").start();
381
+ try {
382
+ spinner.text = "Generating types...";
383
+ const { tables, outputDir } = await generateTypes();
384
+ if (tables.length === 0) {
385
+ spinner.warn("No tables found in schema");
386
+ console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
387
+ return;
388
+ }
389
+ spinner.succeed(`Types generated for ${tables.length} table(s)`);
390
+ console.log("\n" + chalk2.green("\u2713") + " Tables:");
391
+ for (const table of tables) {
392
+ console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
393
+ }
394
+ const relativeOutput = path3.relative(process.cwd(), outputDir);
395
+ console.log("\n" + chalk2.green("\u2713") + " Generated files:");
396
+ console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
397
+ console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
398
+ console.log(chalk2.dim(` ${relativeOutput}/index.ts
399
+ `));
400
+ } catch (error) {
401
+ spinner.fail("Failed to generate types");
402
+ console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
403
+ process.exit(1);
404
+ }
405
+ }
406
+
407
+ export {
408
+ getCredentials,
409
+ requireAuth,
410
+ saveCredentials,
411
+ clearCredentials,
412
+ loadConfig,
413
+ resolvePath,
414
+ detectFramework,
415
+ getFrameworkDevCommand,
416
+ generateTypes,
417
+ generateCommand
418
+ };
@@ -0,0 +1,8 @@
1
+ import {
2
+ generateCommand,
3
+ generateTypes
4
+ } from "./chunk-2IDRK6NG.js";
5
+ export {
6
+ generateCommand,
7
+ generateTypes
8
+ };
@@ -0,0 +1,8 @@
1
+ import {
2
+ generateCommand,
3
+ generateTypes
4
+ } from "./chunk-TU5LOAQ5.js";
5
+ export {
6
+ generateCommand,
7
+ generateTypes
8
+ };
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  requireAuth,
10
10
  resolvePath,
11
11
  saveCredentials
12
- } from "./chunk-LXODXQ5V.js";
12
+ } from "./chunk-TU5LOAQ5.js";
13
13
 
14
14
  // src/index.ts
15
15
  import { Command } from "commander";
@@ -98,8 +98,7 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
98
98
  return;
99
99
  }
100
100
  spinner.text = "Deploying schema...";
101
- const envPath = environment !== "production" ? `/env/${environment}` : "";
102
- const schemaUrl = `${API_URL}/projects/${projectId}${envPath}/deploy/schema`;
101
+ const schemaUrl = `${API_URL}/projects/${projectId}/env/${environment}/deploy/schema`;
103
102
  console.log(chalk.dim(`
104
103
  URL: ${schemaUrl}`));
105
104
  const requestBody = {
@@ -179,8 +178,7 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
179
178
  return;
180
179
  }
181
180
  spinner.text = "Deploying functions...";
182
- const envPath = environment !== "production" ? `/env/${environment}` : "";
183
- const functionsUrl = `${API_URL}/projects/${projectId}${envPath}/deploy/functions`;
181
+ const functionsUrl = `${API_URL}/projects/${projectId}/env/${environment}/deploy/functions`;
184
182
  console.log(chalk.dim(`
185
183
  URL: ${functionsUrl}`));
186
184
  const response = await fetch(functionsUrl, {
@@ -279,9 +277,18 @@ function parseSchema(source) {
279
277
  const colName = colMatch[1];
280
278
  const colType = colMatch[2];
281
279
  const modifiers = colMatch[3];
280
+ if (modifiers.includes(".primaryKey()")) {
281
+ throw new Error(
282
+ `Cannot use .primaryKey() on column "${colName}" in table "${tableName}". Tether automatically adds a "_id" primary key column to all tables. Remove the .primaryKey() modifier from your schema.`
283
+ );
284
+ }
285
+ if (colName === "id") {
286
+ throw new Error(
287
+ `Cannot use column name "id" in table "${tableName}". Tether automatically adds a "_id" primary key column. Use a different column name or rely on the system-provided "_id".`
288
+ );
289
+ }
282
290
  columns[colName] = {
283
291
  type: colType,
284
- primaryKey: modifiers.includes(".primaryKey()"),
285
292
  notNull: modifiers.includes(".notNull()"),
286
293
  unique: modifiers.includes(".unique()"),
287
294
  hasDefault: modifiers.includes(".default("),
@@ -308,6 +315,9 @@ function getColumnSqlType(type) {
308
315
  return "INTEGER";
309
316
  case "json":
310
317
  return "JSON";
318
+ case "asset":
319
+ return "TEXT";
320
+ // Stores asset ID (UUID reference to _tether_files)
311
321
  default:
312
322
  return "TEXT";
313
323
  }
@@ -315,10 +325,8 @@ function getColumnSqlType(type) {
315
325
  function buildColumnSql(colName, def, forAlterTable = false) {
316
326
  const sqlType = getColumnSqlType(def.type);
317
327
  let colSql = `${colName} ${sqlType}`;
318
- const addingPrimaryKey = def.primaryKey && !forAlterTable;
319
- if (addingPrimaryKey) colSql += " PRIMARY KEY";
320
328
  if (def.notNull && !forAlterTable) colSql += " NOT NULL";
321
- if (def.unique && !addingPrimaryKey) colSql += " UNIQUE";
329
+ if (def.unique) colSql += " UNIQUE";
322
330
  if (def.hasDefault && def.type === "timestamp" && !forAlterTable) {
323
331
  colSql += " DEFAULT (datetime('now'))";
324
332
  }
@@ -703,40 +711,28 @@ export default defineConfig({
703
711
 
704
712
  // API endpoint (defaults to Tether Cloud)
705
713
  url: process.env.TETHER_URL || 'https://tether-api.strands.gg',
706
-
707
- // Schema file location
708
- schema: './tether/schema.ts',
709
-
710
- // Functions directory
711
- functions: './tether/functions',
712
-
713
- // Generated types output
714
- output: './tether/_generated',
715
714
  });
716
715
  `
717
716
  );
718
717
  await fs2.writeFile(
719
718
  path2.join(projectPath, "tether", "schema.ts"),
720
- `import { defineSchema, text, integer, timestamp } from '@tthr/schema';
719
+ `import { defineSchema, text, timestamp } from '@tthr/schema';
721
720
 
722
721
  export default defineSchema({
723
722
  // Example: posts table
723
+ // Note: _id and _createdAt are automatically added by Tether
724
724
  posts: {
725
- id: text().primaryKey(),
726
725
  title: text().notNull(),
727
726
  content: text(),
728
727
  authorId: text().notNull(),
729
- createdAt: timestamp().notNull().default('now'),
730
728
  updatedAt: timestamp().notNull().default('now'),
731
729
  },
732
730
 
733
731
  // Example: comments table
734
732
  comments: {
735
- id: text().primaryKey(),
736
- postId: text().notNull().references('posts.id'),
733
+ postId: text().notNull().references('posts._id'),
737
734
  content: text().notNull(),
738
735
  authorId: text().notNull(),
739
- createdAt: timestamp().notNull().default('now'),
740
736
  },
741
737
  });
742
738
  `
@@ -752,59 +748,55 @@ export const list = query({
752
748
  }),
753
749
  handler: async ({ args, db }) => {
754
750
  return db.posts.findMany({
755
- orderBy: { createdAt: 'desc' },
751
+ orderBy: { _createdAt: 'desc' },
756
752
  limit: args.limit,
757
753
  });
758
754
  },
759
755
  });
760
756
 
761
- // Get a single post
757
+ // Get a single post by _id
762
758
  export const get = query({
763
759
  args: z.object({
764
- id: z.string(),
760
+ _id: z.string(),
765
761
  }),
766
762
  handler: async ({ args, db }) => {
767
763
  return db.posts.findUnique({
768
- where: { id: args.id },
764
+ where: { _id: args._id },
769
765
  });
770
766
  },
771
767
  });
772
768
 
773
769
  // Create a new post
770
+ // Note: _id and _createdAt are automatically added by Tether
774
771
  export const create = mutation({
775
772
  args: z.object({
776
773
  title: z.string().min(1),
777
774
  content: z.string().optional(),
778
775
  }),
779
776
  handler: async ({ args, ctx, db }) => {
780
- const id = crypto.randomUUID();
781
- const now = new Date().toISOString();
782
-
783
- await db.posts.create({
777
+ const post = await db.posts.create({
784
778
  data: {
785
- id,
786
779
  title: args.title,
787
780
  content: args.content ?? '',
788
781
  authorId: ctx.userId ?? 'anonymous',
789
- createdAt: now,
790
- updatedAt: now,
782
+ updatedAt: new Date().toISOString(),
791
783
  },
792
784
  });
793
785
 
794
- return { id };
786
+ return { _id: post._id };
795
787
  },
796
788
  });
797
789
 
798
790
  // Update a post
799
791
  export const update = mutation({
800
792
  args: z.object({
801
- id: z.string(),
793
+ _id: z.string(),
802
794
  title: z.string().min(1).optional(),
803
795
  content: z.string().optional(),
804
796
  }),
805
797
  handler: async ({ args, db }) => {
806
798
  await db.posts.update({
807
- where: { id: args.id },
799
+ where: { _id: args._id },
808
800
  data: {
809
801
  ...(args.title && { title: args.title }),
810
802
  ...(args.content !== undefined && { content: args.content }),
@@ -817,11 +809,11 @@ export const update = mutation({
817
809
  // Delete a post
818
810
  export const remove = mutation({
819
811
  args: z.object({
820
- id: z.string(),
812
+ _id: z.string(),
821
813
  }),
822
814
  handler: async ({ args, db }) => {
823
815
  await db.posts.delete({
824
- where: { id: args.id },
816
+ where: { _id: args._id },
825
817
  });
826
818
  },
827
819
  });
@@ -847,7 +839,7 @@ export const list = query({
847
839
  }),
848
840
  handler: async ({ args, db }) => {
849
841
  return db.comments.findMany({
850
- orderBy: { createdAt: 'desc' },
842
+ orderBy: { _createdAt: 'desc' },
851
843
  limit: args.limit,
852
844
  });
853
845
  },
@@ -862,44 +854,40 @@ export const listByPost = query({
862
854
  handler: async ({ args, db }) => {
863
855
  return db.comments.findMany({
864
856
  where: { postId: args.postId },
865
- orderBy: { createdAt: 'asc' },
857
+ orderBy: { _createdAt: 'asc' },
866
858
  limit: args.limit,
867
859
  });
868
860
  },
869
861
  });
870
862
 
871
863
  // Create a new comment
864
+ // Note: _id and _createdAt are automatically added by Tether
872
865
  export const create = mutation({
873
866
  args: z.object({
874
867
  postId: z.string(),
875
868
  content: z.string().min(1),
876
869
  }),
877
870
  handler: async ({ args, ctx, db }) => {
878
- const id = crypto.randomUUID();
879
- const now = new Date().toISOString();
880
-
881
- await db.comments.create({
871
+ const comment = await db.comments.create({
882
872
  data: {
883
- id,
884
873
  postId: args.postId,
885
874
  content: args.content,
886
875
  authorId: ctx.userId ?? 'anonymous',
887
- createdAt: now,
888
876
  },
889
877
  });
890
878
 
891
- return { id };
879
+ return { _id: comment._id };
892
880
  },
893
881
  });
894
882
 
895
883
  // Delete a comment
896
884
  export const remove = mutation({
897
885
  args: z.object({
898
- id: z.string(),
886
+ _id: z.string(),
899
887
  }),
900
888
  handler: async ({ args, db }) => {
901
889
  await db.comments.delete({
902
- where: { id: args.id },
890
+ where: { _id: args._id },
903
891
  });
904
892
  },
905
893
  });
@@ -1522,7 +1510,7 @@ async function runGenerate(spinner) {
1522
1510
  }
1523
1511
  isGenerating = true;
1524
1512
  try {
1525
- const { generateTypes } = await import("./generate-YQ4QRPXF.js");
1513
+ const { generateTypes } = await import("./generate-HRIVNCH3.js");
1526
1514
  spinner.text = "Regenerating types...";
1527
1515
  await generateTypes({ silent: true });
1528
1516
  spinner.succeed("Types regenerated");
@@ -1703,7 +1691,7 @@ async function devCommand(options) {
1703
1691
  }
1704
1692
  }
1705
1693
  spinner.text = "Generating types...";
1706
- const { generateTypes } = await import("./generate-YQ4QRPXF.js");
1694
+ const { generateTypes } = await import("./generate-HRIVNCH3.js");
1707
1695
  await generateTypes({ silent: true });
1708
1696
  spinner.succeed("Types generated");
1709
1697
  spinner.start("Setting up file watchers...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tthr",
3
- "version": "0.0.41",
3
+ "version": "0.0.44",
4
4
  "description": "Tether CLI - project scaffolding and deployment",
5
5
  "type": "module",
6
6
  "bin": {