tthr 0.0.32 → 0.0.34
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-ZWLVHKNL.js +414 -0
- package/dist/generate-26HERX3T.js +8 -0
- package/dist/index.js +431 -693
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -1,145 +1,57 @@
|
|
|
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-ZWLVHKNL.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
|
-
console.log(
|
|
141
|
-
console.log(
|
|
142
|
-
console.log(
|
|
52
|
+
console.log(chalk.bold("\n\u26A1 Deploying to Tether\n"));
|
|
53
|
+
console.log(chalk.dim(` Project: ${projectId}`));
|
|
54
|
+
console.log(chalk.dim(` API: ${API_URL}
|
|
143
55
|
`));
|
|
144
56
|
const config = await loadConfig();
|
|
145
57
|
const deploySchema = options.schema || !options.schema && !options.functions;
|
|
@@ -152,29 +64,29 @@ async function deployCommand(options) {
|
|
|
152
64
|
const functionsDir = resolvePath(config.functions);
|
|
153
65
|
await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, options.dryRun);
|
|
154
66
|
}
|
|
155
|
-
console.log(
|
|
67
|
+
console.log(chalk.green("\n\u2713 Deployment complete\n"));
|
|
156
68
|
}
|
|
157
69
|
async function deploySchemaToServer(projectId, token, schemaPath, dryRun) {
|
|
158
70
|
const spinner = ora("Reading schema...").start();
|
|
159
71
|
try {
|
|
160
|
-
if (!await
|
|
72
|
+
if (!await fs.pathExists(schemaPath)) {
|
|
161
73
|
spinner.warn("No schema file found");
|
|
162
|
-
const relativePath =
|
|
163
|
-
console.log(
|
|
74
|
+
const relativePath = path.relative(process.cwd(), schemaPath);
|
|
75
|
+
console.log(chalk.dim(` Create ${relativePath} to define your database schema
|
|
164
76
|
`));
|
|
165
77
|
return;
|
|
166
78
|
}
|
|
167
|
-
const schemaSource = await
|
|
79
|
+
const schemaSource = await fs.readFile(schemaPath, "utf-8");
|
|
168
80
|
const tables = parseSchema(schemaSource);
|
|
169
81
|
spinner.text = `Found ${tables.length} table(s)`;
|
|
170
82
|
const sql = generateSchemaSQL(tables);
|
|
171
83
|
if (dryRun) {
|
|
172
84
|
spinner.info("Dry run - would deploy schema:");
|
|
173
85
|
for (const table of tables) {
|
|
174
|
-
console.log(
|
|
86
|
+
console.log(chalk.dim(` - ${table.name} (${Object.keys(table.columns).length} columns)`));
|
|
175
87
|
}
|
|
176
|
-
console.log(
|
|
177
|
-
console.log(
|
|
88
|
+
console.log(chalk.bold("\nGenerated SQL:\n"));
|
|
89
|
+
console.log(chalk.cyan(sql));
|
|
178
90
|
return;
|
|
179
91
|
}
|
|
180
92
|
spinner.text = "Deploying schema...";
|
|
@@ -200,20 +112,20 @@ async function deploySchemaToServer(projectId, token, schemaPath, dryRun) {
|
|
|
200
112
|
spinner.succeed(`Schema deployed (${tables.length} table(s))`);
|
|
201
113
|
} catch (error) {
|
|
202
114
|
spinner.fail("Failed to deploy schema");
|
|
203
|
-
console.error(
|
|
115
|
+
console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
|
|
204
116
|
}
|
|
205
117
|
}
|
|
206
118
|
async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
|
|
207
119
|
const spinner = ora("Reading functions...").start();
|
|
208
120
|
try {
|
|
209
|
-
if (!await
|
|
121
|
+
if (!await fs.pathExists(functionsDir)) {
|
|
210
122
|
spinner.warn("No functions directory found");
|
|
211
|
-
const relativePath =
|
|
212
|
-
console.log(
|
|
123
|
+
const relativePath = path.relative(process.cwd(), functionsDir);
|
|
124
|
+
console.log(chalk.dim(` Create ${relativePath}/ to define your API functions
|
|
213
125
|
`));
|
|
214
126
|
return;
|
|
215
127
|
}
|
|
216
|
-
const files = await
|
|
128
|
+
const files = await fs.readdir(functionsDir);
|
|
217
129
|
const tsFiles = files.filter((f) => f.endsWith(".ts"));
|
|
218
130
|
if (tsFiles.length === 0) {
|
|
219
131
|
spinner.info("No function files found");
|
|
@@ -221,8 +133,8 @@ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
|
|
|
221
133
|
}
|
|
222
134
|
const functions = [];
|
|
223
135
|
for (const file of tsFiles) {
|
|
224
|
-
const filePath =
|
|
225
|
-
const source = await
|
|
136
|
+
const filePath = path.join(functionsDir, file);
|
|
137
|
+
const source = await fs.readFile(filePath, "utf-8");
|
|
226
138
|
const moduleName = file.replace(".ts", "");
|
|
227
139
|
const parsedFunctions = parseFunctions(moduleName, source);
|
|
228
140
|
functions.push(...parsedFunctions);
|
|
@@ -232,7 +144,7 @@ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
|
|
|
232
144
|
spinner.info("Dry run - would deploy functions:");
|
|
233
145
|
for (const fn of functions) {
|
|
234
146
|
const icon = fn.type === "query" ? "\u{1F50D}" : fn.type === "mutation" ? "\u270F\uFE0F" : "\u26A1";
|
|
235
|
-
console.log(
|
|
147
|
+
console.log(chalk.dim(` ${icon} ${fn.name} (${fn.type})`));
|
|
236
148
|
}
|
|
237
149
|
return;
|
|
238
150
|
}
|
|
@@ -260,29 +172,29 @@ async function deployFunctionsToServer(projectId, token, functionsDir, dryRun) {
|
|
|
260
172
|
const mutations = functions.filter((f) => f.type === "mutation");
|
|
261
173
|
const actions = functions.filter((f) => f.type === "action");
|
|
262
174
|
if (queries.length > 0) {
|
|
263
|
-
console.log(
|
|
175
|
+
console.log(chalk.dim(`
|
|
264
176
|
Queries:`));
|
|
265
177
|
for (const fn of queries) {
|
|
266
|
-
console.log(
|
|
178
|
+
console.log(chalk.dim(` - ${fn.name}`));
|
|
267
179
|
}
|
|
268
180
|
}
|
|
269
181
|
if (mutations.length > 0) {
|
|
270
|
-
console.log(
|
|
182
|
+
console.log(chalk.dim(`
|
|
271
183
|
Mutations:`));
|
|
272
184
|
for (const fn of mutations) {
|
|
273
|
-
console.log(
|
|
185
|
+
console.log(chalk.dim(` - ${fn.name}`));
|
|
274
186
|
}
|
|
275
187
|
}
|
|
276
188
|
if (actions.length > 0) {
|
|
277
|
-
console.log(
|
|
189
|
+
console.log(chalk.dim(`
|
|
278
190
|
Actions:`));
|
|
279
191
|
for (const fn of actions) {
|
|
280
|
-
console.log(
|
|
192
|
+
console.log(chalk.dim(` - ${fn.name}`));
|
|
281
193
|
}
|
|
282
194
|
}
|
|
283
195
|
} catch (error) {
|
|
284
196
|
spinner.fail("Failed to deploy functions");
|
|
285
|
-
console.error(
|
|
197
|
+
console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
|
|
286
198
|
}
|
|
287
199
|
}
|
|
288
200
|
function parseSchema(source) {
|
|
@@ -451,7 +363,7 @@ function isPackageManagerInstalled(pm) {
|
|
|
451
363
|
}
|
|
452
364
|
async function initCommand(name, options) {
|
|
453
365
|
const credentials = await requireAuth();
|
|
454
|
-
console.log(
|
|
366
|
+
console.log(chalk2.bold("\n\u26A1 Create a new Tether project\n"));
|
|
455
367
|
let projectName = name;
|
|
456
368
|
if (!projectName) {
|
|
457
369
|
const response = await prompts({
|
|
@@ -462,7 +374,7 @@ async function initCommand(name, options) {
|
|
|
462
374
|
});
|
|
463
375
|
projectName = response.name;
|
|
464
376
|
if (!projectName) {
|
|
465
|
-
console.log(
|
|
377
|
+
console.log(chalk2.red("Project name is required"));
|
|
466
378
|
process.exit(1);
|
|
467
379
|
}
|
|
468
380
|
}
|
|
@@ -480,9 +392,9 @@ async function initCommand(name, options) {
|
|
|
480
392
|
});
|
|
481
393
|
const packageManager = pmResponse.packageManager || "npm";
|
|
482
394
|
if (!isPackageManagerInstalled(packageManager)) {
|
|
483
|
-
console.log(
|
|
395
|
+
console.log(chalk2.red(`
|
|
484
396
|
${packageManager} is not installed on your system.`));
|
|
485
|
-
console.log(
|
|
397
|
+
console.log(chalk2.dim(`Please install ${packageManager} and try again.
|
|
486
398
|
`));
|
|
487
399
|
process.exit(1);
|
|
488
400
|
}
|
|
@@ -502,8 +414,8 @@ ${packageManager} is not installed on your system.`));
|
|
|
502
414
|
});
|
|
503
415
|
template = response.template || "nuxt";
|
|
504
416
|
}
|
|
505
|
-
const projectPath =
|
|
506
|
-
if (await
|
|
417
|
+
const projectPath = path2.resolve(process.cwd(), projectName);
|
|
418
|
+
if (await fs2.pathExists(projectPath)) {
|
|
507
419
|
const { overwrite } = await prompts({
|
|
508
420
|
type: "confirm",
|
|
509
421
|
name: "overwrite",
|
|
@@ -511,10 +423,10 @@ ${packageManager} is not installed on your system.`));
|
|
|
511
423
|
initial: false
|
|
512
424
|
});
|
|
513
425
|
if (!overwrite) {
|
|
514
|
-
console.log(
|
|
426
|
+
console.log(chalk2.yellow("Cancelled"));
|
|
515
427
|
process.exit(0);
|
|
516
428
|
}
|
|
517
|
-
await
|
|
429
|
+
await fs2.remove(projectPath);
|
|
518
430
|
}
|
|
519
431
|
const spinner = ora2(`Scaffolding ${template} project...`).start();
|
|
520
432
|
let projectId;
|
|
@@ -562,17 +474,17 @@ ${packageManager} is not installed on your system.`));
|
|
|
562
474
|
} finally {
|
|
563
475
|
process.chdir(originalCwd);
|
|
564
476
|
}
|
|
565
|
-
console.log("\n" +
|
|
477
|
+
console.log("\n" + chalk2.green("\u2713") + " Project created successfully!\n");
|
|
566
478
|
console.log("Next steps:\n");
|
|
567
|
-
console.log(
|
|
479
|
+
console.log(chalk2.cyan(` cd ${projectName}`));
|
|
568
480
|
if (template === "vanilla") {
|
|
569
|
-
console.log(
|
|
481
|
+
console.log(chalk2.cyan(` ${packageManager} install`));
|
|
570
482
|
}
|
|
571
|
-
console.log(
|
|
572
|
-
console.log("\n" +
|
|
483
|
+
console.log(chalk2.cyan(` ${packageManager} run dev`));
|
|
484
|
+
console.log("\n" + chalk2.dim("For more information, visit https://tthr.io/docs\n"));
|
|
573
485
|
} catch (error) {
|
|
574
486
|
spinner.fail("Failed to create project");
|
|
575
|
-
console.error(
|
|
487
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
576
488
|
process.exit(1);
|
|
577
489
|
}
|
|
578
490
|
}
|
|
@@ -589,16 +501,16 @@ function getPackageRunnerCommand(pm) {
|
|
|
589
501
|
}
|
|
590
502
|
}
|
|
591
503
|
async function scaffoldNuxtProject(projectName, projectPath, spinner, packageManager) {
|
|
592
|
-
const parentDir =
|
|
504
|
+
const parentDir = path2.dirname(projectPath);
|
|
593
505
|
const runner = getPackageRunnerCommand(packageManager);
|
|
594
506
|
spinner.stop();
|
|
595
|
-
console.log(
|
|
507
|
+
console.log(chalk2.dim("\nRunning nuxi init...\n"));
|
|
596
508
|
try {
|
|
597
509
|
execSync(`${runner} nuxi@latest init ${projectName} -t minimal --packageManager ${packageManager} --gitInit false`, {
|
|
598
510
|
cwd: parentDir,
|
|
599
511
|
stdio: "inherit"
|
|
600
512
|
});
|
|
601
|
-
if (!await
|
|
513
|
+
if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
|
|
602
514
|
throw new Error("Nuxt project was not created successfully - package.json missing");
|
|
603
515
|
}
|
|
604
516
|
spinner.start();
|
|
@@ -611,17 +523,17 @@ async function scaffoldNuxtProject(projectName, projectPath, spinner, packageMan
|
|
|
611
523
|
}
|
|
612
524
|
}
|
|
613
525
|
async function scaffoldNextProject(projectName, projectPath, spinner, packageManager) {
|
|
614
|
-
const parentDir =
|
|
526
|
+
const parentDir = path2.dirname(projectPath);
|
|
615
527
|
const runner = getPackageRunnerCommand(packageManager);
|
|
616
528
|
const pmFlag = packageManager === "npm" ? "--use-npm" : packageManager === "pnpm" ? "--use-pnpm" : packageManager === "yarn" ? "--use-yarn" : "--use-bun";
|
|
617
529
|
spinner.stop();
|
|
618
|
-
console.log(
|
|
530
|
+
console.log(chalk2.dim("\nRunning create-next-app...\n"));
|
|
619
531
|
try {
|
|
620
532
|
execSync(`${runner} create-next-app@latest ${projectName} --typescript --eslint --tailwind --src-dir --app --import-alias "@/*" ${pmFlag}`, {
|
|
621
533
|
cwd: parentDir,
|
|
622
534
|
stdio: "inherit"
|
|
623
535
|
});
|
|
624
|
-
if (!await
|
|
536
|
+
if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
|
|
625
537
|
throw new Error("Next.js project was not created successfully - package.json missing");
|
|
626
538
|
}
|
|
627
539
|
spinner.start();
|
|
@@ -634,16 +546,16 @@ async function scaffoldNextProject(projectName, projectPath, spinner, packageMan
|
|
|
634
546
|
}
|
|
635
547
|
}
|
|
636
548
|
async function scaffoldSvelteKitProject(projectName, projectPath, spinner, packageManager) {
|
|
637
|
-
const parentDir =
|
|
549
|
+
const parentDir = path2.dirname(projectPath);
|
|
638
550
|
const runner = getPackageRunnerCommand(packageManager);
|
|
639
551
|
spinner.stop();
|
|
640
|
-
console.log(
|
|
552
|
+
console.log(chalk2.dim("\nRunning sv create...\n"));
|
|
641
553
|
try {
|
|
642
554
|
execSync(`${runner} sv create ${projectName} --template minimal --types ts --no-add-ons --no-install`, {
|
|
643
555
|
cwd: parentDir,
|
|
644
556
|
stdio: "inherit"
|
|
645
557
|
});
|
|
646
|
-
if (!await
|
|
558
|
+
if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
|
|
647
559
|
throw new Error("SvelteKit project was not created successfully - package.json missing");
|
|
648
560
|
}
|
|
649
561
|
spinner.start();
|
|
@@ -662,8 +574,8 @@ async function scaffoldSvelteKitProject(projectName, projectPath, spinner, packa
|
|
|
662
574
|
}
|
|
663
575
|
async function scaffoldVanillaProject(projectName, projectPath, spinner) {
|
|
664
576
|
spinner.text = "Creating vanilla TypeScript project...";
|
|
665
|
-
await
|
|
666
|
-
await
|
|
577
|
+
await fs2.ensureDir(projectPath);
|
|
578
|
+
await fs2.ensureDir(path2.join(projectPath, "src"));
|
|
667
579
|
const packageJson = {
|
|
668
580
|
name: projectName,
|
|
669
581
|
version: "0.0.1",
|
|
@@ -680,9 +592,9 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
|
|
|
680
592
|
"@types/node": "latest"
|
|
681
593
|
}
|
|
682
594
|
};
|
|
683
|
-
await
|
|
684
|
-
await
|
|
685
|
-
|
|
595
|
+
await fs2.writeJSON(path2.join(projectPath, "package.json"), packageJson, { spaces: 2 });
|
|
596
|
+
await fs2.writeJSON(
|
|
597
|
+
path2.join(projectPath, "tsconfig.json"),
|
|
686
598
|
{
|
|
687
599
|
compilerOptions: {
|
|
688
600
|
target: "ES2022",
|
|
@@ -703,8 +615,8 @@ async function scaffoldVanillaProject(projectName, projectPath, spinner) {
|
|
|
703
615
|
},
|
|
704
616
|
{ spaces: 2 }
|
|
705
617
|
);
|
|
706
|
-
await
|
|
707
|
-
|
|
618
|
+
await fs2.writeFile(
|
|
619
|
+
path2.join(projectPath, "src", "index.ts"),
|
|
708
620
|
`import { createClient } from '@tthr/client';
|
|
709
621
|
|
|
710
622
|
const tether = createClient({
|
|
@@ -723,8 +635,8 @@ async function main() {
|
|
|
723
635
|
main().catch(console.error);
|
|
724
636
|
`
|
|
725
637
|
);
|
|
726
|
-
await
|
|
727
|
-
|
|
638
|
+
await fs2.writeFile(
|
|
639
|
+
path2.join(projectPath, ".gitignore"),
|
|
728
640
|
`node_modules/
|
|
729
641
|
dist/
|
|
730
642
|
.env
|
|
@@ -734,11 +646,11 @@ dist/
|
|
|
734
646
|
);
|
|
735
647
|
}
|
|
736
648
|
async function addTetherFiles(projectPath, projectId, apiKey, template) {
|
|
737
|
-
await
|
|
738
|
-
await
|
|
649
|
+
await fs2.ensureDir(path2.join(projectPath, "tether"));
|
|
650
|
+
await fs2.ensureDir(path2.join(projectPath, "tether", "functions"));
|
|
739
651
|
const configPackage = template === "nuxt" ? "@tthr/vue" : template === "next" ? "@tthr/react" : template === "sveltekit" ? "@tthr/svelte" : "@tthr/client";
|
|
740
|
-
await
|
|
741
|
-
|
|
652
|
+
await fs2.writeFile(
|
|
653
|
+
path2.join(projectPath, "tether.config.ts"),
|
|
742
654
|
`import { defineConfig } from '${configPackage}';
|
|
743
655
|
|
|
744
656
|
export default defineConfig({
|
|
@@ -759,8 +671,8 @@ export default defineConfig({
|
|
|
759
671
|
});
|
|
760
672
|
`
|
|
761
673
|
);
|
|
762
|
-
await
|
|
763
|
-
|
|
674
|
+
await fs2.writeFile(
|
|
675
|
+
path2.join(projectPath, "tether", "schema.ts"),
|
|
764
676
|
`import { defineSchema, text, integer, timestamp } from '@tthr/schema';
|
|
765
677
|
|
|
766
678
|
export default defineSchema({
|
|
@@ -785,8 +697,8 @@ export default defineSchema({
|
|
|
785
697
|
});
|
|
786
698
|
`
|
|
787
699
|
);
|
|
788
|
-
await
|
|
789
|
-
|
|
700
|
+
await fs2.writeFile(
|
|
701
|
+
path2.join(projectPath, "tether", "functions", "posts.ts"),
|
|
790
702
|
`import { query, mutation, z } from '@tthr/server';
|
|
791
703
|
|
|
792
704
|
// List all posts
|
|
@@ -871,8 +783,8 @@ export const remove = mutation({
|
|
|
871
783
|
});
|
|
872
784
|
`
|
|
873
785
|
);
|
|
874
|
-
await
|
|
875
|
-
|
|
786
|
+
await fs2.writeFile(
|
|
787
|
+
path2.join(projectPath, "tether", "functions", "index.ts"),
|
|
876
788
|
`// Re-export all function modules
|
|
877
789
|
// The Tether SDK uses this file to discover custom functions
|
|
878
790
|
|
|
@@ -880,8 +792,8 @@ export * as posts from './posts';
|
|
|
880
792
|
export * as comments from './comments';
|
|
881
793
|
`
|
|
882
794
|
);
|
|
883
|
-
await
|
|
884
|
-
|
|
795
|
+
await fs2.writeFile(
|
|
796
|
+
path2.join(projectPath, "tether", "functions", "comments.ts"),
|
|
885
797
|
`import { query, mutation, z } from '@tthr/server';
|
|
886
798
|
|
|
887
799
|
// List all comments
|
|
@@ -953,27 +865,27 @@ export const remove = mutation({
|
|
|
953
865
|
TETHER_PROJECT_ID=${projectId}
|
|
954
866
|
TETHER_API_KEY=${apiKey}
|
|
955
867
|
`;
|
|
956
|
-
const envPath =
|
|
957
|
-
if (await
|
|
958
|
-
const existing = await
|
|
959
|
-
await
|
|
868
|
+
const envPath = path2.join(projectPath, ".env");
|
|
869
|
+
if (await fs2.pathExists(envPath)) {
|
|
870
|
+
const existing = await fs2.readFile(envPath, "utf-8");
|
|
871
|
+
await fs2.writeFile(envPath, existing + "\n" + envContent);
|
|
960
872
|
} else {
|
|
961
|
-
await
|
|
873
|
+
await fs2.writeFile(envPath, envContent);
|
|
962
874
|
}
|
|
963
|
-
const gitignorePath =
|
|
875
|
+
const gitignorePath = path2.join(projectPath, ".gitignore");
|
|
964
876
|
const tetherGitignore = `
|
|
965
877
|
# Tether
|
|
966
878
|
_generated/
|
|
967
879
|
.env
|
|
968
880
|
.env.local
|
|
969
881
|
`;
|
|
970
|
-
if (await
|
|
971
|
-
const existing = await
|
|
882
|
+
if (await fs2.pathExists(gitignorePath)) {
|
|
883
|
+
const existing = await fs2.readFile(gitignorePath, "utf-8");
|
|
972
884
|
if (!existing.includes("_generated/")) {
|
|
973
|
-
await
|
|
885
|
+
await fs2.writeFile(gitignorePath, existing + tetherGitignore);
|
|
974
886
|
}
|
|
975
887
|
} else {
|
|
976
|
-
await
|
|
888
|
+
await fs2.writeFile(gitignorePath, tetherGitignore.trim());
|
|
977
889
|
}
|
|
978
890
|
}
|
|
979
891
|
async function configureFramework(projectPath, template) {
|
|
@@ -986,9 +898,9 @@ async function configureFramework(projectPath, template) {
|
|
|
986
898
|
}
|
|
987
899
|
}
|
|
988
900
|
async function configureNuxt(projectPath) {
|
|
989
|
-
const configPath =
|
|
990
|
-
if (!await
|
|
991
|
-
await
|
|
901
|
+
const configPath = path2.join(projectPath, "nuxt.config.ts");
|
|
902
|
+
if (!await fs2.pathExists(configPath)) {
|
|
903
|
+
await fs2.writeFile(
|
|
992
904
|
configPath,
|
|
993
905
|
`// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
994
906
|
export default defineNuxtConfig({
|
|
@@ -1006,7 +918,7 @@ export default defineNuxtConfig({
|
|
|
1006
918
|
);
|
|
1007
919
|
return;
|
|
1008
920
|
}
|
|
1009
|
-
let config = await
|
|
921
|
+
let config = await fs2.readFile(configPath, "utf-8");
|
|
1010
922
|
if (config.includes("modules:")) {
|
|
1011
923
|
config = config.replace(
|
|
1012
924
|
/modules:\s*\[/,
|
|
@@ -1033,11 +945,11 @@ export default defineNuxtConfig({
|
|
|
1033
945
|
`
|
|
1034
946
|
);
|
|
1035
947
|
}
|
|
1036
|
-
await
|
|
948
|
+
await fs2.writeFile(configPath, config);
|
|
1037
949
|
}
|
|
1038
950
|
async function configureNext(projectPath) {
|
|
1039
|
-
const providersPath =
|
|
1040
|
-
await
|
|
951
|
+
const providersPath = path2.join(projectPath, "src", "app", "providers.tsx");
|
|
952
|
+
await fs2.writeFile(
|
|
1041
953
|
providersPath,
|
|
1042
954
|
`'use client';
|
|
1043
955
|
|
|
@@ -1055,9 +967,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
1055
967
|
}
|
|
1056
968
|
`
|
|
1057
969
|
);
|
|
1058
|
-
const layoutPath =
|
|
1059
|
-
if (await
|
|
1060
|
-
let layout = await
|
|
970
|
+
const layoutPath = path2.join(projectPath, "src", "app", "layout.tsx");
|
|
971
|
+
if (await fs2.pathExists(layoutPath)) {
|
|
972
|
+
let layout = await fs2.readFile(layoutPath, "utf-8");
|
|
1061
973
|
if (!layout.includes("import { Providers }")) {
|
|
1062
974
|
layout = layout.replace(
|
|
1063
975
|
/^(import.*\n)+/m,
|
|
@@ -1070,24 +982,24 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
1070
982
|
"$1<Providers>$2</Providers>$3"
|
|
1071
983
|
);
|
|
1072
984
|
}
|
|
1073
|
-
await
|
|
985
|
+
await fs2.writeFile(layoutPath, layout);
|
|
1074
986
|
}
|
|
1075
|
-
const envLocalPath =
|
|
987
|
+
const envLocalPath = path2.join(projectPath, ".env.local");
|
|
1076
988
|
const nextEnvContent = `# Tether Configuration (client-side)
|
|
1077
989
|
NEXT_PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
|
|
1078
990
|
`;
|
|
1079
|
-
if (await
|
|
1080
|
-
const existing = await
|
|
1081
|
-
await
|
|
991
|
+
if (await fs2.pathExists(envLocalPath)) {
|
|
992
|
+
const existing = await fs2.readFile(envLocalPath, "utf-8");
|
|
993
|
+
await fs2.writeFile(envLocalPath, existing + "\n" + nextEnvContent);
|
|
1082
994
|
} else {
|
|
1083
|
-
await
|
|
995
|
+
await fs2.writeFile(envLocalPath, nextEnvContent);
|
|
1084
996
|
}
|
|
1085
997
|
}
|
|
1086
998
|
async function configureSvelteKit(projectPath) {
|
|
1087
|
-
const libPath =
|
|
1088
|
-
await
|
|
1089
|
-
await
|
|
1090
|
-
|
|
999
|
+
const libPath = path2.join(projectPath, "src", "lib");
|
|
1000
|
+
await fs2.ensureDir(libPath);
|
|
1001
|
+
await fs2.writeFile(
|
|
1002
|
+
path2.join(libPath, "tether.ts"),
|
|
1091
1003
|
`import { createClient } from '@tthr/svelte';
|
|
1092
1004
|
import { PUBLIC_TETHER_PROJECT_ID, PUBLIC_TETHER_URL } from '$env/static/public';
|
|
1093
1005
|
|
|
@@ -1097,14 +1009,14 @@ export const tether = createClient({
|
|
|
1097
1009
|
});
|
|
1098
1010
|
`
|
|
1099
1011
|
);
|
|
1100
|
-
const envPath =
|
|
1012
|
+
const envPath = path2.join(projectPath, ".env");
|
|
1101
1013
|
const svelteEnvContent = `# Tether Configuration (public)
|
|
1102
1014
|
PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
|
|
1103
1015
|
`;
|
|
1104
|
-
if (await
|
|
1105
|
-
const existing = await
|
|
1016
|
+
if (await fs2.pathExists(envPath)) {
|
|
1017
|
+
const existing = await fs2.readFile(envPath, "utf-8");
|
|
1106
1018
|
if (!existing.includes("PUBLIC_TETHER_")) {
|
|
1107
|
-
await
|
|
1019
|
+
await fs2.writeFile(envPath, existing + "\n" + svelteEnvContent);
|
|
1108
1020
|
}
|
|
1109
1021
|
}
|
|
1110
1022
|
}
|
|
@@ -1154,15 +1066,15 @@ async function installTetherPackages(projectPath, template, packageManager) {
|
|
|
1154
1066
|
stdio: "pipe"
|
|
1155
1067
|
});
|
|
1156
1068
|
} catch (error) {
|
|
1157
|
-
console.warn(
|
|
1069
|
+
console.warn(chalk2.yellow("\nNote: Some Tether packages may not be published yet."));
|
|
1158
1070
|
}
|
|
1159
1071
|
}
|
|
1160
1072
|
async function createDemoPage(projectPath, template) {
|
|
1161
1073
|
if (template === "nuxt") {
|
|
1162
|
-
const nuxt4AppVuePath =
|
|
1163
|
-
const nuxt3AppVuePath =
|
|
1164
|
-
const appVuePath = await
|
|
1165
|
-
await
|
|
1074
|
+
const nuxt4AppVuePath = path2.join(projectPath, "app", "app.vue");
|
|
1075
|
+
const nuxt3AppVuePath = path2.join(projectPath, "app.vue");
|
|
1076
|
+
const appVuePath = await fs2.pathExists(path2.join(projectPath, "app")) ? nuxt4AppVuePath : nuxt3AppVuePath;
|
|
1077
|
+
await fs2.writeFile(
|
|
1166
1078
|
appVuePath,
|
|
1167
1079
|
`<template>
|
|
1168
1080
|
<TetherWelcome />
|
|
@@ -1170,8 +1082,8 @@ async function createDemoPage(projectPath, template) {
|
|
|
1170
1082
|
`
|
|
1171
1083
|
);
|
|
1172
1084
|
} else if (template === "next") {
|
|
1173
|
-
const pagePath =
|
|
1174
|
-
await
|
|
1085
|
+
const pagePath = path2.join(projectPath, "src", "app", "page.tsx");
|
|
1086
|
+
await fs2.writeFile(
|
|
1175
1087
|
pagePath,
|
|
1176
1088
|
`'use client';
|
|
1177
1089
|
|
|
@@ -1281,8 +1193,8 @@ export default function Home() {
|
|
|
1281
1193
|
`
|
|
1282
1194
|
);
|
|
1283
1195
|
} else if (template === "sveltekit") {
|
|
1284
|
-
const pagePath =
|
|
1285
|
-
await
|
|
1196
|
+
const pagePath = path2.join(projectPath, "src", "routes", "+page.svelte");
|
|
1197
|
+
await fs2.writeFile(
|
|
1286
1198
|
pagePath,
|
|
1287
1199
|
`<script lang="ts">
|
|
1288
1200
|
import { onMount } from 'svelte';
|
|
@@ -1548,510 +1460,337 @@ export default function Home() {
|
|
|
1548
1460
|
}
|
|
1549
1461
|
|
|
1550
1462
|
// src/commands/dev.ts
|
|
1551
|
-
import
|
|
1463
|
+
import chalk3 from "chalk";
|
|
1552
1464
|
import ora3 from "ora";
|
|
1553
|
-
import
|
|
1554
|
-
import
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1465
|
+
import fs3 from "fs-extra";
|
|
1466
|
+
import path3 from "path";
|
|
1467
|
+
import { spawn } from "child_process";
|
|
1468
|
+
import { watch } from "chokidar";
|
|
1469
|
+
var frameworkProcess = null;
|
|
1470
|
+
var schemaWatcher = null;
|
|
1471
|
+
var functionsWatcher = null;
|
|
1472
|
+
var isGenerating = false;
|
|
1473
|
+
var pendingGenerate = false;
|
|
1474
|
+
async function runGenerate(spinner) {
|
|
1475
|
+
if (isGenerating) {
|
|
1476
|
+
pendingGenerate = true;
|
|
1477
|
+
return true;
|
|
1562
1478
|
}
|
|
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();
|
|
1479
|
+
isGenerating = true;
|
|
1567
1480
|
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
|
-
});
|
|
1481
|
+
const { generateTypes } = await import("./generate-26HERX3T.js");
|
|
1482
|
+
spinner.text = "Regenerating types...";
|
|
1483
|
+
await generateTypes({ silent: true });
|
|
1484
|
+
spinner.succeed("Types regenerated");
|
|
1485
|
+
spinner.start("Watching for changes...");
|
|
1486
|
+
return true;
|
|
1579
1487
|
} catch (error) {
|
|
1580
|
-
spinner.fail("
|
|
1581
|
-
console.error(
|
|
1582
|
-
|
|
1488
|
+
spinner.fail("Type generation failed");
|
|
1489
|
+
console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1490
|
+
spinner.start("Watching for changes...");
|
|
1491
|
+
return false;
|
|
1492
|
+
} finally {
|
|
1493
|
+
isGenerating = false;
|
|
1494
|
+
if (pendingGenerate) {
|
|
1495
|
+
pendingGenerate = false;
|
|
1496
|
+
setTimeout(() => runGenerate(spinner), 100);
|
|
1497
|
+
}
|
|
1583
1498
|
}
|
|
1584
1499
|
}
|
|
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];
|
|
1500
|
+
async function validateSchema(schemaPath) {
|
|
1501
|
+
try {
|
|
1502
|
+
const content = await fs3.readFile(schemaPath, "utf-8");
|
|
1503
|
+
if (!content.includes("defineSchema")) {
|
|
1504
|
+
return { valid: false, error: "Missing defineSchema() call" };
|
|
1505
|
+
}
|
|
1506
|
+
let braceCount = 0;
|
|
1507
|
+
for (const char of content) {
|
|
1605
1508
|
if (char === "{") braceCount++;
|
|
1606
1509
|
else if (char === "}") braceCount--;
|
|
1607
|
-
|
|
1510
|
+
if (braceCount < 0) {
|
|
1511
|
+
return { valid: false, error: "Unmatched closing brace" };
|
|
1512
|
+
}
|
|
1608
1513
|
}
|
|
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};`);
|
|
1514
|
+
if (braceCount !== 0) {
|
|
1515
|
+
return { valid: false, error: "Unmatched opening brace" };
|
|
1677
1516
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1517
|
+
return { valid: true };
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
return { valid: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1680
1520
|
}
|
|
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
1521
|
}
|
|
1694
|
-
async function
|
|
1695
|
-
const
|
|
1696
|
-
if (!await
|
|
1697
|
-
return
|
|
1522
|
+
async function validateFunctions(functionsDir) {
|
|
1523
|
+
const errors = [];
|
|
1524
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
1525
|
+
return { valid: true, errors: [] };
|
|
1698
1526
|
}
|
|
1699
|
-
const files = await
|
|
1527
|
+
const files = await fs3.readdir(functionsDir);
|
|
1700
1528
|
for (const file of files) {
|
|
1701
1529
|
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
1702
|
-
const filePath =
|
|
1703
|
-
const stat = await
|
|
1530
|
+
const filePath = path3.join(functionsDir, file);
|
|
1531
|
+
const stat = await fs3.stat(filePath);
|
|
1704
1532
|
if (!stat.isFile()) continue;
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1533
|
+
try {
|
|
1534
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
1535
|
+
const hasExports = /export\s+const\s+\w+\s*=\s*(query|mutation|action)\s*\(/.test(content);
|
|
1536
|
+
if (!hasExports && !file.includes("index")) {
|
|
1537
|
+
errors.push(`${file}: No query, mutation, or action exports found`);
|
|
1538
|
+
}
|
|
1539
|
+
let braceCount = 0;
|
|
1540
|
+
for (const char of content) {
|
|
1541
|
+
if (char === "{") braceCount++;
|
|
1542
|
+
else if (char === "}") braceCount--;
|
|
1543
|
+
}
|
|
1544
|
+
if (braceCount !== 0) {
|
|
1545
|
+
errors.push(`${file}: Mismatched braces`);
|
|
1546
|
+
}
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
errors.push(`${file}: ${error instanceof Error ? error.message : "Read error"}`);
|
|
1714
1549
|
}
|
|
1715
1550
|
}
|
|
1716
|
-
return
|
|
1551
|
+
return { valid: errors.length === 0, errors };
|
|
1717
1552
|
}
|
|
1718
|
-
|
|
1719
|
-
const
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1553
|
+
function startFrameworkDev(command, port, spinner) {
|
|
1554
|
+
const [cmd, ...args] = command.split(" ");
|
|
1555
|
+
const portArgs = ["--port", port];
|
|
1556
|
+
spinner.text = `Starting ${command}...`;
|
|
1557
|
+
const child = spawn(cmd, [...args, ...portArgs], {
|
|
1558
|
+
cwd: process.cwd(),
|
|
1559
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
1560
|
+
shell: true,
|
|
1561
|
+
env: {
|
|
1562
|
+
...process.env,
|
|
1563
|
+
FORCE_COLOR: "1"
|
|
1724
1564
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
""
|
|
1731
|
-
|
|
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("");
|
|
1753
|
-
}
|
|
1754
|
-
lines.push("export interface Api {");
|
|
1755
|
-
for (const moduleName of moduleMap.keys()) {
|
|
1756
|
-
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
1757
|
-
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
1758
|
-
}
|
|
1759
|
-
lines.push("}");
|
|
1760
|
-
} else {
|
|
1761
|
-
lines.push("/**");
|
|
1762
|
-
lines.push(" * Flexible API type that allows access to any function path.");
|
|
1763
|
-
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
1764
|
-
lines.push(" * The actual types depend on your function definitions.");
|
|
1765
|
-
lines.push(" */");
|
|
1766
|
-
lines.push("export type Api = {");
|
|
1767
|
-
lines.push(" [module: string]: {");
|
|
1768
|
-
lines.push(" [fn: string]: ApiFunction;");
|
|
1769
|
-
lines.push(" };");
|
|
1770
|
-
lines.push("};");
|
|
1771
|
-
}
|
|
1772
|
-
lines.push("");
|
|
1773
|
-
lines.push("// API client proxy - provides typed access to your functions");
|
|
1774
|
-
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
1775
|
-
lines.push("// In Tether functions: calls the actual function implementation");
|
|
1776
|
-
lines.push("export const api = createApiProxy<Api>();");
|
|
1777
|
-
lines.push("");
|
|
1778
|
-
return lines.join("\n");
|
|
1779
|
-
}
|
|
1780
|
-
async function generateCommand() {
|
|
1781
|
-
await requireAuth();
|
|
1782
|
-
const configPath = path6.resolve(process.cwd(), "tether.config.ts");
|
|
1783
|
-
if (!await fs6.pathExists(configPath)) {
|
|
1784
|
-
console.log(chalk5.red("\nError: Not a Tether project"));
|
|
1785
|
-
console.log(chalk5.dim("Run `tthr init` to create a new project\n"));
|
|
1786
|
-
process.exit(1);
|
|
1787
|
-
}
|
|
1788
|
-
console.log(chalk5.bold("\n\u26A1 Generating types from schema\n"));
|
|
1789
|
-
const spinner = ora4("Reading schema...").start();
|
|
1790
|
-
try {
|
|
1791
|
-
const config = await loadConfig();
|
|
1792
|
-
const schemaPath = resolvePath(config.schema);
|
|
1793
|
-
const outputDir = resolvePath(config.output);
|
|
1794
|
-
const functionsDir = resolvePath(config.functions);
|
|
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);
|
|
1565
|
+
});
|
|
1566
|
+
let started = false;
|
|
1567
|
+
child.stdout?.on("data", (data) => {
|
|
1568
|
+
const output = data.toString();
|
|
1569
|
+
process.stdout.write(output);
|
|
1570
|
+
if (!started && (output.includes("Local:") || output.includes("ready") || output.includes("listening"))) {
|
|
1571
|
+
started = true;
|
|
1572
|
+
spinner.succeed(`Framework dev server running`);
|
|
1573
|
+
spinner.start("Watching for changes...");
|
|
1800
1574
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
return;
|
|
1575
|
+
});
|
|
1576
|
+
child.stderr?.on("data", (data) => {
|
|
1577
|
+
const output = data.toString();
|
|
1578
|
+
if (!output.includes("ExperimentalWarning") && !output.includes("--trace-warnings")) {
|
|
1579
|
+
process.stderr.write(chalk3.dim(output));
|
|
1807
1580
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
)
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
await generateApiFile(functionsDir)
|
|
1817
|
-
);
|
|
1818
|
-
await fs6.writeFile(
|
|
1819
|
-
path6.join(outputDir, "index.ts"),
|
|
1820
|
-
`// Auto-generated by Tether CLI - do not edit manually
|
|
1821
|
-
export * from './db';
|
|
1822
|
-
export * from './api';
|
|
1823
|
-
`
|
|
1824
|
-
);
|
|
1825
|
-
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
1826
|
-
console.log("\n" + chalk5.green("\u2713") + " Tables:");
|
|
1827
|
-
for (const table of tables) {
|
|
1828
|
-
console.log(chalk5.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
1581
|
+
});
|
|
1582
|
+
child.on("error", (error) => {
|
|
1583
|
+
spinner.fail(`Failed to start framework: ${error.message}`);
|
|
1584
|
+
});
|
|
1585
|
+
child.on("exit", (code) => {
|
|
1586
|
+
if (code !== 0 && code !== null) {
|
|
1587
|
+
console.log(chalk3.yellow(`
|
|
1588
|
+
Framework process exited with code ${code}`));
|
|
1829
1589
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
console.log(chalk5.dim(` ${relativeOutput}/db.ts`));
|
|
1833
|
-
console.log(chalk5.dim(` ${relativeOutput}/api.ts`));
|
|
1834
|
-
console.log(chalk5.dim(` ${relativeOutput}/index.ts
|
|
1835
|
-
`));
|
|
1836
|
-
} catch (error) {
|
|
1837
|
-
spinner.fail("Failed to generate types");
|
|
1838
|
-
console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1839
|
-
process.exit(1);
|
|
1840
|
-
}
|
|
1590
|
+
});
|
|
1591
|
+
return child;
|
|
1841
1592
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
import fs7 from "fs-extra";
|
|
1847
|
-
import path7 from "path";
|
|
1848
|
-
import prompts2 from "prompts";
|
|
1849
|
-
async function migrateCommand(action, options) {
|
|
1850
|
-
await requireAuth();
|
|
1851
|
-
const configPath = path7.resolve(process.cwd(), "tether.config.ts");
|
|
1852
|
-
if (!await fs7.pathExists(configPath)) {
|
|
1853
|
-
console.log(chalk6.red("\nError: Not a Tether project"));
|
|
1854
|
-
console.log(chalk6.dim("Run `tthr init` to create a new project\n"));
|
|
1855
|
-
process.exit(1);
|
|
1593
|
+
function cleanup() {
|
|
1594
|
+
if (frameworkProcess) {
|
|
1595
|
+
frameworkProcess.kill("SIGTERM");
|
|
1596
|
+
frameworkProcess = null;
|
|
1856
1597
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
await createMigration(migrationsDir, options.name);
|
|
1861
|
-
break;
|
|
1862
|
-
case "up":
|
|
1863
|
-
await runMigrations(migrationsDir, "up");
|
|
1864
|
-
break;
|
|
1865
|
-
case "down":
|
|
1866
|
-
await runMigrations(migrationsDir, "down");
|
|
1867
|
-
break;
|
|
1868
|
-
case "status":
|
|
1869
|
-
default:
|
|
1870
|
-
await showStatus(migrationsDir);
|
|
1871
|
-
break;
|
|
1598
|
+
if (schemaWatcher) {
|
|
1599
|
+
schemaWatcher.close();
|
|
1600
|
+
schemaWatcher = null;
|
|
1872
1601
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
let migrationName = name;
|
|
1877
|
-
if (!migrationName) {
|
|
1878
|
-
const response = await prompts2({
|
|
1879
|
-
type: "text",
|
|
1880
|
-
name: "name",
|
|
1881
|
-
message: "Migration name:",
|
|
1882
|
-
initial: "update_schema"
|
|
1883
|
-
});
|
|
1884
|
-
migrationName = response.name;
|
|
1885
|
-
if (!migrationName) {
|
|
1886
|
-
console.log(chalk6.red("Migration name is required"));
|
|
1887
|
-
process.exit(1);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
const spinner = ora5("Creating migration...").start();
|
|
1891
|
-
try {
|
|
1892
|
-
await fs7.ensureDir(migrationsDir);
|
|
1893
|
-
const timestamp = Date.now();
|
|
1894
|
-
const filename = `${timestamp}_${migrationName}.sql`;
|
|
1895
|
-
const filepath = path7.join(migrationsDir, filename);
|
|
1896
|
-
await fs7.writeFile(
|
|
1897
|
-
filepath,
|
|
1898
|
-
`-- Migration: ${migrationName}
|
|
1899
|
-
-- Created at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1900
|
-
|
|
1901
|
-
-- Up migration
|
|
1902
|
-
-- Add your schema changes here
|
|
1903
|
-
|
|
1904
|
-
-- Example:
|
|
1905
|
-
-- CREATE TABLE IF NOT EXISTS users (
|
|
1906
|
-
-- id TEXT PRIMARY KEY,
|
|
1907
|
-
-- email TEXT NOT NULL UNIQUE,
|
|
1908
|
-
-- name TEXT,
|
|
1909
|
-
-- created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1910
|
-
-- );
|
|
1911
|
-
|
|
1912
|
-
-- Down migration (for rollback)
|
|
1913
|
-
-- DROP TABLE IF EXISTS users;
|
|
1914
|
-
`
|
|
1915
|
-
);
|
|
1916
|
-
spinner.succeed("Migration created");
|
|
1917
|
-
console.log(chalk6.dim(`
|
|
1918
|
-
${filepath}
|
|
1919
|
-
`));
|
|
1920
|
-
} catch (error) {
|
|
1921
|
-
spinner.fail("Failed to create migration");
|
|
1922
|
-
console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1923
|
-
process.exit(1);
|
|
1602
|
+
if (functionsWatcher) {
|
|
1603
|
+
functionsWatcher.close();
|
|
1604
|
+
functionsWatcher = null;
|
|
1924
1605
|
}
|
|
1925
1606
|
}
|
|
1926
|
-
async function
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
if (!await fs7.pathExists(migrationsDir)) {
|
|
1933
|
-
spinner.info("No migrations directory found");
|
|
1934
|
-
console.log(chalk6.dim("\nRun `tthr migrate create` to create your first migration\n"));
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
const files = await fs7.readdir(migrationsDir);
|
|
1938
|
-
const migrations = files.filter((f) => f.endsWith(".sql")).sort((a, b) => direction === "up" ? a.localeCompare(b) : b.localeCompare(a));
|
|
1939
|
-
if (migrations.length === 0) {
|
|
1940
|
-
spinner.info("No migrations found");
|
|
1941
|
-
console.log(chalk6.dim("\nRun `tthr migrate create` to create your first migration\n"));
|
|
1942
|
-
return;
|
|
1943
|
-
}
|
|
1944
|
-
spinner.text = `Found ${migrations.length} migration(s)`;
|
|
1945
|
-
spinner.succeed(`Would run ${migrations.length} migration(s)`);
|
|
1946
|
-
console.log("\nMigrations to run:");
|
|
1947
|
-
for (const migration of migrations) {
|
|
1948
|
-
console.log(chalk6.dim(` ${migration}`));
|
|
1949
|
-
}
|
|
1950
|
-
console.log(chalk6.yellow("\nNote: Migration execution not yet implemented\n"));
|
|
1951
|
-
} catch (error) {
|
|
1952
|
-
spinner.fail("Failed to run migrations");
|
|
1953
|
-
console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1607
|
+
async function devCommand(options) {
|
|
1608
|
+
await requireAuth();
|
|
1609
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
1610
|
+
if (!await fs3.pathExists(configPath)) {
|
|
1611
|
+
console.log(chalk3.red("\nError: Not a Tether project"));
|
|
1612
|
+
console.log(chalk3.dim("Run `tthr init` to create a new project\n"));
|
|
1954
1613
|
process.exit(1);
|
|
1955
1614
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1615
|
+
const config = await loadConfig();
|
|
1616
|
+
const port = options.port || String(config.dev?.port || 3001);
|
|
1617
|
+
const schemaPath = resolvePath(config.schema);
|
|
1618
|
+
const functionsDir = resolvePath(config.functions);
|
|
1619
|
+
const outputDir = resolvePath(config.output);
|
|
1620
|
+
const framework = config.framework || await detectFramework();
|
|
1621
|
+
const devCommand2 = config.dev?.command || getFrameworkDevCommand(framework);
|
|
1622
|
+
console.log(chalk3.bold("\n\u26A1 Starting Tether development server\n"));
|
|
1623
|
+
if (framework !== "unknown") {
|
|
1624
|
+
console.log(chalk3.dim(` Framework: ${framework}`));
|
|
1625
|
+
}
|
|
1626
|
+
if (devCommand2) {
|
|
1627
|
+
console.log(chalk3.dim(` Dev command: ${devCommand2}`));
|
|
1628
|
+
}
|
|
1629
|
+
console.log(chalk3.dim(` Schema: ${path3.relative(process.cwd(), schemaPath)}`));
|
|
1630
|
+
console.log(chalk3.dim(` Functions: ${path3.relative(process.cwd(), functionsDir)}`));
|
|
1631
|
+
console.log(chalk3.dim(` Output: ${path3.relative(process.cwd(), outputDir)}`));
|
|
1632
|
+
console.log("");
|
|
1633
|
+
const spinner = ora3("Initialising...").start();
|
|
1634
|
+
process.on("SIGINT", () => {
|
|
1635
|
+
spinner.stop();
|
|
1636
|
+
console.log(chalk3.yellow("\n\nShutting down...\n"));
|
|
1637
|
+
cleanup();
|
|
1638
|
+
process.exit(0);
|
|
1639
|
+
});
|
|
1640
|
+
process.on("SIGTERM", () => {
|
|
1641
|
+
cleanup();
|
|
1642
|
+
process.exit(0);
|
|
1643
|
+
});
|
|
1959
1644
|
try {
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1645
|
+
spinner.text = "Validating schema...";
|
|
1646
|
+
const schemaValidation = await validateSchema(schemaPath);
|
|
1647
|
+
if (!schemaValidation.valid) {
|
|
1648
|
+
spinner.warn(`Schema validation warning: ${schemaValidation.error}`);
|
|
1649
|
+
}
|
|
1650
|
+
spinner.text = "Validating functions...";
|
|
1651
|
+
const functionsValidation = await validateFunctions(functionsDir);
|
|
1652
|
+
if (!functionsValidation.valid) {
|
|
1653
|
+
spinner.warn("Function validation warnings:");
|
|
1654
|
+
for (const error of functionsValidation.errors) {
|
|
1655
|
+
console.log(chalk3.yellow(` - ${error}`));
|
|
1656
|
+
}
|
|
1964
1657
|
}
|
|
1965
|
-
|
|
1966
|
-
const
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1658
|
+
spinner.text = "Generating types...";
|
|
1659
|
+
const { generateTypes } = await import("./generate-26HERX3T.js");
|
|
1660
|
+
await generateTypes({ silent: true });
|
|
1661
|
+
spinner.succeed("Types generated");
|
|
1662
|
+
spinner.start("Setting up file watchers...");
|
|
1663
|
+
if (await fs3.pathExists(schemaPath)) {
|
|
1664
|
+
schemaWatcher = watch(schemaPath, {
|
|
1665
|
+
ignoreInitial: true,
|
|
1666
|
+
awaitWriteFinish: {
|
|
1667
|
+
stabilityThreshold: 300,
|
|
1668
|
+
pollInterval: 100
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
schemaWatcher.on("change", async () => {
|
|
1672
|
+
console.log(chalk3.cyan("\n Schema changed, validating..."));
|
|
1673
|
+
const validation = await validateSchema(schemaPath);
|
|
1674
|
+
if (!validation.valid) {
|
|
1675
|
+
console.log(chalk3.yellow(` Schema error: ${validation.error}`));
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
await runGenerate(spinner);
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
if (await fs3.pathExists(functionsDir)) {
|
|
1682
|
+
functionsWatcher = watch(path3.join(functionsDir, "**/*.{ts,js}"), {
|
|
1683
|
+
ignoreInitial: true,
|
|
1684
|
+
awaitWriteFinish: {
|
|
1685
|
+
stabilityThreshold: 300,
|
|
1686
|
+
pollInterval: 100
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
functionsWatcher.on("all", async (event, filePath) => {
|
|
1690
|
+
const relativePath = path3.relative(functionsDir, filePath);
|
|
1691
|
+
console.log(chalk3.cyan(`
|
|
1692
|
+
Function ${event}: ${relativePath}`));
|
|
1693
|
+
const validation = await validateFunctions(functionsDir);
|
|
1694
|
+
if (!validation.valid) {
|
|
1695
|
+
for (const error of validation.errors) {
|
|
1696
|
+
console.log(chalk3.yellow(` Warning: ${error}`));
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
await runGenerate(spinner);
|
|
1700
|
+
});
|
|
1971
1701
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1702
|
+
spinner.succeed("File watchers ready");
|
|
1703
|
+
if (devCommand2 && !options.skipFramework) {
|
|
1704
|
+
spinner.start(`Starting ${framework} dev server...`);
|
|
1705
|
+
frameworkProcess = startFrameworkDev(devCommand2, port, spinner);
|
|
1706
|
+
} else {
|
|
1707
|
+
spinner.start("Watching for changes...");
|
|
1708
|
+
console.log("\n" + chalk3.cyan(` Tether dev ready`));
|
|
1709
|
+
console.log(chalk3.dim(` Watching schema and functions for changes`));
|
|
1710
|
+
console.log(chalk3.dim(` Press Ctrl+C to stop
|
|
1711
|
+
`));
|
|
1976
1712
|
}
|
|
1977
|
-
|
|
1713
|
+
await new Promise(() => {
|
|
1714
|
+
});
|
|
1978
1715
|
} catch (error) {
|
|
1979
|
-
|
|
1716
|
+
spinner.fail("Failed to start dev server");
|
|
1717
|
+
console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1718
|
+
cleanup();
|
|
1980
1719
|
process.exit(1);
|
|
1981
1720
|
}
|
|
1982
1721
|
}
|
|
1983
1722
|
|
|
1984
1723
|
// src/commands/login.ts
|
|
1985
|
-
import
|
|
1986
|
-
import
|
|
1724
|
+
import chalk4 from "chalk";
|
|
1725
|
+
import ora4 from "ora";
|
|
1987
1726
|
import os from "os";
|
|
1988
1727
|
var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
1989
1728
|
var API_URL3 = isDev3 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
1990
1729
|
var AUTH_URL = isDev3 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
|
|
1991
1730
|
async function loginCommand() {
|
|
1992
|
-
console.log(
|
|
1731
|
+
console.log(chalk4.bold("\u26A1 Login to Tether\n"));
|
|
1993
1732
|
const existing = await getCredentials();
|
|
1994
1733
|
if (existing) {
|
|
1995
|
-
console.log(
|
|
1996
|
-
console.log(
|
|
1734
|
+
console.log(chalk4.green("\u2713") + ` Already logged in as ${chalk4.cyan(existing.email)}`);
|
|
1735
|
+
console.log(chalk4.dim("\nRun `tthr logout` to sign out\n"));
|
|
1997
1736
|
return;
|
|
1998
1737
|
}
|
|
1999
|
-
const spinner =
|
|
1738
|
+
const spinner = ora4("Generating authentication code...").start();
|
|
2000
1739
|
try {
|
|
2001
1740
|
const deviceCode = await requestDeviceCode();
|
|
2002
1741
|
spinner.stop();
|
|
2003
1742
|
const authUrl = `${AUTH_URL}/${deviceCode.userCode}`;
|
|
2004
|
-
console.log(
|
|
2005
|
-
console.log(
|
|
1743
|
+
console.log(chalk4.dim("Open this URL in your browser to authenticate:\n"));
|
|
1744
|
+
console.log(chalk4.cyan.bold(` ${authUrl}
|
|
2006
1745
|
`));
|
|
2007
|
-
console.log(
|
|
1746
|
+
console.log(chalk4.dim(`Your code: ${chalk4.white.bold(deviceCode.userCode)}
|
|
2008
1747
|
`));
|
|
2009
1748
|
const credentials = await pollForApproval(deviceCode.deviceCode, deviceCode.interval, deviceCode.expiresIn);
|
|
2010
1749
|
await saveCredentials(credentials);
|
|
2011
|
-
console.log(
|
|
1750
|
+
console.log(chalk4.green("\u2713") + ` Logged in as ${chalk4.cyan(credentials.email)}`);
|
|
2012
1751
|
console.log();
|
|
2013
1752
|
} catch (error) {
|
|
2014
1753
|
spinner.fail("Authentication failed");
|
|
2015
|
-
console.error(
|
|
2016
|
-
console.log(
|
|
1754
|
+
console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1755
|
+
console.log(chalk4.dim("\nTry again or visit https://tthr.io/docs/cli for help\n"));
|
|
2017
1756
|
process.exit(1);
|
|
2018
1757
|
}
|
|
2019
1758
|
}
|
|
2020
1759
|
async function logoutCommand() {
|
|
2021
|
-
console.log(
|
|
1760
|
+
console.log(chalk4.bold("\n\u26A1 Logout from Tether\n"));
|
|
2022
1761
|
const credentials = await getCredentials();
|
|
2023
1762
|
if (!credentials) {
|
|
2024
|
-
console.log(
|
|
1763
|
+
console.log(chalk4.dim("Not logged in\n"));
|
|
2025
1764
|
return;
|
|
2026
1765
|
}
|
|
2027
|
-
const spinner =
|
|
1766
|
+
const spinner = ora4("Logging out...").start();
|
|
2028
1767
|
try {
|
|
2029
1768
|
await clearCredentials();
|
|
2030
|
-
spinner.succeed(`Logged out from ${
|
|
1769
|
+
spinner.succeed(`Logged out from ${chalk4.cyan(credentials.email)}`);
|
|
2031
1770
|
console.log();
|
|
2032
1771
|
} catch (error) {
|
|
2033
1772
|
spinner.fail("Logout failed");
|
|
2034
|
-
console.error(
|
|
1773
|
+
console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2035
1774
|
process.exit(1);
|
|
2036
1775
|
}
|
|
2037
1776
|
}
|
|
2038
1777
|
async function whoamiCommand() {
|
|
2039
1778
|
const credentials = await getCredentials();
|
|
2040
1779
|
if (!credentials) {
|
|
2041
|
-
console.log(
|
|
2042
|
-
console.log(
|
|
1780
|
+
console.log(chalk4.dim("\nNot logged in"));
|
|
1781
|
+
console.log(chalk4.dim("Run `tthr login` to authenticate\n"));
|
|
2043
1782
|
return;
|
|
2044
1783
|
}
|
|
2045
|
-
console.log(
|
|
2046
|
-
console.log(` Email: ${
|
|
2047
|
-
console.log(` User ID: ${
|
|
1784
|
+
console.log(chalk4.bold("\n\u26A1 Current user\n"));
|
|
1785
|
+
console.log(` Email: ${chalk4.cyan(credentials.email)}`);
|
|
1786
|
+
console.log(` User ID: ${chalk4.dim(credentials.userId)}`);
|
|
2048
1787
|
if (credentials.expiresAt) {
|
|
2049
1788
|
const expiresAt = new Date(credentials.expiresAt);
|
|
2050
1789
|
const now = /* @__PURE__ */ new Date();
|
|
2051
1790
|
if (expiresAt > now) {
|
|
2052
|
-
console.log(` Session: ${
|
|
1791
|
+
console.log(` Session: ${chalk4.green("Active")}`);
|
|
2053
1792
|
} else {
|
|
2054
|
-
console.log(` Session: ${
|
|
1793
|
+
console.log(` Session: ${chalk4.yellow("Expired")}`);
|
|
2055
1794
|
}
|
|
2056
1795
|
}
|
|
2057
1796
|
console.log();
|
|
@@ -2093,20 +1832,20 @@ async function requestDeviceCode() {
|
|
|
2093
1832
|
async function pollForApproval(deviceCode, interval, expiresIn) {
|
|
2094
1833
|
const startTime = Date.now();
|
|
2095
1834
|
const expiresAt = startTime + expiresIn * 1e3;
|
|
2096
|
-
const spinner =
|
|
1835
|
+
const spinner = ora4("").start();
|
|
2097
1836
|
const updateCountdown = () => {
|
|
2098
1837
|
const remaining = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1e3));
|
|
2099
1838
|
const mins = Math.floor(remaining / 60);
|
|
2100
1839
|
const secs = remaining % 60;
|
|
2101
1840
|
const timeStr = mins > 0 ? `${mins}:${secs.toString().padStart(2, "0")}` : `${secs}s`;
|
|
2102
|
-
spinner.text = `Waiting for approval... ${
|
|
1841
|
+
spinner.text = `Waiting for approval... ${chalk4.dim(`(${timeStr} remaining)`)}`;
|
|
2103
1842
|
};
|
|
2104
1843
|
updateCountdown();
|
|
2105
1844
|
const countdownInterval = setInterval(updateCountdown, 1e3);
|
|
2106
1845
|
try {
|
|
2107
1846
|
while (Date.now() < expiresAt) {
|
|
2108
1847
|
await sleep(interval * 1e3);
|
|
2109
|
-
spinner.text = `Checking... ${
|
|
1848
|
+
spinner.text = `Checking... ${chalk4.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
|
|
2110
1849
|
const response = await fetch(`${API_URL3}/auth/device/${deviceCode}`, {
|
|
2111
1850
|
method: "GET"
|
|
2112
1851
|
}).catch(() => null);
|
|
@@ -2139,10 +1878,10 @@ function sleep(ms) {
|
|
|
2139
1878
|
}
|
|
2140
1879
|
|
|
2141
1880
|
// src/commands/update.ts
|
|
2142
|
-
import
|
|
2143
|
-
import
|
|
2144
|
-
import
|
|
2145
|
-
import
|
|
1881
|
+
import chalk5 from "chalk";
|
|
1882
|
+
import ora5 from "ora";
|
|
1883
|
+
import fs4 from "fs-extra";
|
|
1884
|
+
import path4 from "path";
|
|
2146
1885
|
import { execSync as execSync2 } from "child_process";
|
|
2147
1886
|
var TETHER_PACKAGES = [
|
|
2148
1887
|
"@tthr/client",
|
|
@@ -2154,13 +1893,13 @@ var TETHER_PACKAGES = [
|
|
|
2154
1893
|
];
|
|
2155
1894
|
function detectPackageManager() {
|
|
2156
1895
|
const cwd = process.cwd();
|
|
2157
|
-
if (
|
|
1896
|
+
if (fs4.existsSync(path4.join(cwd, "bun.lockb")) || fs4.existsSync(path4.join(cwd, "bun.lock"))) {
|
|
2158
1897
|
return "bun";
|
|
2159
1898
|
}
|
|
2160
|
-
if (
|
|
1899
|
+
if (fs4.existsSync(path4.join(cwd, "pnpm-lock.yaml"))) {
|
|
2161
1900
|
return "pnpm";
|
|
2162
1901
|
}
|
|
2163
|
-
if (
|
|
1902
|
+
if (fs4.existsSync(path4.join(cwd, "yarn.lock"))) {
|
|
2164
1903
|
return "yarn";
|
|
2165
1904
|
}
|
|
2166
1905
|
return "npm";
|
|
@@ -2190,14 +1929,14 @@ async function getLatestVersion2(packageName) {
|
|
|
2190
1929
|
}
|
|
2191
1930
|
}
|
|
2192
1931
|
async function updateCommand(options) {
|
|
2193
|
-
const packageJsonPath =
|
|
2194
|
-
if (!await
|
|
2195
|
-
console.log(
|
|
2196
|
-
console.log(
|
|
1932
|
+
const packageJsonPath = path4.resolve(process.cwd(), "package.json");
|
|
1933
|
+
if (!await fs4.pathExists(packageJsonPath)) {
|
|
1934
|
+
console.log(chalk5.red("\nError: No package.json found"));
|
|
1935
|
+
console.log(chalk5.dim("Make sure you're in the root of your project\n"));
|
|
2197
1936
|
process.exit(1);
|
|
2198
1937
|
}
|
|
2199
|
-
console.log(
|
|
2200
|
-
const packageJson = await
|
|
1938
|
+
console.log(chalk5.bold("\n\u26A1 Updating Tether packages\n"));
|
|
1939
|
+
const packageJson = await fs4.readJson(packageJsonPath);
|
|
2201
1940
|
const deps = packageJson.dependencies || {};
|
|
2202
1941
|
const devDeps = packageJson.devDependencies || {};
|
|
2203
1942
|
const installedDeps = [];
|
|
@@ -2210,13 +1949,13 @@ async function updateCommand(options) {
|
|
|
2210
1949
|
}
|
|
2211
1950
|
}
|
|
2212
1951
|
if (installedDeps.length === 0) {
|
|
2213
|
-
console.log(
|
|
2214
|
-
console.log(
|
|
1952
|
+
console.log(chalk5.yellow(" No updatable Tether packages found in package.json"));
|
|
1953
|
+
console.log(chalk5.dim(" Tether packages start with @tthr/ (workspace: dependencies are skipped)\n"));
|
|
2215
1954
|
return;
|
|
2216
1955
|
}
|
|
2217
|
-
console.log(
|
|
1956
|
+
console.log(chalk5.dim(` Found ${installedDeps.length} Tether package(s):
|
|
2218
1957
|
`));
|
|
2219
|
-
const spinner =
|
|
1958
|
+
const spinner = ora5("Checking for updates...").start();
|
|
2220
1959
|
const packagesToUpdate = [];
|
|
2221
1960
|
for (const pkg of installedDeps) {
|
|
2222
1961
|
const latestVersion = await getLatestVersion2(pkg.name);
|
|
@@ -2230,32 +1969,32 @@ async function updateCommand(options) {
|
|
|
2230
1969
|
isDev: pkg.isDev
|
|
2231
1970
|
});
|
|
2232
1971
|
} else {
|
|
2233
|
-
console.log(
|
|
1972
|
+
console.log(chalk5.dim(` ${pkg.name}@${currentClean} (up to date)`));
|
|
2234
1973
|
}
|
|
2235
1974
|
}
|
|
2236
1975
|
}
|
|
2237
1976
|
spinner.stop();
|
|
2238
1977
|
if (packagesToUpdate.length === 0) {
|
|
2239
|
-
console.log(
|
|
1978
|
+
console.log(chalk5.green("\n\u2713 All Tether packages are up to date\n"));
|
|
2240
1979
|
return;
|
|
2241
1980
|
}
|
|
2242
|
-
console.log(
|
|
1981
|
+
console.log(chalk5.cyan("\n Updates available:\n"));
|
|
2243
1982
|
for (const pkg of packagesToUpdate) {
|
|
2244
1983
|
console.log(
|
|
2245
|
-
|
|
1984
|
+
chalk5.white(` ${pkg.name}`) + chalk5.red(` ${pkg.current}`) + chalk5.dim(" \u2192 ") + chalk5.green(`${pkg.latest}`) + (pkg.isDev ? chalk5.dim(" (dev)") : "")
|
|
2246
1985
|
);
|
|
2247
1986
|
}
|
|
2248
1987
|
if (options.dryRun) {
|
|
2249
|
-
console.log(
|
|
1988
|
+
console.log(chalk5.yellow("\n Dry run - no packages were updated\n"));
|
|
2250
1989
|
return;
|
|
2251
1990
|
}
|
|
2252
1991
|
const pm = detectPackageManager();
|
|
2253
|
-
console.log(
|
|
1992
|
+
console.log(chalk5.dim(`
|
|
2254
1993
|
Using ${pm} to update packages...
|
|
2255
1994
|
`));
|
|
2256
1995
|
const depsToUpdate = packagesToUpdate.filter((p) => !p.isDev).map((p) => `${p.name}@latest`);
|
|
2257
1996
|
const devDepsToUpdate = packagesToUpdate.filter((p) => p.isDev).map((p) => `${p.name}@latest`);
|
|
2258
|
-
const updateSpinner =
|
|
1997
|
+
const updateSpinner = ora5("Installing updates...").start();
|
|
2259
1998
|
try {
|
|
2260
1999
|
if (depsToUpdate.length > 0) {
|
|
2261
2000
|
const cmd = getInstallCommand2(pm, depsToUpdate, false);
|
|
@@ -2266,43 +2005,43 @@ async function updateCommand(options) {
|
|
|
2266
2005
|
execSync2(cmd, { stdio: "pipe", cwd: process.cwd() });
|
|
2267
2006
|
}
|
|
2268
2007
|
updateSpinner.succeed("Packages updated successfully");
|
|
2269
|
-
console.log(
|
|
2008
|
+
console.log(chalk5.green(`
|
|
2270
2009
|
\u2713 Updated ${packagesToUpdate.length} package(s)
|
|
2271
2010
|
`));
|
|
2272
2011
|
} catch (error) {
|
|
2273
2012
|
updateSpinner.fail("Failed to update packages");
|
|
2274
|
-
console.error(
|
|
2013
|
+
console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2275
2014
|
process.exit(1);
|
|
2276
2015
|
}
|
|
2277
2016
|
}
|
|
2278
2017
|
|
|
2279
2018
|
// src/commands/exec.ts
|
|
2280
|
-
import
|
|
2281
|
-
import
|
|
2282
|
-
import
|
|
2283
|
-
import
|
|
2019
|
+
import chalk6 from "chalk";
|
|
2020
|
+
import ora6 from "ora";
|
|
2021
|
+
import fs5 from "fs-extra";
|
|
2022
|
+
import path5 from "path";
|
|
2284
2023
|
var isDev4 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
2285
2024
|
var API_URL4 = isDev4 ? "http://localhost:3001/api/v1" : "https://tether-api.strands.gg/api/v1";
|
|
2286
2025
|
async function execCommand(sql) {
|
|
2287
2026
|
if (!sql || sql.trim() === "") {
|
|
2288
|
-
console.log(
|
|
2289
|
-
console.log(
|
|
2027
|
+
console.log(chalk6.red("\nError: SQL query required"));
|
|
2028
|
+
console.log(chalk6.dim('Usage: tthr exec "SELECT * FROM table_name"\n'));
|
|
2290
2029
|
process.exit(1);
|
|
2291
2030
|
}
|
|
2292
2031
|
const credentials = await requireAuth();
|
|
2293
|
-
const envPath =
|
|
2032
|
+
const envPath = path5.resolve(process.cwd(), ".env");
|
|
2294
2033
|
let projectId;
|
|
2295
|
-
if (await
|
|
2296
|
-
const envContent = await
|
|
2034
|
+
if (await fs5.pathExists(envPath)) {
|
|
2035
|
+
const envContent = await fs5.readFile(envPath, "utf-8");
|
|
2297
2036
|
const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
|
|
2298
2037
|
projectId = match?.[1]?.trim();
|
|
2299
2038
|
}
|
|
2300
2039
|
if (!projectId) {
|
|
2301
|
-
console.log(
|
|
2302
|
-
console.log(
|
|
2040
|
+
console.log(chalk6.red("\nError: Project ID not found"));
|
|
2041
|
+
console.log(chalk6.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
|
|
2303
2042
|
process.exit(1);
|
|
2304
2043
|
}
|
|
2305
|
-
const spinner =
|
|
2044
|
+
const spinner = ora6("Executing query...").start();
|
|
2306
2045
|
try {
|
|
2307
2046
|
const response = await fetch(`${API_URL4}/projects/${projectId}/exec`, {
|
|
2308
2047
|
method: "POST",
|
|
@@ -2322,24 +2061,24 @@ async function execCommand(sql) {
|
|
|
2322
2061
|
if (result.columns && result.rows) {
|
|
2323
2062
|
console.log();
|
|
2324
2063
|
if (result.rows.length === 0) {
|
|
2325
|
-
console.log(
|
|
2064
|
+
console.log(chalk6.dim(" No rows returned"));
|
|
2326
2065
|
} else {
|
|
2327
|
-
console.log(
|
|
2328
|
-
console.log(
|
|
2066
|
+
console.log(chalk6.bold(" " + result.columns.join(" ")));
|
|
2067
|
+
console.log(chalk6.dim(" " + result.columns.map(() => "--------").join(" ")));
|
|
2329
2068
|
for (const row of result.rows) {
|
|
2330
2069
|
const values = result.columns.map((col) => {
|
|
2331
2070
|
const val = row[col];
|
|
2332
|
-
if (val === null) return
|
|
2071
|
+
if (val === null) return chalk6.dim("NULL");
|
|
2333
2072
|
if (typeof val === "object") return JSON.stringify(val);
|
|
2334
2073
|
return String(val);
|
|
2335
2074
|
});
|
|
2336
2075
|
console.log(" " + values.join(" "));
|
|
2337
2076
|
}
|
|
2338
|
-
console.log(
|
|
2077
|
+
console.log(chalk6.dim(`
|
|
2339
2078
|
${result.rows.length} row(s)`));
|
|
2340
2079
|
}
|
|
2341
2080
|
} else if (result.rowsAffected !== void 0) {
|
|
2342
|
-
console.log(
|
|
2081
|
+
console.log(chalk6.dim(`
|
|
2343
2082
|
${result.rowsAffected} row(s) affected`));
|
|
2344
2083
|
}
|
|
2345
2084
|
console.log();
|
|
@@ -2353,9 +2092,8 @@ async function execCommand(sql) {
|
|
|
2353
2092
|
var program = new Command();
|
|
2354
2093
|
program.name("tthr").description("Tether CLI - Realtime SQLite for modern applications").version("0.0.1");
|
|
2355
2094
|
program.command("init [name]").description("Create a new Tether project").option("-t, --template <template>", "Project template (vue, svelte, react, vanilla)", "vue").action(initCommand);
|
|
2356
|
-
program.command("dev").description("Start local development server with hot reload").option("-p, --port <port>", "Port to run on", "
|
|
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);
|
|
2357
2096
|
program.command("generate").alias("gen").description("Generate types from schema").action(generateCommand);
|
|
2358
|
-
program.command("migrate").description("Database migrations").argument("[action]", "Migration action: create, up, down, status", "status").option("-n, --name <name>", "Migration name (for create)").action(migrateCommand);
|
|
2359
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);
|
|
2360
2098
|
program.command("login").description("Authenticate with Tether").action(loginCommand);
|
|
2361
2099
|
program.command("logout").description("Sign out of Tether").action(logoutCommand);
|