@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
@@ -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;
@@ -377,6 +478,7 @@ Expected a default export of EntityCollection[] or an object with named collecti
377
478
  path.join(backendDir, "node_modules", pluginName, "src", "cli.ts"),
378
479
  path.join(backendDir, "node_modules", pluginName, "dist", "cli.js"),
379
480
  // For monorepo dev mode:
481
+ path.resolve(backendDir, "..", "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
380
482
  path.resolve(backendDir, "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
381
483
  path.resolve(backendDir, "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts")
382
484
  ];
@@ -473,13 +575,13 @@ Expected a default export of EntityCollection[] or an object with named collecti
473
575
  console.error(chalk.red("✗ Could not find tsx binary."));
474
576
  process.exit(1);
475
577
  }
476
- await execa(tsxBin, [pluginCli, "schema", subcommand, ...rawArgs.slice(2)], {
578
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
477
579
  cwd: backendDir,
478
580
  stdio: "inherit",
479
581
  env
480
582
  });
481
583
  } else {
482
- await execa("node", [pluginCli, "schema", subcommand, ...rawArgs.slice(2)], {
584
+ await execa("node", [pluginCli, ...rawArgs.slice(2)], {
483
585
  cwd: backendDir,
484
586
  stdio: "inherit",
485
587
  env
@@ -499,11 +601,15 @@ ${chalk.green.bold("Usage")}
499
601
  ${chalk.green.bold("Commands")}
500
602
  ${chalk.gray("(Commands are provided by your active database driver plugin)")}
501
603
  ${chalk.blue.bold("generate")} Generate Schema from collection definitions
604
+ ${chalk.blue.bold("introspect")} Introspect an existing database to generate collection definitions
502
605
 
503
606
  ${chalk.green.bold("generate Options")}
504
607
  ${chalk.blue("--collections, -c")} Path to collections directory
505
608
  ${chalk.blue("--output, -o")} Output path for generated schema
506
609
  ${chalk.blue("--watch, -w")} Watch for changes and regenerate automatically
610
+
611
+ ${chalk.green.bold("introspect Options")}
612
+ ${chalk.blue("--output, -o")} Output directory for generated collection files
507
613
  `);
508
614
  }
509
615
  async function dbCommand(subcommand, rawArgs) {
@@ -537,13 +643,13 @@ ${chalk.green.bold("generate Options")}
537
643
  console.error(chalk.red("✗ Could not find tsx binary."));
538
644
  process.exit(1);
539
645
  }
540
- await execa(tsxBin, [pluginCli, "db", subcommand, ...rawArgs.slice(2)], {
646
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
541
647
  cwd: backendDir,
542
648
  stdio: "inherit",
543
649
  env
544
650
  });
545
651
  } else {
546
- await execa("node", [pluginCli, "db", subcommand, ...rawArgs.slice(2)], {
652
+ await execa("node", [pluginCli, ...rawArgs.slice(2)], {
547
653
  cwd: backendDir,
548
654
  stdio: "inherit",
549
655
  env
@@ -581,6 +687,27 @@ ${chalk.green.bold("Examples")}
581
687
  rebase db branch create feature_auth
582
688
  `);
583
689
  }
690
+ const DEV_PORT_FILENAME = ".rebase-dev-port";
691
+ function getProjectPort(projectRoot) {
692
+ let hash = 0;
693
+ for (let i = 0; i < projectRoot.length; i++) {
694
+ hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
695
+ }
696
+ return 3001 + Math.abs(hash) % 999;
697
+ }
698
+ function resolveStartPort(projectRoot, explicitPort) {
699
+ if (explicitPort) return explicitPort;
700
+ if (process.env.PORT) return parseInt(process.env.PORT, 10);
701
+ try {
702
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
703
+ if (fs.existsSync(portFile)) {
704
+ const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
705
+ if (saved > 0 && saved < 65536) return saved;
706
+ }
707
+ } catch {
708
+ }
709
+ return getProjectPort(projectRoot);
710
+ }
584
711
  async function devCommand(rawArgs) {
585
712
  const args = arg(
586
713
  {
@@ -608,6 +735,7 @@ ${chalk.green.bold("Examples")}
608
735
  const frontendDir = findFrontendDir(projectRoot);
609
736
  const backendOnly = args["--backend-only"] || false;
610
737
  const frontendOnly = args["--frontend-only"] || false;
738
+ const startPort = resolveStartPort(projectRoot, args["--port"]);
611
739
  console.log("");
612
740
  console.log(chalk.bold(" 🚀 Rebase Dev Server"));
613
741
  console.log("");
@@ -616,6 +744,7 @@ ${chalk.green.bold("Examples")}
616
744
  let backendUrl = "";
617
745
  let debounceSummary = null;
618
746
  let bannerPrinted = false;
747
+ let resolvedBackendPort = null;
619
748
  const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
620
749
  function printSummary() {
621
750
  if (!frontendUrl || !backendUrl) return;
@@ -628,7 +757,7 @@ ${chalk.green.bold("Examples")}
628
757
  console.log(chalk.cyan("│ ✨ Rebase Admin App is ready! │"));
629
758
  const cleanUrl = stripAnsi(frontendUrl);
630
759
  const paddedUrl = cleanUrl.padEnd(40);
631
- console.log(chalk.cyan(`│ 👉 Frontend URL: `) + chalk.white(paddedUrl) + chalk.cyan(`│`));
760
+ console.log(chalk.cyan("│ 👉 Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
632
761
  console.log(chalk.cyan("│ │"));
633
762
  console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
634
763
  console.log("");
@@ -636,15 +765,74 @@ ${chalk.green.bold("Examples")}
636
765
  }, 500);
637
766
  }
638
767
  const cleanup = () => {
768
+ try {
769
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
770
+ if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
771
+ const urlFile = path.join(projectRoot, ".rebase-dev-url");
772
+ if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
773
+ } catch {
774
+ }
639
775
  children.forEach((child) => {
640
- if (!child.killed) {
641
- child.kill("SIGTERM");
776
+ if (child.pid && !child.killed) {
777
+ try {
778
+ if (process.platform === "win32") {
779
+ execa.commandSync(`taskkill /pid ${child.pid} /T /F`);
780
+ } else {
781
+ process.kill(-child.pid, "SIGKILL");
782
+ }
783
+ } catch (e) {
784
+ try {
785
+ child.kill("SIGKILL");
786
+ } catch (err) {
787
+ }
788
+ }
642
789
  }
643
790
  });
644
791
  process.exit(0);
645
792
  };
646
793
  process.on("SIGINT", cleanup);
647
794
  process.on("SIGTERM", cleanup);
795
+ function startFrontend(backendPort) {
796
+ if (!frontendDir) return;
797
+ console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
798
+ const frontendEnv = { ...process.env };
799
+ if (backendPort) {
800
+ frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
801
+ console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
802
+ }
803
+ const frontendChild = execa(
804
+ "pnpm",
805
+ ["run", "dev"],
806
+ {
807
+ cwd: frontendDir,
808
+ stdio: ["inherit", "pipe", "pipe"],
809
+ env: frontendEnv,
810
+ shell: true,
811
+ detached: process.platform !== "win32"
812
+ }
813
+ );
814
+ frontendChild.catch(() => {
815
+ });
816
+ frontendChild.stdout?.on("data", (data) => {
817
+ const lines = data.toString().split("\n").filter(Boolean);
818
+ lines.forEach((line) => {
819
+ console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
820
+ const cleanLine = stripAnsi(line);
821
+ const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
822
+ if (cleanLine.includes("Local:") && urlMatch) {
823
+ frontendUrl = urlMatch[1];
824
+ printSummary();
825
+ }
826
+ });
827
+ });
828
+ frontendChild.stderr?.on("data", (data) => {
829
+ const lines = data.toString().split("\n").filter(Boolean);
830
+ lines.forEach((line) => {
831
+ console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
832
+ });
833
+ });
834
+ children.push(frontendChild);
835
+ }
648
836
  if (!frontendOnly && backendDir) {
649
837
  const tsxBin = resolveTsx(projectRoot);
650
838
  if (!tsxBin) {
@@ -657,22 +845,19 @@ ${chalk.green.bold("Examples")}
657
845
  if (envFile) {
658
846
  env.DOTENV_CONFIG_PATH = envFile;
659
847
  }
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
- ];
848
+ env.PORT = String(startPort);
667
849
  console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
850
+ console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
851
+ let frontendLaunched = false;
668
852
  const backendChild = execa(
669
853
  tsxBin,
670
- ["watch", ...watchDirs, "--conditions", "development", "src/index.ts"],
854
+ ["watch", `--watch="${path.join("..", "config", "**", "*")}"`, "--conditions", "development", "src/index.ts"],
671
855
  {
672
856
  cwd: backendDir,
673
857
  stdio: ["inherit", "pipe", "pipe"],
674
858
  env,
675
- shell: true
859
+ shell: true,
860
+ detached: process.platform !== "win32"
676
861
  }
677
862
  );
678
863
  backendChild.catch(() => {
@@ -682,9 +867,19 @@ ${chalk.green.bold("Examples")}
682
867
  lines.forEach((line) => {
683
868
  console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
684
869
  const cleanLine = stripAnsi(line);
685
- if (cleanLine.includes("Server running at http://")) {
870
+ const serverMatch = cleanLine.match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
871
+ if (serverMatch) {
872
+ resolvedBackendPort = parseInt(serverMatch[1], 10);
686
873
  backendUrl = "started";
687
874
  printSummary();
875
+ const urlFile = path.join(projectRoot, ".rebase-dev-url");
876
+ fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
877
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
878
+ fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
879
+ if (!backendOnly && frontendDir && !frontendLaunched) {
880
+ frontendLaunched = true;
881
+ startFrontend(resolvedBackendPort);
882
+ }
688
883
  }
689
884
  });
690
885
  });
@@ -698,39 +893,8 @@ ${chalk.green.bold("Examples")}
698
893
  } else if (!frontendOnly && !backendDir) {
699
894
  console.warn(chalk.yellow(" ⚠ No backend/ directory found, skipping backend."));
700
895
  }
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);
896
+ if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) {
897
+ startFrontend(null);
734
898
  } else if (!backendOnly && !frontendDir) {
735
899
  console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
736
900
  }
@@ -759,11 +923,19 @@ ${chalk.green.bold("Usage")}
759
923
  ${chalk.green.bold("Options")}
760
924
  ${chalk.blue("--backend-only, -b")} Only start the backend server
761
925
  ${chalk.blue("--frontend-only, -f")} Only start the frontend server
762
- ${chalk.blue("--port, -p")} Backend port (default: 3001)
926
+ ${chalk.blue("--port, -p")} Backend port (default: auto-detected per project)
763
927
 
764
928
  ${chalk.green.bold("Description")}
765
- Starts both the backend (tsx watch + Express) and frontend (Vite)
929
+ Starts both the backend (tsx watch + Hono) and frontend (Vite)
766
930
  dev servers concurrently with color-coded output prefixes.
931
+
932
+ Each project automatically receives a unique default port derived
933
+ from its directory path, preventing collisions when running multiple
934
+ Rebase instances simultaneously.
935
+
936
+ If the assigned port is already in use, the server will automatically
937
+ try the next available port. The frontend is started only after the
938
+ backend is ready, and VITE_API_URL is injected automatically.
767
939
  `);
768
940
  }
769
941
  async function authCommand(subcommand, rawArgs) {
@@ -900,6 +1072,49 @@ ${chalk.green.bold("Examples")}
900
1072
  rebase auth reset-password --email user@example.com --password MyNewPass!
901
1073
  `);
902
1074
  }
1075
+ async function doctorCommand(rawArgs) {
1076
+ const projectRoot = requireProjectRoot();
1077
+ const backendDir = requireBackendDir(projectRoot);
1078
+ const activePlugin = getActiveBackendPlugin(backendDir);
1079
+ if (!activePlugin) {
1080
+ console.error(chalk.red("✗ Could not detect an active database plugin."));
1081
+ console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
1082
+ process.exit(1);
1083
+ }
1084
+ const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
1085
+ if (!pluginCli) {
1086
+ console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
1087
+ process.exit(1);
1088
+ }
1089
+ const envFile = findEnvFile(projectRoot);
1090
+ const env = { ...process.env };
1091
+ if (envFile) {
1092
+ env.DOTENV_CONFIG_PATH = envFile;
1093
+ }
1094
+ try {
1095
+ const isTs = pluginCli.endsWith(".ts");
1096
+ if (isTs) {
1097
+ const tsxBin = resolveTsx(projectRoot);
1098
+ if (!tsxBin) {
1099
+ console.error(chalk.red("✗ Could not find tsx binary."));
1100
+ process.exit(1);
1101
+ }
1102
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
1103
+ cwd: backendDir,
1104
+ stdio: "inherit",
1105
+ env
1106
+ });
1107
+ } else {
1108
+ await execa("node", [pluginCli, ...rawArgs.slice(2)], {
1109
+ cwd: backendDir,
1110
+ stdio: "inherit",
1111
+ env
1112
+ });
1113
+ }
1114
+ } catch {
1115
+ process.exit(1);
1116
+ }
1117
+ }
903
1118
  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
1119
  const __dirname$1 = path.dirname(__filename$1);
905
1120
  function getVersion() {
@@ -931,7 +1146,7 @@ ${chalk.green.bold("Examples")}
931
1146
  }
932
1147
  const command = parsedArgs._[0];
933
1148
  const subcommand = parsedArgs._[1];
934
- const namespacedCommands = ["schema", "db", "dev", "auth"];
1149
+ const namespacedCommands = ["schema", "db", "dev", "auth", "doctor"];
935
1150
  if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
936
1151
  printHelp();
937
1152
  return;
@@ -941,7 +1156,7 @@ ${chalk.green.bold("Examples")}
941
1156
  case "init":
942
1157
  await createRebaseApp(args);
943
1158
  break;
944
- case "generate-sdk":
1159
+ case "generate-sdk": {
945
1160
  const sdkArgs = arg(
946
1161
  {
947
1162
  "--collections-dir": String,
@@ -955,11 +1170,12 @@ ${chalk.green.bold("Examples")}
955
1170
  }
956
1171
  );
957
1172
  await generateSdkCommand({
958
- collectionsDir: sdkArgs["--collections-dir"] || "./shared/collections",
1173
+ collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
959
1174
  output: sdkArgs["--output"] || "./generated/sdk",
960
1175
  cwd: process.cwd()
961
1176
  });
962
1177
  break;
1178
+ }
963
1179
  case "schema":
964
1180
  await schemaCommand(effectiveSubcommand, args);
965
1181
  break;
@@ -972,6 +1188,9 @@ ${chalk.green.bold("Examples")}
972
1188
  case "auth":
973
1189
  await authCommand(effectiveSubcommand, args);
974
1190
  break;
1191
+ case "doctor":
1192
+ await doctorCommand(args);
1193
+ break;
975
1194
  default:
976
1195
  console.log(chalk.red(`Unknown command: ${command}`));
977
1196
  console.log("");
@@ -1008,6 +1227,9 @@ ${chalk.green.bold("Auth")}
1008
1227
  ${chalk.blue.bold("auth reset-password")} Reset a user's password
1009
1228
  ${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
1010
1229
 
1230
+ ${chalk.green.bold("Diagnostics")}
1231
+ ${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
1232
+
1011
1233
  ${chalk.green.bold("Options")}
1012
1234
  ${chalk.blue("--version, -v")} Show version number
1013
1235
  ${chalk.blue("--help, -h")} Show this help message