@rebasepro/cli 0.6.1 → 0.7.0
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/README.md +0 -1
- package/dist/commands/api-keys.d.ts +1 -0
- package/dist/commands/generate_sdk.d.ts +1 -1
- package/dist/index.es.js +399 -57
- package/dist/index.es.js.map +1 -1
- package/package.json +20 -15
- package/templates/template/backend/package.json +0 -1
- package/templates/template/backend/src/index.ts +1 -1
- package/templates/template/config/collections/posts.ts +1 -3
- package/templates/template/config/collections/tags.ts +1 -3
- package/templates/template/config/collections/users.ts +1 -4
- package/templates/template/package.json +0 -1
- package/templates/template/scripts/example.ts +4 -1
- package/skills/rebase-admin/SKILL.md +0 -710
- package/skills/rebase-api/SKILL.md +0 -662
- package/skills/rebase-api/references/.gitkeep +0 -3
- package/skills/rebase-auth/SKILL.md +0 -1143
- package/skills/rebase-auth/references/.gitkeep +0 -3
- package/skills/rebase-backend-postgres/SKILL.md +0 -633
- package/skills/rebase-backend-postgres/references/.gitkeep +0 -3
- package/skills/rebase-basics/SKILL.md +0 -749
- package/skills/rebase-basics/references/.gitkeep +0 -3
- package/skills/rebase-collections/SKILL.md +0 -1328
- package/skills/rebase-collections/references/.gitkeep +0 -3
- package/skills/rebase-cron-jobs/SKILL.md +0 -699
- package/skills/rebase-cron-jobs/references/.gitkeep +0 -1
- package/skills/rebase-custom-functions/SKILL.md +0 -233
- package/skills/rebase-deployment/SKILL.md +0 -885
- package/skills/rebase-deployment/references/.gitkeep +0 -3
- package/skills/rebase-design-language/SKILL.md +0 -692
- package/skills/rebase-email/SKILL.md +0 -701
- package/skills/rebase-email/references/.gitkeep +0 -1
- package/skills/rebase-entity-history/SKILL.md +0 -485
- package/skills/rebase-entity-history/references/.gitkeep +0 -1
- package/skills/rebase-local-env-setup/SKILL.md +0 -189
- package/skills/rebase-local-env-setup/references/.gitkeep +0 -3
- package/skills/rebase-realtime/SKILL.md +0 -755
- package/skills/rebase-realtime/references/.gitkeep +0 -3
- package/skills/rebase-sdk/SKILL.md +0 -594
- package/skills/rebase-sdk/references/.gitkeep +0 -0
- package/skills/rebase-storage/SKILL.md +0 -765
- package/skills/rebase-storage/references/.gitkeep +0 -3
- package/skills/rebase-studio/SKILL.md +0 -746
- package/skills/rebase-studio/references/.gitkeep +0 -3
- package/skills/rebase-ui-components/SKILL.md +0 -1488
- package/skills/rebase-ui-components/references/.gitkeep +0 -3
- package/skills/rebase-webhooks/SKILL.md +0 -623
- package/skills/rebase-webhooks/references/.gitkeep +0 -1
- package/templates/template/backend/drizzle.config.ts +0 -51
package/README.md
CHANGED
|
@@ -23,7 +23,6 @@ The CLI is also bundled with every Rebase project as a local dependency.
|
|
|
23
23
|
| `rebase db push` | Apply schema directly to database (dev) |
|
|
24
24
|
| `rebase db generate` | Generate SQL migration files |
|
|
25
25
|
| `rebase db migrate` | Run pending migrations |
|
|
26
|
-
| `rebase db studio` | Open Drizzle Studio |
|
|
27
26
|
| `rebase generate-sdk` | Generate a typed TypeScript SDK from collections |
|
|
28
27
|
| `rebase auth reset-password` | Reset a user's password |
|
|
29
28
|
| `rebase doctor` | Detect schema drift between collections, Drizzle schema, and database |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function apiKeysCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* CLI command: generate-sdk
|
|
3
3
|
*
|
|
4
4
|
* Reads collection definitions from a specified directory (default: ./config/collections),
|
|
5
|
-
* generates a typed
|
|
5
|
+
* generates a typed TypeScript SDK, and writes it to the output directory (default: ./generated/sdk).
|
|
6
6
|
*
|
|
7
7
|
* Uses jiti for dynamic TypeScript import of collection files.
|
|
8
8
|
*/
|
package/dist/index.es.js
CHANGED
|
@@ -11,6 +11,7 @@ import { fileURLToPath } from "url";
|
|
|
11
11
|
import crypto from "crypto";
|
|
12
12
|
import { generateSDK } from "@rebasepro/sdk-generator";
|
|
13
13
|
import { execSync, spawn } from "child_process";
|
|
14
|
+
import { createRequire } from "module";
|
|
14
15
|
//#region src/utils/package-manager.ts
|
|
15
16
|
/**
|
|
16
17
|
* Package manager detection and command abstraction.
|
|
@@ -130,9 +131,9 @@ function getPMCommands(pm) {
|
|
|
130
131
|
//#endregion
|
|
131
132
|
//#region src/commands/init.ts
|
|
132
133
|
var access = promisify(fs.access);
|
|
133
|
-
var __filename$
|
|
134
|
-
var __dirname$
|
|
135
|
-
function findParentDir
|
|
134
|
+
var __filename$1 = fileURLToPath(import.meta.url);
|
|
135
|
+
var __dirname$1 = path.dirname(__filename$1);
|
|
136
|
+
function findParentDir(currentDir, targetName) {
|
|
136
137
|
const root = path.parse(currentDir).root;
|
|
137
138
|
while (currentDir && currentDir !== root) {
|
|
138
139
|
if (path.basename(currentDir) === targetName) return currentDir;
|
|
@@ -140,7 +141,7 @@ function findParentDir$1(currentDir, targetName) {
|
|
|
140
141
|
}
|
|
141
142
|
return null;
|
|
142
143
|
}
|
|
143
|
-
var cliRoot = findParentDir
|
|
144
|
+
var cliRoot = findParentDir(__dirname$1, "cli");
|
|
144
145
|
var PRESET_CHOICES = [
|
|
145
146
|
{
|
|
146
147
|
name: "Blog — Posts, Authors, Tags (with markdown editor)",
|
|
@@ -574,7 +575,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
574
575
|
* CLI command: generate-sdk
|
|
575
576
|
*
|
|
576
577
|
* Reads collection definitions from a specified directory (default: ./config/collections),
|
|
577
|
-
* generates a typed
|
|
578
|
+
* generates a typed TypeScript SDK, and writes it to the output directory (default: ./generated/sdk).
|
|
578
579
|
*
|
|
579
580
|
* Uses jiti for dynamic TypeScript import of collection files.
|
|
580
581
|
*/
|
|
@@ -720,7 +721,7 @@ function findProjectRoot(startDir = process.cwd()) {
|
|
|
720
721
|
try {
|
|
721
722
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
722
723
|
if (pkg.workspaces && Array.isArray(pkg.workspaces)) {
|
|
723
|
-
if (pkg.workspaces.some((w) => w === "backend"
|
|
724
|
+
if (pkg.workspaces.some((w) => w === "backend")) return dir;
|
|
724
725
|
}
|
|
725
726
|
} catch {}
|
|
726
727
|
if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) return dir;
|
|
@@ -962,7 +963,6 @@ ${chalk.green.bold("Commands")}
|
|
|
962
963
|
${chalk.blue.bold("push")} Apply schema directly to database (development)
|
|
963
964
|
${chalk.blue.bold("generate")} Generate migration files
|
|
964
965
|
${chalk.blue.bold("migrate")} Run pending migrations
|
|
965
|
-
${chalk.blue.bold("studio")} Open Studio viewer
|
|
966
966
|
${chalk.blue.bold("branch")} Database branching (create, list, delete, info)
|
|
967
967
|
|
|
968
968
|
${chalk.green.bold("Examples")}
|
|
@@ -1431,19 +1431,90 @@ async function resetPassword(rawArgs) {
|
|
|
1431
1431
|
process.exit(1);
|
|
1432
1432
|
}
|
|
1433
1433
|
const projectRoot = requireProjectRoot();
|
|
1434
|
+
let envServiceKey;
|
|
1435
|
+
const envFile = findEnvFile(projectRoot);
|
|
1436
|
+
if (envFile && fs.existsSync(envFile)) try {
|
|
1437
|
+
const match = fs.readFileSync(envFile, "utf8").match(/^\s*REBASE_SERVICE_KEY\s*=\s*['"]?(.*?)['"]?\s*$/m);
|
|
1438
|
+
if (match && match[1]) envServiceKey = match[1];
|
|
1439
|
+
} catch {}
|
|
1440
|
+
let baseUrl = process.env.REBASE_BASE_URL;
|
|
1441
|
+
let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;
|
|
1442
|
+
const statePath = path.join(projectRoot, ".rebase", "state.json");
|
|
1443
|
+
if (fs.existsSync(statePath)) try {
|
|
1444
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
1445
|
+
if (state && typeof state === "object") {
|
|
1446
|
+
if (typeof state.baseUrl === "string" && !baseUrl) baseUrl = state.baseUrl;
|
|
1447
|
+
if (typeof state.serviceKey === "string" && !serviceKey) serviceKey = state.serviceKey;
|
|
1448
|
+
}
|
|
1449
|
+
} catch {}
|
|
1450
|
+
const devUrlPath = path.join(projectRoot, ".rebase-dev-url");
|
|
1451
|
+
if (fs.existsSync(devUrlPath) && !baseUrl) try {
|
|
1452
|
+
baseUrl = fs.readFileSync(devUrlPath, "utf8").trim();
|
|
1453
|
+
} catch {}
|
|
1454
|
+
if (baseUrl && serviceKey) {
|
|
1455
|
+
console.log("Trying API-first reset via running backend...");
|
|
1456
|
+
try {
|
|
1457
|
+
const finalPass = newPassword || "NewPassword123!";
|
|
1458
|
+
const cleanBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
1459
|
+
const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=1`;
|
|
1460
|
+
const searchRes = await fetch(searchUrl, { headers: {
|
|
1461
|
+
"Authorization": `Bearer ${serviceKey}`,
|
|
1462
|
+
"Accept": "application/json"
|
|
1463
|
+
} });
|
|
1464
|
+
if (!searchRes.ok) throw new Error(`Failed to list users: ${searchRes.statusText}`);
|
|
1465
|
+
const searchData = await searchRes.json();
|
|
1466
|
+
if (!searchData || typeof searchData !== "object") throw new Error("Invalid response format from user search API.");
|
|
1467
|
+
let userId;
|
|
1468
|
+
if (Array.isArray(searchData)) {
|
|
1469
|
+
const firstUser = searchData[0];
|
|
1470
|
+
if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
|
|
1471
|
+
else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
|
|
1472
|
+
} else if ("users" in searchData && Array.isArray(searchData.users)) {
|
|
1473
|
+
const firstUser = searchData.users[0];
|
|
1474
|
+
if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
|
|
1475
|
+
else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
|
|
1476
|
+
}
|
|
1477
|
+
if (!userId) throw new Error(`User not found with email: ${email}`);
|
|
1478
|
+
const resetUrl = `${cleanBaseUrl}/api/admin/users/${userId}/reset-password`;
|
|
1479
|
+
const resetRes = await fetch(resetUrl, {
|
|
1480
|
+
method: "POST",
|
|
1481
|
+
headers: {
|
|
1482
|
+
"Authorization": `Bearer ${serviceKey}`,
|
|
1483
|
+
"Content-Type": "application/json",
|
|
1484
|
+
"Accept": "application/json"
|
|
1485
|
+
},
|
|
1486
|
+
body: JSON.stringify({ password: finalPass })
|
|
1487
|
+
});
|
|
1488
|
+
if (!resetRes.ok) {
|
|
1489
|
+
const errText = await resetRes.text();
|
|
1490
|
+
throw new Error(`Password reset endpoint failed: ${errText || resetRes.statusText}`);
|
|
1491
|
+
}
|
|
1492
|
+
console.log("API reset successful.");
|
|
1493
|
+
console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (via API)"));
|
|
1494
|
+
console.log("");
|
|
1495
|
+
console.log(` ${chalk.gray("Email:")} ${email}`);
|
|
1496
|
+
console.log(` ${chalk.gray("Password:")} ${finalPass}`);
|
|
1497
|
+
console.log("");
|
|
1498
|
+
return;
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1501
|
+
console.warn(chalk.yellow("API reset failed, falling back to direct database update..."));
|
|
1502
|
+
console.warn(chalk.gray(` Details: ${errMsg}`));
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1434
1505
|
const backendDir = requireBackendDir(projectRoot);
|
|
1435
1506
|
const tsxBin = resolveTsx(projectRoot);
|
|
1436
1507
|
if (!tsxBin) {
|
|
1437
1508
|
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
1438
1509
|
process.exit(1);
|
|
1439
1510
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1511
|
+
try {
|
|
1512
|
+
const env = { ...process.env };
|
|
1513
|
+
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
1514
|
+
env.REBASE_RESET_EMAIL = email;
|
|
1515
|
+
env.REBASE_RESET_PASSWORD = newPassword || "NewPassword123!";
|
|
1516
|
+
env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
|
|
1517
|
+
const scriptContent = `
|
|
1447
1518
|
import { createPostgresDatabaseConnection } from "@rebasepro/server-postgresql";
|
|
1448
1519
|
import { hashPassword } from "@rebasepro/server-core";
|
|
1449
1520
|
import { eq } from "drizzle-orm";
|
|
@@ -1497,28 +1568,33 @@ async function resetPassword() {
|
|
|
1497
1568
|
|
|
1498
1569
|
resetPassword().catch(console.error);
|
|
1499
1570
|
`;
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
});
|
|
1513
|
-
return new Promise((resolve) => {
|
|
1514
|
-
child.on("close", (code) => {
|
|
1515
|
-
try {
|
|
1516
|
-
fs.unlinkSync(tmpScriptPath);
|
|
1517
|
-
} catch {}
|
|
1518
|
-
if (code !== 0) process.exit(code ?? 1);
|
|
1519
|
-
resolve();
|
|
1571
|
+
const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
|
|
1572
|
+
fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
|
|
1573
|
+
console.log("");
|
|
1574
|
+
console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)"));
|
|
1575
|
+
console.log("");
|
|
1576
|
+
console.log(` ${chalk.gray("Email:")} ${email}`);
|
|
1577
|
+
if (newPassword) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
|
|
1578
|
+
console.log("");
|
|
1579
|
+
const child = spawn(tsxBin, [tmpScriptPath], {
|
|
1580
|
+
cwd: backendDir,
|
|
1581
|
+
stdio: "inherit",
|
|
1582
|
+
env
|
|
1520
1583
|
});
|
|
1521
|
-
|
|
1584
|
+
return new Promise((resolve) => {
|
|
1585
|
+
child.on("close", (code) => {
|
|
1586
|
+
try {
|
|
1587
|
+
fs.unlinkSync(tmpScriptPath);
|
|
1588
|
+
} catch {}
|
|
1589
|
+
if (code !== 0) process.exit(code ?? 1);
|
|
1590
|
+
resolve();
|
|
1591
|
+
});
|
|
1592
|
+
});
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
console.error(chalk.red("✗ Direct database update failed."));
|
|
1595
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1596
|
+
process.exit(1);
|
|
1597
|
+
}
|
|
1522
1598
|
}
|
|
1523
1599
|
function printAuthHelp() {
|
|
1524
1600
|
console.log(`
|
|
@@ -1587,8 +1663,7 @@ async function doctorCommand(rawArgs) {
|
|
|
1587
1663
|
}
|
|
1588
1664
|
//#endregion
|
|
1589
1665
|
//#region src/commands/skills.ts
|
|
1590
|
-
var
|
|
1591
|
-
var __dirname$1 = path.dirname(__filename$1);
|
|
1666
|
+
var require = createRequire(import.meta.url);
|
|
1592
1667
|
/** Supported agent environments and their target directories. */
|
|
1593
1668
|
var AGENTS = {
|
|
1594
1669
|
cursor: {
|
|
@@ -1632,24 +1707,16 @@ var AGENTS = {
|
|
|
1632
1707
|
})
|
|
1633
1708
|
}
|
|
1634
1709
|
};
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
currentDir = path.dirname(currentDir);
|
|
1640
|
-
}
|
|
1641
|
-
return null;
|
|
1642
|
-
}
|
|
1643
|
-
/** Resolve the path to the bundled skills directory. */
|
|
1710
|
+
/**
|
|
1711
|
+
* Resolve the path to the skills directory from @rebasepro/agent-skills.
|
|
1712
|
+
* Works in both workspace (symlink) and published (real files) layouts.
|
|
1713
|
+
*/
|
|
1644
1714
|
function getSkillsSourceDir() {
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
const distSkills = path.resolve(__dirname$1, "../../skills");
|
|
1651
|
-
if (fs.existsSync(distSkills)) return distSkills;
|
|
1652
|
-
throw new Error("Could not find bundled skills directory. Make sure the CLI was built with `pnpm build`.");
|
|
1715
|
+
const pkgJsonPath = require.resolve("@rebasepro/agent-skills/package.json");
|
|
1716
|
+
const pkgRoot = path.dirname(pkgJsonPath);
|
|
1717
|
+
const skillsDir = path.join(pkgRoot, "skills");
|
|
1718
|
+
if (!fs.existsSync(skillsDir)) throw new Error(`Skills directory not found at ${skillsDir}. Make sure @rebasepro/agent-skills is installed.`);
|
|
1719
|
+
return skillsDir;
|
|
1653
1720
|
}
|
|
1654
1721
|
/** Read all skill directories and return their names + content. */
|
|
1655
1722
|
function loadSkills(skillsDir) {
|
|
@@ -1764,6 +1831,272 @@ ${chalk.green.bold("Examples")}
|
|
|
1764
1831
|
`);
|
|
1765
1832
|
}
|
|
1766
1833
|
//#endregion
|
|
1834
|
+
//#region src/commands/api-keys.ts
|
|
1835
|
+
/**
|
|
1836
|
+
* CLI command: rebase api-keys <action>
|
|
1837
|
+
*
|
|
1838
|
+
* Subcommands:
|
|
1839
|
+
* list — List all API keys (masked)
|
|
1840
|
+
* create — Create a new API key
|
|
1841
|
+
* revoke — Revoke an existing API key
|
|
1842
|
+
*/
|
|
1843
|
+
function loadEnv(projectRoot) {
|
|
1844
|
+
const envFile = findEnvFile(projectRoot);
|
|
1845
|
+
const env = {};
|
|
1846
|
+
if (envFile && fs.existsSync(envFile)) {
|
|
1847
|
+
const content = fs.readFileSync(envFile, "utf-8");
|
|
1848
|
+
for (const line of content.split("\n")) {
|
|
1849
|
+
const trimmed = line.trim();
|
|
1850
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1851
|
+
const idx = trimmed.indexOf("=");
|
|
1852
|
+
if (idx > 0) {
|
|
1853
|
+
const key = trimmed.slice(0, idx).trim();
|
|
1854
|
+
let value = trimmed.slice(idx + 1).trim();
|
|
1855
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1856
|
+
env[key] = value;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return env;
|
|
1861
|
+
}
|
|
1862
|
+
function resolveBaseUrl(env) {
|
|
1863
|
+
const port = env.PORT || env.REBASE_PORT || "3001";
|
|
1864
|
+
return env.REBASE_BASE_URL || `http://localhost:${port}`;
|
|
1865
|
+
}
|
|
1866
|
+
async function apiKeysCommand(subcommand, rawArgs) {
|
|
1867
|
+
if (!subcommand || subcommand === "--help") {
|
|
1868
|
+
printApiKeysHelp();
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
switch (subcommand) {
|
|
1872
|
+
case "list":
|
|
1873
|
+
await listKeys(rawArgs);
|
|
1874
|
+
break;
|
|
1875
|
+
case "create":
|
|
1876
|
+
await createKey(rawArgs);
|
|
1877
|
+
break;
|
|
1878
|
+
case "revoke":
|
|
1879
|
+
await revokeKey(rawArgs);
|
|
1880
|
+
break;
|
|
1881
|
+
default:
|
|
1882
|
+
console.error(chalk.red(`Unknown api-keys command: ${subcommand}`));
|
|
1883
|
+
console.log("");
|
|
1884
|
+
printApiKeysHelp();
|
|
1885
|
+
process.exit(1);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
async function listKeys(_rawArgs) {
|
|
1889
|
+
const env = loadEnv(requireProjectRoot());
|
|
1890
|
+
const baseUrl = resolveBaseUrl(env);
|
|
1891
|
+
const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
|
|
1892
|
+
if (!serviceKey) {
|
|
1893
|
+
console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
|
|
1894
|
+
process.exit(1);
|
|
1895
|
+
}
|
|
1896
|
+
try {
|
|
1897
|
+
const res = await fetch(`${baseUrl}/api/admin/api-keys`, { headers: { Authorization: `Bearer ${serviceKey}` } });
|
|
1898
|
+
if (!res.ok) {
|
|
1899
|
+
const body = await res.text();
|
|
1900
|
+
console.error(chalk.red(`✗ Failed to list API keys: ${res.status} ${body}`));
|
|
1901
|
+
process.exit(1);
|
|
1902
|
+
}
|
|
1903
|
+
const { keys } = await res.json();
|
|
1904
|
+
console.log("");
|
|
1905
|
+
console.log(chalk.bold(" 🔑 API Keys"));
|
|
1906
|
+
console.log("");
|
|
1907
|
+
if (keys.length === 0) {
|
|
1908
|
+
console.log(chalk.gray(" No API keys found."));
|
|
1909
|
+
console.log("");
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
for (const key of keys) {
|
|
1913
|
+
const status = key.revoked_at ? chalk.red("revoked") : key.expires_at && new Date(key.expires_at) < /* @__PURE__ */ new Date() ? chalk.yellow("expired") : chalk.green("active");
|
|
1914
|
+
const perms = key.permissions.map((p) => `${p.collection}(${p.operations.join(",")})`).join(", ");
|
|
1915
|
+
console.log(` ${chalk.bold(key.name)} ${chalk.gray(`[${key.key_prefix}•••]`)} ${status}`);
|
|
1916
|
+
console.log(` ${chalk.gray("ID:")} ${key.id}`);
|
|
1917
|
+
console.log(` ${chalk.gray("Permissions:")} ${perms || "none"}`);
|
|
1918
|
+
console.log(` ${chalk.gray("Created:")} ${new Date(key.created_at).toLocaleDateString()}`);
|
|
1919
|
+
if (key.last_used_at) console.log(` ${chalk.gray("Last used:")} ${new Date(key.last_used_at).toLocaleDateString()}`);
|
|
1920
|
+
console.log("");
|
|
1921
|
+
}
|
|
1922
|
+
} catch (e) {
|
|
1923
|
+
console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
|
|
1924
|
+
console.error(chalk.gray(" Is the Rebase server running?"));
|
|
1925
|
+
process.exit(1);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
async function createKey(rawArgs) {
|
|
1929
|
+
const args = arg({
|
|
1930
|
+
"--name": String,
|
|
1931
|
+
"--permissions": String,
|
|
1932
|
+
"--admin": Boolean,
|
|
1933
|
+
"--rate-limit": Number,
|
|
1934
|
+
"--expires": String,
|
|
1935
|
+
"-n": "--name"
|
|
1936
|
+
}, {
|
|
1937
|
+
argv: rawArgs.slice(4),
|
|
1938
|
+
permissive: true
|
|
1939
|
+
});
|
|
1940
|
+
const name = args["--name"] || args._[0];
|
|
1941
|
+
const permissionsRaw = args["--permissions"];
|
|
1942
|
+
if (!name) {
|
|
1943
|
+
console.error(chalk.red("✗ Name is required."));
|
|
1944
|
+
console.log("");
|
|
1945
|
+
console.log(chalk.gray(" Usage: rebase api-keys create --name \"My Key\" --permissions '[{\"collection\":\"*\",\"operations\":[\"read\"]}]'"));
|
|
1946
|
+
process.exit(1);
|
|
1947
|
+
}
|
|
1948
|
+
let permissions;
|
|
1949
|
+
if (permissionsRaw) try {
|
|
1950
|
+
permissions = JSON.parse(permissionsRaw);
|
|
1951
|
+
} catch {
|
|
1952
|
+
console.error(chalk.red("✗ Invalid --permissions JSON."));
|
|
1953
|
+
console.log(chalk.gray(" Example: '[{\"collection\":\"*\",\"operations\":[\"read\",\"write\"]}]'"));
|
|
1954
|
+
process.exit(1);
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
else permissions = [{
|
|
1958
|
+
collection: "*",
|
|
1959
|
+
operations: [
|
|
1960
|
+
"read",
|
|
1961
|
+
"write",
|
|
1962
|
+
"delete"
|
|
1963
|
+
]
|
|
1964
|
+
}];
|
|
1965
|
+
let expires_at = null;
|
|
1966
|
+
const expiresFlag = args["--expires"];
|
|
1967
|
+
if (expiresFlag) {
|
|
1968
|
+
const days = {
|
|
1969
|
+
"7d": 7,
|
|
1970
|
+
"30d": 30,
|
|
1971
|
+
"90d": 90,
|
|
1972
|
+
"1y": 365
|
|
1973
|
+
};
|
|
1974
|
+
if (days[expiresFlag]) expires_at = new Date(Date.now() + days[expiresFlag] * 864e5).toISOString();
|
|
1975
|
+
else {
|
|
1976
|
+
const parsed = new Date(expiresFlag);
|
|
1977
|
+
if (isNaN(parsed.getTime())) {
|
|
1978
|
+
console.error(chalk.red("✗ Invalid --expires value. Use 7d, 30d, 90d, 1y, or an ISO date."));
|
|
1979
|
+
process.exit(1);
|
|
1980
|
+
}
|
|
1981
|
+
expires_at = parsed.toISOString();
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
const env = loadEnv(requireProjectRoot());
|
|
1985
|
+
const baseUrl = resolveBaseUrl(env);
|
|
1986
|
+
const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
|
|
1987
|
+
if (!serviceKey) {
|
|
1988
|
+
console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
|
|
1989
|
+
process.exit(1);
|
|
1990
|
+
}
|
|
1991
|
+
try {
|
|
1992
|
+
const body = {
|
|
1993
|
+
name,
|
|
1994
|
+
permissions,
|
|
1995
|
+
admin: args["--admin"] ?? false,
|
|
1996
|
+
rate_limit: args["--rate-limit"] ?? null,
|
|
1997
|
+
expires_at
|
|
1998
|
+
};
|
|
1999
|
+
const res = await fetch(`${baseUrl}/api/admin/api-keys`, {
|
|
2000
|
+
method: "POST",
|
|
2001
|
+
headers: {
|
|
2002
|
+
Authorization: `Bearer ${serviceKey}`,
|
|
2003
|
+
"Content-Type": "application/json"
|
|
2004
|
+
},
|
|
2005
|
+
body: JSON.stringify(body)
|
|
2006
|
+
});
|
|
2007
|
+
if (!res.ok) {
|
|
2008
|
+
const errBody = await res.text();
|
|
2009
|
+
console.error(chalk.red(`✗ Failed to create API key: ${res.status} ${errBody}`));
|
|
2010
|
+
process.exit(1);
|
|
2011
|
+
}
|
|
2012
|
+
const { key } = await res.json();
|
|
2013
|
+
console.log("");
|
|
2014
|
+
console.log(chalk.bold.green(" ✓ API key created successfully"));
|
|
2015
|
+
console.log("");
|
|
2016
|
+
console.log(` ${chalk.gray("Name:")} ${key.name}`);
|
|
2017
|
+
console.log(` ${chalk.gray("ID:")} ${key.id}`);
|
|
2018
|
+
console.log(` ${chalk.gray("Prefix:")} ${key.key_prefix}`);
|
|
2019
|
+
console.log("");
|
|
2020
|
+
console.log(chalk.bold.yellow(" ⚠ Copy your key now — it won't be shown again:"));
|
|
2021
|
+
console.log("");
|
|
2022
|
+
console.log(` ${chalk.cyan(key.key)}`);
|
|
2023
|
+
console.log("");
|
|
2024
|
+
} catch (e) {
|
|
2025
|
+
console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
|
|
2026
|
+
console.error(chalk.gray(" Is the Rebase server running?"));
|
|
2027
|
+
process.exit(1);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
async function revokeKey(rawArgs) {
|
|
2031
|
+
const args = arg({ "--id": String }, {
|
|
2032
|
+
argv: rawArgs.slice(4),
|
|
2033
|
+
permissive: true
|
|
2034
|
+
});
|
|
2035
|
+
const id = args["--id"] || args._[0];
|
|
2036
|
+
if (!id) {
|
|
2037
|
+
console.error(chalk.red("✗ Key ID is required."));
|
|
2038
|
+
console.log("");
|
|
2039
|
+
console.log(chalk.gray(" Usage: rebase api-keys revoke <key-id>"));
|
|
2040
|
+
process.exit(1);
|
|
2041
|
+
}
|
|
2042
|
+
const env = loadEnv(requireProjectRoot());
|
|
2043
|
+
const baseUrl = resolveBaseUrl(env);
|
|
2044
|
+
const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
|
|
2045
|
+
if (!serviceKey) {
|
|
2046
|
+
console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
|
|
2047
|
+
process.exit(1);
|
|
2048
|
+
}
|
|
2049
|
+
try {
|
|
2050
|
+
const res = await fetch(`${baseUrl}/api/admin/api-keys/${encodeURIComponent(id)}`, {
|
|
2051
|
+
method: "DELETE",
|
|
2052
|
+
headers: { Authorization: `Bearer ${serviceKey}` }
|
|
2053
|
+
});
|
|
2054
|
+
if (!res.ok) {
|
|
2055
|
+
const errBody = await res.text();
|
|
2056
|
+
console.error(chalk.red(`✗ Failed to revoke API key: ${res.status} ${errBody}`));
|
|
2057
|
+
process.exit(1);
|
|
2058
|
+
}
|
|
2059
|
+
console.log("");
|
|
2060
|
+
console.log(chalk.bold.green(" ✓ API key revoked successfully"));
|
|
2061
|
+
console.log(` ${chalk.gray("ID:")} ${id}`);
|
|
2062
|
+
console.log("");
|
|
2063
|
+
} catch (e) {
|
|
2064
|
+
console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
|
|
2065
|
+
console.error(chalk.gray(" Is the Rebase server running?"));
|
|
2066
|
+
process.exit(1);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
function printApiKeysHelp() {
|
|
2070
|
+
console.log(`
|
|
2071
|
+
${chalk.bold("rebase api-keys")} — Manage Service API Keys
|
|
2072
|
+
|
|
2073
|
+
${chalk.green.bold("Usage")}
|
|
2074
|
+
rebase api-keys ${chalk.blue("<command>")} [options]
|
|
2075
|
+
|
|
2076
|
+
${chalk.green.bold("Commands")}
|
|
2077
|
+
${chalk.blue.bold("list")} List all API keys
|
|
2078
|
+
${chalk.blue.bold("create")} Create a new API key
|
|
2079
|
+
${chalk.blue.bold("revoke")} Revoke an API key
|
|
2080
|
+
|
|
2081
|
+
${chalk.green.bold("create Options")}
|
|
2082
|
+
${chalk.blue("--name, -n")} Key name ${chalk.gray("(required)")}
|
|
2083
|
+
${chalk.blue("--permissions")} JSON array of permissions
|
|
2084
|
+
${chalk.gray("Default: [{\"collection\":\"*\",\"operations\":[\"read\",\"write\",\"delete\"]}]")}
|
|
2085
|
+
${chalk.blue("--admin")} Grant admin role (access to admin routes)
|
|
2086
|
+
${chalk.blue("--rate-limit")} Requests per 15-min window ${chalk.gray("(default: 1000)")}
|
|
2087
|
+
${chalk.blue("--expires")} Expiration: 7d, 30d, 90d, 1y, or ISO date
|
|
2088
|
+
|
|
2089
|
+
${chalk.green.bold("revoke Options")}
|
|
2090
|
+
${chalk.blue("--id")} API key ID to revoke ${chalk.gray("(or positional arg)")}
|
|
2091
|
+
|
|
2092
|
+
${chalk.green.bold("Examples")}
|
|
2093
|
+
rebase api-keys list
|
|
2094
|
+
rebase api-keys create --name "Analytics" --permissions '[{"collection":"events","operations":["read"]}]'
|
|
2095
|
+
rebase api-keys create -n "Full Access" --expires 90d
|
|
2096
|
+
rebase api-keys revoke abc123-def456
|
|
2097
|
+
`);
|
|
2098
|
+
}
|
|
2099
|
+
//#endregion
|
|
1767
2100
|
//#region src/cli.ts
|
|
1768
2101
|
var __filename = fileURLToPath(import.meta.url);
|
|
1769
2102
|
var __dirname = path.dirname(__filename);
|
|
@@ -1798,7 +2131,8 @@ async function entry(args) {
|
|
|
1798
2131
|
"start",
|
|
1799
2132
|
"auth",
|
|
1800
2133
|
"doctor",
|
|
1801
|
-
"skills"
|
|
2134
|
+
"skills",
|
|
2135
|
+
"api-keys"
|
|
1802
2136
|
].includes(command)) {
|
|
1803
2137
|
printHelp();
|
|
1804
2138
|
return;
|
|
@@ -1849,6 +2183,9 @@ async function entry(args) {
|
|
|
1849
2183
|
case "skills":
|
|
1850
2184
|
await skillsCommand(effectiveSubcommand, args);
|
|
1851
2185
|
break;
|
|
2186
|
+
case "api-keys":
|
|
2187
|
+
await apiKeysCommand(effectiveSubcommand, args);
|
|
2188
|
+
break;
|
|
1852
2189
|
default:
|
|
1853
2190
|
console.log(chalk.red(`Unknown command: ${command}`));
|
|
1854
2191
|
console.log("");
|
|
@@ -1877,11 +2214,10 @@ ${chalk.green.bold("Database")}
|
|
|
1877
2214
|
${chalk.blue.bold("db push")} Apply schema directly to database ${chalk.gray("(dev)")}
|
|
1878
2215
|
${chalk.blue.bold("db generate")} Generate SQL migration files
|
|
1879
2216
|
${chalk.blue.bold("db migrate")} Run pending migrations
|
|
1880
|
-
${chalk.blue.bold("db studio")} Open Drizzle Studio
|
|
1881
2217
|
${chalk.blue.bold("db")} ${chalk.gray("--help")} Show database command help
|
|
1882
2218
|
|
|
1883
2219
|
${chalk.green.bold("SDK")}
|
|
1884
|
-
${chalk.blue.bold("generate-sdk")} Generate a typed
|
|
2220
|
+
${chalk.blue.bold("generate-sdk")} Generate a typed TypeScript SDK from collections
|
|
1885
2221
|
|
|
1886
2222
|
${chalk.green.bold("Auth")}
|
|
1887
2223
|
${chalk.blue.bold("auth reset-password")} Reset a user's password
|
|
@@ -1893,6 +2229,12 @@ ${chalk.green.bold("Diagnostics")}
|
|
|
1893
2229
|
${chalk.green.bold("AI Agent Skills")}
|
|
1894
2230
|
${chalk.blue.bold("skills install")} Install Rebase agent skills for your AI coding assistant
|
|
1895
2231
|
|
|
2232
|
+
${chalk.green.bold("API Keys")}
|
|
2233
|
+
${chalk.blue.bold("api-keys list")} List all service API keys
|
|
2234
|
+
${chalk.blue.bold("api-keys create")} Create a new scoped API key
|
|
2235
|
+
${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
|
|
2236
|
+
${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
|
|
2237
|
+
|
|
1896
2238
|
${chalk.green.bold("Options")}
|
|
1897
2239
|
${chalk.blue("--version, -v")} Show version number
|
|
1898
2240
|
${chalk.blue("--help, -h")} Show this help message
|