@rebasepro/cli 0.6.1 → 0.8.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.
Files changed (57) hide show
  1. package/README.md +0 -1
  2. package/dist/commands/api-keys.d.ts +1 -0
  3. package/dist/commands/generate_sdk.d.ts +1 -1
  4. package/dist/index.es.js +464 -58
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/utils/project.d.ts +15 -0
  7. package/package.json +8 -9
  8. package/templates/template/backend/Dockerfile +4 -3
  9. package/templates/template/backend/functions/hello.ts +32 -32
  10. package/templates/template/backend/package.json +1 -1
  11. package/templates/template/backend/src/index.ts +1 -2
  12. package/templates/template/config/collections/posts.ts +4 -4
  13. package/templates/template/config/collections/presets/ecommerce/categories.ts +3 -1
  14. package/templates/template/config/collections/presets/ecommerce/orders.ts +3 -1
  15. package/templates/template/config/collections/presets/ecommerce/products.ts +3 -1
  16. package/templates/template/config/collections/tags.ts +1 -3
  17. package/templates/template/config/collections/users.ts +4 -5
  18. package/templates/template/frontend/Dockerfile +5 -3
  19. package/templates/template/package.json +3 -2
  20. package/templates/template/pnpm-workspace.yaml +2 -0
  21. package/templates/template/scripts/example.ts +5 -2
  22. package/skills/rebase-admin/SKILL.md +0 -710
  23. package/skills/rebase-api/SKILL.md +0 -662
  24. package/skills/rebase-api/references/.gitkeep +0 -3
  25. package/skills/rebase-auth/SKILL.md +0 -1143
  26. package/skills/rebase-auth/references/.gitkeep +0 -3
  27. package/skills/rebase-backend-postgres/SKILL.md +0 -633
  28. package/skills/rebase-backend-postgres/references/.gitkeep +0 -3
  29. package/skills/rebase-basics/SKILL.md +0 -749
  30. package/skills/rebase-basics/references/.gitkeep +0 -3
  31. package/skills/rebase-collections/SKILL.md +0 -1328
  32. package/skills/rebase-collections/references/.gitkeep +0 -3
  33. package/skills/rebase-cron-jobs/SKILL.md +0 -699
  34. package/skills/rebase-cron-jobs/references/.gitkeep +0 -1
  35. package/skills/rebase-custom-functions/SKILL.md +0 -233
  36. package/skills/rebase-deployment/SKILL.md +0 -885
  37. package/skills/rebase-deployment/references/.gitkeep +0 -3
  38. package/skills/rebase-design-language/SKILL.md +0 -692
  39. package/skills/rebase-email/SKILL.md +0 -701
  40. package/skills/rebase-email/references/.gitkeep +0 -1
  41. package/skills/rebase-entity-history/SKILL.md +0 -485
  42. package/skills/rebase-entity-history/references/.gitkeep +0 -1
  43. package/skills/rebase-local-env-setup/SKILL.md +0 -189
  44. package/skills/rebase-local-env-setup/references/.gitkeep +0 -3
  45. package/skills/rebase-realtime/SKILL.md +0 -755
  46. package/skills/rebase-realtime/references/.gitkeep +0 -3
  47. package/skills/rebase-sdk/SKILL.md +0 -594
  48. package/skills/rebase-sdk/references/.gitkeep +0 -0
  49. package/skills/rebase-storage/SKILL.md +0 -765
  50. package/skills/rebase-storage/references/.gitkeep +0 -3
  51. package/skills/rebase-studio/SKILL.md +0 -746
  52. package/skills/rebase-studio/references/.gitkeep +0 -3
  53. package/skills/rebase-ui-components/SKILL.md +0 -1488
  54. package/skills/rebase-ui-components/references/.gitkeep +0 -3
  55. package/skills/rebase-webhooks/SKILL.md +0 -623
  56. package/skills/rebase-webhooks/references/.gitkeep +0 -1
  57. 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 JS SDK, and writes it to the output directory (default: ./generated/sdk).
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$2 = fileURLToPath(import.meta.url);
134
- var __dirname$2 = path.dirname(__filename$2);
135
- function findParentDir$1(currentDir, targetName) {
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$1(__dirname$2, "cli");
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 JS SDK, and writes it to the output directory (default: ./generated/sdk).
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" || w.includes("backend"))) return dir;
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;
@@ -811,6 +812,41 @@ function resolveTsx(projectRoot) {
811
812
  return resolveLocalBin(projectRoot, "tsx");
812
813
  }
813
814
  /**
815
+ * Validate that a resolved tsx binary actually has an intact installation.
816
+ *
817
+ * `resolveLocalBin` only checks whether `node_modules/.bin/tsx` (a symlink)
818
+ * exists. If the pnpm content-addressable store was cleaned or a previous
819
+ * install was interrupted, the symlink can exist while critical files inside
820
+ * the tsx package (e.g. `dist/preflight.cjs`) are missing — causing a
821
+ * confusing MODULE_NOT_FOUND error at runtime.
822
+ *
823
+ * This function follows the symlink, walks up to find the tsx package root
824
+ * (`package.json` with `name: "tsx"`), and verifies that `dist/preflight.cjs`
825
+ * is present. Returns `null` when the installation looks healthy, or an
826
+ * error description string when it appears corrupted.
827
+ */
828
+ function validateTsxInstallation(tsxBinPath) {
829
+ try {
830
+ const realPath = fs.realpathSync(tsxBinPath);
831
+ let dir = path.dirname(realPath);
832
+ const fsRoot = path.parse(dir).root;
833
+ for (let depth = 0; depth < 10 && dir !== fsRoot; depth++) {
834
+ const pkgPath = path.join(dir, "package.json");
835
+ if (fs.existsSync(pkgPath)) try {
836
+ if (JSON.parse(fs.readFileSync(pkgPath, "utf-8")).name === "tsx") {
837
+ const preflightPath = path.join(dir, "dist", "preflight.cjs");
838
+ if (!fs.existsSync(preflightPath)) return `tsx package at ${dir} is missing dist/preflight.cjs`;
839
+ return null;
840
+ }
841
+ } catch {}
842
+ dir = path.dirname(dir);
843
+ }
844
+ return null;
845
+ } catch (err) {
846
+ return `tsx binary symlink is broken: ${err instanceof Error ? err.message : String(err)}`;
847
+ }
848
+ }
849
+ /**
814
850
  * Require the project root or exit with a helpful error.
815
851
  */
816
852
  function requireProjectRoot() {
@@ -962,7 +998,6 @@ ${chalk.green.bold("Commands")}
962
998
  ${chalk.blue.bold("push")} Apply schema directly to database (development)
963
999
  ${chalk.blue.bold("generate")} Generate migration files
964
1000
  ${chalk.blue.bold("migrate")} Run pending migrations
965
- ${chalk.blue.bold("studio")} Open Studio viewer
966
1001
  ${chalk.blue.bold("branch")} Database branching (create, list, delete, info)
967
1002
 
968
1003
  ${chalk.green.bold("Examples")}
@@ -1156,6 +1191,16 @@ async function devCommand(rawArgs) {
1156
1191
  console.error(chalk.gray(` Install it with: ${addCmd}`));
1157
1192
  process.exit(1);
1158
1193
  }
1194
+ const tsxValidationError = validateTsxInstallation(tsxBin);
1195
+ if (tsxValidationError) {
1196
+ const installCmd = getPMCommands(detectPackageManager(projectRoot)).install.join(" ");
1197
+ console.error(chalk.red(" ✗ tsx installation appears corrupted."));
1198
+ console.error(chalk.gray(` ${tsxValidationError}`));
1199
+ console.error("");
1200
+ console.error(chalk.gray(" To fix, run:"));
1201
+ console.error(chalk.cyan(` rm -rf node_modules && ${installCmd}`));
1202
+ process.exit(1);
1203
+ }
1159
1204
  const envFile = findEnvFile(projectRoot);
1160
1205
  const env = { ...process.env };
1161
1206
  if (envFile) env.DOTENV_CONFIG_PATH = envFile;
@@ -1286,9 +1331,28 @@ async function devCommand(rawArgs) {
1286
1331
  }
1287
1332
  });
1288
1333
  });
1334
+ /** Whether we've already shown a corrupted-modules recovery hint. */
1335
+ let corruptedModulesWarned = false;
1289
1336
  backendChild.stderr?.on("data", (data) => {
1290
1337
  data.toString().split("\n").filter(Boolean).forEach((line) => {
1291
1338
  console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
1339
+ if (!corruptedModulesWarned) {
1340
+ const cleanLine = stripAnsi(line);
1341
+ if (cleanLine.includes("Cannot find module") && cleanLine.includes("node_modules/.pnpm/")) {
1342
+ corruptedModulesWarned = true;
1343
+ setTimeout(() => {
1344
+ const installCmd = getPMCommands(detectPackageManager(projectRoot)).install.join(" ");
1345
+ console.error("");
1346
+ console.error(chalk.red(" ✗ node_modules appears corrupted — a required file is missing."));
1347
+ console.error(chalk.gray(" This usually happens when a previous install was interrupted"));
1348
+ console.error(chalk.gray(" or the package manager store was cleaned."));
1349
+ console.error("");
1350
+ console.error(chalk.gray(" To fix, stop the dev server and run:"));
1351
+ console.error(chalk.cyan(` rm -rf node_modules && ${installCmd}`));
1352
+ console.error("");
1353
+ }, 200);
1354
+ }
1355
+ }
1292
1356
  });
1293
1357
  });
1294
1358
  children.push(backendChild);
@@ -1431,19 +1495,90 @@ async function resetPassword(rawArgs) {
1431
1495
  process.exit(1);
1432
1496
  }
1433
1497
  const projectRoot = requireProjectRoot();
1498
+ let envServiceKey;
1499
+ const envFile = findEnvFile(projectRoot);
1500
+ if (envFile && fs.existsSync(envFile)) try {
1501
+ const match = fs.readFileSync(envFile, "utf8").match(/^\s*REBASE_SERVICE_KEY\s*=\s*['"]?(.*?)['"]?\s*$/m);
1502
+ if (match && match[1]) envServiceKey = match[1];
1503
+ } catch {}
1504
+ let baseUrl = process.env.REBASE_BASE_URL;
1505
+ let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;
1506
+ const statePath = path.join(projectRoot, ".rebase", "state.json");
1507
+ if (fs.existsSync(statePath)) try {
1508
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
1509
+ if (state && typeof state === "object") {
1510
+ if (typeof state.baseUrl === "string" && !baseUrl) baseUrl = state.baseUrl;
1511
+ if (typeof state.serviceKey === "string" && !serviceKey) serviceKey = state.serviceKey;
1512
+ }
1513
+ } catch {}
1514
+ const devUrlPath = path.join(projectRoot, ".rebase-dev-url");
1515
+ if (fs.existsSync(devUrlPath) && !baseUrl) try {
1516
+ baseUrl = fs.readFileSync(devUrlPath, "utf8").trim();
1517
+ } catch {}
1518
+ if (baseUrl && serviceKey) {
1519
+ console.log("Trying API-first reset via running backend...");
1520
+ try {
1521
+ const finalPass = newPassword || "NewPassword123!";
1522
+ const cleanBaseUrl = baseUrl.replace(/\/+$/, "");
1523
+ const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=1`;
1524
+ const searchRes = await fetch(searchUrl, { headers: {
1525
+ "Authorization": `Bearer ${serviceKey}`,
1526
+ "Accept": "application/json"
1527
+ } });
1528
+ if (!searchRes.ok) throw new Error(`Failed to list users: ${searchRes.statusText}`);
1529
+ const searchData = await searchRes.json();
1530
+ if (!searchData || typeof searchData !== "object") throw new Error("Invalid response format from user search API.");
1531
+ let userId;
1532
+ if (Array.isArray(searchData)) {
1533
+ const firstUser = searchData[0];
1534
+ if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
1535
+ else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
1536
+ } else if ("users" in searchData && Array.isArray(searchData.users)) {
1537
+ const firstUser = searchData.users[0];
1538
+ if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
1539
+ else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
1540
+ }
1541
+ if (!userId) throw new Error(`User not found with email: ${email}`);
1542
+ const resetUrl = `${cleanBaseUrl}/api/admin/users/${userId}/reset-password`;
1543
+ const resetRes = await fetch(resetUrl, {
1544
+ method: "POST",
1545
+ headers: {
1546
+ "Authorization": `Bearer ${serviceKey}`,
1547
+ "Content-Type": "application/json",
1548
+ "Accept": "application/json"
1549
+ },
1550
+ body: JSON.stringify({ password: finalPass })
1551
+ });
1552
+ if (!resetRes.ok) {
1553
+ const errText = await resetRes.text();
1554
+ throw new Error(`Password reset endpoint failed: ${errText || resetRes.statusText}`);
1555
+ }
1556
+ console.log("API reset successful.");
1557
+ console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (via API)"));
1558
+ console.log("");
1559
+ console.log(` ${chalk.gray("Email:")} ${email}`);
1560
+ console.log(` ${chalk.gray("Password:")} ${finalPass}`);
1561
+ console.log("");
1562
+ return;
1563
+ } catch (err) {
1564
+ const errMsg = err instanceof Error ? err.message : String(err);
1565
+ console.warn(chalk.yellow("API reset failed, falling back to direct database update..."));
1566
+ console.warn(chalk.gray(` Details: ${errMsg}`));
1567
+ }
1568
+ }
1434
1569
  const backendDir = requireBackendDir(projectRoot);
1435
1570
  const tsxBin = resolveTsx(projectRoot);
1436
1571
  if (!tsxBin) {
1437
1572
  console.error(chalk.red("✗ Could not find tsx binary."));
1438
1573
  process.exit(1);
1439
1574
  }
1440
- const envFile = findEnvFile(projectRoot);
1441
- const env = { ...process.env };
1442
- if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1443
- env.REBASE_RESET_EMAIL = email;
1444
- env.REBASE_RESET_PASSWORD = newPassword || "NewPassword123!";
1445
- env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
1446
- const scriptContent = `
1575
+ try {
1576
+ const env = { ...process.env };
1577
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1578
+ env.REBASE_RESET_EMAIL = email;
1579
+ env.REBASE_RESET_PASSWORD = newPassword || "NewPassword123!";
1580
+ env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
1581
+ const scriptContent = `
1447
1582
  import { createPostgresDatabaseConnection } from "@rebasepro/server-postgresql";
1448
1583
  import { hashPassword } from "@rebasepro/server-core";
1449
1584
  import { eq } from "drizzle-orm";
@@ -1497,28 +1632,33 @@ async function resetPassword() {
1497
1632
 
1498
1633
  resetPassword().catch(console.error);
1499
1634
  `;
1500
- const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
1501
- fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
1502
- console.log("");
1503
- console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password"));
1504
- console.log("");
1505
- console.log(` ${chalk.gray("Email:")} ${email}`);
1506
- if (newPassword) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
1507
- console.log("");
1508
- const child = spawn(tsxBin, [tmpScriptPath], {
1509
- cwd: backendDir,
1510
- stdio: "inherit",
1511
- env
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();
1635
+ const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
1636
+ fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
1637
+ console.log("");
1638
+ console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)"));
1639
+ console.log("");
1640
+ console.log(` ${chalk.gray("Email:")} ${email}`);
1641
+ if (newPassword) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
1642
+ console.log("");
1643
+ const child = spawn(tsxBin, [tmpScriptPath], {
1644
+ cwd: backendDir,
1645
+ stdio: "inherit",
1646
+ env
1520
1647
  });
1521
- });
1648
+ return new Promise((resolve) => {
1649
+ child.on("close", (code) => {
1650
+ try {
1651
+ fs.unlinkSync(tmpScriptPath);
1652
+ } catch {}
1653
+ if (code !== 0) process.exit(code ?? 1);
1654
+ resolve();
1655
+ });
1656
+ });
1657
+ } catch (err) {
1658
+ console.error(chalk.red("✗ Direct database update failed."));
1659
+ console.error(err instanceof Error ? err.message : String(err));
1660
+ process.exit(1);
1661
+ }
1522
1662
  }
1523
1663
  function printAuthHelp() {
1524
1664
  console.log(`
@@ -1587,8 +1727,7 @@ async function doctorCommand(rawArgs) {
1587
1727
  }
1588
1728
  //#endregion
1589
1729
  //#region src/commands/skills.ts
1590
- var __filename$1 = fileURLToPath(import.meta.url);
1591
- var __dirname$1 = path.dirname(__filename$1);
1730
+ var require = createRequire(import.meta.url);
1592
1731
  /** Supported agent environments and their target directories. */
1593
1732
  var AGENTS = {
1594
1733
  cursor: {
@@ -1632,24 +1771,16 @@ var AGENTS = {
1632
1771
  })
1633
1772
  }
1634
1773
  };
1635
- function findParentDir(currentDir, targetName) {
1636
- const root = path.parse(currentDir).root;
1637
- while (currentDir && currentDir !== root) {
1638
- if (path.basename(currentDir) === targetName) return currentDir;
1639
- currentDir = path.dirname(currentDir);
1640
- }
1641
- return null;
1642
- }
1643
- /** Resolve the path to the bundled skills directory. */
1774
+ /**
1775
+ * Resolve the path to the skills directory from @rebasepro/agent-skills.
1776
+ * Works in both workspace (symlink) and published (real files) layouts.
1777
+ */
1644
1778
  function getSkillsSourceDir() {
1645
- const cliRoot = findParentDir(__dirname$1, "cli");
1646
- if (cliRoot) {
1647
- const dir = path.join(cliRoot, "skills");
1648
- if (fs.existsSync(dir)) return dir;
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`.");
1779
+ const pkgJsonPath = require.resolve("@rebasepro/agent-skills/package.json");
1780
+ const pkgRoot = path.dirname(pkgJsonPath);
1781
+ const skillsDir = path.join(pkgRoot, "skills");
1782
+ if (!fs.existsSync(skillsDir)) throw new Error(`Skills directory not found at ${skillsDir}. Make sure @rebasepro/agent-skills is installed.`);
1783
+ return skillsDir;
1653
1784
  }
1654
1785
  /** Read all skill directories and return their names + content. */
1655
1786
  function loadSkills(skillsDir) {
@@ -1764,6 +1895,272 @@ ${chalk.green.bold("Examples")}
1764
1895
  `);
1765
1896
  }
1766
1897
  //#endregion
1898
+ //#region src/commands/api-keys.ts
1899
+ /**
1900
+ * CLI command: rebase api-keys <action>
1901
+ *
1902
+ * Subcommands:
1903
+ * list — List all API keys (masked)
1904
+ * create — Create a new API key
1905
+ * revoke — Revoke an existing API key
1906
+ */
1907
+ function loadEnv(projectRoot) {
1908
+ const envFile = findEnvFile(projectRoot);
1909
+ const env = {};
1910
+ if (envFile && fs.existsSync(envFile)) {
1911
+ const content = fs.readFileSync(envFile, "utf-8");
1912
+ for (const line of content.split("\n")) {
1913
+ const trimmed = line.trim();
1914
+ if (!trimmed || trimmed.startsWith("#")) continue;
1915
+ const idx = trimmed.indexOf("=");
1916
+ if (idx > 0) {
1917
+ const key = trimmed.slice(0, idx).trim();
1918
+ let value = trimmed.slice(idx + 1).trim();
1919
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1920
+ env[key] = value;
1921
+ }
1922
+ }
1923
+ }
1924
+ return env;
1925
+ }
1926
+ function resolveBaseUrl(env) {
1927
+ const port = env.PORT || env.REBASE_PORT || "3001";
1928
+ return env.REBASE_BASE_URL || `http://localhost:${port}`;
1929
+ }
1930
+ async function apiKeysCommand(subcommand, rawArgs) {
1931
+ if (!subcommand || subcommand === "--help") {
1932
+ printApiKeysHelp();
1933
+ return;
1934
+ }
1935
+ switch (subcommand) {
1936
+ case "list":
1937
+ await listKeys(rawArgs);
1938
+ break;
1939
+ case "create":
1940
+ await createKey(rawArgs);
1941
+ break;
1942
+ case "revoke":
1943
+ await revokeKey(rawArgs);
1944
+ break;
1945
+ default:
1946
+ console.error(chalk.red(`Unknown api-keys command: ${subcommand}`));
1947
+ console.log("");
1948
+ printApiKeysHelp();
1949
+ process.exit(1);
1950
+ }
1951
+ }
1952
+ async function listKeys(_rawArgs) {
1953
+ const env = loadEnv(requireProjectRoot());
1954
+ const baseUrl = resolveBaseUrl(env);
1955
+ const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
1956
+ if (!serviceKey) {
1957
+ console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
1958
+ process.exit(1);
1959
+ }
1960
+ try {
1961
+ const res = await fetch(`${baseUrl}/api/admin/api-keys`, { headers: { Authorization: `Bearer ${serviceKey}` } });
1962
+ if (!res.ok) {
1963
+ const body = await res.text();
1964
+ console.error(chalk.red(`✗ Failed to list API keys: ${res.status} ${body}`));
1965
+ process.exit(1);
1966
+ }
1967
+ const { keys } = await res.json();
1968
+ console.log("");
1969
+ console.log(chalk.bold(" 🔑 API Keys"));
1970
+ console.log("");
1971
+ if (keys.length === 0) {
1972
+ console.log(chalk.gray(" No API keys found."));
1973
+ console.log("");
1974
+ return;
1975
+ }
1976
+ for (const key of keys) {
1977
+ 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");
1978
+ const perms = key.permissions.map((p) => `${p.collection}(${p.operations.join(",")})`).join(", ");
1979
+ console.log(` ${chalk.bold(key.name)} ${chalk.gray(`[${key.key_prefix}•••]`)} ${status}`);
1980
+ console.log(` ${chalk.gray("ID:")} ${key.id}`);
1981
+ console.log(` ${chalk.gray("Permissions:")} ${perms || "none"}`);
1982
+ console.log(` ${chalk.gray("Created:")} ${new Date(key.created_at).toLocaleDateString()}`);
1983
+ if (key.last_used_at) console.log(` ${chalk.gray("Last used:")} ${new Date(key.last_used_at).toLocaleDateString()}`);
1984
+ console.log("");
1985
+ }
1986
+ } catch (e) {
1987
+ console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
1988
+ console.error(chalk.gray(" Is the Rebase server running?"));
1989
+ process.exit(1);
1990
+ }
1991
+ }
1992
+ async function createKey(rawArgs) {
1993
+ const args = arg({
1994
+ "--name": String,
1995
+ "--permissions": String,
1996
+ "--admin": Boolean,
1997
+ "--rate-limit": Number,
1998
+ "--expires": String,
1999
+ "-n": "--name"
2000
+ }, {
2001
+ argv: rawArgs.slice(4),
2002
+ permissive: true
2003
+ });
2004
+ const name = args["--name"] || args._[0];
2005
+ const permissionsRaw = args["--permissions"];
2006
+ if (!name) {
2007
+ console.error(chalk.red("✗ Name is required."));
2008
+ console.log("");
2009
+ console.log(chalk.gray(" Usage: rebase api-keys create --name \"My Key\" --permissions '[{\"collection\":\"*\",\"operations\":[\"read\"]}]'"));
2010
+ process.exit(1);
2011
+ }
2012
+ let permissions;
2013
+ if (permissionsRaw) try {
2014
+ permissions = JSON.parse(permissionsRaw);
2015
+ } catch {
2016
+ console.error(chalk.red("✗ Invalid --permissions JSON."));
2017
+ console.log(chalk.gray(" Example: '[{\"collection\":\"*\",\"operations\":[\"read\",\"write\"]}]'"));
2018
+ process.exit(1);
2019
+ return;
2020
+ }
2021
+ else permissions = [{
2022
+ collection: "*",
2023
+ operations: [
2024
+ "read",
2025
+ "write",
2026
+ "delete"
2027
+ ]
2028
+ }];
2029
+ let expires_at = null;
2030
+ const expiresFlag = args["--expires"];
2031
+ if (expiresFlag) {
2032
+ const days = {
2033
+ "7d": 7,
2034
+ "30d": 30,
2035
+ "90d": 90,
2036
+ "1y": 365
2037
+ };
2038
+ if (days[expiresFlag]) expires_at = new Date(Date.now() + days[expiresFlag] * 864e5).toISOString();
2039
+ else {
2040
+ const parsed = new Date(expiresFlag);
2041
+ if (isNaN(parsed.getTime())) {
2042
+ console.error(chalk.red("✗ Invalid --expires value. Use 7d, 30d, 90d, 1y, or an ISO date."));
2043
+ process.exit(1);
2044
+ }
2045
+ expires_at = parsed.toISOString();
2046
+ }
2047
+ }
2048
+ const env = loadEnv(requireProjectRoot());
2049
+ const baseUrl = resolveBaseUrl(env);
2050
+ const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
2051
+ if (!serviceKey) {
2052
+ console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
2053
+ process.exit(1);
2054
+ }
2055
+ try {
2056
+ const body = {
2057
+ name,
2058
+ permissions,
2059
+ admin: args["--admin"] ?? false,
2060
+ rate_limit: args["--rate-limit"] ?? null,
2061
+ expires_at
2062
+ };
2063
+ const res = await fetch(`${baseUrl}/api/admin/api-keys`, {
2064
+ method: "POST",
2065
+ headers: {
2066
+ Authorization: `Bearer ${serviceKey}`,
2067
+ "Content-Type": "application/json"
2068
+ },
2069
+ body: JSON.stringify(body)
2070
+ });
2071
+ if (!res.ok) {
2072
+ const errBody = await res.text();
2073
+ console.error(chalk.red(`✗ Failed to create API key: ${res.status} ${errBody}`));
2074
+ process.exit(1);
2075
+ }
2076
+ const { key } = await res.json();
2077
+ console.log("");
2078
+ console.log(chalk.bold.green(" ✓ API key created successfully"));
2079
+ console.log("");
2080
+ console.log(` ${chalk.gray("Name:")} ${key.name}`);
2081
+ console.log(` ${chalk.gray("ID:")} ${key.id}`);
2082
+ console.log(` ${chalk.gray("Prefix:")} ${key.key_prefix}`);
2083
+ console.log("");
2084
+ console.log(chalk.bold.yellow(" ⚠ Copy your key now — it won't be shown again:"));
2085
+ console.log("");
2086
+ console.log(` ${chalk.cyan(key.key)}`);
2087
+ console.log("");
2088
+ } catch (e) {
2089
+ console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
2090
+ console.error(chalk.gray(" Is the Rebase server running?"));
2091
+ process.exit(1);
2092
+ }
2093
+ }
2094
+ async function revokeKey(rawArgs) {
2095
+ const args = arg({ "--id": String }, {
2096
+ argv: rawArgs.slice(4),
2097
+ permissive: true
2098
+ });
2099
+ const id = args["--id"] || args._[0];
2100
+ if (!id) {
2101
+ console.error(chalk.red("✗ Key ID is required."));
2102
+ console.log("");
2103
+ console.log(chalk.gray(" Usage: rebase api-keys revoke <key-id>"));
2104
+ process.exit(1);
2105
+ }
2106
+ const env = loadEnv(requireProjectRoot());
2107
+ const baseUrl = resolveBaseUrl(env);
2108
+ const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
2109
+ if (!serviceKey) {
2110
+ console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
2111
+ process.exit(1);
2112
+ }
2113
+ try {
2114
+ const res = await fetch(`${baseUrl}/api/admin/api-keys/${encodeURIComponent(id)}`, {
2115
+ method: "DELETE",
2116
+ headers: { Authorization: `Bearer ${serviceKey}` }
2117
+ });
2118
+ if (!res.ok) {
2119
+ const errBody = await res.text();
2120
+ console.error(chalk.red(`✗ Failed to revoke API key: ${res.status} ${errBody}`));
2121
+ process.exit(1);
2122
+ }
2123
+ console.log("");
2124
+ console.log(chalk.bold.green(" ✓ API key revoked successfully"));
2125
+ console.log(` ${chalk.gray("ID:")} ${id}`);
2126
+ console.log("");
2127
+ } catch (e) {
2128
+ console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
2129
+ console.error(chalk.gray(" Is the Rebase server running?"));
2130
+ process.exit(1);
2131
+ }
2132
+ }
2133
+ function printApiKeysHelp() {
2134
+ console.log(`
2135
+ ${chalk.bold("rebase api-keys")} — Manage Service API Keys
2136
+
2137
+ ${chalk.green.bold("Usage")}
2138
+ rebase api-keys ${chalk.blue("<command>")} [options]
2139
+
2140
+ ${chalk.green.bold("Commands")}
2141
+ ${chalk.blue.bold("list")} List all API keys
2142
+ ${chalk.blue.bold("create")} Create a new API key
2143
+ ${chalk.blue.bold("revoke")} Revoke an API key
2144
+
2145
+ ${chalk.green.bold("create Options")}
2146
+ ${chalk.blue("--name, -n")} Key name ${chalk.gray("(required)")}
2147
+ ${chalk.blue("--permissions")} JSON array of permissions
2148
+ ${chalk.gray("Default: [{\"collection\":\"*\",\"operations\":[\"read\",\"write\",\"delete\"]}]")}
2149
+ ${chalk.blue("--admin")} Grant admin role (access to admin routes)
2150
+ ${chalk.blue("--rate-limit")} Requests per 15-min window ${chalk.gray("(default: 1000)")}
2151
+ ${chalk.blue("--expires")} Expiration: 7d, 30d, 90d, 1y, or ISO date
2152
+
2153
+ ${chalk.green.bold("revoke Options")}
2154
+ ${chalk.blue("--id")} API key ID to revoke ${chalk.gray("(or positional arg)")}
2155
+
2156
+ ${chalk.green.bold("Examples")}
2157
+ rebase api-keys list
2158
+ rebase api-keys create --name "Analytics" --permissions '[{"collection":"events","operations":["read"]}]'
2159
+ rebase api-keys create -n "Full Access" --expires 90d
2160
+ rebase api-keys revoke abc123-def456
2161
+ `);
2162
+ }
2163
+ //#endregion
1767
2164
  //#region src/cli.ts
1768
2165
  var __filename = fileURLToPath(import.meta.url);
1769
2166
  var __dirname = path.dirname(__filename);
@@ -1798,7 +2195,8 @@ async function entry(args) {
1798
2195
  "start",
1799
2196
  "auth",
1800
2197
  "doctor",
1801
- "skills"
2198
+ "skills",
2199
+ "api-keys"
1802
2200
  ].includes(command)) {
1803
2201
  printHelp();
1804
2202
  return;
@@ -1849,6 +2247,9 @@ async function entry(args) {
1849
2247
  case "skills":
1850
2248
  await skillsCommand(effectiveSubcommand, args);
1851
2249
  break;
2250
+ case "api-keys":
2251
+ await apiKeysCommand(effectiveSubcommand, args);
2252
+ break;
1852
2253
  default:
1853
2254
  console.log(chalk.red(`Unknown command: ${command}`));
1854
2255
  console.log("");
@@ -1877,11 +2278,10 @@ ${chalk.green.bold("Database")}
1877
2278
  ${chalk.blue.bold("db push")} Apply schema directly to database ${chalk.gray("(dev)")}
1878
2279
  ${chalk.blue.bold("db generate")} Generate SQL migration files
1879
2280
  ${chalk.blue.bold("db migrate")} Run pending migrations
1880
- ${chalk.blue.bold("db studio")} Open Drizzle Studio
1881
2281
  ${chalk.blue.bold("db")} ${chalk.gray("--help")} Show database command help
1882
2282
 
1883
2283
  ${chalk.green.bold("SDK")}
1884
- ${chalk.blue.bold("generate-sdk")} Generate a typed JS SDK from collections
2284
+ ${chalk.blue.bold("generate-sdk")} Generate a typed TypeScript SDK from collections
1885
2285
 
1886
2286
  ${chalk.green.bold("Auth")}
1887
2287
  ${chalk.blue.bold("auth reset-password")} Reset a user's password
@@ -1893,6 +2293,12 @@ ${chalk.green.bold("Diagnostics")}
1893
2293
  ${chalk.green.bold("AI Agent Skills")}
1894
2294
  ${chalk.blue.bold("skills install")} Install Rebase agent skills for your AI coding assistant
1895
2295
 
2296
+ ${chalk.green.bold("API Keys")}
2297
+ ${chalk.blue.bold("api-keys list")} List all service API keys
2298
+ ${chalk.blue.bold("api-keys create")} Create a new scoped API key
2299
+ ${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
2300
+ ${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
2301
+
1896
2302
  ${chalk.green.bold("Options")}
1897
2303
  ${chalk.blue("--version, -v")} Show version number
1898
2304
  ${chalk.blue("--help, -h")} Show this help message
@@ -1901,6 +2307,6 @@ ${chalk.gray("Documentation: https://rebase.pro/docs")}
1901
2307
  `);
1902
2308
  }
1903
2309
  //#endregion
1904
- export { authCommand, buildCommand, buildInitQuestions, configureEnvFile, createRebaseApp, dbCommand, detectPackageManager, devCommand, doctorCommand, entry, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, generateSdkCommand, getActiveBackendPlugin, getPMCommands, requireBackendDir, requireProjectRoot, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand };
2310
+ export { authCommand, buildCommand, buildInitQuestions, configureEnvFile, createRebaseApp, dbCommand, detectPackageManager, devCommand, doctorCommand, entry, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, generateSdkCommand, getActiveBackendPlugin, getPMCommands, requireBackendDir, requireProjectRoot, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, validateTsxInstallation };
1905
2311
 
1906
2312
  //# sourceMappingURL=index.es.js.map