@tthr/cli 0.0.2 → 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.
- package/dist/index.js +293 -29
- 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/
|
|
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
|
-
|
|
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(
|
|
1629
|
+
console.log(chalk7.bold("\u26A1 Login to Tether\n"));
|
|
1367
1630
|
const existing = await getCredentials();
|
|
1368
1631
|
if (existing) {
|
|
1369
|
-
console.log(
|
|
1370
|
-
console.log(
|
|
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 =
|
|
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(
|
|
1379
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
1390
|
-
console.log(
|
|
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(
|
|
1658
|
+
console.log(chalk7.bold("\n\u26A1 Logout from Tether\n"));
|
|
1396
1659
|
const credentials = await getCredentials();
|
|
1397
1660
|
if (!credentials) {
|
|
1398
|
-
console.log(
|
|
1661
|
+
console.log(chalk7.dim("Not logged in\n"));
|
|
1399
1662
|
return;
|
|
1400
1663
|
}
|
|
1401
|
-
const spinner =
|
|
1664
|
+
const spinner = ora6("Logging out...").start();
|
|
1402
1665
|
try {
|
|
1403
1666
|
await clearCredentials();
|
|
1404
|
-
spinner.succeed(`Logged out from ${
|
|
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(
|
|
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(
|
|
1416
|
-
console.log(
|
|
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(
|
|
1420
|
-
console.log(` Email: ${
|
|
1421
|
-
console.log(` User ID: ${
|
|
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: ${
|
|
1689
|
+
console.log(` Session: ${chalk7.green("Active")}`);
|
|
1427
1690
|
} else {
|
|
1428
|
-
console.log(` Session: ${
|
|
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(`${
|
|
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 =
|
|
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... ${
|
|
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... ${
|
|
1483
|
-
const response = await fetch(`${
|
|
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);
|