tthr 0.2.4 → 0.3.2
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-GYMF5VG7.js +617 -0
- package/dist/generate-ZY23LDYI.js +8 -0
- package/dist/index.js +121 -20
- package/package.json +1 -1
|
@@ -0,0 +1,617 @@
|
|
|
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
|
+
var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
190
|
+
var API_URL2 = isDev2 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
191
|
+
function parseSchemaFile(source) {
|
|
192
|
+
const tables = [];
|
|
193
|
+
const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
|
|
194
|
+
if (!schemaMatch) return tables;
|
|
195
|
+
const schemaContent = schemaMatch[1];
|
|
196
|
+
const tableStartRegex = /(\w+)\s*:\s*\{/g;
|
|
197
|
+
let match;
|
|
198
|
+
while ((match = tableStartRegex.exec(schemaContent)) !== null) {
|
|
199
|
+
const tableName = match[1];
|
|
200
|
+
const startOffset = match.index + match[0].length;
|
|
201
|
+
let braceCount = 1;
|
|
202
|
+
let endOffset = startOffset;
|
|
203
|
+
for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
|
|
204
|
+
const char = schemaContent[i];
|
|
205
|
+
if (char === "{") braceCount++;
|
|
206
|
+
else if (char === "}") braceCount--;
|
|
207
|
+
endOffset = i;
|
|
208
|
+
}
|
|
209
|
+
const columnsContent = schemaContent.slice(startOffset, endOffset);
|
|
210
|
+
const columns = parseColumns(columnsContent);
|
|
211
|
+
tables.push({ name: tableName, columns });
|
|
212
|
+
}
|
|
213
|
+
return tables;
|
|
214
|
+
}
|
|
215
|
+
function parseColumns(content) {
|
|
216
|
+
const columns = [];
|
|
217
|
+
const columnRegex = /(\w+)\s*:\s*(\w+)(?:<([^>]+)>)?\s*\(\s*\)((?:\[.*?\]|[^,\n}])*)/g;
|
|
218
|
+
let match;
|
|
219
|
+
while ((match = columnRegex.exec(content)) !== null) {
|
|
220
|
+
const name = match[1];
|
|
221
|
+
const schemaType = match[2];
|
|
222
|
+
const genericType = match[3];
|
|
223
|
+
const modifiers = match[4] || "";
|
|
224
|
+
const nullable = !modifiers.includes(".notNull()");
|
|
225
|
+
const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
|
|
226
|
+
let oneOf;
|
|
227
|
+
if (oneOfMatch) {
|
|
228
|
+
oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
229
|
+
}
|
|
230
|
+
columns.push({
|
|
231
|
+
name,
|
|
232
|
+
type: schemaTypeToTS(schemaType),
|
|
233
|
+
nullable,
|
|
234
|
+
jsonType: genericType,
|
|
235
|
+
// Store the generic type for json columns
|
|
236
|
+
oneOf
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return columns;
|
|
240
|
+
}
|
|
241
|
+
function schemaTypeToTS(schemaType) {
|
|
242
|
+
switch (schemaType) {
|
|
243
|
+
case "text":
|
|
244
|
+
return "string";
|
|
245
|
+
case "integer":
|
|
246
|
+
return "number";
|
|
247
|
+
case "real":
|
|
248
|
+
return "number";
|
|
249
|
+
case "boolean":
|
|
250
|
+
return "boolean";
|
|
251
|
+
case "timestamp":
|
|
252
|
+
return "string";
|
|
253
|
+
case "json":
|
|
254
|
+
return "unknown";
|
|
255
|
+
case "blob":
|
|
256
|
+
return "Uint8Array";
|
|
257
|
+
case "asset":
|
|
258
|
+
return "TetherAsset";
|
|
259
|
+
// Asset object returned by API
|
|
260
|
+
default:
|
|
261
|
+
return "unknown";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function tableNameToInterface(tableName) {
|
|
265
|
+
let name = tableName;
|
|
266
|
+
if (name.endsWith("ies")) {
|
|
267
|
+
name = name.slice(0, -3) + "y";
|
|
268
|
+
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
269
|
+
name = name.slice(0, -1);
|
|
270
|
+
}
|
|
271
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
272
|
+
}
|
|
273
|
+
function generateDbFile(tables) {
|
|
274
|
+
const TS_PRIMITIVES = /* @__PURE__ */ new Set(["number", "string", "boolean", "unknown", "any", "void", "never", "undefined", "null", "object"]);
|
|
275
|
+
const jsonTypes = /* @__PURE__ */ new Set();
|
|
276
|
+
for (const table of tables) {
|
|
277
|
+
for (const col of table.columns) {
|
|
278
|
+
if (col.jsonType) {
|
|
279
|
+
const baseType = col.jsonType.replace(/\[\]$/, "");
|
|
280
|
+
if (!TS_PRIMITIVES.has(baseType)) {
|
|
281
|
+
jsonTypes.add(baseType);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const lines = [
|
|
287
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
288
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
289
|
+
"",
|
|
290
|
+
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
291
|
+
"import {",
|
|
292
|
+
" query as baseQuery,",
|
|
293
|
+
" mutation as baseMutation,",
|
|
294
|
+
" type QueryDefinition,",
|
|
295
|
+
" type MutationDefinition,",
|
|
296
|
+
" z,",
|
|
297
|
+
"} from '@tthr/server';",
|
|
298
|
+
"import type { TetherEnv } from './env';"
|
|
299
|
+
];
|
|
300
|
+
if (jsonTypes.size > 0) {
|
|
301
|
+
const typeImports = Array.from(jsonTypes).sort().join(", ");
|
|
302
|
+
lines.push(`import type { ${typeImports} } from '../schema';`);
|
|
303
|
+
}
|
|
304
|
+
lines.push("");
|
|
305
|
+
lines.push("// Asset type returned by the API for asset columns");
|
|
306
|
+
lines.push("export interface TetherAsset {");
|
|
307
|
+
lines.push(" id: string;");
|
|
308
|
+
lines.push(" filename: string;");
|
|
309
|
+
lines.push(" contentType: string;");
|
|
310
|
+
lines.push(" size: number;");
|
|
311
|
+
lines.push(" url: string;");
|
|
312
|
+
lines.push(" createdAt: string;");
|
|
313
|
+
lines.push("}");
|
|
314
|
+
lines.push("");
|
|
315
|
+
for (const table of tables) {
|
|
316
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
317
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
318
|
+
lines.push(" _id: string;");
|
|
319
|
+
for (const col of table.columns) {
|
|
320
|
+
let colType;
|
|
321
|
+
if (col.oneOf && col.oneOf.length > 0) {
|
|
322
|
+
colType = col.oneOf.map((v) => `'${v}'`).join(" | ");
|
|
323
|
+
} else {
|
|
324
|
+
colType = col.jsonType || col.type;
|
|
325
|
+
}
|
|
326
|
+
const typeStr = col.nullable ? `${colType} | null` : colType;
|
|
327
|
+
const optional = col.nullable ? "?" : "";
|
|
328
|
+
lines.push(` ${col.name}${optional}: ${typeStr};`);
|
|
329
|
+
}
|
|
330
|
+
lines.push(" _createdAt: string;");
|
|
331
|
+
lines.push(" _updatedAt?: string | null;");
|
|
332
|
+
lines.push(" _deletedAt?: string | null;");
|
|
333
|
+
lines.push("}");
|
|
334
|
+
lines.push("");
|
|
335
|
+
}
|
|
336
|
+
lines.push("export interface Schema {");
|
|
337
|
+
for (const table of tables) {
|
|
338
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
339
|
+
lines.push(` ${table.name}: ${interfaceName};`);
|
|
340
|
+
}
|
|
341
|
+
lines.push("}");
|
|
342
|
+
lines.push("");
|
|
343
|
+
lines.push("// Database client with typed tables");
|
|
344
|
+
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
345
|
+
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
346
|
+
lines.push("");
|
|
347
|
+
lines.push("// ============================================================================");
|
|
348
|
+
lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
|
|
349
|
+
lines.push("// This ensures the `db` parameter in handlers is properly typed with Schema");
|
|
350
|
+
lines.push("// ============================================================================");
|
|
351
|
+
lines.push("");
|
|
352
|
+
lines.push("/**");
|
|
353
|
+
lines.push(" * Define a query function with typed database and environment variable access.");
|
|
354
|
+
lines.push(" * The `db` and `env` parameters in the handler will have full type safety.");
|
|
355
|
+
lines.push(" */");
|
|
356
|
+
lines.push("export function query<TArgs = void, TResult = unknown>(");
|
|
357
|
+
lines.push(" definition: QueryDefinition<TArgs, TResult, Schema, TetherEnv>");
|
|
358
|
+
lines.push("): QueryDefinition<TArgs, TResult, Schema, TetherEnv> {");
|
|
359
|
+
lines.push(" return baseQuery(definition);");
|
|
360
|
+
lines.push("}");
|
|
361
|
+
lines.push("");
|
|
362
|
+
lines.push("/**");
|
|
363
|
+
lines.push(" * Define a mutation function with typed database and environment variable access.");
|
|
364
|
+
lines.push(" * The `db` and `env` parameters in the handler will have full type safety.");
|
|
365
|
+
lines.push(" */");
|
|
366
|
+
lines.push("export function mutation<TArgs = void, TResult = unknown>(");
|
|
367
|
+
lines.push(" definition: MutationDefinition<TArgs, TResult, Schema, TetherEnv>");
|
|
368
|
+
lines.push("): MutationDefinition<TArgs, TResult, Schema, TetherEnv> {");
|
|
369
|
+
lines.push(" return baseMutation(definition);");
|
|
370
|
+
lines.push("}");
|
|
371
|
+
lines.push("");
|
|
372
|
+
lines.push("// Re-export z for convenience");
|
|
373
|
+
lines.push("export { z };");
|
|
374
|
+
if (jsonTypes.size > 0) {
|
|
375
|
+
lines.push("");
|
|
376
|
+
lines.push("// Re-export JSON schema types");
|
|
377
|
+
const typeExports = Array.from(jsonTypes).sort().join(", ");
|
|
378
|
+
lines.push(`export type { ${typeExports} } from '../schema';`);
|
|
379
|
+
}
|
|
380
|
+
lines.push("");
|
|
381
|
+
return lines.join("\n");
|
|
382
|
+
}
|
|
383
|
+
async function parseFunctionsDir(functionsDir) {
|
|
384
|
+
const functions = [];
|
|
385
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
386
|
+
return functions;
|
|
387
|
+
}
|
|
388
|
+
const files = await fs3.readdir(functionsDir);
|
|
389
|
+
for (const file of files) {
|
|
390
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
391
|
+
const filePath = path3.join(functionsDir, file);
|
|
392
|
+
const stat = await fs3.stat(filePath);
|
|
393
|
+
if (!stat.isFile()) continue;
|
|
394
|
+
const moduleName = file.replace(/\.(ts|js)$/, "");
|
|
395
|
+
const source = await fs3.readFile(filePath, "utf-8");
|
|
396
|
+
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation)\s*\(/g;
|
|
397
|
+
let match;
|
|
398
|
+
while ((match = exportRegex.exec(source)) !== null) {
|
|
399
|
+
functions.push({
|
|
400
|
+
name: match[1],
|
|
401
|
+
moduleName
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return functions;
|
|
406
|
+
}
|
|
407
|
+
async function generateApiFile(functionsDir) {
|
|
408
|
+
const functions = await parseFunctionsDir(functionsDir);
|
|
409
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
410
|
+
for (const fn of functions) {
|
|
411
|
+
if (!moduleMap.has(fn.moduleName)) {
|
|
412
|
+
moduleMap.set(fn.moduleName, []);
|
|
413
|
+
}
|
|
414
|
+
moduleMap.get(fn.moduleName).push(fn.name);
|
|
415
|
+
}
|
|
416
|
+
const lines = [
|
|
417
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
418
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
419
|
+
"",
|
|
420
|
+
"import { createApiProxy } from '@tthr/client';",
|
|
421
|
+
"",
|
|
422
|
+
"/**",
|
|
423
|
+
" * API function reference type for useQuery/useMutation.",
|
|
424
|
+
' * The _name property contains the function path (e.g., "users.list").',
|
|
425
|
+
" */",
|
|
426
|
+
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
427
|
+
" _name: string;",
|
|
428
|
+
" _args?: TArgs;",
|
|
429
|
+
" _result?: TResult;",
|
|
430
|
+
"}",
|
|
431
|
+
""
|
|
432
|
+
];
|
|
433
|
+
if (moduleMap.size > 0) {
|
|
434
|
+
for (const [moduleName, fnNames] of moduleMap) {
|
|
435
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
436
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
437
|
+
for (const fnName of fnNames) {
|
|
438
|
+
lines.push(` ${fnName}: ApiFunction;`);
|
|
439
|
+
}
|
|
440
|
+
lines.push("}");
|
|
441
|
+
lines.push("");
|
|
442
|
+
}
|
|
443
|
+
lines.push("export interface Api {");
|
|
444
|
+
for (const moduleName of moduleMap.keys()) {
|
|
445
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
446
|
+
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
447
|
+
}
|
|
448
|
+
lines.push("}");
|
|
449
|
+
} else {
|
|
450
|
+
lines.push("/**");
|
|
451
|
+
lines.push(" * Flexible API type that allows access to any function path.");
|
|
452
|
+
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
453
|
+
lines.push(" * The actual types depend on your function definitions.");
|
|
454
|
+
lines.push(" */");
|
|
455
|
+
lines.push("export type Api = {");
|
|
456
|
+
lines.push(" [module: string]: {");
|
|
457
|
+
lines.push(" [fn: string]: ApiFunction;");
|
|
458
|
+
lines.push(" };");
|
|
459
|
+
lines.push("};");
|
|
460
|
+
}
|
|
461
|
+
lines.push("");
|
|
462
|
+
lines.push("// API client proxy - provides typed access to your functions");
|
|
463
|
+
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
464
|
+
lines.push("// In Tether functions: calls the actual function implementation");
|
|
465
|
+
lines.push("export const api = createApiProxy<Api>();");
|
|
466
|
+
lines.push("");
|
|
467
|
+
return lines.join("\n");
|
|
468
|
+
}
|
|
469
|
+
async function fetchEnvVarKeys(projectId, environment) {
|
|
470
|
+
const credentials = await getCredentials();
|
|
471
|
+
if (!credentials) return [];
|
|
472
|
+
const envPath = environment !== "production" ? `/projects/${projectId}/env/${environment}/env-vars` : `/projects/${projectId}/env-vars`;
|
|
473
|
+
try {
|
|
474
|
+
const response = await fetch(`${API_URL2}${envPath}`, {
|
|
475
|
+
headers: {
|
|
476
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
if (!response.ok) return [];
|
|
480
|
+
const data = await response.json();
|
|
481
|
+
return (data.envVars || []).map((v) => v.key).sort();
|
|
482
|
+
} catch {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function generateEnvFile(keys) {
|
|
487
|
+
const lines = [
|
|
488
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
489
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
490
|
+
"",
|
|
491
|
+
"/**",
|
|
492
|
+
" * Typed environment variables for this project.",
|
|
493
|
+
" * Keys are fetched from the Tether API \u2014 values are never exposed.",
|
|
494
|
+
' * Use `env.get("KEY")` in your functions to access them.',
|
|
495
|
+
" */",
|
|
496
|
+
""
|
|
497
|
+
];
|
|
498
|
+
if (keys.length > 0) {
|
|
499
|
+
lines.push("export type EnvVarKey =");
|
|
500
|
+
for (let i = 0; i < keys.length; i++) {
|
|
501
|
+
const sep = i === keys.length - 1 ? ";" : "";
|
|
502
|
+
lines.push(` | '${keys[i]}'${sep}`);
|
|
503
|
+
}
|
|
504
|
+
lines.push("");
|
|
505
|
+
lines.push("export interface TetherEnv {");
|
|
506
|
+
for (const key of keys) {
|
|
507
|
+
lines.push(` ${key}: string;`);
|
|
508
|
+
}
|
|
509
|
+
lines.push("}");
|
|
510
|
+
} else {
|
|
511
|
+
lines.push("export type EnvVarKey = string;");
|
|
512
|
+
lines.push("");
|
|
513
|
+
lines.push("export interface TetherEnv {");
|
|
514
|
+
lines.push(" [key: string]: string;");
|
|
515
|
+
lines.push("}");
|
|
516
|
+
}
|
|
517
|
+
lines.push("");
|
|
518
|
+
return lines.join("\n");
|
|
519
|
+
}
|
|
520
|
+
async function generateTypes(options = {}) {
|
|
521
|
+
const config = await loadConfig();
|
|
522
|
+
const schemaPath = resolvePath(config.schema);
|
|
523
|
+
const outputDir = resolvePath(config.output);
|
|
524
|
+
const functionsDir = resolvePath(config.functions);
|
|
525
|
+
if (!await fs3.pathExists(schemaPath)) {
|
|
526
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
527
|
+
}
|
|
528
|
+
const schemaSource = await fs3.readFile(schemaPath, "utf-8");
|
|
529
|
+
const tables = parseSchemaFile(schemaSource);
|
|
530
|
+
const environment = config.environment || "development";
|
|
531
|
+
let projectId;
|
|
532
|
+
const envFilePath = path3.resolve(process.cwd(), ".env");
|
|
533
|
+
if (await fs3.pathExists(envFilePath)) {
|
|
534
|
+
const envContent = await fs3.readFile(envFilePath, "utf-8");
|
|
535
|
+
const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
|
|
536
|
+
projectId = match?.[1]?.trim();
|
|
537
|
+
}
|
|
538
|
+
const envVarKeys = projectId ? await fetchEnvVarKeys(projectId, environment) : [];
|
|
539
|
+
await fs3.ensureDir(outputDir);
|
|
540
|
+
await fs3.writeFile(
|
|
541
|
+
path3.join(outputDir, "db.ts"),
|
|
542
|
+
generateDbFile(tables)
|
|
543
|
+
);
|
|
544
|
+
await fs3.writeFile(
|
|
545
|
+
path3.join(outputDir, "api.ts"),
|
|
546
|
+
await generateApiFile(functionsDir)
|
|
547
|
+
);
|
|
548
|
+
await fs3.writeFile(
|
|
549
|
+
path3.join(outputDir, "env.ts"),
|
|
550
|
+
generateEnvFile(envVarKeys)
|
|
551
|
+
);
|
|
552
|
+
await fs3.writeFile(
|
|
553
|
+
path3.join(outputDir, "index.ts"),
|
|
554
|
+
`// Auto-generated by Tether CLI - do not edit manually
|
|
555
|
+
export * from './db';
|
|
556
|
+
export * from './api';
|
|
557
|
+
export * from './env';
|
|
558
|
+
`
|
|
559
|
+
);
|
|
560
|
+
return { tables, envVarKeys, outputDir };
|
|
561
|
+
}
|
|
562
|
+
async function generateCommand() {
|
|
563
|
+
await requireAuth();
|
|
564
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
565
|
+
if (!await fs3.pathExists(configPath)) {
|
|
566
|
+
console.log(chalk2.red("\nError: Not a Tether project"));
|
|
567
|
+
console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
|
|
571
|
+
const spinner = ora("Reading schema...").start();
|
|
572
|
+
try {
|
|
573
|
+
spinner.text = "Generating types...";
|
|
574
|
+
const { tables, envVarKeys, outputDir } = await generateTypes();
|
|
575
|
+
if (tables.length === 0) {
|
|
576
|
+
spinner.warn("No tables found in schema");
|
|
577
|
+
console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
581
|
+
console.log("\n" + chalk2.green("\u2713") + " Tables:");
|
|
582
|
+
for (const table of tables) {
|
|
583
|
+
console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
584
|
+
}
|
|
585
|
+
if (envVarKeys.length > 0) {
|
|
586
|
+
console.log("\n" + chalk2.green("\u2713") + ` Environment variables: ${envVarKeys.length} key(s)`);
|
|
587
|
+
for (const key of envVarKeys) {
|
|
588
|
+
console.log(chalk2.dim(` - ${key}`));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const relativeOutput = path3.relative(process.cwd(), outputDir);
|
|
592
|
+
console.log("\n" + chalk2.green("\u2713") + " Generated files:");
|
|
593
|
+
console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
|
|
594
|
+
console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
|
|
595
|
+
console.log(chalk2.dim(` ${relativeOutput}/env.ts`));
|
|
596
|
+
console.log(chalk2.dim(` ${relativeOutput}/index.ts
|
|
597
|
+
`));
|
|
598
|
+
} catch (error) {
|
|
599
|
+
spinner.fail("Failed to generate types");
|
|
600
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export {
|
|
606
|
+
API_URL,
|
|
607
|
+
getCredentials,
|
|
608
|
+
requireAuth,
|
|
609
|
+
saveCredentials,
|
|
610
|
+
clearCredentials,
|
|
611
|
+
loadConfig,
|
|
612
|
+
resolvePath,
|
|
613
|
+
detectFramework,
|
|
614
|
+
getFrameworkDevCommand,
|
|
615
|
+
generateTypes,
|
|
616
|
+
generateCommand
|
|
617
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -11,10 +11,13 @@ import {
|
|
|
11
11
|
requireAuth,
|
|
12
12
|
resolvePath,
|
|
13
13
|
saveCredentials
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-GYMF5VG7.js";
|
|
15
15
|
|
|
16
16
|
// src/index.ts
|
|
17
17
|
import { Command } from "commander";
|
|
18
|
+
import fs8 from "fs-extra";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
import path8 from "path";
|
|
18
21
|
|
|
19
22
|
// src/commands/init.ts
|
|
20
23
|
import chalk2 from "chalk";
|
|
@@ -111,6 +114,12 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
|
|
|
111
114
|
const schemaUrl = `${API_URL2}/projects/${projectId}/env/${environment}/deploy/schema`;
|
|
112
115
|
console.log(chalk.dim(`
|
|
113
116
|
URL: ${schemaUrl}`));
|
|
117
|
+
const rlsConfigs = tables.filter((t) => t.rls).map((t) => ({
|
|
118
|
+
tableName: t.name,
|
|
119
|
+
enabled: t.rls.enabled,
|
|
120
|
+
defaultDeny: t.rls.defaultDeny ?? true,
|
|
121
|
+
policies: t.rls.policies
|
|
122
|
+
}));
|
|
114
123
|
const requestBody = {
|
|
115
124
|
sql,
|
|
116
125
|
tables: tables.map((t) => ({
|
|
@@ -119,6 +128,9 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
|
|
|
119
128
|
columns: t.columns
|
|
120
129
|
}))
|
|
121
130
|
};
|
|
131
|
+
if (rlsConfigs.length > 0) {
|
|
132
|
+
requestBody.rls = rlsConfigs;
|
|
133
|
+
}
|
|
122
134
|
const response = await fetch(schemaUrl, {
|
|
123
135
|
method: "POST",
|
|
124
136
|
headers: {
|
|
@@ -277,10 +289,11 @@ function parseSchema(source) {
|
|
|
277
289
|
if (!schemaMatch) return tables;
|
|
278
290
|
const schemaContent = schemaMatch[1];
|
|
279
291
|
const schemaStartIndex = source.indexOf(schemaMatch[0]) + "defineSchema({".length;
|
|
280
|
-
const tableStartRegex = /(\w+)\s*:\s*\{/g;
|
|
292
|
+
const tableStartRegex = /(\w+)\s*:\s*(?:defineTable\s*\(\s*)?\{/g;
|
|
281
293
|
let match;
|
|
282
294
|
while ((match = tableStartRegex.exec(schemaContent)) !== null) {
|
|
283
295
|
const tableName = match[1];
|
|
296
|
+
const isDefineTable = match[0].includes("defineTable");
|
|
284
297
|
const startOffset = match.index + match[0].length;
|
|
285
298
|
let braceCount = 1;
|
|
286
299
|
let endOffset = startOffset;
|
|
@@ -290,7 +303,28 @@ function parseSchema(source) {
|
|
|
290
303
|
else if (char === "}") braceCount--;
|
|
291
304
|
endOffset = i;
|
|
292
305
|
}
|
|
293
|
-
|
|
306
|
+
let rawContent = schemaContent.slice(startOffset, endOffset);
|
|
307
|
+
let rlsConfig;
|
|
308
|
+
let columnsContent;
|
|
309
|
+
if (isDefineTable) {
|
|
310
|
+
const columnsBlockMatch = rawContent.match(/columns\s*:\s*\{/);
|
|
311
|
+
if (columnsBlockMatch) {
|
|
312
|
+
const colStart = rawContent.indexOf(columnsBlockMatch[0]) + columnsBlockMatch[0].length;
|
|
313
|
+
let colBraceCount = 1;
|
|
314
|
+
let colEnd = colStart;
|
|
315
|
+
for (let i = colStart; i < rawContent.length && colBraceCount > 0; i++) {
|
|
316
|
+
if (rawContent[i] === "{") colBraceCount++;
|
|
317
|
+
else if (rawContent[i] === "}") colBraceCount--;
|
|
318
|
+
colEnd = i;
|
|
319
|
+
}
|
|
320
|
+
columnsContent = rawContent.slice(colStart, colEnd);
|
|
321
|
+
} else {
|
|
322
|
+
columnsContent = rawContent;
|
|
323
|
+
}
|
|
324
|
+
rlsConfig = parseRlsConfig(rawContent);
|
|
325
|
+
} else {
|
|
326
|
+
columnsContent = rawContent;
|
|
327
|
+
}
|
|
294
328
|
const lines = columnsContent.split("\n");
|
|
295
329
|
const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
|
|
296
330
|
const minIndent = nonEmptyLines.reduce((min, line) => {
|
|
@@ -334,10 +368,74 @@ function parseSchema(source) {
|
|
|
334
368
|
oneOf
|
|
335
369
|
};
|
|
336
370
|
}
|
|
337
|
-
tables.push({ name: tableName, columns, source: tableSource });
|
|
371
|
+
tables.push({ name: tableName, columns, source: tableSource, rls: rlsConfig });
|
|
338
372
|
}
|
|
339
373
|
return tables;
|
|
340
374
|
}
|
|
375
|
+
function parseRlsConfig(content) {
|
|
376
|
+
const rlsMatch = content.match(/rls\s*:\s*\{/);
|
|
377
|
+
if (!rlsMatch) return void 0;
|
|
378
|
+
const rlsStart = content.indexOf(rlsMatch[0]) + rlsMatch[0].length;
|
|
379
|
+
let braceCount = 1;
|
|
380
|
+
let rlsEnd = rlsStart;
|
|
381
|
+
for (let i = rlsStart; i < content.length && braceCount > 0; i++) {
|
|
382
|
+
if (content[i] === "{") braceCount++;
|
|
383
|
+
else if (content[i] === "}") braceCount--;
|
|
384
|
+
rlsEnd = i;
|
|
385
|
+
}
|
|
386
|
+
const rlsContent = content.slice(rlsStart, rlsEnd);
|
|
387
|
+
const enabledMatch = rlsContent.match(/enabled\s*:\s*(true|false)/);
|
|
388
|
+
const enabled = enabledMatch ? enabledMatch[1] === "true" : false;
|
|
389
|
+
const defaultDenyMatch = rlsContent.match(/defaultDeny\s*:\s*(true|false)/);
|
|
390
|
+
const defaultDeny = defaultDenyMatch ? defaultDenyMatch[1] === "true" : true;
|
|
391
|
+
const policies = [];
|
|
392
|
+
const policiesMatch = rlsContent.match(/policies\s*:\s*\[/);
|
|
393
|
+
if (policiesMatch) {
|
|
394
|
+
const pStart = rlsContent.indexOf(policiesMatch[0]) + policiesMatch[0].length;
|
|
395
|
+
let bracketCount = 1;
|
|
396
|
+
let pEnd = pStart;
|
|
397
|
+
for (let i = pStart; i < rlsContent.length && bracketCount > 0; i++) {
|
|
398
|
+
if (rlsContent[i] === "[") bracketCount++;
|
|
399
|
+
else if (rlsContent[i] === "]") bracketCount--;
|
|
400
|
+
pEnd = i;
|
|
401
|
+
}
|
|
402
|
+
const policiesContent = rlsContent.slice(pStart, pEnd);
|
|
403
|
+
const policyRegex = /\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
404
|
+
let policyMatch;
|
|
405
|
+
while ((policyMatch = policyRegex.exec(policiesContent)) !== null) {
|
|
406
|
+
const pBody = policyMatch[1];
|
|
407
|
+
const name = pBody.match(/name\s*:\s*['"]([^'"]+)['"]/)?.[1];
|
|
408
|
+
const operation = pBody.match(/operation\s*:\s*['"]([^'"]+)['"]/)?.[1];
|
|
409
|
+
const role = pBody.match(/role\s*:\s*['"]([^'"]+)['"]/)?.[1];
|
|
410
|
+
if (!name || !operation) continue;
|
|
411
|
+
const usingExpr = extractJsonExpression(pBody, "using");
|
|
412
|
+
const checkExpr = extractJsonExpression(pBody, "check");
|
|
413
|
+
policies.push({ name, operation, using: usingExpr, check: checkExpr, role });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!enabled && policies.length === 0) return void 0;
|
|
417
|
+
return { enabled, defaultDeny, policies };
|
|
418
|
+
}
|
|
419
|
+
function extractJsonExpression(text, key) {
|
|
420
|
+
const keyRegex = new RegExp(`${key}\\s*:\\s*\\{`);
|
|
421
|
+
const keyMatch = text.match(keyRegex);
|
|
422
|
+
if (!keyMatch) return void 0;
|
|
423
|
+
const start = text.indexOf(keyMatch[0]) + keyMatch[0].length;
|
|
424
|
+
let braceCount = 1;
|
|
425
|
+
let end = start;
|
|
426
|
+
for (let i = start; i < text.length && braceCount > 0; i++) {
|
|
427
|
+
if (text[i] === "{") braceCount++;
|
|
428
|
+
else if (text[i] === "}") braceCount--;
|
|
429
|
+
end = i;
|
|
430
|
+
}
|
|
431
|
+
const raw = "{" + text.slice(start, end) + "}";
|
|
432
|
+
try {
|
|
433
|
+
const jsonStr = raw.replace(/(\w+)\s*:/g, '"$1":').replace(/'/g, '"').replace(/,\s*([}\]])/g, "$1").replace(/"(\$\w+(?:\.\w+)?)"/g, '"$1"');
|
|
434
|
+
return JSON.parse(jsonStr);
|
|
435
|
+
} catch {
|
|
436
|
+
return void 0;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
341
439
|
function getColumnSqlType(type) {
|
|
342
440
|
switch (type) {
|
|
343
441
|
case "text":
|
|
@@ -1586,7 +1684,7 @@ async function runGenerate(spinner) {
|
|
|
1586
1684
|
}
|
|
1587
1685
|
isGenerating = true;
|
|
1588
1686
|
try {
|
|
1589
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1687
|
+
const { generateTypes: generateTypes2 } = await import("./generate-ZY23LDYI.js");
|
|
1590
1688
|
spinner.text = "Regenerating types...";
|
|
1591
1689
|
await generateTypes2({ silent: true });
|
|
1592
1690
|
spinner.succeed("Types regenerated");
|
|
@@ -1806,7 +1904,7 @@ async function devCommand(options) {
|
|
|
1806
1904
|
}
|
|
1807
1905
|
}
|
|
1808
1906
|
spinner.text = "Generating types...";
|
|
1809
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1907
|
+
const { generateTypes: generateTypes2 } = await import("./generate-ZY23LDYI.js");
|
|
1810
1908
|
await generateTypes2({ silent: true });
|
|
1811
1909
|
spinner.succeed("Types generated");
|
|
1812
1910
|
if (config.projectId) {
|
|
@@ -2141,12 +2239,12 @@ async function updateCommand(options) {
|
|
|
2141
2239
|
const deps = packageJson.dependencies || {};
|
|
2142
2240
|
const devDeps = packageJson.devDependencies || {};
|
|
2143
2241
|
const installedDeps = [];
|
|
2144
|
-
for (const
|
|
2145
|
-
if (deps[
|
|
2146
|
-
installedDeps.push({ name:
|
|
2242
|
+
for (const pkg2 of TETHER_PACKAGES) {
|
|
2243
|
+
if (deps[pkg2] && !deps[pkg2].startsWith("workspace:")) {
|
|
2244
|
+
installedDeps.push({ name: pkg2, current: deps[pkg2], isDev: false });
|
|
2147
2245
|
}
|
|
2148
|
-
if (devDeps[
|
|
2149
|
-
installedDeps.push({ name:
|
|
2246
|
+
if (devDeps[pkg2] && !devDeps[pkg2].startsWith("workspace:")) {
|
|
2247
|
+
installedDeps.push({ name: pkg2, current: devDeps[pkg2], isDev: true });
|
|
2150
2248
|
}
|
|
2151
2249
|
}
|
|
2152
2250
|
if (installedDeps.length === 0) {
|
|
@@ -2158,19 +2256,19 @@ async function updateCommand(options) {
|
|
|
2158
2256
|
`));
|
|
2159
2257
|
const spinner = ora5("Checking for updates...").start();
|
|
2160
2258
|
const packagesToUpdate = [];
|
|
2161
|
-
for (const
|
|
2162
|
-
const latestVersion = await getLatestVersion2(
|
|
2259
|
+
for (const pkg2 of installedDeps) {
|
|
2260
|
+
const latestVersion = await getLatestVersion2(pkg2.name);
|
|
2163
2261
|
if (latestVersion) {
|
|
2164
|
-
const currentClean =
|
|
2262
|
+
const currentClean = pkg2.current.replace(/^[\^~>=<]+/, "");
|
|
2165
2263
|
if (currentClean !== latestVersion) {
|
|
2166
2264
|
packagesToUpdate.push({
|
|
2167
|
-
name:
|
|
2265
|
+
name: pkg2.name,
|
|
2168
2266
|
current: currentClean,
|
|
2169
2267
|
latest: latestVersion,
|
|
2170
|
-
isDev:
|
|
2268
|
+
isDev: pkg2.isDev
|
|
2171
2269
|
});
|
|
2172
2270
|
} else {
|
|
2173
|
-
console.log(chalk5.dim(` ${
|
|
2271
|
+
console.log(chalk5.dim(` ${pkg2.name}@${currentClean} (up to date)`));
|
|
2174
2272
|
}
|
|
2175
2273
|
}
|
|
2176
2274
|
}
|
|
@@ -2180,9 +2278,9 @@ async function updateCommand(options) {
|
|
|
2180
2278
|
return;
|
|
2181
2279
|
}
|
|
2182
2280
|
console.log(chalk5.cyan("\n Updates available:\n"));
|
|
2183
|
-
for (const
|
|
2281
|
+
for (const pkg2 of packagesToUpdate) {
|
|
2184
2282
|
console.log(
|
|
2185
|
-
chalk5.white(` ${
|
|
2283
|
+
chalk5.white(` ${pkg2.name}`) + chalk5.red(` ${pkg2.current}`) + chalk5.dim(" \u2192 ") + chalk5.green(`${pkg2.latest}`) + (pkg2.isDev ? chalk5.dim(" (dev)") : "")
|
|
2186
2284
|
);
|
|
2187
2285
|
}
|
|
2188
2286
|
if (options.dryRun) {
|
|
@@ -2601,8 +2699,11 @@ function askConfirmation(prompt) {
|
|
|
2601
2699
|
}
|
|
2602
2700
|
|
|
2603
2701
|
// src/index.ts
|
|
2702
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
2703
|
+
var __dirname = path8.dirname(__filename);
|
|
2704
|
+
var pkg = fs8.readJsonSync(path8.resolve(__dirname, "../package.json"));
|
|
2604
2705
|
var program = new Command();
|
|
2605
|
-
program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version(
|
|
2706
|
+
program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version(pkg.version).enablePositionalOptions();
|
|
2606
2707
|
program.command("init [name]").description("Create a new Tether project").option("-t, --template <template>", "Project template (vue, svelte, react, vanilla)", "vue").action(initCommand);
|
|
2607
2708
|
program.command("dev").description("Start local development server with hot reload").option("-p, --port <port>", "Port to run on").option("-e, --env <environment>", "Target environment (default: development)").option("--local", "Run fully local (do not connect to cloud)").option("--skip-framework", "Only run Tether watchers, skip framework dev server").allowUnknownOption().passThroughOptions().action((options, command) => {
|
|
2608
2709
|
const extraArgs = command.args || [];
|