@tthr/cli 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +821 -167
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import ora from "ora";
|
|
|
9
9
|
import prompts from "prompts";
|
|
10
10
|
import fs2 from "fs-extra";
|
|
11
11
|
import path2 from "path";
|
|
12
|
+
import { execSync } from "child_process";
|
|
12
13
|
|
|
13
14
|
// src/utils/auth.ts
|
|
14
15
|
import chalk from "chalk";
|
|
@@ -32,6 +33,14 @@ async function requireAuth() {
|
|
|
32
33
|
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
33
34
|
process.exit(1);
|
|
34
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
|
+
}
|
|
35
44
|
return credentials;
|
|
36
45
|
}
|
|
37
46
|
async function saveCredentials(credentials) {
|
|
@@ -84,14 +93,14 @@ async function initCommand(name, options) {
|
|
|
84
93
|
name: "template",
|
|
85
94
|
message: "Select a framework:",
|
|
86
95
|
choices: [
|
|
87
|
-
{ title: "Vue
|
|
88
|
-
{ title: "
|
|
89
|
-
{ title: "
|
|
90
|
-
{ title: "Vanilla
|
|
96
|
+
{ title: "Nuxt (Vue)", value: "nuxt", description: "Full-stack Vue framework with SSR" },
|
|
97
|
+
{ title: "Next.js (React)", value: "next", description: "Full-stack React framework with SSR" },
|
|
98
|
+
{ title: "SvelteKit (Svelte)", value: "sveltekit", description: "Full-stack Svelte framework with SSR" },
|
|
99
|
+
{ title: "Vanilla TypeScript", value: "vanilla", description: "Minimal setup for scripts and APIs" }
|
|
91
100
|
],
|
|
92
101
|
initial: 0
|
|
93
102
|
});
|
|
94
|
-
template = response.template || "
|
|
103
|
+
template = response.template || "nuxt";
|
|
95
104
|
}
|
|
96
105
|
const projectPath = path2.resolve(process.cwd(), projectName);
|
|
97
106
|
if (await fs2.pathExists(projectPath)) {
|
|
@@ -126,14 +135,184 @@ async function initCommand(name, options) {
|
|
|
126
135
|
const data = await response.json();
|
|
127
136
|
projectId = data.project.id;
|
|
128
137
|
apiKey = data.apiKey;
|
|
129
|
-
spinner.text =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
spinner.text = `Scaffolding ${template} project...`;
|
|
139
|
+
if (template === "nuxt") {
|
|
140
|
+
await scaffoldNuxtProject(projectName, projectPath, spinner);
|
|
141
|
+
} else if (template === "next") {
|
|
142
|
+
await scaffoldNextProject(projectName, projectPath, spinner);
|
|
143
|
+
} else if (template === "sveltekit") {
|
|
144
|
+
await scaffoldSvelteKitProject(projectName, projectPath, spinner);
|
|
145
|
+
} else {
|
|
146
|
+
await scaffoldVanillaProject(projectName, projectPath, spinner);
|
|
147
|
+
}
|
|
148
|
+
spinner.text = "Adding Tether configuration...";
|
|
149
|
+
await addTetherFiles(projectPath, projectId, apiKey, template);
|
|
150
|
+
spinner.text = "Configuring framework integration...";
|
|
151
|
+
await configureFramework(projectPath, template);
|
|
152
|
+
spinner.text = "Installing Tether packages...";
|
|
153
|
+
await installTetherPackages(projectPath, template);
|
|
154
|
+
spinner.text = "Creating demo page...";
|
|
155
|
+
await createDemoPage(projectPath, template);
|
|
156
|
+
spinner.text = "Running initial migration...";
|
|
157
|
+
await runInitialMigration(projectPath, projectId, apiKey);
|
|
158
|
+
spinner.succeed("Project created successfully!");
|
|
159
|
+
console.log("\n" + chalk2.green("\u2713") + " Project created successfully!\n");
|
|
160
|
+
console.log("Next steps:\n");
|
|
161
|
+
console.log(chalk2.cyan(` cd ${projectName}`));
|
|
162
|
+
if (template === "vanilla") {
|
|
163
|
+
console.log(chalk2.cyan(" npm install"));
|
|
164
|
+
}
|
|
165
|
+
console.log(chalk2.cyan(" npm run dev"));
|
|
166
|
+
console.log("\n" + chalk2.dim("For more information, visit https://tthr.io/docs\n"));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
spinner.fail("Failed to create project");
|
|
169
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function scaffoldNuxtProject(projectName, projectPath, spinner) {
|
|
174
|
+
const parentDir = path2.dirname(projectPath);
|
|
175
|
+
spinner.stop();
|
|
176
|
+
console.log(chalk2.dim("\nRunning nuxi init...\n"));
|
|
177
|
+
try {
|
|
178
|
+
execSync(`npx nuxi@latest init ${projectName} --packageManager npm --gitInit false`, {
|
|
179
|
+
cwd: parentDir,
|
|
180
|
+
stdio: "inherit"
|
|
181
|
+
});
|
|
182
|
+
if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
|
|
183
|
+
throw new Error("Nuxt project was not created successfully - package.json missing");
|
|
184
|
+
}
|
|
185
|
+
spinner.start();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
spinner.start();
|
|
188
|
+
if (error instanceof Error && error.message.includes("package.json missing")) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
throw new Error("Failed to create Nuxt project. Make sure you have npx installed.");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function scaffoldNextProject(projectName, projectPath, spinner) {
|
|
195
|
+
const parentDir = path2.dirname(projectPath);
|
|
196
|
+
spinner.stop();
|
|
197
|
+
console.log(chalk2.dim("\nRunning create-next-app...\n"));
|
|
198
|
+
try {
|
|
199
|
+
execSync(`npx create-next-app@latest ${projectName} --typescript --eslint --tailwind --src-dir --app --import-alias "@/*" --use-npm`, {
|
|
200
|
+
cwd: parentDir,
|
|
201
|
+
stdio: "inherit"
|
|
202
|
+
});
|
|
203
|
+
if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
|
|
204
|
+
throw new Error("Next.js project was not created successfully - package.json missing");
|
|
205
|
+
}
|
|
206
|
+
spinner.start();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
spinner.start();
|
|
209
|
+
if (error instanceof Error && error.message.includes("package.json missing")) {
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
throw new Error("Failed to create Next.js project. Make sure you have npx installed.");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function scaffoldSvelteKitProject(projectName, projectPath, spinner) {
|
|
216
|
+
const parentDir = path2.dirname(projectPath);
|
|
217
|
+
spinner.stop();
|
|
218
|
+
console.log(chalk2.dim("\nRunning sv create...\n"));
|
|
219
|
+
try {
|
|
220
|
+
execSync(`npx sv create ${projectName} --template minimal --types ts --no-add-ons --no-install`, {
|
|
221
|
+
cwd: parentDir,
|
|
222
|
+
stdio: "inherit"
|
|
223
|
+
});
|
|
224
|
+
if (!await fs2.pathExists(path2.join(projectPath, "package.json"))) {
|
|
225
|
+
throw new Error("SvelteKit project was not created successfully - package.json missing");
|
|
226
|
+
}
|
|
227
|
+
spinner.start();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
spinner.start();
|
|
230
|
+
if (error instanceof Error && error.message.includes("package.json missing")) {
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
throw new Error("Failed to create SvelteKit project. Make sure you have npx installed.");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function scaffoldVanillaProject(projectName, projectPath, spinner) {
|
|
237
|
+
spinner.text = "Creating vanilla TypeScript project...";
|
|
238
|
+
await fs2.ensureDir(projectPath);
|
|
239
|
+
await fs2.ensureDir(path2.join(projectPath, "src"));
|
|
240
|
+
const packageJson = {
|
|
241
|
+
name: projectName,
|
|
242
|
+
version: "0.0.1",
|
|
243
|
+
private: true,
|
|
244
|
+
type: "module",
|
|
245
|
+
scripts: {
|
|
246
|
+
"dev": "tsx watch src/index.ts",
|
|
247
|
+
"build": "tsc",
|
|
248
|
+
"start": "node dist/index.js"
|
|
249
|
+
},
|
|
250
|
+
devDependencies: {
|
|
251
|
+
"typescript": "latest",
|
|
252
|
+
"tsx": "latest",
|
|
253
|
+
"@types/node": "latest"
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
await fs2.writeJSON(path2.join(projectPath, "package.json"), packageJson, { spaces: 2 });
|
|
257
|
+
await fs2.writeJSON(
|
|
258
|
+
path2.join(projectPath, "tsconfig.json"),
|
|
259
|
+
{
|
|
260
|
+
compilerOptions: {
|
|
261
|
+
target: "ES2022",
|
|
262
|
+
module: "ESNext",
|
|
263
|
+
moduleResolution: "bundler",
|
|
264
|
+
strict: true,
|
|
265
|
+
esModuleInterop: true,
|
|
266
|
+
skipLibCheck: true,
|
|
267
|
+
forceConsistentCasingInFileNames: true,
|
|
268
|
+
outDir: "./dist",
|
|
269
|
+
rootDir: "./src",
|
|
270
|
+
paths: {
|
|
271
|
+
"@/*": ["./src/*"]
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
include: ["src/**/*.ts"],
|
|
275
|
+
exclude: ["node_modules", "dist"]
|
|
276
|
+
},
|
|
277
|
+
{ spaces: 2 }
|
|
278
|
+
);
|
|
279
|
+
await fs2.writeFile(
|
|
280
|
+
path2.join(projectPath, "src", "index.ts"),
|
|
281
|
+
`import { createClient } from '@tthr/client';
|
|
282
|
+
|
|
283
|
+
const tether = createClient({
|
|
284
|
+
projectId: process.env.TETHER_PROJECT_ID!,
|
|
285
|
+
url: process.env.TETHER_URL || 'https://tthr.io',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
async function main() {
|
|
289
|
+
console.log('Hello from Tether!');
|
|
290
|
+
|
|
291
|
+
// Example: List all posts
|
|
292
|
+
// const posts = await tether.query('posts.list', { limit: 10 });
|
|
293
|
+
// console.log(posts);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
main().catch(console.error);
|
|
297
|
+
`
|
|
298
|
+
);
|
|
299
|
+
await fs2.writeFile(
|
|
300
|
+
path2.join(projectPath, ".gitignore"),
|
|
301
|
+
`node_modules/
|
|
302
|
+
dist/
|
|
303
|
+
.env
|
|
304
|
+
.env.local
|
|
305
|
+
.DS_Store
|
|
306
|
+
`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
async function addTetherFiles(projectPath, projectId, apiKey, template) {
|
|
310
|
+
await fs2.ensureDir(path2.join(projectPath, "tether"));
|
|
311
|
+
await fs2.ensureDir(path2.join(projectPath, "tether", "functions"));
|
|
312
|
+
const configPackage = template === "nuxt" ? "@tthr/vue" : template === "next" ? "@tthr/react" : template === "sveltekit" ? "@tthr/svelte" : "@tthr/client";
|
|
313
|
+
await fs2.writeFile(
|
|
314
|
+
path2.join(projectPath, "tether.config.ts"),
|
|
315
|
+
`import { defineConfig } from '${configPackage}';
|
|
137
316
|
|
|
138
317
|
export default defineConfig({
|
|
139
318
|
// Project configuration
|
|
@@ -152,10 +331,10 @@ export default defineConfig({
|
|
|
152
331
|
output: './_generated',
|
|
153
332
|
});
|
|
154
333
|
`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
334
|
+
);
|
|
335
|
+
await fs2.writeFile(
|
|
336
|
+
path2.join(projectPath, "tether", "schema.ts"),
|
|
337
|
+
`import { defineSchema, text, integer, timestamp } from '@tthr/schema';
|
|
159
338
|
|
|
160
339
|
export default defineSchema({
|
|
161
340
|
// Example: posts table
|
|
@@ -178,18 +357,17 @@ export default defineSchema({
|
|
|
178
357
|
},
|
|
179
358
|
});
|
|
180
359
|
`
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
import { db } from '../../_generated/db';
|
|
360
|
+
);
|
|
361
|
+
await fs2.writeFile(
|
|
362
|
+
path2.join(projectPath, "tether", "functions", "posts.ts"),
|
|
363
|
+
`import { query, mutation, z } from '@tthr/server';
|
|
186
364
|
|
|
187
365
|
// List all posts
|
|
188
366
|
export const list = query({
|
|
189
367
|
args: z.object({
|
|
190
368
|
limit: z.number().optional().default(20),
|
|
191
369
|
}),
|
|
192
|
-
handler: async ({ args }) => {
|
|
370
|
+
handler: async ({ args, db }) => {
|
|
193
371
|
return db.posts.findMany({
|
|
194
372
|
orderBy: { createdAt: 'desc' },
|
|
195
373
|
limit: args.limit,
|
|
@@ -202,7 +380,7 @@ export const get = query({
|
|
|
202
380
|
args: z.object({
|
|
203
381
|
id: z.string(),
|
|
204
382
|
}),
|
|
205
|
-
handler: async ({ args }) => {
|
|
383
|
+
handler: async ({ args, db }) => {
|
|
206
384
|
return db.posts.findUnique({
|
|
207
385
|
where: { id: args.id },
|
|
208
386
|
});
|
|
@@ -215,7 +393,7 @@ export const create = mutation({
|
|
|
215
393
|
title: z.string().min(1),
|
|
216
394
|
content: z.string().optional(),
|
|
217
395
|
}),
|
|
218
|
-
handler: async ({ args, ctx }) => {
|
|
396
|
+
handler: async ({ args, ctx, db }) => {
|
|
219
397
|
const id = crypto.randomUUID();
|
|
220
398
|
const now = new Date().toISOString();
|
|
221
399
|
|
|
@@ -241,7 +419,7 @@ export const update = mutation({
|
|
|
241
419
|
title: z.string().min(1).optional(),
|
|
242
420
|
content: z.string().optional(),
|
|
243
421
|
}),
|
|
244
|
-
handler: async ({ args }) => {
|
|
422
|
+
handler: async ({ args, db }) => {
|
|
245
423
|
await db.posts.update({
|
|
246
424
|
where: { id: args.id },
|
|
247
425
|
data: {
|
|
@@ -258,170 +436,646 @@ export const remove = mutation({
|
|
|
258
436
|
args: z.object({
|
|
259
437
|
id: z.string(),
|
|
260
438
|
}),
|
|
261
|
-
handler: async ({ args }) => {
|
|
439
|
+
handler: async ({ args, db }) => {
|
|
262
440
|
await db.posts.delete({
|
|
263
441
|
where: { id: args.id },
|
|
264
442
|
});
|
|
265
443
|
},
|
|
266
444
|
});
|
|
267
445
|
`
|
|
268
|
-
|
|
446
|
+
);
|
|
447
|
+
const envContent = `# Tether Configuration
|
|
448
|
+
TETHER_PROJECT_ID=${projectId}
|
|
449
|
+
TETHER_URL=${isDev ? "http://localhost:3001" : "https://tthr.io"}
|
|
450
|
+
TETHER_API_KEY=${apiKey}
|
|
451
|
+
`;
|
|
452
|
+
const envPath = path2.join(projectPath, ".env");
|
|
453
|
+
if (await fs2.pathExists(envPath)) {
|
|
454
|
+
const existing = await fs2.readFile(envPath, "utf-8");
|
|
455
|
+
await fs2.writeFile(envPath, existing + "\n" + envContent);
|
|
456
|
+
} else {
|
|
457
|
+
await fs2.writeFile(envPath, envContent);
|
|
458
|
+
}
|
|
459
|
+
const gitignorePath = path2.join(projectPath, ".gitignore");
|
|
460
|
+
const tetherGitignore = `
|
|
461
|
+
# Tether
|
|
462
|
+
_generated/
|
|
463
|
+
.env
|
|
464
|
+
.env.local
|
|
465
|
+
`;
|
|
466
|
+
if (await fs2.pathExists(gitignorePath)) {
|
|
467
|
+
const existing = await fs2.readFile(gitignorePath, "utf-8");
|
|
468
|
+
if (!existing.includes("_generated/")) {
|
|
469
|
+
await fs2.writeFile(gitignorePath, existing + tetherGitignore);
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
await fs2.writeFile(gitignorePath, tetherGitignore.trim());
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function configureFramework(projectPath, template) {
|
|
476
|
+
if (template === "nuxt") {
|
|
477
|
+
await configureNuxt(projectPath);
|
|
478
|
+
} else if (template === "next") {
|
|
479
|
+
await configureNext(projectPath);
|
|
480
|
+
} else if (template === "sveltekit") {
|
|
481
|
+
await configureSvelteKit(projectPath);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async function configureNuxt(projectPath) {
|
|
485
|
+
const configPath = path2.join(projectPath, "nuxt.config.ts");
|
|
486
|
+
if (!await fs2.pathExists(configPath)) {
|
|
269
487
|
await fs2.writeFile(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
488
|
+
configPath,
|
|
489
|
+
`// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
490
|
+
export default defineNuxtConfig({
|
|
491
|
+
compatibilityDate: '2024-11-01',
|
|
492
|
+
devtools: { enabled: true },
|
|
493
|
+
|
|
494
|
+
modules: ['@tthr/vue/nuxt'],
|
|
495
|
+
|
|
496
|
+
tether: {
|
|
497
|
+
projectId: process.env.TETHER_PROJECT_ID,
|
|
498
|
+
url: process.env.TETHER_URL || 'https://tthr.io',
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
`
|
|
502
|
+
);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
let config = await fs2.readFile(configPath, "utf-8");
|
|
506
|
+
if (config.includes("modules:")) {
|
|
507
|
+
config = config.replace(
|
|
508
|
+
/modules:\s*\[/,
|
|
509
|
+
"modules: ['@tthr/vue/nuxt', "
|
|
510
|
+
);
|
|
511
|
+
} else {
|
|
512
|
+
config = config.replace(
|
|
513
|
+
/defineNuxtConfig\(\{/,
|
|
514
|
+
`defineNuxtConfig({
|
|
515
|
+
modules: ['@tthr/vue/nuxt'],
|
|
516
|
+
`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (!config.includes("tether:")) {
|
|
520
|
+
config = config.replace(
|
|
521
|
+
/(\}|\]|'|"|true|false|\d)\s*\n(\s*}\);?\s*)$/,
|
|
522
|
+
`$1,
|
|
289
523
|
|
|
290
|
-
|
|
524
|
+
tether: {
|
|
525
|
+
projectId: process.env.TETHER_PROJECT_ID,
|
|
526
|
+
url: process.env.TETHER_URL || 'https://tthr.io',
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
await fs2.writeFile(configPath, config);
|
|
533
|
+
}
|
|
534
|
+
async function configureNext(projectPath) {
|
|
535
|
+
const providersPath = path2.join(projectPath, "src", "app", "providers.tsx");
|
|
536
|
+
await fs2.writeFile(
|
|
537
|
+
providersPath,
|
|
538
|
+
`'use client';
|
|
539
|
+
|
|
540
|
+
import { TetherProvider } from '@tthr/react';
|
|
541
|
+
|
|
542
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
543
|
+
return (
|
|
544
|
+
<TetherProvider
|
|
545
|
+
projectId={process.env.NEXT_PUBLIC_TETHER_PROJECT_ID!}
|
|
546
|
+
url={process.env.NEXT_PUBLIC_TETHER_URL || 'https://tthr.io'}
|
|
547
|
+
>
|
|
548
|
+
{children}
|
|
549
|
+
</TetherProvider>
|
|
550
|
+
);
|
|
291
551
|
}
|
|
552
|
+
`
|
|
553
|
+
);
|
|
554
|
+
const layoutPath = path2.join(projectPath, "src", "app", "layout.tsx");
|
|
555
|
+
if (await fs2.pathExists(layoutPath)) {
|
|
556
|
+
let layout = await fs2.readFile(layoutPath, "utf-8");
|
|
557
|
+
if (!layout.includes("import { Providers }")) {
|
|
558
|
+
layout = layout.replace(
|
|
559
|
+
/^(import.*\n)+/m,
|
|
560
|
+
(match) => match + "import { Providers } from './providers';\n"
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
if (!layout.includes("<Providers>")) {
|
|
564
|
+
layout = layout.replace(
|
|
565
|
+
/(<body[^>]*>)([\s\S]*?)(<\/body>)/,
|
|
566
|
+
"$1<Providers>$2</Providers>$3"
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
await fs2.writeFile(layoutPath, layout);
|
|
570
|
+
}
|
|
571
|
+
const envLocalPath = path2.join(projectPath, ".env.local");
|
|
572
|
+
const nextEnvContent = `# Tether Configuration (client-side)
|
|
573
|
+
NEXT_PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
|
|
574
|
+
NEXT_PUBLIC_TETHER_URL=\${TETHER_URL}
|
|
575
|
+
`;
|
|
576
|
+
if (await fs2.pathExists(envLocalPath)) {
|
|
577
|
+
const existing = await fs2.readFile(envLocalPath, "utf-8");
|
|
578
|
+
await fs2.writeFile(envLocalPath, existing + "\n" + nextEnvContent);
|
|
579
|
+
} else {
|
|
580
|
+
await fs2.writeFile(envLocalPath, nextEnvContent);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async function configureSvelteKit(projectPath) {
|
|
584
|
+
const libPath = path2.join(projectPath, "src", "lib");
|
|
585
|
+
await fs2.ensureDir(libPath);
|
|
586
|
+
await fs2.writeFile(
|
|
587
|
+
path2.join(libPath, "tether.ts"),
|
|
588
|
+
`import { createClient } from '@tthr/svelte';
|
|
589
|
+
import { PUBLIC_TETHER_PROJECT_ID, PUBLIC_TETHER_URL } from '$env/static/public';
|
|
590
|
+
|
|
591
|
+
export const tether = createClient({
|
|
592
|
+
projectId: PUBLIC_TETHER_PROJECT_ID,
|
|
593
|
+
url: PUBLIC_TETHER_URL || 'https://tthr.io',
|
|
594
|
+
});
|
|
595
|
+
`
|
|
596
|
+
);
|
|
597
|
+
const envPath = path2.join(projectPath, ".env");
|
|
598
|
+
const svelteEnvContent = `# Tether Configuration (public)
|
|
599
|
+
PUBLIC_TETHER_PROJECT_ID=\${TETHER_PROJECT_ID}
|
|
600
|
+
PUBLIC_TETHER_URL=\${TETHER_URL}
|
|
601
|
+
`;
|
|
602
|
+
if (await fs2.pathExists(envPath)) {
|
|
603
|
+
const existing = await fs2.readFile(envPath, "utf-8");
|
|
604
|
+
if (!existing.includes("PUBLIC_TETHER_")) {
|
|
605
|
+
await fs2.writeFile(envPath, existing + "\n" + svelteEnvContent);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async function installTetherPackages(projectPath, template) {
|
|
610
|
+
const [
|
|
611
|
+
tthrClientVersion,
|
|
612
|
+
tthrSchemaVersion,
|
|
613
|
+
tthrServerVersion,
|
|
614
|
+
tthrCliVersion
|
|
615
|
+
] = await Promise.all([
|
|
616
|
+
getLatestVersion("@tthr/client"),
|
|
617
|
+
getLatestVersion("@tthr/schema"),
|
|
618
|
+
getLatestVersion("@tthr/server"),
|
|
619
|
+
getLatestVersion("@tthr/cli")
|
|
620
|
+
]);
|
|
621
|
+
const packages = [
|
|
622
|
+
`@tthr/client@${tthrClientVersion}`,
|
|
623
|
+
`@tthr/schema@${tthrSchemaVersion}`,
|
|
624
|
+
`@tthr/server@${tthrServerVersion}`
|
|
625
|
+
];
|
|
626
|
+
const devPackages = [
|
|
627
|
+
`@tthr/cli@${tthrCliVersion}`
|
|
628
|
+
];
|
|
629
|
+
if (template === "nuxt") {
|
|
630
|
+
const tthrVueVersion = await getLatestVersion("@tthr/vue");
|
|
631
|
+
packages.push(`@tthr/vue@${tthrVueVersion}`);
|
|
632
|
+
} else if (template === "next") {
|
|
633
|
+
const tthrReactVersion = await getLatestVersion("@tthr/react");
|
|
634
|
+
packages.push(`@tthr/react@${tthrReactVersion}`);
|
|
635
|
+
} else if (template === "sveltekit") {
|
|
636
|
+
const tthrSvelteVersion = await getLatestVersion("@tthr/svelte");
|
|
637
|
+
packages.push(`@tthr/svelte@${tthrSvelteVersion}`);
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
execSync(`npm install ${packages.join(" ")}`, {
|
|
641
|
+
cwd: projectPath,
|
|
642
|
+
stdio: "pipe"
|
|
643
|
+
});
|
|
644
|
+
execSync(`npm install -D ${devPackages.join(" ")}`, {
|
|
645
|
+
cwd: projectPath,
|
|
646
|
+
stdio: "pipe"
|
|
647
|
+
});
|
|
648
|
+
} catch (error) {
|
|
649
|
+
console.warn(chalk2.yellow("\nNote: Some Tether packages may not be published yet."));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
async function createDemoPage(projectPath, template) {
|
|
653
|
+
if (template === "nuxt") {
|
|
654
|
+
const nuxt4AppVuePath = path2.join(projectPath, "app", "app.vue");
|
|
655
|
+
const nuxt3AppVuePath = path2.join(projectPath, "app.vue");
|
|
656
|
+
const appVuePath = await fs2.pathExists(path2.join(projectPath, "app")) ? nuxt4AppVuePath : nuxt3AppVuePath;
|
|
657
|
+
await fs2.writeFile(
|
|
658
|
+
appVuePath,
|
|
659
|
+
`<template>
|
|
660
|
+
<TetherWelcome />
|
|
661
|
+
</template>
|
|
292
662
|
`
|
|
293
663
|
);
|
|
664
|
+
} else if (template === "next") {
|
|
665
|
+
const pagePath = path2.join(projectPath, "src", "app", "page.tsx");
|
|
294
666
|
await fs2.writeFile(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
667
|
+
pagePath,
|
|
668
|
+
`'use client';
|
|
669
|
+
|
|
670
|
+
import { useState } from 'react';
|
|
671
|
+
import { useQuery, useMutation } from '@tthr/react';
|
|
672
|
+
|
|
673
|
+
interface Post {
|
|
674
|
+
id: string;
|
|
675
|
+
title: string;
|
|
676
|
+
createdAt: string;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export default function Home() {
|
|
680
|
+
const [newPostTitle, setNewPostTitle] = useState('');
|
|
681
|
+
const { data: posts, isLoading, refetch } = useQuery<Post[]>('posts.list');
|
|
682
|
+
const { mutate: createPost, isPending } = useMutation('posts.create');
|
|
683
|
+
const { mutate: deletePost } = useMutation('posts.remove');
|
|
684
|
+
|
|
685
|
+
const handleCreatePost = async (e: React.FormEvent) => {
|
|
686
|
+
e.preventDefault();
|
|
687
|
+
if (!newPostTitle.trim()) return;
|
|
688
|
+
|
|
689
|
+
await createPost({ title: newPostTitle.trim(), content: '' });
|
|
690
|
+
setNewPostTitle('');
|
|
691
|
+
refetch();
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const handleDeletePost = async (id: string) => {
|
|
695
|
+
await deletePost({ id });
|
|
696
|
+
refetch();
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
return (
|
|
700
|
+
<main className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 text-white p-8">
|
|
701
|
+
<div className="max-w-2xl mx-auto space-y-8">
|
|
702
|
+
<header className="text-center space-y-4">
|
|
703
|
+
<h1 className="text-4xl font-bold bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">
|
|
704
|
+
Welcome to Tether
|
|
705
|
+
</h1>
|
|
706
|
+
<p className="text-gray-400">Realtime SQLite for modern apps</p>
|
|
707
|
+
</header>
|
|
708
|
+
|
|
709
|
+
<section className="bg-white/5 border border-white/10 rounded-xl p-6 space-y-4">
|
|
710
|
+
<h2 className="text-xl font-semibold">Try it out</h2>
|
|
711
|
+
<p className="text-gray-400 text-sm">Create posts in realtime. Open in multiple tabs to see live updates!</p>
|
|
712
|
+
|
|
713
|
+
<form onSubmit={handleCreatePost} className="flex gap-3">
|
|
714
|
+
<input
|
|
715
|
+
type="text"
|
|
716
|
+
value={newPostTitle}
|
|
717
|
+
onChange={(e) => setNewPostTitle(e.target.value)}
|
|
718
|
+
placeholder="Enter a post title..."
|
|
719
|
+
className="flex-1 px-4 py-2 bg-black/30 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500"
|
|
720
|
+
disabled={isPending}
|
|
721
|
+
/>
|
|
722
|
+
<button
|
|
723
|
+
type="submit"
|
|
724
|
+
disabled={!newPostTitle.trim() || isPending}
|
|
725
|
+
className="px-6 py-2 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed hover:opacity-90 transition"
|
|
726
|
+
>
|
|
727
|
+
{isPending ? 'Creating...' : 'Create Post'}
|
|
728
|
+
</button>
|
|
729
|
+
</form>
|
|
730
|
+
|
|
731
|
+
<div className="space-y-2 min-h-[100px]">
|
|
732
|
+
{isLoading ? (
|
|
733
|
+
<p className="text-gray-500 text-center py-8">Loading posts...</p>
|
|
734
|
+
) : posts?.length ? (
|
|
735
|
+
posts.map((post) => (
|
|
736
|
+
<div
|
|
737
|
+
key={post.id}
|
|
738
|
+
className="flex items-center justify-between p-3 bg-black/20 border border-white/5 rounded-lg hover:bg-black/30 transition"
|
|
739
|
+
>
|
|
740
|
+
<div>
|
|
741
|
+
<h3 className="font-medium">{post.title}</h3>
|
|
742
|
+
<time className="text-xs text-gray-500">
|
|
743
|
+
{new Date(post.createdAt).toLocaleDateString('en-GB', {
|
|
744
|
+
day: 'numeric',
|
|
745
|
+
month: 'short',
|
|
746
|
+
year: 'numeric',
|
|
747
|
+
})}
|
|
748
|
+
</time>
|
|
749
|
+
</div>
|
|
750
|
+
<button
|
|
751
|
+
onClick={() => handleDeletePost(post.id)}
|
|
752
|
+
className="p-2 text-gray-500 hover:text-red-400 hover:bg-red-400/10 rounded transition"
|
|
753
|
+
>
|
|
754
|
+
<svg className="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
|
755
|
+
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
756
|
+
</svg>
|
|
757
|
+
</button>
|
|
758
|
+
</div>
|
|
759
|
+
))
|
|
760
|
+
) : (
|
|
761
|
+
<p className="text-gray-500 text-center py-8">No posts yet. Create your first one above!</p>
|
|
762
|
+
)}
|
|
763
|
+
</div>
|
|
764
|
+
</section>
|
|
765
|
+
|
|
766
|
+
<footer className="text-center text-gray-600 text-sm border-t border-white/5 pt-6">
|
|
767
|
+
Built with Tether by <a href="https://strands.gg" className="text-indigo-400 hover:underline">Strands Services</a>
|
|
768
|
+
</footer>
|
|
769
|
+
</div>
|
|
770
|
+
</main>
|
|
771
|
+
);
|
|
772
|
+
}
|
|
300
773
|
`
|
|
301
774
|
);
|
|
775
|
+
} else if (template === "sveltekit") {
|
|
776
|
+
const pagePath = path2.join(projectPath, "src", "routes", "+page.svelte");
|
|
302
777
|
await fs2.writeFile(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
778
|
+
pagePath,
|
|
779
|
+
`<script lang="ts">
|
|
780
|
+
import { onMount } from 'svelte';
|
|
781
|
+
import { tether } from '$lib/tether';
|
|
306
782
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
783
|
+
interface Post {
|
|
784
|
+
id: string;
|
|
785
|
+
title: string;
|
|
786
|
+
createdAt: string;
|
|
787
|
+
}
|
|
310
788
|
|
|
311
|
-
|
|
312
|
-
|
|
789
|
+
let posts: Post[] = [];
|
|
790
|
+
let isLoading = true;
|
|
791
|
+
let newPostTitle = '';
|
|
792
|
+
let isPending = false;
|
|
313
793
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
.env.*.local
|
|
794
|
+
onMount(async () => {
|
|
795
|
+
await loadPosts();
|
|
796
|
+
});
|
|
318
797
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
798
|
+
async function loadPosts() {
|
|
799
|
+
isLoading = true;
|
|
800
|
+
try {
|
|
801
|
+
posts = await tether.query('posts.list');
|
|
802
|
+
} catch (e) {
|
|
803
|
+
console.error('Failed to load posts:', e);
|
|
804
|
+
} finally {
|
|
805
|
+
isLoading = false;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
324
808
|
|
|
325
|
-
|
|
326
|
-
.
|
|
327
|
-
|
|
809
|
+
async function handleCreatePost() {
|
|
810
|
+
if (!newPostTitle.trim()) return;
|
|
811
|
+
isPending = true;
|
|
812
|
+
try {
|
|
813
|
+
await tether.mutation('posts.create', { title: newPostTitle.trim(), content: '' });
|
|
814
|
+
newPostTitle = '';
|
|
815
|
+
await loadPosts();
|
|
816
|
+
} catch (e) {
|
|
817
|
+
console.error('Failed to create post:', e);
|
|
818
|
+
} finally {
|
|
819
|
+
isPending = false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function handleDeletePost(id: string) {
|
|
824
|
+
try {
|
|
825
|
+
await tether.mutation('posts.remove', { id });
|
|
826
|
+
await loadPosts();
|
|
827
|
+
} catch (e) {
|
|
828
|
+
console.error('Failed to delete post:', e);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function formatDate(dateString: string): string {
|
|
833
|
+
return new Date(dateString).toLocaleDateString('en-GB', {
|
|
834
|
+
day: 'numeric',
|
|
835
|
+
month: 'short',
|
|
836
|
+
year: 'numeric',
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
</script>
|
|
840
|
+
|
|
841
|
+
<main>
|
|
842
|
+
<div class="container">
|
|
843
|
+
<header>
|
|
844
|
+
<h1>Welcome to Tether</h1>
|
|
845
|
+
<p>Realtime SQLite for modern apps</p>
|
|
846
|
+
</header>
|
|
847
|
+
|
|
848
|
+
<section class="demo">
|
|
849
|
+
<h2>Try it out</h2>
|
|
850
|
+
<p class="intro">Create posts in realtime. Open in multiple tabs to see live updates!</p>
|
|
851
|
+
|
|
852
|
+
<form on:submit|preventDefault={handleCreatePost}>
|
|
853
|
+
<input
|
|
854
|
+
type="text"
|
|
855
|
+
bind:value={newPostTitle}
|
|
856
|
+
placeholder="Enter a post title..."
|
|
857
|
+
disabled={isPending}
|
|
858
|
+
/>
|
|
859
|
+
<button type="submit" disabled={!newPostTitle.trim() || isPending}>
|
|
860
|
+
{isPending ? 'Creating...' : 'Create Post'}
|
|
861
|
+
</button>
|
|
862
|
+
</form>
|
|
863
|
+
|
|
864
|
+
<div class="posts">
|
|
865
|
+
{#if isLoading}
|
|
866
|
+
<p class="empty">Loading posts...</p>
|
|
867
|
+
{:else if posts.length}
|
|
868
|
+
{#each posts as post (post.id)}
|
|
869
|
+
<article>
|
|
870
|
+
<div class="content">
|
|
871
|
+
<h3>{post.title}</h3>
|
|
872
|
+
<time>{formatDate(post.createdAt)}</time>
|
|
873
|
+
</div>
|
|
874
|
+
<button class="delete" on:click={() => handleDeletePost(post.id)}>
|
|
875
|
+
Delete
|
|
876
|
+
</button>
|
|
877
|
+
</article>
|
|
878
|
+
{/each}
|
|
879
|
+
{:else}
|
|
880
|
+
<p class="empty">No posts yet. Create your first one above!</p>
|
|
881
|
+
{/if}
|
|
882
|
+
</div>
|
|
883
|
+
</section>
|
|
884
|
+
|
|
885
|
+
<footer>
|
|
886
|
+
Built with Tether by <a href="https://strands.gg">Strands Services</a>
|
|
887
|
+
</footer>
|
|
888
|
+
</div>
|
|
889
|
+
</main>
|
|
890
|
+
|
|
891
|
+
<style>
|
|
892
|
+
main {
|
|
893
|
+
min-height: 100vh;
|
|
894
|
+
background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%);
|
|
895
|
+
color: #e4e4e7;
|
|
896
|
+
font-family: system-ui, sans-serif;
|
|
897
|
+
padding: 2rem;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.container {
|
|
901
|
+
max-width: 640px;
|
|
902
|
+
margin: 0 auto;
|
|
903
|
+
display: flex;
|
|
904
|
+
flex-direction: column;
|
|
905
|
+
gap: 2rem;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
header {
|
|
909
|
+
text-align: center;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
h1 {
|
|
913
|
+
font-size: 2.5rem;
|
|
914
|
+
background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
|
|
915
|
+
-webkit-background-clip: text;
|
|
916
|
+
-webkit-text-fill-color: transparent;
|
|
917
|
+
margin: 0 0 0.5rem;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
header p {
|
|
921
|
+
color: #a1a1aa;
|
|
922
|
+
margin: 0;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.demo {
|
|
926
|
+
background: rgba(255, 255, 255, 0.03);
|
|
927
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
928
|
+
border-radius: 1rem;
|
|
929
|
+
padding: 1.5rem;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
.demo h2 {
|
|
933
|
+
margin: 0 0 0.5rem;
|
|
934
|
+
font-size: 1.25rem;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.intro {
|
|
938
|
+
color: #a1a1aa;
|
|
939
|
+
font-size: 0.875rem;
|
|
940
|
+
margin: 0 0 1rem;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
form {
|
|
944
|
+
display: flex;
|
|
945
|
+
gap: 0.75rem;
|
|
946
|
+
margin-bottom: 1rem;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
input {
|
|
950
|
+
flex: 1;
|
|
951
|
+
padding: 0.75rem 1rem;
|
|
952
|
+
background: rgba(0, 0, 0, 0.3);
|
|
953
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
954
|
+
border-radius: 0.5rem;
|
|
955
|
+
color: #e4e4e7;
|
|
956
|
+
font-size: 0.875rem;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
input:focus {
|
|
960
|
+
outline: none;
|
|
961
|
+
border-color: #6366f1;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
button {
|
|
965
|
+
padding: 0.75rem 1.5rem;
|
|
966
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
967
|
+
border: none;
|
|
968
|
+
border-radius: 0.5rem;
|
|
969
|
+
color: white;
|
|
970
|
+
font-weight: 500;
|
|
971
|
+
cursor: pointer;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
button:disabled {
|
|
975
|
+
opacity: 0.5;
|
|
976
|
+
cursor: not-allowed;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.posts {
|
|
980
|
+
display: flex;
|
|
981
|
+
flex-direction: column;
|
|
982
|
+
gap: 0.5rem;
|
|
983
|
+
min-height: 100px;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.empty {
|
|
987
|
+
color: #71717a;
|
|
988
|
+
text-align: center;
|
|
989
|
+
padding: 2rem;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
article {
|
|
993
|
+
display: flex;
|
|
994
|
+
align-items: center;
|
|
995
|
+
justify-content: space-between;
|
|
996
|
+
padding: 0.875rem 1rem;
|
|
997
|
+
background: rgba(0, 0, 0, 0.2);
|
|
998
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
999
|
+
border-radius: 0.5rem;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.content h3 {
|
|
1003
|
+
margin: 0 0 0.25rem;
|
|
1004
|
+
font-size: 0.9375rem;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.content time {
|
|
1008
|
+
font-size: 0.75rem;
|
|
1009
|
+
color: #71717a;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.delete {
|
|
1013
|
+
padding: 0.5rem 1rem;
|
|
1014
|
+
background: transparent;
|
|
1015
|
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
1016
|
+
color: #fca5a5;
|
|
1017
|
+
font-size: 0.75rem;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.delete:hover {
|
|
1021
|
+
background: rgba(239, 68, 68, 0.1);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
footer {
|
|
1025
|
+
text-align: center;
|
|
1026
|
+
color: #52525b;
|
|
1027
|
+
font-size: 0.8125rem;
|
|
1028
|
+
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
|
1029
|
+
padding-top: 1rem;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
footer a {
|
|
1033
|
+
color: #6366f1;
|
|
1034
|
+
text-decoration: none;
|
|
1035
|
+
}
|
|
1036
|
+
</style>
|
|
328
1037
|
`
|
|
329
1038
|
);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
getLatestVersion("@tthr/server"),
|
|
342
|
-
getLatestVersion("@tthr/cli"),
|
|
343
|
-
getLatestVersion("typescript"),
|
|
344
|
-
getLatestVersion("tsx")
|
|
345
|
-
]);
|
|
346
|
-
const dependencies = {
|
|
347
|
-
"@tthr/client": tthrClientVersion,
|
|
348
|
-
"@tthr/schema": tthrSchemaVersion,
|
|
349
|
-
"@tthr/server": tthrServerVersion
|
|
350
|
-
};
|
|
351
|
-
if (template === "vue") {
|
|
352
|
-
const [tthrVueVersion, vueVersion] = await Promise.all([
|
|
353
|
-
getLatestVersion("@tthr/vue"),
|
|
354
|
-
getLatestVersion("vue")
|
|
355
|
-
]);
|
|
356
|
-
dependencies["@tthr/vue"] = tthrVueVersion;
|
|
357
|
-
dependencies["vue"] = vueVersion;
|
|
358
|
-
} else if (template === "react") {
|
|
359
|
-
const [tthrReactVersion, reactVersion, reactDomVersion] = await Promise.all([
|
|
360
|
-
getLatestVersion("@tthr/react"),
|
|
361
|
-
getLatestVersion("react"),
|
|
362
|
-
getLatestVersion("react-dom")
|
|
363
|
-
]);
|
|
364
|
-
dependencies["@tthr/react"] = tthrReactVersion;
|
|
365
|
-
dependencies["react"] = reactVersion;
|
|
366
|
-
dependencies["react-dom"] = reactDomVersion;
|
|
367
|
-
} else if (template === "svelte") {
|
|
368
|
-
const [tthrSvelteVersion, svelteVersion] = await Promise.all([
|
|
369
|
-
getLatestVersion("@tthr/svelte"),
|
|
370
|
-
getLatestVersion("svelte")
|
|
371
|
-
]);
|
|
372
|
-
dependencies["@tthr/svelte"] = tthrSvelteVersion;
|
|
373
|
-
dependencies["svelte"] = svelteVersion;
|
|
374
|
-
}
|
|
375
|
-
const packageJson = {
|
|
376
|
-
name: projectName,
|
|
377
|
-
version: "0.0.1",
|
|
378
|
-
private: true,
|
|
379
|
-
type: "module",
|
|
380
|
-
scripts: {
|
|
381
|
-
"dev": "tthr dev",
|
|
382
|
-
"generate": "tthr generate",
|
|
383
|
-
"migrate": "tthr migrate",
|
|
384
|
-
"seed": "tsx tether/seed.ts"
|
|
385
|
-
},
|
|
386
|
-
dependencies,
|
|
387
|
-
devDependencies: {
|
|
388
|
-
"@tthr/cli": tthrCliVersion,
|
|
389
|
-
"typescript": typescriptVersion,
|
|
390
|
-
"tsx": tsxVersion
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
await fs2.writeJSON(path2.join(projectPath, "package.json"), packageJson, { spaces: 2 });
|
|
394
|
-
await fs2.writeJSON(
|
|
395
|
-
path2.join(projectPath, "tsconfig.json"),
|
|
396
|
-
{
|
|
397
|
-
compilerOptions: {
|
|
398
|
-
target: "ES2022",
|
|
399
|
-
module: "ESNext",
|
|
400
|
-
moduleResolution: "bundler",
|
|
401
|
-
strict: true,
|
|
402
|
-
esModuleInterop: true,
|
|
403
|
-
skipLibCheck: true,
|
|
404
|
-
forceConsistentCasingInFileNames: true,
|
|
405
|
-
paths: {
|
|
406
|
-
"@/*": ["./*"]
|
|
407
|
-
}
|
|
408
|
-
},
|
|
409
|
-
include: ["**/*.ts"],
|
|
410
|
-
exclude: ["node_modules", "dist"]
|
|
411
|
-
},
|
|
412
|
-
{ spaces: 2 }
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
async function runInitialMigration(projectPath, projectId, apiKey) {
|
|
1042
|
+
const migrationSql = `
|
|
1043
|
+
CREATE TABLE IF NOT EXISTS posts (
|
|
1044
|
+
id TEXT PRIMARY KEY,
|
|
1045
|
+
title TEXT NOT NULL,
|
|
1046
|
+
content TEXT,
|
|
1047
|
+
authorId TEXT NOT NULL,
|
|
1048
|
+
createdAt TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1049
|
+
updatedAt TEXT NOT NULL DEFAULT (datetime('now'))
|
|
413
1050
|
);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
1051
|
+
|
|
1052
|
+
CREATE TABLE IF NOT EXISTS comments (
|
|
1053
|
+
id TEXT PRIMARY KEY,
|
|
1054
|
+
postId TEXT NOT NULL REFERENCES posts(id),
|
|
1055
|
+
content TEXT NOT NULL,
|
|
1056
|
+
authorId TEXT NOT NULL,
|
|
1057
|
+
createdAt TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
CREATE INDEX IF NOT EXISTS idx_posts_authorId ON posts(authorId);
|
|
1061
|
+
CREATE INDEX IF NOT EXISTS idx_posts_createdAt ON posts(createdAt);
|
|
1062
|
+
CREATE INDEX IF NOT EXISTS idx_comments_postId ON comments(postId);
|
|
1063
|
+
`;
|
|
1064
|
+
try {
|
|
1065
|
+
const response = await fetch(`${API_URL}/${projectId}/mutation`, {
|
|
1066
|
+
method: "POST",
|
|
1067
|
+
headers: {
|
|
1068
|
+
"Content-Type": "application/json",
|
|
1069
|
+
"Authorization": `Bearer ${apiKey}`
|
|
1070
|
+
},
|
|
1071
|
+
body: JSON.stringify({
|
|
1072
|
+
function: "_migrate",
|
|
1073
|
+
args: { sql: migrationSql }
|
|
1074
|
+
})
|
|
1075
|
+
});
|
|
1076
|
+
if (!response.ok) {
|
|
1077
|
+
}
|
|
1078
|
+
} catch {
|
|
425
1079
|
}
|
|
426
1080
|
}
|
|
427
1081
|
|