tthr 0.0.59 → 0.0.61
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 +176 -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, {
|
|
@@ -301,6 +302,7 @@ function parseSchema(source) {
|
|
|
301
302
|
unique: modifiers.includes(".unique()"),
|
|
302
303
|
hasDefault: modifiers.includes(".default("),
|
|
303
304
|
references: modifiers.match(/\.references\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1],
|
|
305
|
+
onDelete: modifiers.match(/\.onDelete\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1],
|
|
304
306
|
oneOf
|
|
305
307
|
};
|
|
306
308
|
}
|
|
@@ -342,6 +344,9 @@ function buildColumnSql(colName, def, forAlterTable = false) {
|
|
|
342
344
|
if (def.references) {
|
|
343
345
|
const [refTable, refCol] = def.references.split(".");
|
|
344
346
|
colSql += ` REFERENCES "${refTable}"("${refCol || "_id"}")`;
|
|
347
|
+
if (def.onDelete) {
|
|
348
|
+
colSql += ` ON DELETE ${def.onDelete.toUpperCase()}`;
|
|
349
|
+
}
|
|
345
350
|
}
|
|
346
351
|
if (def.oneOf && def.oneOf.length > 0) {
|
|
347
352
|
const values = def.oneOf.map((v) => `'${v}'`).join(", ");
|
|
@@ -353,6 +358,10 @@ function generateSchemaSQL(tables) {
|
|
|
353
358
|
const statements = [];
|
|
354
359
|
for (const table of tables) {
|
|
355
360
|
const columnDefs = [];
|
|
361
|
+
columnDefs.push('"_id" TEXT PRIMARY KEY');
|
|
362
|
+
columnDefs.push(`"_createdAt" TEXT DEFAULT (datetime('now'))`);
|
|
363
|
+
columnDefs.push('"_updatedAt" TEXT');
|
|
364
|
+
columnDefs.push('"_deletedAt" TEXT');
|
|
356
365
|
for (const [colName, colDef] of Object.entries(table.columns)) {
|
|
357
366
|
const def = colDef;
|
|
358
367
|
columnDefs.push(buildColumnSql(colName, def));
|
|
@@ -427,7 +436,7 @@ function parseFunctions(moduleName, source) {
|
|
|
427
436
|
|
|
428
437
|
// src/commands/init.ts
|
|
429
438
|
var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
430
|
-
var
|
|
439
|
+
var API_URL3 = isDev2 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
431
440
|
async function getLatestVersion(packageName) {
|
|
432
441
|
try {
|
|
433
442
|
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
@@ -529,7 +538,7 @@ ${packageManager} is not installed on your system.`));
|
|
|
529
538
|
await scaffoldVanillaProject(projectName, projectPath, spinner);
|
|
530
539
|
}
|
|
531
540
|
spinner.text = "Creating project on Tether...";
|
|
532
|
-
const response = await fetch(`${
|
|
541
|
+
const response = await fetch(`${API_URL3}/projects`, {
|
|
533
542
|
method: "POST",
|
|
534
543
|
headers: {
|
|
535
544
|
"Content-Type": "application/json",
|
|
@@ -1544,7 +1553,7 @@ async function runGenerate(spinner) {
|
|
|
1544
1553
|
}
|
|
1545
1554
|
isGenerating = true;
|
|
1546
1555
|
try {
|
|
1547
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1556
|
+
const { generateTypes: generateTypes2 } = await import("./generate-Q4MUBHHO.js");
|
|
1548
1557
|
spinner.text = "Regenerating types...";
|
|
1549
1558
|
await generateTypes2({ silent: true });
|
|
1550
1559
|
spinner.succeed("Types regenerated");
|
|
@@ -1729,7 +1738,7 @@ async function devCommand(options) {
|
|
|
1729
1738
|
}
|
|
1730
1739
|
}
|
|
1731
1740
|
spinner.text = "Generating types...";
|
|
1732
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1741
|
+
const { generateTypes: generateTypes2 } = await import("./generate-Q4MUBHHO.js");
|
|
1733
1742
|
await generateTypes2({ silent: true });
|
|
1734
1743
|
spinner.succeed("Types generated");
|
|
1735
1744
|
spinner.start("Setting up file watchers...");
|
|
@@ -1800,7 +1809,7 @@ import os from "os";
|
|
|
1800
1809
|
import { exec } from "child_process";
|
|
1801
1810
|
import readline from "readline";
|
|
1802
1811
|
var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
1803
|
-
var
|
|
1812
|
+
var API_URL4 = isDev3 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
1804
1813
|
var AUTH_URL = isDev3 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
|
|
1805
1814
|
async function loginCommand() {
|
|
1806
1815
|
console.log(chalk4.bold("\u26A1 Login to Tether\n"));
|
|
@@ -1890,7 +1899,7 @@ async function requestDeviceCode() {
|
|
|
1890
1899
|
const userCode = generateUserCode();
|
|
1891
1900
|
const deviceCode = crypto.randomUUID();
|
|
1892
1901
|
const deviceName = os.hostname();
|
|
1893
|
-
const response = await fetch(`${
|
|
1902
|
+
const response = await fetch(`${API_URL4}/auth/device`, {
|
|
1894
1903
|
method: "POST",
|
|
1895
1904
|
headers: { "Content-Type": "application/json" },
|
|
1896
1905
|
body: JSON.stringify({ userCode, deviceCode, deviceName })
|
|
@@ -1924,7 +1933,7 @@ async function pollForApproval(deviceCode, interval, expiresIn) {
|
|
|
1924
1933
|
while (Date.now() < expiresAt) {
|
|
1925
1934
|
await sleep(interval * 1e3);
|
|
1926
1935
|
spinner.text = `Checking... ${chalk4.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
|
|
1927
|
-
const response = await fetch(`${
|
|
1936
|
+
const response = await fetch(`${API_URL4}/auth/device/${deviceCode}`, {
|
|
1928
1937
|
method: "GET"
|
|
1929
1938
|
}).catch(() => null);
|
|
1930
1939
|
updateCountdown();
|
|
@@ -2137,7 +2146,7 @@ import ora6 from "ora";
|
|
|
2137
2146
|
import fs5 from "fs-extra";
|
|
2138
2147
|
import path5 from "path";
|
|
2139
2148
|
var isDev4 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
2140
|
-
var
|
|
2149
|
+
var API_URL5 = isDev4 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
2141
2150
|
async function execCommand(sql) {
|
|
2142
2151
|
if (!sql || sql.trim() === "") {
|
|
2143
2152
|
console.log(chalk6.red("\nError: SQL query required"));
|
|
@@ -2159,7 +2168,7 @@ async function execCommand(sql) {
|
|
|
2159
2168
|
}
|
|
2160
2169
|
const spinner = ora6("Executing query...").start();
|
|
2161
2170
|
try {
|
|
2162
|
-
const response = await fetch(`${
|
|
2171
|
+
const response = await fetch(`${API_URL5}/projects/${projectId}/exec`, {
|
|
2163
2172
|
method: "POST",
|
|
2164
2173
|
headers: {
|
|
2165
2174
|
"Content-Type": "application/json",
|
|
@@ -2210,7 +2219,7 @@ import ora7 from "ora";
|
|
|
2210
2219
|
import fs6 from "fs-extra";
|
|
2211
2220
|
import path6 from "path";
|
|
2212
2221
|
var isDev5 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
2213
|
-
var
|
|
2222
|
+
var API_URL6 = isDev5 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
2214
2223
|
async function getProjectId() {
|
|
2215
2224
|
const envPath = path6.resolve(process.cwd(), ".env");
|
|
2216
2225
|
let projectId;
|
|
@@ -2231,7 +2240,7 @@ async function envListCommand() {
|
|
|
2231
2240
|
const projectId = await getProjectId();
|
|
2232
2241
|
const spinner = ora7("Fetching environments...").start();
|
|
2233
2242
|
try {
|
|
2234
|
-
const response = await fetch(`${
|
|
2243
|
+
const response = await fetch(`${API_URL6}/projects/${projectId}/environments`, {
|
|
2235
2244
|
headers: {
|
|
2236
2245
|
"Authorization": `Bearer ${credentials.accessToken}`
|
|
2237
2246
|
}
|
|
@@ -2274,7 +2283,7 @@ async function envCreateCommand(name, options) {
|
|
|
2274
2283
|
body.cloneFrom = options.from;
|
|
2275
2284
|
spinner.text = `Creating environment '${normalizedName}' from '${options.from}'...`;
|
|
2276
2285
|
}
|
|
2277
|
-
const response = await fetch(`${
|
|
2286
|
+
const response = await fetch(`${API_URL6}/projects/${projectId}/environments`, {
|
|
2278
2287
|
method: "POST",
|
|
2279
2288
|
headers: {
|
|
2280
2289
|
"Content-Type": "application/json",
|
|
@@ -2305,7 +2314,7 @@ async function envDeleteCommand(name) {
|
|
|
2305
2314
|
const projectId = await getProjectId();
|
|
2306
2315
|
const spinner = ora7(`Deleting environment '${name}'...`).start();
|
|
2307
2316
|
try {
|
|
2308
|
-
const response = await fetch(`${
|
|
2317
|
+
const response = await fetch(`${API_URL6}/projects/${projectId}/environments/${name}`, {
|
|
2309
2318
|
method: "DELETE",
|
|
2310
2319
|
headers: {
|
|
2311
2320
|
"Authorization": `Bearer ${credentials.accessToken}`
|
|
@@ -2327,7 +2336,7 @@ async function envDefaultCommand(name) {
|
|
|
2327
2336
|
const projectId = await getProjectId();
|
|
2328
2337
|
const spinner = ora7(`Setting '${name}' as default environment...`).start();
|
|
2329
2338
|
try {
|
|
2330
|
-
const response = await fetch(`${
|
|
2339
|
+
const response = await fetch(`${API_URL6}/projects/${projectId}/environments/default`, {
|
|
2331
2340
|
method: "PATCH",
|
|
2332
2341
|
headers: {
|
|
2333
2342
|
"Content-Type": "application/json",
|
|
@@ -2365,6 +2374,152 @@ function maskApiKey(key) {
|
|
|
2365
2374
|
return `${visibleStart}...${visibleEnd}`;
|
|
2366
2375
|
}
|
|
2367
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
|
+
|
|
2368
2523
|
// src/index.ts
|
|
2369
2524
|
var program = new Command();
|
|
2370
2525
|
program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version("0.0.1").enablePositionalOptions();
|
|
@@ -2380,6 +2535,8 @@ program.command("logout").description("Sign out of Tether").action(logoutCommand
|
|
|
2380
2535
|
program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
|
|
2381
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);
|
|
2382
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);
|
|
2383
2540
|
var env = program.command("env").description("Manage project environments");
|
|
2384
2541
|
env.command("list").description("List all environments").action(envListCommand);
|
|
2385
2542
|
env.command("create <name>").description("Create a new environment").option("--from <env>", "Clone schema and functions from an existing environment").action(envCreateCommand);
|