tthr 0.0.33 → 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/chunk-ZWLVHKNL.js +414 -0
- package/dist/generate-26HERX3T.js +8 -0
- package/dist/generate-YQ4QRPXF.js +8 -0
- package/dist/index.js +655 -575
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -1,184 +1,99 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
clearCredentials,
|
|
4
|
+
detectFramework,
|
|
5
|
+
generateCommand,
|
|
6
|
+
getCredentials,
|
|
7
|
+
getFrameworkDevCommand,
|
|
8
|
+
loadConfig,
|
|
9
|
+
requireAuth,
|
|
10
|
+
resolvePath,
|
|
11
|
+
saveCredentials
|
|
12
|
+
} from "./chunk-LXODXQ5V.js";
|
|
2
13
|
|
|
3
14
|
// src/index.ts
|
|
4
15
|
import { Command } from "commander";
|
|
5
16
|
|
|
6
17
|
// src/commands/init.ts
|
|
7
|
-
import
|
|
18
|
+
import chalk2 from "chalk";
|
|
8
19
|
import ora2 from "ora";
|
|
9
20
|
import prompts from "prompts";
|
|
10
|
-
import
|
|
11
|
-
import
|
|
21
|
+
import fs2 from "fs-extra";
|
|
22
|
+
import path2 from "path";
|
|
12
23
|
import { execSync } from "child_process";
|
|
13
24
|
|
|
14
|
-
// src/
|
|
25
|
+
// src/commands/deploy.ts
|
|
15
26
|
import chalk from "chalk";
|
|
27
|
+
import ora from "ora";
|
|
16
28
|
import fs from "fs-extra";
|
|
17
29
|
import path from "path";
|
|
18
|
-
var CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
|
|
19
|
-
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
20
|
-
async function getCredentials() {
|
|
21
|
-
try {
|
|
22
|
-
if (await fs.pathExists(CREDENTIALS_FILE)) {
|
|
23
|
-
return await fs.readJSON(CREDENTIALS_FILE);
|
|
24
|
-
}
|
|
25
|
-
} catch {
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
async function requireAuth() {
|
|
30
|
-
const credentials = await getCredentials();
|
|
31
|
-
if (!credentials) {
|
|
32
|
-
console.error(chalk.red("\n\u2717 Not logged in\n"));
|
|
33
|
-
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
if (credentials.expiresAt) {
|
|
37
|
-
const expiresAt = new Date(credentials.expiresAt);
|
|
38
|
-
if (/* @__PURE__ */ new Date() > expiresAt) {
|
|
39
|
-
console.error(chalk.red("\n\u2717 Session expired\n"));
|
|
40
|
-
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return credentials;
|
|
45
|
-
}
|
|
46
|
-
async function saveCredentials(credentials) {
|
|
47
|
-
await fs.ensureDir(CONFIG_DIR);
|
|
48
|
-
await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
|
|
49
|
-
}
|
|
50
|
-
async function clearCredentials() {
|
|
51
|
-
try {
|
|
52
|
-
await fs.remove(CREDENTIALS_FILE);
|
|
53
|
-
} catch {
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// src/commands/deploy.ts
|
|
58
|
-
import chalk2 from "chalk";
|
|
59
|
-
import ora from "ora";
|
|
60
|
-
import fs3 from "fs-extra";
|
|
61
|
-
import path3 from "path";
|
|
62
|
-
|
|
63
|
-
// src/utils/config.ts
|
|
64
|
-
import fs2 from "fs-extra";
|
|
65
|
-
import path2 from "path";
|
|
66
|
-
var DEFAULT_CONFIG = {
|
|
67
|
-
schema: "./tether/schema.ts",
|
|
68
|
-
functions: "./tether/functions",
|
|
69
|
-
output: "./tether/_generated",
|
|
70
|
-
dev: {
|
|
71
|
-
port: 3001,
|
|
72
|
-
host: "localhost"
|
|
73
|
-
},
|
|
74
|
-
database: {
|
|
75
|
-
walMode: true
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
async function loadConfig(cwd = process.cwd()) {
|
|
79
|
-
const configPath = path2.resolve(cwd, "tether.config.ts");
|
|
80
|
-
if (!await fs2.pathExists(configPath)) {
|
|
81
|
-
return DEFAULT_CONFIG;
|
|
82
|
-
}
|
|
83
|
-
const configSource = await fs2.readFile(configPath, "utf-8");
|
|
84
|
-
const config = {};
|
|
85
|
-
const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
|
|
86
|
-
if (schemaMatch) {
|
|
87
|
-
config.schema = schemaMatch[1];
|
|
88
|
-
}
|
|
89
|
-
const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
|
|
90
|
-
if (functionsMatch) {
|
|
91
|
-
config.functions = functionsMatch[1];
|
|
92
|
-
}
|
|
93
|
-
const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
|
|
94
|
-
if (outputMatch) {
|
|
95
|
-
config.output = outputMatch[1];
|
|
96
|
-
}
|
|
97
|
-
const portMatch = configSource.match(/port\s*:\s*(\d+)/);
|
|
98
|
-
if (portMatch) {
|
|
99
|
-
config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
|
|
100
|
-
}
|
|
101
|
-
const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
|
|
102
|
-
if (hostMatch) {
|
|
103
|
-
config.dev = { ...config.dev, host: hostMatch[1] };
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
...DEFAULT_CONFIG,
|
|
107
|
-
...config,
|
|
108
|
-
dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
|
|
109
|
-
database: { ...DEFAULT_CONFIG.database, ...config.database }
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function resolvePath(configPath, cwd = process.cwd()) {
|
|
113
|
-
const normalised = configPath.replace(/^\.\//, "");
|
|
114
|
-
return path2.resolve(cwd, normalised);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// src/commands/deploy.ts
|
|
118
30
|
var isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
119
31
|
var API_URL = isDev ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
120
32
|
async function deployCommand(options) {
|
|
121
33
|
const credentials = await requireAuth();
|
|
122
|
-
const configPath =
|
|
123
|
-
if (!await
|
|
124
|
-
console.log(
|
|
125
|
-
console.log(
|
|
34
|
+
const configPath = path.resolve(process.cwd(), "tether.config.ts");
|
|
35
|
+
if (!await fs.pathExists(configPath)) {
|
|
36
|
+
console.log(chalk.red("\nError: Not a Tether project"));
|
|
37
|
+
console.log(chalk.dim("Run `tthr init` to create a new project\n"));
|
|
126
38
|
process.exit(1);
|
|
127
39
|
}
|
|
128
|
-
const envPath =
|
|
40
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
129
41
|
let projectId;
|
|
130
|
-
if (await
|
|
131
|
-
const envContent = await
|
|
42
|
+
if (await fs.pathExists(envPath)) {
|
|
43
|
+
const envContent = await fs.readFile(envPath, "utf-8");
|
|
132
44
|
const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
|
|
133
45
|
projectId = match?.[1]?.trim();
|
|
134
46
|
}
|
|
135
47
|
if (!projectId) {
|
|
136
|
-
console.log(
|
|
137
|
-
console.log(
|
|
48
|
+
console.log(chalk.red("\nError: Project ID not found"));
|
|
49
|
+
console.log(chalk.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
|
|
138
50
|
process.exit(1);
|
|
139
51
|
}
|
|
140
|
-
|
|
141
|
-
console.log(
|
|
142
|
-
console.log(
|
|
52
|
+
const environment = options.env || "development";
|
|
53
|
+
console.log(chalk.bold("\n\u26A1 Deploying to Tether\n"));
|
|
54
|
+
console.log(chalk.dim(` Project: ${projectId}`));
|
|
55
|
+
console.log(chalk.dim(` Environment: ${environment}`));
|
|
56
|
+
console.log(chalk.dim(` API: ${API_URL}
|
|
143
57
|
`));
|
|
144
58
|
const config = await loadConfig();
|
|
145
59
|
const deploySchema = options.schema || !options.schema && !options.functions;
|
|
146
60
|
const deployFunctions = options.functions || !options.schema && !options.functions;
|
|
147
61
|
if (deploySchema) {
|
|
148
62
|
const schemaPath = resolvePath(config.schema);
|
|
149
|
-
await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, options.dryRun);
|
|
63
|
+
await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, environment, options.dryRun);
|
|
150
64
|
}
|
|
151
65
|
if (deployFunctions) {
|
|
152
66
|
const functionsDir = resolvePath(config.functions);
|
|
153
|
-
await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, options.dryRun);
|
|
67
|
+
await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, environment, options.dryRun);
|
|
154
68
|
}
|
|
155
|
-
console.log(
|
|
69
|
+
console.log(chalk.green("\n\u2713 Deployment complete\n"));
|
|
156
70
|
}
|
|
157
|
-
async function deploySchemaToServer(projectId, token, schemaPath, dryRun) {
|
|
71
|
+
async function deploySchemaToServer(projectId, token, schemaPath, environment, dryRun) {
|
|
158
72
|
const spinner = ora("Reading schema...").start();
|
|
159
73
|
try {
|
|
160
|
-
if (!await
|
|
74
|
+
if (!await fs.pathExists(schemaPath)) {
|
|
161
75
|
spinner.warn("No schema file found");
|
|
162
|
-
const relativePath =
|
|
163
|
-
console.log(
|
|
76
|
+
const relativePath = path.relative(process.cwd(), schemaPath);
|
|
77
|
+
console.log(chalk.dim(` Create ${relativePath} to define your database schema
|
|
164
78
|
`));
|
|
165
79
|
return;
|
|
166
80
|
}
|
|
167
|
-
const schemaSource = await
|
|
81
|
+
const schemaSource = await fs.readFile(schemaPath, "utf-8");
|
|
168
82
|
const tables = parseSchema(schemaSource);
|
|
169
83
|
spinner.text = `Found ${tables.length} table(s)`;
|
|
170
84
|
const sql = generateSchemaSQL(tables);
|
|
171
85
|
if (dryRun) {
|
|
172
86
|
spinner.info("Dry run - would deploy schema:");
|
|
173
87
|
for (const table of tables) {
|
|
174
|
-
console.log(
|
|
88
|
+
console.log(chalk.dim(` - ${table.name} (${Object.keys(table.columns).length} columns)`));
|
|
175
89
|
}
|
|
176
|
-
console.log(
|
|
177
|
-
console.log(
|
|
90
|
+
console.log(chalk.bold("\nGenerated SQL:\n"));
|
|
91
|
+
console.log(chalk.cyan(sql));
|
|
178
92
|
return;
|
|
179
93
|
}
|
|
180
94
|
spinner.text = "Deploying schema...";
|
|
181
|
-
const
|
|
95
|
+
const envPath = environment !== "production" ? `/env/${environment}` : "";
|
|
96
|
+
const response = await fetch(`${API_URL}/projects/${projectId}${envPath}/deploy/schema`, {
|
|
182
97
|
method: "POST",
|
|
183
98
|
headers: {
|
|
184
99
|
"Content-Type": "application/json",
|
|
@@ -200,20 +115,20 @@ async function deploySchemaToServer(projectId, token, schemaPath, dryRun) {
|
|
|
200
115
|
spinner.succeed(`Schema deployed (${tables.length} table(s))`);
|
|
201
116
|
} catch (error) {
|
|
202
117
|
spinner.fail("Failed to deploy schema");
|
|
203
|
-
console.error(
|
|
118
|
+
console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
|
|
204
119
|
}
|
|
205
120
|
}
|
|
206
|
-
async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
|
|
121
|
+
async function deployFunctionsToServer(projectId, token, functionsDir, environment, dryRun) {
|
|
207
122
|
const spinner = ora("Reading functions...").start();
|
|
208
123
|
try {
|
|
209
|
-
if (!await
|
|
124
|
+
if (!await fs.pathExists(functionsDir)) {
|
|
210
125
|
spinner.warn("No functions directory found");
|
|
211
|
-
const relativePath =
|
|
212
|
-
console.log(
|
|
126
|
+
const relativePath = path.relative(process.cwd(), functionsDir);
|
|
127
|
+
console.log(chalk.dim(` Create ${relativePath}/ to define your API functions
|
|
213
128
|
`));
|
|
214
129
|
return;
|
|
215
130
|
}
|
|
216
|
-
const files = await
|
|
131
|
+
const files = await fs.readdir(functionsDir);
|
|
217
132
|
const tsFiles = files.filter((f) => f.endsWith(".ts"));
|
|
218
133
|
if (tsFiles.length === 0) {
|
|
219
134
|
spinner.info("No function files found");
|
|
@@ -221,8 +136,8 @@ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
|
|
|
221
136
|
}
|
|
222
137
|
const functions = [];
|
|
223
138
|
for (const file of tsFiles) {
|
|
224
|
-
const filePath =
|
|
225
|
-
const source = await
|
|
139
|
+
const filePath = path.join(functionsDir, file);
|
|
140
|
+
const source = await fs.readFile(filePath, "utf-8");
|
|
226
141
|
const moduleName = file.replace(".ts", "");
|
|
227
142
|
const parsedFunctions = parseFunctions(moduleName, source);
|
|
228
143
|
functions.push(...parsedFunctions);
|
|
@@ -232,12 +147,13 @@ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
|
|
|
232
147
|
spinner.info("Dry run - would deploy functions:");
|
|
233
148
|
for (const fn of functions) {
|
|
234
149
|
const icon = fn.type === "query" ? "\u{1F50D}" : fn.type === "mutation" ? "\u270F\uFE0F" : "\u26A1";
|
|
235
|
-
console.log(
|
|
150
|
+
console.log(chalk.dim(` ${icon} ${fn.name} (${fn.type})`));
|
|
236
151
|
}
|
|
237
152
|
return;
|
|
238
153
|
}
|
|
239
154
|
spinner.text = "Deploying functions...";
|
|
240
|
-
const
|
|
155
|
+
const envPath = environment !== "production" ? `/env/${environment}` : "";
|
|
156
|
+
const response = await fetch(`${API_URL}/projects/${projectId}${envPath}/deploy/functions`, {
|
|
241
157
|
method: "POST",
|
|
242
158
|
headers: {
|
|
243
159
|
"Content-Type": "application/json",
|
|
@@ -260,29 +176,29 @@ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
|
|
|
260
176
|
const mutations = functions.filter((f) => f.type === "mutation");
|
|
261
177
|
const actions = functions.filter((f) => f.type === "action");
|
|
262
178
|
if (queries.length > 0) {
|
|
263
|
-
console.log(
|
|
179
|
+
console.log(chalk.dim(`
|
|
264
180
|
Queries:`));
|
|
265
181
|
for (const fn of queries) {
|
|
266
|
-
console.log(
|
|
182
|
+
console.log(chalk.dim(` - ${fn.name}`));
|
|
267
183
|
}
|
|
268
184
|
}
|
|
269
185
|
if (mutations.length > 0) {
|
|
270
|
-
console.log(
|
|
186
|
+
console.log(chalk.dim(`
|
|
271
187
|
Mutations:`));
|
|
272
188
|
for (const fn of mutations) {
|
|
273
|
-
console.log(
|
|
189
|
+
console.log(chalk.dim(` - ${fn.name}`));
|
|
274
190
|
}
|
|
275
191
|
}
|
|
276
192
|
if (actions.length > 0) {
|
|
277
|
-
console.log(
|
|
193
|
+
console.log(chalk.dim(`
|
|
278
194
|
Actions:`));
|
|
279
195
|
for (const fn of actions) {
|
|
280
|
-
console.log(
|
|
196
|
+
console.log(chalk.dim(` - ${fn.name}`));
|
|
281
197
|
}
|
|
282
198
|
}
|
|
283
199
|
} catch (error) {
|
|
284
200
|
spinner.fail("Failed to deploy functions");
|
|
285
|
-
console.error(
|
|
201
|
+
console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
|
|
286
202
|
}
|
|
287
203
|
}
|
|
288
204
|
function parseSchema(source) {
|
|
@@ -323,15 +239,42 @@ function parseSchema(source) {
|
|
|
323
239
|
const colName = colMatch[1];
|
|
324
240
|
const colType = colMatch[2];
|
|
325
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
|
+
}
|
|
326
250
|
columns[colName] = {
|
|
327
251
|
type: colType,
|
|
328
|
-
primaryKey:
|
|
252
|
+
primaryKey: false,
|
|
253
|
+
// _id is always the primary key, ignore user-defined primary keys
|
|
329
254
|
notNull: modifiers.includes(".notNull()"),
|
|
330
|
-
unique: modifiers.includes(".unique()"),
|
|
255
|
+
unique: modifiers.includes(".unique()") || userWantsPrimaryKey,
|
|
256
|
+
// Make it unique if they wanted PK
|
|
331
257
|
hasDefault: modifiers.includes(".default("),
|
|
332
258
|
references: modifiers.match(/\.references\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1]
|
|
333
259
|
};
|
|
334
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
|
+
};
|
|
335
278
|
tables.push({ name: tableName, columns, source: tableSource });
|
|
336
279
|
}
|
|
337
280
|
return tables;
|
|
@@ -359,9 +302,10 @@ function getColumnSqlType(type) {
|
|
|
359
302
|
function buildColumnSql(colName, def, forAlterTable = false) {
|
|
360
303
|
const sqlType = getColumnSqlType(def.type);
|
|
361
304
|
let colSql = `${colName} ${sqlType}`;
|
|
362
|
-
|
|
305
|
+
const addingPrimaryKey = def.primaryKey && !forAlterTable;
|
|
306
|
+
if (addingPrimaryKey) colSql += " PRIMARY KEY";
|
|
363
307
|
if (def.notNull && !forAlterTable) colSql += " NOT NULL";
|
|
364
|
-
if (def.unique) colSql += " UNIQUE";
|
|
308
|
+
if (def.unique && !addingPrimaryKey) colSql += " UNIQUE";
|
|
365
309
|
if (def.hasDefault && def.type === "timestamp") {
|
|
366
310
|
colSql += " DEFAULT (datetime('now'))";
|
|
367
311
|
}
|
|
@@ -386,7 +330,6 @@ function generateSchemaSQL(tables) {
|
|
|
386
330
|
);
|
|
387
331
|
for (const [colName, colDef] of Object.entries(table.columns)) {
|
|
388
332
|
const def = colDef;
|
|
389
|
-
if (def.primaryKey) continue;
|
|
390
333
|
const alterColSql = buildColumnSql(colName, def, true);
|
|
391
334
|
statements.push(
|
|
392
335
|
`-- Add column if missing (will error if exists, which is OK)
|
|
@@ -451,7 +394,7 @@ function isPackageManagerInstalled(pm) {
|
|
|
451
394
|
}
|
|
452
395
|
async function initCommand(name, options) {
|
|
453
396
|
const credentials = await requireAuth();
|
|
454
|
-
console.log(
|
|
397
|
+
console.log(chalk2.bold("\n\u26A1 Create a new Tether project\n"));
|
|
455
398
|
let projectName = name;
|
|
456
399
|
if (!projectName) {
|
|
457
400
|
const response = await prompts({
|
|
@@ -462,7 +405,7 @@ async function initCommand(name, options) {
|
|
|
462
405
|
});
|
|
463
406
|
projectName = response.name;
|
|
464
407
|
if (!projectName) {
|
|
465
|
-
console.log(
|
|
408
|
+
console.log(chalk2.red("Project name is required"));
|
|
466
409
|
process.exit(1);
|
|
467
410
|
}
|
|
468
411
|
}
|
|
@@ -480,9 +423,9 @@ async function initCommand(name, options) {
|
|
|
480
423
|
});
|
|
481
424
|
const packageManager = pmResponse.packageManager || "npm";
|
|
482
425
|
if (!isPackageManagerInstalled(packageManager)) {
|
|
483
|
-
console.log(
|
|
426
|
+
console.log(chalk2.red(`
|
|
484
427
|
${packageManager} is not installed on your system.`));
|
|
485
|
-
console.log(
|
|
428
|
+
console.log(chalk2.dim(`Please install ${packageManager} and try again.
|
|
486
429
|
`));
|
|
487
430
|
process.exit(1);
|
|
488
431
|
}
|
|
@@ -502,8 +445,8 @@ ${packageManager} is not installed on your system.`));
|
|
|
502
445
|
});
|
|
503
446
|
template = response.template || "nuxt";
|
|
504
447
|
}
|
|
505
|
-
const projectPath =
|
|
506
|
-
if (await
|
|
448
|
+
const projectPath = path2.resolve(process.cwd(), projectName);
|
|
449
|
+
if (await fs2.pathExists(projectPath)) {
|
|
507
450
|
const { overwrite } = await prompts({
|
|
508
451
|
type: "confirm",
|
|
509
452
|
name: "overwrite",
|
|
@@ -511,10 +454,10 @@ ${packageManager} is not installed on your system.`));
|
|
|
511
454
|
initial: false
|
|
512
455
|
});
|
|
513
456
|
if (!overwrite) {
|
|
514
|
-
console.log(
|
|
457
|
+
console.log(chalk2.yellow("Cancelled"));
|
|
515
458
|
process.exit(0);
|
|
516
459
|
}
|
|
517
|
-
await
|
|
460
|
+
await fs2.remove(projectPath);
|
|
518
461
|
}
|
|
519
462
|
const spinner = ora2(`Scaffolding ${template} project...`).start();
|
|
520
463
|
let projectId;
|
|
@@ -562,17 +505,17 @@ ${packageManager} is not installed on your system.`));
|
|
|
562
505
|
} finally {
|
|
563
506
|
process.chdir(originalCwd);
|
|
564
507
|
}
|
|
565
|
-
console.log("\n" +
|
|
508
|
+
console.log("\n" + chalk2.green("\u2713") + " Project created successfully!\n");
|
|
566
509
|
console.log("Next steps:\n");
|
|
567
|
-
console.log(
|
|
510
|
+
console.log(chalk2.cyan(` cd ${projectName}`));
|
|
568
511
|
if (template === "vanilla") {
|
|
569
|
-
console.log(
|
|
512
|
+
console.log(chalk2.cyan(` ${packageManager} install`));
|
|
570
513
|
}
|
|
571
|
-
console.log(
|
|
572
|
-
console.log("\n" +
|
|
514
|
+
console.log(chalk2.cyan(` ${packageManager} run dev`));
|
|
515
|
+
console.log("\n" + chalk2.dim("For more information, visit https://tthr.io/docs\n"));
|
|
573
516
|
} catch (error) {
|
|
574
517
|
spinner.fail("Failed to create project");
|
|
575
|
-
console.error(
|
|
518
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
576
519
|
process.exit(1);
|
|
577
520
|
}
|
|
578
521
|
}
|
|
@@ -589,16 +532,16 @@ function getPackageRunnerCommand(pm) {
|
|
|
589
532
|
}
|
|
590
533
|
}
|
|
591
534
|
async function scaffoldNuxtProject(projectName, projectPath, spinner, packageManager) {
|
|
592
|
-
const parentDir =
|
|
535
|
+
const parentDir = path2.dirname(projectPath);
|
|
593
536
|
const runner = getPackageRunnerCommand(packageManager);
|
|
594
537
|
spinner.stop();
|
|
595
|
-
console.log(
|
|
538
|
+
console.log(chalk2.dim("\nRunning nuxi init...\n"));
|
|
596
539
|
try {
|
|
597
540
|
execSync(`${runner} nuxi@latest init ${projectName} -t minimal --packageManager ${packageManager} --gitInit false`, {
|
|
598
541
|
cwd: parentDir,
|
|
599
542
|
stdio: "inherit"
|
|
600
543
|
});
|
|
601
|
-
if (!await
|
|
544
|
+
if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
|
|
602
545
|
throw new Error("Nuxt project was not created successfully - package.json missing");
|
|
603
546
|
}
|
|
604
547
|
spinner.start();
|
|
@@ -611,17 +554,17 @@ async function scaffoldNuxtProject(projectName, projectPath, spinner, packageMan
|
|
|
611
554
|
}
|
|
612
555
|
}
|
|
613
556
|
async function scaffoldNextProject(projectName, projectPath, spinner, packageManager) {
|
|
614
|
-
const parentDir =
|
|
557
|
+
const parentDir = path2.dirname(projectPath);
|
|
615
558
|
const runner = getPackageRunnerCommand(packageManager);
|
|
616
559
|
const pmFlag = packageManager === "npm" ? "--use-npm" : packageManager === "pnpm" ? "--use-pnpm" : packageManager === "yarn" ? "--use-yarn" : "--use-bun";
|
|
617
560
|
spinner.stop();
|
|
618
|
-
console.log(
|
|
561
|
+
console.log(chalk2.dim("\nRunning create-next-app...\n"));
|
|
619
562
|
try {
|
|
620
563
|
execSync(`${runner} create-next-app@latest ${projectName} --typescript --eslint --tailwind --src-dir --app --import-alias "@/*" ${pmFlag}`, {
|
|
621
564
|
cwd: parentDir,
|
|
622
565
|
stdio: "inherit"
|
|
623
566
|
});
|
|
624
|
-
if (!await
|
|
567
|
+
if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
|
|
625
568
|
throw new Error("Next.js project was not created successfully - package.json missing");
|
|
626
569
|
}
|
|
627
570
|
spinner.start();
|
|
@@ -634,16 +577,16 @@ async function scaffoldNextProject(projectName, projectPath, spinner, packageMan
|
|
|
634
577
|
}
|
|
635
578
|
}
|
|
636
579
|
async function scaffoldSvelteKitProject(projectName, projectPath, spinner, packageManager) {
|
|
637
|
-
const parentDir =
|
|
580
|
+
const parentDir = path2.dirname(projectPath);
|
|
638
581
|
const runner = getPackageRunnerCommand(packageManager);
|
|
639
582
|
spinner.stop();
|
|
640
|
-
console.log(
|
|
583
|
+
console.log(chalk2.dim("\nRunning sv create...\n"));
|
|
641
584
|
try {
|
|
642
585
|
execSync(`${runner} sv create ${projectName} --template minimal --types ts --no-add-ons --no-install`, {
|
|
643
586
|
cwd: parentDir,
|
|
644
587
|
stdio: "inherit"
|
|
645
588
|
});
|
|
646
|
-
if (!await
|
|
589
|
+
if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
|
|
647
590
|
throw new Error("SvelteKit project was not created successfully - package.json missing");
|
|
648
591
|
}
|
|
649
592
|
spinner.start();
|
|
@@ -662,8 +605,8 @@ async function scaffoldSvelteKitProject(projectName, projectPath, spinner, packa
|
|
|
662
605
|
}
|
|
663
606
|
async function scaffoldVanillaProject(projectName, projectPath, spinner) {
|
|
664
607
|
spinner.text = "Creating vanilla TypeScript project...";
|
|
665
|
-
await
|
|
666
|
-
await
|
|
608
|
+
await fs2.ensureDir(projectPath);
|
|
609
|
+
await fs2.ensureDir(path2.join(projectPath, "src"));
|
|
667
610
|
const packageJson = {
|
|
668
611
|
name: projectName,
|
|
669
612
|
version: "0.0.1",
|
|
@@ -680,9 +623,9 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
|
|
|
680
623
|
"@types/node": "latest"
|
|
681
624
|
}
|
|
682
625
|
};
|
|
683
|
-
await
|
|
684
|
-
await
|
|
685
|
-
|
|
626
|
+
await fs2.writeJSON(path2.join(projectPath, "package.json"), packageJson, { spaces: 2 });
|
|
627
|
+
await fs2.writeJSON(
|
|
628
|
+
path2.join(projectPath, "tsconfig.json"),
|
|
686
629
|
{
|
|
687
630
|
compilerOptions: {
|
|
688
631
|
target: "ES2022",
|
|
@@ -703,8 +646,8 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
|
|
|
703
646
|
},
|
|
704
647
|
{ spaces: 2 }
|
|
705
648
|
);
|
|
706
|
-
await
|
|
707
|
-
|
|
649
|
+
await fs2.writeFile(
|
|
650
|
+
path2.join(projectPath, "src", "index.ts"),
|
|
708
651
|
`import { createClient } from '@tthr/client';
|
|
709
652
|
|
|
710
653
|
const tether = createClient({
|
|
@@ -723,8 +666,8 @@ async function main() {
|
|
|
723
666
|
main().catch(console.error);
|
|
724
667
|
`
|
|
725
668
|
);
|
|
726
|
-
await
|
|
727
|
-
|
|
669
|
+
await fs2.writeFile(
|
|
670
|
+
path2.join(projectPath, ".gitignore"),
|
|
728
671
|
`node_modules/
|
|
729
672
|
dist/
|
|
730
673
|
.env
|
|
@@ -734,11 +677,11 @@ dist/
|
|
|
734
677
|
);
|
|
735
678
|
}
|
|
736
679
|
async function addTetherFiles(projectPath, projectId, apiKey, template) {
|
|
737
|
-
await
|
|
738
|
-
await
|
|
680
|
+
await fs2.ensureDir(path2.join(projectPath, "tether"));
|
|
681
|
+
await fs2.ensureDir(path2.join(projectPath, "tether", "functions"));
|
|
739
682
|
const configPackage = template === "nuxt" ? "@tthr/vue" : template === "next" ? "@tthr/react" : template === "sveltekit" ? "@tthr/svelte" : "@tthr/client";
|
|
740
|
-
await
|
|
741
|
-
|
|
683
|
+
await fs2.writeFile(
|
|
684
|
+
path2.join(projectPath, "tether.config.ts"),
|
|
742
685
|
`import { defineConfig } from '${configPackage}';
|
|
743
686
|
|
|
744
687
|
export default defineConfig({
|
|
@@ -759,8 +702,8 @@ export default defineConfig({
|
|
|
759
702
|
});
|
|
760
703
|
`
|
|
761
704
|
);
|
|
762
|
-
await
|
|
763
|
-
|
|
705
|
+
await fs2.writeFile(
|
|
706
|
+
path2.join(projectPath, "tether", "schema.ts"),
|
|
764
707
|
`import { defineSchema, text, integer, timestamp } from '@tthr/schema';
|
|
765
708
|
|
|
766
709
|
export default defineSchema({
|
|
@@ -785,8 +728,8 @@ export default defineSchema({
|
|
|
785
728
|
});
|
|
786
729
|
`
|
|
787
730
|
);
|
|
788
|
-
await
|
|
789
|
-
|
|
731
|
+
await fs2.writeFile(
|
|
732
|
+
path2.join(projectPath, "tether", "functions", "posts.ts"),
|
|
790
733
|
`import { query, mutation, z } from '@tthr/server';
|
|
791
734
|
|
|
792
735
|
// List all posts
|
|
@@ -871,8 +814,8 @@ export const remove = mutation({
|
|
|
871
814
|
});
|
|
872
815
|
`
|
|
873
816
|
);
|
|
874
|
-
await
|
|
875
|
-
|
|
817
|
+
await fs2.writeFile(
|
|
818
|
+
path2.join(projectPath, "tether", "functions", "index.ts"),
|
|
876
819
|
`// Re-export all function modules
|
|
877
820
|
// The Tether SDK uses this file to discover custom functions
|
|
878
821
|
|
|
@@ -880,8 +823,8 @@ export * as posts from './posts';
|
|
|
880
823
|
export * as comments from './comments';
|
|
881
824
|
`
|
|
882
825
|
);
|
|
883
|
-
await
|
|
884
|
-
|
|
826
|
+
await fs2.writeFile(
|
|
827
|
+
path2.join(projectPath, "tether", "functions", "comments.ts"),
|
|
885
828
|
`import { query, mutation, z } from '@tthr/server';
|
|
886
829
|
|
|
887
830
|
// List all comments
|
|
@@ -953,27 +896,27 @@ export const remove = mutation({
|
|
|
953
896
|
TETHER_PROJECT_ID=${projectId}
|
|
954
897
|
TETHER_API_KEY=${apiKey}
|
|
955
898
|
`;
|
|
956
|
-
const envPath =
|
|
957
|
-
if (await
|
|
958
|
-
const existing = await
|
|
959
|
-
await
|
|
899
|
+
const envPath = path2.join(projectPath, ".env");
|
|
900
|
+
if (await fs2.pathExists(envPath)) {
|
|
901
|
+
const existing = await fs2.readFile(envPath, "utf-8");
|
|
902
|
+
await fs2.writeFile(envPath, existing + "\n" + envContent);
|
|
960
903
|
} else {
|
|
961
|
-
await
|
|
904
|
+
await fs2.writeFile(envPath, envContent);
|
|
962
905
|
}
|
|
963
|
-
const gitignorePath =
|
|
906
|
+
const gitignorePath = path2.join(projectPath, ".gitignore");
|
|
964
907
|
const tetherGitignore = `
|
|
965
908
|
# Tether
|
|
966
909
|
_generated/
|
|
967
910
|
.env
|
|
968
911
|
.env.local
|
|
969
912
|
`;
|
|
970
|
-
if (await
|
|
971
|
-
const existing = await
|
|
913
|
+
if (await fs2.pathExists(gitignorePath)) {
|
|
914
|
+
const existing = await fs2.readFile(gitignorePath, "utf-8");
|
|
972
915
|
if (!existing.includes("_generated/")) {
|
|
973
|
-
await
|
|
916
|
+
await fs2.writeFile(gitignorePath, existing + tetherGitignore);
|
|
974
917
|
}
|
|
975
918
|
} else {
|
|
976
|
-
await
|
|
919
|
+
await fs2.writeFile(gitignorePath, tetherGitignore.trim());
|
|
977
920
|
}
|
|
978
921
|
}
|
|
979
922
|
async function configureFramework(projectPath, template) {
|
|
@@ -986,9 +929,9 @@ async function configureFramework(projectPath, template) {
|
|
|
986
929
|
}
|
|
987
930
|
}
|
|
988
931
|
async function configureNuxt(projectPath) {
|
|
989
|
-
const configPath =
|
|
990
|
-
if (!await
|
|
991
|
-
await
|
|
932
|
+
const configPath = path2.join(projectPath, "nuxt.config.ts");
|
|
933
|
+
if (!await fs2.pathExists(configPath)) {
|
|
934
|
+
await fs2.writeFile(
|
|
992
935
|
configPath,
|
|
993
936
|
`// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
994
937
|
export default defineNuxtConfig({
|
|
@@ -1006,7 +949,7 @@ export default defineNuxtConfig({
|
|
|
1006
949
|
);
|
|
1007
950
|
return;
|
|
1008
951
|
}
|
|
1009
|
-
let config = await
|
|
952
|
+
let config = await fs2.readFile(configPath, "utf-8");
|
|
1010
953
|
if (config.includes("modules:")) {
|
|
1011
954
|
config = config.replace(
|
|
1012
955
|
/modules:\s*\[/,
|
|
@@ -1033,11 +976,11 @@ export default defineNuxtConfig({
|
|
|
1033
976
|
`
|
|
1034
977
|
);
|
|
1035
978
|
}
|
|
1036
|
-
await
|
|
979
|
+
await fs2.writeFile(configPath, config);
|
|
1037
980
|
}
|
|
1038
981
|
async function configureNext(projectPath) {
|
|
1039
|
-
const providersPath =
|
|
1040
|
-
await
|
|
982
|
+
const providersPath = path2.join(projectPath, "src", "app", "providers.tsx");
|
|
983
|
+
await fs2.writeFile(
|
|
1041
984
|
providersPath,
|
|
1042
985
|
`'use client';
|
|
1043
986
|
|
|
@@ -1055,9 +998,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
1055
998
|
}
|
|
1056
999
|
`
|
|
1057
1000
|
);
|
|
1058
|
-
const layoutPath =
|
|
1059
|
-
if (await
|
|
1060
|
-
let layout = await
|
|
1001
|
+
const layoutPath = path2.join(projectPath, "src", "app", "layout.tsx");
|
|
1002
|
+
if (await fs2.pathExists(layoutPath)) {
|
|
1003
|
+
let layout = await fs2.readFile(layoutPath, "utf-8");
|
|
1061
1004
|
if (!layout.includes("import { Providers }")) {
|
|
1062
1005
|
layout = layout.replace(
|
|
1063
1006
|
/^(import.*\n)+/m,
|
|
@@ -1070,24 +1013,24 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
1070
1013
|
"$1<Providers>$2</Providers>$3"
|
|
1071
1014
|
);
|
|
1072
1015
|
}
|
|
1073
|
-
await
|
|
1016
|
+
await fs2.writeFile(layoutPath, layout);
|
|
1074
1017
|
}
|
|
1075
|
-
const envLocalPath =
|
|
1018
|
+
const envLocalPath = path2.join(projectPath, ".env.local");
|
|
1076
1019
|
const nextEnvContent = `# Tether Configuration (client-side)
|
|
1077
1020
|
NEXT_PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
|
|
1078
1021
|
`;
|
|
1079
|
-
if (await
|
|
1080
|
-
const existing = await
|
|
1081
|
-
await
|
|
1022
|
+
if (await fs2.pathExists(envLocalPath)) {
|
|
1023
|
+
const existing = await fs2.readFile(envLocalPath, "utf-8");
|
|
1024
|
+
await fs2.writeFile(envLocalPath, existing + "\n" + nextEnvContent);
|
|
1082
1025
|
} else {
|
|
1083
|
-
await
|
|
1026
|
+
await fs2.writeFile(envLocalPath, nextEnvContent);
|
|
1084
1027
|
}
|
|
1085
1028
|
}
|
|
1086
1029
|
async function configureSvelteKit(projectPath) {
|
|
1087
|
-
const libPath =
|
|
1088
|
-
await
|
|
1089
|
-
await
|
|
1090
|
-
|
|
1030
|
+
const libPath = path2.join(projectPath, "src", "lib");
|
|
1031
|
+
await fs2.ensureDir(libPath);
|
|
1032
|
+
await fs2.writeFile(
|
|
1033
|
+
path2.join(libPath, "tether.ts"),
|
|
1091
1034
|
`import { createClient } from '@tthr/svelte';
|
|
1092
1035
|
import { PUBLIC_TETHER_PROJECT_ID, PUBLIC_TETHER_URL } from '$env/static/public';
|
|
1093
1036
|
|
|
@@ -1097,19 +1040,19 @@ export const tether = createClient({
|
|
|
1097
1040
|
});
|
|
1098
1041
|
`
|
|
1099
1042
|
);
|
|
1100
|
-
const envPath =
|
|
1043
|
+
const envPath = path2.join(projectPath, ".env");
|
|
1101
1044
|
const svelteEnvContent = `# Tether Configuration (public)
|
|
1102
1045
|
PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
|
|
1103
1046
|
`;
|
|
1104
|
-
if (await
|
|
1105
|
-
const existing = await
|
|
1047
|
+
if (await fs2.pathExists(envPath)) {
|
|
1048
|
+
const existing = await fs2.readFile(envPath, "utf-8");
|
|
1106
1049
|
if (!existing.includes("PUBLIC_TETHER_")) {
|
|
1107
|
-
await
|
|
1050
|
+
await fs2.writeFile(envPath, existing + "\n" + svelteEnvContent);
|
|
1108
1051
|
}
|
|
1109
1052
|
}
|
|
1110
1053
|
}
|
|
1111
|
-
function getInstallCommand(pm,
|
|
1112
|
-
const devFlag =
|
|
1054
|
+
function getInstallCommand(pm, isDev6 = false) {
|
|
1055
|
+
const devFlag = isDev6 ? pm === "npm" ? "-D" : pm === "yarn" ? "-D" : pm === "pnpm" ? "-D" : "-d" : "";
|
|
1113
1056
|
return `${pm} ${pm === "npm" ? "install" : "add"} ${devFlag}`.trim();
|
|
1114
1057
|
}
|
|
1115
1058
|
async function installTetherPackages(projectPath, template, packageManager) {
|
|
@@ -1154,15 +1097,15 @@ async function installTetherPackages(projectPath, template, packageManager) {
|
|
|
1154
1097
|
stdio: "pipe"
|
|
1155
1098
|
});
|
|
1156
1099
|
} catch (error) {
|
|
1157
|
-
console.warn(
|
|
1100
|
+
console.warn(chalk2.yellow("\nNote: Some Tether packages may not be published yet."));
|
|
1158
1101
|
}
|
|
1159
1102
|
}
|
|
1160
1103
|
async function createDemoPage(projectPath, template) {
|
|
1161
1104
|
if (template === "nuxt") {
|
|
1162
|
-
const nuxt4AppVuePath =
|
|
1163
|
-
const nuxt3AppVuePath =
|
|
1164
|
-
const appVuePath = await
|
|
1165
|
-
await
|
|
1105
|
+
const nuxt4AppVuePath = path2.join(projectPath, "app", "app.vue");
|
|
1106
|
+
const nuxt3AppVuePath = path2.join(projectPath, "app.vue");
|
|
1107
|
+
const appVuePath = await fs2.pathExists(path2.join(projectPath, "app")) ? nuxt4AppVuePath : nuxt3AppVuePath;
|
|
1108
|
+
await fs2.writeFile(
|
|
1166
1109
|
appVuePath,
|
|
1167
1110
|
`<template>
|
|
1168
1111
|
<TetherWelcome />
|
|
@@ -1170,8 +1113,8 @@ async function createDemoPage(projectPath, template) {
|
|
|
1170
1113
|
`
|
|
1171
1114
|
);
|
|
1172
1115
|
} else if (template === "next") {
|
|
1173
|
-
const pagePath =
|
|
1174
|
-
await
|
|
1116
|
+
const pagePath = path2.join(projectPath, "src", "app", "page.tsx");
|
|
1117
|
+
await fs2.writeFile(
|
|
1175
1118
|
pagePath,
|
|
1176
1119
|
`'use client';
|
|
1177
1120
|
|
|
@@ -1281,8 +1224,8 @@ export default function Home() {
|
|
|
1281
1224
|
`
|
|
1282
1225
|
);
|
|
1283
1226
|
} else if (template === "sveltekit") {
|
|
1284
|
-
const pagePath =
|
|
1285
|
-
await
|
|
1227
|
+
const pagePath = path2.join(projectPath, "src", "routes", "+page.svelte");
|
|
1228
|
+
await fs2.writeFile(
|
|
1286
1229
|
pagePath,
|
|
1287
1230
|
`<script lang="ts">
|
|
1288
1231
|
import { onMount } from 'svelte';
|
|
@@ -1548,369 +1491,340 @@ export default function Home() {
|
|
|
1548
1491
|
}
|
|
1549
1492
|
|
|
1550
1493
|
// src/commands/dev.ts
|
|
1551
|
-
import
|
|
1494
|
+
import chalk3 from "chalk";
|
|
1552
1495
|
import ora3 from "ora";
|
|
1553
|
-
import
|
|
1554
|
-
import
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1496
|
+
import fs3 from "fs-extra";
|
|
1497
|
+
import path3 from "path";
|
|
1498
|
+
import { spawn } from "child_process";
|
|
1499
|
+
import { watch } from "chokidar";
|
|
1500
|
+
var frameworkProcess = null;
|
|
1501
|
+
var schemaWatcher = null;
|
|
1502
|
+
var functionsWatcher = null;
|
|
1503
|
+
var isGenerating = false;
|
|
1504
|
+
var pendingGenerate = false;
|
|
1505
|
+
async function runGenerate(spinner) {
|
|
1506
|
+
if (isGenerating) {
|
|
1507
|
+
pendingGenerate = true;
|
|
1508
|
+
return true;
|
|
1562
1509
|
}
|
|
1563
|
-
|
|
1564
|
-
const port = options.port || String(config.dev?.port || 3001);
|
|
1565
|
-
console.log(chalk4.bold("\n\u26A1 Starting Tether development server\n"));
|
|
1566
|
-
const spinner = ora3("Starting server...").start();
|
|
1510
|
+
isGenerating = true;
|
|
1567
1511
|
try {
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
console.log(chalk4.yellow("\n\nShutting down...\n"));
|
|
1575
|
-
process.exit(0);
|
|
1576
|
-
});
|
|
1577
|
-
await new Promise(() => {
|
|
1578
|
-
});
|
|
1512
|
+
const { generateTypes } = await import("./generate-YQ4QRPXF.js");
|
|
1513
|
+
spinner.text = "Regenerating types...";
|
|
1514
|
+
await generateTypes({ silent: true });
|
|
1515
|
+
spinner.succeed("Types regenerated");
|
|
1516
|
+
spinner.start("Watching for changes...");
|
|
1517
|
+
return true;
|
|
1579
1518
|
} catch (error) {
|
|
1580
|
-
spinner.fail("
|
|
1581
|
-
console.error(
|
|
1582
|
-
|
|
1519
|
+
spinner.fail("Type generation failed");
|
|
1520
|
+
console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1521
|
+
spinner.start("Watching for changes...");
|
|
1522
|
+
return false;
|
|
1523
|
+
} finally {
|
|
1524
|
+
isGenerating = false;
|
|
1525
|
+
if (pendingGenerate) {
|
|
1526
|
+
pendingGenerate = false;
|
|
1527
|
+
setTimeout(() => runGenerate(spinner), 100);
|
|
1528
|
+
}
|
|
1583
1529
|
}
|
|
1584
1530
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
|
|
1594
|
-
if (!schemaMatch) return tables;
|
|
1595
|
-
const schemaContent = schemaMatch[1];
|
|
1596
|
-
const tableStartRegex = /(\w+)\s*:\s*\{/g;
|
|
1597
|
-
let match;
|
|
1598
|
-
while ((match = tableStartRegex.exec(schemaContent)) !== null) {
|
|
1599
|
-
const tableName = match[1];
|
|
1600
|
-
const startOffset = match.index + match[0].length;
|
|
1601
|
-
let braceCount = 1;
|
|
1602
|
-
let endOffset = startOffset;
|
|
1603
|
-
for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
|
|
1604
|
-
const char = schemaContent[i];
|
|
1531
|
+
async function validateSchema(schemaPath) {
|
|
1532
|
+
try {
|
|
1533
|
+
const content = await fs3.readFile(schemaPath, "utf-8");
|
|
1534
|
+
if (!content.includes("defineSchema")) {
|
|
1535
|
+
return { valid: false, error: "Missing defineSchema() call" };
|
|
1536
|
+
}
|
|
1537
|
+
let braceCount = 0;
|
|
1538
|
+
for (const char of content) {
|
|
1605
1539
|
if (char === "{") braceCount++;
|
|
1606
1540
|
else if (char === "}") braceCount--;
|
|
1607
|
-
|
|
1541
|
+
if (braceCount < 0) {
|
|
1542
|
+
return { valid: false, error: "Unmatched closing brace" };
|
|
1543
|
+
}
|
|
1608
1544
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
tables.push({ name: tableName, columns });
|
|
1612
|
-
}
|
|
1613
|
-
return tables;
|
|
1614
|
-
}
|
|
1615
|
-
function parseColumns(content) {
|
|
1616
|
-
const columns = [];
|
|
1617
|
-
const columnRegex = /(\w+)\s*:\s*(\w+)\s*\(\s*\)([^,\n}]*)/g;
|
|
1618
|
-
let match;
|
|
1619
|
-
while ((match = columnRegex.exec(content)) !== null) {
|
|
1620
|
-
const name = match[1];
|
|
1621
|
-
const schemaType = match[2];
|
|
1622
|
-
const modifiers = match[3] || "";
|
|
1623
|
-
const nullable = !modifiers.includes(".notNull()") && !modifiers.includes(".primaryKey()");
|
|
1624
|
-
const primaryKey = modifiers.includes(".primaryKey()");
|
|
1625
|
-
columns.push({
|
|
1626
|
-
name,
|
|
1627
|
-
type: schemaTypeToTS(schemaType),
|
|
1628
|
-
nullable,
|
|
1629
|
-
primaryKey
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
return columns;
|
|
1633
|
-
}
|
|
1634
|
-
function schemaTypeToTS(schemaType) {
|
|
1635
|
-
switch (schemaType) {
|
|
1636
|
-
case "text":
|
|
1637
|
-
return "string";
|
|
1638
|
-
case "integer":
|
|
1639
|
-
return "number";
|
|
1640
|
-
case "real":
|
|
1641
|
-
return "number";
|
|
1642
|
-
case "boolean":
|
|
1643
|
-
return "boolean";
|
|
1644
|
-
case "timestamp":
|
|
1645
|
-
return "string";
|
|
1646
|
-
case "json":
|
|
1647
|
-
return "unknown";
|
|
1648
|
-
case "blob":
|
|
1649
|
-
return "Uint8Array";
|
|
1650
|
-
default:
|
|
1651
|
-
return "unknown";
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
function tableNameToInterface(tableName) {
|
|
1655
|
-
let name = tableName;
|
|
1656
|
-
if (name.endsWith("ies")) {
|
|
1657
|
-
name = name.slice(0, -3) + "y";
|
|
1658
|
-
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
1659
|
-
name = name.slice(0, -1);
|
|
1660
|
-
}
|
|
1661
|
-
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
1662
|
-
}
|
|
1663
|
-
function generateDbFile(tables) {
|
|
1664
|
-
const lines = [
|
|
1665
|
-
"// Auto-generated by Tether CLI - do not edit manually",
|
|
1666
|
-
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1667
|
-
"",
|
|
1668
|
-
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
1669
|
-
""
|
|
1670
|
-
];
|
|
1671
|
-
for (const table of tables) {
|
|
1672
|
-
const interfaceName = tableNameToInterface(table.name);
|
|
1673
|
-
lines.push(`export interface ${interfaceName} {`);
|
|
1674
|
-
for (const col of table.columns) {
|
|
1675
|
-
const typeStr = col.nullable ? `${col.type} | null` : col.type;
|
|
1676
|
-
lines.push(` ${col.name}: ${typeStr};`);
|
|
1545
|
+
if (braceCount !== 0) {
|
|
1546
|
+
return { valid: false, error: "Unmatched opening brace" };
|
|
1677
1547
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1548
|
+
return { valid: true };
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
return { valid: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1680
1551
|
}
|
|
1681
|
-
lines.push("export interface Schema {");
|
|
1682
|
-
for (const table of tables) {
|
|
1683
|
-
const interfaceName = tableNameToInterface(table.name);
|
|
1684
|
-
lines.push(` ${table.name}: ${interfaceName};`);
|
|
1685
|
-
}
|
|
1686
|
-
lines.push("}");
|
|
1687
|
-
lines.push("");
|
|
1688
|
-
lines.push("// Database client with typed tables");
|
|
1689
|
-
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
1690
|
-
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
1691
|
-
lines.push("");
|
|
1692
|
-
return lines.join("\n");
|
|
1693
1552
|
}
|
|
1694
|
-
async function
|
|
1695
|
-
const
|
|
1696
|
-
if (!await
|
|
1697
|
-
return
|
|
1553
|
+
async function validateFunctions(functionsDir) {
|
|
1554
|
+
const errors = [];
|
|
1555
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
1556
|
+
return { valid: true, errors: [] };
|
|
1698
1557
|
}
|
|
1699
|
-
const files = await
|
|
1558
|
+
const files = await fs3.readdir(functionsDir);
|
|
1700
1559
|
for (const file of files) {
|
|
1701
1560
|
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
1702
|
-
const filePath =
|
|
1703
|
-
const stat = await
|
|
1561
|
+
const filePath = path3.join(functionsDir, file);
|
|
1562
|
+
const stat = await fs3.stat(filePath);
|
|
1704
1563
|
if (!stat.isFile()) continue;
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1564
|
+
try {
|
|
1565
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
1566
|
+
const hasExports = /export\s+const\s+\w+\s*=\s*(query|mutation|action)\s*\(/.test(content);
|
|
1567
|
+
if (!hasExports && !file.includes("index")) {
|
|
1568
|
+
errors.push(`${file}: No query, mutation, or action exports found`);
|
|
1569
|
+
}
|
|
1570
|
+
let braceCount = 0;
|
|
1571
|
+
for (const char of content) {
|
|
1572
|
+
if (char === "{") braceCount++;
|
|
1573
|
+
else if (char === "}") braceCount--;
|
|
1574
|
+
}
|
|
1575
|
+
if (braceCount !== 0) {
|
|
1576
|
+
errors.push(`${file}: Mismatched braces`);
|
|
1577
|
+
}
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
errors.push(`${file}: ${error instanceof Error ? error.message : "Read error"}`);
|
|
1714
1580
|
}
|
|
1715
1581
|
}
|
|
1716
|
-
return
|
|
1582
|
+
return { valid: errors.length === 0, errors };
|
|
1717
1583
|
}
|
|
1718
|
-
|
|
1719
|
-
const
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1730
|
-
"",
|
|
1731
|
-
"import { createApiProxy } from '@tthr/client';",
|
|
1732
|
-
"",
|
|
1733
|
-
"/**",
|
|
1734
|
-
" * API function reference type for useQuery/useMutation.",
|
|
1735
|
-
' * The _name property contains the function path (e.g., "users.list").',
|
|
1736
|
-
" */",
|
|
1737
|
-
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
1738
|
-
" _name: string;",
|
|
1739
|
-
" _args?: TArgs;",
|
|
1740
|
-
" _result?: TResult;",
|
|
1741
|
-
"}",
|
|
1742
|
-
""
|
|
1743
|
-
];
|
|
1744
|
-
if (moduleMap.size > 0) {
|
|
1745
|
-
for (const [moduleName, fnNames] of moduleMap) {
|
|
1746
|
-
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
1747
|
-
lines.push(`export interface ${interfaceName} {`);
|
|
1748
|
-
for (const fnName of fnNames) {
|
|
1749
|
-
lines.push(` ${fnName}: ApiFunction;`);
|
|
1750
|
-
}
|
|
1751
|
-
lines.push("}");
|
|
1752
|
-
lines.push("");
|
|
1584
|
+
function startFrameworkDev(command, port, spinner) {
|
|
1585
|
+
const [cmd, ...args] = command.split(" ");
|
|
1586
|
+
const portArgs = ["--port", port];
|
|
1587
|
+
spinner.text = `Starting ${command}...`;
|
|
1588
|
+
const child = spawn(cmd, [...args, ...portArgs], {
|
|
1589
|
+
cwd: process.cwd(),
|
|
1590
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
1591
|
+
shell: true,
|
|
1592
|
+
env: {
|
|
1593
|
+
...process.env,
|
|
1594
|
+
FORCE_COLOR: "1"
|
|
1753
1595
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1596
|
+
});
|
|
1597
|
+
let started = false;
|
|
1598
|
+
child.stdout?.on("data", (data) => {
|
|
1599
|
+
const output = data.toString();
|
|
1600
|
+
process.stdout.write(output);
|
|
1601
|
+
if (!started && (output.includes("Local:") || output.includes("ready") || output.includes("listening"))) {
|
|
1602
|
+
started = true;
|
|
1603
|
+
spinner.succeed(`Framework dev server running`);
|
|
1604
|
+
spinner.start("Watching for changes...");
|
|
1758
1605
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
lines.push("export const api = createApiProxy<Api>();");
|
|
1777
|
-
lines.push("");
|
|
1778
|
-
return lines.join("\n");
|
|
1606
|
+
});
|
|
1607
|
+
child.stderr?.on("data", (data) => {
|
|
1608
|
+
const output = data.toString();
|
|
1609
|
+
if (!output.includes("ExperimentalWarning") && !output.includes("--trace-warnings")) {
|
|
1610
|
+
process.stderr.write(chalk3.dim(output));
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
child.on("error", (error) => {
|
|
1614
|
+
spinner.fail(`Failed to start framework: ${error.message}`);
|
|
1615
|
+
});
|
|
1616
|
+
child.on("exit", (code) => {
|
|
1617
|
+
if (code !== 0 && code !== null) {
|
|
1618
|
+
console.log(chalk3.yellow(`
|
|
1619
|
+
Framework process exited with code ${code}`));
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
return child;
|
|
1779
1623
|
}
|
|
1780
|
-
|
|
1624
|
+
function cleanup() {
|
|
1625
|
+
if (frameworkProcess) {
|
|
1626
|
+
frameworkProcess.kill("SIGTERM");
|
|
1627
|
+
frameworkProcess = null;
|
|
1628
|
+
}
|
|
1629
|
+
if (schemaWatcher) {
|
|
1630
|
+
schemaWatcher.close();
|
|
1631
|
+
schemaWatcher = null;
|
|
1632
|
+
}
|
|
1633
|
+
if (functionsWatcher) {
|
|
1634
|
+
functionsWatcher.close();
|
|
1635
|
+
functionsWatcher = null;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
async function devCommand(options) {
|
|
1781
1639
|
await requireAuth();
|
|
1782
|
-
const configPath =
|
|
1783
|
-
if (!await
|
|
1784
|
-
console.log(
|
|
1785
|
-
console.log(
|
|
1640
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
1641
|
+
if (!await fs3.pathExists(configPath)) {
|
|
1642
|
+
console.log(chalk3.red("\nError: Not a Tether project"));
|
|
1643
|
+
console.log(chalk3.dim("Run `tthr init` to create a new project\n"));
|
|
1786
1644
|
process.exit(1);
|
|
1787
1645
|
}
|
|
1788
|
-
|
|
1789
|
-
const
|
|
1646
|
+
const config = await loadConfig();
|
|
1647
|
+
const port = options.port || String(config.dev?.port || 3001);
|
|
1648
|
+
const schemaPath = resolvePath(config.schema);
|
|
1649
|
+
const functionsDir = resolvePath(config.functions);
|
|
1650
|
+
const outputDir = resolvePath(config.output);
|
|
1651
|
+
const framework = config.framework || await detectFramework();
|
|
1652
|
+
const devCmd = config.dev?.command || getFrameworkDevCommand(framework);
|
|
1653
|
+
const environment = options.env || config.environment || "development";
|
|
1654
|
+
const isLocal = options.local ?? false;
|
|
1655
|
+
console.log(chalk3.bold("\n\u26A1 Starting Tether development server\n"));
|
|
1656
|
+
if (framework !== "unknown") {
|
|
1657
|
+
console.log(chalk3.dim(` Framework: ${framework}`));
|
|
1658
|
+
}
|
|
1659
|
+
if (devCmd) {
|
|
1660
|
+
console.log(chalk3.dim(` Dev command: ${devCmd}`));
|
|
1661
|
+
}
|
|
1662
|
+
console.log(chalk3.dim(` Environment: ${environment}${isLocal ? " (local)" : " (cloud)"}`));
|
|
1663
|
+
console.log(chalk3.dim(` Schema: ${path3.relative(process.cwd(), schemaPath)}`));
|
|
1664
|
+
console.log(chalk3.dim(` Functions: ${path3.relative(process.cwd(), functionsDir)}`));
|
|
1665
|
+
console.log(chalk3.dim(` Output: ${path3.relative(process.cwd(), outputDir)}`));
|
|
1666
|
+
console.log("");
|
|
1667
|
+
const spinner = ora3("Initialising...").start();
|
|
1668
|
+
process.on("SIGINT", () => {
|
|
1669
|
+
spinner.stop();
|
|
1670
|
+
console.log(chalk3.yellow("\n\nShutting down...\n"));
|
|
1671
|
+
cleanup();
|
|
1672
|
+
process.exit(0);
|
|
1673
|
+
});
|
|
1674
|
+
process.on("SIGTERM", () => {
|
|
1675
|
+
cleanup();
|
|
1676
|
+
process.exit(0);
|
|
1677
|
+
});
|
|
1790
1678
|
try {
|
|
1791
|
-
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
if (!await fs6.pathExists(schemaPath)) {
|
|
1796
|
-
spinner.fail("Schema file not found");
|
|
1797
|
-
console.log(chalk5.dim(`Expected: ${schemaPath}
|
|
1798
|
-
`));
|
|
1799
|
-
process.exit(1);
|
|
1679
|
+
spinner.text = "Validating schema...";
|
|
1680
|
+
const schemaValidation = await validateSchema(schemaPath);
|
|
1681
|
+
if (!schemaValidation.valid) {
|
|
1682
|
+
spinner.warn(`Schema validation warning: ${schemaValidation.error}`);
|
|
1800
1683
|
}
|
|
1801
|
-
|
|
1802
|
-
const
|
|
1803
|
-
if (
|
|
1804
|
-
spinner.warn("
|
|
1805
|
-
|
|
1806
|
-
|
|
1684
|
+
spinner.text = "Validating functions...";
|
|
1685
|
+
const functionsValidation = await validateFunctions(functionsDir);
|
|
1686
|
+
if (!functionsValidation.valid) {
|
|
1687
|
+
spinner.warn("Function validation warnings:");
|
|
1688
|
+
for (const error of functionsValidation.errors) {
|
|
1689
|
+
console.log(chalk3.yellow(` - ${error}`));
|
|
1690
|
+
}
|
|
1807
1691
|
}
|
|
1808
|
-
spinner.text =
|
|
1809
|
-
await
|
|
1810
|
-
await
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
)
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1692
|
+
spinner.text = "Generating types...";
|
|
1693
|
+
const { generateTypes } = await import("./generate-YQ4QRPXF.js");
|
|
1694
|
+
await generateTypes({ silent: true });
|
|
1695
|
+
spinner.succeed("Types generated");
|
|
1696
|
+
spinner.start("Setting up file watchers...");
|
|
1697
|
+
if (await fs3.pathExists(schemaPath)) {
|
|
1698
|
+
schemaWatcher = watch(schemaPath, {
|
|
1699
|
+
ignoreInitial: true,
|
|
1700
|
+
awaitWriteFinish: {
|
|
1701
|
+
stabilityThreshold: 300,
|
|
1702
|
+
pollInterval: 100
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
schemaWatcher.on("change", async () => {
|
|
1706
|
+
console.log(chalk3.cyan("\n Schema changed, validating..."));
|
|
1707
|
+
const validation = await validateSchema(schemaPath);
|
|
1708
|
+
if (!validation.valid) {
|
|
1709
|
+
console.log(chalk3.yellow(` Schema error: ${validation.error}`));
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
await runGenerate(spinner);
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
if (await fs3.pathExists(functionsDir)) {
|
|
1716
|
+
functionsWatcher = watch(path3.join(functionsDir, "**/*.{ts,js}"), {
|
|
1717
|
+
ignoreInitial: true,
|
|
1718
|
+
awaitWriteFinish: {
|
|
1719
|
+
stabilityThreshold: 300,
|
|
1720
|
+
pollInterval: 100
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
functionsWatcher.on("all", async (event, filePath) => {
|
|
1724
|
+
const relativePath = path3.relative(functionsDir, filePath);
|
|
1725
|
+
console.log(chalk3.cyan(`
|
|
1726
|
+
Function ${event}: ${relativePath}`));
|
|
1727
|
+
const validation = await validateFunctions(functionsDir);
|
|
1728
|
+
if (!validation.valid) {
|
|
1729
|
+
for (const error of validation.errors) {
|
|
1730
|
+
console.log(chalk3.yellow(` Warning: ${error}`));
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
await runGenerate(spinner);
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
spinner.succeed("File watchers ready");
|
|
1737
|
+
if (devCmd && !options.skipFramework) {
|
|
1738
|
+
spinner.start(`Starting ${framework} dev server...`);
|
|
1739
|
+
frameworkProcess = startFrameworkDev(devCmd, port, spinner);
|
|
1740
|
+
} else {
|
|
1741
|
+
spinner.start("Watching for changes...");
|
|
1742
|
+
console.log("\n" + chalk3.cyan(` Tether dev ready`));
|
|
1743
|
+
console.log(chalk3.dim(` Watching schema and functions for changes`));
|
|
1744
|
+
console.log(chalk3.dim(` Press Ctrl+C to stop
|
|
1835
1745
|
`));
|
|
1746
|
+
}
|
|
1747
|
+
await new Promise(() => {
|
|
1748
|
+
});
|
|
1836
1749
|
} catch (error) {
|
|
1837
|
-
spinner.fail("Failed to
|
|
1838
|
-
console.error(
|
|
1750
|
+
spinner.fail("Failed to start dev server");
|
|
1751
|
+
console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1752
|
+
cleanup();
|
|
1839
1753
|
process.exit(1);
|
|
1840
1754
|
}
|
|
1841
1755
|
}
|
|
1842
1756
|
|
|
1843
1757
|
// src/commands/login.ts
|
|
1844
|
-
import
|
|
1845
|
-
import
|
|
1758
|
+
import chalk4 from "chalk";
|
|
1759
|
+
import ora4 from "ora";
|
|
1846
1760
|
import os from "os";
|
|
1847
1761
|
var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
1848
1762
|
var API_URL3 = isDev3 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
1849
1763
|
var AUTH_URL = isDev3 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
|
|
1850
1764
|
async function loginCommand() {
|
|
1851
|
-
console.log(
|
|
1765
|
+
console.log(chalk4.bold("\u26A1 Login to Tether\n"));
|
|
1852
1766
|
const existing = await getCredentials();
|
|
1853
1767
|
if (existing) {
|
|
1854
|
-
console.log(
|
|
1855
|
-
console.log(
|
|
1768
|
+
console.log(chalk4.green("\u2713") + ` Already logged in as ${chalk4.cyan(existing.email)}`);
|
|
1769
|
+
console.log(chalk4.dim("\nRun `tthr logout` to sign out\n"));
|
|
1856
1770
|
return;
|
|
1857
1771
|
}
|
|
1858
|
-
const spinner =
|
|
1772
|
+
const spinner = ora4("Generating authentication code...").start();
|
|
1859
1773
|
try {
|
|
1860
1774
|
const deviceCode = await requestDeviceCode();
|
|
1861
1775
|
spinner.stop();
|
|
1862
1776
|
const authUrl = `${AUTH_URL}/${deviceCode.userCode}`;
|
|
1863
|
-
console.log(
|
|
1864
|
-
console.log(
|
|
1777
|
+
console.log(chalk4.dim("Open this URL in your browser to authenticate:\n"));
|
|
1778
|
+
console.log(chalk4.cyan.bold(` ${authUrl}
|
|
1865
1779
|
`));
|
|
1866
|
-
console.log(
|
|
1780
|
+
console.log(chalk4.dim(`Your code: ${chalk4.white.bold(deviceCode.userCode)}
|
|
1867
1781
|
`));
|
|
1868
1782
|
const credentials = await pollForApproval(deviceCode.deviceCode, deviceCode.interval, deviceCode.expiresIn);
|
|
1869
1783
|
await saveCredentials(credentials);
|
|
1870
|
-
console.log(
|
|
1784
|
+
console.log(chalk4.green("\u2713") + ` Logged in as ${chalk4.cyan(credentials.email)}`);
|
|
1871
1785
|
console.log();
|
|
1872
1786
|
} catch (error) {
|
|
1873
1787
|
spinner.fail("Authentication failed");
|
|
1874
|
-
console.error(
|
|
1875
|
-
console.log(
|
|
1788
|
+
console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1789
|
+
console.log(chalk4.dim("\nTry again or visit https://tthr.io/docs/cli for help\n"));
|
|
1876
1790
|
process.exit(1);
|
|
1877
1791
|
}
|
|
1878
1792
|
}
|
|
1879
1793
|
async function logoutCommand() {
|
|
1880
|
-
console.log(
|
|
1794
|
+
console.log(chalk4.bold("\n\u26A1 Logout from Tether\n"));
|
|
1881
1795
|
const credentials = await getCredentials();
|
|
1882
1796
|
if (!credentials) {
|
|
1883
|
-
console.log(
|
|
1797
|
+
console.log(chalk4.dim("Not logged in\n"));
|
|
1884
1798
|
return;
|
|
1885
1799
|
}
|
|
1886
|
-
const spinner =
|
|
1800
|
+
const spinner = ora4("Logging out...").start();
|
|
1887
1801
|
try {
|
|
1888
1802
|
await clearCredentials();
|
|
1889
|
-
spinner.succeed(`Logged out from ${
|
|
1803
|
+
spinner.succeed(`Logged out from ${chalk4.cyan(credentials.email)}`);
|
|
1890
1804
|
console.log();
|
|
1891
1805
|
} catch (error) {
|
|
1892
1806
|
spinner.fail("Logout failed");
|
|
1893
|
-
console.error(
|
|
1807
|
+
console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1894
1808
|
process.exit(1);
|
|
1895
1809
|
}
|
|
1896
1810
|
}
|
|
1897
1811
|
async function whoamiCommand() {
|
|
1898
1812
|
const credentials = await getCredentials();
|
|
1899
1813
|
if (!credentials) {
|
|
1900
|
-
console.log(
|
|
1901
|
-
console.log(
|
|
1814
|
+
console.log(chalk4.dim("\nNot logged in"));
|
|
1815
|
+
console.log(chalk4.dim("Run `tthr login` to authenticate\n"));
|
|
1902
1816
|
return;
|
|
1903
1817
|
}
|
|
1904
|
-
console.log(
|
|
1905
|
-
console.log(` Email: ${
|
|
1906
|
-
console.log(` User ID: ${
|
|
1818
|
+
console.log(chalk4.bold("\n\u26A1 Current user\n"));
|
|
1819
|
+
console.log(` Email: ${chalk4.cyan(credentials.email)}`);
|
|
1820
|
+
console.log(` User ID: ${chalk4.dim(credentials.userId)}`);
|
|
1907
1821
|
if (credentials.expiresAt) {
|
|
1908
1822
|
const expiresAt = new Date(credentials.expiresAt);
|
|
1909
1823
|
const now = /* @__PURE__ */ new Date();
|
|
1910
1824
|
if (expiresAt > now) {
|
|
1911
|
-
console.log(` Session: ${
|
|
1825
|
+
console.log(` Session: ${chalk4.green("Active")}`);
|
|
1912
1826
|
} else {
|
|
1913
|
-
console.log(` Session: ${
|
|
1827
|
+
console.log(` Session: ${chalk4.yellow("Expired")}`);
|
|
1914
1828
|
}
|
|
1915
1829
|
}
|
|
1916
1830
|
console.log();
|
|
@@ -1952,20 +1866,20 @@ async function requestDeviceCode() {
|
|
|
1952
1866
|
async function pollForApproval(deviceCode, interval, expiresIn) {
|
|
1953
1867
|
const startTime = Date.now();
|
|
1954
1868
|
const expiresAt = startTime + expiresIn * 1e3;
|
|
1955
|
-
const spinner =
|
|
1869
|
+
const spinner = ora4("").start();
|
|
1956
1870
|
const updateCountdown = () => {
|
|
1957
1871
|
const remaining = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1e3));
|
|
1958
1872
|
const mins = Math.floor(remaining / 60);
|
|
1959
1873
|
const secs = remaining % 60;
|
|
1960
1874
|
const timeStr = mins > 0 ? `${mins}:${secs.toString().padStart(2, "0")}` : `${secs}s`;
|
|
1961
|
-
spinner.text = `Waiting for approval... ${
|
|
1875
|
+
spinner.text = `Waiting for approval... ${chalk4.dim(`(${timeStr} remaining)`)}`;
|
|
1962
1876
|
};
|
|
1963
1877
|
updateCountdown();
|
|
1964
1878
|
const countdownInterval = setInterval(updateCountdown, 1e3);
|
|
1965
1879
|
try {
|
|
1966
1880
|
while (Date.now() < expiresAt) {
|
|
1967
1881
|
await sleep(interval * 1e3);
|
|
1968
|
-
spinner.text = `Checking... ${
|
|
1882
|
+
spinner.text = `Checking... ${chalk4.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
|
|
1969
1883
|
const response = await fetch(`${API_URL3}/auth/device/${deviceCode}`, {
|
|
1970
1884
|
method: "GET"
|
|
1971
1885
|
}).catch(() => null);
|
|
@@ -1998,10 +1912,10 @@ function sleep(ms) {
|
|
|
1998
1912
|
}
|
|
1999
1913
|
|
|
2000
1914
|
// src/commands/update.ts
|
|
2001
|
-
import
|
|
2002
|
-
import
|
|
2003
|
-
import
|
|
2004
|
-
import
|
|
1915
|
+
import chalk5 from "chalk";
|
|
1916
|
+
import ora5 from "ora";
|
|
1917
|
+
import fs4 from "fs-extra";
|
|
1918
|
+
import path4 from "path";
|
|
2005
1919
|
import { execSync as execSync2 } from "child_process";
|
|
2006
1920
|
var TETHER_PACKAGES = [
|
|
2007
1921
|
"@tthr/client",
|
|
@@ -2013,29 +1927,29 @@ var TETHER_PACKAGES = [
|
|
|
2013
1927
|
];
|
|
2014
1928
|
function detectPackageManager() {
|
|
2015
1929
|
const cwd = process.cwd();
|
|
2016
|
-
if (
|
|
1930
|
+
if (fs4.existsSync(path4.join(cwd, "bun.lockb")) || fs4.existsSync(path4.join(cwd, "bun.lock"))) {
|
|
2017
1931
|
return "bun";
|
|
2018
1932
|
}
|
|
2019
|
-
if (
|
|
1933
|
+
if (fs4.existsSync(path4.join(cwd, "pnpm-lock.yaml"))) {
|
|
2020
1934
|
return "pnpm";
|
|
2021
1935
|
}
|
|
2022
|
-
if (
|
|
1936
|
+
if (fs4.existsSync(path4.join(cwd, "yarn.lock"))) {
|
|
2023
1937
|
return "yarn";
|
|
2024
1938
|
}
|
|
2025
1939
|
return "npm";
|
|
2026
1940
|
}
|
|
2027
|
-
function getInstallCommand2(pm, packages,
|
|
1941
|
+
function getInstallCommand2(pm, packages, isDev6) {
|
|
2028
1942
|
const packagesStr = packages.join(" ");
|
|
2029
1943
|
switch (pm) {
|
|
2030
1944
|
case "bun":
|
|
2031
|
-
return
|
|
1945
|
+
return isDev6 ? `bun add -d ${packagesStr}` : `bun add ${packagesStr}`;
|
|
2032
1946
|
case "pnpm":
|
|
2033
|
-
return
|
|
1947
|
+
return isDev6 ? `pnpm add -D ${packagesStr}` : `pnpm add ${packagesStr}`;
|
|
2034
1948
|
case "yarn":
|
|
2035
|
-
return
|
|
1949
|
+
return isDev6 ? `yarn add -D ${packagesStr}` : `yarn add ${packagesStr}`;
|
|
2036
1950
|
case "npm":
|
|
2037
1951
|
default:
|
|
2038
|
-
return
|
|
1952
|
+
return isDev6 ? `npm install -D ${packagesStr}` : `npm install ${packagesStr}`;
|
|
2039
1953
|
}
|
|
2040
1954
|
}
|
|
2041
1955
|
async function getLatestVersion2(packageName) {
|
|
@@ -2049,14 +1963,14 @@ async function getLatestVersion2(packageName) {
|
|
|
2049
1963
|
}
|
|
2050
1964
|
}
|
|
2051
1965
|
async function updateCommand(options) {
|
|
2052
|
-
const packageJsonPath =
|
|
2053
|
-
if (!await
|
|
2054
|
-
console.log(
|
|
2055
|
-
console.log(
|
|
1966
|
+
const packageJsonPath = path4.resolve(process.cwd(), "package.json");
|
|
1967
|
+
if (!await fs4.pathExists(packageJsonPath)) {
|
|
1968
|
+
console.log(chalk5.red("\nError: No package.json found"));
|
|
1969
|
+
console.log(chalk5.dim("Make sure you're in the root of your project\n"));
|
|
2056
1970
|
process.exit(1);
|
|
2057
1971
|
}
|
|
2058
|
-
console.log(
|
|
2059
|
-
const packageJson = await
|
|
1972
|
+
console.log(chalk5.bold("\n\u26A1 Updating Tether packages\n"));
|
|
1973
|
+
const packageJson = await fs4.readJson(packageJsonPath);
|
|
2060
1974
|
const deps = packageJson.dependencies || {};
|
|
2061
1975
|
const devDeps = packageJson.devDependencies || {};
|
|
2062
1976
|
const installedDeps = [];
|
|
@@ -2069,13 +1983,13 @@ async function updateCommand(options) {
|
|
|
2069
1983
|
}
|
|
2070
1984
|
}
|
|
2071
1985
|
if (installedDeps.length === 0) {
|
|
2072
|
-
console.log(
|
|
2073
|
-
console.log(
|
|
1986
|
+
console.log(chalk5.yellow(" No updatable Tether packages found in package.json"));
|
|
1987
|
+
console.log(chalk5.dim(" Tether packages start with @tthr/ (workspace: dependencies are skipped)\n"));
|
|
2074
1988
|
return;
|
|
2075
1989
|
}
|
|
2076
|
-
console.log(
|
|
1990
|
+
console.log(chalk5.dim(` Found ${installedDeps.length} Tether package(s):
|
|
2077
1991
|
`));
|
|
2078
|
-
const spinner =
|
|
1992
|
+
const spinner = ora5("Checking for updates...").start();
|
|
2079
1993
|
const packagesToUpdate = [];
|
|
2080
1994
|
for (const pkg of installedDeps) {
|
|
2081
1995
|
const latestVersion = await getLatestVersion2(pkg.name);
|
|
@@ -2089,32 +2003,32 @@ async function updateCommand(options) {
|
|
|
2089
2003
|
isDev: pkg.isDev
|
|
2090
2004
|
});
|
|
2091
2005
|
} else {
|
|
2092
|
-
console.log(
|
|
2006
|
+
console.log(chalk5.dim(` ${pkg.name}@${currentClean} (up to date)`));
|
|
2093
2007
|
}
|
|
2094
2008
|
}
|
|
2095
2009
|
}
|
|
2096
2010
|
spinner.stop();
|
|
2097
2011
|
if (packagesToUpdate.length === 0) {
|
|
2098
|
-
console.log(
|
|
2012
|
+
console.log(chalk5.green("\n\u2713 All Tether packages are up to date\n"));
|
|
2099
2013
|
return;
|
|
2100
2014
|
}
|
|
2101
|
-
console.log(
|
|
2015
|
+
console.log(chalk5.cyan("\n Updates available:\n"));
|
|
2102
2016
|
for (const pkg of packagesToUpdate) {
|
|
2103
2017
|
console.log(
|
|
2104
|
-
|
|
2018
|
+
chalk5.white(` ${pkg.name}`) + chalk5.red(` ${pkg.current}`) + chalk5.dim(" \u2192 ") + chalk5.green(`${pkg.latest}`) + (pkg.isDev ? chalk5.dim(" (dev)") : "")
|
|
2105
2019
|
);
|
|
2106
2020
|
}
|
|
2107
2021
|
if (options.dryRun) {
|
|
2108
|
-
console.log(
|
|
2022
|
+
console.log(chalk5.yellow("\n Dry run - no packages were updated\n"));
|
|
2109
2023
|
return;
|
|
2110
2024
|
}
|
|
2111
2025
|
const pm = detectPackageManager();
|
|
2112
|
-
console.log(
|
|
2026
|
+
console.log(chalk5.dim(`
|
|
2113
2027
|
Using ${pm} to update packages...
|
|
2114
2028
|
`));
|
|
2115
2029
|
const depsToUpdate = packagesToUpdate.filter((p) => !p.isDev).map((p) => `${p.name}@latest`);
|
|
2116
2030
|
const devDepsToUpdate = packagesToUpdate.filter((p) => p.isDev).map((p) => `${p.name}@latest`);
|
|
2117
|
-
const updateSpinner =
|
|
2031
|
+
const updateSpinner = ora5("Installing updates...").start();
|
|
2118
2032
|
try {
|
|
2119
2033
|
if (depsToUpdate.length > 0) {
|
|
2120
2034
|
const cmd = getInstallCommand2(pm, depsToUpdate, false);
|
|
@@ -2125,43 +2039,43 @@ async function updateCommand(options) {
|
|
|
2125
2039
|
execSync2(cmd, { stdio: "pipe", cwd: process.cwd() });
|
|
2126
2040
|
}
|
|
2127
2041
|
updateSpinner.succeed("Packages updated successfully");
|
|
2128
|
-
console.log(
|
|
2042
|
+
console.log(chalk5.green(`
|
|
2129
2043
|
\u2713 Updated ${packagesToUpdate.length} package(s)
|
|
2130
2044
|
`));
|
|
2131
2045
|
} catch (error) {
|
|
2132
2046
|
updateSpinner.fail("Failed to update packages");
|
|
2133
|
-
console.error(
|
|
2047
|
+
console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2134
2048
|
process.exit(1);
|
|
2135
2049
|
}
|
|
2136
2050
|
}
|
|
2137
2051
|
|
|
2138
2052
|
// src/commands/exec.ts
|
|
2139
|
-
import
|
|
2140
|
-
import
|
|
2141
|
-
import
|
|
2142
|
-
import
|
|
2053
|
+
import chalk6 from "chalk";
|
|
2054
|
+
import ora6 from "ora";
|
|
2055
|
+
import fs5 from "fs-extra";
|
|
2056
|
+
import path5 from "path";
|
|
2143
2057
|
var isDev4 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
2144
2058
|
var API_URL4 = isDev4 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
2145
2059
|
async function execCommand(sql) {
|
|
2146
2060
|
if (!sql || sql.trim() === "") {
|
|
2147
|
-
console.log(
|
|
2148
|
-
console.log(
|
|
2061
|
+
console.log(chalk6.red("\nError: SQL query required"));
|
|
2062
|
+
console.log(chalk6.dim('Usage: tthr exec "SELECT * FROM table_name"\n'));
|
|
2149
2063
|
process.exit(1);
|
|
2150
2064
|
}
|
|
2151
2065
|
const credentials = await requireAuth();
|
|
2152
|
-
const envPath =
|
|
2066
|
+
const envPath = path5.resolve(process.cwd(), ".env");
|
|
2153
2067
|
let projectId;
|
|
2154
|
-
if (await
|
|
2155
|
-
const envContent = await
|
|
2068
|
+
if (await fs5.pathExists(envPath)) {
|
|
2069
|
+
const envContent = await fs5.readFile(envPath, "utf-8");
|
|
2156
2070
|
const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
|
|
2157
2071
|
projectId = match?.[1]?.trim();
|
|
2158
2072
|
}
|
|
2159
2073
|
if (!projectId) {
|
|
2160
|
-
console.log(
|
|
2161
|
-
console.log(
|
|
2074
|
+
console.log(chalk6.red("\nError: Project ID not found"));
|
|
2075
|
+
console.log(chalk6.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
|
|
2162
2076
|
process.exit(1);
|
|
2163
2077
|
}
|
|
2164
|
-
const spinner =
|
|
2078
|
+
const spinner = ora6("Executing query...").start();
|
|
2165
2079
|
try {
|
|
2166
2080
|
const response = await fetch(`${API_URL4}/projects/${projectId}/exec`, {
|
|
2167
2081
|
method: "POST",
|
|
@@ -2181,24 +2095,24 @@ async function execCommand(sql) {
|
|
|
2181
2095
|
if (result.columns && result.rows) {
|
|
2182
2096
|
console.log();
|
|
2183
2097
|
if (result.rows.length === 0) {
|
|
2184
|
-
console.log(
|
|
2098
|
+
console.log(chalk6.dim(" No rows returned"));
|
|
2185
2099
|
} else {
|
|
2186
|
-
console.log(
|
|
2187
|
-
console.log(
|
|
2100
|
+
console.log(chalk6.bold(" " + result.columns.join(" ")));
|
|
2101
|
+
console.log(chalk6.dim(" " + result.columns.map(() => "--------").join(" ")));
|
|
2188
2102
|
for (const row of result.rows) {
|
|
2189
2103
|
const values = result.columns.map((col) => {
|
|
2190
2104
|
const val = row[col];
|
|
2191
|
-
if (val === null) return
|
|
2105
|
+
if (val === null) return chalk6.dim("NULL");
|
|
2192
2106
|
if (typeof val === "object") return JSON.stringify(val);
|
|
2193
2107
|
return String(val);
|
|
2194
2108
|
});
|
|
2195
2109
|
console.log(" " + values.join(" "));
|
|
2196
2110
|
}
|
|
2197
|
-
console.log(
|
|
2111
|
+
console.log(chalk6.dim(`
|
|
2198
2112
|
${result.rows.length} row(s)`));
|
|
2199
2113
|
}
|
|
2200
2114
|
} else if (result.rowsAffected !== void 0) {
|
|
2201
|
-
console.log(
|
|
2115
|
+
console.log(chalk6.dim(`
|
|
2202
2116
|
${result.rowsAffected} row(s) affected`));
|
|
2203
2117
|
}
|
|
2204
2118
|
console.log();
|
|
@@ -2208,16 +2122,182 @@ async function execCommand(sql) {
|
|
|
2208
2122
|
}
|
|
2209
2123
|
}
|
|
2210
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
|
+
|
|
2211
2286
|
// src/index.ts
|
|
2212
2287
|
var program = new Command();
|
|
2213
2288
|
program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version("0.0.1");
|
|
2214
2289
|
program.command("init [name]").description("Create a new Tether project").option("-t, --template <template>", "Project template (vue, svelte, react, vanilla)", "vue").action(initCommand);
|
|
2215
|
-
program.command("dev").description("Start local development server with hot reload").option("-p, --port <port>", "Port to run on", "
|
|
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);
|
|
2216
2291
|
program.command("generate").alias("gen").description("Generate types from schema").action(generateCommand);
|
|
2217
|
-
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);
|
|
2218
2293
|
program.command("login").description("Authenticate with Tether").action(loginCommand);
|
|
2219
2294
|
program.command("logout").description("Sign out of Tether").action(logoutCommand);
|
|
2220
2295
|
program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
|
|
2221
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);
|
|
2222
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);
|
|
2223
2303
|
program.parse();
|