tthr 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-FBTKIMH3.js +546 -0
- package/dist/generate-ZHAYHYVW.js +8 -0
- package/dist/index.js +114 -6
- package/package.json +2 -1
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/commands/generate.ts
|
|
9
|
+
import chalk2 from "chalk";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
import fs3 from "fs-extra";
|
|
12
|
+
import path3 from "path";
|
|
13
|
+
|
|
14
|
+
// src/utils/auth.ts
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
import fs from "fs-extra";
|
|
17
|
+
import path from "path";
|
|
18
|
+
var CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
|
|
19
|
+
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
20
|
+
var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
21
|
+
var API_URL = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
22
|
+
var REFRESH_THRESHOLD_DAYS = 7;
|
|
23
|
+
async function getCredentials() {
|
|
24
|
+
try {
|
|
25
|
+
if (await fs.pathExists(CREDENTIALS_FILE)) {
|
|
26
|
+
return await fs.readJSON(CREDENTIALS_FILE);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
async function refreshSession(credentials) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(`${API_URL}/auth/session/refresh`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
const updated = {
|
|
46
|
+
...credentials,
|
|
47
|
+
expiresAt: data.expiresAt
|
|
48
|
+
};
|
|
49
|
+
await saveCredentials(updated);
|
|
50
|
+
return updated;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function requireAuth() {
|
|
56
|
+
const credentials = await getCredentials();
|
|
57
|
+
if (!credentials) {
|
|
58
|
+
console.error(chalk.red("\n\u2717 Not logged in\n"));
|
|
59
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
if (credentials.expiresAt) {
|
|
63
|
+
const expiresAt = new Date(credentials.expiresAt);
|
|
64
|
+
const now = /* @__PURE__ */ new Date();
|
|
65
|
+
if (now > expiresAt) {
|
|
66
|
+
await clearCredentials();
|
|
67
|
+
console.error(chalk.red("\n\u2717 Session expired \u2014 you have been signed out\n"));
|
|
68
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const daysUntilExpiry = (expiresAt.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24);
|
|
72
|
+
if (daysUntilExpiry <= REFRESH_THRESHOLD_DAYS) {
|
|
73
|
+
const refreshed = await refreshSession(credentials);
|
|
74
|
+
if (refreshed) {
|
|
75
|
+
return refreshed;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return credentials;
|
|
80
|
+
}
|
|
81
|
+
async function saveCredentials(credentials) {
|
|
82
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
83
|
+
await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
|
|
84
|
+
}
|
|
85
|
+
async function clearCredentials() {
|
|
86
|
+
try {
|
|
87
|
+
await fs.remove(CREDENTIALS_FILE);
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/utils/config.ts
|
|
93
|
+
import fs2 from "fs-extra";
|
|
94
|
+
import path2 from "path";
|
|
95
|
+
var DEFAULT_CONFIG = {
|
|
96
|
+
schema: "./tether/schema.ts",
|
|
97
|
+
functions: "./tether/functions",
|
|
98
|
+
output: "./tether/_generated",
|
|
99
|
+
dev: {
|
|
100
|
+
port: 3001,
|
|
101
|
+
host: "localhost"
|
|
102
|
+
},
|
|
103
|
+
database: {
|
|
104
|
+
walMode: true
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
108
|
+
const configPath = path2.resolve(cwd, "tether.config.ts");
|
|
109
|
+
if (!await fs2.pathExists(configPath)) {
|
|
110
|
+
return DEFAULT_CONFIG;
|
|
111
|
+
}
|
|
112
|
+
const configSource = await fs2.readFile(configPath, "utf-8");
|
|
113
|
+
const config = {};
|
|
114
|
+
const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
|
|
115
|
+
if (schemaMatch) {
|
|
116
|
+
config.schema = schemaMatch[1];
|
|
117
|
+
}
|
|
118
|
+
const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
|
|
119
|
+
if (functionsMatch) {
|
|
120
|
+
config.functions = functionsMatch[1];
|
|
121
|
+
}
|
|
122
|
+
const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
|
|
123
|
+
if (outputMatch) {
|
|
124
|
+
config.output = outputMatch[1];
|
|
125
|
+
}
|
|
126
|
+
const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
|
|
127
|
+
if (envMatch) {
|
|
128
|
+
config.environment = envMatch[1];
|
|
129
|
+
}
|
|
130
|
+
const portMatch = configSource.match(/port\s*:\s*(\d+)/);
|
|
131
|
+
if (portMatch) {
|
|
132
|
+
config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
|
|
133
|
+
}
|
|
134
|
+
const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
|
|
135
|
+
if (hostMatch) {
|
|
136
|
+
config.dev = { ...config.dev, host: hostMatch[1] };
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...DEFAULT_CONFIG,
|
|
140
|
+
...config,
|
|
141
|
+
dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
|
|
142
|
+
database: { ...DEFAULT_CONFIG.database, ...config.database }
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function resolvePath(configPath, cwd = process.cwd()) {
|
|
146
|
+
const normalised = configPath.replace(/^\.\//, "");
|
|
147
|
+
return path2.resolve(cwd, normalised);
|
|
148
|
+
}
|
|
149
|
+
async function detectFramework(cwd = process.cwd()) {
|
|
150
|
+
const packageJsonPath = path2.resolve(cwd, "package.json");
|
|
151
|
+
if (!await fs2.pathExists(packageJsonPath)) {
|
|
152
|
+
return "unknown";
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const packageJson = await fs2.readJson(packageJsonPath);
|
|
156
|
+
const deps = {
|
|
157
|
+
...packageJson.dependencies,
|
|
158
|
+
...packageJson.devDependencies
|
|
159
|
+
};
|
|
160
|
+
if (deps.nuxt || deps["@nuxt/kit"]) {
|
|
161
|
+
return "nuxt";
|
|
162
|
+
}
|
|
163
|
+
if (deps.next) {
|
|
164
|
+
return "next";
|
|
165
|
+
}
|
|
166
|
+
if (deps["@sveltejs/kit"]) {
|
|
167
|
+
return "sveltekit";
|
|
168
|
+
}
|
|
169
|
+
if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
|
|
170
|
+
return "vite";
|
|
171
|
+
}
|
|
172
|
+
return "vanilla";
|
|
173
|
+
} catch {
|
|
174
|
+
return "unknown";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function getFrameworkDevCommand(framework) {
|
|
178
|
+
switch (framework) {
|
|
179
|
+
case "nuxt":
|
|
180
|
+
return "nuxt dev";
|
|
181
|
+
case "next":
|
|
182
|
+
return "next dev";
|
|
183
|
+
case "sveltekit":
|
|
184
|
+
return "vite dev";
|
|
185
|
+
case "vite":
|
|
186
|
+
return "vite dev";
|
|
187
|
+
case "vanilla":
|
|
188
|
+
return null;
|
|
189
|
+
// No default dev server for vanilla
|
|
190
|
+
default:
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/commands/generate.ts
|
|
196
|
+
function parseSchemaFile(source) {
|
|
197
|
+
const tables = [];
|
|
198
|
+
const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
|
|
199
|
+
if (!schemaMatch) return tables;
|
|
200
|
+
const schemaContent = schemaMatch[1];
|
|
201
|
+
const tableStartRegex = /(\w+)\s*:\s*\{/g;
|
|
202
|
+
let match;
|
|
203
|
+
while ((match = tableStartRegex.exec(schemaContent)) !== null) {
|
|
204
|
+
const tableName = match[1];
|
|
205
|
+
const startOffset = match.index + match[0].length;
|
|
206
|
+
let braceCount = 1;
|
|
207
|
+
let endOffset = startOffset;
|
|
208
|
+
for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
|
|
209
|
+
const char = schemaContent[i];
|
|
210
|
+
if (char === "{") braceCount++;
|
|
211
|
+
else if (char === "}") braceCount--;
|
|
212
|
+
endOffset = i;
|
|
213
|
+
}
|
|
214
|
+
const columnsContent = schemaContent.slice(startOffset, endOffset);
|
|
215
|
+
const columns = parseColumns(columnsContent);
|
|
216
|
+
tables.push({ name: tableName, columns });
|
|
217
|
+
}
|
|
218
|
+
return tables;
|
|
219
|
+
}
|
|
220
|
+
function parseColumns(content) {
|
|
221
|
+
const columns = [];
|
|
222
|
+
const columnRegex = /(\w+)\s*:\s*(\w+)(?:<([^>]+)>)?\s*\(\s*\)((?:\[.*?\]|[^,\n}])*)/g;
|
|
223
|
+
let match;
|
|
224
|
+
while ((match = columnRegex.exec(content)) !== null) {
|
|
225
|
+
const name = match[1];
|
|
226
|
+
const schemaType = match[2];
|
|
227
|
+
const genericType = match[3];
|
|
228
|
+
const modifiers = match[4] || "";
|
|
229
|
+
const nullable = !modifiers.includes(".notNull()");
|
|
230
|
+
const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
|
|
231
|
+
let oneOf;
|
|
232
|
+
if (oneOfMatch) {
|
|
233
|
+
oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
234
|
+
}
|
|
235
|
+
columns.push({
|
|
236
|
+
name,
|
|
237
|
+
type: schemaTypeToTS(schemaType),
|
|
238
|
+
nullable,
|
|
239
|
+
jsonType: genericType,
|
|
240
|
+
// Store the generic type for json columns
|
|
241
|
+
oneOf
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return columns;
|
|
245
|
+
}
|
|
246
|
+
function schemaTypeToTS(schemaType) {
|
|
247
|
+
switch (schemaType) {
|
|
248
|
+
case "text":
|
|
249
|
+
return "string";
|
|
250
|
+
case "integer":
|
|
251
|
+
return "number";
|
|
252
|
+
case "real":
|
|
253
|
+
return "number";
|
|
254
|
+
case "boolean":
|
|
255
|
+
return "boolean";
|
|
256
|
+
case "timestamp":
|
|
257
|
+
return "string";
|
|
258
|
+
case "json":
|
|
259
|
+
return "unknown";
|
|
260
|
+
case "blob":
|
|
261
|
+
return "Uint8Array";
|
|
262
|
+
case "asset":
|
|
263
|
+
return "TetherAsset";
|
|
264
|
+
// Asset object returned by API
|
|
265
|
+
default:
|
|
266
|
+
return "unknown";
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function tableNameToInterface(tableName) {
|
|
270
|
+
let name = tableName;
|
|
271
|
+
if (name.endsWith("ies")) {
|
|
272
|
+
name = name.slice(0, -3) + "y";
|
|
273
|
+
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
274
|
+
name = name.slice(0, -1);
|
|
275
|
+
}
|
|
276
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
277
|
+
}
|
|
278
|
+
function generateDbFile(tables) {
|
|
279
|
+
const jsonTypes = /* @__PURE__ */ new Set();
|
|
280
|
+
for (const table of tables) {
|
|
281
|
+
for (const col of table.columns) {
|
|
282
|
+
if (col.jsonType) {
|
|
283
|
+
jsonTypes.add(col.jsonType.replace(/\[\]$/, ""));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const lines = [
|
|
288
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
289
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
290
|
+
"",
|
|
291
|
+
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
292
|
+
"import {",
|
|
293
|
+
" query as baseQuery,",
|
|
294
|
+
" mutation as baseMutation,",
|
|
295
|
+
" type QueryDefinition,",
|
|
296
|
+
" type MutationDefinition,",
|
|
297
|
+
" z,",
|
|
298
|
+
"} from '@tthr/server';"
|
|
299
|
+
];
|
|
300
|
+
if (jsonTypes.size > 0) {
|
|
301
|
+
const typeImports = Array.from(jsonTypes).sort().join(", ");
|
|
302
|
+
lines.push(`import type { ${typeImports} } from '../schema';`);
|
|
303
|
+
}
|
|
304
|
+
lines.push("");
|
|
305
|
+
lines.push("// Asset type returned by the API for asset columns");
|
|
306
|
+
lines.push("export interface TetherAsset {");
|
|
307
|
+
lines.push(" id: string;");
|
|
308
|
+
lines.push(" filename: string;");
|
|
309
|
+
lines.push(" contentType: string;");
|
|
310
|
+
lines.push(" size: number;");
|
|
311
|
+
lines.push(" url: string;");
|
|
312
|
+
lines.push(" createdAt: string;");
|
|
313
|
+
lines.push("}");
|
|
314
|
+
lines.push("");
|
|
315
|
+
for (const table of tables) {
|
|
316
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
317
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
318
|
+
lines.push(" _id: string;");
|
|
319
|
+
for (const col of table.columns) {
|
|
320
|
+
let colType;
|
|
321
|
+
if (col.oneOf && col.oneOf.length > 0) {
|
|
322
|
+
colType = col.oneOf.map((v) => `'${v}'`).join(" | ");
|
|
323
|
+
} else {
|
|
324
|
+
colType = col.jsonType || col.type;
|
|
325
|
+
}
|
|
326
|
+
const typeStr = col.nullable ? `${colType} | null` : colType;
|
|
327
|
+
const optional = col.nullable ? "?" : "";
|
|
328
|
+
lines.push(` ${col.name}${optional}: ${typeStr};`);
|
|
329
|
+
}
|
|
330
|
+
lines.push(" _createdAt: string;");
|
|
331
|
+
lines.push(" _updatedAt?: string | null;");
|
|
332
|
+
lines.push(" _deletedAt?: string | null;");
|
|
333
|
+
lines.push("}");
|
|
334
|
+
lines.push("");
|
|
335
|
+
}
|
|
336
|
+
lines.push("export interface Schema {");
|
|
337
|
+
for (const table of tables) {
|
|
338
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
339
|
+
lines.push(` ${table.name}: ${interfaceName};`);
|
|
340
|
+
}
|
|
341
|
+
lines.push("}");
|
|
342
|
+
lines.push("");
|
|
343
|
+
lines.push("// Database client with typed tables");
|
|
344
|
+
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
345
|
+
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
346
|
+
lines.push("");
|
|
347
|
+
lines.push("// ============================================================================");
|
|
348
|
+
lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
|
|
349
|
+
lines.push("// This ensures the `db` parameter in handlers is properly typed with Schema");
|
|
350
|
+
lines.push("// ============================================================================");
|
|
351
|
+
lines.push("");
|
|
352
|
+
lines.push("/**");
|
|
353
|
+
lines.push(" * Define a query function with typed database access.");
|
|
354
|
+
lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
|
|
355
|
+
lines.push(" */");
|
|
356
|
+
lines.push("export function query<TArgs = void, TResult = unknown>(");
|
|
357
|
+
lines.push(" definition: QueryDefinition<TArgs, TResult, Schema>");
|
|
358
|
+
lines.push("): QueryDefinition<TArgs, TResult, Schema> {");
|
|
359
|
+
lines.push(" return baseQuery(definition);");
|
|
360
|
+
lines.push("}");
|
|
361
|
+
lines.push("");
|
|
362
|
+
lines.push("/**");
|
|
363
|
+
lines.push(" * Define a mutation function with typed database access.");
|
|
364
|
+
lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
|
|
365
|
+
lines.push(" */");
|
|
366
|
+
lines.push("export function mutation<TArgs = void, TResult = unknown>(");
|
|
367
|
+
lines.push(" definition: MutationDefinition<TArgs, TResult, Schema>");
|
|
368
|
+
lines.push("): MutationDefinition<TArgs, TResult, Schema> {");
|
|
369
|
+
lines.push(" return baseMutation(definition);");
|
|
370
|
+
lines.push("}");
|
|
371
|
+
lines.push("");
|
|
372
|
+
lines.push("// Re-export z for convenience");
|
|
373
|
+
lines.push("export { z };");
|
|
374
|
+
if (jsonTypes.size > 0) {
|
|
375
|
+
lines.push("");
|
|
376
|
+
lines.push("// Re-export JSON schema types");
|
|
377
|
+
const typeExports = Array.from(jsonTypes).sort().join(", ");
|
|
378
|
+
lines.push(`export type { ${typeExports} } from '../schema';`);
|
|
379
|
+
}
|
|
380
|
+
lines.push("");
|
|
381
|
+
return lines.join("\n");
|
|
382
|
+
}
|
|
383
|
+
async function parseFunctionsDir(functionsDir) {
|
|
384
|
+
const functions = [];
|
|
385
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
386
|
+
return functions;
|
|
387
|
+
}
|
|
388
|
+
const files = await fs3.readdir(functionsDir);
|
|
389
|
+
for (const file of files) {
|
|
390
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
391
|
+
const filePath = path3.join(functionsDir, file);
|
|
392
|
+
const stat = await fs3.stat(filePath);
|
|
393
|
+
if (!stat.isFile()) continue;
|
|
394
|
+
const moduleName = file.replace(/\.(ts|js)$/, "");
|
|
395
|
+
const source = await fs3.readFile(filePath, "utf-8");
|
|
396
|
+
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation)\s*\(/g;
|
|
397
|
+
let match;
|
|
398
|
+
while ((match = exportRegex.exec(source)) !== null) {
|
|
399
|
+
functions.push({
|
|
400
|
+
name: match[1],
|
|
401
|
+
moduleName
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return functions;
|
|
406
|
+
}
|
|
407
|
+
async function generateApiFile(functionsDir) {
|
|
408
|
+
const functions = await parseFunctionsDir(functionsDir);
|
|
409
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
410
|
+
for (const fn of functions) {
|
|
411
|
+
if (!moduleMap.has(fn.moduleName)) {
|
|
412
|
+
moduleMap.set(fn.moduleName, []);
|
|
413
|
+
}
|
|
414
|
+
moduleMap.get(fn.moduleName).push(fn.name);
|
|
415
|
+
}
|
|
416
|
+
const lines = [
|
|
417
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
418
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
419
|
+
"",
|
|
420
|
+
"import { createApiProxy } from '@tthr/client';",
|
|
421
|
+
"",
|
|
422
|
+
"/**",
|
|
423
|
+
" * API function reference type for useQuery/useMutation.",
|
|
424
|
+
' * The _name property contains the function path (e.g., "users.list").',
|
|
425
|
+
" */",
|
|
426
|
+
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
427
|
+
" _name: string;",
|
|
428
|
+
" _args?: TArgs;",
|
|
429
|
+
" _result?: TResult;",
|
|
430
|
+
"}",
|
|
431
|
+
""
|
|
432
|
+
];
|
|
433
|
+
if (moduleMap.size > 0) {
|
|
434
|
+
for (const [moduleName, fnNames] of moduleMap) {
|
|
435
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
436
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
437
|
+
for (const fnName of fnNames) {
|
|
438
|
+
lines.push(` ${fnName}: ApiFunction;`);
|
|
439
|
+
}
|
|
440
|
+
lines.push("}");
|
|
441
|
+
lines.push("");
|
|
442
|
+
}
|
|
443
|
+
lines.push("export interface Api {");
|
|
444
|
+
for (const moduleName of moduleMap.keys()) {
|
|
445
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
446
|
+
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
447
|
+
}
|
|
448
|
+
lines.push("}");
|
|
449
|
+
} else {
|
|
450
|
+
lines.push("/**");
|
|
451
|
+
lines.push(" * Flexible API type that allows access to any function path.");
|
|
452
|
+
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
453
|
+
lines.push(" * The actual types depend on your function definitions.");
|
|
454
|
+
lines.push(" */");
|
|
455
|
+
lines.push("export type Api = {");
|
|
456
|
+
lines.push(" [module: string]: {");
|
|
457
|
+
lines.push(" [fn: string]: ApiFunction;");
|
|
458
|
+
lines.push(" };");
|
|
459
|
+
lines.push("};");
|
|
460
|
+
}
|
|
461
|
+
lines.push("");
|
|
462
|
+
lines.push("// API client proxy - provides typed access to your functions");
|
|
463
|
+
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
464
|
+
lines.push("// In Tether functions: calls the actual function implementation");
|
|
465
|
+
lines.push("export const api = createApiProxy<Api>();");
|
|
466
|
+
lines.push("");
|
|
467
|
+
return lines.join("\n");
|
|
468
|
+
}
|
|
469
|
+
async function generateTypes(options = {}) {
|
|
470
|
+
const config = await loadConfig();
|
|
471
|
+
const schemaPath = resolvePath(config.schema);
|
|
472
|
+
const outputDir = resolvePath(config.output);
|
|
473
|
+
const functionsDir = resolvePath(config.functions);
|
|
474
|
+
if (!await fs3.pathExists(schemaPath)) {
|
|
475
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
476
|
+
}
|
|
477
|
+
const schemaSource = await fs3.readFile(schemaPath, "utf-8");
|
|
478
|
+
const tables = parseSchemaFile(schemaSource);
|
|
479
|
+
await fs3.ensureDir(outputDir);
|
|
480
|
+
await fs3.writeFile(
|
|
481
|
+
path3.join(outputDir, "db.ts"),
|
|
482
|
+
generateDbFile(tables)
|
|
483
|
+
);
|
|
484
|
+
await fs3.writeFile(
|
|
485
|
+
path3.join(outputDir, "api.ts"),
|
|
486
|
+
await generateApiFile(functionsDir)
|
|
487
|
+
);
|
|
488
|
+
await fs3.writeFile(
|
|
489
|
+
path3.join(outputDir, "index.ts"),
|
|
490
|
+
`// Auto-generated by Tether CLI - do not edit manually
|
|
491
|
+
export * from './db';
|
|
492
|
+
export * from './api';
|
|
493
|
+
`
|
|
494
|
+
);
|
|
495
|
+
return { tables, outputDir };
|
|
496
|
+
}
|
|
497
|
+
async function generateCommand() {
|
|
498
|
+
await requireAuth();
|
|
499
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
500
|
+
if (!await fs3.pathExists(configPath)) {
|
|
501
|
+
console.log(chalk2.red("\nError: Not a Tether project"));
|
|
502
|
+
console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
|
|
506
|
+
const spinner = ora("Reading schema...").start();
|
|
507
|
+
try {
|
|
508
|
+
spinner.text = "Generating types...";
|
|
509
|
+
const { tables, outputDir } = await generateTypes();
|
|
510
|
+
if (tables.length === 0) {
|
|
511
|
+
spinner.warn("No tables found in schema");
|
|
512
|
+
console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
516
|
+
console.log("\n" + chalk2.green("\u2713") + " Tables:");
|
|
517
|
+
for (const table of tables) {
|
|
518
|
+
console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
519
|
+
}
|
|
520
|
+
const relativeOutput = path3.relative(process.cwd(), outputDir);
|
|
521
|
+
console.log("\n" + chalk2.green("\u2713") + " Generated files:");
|
|
522
|
+
console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
|
|
523
|
+
console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
|
|
524
|
+
console.log(chalk2.dim(` ${relativeOutput}/index.ts
|
|
525
|
+
`));
|
|
526
|
+
} catch (error) {
|
|
527
|
+
spinner.fail("Failed to generate types");
|
|
528
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export {
|
|
534
|
+
__require,
|
|
535
|
+
API_URL,
|
|
536
|
+
getCredentials,
|
|
537
|
+
requireAuth,
|
|
538
|
+
saveCredentials,
|
|
539
|
+
clearCredentials,
|
|
540
|
+
loadConfig,
|
|
541
|
+
resolvePath,
|
|
542
|
+
detectFramework,
|
|
543
|
+
getFrameworkDevCommand,
|
|
544
|
+
generateTypes,
|
|
545
|
+
generateCommand
|
|
546
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import chalk from "chalk";
|
|
|
29
29
|
import ora from "ora";
|
|
30
30
|
import fs from "fs-extra";
|
|
31
31
|
import path from "path";
|
|
32
|
+
import * as esbuild from "esbuild";
|
|
32
33
|
var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
33
34
|
var API_URL2 = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
34
35
|
async function deployCommand(options) {
|
|
@@ -153,6 +154,57 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
}
|
|
157
|
+
async function bundleFunctionFile(filePath) {
|
|
158
|
+
const result = await esbuild.build({
|
|
159
|
+
entryPoints: [filePath],
|
|
160
|
+
bundle: true,
|
|
161
|
+
format: "esm",
|
|
162
|
+
platform: "neutral",
|
|
163
|
+
write: false,
|
|
164
|
+
// Target modern JS — the Deno runtime handles it
|
|
165
|
+
target: "es2022",
|
|
166
|
+
// Provide shims for the Tether packages — the runtime injects the real implementations
|
|
167
|
+
plugins: [{
|
|
168
|
+
name: "tether-shims",
|
|
169
|
+
setup(build2) {
|
|
170
|
+
build2.onResolve({ filter: /^@tthr\/server$/ }, () => ({
|
|
171
|
+
path: "@tthr/server",
|
|
172
|
+
namespace: "tether-shim"
|
|
173
|
+
}));
|
|
174
|
+
build2.onLoad({ filter: /.*/, namespace: "tether-shim" }, () => ({
|
|
175
|
+
contents: `
|
|
176
|
+
export function query(def) { return def; }
|
|
177
|
+
export function mutation(def) { return def; }
|
|
178
|
+
export { z } from "zod";
|
|
179
|
+
`,
|
|
180
|
+
loader: "js",
|
|
181
|
+
resolveDir: path.dirname(filePath)
|
|
182
|
+
}));
|
|
183
|
+
build2.onResolve({ filter: /^@tthr\/client$/ }, () => ({
|
|
184
|
+
path: "@tthr/client",
|
|
185
|
+
namespace: "tether-client-shim"
|
|
186
|
+
}));
|
|
187
|
+
build2.onLoad({ filter: /.*/, namespace: "tether-client-shim" }, () => ({
|
|
188
|
+
contents: `
|
|
189
|
+
export function createDatabaseProxy() { return {}; }
|
|
190
|
+
`,
|
|
191
|
+
loader: "js"
|
|
192
|
+
}));
|
|
193
|
+
build2.onResolve({ filter: /^zod$/ }, () => ({
|
|
194
|
+
path: "https://esm.sh/zod@3.24.4",
|
|
195
|
+
external: true
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
}],
|
|
199
|
+
logLevel: "silent"
|
|
200
|
+
});
|
|
201
|
+
if (result.errors.length > 0) {
|
|
202
|
+
const messages = await esbuild.formatMessages(result.errors, { kind: "error" });
|
|
203
|
+
throw new Error(`Bundle failed for ${filePath}:
|
|
204
|
+
${messages.join("\n")}`);
|
|
205
|
+
}
|
|
206
|
+
return result.outputFiles[0].text;
|
|
207
|
+
}
|
|
156
208
|
async function deployFunctionsToServer(projectId, token, functionsDir, environment, dryRun) {
|
|
157
209
|
const spinner = ora("Reading functions...").start();
|
|
158
210
|
try {
|
|
@@ -164,7 +216,7 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
|
|
|
164
216
|
return;
|
|
165
217
|
}
|
|
166
218
|
const files = await fs.readdir(functionsDir);
|
|
167
|
-
const tsFiles = files.filter((f) => f.endsWith(".ts"));
|
|
219
|
+
const tsFiles = files.filter((f) => f.endsWith(".ts") && !f.startsWith("_"));
|
|
168
220
|
if (tsFiles.length === 0) {
|
|
169
221
|
spinner.info("No function files found");
|
|
170
222
|
return;
|
|
@@ -175,6 +227,12 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
|
|
|
175
227
|
const source = await fs.readFile(filePath, "utf-8");
|
|
176
228
|
const moduleName = file.replace(".ts", "");
|
|
177
229
|
const parsedFunctions = parseFunctions(moduleName, source);
|
|
230
|
+
if (parsedFunctions.length === 0) continue;
|
|
231
|
+
spinner.text = `Bundling ${file}...`;
|
|
232
|
+
const bundledSource = await bundleFunctionFile(filePath);
|
|
233
|
+
for (const fn of parsedFunctions) {
|
|
234
|
+
fn.source = bundledSource;
|
|
235
|
+
}
|
|
178
236
|
functions.push(...parsedFunctions);
|
|
179
237
|
}
|
|
180
238
|
spinner.text = `Found ${functions.length} function(s)`;
|
|
@@ -407,14 +465,14 @@ function parseFunctions(moduleName, source) {
|
|
|
407
465
|
if (argsStartMatch && argsStartMatch.index !== void 0) {
|
|
408
466
|
const argsStartIndex = argsStartMatch.index + argsStartMatch[0].length - 1;
|
|
409
467
|
let braceCount = 1;
|
|
410
|
-
let
|
|
468
|
+
let braceEndIndex = argsStartIndex + 1;
|
|
411
469
|
for (let i = argsStartIndex + 1; i < fnBody.length && braceCount > 0; i++) {
|
|
412
470
|
const char = fnBody[i];
|
|
413
471
|
if (char === "{") braceCount++;
|
|
414
472
|
else if (char === "}") braceCount--;
|
|
415
|
-
|
|
473
|
+
braceEndIndex = i;
|
|
416
474
|
}
|
|
417
|
-
let parenEndIndex =
|
|
475
|
+
let parenEndIndex = braceEndIndex + 1;
|
|
418
476
|
while (parenEndIndex < fnBody.length && fnBody[parenEndIndex] !== ")") {
|
|
419
477
|
parenEndIndex++;
|
|
420
478
|
}
|
|
@@ -427,7 +485,8 @@ function parseFunctions(moduleName, source) {
|
|
|
427
485
|
name: `${moduleName}.${fnName}`,
|
|
428
486
|
type: fnType,
|
|
429
487
|
file: `${moduleName}.ts`,
|
|
430
|
-
source:
|
|
488
|
+
source: "",
|
|
489
|
+
// Set by caller from bundled output
|
|
431
490
|
args
|
|
432
491
|
});
|
|
433
492
|
}
|
|
@@ -1546,6 +1605,8 @@ var schemaWatcher = null;
|
|
|
1546
1605
|
var functionsWatcher = null;
|
|
1547
1606
|
var isGenerating = false;
|
|
1548
1607
|
var pendingGenerate = false;
|
|
1608
|
+
var isDeploying = false;
|
|
1609
|
+
var pendingDeploy = null;
|
|
1549
1610
|
async function runGenerate(spinner) {
|
|
1550
1611
|
if (isGenerating) {
|
|
1551
1612
|
pendingGenerate = true;
|
|
@@ -1572,6 +1633,41 @@ async function runGenerate(spinner) {
|
|
|
1572
1633
|
}
|
|
1573
1634
|
}
|
|
1574
1635
|
}
|
|
1636
|
+
async function runDeploy(what, projectId, token, schemaPath, functionsDir, environment, spinner) {
|
|
1637
|
+
if (isDeploying) {
|
|
1638
|
+
if (pendingDeploy === null) pendingDeploy = what;
|
|
1639
|
+
else if (pendingDeploy !== what) pendingDeploy = "all";
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
isDeploying = true;
|
|
1643
|
+
try {
|
|
1644
|
+
if (what === "schema" || what === "all") {
|
|
1645
|
+
spinner.text = "Deploying schema to development...";
|
|
1646
|
+
try {
|
|
1647
|
+
await deploySchemaToServer(projectId, token, schemaPath, environment);
|
|
1648
|
+
} catch (e) {
|
|
1649
|
+
console.log(chalk3.yellow(` Schema deploy failed: ${e instanceof Error ? e.message : e}`));
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (what === "functions" || what === "all") {
|
|
1653
|
+
spinner.text = "Deploying functions to development...";
|
|
1654
|
+
try {
|
|
1655
|
+
await deployFunctionsToServer(projectId, token, functionsDir, environment);
|
|
1656
|
+
} catch (e) {
|
|
1657
|
+
console.log(chalk3.yellow(` Functions deploy failed: ${e instanceof Error ? e.message : e}`));
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
spinner.succeed(`Deployed ${what} to ${environment}`);
|
|
1661
|
+
spinner.start("Watching for changes...");
|
|
1662
|
+
} finally {
|
|
1663
|
+
isDeploying = false;
|
|
1664
|
+
if (pendingDeploy) {
|
|
1665
|
+
const next = pendingDeploy;
|
|
1666
|
+
pendingDeploy = null;
|
|
1667
|
+
setTimeout(() => runDeploy(next, projectId, token, schemaPath, functionsDir, environment, spinner), 100);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1575
1671
|
async function validateSchema(schemaPath) {
|
|
1576
1672
|
try {
|
|
1577
1673
|
const content = await fs3.readFile(schemaPath, "utf-8");
|
|
@@ -1681,7 +1777,7 @@ function cleanup() {
|
|
|
1681
1777
|
}
|
|
1682
1778
|
}
|
|
1683
1779
|
async function devCommand(options) {
|
|
1684
|
-
await requireAuth();
|
|
1780
|
+
const credentials = await requireAuth();
|
|
1685
1781
|
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
1686
1782
|
if (!await fs3.pathExists(configPath)) {
|
|
1687
1783
|
console.log(chalk3.red("\nError: Not a Tether project"));
|
|
@@ -1741,6 +1837,12 @@ async function devCommand(options) {
|
|
|
1741
1837
|
const { generateTypes: generateTypes2 } = await import("./generate-Q4MUBHHO.js");
|
|
1742
1838
|
await generateTypes2({ silent: true });
|
|
1743
1839
|
spinner.succeed("Types generated");
|
|
1840
|
+
if (config.projectId) {
|
|
1841
|
+
spinner.start("Deploying to development environment...");
|
|
1842
|
+
await runDeploy("all", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner);
|
|
1843
|
+
} else {
|
|
1844
|
+
spinner.warn("No project ID configured \u2014 skipping auto-deploy");
|
|
1845
|
+
}
|
|
1744
1846
|
spinner.start("Setting up file watchers...");
|
|
1745
1847
|
if (await fs3.pathExists(schemaPath)) {
|
|
1746
1848
|
schemaWatcher = watch(schemaPath, {
|
|
@@ -1758,6 +1860,9 @@ async function devCommand(options) {
|
|
|
1758
1860
|
return;
|
|
1759
1861
|
}
|
|
1760
1862
|
await runGenerate(spinner);
|
|
1863
|
+
if (config.projectId) {
|
|
1864
|
+
await runDeploy("schema", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner);
|
|
1865
|
+
}
|
|
1761
1866
|
});
|
|
1762
1867
|
}
|
|
1763
1868
|
if (await fs3.pathExists(functionsDir)) {
|
|
@@ -1779,6 +1884,9 @@ async function devCommand(options) {
|
|
|
1779
1884
|
}
|
|
1780
1885
|
}
|
|
1781
1886
|
await runGenerate(spinner);
|
|
1887
|
+
if (config.projectId) {
|
|
1888
|
+
await runDeploy("functions", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner);
|
|
1889
|
+
}
|
|
1782
1890
|
});
|
|
1783
1891
|
}
|
|
1784
1892
|
spinner.succeed("File watchers ready");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tthr",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Tether CLI - project scaffolding and deployment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"chalk": "^5.4.1",
|
|
22
22
|
"chokidar": "^5.0.0",
|
|
23
23
|
"commander": "^13.0.0",
|
|
24
|
+
"esbuild": "^0.27.3",
|
|
24
25
|
"fs-extra": "^11.2.0",
|
|
25
26
|
"ora": "^8.1.1",
|
|
26
27
|
"prompts": "^2.4.2"
|