@rebasepro/cli 0.0.1-canary.2 → 0.0.1-canary.4829d6e

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 (83) 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/auth.d.ts +1 -0
  5. package/dist/commands/build.d.ts +1 -0
  6. package/dist/commands/cloud/auth.d.ts +3 -0
  7. package/dist/commands/cloud/context.d.ts +61 -0
  8. package/dist/commands/cloud/databases.d.ts +1 -0
  9. package/dist/commands/cloud/deploy.d.ts +2 -0
  10. package/dist/commands/cloud/index.d.ts +1 -0
  11. package/dist/commands/cloud/link.d.ts +5 -0
  12. package/dist/commands/cloud/orgs.d.ts +1 -0
  13. package/dist/commands/cloud/projects.d.ts +13 -0
  14. package/dist/commands/cloud/resources.d.ts +6 -0
  15. package/dist/commands/db.d.ts +1 -0
  16. package/dist/commands/dev.d.ts +1 -0
  17. package/dist/commands/doctor.d.ts +1 -0
  18. package/dist/commands/generate_sdk.d.ts +18 -0
  19. package/dist/commands/init.d.ts +40 -0
  20. package/dist/commands/schema.d.ts +1 -0
  21. package/dist/commands/skills.d.ts +1 -0
  22. package/dist/commands/start.d.ts +1 -0
  23. package/dist/index.d.ts +11 -1
  24. package/dist/index.es.js +3952 -245
  25. package/dist/index.es.js.map +1 -1
  26. package/dist/utils/package-manager.d.ts +33 -0
  27. package/dist/utils/project.d.ts +60 -0
  28. package/package.json +35 -23
  29. package/templates/overlays/baas/README.md +37 -0
  30. package/templates/overlays/baas/backend/package.json +31 -0
  31. package/templates/overlays/baas/backend/src/index.ts +172 -0
  32. package/templates/overlays/baas/backend/tsconfig.json +18 -0
  33. package/templates/overlays/baas/package.json +42 -0
  34. package/templates/overlays/baas/pnpm-workspace.yaml +9 -0
  35. package/templates/template/.cursorrules +2 -0
  36. package/templates/template/.dockerignore +28 -0
  37. package/templates/template/.env.example +120 -0
  38. package/templates/template/.github/copilot-instructions.md +2 -0
  39. package/templates/template/.windsurfrules +2 -0
  40. package/templates/template/AGENTS.md +2 -0
  41. package/templates/template/CLAUDE.md +2 -0
  42. package/templates/template/README.md +88 -21
  43. package/templates/template/ai-instructions.md +17 -0
  44. package/templates/template/backend/Dockerfile +57 -11
  45. package/templates/template/backend/functions/hello.ts +56 -0
  46. package/templates/template/backend/package.json +30 -34
  47. package/templates/template/backend/src/env.ts +23 -0
  48. package/templates/template/backend/src/index.ts +140 -100
  49. package/templates/template/backend/tsconfig.json +1 -1
  50. package/templates/template/config/collections/authors.ts +45 -0
  51. package/templates/template/config/collections/index.ts +26 -0
  52. package/templates/template/{shared → config}/collections/posts.ts +28 -10
  53. package/templates/template/config/collections/presets/blank/index.ts +3 -0
  54. package/templates/template/config/collections/presets/ecommerce/categories.ts +40 -0
  55. package/templates/template/config/collections/presets/ecommerce/index.ts +6 -0
  56. package/templates/template/config/collections/presets/ecommerce/orders.ts +66 -0
  57. package/templates/template/config/collections/presets/ecommerce/products.ts +64 -0
  58. package/templates/template/config/collections/tags.ts +25 -0
  59. package/templates/template/config/collections/users.ts +142 -0
  60. package/templates/template/{shared → config}/index.ts +1 -1
  61. package/templates/template/config/package.json +28 -0
  62. package/templates/template/docker-compose.yml +50 -14
  63. package/templates/template/frontend/Dockerfile +39 -15
  64. package/templates/template/frontend/nginx.conf +40 -0
  65. package/templates/template/frontend/package.json +13 -13
  66. package/templates/template/frontend/src/App.tsx +30 -116
  67. package/templates/template/frontend/src/index.css +15 -1
  68. package/templates/template/frontend/src/main.tsx +6 -2
  69. package/templates/template/frontend/src/virtual.d.ts +6 -0
  70. package/templates/template/frontend/tsconfig.json +3 -2
  71. package/templates/template/frontend/vite.config.ts +48 -5
  72. package/templates/template/package.json +21 -12
  73. package/templates/template/pnpm-workspace.yaml +13 -0
  74. package/templates/template/scripts/example.ts +94 -0
  75. package/dist/auth.d.ts +0 -5
  76. package/dist/index.cjs +0 -293
  77. package/dist/index.cjs.map +0 -1
  78. package/templates/template/.env.template +0 -31
  79. package/templates/template/backend/drizzle.config.ts +0 -22
  80. package/templates/template/backend/scripts/db-generate.ts +0 -60
  81. package/templates/template/shared/collections/index.ts +0 -3
  82. package/templates/template/shared/package.json +0 -28
  83. /package/templates/template/{shared → config}/tsconfig.json +0 -0
package/dist/index.es.js CHANGED
@@ -3,222 +3,3978 @@ 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 * as os from "os";
12
- const access = promisify(fs.access);
13
- const copy = promisify(ncp);
14
- const __filename$2 = fileURLToPath(import.meta.url);
15
- const __dirname$2 = path.dirname(__filename$2);
12
+ import { generateSDK } from "@rebasepro/codegen";
13
+ import { execSync, spawn } from "child_process";
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);
16
138
  function findParentDir(currentDir, targetName) {
17
- const root = path.parse(currentDir).root;
18
- while (currentDir && currentDir !== root) {
19
- if (path.basename(currentDir) === targetName) {
20
- return currentDir;
21
- }
22
- currentDir = path.dirname(currentDir);
23
- }
24
- 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;
25
237
  }
26
- const cliRoot = findParentDir(__dirname$2, "cli");
27
238
  async function createRebaseApp(rawArgs) {
28
- console.log(`
239
+ console.log(`
29
240
  ${chalk.bold("Rebase")} — Create a new project 🚀
30
241
  `);
31
- const options = await promptForOptions(rawArgs);
32
- await createProject(options);
33
- }
34
- async function promptForOptions(rawArgs) {
35
- const args = arg(
36
- {
37
- "--git": Boolean,
38
- "-g": "--git"
39
- },
40
- {
41
- argv: rawArgs.slice(3),
42
- // skip "node", "rebase", "init"
43
- permissive: true
44
- }
45
- );
46
- const nameArg = args._[0];
47
- const questions = [];
48
- if (!nameArg) {
49
- questions.push({
50
- type: "input",
51
- name: "projectName",
52
- message: "Project name:",
53
- default: "my-rebase-app",
54
- validate: (input) => {
55
- if (!input.trim()) return "Project name is required";
56
- if (!/^[a-z0-9][a-z0-9._-]*$/.test(input)) {
57
- return "Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, hyphens, dots, or underscores";
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
+ });
519
+ }
520
+ async function replacePlaceholders(options) {
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
+ });
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
+ */
656
+ async function loadCollections(collectionsDir) {
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.`);
712
+ }
713
+ /**
714
+ * Write generated files to the output directory.
715
+ */
716
+ function writeFiles(outputDir, files) {
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
+ }
725
+ }
726
+ /**
727
+ * Main entry point for the generate-sdk command.
728
+ */
729
+ async function generateSdkCommand(args) {
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("");
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
+ */
783
+ function findProjectRoot(startDir = process.cwd()) {
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;
800
+ }
801
+ /**
802
+ * Locate the backend directory within the project root.
803
+ */
804
+ function findBackendDir(projectRoot) {
805
+ const backendDir = path.join(projectRoot, "backend");
806
+ return fs.existsSync(backendDir) ? backendDir : null;
807
+ }
808
+ /**
809
+ * Detect the active backend plugin (e.g. @rebasepro/server-postgres) from the backend's package.json.
810
+ */
811
+ function getActiveBackendPlugin(backendDir) {
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;
827
+ }
828
+ /**
829
+ * Resolve the active plugin's CLI script.
830
+ */
831
+ function resolvePluginCliScript(backendDir, pluginName) {
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;
841
+ }
842
+ /**
843
+ * Locate the frontend directory within the project root.
844
+ */
845
+ function findFrontendDir(projectRoot) {
846
+ const frontendDir = path.join(projectRoot, "frontend");
847
+ return fs.existsSync(frontendDir) ? frontendDir : null;
848
+ }
849
+ /**
850
+ * Find the .env file. Checks the project root first, then backend.
851
+ */
852
+ function findEnvFile(projectRoot) {
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;
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
+ */
861
+ function resolveLocalBin(projectRoot, binName) {
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;
875
+ }
876
+ /**
877
+ * Resolve the tsx binary. Checks backend node_modules first, then root.
878
+ */
879
+ function resolveTsx(projectRoot) {
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
+ }
916
+ }
917
+ /**
918
+ * Require the project root or exit with a helpful error.
919
+ */
920
+ function requireProjectRoot() {
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;
929
+ }
930
+ /**
931
+ * Require the backend directory or exit with a helpful error.
932
+ */
933
+ function requireBackendDir(projectRoot) {
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;
941
+ }
942
+ //#endregion
943
+ //#region src/commands/schema.ts
944
+ /**
945
+ * CLI command: rebase schema <action>
946
+ */
947
+ async function schemaCommand(subcommand, rawArgs) {
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
+ }
988
+ }
989
+ function printSchemaHelp() {
990
+ console.log(`
991
+ ${chalk.bold("rebase schema")} — Schema management commands
992
+
993
+ ${chalk.green.bold("Usage")}
994
+ rebase schema ${chalk.blue("<command>")} [options]
995
+
996
+ ${chalk.green.bold("Commands")}
997
+ ${chalk.gray("(Commands are provided by your active database driver plugin)")}
998
+ ${chalk.blue.bold("generate")} Generate Schema from collection definitions
999
+ ${chalk.blue.bold("introspect")} Introspect an existing database to generate collection definitions
1000
+
1001
+ ${chalk.green.bold("generate Options")}
1002
+ ${chalk.blue("--collections, -c")} Path to collections directory
1003
+ ${chalk.blue("--output, -o")} Output path for generated schema
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
1008
+ `);
1009
+ }
1010
+ //#endregion
1011
+ //#region src/commands/db.ts
1012
+ /**
1013
+ * CLI command: rebase db <action>
1014
+ */
1015
+ async function dbCommand(subcommand, rawArgs) {
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
+ }
1056
+ }
1057
+ function printDbHelp$1() {
1058
+ console.log(`
1059
+ ${chalk.bold("rebase db")} — Database management commands
1060
+
1061
+ ${chalk.green.bold("Usage")}
1062
+ rebase db ${chalk.blue("<command>")} [options]
1063
+
1064
+ ${chalk.green.bold("Commands")}
1065
+ ${chalk.gray("(Commands are provided by your active database driver plugin)")}
1066
+ ${chalk.blue.bold("push")} Apply schema directly to database (development)
1067
+ ${chalk.blue.bold("generate")} Generate migration files
1068
+ ${chalk.blue.bold("migrate")} Run pending migrations
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)
1073
+
1074
+ ${chalk.green.bold("Examples")}
1075
+ ${chalk.gray("# Quick development workflow")}
1076
+ rebase schema generate && rebase db push
1077
+
1078
+ ${chalk.gray("# Production migration workflow")}
1079
+ rebase db generate
1080
+ rebase db migrate
1081
+
1082
+ ${chalk.gray("# Create a database branch")}
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
1091
+ `);
1092
+ }
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
+ */
1118
+ function getProjectPort(projectRoot) {
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;
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
+ */
1130
+ function resolveStartPort(projectRoot, explicitPort) {
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);
1141
+ }
1142
+ async function devCommand(rawArgs) {
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
+ })));
1463
+ }
1464
+ function printDevHelp() {
1465
+ console.log(`
1466
+ ${chalk.bold("rebase dev")} — Start the development server
1467
+
1468
+ ${chalk.green.bold("Usage")}
1469
+ rebase dev [options]
1470
+
1471
+ ${chalk.green.bold("Options")}
1472
+ ${chalk.blue("--backend-only, -b")} Only start the backend server
1473
+ ${chalk.blue("--frontend-only, -f")} Only start the frontend server
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
1476
+
1477
+ ${chalk.green.bold("Description")}
1478
+ Starts both the backend (tsx watch + Hono) and frontend (Vite)
1479
+ dev servers concurrently with color-coded output prefixes.
1480
+
1481
+ Each project automatically receives a unique default port derived
1482
+ from its directory path, preventing collisions when running multiple
1483
+ Rebase instances simultaneously.
1484
+
1485
+ If the assigned port is already in use, the server will automatically
1486
+ try the next available port. The frontend is started only after the
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.
1492
+ `);
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
+ */
1553
+ async function authCommand(subcommand, rawArgs) {
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
+ }
1568
+ }
1569
+ async function resetPassword(rawArgs) {
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";
1675
+ import { eq } from "drizzle-orm";
1676
+ import * as dotenv from "dotenv";
1677
+ import path from "path";
1678
+ import fs from "fs";
1679
+
1680
+ dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH });
1681
+
1682
+ const email = process.env.REBASE_RESET_EMAIL!;
1683
+ const newPassword = process.env.REBASE_RESET_PASSWORD!;
1684
+
1685
+ async function resetPassword() {
1686
+ const { db } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);
1687
+ const hash = await hashPassword(newPassword);
1688
+
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;
58
1695
  }
59
- return true;
60
- }
61
- });
62
- }
63
- if (!args["--git"]) {
64
- questions.push({
65
- type: "confirm",
66
- name: "git",
67
- message: "Initialize a git repository?",
68
- default: true
69
- });
70
- }
71
- const answers = await inquirer.prompt(questions);
72
- const projectName = nameArg || answers.projectName;
73
- const targetDirectory = path.resolve(process.cwd(), projectName);
74
- const templateDirectory = path.resolve(cliRoot, "templates", "template");
75
- return {
76
- projectName,
77
- git: args["--git"] || answers.git || false,
78
- targetDirectory,
79
- templateDirectory
80
- };
81
- }
82
- async function createProject(options) {
83
- if (fs.existsSync(options.targetDirectory)) {
84
- if (fs.readdirSync(options.targetDirectory).length !== 0) {
85
- console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
86
- process.exit(1);
1696
+ } catch (e) {
1697
+ // ignore and fallback
87
1698
  }
88
- } else {
89
- fs.mkdirSync(options.targetDirectory, { recursive: true });
90
- }
91
- try {
92
- await access(options.templateDirectory, fs.constants.R_OK);
93
- } catch {
94
- console.error(`${chalk.red.bold("ERROR")} Template not found at ${options.templateDirectory}`);
95
- process.exit(1);
96
- }
97
- console.log(chalk.gray(" Copying project files..."));
98
- await copy(options.templateDirectory, options.targetDirectory, {
99
- clobber: false,
100
- dot: true,
101
- filter: (source) => {
102
- const basename = path.basename(source);
103
- return basename !== "node_modules" && basename !== ".DS_Store";
1699
+
1700
+ if (!usersTable) {
1701
+ const pgServer = await import("@rebasepro/server-postgres");
1702
+ usersTable = pgServer.users;
104
1703
  }
105
- });
106
- await replacePlaceholders(options);
107
- const envTemplatePath = path.join(options.targetDirectory, ".env.template");
108
- const envPath = path.join(options.targetDirectory, ".env");
109
- if (fs.existsSync(envTemplatePath) && !fs.existsSync(envPath)) {
110
- fs.renameSync(envTemplatePath, envPath);
111
- const jwtSecret = crypto.randomBytes(32).toString("hex");
112
- const dbPassword = crypto.randomBytes(16).toString("hex");
113
- let envContent = fs.readFileSync(envPath, "utf-8");
114
- envContent = envContent.replace(
115
- "postgresql://rebase:password@localhost:5432/rebase",
116
- `postgresql://rebase:${dbPassword}@localhost:5432/rebase`
117
- );
118
- envContent = envContent.replace(
119
- "change-this-to-a-secure-random-string",
120
- jwtSecret
121
- );
122
- envContent += `
123
- # Docker Compose Database Password
124
- POSTGRES_PASSWORD=${dbPassword}
125
- `;
126
- fs.writeFileSync(envPath, envContent, "utf-8");
127
- }
128
- if (options.git) {
129
- console.log(chalk.gray(" Initializing git repository..."));
130
- try {
131
- await execa("git", ["init"], { cwd: options.targetDirectory });
132
- } catch {
133
- console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
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))
1710
+ .returning({
1711
+ id: usersTable.id,
1712
+ email: usersTable.email
1713
+ });
1714
+
1715
+ if (result.length > 0) {
1716
+ console.log("✅ Password reset for: " + result[0].email);
1717
+ ${!newPassword ? "console.log(\" New password: \" + newPassword);" : ""}
1718
+ } else {
1719
+ console.log("✗ User not found: " + email);
134
1720
  }
135
- }
136
- console.log("");
137
- console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
138
- console.log("");
139
- console.log(chalk.bold("Next steps:"));
140
- console.log("");
141
- console.log(` ${chalk.cyan("cd")} ${options.projectName}`);
142
- console.log(` ${chalk.cyan("pnpm install")}`);
143
- console.log("");
144
- console.log(chalk.gray(" # Set up your database connection in .env"));
145
- console.log(chalk.gray(" # Then run:"));
146
- console.log("");
147
- console.log(` ${chalk.cyan("pnpm dev")}`);
148
- console.log("");
149
- console.log(chalk.gray("This starts both the backend (Express + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
150
- console.log("");
151
- console.log(chalk.gray("Docs: https://rebase.pro/docs"));
152
- console.log(chalk.gray("GitHub: https://github.com/rebaseco/rebase"));
153
- console.log("");
1721
+ process.exit(0);
154
1722
  }
155
- async function replacePlaceholders(options) {
156
- const filesToProcess = [
157
- "package.json",
158
- "frontend/package.json",
159
- "backend/package.json",
160
- "shared/package.json",
161
- "frontend/index.html"
162
- ];
163
- for (const file of filesToProcess) {
164
- const fullPath = path.resolve(options.targetDirectory, file);
165
- if (!fs.existsSync(fullPath)) continue;
166
- let content = fs.readFileSync(fullPath, "utf-8");
167
- content = content.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
168
- fs.writeFileSync(fullPath, content, "utf-8");
169
- }
170
- }
171
- const __filename$1 = fileURLToPath(import.meta.url);
172
- const __dirname$1 = path.dirname(__filename$1);
1723
+
1724
+ resetPassword().catch(console.error);
1725
+ `;
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
+ }
1753
+ }
1754
+ function printAuthHelp() {
1755
+ console.log(`
1756
+ ${chalk.bold("rebase auth")} — Authentication management commands
1757
+
1758
+ ${chalk.green.bold("Usage")}
1759
+ rebase auth ${chalk.blue("<command>")} [options]
1760
+
1761
+ ${chalk.green.bold("Commands")}
1762
+ ${chalk.blue.bold("reset-password")} Reset a user's password
1763
+
1764
+ ${chalk.green.bold("reset-password Options")}
1765
+ ${chalk.blue("--email, -e")} User's email address
1766
+ ${chalk.blue("--password, -p")} New password (default: NewPassword123!)
1767
+
1768
+ ${chalk.green.bold("Examples")}
1769
+ rebase auth reset-password user@example.com
1770
+ rebase auth reset-password --email user@example.com --password MyNewPass!
1771
+ `);
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
+ */
1781
+ async function doctorCommand(rawArgs) {
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
+ }
2293
+ }
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);
173
3834
  function getVersion() {
174
- try {
175
- const pkgPath = path.resolve(__dirname$1, "../package.json");
176
- if (fs.existsSync(pkgPath)) {
177
- return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
178
- }
179
- } catch {
180
- }
181
- 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";
182
3840
  }
183
3841
  async function entry(args) {
184
- const parsedArgs = arg(
185
- {
186
- "--version": Boolean,
187
- "--help": Boolean,
188
- "-v": "--version",
189
- "-h": "--help"
190
- },
191
- {
192
- argv: args.slice(2),
193
- permissive: true
194
- }
195
- );
196
- if (parsedArgs["--version"]) {
197
- console.log(getVersion());
198
- return;
199
- }
200
- const command = parsedArgs._[0];
201
- if (!command || parsedArgs["--help"]) {
202
- printHelp();
203
- return;
204
- }
205
- if (command === "init") {
206
- await createRebaseApp(args);
207
- } else {
208
- console.log(chalk.red(`Unknown command: ${command}`));
209
- console.log("");
210
- printHelp();
211
- }
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
+ }
212
3929
  }
213
3930
  function printHelp() {
214
- console.log(`
3931
+ console.log(`
215
3932
  ${chalk.bold("Rebase CLI")} — Developer tools for Rebase projects
216
3933
 
217
3934
  ${chalk.green.bold("Usage")}
218
3935
  rebase ${chalk.blue("<command>")} [options]
219
3936
 
220
3937
  ${chalk.green.bold("Commands")}
221
- ${chalk.blue.bold("init")} Create a new Rebase project
3938
+ ${chalk.blue.bold("init")} Create a new Rebase project
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)")}
3942
+
3943
+ ${chalk.green.bold("Schema")}
3944
+ ${chalk.blue.bold("schema generate")} Generate Drizzle schema from collections
3945
+ ${chalk.blue.bold("schema introspect")} Introspect database → Rebase collections
3946
+ ${chalk.blue.bold("schema")} ${chalk.gray("--help")} Show schema command help
3947
+
3948
+ ${chalk.green.bold("Database")}
3949
+ ${chalk.blue.bold("db push")} Apply schema directly to database ${chalk.gray("(dev)")}
3950
+ ${chalk.blue.bold("db generate")} Generate SQL migration files
3951
+ ${chalk.blue.bold("db migrate")} Run pending migrations
3952
+ ${chalk.blue.bold("db")} ${chalk.gray("--help")} Show database command help
3953
+
3954
+ ${chalk.green.bold("SDK")}
3955
+ ${chalk.blue.bold("generate-sdk")} Generate a typed TypeScript SDK from collections
3956
+
3957
+ ${chalk.green.bold("Auth")}
3958
+ ${chalk.blue.bold("auth reset-password")} Reset a user's password
3959
+ ${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
3960
+
3961
+ ${chalk.green.bold("Diagnostics")}
3962
+ ${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
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
222
3978
 
223
3979
  ${chalk.green.bold("Options")}
224
3980
  ${chalk.blue("--version, -v")} Show version number
@@ -227,56 +3983,7 @@ ${chalk.green.bold("Options")}
227
3983
  ${chalk.gray("Documentation: https://rebase.pro/docs")}
228
3984
  `);
229
3985
  }
230
- const TOKEN_DIR = path.join(os.homedir(), ".rebase");
231
- function tokenPath(env) {
232
- return path.join(TOKEN_DIR, (env === "dev" ? "staging." : "") + "tokens.json");
233
- }
234
- async function getTokens(env, _debug) {
235
- const fp = tokenPath(env);
236
- if (!fs.existsSync(fp)) return null;
237
- try {
238
- const data = fs.readFileSync(fp, "utf-8");
239
- return JSON.parse(data);
240
- } catch {
241
- return null;
242
- }
243
- }
244
- function parseJwt(token) {
245
- if (!token) throw new Error("No JWT token");
246
- const base64Url = token.split(".")[1];
247
- const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
248
- const buffer = Buffer.from(base64, "base64");
249
- return JSON.parse(buffer.toString());
250
- }
251
- async function refreshCredentials(env, credentials, _onErr) {
252
- if (!credentials) return null;
253
- const expiryDate = new Date(credentials["expiry_date"]);
254
- if (expiryDate.getTime() > Date.now()) {
255
- return credentials;
256
- }
257
- return null;
258
- }
259
- async function login(env, _debug) {
260
- console.log(
261
- "Interactive login is not yet implemented in @rebasepro/cli.\nPlease authenticate via the Rebase dashboard and copy your tokens to " + tokenPath(env)
262
- );
263
- }
264
- async function logout(env, _debug) {
265
- const fp = tokenPath(env);
266
- if (fs.existsSync(fp)) {
267
- fs.unlinkSync(fp);
268
- console.log("You have been logged out.");
269
- } else {
270
- console.log("You are not logged in.");
271
- }
272
- }
273
- export {
274
- createRebaseApp,
275
- entry,
276
- getTokens,
277
- login,
278
- logout,
279
- parseJwt,
280
- refreshCredentials
281
- };
282
- //# 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