tthr 0.0.60 → 0.1.0
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-NDEYB3IH.js +538 -0
- package/dist/generate-Q4MUBHHO.js +8 -0
- package/dist/index.js +168 -19
- package/package.json +1 -1
|
@@ -0,0 +1,538 @@
|
|
|
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
|
+
var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
14
|
+
var API_URL = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
15
|
+
var REFRESH_THRESHOLD_DAYS = 7;
|
|
16
|
+
async function getCredentials() {
|
|
17
|
+
try {
|
|
18
|
+
if (await fs.pathExists(CREDENTIALS_FILE)) {
|
|
19
|
+
return await fs.readJSON(CREDENTIALS_FILE);
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
async function refreshSession(credentials) {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(`${API_URL}/auth/session/refresh`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
const updated = {
|
|
39
|
+
...credentials,
|
|
40
|
+
expiresAt: data.expiresAt
|
|
41
|
+
};
|
|
42
|
+
await saveCredentials(updated);
|
|
43
|
+
return updated;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function requireAuth() {
|
|
49
|
+
const credentials = await getCredentials();
|
|
50
|
+
if (!credentials) {
|
|
51
|
+
console.error(chalk.red("\n\u2717 Not logged in\n"));
|
|
52
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
if (credentials.expiresAt) {
|
|
56
|
+
const expiresAt = new Date(credentials.expiresAt);
|
|
57
|
+
const now = /* @__PURE__ */ new Date();
|
|
58
|
+
if (now > expiresAt) {
|
|
59
|
+
await clearCredentials();
|
|
60
|
+
console.error(chalk.red("\n\u2717 Session expired \u2014 you have been signed out\n"));
|
|
61
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const daysUntilExpiry = (expiresAt.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24);
|
|
65
|
+
if (daysUntilExpiry <= REFRESH_THRESHOLD_DAYS) {
|
|
66
|
+
const refreshed = await refreshSession(credentials);
|
|
67
|
+
if (refreshed) {
|
|
68
|
+
return refreshed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return credentials;
|
|
73
|
+
}
|
|
74
|
+
async function saveCredentials(credentials) {
|
|
75
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
76
|
+
await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
|
|
77
|
+
}
|
|
78
|
+
async function clearCredentials() {
|
|
79
|
+
try {
|
|
80
|
+
await fs.remove(CREDENTIALS_FILE);
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/utils/config.ts
|
|
86
|
+
import fs2 from "fs-extra";
|
|
87
|
+
import path2 from "path";
|
|
88
|
+
var DEFAULT_CONFIG = {
|
|
89
|
+
schema: "./tether/schema.ts",
|
|
90
|
+
functions: "./tether/functions",
|
|
91
|
+
output: "./tether/_generated",
|
|
92
|
+
dev: {
|
|
93
|
+
port: 3001,
|
|
94
|
+
host: "localhost"
|
|
95
|
+
},
|
|
96
|
+
database: {
|
|
97
|
+
walMode: true
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
101
|
+
const configPath = path2.resolve(cwd, "tether.config.ts");
|
|
102
|
+
if (!await fs2.pathExists(configPath)) {
|
|
103
|
+
return DEFAULT_CONFIG;
|
|
104
|
+
}
|
|
105
|
+
const configSource = await fs2.readFile(configPath, "utf-8");
|
|
106
|
+
const config = {};
|
|
107
|
+
const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
|
|
108
|
+
if (schemaMatch) {
|
|
109
|
+
config.schema = schemaMatch[1];
|
|
110
|
+
}
|
|
111
|
+
const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
|
|
112
|
+
if (functionsMatch) {
|
|
113
|
+
config.functions = functionsMatch[1];
|
|
114
|
+
}
|
|
115
|
+
const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
|
|
116
|
+
if (outputMatch) {
|
|
117
|
+
config.output = outputMatch[1];
|
|
118
|
+
}
|
|
119
|
+
const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
|
|
120
|
+
if (envMatch) {
|
|
121
|
+
config.environment = envMatch[1];
|
|
122
|
+
}
|
|
123
|
+
const portMatch = configSource.match(/port\s*:\s*(\d+)/);
|
|
124
|
+
if (portMatch) {
|
|
125
|
+
config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
|
|
126
|
+
}
|
|
127
|
+
const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
|
|
128
|
+
if (hostMatch) {
|
|
129
|
+
config.dev = { ...config.dev, host: hostMatch[1] };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
...DEFAULT_CONFIG,
|
|
133
|
+
...config,
|
|
134
|
+
dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
|
|
135
|
+
database: { ...DEFAULT_CONFIG.database, ...config.database }
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function resolvePath(configPath, cwd = process.cwd()) {
|
|
139
|
+
const normalised = configPath.replace(/^\.\//, "");
|
|
140
|
+
return path2.resolve(cwd, normalised);
|
|
141
|
+
}
|
|
142
|
+
async function detectFramework(cwd = process.cwd()) {
|
|
143
|
+
const packageJsonPath = path2.resolve(cwd, "package.json");
|
|
144
|
+
if (!await fs2.pathExists(packageJsonPath)) {
|
|
145
|
+
return "unknown";
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const packageJson = await fs2.readJson(packageJsonPath);
|
|
149
|
+
const deps = {
|
|
150
|
+
...packageJson.dependencies,
|
|
151
|
+
...packageJson.devDependencies
|
|
152
|
+
};
|
|
153
|
+
if (deps.nuxt || deps["@nuxt/kit"]) {
|
|
154
|
+
return "nuxt";
|
|
155
|
+
}
|
|
156
|
+
if (deps.next) {
|
|
157
|
+
return "next";
|
|
158
|
+
}
|
|
159
|
+
if (deps["@sveltejs/kit"]) {
|
|
160
|
+
return "sveltekit";
|
|
161
|
+
}
|
|
162
|
+
if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
|
|
163
|
+
return "vite";
|
|
164
|
+
}
|
|
165
|
+
return "vanilla";
|
|
166
|
+
} catch {
|
|
167
|
+
return "unknown";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function getFrameworkDevCommand(framework) {
|
|
171
|
+
switch (framework) {
|
|
172
|
+
case "nuxt":
|
|
173
|
+
return "nuxt dev";
|
|
174
|
+
case "next":
|
|
175
|
+
return "next dev";
|
|
176
|
+
case "sveltekit":
|
|
177
|
+
return "vite dev";
|
|
178
|
+
case "vite":
|
|
179
|
+
return "vite dev";
|
|
180
|
+
case "vanilla":
|
|
181
|
+
return null;
|
|
182
|
+
// No default dev server for vanilla
|
|
183
|
+
default:
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/commands/generate.ts
|
|
189
|
+
function parseSchemaFile(source) {
|
|
190
|
+
const tables = [];
|
|
191
|
+
const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
|
|
192
|
+
if (!schemaMatch) return tables;
|
|
193
|
+
const schemaContent = schemaMatch[1];
|
|
194
|
+
const tableStartRegex = /(\w+)\s*:\s*\{/g;
|
|
195
|
+
let match;
|
|
196
|
+
while ((match = tableStartRegex.exec(schemaContent)) !== null) {
|
|
197
|
+
const tableName = match[1];
|
|
198
|
+
const startOffset = match.index + match[0].length;
|
|
199
|
+
let braceCount = 1;
|
|
200
|
+
let endOffset = startOffset;
|
|
201
|
+
for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
|
|
202
|
+
const char = schemaContent[i];
|
|
203
|
+
if (char === "{") braceCount++;
|
|
204
|
+
else if (char === "}") braceCount--;
|
|
205
|
+
endOffset = i;
|
|
206
|
+
}
|
|
207
|
+
const columnsContent = schemaContent.slice(startOffset, endOffset);
|
|
208
|
+
const columns = parseColumns(columnsContent);
|
|
209
|
+
tables.push({ name: tableName, columns });
|
|
210
|
+
}
|
|
211
|
+
return tables;
|
|
212
|
+
}
|
|
213
|
+
function parseColumns(content) {
|
|
214
|
+
const columns = [];
|
|
215
|
+
const columnRegex = /(\w+)\s*:\s*(\w+)(?:<([^>]+)>)?\s*\(\s*\)((?:\[.*?\]|[^,\n}])*)/g;
|
|
216
|
+
let match;
|
|
217
|
+
while ((match = columnRegex.exec(content)) !== null) {
|
|
218
|
+
const name = match[1];
|
|
219
|
+
const schemaType = match[2];
|
|
220
|
+
const genericType = match[3];
|
|
221
|
+
const modifiers = match[4] || "";
|
|
222
|
+
const nullable = !modifiers.includes(".notNull()");
|
|
223
|
+
const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
|
|
224
|
+
let oneOf;
|
|
225
|
+
if (oneOfMatch) {
|
|
226
|
+
oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
227
|
+
}
|
|
228
|
+
columns.push({
|
|
229
|
+
name,
|
|
230
|
+
type: schemaTypeToTS(schemaType),
|
|
231
|
+
nullable,
|
|
232
|
+
jsonType: genericType,
|
|
233
|
+
// Store the generic type for json columns
|
|
234
|
+
oneOf
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return columns;
|
|
238
|
+
}
|
|
239
|
+
function schemaTypeToTS(schemaType) {
|
|
240
|
+
switch (schemaType) {
|
|
241
|
+
case "text":
|
|
242
|
+
return "string";
|
|
243
|
+
case "integer":
|
|
244
|
+
return "number";
|
|
245
|
+
case "real":
|
|
246
|
+
return "number";
|
|
247
|
+
case "boolean":
|
|
248
|
+
return "boolean";
|
|
249
|
+
case "timestamp":
|
|
250
|
+
return "string";
|
|
251
|
+
case "json":
|
|
252
|
+
return "unknown";
|
|
253
|
+
case "blob":
|
|
254
|
+
return "Uint8Array";
|
|
255
|
+
case "asset":
|
|
256
|
+
return "TetherAsset";
|
|
257
|
+
// Asset object returned by API
|
|
258
|
+
default:
|
|
259
|
+
return "unknown";
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function tableNameToInterface(tableName) {
|
|
263
|
+
let name = tableName;
|
|
264
|
+
if (name.endsWith("ies")) {
|
|
265
|
+
name = name.slice(0, -3) + "y";
|
|
266
|
+
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
267
|
+
name = name.slice(0, -1);
|
|
268
|
+
}
|
|
269
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
270
|
+
}
|
|
271
|
+
function generateDbFile(tables) {
|
|
272
|
+
const jsonTypes = /* @__PURE__ */ new Set();
|
|
273
|
+
for (const table of tables) {
|
|
274
|
+
for (const col of table.columns) {
|
|
275
|
+
if (col.jsonType) {
|
|
276
|
+
jsonTypes.add(col.jsonType.replace(/\[\]$/, ""));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const lines = [
|
|
281
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
282
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
283
|
+
"",
|
|
284
|
+
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
285
|
+
"import {",
|
|
286
|
+
" query as baseQuery,",
|
|
287
|
+
" mutation as baseMutation,",
|
|
288
|
+
" type QueryDefinition,",
|
|
289
|
+
" type MutationDefinition,",
|
|
290
|
+
" z,",
|
|
291
|
+
"} from '@tthr/server';"
|
|
292
|
+
];
|
|
293
|
+
if (jsonTypes.size > 0) {
|
|
294
|
+
const typeImports = Array.from(jsonTypes).sort().join(", ");
|
|
295
|
+
lines.push(`import type { ${typeImports} } from '../schema';`);
|
|
296
|
+
}
|
|
297
|
+
lines.push("");
|
|
298
|
+
lines.push("// Asset type returned by the API for asset columns");
|
|
299
|
+
lines.push("export interface TetherAsset {");
|
|
300
|
+
lines.push(" id: string;");
|
|
301
|
+
lines.push(" filename: string;");
|
|
302
|
+
lines.push(" contentType: string;");
|
|
303
|
+
lines.push(" size: number;");
|
|
304
|
+
lines.push(" url: string;");
|
|
305
|
+
lines.push(" createdAt: string;");
|
|
306
|
+
lines.push("}");
|
|
307
|
+
lines.push("");
|
|
308
|
+
for (const table of tables) {
|
|
309
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
310
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
311
|
+
lines.push(" _id: string;");
|
|
312
|
+
for (const col of table.columns) {
|
|
313
|
+
let colType;
|
|
314
|
+
if (col.oneOf && col.oneOf.length > 0) {
|
|
315
|
+
colType = col.oneOf.map((v) => `'${v}'`).join(" | ");
|
|
316
|
+
} else {
|
|
317
|
+
colType = col.jsonType || col.type;
|
|
318
|
+
}
|
|
319
|
+
const typeStr = col.nullable ? `${colType} | null` : colType;
|
|
320
|
+
const optional = col.nullable ? "?" : "";
|
|
321
|
+
lines.push(` ${col.name}${optional}: ${typeStr};`);
|
|
322
|
+
}
|
|
323
|
+
lines.push(" _createdAt: string;");
|
|
324
|
+
lines.push(" _updatedAt?: string | null;");
|
|
325
|
+
lines.push(" _deletedAt?: string | null;");
|
|
326
|
+
lines.push("}");
|
|
327
|
+
lines.push("");
|
|
328
|
+
}
|
|
329
|
+
lines.push("export interface Schema {");
|
|
330
|
+
for (const table of tables) {
|
|
331
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
332
|
+
lines.push(` ${table.name}: ${interfaceName};`);
|
|
333
|
+
}
|
|
334
|
+
lines.push("}");
|
|
335
|
+
lines.push("");
|
|
336
|
+
lines.push("// Database client with typed tables");
|
|
337
|
+
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
338
|
+
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
339
|
+
lines.push("");
|
|
340
|
+
lines.push("// ============================================================================");
|
|
341
|
+
lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
|
|
342
|
+
lines.push("// This ensures the `db` parameter in handlers is properly typed with Schema");
|
|
343
|
+
lines.push("// ============================================================================");
|
|
344
|
+
lines.push("");
|
|
345
|
+
lines.push("/**");
|
|
346
|
+
lines.push(" * Define a query function with typed database access.");
|
|
347
|
+
lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
|
|
348
|
+
lines.push(" */");
|
|
349
|
+
lines.push("export function query<TArgs = void, TResult = unknown>(");
|
|
350
|
+
lines.push(" definition: QueryDefinition<TArgs, TResult, Schema>");
|
|
351
|
+
lines.push("): QueryDefinition<TArgs, TResult, Schema> {");
|
|
352
|
+
lines.push(" return baseQuery(definition);");
|
|
353
|
+
lines.push("}");
|
|
354
|
+
lines.push("");
|
|
355
|
+
lines.push("/**");
|
|
356
|
+
lines.push(" * Define a mutation function with typed database access.");
|
|
357
|
+
lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
|
|
358
|
+
lines.push(" */");
|
|
359
|
+
lines.push("export function mutation<TArgs = void, TResult = unknown>(");
|
|
360
|
+
lines.push(" definition: MutationDefinition<TArgs, TResult, Schema>");
|
|
361
|
+
lines.push("): MutationDefinition<TArgs, TResult, Schema> {");
|
|
362
|
+
lines.push(" return baseMutation(definition);");
|
|
363
|
+
lines.push("}");
|
|
364
|
+
lines.push("");
|
|
365
|
+
lines.push("// Re-export z for convenience");
|
|
366
|
+
lines.push("export { z };");
|
|
367
|
+
if (jsonTypes.size > 0) {
|
|
368
|
+
lines.push("");
|
|
369
|
+
lines.push("// Re-export JSON schema types");
|
|
370
|
+
const typeExports = Array.from(jsonTypes).sort().join(", ");
|
|
371
|
+
lines.push(`export type { ${typeExports} } from '../schema';`);
|
|
372
|
+
}
|
|
373
|
+
lines.push("");
|
|
374
|
+
return lines.join("\n");
|
|
375
|
+
}
|
|
376
|
+
async function parseFunctionsDir(functionsDir) {
|
|
377
|
+
const functions = [];
|
|
378
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
379
|
+
return functions;
|
|
380
|
+
}
|
|
381
|
+
const files = await fs3.readdir(functionsDir);
|
|
382
|
+
for (const file of files) {
|
|
383
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
384
|
+
const filePath = path3.join(functionsDir, file);
|
|
385
|
+
const stat = await fs3.stat(filePath);
|
|
386
|
+
if (!stat.isFile()) continue;
|
|
387
|
+
const moduleName = file.replace(/\.(ts|js)$/, "");
|
|
388
|
+
const source = await fs3.readFile(filePath, "utf-8");
|
|
389
|
+
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation)\s*\(/g;
|
|
390
|
+
let match;
|
|
391
|
+
while ((match = exportRegex.exec(source)) !== null) {
|
|
392
|
+
functions.push({
|
|
393
|
+
name: match[1],
|
|
394
|
+
moduleName
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return functions;
|
|
399
|
+
}
|
|
400
|
+
async function generateApiFile(functionsDir) {
|
|
401
|
+
const functions = await parseFunctionsDir(functionsDir);
|
|
402
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
403
|
+
for (const fn of functions) {
|
|
404
|
+
if (!moduleMap.has(fn.moduleName)) {
|
|
405
|
+
moduleMap.set(fn.moduleName, []);
|
|
406
|
+
}
|
|
407
|
+
moduleMap.get(fn.moduleName).push(fn.name);
|
|
408
|
+
}
|
|
409
|
+
const lines = [
|
|
410
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
411
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
412
|
+
"",
|
|
413
|
+
"import { createApiProxy } from '@tthr/client';",
|
|
414
|
+
"",
|
|
415
|
+
"/**",
|
|
416
|
+
" * API function reference type for useQuery/useMutation.",
|
|
417
|
+
' * The _name property contains the function path (e.g., "users.list").',
|
|
418
|
+
" */",
|
|
419
|
+
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
420
|
+
" _name: string;",
|
|
421
|
+
" _args?: TArgs;",
|
|
422
|
+
" _result?: TResult;",
|
|
423
|
+
"}",
|
|
424
|
+
""
|
|
425
|
+
];
|
|
426
|
+
if (moduleMap.size > 0) {
|
|
427
|
+
for (const [moduleName, fnNames] of moduleMap) {
|
|
428
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
429
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
430
|
+
for (const fnName of fnNames) {
|
|
431
|
+
lines.push(` ${fnName}: ApiFunction;`);
|
|
432
|
+
}
|
|
433
|
+
lines.push("}");
|
|
434
|
+
lines.push("");
|
|
435
|
+
}
|
|
436
|
+
lines.push("export interface Api {");
|
|
437
|
+
for (const moduleName of moduleMap.keys()) {
|
|
438
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
439
|
+
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
440
|
+
}
|
|
441
|
+
lines.push("}");
|
|
442
|
+
} else {
|
|
443
|
+
lines.push("/**");
|
|
444
|
+
lines.push(" * Flexible API type that allows access to any function path.");
|
|
445
|
+
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
446
|
+
lines.push(" * The actual types depend on your function definitions.");
|
|
447
|
+
lines.push(" */");
|
|
448
|
+
lines.push("export type Api = {");
|
|
449
|
+
lines.push(" [module: string]: {");
|
|
450
|
+
lines.push(" [fn: string]: ApiFunction;");
|
|
451
|
+
lines.push(" };");
|
|
452
|
+
lines.push("};");
|
|
453
|
+
}
|
|
454
|
+
lines.push("");
|
|
455
|
+
lines.push("// API client proxy - provides typed access to your functions");
|
|
456
|
+
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
457
|
+
lines.push("// In Tether functions: calls the actual function implementation");
|
|
458
|
+
lines.push("export const api = createApiProxy<Api>();");
|
|
459
|
+
lines.push("");
|
|
460
|
+
return lines.join("\n");
|
|
461
|
+
}
|
|
462
|
+
async function generateTypes(options = {}) {
|
|
463
|
+
const config = await loadConfig();
|
|
464
|
+
const schemaPath = resolvePath(config.schema);
|
|
465
|
+
const outputDir = resolvePath(config.output);
|
|
466
|
+
const functionsDir = resolvePath(config.functions);
|
|
467
|
+
if (!await fs3.pathExists(schemaPath)) {
|
|
468
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
469
|
+
}
|
|
470
|
+
const schemaSource = await fs3.readFile(schemaPath, "utf-8");
|
|
471
|
+
const tables = parseSchemaFile(schemaSource);
|
|
472
|
+
await fs3.ensureDir(outputDir);
|
|
473
|
+
await fs3.writeFile(
|
|
474
|
+
path3.join(outputDir, "db.ts"),
|
|
475
|
+
generateDbFile(tables)
|
|
476
|
+
);
|
|
477
|
+
await fs3.writeFile(
|
|
478
|
+
path3.join(outputDir, "api.ts"),
|
|
479
|
+
await generateApiFile(functionsDir)
|
|
480
|
+
);
|
|
481
|
+
await fs3.writeFile(
|
|
482
|
+
path3.join(outputDir, "index.ts"),
|
|
483
|
+
`// Auto-generated by Tether CLI - do not edit manually
|
|
484
|
+
export * from './db';
|
|
485
|
+
export * from './api';
|
|
486
|
+
`
|
|
487
|
+
);
|
|
488
|
+
return { tables, outputDir };
|
|
489
|
+
}
|
|
490
|
+
async function generateCommand() {
|
|
491
|
+
await requireAuth();
|
|
492
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
493
|
+
if (!await fs3.pathExists(configPath)) {
|
|
494
|
+
console.log(chalk2.red("\nError: Not a Tether project"));
|
|
495
|
+
console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
|
|
499
|
+
const spinner = ora("Reading schema...").start();
|
|
500
|
+
try {
|
|
501
|
+
spinner.text = "Generating types...";
|
|
502
|
+
const { tables, outputDir } = await generateTypes();
|
|
503
|
+
if (tables.length === 0) {
|
|
504
|
+
spinner.warn("No tables found in schema");
|
|
505
|
+
console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
509
|
+
console.log("\n" + chalk2.green("\u2713") + " Tables:");
|
|
510
|
+
for (const table of tables) {
|
|
511
|
+
console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
512
|
+
}
|
|
513
|
+
const relativeOutput = path3.relative(process.cwd(), outputDir);
|
|
514
|
+
console.log("\n" + chalk2.green("\u2713") + " Generated files:");
|
|
515
|
+
console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
|
|
516
|
+
console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
|
|
517
|
+
console.log(chalk2.dim(` ${relativeOutput}/index.ts
|
|
518
|
+
`));
|
|
519
|
+
} catch (error) {
|
|
520
|
+
spinner.fail("Failed to generate types");
|
|
521
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export {
|
|
527
|
+
API_URL,
|
|
528
|
+
getCredentials,
|
|
529
|
+
requireAuth,
|
|
530
|
+
saveCredentials,
|
|
531
|
+
clearCredentials,
|
|
532
|
+
loadConfig,
|
|
533
|
+
resolvePath,
|
|
534
|
+
detectFramework,
|
|
535
|
+
getFrameworkDevCommand,
|
|
536
|
+
generateTypes,
|
|
537
|
+
generateCommand
|
|
538
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
API_URL,
|
|
3
4
|
clearCredentials,
|
|
4
5
|
detectFramework,
|
|
5
6
|
generateCommand,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
requireAuth,
|
|
11
12
|
resolvePath,
|
|
12
13
|
saveCredentials
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-NDEYB3IH.js";
|
|
14
15
|
|
|
15
16
|
// src/index.ts
|
|
16
17
|
import { Command } from "commander";
|
|
@@ -29,7 +30,7 @@ import ora from "ora";
|
|
|
29
30
|
import fs from "fs-extra";
|
|
30
31
|
import path from "path";
|
|
31
32
|
var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
32
|
-
var
|
|
33
|
+
var API_URL2 = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
33
34
|
async function deployCommand(options) {
|
|
34
35
|
const credentials = await requireAuth();
|
|
35
36
|
const configPath = path.resolve(process.cwd(), "tether.config.ts");
|
|
@@ -54,7 +55,7 @@ async function deployCommand(options) {
|
|
|
54
55
|
console.log(chalk.bold("\n\u26A1 Deploying to Tether\n"));
|
|
55
56
|
console.log(chalk.dim(` Project: ${projectId}`));
|
|
56
57
|
console.log(chalk.dim(` Environment: ${environment}`));
|
|
57
|
-
console.log(chalk.dim(` API: ${
|
|
58
|
+
console.log(chalk.dim(` API: ${API_URL2}
|
|
58
59
|
`));
|
|
59
60
|
console.log(chalk.dim(" Generating types..."));
|
|
60
61
|
try {
|
|
@@ -106,7 +107,7 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
|
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
108
109
|
spinner.text = "Deploying schema...";
|
|
109
|
-
const schemaUrl = `${
|
|
110
|
+
const schemaUrl = `${API_URL2}/projects/${projectId}/env/${environment}/deploy/schema`;
|
|
110
111
|
console.log(chalk.dim(`
|
|
111
112
|
URL: ${schemaUrl}`));
|
|
112
113
|
const requestBody = {
|
|
@@ -186,7 +187,7 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
|
|
|
186
187
|
return;
|
|
187
188
|
}
|
|
188
189
|
spinner.text = "Deploying functions...";
|
|
189
|
-
const functionsUrl = `${
|
|
190
|
+
const functionsUrl = `${API_URL2}/projects/${projectId}/env/${environment}/deploy/functions`;
|
|
190
191
|
console.log(chalk.dim(`
|
|
191
192
|
URL: ${functionsUrl}`));
|
|
192
193
|
const response = await fetch(functionsUrl, {
|
|
@@ -435,7 +436,7 @@ function parseFunctions(moduleName, source) {
|
|
|
435
436
|
|
|
436
437
|
// src/commands/init.ts
|
|
437
438
|
var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
438
|
-
var
|
|
439
|
+
var API_URL3 = isDev2 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
439
440
|
async function getLatestVersion(packageName) {
|
|
440
441
|
try {
|
|
441
442
|
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
@@ -537,7 +538,7 @@ ${packageManager} is not installed on your system.`));
|
|
|
537
538
|
await scaffoldVanillaProject(projectName, projectPath, spinner);
|
|
538
539
|
}
|
|
539
540
|
spinner.text = "Creating project on Tether...";
|
|
540
|
-
const response = await fetch(`${
|
|
541
|
+
const response = await fetch(`${API_URL3}/projects`, {
|
|
541
542
|
method: "POST",
|
|
542
543
|
headers: {
|
|
543
544
|
"Content-Type": "application/json",
|
|
@@ -1552,7 +1553,7 @@ async function runGenerate(spinner) {
|
|
|
1552
1553
|
}
|
|
1553
1554
|
isGenerating = true;
|
|
1554
1555
|
try {
|
|
1555
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1556
|
+
const { generateTypes: generateTypes2 } = await import("./generate-Q4MUBHHO.js");
|
|
1556
1557
|
spinner.text = "Regenerating types...";
|
|
1557
1558
|
await generateTypes2({ silent: true });
|
|
1558
1559
|
spinner.succeed("Types regenerated");
|
|
@@ -1737,7 +1738,7 @@ async function devCommand(options) {
|
|
|
1737
1738
|
}
|
|
1738
1739
|
}
|
|
1739
1740
|
spinner.text = "Generating types...";
|
|
1740
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1741
|
+
const { generateTypes: generateTypes2 } = await import("./generate-Q4MUBHHO.js");
|
|
1741
1742
|
await generateTypes2({ silent: true });
|
|
1742
1743
|
spinner.succeed("Types generated");
|
|
1743
1744
|
spinner.start("Setting up file watchers...");
|
|
@@ -1808,7 +1809,7 @@ import os from "os";
|
|
|
1808
1809
|
import { exec } from "child_process";
|
|
1809
1810
|
import readline from "readline";
|
|
1810
1811
|
var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
1811
|
-
var
|
|
1812
|
+
var API_URL4 = isDev3 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
1812
1813
|
var AUTH_URL = isDev3 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
|
|
1813
1814
|
async function loginCommand() {
|
|
1814
1815
|
console.log(chalk4.bold("\u26A1 Login to Tether\n"));
|
|
@@ -1898,7 +1899,7 @@ async function requestDeviceCode() {
|
|
|
1898
1899
|
const userCode = generateUserCode();
|
|
1899
1900
|
const deviceCode = crypto.randomUUID();
|
|
1900
1901
|
const deviceName = os.hostname();
|
|
1901
|
-
const response = await fetch(`${
|
|
1902
|
+
const response = await fetch(`${API_URL4}/auth/device`, {
|
|
1902
1903
|
method: "POST",
|
|
1903
1904
|
headers: { "Content-Type": "application/json" },
|
|
1904
1905
|
body: JSON.stringify({ userCode, deviceCode, deviceName })
|
|
@@ -1932,7 +1933,7 @@ async function pollForApproval(deviceCode, interval, expiresIn) {
|
|
|
1932
1933
|
while (Date.now() < expiresAt) {
|
|
1933
1934
|
await sleep(interval * 1e3);
|
|
1934
1935
|
spinner.text = `Checking... ${chalk4.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
|
|
1935
|
-
const response = await fetch(`${
|
|
1936
|
+
const response = await fetch(`${API_URL4}/auth/device/${deviceCode}`, {
|
|
1936
1937
|
method: "GET"
|
|
1937
1938
|
}).catch(() => null);
|
|
1938
1939
|
updateCountdown();
|
|
@@ -2145,7 +2146,7 @@ import ora6 from "ora";
|
|
|
2145
2146
|
import fs5 from "fs-extra";
|
|
2146
2147
|
import path5 from "path";
|
|
2147
2148
|
var isDev4 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
2148
|
-
var
|
|
2149
|
+
var API_URL5 = isDev4 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
2149
2150
|
async function execCommand(sql) {
|
|
2150
2151
|
if (!sql || sql.trim() === "") {
|
|
2151
2152
|
console.log(chalk6.red("\nError: SQL query required"));
|
|
@@ -2167,7 +2168,7 @@ async function execCommand(sql) {
|
|
|
2167
2168
|
}
|
|
2168
2169
|
const spinner = ora6("Executing query...").start();
|
|
2169
2170
|
try {
|
|
2170
|
-
const response = await fetch(`${
|
|
2171
|
+
const response = await fetch(`${API_URL5}/projects/${projectId}/exec`, {
|
|
2171
2172
|
method: "POST",
|
|
2172
2173
|
headers: {
|
|
2173
2174
|
"Content-Type": "application/json",
|
|
@@ -2218,7 +2219,7 @@ import ora7 from "ora";
|
|
|
2218
2219
|
import fs6 from "fs-extra";
|
|
2219
2220
|
import path6 from "path";
|
|
2220
2221
|
var isDev5 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
2221
|
-
var
|
|
2222
|
+
var API_URL6 = isDev5 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
2222
2223
|
async function getProjectId() {
|
|
2223
2224
|
const envPath = path6.resolve(process.cwd(), ".env");
|
|
2224
2225
|
let projectId;
|
|
@@ -2239,7 +2240,7 @@ async function envListCommand() {
|
|
|
2239
2240
|
const projectId = await getProjectId();
|
|
2240
2241
|
const spinner = ora7("Fetching environments...").start();
|
|
2241
2242
|
try {
|
|
2242
|
-
const response = await fetch(`${
|
|
2243
|
+
const response = await fetch(`${API_URL6}/projects/${projectId}/environments`, {
|
|
2243
2244
|
headers: {
|
|
2244
2245
|
"Authorization": `Bearer ${credentials.accessToken}`
|
|
2245
2246
|
}
|
|
@@ -2282,7 +2283,7 @@ async function envCreateCommand(name, options) {
|
|
|
2282
2283
|
body.cloneFrom = options.from;
|
|
2283
2284
|
spinner.text = `Creating environment '${normalizedName}' from '${options.from}'...`;
|
|
2284
2285
|
}
|
|
2285
|
-
const response = await fetch(`${
|
|
2286
|
+
const response = await fetch(`${API_URL6}/projects/${projectId}/environments`, {
|
|
2286
2287
|
method: "POST",
|
|
2287
2288
|
headers: {
|
|
2288
2289
|
"Content-Type": "application/json",
|
|
@@ -2313,7 +2314,7 @@ async function envDeleteCommand(name) {
|
|
|
2313
2314
|
const projectId = await getProjectId();
|
|
2314
2315
|
const spinner = ora7(`Deleting environment '${name}'...`).start();
|
|
2315
2316
|
try {
|
|
2316
|
-
const response = await fetch(`${
|
|
2317
|
+
const response = await fetch(`${API_URL6}/projects/${projectId}/environments/${name}`, {
|
|
2317
2318
|
method: "DELETE",
|
|
2318
2319
|
headers: {
|
|
2319
2320
|
"Authorization": `Bearer ${credentials.accessToken}`
|
|
@@ -2335,7 +2336,7 @@ async function envDefaultCommand(name) {
|
|
|
2335
2336
|
const projectId = await getProjectId();
|
|
2336
2337
|
const spinner = ora7(`Setting '${name}' as default environment...`).start();
|
|
2337
2338
|
try {
|
|
2338
|
-
const response = await fetch(`${
|
|
2339
|
+
const response = await fetch(`${API_URL6}/projects/${projectId}/environments/default`, {
|
|
2339
2340
|
method: "PATCH",
|
|
2340
2341
|
headers: {
|
|
2341
2342
|
"Content-Type": "application/json",
|
|
@@ -2373,6 +2374,152 @@ function maskApiKey(key) {
|
|
|
2373
2374
|
return `${visibleStart}...${visibleEnd}`;
|
|
2374
2375
|
}
|
|
2375
2376
|
|
|
2377
|
+
// src/commands/migrate.ts
|
|
2378
|
+
import chalk8 from "chalk";
|
|
2379
|
+
import ora8 from "ora";
|
|
2380
|
+
import fs7 from "fs-extra";
|
|
2381
|
+
import path7 from "path";
|
|
2382
|
+
import readline2 from "readline";
|
|
2383
|
+
async function migrateSystemColumnsCommand(options) {
|
|
2384
|
+
const credentials = await requireAuth();
|
|
2385
|
+
const envPath = path7.resolve(process.cwd(), ".env");
|
|
2386
|
+
let projectId;
|
|
2387
|
+
if (await fs7.pathExists(envPath)) {
|
|
2388
|
+
const envContent = await fs7.readFile(envPath, "utf-8");
|
|
2389
|
+
const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
|
|
2390
|
+
projectId = match?.[1]?.trim();
|
|
2391
|
+
}
|
|
2392
|
+
if (!projectId) {
|
|
2393
|
+
console.log(chalk8.red("\nError: Project ID not found"));
|
|
2394
|
+
console.log(chalk8.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
|
|
2395
|
+
process.exit(1);
|
|
2396
|
+
}
|
|
2397
|
+
const environment = options.env || "development";
|
|
2398
|
+
console.log(chalk8.bold("\n\u26A1 Migrate Legacy IDs to System Columns\n"));
|
|
2399
|
+
console.log(chalk8.dim(` Project: ${projectId}`));
|
|
2400
|
+
console.log(chalk8.dim(` Environment: ${environment}
|
|
2401
|
+
`));
|
|
2402
|
+
const spinner = ora8("Analysing database tables...").start();
|
|
2403
|
+
try {
|
|
2404
|
+
const baseUrl = `${API_URL}/projects/${projectId}/env/${environment}/migrate/system-columns`;
|
|
2405
|
+
const dryRunResponse = await fetch(`${baseUrl}?dry_run=true`, {
|
|
2406
|
+
method: "POST",
|
|
2407
|
+
headers: {
|
|
2408
|
+
"Content-Type": "application/json",
|
|
2409
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
if (!dryRunResponse.ok) {
|
|
2413
|
+
const text = await dryRunResponse.text();
|
|
2414
|
+
let errorMessage;
|
|
2415
|
+
try {
|
|
2416
|
+
const error = JSON.parse(text);
|
|
2417
|
+
errorMessage = error.error || `HTTP ${dryRunResponse.status}`;
|
|
2418
|
+
} catch {
|
|
2419
|
+
errorMessage = text || `HTTP ${dryRunResponse.status}: ${dryRunResponse.statusText}`;
|
|
2420
|
+
}
|
|
2421
|
+
throw new Error(errorMessage);
|
|
2422
|
+
}
|
|
2423
|
+
const preview = await dryRunResponse.json();
|
|
2424
|
+
spinner.stop();
|
|
2425
|
+
const tablesToMigrate = preview.tables.filter((t) => t.status === "migrated");
|
|
2426
|
+
const tablesSkipped = preview.tables.filter((t) => t.status === "skipped");
|
|
2427
|
+
if (tablesToMigrate.length === 0) {
|
|
2428
|
+
console.log(chalk8.green("\u2713") + " All tables already have system columns \u2014 nothing to migrate\n");
|
|
2429
|
+
if (tablesSkipped.length > 0) {
|
|
2430
|
+
console.log(chalk8.dim(` ${tablesSkipped.length} table(s) already up to date:`));
|
|
2431
|
+
for (const table of tablesSkipped) {
|
|
2432
|
+
console.log(chalk8.dim(` - ${table.tableName}`));
|
|
2433
|
+
}
|
|
2434
|
+
console.log();
|
|
2435
|
+
}
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
console.log(chalk8.yellow(` ${tablesToMigrate.length} table(s) to migrate:
|
|
2439
|
+
`));
|
|
2440
|
+
for (const table of tablesToMigrate) {
|
|
2441
|
+
console.log(` ${chalk8.cyan(table.tableName)} ${chalk8.dim(`(${table.rowsMigrated} rows)`)}`);
|
|
2442
|
+
if (table.columnsAdded.length > 0) {
|
|
2443
|
+
console.log(chalk8.dim(` Adding columns: ${table.columnsAdded.join(", ")}`));
|
|
2444
|
+
}
|
|
2445
|
+
if (table.columnMappings.length > 0) {
|
|
2446
|
+
for (const mapping of table.columnMappings) {
|
|
2447
|
+
console.log(chalk8.dim(` Mapping: ${mapping.from} \u2192 ${mapping.to}`));
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
console.log();
|
|
2451
|
+
}
|
|
2452
|
+
if (tablesSkipped.length > 0) {
|
|
2453
|
+
console.log(chalk8.dim(` ${tablesSkipped.length} table(s) already up to date`));
|
|
2454
|
+
console.log();
|
|
2455
|
+
}
|
|
2456
|
+
if (options.dryRun) {
|
|
2457
|
+
console.log(chalk8.dim(" Dry run complete \u2014 no changes were made\n"));
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
console.log(chalk8.yellow(" \u26A0 This will modify your database tables."));
|
|
2461
|
+
console.log(chalk8.yellow(" Existing columns will be kept. Old primary key values"));
|
|
2462
|
+
console.log(chalk8.yellow(" will be copied to the new system columns where possible.\n"));
|
|
2463
|
+
const confirmed = await askConfirmation(" Proceed with migration?");
|
|
2464
|
+
if (!confirmed) {
|
|
2465
|
+
console.log(chalk8.dim("\n Migration cancelled\n"));
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
console.log();
|
|
2469
|
+
const migrateSpinner = ora8("Migrating tables...").start();
|
|
2470
|
+
const migrateResponse = await fetch(baseUrl, {
|
|
2471
|
+
method: "POST",
|
|
2472
|
+
headers: {
|
|
2473
|
+
"Content-Type": "application/json",
|
|
2474
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
if (!migrateResponse.ok) {
|
|
2478
|
+
const text = await migrateResponse.text();
|
|
2479
|
+
let errorMessage;
|
|
2480
|
+
try {
|
|
2481
|
+
const error = JSON.parse(text);
|
|
2482
|
+
errorMessage = error.error || `HTTP ${migrateResponse.status}`;
|
|
2483
|
+
} catch {
|
|
2484
|
+
errorMessage = text || `HTTP ${migrateResponse.status}: ${migrateResponse.statusText}`;
|
|
2485
|
+
}
|
|
2486
|
+
throw new Error(errorMessage);
|
|
2487
|
+
}
|
|
2488
|
+
const result = await migrateResponse.json();
|
|
2489
|
+
migrateSpinner.succeed("Migration complete");
|
|
2490
|
+
console.log();
|
|
2491
|
+
const migrated = result.tables.filter((t) => t.status === "migrated");
|
|
2492
|
+
for (const table of migrated) {
|
|
2493
|
+
console.log(` ${chalk8.green("\u2713")} ${chalk8.cyan(table.tableName)} \u2014 ${table.rowsMigrated} rows migrated`);
|
|
2494
|
+
for (const mapping of table.columnMappings) {
|
|
2495
|
+
console.log(chalk8.dim(` ${mapping.from} \u2192 ${mapping.to}`));
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
console.log();
|
|
2499
|
+
console.log(chalk8.dim(` ${result.summary}`));
|
|
2500
|
+
console.log();
|
|
2501
|
+
console.log(chalk8.dim(" Old columns have been preserved. You can remove them manually"));
|
|
2502
|
+
console.log(chalk8.dim(" once you have updated your application code.\n"));
|
|
2503
|
+
} catch (error) {
|
|
2504
|
+
spinner.fail("Migration failed");
|
|
2505
|
+
console.error(chalk8.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2506
|
+
console.log();
|
|
2507
|
+
process.exit(1);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
function askConfirmation(prompt) {
|
|
2511
|
+
return new Promise((resolve) => {
|
|
2512
|
+
const rl = readline2.createInterface({
|
|
2513
|
+
input: process.stdin,
|
|
2514
|
+
output: process.stdout
|
|
2515
|
+
});
|
|
2516
|
+
rl.question(`${prompt} ${chalk8.dim("(y/N)")} `, (answer) => {
|
|
2517
|
+
rl.close();
|
|
2518
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
2519
|
+
});
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2376
2523
|
// src/index.ts
|
|
2377
2524
|
var program = new Command();
|
|
2378
2525
|
program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version("0.0.1").enablePositionalOptions();
|
|
@@ -2388,6 +2535,8 @@ program.command("logout").description("Sign out of Tether").action(logoutCommand
|
|
|
2388
2535
|
program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
|
|
2389
2536
|
program.command("update").description("Update all Tether packages to the latest version").option("--dry-run", "Show what would be updated without updating").action(updateCommand);
|
|
2390
2537
|
program.command("exec <sql>").description("Execute a SQL query against the project database").action(execCommand);
|
|
2538
|
+
var migrate = program.command("migrate").description("Database migration utilities");
|
|
2539
|
+
migrate.command("system-columns").description("Migrate legacy tables to use system-managed _id, _createdAt, _updatedAt, _deletedAt columns").option("-e, --env <environment>", "Target environment (default: development)").option("--dry-run", "Show what would be migrated without making changes").action(migrateSystemColumnsCommand);
|
|
2391
2540
|
var env = program.command("env").description("Manage project environments");
|
|
2392
2541
|
env.command("list").description("List all environments").action(envListCommand);
|
|
2393
2542
|
env.command("create <name>").description("Create a new environment").option("--from <env>", "Clone schema and functions from an existing environment").action(envCreateCommand);
|