tthr 0.3.10 → 0.3.12

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.
@@ -0,0 +1,721 @@
1
+ // src/commands/generate.ts
2
+ import chalk2 from "chalk";
3
+ import ora from "ora";
4
+ import fs3 from "fs-extra";
5
+ import path3 from "path";
6
+
7
+ // src/utils/auth.ts
8
+ import chalk from "chalk";
9
+ import 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 projectIdMatch = configSource.match(/projectId\s*:\s*['"]([^'"]+)['"]/);
120
+ if (projectIdMatch) {
121
+ config.projectId = projectIdMatch[1];
122
+ }
123
+ const urlMatch = configSource.match(/(?:^|\n)\s*url\s*:\s*['"]([^'"]+)['"]/);
124
+ if (urlMatch) {
125
+ config.url = urlMatch[1];
126
+ }
127
+ const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
128
+ if (envMatch) {
129
+ config.environment = envMatch[1];
130
+ }
131
+ const portMatch = configSource.match(/port\s*:\s*(\d+)/);
132
+ if (portMatch) {
133
+ config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
134
+ }
135
+ const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
136
+ if (hostMatch) {
137
+ config.dev = { ...config.dev, host: hostMatch[1] };
138
+ }
139
+ return {
140
+ ...DEFAULT_CONFIG,
141
+ ...config,
142
+ dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
143
+ database: { ...DEFAULT_CONFIG.database, ...config.database }
144
+ };
145
+ }
146
+ async function resolveEnvironmentFromApiKey(cwd = process.cwd()) {
147
+ const envPath = path2.resolve(cwd, ".env");
148
+ if (!await fs2.pathExists(envPath)) return void 0;
149
+ const envContent = await fs2.readFile(envPath, "utf-8");
150
+ const match = envContent.match(/TETHER_API_KEY=\s*(tthr_([^_]+)_[0-9a-f]+)/);
151
+ if (!match) return void 0;
152
+ const prefix = match[2];
153
+ switch (prefix) {
154
+ case "dev":
155
+ return "development";
156
+ case "prod":
157
+ return "production";
158
+ default:
159
+ return prefix;
160
+ }
161
+ }
162
+ var DEFAULT_API_URL = "https://tether-api.strands.gg";
163
+ function getApiUrl(config) {
164
+ if (process.env.TETHER_API_URL) {
165
+ return process.env.TETHER_API_URL.replace(/\/+$/, "");
166
+ }
167
+ if (config?.url) {
168
+ return config.url.replace(/\/+$/, "");
169
+ }
170
+ const isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
171
+ return isDev3 ? "http://localhost:3001" : DEFAULT_API_URL;
172
+ }
173
+ function resolvePath(configPath, cwd = process.cwd()) {
174
+ const normalised = configPath.replace(/^\.\//, "");
175
+ return path2.resolve(cwd, normalised);
176
+ }
177
+ async function detectFramework(cwd = process.cwd()) {
178
+ const packageJsonPath = path2.resolve(cwd, "package.json");
179
+ if (!await fs2.pathExists(packageJsonPath)) {
180
+ return "unknown";
181
+ }
182
+ try {
183
+ const packageJson = await fs2.readJson(packageJsonPath);
184
+ const deps = {
185
+ ...packageJson.dependencies,
186
+ ...packageJson.devDependencies
187
+ };
188
+ if (deps.nuxt || deps["@nuxt/kit"]) {
189
+ return "nuxt";
190
+ }
191
+ if (deps.next) {
192
+ return "next";
193
+ }
194
+ if (deps["@sveltejs/kit"]) {
195
+ return "sveltekit";
196
+ }
197
+ if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
198
+ return "vite";
199
+ }
200
+ return "vanilla";
201
+ } catch {
202
+ return "unknown";
203
+ }
204
+ }
205
+ function getFrameworkDevCommand(framework) {
206
+ switch (framework) {
207
+ case "nuxt":
208
+ return "nuxt dev";
209
+ case "next":
210
+ return "next dev";
211
+ case "sveltekit":
212
+ return "vite dev";
213
+ case "vite":
214
+ return "vite dev";
215
+ case "vanilla":
216
+ return null;
217
+ // No default dev server for vanilla
218
+ default:
219
+ return null;
220
+ }
221
+ }
222
+
223
+ // src/commands/generate.ts
224
+ var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
225
+ var API_URL2 = isDev2 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
226
+ function parseSchemaFile(source) {
227
+ const tables = [];
228
+ const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
229
+ if (!schemaMatch) return tables;
230
+ const schemaContent = schemaMatch[1];
231
+ const tableStartRegex = /(\w+)\s*:\s*\{/g;
232
+ let match;
233
+ while ((match = tableStartRegex.exec(schemaContent)) !== null) {
234
+ const tableName = match[1];
235
+ const startOffset = match.index + match[0].length;
236
+ let braceCount = 1;
237
+ let endOffset = startOffset;
238
+ for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
239
+ const char = schemaContent[i];
240
+ if (char === "{") braceCount++;
241
+ else if (char === "}") braceCount--;
242
+ endOffset = i;
243
+ }
244
+ const columnsContent = schemaContent.slice(startOffset, endOffset);
245
+ const columns = parseColumns(columnsContent);
246
+ tables.push({ name: tableName, columns });
247
+ }
248
+ return tables;
249
+ }
250
+ function parseColumns(content) {
251
+ const columns = [];
252
+ const columnRegex = /(\w+)\s*:\s*(\w+)(?:<([^>]+)>)?\s*\(\s*\)((?:\[.*?\]|[^,\n}])*)/g;
253
+ let match;
254
+ while ((match = columnRegex.exec(content)) !== null) {
255
+ const name = match[1];
256
+ const schemaType = match[2];
257
+ const genericType = match[3];
258
+ const modifiers = match[4] || "";
259
+ const nullable = !modifiers.includes(".notNull()");
260
+ const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
261
+ let oneOf;
262
+ if (oneOfMatch) {
263
+ oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
264
+ }
265
+ columns.push({
266
+ name,
267
+ type: schemaTypeToTS(schemaType),
268
+ nullable,
269
+ jsonType: genericType,
270
+ // Store the generic type for json columns
271
+ oneOf
272
+ });
273
+ }
274
+ return columns;
275
+ }
276
+ function schemaTypeToTS(schemaType) {
277
+ switch (schemaType) {
278
+ case "text":
279
+ return "string";
280
+ case "integer":
281
+ return "number";
282
+ case "real":
283
+ return "number";
284
+ case "boolean":
285
+ return "boolean";
286
+ case "timestamp":
287
+ return "string";
288
+ case "json":
289
+ return "unknown";
290
+ case "blob":
291
+ return "Uint8Array";
292
+ case "asset":
293
+ return "TetherAsset";
294
+ // Asset object returned by API
295
+ default:
296
+ return "unknown";
297
+ }
298
+ }
299
+ function extractSingleType(source, typeName) {
300
+ const interfaceStart = new RegExp(`(?:export\\s+)?interface\\s+${typeName}\\s*\\{`);
301
+ const interfaceMatch = interfaceStart.exec(source);
302
+ if (interfaceMatch) {
303
+ const braceStart = interfaceMatch.index + interfaceMatch[0].length;
304
+ let braceCount = 1;
305
+ let endIdx = braceStart;
306
+ for (let i = braceStart; i < source.length && braceCount > 0; i++) {
307
+ if (source[i] === "{") braceCount++;
308
+ else if (source[i] === "}") braceCount--;
309
+ endIdx = i;
310
+ }
311
+ return source.slice(interfaceMatch.index, endIdx + 1).replace(/^export\s+/, "");
312
+ }
313
+ const typeStart = new RegExp(`(?:export\\s+)?type\\s+${typeName}\\s*=`);
314
+ const typeMatch = typeStart.exec(source);
315
+ if (typeMatch) {
316
+ const semiIdx = source.indexOf(";", typeMatch.index);
317
+ if (semiIdx !== -1) {
318
+ return source.slice(typeMatch.index, semiIdx + 1).replace(/^export\s+/, "");
319
+ }
320
+ }
321
+ return null;
322
+ }
323
+ function extractTypeDefinitions(source, typeNames) {
324
+ if (typeNames.size === 0) return [];
325
+ const allDefinedTypes = /* @__PURE__ */ new Set();
326
+ const typeDefRegex = /(?:export\s+)?(?:interface|type)\s+(\w+)/g;
327
+ let m;
328
+ while ((m = typeDefRegex.exec(source)) !== null) {
329
+ allDefinedTypes.add(m[1]);
330
+ }
331
+ const resolved = /* @__PURE__ */ new Map();
332
+ const queue = [...typeNames];
333
+ while (queue.length > 0) {
334
+ const name = queue.shift();
335
+ if (resolved.has(name)) continue;
336
+ const block = extractSingleType(source, name);
337
+ if (!block) continue;
338
+ resolved.set(name, block);
339
+ for (const definedType of allDefinedTypes) {
340
+ if (!resolved.has(definedType) && !queue.includes(definedType)) {
341
+ const refRegex = new RegExp(`\\b${definedType}\\b`);
342
+ if (refRegex.test(block)) {
343
+ queue.push(definedType);
344
+ }
345
+ }
346
+ }
347
+ }
348
+ const ordered = [];
349
+ const visited = /* @__PURE__ */ new Set();
350
+ function visit(name) {
351
+ if (visited.has(name)) return;
352
+ visited.add(name);
353
+ const block = resolved.get(name);
354
+ if (!block) return;
355
+ for (const dep of resolved.keys()) {
356
+ if (dep !== name && new RegExp(`\\b${dep}\\b`).test(block)) {
357
+ visit(dep);
358
+ }
359
+ }
360
+ ordered.push(name);
361
+ }
362
+ for (const name of resolved.keys()) {
363
+ visit(name);
364
+ }
365
+ return ordered.map((name) => resolved.get(name));
366
+ }
367
+ function tableNameToInterface(tableName) {
368
+ let name = tableName;
369
+ if (name.endsWith("ies")) {
370
+ name = name.slice(0, -3) + "y";
371
+ } else if (name.endsWith("s") && !name.endsWith("ss")) {
372
+ name = name.slice(0, -1);
373
+ }
374
+ return name.charAt(0).toUpperCase() + name.slice(1);
375
+ }
376
+ function generateDbFile(tables, schemaSource) {
377
+ const TS_PRIMITIVES = /* @__PURE__ */ new Set(["number", "string", "boolean", "unknown", "any", "void", "never", "undefined", "null", "object"]);
378
+ const jsonTypes = /* @__PURE__ */ new Set();
379
+ for (const table of tables) {
380
+ for (const col of table.columns) {
381
+ if (col.jsonType) {
382
+ const baseType = col.jsonType.replace(/\s*\|\s*null/g, "").replace(/\[\]/g, "").trim();
383
+ if (!TS_PRIMITIVES.has(baseType)) {
384
+ jsonTypes.add(baseType);
385
+ }
386
+ }
387
+ }
388
+ }
389
+ const lines = [
390
+ "// Auto-generated by Tether CLI - do not edit manually",
391
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
392
+ "",
393
+ "import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
394
+ "import {",
395
+ " type QueryDefinition,",
396
+ " type MutationDefinition,",
397
+ " z,",
398
+ "} from '@tthr/server';",
399
+ "import type { TetherEnv } from './env';"
400
+ ];
401
+ if (jsonTypes.size > 0) {
402
+ const typeBlocks = extractTypeDefinitions(schemaSource, jsonTypes);
403
+ if (typeBlocks.length > 0) {
404
+ lines.push("");
405
+ lines.push("// Schema types (inlined from schema.ts)");
406
+ for (const block of typeBlocks) {
407
+ lines.push(block);
408
+ lines.push("");
409
+ }
410
+ }
411
+ }
412
+ lines.push("");
413
+ lines.push("// Asset type returned by the API for asset columns");
414
+ lines.push("export interface TetherAsset {");
415
+ lines.push(" id: string;");
416
+ lines.push(" filename: string;");
417
+ lines.push(" contentType: string;");
418
+ lines.push(" size: number;");
419
+ lines.push(" url: string;");
420
+ lines.push(" createdAt: string;");
421
+ lines.push("}");
422
+ lines.push("");
423
+ for (const table of tables) {
424
+ const interfaceName = tableNameToInterface(table.name);
425
+ lines.push(`export interface ${interfaceName} {`);
426
+ lines.push(" _id: string;");
427
+ for (const col of table.columns) {
428
+ let colType;
429
+ if (col.oneOf && col.oneOf.length > 0) {
430
+ colType = col.oneOf.map((v) => `'${v}'`).join(" | ");
431
+ } else {
432
+ colType = col.jsonType || col.type;
433
+ }
434
+ const typeStr = col.nullable ? `${colType} | null` : colType;
435
+ const optional = col.nullable ? "?" : "";
436
+ lines.push(` ${col.name}${optional}: ${typeStr};`);
437
+ }
438
+ lines.push(" _createdAt: string;");
439
+ lines.push(" _updatedAt?: string | null;");
440
+ lines.push(" _deletedAt?: string | null;");
441
+ lines.push("}");
442
+ lines.push("");
443
+ }
444
+ lines.push("export interface Schema {");
445
+ for (const table of tables) {
446
+ const interfaceName = tableNameToInterface(table.name);
447
+ lines.push(` ${table.name}: ${interfaceName};`);
448
+ }
449
+ lines.push("}");
450
+ lines.push("");
451
+ lines.push("// Database client with typed tables");
452
+ lines.push("// This is a proxy that will be populated by the Tether runtime");
453
+ lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
454
+ lines.push("");
455
+ lines.push("// ============================================================================");
456
+ lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
457
+ lines.push("// This ensures the `db` and `env` parameters in handlers are properly typed");
458
+ lines.push("// ============================================================================");
459
+ lines.push("");
460
+ lines.push("/**");
461
+ lines.push(" * Define a query function with typed database and environment variable access.");
462
+ lines.push(" * The `db` and `env` parameters in the handler will have full type safety.");
463
+ lines.push(" */");
464
+ lines.push("export function query<TArgs = void, TResult = unknown>(");
465
+ lines.push(" definition: QueryDefinition<TArgs, TResult, Schema, TetherEnv>");
466
+ lines.push("): QueryDefinition<TArgs, TResult, Schema, TetherEnv> {");
467
+ lines.push(" return definition;");
468
+ lines.push("}");
469
+ lines.push("");
470
+ lines.push("/**");
471
+ lines.push(" * Define a mutation function with typed database and environment variable access.");
472
+ lines.push(" * The `db` and `env` parameters in the handler will have full type safety.");
473
+ lines.push(" */");
474
+ lines.push("export function mutation<TArgs = void, TResult = unknown>(");
475
+ lines.push(" definition: MutationDefinition<TArgs, TResult, Schema, TetherEnv>");
476
+ lines.push("): MutationDefinition<TArgs, TResult, Schema, TetherEnv> {");
477
+ lines.push(" return definition;");
478
+ lines.push("}");
479
+ lines.push("");
480
+ lines.push("// Re-export z for convenience");
481
+ lines.push("export { z };");
482
+ lines.push("");
483
+ return lines.join("\n");
484
+ }
485
+ async function parseFunctionsDir(functionsDir) {
486
+ const functions = [];
487
+ if (!await fs3.pathExists(functionsDir)) {
488
+ return functions;
489
+ }
490
+ const files = await fs3.readdir(functionsDir);
491
+ for (const file of files) {
492
+ if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
493
+ const filePath = path3.join(functionsDir, file);
494
+ const stat = await fs3.stat(filePath);
495
+ if (!stat.isFile()) continue;
496
+ const moduleName = file.replace(/\.(ts|js)$/, "");
497
+ const source = await fs3.readFile(filePath, "utf-8");
498
+ const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation)\s*\(/g;
499
+ let match;
500
+ while ((match = exportRegex.exec(source)) !== null) {
501
+ functions.push({
502
+ name: match[1],
503
+ moduleName
504
+ });
505
+ }
506
+ }
507
+ return functions;
508
+ }
509
+ async function generateApiFile(functionsDir) {
510
+ const functions = await parseFunctionsDir(functionsDir);
511
+ const moduleMap = /* @__PURE__ */ new Map();
512
+ for (const fn of functions) {
513
+ if (!moduleMap.has(fn.moduleName)) {
514
+ moduleMap.set(fn.moduleName, []);
515
+ }
516
+ moduleMap.get(fn.moduleName).push(fn.name);
517
+ }
518
+ const lines = [
519
+ "// Auto-generated by Tether CLI - do not edit manually",
520
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
521
+ "",
522
+ "import { createApiProxy } from '@tthr/client';",
523
+ "",
524
+ "/**",
525
+ " * API function reference type for useQuery/useMutation.",
526
+ ' * The _name property contains the function path (e.g., "users.list").',
527
+ " */",
528
+ "export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
529
+ " _name: string;",
530
+ " _args?: TArgs;",
531
+ " _result?: TResult;",
532
+ "}",
533
+ ""
534
+ ];
535
+ if (moduleMap.size > 0) {
536
+ for (const [moduleName, fnNames] of moduleMap) {
537
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
538
+ lines.push(`export interface ${interfaceName} {`);
539
+ for (const fnName of fnNames) {
540
+ lines.push(` ${fnName}: ApiFunction;`);
541
+ }
542
+ lines.push("}");
543
+ lines.push("");
544
+ }
545
+ lines.push("export interface Api {");
546
+ for (const moduleName of moduleMap.keys()) {
547
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
548
+ lines.push(` ${moduleName}: ${interfaceName};`);
549
+ }
550
+ lines.push("}");
551
+ } else {
552
+ lines.push("/**");
553
+ lines.push(" * Flexible API type that allows access to any function path.");
554
+ lines.push(" * Functions are accessed as api.moduleName.functionName");
555
+ lines.push(" * The actual types depend on your function definitions.");
556
+ lines.push(" */");
557
+ lines.push("export type Api = {");
558
+ lines.push(" [module: string]: {");
559
+ lines.push(" [fn: string]: ApiFunction;");
560
+ lines.push(" };");
561
+ lines.push("};");
562
+ }
563
+ lines.push("");
564
+ lines.push("// API client proxy - provides typed access to your functions");
565
+ lines.push("// On the client: returns { _name } references for useQuery/useMutation");
566
+ lines.push("// In Tether functions: calls the actual function implementation");
567
+ lines.push("export const api = createApiProxy<Api>();");
568
+ lines.push("");
569
+ return lines.join("\n");
570
+ }
571
+ async function fetchEnvVarKeys(projectId, environment) {
572
+ const credentials = await getCredentials();
573
+ if (!credentials) return [];
574
+ const envPath = environment !== "production" ? `/projects/${projectId}/env/${environment}/env-vars` : `/projects/${projectId}/env-vars`;
575
+ try {
576
+ const response = await fetch(`${API_URL2}${envPath}`, {
577
+ headers: {
578
+ "Authorization": `Bearer ${credentials.accessToken}`
579
+ }
580
+ });
581
+ if (!response.ok) return [];
582
+ const data = await response.json();
583
+ return (data.envVars || []).map((v) => v.key).sort();
584
+ } catch {
585
+ return [];
586
+ }
587
+ }
588
+ function generateEnvFile(keys) {
589
+ const lines = [
590
+ "// Auto-generated by Tether CLI - do not edit manually",
591
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
592
+ "",
593
+ "/**",
594
+ " * Typed environment variables for this project.",
595
+ " * Keys are fetched from the Tether API \u2014 values are never exposed.",
596
+ ' * Use `env.get("KEY")` in your functions to access them.',
597
+ " */",
598
+ ""
599
+ ];
600
+ if (keys.length > 0) {
601
+ lines.push("export type EnvVarKey =");
602
+ for (let i = 0; i < keys.length; i++) {
603
+ const sep = i === keys.length - 1 ? ";" : "";
604
+ lines.push(` | '${keys[i]}'${sep}`);
605
+ }
606
+ lines.push("");
607
+ lines.push("export interface TetherEnv {");
608
+ for (const key of keys) {
609
+ lines.push(` ${key}: string;`);
610
+ }
611
+ lines.push("}");
612
+ } else {
613
+ lines.push("export type EnvVarKey = string;");
614
+ lines.push("");
615
+ lines.push("export interface TetherEnv {");
616
+ lines.push(" [key: string]: string;");
617
+ lines.push("}");
618
+ }
619
+ lines.push("");
620
+ return lines.join("\n");
621
+ }
622
+ async function generateTypes(options = {}) {
623
+ const config = await loadConfig();
624
+ const schemaPath = resolvePath(config.schema);
625
+ const outputDir = resolvePath(config.output);
626
+ const functionsDir = resolvePath(config.functions);
627
+ if (!await fs3.pathExists(schemaPath)) {
628
+ throw new Error(`Schema file not found: ${schemaPath}`);
629
+ }
630
+ const schemaSource = await fs3.readFile(schemaPath, "utf-8");
631
+ const tables = parseSchemaFile(schemaSource);
632
+ const environment = config.environment || await resolveEnvironmentFromApiKey() || "development";
633
+ let projectId;
634
+ const envFilePath = path3.resolve(process.cwd(), ".env");
635
+ if (await fs3.pathExists(envFilePath)) {
636
+ const envContent = await fs3.readFile(envFilePath, "utf-8");
637
+ const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
638
+ projectId = match?.[1]?.trim();
639
+ }
640
+ const envVarKeys = projectId ? await fetchEnvVarKeys(projectId, environment) : [];
641
+ await fs3.ensureDir(outputDir);
642
+ await fs3.writeFile(
643
+ path3.join(outputDir, "db.ts"),
644
+ generateDbFile(tables, schemaSource)
645
+ );
646
+ await fs3.writeFile(
647
+ path3.join(outputDir, "api.ts"),
648
+ await generateApiFile(functionsDir)
649
+ );
650
+ await fs3.writeFile(
651
+ path3.join(outputDir, "env.ts"),
652
+ generateEnvFile(envVarKeys)
653
+ );
654
+ await fs3.writeFile(
655
+ path3.join(outputDir, "index.ts"),
656
+ `// Auto-generated by Tether CLI - do not edit manually
657
+ export * from './db';
658
+ export * from './api';
659
+ export * from './env';
660
+ `
661
+ );
662
+ return { tables, envVarKeys, outputDir };
663
+ }
664
+ async function generateCommand() {
665
+ await requireAuth();
666
+ const configPath = path3.resolve(process.cwd(), "tether.config.ts");
667
+ if (!await fs3.pathExists(configPath)) {
668
+ console.log(chalk2.red("\nError: Not a Tether project"));
669
+ console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
670
+ process.exit(1);
671
+ }
672
+ console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
673
+ const spinner = ora("Reading schema...").start();
674
+ try {
675
+ spinner.text = "Generating types...";
676
+ const { tables, envVarKeys, outputDir } = await generateTypes();
677
+ if (tables.length === 0) {
678
+ spinner.warn("No tables found in schema");
679
+ console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
680
+ return;
681
+ }
682
+ spinner.succeed(`Types generated for ${tables.length} table(s)`);
683
+ console.log("\n" + chalk2.green("\u2713") + " Tables:");
684
+ for (const table of tables) {
685
+ console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
686
+ }
687
+ if (envVarKeys.length > 0) {
688
+ console.log("\n" + chalk2.green("\u2713") + ` Environment variables: ${envVarKeys.length} key(s)`);
689
+ for (const key of envVarKeys) {
690
+ console.log(chalk2.dim(` - ${key}`));
691
+ }
692
+ }
693
+ const relativeOutput = path3.relative(process.cwd(), outputDir);
694
+ console.log("\n" + chalk2.green("\u2713") + " Generated files:");
695
+ console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
696
+ console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
697
+ console.log(chalk2.dim(` ${relativeOutput}/env.ts`));
698
+ console.log(chalk2.dim(` ${relativeOutput}/index.ts
699
+ `));
700
+ } catch (error) {
701
+ spinner.fail("Failed to generate types");
702
+ console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
703
+ process.exit(1);
704
+ }
705
+ }
706
+
707
+ export {
708
+ API_URL,
709
+ getCredentials,
710
+ requireAuth,
711
+ saveCredentials,
712
+ clearCredentials,
713
+ loadConfig,
714
+ resolveEnvironmentFromApiKey,
715
+ getApiUrl,
716
+ resolvePath,
717
+ detectFramework,
718
+ getFrameworkDevCommand,
719
+ generateTypes,
720
+ generateCommand
721
+ };
@@ -0,0 +1,8 @@
1
+ import {
2
+ generateCommand,
3
+ generateTypes
4
+ } from "./chunk-2QPN6NDY.js";
5
+ export {
6
+ generateCommand,
7
+ generateTypes
8
+ };
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  detectFramework,
6
6
  generateCommand,
7
7
  generateTypes,
8
+ getApiUrl,
8
9
  getCredentials,
9
10
  getFrameworkDevCommand,
10
11
  loadConfig,
@@ -12,7 +13,7 @@ import {
12
13
  resolveEnvironmentFromApiKey,
13
14
  resolvePath,
14
15
  saveCredentials
15
- } from "./chunk-QNUE3MEY.js";
16
+ } from "./chunk-2QPN6NDY.js";
16
17
 
17
18
  // src/index.ts
18
19
  import { Command } from "commander";
@@ -34,8 +35,6 @@ import ora from "ora";
34
35
  import fs from "fs-extra";
35
36
  import path from "path";
36
37
  import * as esbuild from "esbuild";
37
- var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
38
- var API_URL2 = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
39
38
  async function deployCommand(options) {
40
39
  const credentials = await requireAuth();
41
40
  const configPath = path.resolve(process.cwd(), "tether.config.ts");
@@ -60,10 +59,11 @@ async function deployCommand(options) {
60
59
  process.exit(1);
61
60
  }
62
61
  const environment = options.env || config.environment || await resolveEnvironmentFromApiKey() || "development";
62
+ const API_URL4 = `${getApiUrl(config)}/api/v1`;
63
63
  console.log(chalk.bold("\n\u26A1 Deploying to Tether\n"));
64
64
  console.log(chalk.dim(` Project: ${projectId}`));
65
65
  console.log(chalk.dim(` Environment: ${environment}`));
66
- console.log(chalk.dim(` API: ${API_URL2}
66
+ console.log(chalk.dim(` API: ${API_URL4}
67
67
  `));
68
68
  console.log(chalk.dim(" Generating types..."));
69
69
  try {
@@ -84,7 +84,8 @@ async function deployCommand(options) {
84
84
  }
85
85
  console.log(chalk.green("\n\u2713 Deployment complete\n"));
86
86
  }
87
- async function deploySchemaToServer(projectId, token, schemaPath, environment, dryRun) {
87
+ async function deploySchemaToServer(projectId, token, schemaPath, environment, dryRun, apiUrl) {
88
+ const API_URL4 = apiUrl || `${getApiUrl()}/api/v1`;
88
89
  const spinner = ora("Reading schema...").start();
89
90
  try {
90
91
  if (!await fs.pathExists(schemaPath)) {
@@ -114,7 +115,7 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
114
115
  return;
115
116
  }
116
117
  spinner.text = "Deploying schema...";
117
- const schemaUrl = `${API_URL2}/projects/${projectId}/env/${environment}/deploy/schema`;
118
+ const schemaUrl = `${API_URL4}/projects/${projectId}/env/${environment}/deploy/schema`;
118
119
  console.log(chalk.dim(`
119
120
  URL: ${schemaUrl}`));
120
121
  const rlsConfigs = tables.filter((t) => t.rls).map((t) => ({
@@ -188,7 +189,8 @@ ${messages.join("\n")}`);
188
189
  }
189
190
  return result.outputFiles[0].text;
190
191
  }
191
- async function deployFunctionsToServer(projectId, token, functionsDir, environment, dryRun) {
192
+ async function deployFunctionsToServer(projectId, token, functionsDir, environment, dryRun, apiUrl) {
193
+ const API_URL4 = apiUrl || `${getApiUrl()}/api/v1`;
192
194
  const spinner = ora("Reading functions...").start();
193
195
  try {
194
196
  if (!await fs.pathExists(functionsDir)) {
@@ -200,10 +202,16 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
200
202
  }
201
203
  const files = await fs.readdir(functionsDir);
202
204
  const tsFiles = files.filter((f) => f.endsWith(".ts") && !f.startsWith("_"));
205
+ const helperFiles = files.filter((f) => f.endsWith(".ts") && f.startsWith("_"));
203
206
  if (tsFiles.length === 0) {
204
207
  spinner.info("No function files found");
205
208
  return;
206
209
  }
210
+ const helperSources = /* @__PURE__ */ new Map();
211
+ for (const hf of helperFiles) {
212
+ const hfPath = path.join(functionsDir, hf);
213
+ helperSources.set(hf, await fs.readFile(hfPath, "utf-8"));
214
+ }
207
215
  const functions = [];
208
216
  for (const file of tsFiles) {
209
217
  const filePath = path.join(functionsDir, file);
@@ -213,9 +221,19 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
213
221
  if (parsedFunctions.length === 0) continue;
214
222
  spinner.text = `Bundling ${file}...`;
215
223
  const bundledSource = await bundleFunctionFile(filePath);
224
+ let combinedRawSource = source;
225
+ for (const [helperFile, helperSource] of helperSources) {
226
+ const helperBase = helperFile.replace(".ts", "");
227
+ if (source.includes(`"./${helperBase}"`) || source.includes(`'./${helperBase}'`)) {
228
+ combinedRawSource += `
229
+
230
+ // --- ${helperFile} ---
231
+ ${helperSource}`;
232
+ }
233
+ }
216
234
  for (const fn of parsedFunctions) {
217
235
  fn.source = bundledSource;
218
- fn.rawSource = source;
236
+ fn.rawSource = combinedRawSource;
219
237
  }
220
238
  functions.push(...parsedFunctions);
221
239
  }
@@ -229,7 +247,7 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
229
247
  return;
230
248
  }
231
249
  spinner.text = "Deploying functions...";
232
- const functionsUrl = `${API_URL2}/projects/${projectId}/env/${environment}/deploy/functions`;
250
+ const functionsUrl = `${API_URL4}/projects/${projectId}/env/${environment}/deploy/functions`;
233
251
  console.log(chalk.dim(`
234
252
  URL: ${functionsUrl}`));
235
253
  const response = await fetch(functionsUrl, {
@@ -567,8 +585,8 @@ function parseFunctions(moduleName, source) {
567
585
  }
568
586
 
569
587
  // src/commands/init.ts
570
- var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
571
- var API_URL3 = isDev2 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
588
+ var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
589
+ var API_URL2 = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
572
590
  async function getLatestVersion(packageName) {
573
591
  try {
574
592
  const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
@@ -670,7 +688,7 @@ ${packageManager} is not installed on your system.`));
670
688
  await scaffoldVanillaProject(projectName, projectPath, spinner);
671
689
  }
672
690
  spinner.text = "Creating project on Tether...";
673
- const response = await fetch(`${API_URL3}/projects`, {
691
+ const response = await fetch(`${API_URL2}/projects`, {
674
692
  method: "POST",
675
693
  headers: {
676
694
  "Content-Type": "application/json",
@@ -1227,8 +1245,8 @@ PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
1227
1245
  }
1228
1246
  }
1229
1247
  }
1230
- function getInstallCommand(pm, isDev7 = false) {
1231
- const devFlag = isDev7 ? pm === "npm" ? "-D" : pm === "yarn" ? "-D" : pm === "pnpm" ? "-D" : "-d" : "";
1248
+ function getInstallCommand(pm, isDev3 = false) {
1249
+ const devFlag = isDev3 ? pm === "npm" ? "-D" : pm === "yarn" ? "-D" : pm === "pnpm" ? "-D" : "-d" : "";
1232
1250
  return `${pm} ${pm === "npm" ? "install" : "add"} ${devFlag}`.trim();
1233
1251
  }
1234
1252
  async function installTetherPackages(projectPath, template, packageManager) {
@@ -1673,8 +1691,6 @@ import fs3 from "fs-extra";
1673
1691
  import path3 from "path";
1674
1692
  import { spawn } from "child_process";
1675
1693
  import { watch } from "chokidar";
1676
- var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
1677
- var API_BASE = isDev3 ? "http://localhost:3001" : "https://tether-api.strands.gg";
1678
1694
  var frameworkProcess = null;
1679
1695
  var schemaWatcher = null;
1680
1696
  var functionsWatcher = null;
@@ -1690,7 +1706,7 @@ async function runGenerate(spinner) {
1690
1706
  }
1691
1707
  isGenerating = true;
1692
1708
  try {
1693
- const { generateTypes: generateTypes2 } = await import("./generate-DYQ67MHG.js");
1709
+ const { generateTypes: generateTypes2 } = await import("./generate-KAPPABOL.js");
1694
1710
  spinner.text = "Regenerating types...";
1695
1711
  await generateTypes2({ silent: true });
1696
1712
  spinner.succeed("Types regenerated");
@@ -1797,8 +1813,9 @@ async function validateFunctions(functionsDir) {
1797
1813
  }
1798
1814
  return { valid: errors.length === 0, errors };
1799
1815
  }
1800
- function connectToEnvironment(projectId, environment, token, spinner) {
1801
- const wsBase = API_BASE.replace("https://", "wss://").replace("http://", "ws://");
1816
+ function connectToEnvironment(projectId, environment, token, spinner, apiBase) {
1817
+ const base = apiBase || getApiUrl();
1818
+ const wsBase = base.replace("https://", "wss://").replace("http://", "ws://");
1802
1819
  const wsUrl = environment && environment !== "production" ? `${wsBase}/ws/${projectId}/${environment}?token=${encodeURIComponent(token)}` : `${wsBase}/ws/${projectId}?token=${encodeURIComponent(token)}`;
1803
1820
  let reconnectAttempts = 0;
1804
1821
  const maxReconnectAttempts = 10;
@@ -1960,7 +1977,7 @@ async function devCommand(options) {
1960
1977
  }
1961
1978
  }
1962
1979
  spinner.text = "Generating types...";
1963
- const { generateTypes: generateTypes2 } = await import("./generate-DYQ67MHG.js");
1980
+ const { generateTypes: generateTypes2 } = await import("./generate-KAPPABOL.js");
1964
1981
  await generateTypes2({ silent: true });
1965
1982
  spinner.succeed("Types generated");
1966
1983
  if (config.projectId) {
@@ -2017,7 +2034,7 @@ async function devCommand(options) {
2017
2034
  }
2018
2035
  spinner.succeed("File watchers ready");
2019
2036
  if (config.projectId) {
2020
- connectToEnvironment(config.projectId, environment, credentials.accessToken, spinner);
2037
+ connectToEnvironment(config.projectId, environment, credentials.accessToken, spinner, getApiUrl(config));
2021
2038
  }
2022
2039
  if (devCmd && !options.skipFramework) {
2023
2040
  spinner.start(`Starting ${framework} dev server...`);
@@ -2045,9 +2062,9 @@ import ora4 from "ora";
2045
2062
  import os from "os";
2046
2063
  import { exec } from "child_process";
2047
2064
  import readline from "readline";
2048
- var isDev4 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
2049
- var API_URL4 = isDev4 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
2050
- var AUTH_URL = isDev4 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
2065
+ var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
2066
+ var API_URL3 = `${getApiUrl()}/api/v1`;
2067
+ var AUTH_URL = isDev2 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
2051
2068
  async function loginCommand() {
2052
2069
  console.log(chalk4.bold("\u26A1 Login to Tether\n"));
2053
2070
  const existing = await getCredentials();
@@ -2136,7 +2153,7 @@ async function requestDeviceCode() {
2136
2153
  const userCode = generateUserCode();
2137
2154
  const deviceCode = crypto.randomUUID();
2138
2155
  const deviceName = os.hostname();
2139
- const response = await fetch(`${API_URL4}/auth/device`, {
2156
+ const response = await fetch(`${API_URL3}/auth/device`, {
2140
2157
  method: "POST",
2141
2158
  headers: { "Content-Type": "application/json" },
2142
2159
  body: JSON.stringify({ userCode, deviceCode, deviceName })
@@ -2170,7 +2187,7 @@ async function pollForApproval(deviceCode, interval, expiresIn) {
2170
2187
  while (Date.now() < expiresAt) {
2171
2188
  await sleep(interval * 1e3);
2172
2189
  spinner.text = `Checking... ${chalk4.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
2173
- const response = await fetch(`${API_URL4}/auth/device/${deviceCode}`, {
2190
+ const response = await fetch(`${API_URL3}/auth/device/${deviceCode}`, {
2174
2191
  method: "GET"
2175
2192
  }).catch(() => null);
2176
2193
  updateCountdown();
@@ -2262,18 +2279,18 @@ function detectPackageManager() {
2262
2279
  }
2263
2280
  return "npm";
2264
2281
  }
2265
- function getInstallCommand2(pm, packages, isDev7) {
2282
+ function getInstallCommand2(pm, packages, isDev3) {
2266
2283
  const packagesStr = packages.join(" ");
2267
2284
  switch (pm) {
2268
2285
  case "bun":
2269
- return isDev7 ? `bun add -d ${packagesStr}` : `bun add ${packagesStr}`;
2286
+ return isDev3 ? `bun add -d ${packagesStr}` : `bun add ${packagesStr}`;
2270
2287
  case "pnpm":
2271
- return isDev7 ? `pnpm add -D ${packagesStr}` : `pnpm add ${packagesStr}`;
2288
+ return isDev3 ? `pnpm add -D ${packagesStr}` : `pnpm add ${packagesStr}`;
2272
2289
  case "yarn":
2273
- return isDev7 ? `yarn add -D ${packagesStr}` : `yarn add ${packagesStr}`;
2290
+ return isDev3 ? `yarn add -D ${packagesStr}` : `yarn add ${packagesStr}`;
2274
2291
  case "npm":
2275
2292
  default:
2276
- return isDev7 ? `npm install -D ${packagesStr}` : `npm install ${packagesStr}`;
2293
+ return isDev3 ? `npm install -D ${packagesStr}` : `npm install ${packagesStr}`;
2277
2294
  }
2278
2295
  }
2279
2296
  async function getLatestVersion2(packageName) {
@@ -2382,8 +2399,6 @@ import chalk6 from "chalk";
2382
2399
  import ora6 from "ora";
2383
2400
  import fs5 from "fs-extra";
2384
2401
  import path5 from "path";
2385
- var isDev5 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
2386
- var API_URL5 = isDev5 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
2387
2402
  async function execCommand(sql) {
2388
2403
  if (!sql || sql.trim() === "") {
2389
2404
  console.log(chalk6.red("\nError: SQL query required"));
@@ -2403,9 +2418,11 @@ async function execCommand(sql) {
2403
2418
  console.log(chalk6.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
2404
2419
  process.exit(1);
2405
2420
  }
2421
+ const config = await loadConfig();
2422
+ const API_URL4 = `${getApiUrl(config)}/api/v1`;
2406
2423
  const spinner = ora6("Executing query...").start();
2407
2424
  try {
2408
- const response = await fetch(`${API_URL5}/projects/${projectId}/exec`, {
2425
+ const response = await fetch(`${API_URL4}/projects/${projectId}/exec`, {
2409
2426
  method: "POST",
2410
2427
  headers: {
2411
2428
  "Content-Type": "application/json",
@@ -2455,8 +2472,10 @@ import chalk7 from "chalk";
2455
2472
  import ora7 from "ora";
2456
2473
  import fs6 from "fs-extra";
2457
2474
  import path6 from "path";
2458
- var isDev6 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
2459
- var API_URL6 = isDev6 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
2475
+ async function resolveApiUrl() {
2476
+ const config = await loadConfig();
2477
+ return `${getApiUrl(config)}/api/v1`;
2478
+ }
2460
2479
  async function getProjectId() {
2461
2480
  const envPath = path6.resolve(process.cwd(), ".env");
2462
2481
  let projectId;
@@ -2475,9 +2494,10 @@ async function getProjectId() {
2475
2494
  async function envListCommand() {
2476
2495
  const credentials = await requireAuth();
2477
2496
  const projectId = await getProjectId();
2497
+ const API_URL4 = await resolveApiUrl();
2478
2498
  const spinner = ora7("Fetching environments...").start();
2479
2499
  try {
2480
- const response = await fetch(`${API_URL6}/projects/${projectId}/environments`, {
2500
+ const response = await fetch(`${API_URL4}/projects/${projectId}/environments`, {
2481
2501
  headers: {
2482
2502
  "Authorization": `Bearer ${credentials.accessToken}`
2483
2503
  }
@@ -2507,6 +2527,7 @@ async function envListCommand() {
2507
2527
  async function envCreateCommand(name, options) {
2508
2528
  const credentials = await requireAuth();
2509
2529
  const projectId = await getProjectId();
2530
+ const API_URL4 = await resolveApiUrl();
2510
2531
  const normalizedName = name.toLowerCase();
2511
2532
  if (!/^[a-z][a-z0-9-_]*$/.test(normalizedName)) {
2512
2533
  console.log(chalk7.red("\nError: Invalid environment name"));
@@ -2520,7 +2541,7 @@ async function envCreateCommand(name, options) {
2520
2541
  body.cloneFrom = options.from;
2521
2542
  spinner.text = `Creating environment '${normalizedName}' from '${options.from}'...`;
2522
2543
  }
2523
- const response = await fetch(`${API_URL6}/projects/${projectId}/environments`, {
2544
+ const response = await fetch(`${API_URL4}/projects/${projectId}/environments`, {
2524
2545
  method: "POST",
2525
2546
  headers: {
2526
2547
  "Content-Type": "application/json",
@@ -2549,9 +2570,10 @@ async function envCreateCommand(name, options) {
2549
2570
  async function envDeleteCommand(name) {
2550
2571
  const credentials = await requireAuth();
2551
2572
  const projectId = await getProjectId();
2573
+ const API_URL4 = await resolveApiUrl();
2552
2574
  const spinner = ora7(`Deleting environment '${name}'...`).start();
2553
2575
  try {
2554
- const response = await fetch(`${API_URL6}/projects/${projectId}/environments/${name}`, {
2576
+ const response = await fetch(`${API_URL4}/projects/${projectId}/environments/${name}`, {
2555
2577
  method: "DELETE",
2556
2578
  headers: {
2557
2579
  "Authorization": `Bearer ${credentials.accessToken}`
@@ -2571,9 +2593,10 @@ async function envDeleteCommand(name) {
2571
2593
  async function envDefaultCommand(name) {
2572
2594
  const credentials = await requireAuth();
2573
2595
  const projectId = await getProjectId();
2596
+ const API_URL4 = await resolveApiUrl();
2574
2597
  const spinner = ora7(`Setting '${name}' as default environment...`).start();
2575
2598
  try {
2576
- const response = await fetch(`${API_URL6}/projects/${projectId}/environments/default`, {
2599
+ const response = await fetch(`${API_URL4}/projects/${projectId}/environments/default`, {
2577
2600
  method: "PATCH",
2578
2601
  headers: {
2579
2602
  "Content-Type": "application/json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tthr",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Tether CLI - project scaffolding and deployment",
5
5
  "type": "module",
6
6
  "bin": {