tthr 0.3.12 → 0.3.14
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-6PDXNJZH.js +721 -0
- package/dist/generate-7DINPVQ5.js +8 -0
- package/dist/index.js +52 -51
- package/package.json +1 -1
|
@@ -0,0 +1,721 @@
|
|
|
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 fs2 from "fs-extra";
|
|
10
|
+
import path2 from "path";
|
|
11
|
+
|
|
12
|
+
// src/utils/config.ts
|
|
13
|
+
import fs from "fs-extra";
|
|
14
|
+
import path from "path";
|
|
15
|
+
var DEFAULT_CONFIG = {
|
|
16
|
+
schema: "./tether/schema.ts",
|
|
17
|
+
functions: "./tether/functions",
|
|
18
|
+
output: "./tether/_generated",
|
|
19
|
+
dev: {
|
|
20
|
+
port: 3001,
|
|
21
|
+
host: "localhost"
|
|
22
|
+
},
|
|
23
|
+
database: {
|
|
24
|
+
walMode: true
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
28
|
+
const configPath = path.resolve(cwd, "tether.config.ts");
|
|
29
|
+
if (!await fs.pathExists(configPath)) {
|
|
30
|
+
return DEFAULT_CONFIG;
|
|
31
|
+
}
|
|
32
|
+
const configSource = await fs.readFile(configPath, "utf-8");
|
|
33
|
+
const config = {};
|
|
34
|
+
const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
|
|
35
|
+
if (schemaMatch) {
|
|
36
|
+
config.schema = schemaMatch[1];
|
|
37
|
+
}
|
|
38
|
+
const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
|
|
39
|
+
if (functionsMatch) {
|
|
40
|
+
config.functions = functionsMatch[1];
|
|
41
|
+
}
|
|
42
|
+
const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
|
|
43
|
+
if (outputMatch) {
|
|
44
|
+
config.output = outputMatch[1];
|
|
45
|
+
}
|
|
46
|
+
const projectIdMatch = configSource.match(/projectId\s*:\s*['"]([^'"]+)['"]/);
|
|
47
|
+
if (projectIdMatch) {
|
|
48
|
+
config.projectId = projectIdMatch[1];
|
|
49
|
+
}
|
|
50
|
+
const urlMatch = configSource.match(/(?:^|\n)\s*url\s*:\s*['"]([^'"]+)['"]/);
|
|
51
|
+
if (urlMatch) {
|
|
52
|
+
config.url = urlMatch[1];
|
|
53
|
+
}
|
|
54
|
+
const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
|
|
55
|
+
if (envMatch) {
|
|
56
|
+
config.environment = envMatch[1];
|
|
57
|
+
}
|
|
58
|
+
const portMatch = configSource.match(/port\s*:\s*(\d+)/);
|
|
59
|
+
if (portMatch) {
|
|
60
|
+
config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
|
|
61
|
+
}
|
|
62
|
+
const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
|
|
63
|
+
if (hostMatch) {
|
|
64
|
+
config.dev = { ...config.dev, host: hostMatch[1] };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...DEFAULT_CONFIG,
|
|
68
|
+
...config,
|
|
69
|
+
dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
|
|
70
|
+
database: { ...DEFAULT_CONFIG.database, ...config.database }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function resolveEnvironmentFromApiKey(cwd = process.cwd()) {
|
|
74
|
+
const envPath = path.resolve(cwd, ".env");
|
|
75
|
+
if (!await fs.pathExists(envPath)) return void 0;
|
|
76
|
+
const envContent = await fs.readFile(envPath, "utf-8");
|
|
77
|
+
const match = envContent.match(/TETHER_API_KEY=\s*(tthr_([^_]+)_[0-9a-f]+)/);
|
|
78
|
+
if (!match) return void 0;
|
|
79
|
+
const prefix = match[2];
|
|
80
|
+
switch (prefix) {
|
|
81
|
+
case "dev":
|
|
82
|
+
return "development";
|
|
83
|
+
case "prod":
|
|
84
|
+
return "production";
|
|
85
|
+
default:
|
|
86
|
+
return prefix;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
var DEFAULT_API_URL = "https://tether-api.strands.gg";
|
|
90
|
+
function getApiUrl(config) {
|
|
91
|
+
if (process.env.TETHER_API_URL) {
|
|
92
|
+
return process.env.TETHER_API_URL.replace(/\/+$/, "");
|
|
93
|
+
}
|
|
94
|
+
if (config?.url) {
|
|
95
|
+
return config.url.replace(/\/+$/, "");
|
|
96
|
+
}
|
|
97
|
+
const isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
98
|
+
return isDev ? "http://localhost:3001" : DEFAULT_API_URL;
|
|
99
|
+
}
|
|
100
|
+
function resolvePath(configPath, cwd = process.cwd()) {
|
|
101
|
+
const normalised = configPath.replace(/^\.\//, "");
|
|
102
|
+
return path.resolve(cwd, normalised);
|
|
103
|
+
}
|
|
104
|
+
async function detectFramework(cwd = process.cwd()) {
|
|
105
|
+
const packageJsonPath = path.resolve(cwd, "package.json");
|
|
106
|
+
if (!await fs.pathExists(packageJsonPath)) {
|
|
107
|
+
return "unknown";
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
111
|
+
const deps = {
|
|
112
|
+
...packageJson.dependencies,
|
|
113
|
+
...packageJson.devDependencies
|
|
114
|
+
};
|
|
115
|
+
if (deps.nuxt || deps["@nuxt/kit"]) {
|
|
116
|
+
return "nuxt";
|
|
117
|
+
}
|
|
118
|
+
if (deps.next) {
|
|
119
|
+
return "next";
|
|
120
|
+
}
|
|
121
|
+
if (deps["@sveltejs/kit"]) {
|
|
122
|
+
return "sveltekit";
|
|
123
|
+
}
|
|
124
|
+
if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
|
|
125
|
+
return "vite";
|
|
126
|
+
}
|
|
127
|
+
return "vanilla";
|
|
128
|
+
} catch {
|
|
129
|
+
return "unknown";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function getFrameworkDevCommand(framework) {
|
|
133
|
+
switch (framework) {
|
|
134
|
+
case "nuxt":
|
|
135
|
+
return "nuxt dev";
|
|
136
|
+
case "next":
|
|
137
|
+
return "next dev";
|
|
138
|
+
case "sveltekit":
|
|
139
|
+
return "vite dev";
|
|
140
|
+
case "vite":
|
|
141
|
+
return "vite dev";
|
|
142
|
+
case "vanilla":
|
|
143
|
+
return null;
|
|
144
|
+
// No default dev server for vanilla
|
|
145
|
+
default:
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/utils/auth.ts
|
|
151
|
+
var CONFIG_DIR = path2.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
|
|
152
|
+
var CREDENTIALS_FILE = path2.join(CONFIG_DIR, "credentials.json");
|
|
153
|
+
var API_URL = `${getApiUrl()}/api/v1`;
|
|
154
|
+
var REFRESH_THRESHOLD_DAYS = 7;
|
|
155
|
+
async function getCredentials() {
|
|
156
|
+
try {
|
|
157
|
+
if (await fs2.pathExists(CREDENTIALS_FILE)) {
|
|
158
|
+
return await fs2.readJSON(CREDENTIALS_FILE);
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
async function refreshSession(credentials) {
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetch(`${API_URL}/auth/session/refresh`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: {
|
|
169
|
+
"Content-Type": "application/json",
|
|
170
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const data = await response.json();
|
|
177
|
+
const updated = {
|
|
178
|
+
...credentials,
|
|
179
|
+
expiresAt: data.expiresAt
|
|
180
|
+
};
|
|
181
|
+
await saveCredentials(updated);
|
|
182
|
+
return updated;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function requireAuth() {
|
|
188
|
+
const credentials = await getCredentials();
|
|
189
|
+
if (!credentials) {
|
|
190
|
+
console.error(chalk.red("\n\u2717 Not logged in\n"));
|
|
191
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
if (credentials.expiresAt) {
|
|
195
|
+
const expiresAt = new Date(credentials.expiresAt);
|
|
196
|
+
const now = /* @__PURE__ */ new Date();
|
|
197
|
+
if (now > expiresAt) {
|
|
198
|
+
await clearCredentials();
|
|
199
|
+
console.error(chalk.red("\n\u2717 Session expired \u2014 you have been signed out\n"));
|
|
200
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
const daysUntilExpiry = (expiresAt.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24);
|
|
204
|
+
if (daysUntilExpiry <= REFRESH_THRESHOLD_DAYS) {
|
|
205
|
+
const refreshed = await refreshSession(credentials);
|
|
206
|
+
if (refreshed) {
|
|
207
|
+
return refreshed;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return credentials;
|
|
212
|
+
}
|
|
213
|
+
async function saveCredentials(credentials) {
|
|
214
|
+
await fs2.ensureDir(CONFIG_DIR);
|
|
215
|
+
await fs2.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
|
|
216
|
+
}
|
|
217
|
+
async function clearCredentials() {
|
|
218
|
+
try {
|
|
219
|
+
await fs2.remove(CREDENTIALS_FILE);
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/commands/generate.ts
|
|
225
|
+
function parseSchemaFile(source) {
|
|
226
|
+
const tables = [];
|
|
227
|
+
const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
|
|
228
|
+
if (!schemaMatch) return tables;
|
|
229
|
+
const schemaContent = schemaMatch[1];
|
|
230
|
+
const tableStartRegex = /(\w+)\s*:\s*\{/g;
|
|
231
|
+
let match;
|
|
232
|
+
while ((match = tableStartRegex.exec(schemaContent)) !== null) {
|
|
233
|
+
const tableName = match[1];
|
|
234
|
+
const startOffset = match.index + match[0].length;
|
|
235
|
+
let braceCount = 1;
|
|
236
|
+
let endOffset = startOffset;
|
|
237
|
+
for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
|
|
238
|
+
const char = schemaContent[i];
|
|
239
|
+
if (char === "{") braceCount++;
|
|
240
|
+
else if (char === "}") braceCount--;
|
|
241
|
+
endOffset = i;
|
|
242
|
+
}
|
|
243
|
+
const columnsContent = schemaContent.slice(startOffset, endOffset);
|
|
244
|
+
const columns = parseColumns(columnsContent);
|
|
245
|
+
tables.push({ name: tableName, columns });
|
|
246
|
+
}
|
|
247
|
+
return tables;
|
|
248
|
+
}
|
|
249
|
+
function parseColumns(content) {
|
|
250
|
+
const columns = [];
|
|
251
|
+
const columnRegex = /(\w+)\s*:\s*(\w+)(?:<([^>]+)>)?\s*\(\s*\)((?:\[.*?\]|[^,\n}])*)/g;
|
|
252
|
+
let match;
|
|
253
|
+
while ((match = columnRegex.exec(content)) !== null) {
|
|
254
|
+
const name = match[1];
|
|
255
|
+
const schemaType = match[2];
|
|
256
|
+
const genericType = match[3];
|
|
257
|
+
const modifiers = match[4] || "";
|
|
258
|
+
const nullable = !modifiers.includes(".notNull()");
|
|
259
|
+
const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
|
|
260
|
+
let oneOf;
|
|
261
|
+
if (oneOfMatch) {
|
|
262
|
+
oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
263
|
+
}
|
|
264
|
+
columns.push({
|
|
265
|
+
name,
|
|
266
|
+
type: schemaTypeToTS(schemaType),
|
|
267
|
+
nullable,
|
|
268
|
+
jsonType: genericType,
|
|
269
|
+
// Store the generic type for json columns
|
|
270
|
+
oneOf
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return columns;
|
|
274
|
+
}
|
|
275
|
+
function schemaTypeToTS(schemaType) {
|
|
276
|
+
switch (schemaType) {
|
|
277
|
+
case "text":
|
|
278
|
+
return "string";
|
|
279
|
+
case "integer":
|
|
280
|
+
return "number";
|
|
281
|
+
case "real":
|
|
282
|
+
return "number";
|
|
283
|
+
case "boolean":
|
|
284
|
+
return "boolean";
|
|
285
|
+
case "timestamp":
|
|
286
|
+
return "string";
|
|
287
|
+
case "json":
|
|
288
|
+
return "unknown";
|
|
289
|
+
case "blob":
|
|
290
|
+
return "Uint8Array";
|
|
291
|
+
case "asset":
|
|
292
|
+
return "TetherAsset";
|
|
293
|
+
// Asset object returned by API
|
|
294
|
+
default:
|
|
295
|
+
return "unknown";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function extractSingleType(source, typeName) {
|
|
299
|
+
const interfaceStart = new RegExp(`(?:export\\s+)?interface\\s+${typeName}\\s*\\{`);
|
|
300
|
+
const interfaceMatch = interfaceStart.exec(source);
|
|
301
|
+
if (interfaceMatch) {
|
|
302
|
+
const braceStart = interfaceMatch.index + interfaceMatch[0].length;
|
|
303
|
+
let braceCount = 1;
|
|
304
|
+
let endIdx = braceStart;
|
|
305
|
+
for (let i = braceStart; i < source.length && braceCount > 0; i++) {
|
|
306
|
+
if (source[i] === "{") braceCount++;
|
|
307
|
+
else if (source[i] === "}") braceCount--;
|
|
308
|
+
endIdx = i;
|
|
309
|
+
}
|
|
310
|
+
return source.slice(interfaceMatch.index, endIdx + 1).replace(/^export\s+/, "");
|
|
311
|
+
}
|
|
312
|
+
const typeStart = new RegExp(`(?:export\\s+)?type\\s+${typeName}\\s*=`);
|
|
313
|
+
const typeMatch = typeStart.exec(source);
|
|
314
|
+
if (typeMatch) {
|
|
315
|
+
const semiIdx = source.indexOf(";", typeMatch.index);
|
|
316
|
+
if (semiIdx !== -1) {
|
|
317
|
+
return source.slice(typeMatch.index, semiIdx + 1).replace(/^export\s+/, "");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
function extractTypeDefinitions(source, typeNames) {
|
|
323
|
+
if (typeNames.size === 0) return [];
|
|
324
|
+
const allDefinedTypes = /* @__PURE__ */ new Set();
|
|
325
|
+
const typeDefRegex = /(?:export\s+)?(?:interface|type)\s+(\w+)/g;
|
|
326
|
+
let m;
|
|
327
|
+
while ((m = typeDefRegex.exec(source)) !== null) {
|
|
328
|
+
allDefinedTypes.add(m[1]);
|
|
329
|
+
}
|
|
330
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
331
|
+
const queue = [...typeNames];
|
|
332
|
+
while (queue.length > 0) {
|
|
333
|
+
const name = queue.shift();
|
|
334
|
+
if (resolved.has(name)) continue;
|
|
335
|
+
const block = extractSingleType(source, name);
|
|
336
|
+
if (!block) continue;
|
|
337
|
+
resolved.set(name, block);
|
|
338
|
+
for (const definedType of allDefinedTypes) {
|
|
339
|
+
if (!resolved.has(definedType) && !queue.includes(definedType)) {
|
|
340
|
+
const refRegex = new RegExp(`\\b${definedType}\\b`);
|
|
341
|
+
if (refRegex.test(block)) {
|
|
342
|
+
queue.push(definedType);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const ordered = [];
|
|
348
|
+
const visited = /* @__PURE__ */ new Set();
|
|
349
|
+
function visit(name) {
|
|
350
|
+
if (visited.has(name)) return;
|
|
351
|
+
visited.add(name);
|
|
352
|
+
const block = resolved.get(name);
|
|
353
|
+
if (!block) return;
|
|
354
|
+
for (const dep of resolved.keys()) {
|
|
355
|
+
if (dep !== name && new RegExp(`\\b${dep}\\b`).test(block)) {
|
|
356
|
+
visit(dep);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
ordered.push(name);
|
|
360
|
+
}
|
|
361
|
+
for (const name of resolved.keys()) {
|
|
362
|
+
visit(name);
|
|
363
|
+
}
|
|
364
|
+
return ordered.map((name) => resolved.get(name));
|
|
365
|
+
}
|
|
366
|
+
function tableNameToInterface(tableName) {
|
|
367
|
+
let name = tableName;
|
|
368
|
+
if (name.endsWith("ies")) {
|
|
369
|
+
name = name.slice(0, -3) + "y";
|
|
370
|
+
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
371
|
+
name = name.slice(0, -1);
|
|
372
|
+
}
|
|
373
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
374
|
+
}
|
|
375
|
+
function generateDbFile(tables, schemaSource) {
|
|
376
|
+
const TS_PRIMITIVES = /* @__PURE__ */ new Set(["number", "string", "boolean", "unknown", "any", "void", "never", "undefined", "null", "object"]);
|
|
377
|
+
const jsonTypes = /* @__PURE__ */ new Set();
|
|
378
|
+
for (const table of tables) {
|
|
379
|
+
for (const col of table.columns) {
|
|
380
|
+
if (col.jsonType) {
|
|
381
|
+
const baseType = col.jsonType.replace(/\s*\|\s*null/g, "").replace(/\[\]/g, "").trim();
|
|
382
|
+
if (!TS_PRIMITIVES.has(baseType)) {
|
|
383
|
+
jsonTypes.add(baseType);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const lines = [
|
|
389
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
390
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
391
|
+
"",
|
|
392
|
+
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
393
|
+
"import {",
|
|
394
|
+
" type QueryDefinition,",
|
|
395
|
+
" type MutationDefinition,",
|
|
396
|
+
" z,",
|
|
397
|
+
"} from '@tthr/server';",
|
|
398
|
+
"import type { TetherEnv } from './env';"
|
|
399
|
+
];
|
|
400
|
+
if (jsonTypes.size > 0) {
|
|
401
|
+
const typeBlocks = extractTypeDefinitions(schemaSource, jsonTypes);
|
|
402
|
+
if (typeBlocks.length > 0) {
|
|
403
|
+
lines.push("");
|
|
404
|
+
lines.push("// Schema types (inlined from schema.ts)");
|
|
405
|
+
for (const block of typeBlocks) {
|
|
406
|
+
lines.push(block);
|
|
407
|
+
lines.push("");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
lines.push("");
|
|
412
|
+
lines.push("// Asset type returned by the API for asset columns");
|
|
413
|
+
lines.push("export interface TetherAsset {");
|
|
414
|
+
lines.push(" id: string;");
|
|
415
|
+
lines.push(" filename: string;");
|
|
416
|
+
lines.push(" contentType: string;");
|
|
417
|
+
lines.push(" size: number;");
|
|
418
|
+
lines.push(" url: string;");
|
|
419
|
+
lines.push(" createdAt: string;");
|
|
420
|
+
lines.push("}");
|
|
421
|
+
lines.push("");
|
|
422
|
+
for (const table of tables) {
|
|
423
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
424
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
425
|
+
lines.push(" _id: string;");
|
|
426
|
+
for (const col of table.columns) {
|
|
427
|
+
let colType;
|
|
428
|
+
if (col.oneOf && col.oneOf.length > 0) {
|
|
429
|
+
colType = col.oneOf.map((v) => `'${v}'`).join(" | ");
|
|
430
|
+
} else {
|
|
431
|
+
colType = col.jsonType || col.type;
|
|
432
|
+
}
|
|
433
|
+
const typeStr = col.nullable ? `${colType} | null` : colType;
|
|
434
|
+
const optional = col.nullable ? "?" : "";
|
|
435
|
+
lines.push(` ${col.name}${optional}: ${typeStr};`);
|
|
436
|
+
}
|
|
437
|
+
lines.push(" _createdAt: string;");
|
|
438
|
+
lines.push(" _updatedAt?: string | null;");
|
|
439
|
+
lines.push(" _deletedAt?: string | null;");
|
|
440
|
+
lines.push("}");
|
|
441
|
+
lines.push("");
|
|
442
|
+
}
|
|
443
|
+
lines.push("export interface Schema {");
|
|
444
|
+
for (const table of tables) {
|
|
445
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
446
|
+
lines.push(` ${table.name}: ${interfaceName};`);
|
|
447
|
+
}
|
|
448
|
+
lines.push("}");
|
|
449
|
+
lines.push("");
|
|
450
|
+
lines.push("// Database client with typed tables");
|
|
451
|
+
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
452
|
+
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
453
|
+
lines.push("");
|
|
454
|
+
lines.push("// ============================================================================");
|
|
455
|
+
lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
|
|
456
|
+
lines.push("// This ensures the `db` and `env` parameters in handlers are properly typed");
|
|
457
|
+
lines.push("// ============================================================================");
|
|
458
|
+
lines.push("");
|
|
459
|
+
lines.push("/**");
|
|
460
|
+
lines.push(" * Define a query function with typed database and environment variable access.");
|
|
461
|
+
lines.push(" * The `db` and `env` parameters in the handler will have full type safety.");
|
|
462
|
+
lines.push(" */");
|
|
463
|
+
lines.push("export function query<TArgs = void, TResult = unknown>(");
|
|
464
|
+
lines.push(" definition: QueryDefinition<TArgs, TResult, Schema, TetherEnv>");
|
|
465
|
+
lines.push("): QueryDefinition<TArgs, TResult, Schema, TetherEnv> {");
|
|
466
|
+
lines.push(" return definition;");
|
|
467
|
+
lines.push("}");
|
|
468
|
+
lines.push("");
|
|
469
|
+
lines.push("/**");
|
|
470
|
+
lines.push(" * Define a mutation function with typed database and environment variable access.");
|
|
471
|
+
lines.push(" * The `db` and `env` parameters in the handler will have full type safety.");
|
|
472
|
+
lines.push(" */");
|
|
473
|
+
lines.push("export function mutation<TArgs = void, TResult = unknown>(");
|
|
474
|
+
lines.push(" definition: MutationDefinition<TArgs, TResult, Schema, TetherEnv>");
|
|
475
|
+
lines.push("): MutationDefinition<TArgs, TResult, Schema, TetherEnv> {");
|
|
476
|
+
lines.push(" return definition;");
|
|
477
|
+
lines.push("}");
|
|
478
|
+
lines.push("");
|
|
479
|
+
lines.push("// Re-export z for convenience");
|
|
480
|
+
lines.push("export { z };");
|
|
481
|
+
lines.push("");
|
|
482
|
+
return lines.join("\n");
|
|
483
|
+
}
|
|
484
|
+
async function parseFunctionsDir(functionsDir) {
|
|
485
|
+
const functions = [];
|
|
486
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
487
|
+
return functions;
|
|
488
|
+
}
|
|
489
|
+
const files = await fs3.readdir(functionsDir);
|
|
490
|
+
for (const file of files) {
|
|
491
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
492
|
+
const filePath = path3.join(functionsDir, file);
|
|
493
|
+
const stat = await fs3.stat(filePath);
|
|
494
|
+
if (!stat.isFile()) continue;
|
|
495
|
+
const moduleName = file.replace(/\.(ts|js)$/, "");
|
|
496
|
+
const source = await fs3.readFile(filePath, "utf-8");
|
|
497
|
+
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation)\s*\(/g;
|
|
498
|
+
let match;
|
|
499
|
+
while ((match = exportRegex.exec(source)) !== null) {
|
|
500
|
+
functions.push({
|
|
501
|
+
name: match[1],
|
|
502
|
+
moduleName
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return functions;
|
|
507
|
+
}
|
|
508
|
+
async function generateApiFile(functionsDir) {
|
|
509
|
+
const functions = await parseFunctionsDir(functionsDir);
|
|
510
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
511
|
+
for (const fn of functions) {
|
|
512
|
+
if (!moduleMap.has(fn.moduleName)) {
|
|
513
|
+
moduleMap.set(fn.moduleName, []);
|
|
514
|
+
}
|
|
515
|
+
moduleMap.get(fn.moduleName).push(fn.name);
|
|
516
|
+
}
|
|
517
|
+
const lines = [
|
|
518
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
519
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
520
|
+
"",
|
|
521
|
+
"import { createApiProxy } from '@tthr/client';",
|
|
522
|
+
"",
|
|
523
|
+
"/**",
|
|
524
|
+
" * API function reference type for useQuery/useMutation.",
|
|
525
|
+
' * The _name property contains the function path (e.g., "users.list").',
|
|
526
|
+
" */",
|
|
527
|
+
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
528
|
+
" _name: string;",
|
|
529
|
+
" _args?: TArgs;",
|
|
530
|
+
" _result?: TResult;",
|
|
531
|
+
"}",
|
|
532
|
+
""
|
|
533
|
+
];
|
|
534
|
+
if (moduleMap.size > 0) {
|
|
535
|
+
for (const [moduleName, fnNames] of moduleMap) {
|
|
536
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
537
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
538
|
+
for (const fnName of fnNames) {
|
|
539
|
+
lines.push(` ${fnName}: ApiFunction;`);
|
|
540
|
+
}
|
|
541
|
+
lines.push("}");
|
|
542
|
+
lines.push("");
|
|
543
|
+
}
|
|
544
|
+
lines.push("export interface Api {");
|
|
545
|
+
for (const moduleName of moduleMap.keys()) {
|
|
546
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
547
|
+
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
548
|
+
}
|
|
549
|
+
lines.push("}");
|
|
550
|
+
} else {
|
|
551
|
+
lines.push("/**");
|
|
552
|
+
lines.push(" * Flexible API type that allows access to any function path.");
|
|
553
|
+
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
554
|
+
lines.push(" * The actual types depend on your function definitions.");
|
|
555
|
+
lines.push(" */");
|
|
556
|
+
lines.push("export type Api = {");
|
|
557
|
+
lines.push(" [module: string]: {");
|
|
558
|
+
lines.push(" [fn: string]: ApiFunction;");
|
|
559
|
+
lines.push(" };");
|
|
560
|
+
lines.push("};");
|
|
561
|
+
}
|
|
562
|
+
lines.push("");
|
|
563
|
+
lines.push("// API client proxy - provides typed access to your functions");
|
|
564
|
+
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
565
|
+
lines.push("// In Tether functions: calls the actual function implementation");
|
|
566
|
+
lines.push("export const api = createApiProxy<Api>();");
|
|
567
|
+
lines.push("");
|
|
568
|
+
return lines.join("\n");
|
|
569
|
+
}
|
|
570
|
+
async function fetchEnvVarKeys(projectId, environment, apiUrl) {
|
|
571
|
+
const credentials = await getCredentials();
|
|
572
|
+
if (!credentials) return [];
|
|
573
|
+
const baseUrl = apiUrl || `${getApiUrl()}/api/v1`;
|
|
574
|
+
const envPath = environment !== "production" ? `/projects/${projectId}/env/${environment}/env-vars` : `/projects/${projectId}/env-vars`;
|
|
575
|
+
try {
|
|
576
|
+
const response = await fetch(`${baseUrl}${envPath}`, {
|
|
577
|
+
headers: {
|
|
578
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
if (!response.ok) return [];
|
|
582
|
+
const data = await response.json();
|
|
583
|
+
return (data.envVars || []).map((v) => v.key).sort();
|
|
584
|
+
} catch {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function generateEnvFile(keys) {
|
|
589
|
+
const lines = [
|
|
590
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
591
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
592
|
+
"",
|
|
593
|
+
"/**",
|
|
594
|
+
" * Typed environment variables for this project.",
|
|
595
|
+
" * Keys are fetched from the Tether API \u2014 values are never exposed.",
|
|
596
|
+
' * Use `env.get("KEY")` in your functions to access them.',
|
|
597
|
+
" */",
|
|
598
|
+
""
|
|
599
|
+
];
|
|
600
|
+
if (keys.length > 0) {
|
|
601
|
+
lines.push("export type EnvVarKey =");
|
|
602
|
+
for (let i = 0; i < keys.length; i++) {
|
|
603
|
+
const sep = i === keys.length - 1 ? ";" : "";
|
|
604
|
+
lines.push(` | '${keys[i]}'${sep}`);
|
|
605
|
+
}
|
|
606
|
+
lines.push("");
|
|
607
|
+
lines.push("export interface TetherEnv {");
|
|
608
|
+
for (const key of keys) {
|
|
609
|
+
lines.push(` ${key}: string;`);
|
|
610
|
+
}
|
|
611
|
+
lines.push("}");
|
|
612
|
+
} else {
|
|
613
|
+
lines.push("export type EnvVarKey = string;");
|
|
614
|
+
lines.push("");
|
|
615
|
+
lines.push("export interface TetherEnv {");
|
|
616
|
+
lines.push(" [key: string]: string;");
|
|
617
|
+
lines.push("}");
|
|
618
|
+
}
|
|
619
|
+
lines.push("");
|
|
620
|
+
return lines.join("\n");
|
|
621
|
+
}
|
|
622
|
+
async function generateTypes(options = {}) {
|
|
623
|
+
const config = await loadConfig();
|
|
624
|
+
const schemaPath = resolvePath(config.schema);
|
|
625
|
+
const outputDir = resolvePath(config.output);
|
|
626
|
+
const functionsDir = resolvePath(config.functions);
|
|
627
|
+
if (!await fs3.pathExists(schemaPath)) {
|
|
628
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
629
|
+
}
|
|
630
|
+
const schemaSource = await fs3.readFile(schemaPath, "utf-8");
|
|
631
|
+
const tables = parseSchemaFile(schemaSource);
|
|
632
|
+
const environment = config.environment || await resolveEnvironmentFromApiKey() || "development";
|
|
633
|
+
let projectId;
|
|
634
|
+
const envFilePath = path3.resolve(process.cwd(), ".env");
|
|
635
|
+
if (await fs3.pathExists(envFilePath)) {
|
|
636
|
+
const envContent = await fs3.readFile(envFilePath, "utf-8");
|
|
637
|
+
const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
|
|
638
|
+
projectId = match?.[1]?.trim();
|
|
639
|
+
}
|
|
640
|
+
const apiUrl = `${getApiUrl(config)}/api/v1`;
|
|
641
|
+
const envVarKeys = projectId ? await fetchEnvVarKeys(projectId, environment, apiUrl) : [];
|
|
642
|
+
await fs3.ensureDir(outputDir);
|
|
643
|
+
await fs3.writeFile(
|
|
644
|
+
path3.join(outputDir, "db.ts"),
|
|
645
|
+
generateDbFile(tables, schemaSource)
|
|
646
|
+
);
|
|
647
|
+
await fs3.writeFile(
|
|
648
|
+
path3.join(outputDir, "api.ts"),
|
|
649
|
+
await generateApiFile(functionsDir)
|
|
650
|
+
);
|
|
651
|
+
await fs3.writeFile(
|
|
652
|
+
path3.join(outputDir, "env.ts"),
|
|
653
|
+
generateEnvFile(envVarKeys)
|
|
654
|
+
);
|
|
655
|
+
await fs3.writeFile(
|
|
656
|
+
path3.join(outputDir, "index.ts"),
|
|
657
|
+
`// Auto-generated by Tether CLI - do not edit manually
|
|
658
|
+
export * from './db';
|
|
659
|
+
export * from './api';
|
|
660
|
+
export * from './env';
|
|
661
|
+
`
|
|
662
|
+
);
|
|
663
|
+
return { tables, envVarKeys, outputDir };
|
|
664
|
+
}
|
|
665
|
+
async function generateCommand() {
|
|
666
|
+
await requireAuth();
|
|
667
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
668
|
+
if (!await fs3.pathExists(configPath)) {
|
|
669
|
+
console.log(chalk2.red("\nError: Not a Tether project"));
|
|
670
|
+
console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
|
|
674
|
+
const spinner = ora("Reading schema...").start();
|
|
675
|
+
try {
|
|
676
|
+
spinner.text = "Generating types...";
|
|
677
|
+
const { tables, envVarKeys, outputDir } = await generateTypes();
|
|
678
|
+
if (tables.length === 0) {
|
|
679
|
+
spinner.warn("No tables found in schema");
|
|
680
|
+
console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
684
|
+
console.log("\n" + chalk2.green("\u2713") + " Tables:");
|
|
685
|
+
for (const table of tables) {
|
|
686
|
+
console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
687
|
+
}
|
|
688
|
+
if (envVarKeys.length > 0) {
|
|
689
|
+
console.log("\n" + chalk2.green("\u2713") + ` Environment variables: ${envVarKeys.length} key(s)`);
|
|
690
|
+
for (const key of envVarKeys) {
|
|
691
|
+
console.log(chalk2.dim(` - ${key}`));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const relativeOutput = path3.relative(process.cwd(), outputDir);
|
|
695
|
+
console.log("\n" + chalk2.green("\u2713") + " Generated files:");
|
|
696
|
+
console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
|
|
697
|
+
console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
|
|
698
|
+
console.log(chalk2.dim(` ${relativeOutput}/env.ts`));
|
|
699
|
+
console.log(chalk2.dim(` ${relativeOutput}/index.ts
|
|
700
|
+
`));
|
|
701
|
+
} catch (error) {
|
|
702
|
+
spinner.fail("Failed to generate types");
|
|
703
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export {
|
|
709
|
+
loadConfig,
|
|
710
|
+
resolveEnvironmentFromApiKey,
|
|
711
|
+
getApiUrl,
|
|
712
|
+
resolvePath,
|
|
713
|
+
detectFramework,
|
|
714
|
+
getFrameworkDevCommand,
|
|
715
|
+
getCredentials,
|
|
716
|
+
requireAuth,
|
|
717
|
+
saveCredentials,
|
|
718
|
+
clearCredentials,
|
|
719
|
+
generateTypes,
|
|
720
|
+
generateCommand
|
|
721
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
API_URL,
|
|
4
3
|
clearCredentials,
|
|
5
4
|
detectFramework,
|
|
6
5
|
generateCommand,
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
resolveEnvironmentFromApiKey,
|
|
14
13
|
resolvePath,
|
|
15
14
|
saveCredentials
|
|
16
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-6PDXNJZH.js";
|
|
17
16
|
|
|
18
17
|
// src/index.ts
|
|
19
18
|
import { Command } from "commander";
|
|
@@ -59,11 +58,11 @@ async function deployCommand(options) {
|
|
|
59
58
|
process.exit(1);
|
|
60
59
|
}
|
|
61
60
|
const environment = options.env || config.environment || await resolveEnvironmentFromApiKey() || "development";
|
|
62
|
-
const
|
|
61
|
+
const API_URL3 = `${getApiUrl(config)}/api/v1`;
|
|
63
62
|
console.log(chalk.bold("\n\u26A1 Deploying to Tether\n"));
|
|
64
63
|
console.log(chalk.dim(` Project: ${projectId}`));
|
|
65
64
|
console.log(chalk.dim(` Environment: ${environment}`));
|
|
66
|
-
console.log(chalk.dim(` API: ${
|
|
65
|
+
console.log(chalk.dim(` API: ${API_URL3}
|
|
67
66
|
`));
|
|
68
67
|
console.log(chalk.dim(" Generating types..."));
|
|
69
68
|
try {
|
|
@@ -76,16 +75,16 @@ async function deployCommand(options) {
|
|
|
76
75
|
const deployFunctions = options.functions || !options.schema && !options.functions;
|
|
77
76
|
if (deploySchema) {
|
|
78
77
|
const schemaPath = resolvePath(config.schema);
|
|
79
|
-
await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, environment, options.dryRun);
|
|
78
|
+
await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, environment, options.dryRun, API_URL3);
|
|
80
79
|
}
|
|
81
80
|
if (deployFunctions) {
|
|
82
81
|
const functionsDir = resolvePath(config.functions);
|
|
83
|
-
await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, environment, options.dryRun);
|
|
82
|
+
await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, environment, options.dryRun, API_URL3);
|
|
84
83
|
}
|
|
85
84
|
console.log(chalk.green("\n\u2713 Deployment complete\n"));
|
|
86
85
|
}
|
|
87
86
|
async function deploySchemaToServer(projectId, token, schemaPath, environment, dryRun, apiUrl) {
|
|
88
|
-
const
|
|
87
|
+
const API_URL3 = apiUrl || `${getApiUrl()}/api/v1`;
|
|
89
88
|
const spinner = ora("Reading schema...").start();
|
|
90
89
|
try {
|
|
91
90
|
if (!await fs.pathExists(schemaPath)) {
|
|
@@ -115,7 +114,7 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
|
|
|
115
114
|
return;
|
|
116
115
|
}
|
|
117
116
|
spinner.text = "Deploying schema...";
|
|
118
|
-
const schemaUrl = `${
|
|
117
|
+
const schemaUrl = `${API_URL3}/projects/${projectId}/env/${environment}/deploy/schema`;
|
|
119
118
|
console.log(chalk.dim(`
|
|
120
119
|
URL: ${schemaUrl}`));
|
|
121
120
|
const rlsConfigs = tables.filter((t) => t.rls).map((t) => ({
|
|
@@ -190,7 +189,7 @@ ${messages.join("\n")}`);
|
|
|
190
189
|
return result.outputFiles[0].text;
|
|
191
190
|
}
|
|
192
191
|
async function deployFunctionsToServer(projectId, token, functionsDir, environment, dryRun, apiUrl) {
|
|
193
|
-
const
|
|
192
|
+
const API_URL3 = apiUrl || `${getApiUrl()}/api/v1`;
|
|
194
193
|
const spinner = ora("Reading functions...").start();
|
|
195
194
|
try {
|
|
196
195
|
if (!await fs.pathExists(functionsDir)) {
|
|
@@ -247,7 +246,7 @@ ${helperSource}`;
|
|
|
247
246
|
return;
|
|
248
247
|
}
|
|
249
248
|
spinner.text = "Deploying functions...";
|
|
250
|
-
const functionsUrl = `${
|
|
249
|
+
const functionsUrl = `${API_URL3}/projects/${projectId}/env/${environment}/deploy/functions`;
|
|
251
250
|
console.log(chalk.dim(`
|
|
252
251
|
URL: ${functionsUrl}`));
|
|
253
252
|
const response = await fetch(functionsUrl, {
|
|
@@ -585,8 +584,7 @@ function parseFunctions(moduleName, source) {
|
|
|
585
584
|
}
|
|
586
585
|
|
|
587
586
|
// src/commands/init.ts
|
|
588
|
-
var
|
|
589
|
-
var API_URL2 = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
587
|
+
var API_URL = `${getApiUrl()}/api/v1`;
|
|
590
588
|
async function getLatestVersion(packageName) {
|
|
591
589
|
try {
|
|
592
590
|
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
@@ -688,7 +686,7 @@ ${packageManager} is not installed on your system.`));
|
|
|
688
686
|
await scaffoldVanillaProject(projectName, projectPath, spinner);
|
|
689
687
|
}
|
|
690
688
|
spinner.text = "Creating project on Tether...";
|
|
691
|
-
const response = await fetch(`${
|
|
689
|
+
const response = await fetch(`${API_URL}/projects`, {
|
|
692
690
|
method: "POST",
|
|
693
691
|
headers: {
|
|
694
692
|
"Content-Type": "application/json",
|
|
@@ -867,7 +865,7 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
|
|
|
867
865
|
|
|
868
866
|
const tether = createClient({
|
|
869
867
|
projectId: process.env.TETHER_PROJECT_ID!,
|
|
870
|
-
url: process.env.TETHER_URL || '
|
|
868
|
+
url: process.env.TETHER_URL || '${getApiUrl()}',
|
|
871
869
|
});
|
|
872
870
|
|
|
873
871
|
async function main() {
|
|
@@ -904,7 +902,7 @@ export default defineConfig({
|
|
|
904
902
|
projectId: process.env.TETHER_PROJECT_ID,
|
|
905
903
|
|
|
906
904
|
// API endpoint (defaults to Tether Cloud)
|
|
907
|
-
url: process.env.TETHER_URL || '
|
|
905
|
+
url: process.env.TETHER_URL || '${getApiUrl()}',
|
|
908
906
|
});
|
|
909
907
|
`
|
|
910
908
|
);
|
|
@@ -1136,7 +1134,7 @@ export default defineNuxtConfig({
|
|
|
1136
1134
|
|
|
1137
1135
|
tether: {
|
|
1138
1136
|
projectId: process.env.TETHER_PROJECT_ID,
|
|
1139
|
-
url: process.env.TETHER_URL || '
|
|
1137
|
+
url: process.env.TETHER_URL || '${getApiUrl()}',
|
|
1140
1138
|
},
|
|
1141
1139
|
});
|
|
1142
1140
|
`
|
|
@@ -1164,7 +1162,7 @@ export default defineNuxtConfig({
|
|
|
1164
1162
|
|
|
1165
1163
|
tether: {
|
|
1166
1164
|
projectId: process.env.TETHER_PROJECT_ID,
|
|
1167
|
-
url: process.env.TETHER_URL || '
|
|
1165
|
+
url: process.env.TETHER_URL || '${getApiUrl()}',
|
|
1168
1166
|
},
|
|
1169
1167
|
});
|
|
1170
1168
|
`
|
|
@@ -1184,7 +1182,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
1184
1182
|
return (
|
|
1185
1183
|
<TetherProvider
|
|
1186
1184
|
projectId={process.env.NEXT_PUBLIC_TETHER_PROJECT_ID!}
|
|
1187
|
-
url={process.env.NEXT_PUBLIC_TETHER_URL || '
|
|
1185
|
+
url={process.env.NEXT_PUBLIC_TETHER_URL || '${getApiUrl()}'}
|
|
1188
1186
|
>
|
|
1189
1187
|
{children}
|
|
1190
1188
|
</TetherProvider>
|
|
@@ -1230,7 +1228,7 @@ import { PUBLIC_TETHER_PROJECT_ID, PUBLIC_TETHER_URL } from '$env/static/public'
|
|
|
1230
1228
|
|
|
1231
1229
|
export const tether = createClient({
|
|
1232
1230
|
projectId: PUBLIC_TETHER_PROJECT_ID,
|
|
1233
|
-
url: PUBLIC_TETHER_URL || '
|
|
1231
|
+
url: PUBLIC_TETHER_URL || '${getApiUrl()}',
|
|
1234
1232
|
});
|
|
1235
1233
|
`
|
|
1236
1234
|
);
|
|
@@ -1245,8 +1243,8 @@ PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
|
|
|
1245
1243
|
}
|
|
1246
1244
|
}
|
|
1247
1245
|
}
|
|
1248
|
-
function getInstallCommand(pm,
|
|
1249
|
-
const devFlag =
|
|
1246
|
+
function getInstallCommand(pm, isDev2 = false) {
|
|
1247
|
+
const devFlag = isDev2 ? pm === "npm" ? "-D" : pm === "yarn" ? "-D" : pm === "pnpm" ? "-D" : "-d" : "";
|
|
1250
1248
|
return `${pm} ${pm === "npm" ? "install" : "add"} ${devFlag}`.trim();
|
|
1251
1249
|
}
|
|
1252
1250
|
async function installTetherPackages(projectPath, template, packageManager) {
|
|
@@ -1706,7 +1704,7 @@ async function runGenerate(spinner) {
|
|
|
1706
1704
|
}
|
|
1707
1705
|
isGenerating = true;
|
|
1708
1706
|
try {
|
|
1709
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1707
|
+
const { generateTypes: generateTypes2 } = await import("./generate-7DINPVQ5.js");
|
|
1710
1708
|
spinner.text = "Regenerating types...";
|
|
1711
1709
|
await generateTypes2({ silent: true });
|
|
1712
1710
|
spinner.succeed("Types regenerated");
|
|
@@ -1725,7 +1723,7 @@ async function runGenerate(spinner) {
|
|
|
1725
1723
|
}
|
|
1726
1724
|
}
|
|
1727
1725
|
}
|
|
1728
|
-
async function runDeploy(what, projectId, token, schemaPath, functionsDir, environment, spinner) {
|
|
1726
|
+
async function runDeploy(what, projectId, token, schemaPath, functionsDir, environment, spinner, apiUrl) {
|
|
1729
1727
|
if (isDeploying) {
|
|
1730
1728
|
if (pendingDeploy === null) pendingDeploy = what;
|
|
1731
1729
|
else if (pendingDeploy !== what) pendingDeploy = "all";
|
|
@@ -1736,7 +1734,7 @@ async function runDeploy(what, projectId, token, schemaPath, functionsDir, envir
|
|
|
1736
1734
|
if (what === "schema" || what === "all") {
|
|
1737
1735
|
spinner.text = "Deploying schema to development...";
|
|
1738
1736
|
try {
|
|
1739
|
-
await deploySchemaToServer(projectId, token, schemaPath, environment);
|
|
1737
|
+
await deploySchemaToServer(projectId, token, schemaPath, environment, false, apiUrl);
|
|
1740
1738
|
} catch (e) {
|
|
1741
1739
|
console.log(chalk3.yellow(` Schema deploy failed: ${e instanceof Error ? e.message : e}`));
|
|
1742
1740
|
}
|
|
@@ -1744,7 +1742,7 @@ async function runDeploy(what, projectId, token, schemaPath, functionsDir, envir
|
|
|
1744
1742
|
if (what === "functions" || what === "all") {
|
|
1745
1743
|
spinner.text = "Deploying functions to development...";
|
|
1746
1744
|
try {
|
|
1747
|
-
await deployFunctionsToServer(projectId, token, functionsDir, environment);
|
|
1745
|
+
await deployFunctionsToServer(projectId, token, functionsDir, environment, false, apiUrl);
|
|
1748
1746
|
} catch (e) {
|
|
1749
1747
|
console.log(chalk3.yellow(` Functions deploy failed: ${e instanceof Error ? e.message : e}`));
|
|
1750
1748
|
}
|
|
@@ -1756,7 +1754,7 @@ async function runDeploy(what, projectId, token, schemaPath, functionsDir, envir
|
|
|
1756
1754
|
if (pendingDeploy) {
|
|
1757
1755
|
const next = pendingDeploy;
|
|
1758
1756
|
pendingDeploy = null;
|
|
1759
|
-
setTimeout(() => runDeploy(next, projectId, token, schemaPath, functionsDir, environment, spinner), 100);
|
|
1757
|
+
setTimeout(() => runDeploy(next, projectId, token, schemaPath, functionsDir, environment, spinner, apiUrl), 100);
|
|
1760
1758
|
}
|
|
1761
1759
|
}
|
|
1762
1760
|
}
|
|
@@ -1936,6 +1934,7 @@ async function devCommand(options) {
|
|
|
1936
1934
|
const devCmd = config.dev?.command || getFrameworkDevCommand(framework);
|
|
1937
1935
|
const environment = options.env || config.environment || "development";
|
|
1938
1936
|
const isLocal = options.local ?? false;
|
|
1937
|
+
const apiUrl = `${getApiUrl(config)}/api/v1`;
|
|
1939
1938
|
console.log(chalk3.bold("\n\u26A1 Starting Tether development server\n"));
|
|
1940
1939
|
if (framework !== "unknown") {
|
|
1941
1940
|
console.log(chalk3.dim(` Framework: ${framework}`));
|
|
@@ -1977,12 +1976,12 @@ async function devCommand(options) {
|
|
|
1977
1976
|
}
|
|
1978
1977
|
}
|
|
1979
1978
|
spinner.text = "Generating types...";
|
|
1980
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1979
|
+
const { generateTypes: generateTypes2 } = await import("./generate-7DINPVQ5.js");
|
|
1981
1980
|
await generateTypes2({ silent: true });
|
|
1982
1981
|
spinner.succeed("Types generated");
|
|
1983
1982
|
if (config.projectId) {
|
|
1984
1983
|
spinner.start("Deploying to development environment...");
|
|
1985
|
-
await runDeploy("all", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner);
|
|
1984
|
+
await runDeploy("all", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner, apiUrl);
|
|
1986
1985
|
} else {
|
|
1987
1986
|
spinner.warn("No project ID configured \u2014 skipping auto-deploy");
|
|
1988
1987
|
}
|
|
@@ -2004,7 +2003,7 @@ async function devCommand(options) {
|
|
|
2004
2003
|
}
|
|
2005
2004
|
await runGenerate(spinner);
|
|
2006
2005
|
if (config.projectId) {
|
|
2007
|
-
await runDeploy("schema", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner);
|
|
2006
|
+
await runDeploy("schema", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner, apiUrl);
|
|
2008
2007
|
}
|
|
2009
2008
|
});
|
|
2010
2009
|
}
|
|
@@ -2028,7 +2027,7 @@ async function devCommand(options) {
|
|
|
2028
2027
|
}
|
|
2029
2028
|
await runGenerate(spinner);
|
|
2030
2029
|
if (config.projectId) {
|
|
2031
|
-
await runDeploy("functions", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner);
|
|
2030
|
+
await runDeploy("functions", config.projectId, credentials.accessToken, schemaPath, functionsDir, environment, spinner, apiUrl);
|
|
2032
2031
|
}
|
|
2033
2032
|
});
|
|
2034
2033
|
}
|
|
@@ -2062,9 +2061,9 @@ import ora4 from "ora";
|
|
|
2062
2061
|
import os from "os";
|
|
2063
2062
|
import { exec } from "child_process";
|
|
2064
2063
|
import readline from "readline";
|
|
2065
|
-
var
|
|
2066
|
-
var
|
|
2067
|
-
var AUTH_URL =
|
|
2064
|
+
var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
2065
|
+
var API_URL2 = `${getApiUrl()}/api/v1`;
|
|
2066
|
+
var AUTH_URL = isDev ? "http://localhost:3000/cli" : "https://tthr.io/cli";
|
|
2068
2067
|
async function loginCommand() {
|
|
2069
2068
|
console.log(chalk4.bold("\u26A1 Login to Tether\n"));
|
|
2070
2069
|
const existing = await getCredentials();
|
|
@@ -2153,7 +2152,7 @@ async function requestDeviceCode() {
|
|
|
2153
2152
|
const userCode = generateUserCode();
|
|
2154
2153
|
const deviceCode = crypto.randomUUID();
|
|
2155
2154
|
const deviceName = os.hostname();
|
|
2156
|
-
const response = await fetch(`${
|
|
2155
|
+
const response = await fetch(`${API_URL2}/auth/device`, {
|
|
2157
2156
|
method: "POST",
|
|
2158
2157
|
headers: { "Content-Type": "application/json" },
|
|
2159
2158
|
body: JSON.stringify({ userCode, deviceCode, deviceName })
|
|
@@ -2187,7 +2186,7 @@ async function pollForApproval(deviceCode, interval, expiresIn) {
|
|
|
2187
2186
|
while (Date.now() < expiresAt) {
|
|
2188
2187
|
await sleep(interval * 1e3);
|
|
2189
2188
|
spinner.text = `Checking... ${chalk4.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
|
|
2190
|
-
const response = await fetch(`${
|
|
2189
|
+
const response = await fetch(`${API_URL2}/auth/device/${deviceCode}`, {
|
|
2191
2190
|
method: "GET"
|
|
2192
2191
|
}).catch(() => null);
|
|
2193
2192
|
updateCountdown();
|
|
@@ -2279,18 +2278,18 @@ function detectPackageManager() {
|
|
|
2279
2278
|
}
|
|
2280
2279
|
return "npm";
|
|
2281
2280
|
}
|
|
2282
|
-
function getInstallCommand2(pm, packages,
|
|
2281
|
+
function getInstallCommand2(pm, packages, isDev2) {
|
|
2283
2282
|
const packagesStr = packages.join(" ");
|
|
2284
2283
|
switch (pm) {
|
|
2285
2284
|
case "bun":
|
|
2286
|
-
return
|
|
2285
|
+
return isDev2 ? `bun add -d ${packagesStr}` : `bun add ${packagesStr}`;
|
|
2287
2286
|
case "pnpm":
|
|
2288
|
-
return
|
|
2287
|
+
return isDev2 ? `pnpm add -D ${packagesStr}` : `pnpm add ${packagesStr}`;
|
|
2289
2288
|
case "yarn":
|
|
2290
|
-
return
|
|
2289
|
+
return isDev2 ? `yarn add -D ${packagesStr}` : `yarn add ${packagesStr}`;
|
|
2291
2290
|
case "npm":
|
|
2292
2291
|
default:
|
|
2293
|
-
return
|
|
2292
|
+
return isDev2 ? `npm install -D ${packagesStr}` : `npm install ${packagesStr}`;
|
|
2294
2293
|
}
|
|
2295
2294
|
}
|
|
2296
2295
|
async function getLatestVersion2(packageName) {
|
|
@@ -2419,10 +2418,10 @@ async function execCommand(sql) {
|
|
|
2419
2418
|
process.exit(1);
|
|
2420
2419
|
}
|
|
2421
2420
|
const config = await loadConfig();
|
|
2422
|
-
const
|
|
2421
|
+
const API_URL3 = `${getApiUrl(config)}/api/v1`;
|
|
2423
2422
|
const spinner = ora6("Executing query...").start();
|
|
2424
2423
|
try {
|
|
2425
|
-
const response = await fetch(`${
|
|
2424
|
+
const response = await fetch(`${API_URL3}/projects/${projectId}/exec`, {
|
|
2426
2425
|
method: "POST",
|
|
2427
2426
|
headers: {
|
|
2428
2427
|
"Content-Type": "application/json",
|
|
@@ -2494,10 +2493,10 @@ async function getProjectId() {
|
|
|
2494
2493
|
async function envListCommand() {
|
|
2495
2494
|
const credentials = await requireAuth();
|
|
2496
2495
|
const projectId = await getProjectId();
|
|
2497
|
-
const
|
|
2496
|
+
const API_URL3 = await resolveApiUrl();
|
|
2498
2497
|
const spinner = ora7("Fetching environments...").start();
|
|
2499
2498
|
try {
|
|
2500
|
-
const response = await fetch(`${
|
|
2499
|
+
const response = await fetch(`${API_URL3}/projects/${projectId}/environments`, {
|
|
2501
2500
|
headers: {
|
|
2502
2501
|
"Authorization": `Bearer ${credentials.accessToken}`
|
|
2503
2502
|
}
|
|
@@ -2527,7 +2526,7 @@ async function envListCommand() {
|
|
|
2527
2526
|
async function envCreateCommand(name, options) {
|
|
2528
2527
|
const credentials = await requireAuth();
|
|
2529
2528
|
const projectId = await getProjectId();
|
|
2530
|
-
const
|
|
2529
|
+
const API_URL3 = await resolveApiUrl();
|
|
2531
2530
|
const normalizedName = name.toLowerCase();
|
|
2532
2531
|
if (!/^[a-z][a-z0-9-_]*$/.test(normalizedName)) {
|
|
2533
2532
|
console.log(chalk7.red("\nError: Invalid environment name"));
|
|
@@ -2541,7 +2540,7 @@ async function envCreateCommand(name, options) {
|
|
|
2541
2540
|
body.cloneFrom = options.from;
|
|
2542
2541
|
spinner.text = `Creating environment '${normalizedName}' from '${options.from}'...`;
|
|
2543
2542
|
}
|
|
2544
|
-
const response = await fetch(`${
|
|
2543
|
+
const response = await fetch(`${API_URL3}/projects/${projectId}/environments`, {
|
|
2545
2544
|
method: "POST",
|
|
2546
2545
|
headers: {
|
|
2547
2546
|
"Content-Type": "application/json",
|
|
@@ -2570,10 +2569,10 @@ async function envCreateCommand(name, options) {
|
|
|
2570
2569
|
async function envDeleteCommand(name) {
|
|
2571
2570
|
const credentials = await requireAuth();
|
|
2572
2571
|
const projectId = await getProjectId();
|
|
2573
|
-
const
|
|
2572
|
+
const API_URL3 = await resolveApiUrl();
|
|
2574
2573
|
const spinner = ora7(`Deleting environment '${name}'...`).start();
|
|
2575
2574
|
try {
|
|
2576
|
-
const response = await fetch(`${
|
|
2575
|
+
const response = await fetch(`${API_URL3}/projects/${projectId}/environments/${name}`, {
|
|
2577
2576
|
method: "DELETE",
|
|
2578
2577
|
headers: {
|
|
2579
2578
|
"Authorization": `Bearer ${credentials.accessToken}`
|
|
@@ -2593,10 +2592,10 @@ async function envDeleteCommand(name) {
|
|
|
2593
2592
|
async function envDefaultCommand(name) {
|
|
2594
2593
|
const credentials = await requireAuth();
|
|
2595
2594
|
const projectId = await getProjectId();
|
|
2596
|
-
const
|
|
2595
|
+
const API_URL3 = await resolveApiUrl();
|
|
2597
2596
|
const spinner = ora7(`Setting '${name}' as default environment...`).start();
|
|
2598
2597
|
try {
|
|
2599
|
-
const response = await fetch(`${
|
|
2598
|
+
const response = await fetch(`${API_URL3}/projects/${projectId}/environments/default`, {
|
|
2600
2599
|
method: "PATCH",
|
|
2601
2600
|
headers: {
|
|
2602
2601
|
"Content-Type": "application/json",
|
|
@@ -2661,7 +2660,9 @@ async function migrateSystemColumnsCommand(options) {
|
|
|
2661
2660
|
`));
|
|
2662
2661
|
const spinner = ora8("Analysing database tables...").start();
|
|
2663
2662
|
try {
|
|
2664
|
-
const
|
|
2663
|
+
const config = await loadConfig();
|
|
2664
|
+
const API_URL3 = `${getApiUrl(config)}/api/v1`;
|
|
2665
|
+
const baseUrl = `${API_URL3}/projects/${projectId}/env/${environment}/migrate/system-columns`;
|
|
2665
2666
|
const dryRunResponse = await fetch(`${baseUrl}?dry_run=true`, {
|
|
2666
2667
|
method: "POST",
|
|
2667
2668
|
headers: {
|