@rebasepro/cli 0.0.1-canary.4d4fb3e → 0.0.1-canary.5584634

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 (38) hide show
  1. package/dist/commands/cli.test.d.ts +1 -0
  2. package/dist/commands/dev.test.d.ts +1 -0
  3. package/dist/commands/doctor.d.ts +1 -0
  4. package/dist/commands/generate_sdk.d.ts +1 -1
  5. package/dist/commands/init.d.ts +2 -0
  6. package/dist/commands/init.test.d.ts +1 -0
  7. package/dist/index.cjs +295 -73
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.es.js +295 -73
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/utils/project.test.d.ts +1 -0
  12. package/package.json +67 -66
  13. package/templates/template/.env.template +22 -1
  14. package/templates/template/README.md +2 -2
  15. package/templates/template/backend/Dockerfile +6 -6
  16. package/templates/template/backend/drizzle.config.ts +15 -3
  17. package/templates/template/backend/functions/hello.ts +24 -6
  18. package/templates/template/backend/package.json +2 -2
  19. package/templates/template/backend/src/env.ts +52 -0
  20. package/templates/template/backend/src/index.ts +86 -34
  21. package/templates/template/backend/tsconfig.json +1 -1
  22. package/templates/template/config/collections/authors.ts +45 -0
  23. package/templates/template/config/collections/index.ts +5 -0
  24. package/templates/template/{shared → config}/collections/posts.ts +39 -1
  25. package/templates/template/config/collections/tags.ts +27 -0
  26. package/templates/template/{shared → config}/package.json +1 -1
  27. package/templates/template/docker-compose.yml +12 -0
  28. package/templates/template/frontend/Dockerfile +3 -3
  29. package/templates/template/frontend/package.json +2 -2
  30. package/templates/template/frontend/src/App.tsx +21 -101
  31. package/templates/template/frontend/src/main.tsx +2 -2
  32. package/templates/template/frontend/vite.config.ts +32 -3
  33. package/templates/template/package.json +5 -4
  34. package/templates/template/pnpm-workspace.yaml +1 -1
  35. package/templates/template/scripts/example.ts +91 -0
  36. package/templates/template/shared/collections/index.ts +0 -3
  37. /package/templates/template/{shared → config}/index.ts +0 -0
  38. /package/templates/template/{shared → config}/tsconfig.json +0 -0
package/dist/index.es.js CHANGED
@@ -80,16 +80,31 @@ async function promptForOptions(rawArgs) {
80
80
  default: true
81
81
  });
82
82
  }
83
+ questions.push({
84
+ type: "input",
85
+ name: "databaseUrl",
86
+ message: "Enter your PostgreSQL database connection string (leave blank to use a local default):",
87
+ default: ""
88
+ });
89
+ questions.push({
90
+ type: "confirm",
91
+ name: "introspect",
92
+ message: "Would you like to introspect this database to automatically generate collections?",
93
+ default: true,
94
+ when: (answers2) => !!answers2.databaseUrl?.trim()
95
+ });
83
96
  const answers = await inquirer.prompt(questions);
84
- const projectName = nameArg || answers.projectName;
85
- const targetDirectory = path.resolve(process.cwd(), projectName);
97
+ const targetDirectory = path.resolve(process.cwd(), nameArg || answers.projectName);
98
+ const projectName = path.basename(targetDirectory);
86
99
  const templateDirectory = path.resolve(cliRoot, "templates", "template");
87
100
  return {
88
101
  projectName,
89
102
  git: args["--git"] || answers.git || false,
90
103
  installDeps: args["--install"] || answers.installDeps || false,
91
104
  targetDirectory,
92
- templateDirectory
105
+ templateDirectory,
106
+ databaseUrl: answers.databaseUrl?.trim() || void 0,
107
+ introspect: answers.introspect || false
93
108
  };
94
109
  }
95
110
  async function createProject(options) {
@@ -127,20 +142,27 @@ async function createProject(options) {
127
142
  if (fs.existsSync(envTemplatePath) && !fs.existsSync(envPath)) {
128
143
  fs.renameSync(envTemplatePath, envPath);
129
144
  const jwtSecret = crypto.randomBytes(32).toString("hex");
130
- const dbPassword = crypto.randomBytes(16).toString("hex");
131
145
  let envContent = fs.readFileSync(envPath, "utf-8");
132
- envContent = envContent.replace(
133
- "postgresql://rebase:password@localhost:5432/rebase",
134
- `postgresql://rebase:${dbPassword}@localhost:5432/rebase`
135
- );
136
146
  envContent = envContent.replace(
137
147
  "change-this-to-a-secure-random-string",
138
148
  jwtSecret
139
149
  );
140
- envContent += `
150
+ if (options.databaseUrl) {
151
+ envContent = envContent.replace(
152
+ "postgresql://rebase:password@localhost:5432/rebase",
153
+ options.databaseUrl
154
+ );
155
+ } else {
156
+ const dbPassword = crypto.randomBytes(16).toString("hex");
157
+ envContent = envContent.replace(
158
+ "postgresql://rebase:password@localhost:5432/rebase",
159
+ `postgresql://rebase:${dbPassword}@localhost:5432/rebase`
160
+ );
161
+ envContent += `
141
162
  # Docker Compose Database Password
142
163
  POSTGRES_PASSWORD=${dbPassword}
143
164
  `;
165
+ }
144
166
  fs.writeFileSync(envPath, envContent, "utf-8");
145
167
  }
146
168
  if (options.git) {
@@ -164,6 +186,26 @@ POSTGRES_PASSWORD=${dbPassword}
164
186
  console.warn(chalk.yellow(" Warning: Failed to install dependencies. You may need to run `pnpm install` manually."));
165
187
  }
166
188
  }
189
+ if (options.introspect) {
190
+ console.log("");
191
+ if (options.installDeps) {
192
+ console.log(chalk.gray(" Introspecting database and generating collections..."));
193
+ console.log("");
194
+ try {
195
+ await execa("pnpm", ["exec", "rebase", "schema", "introspect"], {
196
+ cwd: options.targetDirectory,
197
+ stdio: "inherit"
198
+ });
199
+ console.log(chalk.green(" Database successfully introspected!"));
200
+ } catch {
201
+ console.warn(chalk.yellow(" Warning: Failed to introspect database automatically."));
202
+ console.warn(chalk.yellow(" You can run `pnpm exec rebase schema introspect` manually after setup."));
203
+ }
204
+ } else {
205
+ console.warn(chalk.yellow(" Skipping introspection because dependencies were not installed."));
206
+ console.warn(chalk.yellow(" Run `pnpm install` then `pnpm exec rebase schema introspect` manually."));
207
+ }
208
+ }
167
209
  console.log("");
168
210
  console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
169
211
  console.log("");
@@ -174,12 +216,19 @@ POSTGRES_PASSWORD=${dbPassword}
174
216
  console.log(` ${chalk.cyan("pnpm install")}`);
175
217
  }
176
218
  console.log("");
177
- console.log(chalk.gray(" # Set up your database connection in .env"));
178
- console.log(chalk.gray(" # Then run:"));
219
+ if (options.databaseUrl) {
220
+ console.log(chalk.gray(" # Your database is configured! Start the dev server:"));
221
+ } else {
222
+ console.log(chalk.gray(" # A local database configuration has been generated in .env"));
223
+ console.log(chalk.gray(" # If using the included docker-compose.yml, start it with:"));
224
+ console.log(` ${chalk.cyan("docker compose up -d")}`);
225
+ console.log("");
226
+ console.log(chalk.gray(" # Then start the dev server:"));
227
+ }
179
228
  console.log("");
180
229
  console.log(` ${chalk.cyan("pnpm dev")}`);
181
230
  console.log("");
182
- console.log(chalk.gray("This starts both the backend (Express + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
231
+ console.log(chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
183
232
  console.log("");
184
233
  console.log(chalk.gray("Docs: https://rebase.pro/docs"));
185
234
  console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
@@ -190,14 +239,63 @@ async function replacePlaceholders(options) {
190
239
  "package.json",
191
240
  "frontend/package.json",
192
241
  "backend/package.json",
193
- "shared/package.json",
242
+ "config/package.json",
194
243
  "frontend/index.html"
195
244
  ];
245
+ const packageJsonPath = path.resolve(cliRoot, "package.json");
246
+ let cliVersion = "latest";
247
+ if (fs.existsSync(packageJsonPath)) {
248
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
249
+ cliVersion = pkg.version || "latest";
250
+ }
251
+ const versionCache = /* @__PURE__ */ new Map();
252
+ const getPackageVersion = async (pkgName) => {
253
+ if (versionCache.has(pkgName)) return versionCache.get(pkgName);
254
+ let versionToUse = cliVersion;
255
+ try {
256
+ const { stdout } = await execa("npm", ["--loglevel", "error", "info", `${pkgName}@${cliVersion}`, "version"]);
257
+ if (!stdout.trim()) throw new Error("Not found");
258
+ versionToUse = stdout.trim();
259
+ } catch {
260
+ try {
261
+ const tag = cliVersion.includes("canary") ? "canary" : "latest";
262
+ const { stdout } = await execa("npm", ["--loglevel", "error", "info", `${pkgName}@${tag}`, "version"]);
263
+ if (!stdout.trim()) throw new Error("Not found");
264
+ versionToUse = stdout.trim();
265
+ } catch {
266
+ try {
267
+ const { stdout } = await execa("npm", ["--loglevel", "error", "info", pkgName, "version"]);
268
+ versionToUse = stdout.trim() || "latest";
269
+ } catch {
270
+ versionToUse = "latest";
271
+ }
272
+ }
273
+ }
274
+ versionCache.set(pkgName, versionToUse);
275
+ return versionToUse;
276
+ };
277
+ const allPackages = /* @__PURE__ */ new Set();
278
+ const fileContents = /* @__PURE__ */ new Map();
196
279
  for (const file of filesToProcess) {
197
280
  const fullPath = path.resolve(options.targetDirectory, file);
198
281
  if (!fs.existsSync(fullPath)) continue;
199
- let content = fs.readFileSync(fullPath, "utf-8");
200
- content = content.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
282
+ const content = fs.readFileSync(fullPath, "utf-8");
283
+ fileContents.set(fullPath, content);
284
+ const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
285
+ for (const match of matches) {
286
+ allPackages.add(match[1]);
287
+ }
288
+ }
289
+ console.log(chalk.gray(" Resolving package versions..."));
290
+ await Promise.all(Array.from(allPackages).map(getPackageVersion));
291
+ for (const [fullPath, originalContent] of fileContents.entries()) {
292
+ let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
293
+ const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
294
+ for (const match of matches) {
295
+ const pkgName = match[1];
296
+ const resolvedVersion = versionCache.get(pkgName) || "latest";
297
+ content = content.replace(new RegExp(`"${pkgName}":\\s*"workspace:\\*"`, "g"), `"${pkgName}": "${resolvedVersion}"`);
298
+ }
201
299
  fs.writeFileSync(fullPath, content, "utf-8");
202
300
  }
203
301
  }
@@ -309,7 +407,7 @@ async function generateSdkCommand(args) {
309
407
  console.log(chalk.green.bold(" ✓ SDK generated successfully!"));
310
408
  console.log("");
311
409
  console.log(chalk.gray(" Usage:"));
312
- console.log(chalk.gray(` import { createRebaseClient } from '@rebasepro/client';`));
410
+ console.log(chalk.gray(" import { createRebaseClient } from '@rebasepro/client';"));
313
411
  console.log(chalk.gray(` import type { Database } from './${path.relative(cwd, path.join(resolvedOutput, "database.types"))}';`));
314
412
  console.log("");
315
413
  console.log(chalk.gray(" const rebase = createRebaseClient<Database>({"));
@@ -336,7 +434,7 @@ function findProjectRoot(startDir = process.cwd()) {
336
434
  }
337
435
  } catch {
338
436
  }
339
- if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "shared"))) {
437
+ if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) {
340
438
  return dir;
341
439
  }
342
440
  }
@@ -353,7 +451,10 @@ function getActiveBackendPlugin(backendDir) {
353
451
  if (!fs.existsSync(pkgPath)) return null;
354
452
  try {
355
453
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
356
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
454
+ const deps = {
455
+ ...pkg.dependencies,
456
+ ...pkg.devDependencies
457
+ };
357
458
  for (const dep of Object.keys(deps)) {
358
459
  if (dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core") {
359
460
  return dep;
@@ -368,6 +469,7 @@ function resolvePluginCliScript(backendDir, pluginName) {
368
469
  path.join(backendDir, "node_modules", pluginName, "src", "cli.ts"),
369
470
  path.join(backendDir, "node_modules", pluginName, "dist", "cli.js"),
370
471
  // For monorepo dev mode:
472
+ path.resolve(backendDir, "..", "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
371
473
  path.resolve(backendDir, "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
372
474
  path.resolve(backendDir, "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts")
373
475
  ];
@@ -464,13 +566,13 @@ async function schemaCommand(subcommand, rawArgs) {
464
566
  console.error(chalk.red("✗ Could not find tsx binary."));
465
567
  process.exit(1);
466
568
  }
467
- await execa(tsxBin, [pluginCli, "schema", subcommand, ...rawArgs.slice(2)], {
569
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
468
570
  cwd: backendDir,
469
571
  stdio: "inherit",
470
572
  env
471
573
  });
472
574
  } else {
473
- await execa("node", [pluginCli, "schema", subcommand, ...rawArgs.slice(2)], {
575
+ await execa("node", [pluginCli, ...rawArgs.slice(2)], {
474
576
  cwd: backendDir,
475
577
  stdio: "inherit",
476
578
  env
@@ -490,11 +592,15 @@ ${chalk.green.bold("Usage")}
490
592
  ${chalk.green.bold("Commands")}
491
593
  ${chalk.gray("(Commands are provided by your active database driver plugin)")}
492
594
  ${chalk.blue.bold("generate")} Generate Schema from collection definitions
595
+ ${chalk.blue.bold("introspect")} Introspect an existing database to generate collection definitions
493
596
 
494
597
  ${chalk.green.bold("generate Options")}
495
598
  ${chalk.blue("--collections, -c")} Path to collections directory
496
599
  ${chalk.blue("--output, -o")} Output path for generated schema
497
600
  ${chalk.blue("--watch, -w")} Watch for changes and regenerate automatically
601
+
602
+ ${chalk.green.bold("introspect Options")}
603
+ ${chalk.blue("--output, -o")} Output directory for generated collection files
498
604
  `);
499
605
  }
500
606
  async function dbCommand(subcommand, rawArgs) {
@@ -528,13 +634,13 @@ async function dbCommand(subcommand, rawArgs) {
528
634
  console.error(chalk.red("✗ Could not find tsx binary."));
529
635
  process.exit(1);
530
636
  }
531
- await execa(tsxBin, [pluginCli, "db", subcommand, ...rawArgs.slice(2)], {
637
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
532
638
  cwd: backendDir,
533
639
  stdio: "inherit",
534
640
  env
535
641
  });
536
642
  } else {
537
- await execa("node", [pluginCli, "db", subcommand, ...rawArgs.slice(2)], {
643
+ await execa("node", [pluginCli, ...rawArgs.slice(2)], {
538
644
  cwd: backendDir,
539
645
  stdio: "inherit",
540
646
  env
@@ -572,6 +678,27 @@ ${chalk.green.bold("Examples")}
572
678
  rebase db branch create feature_auth
573
679
  `);
574
680
  }
681
+ const DEV_PORT_FILENAME = ".rebase-dev-port";
682
+ function getProjectPort(projectRoot) {
683
+ let hash = 0;
684
+ for (let i = 0; i < projectRoot.length; i++) {
685
+ hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
686
+ }
687
+ return 3001 + Math.abs(hash) % 999;
688
+ }
689
+ function resolveStartPort(projectRoot, explicitPort) {
690
+ if (explicitPort) return explicitPort;
691
+ if (process.env.PORT) return parseInt(process.env.PORT, 10);
692
+ try {
693
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
694
+ if (fs.existsSync(portFile)) {
695
+ const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
696
+ if (saved > 0 && saved < 65536) return saved;
697
+ }
698
+ } catch {
699
+ }
700
+ return getProjectPort(projectRoot);
701
+ }
575
702
  async function devCommand(rawArgs) {
576
703
  const args = arg(
577
704
  {
@@ -599,6 +726,7 @@ async function devCommand(rawArgs) {
599
726
  const frontendDir = findFrontendDir(projectRoot);
600
727
  const backendOnly = args["--backend-only"] || false;
601
728
  const frontendOnly = args["--frontend-only"] || false;
729
+ const startPort = resolveStartPort(projectRoot, args["--port"]);
602
730
  console.log("");
603
731
  console.log(chalk.bold(" 🚀 Rebase Dev Server"));
604
732
  console.log("");
@@ -607,6 +735,7 @@ async function devCommand(rawArgs) {
607
735
  let backendUrl = "";
608
736
  let debounceSummary = null;
609
737
  let bannerPrinted = false;
738
+ let resolvedBackendPort = null;
610
739
  const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
611
740
  function printSummary() {
612
741
  if (!frontendUrl || !backendUrl) return;
@@ -619,7 +748,7 @@ async function devCommand(rawArgs) {
619
748
  console.log(chalk.cyan("│ ✨ Rebase Admin App is ready! │"));
620
749
  const cleanUrl = stripAnsi(frontendUrl);
621
750
  const paddedUrl = cleanUrl.padEnd(40);
622
- console.log(chalk.cyan(`│ 👉 Frontend URL: `) + chalk.white(paddedUrl) + chalk.cyan(`│`));
751
+ console.log(chalk.cyan("│ 👉 Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
623
752
  console.log(chalk.cyan("│ │"));
624
753
  console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
625
754
  console.log("");
@@ -627,15 +756,74 @@ async function devCommand(rawArgs) {
627
756
  }, 500);
628
757
  }
629
758
  const cleanup = () => {
759
+ try {
760
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
761
+ if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
762
+ const urlFile = path.join(projectRoot, ".rebase-dev-url");
763
+ if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
764
+ } catch {
765
+ }
630
766
  children.forEach((child) => {
631
- if (!child.killed) {
632
- child.kill("SIGTERM");
767
+ if (child.pid && !child.killed) {
768
+ try {
769
+ if (process.platform === "win32") {
770
+ execa.commandSync(`taskkill /pid ${child.pid} /T /F`);
771
+ } else {
772
+ process.kill(-child.pid, "SIGKILL");
773
+ }
774
+ } catch (e) {
775
+ try {
776
+ child.kill("SIGKILL");
777
+ } catch (err) {
778
+ }
779
+ }
633
780
  }
634
781
  });
635
782
  process.exit(0);
636
783
  };
637
784
  process.on("SIGINT", cleanup);
638
785
  process.on("SIGTERM", cleanup);
786
+ function startFrontend(backendPort) {
787
+ if (!frontendDir) return;
788
+ console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
789
+ const frontendEnv = { ...process.env };
790
+ if (backendPort) {
791
+ frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
792
+ console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
793
+ }
794
+ const frontendChild = execa(
795
+ "pnpm",
796
+ ["run", "dev"],
797
+ {
798
+ cwd: frontendDir,
799
+ stdio: ["inherit", "pipe", "pipe"],
800
+ env: frontendEnv,
801
+ shell: true,
802
+ detached: process.platform !== "win32"
803
+ }
804
+ );
805
+ frontendChild.catch(() => {
806
+ });
807
+ frontendChild.stdout?.on("data", (data) => {
808
+ const lines = data.toString().split("\n").filter(Boolean);
809
+ lines.forEach((line) => {
810
+ console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
811
+ const cleanLine = stripAnsi(line);
812
+ const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
813
+ if (cleanLine.includes("Local:") && urlMatch) {
814
+ frontendUrl = urlMatch[1];
815
+ printSummary();
816
+ }
817
+ });
818
+ });
819
+ frontendChild.stderr?.on("data", (data) => {
820
+ const lines = data.toString().split("\n").filter(Boolean);
821
+ lines.forEach((line) => {
822
+ console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
823
+ });
824
+ });
825
+ children.push(frontendChild);
826
+ }
639
827
  if (!frontendOnly && backendDir) {
640
828
  const tsxBin = resolveTsx(projectRoot);
641
829
  if (!tsxBin) {
@@ -648,22 +836,19 @@ async function devCommand(rawArgs) {
648
836
  if (envFile) {
649
837
  env.DOTENV_CONFIG_PATH = envFile;
650
838
  }
651
- if (args["--port"]) {
652
- env.PORT = String(args["--port"]);
653
- }
654
- path.join(backendDir, "src", "index.ts");
655
- const watchDirs = [
656
- `--watch="${path.join("..", "shared", "**", "*")}"`
657
- ];
839
+ env.PORT = String(startPort);
658
840
  console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
841
+ console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
842
+ let frontendLaunched = false;
659
843
  const backendChild = execa(
660
844
  tsxBin,
661
- ["watch", ...watchDirs, "--conditions", "development", "src/index.ts"],
845
+ ["watch", `--watch="${path.join("..", "config", "**", "*")}"`, "--conditions", "development", "src/index.ts"],
662
846
  {
663
847
  cwd: backendDir,
664
848
  stdio: ["inherit", "pipe", "pipe"],
665
849
  env,
666
- shell: true
850
+ shell: true,
851
+ detached: process.platform !== "win32"
667
852
  }
668
853
  );
669
854
  backendChild.catch(() => {
@@ -673,9 +858,19 @@ async function devCommand(rawArgs) {
673
858
  lines.forEach((line) => {
674
859
  console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
675
860
  const cleanLine = stripAnsi(line);
676
- if (cleanLine.includes("Server running at http://")) {
861
+ const serverMatch = cleanLine.match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
862
+ if (serverMatch) {
863
+ resolvedBackendPort = parseInt(serverMatch[1], 10);
677
864
  backendUrl = "started";
678
865
  printSummary();
866
+ const urlFile = path.join(projectRoot, ".rebase-dev-url");
867
+ fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
868
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
869
+ fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
870
+ if (!backendOnly && frontendDir && !frontendLaunched) {
871
+ frontendLaunched = true;
872
+ startFrontend(resolvedBackendPort);
873
+ }
679
874
  }
680
875
  });
681
876
  });
@@ -689,39 +884,8 @@ async function devCommand(rawArgs) {
689
884
  } else if (!frontendOnly && !backendDir) {
690
885
  console.warn(chalk.yellow(" ⚠ No backend/ directory found, skipping backend."));
691
886
  }
692
- if (!backendOnly && frontendDir) {
693
- console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
694
- const frontendChild = execa(
695
- "pnpm",
696
- ["run", "dev"],
697
- {
698
- cwd: frontendDir,
699
- stdio: ["inherit", "pipe", "pipe"],
700
- env: process.env,
701
- shell: true
702
- }
703
- );
704
- frontendChild.catch(() => {
705
- });
706
- frontendChild.stdout?.on("data", (data) => {
707
- const lines = data.toString().split("\n").filter(Boolean);
708
- lines.forEach((line) => {
709
- console.log(`${chalk.magenta.bold("[frontend]")} ${line}`);
710
- const cleanLine = stripAnsi(line);
711
- const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
712
- if (cleanLine.includes("Local:") && urlMatch) {
713
- frontendUrl = urlMatch[1];
714
- printSummary();
715
- }
716
- });
717
- });
718
- frontendChild.stderr?.on("data", (data) => {
719
- const lines = data.toString().split("\n").filter(Boolean);
720
- lines.forEach((line) => {
721
- console.log(`${chalk.magenta.bold("[frontend]")} ${line}`);
722
- });
723
- });
724
- children.push(frontendChild);
887
+ if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) {
888
+ startFrontend(null);
725
889
  } else if (!backendOnly && !frontendDir) {
726
890
  console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
727
891
  }
@@ -750,11 +914,19 @@ ${chalk.green.bold("Usage")}
750
914
  ${chalk.green.bold("Options")}
751
915
  ${chalk.blue("--backend-only, -b")} Only start the backend server
752
916
  ${chalk.blue("--frontend-only, -f")} Only start the frontend server
753
- ${chalk.blue("--port, -p")} Backend port (default: 3001)
917
+ ${chalk.blue("--port, -p")} Backend port (default: auto-detected per project)
754
918
 
755
919
  ${chalk.green.bold("Description")}
756
- Starts both the backend (tsx watch + Express) and frontend (Vite)
920
+ Starts both the backend (tsx watch + Hono) and frontend (Vite)
757
921
  dev servers concurrently with color-coded output prefixes.
922
+
923
+ Each project automatically receives a unique default port derived
924
+ from its directory path, preventing collisions when running multiple
925
+ Rebase instances simultaneously.
926
+
927
+ If the assigned port is already in use, the server will automatically
928
+ try the next available port. The frontend is started only after the
929
+ backend is ready, and VITE_API_URL is injected automatically.
758
930
  `);
759
931
  }
760
932
  async function authCommand(subcommand, rawArgs) {
@@ -891,6 +1063,49 @@ ${chalk.green.bold("Examples")}
891
1063
  rebase auth reset-password --email user@example.com --password MyNewPass!
892
1064
  `);
893
1065
  }
1066
+ async function doctorCommand(rawArgs) {
1067
+ const projectRoot = requireProjectRoot();
1068
+ const backendDir = requireBackendDir(projectRoot);
1069
+ const activePlugin = getActiveBackendPlugin(backendDir);
1070
+ if (!activePlugin) {
1071
+ console.error(chalk.red("✗ Could not detect an active database plugin."));
1072
+ console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
1073
+ process.exit(1);
1074
+ }
1075
+ const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
1076
+ if (!pluginCli) {
1077
+ console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
1078
+ process.exit(1);
1079
+ }
1080
+ const envFile = findEnvFile(projectRoot);
1081
+ const env = { ...process.env };
1082
+ if (envFile) {
1083
+ env.DOTENV_CONFIG_PATH = envFile;
1084
+ }
1085
+ try {
1086
+ const isTs = pluginCli.endsWith(".ts");
1087
+ if (isTs) {
1088
+ const tsxBin = resolveTsx(projectRoot);
1089
+ if (!tsxBin) {
1090
+ console.error(chalk.red("✗ Could not find tsx binary."));
1091
+ process.exit(1);
1092
+ }
1093
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
1094
+ cwd: backendDir,
1095
+ stdio: "inherit",
1096
+ env
1097
+ });
1098
+ } else {
1099
+ await execa("node", [pluginCli, ...rawArgs.slice(2)], {
1100
+ cwd: backendDir,
1101
+ stdio: "inherit",
1102
+ env
1103
+ });
1104
+ }
1105
+ } catch {
1106
+ process.exit(1);
1107
+ }
1108
+ }
894
1109
  const __filename$1 = fileURLToPath(import.meta.url);
895
1110
  const __dirname$1 = path.dirname(__filename$1);
896
1111
  function getVersion() {
@@ -922,7 +1137,7 @@ async function entry(args) {
922
1137
  }
923
1138
  const command = parsedArgs._[0];
924
1139
  const subcommand = parsedArgs._[1];
925
- const namespacedCommands = ["schema", "db", "dev", "auth"];
1140
+ const namespacedCommands = ["schema", "db", "dev", "auth", "doctor"];
926
1141
  if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
927
1142
  printHelp();
928
1143
  return;
@@ -932,7 +1147,7 @@ async function entry(args) {
932
1147
  case "init":
933
1148
  await createRebaseApp(args);
934
1149
  break;
935
- case "generate-sdk":
1150
+ case "generate-sdk": {
936
1151
  const sdkArgs = arg(
937
1152
  {
938
1153
  "--collections-dir": String,
@@ -946,11 +1161,12 @@ async function entry(args) {
946
1161
  }
947
1162
  );
948
1163
  await generateSdkCommand({
949
- collectionsDir: sdkArgs["--collections-dir"] || "./shared/collections",
1164
+ collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
950
1165
  output: sdkArgs["--output"] || "./generated/sdk",
951
1166
  cwd: process.cwd()
952
1167
  });
953
1168
  break;
1169
+ }
954
1170
  case "schema":
955
1171
  await schemaCommand(effectiveSubcommand, args);
956
1172
  break;
@@ -963,6 +1179,9 @@ async function entry(args) {
963
1179
  case "auth":
964
1180
  await authCommand(effectiveSubcommand, args);
965
1181
  break;
1182
+ case "doctor":
1183
+ await doctorCommand(args);
1184
+ break;
966
1185
  default:
967
1186
  console.log(chalk.red(`Unknown command: ${command}`));
968
1187
  console.log("");
@@ -999,6 +1218,9 @@ ${chalk.green.bold("Auth")}
999
1218
  ${chalk.blue.bold("auth reset-password")} Reset a user's password
1000
1219
  ${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
1001
1220
 
1221
+ ${chalk.green.bold("Diagnostics")}
1222
+ ${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
1223
+
1002
1224
  ${chalk.green.bold("Options")}
1003
1225
  ${chalk.blue("--version, -v")} Show version number
1004
1226
  ${chalk.blue("--help, -h")} Show this help message