@rebasepro/cli 0.1.0 → 0.2.1

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 (30) hide show
  1. package/LICENSE +17 -196
  2. package/dist/index.cjs +158 -44
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.es.js +142 -28
  5. package/dist/index.es.js.map +1 -1
  6. package/package.json +13 -11
  7. package/templates/template/.cursorrules +2 -0
  8. package/templates/template/.env.example +7 -0
  9. package/templates/template/.github/copilot-instructions.md +2 -0
  10. package/templates/template/.windsurfrules +2 -0
  11. package/templates/template/README.md +2 -2
  12. package/templates/template/ai-instructions.md +14 -0
  13. package/templates/template/backend/Dockerfile +1 -1
  14. package/templates/template/backend/drizzle.config.ts +11 -5
  15. package/templates/template/backend/package.json +4 -3
  16. package/templates/template/backend/src/env.ts +2 -43
  17. package/templates/template/backend/src/index.ts +10 -12
  18. package/templates/template/config/collections/index.ts +6 -4
  19. package/templates/template/config/collections/posts.ts +3 -17
  20. package/templates/template/config/collections/roles.ts +72 -0
  21. package/templates/template/config/collections/users.ts +141 -0
  22. package/templates/template/config/index.ts +1 -1
  23. package/templates/template/config/package.json +2 -1
  24. package/templates/template/docker-compose.yml +1 -1
  25. package/templates/template/frontend/package.json +3 -1
  26. package/templates/template/frontend/src/App.tsx +3 -3
  27. package/templates/template/frontend/src/virtual.d.ts +6 -0
  28. package/templates/template/frontend/tsconfig.json +3 -2
  29. package/templates/template/frontend/vite.config.ts +1 -1
  30. package/templates/template/pnpm-workspace.yaml +6 -0
package/dist/index.es.js CHANGED
@@ -4,8 +4,8 @@ import inquirer from "inquirer";
4
4
  import path from "path";
5
5
  import fs from "fs";
6
6
  import { promisify } from "util";
7
- import execa from "execa";
8
- import ncp from "ncp";
7
+ import { execa, execaCommandSync } from "execa";
8
+ import { cp } from "fs/promises";
9
9
  import { fileURLToPath } from "url";
10
10
  import crypto from "crypto";
11
11
  import { generateSDK } from "@rebasepro/sdk-generator";
@@ -51,7 +51,6 @@ function getPMCommands(pm) {
51
51
  };
52
52
  }
53
53
  const access = promisify(fs.access);
54
- const copy = promisify(ncp);
55
54
  const __filename$2 = fileURLToPath(import.meta.url);
56
55
  const __dirname$2 = path.dirname(__filename$2);
57
56
  function findParentDir(currentDir, targetName) {
@@ -78,8 +77,12 @@ async function promptForOptions(rawArgs, pm) {
78
77
  {
79
78
  "--git": Boolean,
80
79
  "--install": Boolean,
80
+ "--database-url": String,
81
+ "--introspect": Boolean,
82
+ "--yes": Boolean,
81
83
  "-g": "--git",
82
- "-i": "--install"
84
+ "-i": "--install",
85
+ "-y": "--yes"
83
86
  },
84
87
  {
85
88
  argv: rawArgs.slice(3),
@@ -88,6 +91,24 @@ async function promptForOptions(rawArgs, pm) {
88
91
  }
89
92
  );
90
93
  const nameArg = args._[0];
94
+ const isNonInteractive = args["--yes"] || false;
95
+ if (isNonInteractive) {
96
+ const projectName2 = nameArg || "my-rebase-app";
97
+ const targetDirectory2 = path.resolve(process.cwd(), projectName2);
98
+ const templateDirectory2 = path.resolve(cliRoot, "templates", "template");
99
+ const pmCommands2 = getPMCommands(pm);
100
+ return {
101
+ projectName: path.basename(targetDirectory2),
102
+ git: args["--git"] ?? false,
103
+ installDeps: args["--install"] ?? false,
104
+ targetDirectory: targetDirectory2,
105
+ templateDirectory: templateDirectory2,
106
+ databaseUrl: args["--database-url"] || void 0,
107
+ introspect: args["--introspect"] || false,
108
+ pm,
109
+ pmCommands: pmCommands2
110
+ };
111
+ }
91
112
  const questions = [];
92
113
  if (!nameArg) {
93
114
  questions.push({
@@ -167,9 +188,8 @@ async function createProject(options) {
167
188
  }
168
189
  console.log(chalk.gray(" Copying project files..."));
169
190
  try {
170
- await copy(options.templateDirectory, options.targetDirectory, {
171
- clobber: false,
172
- dot: true,
191
+ await cp(options.templateDirectory, options.targetDirectory, {
192
+ recursive: true,
173
193
  filter: (source) => {
174
194
  const basename = path.basename(source);
175
195
  return basename !== "node_modules" && basename !== ".DS_Store";
@@ -273,6 +293,10 @@ async function replacePlaceholders(options) {
273
293
  const viewBin = "npm";
274
294
  const getPackageVersion = async (pkgName) => {
275
295
  if (versionCache.has(pkgName)) return versionCache.get(pkgName);
296
+ if (process.env.REBASE_E2E === "true") {
297
+ versionCache.set(pkgName, cliVersion);
298
+ return cliVersion;
299
+ }
276
300
  let versionToUse = cliVersion;
277
301
  try {
278
302
  const { stdout } = await execa(viewBin, ["view", `${pkgName}@${cliVersion}`, "version"]);
@@ -440,6 +464,7 @@ async function generateSdkCommand(args) {
440
464
  console.log("");
441
465
  console.log(chalk.cyan(" → Loading collection definitions..."));
442
466
  const collections = await loadCollections(resolvedCollectionsDir);
467
+ collections.sort((a, b) => a.slug.localeCompare(b.slug));
443
468
  if (collections.length === 0) {
444
469
  console.log(chalk.red(" ✗ No collections found. Nothing to generate."));
445
470
  process.exit(1);
@@ -499,15 +524,20 @@ function getActiveBackendPlugin(backendDir) {
499
524
  if (!fs.existsSync(pkgPath)) return null;
500
525
  try {
501
526
  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;
527
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
528
+ const candidates = Object.keys(deps).filter(
529
+ (dep) => dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core"
530
+ );
531
+ if (candidates.length === 0) return null;
532
+ if (candidates.includes("@rebasepro/server-postgresql")) {
533
+ return "@rebasepro/server-postgresql";
534
+ }
535
+ for (const candidate of candidates) {
536
+ if (resolvePluginCliScript(backendDir, candidate)) {
537
+ return candidate;
509
538
  }
510
539
  }
540
+ return candidates[0];
511
541
  } catch {
512
542
  }
513
543
  return null;
@@ -569,7 +599,7 @@ function requireProjectRoot() {
569
599
  if (!root) {
570
600
  console.error(chalk.red("✗ Could not find a Rebase project root."));
571
601
  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)."));
602
+ console.error(chalk.gray(" (one with backend/, frontend/, and config/ directories)."));
573
603
  process.exit(1);
574
604
  }
575
605
  return root;
@@ -752,10 +782,12 @@ async function devCommand(rawArgs) {
752
782
  "--backend-only": Boolean,
753
783
  "--frontend-only": Boolean,
754
784
  "--port": Number,
785
+ "--generate": Boolean,
755
786
  "--help": Boolean,
756
787
  "-b": "--backend-only",
757
788
  "-f": "--frontend-only",
758
789
  "-p": "--port",
790
+ "-g": "--generate",
759
791
  "-h": "--help"
760
792
  },
761
793
  {
@@ -773,6 +805,7 @@ async function devCommand(rawArgs) {
773
805
  const frontendDir = findFrontendDir(projectRoot);
774
806
  const backendOnly = args["--backend-only"] || false;
775
807
  const frontendOnly = args["--frontend-only"] || false;
808
+ const shouldGenerate = args["--generate"] || process.env.REBASE_AUTO_GENERATE === "true" || process.env.REBASE_GENERATE === "true";
776
809
  const startPort = resolveStartPort(projectRoot, args["--port"]);
777
810
  console.log("");
778
811
  console.log(chalk.bold(" 🚀 Rebase Dev Server"));
@@ -792,10 +825,10 @@ async function devCommand(rawArgs) {
792
825
  console.log("");
793
826
  console.log(chalk.cyan("┌────────────────────────────────────────────────────────────┐"));
794
827
  console.log(chalk.cyan("│ │"));
795
- console.log(chalk.cyan("│ Rebase Admin App is ready! │"));
828
+ console.log(chalk.cyan("│ Rebase Admin App is ready! │"));
796
829
  const cleanUrl = stripAnsi(frontendUrl);
797
- const paddedUrl = cleanUrl.padEnd(40);
798
- console.log(chalk.cyan("│ 👉 Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
830
+ const paddedUrl = cleanUrl.padEnd(41);
831
+ console.log(chalk.cyan("│ Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
799
832
  console.log(chalk.cyan("│ │"));
800
833
  console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
801
834
  console.log("");
@@ -814,7 +847,7 @@ async function devCommand(rawArgs) {
814
847
  if (child.pid && !child.killed) {
815
848
  try {
816
849
  if (process.platform === "win32") {
817
- execa.commandSync(`taskkill /pid ${child.pid} /T /F`);
850
+ execaCommandSync(`taskkill /pid ${child.pid} /T /F`);
818
851
  } else {
819
852
  process.kill(-child.pid, "SIGKILL");
820
853
  }
@@ -892,9 +925,67 @@ async function devCommand(rawArgs) {
892
925
  console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
893
926
  console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
894
927
  let frontendLaunched = false;
928
+ if (shouldGenerate) {
929
+ console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
930
+ try {
931
+ const activePlugin = getActiveBackendPlugin(backendDir);
932
+ const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
933
+ if (pluginCli) {
934
+ await execa(tsxBin, [pluginCli, "schema", "generate"], {
935
+ cwd: backendDir,
936
+ stdio: "inherit",
937
+ env
938
+ });
939
+ }
940
+ await execa("npx", ["rebase", "generate-sdk"], {
941
+ cwd: projectRoot,
942
+ stdio: "inherit",
943
+ env
944
+ });
945
+ console.log(chalk.green(" ✓ Initial schema and SDK generated successfully.\n"));
946
+ } catch (err) {
947
+ console.error(chalk.red(` ✗ Initial schema/SDK generation failed: ${err.message || err}
948
+ `));
949
+ }
950
+ const collectionsDir = path.join(projectRoot, "config", "collections");
951
+ if (fs.existsSync(collectionsDir)) {
952
+ let watchDebounce = null;
953
+ fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
954
+ if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
955
+ if (watchDebounce) clearTimeout(watchDebounce);
956
+ watchDebounce = setTimeout(async () => {
957
+ console.log(chalk.yellow(`
958
+ 🔄 Collection change detected (${filename}). Regenerating schema & SDK...`));
959
+ try {
960
+ const activePlugin = getActiveBackendPlugin(backendDir);
961
+ const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
962
+ if (pluginCli) {
963
+ await execa(tsxBin, [pluginCli, "schema", "generate"], {
964
+ cwd: backendDir,
965
+ stdio: "inherit",
966
+ env
967
+ });
968
+ }
969
+ await execa("npx", ["rebase", "generate-sdk"], {
970
+ cwd: projectRoot,
971
+ stdio: "inherit",
972
+ env
973
+ });
974
+ console.log(chalk.green(" ✓ Schema & SDK regenerated successfully. Hono will reload."));
975
+ } catch (err) {
976
+ console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err.message || err}`));
977
+ }
978
+ }, 300);
979
+ });
980
+ }
981
+ }
982
+ const watchArgs = ["watch", "--conditions", "development", "src/index.ts"];
983
+ if (!shouldGenerate) {
984
+ watchArgs.splice(1, 0, `--watch="${path.join("..", "config", "**", "*")}"`);
985
+ }
895
986
  const backendChild = execa(
896
987
  tsxBin,
897
- ["watch", `--watch="${path.join("..", "config", "**", "*")}"`, "--conditions", "development", "src/index.ts"],
988
+ watchArgs,
898
989
  {
899
990
  cwd: backendDir,
900
991
  stdio: ["inherit", "pipe", "pipe"],
@@ -967,6 +1058,7 @@ ${chalk.green.bold("Options")}
967
1058
  ${chalk.blue("--backend-only, -b")} Only start the backend server
968
1059
  ${chalk.blue("--frontend-only, -f")} Only start the frontend server
969
1060
  ${chalk.blue("--port, -p")} Backend port (default: auto-detected per project)
1061
+ ${chalk.blue("--generate, -g")} Enable automatic schema and SDK generation on startup and file changes
970
1062
 
971
1063
  ${chalk.green.bold("Description")}
972
1064
  Starts both the backend (tsx watch + Hono) and frontend (Vite)
@@ -979,6 +1071,10 @@ ${chalk.green.bold("Description")}
979
1071
  If the assigned port is already in use, the server will automatically
980
1072
  try the next available port. The frontend is started only after the
981
1073
  backend is ready, and VITE_API_URL is injected automatically.
1074
+
1075
+ By default, automatic schema and SDK generation is disabled on startup
1076
+ and file changes. Pass --generate (-g) or set REBASE_AUTO_GENERATE=true
1077
+ in your environment to enable it.
982
1078
  `);
983
1079
  }
984
1080
  async function buildCommand() {
@@ -1073,12 +1169,12 @@ async function resetPassword(rawArgs) {
1073
1169
  env.DOTENV_CONFIG_PATH = envFile;
1074
1170
  }
1075
1171
  const scriptContent = `
1076
- import { createPostgresDatabaseConnection } from "@rebasepro/server-core";
1077
- import { hashPassword } from "@rebasepro/server-core/src/auth/password";
1172
+ import { createPostgresDatabaseConnection } from "@rebasepro/server-postgresql";
1173
+ import { hashPassword } from "@rebasepro/server-core";
1078
1174
  import { eq } from "drizzle-orm";
1079
- import { users } from "@rebasepro/server-core/src/db/auth-schema";
1080
1175
  import * as dotenv from "dotenv";
1081
1176
  import path from "path";
1177
+ import fs from "fs";
1082
1178
 
1083
1179
  dotenv.config({ path: "${envFile || path.join(projectRoot, ".env")}" });
1084
1180
 
@@ -1089,12 +1185,30 @@ async function resetPassword() {
1089
1185
  const { db } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);
1090
1186
  const hash = await hashPassword(newPassword);
1091
1187
 
1092
- const result = await db.update(users)
1093
- .set({ passwordHash: hash })
1094
- .where(eq(users.email, email))
1188
+ let usersTable;
1189
+ try {
1190
+ const schemaPath = path.resolve("./src/schema.generated.ts");
1191
+ if (fs.existsSync(schemaPath)) {
1192
+ const schema = await import("file://" + schemaPath);
1193
+ usersTable = schema.users || schema.tables?.users;
1194
+ }
1195
+ } catch (e) {
1196
+ // ignore and fallback
1197
+ }
1198
+
1199
+ if (!usersTable) {
1200
+ const pgServer = await import("@rebasepro/server-postgresql");
1201
+ usersTable = pgServer.users;
1202
+ }
1203
+
1204
+ const passwordHashKey = (usersTable.passwordHash || "passwordHash" in usersTable) ? "passwordHash" : "password_hash";
1205
+
1206
+ const result = await db.update(usersTable)
1207
+ .set({ [passwordHashKey]: hash })
1208
+ .where(eq(usersTable.email, email))
1095
1209
  .returning({
1096
- id: users.id,
1097
- email: users.email
1210
+ id: usersTable.id,
1211
+ email: usersTable.email
1098
1212
  });
1099
1213
 
1100
1214
  if (result.length > 0) {