@rebasepro/cli 0.0.1-canary.eae7889 → 0.0.1-canary.eb08332

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 (74) hide show
  1. package/LICENSE +17 -196
  2. package/README.md +57 -33
  3. package/dist/commands/api-keys.d.ts +1 -0
  4. package/dist/commands/build.d.ts +1 -0
  5. package/dist/commands/cloud/auth.d.ts +3 -0
  6. package/dist/commands/cloud/context.d.ts +61 -0
  7. package/dist/commands/cloud/databases.d.ts +1 -0
  8. package/dist/commands/cloud/deploy.d.ts +2 -0
  9. package/dist/commands/cloud/index.d.ts +1 -0
  10. package/dist/commands/cloud/link.d.ts +5 -0
  11. package/dist/commands/cloud/orgs.d.ts +1 -0
  12. package/dist/commands/cloud/projects.d.ts +13 -0
  13. package/dist/commands/cloud/resources.d.ts +6 -0
  14. package/dist/commands/generate_sdk.d.ts +1 -1
  15. package/dist/commands/init.d.ts +39 -0
  16. package/dist/commands/skills.d.ts +1 -0
  17. package/dist/commands/start.d.ts +1 -0
  18. package/dist/index.d.ts +6 -1
  19. package/dist/index.es.js +3785 -990
  20. package/dist/index.es.js.map +1 -1
  21. package/dist/utils/package-manager.d.ts +33 -0
  22. package/dist/utils/project.d.ts +16 -1
  23. package/package.json +26 -23
  24. package/templates/overlays/baas/README.md +37 -0
  25. package/templates/overlays/baas/backend/package.json +31 -0
  26. package/templates/overlays/baas/backend/src/index.ts +172 -0
  27. package/templates/overlays/baas/backend/tsconfig.json +18 -0
  28. package/templates/overlays/baas/package.json +42 -0
  29. package/templates/overlays/baas/pnpm-workspace.yaml +9 -0
  30. package/templates/template/.cursorrules +2 -0
  31. package/templates/template/.dockerignore +1 -1
  32. package/templates/template/.env.example +120 -0
  33. package/templates/template/.github/copilot-instructions.md +2 -0
  34. package/templates/template/.windsurfrules +2 -0
  35. package/templates/template/AGENTS.md +2 -0
  36. package/templates/template/CLAUDE.md +2 -0
  37. package/templates/template/README.md +20 -10
  38. package/templates/template/ai-instructions.md +17 -0
  39. package/templates/template/backend/Dockerfile +5 -4
  40. package/templates/template/backend/functions/hello.ts +36 -32
  41. package/templates/template/backend/package.json +11 -11
  42. package/templates/template/backend/src/env.ts +13 -42
  43. package/templates/template/backend/src/index.ts +54 -28
  44. package/templates/template/config/collections/authors.ts +2 -2
  45. package/templates/template/config/collections/index.ts +25 -4
  46. package/templates/template/config/collections/posts.ts +15 -35
  47. package/templates/template/config/collections/presets/blank/index.ts +3 -0
  48. package/templates/template/config/collections/presets/ecommerce/categories.ts +40 -0
  49. package/templates/template/config/collections/presets/ecommerce/index.ts +6 -0
  50. package/templates/template/config/collections/presets/ecommerce/orders.ts +66 -0
  51. package/templates/template/config/collections/presets/ecommerce/products.ts +64 -0
  52. package/templates/template/config/collections/tags.ts +3 -5
  53. package/templates/template/config/collections/users.ts +142 -0
  54. package/templates/template/config/index.ts +1 -1
  55. package/templates/template/docker-compose.yml +14 -39
  56. package/templates/template/frontend/Dockerfile +5 -3
  57. package/templates/template/frontend/package.json +8 -7
  58. package/templates/template/frontend/src/App.tsx +26 -105
  59. package/templates/template/frontend/src/main.tsx +4 -0
  60. package/templates/template/frontend/src/virtual.d.ts +6 -0
  61. package/templates/template/frontend/tsconfig.json +3 -2
  62. package/templates/template/frontend/vite.config.ts +47 -4
  63. package/templates/template/package.json +22 -8
  64. package/templates/template/pnpm-workspace.yaml +9 -0
  65. package/templates/template/scripts/example.ts +5 -2
  66. package/dist/auth.d.ts +0 -5
  67. package/dist/commands/cli.test.d.ts +0 -1
  68. package/dist/commands/dev.test.d.ts +0 -1
  69. package/dist/commands/init.test.d.ts +0 -1
  70. package/dist/index.cjs +0 -1203
  71. package/dist/index.cjs.map +0 -1
  72. package/dist/utils/project.test.d.ts +0 -1
  73. package/templates/template/.env.template +0 -62
  74. package/templates/template/backend/drizzle.config.ts +0 -22
package/dist/index.es.js CHANGED
@@ -3,488 +3,991 @@ import arg from "arg";
3
3
  import inquirer from "inquirer";
4
4
  import path from "path";
5
5
  import fs from "fs";
6
+ import net from "net";
6
7
  import { promisify } from "util";
7
- import execa from "execa";
8
- import ncp from "ncp";
8
+ import { execa, execaCommandSync } from "execa";
9
+ import { cp } from "fs/promises";
9
10
  import { fileURLToPath } from "url";
10
11
  import crypto from "crypto";
11
- import { generateSDK } from "@rebasepro/sdk-generator";
12
+ import { generateSDK } from "@rebasepro/codegen";
12
13
  import { execSync, spawn } from "child_process";
13
- import * as os from "os";
14
- const access = promisify(fs.access);
15
- const copy = promisify(ncp);
16
- const __filename$2 = fileURLToPath(import.meta.url);
17
- const __dirname$2 = path.dirname(__filename$2);
14
+ import { createRequire } from "module";
15
+ import os from "os";
16
+ import { createRebaseClient } from "@rebasepro/client";
17
+ //#region src/utils/package-manager.ts
18
+ /**
19
+ * Package manager detection and command abstraction.
20
+ *
21
+ * Detects whether the user is running pnpm or npm and provides
22
+ * a unified interface for common package-manager operations so
23
+ * the rest of the CLI never has to hardcode a specific PM.
24
+ */
25
+ /**
26
+ * Detect the package manager from the environment or the target directory.
27
+ *
28
+ * Detection order:
29
+ * 1. Explicit override (if provided)
30
+ * 2. `npm_config_user_agent` env var (set by npm/pnpm when running via `npx`/`pnpm dlx`)
31
+ * 3. Lock-file presence in the target directory
32
+ * 4. Default to pnpm (Rebase's recommended PM)
33
+ */
34
+ function detectPackageManager(targetDir) {
35
+ const userAgent = process.env.npm_config_user_agent ?? "";
36
+ if (userAgent.startsWith("npm/")) return "npm";
37
+ if (userAgent.startsWith("pnpm/")) return "pnpm";
38
+ if (targetDir) {
39
+ if (fs.existsSync(path.join(targetDir, "package-lock.json"))) return "npm";
40
+ if (fs.existsSync(path.join(targetDir, "pnpm-lock.yaml"))) return "pnpm";
41
+ }
42
+ const cwd = process.cwd();
43
+ if (cwd !== targetDir) {
44
+ if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm";
45
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
46
+ }
47
+ return "pnpm";
48
+ }
49
+ /** Build the command helpers for a given package manager. */
50
+ function getPMCommands(pm) {
51
+ if (pm === "npm") return {
52
+ name: "npm",
53
+ install: ["npm", "install"],
54
+ run: (script) => [
55
+ "npm",
56
+ "run",
57
+ script
58
+ ],
59
+ exec: (bin, args) => [
60
+ "npx",
61
+ bin,
62
+ ...args
63
+ ],
64
+ view: (pkg, field) => [
65
+ "npm",
66
+ "view",
67
+ pkg,
68
+ field
69
+ ],
70
+ runAll: (script) => [
71
+ "npm",
72
+ "run",
73
+ script,
74
+ "--workspaces",
75
+ "--if-present"
76
+ ],
77
+ runWorkspace: (workspace, script) => [
78
+ "npm",
79
+ "run",
80
+ script,
81
+ "-w",
82
+ workspace
83
+ ],
84
+ dlx: (pkg, args) => [
85
+ "npx",
86
+ "-y",
87
+ pkg,
88
+ ...args
89
+ ],
90
+ workspaceProtocol: "*"
91
+ };
92
+ return {
93
+ name: "pnpm",
94
+ install: ["pnpm", "install"],
95
+ run: (script) => [
96
+ "pnpm",
97
+ "run",
98
+ script
99
+ ],
100
+ exec: (bin, args) => [
101
+ "pnpm",
102
+ "exec",
103
+ bin,
104
+ ...args
105
+ ],
106
+ view: (pkg, field) => [
107
+ "pnpm",
108
+ "view",
109
+ pkg,
110
+ field
111
+ ],
112
+ runAll: (script) => [
113
+ "pnpm",
114
+ "-r",
115
+ "run",
116
+ script
117
+ ],
118
+ runWorkspace: (workspace, script) => [
119
+ "pnpm",
120
+ "--filter",
121
+ workspace,
122
+ script
123
+ ],
124
+ dlx: (pkg, args) => [
125
+ "pnpm",
126
+ "dlx",
127
+ pkg,
128
+ ...args
129
+ ],
130
+ workspaceProtocol: "workspace:*"
131
+ };
132
+ }
133
+ //#endregion
134
+ //#region src/commands/init.ts
135
+ var access = promisify(fs.access);
136
+ var __filename$1 = fileURLToPath(import.meta.url);
137
+ var __dirname$1 = path.dirname(__filename$1);
18
138
  function findParentDir(currentDir, targetName) {
19
- const root = path.parse(currentDir).root;
20
- while (currentDir && currentDir !== root) {
21
- if (path.basename(currentDir) === targetName) {
22
- return currentDir;
23
- }
24
- currentDir = path.dirname(currentDir);
25
- }
26
- return null;
139
+ const root = path.parse(currentDir).root;
140
+ while (currentDir && currentDir !== root) {
141
+ if (path.basename(currentDir) === targetName) return currentDir;
142
+ currentDir = path.dirname(currentDir);
143
+ }
144
+ return null;
145
+ }
146
+ var cliRoot = findParentDir(__dirname$1, "cli");
147
+ var FLAVOR_CHOICES = [{
148
+ name: "BaaS + admin — API plus an admin UI, driven by collections you define (like Payload/Directus)",
149
+ value: "cms",
150
+ short: "BaaS + admin"
151
+ }, {
152
+ name: "BaaS only — headless API over your database. No collections, no UI (like Supabase)",
153
+ value: "baas",
154
+ short: "BaaS only"
155
+ }];
156
+ var PRESET_CHOICES = [
157
+ {
158
+ name: "Blog — Posts, Authors, Tags (with markdown editor)",
159
+ value: "blog",
160
+ short: "Blog"
161
+ },
162
+ {
163
+ name: "E-commerce — Products, Categories, Orders",
164
+ value: "ecommerce",
165
+ short: "E-commerce"
166
+ },
167
+ {
168
+ name: "Blank — Empty project, just authentication",
169
+ value: "blank",
170
+ short: "Blank"
171
+ }
172
+ ];
173
+ /**
174
+ * Builds the interactive prompt questions for `rebase init`.
175
+ * Exported for testability — all prompt `type` values must match
176
+ * types registered by the installed version of inquirer.
177
+ */
178
+ function buildInitQuestions(params) {
179
+ const { nameArg, templateArg, flavorArg, hasGitFlag, hasInstallFlag, pm } = params;
180
+ const questions = [];
181
+ if (!nameArg) questions.push({
182
+ type: "input",
183
+ name: "projectName",
184
+ message: "Project name:",
185
+ default: "my-rebase-app",
186
+ validate: (input) => {
187
+ if (!input.trim()) return "Project name is required";
188
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(input)) return "Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, hyphens, dots, or underscores";
189
+ return true;
190
+ }
191
+ });
192
+ if (!flavorArg) questions.push({
193
+ type: "select",
194
+ name: "flavor",
195
+ message: "What do you want to build?",
196
+ choices: FLAVOR_CHOICES,
197
+ default: "cms"
198
+ });
199
+ if (!templateArg) questions.push({
200
+ type: "select",
201
+ name: "preset",
202
+ message: "Choose a starter template:",
203
+ choices: PRESET_CHOICES,
204
+ default: "blog",
205
+ when: (answers) => (flavorArg ?? answers.flavor) !== "baas"
206
+ });
207
+ if (!hasGitFlag) questions.push({
208
+ type: "confirm",
209
+ name: "git",
210
+ message: "Initialize a git repository?",
211
+ default: true
212
+ });
213
+ if (!hasInstallFlag) questions.push({
214
+ type: "confirm",
215
+ name: "installDeps",
216
+ message: `Install dependencies with ${pm}?`,
217
+ default: true
218
+ });
219
+ questions.push({
220
+ type: "input",
221
+ name: "databaseUrl",
222
+ message: "Enter your PostgreSQL database connection string (leave blank to use a local default):",
223
+ default: "",
224
+ validate: (input) => {
225
+ if (input.trim() && /[\r\n]/.test(input)) return "Database URL cannot contain newline characters.";
226
+ return true;
227
+ }
228
+ });
229
+ questions.push({
230
+ type: "confirm",
231
+ name: "introspect",
232
+ message: "Would you like to introspect this database to automatically generate collections?",
233
+ default: true,
234
+ when: (answers) => !!answers.databaseUrl?.trim()
235
+ });
236
+ return questions;
27
237
  }
28
- const cliRoot = findParentDir(__dirname$2, "cli");
29
238
  async function createRebaseApp(rawArgs) {
30
- console.log(`
239
+ console.log(`
31
240
  ${chalk.bold("Rebase")} — Create a new project 🚀
32
241
  `);
33
- const options = await promptForOptions(rawArgs);
34
- await createProject(options);
35
- }
36
- async function promptForOptions(rawArgs) {
37
- const args = arg(
38
- {
39
- "--git": Boolean,
40
- "--install": Boolean,
41
- "-g": "--git",
42
- "-i": "--install"
43
- },
44
- {
45
- argv: rawArgs.slice(3),
46
- // skip "node", "rebase", "init"
47
- permissive: true
48
- }
49
- );
50
- const nameArg = args._[0];
51
- const questions = [];
52
- if (!nameArg) {
53
- questions.push({
54
- type: "input",
55
- name: "projectName",
56
- message: "Project name:",
57
- default: "my-rebase-app",
58
- validate: (input) => {
59
- if (!input.trim()) return "Project name is required";
60
- if (!/^[a-z0-9][a-z0-9._-]*$/.test(input)) {
61
- return "Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, hyphens, dots, or underscores";
62
- }
63
- return true;
64
- }
65
- });
66
- }
67
- if (!args["--git"]) {
68
- questions.push({
69
- type: "confirm",
70
- name: "git",
71
- message: "Initialize a git repository?",
72
- default: true
73
- });
74
- }
75
- if (!args["--install"]) {
76
- questions.push({
77
- type: "confirm",
78
- name: "installDeps",
79
- message: "Install dependencies with pnpm?",
80
- default: true
81
- });
82
- }
83
- const answers = await inquirer.prompt(questions);
84
- const projectName = nameArg || answers.projectName;
85
- const targetDirectory = path.resolve(process.cwd(), projectName);
86
- const templateDirectory = path.resolve(cliRoot, "templates", "template");
87
- return {
88
- projectName,
89
- git: args["--git"] || answers.git || false,
90
- installDeps: args["--install"] || answers.installDeps || false,
91
- targetDirectory,
92
- templateDirectory
93
- };
94
- }
95
- async function createProject(options) {
96
- if (fs.existsSync(options.targetDirectory)) {
97
- if (fs.readdirSync(options.targetDirectory).length !== 0) {
98
- console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
99
- process.exit(1);
100
- }
101
- } else {
102
- fs.mkdirSync(options.targetDirectory, { recursive: true });
103
- }
104
- try {
105
- await access(options.templateDirectory, fs.constants.R_OK);
106
- } catch {
107
- console.error(`${chalk.red.bold("ERROR")} Template not found at ${options.templateDirectory}`);
108
- process.exit(1);
109
- }
110
- console.log(chalk.gray(" Copying project files..."));
111
- try {
112
- await copy(options.templateDirectory, options.targetDirectory, {
113
- clobber: false,
114
- dot: true,
115
- filter: (source) => {
116
- const basename = path.basename(source);
117
- return basename !== "node_modules" && basename !== ".DS_Store";
118
- }
119
- });
120
- } catch (err) {
121
- console.error(`${chalk.red.bold("ERROR")} Failed to copy template files: ${err instanceof Error ? err.message : String(err)}`);
122
- process.exit(1);
123
- }
124
- await replacePlaceholders(options);
125
- const envTemplatePath = path.join(options.targetDirectory, ".env.template");
126
- const envPath = path.join(options.targetDirectory, ".env");
127
- if (fs.existsSync(envTemplatePath) && !fs.existsSync(envPath)) {
128
- fs.renameSync(envTemplatePath, envPath);
129
- const jwtSecret = crypto.randomBytes(32).toString("hex");
130
- const dbPassword = crypto.randomBytes(16).toString("hex");
131
- let envContent = fs.readFileSync(envPath, "utf-8");
132
- envContent = envContent.replace(
133
- "postgresql://rebase:password@localhost:5432/rebase",
134
- `postgresql://rebase:${dbPassword}@localhost:5432/rebase`
135
- );
136
- envContent = envContent.replace(
137
- "change-this-to-a-secure-random-string",
138
- jwtSecret
139
- );
140
- envContent += `
141
- # Docker Compose Database Password
142
- POSTGRES_PASSWORD=${dbPassword}
143
- `;
144
- fs.writeFileSync(envPath, envContent, "utf-8");
145
- }
146
- if (options.git) {
147
- console.log(chalk.gray(" Initializing git repository..."));
148
- try {
149
- await execa("git", ["init"], { cwd: options.targetDirectory });
150
- } catch {
151
- console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
152
- }
153
- }
154
- if (options.installDeps) {
155
- console.log("");
156
- console.log(chalk.gray(" Installing dependencies with pnpm..."));
157
- console.log("");
158
- try {
159
- await execa("pnpm", ["install"], {
160
- cwd: options.targetDirectory,
161
- stdio: "inherit"
162
- });
163
- } catch {
164
- console.warn(chalk.yellow(" Warning: Failed to install dependencies. You may need to run `pnpm install` manually."));
165
- }
166
- }
167
- console.log("");
168
- console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
169
- console.log("");
170
- console.log(chalk.bold("Next steps:"));
171
- console.log("");
172
- console.log(` ${chalk.cyan("cd")} ${options.projectName}`);
173
- if (!options.installDeps) {
174
- console.log(` ${chalk.cyan("pnpm install")}`);
175
- }
176
- console.log("");
177
- console.log(chalk.gray(" # Set up your database connection in .env"));
178
- console.log(chalk.gray(" # Then run:"));
179
- console.log("");
180
- console.log(` ${chalk.cyan("pnpm dev")}`);
181
- console.log("");
182
- console.log(chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
183
- console.log("");
184
- console.log(chalk.gray("Docs: https://rebase.pro/docs"));
185
- console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
186
- console.log("");
242
+ await createProject$1(await promptForOptions(rawArgs, detectPackageManager()));
243
+ }
244
+ async function promptForOptions(rawArgs, pm) {
245
+ const args = arg({
246
+ "--git": Boolean,
247
+ "--install": Boolean,
248
+ "--database-url": String,
249
+ "--introspect": Boolean,
250
+ "--template": String,
251
+ "--flavor": String,
252
+ "--yes": Boolean,
253
+ "-g": "--git",
254
+ "-i": "--install",
255
+ "-t": "--template",
256
+ "-f": "--flavor",
257
+ "-y": "--yes"
258
+ }, {
259
+ argv: rawArgs.slice(3),
260
+ permissive: true
261
+ });
262
+ const nameArg = args._[0];
263
+ const isNonInteractive = args["--yes"] || false;
264
+ const templateArg = args["--template"];
265
+ if (templateArg && !PRESET_CHOICES.some((p) => p.value === templateArg)) {
266
+ console.error(chalk.red(`Unknown template "${templateArg}". Available: ${PRESET_CHOICES.map((p) => p.value).join(", ")}`));
267
+ process.exit(1);
268
+ }
269
+ const flavorArg = args["--flavor"];
270
+ if (flavorArg && !FLAVOR_CHOICES.some((f) => f.value === flavorArg)) {
271
+ console.error(chalk.red(`Unknown flavor "${flavorArg}". Available: ${FLAVOR_CHOICES.map((f) => f.value).join(", ")}`));
272
+ process.exit(1);
273
+ }
274
+ if (isNonInteractive) {
275
+ const projectName = nameArg || "my-rebase-app";
276
+ const targetDirectory = path.resolve(process.cwd(), projectName);
277
+ const templateDirectory = path.resolve(cliRoot, "templates", "template");
278
+ const pmCommands = getPMCommands(pm);
279
+ return {
280
+ projectName: path.basename(targetDirectory),
281
+ git: args["--git"] ?? false,
282
+ installDeps: args["--install"] ?? false,
283
+ targetDirectory,
284
+ templateDirectory,
285
+ databaseUrl: args["--database-url"] || void 0,
286
+ introspect: args["--introspect"] || false,
287
+ preset: templateArg || "blog",
288
+ flavor: flavorArg || "cms",
289
+ pm,
290
+ pmCommands
291
+ };
292
+ }
293
+ const questions = buildInitQuestions({
294
+ nameArg,
295
+ templateArg,
296
+ flavorArg,
297
+ hasGitFlag: !!args["--git"],
298
+ hasInstallFlag: !!args["--install"],
299
+ pm
300
+ });
301
+ const answers = await inquirer.prompt(questions);
302
+ const targetDirectory = path.resolve(process.cwd(), nameArg || answers.projectName);
303
+ const projectName = path.basename(targetDirectory);
304
+ const templateDirectory = path.resolve(cliRoot, "templates", "template");
305
+ const pmCommands = getPMCommands(pm);
306
+ return {
307
+ projectName,
308
+ git: args["--git"] || answers.git || false,
309
+ installDeps: args["--install"] || answers.installDeps || false,
310
+ targetDirectory,
311
+ templateDirectory,
312
+ databaseUrl: answers.databaseUrl?.trim() || void 0,
313
+ introspect: answers.introspect || false,
314
+ preset: templateArg || answers.preset || "blog",
315
+ flavor: flavorArg || answers.flavor || "cms",
316
+ pm,
317
+ pmCommands
318
+ };
319
+ }
320
+ async function createProject$1(options) {
321
+ if (fs.existsSync(options.targetDirectory)) {
322
+ if (fs.readdirSync(options.targetDirectory).length !== 0) {
323
+ console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
324
+ process.exit(1);
325
+ }
326
+ } else fs.mkdirSync(options.targetDirectory, { recursive: true });
327
+ try {
328
+ await access(options.templateDirectory, fs.constants.R_OK);
329
+ } catch {
330
+ console.error(`${chalk.red.bold("ERROR")} Template not found at ${options.templateDirectory}`);
331
+ process.exit(1);
332
+ }
333
+ console.log(chalk.gray(" Copying project files..."));
334
+ try {
335
+ await cp(options.templateDirectory, options.targetDirectory, {
336
+ recursive: true,
337
+ filter: (source) => {
338
+ const basename = path.basename(source);
339
+ return basename !== "node_modules" && basename !== ".DS_Store";
340
+ }
341
+ });
342
+ } catch (err) {
343
+ console.error(`${chalk.red.bold("ERROR")} Failed to copy template files: ${err instanceof Error ? err.message : String(err)}`);
344
+ process.exit(1);
345
+ }
346
+ if (options.flavor !== "baas") await applyPreset(options.targetDirectory, options.preset);
347
+ await applyFlavor(options.targetDirectory, options.flavor);
348
+ await replacePlaceholders(options);
349
+ await configureEnvFile(options.targetDirectory, options.databaseUrl);
350
+ if (options.git) {
351
+ console.log(chalk.gray(" Initializing git repository..."));
352
+ try {
353
+ await execa("git", ["init"], { cwd: options.targetDirectory });
354
+ } catch {
355
+ console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
356
+ }
357
+ }
358
+ const { pm, pmCommands } = options;
359
+ const installCmd = pmCommands.install;
360
+ const execCmd = pmCommands.exec("rebase", [
361
+ "schema",
362
+ "introspect",
363
+ "--force"
364
+ ]);
365
+ if (options.installDeps) {
366
+ console.log("");
367
+ console.log(chalk.gray(` Installing dependencies with ${pm}...`));
368
+ console.log("");
369
+ try {
370
+ await execa(installCmd[0], installCmd.slice(1), {
371
+ cwd: options.targetDirectory,
372
+ stdio: "inherit"
373
+ });
374
+ } catch {
375
+ console.warn(chalk.yellow(` Warning: Failed to install dependencies. You may need to run \`${installCmd.join(" ")}\` manually.`));
376
+ }
377
+ }
378
+ if (options.introspect) {
379
+ console.log("");
380
+ if (options.installDeps) {
381
+ console.log(chalk.gray(" Introspecting database and generating collections..."));
382
+ console.log("");
383
+ try {
384
+ await execa(execCmd[0], execCmd.slice(1), {
385
+ cwd: options.targetDirectory,
386
+ stdio: "inherit"
387
+ });
388
+ console.log(chalk.green(" Database successfully introspected!"));
389
+ } catch {
390
+ console.warn(chalk.yellow(" Warning: Failed to introspect database automatically."));
391
+ console.warn(chalk.yellow(` You can run \`${execCmd.join(" ")}\` manually after setup.`));
392
+ }
393
+ } else {
394
+ console.warn(chalk.yellow(" Skipping introspection because dependencies were not installed."));
395
+ console.warn(chalk.yellow(` Run \`${installCmd.join(" ")}\` then \`${execCmd.join(" ")}\` manually.`));
396
+ }
397
+ }
398
+ console.log("");
399
+ console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
400
+ console.log("");
401
+ console.log(chalk.bold("Next steps:"));
402
+ console.log("");
403
+ const runDev = pmCommands.run("dev");
404
+ const runDbPush = pmCommands.run("db:push");
405
+ const isBaas = options.flavor === "baas";
406
+ console.log(` ${chalk.cyan("cd")} ${options.projectName}`);
407
+ if (!options.installDeps) console.log(` ${chalk.cyan(installCmd.join(" "))}`);
408
+ console.log("");
409
+ if (options.databaseUrl) if (options.introspect) {
410
+ console.log(chalk.gray(" # Database has been introspected & collections generated!"));
411
+ console.log(chalk.gray(" # Start the development server (frontend + backend):"));
412
+ console.log(` ${chalk.cyan(runDev.join(" "))}`);
413
+ } else {
414
+ console.log(chalk.gray(" # Your custom database is configured in .env."));
415
+ console.log(chalk.gray(" # If the database is empty, push the Rebase schema to initialize it:"));
416
+ console.log(` ${chalk.cyan(runDbPush.join(" "))}`);
417
+ console.log("");
418
+ console.log(chalk.gray(" # Then start the development server:"));
419
+ console.log(` ${chalk.cyan(runDev.join(" "))}`);
420
+ }
421
+ else if (isBaas) {
422
+ console.log(chalk.gray(" # A local database configuration has been generated in .env."));
423
+ console.log(chalk.gray(" # 1. Start the PostgreSQL database container:"));
424
+ console.log(` ${chalk.cyan("docker compose up -d db")}`);
425
+ console.log("");
426
+ console.log(chalk.gray(" # 2. Create your tables (migrations, SQL, any tool you like)."));
427
+ console.log("");
428
+ console.log(chalk.gray(" # 3. Start the API — every table is served automatically:"));
429
+ console.log(` ${chalk.cyan(runDev.join(" "))}`);
430
+ } else {
431
+ console.log(chalk.gray(" # A local database configuration has been generated in .env."));
432
+ console.log(chalk.gray(" # 1. Start the PostgreSQL database container:"));
433
+ console.log(` ${chalk.cyan("docker compose up -d db")}`);
434
+ console.log("");
435
+ console.log(chalk.gray(" # 2. Push the Rebase schema to initialize database tables:"));
436
+ console.log(` ${chalk.cyan(runDbPush.join(" "))}`);
437
+ console.log("");
438
+ console.log(chalk.gray(" # 3. Start the development server (frontend + backend):"));
439
+ console.log(` ${chalk.cyan(runDev.join(" "))}`);
440
+ }
441
+ console.log("");
442
+ console.log(isBaas ? chalk.gray("This starts a headless API (Hono + PostgreSQL). There are no collection files: ") + chalk.gray("the API is derived from your database schema. Docs at /api/swagger.") : chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
443
+ console.log("");
444
+ console.log(chalk.gray("Docs: https://rebase.pro/docs"));
445
+ console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
446
+ console.log("");
447
+ console.log(chalk.bold("🤖 AI Agent Skills"));
448
+ console.log("");
449
+ console.log(chalk.gray(" Install Rebase agent skills for your AI coding assistant:"));
450
+ console.log("");
451
+ console.log(` ${chalk.cyan("rebase skills install")} ${chalk.gray("or")} ${chalk.cyan(pmCommands.run("skills:install").join(" "))}`);
452
+ console.log("");
453
+ }
454
+ /**
455
+ * Apply a template preset by replacing the default collection files.
456
+ *
457
+ * The template ships with blog collections at the top level and
458
+ * preset alternatives under `config/collections/presets/<name>/`.
459
+ * This function swaps the active collection files and removes the
460
+ * presets directory so the final project is clean.
461
+ */
462
+ /**
463
+ * Reduce the scaffolded project to the chosen flavor.
464
+ *
465
+ * The base template is the full CMS triad. `baas` drops the frontend and the
466
+ * collections config entirely — there is nothing to define, since the server
467
+ * derives its API from the database — and overlays the files that differ.
468
+ */
469
+ async function applyFlavor(targetDirectory, flavor) {
470
+ if (flavor !== "baas") return;
471
+ for (const dir of ["frontend", "config"]) fs.rmSync(path.join(targetDirectory, dir), {
472
+ recursive: true,
473
+ force: true
474
+ });
475
+ fs.rmSync(path.join(targetDirectory, "backend", "src", "schema.generated.ts"), { force: true });
476
+ const overlayDir = path.resolve(cliRoot, "templates", "overlays", "baas");
477
+ if (!fs.existsSync(overlayDir)) {
478
+ console.error(`${chalk.red.bold("ERROR")} BaaS template overlay not found at ${overlayDir}`);
479
+ process.exit(1);
480
+ }
481
+ await cp(overlayDir, targetDirectory, {
482
+ recursive: true,
483
+ force: true,
484
+ filter: (source) => {
485
+ const basename = path.basename(source);
486
+ return basename !== "node_modules" && basename !== ".DS_Store";
487
+ }
488
+ });
489
+ }
490
+ async function applyPreset(targetDirectory, preset) {
491
+ const collectionsDir = path.join(targetDirectory, "config", "collections");
492
+ const presetsDir = path.join(collectionsDir, "presets");
493
+ if (preset !== "blog") {
494
+ const presetDir = path.join(presetsDir, preset);
495
+ if (!fs.existsSync(presetDir)) {
496
+ console.warn(chalk.yellow(` Warning: Preset "${preset}" not found, falling back to blog template.`));
497
+ cleanupPresets(presetsDir);
498
+ return;
499
+ }
500
+ for (const file of [
501
+ "posts.ts",
502
+ "authors.ts",
503
+ "tags.ts",
504
+ "index.ts"
505
+ ]) {
506
+ const filePath = path.join(collectionsDir, file);
507
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
508
+ }
509
+ const presetFiles = fs.readdirSync(presetDir).filter((f) => f.endsWith(".ts"));
510
+ for (const file of presetFiles) fs.copyFileSync(path.join(presetDir, file), path.join(collectionsDir, file));
511
+ }
512
+ cleanupPresets(presetsDir);
513
+ }
514
+ function cleanupPresets(presetsDir) {
515
+ if (fs.existsSync(presetsDir)) fs.rmSync(presetsDir, {
516
+ recursive: true,
517
+ force: true
518
+ });
187
519
  }
188
520
  async function replacePlaceholders(options) {
189
- const filesToProcess = [
190
- "package.json",
191
- "frontend/package.json",
192
- "backend/package.json",
193
- "config/package.json",
194
- "frontend/index.html"
195
- ];
196
- for (const file of filesToProcess) {
197
- const fullPath = path.resolve(options.targetDirectory, file);
198
- if (!fs.existsSync(fullPath)) continue;
199
- let content = fs.readFileSync(fullPath, "utf-8");
200
- content = content.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
201
- fs.writeFileSync(fullPath, content, "utf-8");
202
- }
521
+ const filesToProcess = [
522
+ "package.json",
523
+ "frontend/package.json",
524
+ "backend/package.json",
525
+ "config/package.json",
526
+ "frontend/index.html",
527
+ "pnpm-workspace.yaml",
528
+ "README.md"
529
+ ];
530
+ const packageJsonPath = path.resolve(cliRoot, "package.json");
531
+ let cliVersion = "latest";
532
+ if (fs.existsSync(packageJsonPath)) cliVersion = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).version || "latest";
533
+ const versionCache = /* @__PURE__ */ new Map();
534
+ const viewBin = "npm";
535
+ const getPackageVersion = async (pkgName) => {
536
+ if (versionCache.has(pkgName)) return versionCache.get(pkgName);
537
+ if (process.env.REBASE_E2E === "true") {
538
+ versionCache.set(pkgName, cliVersion);
539
+ return cliVersion;
540
+ }
541
+ let versionToUse = cliVersion;
542
+ try {
543
+ const { stdout } = await execa(viewBin, [
544
+ "view",
545
+ `${pkgName}@${cliVersion}`,
546
+ "version"
547
+ ]);
548
+ if (!stdout.trim()) throw new Error("Not found");
549
+ versionToUse = stdout.trim();
550
+ } catch {
551
+ try {
552
+ const { stdout } = await execa(viewBin, [
553
+ "view",
554
+ `${pkgName}@${cliVersion.includes("canary") ? "canary" : "latest"}`,
555
+ "version"
556
+ ]);
557
+ if (!stdout.trim()) throw new Error("Not found");
558
+ versionToUse = stdout.trim();
559
+ } catch {
560
+ try {
561
+ const { stdout } = await execa(viewBin, [
562
+ "view",
563
+ pkgName,
564
+ "version"
565
+ ]);
566
+ versionToUse = stdout.trim() || "latest";
567
+ } catch {
568
+ versionToUse = "latest";
569
+ }
570
+ }
571
+ }
572
+ versionCache.set(pkgName, versionToUse);
573
+ return versionToUse;
574
+ };
575
+ const allPackages = /* @__PURE__ */ new Set();
576
+ const fileContents = /* @__PURE__ */ new Map();
577
+ for (const file of filesToProcess) {
578
+ const fullPath = path.resolve(options.targetDirectory, file);
579
+ if (!fs.existsSync(fullPath)) continue;
580
+ const content = fs.readFileSync(fullPath, "utf-8");
581
+ fileContents.set(fullPath, content);
582
+ const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
583
+ for (const match of matches) allPackages.add(match[1]);
584
+ }
585
+ console.log(chalk.gray(" Resolving package versions..."));
586
+ await Promise.all(Array.from(allPackages).map(getPackageVersion));
587
+ for (const [fullPath, originalContent] of fileContents.entries()) {
588
+ let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
589
+ const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
590
+ for (const match of matches) {
591
+ const pkgName = match[1];
592
+ const resolvedVersion = versionCache.get(pkgName) || "latest";
593
+ content = content.replace(new RegExp(`"${pkgName}":\\s*"workspace:\\*"`, "g"), `"${pkgName}": "${resolvedVersion}"`);
594
+ }
595
+ fs.writeFileSync(fullPath, content, "utf-8");
596
+ }
597
+ }
598
+ async function isPortAvailable(port) {
599
+ return new Promise((resolve) => {
600
+ const server = net.createServer();
601
+ server.once("error", () => {
602
+ resolve(false);
603
+ });
604
+ server.once("listening", () => {
605
+ server.close(() => resolve(true));
606
+ });
607
+ server.listen(port);
608
+ });
203
609
  }
610
+ async function findAvailablePort(startPort) {
611
+ let port = startPort;
612
+ while (!await isPortAvailable(port)) port++;
613
+ return port;
614
+ }
615
+ async function configureEnvFile(targetDirectory, databaseUrl) {
616
+ const envExamplePath = path.join(targetDirectory, ".env.example");
617
+ const envPath = path.join(targetDirectory, ".env");
618
+ if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
619
+ fs.copyFileSync(envExamplePath, envPath);
620
+ const jwtSecret = crypto.randomBytes(32).toString("hex");
621
+ const dbPassword = crypto.randomBytes(16).toString("hex");
622
+ let envContent = fs.readFileSync(envPath, "utf-8");
623
+ envContent = envContent.replace(/^JWT_SECRET=.*$/m, `JWT_SECRET=${jwtSecret}`);
624
+ if (databaseUrl) {
625
+ if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
626
+ envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${databaseUrl}`);
627
+ } else {
628
+ const dbPort = await findAvailablePort(5432);
629
+ envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public\nDATABASE_PASSWORD=${dbPassword}`);
630
+ const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
631
+ if (fs.existsSync(dockerComposePath)) {
632
+ let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
633
+ dockerComposeContent = dockerComposeContent.replace(/-\s*"5432:5432"/g, `- "${dbPort}:5432"`);
634
+ fs.writeFileSync(dockerComposePath, dockerComposeContent, "utf-8");
635
+ }
636
+ }
637
+ fs.writeFileSync(envPath, envContent, "utf-8");
638
+ }
639
+ }
640
+ //#endregion
641
+ //#region src/commands/generate_sdk.ts
642
+ /**
643
+ * CLI command: generate-sdk
644
+ *
645
+ * Reads collection definitions from a specified directory (default: ./config/collections),
646
+ * generates a typed TypeScript SDK, and writes it to the output directory (default: ./generated/sdk).
647
+ *
648
+ * Uses jiti for dynamic TypeScript import of collection files.
649
+ */
650
+ /**
651
+ * Dynamically load collection definitions from a directory.
652
+ *
653
+ * Expects the directory to have an index.ts/index.js that exports a default
654
+ * array of CollectionConfig objects (matching the app/config/collections pattern).
655
+ */
204
656
  async function loadCollections(collectionsDir) {
205
- const absDir = path.resolve(collectionsDir);
206
- if (!fs.existsSync(absDir)) {
207
- throw new Error(`Collections directory not found: ${absDir}`);
208
- }
209
- let jiti;
210
- try {
211
- const jitiModule = await import("jiti");
212
- jiti = jitiModule.default || jitiModule;
213
- } catch {
214
- throw new Error(
215
- "Could not load 'jiti'. Install it with: pnpm add -D jiti\njiti is required to dynamically import TypeScript collection definitions."
216
- );
217
- }
218
- const jitiInstance = jiti(absDir, {
219
- interopDefault: true,
220
- esmResolve: true
221
- });
222
- const indexCandidates = ["index.ts", "index.js", "index.mjs"];
223
- let indexPath = null;
224
- for (const candidate of indexCandidates) {
225
- const p = path.join(absDir, candidate);
226
- if (fs.existsSync(p)) {
227
- indexPath = p;
228
- break;
229
- }
230
- }
231
- if (!indexPath) {
232
- console.log(chalk.yellow(" No index file found, scanning individual collection files..."));
233
- const collections = [];
234
- const files = fs.readdirSync(absDir).filter(
235
- (f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith(".")
236
- );
237
- for (const file of files) {
238
- try {
239
- const mod2 = jitiInstance(path.join(absDir, file));
240
- const exported2 = mod2.default || mod2;
241
- if (exported2 && typeof exported2 === "object" && "slug" in exported2) {
242
- collections.push(exported2);
243
- } else if (Array.isArray(exported2)) {
244
- collections.push(...exported2);
245
- }
246
- } catch (err) {
247
- console.warn(chalk.yellow(` ⚠ Skipping ${file}: ${err.message}`));
248
- }
249
- }
250
- return collections;
251
- }
252
- const mod = jitiInstance(indexPath);
253
- const exported = mod.default || mod;
254
- if (Array.isArray(exported)) {
255
- return exported;
256
- } else if (typeof exported === "object" && exported !== null) {
257
- if ("collections" in exported && Array.isArray(exported.collections)) {
258
- return exported.collections;
259
- }
260
- const collections = [];
261
- for (const value of Object.values(exported)) {
262
- if (value && typeof value === "object" && "slug" in value) {
263
- collections.push(value);
264
- }
265
- }
266
- if (collections.length > 0) return collections;
267
- }
268
- throw new Error(
269
- `Could not extract collections from ${indexPath}.
270
- Expected a default export of EntityCollection[] or an object with named collection exports.`
271
- );
657
+ const absDir = path.resolve(collectionsDir);
658
+ if (!fs.existsSync(absDir)) throw new Error(`Collections directory not found: ${absDir}`);
659
+ let jiti;
660
+ try {
661
+ const jitiModule = await import("jiti");
662
+ jiti = jitiModule.default || jitiModule;
663
+ } catch {
664
+ const installCmd = [
665
+ ...getPMCommands(detectPackageManager()).install,
666
+ "-D",
667
+ "jiti"
668
+ ].join(" ");
669
+ throw new Error(`Could not load 'jiti'. Install it with: ${installCmd}\njiti is required to dynamically import TypeScript collection definitions.`);
670
+ }
671
+ const jitiInstance = jiti(absDir, {
672
+ interopDefault: true,
673
+ esmResolve: true
674
+ });
675
+ const indexCandidates = [
676
+ "index.ts",
677
+ "index.js",
678
+ "index.mjs"
679
+ ];
680
+ let indexPath = null;
681
+ for (const candidate of indexCandidates) {
682
+ const p = path.join(absDir, candidate);
683
+ if (fs.existsSync(p)) {
684
+ indexPath = p;
685
+ break;
686
+ }
687
+ }
688
+ if (!indexPath) {
689
+ console.log(chalk.yellow(" No index file found, scanning individual collection files..."));
690
+ const collections = [];
691
+ const files = fs.readdirSync(absDir).filter((f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith("."));
692
+ for (const file of files) try {
693
+ const mod = jitiInstance(path.join(absDir, file));
694
+ const exported = mod.default || mod;
695
+ if (exported && typeof exported === "object" && "slug" in exported) collections.push(exported);
696
+ else if (Array.isArray(exported)) collections.push(...exported);
697
+ } catch (err) {
698
+ console.warn(chalk.yellow(` ⚠ Skipping ${file}: ${err.message}`));
699
+ }
700
+ return collections;
701
+ }
702
+ const mod = jitiInstance(indexPath);
703
+ const exported = mod.default || mod;
704
+ if (Array.isArray(exported)) return exported;
705
+ else if (typeof exported === "object" && exported !== null) {
706
+ if ("collections" in exported && Array.isArray(exported.collections)) return exported.collections;
707
+ const collections = [];
708
+ for (const value of Object.values(exported)) if (value && typeof value === "object" && "slug" in value) collections.push(value);
709
+ if (collections.length > 0) return collections;
710
+ }
711
+ throw new Error(`Could not extract collections from ${indexPath}.\nExpected a default export of CollectionConfig[] or an object with named collection exports.`);
272
712
  }
713
+ /**
714
+ * Write generated files to the output directory.
715
+ */
273
716
  function writeFiles(outputDir, files) {
274
- const absOutput = path.resolve(outputDir);
275
- fs.mkdirSync(absOutput, { recursive: true });
276
- for (const file of files) {
277
- const filePath = path.join(absOutput, file.path);
278
- const dir = path.dirname(filePath);
279
- if (!fs.existsSync(dir)) {
280
- fs.mkdirSync(dir, { recursive: true });
281
- }
282
- fs.writeFileSync(filePath, file.content, "utf-8");
283
- }
717
+ const absOutput = path.resolve(outputDir);
718
+ fs.mkdirSync(absOutput, { recursive: true });
719
+ for (const file of files) {
720
+ const filePath = path.join(absOutput, file.path);
721
+ const dir = path.dirname(filePath);
722
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
723
+ fs.writeFileSync(filePath, file.content, "utf-8");
724
+ }
284
725
  }
726
+ /**
727
+ * Main entry point for the generate-sdk command.
728
+ */
285
729
  async function generateSdkCommand(args) {
286
- const { collectionsDir, output, cwd } = args;
287
- const resolvedCollectionsDir = path.isAbsolute(collectionsDir) ? collectionsDir : path.join(cwd, collectionsDir);
288
- const resolvedOutput = path.isAbsolute(output) ? output : path.join(cwd, output);
289
- console.log("");
290
- console.log(chalk.bold(" 🔧 Rebase SDK Generator"));
291
- console.log("");
292
- console.log(` ${chalk.gray("Collections:")} ${resolvedCollectionsDir}`);
293
- console.log(` ${chalk.gray("Output:")} ${resolvedOutput}`);
294
- console.log("");
295
- console.log(chalk.cyan(" → Loading collection definitions..."));
296
- const collections = await loadCollections(resolvedCollectionsDir);
297
- if (collections.length === 0) {
298
- console.log(chalk.red(" ✗ No collections found. Nothing to generate."));
299
- process.exit(1);
300
- }
301
- console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
302
- console.log("");
303
- console.log(chalk.cyan(" → Generating SDK files..."));
304
- const files = generateSDK(collections);
305
- console.log(chalk.green(` ✓ Generated ${files.length} file(s)`));
306
- console.log(chalk.cyan(` Writing to ${resolvedOutput}...`));
307
- writeFiles(resolvedOutput, files);
308
- console.log("");
309
- console.log(chalk.green.bold(" ✓ SDK generated successfully!"));
310
- console.log("");
311
- console.log(chalk.gray(" Usage:"));
312
- console.log(chalk.gray(" import { createRebaseClient } from '@rebasepro/client';"));
313
- console.log(chalk.gray(` import type { Database } from './${path.relative(cwd, path.join(resolvedOutput, "database.types"))}';`));
314
- console.log("");
315
- console.log(chalk.gray(" const rebase = createRebaseClient<Database>({"));
316
- console.log(chalk.gray(" baseUrl: 'http://localhost:3001',"));
317
- console.log(chalk.gray(" // token: 'your-jwt-token',"));
318
- console.log(chalk.gray(" });"));
319
- console.log("");
320
- console.log(chalk.gray(` const { data } = await rebase.collection('${collections[0]?.slug || "my_collection"}').find();`));
321
- console.log("");
730
+ const { collectionsDir, output, cwd } = args;
731
+ const resolvedCollectionsDir = path.isAbsolute(collectionsDir) ? collectionsDir : path.join(cwd, collectionsDir);
732
+ const resolvedOutput = path.isAbsolute(output) ? output : path.join(cwd, output);
733
+ console.log("");
734
+ console.log(chalk.bold(" 🔧 Rebase SDK Generator"));
735
+ console.log("");
736
+ console.log(` ${chalk.gray("Collections:")} ${resolvedCollectionsDir}`);
737
+ console.log(` ${chalk.gray("Output:")} ${resolvedOutput}`);
738
+ console.log("");
739
+ console.log(chalk.cyan(" → Loading collection definitions..."));
740
+ const collections = await loadCollections(resolvedCollectionsDir);
741
+ collections.sort((a, b) => a.slug.localeCompare(b.slug));
742
+ if (collections.length === 0) {
743
+ console.log(chalk.red(" ✗ No collections found. Nothing to generate."));
744
+ process.exit(1);
745
+ }
746
+ console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
747
+ console.log("");
748
+ console.log(chalk.cyan(" Generating SDK files..."));
749
+ const files = generateSDK(collections);
750
+ console.log(chalk.green(` Generated ${files.length} file(s)`));
751
+ console.log(chalk.cyan(` → Writing to ${resolvedOutput}...`));
752
+ writeFiles(resolvedOutput, files);
753
+ console.log("");
754
+ console.log(chalk.green.bold(" ✓ SDK generated successfully!"));
755
+ console.log("");
756
+ console.log(chalk.gray(" Usage:"));
757
+ console.log(chalk.gray(" import { createRebaseClient } from '@rebasepro/client';"));
758
+ console.log(chalk.gray(` import type { Database } from './${path.relative(cwd, path.join(resolvedOutput, "database.types"))}';`));
759
+ console.log("");
760
+ console.log(chalk.gray(" const rebase = createRebaseClient<Database>({"));
761
+ console.log(chalk.gray(" baseUrl: 'http://localhost:3001',"));
762
+ console.log(chalk.gray(" // token: 'your-jwt-token',"));
763
+ console.log(chalk.gray(" });"));
764
+ console.log("");
765
+ console.log(chalk.gray(` const { data } = await rebase.collection('${collections[0]?.slug || "my_collection"}').find();`));
766
+ console.log("");
322
767
  }
768
+ //#endregion
769
+ //#region src/utils/project.ts
770
+ /**
771
+ * Project discovery utilities for the Rebase CLI.
772
+ *
773
+ * These helpers locate the project root, backend directory, .env file,
774
+ * and local binaries — used by all CLI command modules.
775
+ */
776
+ /**
777
+ * Walk up from `startDir` to find the Rebase project root.
778
+ *
779
+ * The root is identified by a `package.json` that either:
780
+ * - has `workspaces` containing "backend" or "frontend", OR
781
+ * - has a sibling `backend/` directory
782
+ */
323
783
  function findProjectRoot(startDir = process.cwd()) {
324
- let dir = path.resolve(startDir);
325
- const root = path.parse(dir).root;
326
- while (dir !== root) {
327
- const pkgPath = path.join(dir, "package.json");
328
- if (fs.existsSync(pkgPath)) {
329
- try {
330
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
331
- if (pkg.workspaces && Array.isArray(pkg.workspaces)) {
332
- const hasBackend = pkg.workspaces.some(
333
- (w) => w === "backend" || w.includes("backend")
334
- );
335
- if (hasBackend) return dir;
336
- }
337
- } catch {
338
- }
339
- if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) {
340
- return dir;
341
- }
342
- }
343
- dir = path.dirname(dir);
344
- }
345
- return null;
784
+ let dir = path.resolve(startDir);
785
+ const root = path.parse(dir).root;
786
+ while (dir !== root) {
787
+ const pkgPath = path.join(dir, "package.json");
788
+ if (fs.existsSync(pkgPath)) {
789
+ try {
790
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
791
+ if (pkg.workspaces && Array.isArray(pkg.workspaces)) {
792
+ if (pkg.workspaces.some((w) => w === "backend")) return dir;
793
+ }
794
+ } catch {}
795
+ if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) return dir;
796
+ }
797
+ dir = path.dirname(dir);
798
+ }
799
+ return null;
346
800
  }
801
+ /**
802
+ * Locate the backend directory within the project root.
803
+ */
347
804
  function findBackendDir(projectRoot) {
348
- const backendDir = path.join(projectRoot, "backend");
349
- return fs.existsSync(backendDir) ? backendDir : null;
805
+ const backendDir = path.join(projectRoot, "backend");
806
+ return fs.existsSync(backendDir) ? backendDir : null;
350
807
  }
808
+ /**
809
+ * Detect the active backend plugin (e.g. @rebasepro/server-postgres) from the backend's package.json.
810
+ */
351
811
  function getActiveBackendPlugin(backendDir) {
352
- const pkgPath = path.join(backendDir, "package.json");
353
- if (!fs.existsSync(pkgPath)) return null;
354
- try {
355
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
356
- const deps = {
357
- ...pkg.dependencies,
358
- ...pkg.devDependencies
359
- };
360
- for (const dep of Object.keys(deps)) {
361
- if (dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core") {
362
- return dep;
363
- }
364
- }
365
- } catch {
366
- }
367
- return null;
812
+ const pkgPath = path.join(backendDir, "package.json");
813
+ if (!fs.existsSync(pkgPath)) return null;
814
+ try {
815
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
816
+ const deps = {
817
+ ...pkg.dependencies,
818
+ ...pkg.devDependencies
819
+ };
820
+ const candidates = Object.keys(deps).filter((dep) => dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server");
821
+ if (candidates.length === 0) return null;
822
+ if (candidates.includes("@rebasepro/server-postgres")) return "@rebasepro/server-postgres";
823
+ for (const candidate of candidates) if (resolvePluginCliScript(backendDir, candidate)) return candidate;
824
+ return candidates[0];
825
+ } catch {}
826
+ return null;
368
827
  }
828
+ /**
829
+ * Resolve the active plugin's CLI script.
830
+ */
369
831
  function resolvePluginCliScript(backendDir, pluginName) {
370
- const candidates = [
371
- path.join(backendDir, "node_modules", pluginName, "src", "cli.ts"),
372
- path.join(backendDir, "node_modules", pluginName, "dist", "cli.js"),
373
- // For monorepo dev mode:
374
- path.resolve(backendDir, "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
375
- path.resolve(backendDir, "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts")
376
- ];
377
- for (const candidate of candidates) {
378
- if (fs.existsSync(candidate)) return candidate;
379
- }
380
- return null;
832
+ const candidates = [
833
+ path.join(backendDir, "node_modules", pluginName, "src", "cli.ts"),
834
+ path.join(backendDir, "node_modules", pluginName, "dist", "cli.js"),
835
+ path.resolve(backendDir, "..", "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
836
+ path.resolve(backendDir, "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
837
+ path.resolve(backendDir, "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts")
838
+ ];
839
+ for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
840
+ return null;
381
841
  }
842
+ /**
843
+ * Locate the frontend directory within the project root.
844
+ */
382
845
  function findFrontendDir(projectRoot) {
383
- const frontendDir = path.join(projectRoot, "frontend");
384
- return fs.existsSync(frontendDir) ? frontendDir : null;
846
+ const frontendDir = path.join(projectRoot, "frontend");
847
+ return fs.existsSync(frontendDir) ? frontendDir : null;
385
848
  }
849
+ /**
850
+ * Find the .env file. Checks the project root first, then backend.
851
+ */
386
852
  function findEnvFile(projectRoot) {
387
- const candidates = [
388
- path.join(projectRoot, ".env"),
389
- path.join(projectRoot, "backend", ".env")
390
- ];
391
- for (const candidate of candidates) {
392
- if (fs.existsSync(candidate)) return candidate;
393
- }
394
- return null;
853
+ const candidates = [path.join(projectRoot, ".env"), path.join(projectRoot, "backend", ".env")];
854
+ for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
855
+ return null;
395
856
  }
857
+ /**
858
+ * Resolve a binary from the project's node_modules/.bin.
859
+ * Checks backend, root, parent monorepo root, then falls back to PATH.
860
+ */
396
861
  function resolveLocalBin(projectRoot, binName) {
397
- const candidates = [
398
- path.join(projectRoot, "backend", "node_modules", ".bin", binName),
399
- path.join(projectRoot, "node_modules", ".bin", binName)
400
- ];
401
- let parent = path.dirname(projectRoot);
402
- const rootDir = path.parse(parent).root;
403
- while (parent !== rootDir) {
404
- candidates.push(path.join(parent, "node_modules", ".bin", binName));
405
- parent = path.dirname(parent);
406
- }
407
- for (const candidate of candidates) {
408
- if (fs.existsSync(candidate)) return candidate;
409
- }
410
- try {
411
- const globalPath = execSync(`which ${binName}`, { encoding: "utf-8" }).trim();
412
- if (globalPath && fs.existsSync(globalPath)) return globalPath;
413
- } catch {
414
- }
415
- return null;
862
+ const candidates = [path.join(projectRoot, "backend", "node_modules", ".bin", binName), path.join(projectRoot, "node_modules", ".bin", binName)];
863
+ let parent = path.dirname(projectRoot);
864
+ const rootDir = path.parse(parent).root;
865
+ while (parent !== rootDir) {
866
+ candidates.push(path.join(parent, "node_modules", ".bin", binName));
867
+ parent = path.dirname(parent);
868
+ }
869
+ for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
870
+ try {
871
+ const globalPath = execSync(`which ${binName}`, { encoding: "utf-8" }).trim();
872
+ if (globalPath && fs.existsSync(globalPath)) return globalPath;
873
+ } catch {}
874
+ return null;
416
875
  }
876
+ /**
877
+ * Resolve the tsx binary. Checks backend node_modules first, then root.
878
+ */
417
879
  function resolveTsx(projectRoot) {
418
- return resolveLocalBin(projectRoot, "tsx");
880
+ return resolveLocalBin(projectRoot, "tsx");
881
+ }
882
+ /**
883
+ * Validate that a resolved tsx binary actually has an intact installation.
884
+ *
885
+ * `resolveLocalBin` only checks whether `node_modules/.bin/tsx` (a symlink)
886
+ * exists. If the pnpm content-addressable store was cleaned or a previous
887
+ * install was interrupted, the symlink can exist while critical files inside
888
+ * the tsx package (e.g. `dist/preflight.cjs`) are missing — causing a
889
+ * confusing MODULE_NOT_FOUND error at runtime.
890
+ *
891
+ * This function follows the symlink, walks up to find the tsx package root
892
+ * (`package.json` with `name: "tsx"`), and verifies that `dist/preflight.cjs`
893
+ * is present. Returns `null` when the installation looks healthy, or an
894
+ * error description string when it appears corrupted.
895
+ */
896
+ function validateTsxInstallation(tsxBinPath) {
897
+ try {
898
+ const realPath = fs.realpathSync(tsxBinPath);
899
+ let dir = path.dirname(realPath);
900
+ const fsRoot = path.parse(dir).root;
901
+ for (let depth = 0; depth < 10 && dir !== fsRoot; depth++) {
902
+ const pkgPath = path.join(dir, "package.json");
903
+ if (fs.existsSync(pkgPath)) try {
904
+ if (JSON.parse(fs.readFileSync(pkgPath, "utf-8")).name === "tsx") {
905
+ const preflightPath = path.join(dir, "dist", "preflight.cjs");
906
+ if (!fs.existsSync(preflightPath)) return `tsx package at ${dir} is missing dist/preflight.cjs`;
907
+ return null;
908
+ }
909
+ } catch {}
910
+ dir = path.dirname(dir);
911
+ }
912
+ return null;
913
+ } catch (err) {
914
+ return `tsx binary symlink is broken: ${err instanceof Error ? err.message : String(err)}`;
915
+ }
419
916
  }
917
+ /**
918
+ * Require the project root or exit with a helpful error.
919
+ */
420
920
  function requireProjectRoot() {
421
- const root = findProjectRoot();
422
- if (!root) {
423
- console.error(chalk.red("✗ Could not find a Rebase project root."));
424
- console.error(chalk.gray(" Make sure you are inside a Rebase project directory"));
425
- console.error(chalk.gray(" (one with backend/, frontend/, and shared/ directories)."));
426
- process.exit(1);
427
- }
428
- return root;
921
+ const root = findProjectRoot();
922
+ if (!root) {
923
+ console.error(chalk.red("✗ Could not find a Rebase project root."));
924
+ console.error(chalk.gray(" Make sure you are inside a Rebase project directory"));
925
+ console.error(chalk.gray(" (one with backend/, frontend/, and config/ directories)."));
926
+ process.exit(1);
927
+ }
928
+ return root;
429
929
  }
930
+ /**
931
+ * Require the backend directory or exit with a helpful error.
932
+ */
430
933
  function requireBackendDir(projectRoot) {
431
- const backendDir = findBackendDir(projectRoot);
432
- if (!backendDir) {
433
- console.error(chalk.red("✗ Could not find a backend/ directory."));
434
- console.error(chalk.gray(` Expected at: ${path.join(projectRoot, "backend")}`));
435
- process.exit(1);
436
- }
437
- return backendDir;
934
+ const backendDir = findBackendDir(projectRoot);
935
+ if (!backendDir) {
936
+ console.error(chalk.red("✗ Could not find a backend/ directory."));
937
+ console.error(chalk.gray(` Expected at: ${path.join(projectRoot, "backend")}`));
938
+ process.exit(1);
939
+ }
940
+ return backendDir;
438
941
  }
942
+ //#endregion
943
+ //#region src/commands/schema.ts
944
+ /**
945
+ * CLI command: rebase schema <action>
946
+ */
439
947
  async function schemaCommand(subcommand, rawArgs) {
440
- if (!subcommand || subcommand === "--help") {
441
- printSchemaHelp();
442
- return;
443
- }
444
- const projectRoot = requireProjectRoot();
445
- const backendDir = requireBackendDir(projectRoot);
446
- const activePlugin = getActiveBackendPlugin(backendDir);
447
- if (!activePlugin) {
448
- console.error(chalk.red("✗ Could not detect an active database plugin."));
449
- console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
450
- process.exit(1);
451
- }
452
- const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
453
- if (!pluginCli) {
454
- console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
455
- process.exit(1);
456
- }
457
- const envFile = findEnvFile(projectRoot);
458
- const env = { ...process.env };
459
- if (envFile) {
460
- env.DOTENV_CONFIG_PATH = envFile;
461
- }
462
- try {
463
- const isTs = pluginCli.endsWith(".ts");
464
- if (isTs) {
465
- const tsxBin = resolveTsx(projectRoot);
466
- if (!tsxBin) {
467
- console.error(chalk.red("✗ Could not find tsx binary."));
468
- process.exit(1);
469
- }
470
- await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
471
- cwd: backendDir,
472
- stdio: "inherit",
473
- env
474
- });
475
- } else {
476
- await execa("node", [pluginCli, ...rawArgs.slice(2)], {
477
- cwd: backendDir,
478
- stdio: "inherit",
479
- env
480
- });
481
- }
482
- } catch (err) {
483
- process.exit(1);
484
- }
948
+ if (!subcommand || subcommand === "--help") {
949
+ printSchemaHelp();
950
+ return;
951
+ }
952
+ const projectRoot = requireProjectRoot();
953
+ const backendDir = requireBackendDir(projectRoot);
954
+ const activePlugin = getActiveBackendPlugin(backendDir);
955
+ if (!activePlugin) {
956
+ console.error(chalk.red("✗ Could not detect an active database plugin."));
957
+ console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json."));
958
+ process.exit(1);
959
+ }
960
+ const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
961
+ if (!pluginCli) {
962
+ console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
963
+ process.exit(1);
964
+ }
965
+ const envFile = findEnvFile(projectRoot);
966
+ const env = { ...process.env };
967
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
968
+ try {
969
+ if (pluginCli.endsWith(".ts")) {
970
+ const tsxBin = resolveTsx(projectRoot);
971
+ if (!tsxBin) {
972
+ console.error(chalk.red("✗ Could not find tsx binary."));
973
+ process.exit(1);
974
+ }
975
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
976
+ cwd: backendDir,
977
+ stdio: "inherit",
978
+ env
979
+ });
980
+ } else await execa("node", [pluginCli, ...rawArgs.slice(2)], {
981
+ cwd: backendDir,
982
+ stdio: "inherit",
983
+ env
984
+ });
985
+ } catch {
986
+ process.exit(1);
987
+ }
485
988
  }
486
989
  function printSchemaHelp() {
487
- console.log(`
990
+ console.log(`
488
991
  ${chalk.bold("rebase schema")} — Schema management commands
489
992
 
490
993
  ${chalk.green.bold("Usage")}
@@ -493,62 +996,66 @@ ${chalk.green.bold("Usage")}
493
996
  ${chalk.green.bold("Commands")}
494
997
  ${chalk.gray("(Commands are provided by your active database driver plugin)")}
495
998
  ${chalk.blue.bold("generate")} Generate Schema from collection definitions
999
+ ${chalk.blue.bold("introspect")} Introspect an existing database to generate collection definitions
496
1000
 
497
1001
  ${chalk.green.bold("generate Options")}
498
1002
  ${chalk.blue("--collections, -c")} Path to collections directory
499
1003
  ${chalk.blue("--output, -o")} Output path for generated schema
500
1004
  ${chalk.blue("--watch, -w")} Watch for changes and regenerate automatically
1005
+
1006
+ ${chalk.green.bold("introspect Options")}
1007
+ ${chalk.blue("--output, -o")} Output directory for generated collection files
501
1008
  `);
502
1009
  }
1010
+ //#endregion
1011
+ //#region src/commands/db.ts
1012
+ /**
1013
+ * CLI command: rebase db <action>
1014
+ */
503
1015
  async function dbCommand(subcommand, rawArgs) {
504
- if (!subcommand || subcommand === "--help") {
505
- printDbHelp();
506
- return;
507
- }
508
- const projectRoot = requireProjectRoot();
509
- const backendDir = requireBackendDir(projectRoot);
510
- const activePlugin = getActiveBackendPlugin(backendDir);
511
- if (!activePlugin) {
512
- console.error(chalk.red("✗ Could not detect an active database plugin."));
513
- console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
514
- process.exit(1);
515
- }
516
- const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
517
- if (!pluginCli) {
518
- console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
519
- process.exit(1);
520
- }
521
- const envFile = findEnvFile(projectRoot);
522
- const env = { ...process.env };
523
- if (envFile) {
524
- env.DOTENV_CONFIG_PATH = envFile;
525
- }
526
- try {
527
- const isTs = pluginCli.endsWith(".ts");
528
- if (isTs) {
529
- const tsxBin = resolveTsx(projectRoot);
530
- if (!tsxBin) {
531
- console.error(chalk.red("✗ Could not find tsx binary."));
532
- process.exit(1);
533
- }
534
- await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
535
- cwd: backendDir,
536
- stdio: "inherit",
537
- env
538
- });
539
- } else {
540
- await execa("node", [pluginCli, ...rawArgs.slice(2)], {
541
- cwd: backendDir,
542
- stdio: "inherit",
543
- env
544
- });
545
- }
546
- } catch (err) {
547
- process.exit(1);
548
- }
1016
+ if (!subcommand || subcommand === "--help") {
1017
+ printDbHelp$1();
1018
+ return;
1019
+ }
1020
+ const projectRoot = requireProjectRoot();
1021
+ const backendDir = requireBackendDir(projectRoot);
1022
+ const activePlugin = getActiveBackendPlugin(backendDir);
1023
+ if (!activePlugin) {
1024
+ console.error(chalk.red("✗ Could not detect an active database plugin."));
1025
+ console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json."));
1026
+ process.exit(1);
1027
+ }
1028
+ const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
1029
+ if (!pluginCli) {
1030
+ console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
1031
+ process.exit(1);
1032
+ }
1033
+ const envFile = findEnvFile(projectRoot);
1034
+ const env = { ...process.env };
1035
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1036
+ try {
1037
+ if (pluginCli.endsWith(".ts")) {
1038
+ const tsxBin = resolveTsx(projectRoot);
1039
+ if (!tsxBin) {
1040
+ console.error(chalk.red("✗ Could not find tsx binary."));
1041
+ process.exit(1);
1042
+ }
1043
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
1044
+ cwd: backendDir,
1045
+ stdio: "inherit",
1046
+ env
1047
+ });
1048
+ } else await execa("node", [pluginCli, ...rawArgs.slice(2)], {
1049
+ cwd: backendDir,
1050
+ stdio: "inherit",
1051
+ env
1052
+ });
1053
+ } catch {
1054
+ process.exit(1);
1055
+ }
549
1056
  }
550
- function printDbHelp() {
551
- console.log(`
1057
+ function printDbHelp$1() {
1058
+ console.log(`
552
1059
  ${chalk.bold("rebase db")} — Database management commands
553
1060
 
554
1061
  ${chalk.green.bold("Usage")}
@@ -557,11 +1064,12 @@ ${chalk.green.bold("Usage")}
557
1064
  ${chalk.green.bold("Commands")}
558
1065
  ${chalk.gray("(Commands are provided by your active database driver plugin)")}
559
1066
  ${chalk.blue.bold("push")} Apply schema directly to database (development)
560
- ${chalk.blue.bold("pull")} Introspect database → Schema
561
1067
  ${chalk.blue.bold("generate")} Generate migration files
562
1068
  ${chalk.blue.bold("migrate")} Run pending migrations
563
- ${chalk.blue.bold("studio")} Open Studio viewer
564
1069
  ${chalk.blue.bold("branch")} Database branching (create, list, delete, info)
1070
+ ${chalk.blue.bold("backup")} Create a backup with pg_dump (--out <path|s3://…>)
1071
+ ${chalk.blue.bold("restore")} Restore a backup with pg_restore (destructive; needs --yes)
1072
+ ${chalk.blue.bold("backups")} List stored backups (backups list)
565
1073
 
566
1074
  ${chalk.green.bold("Examples")}
567
1075
  ${chalk.gray("# Quick development workflow")}
@@ -573,236 +1081,388 @@ ${chalk.green.bold("Examples")}
573
1081
 
574
1082
  ${chalk.gray("# Create a database branch")}
575
1083
  rebase db branch create feature_auth
1084
+
1085
+ ${chalk.gray("# Back up to a local directory, then to object storage")}
1086
+ rebase db backup --out ./backups
1087
+ rebase db backup --out s3://my-private-bucket/backups
1088
+
1089
+ ${chalk.gray("# Restore into a fresh database (safe: does not touch the live one)")}
1090
+ rebase db restore ./backups/rebase-app-20260714T030000Z.dump --create-db --target-db app_restored
576
1091
  `);
577
1092
  }
578
- const DEV_PORT_FILENAME = ".rebase-dev-port";
1093
+ //#endregion
1094
+ //#region src/commands/dev.ts
1095
+ /**
1096
+ * CLI command: rebase dev
1097
+ *
1098
+ * Starts the full development environment:
1099
+ * - Backend: tsx watch with auto-reload
1100
+ * - Frontend: vite dev server
1101
+ *
1102
+ * Both processes stream output with color-coded prefixes.
1103
+ *
1104
+ * When the backend uses port-retry (i.e. the configured port is busy and it
1105
+ * binds to the next free one), the CLI detects the actual port from stdout
1106
+ * and injects VITE_API_URL into the frontend so it connects automatically.
1107
+ *
1108
+ * Each project gets a deterministic default port derived from the project
1109
+ * root path, so multiple Rebase instances never collide.
1110
+ */
1111
+ /** Well-known filename the backend writes its actual port to. */
1112
+ var DEV_PORT_FILENAME = ".rebase-dev-port";
1113
+ /**
1114
+ * Compute a deterministic port from the project root path.
1115
+ * Range: 3001–3999 (avoids privileged ports and common services).
1116
+ * Two different project directories will almost always get different ports.
1117
+ */
579
1118
  function getProjectPort(projectRoot) {
580
- let hash = 0;
581
- for (let i = 0; i < projectRoot.length; i++) {
582
- hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
583
- }
584
- return 3001 + Math.abs(hash) % 999;
1119
+ let hash = 0;
1120
+ for (let i = 0; i < projectRoot.length; i++) hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
1121
+ return 3001 + Math.abs(hash) % 999;
585
1122
  }
1123
+ /**
1124
+ * Resolve the best starting port for this project:
1125
+ * 1. Explicit --port flag (highest priority)
1126
+ * 2. PORT env var
1127
+ * 3. Previously used port from .rebase-dev-port (port affinity across restarts)
1128
+ * 4. Deterministic hash from project path (unique per project)
1129
+ */
586
1130
  function resolveStartPort(projectRoot, explicitPort) {
587
- if (explicitPort) return explicitPort;
588
- if (process.env.PORT) return parseInt(process.env.PORT, 10);
589
- try {
590
- const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
591
- if (fs.existsSync(portFile)) {
592
- const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
593
- if (saved > 0 && saved < 65536) return saved;
594
- }
595
- } catch {
596
- }
597
- return getProjectPort(projectRoot);
1131
+ if (explicitPort) return explicitPort;
1132
+ if (process.env.PORT) return parseInt(process.env.PORT, 10);
1133
+ try {
1134
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
1135
+ if (fs.existsSync(portFile)) {
1136
+ const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
1137
+ if (saved > 0 && saved < 65536) return saved;
1138
+ }
1139
+ } catch {}
1140
+ return getProjectPort(projectRoot);
598
1141
  }
599
1142
  async function devCommand(rawArgs) {
600
- const args = arg(
601
- {
602
- "--backend-only": Boolean,
603
- "--frontend-only": Boolean,
604
- "--port": Number,
605
- "--help": Boolean,
606
- "-b": "--backend-only",
607
- "-f": "--frontend-only",
608
- "-p": "--port",
609
- "-h": "--help"
610
- },
611
- {
612
- argv: rawArgs.slice(3),
613
- // skip "node rebase dev"
614
- permissive: true
615
- }
616
- );
617
- if (args["--help"]) {
618
- printDevHelp();
619
- return;
620
- }
621
- const projectRoot = requireProjectRoot();
622
- const backendDir = findBackendDir(projectRoot);
623
- const frontendDir = findFrontendDir(projectRoot);
624
- const backendOnly = args["--backend-only"] || false;
625
- const frontendOnly = args["--frontend-only"] || false;
626
- const startPort = resolveStartPort(projectRoot, args["--port"]);
627
- console.log("");
628
- console.log(chalk.bold(" 🚀 Rebase Dev Server"));
629
- console.log("");
630
- const children = [];
631
- let frontendUrl = "";
632
- let backendUrl = "";
633
- let debounceSummary = null;
634
- let bannerPrinted = false;
635
- let resolvedBackendPort = null;
636
- const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
637
- function printSummary() {
638
- if (!frontendUrl || !backendUrl) return;
639
- if (debounceSummary) clearTimeout(debounceSummary);
640
- debounceSummary = setTimeout(() => {
641
- if (bannerPrinted) return;
642
- console.log("");
643
- console.log(chalk.cyan("┌────────────────────────────────────────────────────────────┐"));
644
- console.log(chalk.cyan("│ │"));
645
- console.log(chalk.cyan("│ Rebase Admin App is ready! │"));
646
- const cleanUrl = stripAnsi(frontendUrl);
647
- const paddedUrl = cleanUrl.padEnd(40);
648
- console.log(chalk.cyan("│ 👉 Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
649
- console.log(chalk.cyan("│ │"));
650
- console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
651
- console.log("");
652
- bannerPrinted = true;
653
- }, 500);
654
- }
655
- const cleanup = () => {
656
- try {
657
- const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
658
- if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
659
- const urlFile = path.join(projectRoot, ".rebase-dev-url");
660
- if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
661
- } catch {
662
- }
663
- children.forEach((child) => {
664
- if (child.pid && !child.killed) {
665
- try {
666
- if (process.platform === "win32") {
667
- execa.commandSync(`taskkill /pid ${child.pid} /T /F`);
668
- } else {
669
- process.kill(-child.pid, "SIGKILL");
670
- }
671
- } catch (e) {
672
- try {
673
- child.kill("SIGKILL");
674
- } catch (err) {
675
- }
676
- }
677
- }
678
- });
679
- process.exit(0);
680
- };
681
- process.on("SIGINT", cleanup);
682
- process.on("SIGTERM", cleanup);
683
- function startFrontend(backendPort) {
684
- if (!frontendDir) return;
685
- console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
686
- const frontendEnv = { ...process.env };
687
- if (backendPort) {
688
- frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
689
- console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
690
- }
691
- const frontendChild = execa(
692
- "pnpm",
693
- ["run", "dev"],
694
- {
695
- cwd: frontendDir,
696
- stdio: ["inherit", "pipe", "pipe"],
697
- env: frontendEnv,
698
- shell: true,
699
- detached: process.platform !== "win32"
700
- }
701
- );
702
- frontendChild.catch(() => {
703
- });
704
- frontendChild.stdout?.on("data", (data) => {
705
- const lines = data.toString().split("\n").filter(Boolean);
706
- lines.forEach((line) => {
707
- console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
708
- const cleanLine = stripAnsi(line);
709
- const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
710
- if (cleanLine.includes("Local:") && urlMatch) {
711
- frontendUrl = urlMatch[1];
712
- printSummary();
713
- }
714
- });
715
- });
716
- frontendChild.stderr?.on("data", (data) => {
717
- const lines = data.toString().split("\n").filter(Boolean);
718
- lines.forEach((line) => {
719
- console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
720
- });
721
- });
722
- children.push(frontendChild);
723
- }
724
- if (!frontendOnly && backendDir) {
725
- const tsxBin = resolveTsx(projectRoot);
726
- if (!tsxBin) {
727
- console.error(chalk.red(" ✗ Could not find tsx binary for backend."));
728
- console.error(chalk.gray(" Install it with: pnpm add -D tsx"));
729
- process.exit(1);
730
- }
731
- const envFile = findEnvFile(projectRoot);
732
- const env = { ...process.env };
733
- if (envFile) {
734
- env.DOTENV_CONFIG_PATH = envFile;
735
- }
736
- env.PORT = String(startPort);
737
- console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
738
- console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
739
- let frontendLaunched = false;
740
- const backendChild = execa(
741
- tsxBin,
742
- ["watch", `--watch="${path.join("..", "config", "**", "*")}"`, "--conditions", "development", "src/index.ts"],
743
- {
744
- cwd: backendDir,
745
- stdio: ["inherit", "pipe", "pipe"],
746
- env,
747
- shell: true,
748
- detached: process.platform !== "win32"
749
- }
750
- );
751
- backendChild.catch(() => {
752
- });
753
- backendChild.stdout?.on("data", (data) => {
754
- const lines = data.toString().split("\n").filter(Boolean);
755
- lines.forEach((line) => {
756
- console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
757
- const cleanLine = stripAnsi(line);
758
- const serverMatch = cleanLine.match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
759
- if (serverMatch) {
760
- resolvedBackendPort = parseInt(serverMatch[1], 10);
761
- backendUrl = "started";
762
- printSummary();
763
- const urlFile = path.join(projectRoot, ".rebase-dev-url");
764
- fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
765
- const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
766
- fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
767
- if (!backendOnly && frontendDir && !frontendLaunched) {
768
- frontendLaunched = true;
769
- startFrontend(resolvedBackendPort);
770
- }
771
- }
772
- });
773
- });
774
- backendChild.stderr?.on("data", (data) => {
775
- const lines = data.toString().split("\n").filter(Boolean);
776
- lines.forEach((line) => {
777
- console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
778
- });
779
- });
780
- children.push(backendChild);
781
- } else if (!frontendOnly && !backendDir) {
782
- console.warn(chalk.yellow(" No backend/ directory found, skipping backend."));
783
- }
784
- if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) {
785
- startFrontend(null);
786
- } else if (!backendOnly && !frontendDir) {
787
- console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
788
- }
789
- if (children.length === 0) {
790
- console.error(chalk.red(" ✗ Nothing to start. Check your project structure."));
791
- process.exit(1);
792
- }
793
- console.log("");
794
- console.log(chalk.gray(" Press Ctrl+C to stop all servers."));
795
- console.log("");
796
- await Promise.all(
797
- children.map(
798
- (child) => new Promise((resolve) => {
799
- child.finally(() => resolve());
800
- })
801
- )
802
- );
1143
+ const args = arg({
1144
+ "--backend-only": Boolean,
1145
+ "--frontend-only": Boolean,
1146
+ "--port": Number,
1147
+ "--generate": Boolean,
1148
+ "--help": Boolean,
1149
+ "-b": "--backend-only",
1150
+ "-f": "--frontend-only",
1151
+ "-p": "--port",
1152
+ "-g": "--generate",
1153
+ "-h": "--help"
1154
+ }, {
1155
+ argv: rawArgs.slice(3),
1156
+ permissive: true
1157
+ });
1158
+ if (args["--help"]) {
1159
+ printDevHelp();
1160
+ return;
1161
+ }
1162
+ const projectRoot = requireProjectRoot();
1163
+ const backendDir = findBackendDir(projectRoot);
1164
+ const frontendDir = findFrontendDir(projectRoot);
1165
+ const backendOnly = args["--backend-only"] || false;
1166
+ const frontendOnly = args["--frontend-only"] || false;
1167
+ const shouldGenerate = args["--generate"] || process.env.REBASE_AUTO_GENERATE === "true" || process.env.REBASE_GENERATE === "true";
1168
+ const startPort = resolveStartPort(projectRoot, args["--port"]);
1169
+ console.log("");
1170
+ console.log(chalk.bold(" 🚀 Rebase Dev Server"));
1171
+ console.log("");
1172
+ const children = [];
1173
+ let frontendUrl = "";
1174
+ let backendUrl = "";
1175
+ let debounceSummary = null;
1176
+ let bannerPrinted = false;
1177
+ /** Actual backend port, resolved once the server prints its URL. */
1178
+ let resolvedBackendPort = null;
1179
+ const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
1180
+ function printSummary() {
1181
+ if (!frontendUrl || !backendUrl) return;
1182
+ if (debounceSummary) clearTimeout(debounceSummary);
1183
+ debounceSummary = setTimeout(() => {
1184
+ if (bannerPrinted) return;
1185
+ console.log("");
1186
+ console.log(chalk.cyan("┌────────────────────────────────────────────────────────────┐"));
1187
+ console.log(chalk.cyan("│ │"));
1188
+ console.log(chalk.cyan("│ Rebase Admin App is ready! │"));
1189
+ const paddedUrl = stripAnsi(frontendUrl).padEnd(41);
1190
+ console.log(chalk.cyan("│ ➜ Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
1191
+ console.log(chalk.cyan("│ │"));
1192
+ console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
1193
+ console.log("");
1194
+ bannerPrinted = true;
1195
+ }, 500);
1196
+ }
1197
+ const cleanup = () => {
1198
+ try {
1199
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
1200
+ if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
1201
+ const urlFile = path.join(projectRoot, ".rebase-dev-url");
1202
+ if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
1203
+ } catch {}
1204
+ children.forEach((child) => {
1205
+ if (child.pid && !child.killed) try {
1206
+ if (process.platform === "win32") execaCommandSync(`taskkill /pid ${child.pid} /T /F`);
1207
+ else process.kill(-child.pid, "SIGKILL");
1208
+ } catch (e) {
1209
+ try {
1210
+ child.kill("SIGKILL");
1211
+ } catch (err) {}
1212
+ }
1213
+ });
1214
+ process.exit(0);
1215
+ };
1216
+ process.on("SIGINT", cleanup);
1217
+ process.on("SIGTERM", cleanup);
1218
+ /**
1219
+ * Start the Vite frontend, optionally injecting the backend port.
1220
+ */
1221
+ function startFrontend(backendPort) {
1222
+ if (!frontendDir) return;
1223
+ console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
1224
+ const frontendEnv = { ...process.env };
1225
+ if (backendPort) {
1226
+ frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
1227
+ console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
1228
+ }
1229
+ const runDevCmd = getPMCommands(detectPackageManager(projectRoot)).run("dev");
1230
+ const frontendChild = execa(runDevCmd[0], runDevCmd.slice(1), {
1231
+ cwd: frontendDir,
1232
+ stdio: [
1233
+ "inherit",
1234
+ "pipe",
1235
+ "pipe"
1236
+ ],
1237
+ env: frontendEnv,
1238
+ shell: true,
1239
+ detached: process.platform !== "win32"
1240
+ });
1241
+ frontendChild.catch(() => {});
1242
+ frontendChild.stdout?.on("data", (data) => {
1243
+ data.toString().split("\n").filter(Boolean).forEach((line) => {
1244
+ console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
1245
+ const cleanLine = stripAnsi(line);
1246
+ const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
1247
+ if (cleanLine.includes("Local:") && urlMatch) {
1248
+ frontendUrl = urlMatch[1];
1249
+ printSummary();
1250
+ }
1251
+ });
1252
+ });
1253
+ frontendChild.stderr?.on("data", (data) => {
1254
+ data.toString().split("\n").filter(Boolean).forEach((line) => {
1255
+ console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
1256
+ });
1257
+ });
1258
+ children.push(frontendChild);
1259
+ }
1260
+ if (!frontendOnly && backendDir) {
1261
+ const tsxBin = resolveTsx(projectRoot);
1262
+ if (!tsxBin) {
1263
+ const addCmd = [
1264
+ ...getPMCommands(detectPackageManager(projectRoot)).install,
1265
+ "-D",
1266
+ "tsx"
1267
+ ].join(" ");
1268
+ console.error(chalk.red(" ✗ Could not find tsx binary for backend."));
1269
+ console.error(chalk.gray(` Install it with: ${addCmd}`));
1270
+ process.exit(1);
1271
+ }
1272
+ const tsxValidationError = validateTsxInstallation(tsxBin);
1273
+ if (tsxValidationError) {
1274
+ const installCmd = getPMCommands(detectPackageManager(projectRoot)).install.join(" ");
1275
+ console.error(chalk.red(" ✗ tsx installation appears corrupted."));
1276
+ console.error(chalk.gray(` ${tsxValidationError}`));
1277
+ console.error("");
1278
+ console.error(chalk.gray(" To fix, run:"));
1279
+ console.error(chalk.cyan(` rm -rf node_modules && ${installCmd}`));
1280
+ process.exit(1);
1281
+ }
1282
+ const envFile = findEnvFile(projectRoot);
1283
+ const env = { ...process.env };
1284
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1285
+ env.PORT = String(startPort);
1286
+ console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
1287
+ console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
1288
+ if (envFile) try {
1289
+ const envText = fs.readFileSync(envFile, "utf-8");
1290
+ const readEnvKey = (key) => {
1291
+ const m = envText.match(new RegExp(`^\\s*${key}\\s*=\\s*(.+?)\\s*$`, "m"));
1292
+ return m ? m[1].replace(/^["']|["']$/g, "") : void 0;
1293
+ };
1294
+ const envPort = readEnvKey("PORT");
1295
+ const envApiUrl = readEnvKey("VITE_API_URL");
1296
+ const overridden = [];
1297
+ if (envPort && envPort !== String(startPort)) overridden.push("PORT");
1298
+ if (envApiUrl && envApiUrl !== `http://localhost:${startPort}`) overridden.push("VITE_API_URL");
1299
+ if (overridden.length > 0) console.log(chalk.yellow(` ⚠ dev uses a derived per-project port (${startPort}); your .env ${overridden.join(" / ")} ${overridden.length > 1 ? "are" : "is"} ignored here (avoids cross-project collisions). Pass ${chalk.white("--port")} to pin a port.`));
1300
+ } catch {}
1301
+ /** Whether the frontend has been launched (we only launch it once). */
1302
+ let frontendLaunched = false;
1303
+ if (shouldGenerate) {
1304
+ console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
1305
+ try {
1306
+ const activePlugin = getActiveBackendPlugin(backendDir);
1307
+ const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
1308
+ if (pluginCli) await execa(tsxBin, [
1309
+ pluginCli,
1310
+ "schema",
1311
+ "generate"
1312
+ ], {
1313
+ cwd: backendDir,
1314
+ stdio: "inherit",
1315
+ env
1316
+ });
1317
+ const sdkCmd = getPMCommands(detectPackageManager(projectRoot)).exec("rebase", ["generate-sdk"]);
1318
+ await execa(sdkCmd[0], sdkCmd.slice(1), {
1319
+ cwd: projectRoot,
1320
+ stdio: "inherit",
1321
+ env
1322
+ });
1323
+ console.log(chalk.green(" ✓ Initial schema and SDK generated successfully.\n"));
1324
+ } catch (err) {
1325
+ console.error(chalk.red(` Initial schema/SDK generation failed: ${err instanceof Error ? err.message : err}\n`));
1326
+ }
1327
+ const collectionsDir = path.join(projectRoot, "config", "collections");
1328
+ if (fs.existsSync(collectionsDir)) {
1329
+ let watchDebounce = null;
1330
+ fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
1331
+ if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
1332
+ if (watchDebounce) clearTimeout(watchDebounce);
1333
+ watchDebounce = setTimeout(async () => {
1334
+ console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating schema & SDK...`));
1335
+ try {
1336
+ const activePlugin = getActiveBackendPlugin(backendDir);
1337
+ const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
1338
+ if (pluginCli) await execa(tsxBin, [
1339
+ pluginCli,
1340
+ "schema",
1341
+ "generate"
1342
+ ], {
1343
+ cwd: backendDir,
1344
+ stdio: "inherit",
1345
+ env
1346
+ });
1347
+ const sdkCmd = getPMCommands(detectPackageManager(projectRoot)).exec("rebase", ["generate-sdk"]);
1348
+ await execa(sdkCmd[0], sdkCmd.slice(1), {
1349
+ cwd: projectRoot,
1350
+ stdio: "inherit",
1351
+ env
1352
+ });
1353
+ console.log(chalk.green(" ✓ Schema & SDK regenerated successfully. Hono will reload."));
1354
+ } catch (err) {
1355
+ console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err instanceof Error ? err.message : err}`));
1356
+ }
1357
+ }, 300);
1358
+ });
1359
+ }
1360
+ }
1361
+ const watchArgs = [
1362
+ "watch",
1363
+ "--conditions",
1364
+ "development",
1365
+ "src/index.ts"
1366
+ ];
1367
+ if (!shouldGenerate) {
1368
+ watchArgs.splice(1, 0, `--watch="${path.join("..", "config", "**", "*")}"`);
1369
+ const collectionsDir = path.join(projectRoot, "config", "collections");
1370
+ if (fs.existsSync(collectionsDir)) {
1371
+ let driftDebounce = null;
1372
+ fs.watch(collectionsDir, { recursive: true }, (_eventType, filename) => {
1373
+ if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
1374
+ if (driftDebounce) clearTimeout(driftDebounce);
1375
+ driftDebounce = setTimeout(() => {
1376
+ console.log([
1377
+ "",
1378
+ chalk.yellow(" ┌──────────────────────────────────────────────────────────────┐"),
1379
+ chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(filename.padEnd(31)) + chalk.yellow("│"),
1380
+ chalk.yellow(" │ │"),
1381
+ chalk.yellow(" │ Your schema may be out of sync. Run: │"),
1382
+ chalk.yellow(" │ ") + chalk.cyan("rebase schema generate") + chalk.yellow(" regenerate Drizzle schema │"),
1383
+ chalk.yellow(" │ ") + chalk.cyan("rebase db push ") + chalk.yellow(" sync schema to database │"),
1384
+ chalk.yellow(" │ ") + chalk.cyan("rebase doctor ") + chalk.yellow(" check for drift │"),
1385
+ chalk.yellow(" │ │"),
1386
+ chalk.yellow(" │ TIP: Use ") + chalk.bold("rebase dev --generate") + chalk.yellow(" for auto-regeneration │"),
1387
+ chalk.yellow(" └──────────────────────────────────────────────────────────────┘"),
1388
+ ""
1389
+ ].join("\n"));
1390
+ }, 500);
1391
+ });
1392
+ }
1393
+ }
1394
+ const backendChild = execa(tsxBin, watchArgs, {
1395
+ cwd: backendDir,
1396
+ stdio: [
1397
+ "inherit",
1398
+ "pipe",
1399
+ "pipe"
1400
+ ],
1401
+ env,
1402
+ shell: true,
1403
+ detached: process.platform !== "win32"
1404
+ });
1405
+ backendChild.catch(() => {});
1406
+ backendChild.stdout?.on("data", (data) => {
1407
+ data.toString().split("\n").filter(Boolean).forEach((line) => {
1408
+ console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
1409
+ const serverMatch = stripAnsi(line).match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
1410
+ if (serverMatch) {
1411
+ resolvedBackendPort = parseInt(serverMatch[1], 10);
1412
+ backendUrl = "started";
1413
+ printSummary();
1414
+ const urlFile = path.join(projectRoot, ".rebase-dev-url");
1415
+ fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
1416
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
1417
+ fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
1418
+ if (!backendOnly && frontendDir && !frontendLaunched) {
1419
+ frontendLaunched = true;
1420
+ startFrontend(resolvedBackendPort);
1421
+ }
1422
+ }
1423
+ });
1424
+ });
1425
+ /** Whether we've already shown a corrupted-modules recovery hint. */
1426
+ let corruptedModulesWarned = false;
1427
+ backendChild.stderr?.on("data", (data) => {
1428
+ data.toString().split("\n").filter(Boolean).forEach((line) => {
1429
+ console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
1430
+ if (!corruptedModulesWarned) {
1431
+ const cleanLine = stripAnsi(line);
1432
+ if (cleanLine.includes("Cannot find module") && cleanLine.includes("node_modules/.pnpm/")) {
1433
+ corruptedModulesWarned = true;
1434
+ setTimeout(() => {
1435
+ const installCmd = getPMCommands(detectPackageManager(projectRoot)).install.join(" ");
1436
+ console.error("");
1437
+ console.error(chalk.red(" ✗ node_modules appears corrupted — a required file is missing."));
1438
+ console.error(chalk.gray(" This usually happens when a previous install was interrupted"));
1439
+ console.error(chalk.gray(" or the package manager store was cleaned."));
1440
+ console.error("");
1441
+ console.error(chalk.gray(" To fix, stop the dev server and run:"));
1442
+ console.error(chalk.cyan(` rm -rf node_modules && ${installCmd}`));
1443
+ console.error("");
1444
+ }, 200);
1445
+ }
1446
+ }
1447
+ });
1448
+ });
1449
+ children.push(backendChild);
1450
+ } else if (!frontendOnly && !backendDir) console.warn(chalk.yellow(" ⚠ No backend/ directory found, skipping backend."));
1451
+ if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) startFrontend(null);
1452
+ else if (!backendOnly && !frontendDir) console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
1453
+ if (children.length === 0) {
1454
+ console.error(chalk.red(" ✗ Nothing to start. Check your project structure."));
1455
+ process.exit(1);
1456
+ }
1457
+ console.log("");
1458
+ console.log(chalk.gray(" Press Ctrl+C to stop all servers."));
1459
+ console.log("");
1460
+ await Promise.all(children.map((child) => new Promise((resolve) => {
1461
+ child.finally(() => resolve());
1462
+ })));
803
1463
  }
804
1464
  function printDevHelp() {
805
- console.log(`
1465
+ console.log(`
806
1466
  ${chalk.bold("rebase dev")} — Start the development server
807
1467
 
808
1468
  ${chalk.green.bold("Usage")}
@@ -812,6 +1472,7 @@ ${chalk.green.bold("Options")}
812
1472
  ${chalk.blue("--backend-only, -b")} Only start the backend server
813
1473
  ${chalk.blue("--frontend-only, -f")} Only start the frontend server
814
1474
  ${chalk.blue("--port, -p")} Backend port (default: auto-detected per project)
1475
+ ${chalk.blue("--generate, -g")} Enable automatic schema and SDK generation on startup and file changes
815
1476
 
816
1477
  ${chalk.green.bold("Description")}
817
1478
  Starts both the backend (tsx watch + Hono) and frontend (Vite)
@@ -824,87 +1485,236 @@ ${chalk.green.bold("Description")}
824
1485
  If the assigned port is already in use, the server will automatically
825
1486
  try the next available port. The frontend is started only after the
826
1487
  backend is ready, and VITE_API_URL is injected automatically.
1488
+
1489
+ By default, automatic schema and SDK generation is disabled on startup
1490
+ and file changes. Pass --generate (-g) or set REBASE_AUTO_GENERATE=true
1491
+ in your environment to enable it.
827
1492
  `);
828
1493
  }
1494
+ //#endregion
1495
+ //#region src/commands/build.ts
1496
+ /**
1497
+ * CLI command: rebase build
1498
+ *
1499
+ * Runs the build script in all workspace packages.
1500
+ * Automatically detects the package manager (pnpm or npm) and
1501
+ * uses the correct workspace-aware command.
1502
+ */
1503
+ async function buildCommand() {
1504
+ const projectRoot = requireProjectRoot();
1505
+ const pm = detectPackageManager(projectRoot);
1506
+ const buildCmd = getPMCommands(pm).runAll("build");
1507
+ console.log(`${chalk.bold("Rebase")} — Building all workspaces with ${chalk.cyan(pm)}...\n`);
1508
+ try {
1509
+ await execa(buildCmd[0], buildCmd.slice(1), {
1510
+ cwd: projectRoot,
1511
+ stdio: "inherit"
1512
+ });
1513
+ } catch {
1514
+ console.error(chalk.red("\n✗ Build failed."));
1515
+ process.exit(1);
1516
+ }
1517
+ }
1518
+ //#endregion
1519
+ //#region src/commands/start.ts
1520
+ /**
1521
+ * CLI command: rebase start
1522
+ *
1523
+ * Starts the backend server in production mode.
1524
+ * Automatically detects the package manager (pnpm or npm) and
1525
+ * runs the start script in the backend workspace.
1526
+ */
1527
+ async function startCommand() {
1528
+ const projectRoot = requireProjectRoot();
1529
+ const startCmd = getPMCommands(detectPackageManager(projectRoot)).runWorkspace("backend", "start");
1530
+ const envFile = findEnvFile(projectRoot);
1531
+ const env = { ...process.env };
1532
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1533
+ console.log(`${chalk.bold("Rebase")} — Starting backend server...\n`);
1534
+ try {
1535
+ await execa(startCmd[0], startCmd.slice(1), {
1536
+ cwd: projectRoot,
1537
+ stdio: "inherit",
1538
+ env
1539
+ });
1540
+ } catch {
1541
+ console.error(chalk.red("\n✗ Failed to start server."));
1542
+ process.exit(1);
1543
+ }
1544
+ }
1545
+ //#endregion
1546
+ //#region src/commands/auth.ts
1547
+ /**
1548
+ * CLI command: rebase auth <action>
1549
+ *
1550
+ * Subcommands:
1551
+ * reset-password — Reset a user's password
1552
+ */
829
1553
  async function authCommand(subcommand, rawArgs) {
830
- if (!subcommand || subcommand === "--help") {
831
- printAuthHelp();
832
- return;
833
- }
834
- switch (subcommand) {
835
- case "reset-password":
836
- await resetPassword(rawArgs);
837
- break;
838
- default:
839
- console.error(chalk.red(`Unknown auth command: ${subcommand}`));
840
- console.log("");
841
- printAuthHelp();
842
- process.exit(1);
843
- }
1554
+ if (!subcommand || subcommand === "--help") {
1555
+ printAuthHelp();
1556
+ return;
1557
+ }
1558
+ switch (subcommand) {
1559
+ case "reset-password":
1560
+ await resetPassword(rawArgs);
1561
+ break;
1562
+ default:
1563
+ console.error(chalk.red(`Unknown auth command: ${subcommand}`));
1564
+ console.log("");
1565
+ printAuthHelp();
1566
+ process.exit(1);
1567
+ }
844
1568
  }
845
1569
  async function resetPassword(rawArgs) {
846
- const args = arg(
847
- {
848
- "--email": String,
849
- "--password": String,
850
- "-e": "--email",
851
- "-p": "--password"
852
- },
853
- {
854
- argv: rawArgs.slice(4),
855
- // skip "node rebase auth reset-password"
856
- permissive: true
857
- }
858
- );
859
- const email = args["--email"] || args._[0];
860
- const newPassword = args["--password"] || args._[1];
861
- if (!email) {
862
- console.error(chalk.red("✗ Email is required."));
863
- console.log("");
864
- console.log(chalk.gray(" Usage: rebase auth reset-password <email> [new-password]"));
865
- console.log(chalk.gray(" rebase auth reset-password --email user@example.com --password NewPass123!"));
866
- process.exit(1);
867
- }
868
- const projectRoot = requireProjectRoot();
869
- const backendDir = requireBackendDir(projectRoot);
870
- const tsxBin = resolveTsx(projectRoot);
871
- if (!tsxBin) {
872
- console.error(chalk.red("✗ Could not find tsx binary."));
873
- process.exit(1);
874
- }
875
- const envFile = findEnvFile(projectRoot);
876
- const env = { ...process.env };
877
- if (envFile) {
878
- env.DOTENV_CONFIG_PATH = envFile;
879
- }
880
- const scriptContent = `
881
- import { createPostgresDatabaseConnection } from "@rebasepro/server-core";
882
- import { hashPassword } from "@rebasepro/server-core/src/auth/password";
1570
+ const args = arg({
1571
+ "--email": String,
1572
+ "--password": String,
1573
+ "-e": "--email",
1574
+ "-p": "--password"
1575
+ }, {
1576
+ argv: rawArgs.slice(4),
1577
+ permissive: true
1578
+ });
1579
+ const email = args["--email"] || args._[0];
1580
+ const newPassword = args["--password"] || args._[1];
1581
+ if (!email) {
1582
+ console.error(chalk.red("✗ Email is required."));
1583
+ console.log("");
1584
+ console.log(chalk.gray(" Usage: rebase auth reset-password <email> [new-password]"));
1585
+ console.log(chalk.gray(" rebase auth reset-password --email user@example.com --password NewPass123!"));
1586
+ process.exit(1);
1587
+ }
1588
+ const projectRoot = requireProjectRoot();
1589
+ let envServiceKey;
1590
+ const envFile = findEnvFile(projectRoot);
1591
+ if (envFile && fs.existsSync(envFile)) try {
1592
+ const match = fs.readFileSync(envFile, "utf8").match(/^\s*REBASE_SERVICE_KEY\s*=\s*['"]?(.*?)['"]?\s*$/m);
1593
+ if (match && match[1]) envServiceKey = match[1];
1594
+ } catch {}
1595
+ let baseUrl = process.env.REBASE_BASE_URL;
1596
+ let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;
1597
+ const statePath = path.join(projectRoot, ".rebase", "state.json");
1598
+ if (fs.existsSync(statePath)) try {
1599
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
1600
+ if (state && typeof state === "object") {
1601
+ if (typeof state.baseUrl === "string" && !baseUrl) baseUrl = state.baseUrl;
1602
+ if (typeof state.serviceKey === "string" && !serviceKey) serviceKey = state.serviceKey;
1603
+ }
1604
+ } catch {}
1605
+ const devUrlPath = path.join(projectRoot, ".rebase-dev-url");
1606
+ if (fs.existsSync(devUrlPath) && !baseUrl) try {
1607
+ baseUrl = fs.readFileSync(devUrlPath, "utf8").trim();
1608
+ } catch {}
1609
+ if (baseUrl && serviceKey) {
1610
+ console.log("Trying API-first reset via running backend...");
1611
+ try {
1612
+ const finalPass = newPassword || "NewPassword123!";
1613
+ const cleanBaseUrl = baseUrl.replace(/\/+$/, "");
1614
+ const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=1`;
1615
+ const searchRes = await fetch(searchUrl, { headers: {
1616
+ "Authorization": `Bearer ${serviceKey}`,
1617
+ "Accept": "application/json"
1618
+ } });
1619
+ if (!searchRes.ok) throw new Error(`Failed to list users: ${searchRes.statusText}`);
1620
+ const searchData = await searchRes.json();
1621
+ if (!searchData || typeof searchData !== "object") throw new Error("Invalid response format from user search API.");
1622
+ let userId;
1623
+ if (Array.isArray(searchData)) {
1624
+ const firstUser = searchData[0];
1625
+ if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
1626
+ else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
1627
+ } else if ("users" in searchData && Array.isArray(searchData.users)) {
1628
+ const firstUser = searchData.users[0];
1629
+ if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
1630
+ else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
1631
+ }
1632
+ if (!userId) throw new Error(`User not found with email: ${email}`);
1633
+ const resetUrl = `${cleanBaseUrl}/api/admin/users/${userId}/reset-password`;
1634
+ const resetRes = await fetch(resetUrl, {
1635
+ method: "POST",
1636
+ headers: {
1637
+ "Authorization": `Bearer ${serviceKey}`,
1638
+ "Content-Type": "application/json",
1639
+ "Accept": "application/json"
1640
+ },
1641
+ body: JSON.stringify({ password: finalPass })
1642
+ });
1643
+ if (!resetRes.ok) {
1644
+ const errText = await resetRes.text();
1645
+ throw new Error(`Password reset endpoint failed: ${errText || resetRes.statusText}`);
1646
+ }
1647
+ console.log("API reset successful.");
1648
+ console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (via API)"));
1649
+ console.log("");
1650
+ console.log(` ${chalk.gray("Email:")} ${email}`);
1651
+ console.log(` ${chalk.gray("Password:")} ${finalPass}`);
1652
+ console.log("");
1653
+ return;
1654
+ } catch (err) {
1655
+ const errMsg = err instanceof Error ? err.message : String(err);
1656
+ console.warn(chalk.yellow("API reset failed, falling back to direct database update..."));
1657
+ console.warn(chalk.gray(` Details: ${errMsg}`));
1658
+ }
1659
+ }
1660
+ const backendDir = requireBackendDir(projectRoot);
1661
+ const tsxBin = resolveTsx(projectRoot);
1662
+ if (!tsxBin) {
1663
+ console.error(chalk.red("✗ Could not find tsx binary."));
1664
+ process.exit(1);
1665
+ }
1666
+ try {
1667
+ const env = { ...process.env };
1668
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1669
+ env.REBASE_RESET_EMAIL = email;
1670
+ env.REBASE_RESET_PASSWORD = newPassword || "NewPassword123!";
1671
+ env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
1672
+ const scriptContent = `
1673
+ import { createPostgresDatabaseConnection } from "@rebasepro/server-postgres";
1674
+ import { hashPassword } from "@rebasepro/server";
883
1675
  import { eq } from "drizzle-orm";
884
- import { users } from "@rebasepro/server-core/src/db/auth-schema";
885
1676
  import * as dotenv from "dotenv";
886
1677
  import path from "path";
1678
+ import fs from "fs";
887
1679
 
888
- dotenv.config({ path: "${envFile || path.join(projectRoot, ".env")}" });
1680
+ dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH });
889
1681
 
890
- const email = "${email}";
891
- const newPassword = "${newPassword || "NewPassword123!"}";
1682
+ const email = process.env.REBASE_RESET_EMAIL!;
1683
+ const newPassword = process.env.REBASE_RESET_PASSWORD!;
892
1684
 
893
1685
  async function resetPassword() {
894
1686
  const { db } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);
895
1687
  const hash = await hashPassword(newPassword);
896
1688
 
897
- const result = await db.update(users)
898
- .set({ passwordHash: hash })
899
- .where(eq(users.email, email))
1689
+ let usersTable;
1690
+ try {
1691
+ const schemaPath = path.resolve("./src/schema.generated.ts");
1692
+ if (fs.existsSync(schemaPath)) {
1693
+ const schema = await import("file://" + schemaPath);
1694
+ usersTable = schema.users || schema.tables?.users;
1695
+ }
1696
+ } catch (e) {
1697
+ // ignore and fallback
1698
+ }
1699
+
1700
+ if (!usersTable) {
1701
+ const pgServer = await import("@rebasepro/server-postgres");
1702
+ usersTable = pgServer.users;
1703
+ }
1704
+
1705
+ const passwordHashKey = (usersTable.passwordHash || "passwordHash" in usersTable) ? "passwordHash" : "password_hash";
1706
+
1707
+ const result = await db.update(usersTable)
1708
+ .set({ [passwordHashKey]: hash })
1709
+ .where(eq(usersTable.email, email))
900
1710
  .returning({
901
- id: users.id,
902
- email: users.email
1711
+ id: usersTable.id,
1712
+ email: usersTable.email
903
1713
  });
904
1714
 
905
1715
  if (result.length > 0) {
906
1716
  console.log("✅ Password reset for: " + result[0].email);
907
- ${!newPassword ? 'console.log(" New password: " + newPassword);' : ""}
1717
+ ${!newPassword ? "console.log(\" New password: \" + newPassword);" : ""}
908
1718
  } else {
909
1719
  console.log("✗ User not found: " + email);
910
1720
  }
@@ -913,36 +1723,36 @@ async function resetPassword() {
913
1723
 
914
1724
  resetPassword().catch(console.error);
915
1725
  `;
916
- const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
917
- fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
918
- console.log("");
919
- console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password"));
920
- console.log("");
921
- console.log(` ${chalk.gray("Email:")} ${email}`);
922
- if (newPassword) {
923
- console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
924
- }
925
- console.log("");
926
- const child = spawn(tsxBin, [tmpScriptPath], {
927
- cwd: backendDir,
928
- stdio: "inherit",
929
- env
930
- });
931
- return new Promise((resolve) => {
932
- child.on("close", (code) => {
933
- try {
934
- fs.unlinkSync(tmpScriptPath);
935
- } catch {
936
- }
937
- if (code !== 0) {
938
- process.exit(code ?? 1);
939
- }
940
- resolve();
941
- });
942
- });
1726
+ const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
1727
+ fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
1728
+ console.log("");
1729
+ console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)"));
1730
+ console.log("");
1731
+ console.log(` ${chalk.gray("Email:")} ${email}`);
1732
+ if (newPassword) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
1733
+ console.log("");
1734
+ const child = spawn(tsxBin, [tmpScriptPath], {
1735
+ cwd: backendDir,
1736
+ stdio: "inherit",
1737
+ env
1738
+ });
1739
+ return new Promise((resolve) => {
1740
+ child.on("close", (code) => {
1741
+ try {
1742
+ fs.unlinkSync(tmpScriptPath);
1743
+ } catch {}
1744
+ if (code !== 0) process.exit(code ?? 1);
1745
+ resolve();
1746
+ });
1747
+ });
1748
+ } catch (err) {
1749
+ console.error(chalk.red("✗ Direct database update failed."));
1750
+ console.error(err instanceof Error ? err.message : String(err));
1751
+ process.exit(1);
1752
+ }
943
1753
  }
944
1754
  function printAuthHelp() {
945
- console.log(`
1755
+ console.log(`
946
1756
  ${chalk.bold("rebase auth")} — Authentication management commands
947
1757
 
948
1758
  ${chalk.green.bold("Usage")}
@@ -960,133 +1770,2165 @@ ${chalk.green.bold("Examples")}
960
1770
  rebase auth reset-password --email user@example.com --password MyNewPass!
961
1771
  `);
962
1772
  }
1773
+ //#endregion
1774
+ //#region src/commands/doctor.ts
1775
+ /**
1776
+ * CLI command: rebase doctor
1777
+ *
1778
+ * Detects three-way schema drift between collection definitions,
1779
+ * the generated Drizzle schema, and the live PostgreSQL database.
1780
+ */
963
1781
  async function doctorCommand(rawArgs) {
964
- const projectRoot = requireProjectRoot();
965
- const backendDir = requireBackendDir(projectRoot);
966
- const activePlugin = getActiveBackendPlugin(backendDir);
967
- if (!activePlugin) {
968
- console.error(chalk.red("✗ Could not detect an active database plugin."));
969
- console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
970
- process.exit(1);
971
- }
972
- const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
973
- if (!pluginCli) {
974
- console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
975
- process.exit(1);
976
- }
977
- const envFile = findEnvFile(projectRoot);
978
- const env = { ...process.env };
979
- if (envFile) {
980
- env.DOTENV_CONFIG_PATH = envFile;
981
- }
982
- try {
983
- const isTs = pluginCli.endsWith(".ts");
984
- if (isTs) {
985
- const tsxBin = resolveTsx(projectRoot);
986
- if (!tsxBin) {
987
- console.error(chalk.red("✗ Could not find tsx binary."));
988
- process.exit(1);
989
- }
990
- await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
991
- cwd: backendDir,
992
- stdio: "inherit",
993
- env
994
- });
995
- } else {
996
- await execa("node", [pluginCli, ...rawArgs.slice(2)], {
997
- cwd: backendDir,
998
- stdio: "inherit",
999
- env
1000
- });
1001
- }
1002
- } catch {
1003
- process.exit(1);
1004
- }
1782
+ const projectRoot = requireProjectRoot();
1783
+ const backendDir = requireBackendDir(projectRoot);
1784
+ const activePlugin = getActiveBackendPlugin(backendDir);
1785
+ if (!activePlugin) {
1786
+ console.error(chalk.red("✗ Could not detect an active database plugin."));
1787
+ console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json."));
1788
+ process.exit(1);
1789
+ }
1790
+ const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
1791
+ if (!pluginCli) {
1792
+ console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
1793
+ process.exit(1);
1794
+ }
1795
+ const envFile = findEnvFile(projectRoot);
1796
+ const env = { ...process.env };
1797
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1798
+ try {
1799
+ if (pluginCli.endsWith(".ts")) {
1800
+ const tsxBin = resolveTsx(projectRoot);
1801
+ if (!tsxBin) {
1802
+ console.error(chalk.red("✗ Could not find tsx binary."));
1803
+ process.exit(1);
1804
+ }
1805
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
1806
+ cwd: backendDir,
1807
+ stdio: "inherit",
1808
+ env
1809
+ });
1810
+ } else await execa("node", [pluginCli, ...rawArgs.slice(2)], {
1811
+ cwd: backendDir,
1812
+ stdio: "inherit",
1813
+ env
1814
+ });
1815
+ } catch {
1816
+ process.exit(1);
1817
+ }
1818
+ }
1819
+ //#endregion
1820
+ //#region src/commands/skills.ts
1821
+ var require = createRequire(import.meta.url);
1822
+ /** Supported agent environments and their target directories. */
1823
+ var AGENTS = {
1824
+ cursor: {
1825
+ label: "Cursor",
1826
+ detectDir: ".cursor",
1827
+ targetDir: ".cursor/rules",
1828
+ /** Cursor uses .mdc files (Markdown with Context). */
1829
+ transformFile: (skillName, content) => ({
1830
+ fileName: `${skillName}.mdc`,
1831
+ content
1832
+ })
1833
+ },
1834
+ claude: {
1835
+ label: "Claude Code",
1836
+ detectDir: ".claude",
1837
+ targetDir: ".claude/skills",
1838
+ /** Claude Code uses the standard SKILL.md format in subdirectories. */
1839
+ transformFile: (skillName, content) => ({
1840
+ fileName: path.join(skillName, "SKILL.md"),
1841
+ content
1842
+ })
1843
+ },
1844
+ windsurf: {
1845
+ label: "Windsurf",
1846
+ detectDir: ".windsurf",
1847
+ targetDir: ".windsurf/rules",
1848
+ /** Windsurf uses plain .md files. */
1849
+ transformFile: (skillName, content) => ({
1850
+ fileName: `${skillName}.md`,
1851
+ content
1852
+ })
1853
+ },
1854
+ gemini: {
1855
+ label: "Gemini CLI / Antigravity",
1856
+ detectDir: ".agents",
1857
+ targetDir: ".agents/skills",
1858
+ /** Gemini uses the standard SKILL.md format in subdirectories. */
1859
+ transformFile: (skillName, content) => ({
1860
+ fileName: path.join(skillName, "SKILL.md"),
1861
+ content
1862
+ })
1863
+ }
1864
+ };
1865
+ /**
1866
+ * Resolve the path to the skills directory from @rebasepro/agent-skills.
1867
+ * Works in both workspace (symlink) and published (real files) layouts.
1868
+ */
1869
+ function getSkillsSourceDir() {
1870
+ const pkgJsonPath = require.resolve("@rebasepro/agent-skills/package.json");
1871
+ const pkgRoot = path.dirname(pkgJsonPath);
1872
+ const skillsDir = path.join(pkgRoot, "skills");
1873
+ if (!fs.existsSync(skillsDir)) throw new Error(`Skills directory not found at ${skillsDir}. Make sure @rebasepro/agent-skills is installed.`);
1874
+ return skillsDir;
1875
+ }
1876
+ /** Read all skill directories and return their names + content. */
1877
+ function loadSkills(skillsDir) {
1878
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
1879
+ const skills = [];
1880
+ for (const entry of entries) {
1881
+ if (!entry.isDirectory()) continue;
1882
+ const skillMdPath = path.join(skillsDir, entry.name, "SKILL.md");
1883
+ if (!fs.existsSync(skillMdPath)) continue;
1884
+ skills.push({
1885
+ name: entry.name,
1886
+ content: fs.readFileSync(skillMdPath, "utf-8")
1887
+ });
1888
+ }
1889
+ return skills;
1890
+ }
1891
+ /** Detect which agent environments already exist in the project. */
1892
+ function detectAgents(projectDir) {
1893
+ const detected = [];
1894
+ for (const [key, agent] of Object.entries(AGENTS)) if (fs.existsSync(path.join(projectDir, agent.detectDir))) detected.push(key);
1895
+ return detected;
1896
+ }
1897
+ /** Install skills for a specific agent into the project directory. */
1898
+ function installForAgent(agentKey, skills, projectDir) {
1899
+ const agent = AGENTS[agentKey];
1900
+ const targetBase = path.join(projectDir, agent.targetDir);
1901
+ fs.mkdirSync(targetBase, { recursive: true });
1902
+ let count = 0;
1903
+ for (const skill of skills) {
1904
+ const { fileName, content } = agent.transformFile(skill.name, skill.content);
1905
+ const targetPath = path.join(targetBase, fileName);
1906
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
1907
+ fs.writeFileSync(targetPath, content, "utf-8");
1908
+ count++;
1909
+ }
1910
+ return count;
1911
+ }
1912
+ async function skillsCommand(subcommand, _args) {
1913
+ switch (subcommand) {
1914
+ case "install":
1915
+ await skillsInstall();
1916
+ break;
1917
+ case "--help":
1918
+ case void 0:
1919
+ printSkillsHelp();
1920
+ break;
1921
+ default:
1922
+ console.log(chalk.red(`Unknown skills subcommand: ${subcommand}`));
1923
+ console.log("");
1924
+ printSkillsHelp();
1925
+ }
1926
+ }
1927
+ async function skillsInstall() {
1928
+ const projectDir = process.cwd();
1929
+ let skillsDir;
1930
+ try {
1931
+ skillsDir = getSkillsSourceDir();
1932
+ } catch (err) {
1933
+ console.error(`${chalk.red.bold("ERROR")} ${err instanceof Error ? err.message : String(err)}`);
1934
+ process.exit(1);
1935
+ }
1936
+ const skills = loadSkills(skillsDir);
1937
+ if (skills.length === 0) {
1938
+ console.error(`${chalk.red.bold("ERROR")} No skills found in ${skillsDir}`);
1939
+ process.exit(1);
1940
+ }
1941
+ let agents = detectAgents(projectDir);
1942
+ if (agents.length === 0) {
1943
+ const choices = Object.entries(AGENTS).map(([key, agent]) => ({
1944
+ name: agent.label,
1945
+ value: key,
1946
+ checked: false
1947
+ }));
1948
+ const { selectedAgents } = await inquirer.prompt([{
1949
+ type: "checkbox",
1950
+ name: "selectedAgents",
1951
+ message: "No AI agent configuration detected. Which agents do you use?",
1952
+ choices,
1953
+ validate: (input) => {
1954
+ if (input.length === 0) return "Please select at least one agent.";
1955
+ return true;
1956
+ }
1957
+ }]);
1958
+ agents = selectedAgents;
1959
+ }
1960
+ console.log("");
1961
+ console.log(chalk.gray(` Found ${chalk.white(skills.length)} Rebase skills`));
1962
+ console.log("");
1963
+ for (const agentKey of agents) {
1964
+ const agent = AGENTS[agentKey];
1965
+ const count = installForAgent(agentKey, skills, projectDir);
1966
+ console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} — ${count} skills installed to ${chalk.gray(agent.targetDir)}`);
1967
+ }
1968
+ console.log("");
1969
+ console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
1970
+ console.log(chalk.gray(" Re-run this command anytime to update to the latest skills."));
1971
+ console.log("");
1972
+ }
1973
+ function printSkillsHelp() {
1974
+ console.log(`
1975
+ ${chalk.bold("rebase skills")} — Manage AI agent skills
1976
+
1977
+ ${chalk.green.bold("Usage")}
1978
+ rebase skills ${chalk.blue("<subcommand>")}
1979
+
1980
+ ${chalk.green.bold("Subcommands")}
1981
+ ${chalk.blue.bold("install")} Install Rebase agent skills for your AI coding assistant
1982
+ Supports: Cursor, Claude Code, Windsurf, Gemini CLI, Antigravity
1983
+
1984
+ ${chalk.green.bold("Examples")}
1985
+ ${chalk.cyan("rebase skills install")}
1986
+ `);
1987
+ }
1988
+ //#endregion
1989
+ //#region src/commands/api-keys.ts
1990
+ /**
1991
+ * CLI command: rebase api-keys <action>
1992
+ *
1993
+ * Subcommands:
1994
+ * list — List all API keys (masked)
1995
+ * create — Create a new API key
1996
+ * revoke — Revoke an existing API key
1997
+ */
1998
+ function loadEnv(projectRoot) {
1999
+ const envFile = findEnvFile(projectRoot);
2000
+ const env = {};
2001
+ if (envFile && fs.existsSync(envFile)) {
2002
+ const content = fs.readFileSync(envFile, "utf-8");
2003
+ for (const line of content.split("\n")) {
2004
+ const trimmed = line.trim();
2005
+ if (!trimmed || trimmed.startsWith("#")) continue;
2006
+ const idx = trimmed.indexOf("=");
2007
+ if (idx > 0) {
2008
+ const key = trimmed.slice(0, idx).trim();
2009
+ let value = trimmed.slice(idx + 1).trim();
2010
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
2011
+ env[key] = value;
2012
+ }
2013
+ }
2014
+ }
2015
+ return env;
2016
+ }
2017
+ function resolveBaseUrl(env) {
2018
+ const port = env.PORT || env.REBASE_PORT || "3001";
2019
+ return env.REBASE_BASE_URL || `http://localhost:${port}`;
2020
+ }
2021
+ async function apiKeysCommand(subcommand, rawArgs) {
2022
+ if (!subcommand || subcommand === "--help") {
2023
+ printApiKeysHelp();
2024
+ return;
2025
+ }
2026
+ switch (subcommand) {
2027
+ case "list":
2028
+ await listKeys(rawArgs);
2029
+ break;
2030
+ case "create":
2031
+ await createKey(rawArgs);
2032
+ break;
2033
+ case "revoke":
2034
+ await revokeKey(rawArgs);
2035
+ break;
2036
+ default:
2037
+ console.error(chalk.red(`Unknown api-keys command: ${subcommand}`));
2038
+ console.log("");
2039
+ printApiKeysHelp();
2040
+ process.exit(1);
2041
+ }
2042
+ }
2043
+ async function listKeys(_rawArgs) {
2044
+ const env = loadEnv(requireProjectRoot());
2045
+ const baseUrl = resolveBaseUrl(env);
2046
+ const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
2047
+ if (!serviceKey) {
2048
+ console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
2049
+ process.exit(1);
2050
+ }
2051
+ try {
2052
+ const res = await fetch(`${baseUrl}/api/admin/api-keys`, { headers: { Authorization: `Bearer ${serviceKey}` } });
2053
+ if (!res.ok) {
2054
+ const body = await res.text();
2055
+ console.error(chalk.red(`✗ Failed to list API keys: ${res.status} ${body}`));
2056
+ process.exit(1);
2057
+ }
2058
+ const { keys } = await res.json();
2059
+ console.log("");
2060
+ console.log(chalk.bold(" 🔑 API Keys"));
2061
+ console.log("");
2062
+ if (keys.length === 0) {
2063
+ console.log(chalk.gray(" No API keys found."));
2064
+ console.log("");
2065
+ return;
2066
+ }
2067
+ for (const key of keys) {
2068
+ 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");
2069
+ const perms = key.permissions.map((p) => `${p.collection}(${p.operations.join(",")})`).join(", ");
2070
+ console.log(` ${chalk.bold(key.name)} ${chalk.gray(`[${key.key_prefix}•••]`)} ${status}`);
2071
+ console.log(` ${chalk.gray("ID:")} ${key.id}`);
2072
+ console.log(` ${chalk.gray("Permissions:")} ${perms || "none"}`);
2073
+ console.log(` ${chalk.gray("Created:")} ${new Date(key.created_at).toLocaleDateString()}`);
2074
+ if (key.last_used_at) console.log(` ${chalk.gray("Last used:")} ${new Date(key.last_used_at).toLocaleDateString()}`);
2075
+ console.log("");
2076
+ }
2077
+ } catch (e) {
2078
+ console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
2079
+ console.error(chalk.gray(" Is the Rebase server running?"));
2080
+ process.exit(1);
2081
+ }
2082
+ }
2083
+ async function createKey(rawArgs) {
2084
+ const args = arg({
2085
+ "--name": String,
2086
+ "--permissions": String,
2087
+ "--admin": Boolean,
2088
+ "--rate-limit": Number,
2089
+ "--expires": String,
2090
+ "-n": "--name"
2091
+ }, {
2092
+ argv: rawArgs.slice(4),
2093
+ permissive: true
2094
+ });
2095
+ const name = args["--name"] || args._[0];
2096
+ const permissionsRaw = args["--permissions"];
2097
+ if (!name) {
2098
+ console.error(chalk.red("✗ Name is required."));
2099
+ console.log("");
2100
+ console.log(chalk.gray(" Usage: rebase api-keys create --name \"My Key\" --permissions '[{\"collection\":\"*\",\"operations\":[\"read\"]}]'"));
2101
+ process.exit(1);
2102
+ }
2103
+ let permissions;
2104
+ if (permissionsRaw) try {
2105
+ permissions = JSON.parse(permissionsRaw);
2106
+ } catch {
2107
+ console.error(chalk.red("✗ Invalid --permissions JSON."));
2108
+ console.log(chalk.gray(" Example: '[{\"collection\":\"*\",\"operations\":[\"read\",\"write\"]}]'"));
2109
+ process.exit(1);
2110
+ return;
2111
+ }
2112
+ else permissions = [{
2113
+ collection: "*",
2114
+ operations: [
2115
+ "read",
2116
+ "write",
2117
+ "delete"
2118
+ ]
2119
+ }];
2120
+ let expires_at = null;
2121
+ const expiresFlag = args["--expires"];
2122
+ if (expiresFlag) {
2123
+ const days = {
2124
+ "7d": 7,
2125
+ "30d": 30,
2126
+ "90d": 90,
2127
+ "1y": 365
2128
+ };
2129
+ if (days[expiresFlag]) expires_at = new Date(Date.now() + days[expiresFlag] * 864e5).toISOString();
2130
+ else {
2131
+ const parsed = new Date(expiresFlag);
2132
+ if (isNaN(parsed.getTime())) {
2133
+ console.error(chalk.red("✗ Invalid --expires value. Use 7d, 30d, 90d, 1y, or an ISO date."));
2134
+ process.exit(1);
2135
+ }
2136
+ expires_at = parsed.toISOString();
2137
+ }
2138
+ }
2139
+ const env = loadEnv(requireProjectRoot());
2140
+ const baseUrl = resolveBaseUrl(env);
2141
+ const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
2142
+ if (!serviceKey) {
2143
+ console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
2144
+ process.exit(1);
2145
+ }
2146
+ try {
2147
+ const body = {
2148
+ name,
2149
+ permissions,
2150
+ admin: args["--admin"] ?? false,
2151
+ rate_limit: args["--rate-limit"] ?? null,
2152
+ expires_at
2153
+ };
2154
+ const res = await fetch(`${baseUrl}/api/admin/api-keys`, {
2155
+ method: "POST",
2156
+ headers: {
2157
+ Authorization: `Bearer ${serviceKey}`,
2158
+ "Content-Type": "application/json"
2159
+ },
2160
+ body: JSON.stringify(body)
2161
+ });
2162
+ if (!res.ok) {
2163
+ const errBody = await res.text();
2164
+ console.error(chalk.red(`✗ Failed to create API key: ${res.status} ${errBody}`));
2165
+ process.exit(1);
2166
+ }
2167
+ const { key } = await res.json();
2168
+ console.log("");
2169
+ console.log(chalk.bold.green(" ✓ API key created successfully"));
2170
+ console.log("");
2171
+ console.log(` ${chalk.gray("Name:")} ${key.name}`);
2172
+ console.log(` ${chalk.gray("ID:")} ${key.id}`);
2173
+ console.log(` ${chalk.gray("Prefix:")} ${key.key_prefix}`);
2174
+ console.log("");
2175
+ console.log(chalk.bold.yellow(" ⚠ Copy your key now — it won't be shown again:"));
2176
+ console.log("");
2177
+ console.log(` ${chalk.cyan(key.key)}`);
2178
+ console.log("");
2179
+ } catch (e) {
2180
+ console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
2181
+ console.error(chalk.gray(" Is the Rebase server running?"));
2182
+ process.exit(1);
2183
+ }
2184
+ }
2185
+ async function revokeKey(rawArgs) {
2186
+ const args = arg({ "--id": String }, {
2187
+ argv: rawArgs.slice(4),
2188
+ permissive: true
2189
+ });
2190
+ const id = args["--id"] || args._[0];
2191
+ if (!id) {
2192
+ console.error(chalk.red("✗ Key ID is required."));
2193
+ console.log("");
2194
+ console.log(chalk.gray(" Usage: rebase api-keys revoke <key-id>"));
2195
+ process.exit(1);
2196
+ }
2197
+ const env = loadEnv(requireProjectRoot());
2198
+ const baseUrl = resolveBaseUrl(env);
2199
+ const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
2200
+ if (!serviceKey) {
2201
+ console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
2202
+ process.exit(1);
2203
+ }
2204
+ try {
2205
+ const res = await fetch(`${baseUrl}/api/admin/api-keys/${encodeURIComponent(id)}`, {
2206
+ method: "DELETE",
2207
+ headers: { Authorization: `Bearer ${serviceKey}` }
2208
+ });
2209
+ if (!res.ok) {
2210
+ const errBody = await res.text();
2211
+ console.error(chalk.red(`✗ Failed to revoke API key: ${res.status} ${errBody}`));
2212
+ process.exit(1);
2213
+ }
2214
+ console.log("");
2215
+ console.log(chalk.bold.green(" ✓ API key revoked successfully"));
2216
+ console.log(` ${chalk.gray("ID:")} ${id}`);
2217
+ console.log("");
2218
+ } catch (e) {
2219
+ console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
2220
+ console.error(chalk.gray(" Is the Rebase server running?"));
2221
+ process.exit(1);
2222
+ }
2223
+ }
2224
+ function printApiKeysHelp() {
2225
+ console.log(`
2226
+ ${chalk.bold("rebase api-keys")} — Manage Service API Keys
2227
+
2228
+ ${chalk.green.bold("Usage")}
2229
+ rebase api-keys ${chalk.blue("<command>")} [options]
2230
+
2231
+ ${chalk.green.bold("Commands")}
2232
+ ${chalk.blue.bold("list")} List all API keys
2233
+ ${chalk.blue.bold("create")} Create a new API key
2234
+ ${chalk.blue.bold("revoke")} Revoke an API key
2235
+
2236
+ ${chalk.green.bold("create Options")}
2237
+ ${chalk.blue("--name, -n")} Key name ${chalk.gray("(required)")}
2238
+ ${chalk.blue("--permissions")} JSON array of permissions
2239
+ ${chalk.gray("Default: [{\"collection\":\"*\",\"operations\":[\"read\",\"write\",\"delete\"]}]")}
2240
+ ${chalk.blue("--admin")} Grant admin role (access to admin routes)
2241
+ ${chalk.blue("--rate-limit")} Requests per 15-min window ${chalk.gray("(default: 1000)")}
2242
+ ${chalk.blue("--expires")} Expiration: 7d, 30d, 90d, 1y, or ISO date
2243
+
2244
+ ${chalk.green.bold("revoke Options")}
2245
+ ${chalk.blue("--id")} API key ID to revoke ${chalk.gray("(or positional arg)")}
2246
+
2247
+ ${chalk.green.bold("Examples")}
2248
+ rebase api-keys list
2249
+ rebase api-keys create --name "Analytics" --permissions '[{"collection":"events","operations":["read"]}]'
2250
+ rebase api-keys create -n "Full Access" --expires 90d
2251
+ rebase api-keys revoke abc123-def456
2252
+ `);
2253
+ }
2254
+ //#endregion
2255
+ //#region src/commands/cloud/context.ts
2256
+ /**
2257
+ * Shared foundation for the `rebase cloud` command family.
2258
+ *
2259
+ * Everything cloud subcommands need in common lives here:
2260
+ * - credential storage (~/.rebase/credentials.json, keyed per control-plane host)
2261
+ * - project link file (.rebase/cloud.json in the project dir)
2262
+ * - control-plane URL resolution
2263
+ * - an authenticated `@rebasepro/client` instance (createCloudClient / requireClient)
2264
+ * - small output helpers shared across subcommands
2265
+ *
2266
+ * The control plane is itself a Rebase app, so we reuse the same SDK the web
2267
+ * console uses (`@rebasepro/client`). Auth, token refresh, the data REST client
2268
+ * and function invocation all come from the SDK — the CLI only supplies a
2269
+ * file-backed AuthStorage so a login persists across invocations.
2270
+ */
2271
+ /** Default hosted control plane (the Rebase Cloud console origin). */
2272
+ var DEFAULT_CLOUD_URL = "https://app.rebase.pro";
2273
+ /** The storage key the SDK's auth module reads/writes the session under. */
2274
+ var SDK_SESSION_KEY = "rebase_auth";
2275
+ /** ~/.rebase/credentials.json — one file, many hosts. */
2276
+ function credentialsPath() {
2277
+ return path.join(os.homedir(), ".rebase", "credentials.json");
2278
+ }
2279
+ /** Project-local link file: <project>/.rebase/cloud.json */
2280
+ function projectLinkPath(cwd = process.cwd()) {
2281
+ const root = findProjectRoot(cwd) || cwd;
2282
+ return path.join(root, ".rebase", "cloud.json");
2283
+ }
2284
+ function readCredentials() {
2285
+ try {
2286
+ const raw = fs.readFileSync(credentialsPath(), "utf-8");
2287
+ const parsed = JSON.parse(raw);
2288
+ if (!parsed.contexts) parsed.contexts = {};
2289
+ return parsed;
2290
+ } catch {
2291
+ return { contexts: {} };
2292
+ }
1005
2293
  }
1006
- const __filename$1 = fileURLToPath(import.meta.url);
1007
- const __dirname$1 = path.dirname(__filename$1);
2294
+ function writeCredentials(data) {
2295
+ const file = credentialsPath();
2296
+ fs.mkdirSync(path.dirname(file), { recursive: true });
2297
+ fs.writeFileSync(file, JSON.stringify(data, null, 2), { mode: 384 });
2298
+ try {
2299
+ fs.chmodSync(file, 384);
2300
+ } catch {}
2301
+ }
2302
+ /** Host that a bare `rebase cloud` command should target, if any. */
2303
+ function currentContextUrl() {
2304
+ return readCredentials().current;
2305
+ }
2306
+ /** Persist the active organization id for a host. */
2307
+ function setContextOrg(url, org) {
2308
+ const creds = readCredentials();
2309
+ const entry = creds.contexts[url] || {};
2310
+ if (org) entry.org = org;
2311
+ else delete entry.org;
2312
+ creds.contexts[url] = entry;
2313
+ writeCredentials(creds);
2314
+ }
2315
+ function getContextOrg(url) {
2316
+ return readCredentials().contexts[url]?.org;
2317
+ }
2318
+ function createFileAuthStorage(url) {
2319
+ return {
2320
+ getItem(key) {
2321
+ if (key !== SDK_SESSION_KEY) return null;
2322
+ return readCredentials().contexts[url]?.auth ?? null;
2323
+ },
2324
+ setItem(key, value) {
2325
+ if (key !== SDK_SESSION_KEY) return;
2326
+ const creds = readCredentials();
2327
+ const entry = creds.contexts[url] || {};
2328
+ entry.auth = value;
2329
+ creds.contexts[url] = entry;
2330
+ if (!creds.current) creds.current = url;
2331
+ writeCredentials(creds);
2332
+ },
2333
+ removeItem(key) {
2334
+ if (key !== SDK_SESSION_KEY) return;
2335
+ const creds = readCredentials();
2336
+ if (creds.contexts[url]) {
2337
+ delete creds.contexts[url].auth;
2338
+ delete creds.contexts[url].org;
2339
+ }
2340
+ writeCredentials(creds);
2341
+ }
2342
+ };
2343
+ }
2344
+ /** Mark a host as the active context (called on login). */
2345
+ function setCurrentContext(url) {
2346
+ const creds = readCredentials();
2347
+ creds.current = url;
2348
+ if (!creds.contexts[url]) creds.contexts[url] = {};
2349
+ writeCredentials(creds);
2350
+ }
2351
+ function resolveCloudUrl(rawArgs) {
2352
+ const explicit = arg({ "--url": String }, {
2353
+ argv: rawArgs.slice(2),
2354
+ permissive: true
2355
+ })["--url"] || process.env.REBASE_CLOUD_URL;
2356
+ if (explicit) return normalizeUrl(explicit);
2357
+ const link = readLink();
2358
+ if (link?.url) return normalizeUrl(link.url);
2359
+ const current = currentContextUrl();
2360
+ if (current) return normalizeUrl(current);
2361
+ return DEFAULT_CLOUD_URL;
2362
+ }
2363
+ function normalizeUrl(url) {
2364
+ let u = url.trim().replace(/\/+$/, "");
2365
+ if (!/^https?:\/\//.test(u)) u = `https://${u}`;
2366
+ return u;
2367
+ }
2368
+ /**
2369
+ * Build an SDK client bound to a control-plane host, backed by the on-disk
2370
+ * credential store. `autoRefresh` is disabled so we never leave a dangling
2371
+ * setTimeout that keeps the CLI process alive; token refresh is done on demand
2372
+ * by `requireClient`.
2373
+ */
2374
+ function createCloudClient(url) {
2375
+ return createRebaseClient({
2376
+ baseUrl: url,
2377
+ websocketUrl: "",
2378
+ auth: {
2379
+ storage: createFileAuthStorage(url),
2380
+ persistSession: true,
2381
+ autoRefresh: false
2382
+ }
2383
+ });
2384
+ }
2385
+ /** Two minutes of head-room before a token is treated as expired. */
2386
+ var EXPIRY_BUFFER_MS = 12e4;
2387
+ /**
2388
+ * Return an authenticated client for the resolved host, refreshing the access
2389
+ * token if it is close to expiry. Exits with a helpful message when there is no
2390
+ * usable session (never logged in, or the refresh token was revoked).
2391
+ */
2392
+ async function requireClient(rawArgs) {
2393
+ const url = resolveCloudUrl(rawArgs);
2394
+ const client = createCloudClient(url);
2395
+ const session = client.auth.getSession();
2396
+ if (!session || !session.accessToken) fail(`Not logged in to ${chalk.cyan(url)}.`, `Run ${chalk.bold("rebase cloud login")} first.`);
2397
+ if (session.expiresAt <= Date.now() + EXPIRY_BUFFER_MS) try {
2398
+ await client.auth.refreshSession();
2399
+ } catch {
2400
+ fail(`Your session for ${chalk.cyan(url)} has expired.`, `Run ${chalk.bold("rebase cloud login")} to sign in again.`);
2401
+ }
2402
+ return {
2403
+ client,
2404
+ url
2405
+ };
2406
+ }
2407
+ function readLink(cwd = process.cwd()) {
2408
+ try {
2409
+ return JSON.parse(fs.readFileSync(projectLinkPath(cwd), "utf-8"));
2410
+ } catch {
2411
+ return null;
2412
+ }
2413
+ }
2414
+ function writeLink(link, cwd = process.cwd()) {
2415
+ const file = projectLinkPath(cwd);
2416
+ fs.mkdirSync(path.dirname(file), { recursive: true });
2417
+ fs.writeFileSync(file, JSON.stringify(link, null, 2));
2418
+ }
2419
+ function removeLink(cwd = process.cwd()) {
2420
+ const file = projectLinkPath(cwd);
2421
+ if (fs.existsSync(file)) {
2422
+ fs.rmSync(file);
2423
+ return true;
2424
+ }
2425
+ return false;
2426
+ }
2427
+ /**
2428
+ * Resolve the project id to operate on: explicit `--project` flag wins,
2429
+ * otherwise the linked project. Exits with guidance when neither is present.
2430
+ */
2431
+ function requireProjectId(rawArgs) {
2432
+ const parsed = arg({
2433
+ "--project": String,
2434
+ "-p": "--project"
2435
+ }, {
2436
+ argv: rawArgs.slice(2),
2437
+ permissive: true
2438
+ });
2439
+ if (parsed["--project"]) return parsed["--project"];
2440
+ const link = readLink();
2441
+ if (link?.projectId) return link.projectId;
2442
+ fail("No project specified and this directory is not linked.", `Pass ${chalk.bold("--project <id>")} or run ${chalk.bold("rebase cloud link")}.`);
2443
+ }
2444
+ /** Print an error (+ optional hint) and exit non-zero. Never returns. */
2445
+ function fail(message, hint) {
2446
+ console.error("");
2447
+ console.error(chalk.red(` ✗ ${message}`));
2448
+ if (hint) console.error(chalk.gray(` ${hint}`));
2449
+ console.error("");
2450
+ process.exit(1);
2451
+ }
2452
+ function success(message) {
2453
+ console.log("");
2454
+ console.log(chalk.bold.green(` ✓ ${message}`));
2455
+ console.log("");
2456
+ }
2457
+ /** Colorize a deployment / resource status token. */
2458
+ function colorStatus(status) {
2459
+ switch (status) {
2460
+ case "active":
2461
+ case "success":
2462
+ case "connected": return chalk.green(status);
2463
+ case "deploying":
2464
+ case "provisioning":
2465
+ case "pending_billing":
2466
+ case "untested": return chalk.yellow(status ?? "");
2467
+ case "failed": return chalk.red(status);
2468
+ case "stopped": return chalk.gray(status);
2469
+ default: return chalk.gray(status ?? "unknown");
2470
+ }
2471
+ }
2472
+ /** Render a two-column key/value block with aligned keys. */
2473
+ function keyValues(rows) {
2474
+ const width = Math.max(...rows.map(([k]) => k.length));
2475
+ for (const [k, v] of rows) {
2476
+ if (v === void 0 || v === "") continue;
2477
+ console.log(` ${chalk.gray(`${k}:`.padEnd(width + 1))} ${v}`);
2478
+ }
2479
+ }
2480
+ /**
2481
+ * Surface an SDK/HTTP error consistently. The SDK throws RebaseApiError with
2482
+ * a `.status` and `.message`; anything else falls back to its string form.
2483
+ */
2484
+ function reportError(e, context) {
2485
+ const err = e;
2486
+ fail(`${context}${err?.status ? ` (${err.status})` : ""}: ${err?.message ?? String(e)}`);
2487
+ }
2488
+ /**
2489
+ * Open a URL in the user's default browser (best effort). Always prints the URL
2490
+ * first so it stays usable over SSH or when no browser is available.
2491
+ */
2492
+ function openUrl(target, label = "Opening") {
2493
+ console.log("");
2494
+ console.log(` ${label} ${chalk.cyan(target)}`);
2495
+ console.log("");
2496
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2497
+ try {
2498
+ const child = spawn(opener, [target], {
2499
+ stdio: "ignore",
2500
+ detached: true,
2501
+ shell: process.platform === "win32"
2502
+ });
2503
+ child.on("error", () => {});
2504
+ child.unref();
2505
+ } catch {}
2506
+ }
2507
+ //#endregion
2508
+ //#region src/commands/cloud/auth.ts
2509
+ /**
2510
+ * `rebase cloud` auth subcommands: login, logout, whoami.
2511
+ */
2512
+ async function loginCommand(rawArgs) {
2513
+ const args = arg({
2514
+ "--email": String,
2515
+ "--password": String,
2516
+ "-e": "--email",
2517
+ "-p": "--password"
2518
+ }, {
2519
+ argv: rawArgs.slice(3),
2520
+ permissive: true
2521
+ });
2522
+ const url = resolveCloudUrl(rawArgs);
2523
+ console.log("");
2524
+ console.log(` Signing in to ${chalk.cyan(url)}`);
2525
+ console.log("");
2526
+ const prompts = [];
2527
+ if (!args["--email"]) prompts.push({
2528
+ type: "input",
2529
+ name: "email",
2530
+ message: "Email:"
2531
+ });
2532
+ if (!args["--password"]) prompts.push({
2533
+ type: "password",
2534
+ name: "password",
2535
+ message: "Password:",
2536
+ mask: "•"
2537
+ });
2538
+ const answers = prompts.length ? await inquirer.prompt(prompts) : {};
2539
+ const email = (args["--email"] || answers.email || "").trim();
2540
+ const password = args["--password"] || answers.password || "";
2541
+ if (!email || !password) fail("Email and password are required.");
2542
+ const client = createCloudClient(url);
2543
+ try {
2544
+ const { user } = await client.auth.signInWithEmail(email, password);
2545
+ setCurrentContext(url);
2546
+ try {
2547
+ const orgs = await client.data.collection("organizations").find({ limit: 2 });
2548
+ if (orgs.data.length === 1 && !getContextOrg(url)) setContextOrg(url, String(orgs.data[0].id));
2549
+ } catch {}
2550
+ success(`Logged in as ${chalk.bold(user.email ?? email)}`);
2551
+ keyValues([
2552
+ ["Host", url],
2553
+ ["User", user.email ?? void 0],
2554
+ ["Active org", getContextOrg(url)]
2555
+ ]);
2556
+ console.log("");
2557
+ } catch (e) {
2558
+ if (e?.status === 401) fail("Invalid email or password.");
2559
+ reportError(e, "Login failed");
2560
+ }
2561
+ }
2562
+ async function logoutCommand(rawArgs) {
2563
+ const url = resolveCloudUrl(rawArgs);
2564
+ const client = createCloudClient(url);
2565
+ if (!client.auth.getSession()) {
2566
+ console.log("");
2567
+ console.log(chalk.gray(` Not logged in to ${url}.`));
2568
+ console.log("");
2569
+ return;
2570
+ }
2571
+ try {
2572
+ await client.auth.signOut();
2573
+ } catch {}
2574
+ success(`Logged out of ${url}`);
2575
+ }
2576
+ async function whoamiCommand(rawArgs) {
2577
+ const { client, url } = await requireClient(rawArgs);
2578
+ try {
2579
+ const user = await client.auth.getUser();
2580
+ if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.");
2581
+ const link = readLink();
2582
+ console.log("");
2583
+ console.log(chalk.bold(" 🔐 Rebase Cloud session"));
2584
+ console.log("");
2585
+ keyValues([
2586
+ ["Host", url],
2587
+ ["User", user.email ?? void 0],
2588
+ ["User ID", user.uid],
2589
+ ["Roles", user.roles?.length ? user.roles.join(", ") : void 0],
2590
+ ["Active org", getContextOrg(url)],
2591
+ ["Linked project", link ? `${link.projectName ?? ""} (${link.projectId})`.trim() : void 0]
2592
+ ]);
2593
+ console.log("");
2594
+ } catch (e) {
2595
+ reportError(e, "Failed to fetch session");
2596
+ }
2597
+ }
2598
+ //#endregion
2599
+ //#region src/commands/cloud/link.ts
2600
+ /**
2601
+ * `rebase cloud` context subcommands: link, unlink, use, open.
2602
+ *
2603
+ * `link` associates the current directory with a cloud project by writing
2604
+ * `.rebase/cloud.json`; deploy/logs/status then operate on it with no flags.
2605
+ */
2606
+ async function linkCommand(rawArgs) {
2607
+ const args = arg({
2608
+ "--project": String,
2609
+ "-p": "--project"
2610
+ }, {
2611
+ argv: rawArgs.slice(3),
2612
+ permissive: true
2613
+ });
2614
+ const { client, url } = await requireClient(rawArgs);
2615
+ try {
2616
+ let project;
2617
+ if (args["--project"]) {
2618
+ project = await client.data.collection("projects").findById(args["--project"]);
2619
+ if (!project) fail(`Project ${args["--project"]} not found.`);
2620
+ } else {
2621
+ const org = getContextOrg(url);
2622
+ const projects = (await client.data.collection("projects").find({
2623
+ where: org ? { organization: ["==", org] } : void 0,
2624
+ limit: 100
2625
+ })).data;
2626
+ if (projects.length === 0) fail("No projects found for your account.", `Create one with ${chalk.bold("rebase cloud projects create")}.`);
2627
+ const { picked } = await inquirer.prompt([{
2628
+ type: "list",
2629
+ name: "picked",
2630
+ message: "Select a project to link:",
2631
+ choices: projects.map((p) => ({
2632
+ name: `${p.name ?? "(unnamed)"} ${chalk.gray(`${p.subdomain ?? ""} · ${p.id}`)}`,
2633
+ value: p
2634
+ }))
2635
+ }]);
2636
+ project = picked;
2637
+ }
2638
+ if (!project) fail("No project selected.");
2639
+ writeLink({
2640
+ url,
2641
+ projectId: String(project.id),
2642
+ projectName: project.name,
2643
+ orgId: project.organization !== void 0 ? String(project.organization) : void 0
2644
+ });
2645
+ success(`Linked to ${chalk.bold(project.name ?? project.id)}`);
2646
+ console.log(chalk.gray(` Wrote ${projectLinkPath()}`));
2647
+ console.log("");
2648
+ } catch (e) {
2649
+ reportError(e, "Failed to link project");
2650
+ }
2651
+ }
2652
+ function unlinkCommand() {
2653
+ if (!readLink()) {
2654
+ console.log("");
2655
+ console.log(chalk.gray(" This directory is not linked to a cloud project."));
2656
+ console.log("");
2657
+ return;
2658
+ }
2659
+ removeLink();
2660
+ success("Unlinked from cloud project");
2661
+ }
2662
+ async function selectOrgCommand(rawArgs) {
2663
+ const target = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
2664
+ const { client, url } = await requireClient(rawArgs);
2665
+ try {
2666
+ const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
2667
+ if (orgs.length === 0) fail("You are not a member of any organization.");
2668
+ let chosen = target ? orgs.find((o) => String(o.id) === target || o.slug === target) : void 0;
2669
+ if (!chosen && !target) {
2670
+ const { picked } = await inquirer.prompt([{
2671
+ type: "list",
2672
+ name: "picked",
2673
+ message: "Select the active organization:",
2674
+ choices: orgs.map((o) => ({
2675
+ name: `${o.name ?? "(unnamed)"} ${chalk.gray(`${o.slug ?? ""} · ${o.id}`)}`,
2676
+ value: o
2677
+ }))
2678
+ }]);
2679
+ chosen = picked;
2680
+ }
2681
+ if (!chosen) fail(`Organization "${target}" not found.`);
2682
+ setContextOrg(url, String(chosen.id));
2683
+ success(`Active organization set to ${chalk.bold(chosen.name ?? chosen.id)}`);
2684
+ } catch (e) {
2685
+ reportError(e, "Failed to set organization");
2686
+ }
2687
+ }
2688
+ /** Open the Rebase Cloud dashboard (or the linked project) in a browser. */
2689
+ function openCommand(rawArgs) {
2690
+ const url = resolveCloudUrl(rawArgs);
2691
+ const link = readLink();
2692
+ openUrl(link ? `${url}/projects/${link.projectId}` : url);
2693
+ }
2694
+ //#endregion
2695
+ //#region src/commands/cloud/projects.ts
2696
+ /**
2697
+ * `rebase cloud projects` — list / create / info / delete.
2698
+ */
2699
+ async function listProjects(rawArgs) {
2700
+ const { client, url } = await requireClient(rawArgs);
2701
+ const org = getContextOrg(url);
2702
+ try {
2703
+ const projects = (await client.data.collection("projects").find({
2704
+ where: org ? { organization: ["==", org] } : void 0,
2705
+ orderBy: ["name", "asc"],
2706
+ limit: 100
2707
+ })).data;
2708
+ console.log("");
2709
+ console.log(chalk.bold(" 📦 Projects") + (org ? chalk.gray(` (org ${org})`) : ""));
2710
+ console.log("");
2711
+ if (projects.length === 0) {
2712
+ console.log(chalk.gray(" No projects yet. Create one with `rebase cloud projects create`."));
2713
+ console.log("");
2714
+ return;
2715
+ }
2716
+ const linkedId = readLink()?.projectId;
2717
+ for (const p of projects) {
2718
+ const marker = String(p.id) === linkedId ? chalk.green(" ●") : " ";
2719
+ console.log(`${marker}${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.id}]`)} ${colorStatus(p.status)}`);
2720
+ console.log(` ${chalk.gray(`${p.subdomain ?? "—"}.rebase.pro`)}${p.provider ? chalk.gray(` · ${p.provider}`) : ""}`);
2721
+ }
2722
+ console.log("");
2723
+ } catch (e) {
2724
+ reportError(e, "Failed to list projects");
2725
+ }
2726
+ }
2727
+ /** Default region + VM size per provider (both are required on create). */
2728
+ function providerDefaults(provider) {
2729
+ switch (provider) {
2730
+ case "gcp": return {
2731
+ region: "europe-west1",
2732
+ vmSize: "e2-small"
2733
+ };
2734
+ case "aws": return {
2735
+ region: "us-east-1",
2736
+ vmSize: "t3.small"
2737
+ };
2738
+ default: return {
2739
+ region: "nbg1",
2740
+ vmSize: "cx21"
2741
+ };
2742
+ }
2743
+ }
2744
+ async function createProject(rawArgs) {
2745
+ const args = arg({
2746
+ "--name": String,
2747
+ "--subdomain": String,
2748
+ "--repo": String,
2749
+ "--branch": String,
2750
+ "--provider": String,
2751
+ "--region": String,
2752
+ "--vm-size": String,
2753
+ "--org": String,
2754
+ "--link": Boolean,
2755
+ "-n": "--name"
2756
+ }, {
2757
+ argv: rawArgs.slice(4),
2758
+ permissive: true
2759
+ });
2760
+ const { client, url } = await requireClient(rawArgs);
2761
+ const org = args["--org"] || getContextOrg(url);
2762
+ if (!org) fail("No organization selected.", `Pass ${chalk.bold("--org <id>")} or run ${chalk.bold("rebase cloud use")}.`);
2763
+ const prompts = [];
2764
+ if (!args["--name"]) prompts.push({
2765
+ type: "input",
2766
+ name: "name",
2767
+ message: "Project name:"
2768
+ });
2769
+ if (!args["--subdomain"]) prompts.push({
2770
+ type: "input",
2771
+ name: "subdomain",
2772
+ message: "Subdomain:"
2773
+ });
2774
+ const a = prompts.length && process.stdin.isTTY ? await inquirer.prompt(prompts) : {};
2775
+ const name = (args["--name"] || a.name || "").trim();
2776
+ const subdomain = (args["--subdomain"] || a.subdomain || "").trim().toLowerCase();
2777
+ const gitRepoUrl = (args["--repo"] || a.repo || "").trim();
2778
+ const gitBranch = (args["--branch"] || a.branch || "main").trim();
2779
+ const provider = (args["--provider"] || a.provider || "hetzner").trim();
2780
+ const defaults = providerDefaults(provider);
2781
+ const region = (args["--region"] || defaults.region).trim();
2782
+ const vmSize = (args["--vm-size"] || defaults.vmSize).trim();
2783
+ if (!name || !subdomain) fail("Name and subdomain are required.");
2784
+ try {
2785
+ const check = await client.functions.invoke("check-subdomain", { subdomain });
2786
+ if (!check.available) fail(`Subdomain "${subdomain}" is not available${check.reason ? ` (${check.reason})` : ""}.`);
2787
+ } catch {}
2788
+ try {
2789
+ const user = await client.auth.getUser();
2790
+ if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.");
2791
+ const created = await client.data.collection("projects").create({
2792
+ name,
2793
+ subdomain,
2794
+ gitRepoUrl,
2795
+ gitBranch,
2796
+ provider,
2797
+ region,
2798
+ vmSize,
2799
+ organization: org,
2800
+ createdById: user.uid,
2801
+ status: "provisioning"
2802
+ });
2803
+ success(`Created project ${chalk.bold(name)}`);
2804
+ keyValues([
2805
+ ["ID", String(created.id)],
2806
+ ["URL", `${subdomain}.rebase.pro`],
2807
+ ["Provider", provider],
2808
+ ["Branch", gitBranch]
2809
+ ]);
2810
+ if (args["--link"]) {
2811
+ writeLink({
2812
+ url,
2813
+ projectId: String(created.id),
2814
+ projectName: name,
2815
+ orgId: String(org)
2816
+ });
2817
+ console.log(chalk.gray(" Linked this directory to the new project."));
2818
+ }
2819
+ console.log("");
2820
+ console.log(chalk.gray(` Deploy it with: ${chalk.bold(`rebase cloud deploy --project ${created.id}`)}`));
2821
+ console.log("");
2822
+ } catch (e) {
2823
+ reportError(e, "Failed to create project");
2824
+ }
2825
+ }
2826
+ async function projectInfo(rawArgs, projectId) {
2827
+ const { client } = await requireClient(rawArgs);
2828
+ try {
2829
+ const p = await client.data.collection("projects").findById(projectId);
2830
+ if (!p) fail(`Project ${projectId} not found.`);
2831
+ const [db, lastDeploy] = await Promise.all([firstRow(client, "databases", projectId), latestDeployment(client, projectId)]);
2832
+ console.log("");
2833
+ console.log(` ${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.id}]`)} ${colorStatus(p.status)}`);
2834
+ console.log("");
2835
+ keyValues([
2836
+ ["Subdomain", p.subdomain ? `${p.subdomain}.rebase.pro` : void 0],
2837
+ ["Custom domain", p.customDomain],
2838
+ ["Repository", p.gitRepoUrl],
2839
+ ["Branch", p.gitBranch],
2840
+ ["Provider", p.provider],
2841
+ ["Region", p.region],
2842
+ ["Organization", p.organization !== void 0 ? String(p.organization) : void 0],
2843
+ ["Database", db ? `${db.type} (${colorStatus(db.connectionStatus)})` : "none"],
2844
+ ["Last deploy", lastDeploy ? `${colorStatus(lastDeploy.status)} · ${fmtDate(lastDeploy.createdAt)}` : "never"]
2845
+ ]);
2846
+ console.log("");
2847
+ } catch (e) {
2848
+ reportError(e, "Failed to load project");
2849
+ }
2850
+ }
2851
+ async function deleteProject(rawArgs, projectId) {
2852
+ const args = arg({
2853
+ "--yes": Boolean,
2854
+ "-y": "--yes"
2855
+ }, {
2856
+ argv: rawArgs.slice(2),
2857
+ permissive: true
2858
+ });
2859
+ const { client } = await requireClient(rawArgs);
2860
+ const p = await client.data.collection("projects").findById(projectId).catch(() => void 0);
2861
+ if (!p) fail(`Project ${projectId} not found.`);
2862
+ if (!args["--yes"]) {
2863
+ const { confirmed } = await inquirer.prompt([{
2864
+ type: "confirm",
2865
+ name: "confirmed",
2866
+ default: false,
2867
+ message: `Permanently delete project "${p.name ?? projectId}" (${projectId})? This tears down its deployment.`
2868
+ }]);
2869
+ if (!confirmed) {
2870
+ console.log(chalk.gray(" Aborted."));
2871
+ return;
2872
+ }
2873
+ }
2874
+ try {
2875
+ await client.data.collection("projects").delete(projectId);
2876
+ success(`Deleted project ${chalk.bold(p.name ?? projectId)}`);
2877
+ } catch (e) {
2878
+ reportError(e, "Failed to delete project");
2879
+ }
2880
+ }
2881
+ async function firstRow(client, collection, projectId) {
2882
+ return (await client.data.collection(collection).find({
2883
+ where: { project: ["==", projectId] },
2884
+ limit: 1
2885
+ })).data[0];
2886
+ }
2887
+ async function latestDeployment(client, projectId) {
2888
+ return (await client.data.collection("deployments").find({
2889
+ where: { project: ["==", projectId] },
2890
+ orderBy: ["createdAt", "desc"],
2891
+ limit: 1
2892
+ })).data[0];
2893
+ }
2894
+ function fmtDate(value) {
2895
+ if (!value) return "—";
2896
+ const d = new Date(value);
2897
+ return isNaN(d.getTime()) ? value : d.toLocaleString();
2898
+ }
2899
+ //#endregion
2900
+ //#region src/commands/cloud/deploy.ts
2901
+ /**
2902
+ * `rebase cloud deploy` and `rebase cloud logs`.
2903
+ *
2904
+ * `deploy` triggers the control-plane `deploy` function, then tails the build
2905
+ * logs from the deployment record until it succeeds or fails. `logs` shows the
2906
+ * latest build log, or runtime logs with `--runtime`.
2907
+ */
2908
+ var POLL_INTERVAL_MS = 1500;
2909
+ var POLL_TIMEOUT_MS = 900 * 1e3;
2910
+ function sleep(ms) {
2911
+ return new Promise((r) => setTimeout(r, ms));
2912
+ }
2913
+ function run(cmd, cmdArgs, cwd) {
2914
+ return new Promise((resolve, reject) => {
2915
+ const child = spawn(cmd, cmdArgs, {
2916
+ cwd,
2917
+ stdio: [
2918
+ "ignore",
2919
+ "ignore",
2920
+ "pipe"
2921
+ ]
2922
+ });
2923
+ let stderr = "";
2924
+ child.stderr.on("data", (d) => stderr += d.toString());
2925
+ child.on("error", reject);
2926
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr || `${cmd} exited ${code}`)));
2927
+ });
2928
+ }
2929
+ /**
2930
+ * Package `sourceDir` into a gzipped tarball, honoring `.gitignore`/`.rebaseignore`
2931
+ * and always excluding `.git` and `node_modules`. Returns the temp archive path.
2932
+ */
2933
+ async function createSourceTarball(sourceDir) {
2934
+ const dir = path.resolve(sourceDir);
2935
+ if (!fs.existsSync(dir)) fail(`Source directory not found: ${dir}`);
2936
+ const tarPath = path.join(os.tmpdir(), `rebase-src-${Date.now()}.tar.gz`);
2937
+ const tarArgs = [
2938
+ "-czf",
2939
+ tarPath,
2940
+ "--exclude=.git",
2941
+ "--exclude=node_modules"
2942
+ ];
2943
+ for (const ignore of [".gitignore", ".rebaseignore"]) if (fs.existsSync(path.join(dir, ignore))) tarArgs.push(`--exclude-from=${ignore}`);
2944
+ tarArgs.push(".");
2945
+ try {
2946
+ await run("tar", tarArgs, dir);
2947
+ } catch (e) {
2948
+ fail(`Failed to package source: ${e instanceof Error ? e.message : String(e)}`);
2949
+ }
2950
+ return tarPath;
2951
+ }
2952
+ /** Upload a build-context tarball; returns the opaque `source` ref for deploy. */
2953
+ async function uploadSource(url, token, projectId, tarPath) {
2954
+ const bytes = fs.readFileSync(tarPath);
2955
+ const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
2956
+ console.log(chalk.gray(` Uploading source (${sizeMb} MB)...`));
2957
+ const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
2958
+ method: "POST",
2959
+ headers: {
2960
+ Authorization: `Bearer ${token}`,
2961
+ "Content-Type": "application/gzip"
2962
+ },
2963
+ body: bytes
2964
+ });
2965
+ if (!res.ok) {
2966
+ const body = await res.text().catch(() => "");
2967
+ fail(`Source upload failed (${res.status}): ${body || res.statusText}`);
2968
+ }
2969
+ const data = await res.json();
2970
+ if (!data.source) fail("Upload endpoint did not return a source reference.");
2971
+ return data.source;
2972
+ }
2973
+ async function deployCommand(rawArgs, projectId) {
2974
+ const args = arg({
2975
+ "--no-follow": Boolean,
2976
+ "--source": String
2977
+ }, {
2978
+ argv: rawArgs.slice(2),
2979
+ permissive: true
2980
+ });
2981
+ const { client, url } = await requireClient(rawArgs);
2982
+ let source;
2983
+ if (args["--source"]) {
2984
+ const tarPath = await createSourceTarball(args["--source"]);
2985
+ try {
2986
+ const token = client.auth.getSession()?.accessToken;
2987
+ if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
2988
+ source = await uploadSource(url, token, projectId, tarPath);
2989
+ } finally {
2990
+ fs.rmSync(tarPath, { force: true });
2991
+ }
2992
+ }
2993
+ console.log("");
2994
+ console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectId)}${source ? " from uploaded source" : ""}...`);
2995
+ let deploymentId;
2996
+ try {
2997
+ const res = await client.functions.invoke("deploy", source ? {
2998
+ projectId,
2999
+ source
3000
+ } : { projectId });
3001
+ if (!res?.deployment?.id) fail("Control plane did not return a deployment id.");
3002
+ deploymentId = String(res.deployment.id);
3003
+ } catch (e) {
3004
+ const err = e;
3005
+ if (err?.status === 409) fail("A deployment is already in progress for this project.");
3006
+ if (err?.status === 402) fail(err.message || "Payment required before deploying.", "Attach a card once with `rebase cloud billing setup`, then deploy again.");
3007
+ reportError(e, "Failed to trigger deployment");
3008
+ }
3009
+ console.log(chalk.gray(` Deployment ${deploymentId} created.`));
3010
+ if (args["--no-follow"]) {
3011
+ console.log(chalk.gray(" Not following logs (--no-follow). Check status with `rebase cloud logs`."));
3012
+ console.log("");
3013
+ return;
3014
+ }
3015
+ console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
3016
+ console.log("");
3017
+ await streamBuildLogs(client, deploymentId);
3018
+ }
3019
+ /** Poll a deployment record and print new log output as it arrives. */
3020
+ async function streamBuildLogs(client, deploymentId) {
3021
+ let printed = 0;
3022
+ const started = Date.now();
3023
+ for (;;) {
3024
+ let dep;
3025
+ try {
3026
+ dep = await client.data.collection("deployments").findById(deploymentId);
3027
+ } catch (e) {
3028
+ reportError(e, "Failed to read deployment status");
3029
+ }
3030
+ if (!dep) fail(`Deployment ${deploymentId} disappeared.`);
3031
+ const logs = dep.logs ?? "";
3032
+ if (logs.length > printed) {
3033
+ process.stdout.write(logs.slice(printed));
3034
+ printed = logs.length;
3035
+ }
3036
+ if (dep.status && dep.status !== "deploying") {
3037
+ console.log("");
3038
+ if (dep.status === "success") console.log(chalk.bold.green(" ✓ Deployment succeeded"));
3039
+ else {
3040
+ console.log(chalk.bold.red(` ✗ Deployment ${dep.status}`));
3041
+ console.log("");
3042
+ process.exit(1);
3043
+ }
3044
+ console.log("");
3045
+ return;
3046
+ }
3047
+ if (Date.now() - started > POLL_TIMEOUT_MS) {
3048
+ console.log("");
3049
+ fail("Timed out waiting for the build to finish.", "The deployment may still be running — check `rebase cloud logs`.");
3050
+ }
3051
+ await sleep(POLL_INTERVAL_MS);
3052
+ }
3053
+ }
3054
+ async function logsCommand(rawArgs, projectId) {
3055
+ const args = arg({
3056
+ "--runtime": Boolean,
3057
+ "--follow": Boolean,
3058
+ "-f": "--follow"
3059
+ }, {
3060
+ argv: rawArgs.slice(2),
3061
+ permissive: true
3062
+ });
3063
+ const { client } = await requireClient(rawArgs);
3064
+ if (args["--runtime"]) {
3065
+ try {
3066
+ const res = await client.functions.invoke("runtime-logs", void 0, {
3067
+ method: "GET",
3068
+ path: projectId
3069
+ });
3070
+ console.log("");
3071
+ console.log(chalk.bold(` 📄 Runtime logs — project ${projectId}`));
3072
+ console.log("");
3073
+ console.log(res.logs ?? chalk.gray(" (no logs)"));
3074
+ console.log("");
3075
+ } catch (e) {
3076
+ reportError(e, "Failed to fetch runtime logs");
3077
+ }
3078
+ return;
3079
+ }
3080
+ try {
3081
+ const dep = await latestDeployment(client, projectId);
3082
+ if (!dep) {
3083
+ console.log("");
3084
+ console.log(chalk.gray(" No deployments yet for this project."));
3085
+ console.log("");
3086
+ return;
3087
+ }
3088
+ console.log("");
3089
+ console.log(chalk.bold(` 📄 Build logs — deployment ${dep.id}`) + ` ${colorStatus(dep.status)}`);
3090
+ console.log("");
3091
+ if (args["--follow"] && dep.status === "deploying") await streamBuildLogs(client, String(dep.id));
3092
+ else {
3093
+ console.log(dep.logs ?? chalk.gray(" (no logs)"));
3094
+ console.log("");
3095
+ }
3096
+ } catch (e) {
3097
+ reportError(e, "Failed to fetch build logs");
3098
+ }
3099
+ }
3100
+ //#endregion
3101
+ //#region src/commands/cloud/orgs.ts
3102
+ /**
3103
+ * `rebase cloud orgs` — list / create / members.
3104
+ */
3105
+ async function orgsCommand(subcommand, rawArgs) {
3106
+ switch (subcommand) {
3107
+ case "list":
3108
+ case void 0:
3109
+ await listOrgs(rawArgs);
3110
+ break;
3111
+ case "create":
3112
+ await createOrg(rawArgs);
3113
+ break;
3114
+ case "members":
3115
+ await listMembers(rawArgs);
3116
+ break;
3117
+ case "--help":
3118
+ printOrgsHelp();
3119
+ break;
3120
+ default: fail(`Unknown orgs command: ${subcommand}`);
3121
+ }
3122
+ }
3123
+ async function listOrgs(rawArgs) {
3124
+ const { client, url } = await requireClient(rawArgs);
3125
+ try {
3126
+ const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
3127
+ const active = getContextOrg(url);
3128
+ console.log("");
3129
+ console.log(chalk.bold(" 🏢 Organizations"));
3130
+ console.log("");
3131
+ if (orgs.length === 0) {
3132
+ console.log(chalk.gray(" You are not a member of any organization."));
3133
+ console.log("");
3134
+ return;
3135
+ }
3136
+ for (const o of orgs) {
3137
+ const marker = String(o.id) === active ? chalk.green(" ●") : " ";
3138
+ console.log(`${marker}${chalk.bold(o.name ?? "(unnamed)")} ${chalk.gray(`[${o.id}]`)}${o.slug ? chalk.gray(` ${o.slug}`) : ""}`);
3139
+ }
3140
+ console.log("");
3141
+ console.log(chalk.gray(" ● = active organization. Switch with `rebase cloud use <id>`."));
3142
+ console.log("");
3143
+ } catch (e) {
3144
+ reportError(e, "Failed to list organizations");
3145
+ }
3146
+ }
3147
+ async function createOrg(rawArgs) {
3148
+ const args = arg({
3149
+ "--name": String,
3150
+ "--slug": String,
3151
+ "-n": "--name"
3152
+ }, {
3153
+ argv: rawArgs.slice(4),
3154
+ permissive: true
3155
+ });
3156
+ const { client, url } = await requireClient(rawArgs);
3157
+ const prompts = [];
3158
+ if (!args["--name"]) prompts.push({
3159
+ type: "input",
3160
+ name: "name",
3161
+ message: "Organization name:"
3162
+ });
3163
+ const answers = prompts.length ? await inquirer.prompt(prompts) : {};
3164
+ const name = (args["--name"] || answers.name || "").trim();
3165
+ if (!name) fail("Organization name is required.");
3166
+ const slug = (args["--slug"] || slugify(name)).trim();
3167
+ try {
3168
+ const created = await client.data.collection("organizations").create({
3169
+ name,
3170
+ slug,
3171
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3172
+ });
3173
+ setContextOrg(url, String(created.id));
3174
+ success(`Created organization ${chalk.bold(name)} and set it active`);
3175
+ } catch (e) {
3176
+ reportError(e, "Failed to create organization");
3177
+ }
3178
+ }
3179
+ async function listMembers(rawArgs) {
3180
+ const { client, url } = await requireClient(rawArgs);
3181
+ const org = getContextOrg(url);
3182
+ if (!org) fail("No active organization.", "Run `rebase cloud use` first.");
3183
+ try {
3184
+ const members = (await client.data.collection("organization-members").find({
3185
+ where: { organization: ["==", org] },
3186
+ limit: 200
3187
+ })).data;
3188
+ console.log("");
3189
+ console.log(chalk.bold(` 👥 Members — org ${org}`));
3190
+ console.log("");
3191
+ if (members.length === 0) {
3192
+ console.log(chalk.gray(" No members found."));
3193
+ console.log("");
3194
+ return;
3195
+ }
3196
+ for (const m of members) console.log(` ${chalk.bold(m.userId ?? "?")} ${colorStatus(m.role)}`);
3197
+ console.log("");
3198
+ } catch (e) {
3199
+ reportError(e, "Failed to list members");
3200
+ }
3201
+ }
3202
+ function slugify(s) {
3203
+ return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3204
+ }
3205
+ function printOrgsHelp() {
3206
+ console.log(`
3207
+ ${chalk.bold("rebase cloud orgs")} — Manage organizations
3208
+
3209
+ ${chalk.green.bold("Commands")}
3210
+ ${chalk.blue.bold("list")} List organizations you belong to
3211
+ ${chalk.blue.bold("create")} Create a new organization ${chalk.gray("(--name, --slug)")}
3212
+ ${chalk.blue.bold("members")} List members of the active organization
3213
+ `);
3214
+ }
3215
+ //#endregion
3216
+ //#region src/commands/cloud/databases.ts
3217
+ /**
3218
+ * `rebase cloud db` — database + backup management for a project.
3219
+ *
3220
+ * db list List databases attached to the project
3221
+ * db create Attach a managed or bring-your-own database
3222
+ * db test Test connectivity to the project's database
3223
+ * db backup list|create|restore
3224
+ */
3225
+ async function dbCommand$1(subcommand, rawArgs) {
3226
+ switch (subcommand) {
3227
+ case "list":
3228
+ case void 0:
3229
+ await listDatabases(rawArgs);
3230
+ break;
3231
+ case "create":
3232
+ await createDatabase(rawArgs);
3233
+ break;
3234
+ case "test":
3235
+ await testDatabase(rawArgs);
3236
+ break;
3237
+ case "backup":
3238
+ await backupCommand(rawArgs);
3239
+ break;
3240
+ case "--help":
3241
+ printDbHelp();
3242
+ break;
3243
+ default: fail(`Unknown db command: ${subcommand}`);
3244
+ }
3245
+ }
3246
+ async function listDatabases(rawArgs) {
3247
+ const projectId = requireProjectId(rawArgs);
3248
+ const { client } = await requireClient(rawArgs);
3249
+ try {
3250
+ const dbs = (await client.data.collection("databases").find({
3251
+ where: { project: ["==", projectId] },
3252
+ limit: 50
3253
+ })).data;
3254
+ console.log("");
3255
+ console.log(chalk.bold(` 🗄 Databases — project ${projectId}`));
3256
+ console.log("");
3257
+ if (dbs.length === 0) {
3258
+ console.log(chalk.gray(" No database attached. Add one with `rebase cloud db create`."));
3259
+ console.log("");
3260
+ return;
3261
+ }
3262
+ for (const d of dbs) {
3263
+ console.log(` ${chalk.bold(d.type ?? "unknown")} ${chalk.gray(`[${d.id}]`)} ${colorStatus(d.connectionStatus)}`);
3264
+ keyValues([["SSH tunnel", d.useSshTunnel ? "yes" : void 0], ["PITR", d.pitrEnabled ? "enabled" : void 0]]);
3265
+ }
3266
+ console.log("");
3267
+ } catch (e) {
3268
+ reportError(e, "Failed to list databases");
3269
+ }
3270
+ }
3271
+ async function createDatabase(rawArgs) {
3272
+ const args = arg({
3273
+ "--type": String,
3274
+ "--connection-string": String,
3275
+ "--project": String,
3276
+ "-p": "--project"
3277
+ }, {
3278
+ argv: rawArgs.slice(4),
3279
+ permissive: true
3280
+ });
3281
+ const projectId = requireProjectId(rawArgs);
3282
+ const { client } = await requireClient(rawArgs);
3283
+ let type = args["--type"];
3284
+ if (!type) {
3285
+ const { picked } = await inquirer.prompt([{
3286
+ type: "list",
3287
+ name: "picked",
3288
+ message: "Database type:",
3289
+ choices: [{
3290
+ name: "SaaS Managed (provisioned for you)",
3291
+ value: "managed"
3292
+ }, {
3293
+ name: "Bring Your Own DB (external PostgreSQL)",
3294
+ value: "byodb"
3295
+ }]
3296
+ }]);
3297
+ type = picked;
3298
+ }
3299
+ let connectionString = args["--connection-string"];
3300
+ if (type === "byodb" && !connectionString) {
3301
+ const { cs } = await inquirer.prompt([{
3302
+ type: "input",
3303
+ name: "cs",
3304
+ message: "PostgreSQL connection string:"
3305
+ }]);
3306
+ connectionString = cs?.trim();
3307
+ if (!connectionString) fail("A connection string is required for bring-your-own databases.");
3308
+ }
3309
+ try {
3310
+ const created = await client.data.collection("databases").create({
3311
+ project: projectId,
3312
+ type,
3313
+ connectionString: type === "byodb" ? connectionString : void 0,
3314
+ connectionStatus: "untested"
3315
+ });
3316
+ success(`Attached ${type} database to project ${projectId}`);
3317
+ keyValues([["ID", String(created.id)]]);
3318
+ if (type === "byodb") {
3319
+ console.log(chalk.gray(" Verify it with `rebase cloud db test`."));
3320
+ console.log("");
3321
+ }
3322
+ } catch (e) {
3323
+ reportError(e, "Failed to attach database");
3324
+ }
3325
+ }
3326
+ async function testDatabase(rawArgs) {
3327
+ const projectId = requireProjectId(rawArgs);
3328
+ const { client } = await requireClient(rawArgs);
3329
+ console.log("");
3330
+ console.log(` Testing database connectivity for project ${chalk.bold(projectId)}...`);
3331
+ try {
3332
+ const res = await client.functions.invoke("db-test", { projectId });
3333
+ console.log("");
3334
+ if (res.logs) console.log(res.logs);
3335
+ if (res.success) success("Database connection succeeded");
3336
+ else fail("Database connection failed. See logs above.");
3337
+ } catch (e) {
3338
+ reportError(e, "Failed to test database");
3339
+ }
3340
+ }
3341
+ async function backupCommand(rawArgs) {
3342
+ const action = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[2] || "list";
3343
+ const projectId = requireProjectId(rawArgs);
3344
+ const { client } = await requireClient(rawArgs);
3345
+ try {
3346
+ if (action === "create") {
3347
+ console.log("");
3348
+ console.log(` Creating backup for project ${chalk.bold(projectId)}...`);
3349
+ const res = await client.functions.invoke("backup", {
3350
+ projectId,
3351
+ type: "manual"
3352
+ }, { path: "create" });
3353
+ if (res.success) success(`Backup created: ${res.backup?.filename ?? "(unknown)"}`);
3354
+ else fail(res.error || "Backup failed.");
3355
+ return;
3356
+ }
3357
+ if (action === "restore") {
3358
+ const filename = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[3];
3359
+ if (!filename) fail("Usage: rebase cloud db backup restore <filename>");
3360
+ const { confirmed } = await inquirer.prompt([{
3361
+ type: "confirm",
3362
+ name: "confirmed",
3363
+ default: false,
3364
+ message: `Restore "${filename}" over the current database for project ${projectId}?`
3365
+ }]);
3366
+ if (!confirmed) {
3367
+ console.log(chalk.gray(" Aborted."));
3368
+ return;
3369
+ }
3370
+ const res = await client.functions.invoke("backup", {
3371
+ projectId,
3372
+ filename
3373
+ }, { path: "restore" });
3374
+ if (res.success) success(res.message || "Restore complete");
3375
+ else fail(res.error || "Restore failed.");
3376
+ return;
3377
+ }
3378
+ const res = await client.functions.invoke("backup", void 0, {
3379
+ method: "GET",
3380
+ path: `list/${projectId}`
3381
+ });
3382
+ console.log("");
3383
+ console.log(chalk.bold(` 💾 Backups — project ${projectId}`));
3384
+ console.log("");
3385
+ if (!res.backups?.length) {
3386
+ console.log(chalk.gray(" No backups yet. Create one with `rebase cloud db backup create`."));
3387
+ console.log("");
3388
+ return;
3389
+ }
3390
+ for (const b of res.backups) {
3391
+ const size = b.size !== void 0 ? `${(b.size / 1024 / 1024).toFixed(1)} MB` : "";
3392
+ console.log(` ${chalk.bold(b.filename)} ${chalk.gray(`${b.type ?? ""} ${size}`.trim())}`);
3393
+ }
3394
+ console.log("");
3395
+ } catch (e) {
3396
+ reportError(e, "Backup operation failed");
3397
+ }
3398
+ }
3399
+ function printDbHelp() {
3400
+ console.log(`
3401
+ ${chalk.bold("rebase cloud db")} — Database & backups
3402
+
3403
+ ${chalk.green.bold("Commands")}
3404
+ ${chalk.blue.bold("list")} List databases attached to the project
3405
+ ${chalk.blue.bold("create")} Attach a managed or bring-your-own database
3406
+ ${chalk.blue.bold("test")} Test database connectivity
3407
+ ${chalk.blue.bold("backup list")} List backups
3408
+ ${chalk.blue.bold("backup create")} Create a manual backup
3409
+ ${chalk.blue.bold("backup restore")} ${chalk.gray("<file>")} Restore a backup
3410
+
3411
+ ${chalk.green.bold("Options")}
3412
+ ${chalk.blue("--project, -p")} Target project id ${chalk.gray("(defaults to the linked project)")}
3413
+ ${chalk.blue("--type")} managed | byodb ${chalk.gray("(create)")}
3414
+ ${chalk.blue("--connection-string")} External DB URL ${chalk.gray("(byodb)")}
3415
+ `);
3416
+ }
3417
+ //#endregion
3418
+ //#region src/commands/cloud/resources.ts
3419
+ /**
3420
+ * `rebase cloud` resource subcommands: status, metrics, webhooks, storage,
3421
+ * clusters, billing.
3422
+ */
3423
+ async function statusCommand(rawArgs) {
3424
+ const projectId = requireProjectId(rawArgs);
3425
+ const { client } = await requireClient(rawArgs);
3426
+ try {
3427
+ const project = await client.data.collection("projects").findById(projectId);
3428
+ if (!project) fail(`Project ${projectId} not found.`);
3429
+ const [db, storage, deploy] = await Promise.all([
3430
+ firstRow(client, "databases", projectId),
3431
+ firstRow(client, "storages", projectId),
3432
+ latestDeployment(client, projectId)
3433
+ ]);
3434
+ console.log("");
3435
+ console.log(` ${chalk.bold(project.name ?? projectId)} ${chalk.gray(`[${projectId}]`)} ${colorStatus(project.status)}`);
3436
+ console.log("");
3437
+ keyValues([
3438
+ ["URL", project.subdomain ? `${project.subdomain}.rebase.pro` : void 0],
3439
+ ["Branch", project.gitBranch],
3440
+ ["Last deploy", deploy ? `${colorStatus(deploy.status)} · ${fmtDate(deploy.createdAt)}` : "never"],
3441
+ ["Database", db ? `${db.type} (${colorStatus(db.connectionStatus)})` : "none"],
3442
+ ["Storage", storage ? `${storage.type} (${colorStatus(storage.status)})` : "none"]
3443
+ ]);
3444
+ console.log("");
3445
+ } catch (e) {
3446
+ reportError(e, "Failed to load status");
3447
+ }
3448
+ }
3449
+ async function metricsCommand(rawArgs) {
3450
+ const projectId = requireProjectId(rawArgs);
3451
+ const { client } = await requireClient(rawArgs);
3452
+ try {
3453
+ const m = await client.functions.invoke("metrics", void 0, {
3454
+ method: "GET",
3455
+ path: projectId
3456
+ });
3457
+ console.log("");
3458
+ console.log(chalk.bold(` 📊 Metrics — project ${projectId}`));
3459
+ console.log("");
3460
+ keyValues([
3461
+ ["Status", m.status ? colorStatus(m.status === "running" ? "active" : m.status) : void 0],
3462
+ ["CPU", m.cpu],
3463
+ ["Memory", m.memory ? `${m.memory}${m.memoryPercent ? ` (${m.memoryPercent})` : ""}` : void 0],
3464
+ ["Disk", m.disk]
3465
+ ]);
3466
+ console.log("");
3467
+ } catch (e) {
3468
+ reportError(e, "Failed to fetch metrics");
3469
+ }
3470
+ }
3471
+ async function webhooksCommand(subcommand, rawArgs) {
3472
+ const projectId = requireProjectId(rawArgs);
3473
+ const { client } = await requireClient(rawArgs);
3474
+ try {
3475
+ if (subcommand === "create") {
3476
+ const args = arg({
3477
+ "--name": String,
3478
+ "--table": String,
3479
+ "--url": String,
3480
+ "--events": String
3481
+ }, {
3482
+ argv: rawArgs.slice(4),
3483
+ permissive: true
3484
+ });
3485
+ const name = args["--name"] || fail("--name is required.");
3486
+ const table = args["--table"] || fail("--table is required.");
3487
+ const url = args["--url"] || fail("--url (endpoint) is required.");
3488
+ const events = (args["--events"] || "insert,update,delete").split(",").map((s) => s.trim());
3489
+ const created = await client.data.collection("webhooks").create({
3490
+ project: projectId,
3491
+ name,
3492
+ table,
3493
+ url,
3494
+ events,
3495
+ enabled: true
3496
+ });
3497
+ success(`Created webhook ${chalk.bold(name)} [${created.id}]`);
3498
+ return;
3499
+ }
3500
+ if (subcommand === "delete") {
3501
+ const id = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[2];
3502
+ if (!id) fail("Usage: rebase cloud webhooks delete <id>");
3503
+ await client.data.collection("webhooks").delete(id);
3504
+ success(`Deleted webhook ${id}`);
3505
+ return;
3506
+ }
3507
+ const hooks = (await client.data.collection("webhooks").find({
3508
+ where: { project: ["==", projectId] },
3509
+ limit: 100
3510
+ })).data;
3511
+ console.log("");
3512
+ console.log(chalk.bold(` 🔗 Webhooks — project ${projectId}`));
3513
+ console.log("");
3514
+ if (hooks.length === 0) {
3515
+ console.log(chalk.gray(" No webhooks. Add one with `rebase cloud webhooks create`."));
3516
+ console.log("");
3517
+ return;
3518
+ }
3519
+ for (const h of hooks) {
3520
+ const state = h.enabled ? chalk.green("enabled") : chalk.gray("disabled");
3521
+ console.log(` ${chalk.bold(h.name ?? "(unnamed)")} ${chalk.gray(`[${h.id}]`)} ${state}`);
3522
+ console.log(` ${chalk.gray(`${h.table ?? "?"} → ${h.url ?? "?"} (${(h.events ?? []).join(", ")})`)}`);
3523
+ }
3524
+ console.log("");
3525
+ } catch (e) {
3526
+ reportError(e, "Webhook operation failed");
3527
+ }
3528
+ }
3529
+ async function storageCommand(rawArgs) {
3530
+ const projectId = requireProjectId(rawArgs);
3531
+ const { client } = await requireClient(rawArgs);
3532
+ try {
3533
+ const stores = (await client.data.collection("storages").find({
3534
+ where: { project: ["==", projectId] },
3535
+ limit: 50
3536
+ })).data;
3537
+ console.log("");
3538
+ console.log(chalk.bold(` 🪣 Storage — project ${projectId}`));
3539
+ console.log("");
3540
+ if (stores.length === 0) {
3541
+ console.log(chalk.gray(" No storage buckets attached."));
3542
+ console.log("");
3543
+ return;
3544
+ }
3545
+ for (const s of stores) {
3546
+ console.log(` ${chalk.bold(s.bucketName ?? s.type ?? "bucket")} ${chalk.gray(`[${s.id}]`)} ${colorStatus(s.status)}`);
3547
+ keyValues([["Provider", s.provider], ["Type", s.type]]);
3548
+ }
3549
+ console.log("");
3550
+ } catch (e) {
3551
+ reportError(e, "Failed to list storage");
3552
+ }
3553
+ }
3554
+ async function clustersCommand(rawArgs) {
3555
+ const { client } = await requireClient(rawArgs);
3556
+ try {
3557
+ const clusters = (await client.data.collection("clusters").find({ limit: 100 })).data;
3558
+ console.log("");
3559
+ console.log(chalk.bold(" ☸ Clusters"));
3560
+ console.log("");
3561
+ if (clusters.length === 0) {
3562
+ console.log(chalk.gray(" No clusters registered."));
3563
+ console.log("");
3564
+ return;
3565
+ }
3566
+ for (const c of clusters) {
3567
+ console.log(` ${chalk.bold(c.name ?? "(unnamed)")} ${chalk.gray(`[${c.id}]`)} ${colorStatus(c.status)}`);
3568
+ keyValues([["Provider", c.provider], ["Region", c.region]]);
3569
+ }
3570
+ console.log("");
3571
+ } catch (e) {
3572
+ reportError(e, "Failed to list clusters");
3573
+ }
3574
+ }
3575
+ async function billingCommand(rawArgs) {
3576
+ const { client, url } = await requireClient(rawArgs);
3577
+ const org = getContextOrg(url);
3578
+ const action = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
3579
+ if (action === "setup") {
3580
+ if (!org) fail("No active organization.", "Run `rebase cloud use` first.");
3581
+ try {
3582
+ const res = await client.functions.invoke("stripe-billing", { organizationId: org }, { path: "setup-session" });
3583
+ if (!res.url) fail("Could not start billing setup.");
3584
+ openUrl(res.url, "Add a payment method in your browser:");
3585
+ if (res.simulated) {
3586
+ console.log(chalk.gray(" (dev mode — Stripe not configured; complete setup from the console)"));
3587
+ console.log("");
3588
+ } else {
3589
+ console.log(chalk.gray(" Once you've added a card, `rebase cloud deploy` runs without further prompts."));
3590
+ console.log("");
3591
+ }
3592
+ } catch (e) {
3593
+ reportError(e, "Failed to start billing setup");
3594
+ }
3595
+ return;
3596
+ }
3597
+ if (action === "checkout") {
3598
+ const projectId = requireProjectId(rawArgs);
3599
+ try {
3600
+ const res = await client.functions.invoke("stripe-billing", { projectId }, { path: "session" });
3601
+ if (!res.url) fail("Billing session could not be created.");
3602
+ console.log("");
3603
+ console.log(" Complete checkout in your browser:");
3604
+ console.log(` ${chalk.cyan(res.url)}`);
3605
+ console.log("");
3606
+ } catch (e) {
3607
+ reportError(e, "Failed to start checkout");
3608
+ }
3609
+ return;
3610
+ }
3611
+ if (!org) fail("No active organization.", "Run `rebase cloud use` first.");
3612
+ try {
3613
+ const orgRow = await client.data.collection("organizations").findById(org);
3614
+ const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
3615
+ if (!billingId) {
3616
+ console.log("");
3617
+ console.log(chalk.gray(` Organization ${org} has no billing account yet.`));
3618
+ console.log("");
3619
+ return;
3620
+ }
3621
+ const acct = await client.data.collection("billing-accounts").findById(billingId);
3622
+ let card = {};
3623
+ try {
3624
+ card = await client.functions.invoke("stripe-billing", void 0, {
3625
+ method: "GET",
3626
+ path: `payment-method/${org}`
3627
+ });
3628
+ } catch {}
3629
+ let plan;
3630
+ try {
3631
+ const projectId = arg({
3632
+ "--project": String,
3633
+ "-p": "--project"
3634
+ }, {
3635
+ argv: rawArgs.slice(2),
3636
+ permissive: true
3637
+ })["--project"] || readLink()?.projectId;
3638
+ if (projectId) {
3639
+ const proj = await client.data.collection("projects").findById(projectId);
3640
+ const hasCluster = proj?.cluster_id != null || proj?.cluster != null;
3641
+ plan = hasCluster ? "platform fee (own cluster)" : "managed compute";
3642
+ try {
3643
+ const pricing = await client.functions.invoke("pricing", void 0, { method: "GET" });
3644
+ const key = hasCluster ? "platform_byo" : `compute_${proj?.provider || "hetzner"}_${proj?.vmSize || "cx21"}`;
3645
+ const item = pricing.items?.find((i) => i.lookupKey === key);
3646
+ if (item) plan = `${plan} — €${item.amountEur.toFixed(2)}/mo`;
3647
+ } catch {}
3648
+ }
3649
+ } catch {}
3650
+ console.log("");
3651
+ console.log(chalk.bold(` 💳 Billing — org ${org}`));
3652
+ console.log("");
3653
+ keyValues([
3654
+ ["Account", acct ? String(acct.id) : void 0],
3655
+ ["Email", acct?.billingEmail],
3656
+ ["Status", acct?.status ? colorStatus(acct.status) : void 0],
3657
+ ["Plan", plan],
3658
+ ["Payment method", card.hasPaymentMethod ? `${card.brand ?? "card"} •••• ${card.last4 ?? "????"}${card.expMonth ? ` (exp ${card.expMonth}/${card.expYear})` : ""}` : chalk.yellow("none — run `rebase cloud billing setup`")]
3659
+ ]);
3660
+ console.log("");
3661
+ } catch (e) {
3662
+ reportError(e, "Failed to load billing");
3663
+ }
3664
+ }
3665
+ //#endregion
3666
+ //#region src/commands/cloud/index.ts
3667
+ /**
3668
+ * CLI command: `rebase cloud <group> [action] [options]`
3669
+ *
3670
+ * A single entry point for everything you do against Rebase Cloud — the hosted
3671
+ * control plane. Auth, project link, deploys, databases, and the rest are all
3672
+ * dispatched from here. Individual groups live in sibling modules; this file
3673
+ * only routes and prints help.
3674
+ */
3675
+ /** Positional tokens after `rebase cloud` (group, action, …). */
3676
+ function positionals(rawArgs) {
3677
+ return arg({}, {
3678
+ argv: rawArgs.slice(3),
3679
+ permissive: true
3680
+ })._;
3681
+ }
3682
+ async function cloudCommand(subcommand, rawArgs) {
3683
+ const pos = positionals(rawArgs);
3684
+ const group = subcommand && subcommand !== "--help" ? subcommand : pos[0];
3685
+ const action = pos[1];
3686
+ if (!group || subcommand === "--help") {
3687
+ printCloudHelp();
3688
+ return;
3689
+ }
3690
+ switch (group) {
3691
+ case "login":
3692
+ await loginCommand(rawArgs);
3693
+ break;
3694
+ case "logout":
3695
+ await logoutCommand(rawArgs);
3696
+ break;
3697
+ case "whoami":
3698
+ await whoamiCommand(rawArgs);
3699
+ break;
3700
+ case "link":
3701
+ await linkCommand(rawArgs);
3702
+ break;
3703
+ case "unlink":
3704
+ unlinkCommand();
3705
+ break;
3706
+ case "use":
3707
+ await selectOrgCommand(rawArgs);
3708
+ break;
3709
+ case "open":
3710
+ openCommand(rawArgs);
3711
+ break;
3712
+ case "projects":
3713
+ case "project":
3714
+ await projectsGroup(action, rawArgs);
3715
+ break;
3716
+ case "deploy":
3717
+ await deployCommand(rawArgs, requireProjectId(rawArgs));
3718
+ break;
3719
+ case "logs":
3720
+ await logsCommand(rawArgs, requireProjectId(rawArgs));
3721
+ break;
3722
+ case "status":
3723
+ await statusCommand(rawArgs);
3724
+ break;
3725
+ case "metrics":
3726
+ await metricsCommand(rawArgs);
3727
+ break;
3728
+ case "orgs":
3729
+ case "org":
3730
+ await orgsCommand(action, rawArgs);
3731
+ break;
3732
+ case "db":
3733
+ case "database":
3734
+ await dbCommand$1(action, rawArgs);
3735
+ break;
3736
+ case "webhooks":
3737
+ await webhooksCommand(action, rawArgs);
3738
+ break;
3739
+ case "storage":
3740
+ await storageCommand(rawArgs);
3741
+ break;
3742
+ case "clusters":
3743
+ await clustersCommand(rawArgs);
3744
+ break;
3745
+ case "billing":
3746
+ await billingCommand(rawArgs);
3747
+ break;
3748
+ default:
3749
+ console.error(chalk.red(`Unknown cloud command: ${group}`));
3750
+ console.log("");
3751
+ printCloudHelp();
3752
+ process.exit(1);
3753
+ }
3754
+ }
3755
+ async function projectsGroup(action, rawArgs) {
3756
+ switch (action) {
3757
+ case "list":
3758
+ case void 0:
3759
+ await listProjects(rawArgs);
3760
+ break;
3761
+ case "create":
3762
+ await createProject(rawArgs);
3763
+ break;
3764
+ case "info":
3765
+ await projectInfo(rawArgs, positionals(rawArgs)[2] || requireProjectId(rawArgs));
3766
+ break;
3767
+ case "delete":
3768
+ await deleteProject(rawArgs, positionals(rawArgs)[2] || requireProjectId(rawArgs));
3769
+ break;
3770
+ case "--help":
3771
+ printCloudHelp();
3772
+ break;
3773
+ default:
3774
+ console.error(chalk.red(`Unknown projects command: ${action}`));
3775
+ process.exit(1);
3776
+ }
3777
+ }
3778
+ function printCloudHelp() {
3779
+ console.log(`
3780
+ ${chalk.bold("rebase cloud")} — Manage your apps on Rebase Cloud
3781
+
3782
+ ${chalk.green.bold("Usage")}
3783
+ rebase cloud ${chalk.blue("<command>")} [options]
3784
+
3785
+ ${chalk.green.bold("Auth")}
3786
+ ${chalk.blue.bold("login")} Sign in to the control plane
3787
+ ${chalk.blue.bold("logout")} Sign out
3788
+ ${chalk.blue.bold("whoami")} Show the current session
3789
+
3790
+ ${chalk.green.bold("Project link")}
3791
+ ${chalk.blue.bold("link")} Link this directory to a cloud project
3792
+ ${chalk.blue.bold("unlink")} Remove the link
3793
+ ${chalk.blue.bold("use")} ${chalk.gray("[org]")} Select the active organization
3794
+ ${chalk.blue.bold("open")} Open the dashboard in a browser
3795
+
3796
+ ${chalk.green.bold("Projects")}
3797
+ ${chalk.blue.bold("projects list")} List projects
3798
+ ${chalk.blue.bold("projects create")} Create a project ${chalk.gray("(--link to link it)")}
3799
+ ${chalk.blue.bold("projects info")} ${chalk.gray("[id]")} Show project details
3800
+ ${chalk.blue.bold("projects delete")} ${chalk.gray("[id]")} Delete a project
3801
+
3802
+ ${chalk.green.bold("Deploy & observe")}
3803
+ ${chalk.blue.bold("deploy")} ${chalk.gray("[--source .]")} Deploy the linked project + stream build logs
3804
+ ${chalk.blue.bold("logs")} ${chalk.gray("[--runtime] [-f]")} Show build (or runtime) logs
3805
+ ${chalk.blue.bold("status")} One-glance project status
3806
+ ${chalk.blue.bold("metrics")} Live CPU / memory / disk
3807
+
3808
+ ${chalk.green.bold("Organizations")}
3809
+ ${chalk.blue.bold("orgs list|create|members")}
3810
+
3811
+ ${chalk.green.bold("Databases")}
3812
+ ${chalk.blue.bold("db list|create|test")}
3813
+ ${chalk.blue.bold("db backup list|create|restore")}
3814
+
3815
+ ${chalk.green.bold("Other resources")}
3816
+ ${chalk.blue.bold("webhooks list|create|delete")}
3817
+ ${chalk.blue.bold("storage")} List storage buckets
3818
+ ${chalk.blue.bold("clusters")} List compute clusters
3819
+ ${chalk.blue.bold("billing setup")} Attach a card to the org ${chalk.gray("(one-time, opens browser)")}
3820
+ ${chalk.blue.bold("billing")} Show billing account + card on file
3821
+
3822
+ ${chalk.green.bold("Global options")}
3823
+ ${chalk.blue("--url <origin>")} Target a specific control plane ${chalk.gray("(or REBASE_CLOUD_URL)")}
3824
+ ${chalk.blue("--project, -p <id>")} Operate on a project without linking
3825
+
3826
+ ${chalk.gray("Most commands act on the linked project (.rebase/cloud.json) unless --project is given.")}
3827
+ ${chalk.gray("Docs: https://rebase.pro/docs")}
3828
+ `);
3829
+ }
3830
+ //#endregion
3831
+ //#region src/cli.ts
3832
+ var __filename = fileURLToPath(import.meta.url);
3833
+ var __dirname = path.dirname(__filename);
1008
3834
  function getVersion() {
1009
- try {
1010
- const pkgPath = path.resolve(__dirname$1, "../package.json");
1011
- if (fs.existsSync(pkgPath)) {
1012
- return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
1013
- }
1014
- } catch {
1015
- }
1016
- return "unknown";
3835
+ try {
3836
+ const pkgPath = path.resolve(__dirname, "../package.json");
3837
+ if (fs.existsSync(pkgPath)) return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
3838
+ } catch {}
3839
+ return "unknown";
1017
3840
  }
1018
3841
  async function entry(args) {
1019
- const parsedArgs = arg(
1020
- {
1021
- "--version": Boolean,
1022
- "--help": Boolean,
1023
- "-v": "--version",
1024
- "-h": "--help"
1025
- },
1026
- {
1027
- argv: args.slice(2),
1028
- permissive: true
1029
- }
1030
- );
1031
- if (parsedArgs["--version"]) {
1032
- console.log(getVersion());
1033
- return;
1034
- }
1035
- const command = parsedArgs._[0];
1036
- const subcommand = parsedArgs._[1];
1037
- const namespacedCommands = ["schema", "db", "dev", "auth", "doctor"];
1038
- if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
1039
- printHelp();
1040
- return;
1041
- }
1042
- const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
1043
- switch (command) {
1044
- case "init":
1045
- await createRebaseApp(args);
1046
- break;
1047
- case "generate-sdk": {
1048
- const sdkArgs = arg(
1049
- {
1050
- "--collections-dir": String,
1051
- "--output": String,
1052
- "-c": "--collections-dir",
1053
- "-o": "--output"
1054
- },
1055
- {
1056
- argv: args.slice(3),
1057
- permissive: true
1058
- }
1059
- );
1060
- await generateSdkCommand({
1061
- collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
1062
- output: sdkArgs["--output"] || "./generated/sdk",
1063
- cwd: process.cwd()
1064
- });
1065
- break;
1066
- }
1067
- case "schema":
1068
- await schemaCommand(effectiveSubcommand, args);
1069
- break;
1070
- case "db":
1071
- await dbCommand(effectiveSubcommand, args);
1072
- break;
1073
- case "dev":
1074
- await devCommand(args);
1075
- break;
1076
- case "auth":
1077
- await authCommand(effectiveSubcommand, args);
1078
- break;
1079
- case "doctor":
1080
- await doctorCommand(args);
1081
- break;
1082
- default:
1083
- console.log(chalk.red(`Unknown command: ${command}`));
1084
- console.log("");
1085
- printHelp();
1086
- }
3842
+ const parsedArgs = arg({
3843
+ "--version": Boolean,
3844
+ "--help": Boolean,
3845
+ "-v": "--version",
3846
+ "-h": "--help"
3847
+ }, {
3848
+ argv: args.slice(2),
3849
+ permissive: true
3850
+ });
3851
+ if (parsedArgs["--version"]) {
3852
+ console.log(getVersion());
3853
+ return;
3854
+ }
3855
+ const command = parsedArgs._[0];
3856
+ const subcommand = parsedArgs._[1];
3857
+ if (!command || parsedArgs["--help"] && ![
3858
+ "schema",
3859
+ "db",
3860
+ "dev",
3861
+ "build",
3862
+ "start",
3863
+ "auth",
3864
+ "doctor",
3865
+ "skills",
3866
+ "api-keys",
3867
+ "cloud"
3868
+ ].includes(command)) {
3869
+ printHelp();
3870
+ return;
3871
+ }
3872
+ const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
3873
+ switch (command) {
3874
+ case "init":
3875
+ await createRebaseApp(args);
3876
+ break;
3877
+ case "generate-sdk": {
3878
+ const sdkArgs = arg({
3879
+ "--collections-dir": String,
3880
+ "--output": String,
3881
+ "-c": "--collections-dir",
3882
+ "-o": "--output"
3883
+ }, {
3884
+ argv: args.slice(3),
3885
+ permissive: true
3886
+ });
3887
+ await generateSdkCommand({
3888
+ collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
3889
+ output: sdkArgs["--output"] || "./generated/sdk",
3890
+ cwd: process.cwd()
3891
+ });
3892
+ break;
3893
+ }
3894
+ case "schema":
3895
+ await schemaCommand(effectiveSubcommand, args);
3896
+ break;
3897
+ case "db":
3898
+ await dbCommand(effectiveSubcommand, args);
3899
+ break;
3900
+ case "dev":
3901
+ await devCommand(args);
3902
+ break;
3903
+ case "build":
3904
+ await buildCommand();
3905
+ break;
3906
+ case "start":
3907
+ await startCommand();
3908
+ break;
3909
+ case "auth":
3910
+ await authCommand(effectiveSubcommand, args);
3911
+ break;
3912
+ case "doctor":
3913
+ await doctorCommand(args);
3914
+ break;
3915
+ case "skills":
3916
+ await skillsCommand(effectiveSubcommand, args);
3917
+ break;
3918
+ case "api-keys":
3919
+ await apiKeysCommand(effectiveSubcommand, args);
3920
+ break;
3921
+ case "cloud":
3922
+ await cloudCommand(effectiveSubcommand, args);
3923
+ break;
3924
+ default:
3925
+ console.log(chalk.red(`Unknown command: ${command}`));
3926
+ console.log("");
3927
+ printHelp();
3928
+ }
1087
3929
  }
1088
3930
  function printHelp() {
1089
- console.log(`
3931
+ console.log(`
1090
3932
  ${chalk.bold("Rebase CLI")} — Developer tools for Rebase projects
1091
3933
 
1092
3934
  ${chalk.green.bold("Usage")}
@@ -1095,21 +3937,22 @@ ${chalk.green.bold("Usage")}
1095
3937
  ${chalk.green.bold("Commands")}
1096
3938
  ${chalk.blue.bold("init")} Create a new Rebase project
1097
3939
  ${chalk.blue.bold("dev")} Start the development server
3940
+ ${chalk.blue.bold("build")} Build all workspace packages
3941
+ ${chalk.blue.bold("start")} Start the backend server ${chalk.gray("(production)")}
1098
3942
 
1099
3943
  ${chalk.green.bold("Schema")}
1100
3944
  ${chalk.blue.bold("schema generate")} Generate Drizzle schema from collections
3945
+ ${chalk.blue.bold("schema introspect")} Introspect database → Rebase collections
1101
3946
  ${chalk.blue.bold("schema")} ${chalk.gray("--help")} Show schema command help
1102
3947
 
1103
3948
  ${chalk.green.bold("Database")}
1104
3949
  ${chalk.blue.bold("db push")} Apply schema directly to database ${chalk.gray("(dev)")}
1105
- ${chalk.blue.bold("db pull")} Introspect database → Drizzle schema
1106
3950
  ${chalk.blue.bold("db generate")} Generate SQL migration files
1107
3951
  ${chalk.blue.bold("db migrate")} Run pending migrations
1108
- ${chalk.blue.bold("db studio")} Open Drizzle Studio
1109
3952
  ${chalk.blue.bold("db")} ${chalk.gray("--help")} Show database command help
1110
3953
 
1111
3954
  ${chalk.green.bold("SDK")}
1112
- ${chalk.blue.bold("generate-sdk")} Generate a typed JS SDK from collections
3955
+ ${chalk.blue.bold("generate-sdk")} Generate a typed TypeScript SDK from collections
1113
3956
 
1114
3957
  ${chalk.green.bold("Auth")}
1115
3958
  ${chalk.blue.bold("auth reset-password")} Reset a user's password
@@ -1118,6 +3961,21 @@ ${chalk.green.bold("Auth")}
1118
3961
  ${chalk.green.bold("Diagnostics")}
1119
3962
  ${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
1120
3963
 
3964
+ ${chalk.green.bold("AI Agent Skills")}
3965
+ ${chalk.blue.bold("skills install")} Install Rebase agent skills for your AI coding assistant
3966
+
3967
+ ${chalk.green.bold("API Keys")}
3968
+ ${chalk.blue.bold("api-keys list")} List all service API keys
3969
+ ${chalk.blue.bold("api-keys create")} Create a new scoped API key
3970
+ ${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
3971
+ ${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
3972
+
3973
+ ${chalk.green.bold("Rebase Cloud")}
3974
+ ${chalk.blue.bold("cloud login")} Sign in to the hosted control plane
3975
+ ${chalk.blue.bold("cloud link")} Link this directory to a cloud project
3976
+ ${chalk.blue.bold("cloud deploy")} Deploy the linked project + stream logs
3977
+ ${chalk.blue.bold("cloud")} ${chalk.gray("--help")} Show all cloud commands
3978
+
1121
3979
  ${chalk.green.bold("Options")}
1122
3980
  ${chalk.blue("--version, -v")} Show version number
1123
3981
  ${chalk.blue("--help, -h")} Show this help message
@@ -1125,70 +3983,7 @@ ${chalk.green.bold("Options")}
1125
3983
  ${chalk.gray("Documentation: https://rebase.pro/docs")}
1126
3984
  `);
1127
3985
  }
1128
- const TOKEN_DIR = path.join(os.homedir(), ".rebase");
1129
- function tokenPath(env) {
1130
- return path.join(TOKEN_DIR, (env === "dev" ? "staging." : "") + "tokens.json");
1131
- }
1132
- async function getTokens(env, _debug) {
1133
- const fp = tokenPath(env);
1134
- if (!fs.existsSync(fp)) return null;
1135
- try {
1136
- const data = fs.readFileSync(fp, "utf-8");
1137
- return JSON.parse(data);
1138
- } catch {
1139
- return null;
1140
- }
1141
- }
1142
- function parseJwt(token) {
1143
- if (!token) throw new Error("No JWT token");
1144
- const base64Url = token.split(".")[1];
1145
- const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
1146
- const buffer = Buffer.from(base64, "base64");
1147
- return JSON.parse(buffer.toString());
1148
- }
1149
- async function refreshCredentials(env, credentials, _onErr) {
1150
- if (!credentials) return null;
1151
- const expiryDate = new Date(credentials["expiry_date"]);
1152
- if (expiryDate.getTime() > Date.now()) {
1153
- return credentials;
1154
- }
1155
- return null;
1156
- }
1157
- async function login(env, _debug) {
1158
- console.log(
1159
- "Interactive login is not yet implemented in @rebasepro/cli.\nPlease authenticate via the Rebase dashboard and copy your tokens to " + tokenPath(env)
1160
- );
1161
- }
1162
- async function logout(env, _debug) {
1163
- const fp = tokenPath(env);
1164
- if (fs.existsSync(fp)) {
1165
- fs.unlinkSync(fp);
1166
- console.log("You have been logged out.");
1167
- } else {
1168
- console.log("You are not logged in.");
1169
- }
1170
- }
1171
- export {
1172
- authCommand,
1173
- createRebaseApp,
1174
- dbCommand,
1175
- devCommand,
1176
- entry,
1177
- findBackendDir,
1178
- findEnvFile,
1179
- findFrontendDir,
1180
- findProjectRoot,
1181
- getActiveBackendPlugin,
1182
- getTokens,
1183
- login,
1184
- logout,
1185
- parseJwt,
1186
- refreshCredentials,
1187
- requireBackendDir,
1188
- requireProjectRoot,
1189
- resolveLocalBin,
1190
- resolvePluginCliScript,
1191
- resolveTsx,
1192
- schemaCommand
1193
- };
1194
- //# sourceMappingURL=index.es.js.map
3986
+ //#endregion
3987
+ export { authCommand, buildCommand, buildInitQuestions, cloudCommand, configureEnvFile, createRebaseApp, dbCommand, detectPackageManager, devCommand, doctorCommand, entry, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, generateSdkCommand, getActiveBackendPlugin, getPMCommands, requireBackendDir, requireProjectRoot, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, validateTsxInstallation };
3988
+
3989
+ //# sourceMappingURL=index.es.js.map