@rebasepro/cli 0.1.2 → 0.2.3

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 (31) hide show
  1. package/LICENSE +17 -196
  2. package/dist/commands/init.d.ts +1 -1
  3. package/dist/index.cjs +194 -49
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.es.js +179 -33
  6. package/dist/index.es.js.map +1 -1
  7. package/package.json +18 -11
  8. package/templates/template/.cursorrules +2 -0
  9. package/templates/template/.env.example +8 -1
  10. package/templates/template/.github/copilot-instructions.md +2 -0
  11. package/templates/template/.windsurfrules +2 -0
  12. package/templates/template/README.md +3 -3
  13. package/templates/template/ai-instructions.md +14 -0
  14. package/templates/template/backend/Dockerfile +1 -1
  15. package/templates/template/backend/drizzle.config.ts +11 -5
  16. package/templates/template/backend/package.json +4 -3
  17. package/templates/template/backend/src/env.ts +2 -43
  18. package/templates/template/backend/src/index.ts +10 -12
  19. package/templates/template/config/collections/index.ts +6 -4
  20. package/templates/template/config/collections/posts.ts +6 -26
  21. package/templates/template/config/collections/roles.ts +72 -0
  22. package/templates/template/config/collections/users.ts +141 -0
  23. package/templates/template/config/index.ts +1 -1
  24. package/templates/template/config/package.json +2 -1
  25. package/templates/template/docker-compose.yml +3 -3
  26. package/templates/template/frontend/package.json +3 -1
  27. package/templates/template/frontend/src/App.tsx +3 -3
  28. package/templates/template/frontend/src/virtual.d.ts +6 -0
  29. package/templates/template/frontend/tsconfig.json +3 -2
  30. package/templates/template/frontend/vite.config.ts +1 -1
  31. package/templates/template/pnpm-workspace.yaml +7 -0
package/dist/index.es.js CHANGED
@@ -3,9 +3,10 @@ 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
12
  import { generateSDK } from "@rebasepro/sdk-generator";
@@ -51,7 +52,6 @@ function getPMCommands(pm) {
51
52
  };
52
53
  }
53
54
  const access = promisify(fs.access);
54
- const copy = promisify(ncp);
55
55
  const __filename$2 = fileURLToPath(import.meta.url);
56
56
  const __dirname$2 = path.dirname(__filename$2);
57
57
  function findParentDir(currentDir, targetName) {
@@ -78,8 +78,12 @@ async function promptForOptions(rawArgs, pm) {
78
78
  {
79
79
  "--git": Boolean,
80
80
  "--install": Boolean,
81
+ "--database-url": String,
82
+ "--introspect": Boolean,
83
+ "--yes": Boolean,
81
84
  "-g": "--git",
82
- "-i": "--install"
85
+ "-i": "--install",
86
+ "-y": "--yes"
83
87
  },
84
88
  {
85
89
  argv: rawArgs.slice(3),
@@ -88,6 +92,24 @@ async function promptForOptions(rawArgs, pm) {
88
92
  }
89
93
  );
90
94
  const nameArg = args._[0];
95
+ const isNonInteractive = args["--yes"] || false;
96
+ if (isNonInteractive) {
97
+ const projectName2 = nameArg || "my-rebase-app";
98
+ const targetDirectory2 = path.resolve(process.cwd(), projectName2);
99
+ const templateDirectory2 = path.resolve(cliRoot, "templates", "template");
100
+ const pmCommands2 = getPMCommands(pm);
101
+ return {
102
+ projectName: path.basename(targetDirectory2),
103
+ git: args["--git"] ?? false,
104
+ installDeps: args["--install"] ?? false,
105
+ targetDirectory: targetDirectory2,
106
+ templateDirectory: templateDirectory2,
107
+ databaseUrl: args["--database-url"] || void 0,
108
+ introspect: args["--introspect"] || false,
109
+ pm,
110
+ pmCommands: pmCommands2
111
+ };
112
+ }
91
113
  const questions = [];
92
114
  if (!nameArg) {
93
115
  questions.push({
@@ -167,9 +189,8 @@ async function createProject(options) {
167
189
  }
168
190
  console.log(chalk.gray(" Copying project files..."));
169
191
  try {
170
- await copy(options.templateDirectory, options.targetDirectory, {
171
- clobber: false,
172
- dot: true,
192
+ await cp(options.templateDirectory, options.targetDirectory, {
193
+ recursive: true,
173
194
  filter: (source) => {
174
195
  const basename = path.basename(source);
175
196
  return basename !== "node_modules" && basename !== ".DS_Store";
@@ -180,7 +201,7 @@ async function createProject(options) {
180
201
  process.exit(1);
181
202
  }
182
203
  await replacePlaceholders(options);
183
- configureEnvFile(options.targetDirectory, options.databaseUrl);
204
+ await configureEnvFile(options.targetDirectory, options.databaseUrl);
184
205
  if (options.git) {
185
206
  console.log(chalk.gray(" Initializing git repository..."));
186
207
  try {
@@ -240,8 +261,8 @@ async function createProject(options) {
240
261
  console.log(chalk.gray(" # Your database is configured! Start the dev server:"));
241
262
  } else {
242
263
  console.log(chalk.gray(" # A local database configuration has been generated in .env."));
243
- console.log(chalk.gray(" # If using the included docker-compose.yml, start it with:"));
244
- console.log(` ${chalk.cyan("docker compose up -d")}`);
264
+ console.log(chalk.gray(" # If using the included docker-compose.yml, start the database with:"));
265
+ console.log(` ${chalk.cyan("docker compose up -d db")}`);
245
266
  console.log("");
246
267
  console.log(chalk.gray(" # Then start the dev server:"));
247
268
  }
@@ -261,6 +282,7 @@ async function replacePlaceholders(options) {
261
282
  "backend/package.json",
262
283
  "config/package.json",
263
284
  "frontend/index.html",
285
+ "pnpm-workspace.yaml",
264
286
  "README.md"
265
287
  ];
266
288
  const packageJsonPath = path.resolve(cliRoot, "package.json");
@@ -273,6 +295,10 @@ async function replacePlaceholders(options) {
273
295
  const viewBin = "npm";
274
296
  const getPackageVersion = async (pkgName) => {
275
297
  if (versionCache.has(pkgName)) return versionCache.get(pkgName);
298
+ if (process.env.REBASE_E2E === "true") {
299
+ versionCache.set(pkgName, cliVersion);
300
+ return cliVersion;
301
+ }
276
302
  let versionToUse = cliVersion;
277
303
  try {
278
304
  const { stdout } = await execa(viewBin, ["view", `${pkgName}@${cliVersion}`, "version"]);
@@ -321,7 +347,26 @@ async function replacePlaceholders(options) {
321
347
  fs.writeFileSync(fullPath, content, "utf-8");
322
348
  }
323
349
  }
324
- function configureEnvFile(targetDirectory, databaseUrl) {
350
+ async function isPortAvailable(port) {
351
+ return new Promise((resolve) => {
352
+ const server = net.createServer();
353
+ server.once("error", () => {
354
+ resolve(false);
355
+ });
356
+ server.once("listening", () => {
357
+ server.close(() => resolve(true));
358
+ });
359
+ server.listen(port);
360
+ });
361
+ }
362
+ async function findAvailablePort(startPort) {
363
+ let port = startPort;
364
+ while (!await isPortAvailable(port)) {
365
+ port++;
366
+ }
367
+ return port;
368
+ }
369
+ async function configureEnvFile(targetDirectory, databaseUrl) {
325
370
  const envExamplePath = path.join(targetDirectory, ".env.example");
326
371
  const envPath = path.join(targetDirectory, ".env");
327
372
  if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
@@ -339,10 +384,21 @@ function configureEnvFile(targetDirectory, databaseUrl) {
339
384
  `DATABASE_URL=${databaseUrl}`
340
385
  );
341
386
  } else {
387
+ const dbPort = await findAvailablePort(5432);
342
388
  envContent = envContent.replace(
343
389
  /^DATABASE_URL=.*$/m,
344
- `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:5432/rebase`
390
+ `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public
391
+ DATABASE_PASSWORD=${dbPassword}`
345
392
  );
393
+ const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
394
+ if (fs.existsSync(dockerComposePath)) {
395
+ let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
396
+ dockerComposeContent = dockerComposeContent.replace(
397
+ /-\s*"5432:5432"/g,
398
+ `- "${dbPort}:5432"`
399
+ );
400
+ fs.writeFileSync(dockerComposePath, dockerComposeContent, "utf-8");
401
+ }
346
402
  }
347
403
  fs.writeFileSync(envPath, envContent, "utf-8");
348
404
  }
@@ -440,6 +496,7 @@ async function generateSdkCommand(args) {
440
496
  console.log("");
441
497
  console.log(chalk.cyan(" → Loading collection definitions..."));
442
498
  const collections = await loadCollections(resolvedCollectionsDir);
499
+ collections.sort((a, b) => a.slug.localeCompare(b.slug));
443
500
  if (collections.length === 0) {
444
501
  console.log(chalk.red(" ✗ No collections found. Nothing to generate."));
445
502
  process.exit(1);
@@ -499,15 +556,20 @@ function getActiveBackendPlugin(backendDir) {
499
556
  if (!fs.existsSync(pkgPath)) return null;
500
557
  try {
501
558
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
502
- const deps = {
503
- ...pkg.dependencies,
504
- ...pkg.devDependencies
505
- };
506
- for (const dep of Object.keys(deps)) {
507
- if (dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core") {
508
- return dep;
559
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
560
+ const candidates = Object.keys(deps).filter(
561
+ (dep) => dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core"
562
+ );
563
+ if (candidates.length === 0) return null;
564
+ if (candidates.includes("@rebasepro/server-postgresql")) {
565
+ return "@rebasepro/server-postgresql";
566
+ }
567
+ for (const candidate of candidates) {
568
+ if (resolvePluginCliScript(backendDir, candidate)) {
569
+ return candidate;
509
570
  }
510
571
  }
572
+ return candidates[0];
511
573
  } catch {
512
574
  }
513
575
  return null;
@@ -569,7 +631,7 @@ function requireProjectRoot() {
569
631
  if (!root) {
570
632
  console.error(chalk.red("✗ Could not find a Rebase project root."));
571
633
  console.error(chalk.gray(" Make sure you are inside a Rebase project directory"));
572
- console.error(chalk.gray(" (one with backend/, frontend/, and shared/ directories)."));
634
+ console.error(chalk.gray(" (one with backend/, frontend/, and config/ directories)."));
573
635
  process.exit(1);
574
636
  }
575
637
  return root;
@@ -752,10 +814,12 @@ async function devCommand(rawArgs) {
752
814
  "--backend-only": Boolean,
753
815
  "--frontend-only": Boolean,
754
816
  "--port": Number,
817
+ "--generate": Boolean,
755
818
  "--help": Boolean,
756
819
  "-b": "--backend-only",
757
820
  "-f": "--frontend-only",
758
821
  "-p": "--port",
822
+ "-g": "--generate",
759
823
  "-h": "--help"
760
824
  },
761
825
  {
@@ -773,6 +837,7 @@ async function devCommand(rawArgs) {
773
837
  const frontendDir = findFrontendDir(projectRoot);
774
838
  const backendOnly = args["--backend-only"] || false;
775
839
  const frontendOnly = args["--frontend-only"] || false;
840
+ const shouldGenerate = args["--generate"] || process.env.REBASE_AUTO_GENERATE === "true" || process.env.REBASE_GENERATE === "true";
776
841
  const startPort = resolveStartPort(projectRoot, args["--port"]);
777
842
  console.log("");
778
843
  console.log(chalk.bold(" 🚀 Rebase Dev Server"));
@@ -792,10 +857,10 @@ async function devCommand(rawArgs) {
792
857
  console.log("");
793
858
  console.log(chalk.cyan("┌────────────────────────────────────────────────────────────┐"));
794
859
  console.log(chalk.cyan("│ │"));
795
- console.log(chalk.cyan("│ Rebase Admin App is ready! │"));
860
+ console.log(chalk.cyan("│ Rebase Admin App is ready! │"));
796
861
  const cleanUrl = stripAnsi(frontendUrl);
797
- const paddedUrl = cleanUrl.padEnd(40);
798
- console.log(chalk.cyan("│ 👉 Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
862
+ const paddedUrl = cleanUrl.padEnd(41);
863
+ console.log(chalk.cyan("│ Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
799
864
  console.log(chalk.cyan("│ │"));
800
865
  console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
801
866
  console.log("");
@@ -814,7 +879,7 @@ async function devCommand(rawArgs) {
814
879
  if (child.pid && !child.killed) {
815
880
  try {
816
881
  if (process.platform === "win32") {
817
- execa.commandSync(`taskkill /pid ${child.pid} /T /F`);
882
+ execaCommandSync(`taskkill /pid ${child.pid} /T /F`);
818
883
  } else {
819
884
  process.kill(-child.pid, "SIGKILL");
820
885
  }
@@ -892,9 +957,67 @@ async function devCommand(rawArgs) {
892
957
  console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
893
958
  console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
894
959
  let frontendLaunched = false;
960
+ if (shouldGenerate) {
961
+ console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
962
+ try {
963
+ const activePlugin = getActiveBackendPlugin(backendDir);
964
+ const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
965
+ if (pluginCli) {
966
+ await execa(tsxBin, [pluginCli, "schema", "generate"], {
967
+ cwd: backendDir,
968
+ stdio: "inherit",
969
+ env
970
+ });
971
+ }
972
+ await execa("npx", ["rebase", "generate-sdk"], {
973
+ cwd: projectRoot,
974
+ stdio: "inherit",
975
+ env
976
+ });
977
+ console.log(chalk.green(" ✓ Initial schema and SDK generated successfully.\n"));
978
+ } catch (err) {
979
+ console.error(chalk.red(` ✗ Initial schema/SDK generation failed: ${err.message || err}
980
+ `));
981
+ }
982
+ const collectionsDir = path.join(projectRoot, "config", "collections");
983
+ if (fs.existsSync(collectionsDir)) {
984
+ let watchDebounce = null;
985
+ fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
986
+ if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
987
+ if (watchDebounce) clearTimeout(watchDebounce);
988
+ watchDebounce = setTimeout(async () => {
989
+ console.log(chalk.yellow(`
990
+ 🔄 Collection change detected (${filename}). Regenerating schema & SDK...`));
991
+ try {
992
+ const activePlugin = getActiveBackendPlugin(backendDir);
993
+ const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
994
+ if (pluginCli) {
995
+ await execa(tsxBin, [pluginCli, "schema", "generate"], {
996
+ cwd: backendDir,
997
+ stdio: "inherit",
998
+ env
999
+ });
1000
+ }
1001
+ await execa("npx", ["rebase", "generate-sdk"], {
1002
+ cwd: projectRoot,
1003
+ stdio: "inherit",
1004
+ env
1005
+ });
1006
+ console.log(chalk.green(" ✓ Schema & SDK regenerated successfully. Hono will reload."));
1007
+ } catch (err) {
1008
+ console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err.message || err}`));
1009
+ }
1010
+ }, 300);
1011
+ });
1012
+ }
1013
+ }
1014
+ const watchArgs = ["watch", "--conditions", "development", "src/index.ts"];
1015
+ if (!shouldGenerate) {
1016
+ watchArgs.splice(1, 0, `--watch="${path.join("..", "config", "**", "*")}"`);
1017
+ }
895
1018
  const backendChild = execa(
896
1019
  tsxBin,
897
- ["watch", `--watch="${path.join("..", "config", "**", "*")}"`, "--conditions", "development", "src/index.ts"],
1020
+ watchArgs,
898
1021
  {
899
1022
  cwd: backendDir,
900
1023
  stdio: ["inherit", "pipe", "pipe"],
@@ -967,6 +1090,7 @@ ${chalk.green.bold("Options")}
967
1090
  ${chalk.blue("--backend-only, -b")} Only start the backend server
968
1091
  ${chalk.blue("--frontend-only, -f")} Only start the frontend server
969
1092
  ${chalk.blue("--port, -p")} Backend port (default: auto-detected per project)
1093
+ ${chalk.blue("--generate, -g")} Enable automatic schema and SDK generation on startup and file changes
970
1094
 
971
1095
  ${chalk.green.bold("Description")}
972
1096
  Starts both the backend (tsx watch + Hono) and frontend (Vite)
@@ -979,6 +1103,10 @@ ${chalk.green.bold("Description")}
979
1103
  If the assigned port is already in use, the server will automatically
980
1104
  try the next available port. The frontend is started only after the
981
1105
  backend is ready, and VITE_API_URL is injected automatically.
1106
+
1107
+ By default, automatic schema and SDK generation is disabled on startup
1108
+ and file changes. Pass --generate (-g) or set REBASE_AUTO_GENERATE=true
1109
+ in your environment to enable it.
982
1110
  `);
983
1111
  }
984
1112
  async function buildCommand() {
@@ -1073,12 +1201,12 @@ async function resetPassword(rawArgs) {
1073
1201
  env.DOTENV_CONFIG_PATH = envFile;
1074
1202
  }
1075
1203
  const scriptContent = `
1076
- import { createPostgresDatabaseConnection } from "@rebasepro/server-core";
1077
- import { hashPassword } from "@rebasepro/server-core/src/auth/password";
1204
+ import { createPostgresDatabaseConnection } from "@rebasepro/server-postgresql";
1205
+ import { hashPassword } from "@rebasepro/server-core";
1078
1206
  import { eq } from "drizzle-orm";
1079
- import { users } from "@rebasepro/server-core/src/db/auth-schema";
1080
1207
  import * as dotenv from "dotenv";
1081
1208
  import path from "path";
1209
+ import fs from "fs";
1082
1210
 
1083
1211
  dotenv.config({ path: "${envFile || path.join(projectRoot, ".env")}" });
1084
1212
 
@@ -1089,12 +1217,30 @@ async function resetPassword() {
1089
1217
  const { db } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);
1090
1218
  const hash = await hashPassword(newPassword);
1091
1219
 
1092
- const result = await db.update(users)
1093
- .set({ passwordHash: hash })
1094
- .where(eq(users.email, email))
1220
+ let usersTable;
1221
+ try {
1222
+ const schemaPath = path.resolve("./src/schema.generated.ts");
1223
+ if (fs.existsSync(schemaPath)) {
1224
+ const schema = await import("file://" + schemaPath);
1225
+ usersTable = schema.users || schema.tables?.users;
1226
+ }
1227
+ } catch (e) {
1228
+ // ignore and fallback
1229
+ }
1230
+
1231
+ if (!usersTable) {
1232
+ const pgServer = await import("@rebasepro/server-postgresql");
1233
+ usersTable = pgServer.users;
1234
+ }
1235
+
1236
+ const passwordHashKey = (usersTable.passwordHash || "passwordHash" in usersTable) ? "passwordHash" : "password_hash";
1237
+
1238
+ const result = await db.update(usersTable)
1239
+ .set({ [passwordHashKey]: hash })
1240
+ .where(eq(usersTable.email, email))
1095
1241
  .returning({
1096
- id: users.id,
1097
- email: users.email
1242
+ id: usersTable.id,
1243
+ email: usersTable.email
1098
1244
  });
1099
1245
 
1100
1246
  if (result.length > 0) {