@rebasepro/cli 0.5.0 → 0.6.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 (61) hide show
  1. package/dist/commands/init.d.ts +13 -0
  2. package/dist/commands/skills.d.ts +1 -0
  3. package/dist/index.es.js +1631 -1308
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/utils/package-manager.d.ts +2 -0
  6. package/package.json +17 -16
  7. package/skills/rebase-admin/SKILL.md +710 -0
  8. package/skills/rebase-api/SKILL.md +662 -0
  9. package/skills/rebase-api/references/.gitkeep +3 -0
  10. package/skills/rebase-auth/SKILL.md +1143 -0
  11. package/skills/rebase-auth/references/.gitkeep +3 -0
  12. package/skills/rebase-backend-postgres/SKILL.md +633 -0
  13. package/skills/rebase-backend-postgres/references/.gitkeep +3 -0
  14. package/skills/rebase-basics/SKILL.md +749 -0
  15. package/skills/rebase-basics/references/.gitkeep +3 -0
  16. package/skills/rebase-collections/SKILL.md +1328 -0
  17. package/skills/rebase-collections/references/.gitkeep +3 -0
  18. package/skills/rebase-cron-jobs/SKILL.md +699 -0
  19. package/skills/rebase-cron-jobs/references/.gitkeep +1 -0
  20. package/skills/rebase-custom-functions/SKILL.md +233 -0
  21. package/skills/rebase-deployment/SKILL.md +885 -0
  22. package/skills/rebase-deployment/references/.gitkeep +3 -0
  23. package/skills/rebase-design-language/SKILL.md +692 -0
  24. package/skills/rebase-email/SKILL.md +701 -0
  25. package/skills/rebase-email/references/.gitkeep +1 -0
  26. package/skills/rebase-entity-history/SKILL.md +485 -0
  27. package/skills/rebase-entity-history/references/.gitkeep +1 -0
  28. package/skills/rebase-local-env-setup/SKILL.md +189 -0
  29. package/skills/rebase-local-env-setup/references/.gitkeep +3 -0
  30. package/skills/rebase-realtime/SKILL.md +755 -0
  31. package/skills/rebase-realtime/references/.gitkeep +3 -0
  32. package/skills/rebase-sdk/SKILL.md +594 -0
  33. package/skills/rebase-sdk/references/.gitkeep +0 -0
  34. package/skills/rebase-storage/SKILL.md +765 -0
  35. package/skills/rebase-storage/references/.gitkeep +3 -0
  36. package/skills/rebase-studio/SKILL.md +746 -0
  37. package/skills/rebase-studio/references/.gitkeep +3 -0
  38. package/skills/rebase-ui-components/SKILL.md +1488 -0
  39. package/skills/rebase-ui-components/references/.gitkeep +3 -0
  40. package/skills/rebase-webhooks/SKILL.md +623 -0
  41. package/skills/rebase-webhooks/references/.gitkeep +1 -0
  42. package/templates/template/AGENTS.md +2 -0
  43. package/templates/template/CLAUDE.md +2 -0
  44. package/templates/template/ai-instructions.md +6 -3
  45. package/templates/template/backend/package.json +1 -1
  46. package/templates/template/backend/src/env.ts +1 -1
  47. package/templates/template/backend/src/index.ts +9 -6
  48. package/templates/template/config/collections/presets/ecommerce/orders.ts +15 -5
  49. package/templates/template/config/collections/presets/ecommerce/products.ts +9 -3
  50. package/templates/template/config/collections/users.ts +7 -10
  51. package/templates/template/frontend/package.json +2 -2
  52. package/templates/template/frontend/src/App.tsx +1 -7
  53. package/templates/template/frontend/vite.config.ts +0 -1
  54. package/templates/template/package.json +1 -0
  55. package/dist/commands/cli.test.d.ts +0 -1
  56. package/dist/commands/dev.test.d.ts +0 -1
  57. package/dist/commands/init.test.d.ts +0 -1
  58. package/dist/index.cjs +0 -1575
  59. package/dist/index.cjs.map +0 -1
  60. package/dist/utils/package-manager.test.d.ts +0 -1
  61. package/dist/utils/project.test.d.ts +0 -1
package/dist/index.cjs DELETED
@@ -1,1575 +0,0 @@
1
- (function(global, factory) {
2
- typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("chalk"), require("arg"), require("inquirer"), require("path"), require("fs"), require("net"), require("util"), require("execa"), require("fs/promises"), require("url"), require("crypto"), require("@rebasepro/sdk-generator"), require("child_process")) : typeof define === "function" && define.amd ? define(["exports", "chalk", "arg", "inquirer", "path", "fs", "net", "util", "execa", "fs/promises", "url", "crypto", "@rebasepro/sdk-generator", "child_process"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global["Rebase CLI"] = {}, global.chalk, global.arg, global.inquirer, global.path, global.fs, global.net, global.util, global.execa, global.promises, global.url, global.crypto, global.sdkGenerator, global.child_process));
3
- })(this, (function(exports2, chalk, arg, inquirer, path, fs, net, util, execa, promises, url, crypto, sdkGenerator, child_process) {
4
- "use strict";
5
- var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
6
- function detectPackageManager(targetDir) {
7
- const userAgent = process.env.npm_config_user_agent ?? "";
8
- if (userAgent.startsWith("npm/")) return "npm";
9
- if (userAgent.startsWith("pnpm/")) return "pnpm";
10
- if (targetDir) {
11
- if (fs.existsSync(path.join(targetDir, "package-lock.json"))) return "npm";
12
- if (fs.existsSync(path.join(targetDir, "pnpm-lock.yaml"))) return "pnpm";
13
- }
14
- const cwd = process.cwd();
15
- if (cwd !== targetDir) {
16
- if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm";
17
- if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
18
- }
19
- return "pnpm";
20
- }
21
- function getPMCommands(pm) {
22
- if (pm === "npm") {
23
- return {
24
- name: "npm",
25
- install: ["npm", "install"],
26
- run: (script) => ["npm", "run", script],
27
- exec: (bin, args) => ["npx", bin, ...args],
28
- view: (pkg, field) => ["npm", "view", pkg, field],
29
- runAll: (script) => ["npm", "run", script, "--workspaces", "--if-present"],
30
- runWorkspace: (workspace, script) => ["npm", "run", script, "-w", workspace],
31
- workspaceProtocol: "*"
32
- };
33
- }
34
- return {
35
- name: "pnpm",
36
- install: ["pnpm", "install"],
37
- run: (script) => ["pnpm", "run", script],
38
- exec: (bin, args) => ["pnpm", "exec", bin, ...args],
39
- view: (pkg, field) => ["pnpm", "view", pkg, field],
40
- runAll: (script) => ["pnpm", "-r", "run", script],
41
- runWorkspace: (workspace, script) => ["pnpm", "--filter", workspace, script],
42
- workspaceProtocol: "workspace:*"
43
- };
44
- }
45
- const access = util.promisify(fs.access);
46
- const __filename$2 = 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);
47
- const __dirname$2 = path.dirname(__filename$2);
48
- function findParentDir(currentDir, targetName) {
49
- const root = path.parse(currentDir).root;
50
- while (currentDir && currentDir !== root) {
51
- if (path.basename(currentDir) === targetName) {
52
- return currentDir;
53
- }
54
- currentDir = path.dirname(currentDir);
55
- }
56
- return null;
57
- }
58
- const cliRoot = findParentDir(__dirname$2, "cli");
59
- const PRESET_CHOICES = [
60
- { name: "Blog — Posts, Authors, Tags (with markdown editor)", value: "blog", short: "Blog" },
61
- { name: "E-commerce — Products, Categories, Orders", value: "ecommerce", short: "E-commerce" },
62
- { name: "Blank — Empty project, just authentication", value: "blank", short: "Blank" }
63
- ];
64
- async function createRebaseApp(rawArgs) {
65
- console.log(`
66
- ${chalk.bold("Rebase")} — Create a new project 🚀
67
- `);
68
- const pm = detectPackageManager();
69
- const options = await promptForOptions(rawArgs, pm);
70
- await createProject(options);
71
- }
72
- async function promptForOptions(rawArgs, pm) {
73
- const args = arg(
74
- {
75
- "--git": Boolean,
76
- "--install": Boolean,
77
- "--database-url": String,
78
- "--introspect": Boolean,
79
- "--template": String,
80
- "--yes": Boolean,
81
- "-g": "--git",
82
- "-i": "--install",
83
- "-t": "--template",
84
- "-y": "--yes"
85
- },
86
- {
87
- argv: rawArgs.slice(3),
88
- // skip "node", "rebase", "init"
89
- permissive: true
90
- }
91
- );
92
- const nameArg = args._[0];
93
- const isNonInteractive = args["--yes"] || false;
94
- const templateArg = args["--template"];
95
- if (templateArg && !PRESET_CHOICES.some((p) => p.value === templateArg)) {
96
- console.error(chalk.red(`Unknown template "${templateArg}". Available: ${PRESET_CHOICES.map((p) => p.value).join(", ")}`));
97
- process.exit(1);
98
- }
99
- if (isNonInteractive) {
100
- const projectName2 = nameArg || "my-rebase-app";
101
- const targetDirectory2 = path.resolve(process.cwd(), projectName2);
102
- const templateDirectory2 = path.resolve(cliRoot, "templates", "template");
103
- const pmCommands2 = getPMCommands(pm);
104
- return {
105
- projectName: path.basename(targetDirectory2),
106
- git: args["--git"] ?? false,
107
- installDeps: args["--install"] ?? false,
108
- targetDirectory: targetDirectory2,
109
- templateDirectory: templateDirectory2,
110
- databaseUrl: args["--database-url"] || void 0,
111
- introspect: args["--introspect"] || false,
112
- preset: templateArg || "blog",
113
- pm,
114
- pmCommands: pmCommands2
115
- };
116
- }
117
- const questions = [];
118
- if (!nameArg) {
119
- questions.push({
120
- type: "input",
121
- name: "projectName",
122
- message: "Project name:",
123
- default: "my-rebase-app",
124
- validate: (input) => {
125
- if (!input.trim()) return "Project name is required";
126
- if (!/^[a-z0-9][a-z0-9._-]*$/.test(input)) {
127
- return "Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, hyphens, dots, or underscores";
128
- }
129
- return true;
130
- }
131
- });
132
- }
133
- if (!templateArg) {
134
- questions.push({
135
- type: "list",
136
- name: "preset",
137
- message: "Choose a starter template:",
138
- choices: PRESET_CHOICES,
139
- default: "blog"
140
- });
141
- }
142
- if (!args["--git"]) {
143
- questions.push({
144
- type: "confirm",
145
- name: "git",
146
- message: "Initialize a git repository?",
147
- default: true
148
- });
149
- }
150
- if (!args["--install"]) {
151
- questions.push({
152
- type: "confirm",
153
- name: "installDeps",
154
- message: `Install dependencies with ${pm}?`,
155
- default: true
156
- });
157
- }
158
- questions.push({
159
- type: "input",
160
- name: "databaseUrl",
161
- message: "Enter your PostgreSQL database connection string (leave blank to use a local default):",
162
- default: "",
163
- validate: (input) => {
164
- if (input.trim() && /[\r\n]/.test(input)) {
165
- return "Database URL cannot contain newline characters.";
166
- }
167
- return true;
168
- }
169
- });
170
- questions.push({
171
- type: "confirm",
172
- name: "introspect",
173
- message: "Would you like to introspect this database to automatically generate collections?",
174
- default: true,
175
- when: (answers2) => !!answers2.databaseUrl?.trim()
176
- });
177
- const answers = await inquirer.prompt(questions);
178
- const targetDirectory = path.resolve(process.cwd(), nameArg || answers.projectName);
179
- const projectName = path.basename(targetDirectory);
180
- const templateDirectory = path.resolve(cliRoot, "templates", "template");
181
- const pmCommands = getPMCommands(pm);
182
- return {
183
- projectName,
184
- git: args["--git"] || answers.git || false,
185
- installDeps: args["--install"] || answers.installDeps || false,
186
- targetDirectory,
187
- templateDirectory,
188
- databaseUrl: answers.databaseUrl?.trim() || void 0,
189
- introspect: answers.introspect || false,
190
- preset: templateArg || answers.preset || "blog",
191
- pm,
192
- pmCommands
193
- };
194
- }
195
- async function createProject(options) {
196
- if (fs.existsSync(options.targetDirectory)) {
197
- if (fs.readdirSync(options.targetDirectory).length !== 0) {
198
- console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
199
- process.exit(1);
200
- }
201
- } else {
202
- fs.mkdirSync(options.targetDirectory, { recursive: true });
203
- }
204
- try {
205
- await access(options.templateDirectory, fs.constants.R_OK);
206
- } catch {
207
- console.error(`${chalk.red.bold("ERROR")} Template not found at ${options.templateDirectory}`);
208
- process.exit(1);
209
- }
210
- console.log(chalk.gray(" Copying project files..."));
211
- try {
212
- await promises.cp(options.templateDirectory, options.targetDirectory, {
213
- recursive: true,
214
- filter: (source) => {
215
- const basename = path.basename(source);
216
- return basename !== "node_modules" && basename !== ".DS_Store";
217
- }
218
- });
219
- } catch (err) {
220
- console.error(`${chalk.red.bold("ERROR")} Failed to copy template files: ${err instanceof Error ? err.message : String(err)}`);
221
- process.exit(1);
222
- }
223
- await applyPreset(options.targetDirectory, options.preset);
224
- await replacePlaceholders(options);
225
- await configureEnvFile(options.targetDirectory, options.databaseUrl);
226
- if (options.git) {
227
- console.log(chalk.gray(" Initializing git repository..."));
228
- try {
229
- await execa.execa("git", ["init"], { cwd: options.targetDirectory });
230
- } catch {
231
- console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
232
- }
233
- }
234
- const { pm, pmCommands } = options;
235
- const installCmd = pmCommands.install;
236
- const execCmd = pmCommands.exec("rebase", ["schema", "introspect", "--force"]);
237
- if (options.installDeps) {
238
- console.log("");
239
- console.log(chalk.gray(` Installing dependencies with ${pm}...`));
240
- console.log("");
241
- try {
242
- await execa.execa(installCmd[0], installCmd.slice(1), {
243
- cwd: options.targetDirectory,
244
- stdio: "inherit"
245
- });
246
- } catch {
247
- console.warn(chalk.yellow(` Warning: Failed to install dependencies. You may need to run \`${installCmd.join(" ")}\` manually.`));
248
- }
249
- }
250
- if (options.introspect) {
251
- console.log("");
252
- if (options.installDeps) {
253
- console.log(chalk.gray(" Introspecting database and generating collections..."));
254
- console.log("");
255
- try {
256
- await execa.execa(execCmd[0], execCmd.slice(1), {
257
- cwd: options.targetDirectory,
258
- stdio: "inherit"
259
- });
260
- console.log(chalk.green(" Database successfully introspected!"));
261
- } catch {
262
- console.warn(chalk.yellow(" Warning: Failed to introspect database automatically."));
263
- console.warn(chalk.yellow(` You can run \`${execCmd.join(" ")}\` manually after setup.`));
264
- }
265
- } else {
266
- console.warn(chalk.yellow(" Skipping introspection because dependencies were not installed."));
267
- console.warn(chalk.yellow(` Run \`${installCmd.join(" ")}\` then \`${execCmd.join(" ")}\` manually.`));
268
- }
269
- }
270
- console.log("");
271
- console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
272
- console.log("");
273
- console.log(chalk.bold("Next steps:"));
274
- console.log("");
275
- const runDev = pmCommands.run("dev");
276
- const runDbPush = pmCommands.run("db:push");
277
- console.log(` ${chalk.cyan("cd")} ${options.projectName}`);
278
- if (!options.installDeps) {
279
- console.log(` ${chalk.cyan(installCmd.join(" "))}`);
280
- }
281
- console.log("");
282
- if (options.databaseUrl) {
283
- if (options.introspect) {
284
- console.log(chalk.gray(" # Database has been introspected & collections generated!"));
285
- console.log(chalk.gray(" # Start the development server (frontend + backend):"));
286
- console.log(` ${chalk.cyan(runDev.join(" "))}`);
287
- } else {
288
- console.log(chalk.gray(" # Your custom database is configured in .env."));
289
- console.log(chalk.gray(" # If the database is empty, push the Rebase schema to initialize it:"));
290
- console.log(` ${chalk.cyan(runDbPush.join(" "))}`);
291
- console.log("");
292
- console.log(chalk.gray(" # Then start the development server:"));
293
- console.log(` ${chalk.cyan(runDev.join(" "))}`);
294
- }
295
- } else {
296
- console.log(chalk.gray(" # A local database configuration has been generated in .env."));
297
- console.log(chalk.gray(" # 1. Start the PostgreSQL database container:"));
298
- console.log(` ${chalk.cyan("docker compose up -d db")}`);
299
- console.log("");
300
- console.log(chalk.gray(" # 2. Push the Rebase schema to initialize database tables:"));
301
- console.log(` ${chalk.cyan(runDbPush.join(" "))}`);
302
- console.log("");
303
- console.log(chalk.gray(" # 3. Start the development server (frontend + backend):"));
304
- console.log(` ${chalk.cyan(runDev.join(" "))}`);
305
- }
306
- console.log("");
307
- console.log(chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
308
- console.log("");
309
- console.log(chalk.gray("Docs: https://rebase.pro/docs"));
310
- console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
311
- console.log("");
312
- }
313
- async function applyPreset(targetDirectory, preset) {
314
- const collectionsDir = path.join(targetDirectory, "config", "collections");
315
- const presetsDir = path.join(collectionsDir, "presets");
316
- if (preset !== "blog") {
317
- const presetDir = path.join(presetsDir, preset);
318
- if (!fs.existsSync(presetDir)) {
319
- console.warn(chalk.yellow(` Warning: Preset "${preset}" not found, falling back to blog template.`));
320
- cleanupPresets(presetsDir);
321
- return;
322
- }
323
- const blogFiles = ["posts.ts", "authors.ts", "tags.ts", "index.ts"];
324
- for (const file of blogFiles) {
325
- const filePath = path.join(collectionsDir, file);
326
- if (fs.existsSync(filePath)) {
327
- fs.unlinkSync(filePath);
328
- }
329
- }
330
- const presetFiles = fs.readdirSync(presetDir).filter((f) => f.endsWith(".ts"));
331
- for (const file of presetFiles) {
332
- fs.copyFileSync(
333
- path.join(presetDir, file),
334
- path.join(collectionsDir, file)
335
- );
336
- }
337
- }
338
- cleanupPresets(presetsDir);
339
- }
340
- function cleanupPresets(presetsDir) {
341
- if (fs.existsSync(presetsDir)) {
342
- fs.rmSync(presetsDir, { recursive: true, force: true });
343
- }
344
- }
345
- async function replacePlaceholders(options) {
346
- const filesToProcess = [
347
- "package.json",
348
- "frontend/package.json",
349
- "backend/package.json",
350
- "config/package.json",
351
- "frontend/index.html",
352
- "pnpm-workspace.yaml",
353
- "README.md"
354
- ];
355
- const packageJsonPath = path.resolve(cliRoot, "package.json");
356
- let cliVersion = "latest";
357
- if (fs.existsSync(packageJsonPath)) {
358
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
359
- cliVersion = pkg.version || "latest";
360
- }
361
- const versionCache = /* @__PURE__ */ new Map();
362
- const viewBin = "npm";
363
- const getPackageVersion = async (pkgName) => {
364
- if (versionCache.has(pkgName)) return versionCache.get(pkgName);
365
- if (process.env.REBASE_E2E === "true") {
366
- versionCache.set(pkgName, cliVersion);
367
- return cliVersion;
368
- }
369
- let versionToUse = cliVersion;
370
- try {
371
- const { stdout } = await execa.execa(viewBin, ["view", `${pkgName}@${cliVersion}`, "version"]);
372
- if (!stdout.trim()) throw new Error("Not found");
373
- versionToUse = stdout.trim();
374
- } catch {
375
- try {
376
- const tag = cliVersion.includes("canary") ? "canary" : "latest";
377
- const { stdout } = await execa.execa(viewBin, ["view", `${pkgName}@${tag}`, "version"]);
378
- if (!stdout.trim()) throw new Error("Not found");
379
- versionToUse = stdout.trim();
380
- } catch {
381
- try {
382
- const { stdout } = await execa.execa(viewBin, ["view", pkgName, "version"]);
383
- versionToUse = stdout.trim() || "latest";
384
- } catch {
385
- versionToUse = "latest";
386
- }
387
- }
388
- }
389
- versionCache.set(pkgName, versionToUse);
390
- return versionToUse;
391
- };
392
- const allPackages = /* @__PURE__ */ new Set();
393
- const fileContents = /* @__PURE__ */ new Map();
394
- for (const file of filesToProcess) {
395
- const fullPath = path.resolve(options.targetDirectory, file);
396
- if (!fs.existsSync(fullPath)) continue;
397
- const content = fs.readFileSync(fullPath, "utf-8");
398
- fileContents.set(fullPath, content);
399
- const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
400
- for (const match of matches) {
401
- allPackages.add(match[1]);
402
- }
403
- }
404
- console.log(chalk.gray(" Resolving package versions..."));
405
- await Promise.all(Array.from(allPackages).map(getPackageVersion));
406
- for (const [fullPath, originalContent] of fileContents.entries()) {
407
- let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
408
- const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
409
- for (const match of matches) {
410
- const pkgName = match[1];
411
- const resolvedVersion = versionCache.get(pkgName) || "latest";
412
- content = content.replace(new RegExp(`"${pkgName}":\\s*"workspace:\\*"`, "g"), `"${pkgName}": "${resolvedVersion}"`);
413
- }
414
- fs.writeFileSync(fullPath, content, "utf-8");
415
- }
416
- }
417
- async function isPortAvailable(port) {
418
- return new Promise((resolve) => {
419
- const server = net.createServer();
420
- server.once("error", () => {
421
- resolve(false);
422
- });
423
- server.once("listening", () => {
424
- server.close(() => resolve(true));
425
- });
426
- server.listen(port);
427
- });
428
- }
429
- async function findAvailablePort(startPort) {
430
- let port = startPort;
431
- while (!await isPortAvailable(port)) {
432
- port++;
433
- }
434
- return port;
435
- }
436
- async function configureEnvFile(targetDirectory, databaseUrl) {
437
- const envExamplePath = path.join(targetDirectory, ".env.example");
438
- const envPath = path.join(targetDirectory, ".env");
439
- if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
440
- fs.copyFileSync(envExamplePath, envPath);
441
- const jwtSecret = crypto.randomBytes(32).toString("hex");
442
- const dbPassword = crypto.randomBytes(16).toString("hex");
443
- let envContent = fs.readFileSync(envPath, "utf-8");
444
- envContent = envContent.replace(
445
- /^JWT_SECRET=.*$/m,
446
- `JWT_SECRET=${jwtSecret}`
447
- );
448
- if (databaseUrl) {
449
- if (/[\r\n]/.test(databaseUrl)) {
450
- throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
451
- }
452
- envContent = envContent.replace(
453
- /^DATABASE_URL=.*$/m,
454
- `DATABASE_URL=${databaseUrl}`
455
- );
456
- } else {
457
- const dbPort = await findAvailablePort(5432);
458
- envContent = envContent.replace(
459
- /^DATABASE_URL=.*$/m,
460
- `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public
461
- DATABASE_PASSWORD=${dbPassword}`
462
- );
463
- const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
464
- if (fs.existsSync(dockerComposePath)) {
465
- let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
466
- dockerComposeContent = dockerComposeContent.replace(
467
- /-\s*"5432:5432"/g,
468
- `- "${dbPort}:5432"`
469
- );
470
- fs.writeFileSync(dockerComposePath, dockerComposeContent, "utf-8");
471
- }
472
- }
473
- fs.writeFileSync(envPath, envContent, "utf-8");
474
- }
475
- }
476
- async function loadCollections(collectionsDir) {
477
- const absDir = path.resolve(collectionsDir);
478
- if (!fs.existsSync(absDir)) {
479
- throw new Error(`Collections directory not found: ${absDir}`);
480
- }
481
- let jiti;
482
- try {
483
- const jitiModule = await import("jiti");
484
- jiti = jitiModule.default || jitiModule;
485
- } catch {
486
- throw new Error(
487
- "Could not load 'jiti'. Install it with: pnpm add -D jiti\njiti is required to dynamically import TypeScript collection definitions."
488
- );
489
- }
490
- const jitiInstance = jiti(absDir, {
491
- interopDefault: true,
492
- esmResolve: true
493
- });
494
- const indexCandidates = ["index.ts", "index.js", "index.mjs"];
495
- let indexPath = null;
496
- for (const candidate of indexCandidates) {
497
- const p = path.join(absDir, candidate);
498
- if (fs.existsSync(p)) {
499
- indexPath = p;
500
- break;
501
- }
502
- }
503
- if (!indexPath) {
504
- console.log(chalk.yellow(" No index file found, scanning individual collection files..."));
505
- const collections = [];
506
- const files = fs.readdirSync(absDir).filter(
507
- (f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith(".")
508
- );
509
- for (const file of files) {
510
- try {
511
- const mod2 = jitiInstance(path.join(absDir, file));
512
- const exported2 = mod2.default || mod2;
513
- if (exported2 && typeof exported2 === "object" && "slug" in exported2) {
514
- collections.push(exported2);
515
- } else if (Array.isArray(exported2)) {
516
- collections.push(...exported2);
517
- }
518
- } catch (err) {
519
- console.warn(chalk.yellow(` ⚠ Skipping ${file}: ${err.message}`));
520
- }
521
- }
522
- return collections;
523
- }
524
- const mod = jitiInstance(indexPath);
525
- const exported = mod.default || mod;
526
- if (Array.isArray(exported)) {
527
- return exported;
528
- } else if (typeof exported === "object" && exported !== null) {
529
- if ("collections" in exported && Array.isArray(exported.collections)) {
530
- return exported.collections;
531
- }
532
- const collections = [];
533
- for (const value of Object.values(exported)) {
534
- if (value && typeof value === "object" && "slug" in value) {
535
- collections.push(value);
536
- }
537
- }
538
- if (collections.length > 0) return collections;
539
- }
540
- throw new Error(
541
- `Could not extract collections from ${indexPath}.
542
- Expected a default export of EntityCollection[] or an object with named collection exports.`
543
- );
544
- }
545
- function writeFiles(outputDir, files) {
546
- const absOutput = path.resolve(outputDir);
547
- fs.mkdirSync(absOutput, { recursive: true });
548
- for (const file of files) {
549
- const filePath = path.join(absOutput, file.path);
550
- const dir = path.dirname(filePath);
551
- if (!fs.existsSync(dir)) {
552
- fs.mkdirSync(dir, { recursive: true });
553
- }
554
- fs.writeFileSync(filePath, file.content, "utf-8");
555
- }
556
- }
557
- async function generateSdkCommand(args) {
558
- const { collectionsDir, output, cwd } = args;
559
- const resolvedCollectionsDir = path.isAbsolute(collectionsDir) ? collectionsDir : path.join(cwd, collectionsDir);
560
- const resolvedOutput = path.isAbsolute(output) ? output : path.join(cwd, output);
561
- console.log("");
562
- console.log(chalk.bold(" 🔧 Rebase SDK Generator"));
563
- console.log("");
564
- console.log(` ${chalk.gray("Collections:")} ${resolvedCollectionsDir}`);
565
- console.log(` ${chalk.gray("Output:")} ${resolvedOutput}`);
566
- console.log("");
567
- console.log(chalk.cyan(" → Loading collection definitions..."));
568
- const collections = await loadCollections(resolvedCollectionsDir);
569
- collections.sort((a, b) => a.slug.localeCompare(b.slug));
570
- if (collections.length === 0) {
571
- console.log(chalk.red(" ✗ No collections found. Nothing to generate."));
572
- process.exit(1);
573
- }
574
- console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
575
- console.log("");
576
- console.log(chalk.cyan(" → Generating SDK files..."));
577
- const files = sdkGenerator.generateSDK(collections);
578
- console.log(chalk.green(` ✓ Generated ${files.length} file(s)`));
579
- console.log(chalk.cyan(` → Writing to ${resolvedOutput}...`));
580
- writeFiles(resolvedOutput, files);
581
- console.log("");
582
- console.log(chalk.green.bold(" ✓ SDK generated successfully!"));
583
- console.log("");
584
- console.log(chalk.gray(" Usage:"));
585
- console.log(chalk.gray(" import { createRebaseClient } from '@rebasepro/client';"));
586
- console.log(chalk.gray(` import type { Database } from './${path.relative(cwd, path.join(resolvedOutput, "database.types"))}';`));
587
- console.log("");
588
- console.log(chalk.gray(" const rebase = createRebaseClient<Database>({"));
589
- console.log(chalk.gray(" baseUrl: 'http://localhost:3001',"));
590
- console.log(chalk.gray(" // token: 'your-jwt-token',"));
591
- console.log(chalk.gray(" });"));
592
- console.log("");
593
- console.log(chalk.gray(` const { data } = await rebase.collection('${collections[0]?.slug || "my_collection"}').find();`));
594
- console.log("");
595
- }
596
- function findProjectRoot(startDir = process.cwd()) {
597
- let dir = path.resolve(startDir);
598
- const root = path.parse(dir).root;
599
- while (dir !== root) {
600
- const pkgPath = path.join(dir, "package.json");
601
- if (fs.existsSync(pkgPath)) {
602
- try {
603
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
604
- if (pkg.workspaces && Array.isArray(pkg.workspaces)) {
605
- const hasBackend = pkg.workspaces.some(
606
- (w) => w === "backend" || w.includes("backend")
607
- );
608
- if (hasBackend) return dir;
609
- }
610
- } catch {
611
- }
612
- if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) {
613
- return dir;
614
- }
615
- }
616
- dir = path.dirname(dir);
617
- }
618
- return null;
619
- }
620
- function findBackendDir(projectRoot) {
621
- const backendDir = path.join(projectRoot, "backend");
622
- return fs.existsSync(backendDir) ? backendDir : null;
623
- }
624
- function getActiveBackendPlugin(backendDir) {
625
- const pkgPath = path.join(backendDir, "package.json");
626
- if (!fs.existsSync(pkgPath)) return null;
627
- try {
628
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
629
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
630
- const candidates = Object.keys(deps).filter(
631
- (dep) => dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core"
632
- );
633
- if (candidates.length === 0) return null;
634
- if (candidates.includes("@rebasepro/server-postgresql")) {
635
- return "@rebasepro/server-postgresql";
636
- }
637
- for (const candidate of candidates) {
638
- if (resolvePluginCliScript(backendDir, candidate)) {
639
- return candidate;
640
- }
641
- }
642
- return candidates[0];
643
- } catch {
644
- }
645
- return null;
646
- }
647
- function resolvePluginCliScript(backendDir, pluginName) {
648
- const candidates = [
649
- path.join(backendDir, "node_modules", pluginName, "src", "cli.ts"),
650
- path.join(backendDir, "node_modules", pluginName, "dist", "cli.js"),
651
- // For monorepo dev mode:
652
- path.resolve(backendDir, "..", "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
653
- path.resolve(backendDir, "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
654
- path.resolve(backendDir, "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts")
655
- ];
656
- for (const candidate of candidates) {
657
- if (fs.existsSync(candidate)) return candidate;
658
- }
659
- return null;
660
- }
661
- function findFrontendDir(projectRoot) {
662
- const frontendDir = path.join(projectRoot, "frontend");
663
- return fs.existsSync(frontendDir) ? frontendDir : null;
664
- }
665
- function findEnvFile(projectRoot) {
666
- const candidates = [
667
- path.join(projectRoot, ".env"),
668
- path.join(projectRoot, "backend", ".env")
669
- ];
670
- for (const candidate of candidates) {
671
- if (fs.existsSync(candidate)) return candidate;
672
- }
673
- return null;
674
- }
675
- function resolveLocalBin(projectRoot, binName) {
676
- const candidates = [
677
- path.join(projectRoot, "backend", "node_modules", ".bin", binName),
678
- path.join(projectRoot, "node_modules", ".bin", binName)
679
- ];
680
- let parent = path.dirname(projectRoot);
681
- const rootDir = path.parse(parent).root;
682
- while (parent !== rootDir) {
683
- candidates.push(path.join(parent, "node_modules", ".bin", binName));
684
- parent = path.dirname(parent);
685
- }
686
- for (const candidate of candidates) {
687
- if (fs.existsSync(candidate)) return candidate;
688
- }
689
- try {
690
- const globalPath = child_process.execSync(`which ${binName}`, { encoding: "utf-8" }).trim();
691
- if (globalPath && fs.existsSync(globalPath)) return globalPath;
692
- } catch {
693
- }
694
- return null;
695
- }
696
- function resolveTsx(projectRoot) {
697
- return resolveLocalBin(projectRoot, "tsx");
698
- }
699
- function requireProjectRoot() {
700
- const root = findProjectRoot();
701
- if (!root) {
702
- console.error(chalk.red("✗ Could not find a Rebase project root."));
703
- console.error(chalk.gray(" Make sure you are inside a Rebase project directory"));
704
- console.error(chalk.gray(" (one with backend/, frontend/, and config/ directories)."));
705
- process.exit(1);
706
- }
707
- return root;
708
- }
709
- function requireBackendDir(projectRoot) {
710
- const backendDir = findBackendDir(projectRoot);
711
- if (!backendDir) {
712
- console.error(chalk.red("✗ Could not find a backend/ directory."));
713
- console.error(chalk.gray(` Expected at: ${path.join(projectRoot, "backend")}`));
714
- process.exit(1);
715
- }
716
- return backendDir;
717
- }
718
- async function schemaCommand(subcommand, rawArgs) {
719
- if (!subcommand || subcommand === "--help") {
720
- printSchemaHelp();
721
- return;
722
- }
723
- const projectRoot = requireProjectRoot();
724
- const backendDir = requireBackendDir(projectRoot);
725
- const activePlugin = getActiveBackendPlugin(backendDir);
726
- if (!activePlugin) {
727
- console.error(chalk.red("✗ Could not detect an active database plugin."));
728
- console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
729
- process.exit(1);
730
- }
731
- const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
732
- if (!pluginCli) {
733
- console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
734
- process.exit(1);
735
- }
736
- const envFile = findEnvFile(projectRoot);
737
- const env = { ...process.env };
738
- if (envFile) {
739
- env.DOTENV_CONFIG_PATH = envFile;
740
- }
741
- try {
742
- const isTs = pluginCli.endsWith(".ts");
743
- if (isTs) {
744
- const tsxBin = resolveTsx(projectRoot);
745
- if (!tsxBin) {
746
- console.error(chalk.red("✗ Could not find tsx binary."));
747
- process.exit(1);
748
- }
749
- await execa.execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
750
- cwd: backendDir,
751
- stdio: "inherit",
752
- env
753
- });
754
- } else {
755
- await execa.execa("node", [pluginCli, ...rawArgs.slice(2)], {
756
- cwd: backendDir,
757
- stdio: "inherit",
758
- env
759
- });
760
- }
761
- } catch {
762
- process.exit(1);
763
- }
764
- }
765
- function printSchemaHelp() {
766
- console.log(`
767
- ${chalk.bold("rebase schema")} — Schema management commands
768
-
769
- ${chalk.green.bold("Usage")}
770
- rebase schema ${chalk.blue("<command>")} [options]
771
-
772
- ${chalk.green.bold("Commands")}
773
- ${chalk.gray("(Commands are provided by your active database driver plugin)")}
774
- ${chalk.blue.bold("generate")} Generate Schema from collection definitions
775
- ${chalk.blue.bold("introspect")} Introspect an existing database to generate collection definitions
776
-
777
- ${chalk.green.bold("generate Options")}
778
- ${chalk.blue("--collections, -c")} Path to collections directory
779
- ${chalk.blue("--output, -o")} Output path for generated schema
780
- ${chalk.blue("--watch, -w")} Watch for changes and regenerate automatically
781
-
782
- ${chalk.green.bold("introspect Options")}
783
- ${chalk.blue("--output, -o")} Output directory for generated collection files
784
- `);
785
- }
786
- async function dbCommand(subcommand, rawArgs) {
787
- if (!subcommand || subcommand === "--help") {
788
- printDbHelp();
789
- return;
790
- }
791
- const projectRoot = requireProjectRoot();
792
- const backendDir = requireBackendDir(projectRoot);
793
- const activePlugin = getActiveBackendPlugin(backendDir);
794
- if (!activePlugin) {
795
- console.error(chalk.red("✗ Could not detect an active database plugin."));
796
- console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
797
- process.exit(1);
798
- }
799
- const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
800
- if (!pluginCli) {
801
- console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
802
- process.exit(1);
803
- }
804
- const envFile = findEnvFile(projectRoot);
805
- const env = { ...process.env };
806
- if (envFile) {
807
- env.DOTENV_CONFIG_PATH = envFile;
808
- }
809
- try {
810
- const isTs = pluginCli.endsWith(".ts");
811
- if (isTs) {
812
- const tsxBin = resolveTsx(projectRoot);
813
- if (!tsxBin) {
814
- console.error(chalk.red("✗ Could not find tsx binary."));
815
- process.exit(1);
816
- }
817
- await execa.execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
818
- cwd: backendDir,
819
- stdio: "inherit",
820
- env
821
- });
822
- } else {
823
- await execa.execa("node", [pluginCli, ...rawArgs.slice(2)], {
824
- cwd: backendDir,
825
- stdio: "inherit",
826
- env
827
- });
828
- }
829
- } catch {
830
- process.exit(1);
831
- }
832
- }
833
- function printDbHelp() {
834
- console.log(`
835
- ${chalk.bold("rebase db")} — Database management commands
836
-
837
- ${chalk.green.bold("Usage")}
838
- rebase db ${chalk.blue("<command>")} [options]
839
-
840
- ${chalk.green.bold("Commands")}
841
- ${chalk.gray("(Commands are provided by your active database driver plugin)")}
842
- ${chalk.blue.bold("push")} Apply schema directly to database (development)
843
- ${chalk.blue.bold("generate")} Generate migration files
844
- ${chalk.blue.bold("migrate")} Run pending migrations
845
- ${chalk.blue.bold("studio")} Open Studio viewer
846
- ${chalk.blue.bold("branch")} Database branching (create, list, delete, info)
847
-
848
- ${chalk.green.bold("Examples")}
849
- ${chalk.gray("# Quick development workflow")}
850
- rebase schema generate && rebase db push
851
-
852
- ${chalk.gray("# Production migration workflow")}
853
- rebase db generate
854
- rebase db migrate
855
-
856
- ${chalk.gray("# Create a database branch")}
857
- rebase db branch create feature_auth
858
- `);
859
- }
860
- const DEV_PORT_FILENAME = ".rebase-dev-port";
861
- function getProjectPort(projectRoot) {
862
- let hash = 0;
863
- for (let i = 0; i < projectRoot.length; i++) {
864
- hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
865
- }
866
- return 3001 + Math.abs(hash) % 999;
867
- }
868
- function resolveStartPort(projectRoot, explicitPort) {
869
- if (explicitPort) return explicitPort;
870
- if (process.env.PORT) return parseInt(process.env.PORT, 10);
871
- try {
872
- const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
873
- if (fs.existsSync(portFile)) {
874
- const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
875
- if (saved > 0 && saved < 65536) return saved;
876
- }
877
- } catch {
878
- }
879
- return getProjectPort(projectRoot);
880
- }
881
- async function devCommand(rawArgs) {
882
- const args = arg(
883
- {
884
- "--backend-only": Boolean,
885
- "--frontend-only": Boolean,
886
- "--port": Number,
887
- "--generate": Boolean,
888
- "--help": Boolean,
889
- "-b": "--backend-only",
890
- "-f": "--frontend-only",
891
- "-p": "--port",
892
- "-g": "--generate",
893
- "-h": "--help"
894
- },
895
- {
896
- argv: rawArgs.slice(3),
897
- // skip "node rebase dev"
898
- permissive: true
899
- }
900
- );
901
- if (args["--help"]) {
902
- printDevHelp();
903
- return;
904
- }
905
- const projectRoot = requireProjectRoot();
906
- const backendDir = findBackendDir(projectRoot);
907
- const frontendDir = findFrontendDir(projectRoot);
908
- const backendOnly = args["--backend-only"] || false;
909
- const frontendOnly = args["--frontend-only"] || false;
910
- const shouldGenerate = args["--generate"] || process.env.REBASE_AUTO_GENERATE === "true" || process.env.REBASE_GENERATE === "true";
911
- const startPort = resolveStartPort(projectRoot, args["--port"]);
912
- console.log("");
913
- console.log(chalk.bold(" 🚀 Rebase Dev Server"));
914
- console.log("");
915
- const children = [];
916
- let frontendUrl = "";
917
- let backendUrl = "";
918
- let debounceSummary = null;
919
- let bannerPrinted = false;
920
- let resolvedBackendPort = null;
921
- const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
922
- function printSummary() {
923
- if (!frontendUrl || !backendUrl) return;
924
- if (debounceSummary) clearTimeout(debounceSummary);
925
- debounceSummary = setTimeout(() => {
926
- if (bannerPrinted) return;
927
- console.log("");
928
- console.log(chalk.cyan("┌────────────────────────────────────────────────────────────┐"));
929
- console.log(chalk.cyan("│ │"));
930
- console.log(chalk.cyan("│ ✦ Rebase Admin App is ready! │"));
931
- const cleanUrl = stripAnsi(frontendUrl);
932
- const paddedUrl = cleanUrl.padEnd(41);
933
- console.log(chalk.cyan("│ ➜ Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
934
- console.log(chalk.cyan("│ │"));
935
- console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
936
- console.log("");
937
- bannerPrinted = true;
938
- }, 500);
939
- }
940
- const cleanup = () => {
941
- try {
942
- const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
943
- if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
944
- const urlFile = path.join(projectRoot, ".rebase-dev-url");
945
- if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
946
- } catch {
947
- }
948
- children.forEach((child) => {
949
- if (child.pid && !child.killed) {
950
- try {
951
- if (process.platform === "win32") {
952
- execa.execaCommandSync(`taskkill /pid ${child.pid} /T /F`);
953
- } else {
954
- process.kill(-child.pid, "SIGKILL");
955
- }
956
- } catch (e) {
957
- try {
958
- child.kill("SIGKILL");
959
- } catch (err) {
960
- }
961
- }
962
- }
963
- });
964
- process.exit(0);
965
- };
966
- process.on("SIGINT", cleanup);
967
- process.on("SIGTERM", cleanup);
968
- function startFrontend(backendPort) {
969
- if (!frontendDir) return;
970
- console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
971
- const frontendEnv = { ...process.env };
972
- if (backendPort) {
973
- frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
974
- console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
975
- }
976
- const pm = detectPackageManager(projectRoot);
977
- const pmCmds = getPMCommands(pm);
978
- const runDevCmd = pmCmds.run("dev");
979
- const frontendChild = execa.execa(
980
- runDevCmd[0],
981
- runDevCmd.slice(1),
982
- {
983
- cwd: frontendDir,
984
- stdio: ["inherit", "pipe", "pipe"],
985
- env: frontendEnv,
986
- shell: true,
987
- detached: process.platform !== "win32"
988
- }
989
- );
990
- frontendChild.catch(() => {
991
- });
992
- frontendChild.stdout?.on("data", (data) => {
993
- const lines = data.toString().split("\n").filter(Boolean);
994
- lines.forEach((line) => {
995
- console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
996
- const cleanLine = stripAnsi(line);
997
- const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
998
- if (cleanLine.includes("Local:") && urlMatch) {
999
- frontendUrl = urlMatch[1];
1000
- printSummary();
1001
- }
1002
- });
1003
- });
1004
- frontendChild.stderr?.on("data", (data) => {
1005
- const lines = data.toString().split("\n").filter(Boolean);
1006
- lines.forEach((line) => {
1007
- console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
1008
- });
1009
- });
1010
- children.push(frontendChild);
1011
- }
1012
- if (!frontendOnly && backendDir) {
1013
- const tsxBin = resolveTsx(projectRoot);
1014
- if (!tsxBin) {
1015
- const pmName = detectPackageManager(projectRoot);
1016
- const addCmd = pmName === "npm" ? "npm install -D tsx" : "pnpm add -D tsx";
1017
- console.error(chalk.red(" ✗ Could not find tsx binary for backend."));
1018
- console.error(chalk.gray(` Install it with: ${addCmd}`));
1019
- process.exit(1);
1020
- }
1021
- const envFile = findEnvFile(projectRoot);
1022
- const env = { ...process.env };
1023
- if (envFile) {
1024
- env.DOTENV_CONFIG_PATH = envFile;
1025
- }
1026
- env.PORT = String(startPort);
1027
- console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
1028
- console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
1029
- let frontendLaunched = false;
1030
- if (shouldGenerate) {
1031
- console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
1032
- try {
1033
- const activePlugin = getActiveBackendPlugin(backendDir);
1034
- const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
1035
- if (pluginCli) {
1036
- await execa.execa(tsxBin, [pluginCli, "schema", "generate"], {
1037
- cwd: backendDir,
1038
- stdio: "inherit",
1039
- env
1040
- });
1041
- }
1042
- await execa.execa("npx", ["rebase", "generate-sdk"], {
1043
- cwd: projectRoot,
1044
- stdio: "inherit",
1045
- env
1046
- });
1047
- console.log(chalk.green(" ✓ Initial schema and SDK generated successfully.\n"));
1048
- } catch (err) {
1049
- console.error(chalk.red(` ✗ Initial schema/SDK generation failed: ${err instanceof Error ? err.message : err}
1050
- `));
1051
- }
1052
- const collectionsDir = path.join(projectRoot, "config", "collections");
1053
- if (fs.existsSync(collectionsDir)) {
1054
- let watchDebounce = null;
1055
- fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
1056
- if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
1057
- if (watchDebounce) clearTimeout(watchDebounce);
1058
- watchDebounce = setTimeout(async () => {
1059
- console.log(chalk.yellow(`
1060
- 🔄 Collection change detected (${filename}). Regenerating schema & SDK...`));
1061
- try {
1062
- const activePlugin = getActiveBackendPlugin(backendDir);
1063
- const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
1064
- if (pluginCli) {
1065
- await execa.execa(tsxBin, [pluginCli, "schema", "generate"], {
1066
- cwd: backendDir,
1067
- stdio: "inherit",
1068
- env
1069
- });
1070
- }
1071
- await execa.execa("npx", ["rebase", "generate-sdk"], {
1072
- cwd: projectRoot,
1073
- stdio: "inherit",
1074
- env
1075
- });
1076
- console.log(chalk.green(" ✓ Schema & SDK regenerated successfully. Hono will reload."));
1077
- } catch (err) {
1078
- console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err instanceof Error ? err.message : err}`));
1079
- }
1080
- }, 300);
1081
- });
1082
- }
1083
- }
1084
- const watchArgs = ["watch", "--conditions", "development", "src/index.ts"];
1085
- if (!shouldGenerate) {
1086
- watchArgs.splice(1, 0, `--watch="${path.join("..", "config", "**", "*")}"`);
1087
- }
1088
- const backendChild = execa.execa(
1089
- tsxBin,
1090
- watchArgs,
1091
- {
1092
- cwd: backendDir,
1093
- stdio: ["inherit", "pipe", "pipe"],
1094
- env,
1095
- shell: true,
1096
- detached: process.platform !== "win32"
1097
- }
1098
- );
1099
- backendChild.catch(() => {
1100
- });
1101
- backendChild.stdout?.on("data", (data) => {
1102
- const lines = data.toString().split("\n").filter(Boolean);
1103
- lines.forEach((line) => {
1104
- console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
1105
- const cleanLine = stripAnsi(line);
1106
- const serverMatch = cleanLine.match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
1107
- if (serverMatch) {
1108
- resolvedBackendPort = parseInt(serverMatch[1], 10);
1109
- backendUrl = "started";
1110
- printSummary();
1111
- const urlFile = path.join(projectRoot, ".rebase-dev-url");
1112
- fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
1113
- const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
1114
- fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
1115
- if (!backendOnly && frontendDir && !frontendLaunched) {
1116
- frontendLaunched = true;
1117
- startFrontend(resolvedBackendPort);
1118
- }
1119
- }
1120
- });
1121
- });
1122
- backendChild.stderr?.on("data", (data) => {
1123
- const lines = data.toString().split("\n").filter(Boolean);
1124
- lines.forEach((line) => {
1125
- console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
1126
- });
1127
- });
1128
- children.push(backendChild);
1129
- } else if (!frontendOnly && !backendDir) {
1130
- console.warn(chalk.yellow(" ⚠ No backend/ directory found, skipping backend."));
1131
- }
1132
- if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) {
1133
- startFrontend(null);
1134
- } else if (!backendOnly && !frontendDir) {
1135
- console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
1136
- }
1137
- if (children.length === 0) {
1138
- console.error(chalk.red(" ✗ Nothing to start. Check your project structure."));
1139
- process.exit(1);
1140
- }
1141
- console.log("");
1142
- console.log(chalk.gray(" Press Ctrl+C to stop all servers."));
1143
- console.log("");
1144
- await Promise.all(
1145
- children.map(
1146
- (child) => new Promise((resolve) => {
1147
- child.finally(() => resolve());
1148
- })
1149
- )
1150
- );
1151
- }
1152
- function printDevHelp() {
1153
- console.log(`
1154
- ${chalk.bold("rebase dev")} — Start the development server
1155
-
1156
- ${chalk.green.bold("Usage")}
1157
- rebase dev [options]
1158
-
1159
- ${chalk.green.bold("Options")}
1160
- ${chalk.blue("--backend-only, -b")} Only start the backend server
1161
- ${chalk.blue("--frontend-only, -f")} Only start the frontend server
1162
- ${chalk.blue("--port, -p")} Backend port (default: auto-detected per project)
1163
- ${chalk.blue("--generate, -g")} Enable automatic schema and SDK generation on startup and file changes
1164
-
1165
- ${chalk.green.bold("Description")}
1166
- Starts both the backend (tsx watch + Hono) and frontend (Vite)
1167
- dev servers concurrently with color-coded output prefixes.
1168
-
1169
- Each project automatically receives a unique default port derived
1170
- from its directory path, preventing collisions when running multiple
1171
- Rebase instances simultaneously.
1172
-
1173
- If the assigned port is already in use, the server will automatically
1174
- try the next available port. The frontend is started only after the
1175
- backend is ready, and VITE_API_URL is injected automatically.
1176
-
1177
- By default, automatic schema and SDK generation is disabled on startup
1178
- and file changes. Pass --generate (-g) or set REBASE_AUTO_GENERATE=true
1179
- in your environment to enable it.
1180
- `);
1181
- }
1182
- async function buildCommand() {
1183
- const projectRoot = requireProjectRoot();
1184
- const pm = detectPackageManager(projectRoot);
1185
- const cmds = getPMCommands(pm);
1186
- const buildCmd = cmds.runAll("build");
1187
- console.log(`${chalk.bold("Rebase")} — Building all workspaces with ${chalk.cyan(pm)}...
1188
- `);
1189
- try {
1190
- await execa.execa(buildCmd[0], buildCmd.slice(1), {
1191
- cwd: projectRoot,
1192
- stdio: "inherit"
1193
- });
1194
- } catch {
1195
- console.error(chalk.red("\n✗ Build failed."));
1196
- process.exit(1);
1197
- }
1198
- }
1199
- async function startCommand() {
1200
- const projectRoot = requireProjectRoot();
1201
- const pm = detectPackageManager(projectRoot);
1202
- const cmds = getPMCommands(pm);
1203
- const startCmd = cmds.runWorkspace("backend", "start");
1204
- const envFile = findEnvFile(projectRoot);
1205
- const env = { ...process.env };
1206
- if (envFile) {
1207
- env.DOTENV_CONFIG_PATH = envFile;
1208
- }
1209
- console.log(`${chalk.bold("Rebase")} — Starting backend server...
1210
- `);
1211
- try {
1212
- await execa.execa(startCmd[0], startCmd.slice(1), {
1213
- cwd: projectRoot,
1214
- stdio: "inherit",
1215
- env
1216
- });
1217
- } catch {
1218
- console.error(chalk.red("\n✗ Failed to start server."));
1219
- process.exit(1);
1220
- }
1221
- }
1222
- async function authCommand(subcommand, rawArgs) {
1223
- if (!subcommand || subcommand === "--help") {
1224
- printAuthHelp();
1225
- return;
1226
- }
1227
- switch (subcommand) {
1228
- case "reset-password":
1229
- await resetPassword(rawArgs);
1230
- break;
1231
- default:
1232
- console.error(chalk.red(`Unknown auth command: ${subcommand}`));
1233
- console.log("");
1234
- printAuthHelp();
1235
- process.exit(1);
1236
- }
1237
- }
1238
- async function resetPassword(rawArgs) {
1239
- const args = arg(
1240
- {
1241
- "--email": String,
1242
- "--password": String,
1243
- "-e": "--email",
1244
- "-p": "--password"
1245
- },
1246
- {
1247
- argv: rawArgs.slice(4),
1248
- // skip "node rebase auth reset-password"
1249
- permissive: true
1250
- }
1251
- );
1252
- const email = args["--email"] || args._[0];
1253
- const newPassword = args["--password"] || args._[1];
1254
- if (!email) {
1255
- console.error(chalk.red("✗ Email is required."));
1256
- console.log("");
1257
- console.log(chalk.gray(" Usage: rebase auth reset-password <email> [new-password]"));
1258
- console.log(chalk.gray(" rebase auth reset-password --email user@example.com --password NewPass123!"));
1259
- process.exit(1);
1260
- }
1261
- const projectRoot = requireProjectRoot();
1262
- const backendDir = requireBackendDir(projectRoot);
1263
- const tsxBin = resolveTsx(projectRoot);
1264
- if (!tsxBin) {
1265
- console.error(chalk.red("✗ Could not find tsx binary."));
1266
- process.exit(1);
1267
- }
1268
- const envFile = findEnvFile(projectRoot);
1269
- const env = { ...process.env };
1270
- if (envFile) {
1271
- env.DOTENV_CONFIG_PATH = envFile;
1272
- }
1273
- env.REBASE_RESET_EMAIL = email;
1274
- env.REBASE_RESET_PASSWORD = newPassword || "NewPassword123!";
1275
- env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
1276
- const scriptContent = `
1277
- import { createPostgresDatabaseConnection } from "@rebasepro/server-postgresql";
1278
- import { hashPassword } from "@rebasepro/server-core";
1279
- import { eq } from "drizzle-orm";
1280
- import * as dotenv from "dotenv";
1281
- import path from "path";
1282
- import fs from "fs";
1283
-
1284
- dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH });
1285
-
1286
- const email = process.env.REBASE_RESET_EMAIL!;
1287
- const newPassword = process.env.REBASE_RESET_PASSWORD!;
1288
-
1289
- async function resetPassword() {
1290
- const { db } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);
1291
- const hash = await hashPassword(newPassword);
1292
-
1293
- let usersTable;
1294
- try {
1295
- const schemaPath = path.resolve("./src/schema.generated.ts");
1296
- if (fs.existsSync(schemaPath)) {
1297
- const schema = await import("file://" + schemaPath);
1298
- usersTable = schema.users || schema.tables?.users;
1299
- }
1300
- } catch (e) {
1301
- // ignore and fallback
1302
- }
1303
-
1304
- if (!usersTable) {
1305
- const pgServer = await import("@rebasepro/server-postgresql");
1306
- usersTable = pgServer.users;
1307
- }
1308
-
1309
- const passwordHashKey = (usersTable.passwordHash || "passwordHash" in usersTable) ? "passwordHash" : "password_hash";
1310
-
1311
- const result = await db.update(usersTable)
1312
- .set({ [passwordHashKey]: hash })
1313
- .where(eq(usersTable.email, email))
1314
- .returning({
1315
- id: usersTable.id,
1316
- email: usersTable.email
1317
- });
1318
-
1319
- if (result.length > 0) {
1320
- console.log("✅ Password reset for: " + result[0].email);
1321
- ${!newPassword ? 'console.log(" New password: " + newPassword);' : ""}
1322
- } else {
1323
- console.log("✗ User not found: " + email);
1324
- }
1325
- process.exit(0);
1326
- }
1327
-
1328
- resetPassword().catch(console.error);
1329
- `;
1330
- const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
1331
- fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
1332
- console.log("");
1333
- console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password"));
1334
- console.log("");
1335
- console.log(` ${chalk.gray("Email:")} ${email}`);
1336
- if (newPassword) {
1337
- console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
1338
- }
1339
- console.log("");
1340
- const child = child_process.spawn(tsxBin, [tmpScriptPath], {
1341
- cwd: backendDir,
1342
- stdio: "inherit",
1343
- env
1344
- });
1345
- return new Promise((resolve) => {
1346
- child.on("close", (code) => {
1347
- try {
1348
- fs.unlinkSync(tmpScriptPath);
1349
- } catch {
1350
- }
1351
- if (code !== 0) {
1352
- process.exit(code ?? 1);
1353
- }
1354
- resolve();
1355
- });
1356
- });
1357
- }
1358
- function printAuthHelp() {
1359
- console.log(`
1360
- ${chalk.bold("rebase auth")} — Authentication management commands
1361
-
1362
- ${chalk.green.bold("Usage")}
1363
- rebase auth ${chalk.blue("<command>")} [options]
1364
-
1365
- ${chalk.green.bold("Commands")}
1366
- ${chalk.blue.bold("reset-password")} Reset a user's password
1367
-
1368
- ${chalk.green.bold("reset-password Options")}
1369
- ${chalk.blue("--email, -e")} User's email address
1370
- ${chalk.blue("--password, -p")} New password (default: NewPassword123!)
1371
-
1372
- ${chalk.green.bold("Examples")}
1373
- rebase auth reset-password user@example.com
1374
- rebase auth reset-password --email user@example.com --password MyNewPass!
1375
- `);
1376
- }
1377
- async function doctorCommand(rawArgs) {
1378
- const projectRoot = requireProjectRoot();
1379
- const backendDir = requireBackendDir(projectRoot);
1380
- const activePlugin = getActiveBackendPlugin(backendDir);
1381
- if (!activePlugin) {
1382
- console.error(chalk.red("✗ Could not detect an active database plugin."));
1383
- console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
1384
- process.exit(1);
1385
- }
1386
- const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
1387
- if (!pluginCli) {
1388
- console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
1389
- process.exit(1);
1390
- }
1391
- const envFile = findEnvFile(projectRoot);
1392
- const env = { ...process.env };
1393
- if (envFile) {
1394
- env.DOTENV_CONFIG_PATH = envFile;
1395
- }
1396
- try {
1397
- const isTs = pluginCli.endsWith(".ts");
1398
- if (isTs) {
1399
- const tsxBin = resolveTsx(projectRoot);
1400
- if (!tsxBin) {
1401
- console.error(chalk.red("✗ Could not find tsx binary."));
1402
- process.exit(1);
1403
- }
1404
- await execa.execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
1405
- cwd: backendDir,
1406
- stdio: "inherit",
1407
- env
1408
- });
1409
- } else {
1410
- await execa.execa("node", [pluginCli, ...rawArgs.slice(2)], {
1411
- cwd: backendDir,
1412
- stdio: "inherit",
1413
- env
1414
- });
1415
- }
1416
- } catch {
1417
- process.exit(1);
1418
- }
1419
- }
1420
- 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);
1421
- const __dirname$1 = path.dirname(__filename$1);
1422
- function getVersion() {
1423
- try {
1424
- const pkgPath = path.resolve(__dirname$1, "../package.json");
1425
- if (fs.existsSync(pkgPath)) {
1426
- return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
1427
- }
1428
- } catch {
1429
- }
1430
- return "unknown";
1431
- }
1432
- async function entry(args) {
1433
- const parsedArgs = arg(
1434
- {
1435
- "--version": Boolean,
1436
- "--help": Boolean,
1437
- "-v": "--version",
1438
- "-h": "--help"
1439
- },
1440
- {
1441
- argv: args.slice(2),
1442
- permissive: true
1443
- }
1444
- );
1445
- if (parsedArgs["--version"]) {
1446
- console.log(getVersion());
1447
- return;
1448
- }
1449
- const command = parsedArgs._[0];
1450
- const subcommand = parsedArgs._[1];
1451
- const namespacedCommands = ["schema", "db", "dev", "build", "start", "auth", "doctor"];
1452
- if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
1453
- printHelp();
1454
- return;
1455
- }
1456
- const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
1457
- switch (command) {
1458
- case "init":
1459
- await createRebaseApp(args);
1460
- break;
1461
- case "generate-sdk": {
1462
- const sdkArgs = arg(
1463
- {
1464
- "--collections-dir": String,
1465
- "--output": String,
1466
- "-c": "--collections-dir",
1467
- "-o": "--output"
1468
- },
1469
- {
1470
- argv: args.slice(3),
1471
- permissive: true
1472
- }
1473
- );
1474
- await generateSdkCommand({
1475
- collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
1476
- output: sdkArgs["--output"] || "./generated/sdk",
1477
- cwd: process.cwd()
1478
- });
1479
- break;
1480
- }
1481
- case "schema":
1482
- await schemaCommand(effectiveSubcommand, args);
1483
- break;
1484
- case "db":
1485
- await dbCommand(effectiveSubcommand, args);
1486
- break;
1487
- case "dev":
1488
- await devCommand(args);
1489
- break;
1490
- case "build":
1491
- await buildCommand();
1492
- break;
1493
- case "start":
1494
- await startCommand();
1495
- break;
1496
- case "auth":
1497
- await authCommand(effectiveSubcommand, args);
1498
- break;
1499
- case "doctor":
1500
- await doctorCommand(args);
1501
- break;
1502
- default:
1503
- console.log(chalk.red(`Unknown command: ${command}`));
1504
- console.log("");
1505
- printHelp();
1506
- }
1507
- }
1508
- function printHelp() {
1509
- console.log(`
1510
- ${chalk.bold("Rebase CLI")} — Developer tools for Rebase projects
1511
-
1512
- ${chalk.green.bold("Usage")}
1513
- rebase ${chalk.blue("<command>")} [options]
1514
-
1515
- ${chalk.green.bold("Commands")}
1516
- ${chalk.blue.bold("init")} Create a new Rebase project
1517
- ${chalk.blue.bold("dev")} Start the development server
1518
- ${chalk.blue.bold("build")} Build all workspace packages
1519
- ${chalk.blue.bold("start")} Start the backend server ${chalk.gray("(production)")}
1520
-
1521
- ${chalk.green.bold("Schema")}
1522
- ${chalk.blue.bold("schema generate")} Generate Drizzle schema from collections
1523
- ${chalk.blue.bold("schema introspect")} Introspect database → Rebase collections
1524
- ${chalk.blue.bold("schema")} ${chalk.gray("--help")} Show schema command help
1525
-
1526
- ${chalk.green.bold("Database")}
1527
- ${chalk.blue.bold("db push")} Apply schema directly to database ${chalk.gray("(dev)")}
1528
- ${chalk.blue.bold("db generate")} Generate SQL migration files
1529
- ${chalk.blue.bold("db migrate")} Run pending migrations
1530
- ${chalk.blue.bold("db studio")} Open Drizzle Studio
1531
- ${chalk.blue.bold("db")} ${chalk.gray("--help")} Show database command help
1532
-
1533
- ${chalk.green.bold("SDK")}
1534
- ${chalk.blue.bold("generate-sdk")} Generate a typed JS SDK from collections
1535
-
1536
- ${chalk.green.bold("Auth")}
1537
- ${chalk.blue.bold("auth reset-password")} Reset a user's password
1538
- ${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
1539
-
1540
- ${chalk.green.bold("Diagnostics")}
1541
- ${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
1542
-
1543
- ${chalk.green.bold("Options")}
1544
- ${chalk.blue("--version, -v")} Show version number
1545
- ${chalk.blue("--help, -h")} Show this help message
1546
-
1547
- ${chalk.gray("Documentation: https://rebase.pro/docs")}
1548
- `);
1549
- }
1550
- exports2.authCommand = authCommand;
1551
- exports2.buildCommand = buildCommand;
1552
- exports2.configureEnvFile = configureEnvFile;
1553
- exports2.createRebaseApp = createRebaseApp;
1554
- exports2.dbCommand = dbCommand;
1555
- exports2.detectPackageManager = detectPackageManager;
1556
- exports2.devCommand = devCommand;
1557
- exports2.doctorCommand = doctorCommand;
1558
- exports2.entry = entry;
1559
- exports2.findBackendDir = findBackendDir;
1560
- exports2.findEnvFile = findEnvFile;
1561
- exports2.findFrontendDir = findFrontendDir;
1562
- exports2.findProjectRoot = findProjectRoot;
1563
- exports2.generateSdkCommand = generateSdkCommand;
1564
- exports2.getActiveBackendPlugin = getActiveBackendPlugin;
1565
- exports2.getPMCommands = getPMCommands;
1566
- exports2.requireBackendDir = requireBackendDir;
1567
- exports2.requireProjectRoot = requireProjectRoot;
1568
- exports2.resolveLocalBin = resolveLocalBin;
1569
- exports2.resolvePluginCliScript = resolvePluginCliScript;
1570
- exports2.resolveTsx = resolveTsx;
1571
- exports2.schemaCommand = schemaCommand;
1572
- exports2.startCommand = startCommand;
1573
- Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
1574
- }));
1575
- //# sourceMappingURL=index.cjs.map