@rebasepro/cli 0.0.1-canary.4d4fb3e → 0.0.1-canary.629af03

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 +294 -73
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.es.js +294 -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
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function doctorCommand(rawArgs: string[]): Promise<void>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CLI command: generate-sdk
3
3
  *
4
- * Reads collection definitions from a specified directory (default: ./shared/collections),
4
+ * Reads collection definitions from a specified directory (default: ./config/collections),
5
5
  * generates a typed JS SDK, and writes it to the output directory (default: ./generated/sdk).
6
6
  *
7
7
  * Uses jiti for dynamic TypeScript import of collection files.
@@ -4,5 +4,7 @@ export interface InitOptions {
4
4
  installDeps: boolean;
5
5
  targetDirectory: string;
6
6
  templateDirectory: string;
7
+ databaseUrl?: string;
8
+ introspect?: boolean;
7
9
  }
8
10
  export declare function createRebaseApp(rawArgs: string[]): Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.cjs CHANGED
@@ -89,16 +89,31 @@ ${chalk.bold("Rebase")} — Create a new project 🚀
89
89
  default: true
90
90
  });
91
91
  }
92
+ questions.push({
93
+ type: "input",
94
+ name: "databaseUrl",
95
+ message: "Enter your PostgreSQL database connection string (leave blank to use a local default):",
96
+ default: ""
97
+ });
98
+ questions.push({
99
+ type: "confirm",
100
+ name: "introspect",
101
+ message: "Would you like to introspect this database to automatically generate collections?",
102
+ default: true,
103
+ when: (answers2) => !!answers2.databaseUrl?.trim()
104
+ });
92
105
  const answers = await inquirer.prompt(questions);
93
- const projectName = nameArg || answers.projectName;
94
- const targetDirectory = path.resolve(process.cwd(), projectName);
106
+ const targetDirectory = path.resolve(process.cwd(), nameArg || answers.projectName);
107
+ const projectName = path.basename(targetDirectory);
95
108
  const templateDirectory = path.resolve(cliRoot, "templates", "template");
96
109
  return {
97
110
  projectName,
98
111
  git: args["--git"] || answers.git || false,
99
112
  installDeps: args["--install"] || answers.installDeps || false,
100
113
  targetDirectory,
101
- templateDirectory
114
+ templateDirectory,
115
+ databaseUrl: answers.databaseUrl?.trim() || void 0,
116
+ introspect: answers.introspect || false
102
117
  };
103
118
  }
104
119
  async function createProject(options) {
@@ -136,20 +151,27 @@ ${chalk.bold("Rebase")} — Create a new project 🚀
136
151
  if (fs.existsSync(envTemplatePath) && !fs.existsSync(envPath)) {
137
152
  fs.renameSync(envTemplatePath, envPath);
138
153
  const jwtSecret = crypto.randomBytes(32).toString("hex");
139
- const dbPassword = crypto.randomBytes(16).toString("hex");
140
154
  let envContent = fs.readFileSync(envPath, "utf-8");
141
- envContent = envContent.replace(
142
- "postgresql://rebase:password@localhost:5432/rebase",
143
- `postgresql://rebase:${dbPassword}@localhost:5432/rebase`
144
- );
145
155
  envContent = envContent.replace(
146
156
  "change-this-to-a-secure-random-string",
147
157
  jwtSecret
148
158
  );
149
- envContent += `
159
+ if (options.databaseUrl) {
160
+ envContent = envContent.replace(
161
+ "postgresql://rebase:password@localhost:5432/rebase",
162
+ options.databaseUrl
163
+ );
164
+ } else {
165
+ const dbPassword = crypto.randomBytes(16).toString("hex");
166
+ envContent = envContent.replace(
167
+ "postgresql://rebase:password@localhost:5432/rebase",
168
+ `postgresql://rebase:${dbPassword}@localhost:5432/rebase`
169
+ );
170
+ envContent += `
150
171
  # Docker Compose Database Password
151
172
  POSTGRES_PASSWORD=${dbPassword}
152
173
  `;
174
+ }
153
175
  fs.writeFileSync(envPath, envContent, "utf-8");
154
176
  }
155
177
  if (options.git) {
@@ -173,6 +195,26 @@ POSTGRES_PASSWORD=${dbPassword}
173
195
  console.warn(chalk.yellow(" Warning: Failed to install dependencies. You may need to run `pnpm install` manually."));
174
196
  }
175
197
  }
198
+ if (options.introspect) {
199
+ console.log("");
200
+ if (options.installDeps) {
201
+ console.log(chalk.gray(" Introspecting database and generating collections..."));
202
+ console.log("");
203
+ try {
204
+ await execa("pnpm", ["exec", "rebase", "schema", "introspect"], {
205
+ cwd: options.targetDirectory,
206
+ stdio: "inherit"
207
+ });
208
+ console.log(chalk.green(" Database successfully introspected!"));
209
+ } catch {
210
+ console.warn(chalk.yellow(" Warning: Failed to introspect database automatically."));
211
+ console.warn(chalk.yellow(" You can run `pnpm exec rebase schema introspect` manually after setup."));
212
+ }
213
+ } else {
214
+ console.warn(chalk.yellow(" Skipping introspection because dependencies were not installed."));
215
+ console.warn(chalk.yellow(" Run `pnpm install` then `pnpm exec rebase schema introspect` manually."));
216
+ }
217
+ }
176
218
  console.log("");
177
219
  console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
178
220
  console.log("");
@@ -183,12 +225,19 @@ POSTGRES_PASSWORD=${dbPassword}
183
225
  console.log(` ${chalk.cyan("pnpm install")}`);
184
226
  }
185
227
  console.log("");
186
- console.log(chalk.gray(" # Set up your database connection in .env"));
187
- console.log(chalk.gray(" # Then run:"));
228
+ if (options.databaseUrl) {
229
+ console.log(chalk.gray(" # Your database is configured! Start the dev server:"));
230
+ } else {
231
+ console.log(chalk.gray(" # A local database configuration has been generated in .env"));
232
+ console.log(chalk.gray(" # If using the included docker-compose.yml, start it with:"));
233
+ console.log(` ${chalk.cyan("docker compose up -d")}`);
234
+ console.log("");
235
+ console.log(chalk.gray(" # Then start the dev server:"));
236
+ }
188
237
  console.log("");
189
238
  console.log(` ${chalk.cyan("pnpm dev")}`);
190
239
  console.log("");
191
- console.log(chalk.gray("This starts both the backend (Express + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
240
+ console.log(chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
192
241
  console.log("");
193
242
  console.log(chalk.gray("Docs: https://rebase.pro/docs"));
194
243
  console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
@@ -199,14 +248,63 @@ POSTGRES_PASSWORD=${dbPassword}
199
248
  "package.json",
200
249
  "frontend/package.json",
201
250
  "backend/package.json",
202
- "shared/package.json",
251
+ "config/package.json",
203
252
  "frontend/index.html"
204
253
  ];
254
+ const packageJsonPath = path.resolve(cliRoot, "package.json");
255
+ let cliVersion = "latest";
256
+ if (fs.existsSync(packageJsonPath)) {
257
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
258
+ cliVersion = pkg.version || "latest";
259
+ }
260
+ const versionCache = /* @__PURE__ */ new Map();
261
+ const getPackageVersion = async (pkgName) => {
262
+ if (versionCache.has(pkgName)) return versionCache.get(pkgName);
263
+ let versionToUse = cliVersion;
264
+ try {
265
+ const { stdout } = await execa("npm", ["--loglevel", "error", "info", `${pkgName}@${cliVersion}`, "version"]);
266
+ if (!stdout.trim()) throw new Error("Not found");
267
+ versionToUse = stdout.trim();
268
+ } catch {
269
+ try {
270
+ const tag = cliVersion.includes("canary") ? "canary" : "latest";
271
+ const { stdout } = await execa("npm", ["--loglevel", "error", "info", `${pkgName}@${tag}`, "version"]);
272
+ if (!stdout.trim()) throw new Error("Not found");
273
+ versionToUse = stdout.trim();
274
+ } catch {
275
+ try {
276
+ const { stdout } = await execa("npm", ["--loglevel", "error", "info", pkgName, "version"]);
277
+ versionToUse = stdout.trim() || "latest";
278
+ } catch {
279
+ versionToUse = "latest";
280
+ }
281
+ }
282
+ }
283
+ versionCache.set(pkgName, versionToUse);
284
+ return versionToUse;
285
+ };
286
+ const allPackages = /* @__PURE__ */ new Set();
287
+ const fileContents = /* @__PURE__ */ new Map();
205
288
  for (const file of filesToProcess) {
206
289
  const fullPath = path.resolve(options.targetDirectory, file);
207
290
  if (!fs.existsSync(fullPath)) continue;
208
- let content = fs.readFileSync(fullPath, "utf-8");
209
- content = content.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
291
+ const content = fs.readFileSync(fullPath, "utf-8");
292
+ fileContents.set(fullPath, content);
293
+ const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
294
+ for (const match of matches) {
295
+ allPackages.add(match[1]);
296
+ }
297
+ }
298
+ console.log(chalk.gray(" Resolving package versions..."));
299
+ await Promise.all(Array.from(allPackages).map(getPackageVersion));
300
+ for (const [fullPath, originalContent] of fileContents.entries()) {
301
+ let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
302
+ const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
303
+ for (const match of matches) {
304
+ const pkgName = match[1];
305
+ const resolvedVersion = versionCache.get(pkgName) || "latest";
306
+ content = content.replace(new RegExp(`"${pkgName}":\\s*"workspace:\\*"`, "g"), `"${pkgName}": "${resolvedVersion}"`);
307
+ }
210
308
  fs.writeFileSync(fullPath, content, "utf-8");
211
309
  }
212
310
  }
@@ -318,7 +416,7 @@ Expected a default export of EntityCollection[] or an object with named collecti
318
416
  console.log(chalk.green.bold(" ✓ SDK generated successfully!"));
319
417
  console.log("");
320
418
  console.log(chalk.gray(" Usage:"));
321
- console.log(chalk.gray(` import { createRebaseClient } from '@rebasepro/client';`));
419
+ console.log(chalk.gray(" import { createRebaseClient } from '@rebasepro/client';"));
322
420
  console.log(chalk.gray(` import type { Database } from './${path.relative(cwd, path.join(resolvedOutput, "database.types"))}';`));
323
421
  console.log("");
324
422
  console.log(chalk.gray(" const rebase = createRebaseClient<Database>({"));
@@ -345,7 +443,7 @@ Expected a default export of EntityCollection[] or an object with named collecti
345
443
  }
346
444
  } catch {
347
445
  }
348
- if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "shared"))) {
446
+ if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) {
349
447
  return dir;
350
448
  }
351
449
  }
@@ -362,7 +460,10 @@ Expected a default export of EntityCollection[] or an object with named collecti
362
460
  if (!fs.existsSync(pkgPath)) return null;
363
461
  try {
364
462
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
365
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
463
+ const deps = {
464
+ ...pkg.dependencies,
465
+ ...pkg.devDependencies
466
+ };
366
467
  for (const dep of Object.keys(deps)) {
367
468
  if (dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core") {
368
469
  return dep;
@@ -473,13 +574,13 @@ Expected a default export of EntityCollection[] or an object with named collecti
473
574
  console.error(chalk.red("✗ Could not find tsx binary."));
474
575
  process.exit(1);
475
576
  }
476
- await execa(tsxBin, [pluginCli, "schema", subcommand, ...rawArgs.slice(2)], {
577
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
477
578
  cwd: backendDir,
478
579
  stdio: "inherit",
479
580
  env
480
581
  });
481
582
  } else {
482
- await execa("node", [pluginCli, "schema", subcommand, ...rawArgs.slice(2)], {
583
+ await execa("node", [pluginCli, ...rawArgs.slice(2)], {
483
584
  cwd: backendDir,
484
585
  stdio: "inherit",
485
586
  env
@@ -499,11 +600,15 @@ ${chalk.green.bold("Usage")}
499
600
  ${chalk.green.bold("Commands")}
500
601
  ${chalk.gray("(Commands are provided by your active database driver plugin)")}
501
602
  ${chalk.blue.bold("generate")} Generate Schema from collection definitions
603
+ ${chalk.blue.bold("introspect")} Introspect an existing database to generate collection definitions
502
604
 
503
605
  ${chalk.green.bold("generate Options")}
504
606
  ${chalk.blue("--collections, -c")} Path to collections directory
505
607
  ${chalk.blue("--output, -o")} Output path for generated schema
506
608
  ${chalk.blue("--watch, -w")} Watch for changes and regenerate automatically
609
+
610
+ ${chalk.green.bold("introspect Options")}
611
+ ${chalk.blue("--output, -o")} Output directory for generated collection files
507
612
  `);
508
613
  }
509
614
  async function dbCommand(subcommand, rawArgs) {
@@ -537,13 +642,13 @@ ${chalk.green.bold("generate Options")}
537
642
  console.error(chalk.red("✗ Could not find tsx binary."));
538
643
  process.exit(1);
539
644
  }
540
- await execa(tsxBin, [pluginCli, "db", subcommand, ...rawArgs.slice(2)], {
645
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
541
646
  cwd: backendDir,
542
647
  stdio: "inherit",
543
648
  env
544
649
  });
545
650
  } else {
546
- await execa("node", [pluginCli, "db", subcommand, ...rawArgs.slice(2)], {
651
+ await execa("node", [pluginCli, ...rawArgs.slice(2)], {
547
652
  cwd: backendDir,
548
653
  stdio: "inherit",
549
654
  env
@@ -581,6 +686,27 @@ ${chalk.green.bold("Examples")}
581
686
  rebase db branch create feature_auth
582
687
  `);
583
688
  }
689
+ const DEV_PORT_FILENAME = ".rebase-dev-port";
690
+ function getProjectPort(projectRoot) {
691
+ let hash = 0;
692
+ for (let i = 0; i < projectRoot.length; i++) {
693
+ hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
694
+ }
695
+ return 3001 + Math.abs(hash) % 999;
696
+ }
697
+ function resolveStartPort(projectRoot, explicitPort) {
698
+ if (explicitPort) return explicitPort;
699
+ if (process.env.PORT) return parseInt(process.env.PORT, 10);
700
+ try {
701
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
702
+ if (fs.existsSync(portFile)) {
703
+ const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
704
+ if (saved > 0 && saved < 65536) return saved;
705
+ }
706
+ } catch {
707
+ }
708
+ return getProjectPort(projectRoot);
709
+ }
584
710
  async function devCommand(rawArgs) {
585
711
  const args = arg(
586
712
  {
@@ -608,6 +734,7 @@ ${chalk.green.bold("Examples")}
608
734
  const frontendDir = findFrontendDir(projectRoot);
609
735
  const backendOnly = args["--backend-only"] || false;
610
736
  const frontendOnly = args["--frontend-only"] || false;
737
+ const startPort = resolveStartPort(projectRoot, args["--port"]);
611
738
  console.log("");
612
739
  console.log(chalk.bold(" 🚀 Rebase Dev Server"));
613
740
  console.log("");
@@ -616,6 +743,7 @@ ${chalk.green.bold("Examples")}
616
743
  let backendUrl = "";
617
744
  let debounceSummary = null;
618
745
  let bannerPrinted = false;
746
+ let resolvedBackendPort = null;
619
747
  const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
620
748
  function printSummary() {
621
749
  if (!frontendUrl || !backendUrl) return;
@@ -628,7 +756,7 @@ ${chalk.green.bold("Examples")}
628
756
  console.log(chalk.cyan("│ ✨ Rebase Admin App is ready! │"));
629
757
  const cleanUrl = stripAnsi(frontendUrl);
630
758
  const paddedUrl = cleanUrl.padEnd(40);
631
- console.log(chalk.cyan(`│ 👉 Frontend URL: `) + chalk.white(paddedUrl) + chalk.cyan(`│`));
759
+ console.log(chalk.cyan("│ 👉 Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
632
760
  console.log(chalk.cyan("│ │"));
633
761
  console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
634
762
  console.log("");
@@ -636,15 +764,74 @@ ${chalk.green.bold("Examples")}
636
764
  }, 500);
637
765
  }
638
766
  const cleanup = () => {
767
+ try {
768
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
769
+ if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
770
+ const urlFile = path.join(projectRoot, ".rebase-dev-url");
771
+ if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
772
+ } catch {
773
+ }
639
774
  children.forEach((child) => {
640
- if (!child.killed) {
641
- child.kill("SIGTERM");
775
+ if (child.pid && !child.killed) {
776
+ try {
777
+ if (process.platform === "win32") {
778
+ execa.commandSync(`taskkill /pid ${child.pid} /T /F`);
779
+ } else {
780
+ process.kill(-child.pid, "SIGKILL");
781
+ }
782
+ } catch (e) {
783
+ try {
784
+ child.kill("SIGKILL");
785
+ } catch (err) {
786
+ }
787
+ }
642
788
  }
643
789
  });
644
790
  process.exit(0);
645
791
  };
646
792
  process.on("SIGINT", cleanup);
647
793
  process.on("SIGTERM", cleanup);
794
+ function startFrontend(backendPort) {
795
+ if (!frontendDir) return;
796
+ console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
797
+ const frontendEnv = { ...process.env };
798
+ if (backendPort) {
799
+ frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
800
+ console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
801
+ }
802
+ const frontendChild = execa(
803
+ "pnpm",
804
+ ["run", "dev"],
805
+ {
806
+ cwd: frontendDir,
807
+ stdio: ["inherit", "pipe", "pipe"],
808
+ env: frontendEnv,
809
+ shell: true,
810
+ detached: process.platform !== "win32"
811
+ }
812
+ );
813
+ frontendChild.catch(() => {
814
+ });
815
+ frontendChild.stdout?.on("data", (data) => {
816
+ const lines = data.toString().split("\n").filter(Boolean);
817
+ lines.forEach((line) => {
818
+ console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
819
+ const cleanLine = stripAnsi(line);
820
+ const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
821
+ if (cleanLine.includes("Local:") && urlMatch) {
822
+ frontendUrl = urlMatch[1];
823
+ printSummary();
824
+ }
825
+ });
826
+ });
827
+ frontendChild.stderr?.on("data", (data) => {
828
+ const lines = data.toString().split("\n").filter(Boolean);
829
+ lines.forEach((line) => {
830
+ console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
831
+ });
832
+ });
833
+ children.push(frontendChild);
834
+ }
648
835
  if (!frontendOnly && backendDir) {
649
836
  const tsxBin = resolveTsx(projectRoot);
650
837
  if (!tsxBin) {
@@ -657,22 +844,19 @@ ${chalk.green.bold("Examples")}
657
844
  if (envFile) {
658
845
  env.DOTENV_CONFIG_PATH = envFile;
659
846
  }
660
- if (args["--port"]) {
661
- env.PORT = String(args["--port"]);
662
- }
663
- path.join(backendDir, "src", "index.ts");
664
- const watchDirs = [
665
- `--watch="${path.join("..", "shared", "**", "*")}"`
666
- ];
847
+ env.PORT = String(startPort);
667
848
  console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
849
+ console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
850
+ let frontendLaunched = false;
668
851
  const backendChild = execa(
669
852
  tsxBin,
670
- ["watch", ...watchDirs, "--conditions", "development", "src/index.ts"],
853
+ ["watch", `--watch="${path.join("..", "config", "**", "*")}"`, "--conditions", "development", "src/index.ts"],
671
854
  {
672
855
  cwd: backendDir,
673
856
  stdio: ["inherit", "pipe", "pipe"],
674
857
  env,
675
- shell: true
858
+ shell: true,
859
+ detached: process.platform !== "win32"
676
860
  }
677
861
  );
678
862
  backendChild.catch(() => {
@@ -682,9 +866,19 @@ ${chalk.green.bold("Examples")}
682
866
  lines.forEach((line) => {
683
867
  console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
684
868
  const cleanLine = stripAnsi(line);
685
- if (cleanLine.includes("Server running at http://")) {
869
+ const serverMatch = cleanLine.match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
870
+ if (serverMatch) {
871
+ resolvedBackendPort = parseInt(serverMatch[1], 10);
686
872
  backendUrl = "started";
687
873
  printSummary();
874
+ const urlFile = path.join(projectRoot, ".rebase-dev-url");
875
+ fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
876
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
877
+ fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
878
+ if (!backendOnly && frontendDir && !frontendLaunched) {
879
+ frontendLaunched = true;
880
+ startFrontend(resolvedBackendPort);
881
+ }
688
882
  }
689
883
  });
690
884
  });
@@ -698,39 +892,8 @@ ${chalk.green.bold("Examples")}
698
892
  } else if (!frontendOnly && !backendDir) {
699
893
  console.warn(chalk.yellow(" ⚠ No backend/ directory found, skipping backend."));
700
894
  }
701
- if (!backendOnly && frontendDir) {
702
- console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
703
- const frontendChild = execa(
704
- "pnpm",
705
- ["run", "dev"],
706
- {
707
- cwd: frontendDir,
708
- stdio: ["inherit", "pipe", "pipe"],
709
- env: process.env,
710
- shell: true
711
- }
712
- );
713
- frontendChild.catch(() => {
714
- });
715
- frontendChild.stdout?.on("data", (data) => {
716
- const lines = data.toString().split("\n").filter(Boolean);
717
- lines.forEach((line) => {
718
- console.log(`${chalk.magenta.bold("[frontend]")} ${line}`);
719
- const cleanLine = stripAnsi(line);
720
- const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
721
- if (cleanLine.includes("Local:") && urlMatch) {
722
- frontendUrl = urlMatch[1];
723
- printSummary();
724
- }
725
- });
726
- });
727
- frontendChild.stderr?.on("data", (data) => {
728
- const lines = data.toString().split("\n").filter(Boolean);
729
- lines.forEach((line) => {
730
- console.log(`${chalk.magenta.bold("[frontend]")} ${line}`);
731
- });
732
- });
733
- children.push(frontendChild);
895
+ if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) {
896
+ startFrontend(null);
734
897
  } else if (!backendOnly && !frontendDir) {
735
898
  console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
736
899
  }
@@ -759,11 +922,19 @@ ${chalk.green.bold("Usage")}
759
922
  ${chalk.green.bold("Options")}
760
923
  ${chalk.blue("--backend-only, -b")} Only start the backend server
761
924
  ${chalk.blue("--frontend-only, -f")} Only start the frontend server
762
- ${chalk.blue("--port, -p")} Backend port (default: 3001)
925
+ ${chalk.blue("--port, -p")} Backend port (default: auto-detected per project)
763
926
 
764
927
  ${chalk.green.bold("Description")}
765
- Starts both the backend (tsx watch + Express) and frontend (Vite)
928
+ Starts both the backend (tsx watch + Hono) and frontend (Vite)
766
929
  dev servers concurrently with color-coded output prefixes.
930
+
931
+ Each project automatically receives a unique default port derived
932
+ from its directory path, preventing collisions when running multiple
933
+ Rebase instances simultaneously.
934
+
935
+ If the assigned port is already in use, the server will automatically
936
+ try the next available port. The frontend is started only after the
937
+ backend is ready, and VITE_API_URL is injected automatically.
767
938
  `);
768
939
  }
769
940
  async function authCommand(subcommand, rawArgs) {
@@ -900,6 +1071,49 @@ ${chalk.green.bold("Examples")}
900
1071
  rebase auth reset-password --email user@example.com --password MyNewPass!
901
1072
  `);
902
1073
  }
1074
+ async function doctorCommand(rawArgs) {
1075
+ const projectRoot = requireProjectRoot();
1076
+ const backendDir = requireBackendDir(projectRoot);
1077
+ const activePlugin = getActiveBackendPlugin(backendDir);
1078
+ if (!activePlugin) {
1079
+ console.error(chalk.red("✗ Could not detect an active database plugin."));
1080
+ console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
1081
+ process.exit(1);
1082
+ }
1083
+ const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
1084
+ if (!pluginCli) {
1085
+ console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
1086
+ process.exit(1);
1087
+ }
1088
+ const envFile = findEnvFile(projectRoot);
1089
+ const env = { ...process.env };
1090
+ if (envFile) {
1091
+ env.DOTENV_CONFIG_PATH = envFile;
1092
+ }
1093
+ try {
1094
+ const isTs = pluginCli.endsWith(".ts");
1095
+ if (isTs) {
1096
+ const tsxBin = resolveTsx(projectRoot);
1097
+ if (!tsxBin) {
1098
+ console.error(chalk.red("✗ Could not find tsx binary."));
1099
+ process.exit(1);
1100
+ }
1101
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
1102
+ cwd: backendDir,
1103
+ stdio: "inherit",
1104
+ env
1105
+ });
1106
+ } else {
1107
+ await execa("node", [pluginCli, ...rawArgs.slice(2)], {
1108
+ cwd: backendDir,
1109
+ stdio: "inherit",
1110
+ env
1111
+ });
1112
+ }
1113
+ } catch {
1114
+ process.exit(1);
1115
+ }
1116
+ }
903
1117
  const __filename$1 = url.fileURLToPath(typeof document === "undefined" && typeof location === "undefined" ? require("url").pathToFileURL(__filename).href : typeof document === "undefined" ? location.href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href);
904
1118
  const __dirname$1 = path.dirname(__filename$1);
905
1119
  function getVersion() {
@@ -931,7 +1145,7 @@ ${chalk.green.bold("Examples")}
931
1145
  }
932
1146
  const command = parsedArgs._[0];
933
1147
  const subcommand = parsedArgs._[1];
934
- const namespacedCommands = ["schema", "db", "dev", "auth"];
1148
+ const namespacedCommands = ["schema", "db", "dev", "auth", "doctor"];
935
1149
  if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
936
1150
  printHelp();
937
1151
  return;
@@ -941,7 +1155,7 @@ ${chalk.green.bold("Examples")}
941
1155
  case "init":
942
1156
  await createRebaseApp(args);
943
1157
  break;
944
- case "generate-sdk":
1158
+ case "generate-sdk": {
945
1159
  const sdkArgs = arg(
946
1160
  {
947
1161
  "--collections-dir": String,
@@ -955,11 +1169,12 @@ ${chalk.green.bold("Examples")}
955
1169
  }
956
1170
  );
957
1171
  await generateSdkCommand({
958
- collectionsDir: sdkArgs["--collections-dir"] || "./shared/collections",
1172
+ collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
959
1173
  output: sdkArgs["--output"] || "./generated/sdk",
960
1174
  cwd: process.cwd()
961
1175
  });
962
1176
  break;
1177
+ }
963
1178
  case "schema":
964
1179
  await schemaCommand(effectiveSubcommand, args);
965
1180
  break;
@@ -972,6 +1187,9 @@ ${chalk.green.bold("Examples")}
972
1187
  case "auth":
973
1188
  await authCommand(effectiveSubcommand, args);
974
1189
  break;
1190
+ case "doctor":
1191
+ await doctorCommand(args);
1192
+ break;
975
1193
  default:
976
1194
  console.log(chalk.red(`Unknown command: ${command}`));
977
1195
  console.log("");
@@ -1008,6 +1226,9 @@ ${chalk.green.bold("Auth")}
1008
1226
  ${chalk.blue.bold("auth reset-password")} Reset a user's password
1009
1227
  ${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
1010
1228
 
1229
+ ${chalk.green.bold("Diagnostics")}
1230
+ ${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
1231
+
1011
1232
  ${chalk.green.bold("Options")}
1012
1233
  ${chalk.blue("--version, -v")} Show version number
1013
1234
  ${chalk.blue("--help, -h")} Show this help message