@tthr/cli 0.0.3 → 0.0.4

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.
Files changed (2) hide show
  1. package/dist/index.js +293 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1356,76 +1356,339 @@ async function showStatus(migrationsDir) {
1356
1356
  }
1357
1357
  }
1358
1358
 
1359
- // src/commands/login.ts
1359
+ // src/commands/deploy.ts
1360
1360
  import chalk6 from "chalk";
1361
1361
  import ora5 from "ora";
1362
+ import fs6 from "fs-extra";
1363
+ import path6 from "path";
1362
1364
  var isDev2 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
1363
1365
  var API_URL2 = isDev2 ? "http://localhost:3001/api/v1" : "https://tthr.io/api/v1";
1364
- var AUTH_URL = isDev2 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
1366
+ async function deployCommand(options) {
1367
+ const credentials = await requireAuth();
1368
+ const configPath = path6.resolve(process.cwd(), "tether.config.ts");
1369
+ if (!await fs6.pathExists(configPath)) {
1370
+ console.log(chalk6.red("\nError: Not a Tether project"));
1371
+ console.log(chalk6.dim("Run `tthr init` to create a new project\n"));
1372
+ process.exit(1);
1373
+ }
1374
+ const envPath = path6.resolve(process.cwd(), ".env");
1375
+ let projectId;
1376
+ if (await fs6.pathExists(envPath)) {
1377
+ const envContent = await fs6.readFile(envPath, "utf-8");
1378
+ const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
1379
+ projectId = match?.[1]?.trim();
1380
+ }
1381
+ if (!projectId) {
1382
+ console.log(chalk6.red("\nError: Project ID not found"));
1383
+ console.log(chalk6.dim("Make sure TETHER_PROJECT_ID is set in your .env file\n"));
1384
+ process.exit(1);
1385
+ }
1386
+ console.log(chalk6.bold("\n\u26A1 Deploying to Tether\n"));
1387
+ console.log(chalk6.dim(` Project: ${projectId}`));
1388
+ console.log(chalk6.dim(` API: ${API_URL2}
1389
+ `));
1390
+ const deploySchema = options.schema || !options.schema && !options.functions;
1391
+ const deployFunctions = options.functions || !options.schema && !options.functions;
1392
+ if (deploySchema) {
1393
+ await deploySchemaToServer(projectId, credentials.accessToken, options.dryRun);
1394
+ }
1395
+ if (deployFunctions) {
1396
+ await deployFunctionsToServer(projectId, credentials.accessToken, options.dryRun);
1397
+ }
1398
+ console.log(chalk6.green("\n\u2713 Deployment complete\n"));
1399
+ }
1400
+ async function deploySchemaToServer(projectId, token, dryRun) {
1401
+ const spinner = ora5("Reading schema...").start();
1402
+ try {
1403
+ const schemaPath = path6.resolve(process.cwd(), "tether", "schema.ts");
1404
+ if (!await fs6.pathExists(schemaPath)) {
1405
+ spinner.warn("No schema file found");
1406
+ console.log(chalk6.dim(" Create tether/schema.ts to define your database schema\n"));
1407
+ return;
1408
+ }
1409
+ const schemaSource = await fs6.readFile(schemaPath, "utf-8");
1410
+ const tables = parseSchema(schemaSource);
1411
+ spinner.text = `Found ${tables.length} table(s)`;
1412
+ if (dryRun) {
1413
+ spinner.info("Dry run - would deploy schema:");
1414
+ for (const table of tables) {
1415
+ console.log(chalk6.dim(` - ${table.name}`));
1416
+ }
1417
+ return;
1418
+ }
1419
+ const sql = generateSchemaSQL(tables);
1420
+ spinner.text = "Deploying schema...";
1421
+ const response = await fetch(`${API_URL2}/${projectId}/mutation`, {
1422
+ method: "POST",
1423
+ headers: {
1424
+ "Content-Type": "application/json",
1425
+ "Authorization": `Bearer ${token}`
1426
+ },
1427
+ body: JSON.stringify({
1428
+ function: "_migrate",
1429
+ args: { sql }
1430
+ })
1431
+ });
1432
+ if (!response.ok) {
1433
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
1434
+ throw new Error(error.error || `HTTP ${response.status}`);
1435
+ }
1436
+ spinner.succeed(`Schema deployed (${tables.length} table(s))`);
1437
+ } catch (error) {
1438
+ spinner.fail("Failed to deploy schema");
1439
+ console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1440
+ }
1441
+ }
1442
+ async function deployFunctionsToServer(projectId, token, dryRun) {
1443
+ const spinner = ora5("Reading functions...").start();
1444
+ try {
1445
+ const functionsDir = path6.resolve(process.cwd(), "tether", "functions");
1446
+ if (!await fs6.pathExists(functionsDir)) {
1447
+ spinner.warn("No functions directory found");
1448
+ console.log(chalk6.dim(" Create tether/functions/ to define your API functions\n"));
1449
+ return;
1450
+ }
1451
+ const files = await fs6.readdir(functionsDir);
1452
+ const tsFiles = files.filter((f) => f.endsWith(".ts"));
1453
+ if (tsFiles.length === 0) {
1454
+ spinner.info("No function files found");
1455
+ return;
1456
+ }
1457
+ const functions = [];
1458
+ for (const file of tsFiles) {
1459
+ const filePath = path6.join(functionsDir, file);
1460
+ const source = await fs6.readFile(filePath, "utf-8");
1461
+ const moduleName = file.replace(".ts", "");
1462
+ const parsedFunctions = parseFunctions(moduleName, source);
1463
+ functions.push(...parsedFunctions);
1464
+ }
1465
+ spinner.text = `Found ${functions.length} function(s)`;
1466
+ if (dryRun) {
1467
+ spinner.info("Dry run - would deploy functions:");
1468
+ for (const fn of functions) {
1469
+ const icon = fn.type === "query" ? "\u{1F50D}" : fn.type === "mutation" ? "\u270F\uFE0F" : "\u26A1";
1470
+ console.log(chalk6.dim(` ${icon} ${fn.name} (${fn.type})`));
1471
+ }
1472
+ return;
1473
+ }
1474
+ spinner.text = "Deploying functions...";
1475
+ const response = await fetch(`${API_URL2}/${projectId}/deploy/functions`, {
1476
+ method: "POST",
1477
+ headers: {
1478
+ "Content-Type": "application/json",
1479
+ "Authorization": `Bearer ${token}`
1480
+ },
1481
+ body: JSON.stringify({
1482
+ functions: functions.map((fn) => ({
1483
+ name: fn.name,
1484
+ type: fn.type,
1485
+ source: fn.source
1486
+ }))
1487
+ })
1488
+ });
1489
+ if (!response.ok) {
1490
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
1491
+ throw new Error(error.error || `HTTP ${response.status}`);
1492
+ }
1493
+ spinner.succeed(`Functions deployed (${functions.length} function(s))`);
1494
+ const queries = functions.filter((f) => f.type === "query");
1495
+ const mutations = functions.filter((f) => f.type === "mutation");
1496
+ const actions = functions.filter((f) => f.type === "action");
1497
+ if (queries.length > 0) {
1498
+ console.log(chalk6.dim(`
1499
+ Queries:`));
1500
+ for (const fn of queries) {
1501
+ console.log(chalk6.dim(` - ${fn.name}`));
1502
+ }
1503
+ }
1504
+ if (mutations.length > 0) {
1505
+ console.log(chalk6.dim(`
1506
+ Mutations:`));
1507
+ for (const fn of mutations) {
1508
+ console.log(chalk6.dim(` - ${fn.name}`));
1509
+ }
1510
+ }
1511
+ if (actions.length > 0) {
1512
+ console.log(chalk6.dim(`
1513
+ Actions:`));
1514
+ for (const fn of actions) {
1515
+ console.log(chalk6.dim(` - ${fn.name}`));
1516
+ }
1517
+ }
1518
+ } catch (error) {
1519
+ spinner.fail("Failed to deploy functions");
1520
+ console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1521
+ }
1522
+ }
1523
+ function parseSchema(source) {
1524
+ const tables = [];
1525
+ const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
1526
+ if (!schemaMatch) return tables;
1527
+ const schemaContent = schemaMatch[1];
1528
+ const tableRegex = /(\w+)\s*:\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
1529
+ let match;
1530
+ while ((match = tableRegex.exec(schemaContent)) !== null) {
1531
+ const tableName = match[1];
1532
+ const columnsContent = match[2];
1533
+ const columns = {};
1534
+ const columnRegex = /(\w+)\s*:\s*(\w+)\s*\(\s*\)([^,\n]*)/g;
1535
+ let colMatch;
1536
+ while ((colMatch = columnRegex.exec(columnsContent)) !== null) {
1537
+ const colName = colMatch[1];
1538
+ const colType = colMatch[2];
1539
+ const modifiers = colMatch[3];
1540
+ columns[colName] = {
1541
+ type: colType,
1542
+ primaryKey: modifiers.includes(".primaryKey()"),
1543
+ notNull: modifiers.includes(".notNull()"),
1544
+ unique: modifiers.includes(".unique()"),
1545
+ hasDefault: modifiers.includes(".default("),
1546
+ references: modifiers.match(/\.references\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1]
1547
+ };
1548
+ }
1549
+ tables.push({ name: tableName, columns });
1550
+ }
1551
+ return tables;
1552
+ }
1553
+ function generateSchemaSQL(tables) {
1554
+ const statements = [];
1555
+ for (const table of tables) {
1556
+ const columnDefs = [];
1557
+ for (const [colName, colDef] of Object.entries(table.columns)) {
1558
+ const def = colDef;
1559
+ let sqlType = "TEXT";
1560
+ switch (def.type) {
1561
+ case "text":
1562
+ sqlType = "TEXT";
1563
+ break;
1564
+ case "integer":
1565
+ sqlType = "INTEGER";
1566
+ break;
1567
+ case "real":
1568
+ sqlType = "REAL";
1569
+ break;
1570
+ case "blob":
1571
+ sqlType = "BLOB";
1572
+ break;
1573
+ case "timestamp":
1574
+ sqlType = "TEXT";
1575
+ break;
1576
+ case "boolean":
1577
+ sqlType = "INTEGER";
1578
+ break;
1579
+ case "json":
1580
+ sqlType = "TEXT";
1581
+ break;
1582
+ }
1583
+ let colSql = `${colName} ${sqlType}`;
1584
+ if (def.primaryKey) colSql += " PRIMARY KEY";
1585
+ if (def.notNull) colSql += " NOT NULL";
1586
+ if (def.unique) colSql += " UNIQUE";
1587
+ if (def.hasDefault && def.type === "timestamp") {
1588
+ colSql += " DEFAULT (datetime('now'))";
1589
+ }
1590
+ if (def.references) {
1591
+ const [refTable, refCol] = def.references.split(".");
1592
+ colSql += ` REFERENCES ${refTable}(${refCol})`;
1593
+ }
1594
+ columnDefs.push(colSql);
1595
+ }
1596
+ statements.push(
1597
+ `CREATE TABLE IF NOT EXISTS ${table.name} (
1598
+ ${columnDefs.join(",\n ")}
1599
+ );`
1600
+ );
1601
+ }
1602
+ return statements.join("\n\n");
1603
+ }
1604
+ function parseFunctions(moduleName, source) {
1605
+ const functions = [];
1606
+ const fnRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action)\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
1607
+ let match;
1608
+ while ((match = fnRegex.exec(source)) !== null) {
1609
+ const fnName = match[1];
1610
+ const fnType = match[2];
1611
+ const fnBody = match[3];
1612
+ functions.push({
1613
+ name: `${moduleName}.${fnName}`,
1614
+ type: fnType,
1615
+ file: `${moduleName}.ts`,
1616
+ source: `${fnType}({${fnBody}})`
1617
+ });
1618
+ }
1619
+ return functions;
1620
+ }
1621
+
1622
+ // src/commands/login.ts
1623
+ import chalk7 from "chalk";
1624
+ import ora6 from "ora";
1625
+ var isDev3 = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
1626
+ var API_URL3 = isDev3 ? "http://localhost:3001/api/v1" : "https://tthr.io/api/v1";
1627
+ var AUTH_URL = isDev3 ? "http://localhost:3000/cli" : "https://tthr.io/cli";
1365
1628
  async function loginCommand() {
1366
- console.log(chalk6.bold("\u26A1 Login to Tether\n"));
1629
+ console.log(chalk7.bold("\u26A1 Login to Tether\n"));
1367
1630
  const existing = await getCredentials();
1368
1631
  if (existing) {
1369
- console.log(chalk6.green("\u2713") + ` Already logged in as ${chalk6.cyan(existing.email)}`);
1370
- console.log(chalk6.dim("\nRun `tthr logout` to sign out\n"));
1632
+ console.log(chalk7.green("\u2713") + ` Already logged in as ${chalk7.cyan(existing.email)}`);
1633
+ console.log(chalk7.dim("\nRun `tthr logout` to sign out\n"));
1371
1634
  return;
1372
1635
  }
1373
- const spinner = ora5("Generating authentication code...").start();
1636
+ const spinner = ora6("Generating authentication code...").start();
1374
1637
  try {
1375
1638
  const deviceCode = await requestDeviceCode();
1376
1639
  spinner.stop();
1377
1640
  const authUrl = `${AUTH_URL}/${deviceCode.userCode}`;
1378
- console.log(chalk6.dim("Open this URL in your browser to authenticate:\n"));
1379
- console.log(chalk6.cyan.bold(` ${authUrl}
1641
+ console.log(chalk7.dim("Open this URL in your browser to authenticate:\n"));
1642
+ console.log(chalk7.cyan.bold(` ${authUrl}
1380
1643
  `));
1381
- console.log(chalk6.dim(`Your code: ${chalk6.white.bold(deviceCode.userCode)}
1644
+ console.log(chalk7.dim(`Your code: ${chalk7.white.bold(deviceCode.userCode)}
1382
1645
  `));
1383
1646
  const credentials = await pollForApproval(deviceCode.deviceCode, deviceCode.interval, deviceCode.expiresIn);
1384
1647
  await saveCredentials(credentials);
1385
- console.log(chalk6.green("\u2713") + ` Logged in as ${chalk6.cyan(credentials.email)}`);
1648
+ console.log(chalk7.green("\u2713") + ` Logged in as ${chalk7.cyan(credentials.email)}`);
1386
1649
  console.log();
1387
1650
  } catch (error) {
1388
1651
  spinner.fail("Authentication failed");
1389
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1390
- console.log(chalk6.dim("\nTry again or visit https://tthr.io/docs/cli for help\n"));
1652
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1653
+ console.log(chalk7.dim("\nTry again or visit https://tthr.io/docs/cli for help\n"));
1391
1654
  process.exit(1);
1392
1655
  }
1393
1656
  }
1394
1657
  async function logoutCommand() {
1395
- console.log(chalk6.bold("\n\u26A1 Logout from Tether\n"));
1658
+ console.log(chalk7.bold("\n\u26A1 Logout from Tether\n"));
1396
1659
  const credentials = await getCredentials();
1397
1660
  if (!credentials) {
1398
- console.log(chalk6.dim("Not logged in\n"));
1661
+ console.log(chalk7.dim("Not logged in\n"));
1399
1662
  return;
1400
1663
  }
1401
- const spinner = ora5("Logging out...").start();
1664
+ const spinner = ora6("Logging out...").start();
1402
1665
  try {
1403
1666
  await clearCredentials();
1404
- spinner.succeed(`Logged out from ${chalk6.cyan(credentials.email)}`);
1667
+ spinner.succeed(`Logged out from ${chalk7.cyan(credentials.email)}`);
1405
1668
  console.log();
1406
1669
  } catch (error) {
1407
1670
  spinner.fail("Logout failed");
1408
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1671
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1409
1672
  process.exit(1);
1410
1673
  }
1411
1674
  }
1412
1675
  async function whoamiCommand() {
1413
1676
  const credentials = await getCredentials();
1414
1677
  if (!credentials) {
1415
- console.log(chalk6.dim("\nNot logged in"));
1416
- console.log(chalk6.dim("Run `tthr login` to authenticate\n"));
1678
+ console.log(chalk7.dim("\nNot logged in"));
1679
+ console.log(chalk7.dim("Run `tthr login` to authenticate\n"));
1417
1680
  return;
1418
1681
  }
1419
- console.log(chalk6.bold("\n\u26A1 Current user\n"));
1420
- console.log(` Email: ${chalk6.cyan(credentials.email)}`);
1421
- console.log(` User ID: ${chalk6.dim(credentials.userId)}`);
1682
+ console.log(chalk7.bold("\n\u26A1 Current user\n"));
1683
+ console.log(` Email: ${chalk7.cyan(credentials.email)}`);
1684
+ console.log(` User ID: ${chalk7.dim(credentials.userId)}`);
1422
1685
  if (credentials.expiresAt) {
1423
1686
  const expiresAt = new Date(credentials.expiresAt);
1424
1687
  const now = /* @__PURE__ */ new Date();
1425
1688
  if (expiresAt > now) {
1426
- console.log(` Session: ${chalk6.green("Active")}`);
1689
+ console.log(` Session: ${chalk7.green("Active")}`);
1427
1690
  } else {
1428
- console.log(` Session: ${chalk6.yellow("Expired")}`);
1691
+ console.log(` Session: ${chalk7.yellow("Expired")}`);
1429
1692
  }
1430
1693
  }
1431
1694
  console.log();
@@ -1446,7 +1709,7 @@ function generateUserCode() {
1446
1709
  async function requestDeviceCode() {
1447
1710
  const userCode = generateUserCode();
1448
1711
  const deviceCode = crypto.randomUUID();
1449
- const response = await fetch(`${API_URL2}/auth/device`, {
1712
+ const response = await fetch(`${API_URL3}/auth/device`, {
1450
1713
  method: "POST",
1451
1714
  headers: { "Content-Type": "application/json" },
1452
1715
  body: JSON.stringify({ userCode, deviceCode })
@@ -1466,21 +1729,21 @@ async function requestDeviceCode() {
1466
1729
  async function pollForApproval(deviceCode, interval, expiresIn) {
1467
1730
  const startTime = Date.now();
1468
1731
  const expiresAt = startTime + expiresIn * 1e3;
1469
- const spinner = ora5("").start();
1732
+ const spinner = ora6("").start();
1470
1733
  const updateCountdown = () => {
1471
1734
  const remaining = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1e3));
1472
1735
  const mins = Math.floor(remaining / 60);
1473
1736
  const secs = remaining % 60;
1474
1737
  const timeStr = mins > 0 ? `${mins}:${secs.toString().padStart(2, "0")}` : `${secs}s`;
1475
- spinner.text = `Waiting for approval... ${chalk6.dim(`(${timeStr} remaining)`)}`;
1738
+ spinner.text = `Waiting for approval... ${chalk7.dim(`(${timeStr} remaining)`)}`;
1476
1739
  };
1477
1740
  updateCountdown();
1478
1741
  const countdownInterval = setInterval(updateCountdown, 1e3);
1479
1742
  try {
1480
1743
  while (Date.now() < expiresAt) {
1481
1744
  await sleep(interval * 1e3);
1482
- spinner.text = `Checking... ${chalk6.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
1483
- const response = await fetch(`${API_URL2}/auth/device/${deviceCode}`, {
1745
+ spinner.text = `Checking... ${chalk7.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
1746
+ const response = await fetch(`${API_URL3}/auth/device/${deviceCode}`, {
1484
1747
  method: "GET"
1485
1748
  }).catch(() => null);
1486
1749
  updateCountdown();
@@ -1518,6 +1781,7 @@ program.command("init [name]").description("Create a new Tether project").option
1518
1781
  program.command("dev").description("Start local development server with hot reload").option("-p, --port <port>", "Port to run on", "3001").action(devCommand);
1519
1782
  program.command("generate").alias("gen").description("Generate types from schema").action(generateCommand);
1520
1783
  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);
1784
+ 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);
1521
1785
  program.command("login").description("Authenticate with Tether").action(loginCommand);
1522
1786
  program.command("logout").description("Sign out of Tether").action(logoutCommand);
1523
1787
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tthr/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Tether CLI - project scaffolding, migrations, and deployment",
5
5
  "type": "module",
6
6
  "bin": {