tthr 0.0.34 → 0.0.35
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-LXODXQ5V.js +418 -0
- package/dist/generate-YQ4QRPXF.js +8 -0
- package/dist/index.js +228 -28
- package/package.json +1 -1
|
@@ -0,0 +1,418 @@
|
|
|
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
|
+
async function getCredentials() {
|
|
14
|
+
try {
|
|
15
|
+
if (await fs.pathExists(CREDENTIALS_FILE)) {
|
|
16
|
+
return await fs.readJSON(CREDENTIALS_FILE);
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
async function requireAuth() {
|
|
23
|
+
const credentials = await getCredentials();
|
|
24
|
+
if (!credentials) {
|
|
25
|
+
console.error(chalk.red("\n\u2717 Not logged in\n"));
|
|
26
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (credentials.expiresAt) {
|
|
30
|
+
const expiresAt = new Date(credentials.expiresAt);
|
|
31
|
+
if (/* @__PURE__ */ new Date() > expiresAt) {
|
|
32
|
+
console.error(chalk.red("\n\u2717 Session expired\n"));
|
|
33
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return credentials;
|
|
38
|
+
}
|
|
39
|
+
async function saveCredentials(credentials) {
|
|
40
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
41
|
+
await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
|
|
42
|
+
}
|
|
43
|
+
async function clearCredentials() {
|
|
44
|
+
try {
|
|
45
|
+
await fs.remove(CREDENTIALS_FILE);
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/utils/config.ts
|
|
51
|
+
import fs2 from "fs-extra";
|
|
52
|
+
import path2 from "path";
|
|
53
|
+
var DEFAULT_CONFIG = {
|
|
54
|
+
schema: "./tether/schema.ts",
|
|
55
|
+
functions: "./tether/functions",
|
|
56
|
+
output: "./tether/_generated",
|
|
57
|
+
dev: {
|
|
58
|
+
port: 3001,
|
|
59
|
+
host: "localhost"
|
|
60
|
+
},
|
|
61
|
+
database: {
|
|
62
|
+
walMode: true
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
66
|
+
const configPath = path2.resolve(cwd, "tether.config.ts");
|
|
67
|
+
if (!await fs2.pathExists(configPath)) {
|
|
68
|
+
return DEFAULT_CONFIG;
|
|
69
|
+
}
|
|
70
|
+
const configSource = await fs2.readFile(configPath, "utf-8");
|
|
71
|
+
const config = {};
|
|
72
|
+
const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
|
|
73
|
+
if (schemaMatch) {
|
|
74
|
+
config.schema = schemaMatch[1];
|
|
75
|
+
}
|
|
76
|
+
const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
|
|
77
|
+
if (functionsMatch) {
|
|
78
|
+
config.functions = functionsMatch[1];
|
|
79
|
+
}
|
|
80
|
+
const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
|
|
81
|
+
if (outputMatch) {
|
|
82
|
+
config.output = outputMatch[1];
|
|
83
|
+
}
|
|
84
|
+
const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
|
|
85
|
+
if (envMatch) {
|
|
86
|
+
config.environment = envMatch[1];
|
|
87
|
+
}
|
|
88
|
+
const portMatch = configSource.match(/port\s*:\s*(\d+)/);
|
|
89
|
+
if (portMatch) {
|
|
90
|
+
config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
|
|
91
|
+
}
|
|
92
|
+
const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
|
|
93
|
+
if (hostMatch) {
|
|
94
|
+
config.dev = { ...config.dev, host: hostMatch[1] };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
...DEFAULT_CONFIG,
|
|
98
|
+
...config,
|
|
99
|
+
dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
|
|
100
|
+
database: { ...DEFAULT_CONFIG.database, ...config.database }
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function resolvePath(configPath, cwd = process.cwd()) {
|
|
104
|
+
const normalised = configPath.replace(/^\.\//, "");
|
|
105
|
+
return path2.resolve(cwd, normalised);
|
|
106
|
+
}
|
|
107
|
+
async function detectFramework(cwd = process.cwd()) {
|
|
108
|
+
const packageJsonPath = path2.resolve(cwd, "package.json");
|
|
109
|
+
if (!await fs2.pathExists(packageJsonPath)) {
|
|
110
|
+
return "unknown";
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const packageJson = await fs2.readJson(packageJsonPath);
|
|
114
|
+
const deps = {
|
|
115
|
+
...packageJson.dependencies,
|
|
116
|
+
...packageJson.devDependencies
|
|
117
|
+
};
|
|
118
|
+
if (deps.nuxt || deps["@nuxt/kit"]) {
|
|
119
|
+
return "nuxt";
|
|
120
|
+
}
|
|
121
|
+
if (deps.next) {
|
|
122
|
+
return "next";
|
|
123
|
+
}
|
|
124
|
+
if (deps["@sveltejs/kit"]) {
|
|
125
|
+
return "sveltekit";
|
|
126
|
+
}
|
|
127
|
+
if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
|
|
128
|
+
return "vite";
|
|
129
|
+
}
|
|
130
|
+
return "vanilla";
|
|
131
|
+
} catch {
|
|
132
|
+
return "unknown";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function getFrameworkDevCommand(framework) {
|
|
136
|
+
switch (framework) {
|
|
137
|
+
case "nuxt":
|
|
138
|
+
return "nuxt dev";
|
|
139
|
+
case "next":
|
|
140
|
+
return "next dev";
|
|
141
|
+
case "sveltekit":
|
|
142
|
+
return "vite dev";
|
|
143
|
+
case "vite":
|
|
144
|
+
return "vite dev";
|
|
145
|
+
case "vanilla":
|
|
146
|
+
return null;
|
|
147
|
+
// No default dev server for vanilla
|
|
148
|
+
default:
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/commands/generate.ts
|
|
154
|
+
function parseSchemaFile(source) {
|
|
155
|
+
const tables = [];
|
|
156
|
+
const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
|
|
157
|
+
if (!schemaMatch) return tables;
|
|
158
|
+
const schemaContent = schemaMatch[1];
|
|
159
|
+
const tableStartRegex = /(\w+)\s*:\s*\{/g;
|
|
160
|
+
let match;
|
|
161
|
+
while ((match = tableStartRegex.exec(schemaContent)) !== null) {
|
|
162
|
+
const tableName = match[1];
|
|
163
|
+
const startOffset = match.index + match[0].length;
|
|
164
|
+
let braceCount = 1;
|
|
165
|
+
let endOffset = startOffset;
|
|
166
|
+
for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
|
|
167
|
+
const char = schemaContent[i];
|
|
168
|
+
if (char === "{") braceCount++;
|
|
169
|
+
else if (char === "}") braceCount--;
|
|
170
|
+
endOffset = i;
|
|
171
|
+
}
|
|
172
|
+
const columnsContent = schemaContent.slice(startOffset, endOffset);
|
|
173
|
+
const columns = parseColumns(columnsContent);
|
|
174
|
+
tables.push({ name: tableName, columns });
|
|
175
|
+
}
|
|
176
|
+
return tables;
|
|
177
|
+
}
|
|
178
|
+
function parseColumns(content) {
|
|
179
|
+
const columns = [];
|
|
180
|
+
const columnRegex = /(\w+)\s*:\s*(\w+)\s*\(\s*\)([^,\n}]*)/g;
|
|
181
|
+
let match;
|
|
182
|
+
while ((match = columnRegex.exec(content)) !== null) {
|
|
183
|
+
const name = match[1];
|
|
184
|
+
const schemaType = match[2];
|
|
185
|
+
const modifiers = match[3] || "";
|
|
186
|
+
const nullable = !modifiers.includes(".notNull()") && !modifiers.includes(".primaryKey()");
|
|
187
|
+
const primaryKey = modifiers.includes(".primaryKey()");
|
|
188
|
+
columns.push({
|
|
189
|
+
name,
|
|
190
|
+
type: schemaTypeToTS(schemaType),
|
|
191
|
+
nullable,
|
|
192
|
+
primaryKey
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return columns;
|
|
196
|
+
}
|
|
197
|
+
function schemaTypeToTS(schemaType) {
|
|
198
|
+
switch (schemaType) {
|
|
199
|
+
case "text":
|
|
200
|
+
return "string";
|
|
201
|
+
case "integer":
|
|
202
|
+
return "number";
|
|
203
|
+
case "real":
|
|
204
|
+
return "number";
|
|
205
|
+
case "boolean":
|
|
206
|
+
return "boolean";
|
|
207
|
+
case "timestamp":
|
|
208
|
+
return "string";
|
|
209
|
+
case "json":
|
|
210
|
+
return "unknown";
|
|
211
|
+
case "blob":
|
|
212
|
+
return "Uint8Array";
|
|
213
|
+
default:
|
|
214
|
+
return "unknown";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function tableNameToInterface(tableName) {
|
|
218
|
+
let name = tableName;
|
|
219
|
+
if (name.endsWith("ies")) {
|
|
220
|
+
name = name.slice(0, -3) + "y";
|
|
221
|
+
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
222
|
+
name = name.slice(0, -1);
|
|
223
|
+
}
|
|
224
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
225
|
+
}
|
|
226
|
+
function generateDbFile(tables) {
|
|
227
|
+
const lines = [
|
|
228
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
229
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
230
|
+
"",
|
|
231
|
+
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
232
|
+
""
|
|
233
|
+
];
|
|
234
|
+
for (const table of tables) {
|
|
235
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
236
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
237
|
+
for (const col of table.columns) {
|
|
238
|
+
const typeStr = col.nullable ? `${col.type} | null` : col.type;
|
|
239
|
+
lines.push(` ${col.name}: ${typeStr};`);
|
|
240
|
+
}
|
|
241
|
+
lines.push("}");
|
|
242
|
+
lines.push("");
|
|
243
|
+
}
|
|
244
|
+
lines.push("export interface Schema {");
|
|
245
|
+
for (const table of tables) {
|
|
246
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
247
|
+
lines.push(` ${table.name}: ${interfaceName};`);
|
|
248
|
+
}
|
|
249
|
+
lines.push("}");
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push("// Database client with typed tables");
|
|
252
|
+
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
253
|
+
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
254
|
+
lines.push("");
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
async function parseFunctionsDir(functionsDir) {
|
|
258
|
+
const functions = [];
|
|
259
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
260
|
+
return functions;
|
|
261
|
+
}
|
|
262
|
+
const files = await fs3.readdir(functionsDir);
|
|
263
|
+
for (const file of files) {
|
|
264
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
265
|
+
const filePath = path3.join(functionsDir, file);
|
|
266
|
+
const stat = await fs3.stat(filePath);
|
|
267
|
+
if (!stat.isFile()) continue;
|
|
268
|
+
const moduleName = file.replace(/\.(ts|js)$/, "");
|
|
269
|
+
const source = await fs3.readFile(filePath, "utf-8");
|
|
270
|
+
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action)\s*\(/g;
|
|
271
|
+
let match;
|
|
272
|
+
while ((match = exportRegex.exec(source)) !== null) {
|
|
273
|
+
functions.push({
|
|
274
|
+
name: match[1],
|
|
275
|
+
moduleName
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return functions;
|
|
280
|
+
}
|
|
281
|
+
async function generateApiFile(functionsDir) {
|
|
282
|
+
const functions = await parseFunctionsDir(functionsDir);
|
|
283
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
284
|
+
for (const fn of functions) {
|
|
285
|
+
if (!moduleMap.has(fn.moduleName)) {
|
|
286
|
+
moduleMap.set(fn.moduleName, []);
|
|
287
|
+
}
|
|
288
|
+
moduleMap.get(fn.moduleName).push(fn.name);
|
|
289
|
+
}
|
|
290
|
+
const lines = [
|
|
291
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
292
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
293
|
+
"",
|
|
294
|
+
"import { createApiProxy } from '@tthr/client';",
|
|
295
|
+
"",
|
|
296
|
+
"/**",
|
|
297
|
+
" * API function reference type for useQuery/useMutation.",
|
|
298
|
+
' * The _name property contains the function path (e.g., "users.list").',
|
|
299
|
+
" */",
|
|
300
|
+
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
301
|
+
" _name: string;",
|
|
302
|
+
" _args?: TArgs;",
|
|
303
|
+
" _result?: TResult;",
|
|
304
|
+
"}",
|
|
305
|
+
""
|
|
306
|
+
];
|
|
307
|
+
if (moduleMap.size > 0) {
|
|
308
|
+
for (const [moduleName, fnNames] of moduleMap) {
|
|
309
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
310
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
311
|
+
for (const fnName of fnNames) {
|
|
312
|
+
lines.push(` ${fnName}: ApiFunction;`);
|
|
313
|
+
}
|
|
314
|
+
lines.push("}");
|
|
315
|
+
lines.push("");
|
|
316
|
+
}
|
|
317
|
+
lines.push("export interface Api {");
|
|
318
|
+
for (const moduleName of moduleMap.keys()) {
|
|
319
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
320
|
+
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
321
|
+
}
|
|
322
|
+
lines.push("}");
|
|
323
|
+
} else {
|
|
324
|
+
lines.push("/**");
|
|
325
|
+
lines.push(" * Flexible API type that allows access to any function path.");
|
|
326
|
+
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
327
|
+
lines.push(" * The actual types depend on your function definitions.");
|
|
328
|
+
lines.push(" */");
|
|
329
|
+
lines.push("export type Api = {");
|
|
330
|
+
lines.push(" [module: string]: {");
|
|
331
|
+
lines.push(" [fn: string]: ApiFunction;");
|
|
332
|
+
lines.push(" };");
|
|
333
|
+
lines.push("};");
|
|
334
|
+
}
|
|
335
|
+
lines.push("");
|
|
336
|
+
lines.push("// API client proxy - provides typed access to your functions");
|
|
337
|
+
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
338
|
+
lines.push("// In Tether functions: calls the actual function implementation");
|
|
339
|
+
lines.push("export const api = createApiProxy<Api>();");
|
|
340
|
+
lines.push("");
|
|
341
|
+
return lines.join("\n");
|
|
342
|
+
}
|
|
343
|
+
async function generateTypes(options = {}) {
|
|
344
|
+
const config = await loadConfig();
|
|
345
|
+
const schemaPath = resolvePath(config.schema);
|
|
346
|
+
const outputDir = resolvePath(config.output);
|
|
347
|
+
const functionsDir = resolvePath(config.functions);
|
|
348
|
+
if (!await fs3.pathExists(schemaPath)) {
|
|
349
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
350
|
+
}
|
|
351
|
+
const schemaSource = await fs3.readFile(schemaPath, "utf-8");
|
|
352
|
+
const tables = parseSchemaFile(schemaSource);
|
|
353
|
+
await fs3.ensureDir(outputDir);
|
|
354
|
+
await fs3.writeFile(
|
|
355
|
+
path3.join(outputDir, "db.ts"),
|
|
356
|
+
generateDbFile(tables)
|
|
357
|
+
);
|
|
358
|
+
await fs3.writeFile(
|
|
359
|
+
path3.join(outputDir, "api.ts"),
|
|
360
|
+
await generateApiFile(functionsDir)
|
|
361
|
+
);
|
|
362
|
+
await fs3.writeFile(
|
|
363
|
+
path3.join(outputDir, "index.ts"),
|
|
364
|
+
`// Auto-generated by Tether CLI - do not edit manually
|
|
365
|
+
export * from './db';
|
|
366
|
+
export * from './api';
|
|
367
|
+
`
|
|
368
|
+
);
|
|
369
|
+
return { tables, outputDir };
|
|
370
|
+
}
|
|
371
|
+
async function generateCommand() {
|
|
372
|
+
await requireAuth();
|
|
373
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
374
|
+
if (!await fs3.pathExists(configPath)) {
|
|
375
|
+
console.log(chalk2.red("\nError: Not a Tether project"));
|
|
376
|
+
console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
|
|
380
|
+
const spinner = ora("Reading schema...").start();
|
|
381
|
+
try {
|
|
382
|
+
spinner.text = "Generating types...";
|
|
383
|
+
const { tables, outputDir } = await generateTypes();
|
|
384
|
+
if (tables.length === 0) {
|
|
385
|
+
spinner.warn("No tables found in schema");
|
|
386
|
+
console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
390
|
+
console.log("\n" + chalk2.green("\u2713") + " Tables:");
|
|
391
|
+
for (const table of tables) {
|
|
392
|
+
console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
393
|
+
}
|
|
394
|
+
const relativeOutput = path3.relative(process.cwd(), outputDir);
|
|
395
|
+
console.log("\n" + chalk2.green("\u2713") + " Generated files:");
|
|
396
|
+
console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
|
|
397
|
+
console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
|
|
398
|
+
console.log(chalk2.dim(` ${relativeOutput}/index.ts
|
|
399
|
+
`));
|
|
400
|
+
} catch (error) {
|
|
401
|
+
spinner.fail("Failed to generate types");
|
|
402
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export {
|
|
408
|
+
getCredentials,
|
|
409
|
+
requireAuth,
|
|
410
|
+
saveCredentials,
|
|
411
|
+
clearCredentials,
|
|
412
|
+
loadConfig,
|
|
413
|
+
resolvePath,
|
|
414
|
+
detectFramework,
|
|
415
|
+
getFrameworkDevCommand,
|
|
416
|
+
generateTypes,
|
|
417
|
+
generateCommand
|
|
418
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
requireAuth,
|
|
10
10
|
resolvePath,
|
|
11
11
|
saveCredentials
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-LXODXQ5V.js";
|
|
13
13
|
|
|
14
14
|
// src/index.ts
|
|
15
15
|
import { Command } from "commander";
|
|
@@ -49,8 +49,10 @@ async function deployCommand(options) {
|
|
|
49
49
|
console.log(chalk.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
|
|
50
50
|
process.exit(1);
|
|
51
51
|
}
|
|
52
|
+
const environment = options.env || "development";
|
|
52
53
|
console.log(chalk.bold("\n\u26A1 Deploying to Tether\n"));
|
|
53
54
|
console.log(chalk.dim(` Project: ${projectId}`));
|
|
55
|
+
console.log(chalk.dim(` Environment: ${environment}`));
|
|
54
56
|
console.log(chalk.dim(` API: ${API_URL}
|
|
55
57
|
`));
|
|
56
58
|
const config = await loadConfig();
|
|
@@ -58,15 +60,15 @@ async function deployCommand(options) {
|
|
|
58
60
|
const deployFunctions = options.functions || !options.schema && !options.functions;
|
|
59
61
|
if (deploySchema) {
|
|
60
62
|
const schemaPath = resolvePath(config.schema);
|
|
61
|
-
await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, options.dryRun);
|
|
63
|
+
await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, environment, options.dryRun);
|
|
62
64
|
}
|
|
63
65
|
if (deployFunctions) {
|
|
64
66
|
const functionsDir = resolvePath(config.functions);
|
|
65
|
-
await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, options.dryRun);
|
|
67
|
+
await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, environment, options.dryRun);
|
|
66
68
|
}
|
|
67
69
|
console.log(chalk.green("\n\u2713 Deployment complete\n"));
|
|
68
70
|
}
|
|
69
|
-
async function deploySchemaToServer(projectId, token, schemaPath, dryRun) {
|
|
71
|
+
async function deploySchemaToServer(projectId, token, schemaPath, environment, dryRun) {
|
|
70
72
|
const spinner = ora("Reading schema...").start();
|
|
71
73
|
try {
|
|
72
74
|
if (!await fs.pathExists(schemaPath)) {
|
|
@@ -90,7 +92,8 @@ async function deploySchemaToServer(projectId, token, schemaPath, dryRun) {
|
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
92
94
|
spinner.text = "Deploying schema...";
|
|
93
|
-
const
|
|
95
|
+
const envPath = environment !== "production" ? `/env/${environment}` : "";
|
|
96
|
+
const response = await fetch(`${API_URL}/projects/${projectId}${envPath}/deploy/schema`, {
|
|
94
97
|
method: "POST",
|
|
95
98
|
headers: {
|
|
96
99
|
"Content-Type": "application/json",
|
|
@@ -115,7 +118,7 @@ async function deploySchemaToServer(projectId, token, schemaPath, dryRun) {
|
|
|
115
118
|
console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
|
-
async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
|
|
121
|
+
async function deployFunctionsToServer(projectId, token, functionsDir, environment, dryRun) {
|
|
119
122
|
const spinner = ora("Reading functions...").start();
|
|
120
123
|
try {
|
|
121
124
|
if (!await fs.pathExists(functionsDir)) {
|
|
@@ -149,7 +152,8 @@ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
|
|
|
149
152
|
return;
|
|
150
153
|
}
|
|
151
154
|
spinner.text = "Deploying functions...";
|
|
152
|
-
const
|
|
155
|
+
const envPath = environment !== "production" ? `/env/${environment}` : "";
|
|
156
|
+
const response = await fetch(`${API_URL}/projects/${projectId}${envPath}/deploy/functions`, {
|
|
153
157
|
method: "POST",
|
|
154
158
|
headers: {
|
|
155
159
|
"Content-Type": "application/json",
|
|
@@ -235,15 +239,42 @@ function parseSchema(source) {
|
|
|
235
239
|
const colName = colMatch[1];
|
|
236
240
|
const colType = colMatch[2];
|
|
237
241
|
const modifiers = colMatch[3];
|
|
242
|
+
if (colName === "_id" || colName === "_createdAt") {
|
|
243
|
+
console.warn(`Warning: Column '${colName}' is a reserved system column and will be ignored.`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const userWantsPrimaryKey = modifiers.includes(".primaryKey()");
|
|
247
|
+
if (userWantsPrimaryKey) {
|
|
248
|
+
console.warn(`Warning: Column '${colName}' has .primaryKey() which will be ignored. Tether uses '_id' as the primary key.`);
|
|
249
|
+
}
|
|
238
250
|
columns[colName] = {
|
|
239
251
|
type: colType,
|
|
240
|
-
primaryKey:
|
|
252
|
+
primaryKey: false,
|
|
253
|
+
// _id is always the primary key, ignore user-defined primary keys
|
|
241
254
|
notNull: modifiers.includes(".notNull()"),
|
|
242
|
-
unique: modifiers.includes(".unique()"),
|
|
255
|
+
unique: modifiers.includes(".unique()") || userWantsPrimaryKey,
|
|
256
|
+
// Make it unique if they wanted PK
|
|
243
257
|
hasDefault: modifiers.includes(".default("),
|
|
244
258
|
references: modifiers.match(/\.references\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1]
|
|
245
259
|
};
|
|
246
260
|
}
|
|
261
|
+
columns["_id"] = {
|
|
262
|
+
type: "text",
|
|
263
|
+
primaryKey: true,
|
|
264
|
+
notNull: true,
|
|
265
|
+
unique: true,
|
|
266
|
+
// Also set unique so ALTER TABLE adds UNIQUE constraint
|
|
267
|
+
hasDefault: false,
|
|
268
|
+
isSystemColumn: true
|
|
269
|
+
};
|
|
270
|
+
columns["_createdAt"] = {
|
|
271
|
+
type: "timestamp",
|
|
272
|
+
primaryKey: false,
|
|
273
|
+
notNull: true,
|
|
274
|
+
unique: false,
|
|
275
|
+
hasDefault: true,
|
|
276
|
+
isSystemColumn: true
|
|
277
|
+
};
|
|
247
278
|
tables.push({ name: tableName, columns, source: tableSource });
|
|
248
279
|
}
|
|
249
280
|
return tables;
|
|
@@ -271,9 +302,10 @@ function getColumnSqlType(type) {
|
|
|
271
302
|
function buildColumnSql(colName, def, forAlterTable = false) {
|
|
272
303
|
const sqlType = getColumnSqlType(def.type);
|
|
273
304
|
let colSql = `${colName} ${sqlType}`;
|
|
274
|
-
|
|
305
|
+
const addingPrimaryKey = def.primaryKey && !forAlterTable;
|
|
306
|
+
if (addingPrimaryKey) colSql += " PRIMARY KEY";
|
|
275
307
|
if (def.notNull && !forAlterTable) colSql += " NOT NULL";
|
|
276
|
-
if (def.unique) colSql += " UNIQUE";
|
|
308
|
+
if (def.unique && !addingPrimaryKey) colSql += " UNIQUE";
|
|
277
309
|
if (def.hasDefault && def.type === "timestamp") {
|
|
278
310
|
colSql += " DEFAULT (datetime('now'))";
|
|
279
311
|
}
|
|
@@ -298,7 +330,6 @@ function generateSchemaSQL(tables) {
|
|
|
298
330
|
);
|
|
299
331
|
for (const [colName, colDef] of Object.entries(table.columns)) {
|
|
300
332
|
const def = colDef;
|
|
301
|
-
if (def.primaryKey) continue;
|
|
302
333
|
const alterColSql = buildColumnSql(colName, def, true);
|
|
303
334
|
statements.push(
|
|
304
335
|
`-- Add column if missing (will error if exists, which is OK)
|
|
@@ -1020,8 +1051,8 @@ PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
|
|
|
1020
1051
|
}
|
|
1021
1052
|
}
|
|
1022
1053
|
}
|
|
1023
|
-
function getInstallCommand(pm,
|
|
1024
|
-
const devFlag =
|
|
1054
|
+
function getInstallCommand(pm, isDev6 = false) {
|
|
1055
|
+
const devFlag = isDev6 ? pm === "npm" ? "-D" : pm === "yarn" ? "-D" : pm === "pnpm" ? "-D" : "-d" : "";
|
|
1025
1056
|
return `${pm} ${pm === "npm" ? "install" : "add"} ${devFlag}`.trim();
|
|
1026
1057
|
}
|
|
1027
1058
|
async function installTetherPackages(projectPath, template, packageManager) {
|
|
@@ -1478,7 +1509,7 @@ async function runGenerate(spinner) {
|
|
|
1478
1509
|
}
|
|
1479
1510
|
isGenerating = true;
|
|
1480
1511
|
try {
|
|
1481
|
-
const { generateTypes } = await import("./generate-
|
|
1512
|
+
const { generateTypes } = await import("./generate-YQ4QRPXF.js");
|
|
1482
1513
|
spinner.text = "Regenerating types...";
|
|
1483
1514
|
await generateTypes({ silent: true });
|
|
1484
1515
|
spinner.succeed("Types regenerated");
|
|
@@ -1618,14 +1649,17 @@ async function devCommand(options) {
|
|
|
1618
1649
|
const functionsDir = resolvePath(config.functions);
|
|
1619
1650
|
const outputDir = resolvePath(config.output);
|
|
1620
1651
|
const framework = config.framework || await detectFramework();
|
|
1621
|
-
const
|
|
1652
|
+
const devCmd = config.dev?.command || getFrameworkDevCommand(framework);
|
|
1653
|
+
const environment = options.env || config.environment || "development";
|
|
1654
|
+
const isLocal = options.local ?? false;
|
|
1622
1655
|
console.log(chalk3.bold("\n\u26A1 Starting Tether development server\n"));
|
|
1623
1656
|
if (framework !== "unknown") {
|
|
1624
1657
|
console.log(chalk3.dim(` Framework: ${framework}`));
|
|
1625
1658
|
}
|
|
1626
|
-
if (
|
|
1627
|
-
console.log(chalk3.dim(` Dev command: ${
|
|
1659
|
+
if (devCmd) {
|
|
1660
|
+
console.log(chalk3.dim(` Dev command: ${devCmd}`));
|
|
1628
1661
|
}
|
|
1662
|
+
console.log(chalk3.dim(` Environment: ${environment}${isLocal ? " (local)" : " (cloud)"}`));
|
|
1629
1663
|
console.log(chalk3.dim(` Schema: ${path3.relative(process.cwd(), schemaPath)}`));
|
|
1630
1664
|
console.log(chalk3.dim(` Functions: ${path3.relative(process.cwd(), functionsDir)}`));
|
|
1631
1665
|
console.log(chalk3.dim(` Output: ${path3.relative(process.cwd(), outputDir)}`));
|
|
@@ -1656,7 +1690,7 @@ async function devCommand(options) {
|
|
|
1656
1690
|
}
|
|
1657
1691
|
}
|
|
1658
1692
|
spinner.text = "Generating types...";
|
|
1659
|
-
const { generateTypes } = await import("./generate-
|
|
1693
|
+
const { generateTypes } = await import("./generate-YQ4QRPXF.js");
|
|
1660
1694
|
await generateTypes({ silent: true });
|
|
1661
1695
|
spinner.succeed("Types generated");
|
|
1662
1696
|
spinner.start("Setting up file watchers...");
|
|
@@ -1700,9 +1734,9 @@ async function devCommand(options) {
|
|
|
1700
1734
|
});
|
|
1701
1735
|
}
|
|
1702
1736
|
spinner.succeed("File watchers ready");
|
|
1703
|
-
if (
|
|
1737
|
+
if (devCmd && !options.skipFramework) {
|
|
1704
1738
|
spinner.start(`Starting ${framework} dev server...`);
|
|
1705
|
-
frameworkProcess = startFrameworkDev(
|
|
1739
|
+
frameworkProcess = startFrameworkDev(devCmd, port, spinner);
|
|
1706
1740
|
} else {
|
|
1707
1741
|
spinner.start("Watching for changes...");
|
|
1708
1742
|
console.log("\n" + chalk3.cyan(` Tether dev ready`));
|
|
@@ -1904,18 +1938,18 @@ function detectPackageManager() {
|
|
|
1904
1938
|
}
|
|
1905
1939
|
return "npm";
|
|
1906
1940
|
}
|
|
1907
|
-
function getInstallCommand2(pm, packages,
|
|
1941
|
+
function getInstallCommand2(pm, packages, isDev6) {
|
|
1908
1942
|
const packagesStr = packages.join(" ");
|
|
1909
1943
|
switch (pm) {
|
|
1910
1944
|
case "bun":
|
|
1911
|
-
return
|
|
1945
|
+
return isDev6 ? `bun add -d ${packagesStr}` : `bun add ${packagesStr}`;
|
|
1912
1946
|
case "pnpm":
|
|
1913
|
-
return
|
|
1947
|
+
return isDev6 ? `pnpm add -D ${packagesStr}` : `pnpm add ${packagesStr}`;
|
|
1914
1948
|
case "yarn":
|
|
1915
|
-
return
|
|
1949
|
+
return isDev6 ? `yarn add -D ${packagesStr}` : `yarn add ${packagesStr}`;
|
|
1916
1950
|
case "npm":
|
|
1917
1951
|
default:
|
|
1918
|
-
return
|
|
1952
|
+
return isDev6 ? `npm install -D ${packagesStr}` : `npm install ${packagesStr}`;
|
|
1919
1953
|
}
|
|
1920
1954
|
}
|
|
1921
1955
|
async function getLatestVersion2(packageName) {
|
|
@@ -2088,16 +2122,182 @@ async function execCommand(sql) {
|
|
|
2088
2122
|
}
|
|
2089
2123
|
}
|
|
2090
2124
|
|
|
2125
|
+
// src/commands/env.ts
|
|
2126
|
+
import chalk7 from "chalk";
|
|
2127
|
+
import ora7 from "ora";
|
|
2128
|
+
import fs6 from "fs-extra";
|
|
2129
|
+
import path6 from "path";
|
|
2130
|
+
var isDev5 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
2131
|
+
var API_URL5 = isDev5 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
2132
|
+
async function getProjectId() {
|
|
2133
|
+
const envPath = path6.resolve(process.cwd(), ".env");
|
|
2134
|
+
let projectId;
|
|
2135
|
+
if (await fs6.pathExists(envPath)) {
|
|
2136
|
+
const envContent = await fs6.readFile(envPath, "utf-8");
|
|
2137
|
+
const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
|
|
2138
|
+
projectId = match?.[1]?.trim();
|
|
2139
|
+
}
|
|
2140
|
+
if (!projectId) {
|
|
2141
|
+
console.log(chalk7.red("\nError: Project ID not found"));
|
|
2142
|
+
console.log(chalk7.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
|
|
2143
|
+
process.exit(1);
|
|
2144
|
+
}
|
|
2145
|
+
return projectId;
|
|
2146
|
+
}
|
|
2147
|
+
async function envListCommand() {
|
|
2148
|
+
const credentials = await requireAuth();
|
|
2149
|
+
const projectId = await getProjectId();
|
|
2150
|
+
const spinner = ora7("Fetching environments...").start();
|
|
2151
|
+
try {
|
|
2152
|
+
const response = await fetch(`${API_URL5}/projects/${projectId}/environments`, {
|
|
2153
|
+
headers: {
|
|
2154
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
if (!response.ok) {
|
|
2158
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
2159
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
2160
|
+
}
|
|
2161
|
+
const data = await response.json();
|
|
2162
|
+
spinner.succeed(`Found ${data.environments.length} environment(s)`);
|
|
2163
|
+
console.log();
|
|
2164
|
+
for (const env2 of data.environments) {
|
|
2165
|
+
const isDefault = env2.name === data.defaultEnvironment;
|
|
2166
|
+
const colour = getEnvColour(env2.name);
|
|
2167
|
+
const defaultBadge = isDefault ? chalk7.yellow(" \u2605 default") : "";
|
|
2168
|
+
console.log(` ${chalk7.hex(colour)("\u25CF")} ${chalk7.bold(env2.name)}${defaultBadge}`);
|
|
2169
|
+
console.log(chalk7.dim(` API Key: ${maskApiKey(env2.apiKey)}`));
|
|
2170
|
+
console.log(chalk7.dim(` Created: ${new Date(env2.createdAt).toLocaleDateString()}`));
|
|
2171
|
+
console.log();
|
|
2172
|
+
}
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
spinner.fail("Failed to fetch environments");
|
|
2175
|
+
console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2176
|
+
process.exit(1);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
async function envCreateCommand(name, options) {
|
|
2180
|
+
const credentials = await requireAuth();
|
|
2181
|
+
const projectId = await getProjectId();
|
|
2182
|
+
const normalizedName = name.toLowerCase();
|
|
2183
|
+
if (!/^[a-z][a-z0-9-_]*$/.test(normalizedName)) {
|
|
2184
|
+
console.log(chalk7.red("\nError: Invalid environment name"));
|
|
2185
|
+
console.log(chalk7.dim("Name must start with a letter and contain only letters, numbers, hyphens, and underscores\n"));
|
|
2186
|
+
process.exit(1);
|
|
2187
|
+
}
|
|
2188
|
+
const spinner = ora7(`Creating environment '${normalizedName}'...`).start();
|
|
2189
|
+
try {
|
|
2190
|
+
const body = { name: normalizedName };
|
|
2191
|
+
if (options.from) {
|
|
2192
|
+
body.cloneFrom = options.from;
|
|
2193
|
+
spinner.text = `Creating environment '${normalizedName}' from '${options.from}'...`;
|
|
2194
|
+
}
|
|
2195
|
+
const response = await fetch(`${API_URL5}/projects/${projectId}/environments`, {
|
|
2196
|
+
method: "POST",
|
|
2197
|
+
headers: {
|
|
2198
|
+
"Content-Type": "application/json",
|
|
2199
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
2200
|
+
},
|
|
2201
|
+
body: JSON.stringify(body)
|
|
2202
|
+
});
|
|
2203
|
+
if (!response.ok) {
|
|
2204
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
2205
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
2206
|
+
}
|
|
2207
|
+
const env2 = await response.json();
|
|
2208
|
+
spinner.succeed(`Environment '${normalizedName}' created`);
|
|
2209
|
+
console.log();
|
|
2210
|
+
console.log(chalk7.dim(` API Key: ${env2.apiKey}`));
|
|
2211
|
+
console.log();
|
|
2212
|
+
console.log(chalk7.dim("Deploy to this environment with:"));
|
|
2213
|
+
console.log(chalk7.cyan(` tthr deploy --env ${normalizedName}`));
|
|
2214
|
+
console.log();
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
spinner.fail("Failed to create environment");
|
|
2217
|
+
console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2218
|
+
process.exit(1);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
async function envDeleteCommand(name) {
|
|
2222
|
+
const credentials = await requireAuth();
|
|
2223
|
+
const projectId = await getProjectId();
|
|
2224
|
+
const spinner = ora7(`Deleting environment '${name}'...`).start();
|
|
2225
|
+
try {
|
|
2226
|
+
const response = await fetch(`${API_URL5}/projects/${projectId}/environments/${name}`, {
|
|
2227
|
+
method: "DELETE",
|
|
2228
|
+
headers: {
|
|
2229
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
if (!response.ok) {
|
|
2233
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
2234
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
2235
|
+
}
|
|
2236
|
+
spinner.succeed(`Environment '${name}' deleted`);
|
|
2237
|
+
} catch (error) {
|
|
2238
|
+
spinner.fail("Failed to delete environment");
|
|
2239
|
+
console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2240
|
+
process.exit(1);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
async function envDefaultCommand(name) {
|
|
2244
|
+
const credentials = await requireAuth();
|
|
2245
|
+
const projectId = await getProjectId();
|
|
2246
|
+
const spinner = ora7(`Setting '${name}' as default environment...`).start();
|
|
2247
|
+
try {
|
|
2248
|
+
const response = await fetch(`${API_URL5}/projects/${projectId}/environments/default`, {
|
|
2249
|
+
method: "PATCH",
|
|
2250
|
+
headers: {
|
|
2251
|
+
"Content-Type": "application/json",
|
|
2252
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
2253
|
+
},
|
|
2254
|
+
body: JSON.stringify({ name })
|
|
2255
|
+
});
|
|
2256
|
+
if (!response.ok) {
|
|
2257
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
2258
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
2259
|
+
}
|
|
2260
|
+
spinner.succeed(`'${name}' is now the default environment`);
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
spinner.fail("Failed to set default environment");
|
|
2263
|
+
console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2264
|
+
process.exit(1);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
function getEnvColour(envName) {
|
|
2268
|
+
switch (envName) {
|
|
2269
|
+
case "production":
|
|
2270
|
+
return "#10b981";
|
|
2271
|
+
// green
|
|
2272
|
+
case "development":
|
|
2273
|
+
return "#f59e0b";
|
|
2274
|
+
// amber
|
|
2275
|
+
default:
|
|
2276
|
+
return "#3b82f6";
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
function maskApiKey(key) {
|
|
2280
|
+
if (!key || key.length < 15) return key;
|
|
2281
|
+
const visibleStart = key.substring(0, 10);
|
|
2282
|
+
const visibleEnd = key.substring(key.length - 4);
|
|
2283
|
+
return `${visibleStart}...${visibleEnd}`;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2091
2286
|
// src/index.ts
|
|
2092
2287
|
var program = new Command();
|
|
2093
2288
|
program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version("0.0.1");
|
|
2094
2289
|
program.command("init [name]").description("Create a new Tether project").option("-t, --template <template>", "Project template (vue, svelte, react, vanilla)", "vue").action(initCommand);
|
|
2095
|
-
program.command("dev").description("Start local development server with hot reload").option("-p, --port <port>", "Port to run on").option("--skip-framework", "Only run Tether watchers, skip framework dev server").action(devCommand);
|
|
2290
|
+
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").action(devCommand);
|
|
2096
2291
|
program.command("generate").alias("gen").description("Generate types from schema").action(generateCommand);
|
|
2097
|
-
program.command("deploy").description("Deploy schema and functions to Tether").option("-s, --schema", "Deploy schema only").option("-f, --functions", "Deploy functions only").option("--dry-run", "Show what would be deployed without deploying").action(deployCommand);
|
|
2292
|
+
program.command("deploy").description("Deploy schema and functions to Tether").option("-s, --schema", "Deploy schema only").option("-f, --functions", "Deploy functions only").option("-e, --env <environment>", "Target environment (default: development)").option("--dry-run", "Show what would be deployed without deploying").action(deployCommand);
|
|
2098
2293
|
program.command("login").description("Authenticate with Tether").action(loginCommand);
|
|
2099
2294
|
program.command("logout").description("Sign out of Tether").action(logoutCommand);
|
|
2100
2295
|
program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
|
|
2101
2296
|
program.command("update").description("Update all Tether packages to the latest version").option("--dry-run", "Show what would be updated without updating").action(updateCommand);
|
|
2102
2297
|
program.command("exec <sql>").description("Execute a SQL query against the project database").action(execCommand);
|
|
2298
|
+
var env = program.command("env").description("Manage project environments");
|
|
2299
|
+
env.command("list").description("List all environments").action(envListCommand);
|
|
2300
|
+
env.command("create <name>").description("Create a new environment").option("--from <env>", "Clone schema and functions from an existing environment").action(envCreateCommand);
|
|
2301
|
+
env.command("delete <name>").description("Delete an environment").action(envDeleteCommand);
|
|
2302
|
+
env.command("default <name>").description("Set the default environment").action(envDefaultCommand);
|
|
2103
2303
|
program.parse();
|