@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.es.js CHANGED
@@ -11,767 +11,879 @@ import { fileURLToPath } from "url";
11
11
  import crypto from "crypto";
12
12
  import { generateSDK } from "@rebasepro/sdk-generator";
13
13
  import { execSync, spawn } from "child_process";
14
+ //#region src/utils/package-manager.ts
15
+ /**
16
+ * Package manager detection and command abstraction.
17
+ *
18
+ * Detects whether the user is running pnpm or npm and provides
19
+ * a unified interface for common package-manager operations so
20
+ * the rest of the CLI never has to hardcode a specific PM.
21
+ */
22
+ /**
23
+ * Detect the package manager from the environment or the target directory.
24
+ *
25
+ * Detection order:
26
+ * 1. Explicit override (if provided)
27
+ * 2. `npm_config_user_agent` env var (set by npm/pnpm when running via `npx`/`pnpm dlx`)
28
+ * 3. Lock-file presence in the target directory
29
+ * 4. Default to pnpm (Rebase's recommended PM)
30
+ */
14
31
  function detectPackageManager(targetDir) {
15
- const userAgent = process.env.npm_config_user_agent ?? "";
16
- if (userAgent.startsWith("npm/")) return "npm";
17
- if (userAgent.startsWith("pnpm/")) return "pnpm";
18
- if (targetDir) {
19
- if (fs.existsSync(path.join(targetDir, "package-lock.json"))) return "npm";
20
- if (fs.existsSync(path.join(targetDir, "pnpm-lock.yaml"))) return "pnpm";
21
- }
22
- const cwd = process.cwd();
23
- if (cwd !== targetDir) {
24
- if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm";
25
- if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
26
- }
27
- return "pnpm";
32
+ const userAgent = process.env.npm_config_user_agent ?? "";
33
+ if (userAgent.startsWith("npm/")) return "npm";
34
+ if (userAgent.startsWith("pnpm/")) return "pnpm";
35
+ if (targetDir) {
36
+ if (fs.existsSync(path.join(targetDir, "package-lock.json"))) return "npm";
37
+ if (fs.existsSync(path.join(targetDir, "pnpm-lock.yaml"))) return "pnpm";
38
+ }
39
+ const cwd = process.cwd();
40
+ if (cwd !== targetDir) {
41
+ if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm";
42
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
43
+ }
44
+ return "pnpm";
28
45
  }
46
+ /** Build the command helpers for a given package manager. */
29
47
  function getPMCommands(pm) {
30
- if (pm === "npm") {
31
- return {
32
- name: "npm",
33
- install: ["npm", "install"],
34
- run: (script) => ["npm", "run", script],
35
- exec: (bin, args) => ["npx", bin, ...args],
36
- view: (pkg, field) => ["npm", "view", pkg, field],
37
- runAll: (script) => ["npm", "run", script, "--workspaces", "--if-present"],
38
- runWorkspace: (workspace, script) => ["npm", "run", script, "-w", workspace],
39
- workspaceProtocol: "*"
40
- };
41
- }
42
- return {
43
- name: "pnpm",
44
- install: ["pnpm", "install"],
45
- run: (script) => ["pnpm", "run", script],
46
- exec: (bin, args) => ["pnpm", "exec", bin, ...args],
47
- view: (pkg, field) => ["pnpm", "view", pkg, field],
48
- runAll: (script) => ["pnpm", "-r", "run", script],
49
- runWorkspace: (workspace, script) => ["pnpm", "--filter", workspace, script],
50
- workspaceProtocol: "workspace:*"
51
- };
48
+ if (pm === "npm") return {
49
+ name: "npm",
50
+ install: ["npm", "install"],
51
+ run: (script) => [
52
+ "npm",
53
+ "run",
54
+ script
55
+ ],
56
+ exec: (bin, args) => [
57
+ "npx",
58
+ bin,
59
+ ...args
60
+ ],
61
+ view: (pkg, field) => [
62
+ "npm",
63
+ "view",
64
+ pkg,
65
+ field
66
+ ],
67
+ runAll: (script) => [
68
+ "npm",
69
+ "run",
70
+ script,
71
+ "--workspaces",
72
+ "--if-present"
73
+ ],
74
+ runWorkspace: (workspace, script) => [
75
+ "npm",
76
+ "run",
77
+ script,
78
+ "-w",
79
+ workspace
80
+ ],
81
+ dlx: (pkg, args) => [
82
+ "npx",
83
+ "-y",
84
+ pkg,
85
+ ...args
86
+ ],
87
+ workspaceProtocol: "*"
88
+ };
89
+ return {
90
+ name: "pnpm",
91
+ install: ["pnpm", "install"],
92
+ run: (script) => [
93
+ "pnpm",
94
+ "run",
95
+ script
96
+ ],
97
+ exec: (bin, args) => [
98
+ "pnpm",
99
+ "exec",
100
+ bin,
101
+ ...args
102
+ ],
103
+ view: (pkg, field) => [
104
+ "pnpm",
105
+ "view",
106
+ pkg,
107
+ field
108
+ ],
109
+ runAll: (script) => [
110
+ "pnpm",
111
+ "-r",
112
+ "run",
113
+ script
114
+ ],
115
+ runWorkspace: (workspace, script) => [
116
+ "pnpm",
117
+ "--filter",
118
+ workspace,
119
+ script
120
+ ],
121
+ dlx: (pkg, args) => [
122
+ "pnpm",
123
+ "dlx",
124
+ pkg,
125
+ ...args
126
+ ],
127
+ workspaceProtocol: "workspace:*"
128
+ };
52
129
  }
53
- const access = promisify(fs.access);
54
- const __filename$2 = fileURLToPath(import.meta.url);
55
- const __dirname$2 = path.dirname(__filename$2);
56
- function findParentDir(currentDir, targetName) {
57
- const root = path.parse(currentDir).root;
58
- while (currentDir && currentDir !== root) {
59
- if (path.basename(currentDir) === targetName) {
60
- return currentDir;
61
- }
62
- currentDir = path.dirname(currentDir);
63
- }
64
- return null;
130
+ //#endregion
131
+ //#region src/commands/init.ts
132
+ var access = promisify(fs.access);
133
+ var __filename$2 = fileURLToPath(import.meta.url);
134
+ var __dirname$2 = path.dirname(__filename$2);
135
+ function findParentDir$1(currentDir, targetName) {
136
+ const root = path.parse(currentDir).root;
137
+ while (currentDir && currentDir !== root) {
138
+ if (path.basename(currentDir) === targetName) return currentDir;
139
+ currentDir = path.dirname(currentDir);
140
+ }
141
+ return null;
65
142
  }
66
- const cliRoot = findParentDir(__dirname$2, "cli");
67
- const PRESET_CHOICES = [
68
- { name: "Blog — Posts, Authors, Tags (with markdown editor)", value: "blog", short: "Blog" },
69
- { name: "E-commerce Products, Categories, Orders", value: "ecommerce", short: "E-commerce" },
70
- { name: "Blank — Empty project, just authentication", value: "blank", short: "Blank" }
143
+ var cliRoot = findParentDir$1(__dirname$2, "cli");
144
+ var PRESET_CHOICES = [
145
+ {
146
+ name: "Blog Posts, Authors, Tags (with markdown editor)",
147
+ value: "blog",
148
+ short: "Blog"
149
+ },
150
+ {
151
+ name: "E-commerce — Products, Categories, Orders",
152
+ value: "ecommerce",
153
+ short: "E-commerce"
154
+ },
155
+ {
156
+ name: "Blank — Empty project, just authentication",
157
+ value: "blank",
158
+ short: "Blank"
159
+ }
71
160
  ];
161
+ /**
162
+ * Builds the interactive prompt questions for `rebase init`.
163
+ * Exported for testability — all prompt `type` values must match
164
+ * types registered by the installed version of inquirer.
165
+ */
166
+ function buildInitQuestions(params) {
167
+ const { nameArg, templateArg, hasGitFlag, hasInstallFlag, pm } = params;
168
+ const questions = [];
169
+ if (!nameArg) questions.push({
170
+ type: "input",
171
+ name: "projectName",
172
+ message: "Project name:",
173
+ default: "my-rebase-app",
174
+ validate: (input) => {
175
+ if (!input.trim()) return "Project name is required";
176
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(input)) return "Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, hyphens, dots, or underscores";
177
+ return true;
178
+ }
179
+ });
180
+ if (!templateArg) questions.push({
181
+ type: "select",
182
+ name: "preset",
183
+ message: "Choose a starter template:",
184
+ choices: PRESET_CHOICES,
185
+ default: "blog"
186
+ });
187
+ if (!hasGitFlag) questions.push({
188
+ type: "confirm",
189
+ name: "git",
190
+ message: "Initialize a git repository?",
191
+ default: true
192
+ });
193
+ if (!hasInstallFlag) questions.push({
194
+ type: "confirm",
195
+ name: "installDeps",
196
+ message: `Install dependencies with ${pm}?`,
197
+ default: true
198
+ });
199
+ questions.push({
200
+ type: "input",
201
+ name: "databaseUrl",
202
+ message: "Enter your PostgreSQL database connection string (leave blank to use a local default):",
203
+ default: "",
204
+ validate: (input) => {
205
+ if (input.trim() && /[\r\n]/.test(input)) return "Database URL cannot contain newline characters.";
206
+ return true;
207
+ }
208
+ });
209
+ questions.push({
210
+ type: "confirm",
211
+ name: "introspect",
212
+ message: "Would you like to introspect this database to automatically generate collections?",
213
+ default: true,
214
+ when: (answers) => !!answers.databaseUrl?.trim()
215
+ });
216
+ return questions;
217
+ }
72
218
  async function createRebaseApp(rawArgs) {
73
- console.log(`
219
+ console.log(`
74
220
  ${chalk.bold("Rebase")} — Create a new project 🚀
75
221
  `);
76
- const pm = detectPackageManager();
77
- const options = await promptForOptions(rawArgs, pm);
78
- await createProject(options);
222
+ await createProject(await promptForOptions(rawArgs, detectPackageManager()));
79
223
  }
80
224
  async function promptForOptions(rawArgs, pm) {
81
- const args = arg(
82
- {
83
- "--git": Boolean,
84
- "--install": Boolean,
85
- "--database-url": String,
86
- "--introspect": Boolean,
87
- "--template": String,
88
- "--yes": Boolean,
89
- "-g": "--git",
90
- "-i": "--install",
91
- "-t": "--template",
92
- "-y": "--yes"
93
- },
94
- {
95
- argv: rawArgs.slice(3),
96
- // skip "node", "rebase", "init"
97
- permissive: true
98
- }
99
- );
100
- const nameArg = args._[0];
101
- const isNonInteractive = args["--yes"] || false;
102
- const templateArg = args["--template"];
103
- if (templateArg && !PRESET_CHOICES.some((p) => p.value === templateArg)) {
104
- console.error(chalk.red(`Unknown template "${templateArg}". Available: ${PRESET_CHOICES.map((p) => p.value).join(", ")}`));
105
- process.exit(1);
106
- }
107
- if (isNonInteractive) {
108
- const projectName2 = nameArg || "my-rebase-app";
109
- const targetDirectory2 = path.resolve(process.cwd(), projectName2);
110
- const templateDirectory2 = path.resolve(cliRoot, "templates", "template");
111
- const pmCommands2 = getPMCommands(pm);
112
- return {
113
- projectName: path.basename(targetDirectory2),
114
- git: args["--git"] ?? false,
115
- installDeps: args["--install"] ?? false,
116
- targetDirectory: targetDirectory2,
117
- templateDirectory: templateDirectory2,
118
- databaseUrl: args["--database-url"] || void 0,
119
- introspect: args["--introspect"] || false,
120
- preset: templateArg || "blog",
121
- pm,
122
- pmCommands: pmCommands2
123
- };
124
- }
125
- const questions = [];
126
- if (!nameArg) {
127
- questions.push({
128
- type: "input",
129
- name: "projectName",
130
- message: "Project name:",
131
- default: "my-rebase-app",
132
- validate: (input) => {
133
- if (!input.trim()) return "Project name is required";
134
- if (!/^[a-z0-9][a-z0-9._-]*$/.test(input)) {
135
- return "Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, hyphens, dots, or underscores";
136
- }
137
- return true;
138
- }
139
- });
140
- }
141
- if (!templateArg) {
142
- questions.push({
143
- type: "list",
144
- name: "preset",
145
- message: "Choose a starter template:",
146
- choices: PRESET_CHOICES,
147
- default: "blog"
148
- });
149
- }
150
- if (!args["--git"]) {
151
- questions.push({
152
- type: "confirm",
153
- name: "git",
154
- message: "Initialize a git repository?",
155
- default: true
156
- });
157
- }
158
- if (!args["--install"]) {
159
- questions.push({
160
- type: "confirm",
161
- name: "installDeps",
162
- message: `Install dependencies with ${pm}?`,
163
- default: true
164
- });
165
- }
166
- questions.push({
167
- type: "input",
168
- name: "databaseUrl",
169
- message: "Enter your PostgreSQL database connection string (leave blank to use a local default):",
170
- default: "",
171
- validate: (input) => {
172
- if (input.trim() && /[\r\n]/.test(input)) {
173
- return "Database URL cannot contain newline characters.";
174
- }
175
- return true;
176
- }
177
- });
178
- questions.push({
179
- type: "confirm",
180
- name: "introspect",
181
- message: "Would you like to introspect this database to automatically generate collections?",
182
- default: true,
183
- when: (answers2) => !!answers2.databaseUrl?.trim()
184
- });
185
- const answers = await inquirer.prompt(questions);
186
- const targetDirectory = path.resolve(process.cwd(), nameArg || answers.projectName);
187
- const projectName = path.basename(targetDirectory);
188
- const templateDirectory = path.resolve(cliRoot, "templates", "template");
189
- const pmCommands = getPMCommands(pm);
190
- return {
191
- projectName,
192
- git: args["--git"] || answers.git || false,
193
- installDeps: args["--install"] || answers.installDeps || false,
194
- targetDirectory,
195
- templateDirectory,
196
- databaseUrl: answers.databaseUrl?.trim() || void 0,
197
- introspect: answers.introspect || false,
198
- preset: templateArg || answers.preset || "blog",
199
- pm,
200
- pmCommands
201
- };
225
+ const args = arg({
226
+ "--git": Boolean,
227
+ "--install": Boolean,
228
+ "--database-url": String,
229
+ "--introspect": Boolean,
230
+ "--template": String,
231
+ "--yes": Boolean,
232
+ "-g": "--git",
233
+ "-i": "--install",
234
+ "-t": "--template",
235
+ "-y": "--yes"
236
+ }, {
237
+ argv: rawArgs.slice(3),
238
+ permissive: true
239
+ });
240
+ const nameArg = args._[0];
241
+ const isNonInteractive = args["--yes"] || false;
242
+ const templateArg = args["--template"];
243
+ if (templateArg && !PRESET_CHOICES.some((p) => p.value === templateArg)) {
244
+ console.error(chalk.red(`Unknown template "${templateArg}". Available: ${PRESET_CHOICES.map((p) => p.value).join(", ")}`));
245
+ process.exit(1);
246
+ }
247
+ if (isNonInteractive) {
248
+ const projectName = nameArg || "my-rebase-app";
249
+ const targetDirectory = path.resolve(process.cwd(), projectName);
250
+ const templateDirectory = path.resolve(cliRoot, "templates", "template");
251
+ const pmCommands = getPMCommands(pm);
252
+ return {
253
+ projectName: path.basename(targetDirectory),
254
+ git: args["--git"] ?? false,
255
+ installDeps: args["--install"] ?? false,
256
+ targetDirectory,
257
+ templateDirectory,
258
+ databaseUrl: args["--database-url"] || void 0,
259
+ introspect: args["--introspect"] || false,
260
+ preset: templateArg || "blog",
261
+ pm,
262
+ pmCommands
263
+ };
264
+ }
265
+ const questions = buildInitQuestions({
266
+ nameArg,
267
+ templateArg,
268
+ hasGitFlag: !!args["--git"],
269
+ hasInstallFlag: !!args["--install"],
270
+ pm
271
+ });
272
+ const answers = await inquirer.prompt(questions);
273
+ const targetDirectory = path.resolve(process.cwd(), nameArg || answers.projectName);
274
+ const projectName = path.basename(targetDirectory);
275
+ const templateDirectory = path.resolve(cliRoot, "templates", "template");
276
+ const pmCommands = getPMCommands(pm);
277
+ return {
278
+ projectName,
279
+ git: args["--git"] || answers.git || false,
280
+ installDeps: args["--install"] || answers.installDeps || false,
281
+ targetDirectory,
282
+ templateDirectory,
283
+ databaseUrl: answers.databaseUrl?.trim() || void 0,
284
+ introspect: answers.introspect || false,
285
+ preset: templateArg || answers.preset || "blog",
286
+ pm,
287
+ pmCommands
288
+ };
202
289
  }
203
290
  async function createProject(options) {
204
- if (fs.existsSync(options.targetDirectory)) {
205
- if (fs.readdirSync(options.targetDirectory).length !== 0) {
206
- console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
207
- process.exit(1);
208
- }
209
- } else {
210
- fs.mkdirSync(options.targetDirectory, { recursive: true });
211
- }
212
- try {
213
- await access(options.templateDirectory, fs.constants.R_OK);
214
- } catch {
215
- console.error(`${chalk.red.bold("ERROR")} Template not found at ${options.templateDirectory}`);
216
- process.exit(1);
217
- }
218
- console.log(chalk.gray(" Copying project files..."));
219
- try {
220
- await cp(options.templateDirectory, options.targetDirectory, {
221
- recursive: true,
222
- filter: (source) => {
223
- const basename = path.basename(source);
224
- return basename !== "node_modules" && basename !== ".DS_Store";
225
- }
226
- });
227
- } catch (err) {
228
- console.error(`${chalk.red.bold("ERROR")} Failed to copy template files: ${err instanceof Error ? err.message : String(err)}`);
229
- process.exit(1);
230
- }
231
- await applyPreset(options.targetDirectory, options.preset);
232
- await replacePlaceholders(options);
233
- await configureEnvFile(options.targetDirectory, options.databaseUrl);
234
- if (options.git) {
235
- console.log(chalk.gray(" Initializing git repository..."));
236
- try {
237
- await execa("git", ["init"], { cwd: options.targetDirectory });
238
- } catch {
239
- console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
240
- }
241
- }
242
- const { pm, pmCommands } = options;
243
- const installCmd = pmCommands.install;
244
- const execCmd = pmCommands.exec("rebase", ["schema", "introspect", "--force"]);
245
- if (options.installDeps) {
246
- console.log("");
247
- console.log(chalk.gray(` Installing dependencies with ${pm}...`));
248
- console.log("");
249
- try {
250
- await execa(installCmd[0], installCmd.slice(1), {
251
- cwd: options.targetDirectory,
252
- stdio: "inherit"
253
- });
254
- } catch {
255
- console.warn(chalk.yellow(` Warning: Failed to install dependencies. You may need to run \`${installCmd.join(" ")}\` manually.`));
256
- }
257
- }
258
- if (options.introspect) {
259
- console.log("");
260
- if (options.installDeps) {
261
- console.log(chalk.gray(" Introspecting database and generating collections..."));
262
- console.log("");
263
- try {
264
- await execa(execCmd[0], execCmd.slice(1), {
265
- cwd: options.targetDirectory,
266
- stdio: "inherit"
267
- });
268
- console.log(chalk.green(" Database successfully introspected!"));
269
- } catch {
270
- console.warn(chalk.yellow(" Warning: Failed to introspect database automatically."));
271
- console.warn(chalk.yellow(` You can run \`${execCmd.join(" ")}\` manually after setup.`));
272
- }
273
- } else {
274
- console.warn(chalk.yellow(" Skipping introspection because dependencies were not installed."));
275
- console.warn(chalk.yellow(` Run \`${installCmd.join(" ")}\` then \`${execCmd.join(" ")}\` manually.`));
276
- }
277
- }
278
- console.log("");
279
- console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
280
- console.log("");
281
- console.log(chalk.bold("Next steps:"));
282
- console.log("");
283
- const runDev = pmCommands.run("dev");
284
- const runDbPush = pmCommands.run("db:push");
285
- console.log(` ${chalk.cyan("cd")} ${options.projectName}`);
286
- if (!options.installDeps) {
287
- console.log(` ${chalk.cyan(installCmd.join(" "))}`);
288
- }
289
- console.log("");
290
- if (options.databaseUrl) {
291
- if (options.introspect) {
292
- console.log(chalk.gray(" # Database has been introspected & collections generated!"));
293
- console.log(chalk.gray(" # Start the development server (frontend + backend):"));
294
- console.log(` ${chalk.cyan(runDev.join(" "))}`);
295
- } else {
296
- console.log(chalk.gray(" # Your custom database is configured in .env."));
297
- console.log(chalk.gray(" # If the database is empty, push the Rebase schema to initialize it:"));
298
- console.log(` ${chalk.cyan(runDbPush.join(" "))}`);
299
- console.log("");
300
- console.log(chalk.gray(" # Then start the development server:"));
301
- console.log(` ${chalk.cyan(runDev.join(" "))}`);
302
- }
303
- } else {
304
- console.log(chalk.gray(" # A local database configuration has been generated in .env."));
305
- console.log(chalk.gray(" # 1. Start the PostgreSQL database container:"));
306
- console.log(` ${chalk.cyan("docker compose up -d db")}`);
307
- console.log("");
308
- console.log(chalk.gray(" # 2. Push the Rebase schema to initialize database tables:"));
309
- console.log(` ${chalk.cyan(runDbPush.join(" "))}`);
310
- console.log("");
311
- console.log(chalk.gray(" # 3. Start the development server (frontend + backend):"));
312
- console.log(` ${chalk.cyan(runDev.join(" "))}`);
313
- }
314
- console.log("");
315
- console.log(chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
316
- console.log("");
317
- console.log(chalk.gray("Docs: https://rebase.pro/docs"));
318
- console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
319
- console.log("");
291
+ if (fs.existsSync(options.targetDirectory)) {
292
+ if (fs.readdirSync(options.targetDirectory).length !== 0) {
293
+ console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
294
+ process.exit(1);
295
+ }
296
+ } else fs.mkdirSync(options.targetDirectory, { recursive: true });
297
+ try {
298
+ await access(options.templateDirectory, fs.constants.R_OK);
299
+ } catch {
300
+ console.error(`${chalk.red.bold("ERROR")} Template not found at ${options.templateDirectory}`);
301
+ process.exit(1);
302
+ }
303
+ console.log(chalk.gray(" Copying project files..."));
304
+ try {
305
+ await cp(options.templateDirectory, options.targetDirectory, {
306
+ recursive: true,
307
+ filter: (source) => {
308
+ const basename = path.basename(source);
309
+ return basename !== "node_modules" && basename !== ".DS_Store";
310
+ }
311
+ });
312
+ } catch (err) {
313
+ console.error(`${chalk.red.bold("ERROR")} Failed to copy template files: ${err instanceof Error ? err.message : String(err)}`);
314
+ process.exit(1);
315
+ }
316
+ await applyPreset(options.targetDirectory, options.preset);
317
+ await replacePlaceholders(options);
318
+ await configureEnvFile(options.targetDirectory, options.databaseUrl);
319
+ if (options.git) {
320
+ console.log(chalk.gray(" Initializing git repository..."));
321
+ try {
322
+ await execa("git", ["init"], { cwd: options.targetDirectory });
323
+ } catch {
324
+ console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
325
+ }
326
+ }
327
+ const { pm, pmCommands } = options;
328
+ const installCmd = pmCommands.install;
329
+ const execCmd = pmCommands.exec("rebase", [
330
+ "schema",
331
+ "introspect",
332
+ "--force"
333
+ ]);
334
+ if (options.installDeps) {
335
+ console.log("");
336
+ console.log(chalk.gray(` Installing dependencies with ${pm}...`));
337
+ console.log("");
338
+ try {
339
+ await execa(installCmd[0], installCmd.slice(1), {
340
+ cwd: options.targetDirectory,
341
+ stdio: "inherit"
342
+ });
343
+ } catch {
344
+ console.warn(chalk.yellow(` Warning: Failed to install dependencies. You may need to run \`${installCmd.join(" ")}\` manually.`));
345
+ }
346
+ }
347
+ if (options.introspect) {
348
+ console.log("");
349
+ if (options.installDeps) {
350
+ console.log(chalk.gray(" Introspecting database and generating collections..."));
351
+ console.log("");
352
+ try {
353
+ await execa(execCmd[0], execCmd.slice(1), {
354
+ cwd: options.targetDirectory,
355
+ stdio: "inherit"
356
+ });
357
+ console.log(chalk.green(" Database successfully introspected!"));
358
+ } catch {
359
+ console.warn(chalk.yellow(" Warning: Failed to introspect database automatically."));
360
+ console.warn(chalk.yellow(` You can run \`${execCmd.join(" ")}\` manually after setup.`));
361
+ }
362
+ } else {
363
+ console.warn(chalk.yellow(" Skipping introspection because dependencies were not installed."));
364
+ console.warn(chalk.yellow(` Run \`${installCmd.join(" ")}\` then \`${execCmd.join(" ")}\` manually.`));
365
+ }
366
+ }
367
+ console.log("");
368
+ console.log(`${chalk.green.bold("")} Project ${chalk.bold(options.projectName)} created successfully!`);
369
+ console.log("");
370
+ console.log(chalk.bold("Next steps:"));
371
+ console.log("");
372
+ const runDev = pmCommands.run("dev");
373
+ const runDbPush = pmCommands.run("db:push");
374
+ console.log(` ${chalk.cyan("cd")} ${options.projectName}`);
375
+ if (!options.installDeps) console.log(` ${chalk.cyan(installCmd.join(" "))}`);
376
+ console.log("");
377
+ if (options.databaseUrl) if (options.introspect) {
378
+ console.log(chalk.gray(" # Database has been introspected & collections generated!"));
379
+ console.log(chalk.gray(" # Start the development server (frontend + backend):"));
380
+ console.log(` ${chalk.cyan(runDev.join(" "))}`);
381
+ } else {
382
+ console.log(chalk.gray(" # Your custom database is configured in .env."));
383
+ console.log(chalk.gray(" # If the database is empty, push the Rebase schema to initialize it:"));
384
+ console.log(` ${chalk.cyan(runDbPush.join(" "))}`);
385
+ console.log("");
386
+ console.log(chalk.gray(" # Then start the development server:"));
387
+ console.log(` ${chalk.cyan(runDev.join(" "))}`);
388
+ }
389
+ else {
390
+ console.log(chalk.gray(" # A local database configuration has been generated in .env."));
391
+ console.log(chalk.gray(" # 1. Start the PostgreSQL database container:"));
392
+ console.log(` ${chalk.cyan("docker compose up -d db")}`);
393
+ console.log("");
394
+ console.log(chalk.gray(" # 2. Push the Rebase schema to initialize database tables:"));
395
+ console.log(` ${chalk.cyan(runDbPush.join(" "))}`);
396
+ console.log("");
397
+ console.log(chalk.gray(" # 3. Start the development server (frontend + backend):"));
398
+ console.log(` ${chalk.cyan(runDev.join(" "))}`);
399
+ }
400
+ console.log("");
401
+ console.log(chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
402
+ console.log("");
403
+ console.log(chalk.gray("Docs: https://rebase.pro/docs"));
404
+ console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
405
+ console.log("");
406
+ console.log(chalk.bold("🤖 AI Agent Skills"));
407
+ console.log("");
408
+ console.log(chalk.gray(" Install Rebase agent skills for your AI coding assistant:"));
409
+ console.log("");
410
+ console.log(` ${chalk.cyan("rebase skills install")} ${chalk.gray("or")} ${chalk.cyan(pmCommands.run("skills:install").join(" "))}`);
411
+ console.log("");
320
412
  }
413
+ /**
414
+ * Apply a template preset by replacing the default collection files.
415
+ *
416
+ * The template ships with blog collections at the top level and
417
+ * preset alternatives under `config/collections/presets/<name>/`.
418
+ * This function swaps the active collection files and removes the
419
+ * presets directory so the final project is clean.
420
+ */
321
421
  async function applyPreset(targetDirectory, preset) {
322
- const collectionsDir = path.join(targetDirectory, "config", "collections");
323
- const presetsDir = path.join(collectionsDir, "presets");
324
- if (preset !== "blog") {
325
- const presetDir = path.join(presetsDir, preset);
326
- if (!fs.existsSync(presetDir)) {
327
- console.warn(chalk.yellow(` Warning: Preset "${preset}" not found, falling back to blog template.`));
328
- cleanupPresets(presetsDir);
329
- return;
330
- }
331
- const blogFiles = ["posts.ts", "authors.ts", "tags.ts", "index.ts"];
332
- for (const file of blogFiles) {
333
- const filePath = path.join(collectionsDir, file);
334
- if (fs.existsSync(filePath)) {
335
- fs.unlinkSync(filePath);
336
- }
337
- }
338
- const presetFiles = fs.readdirSync(presetDir).filter((f) => f.endsWith(".ts"));
339
- for (const file of presetFiles) {
340
- fs.copyFileSync(
341
- path.join(presetDir, file),
342
- path.join(collectionsDir, file)
343
- );
344
- }
345
- }
346
- cleanupPresets(presetsDir);
422
+ const collectionsDir = path.join(targetDirectory, "config", "collections");
423
+ const presetsDir = path.join(collectionsDir, "presets");
424
+ if (preset !== "blog") {
425
+ const presetDir = path.join(presetsDir, preset);
426
+ if (!fs.existsSync(presetDir)) {
427
+ console.warn(chalk.yellow(` Warning: Preset "${preset}" not found, falling back to blog template.`));
428
+ cleanupPresets(presetsDir);
429
+ return;
430
+ }
431
+ for (const file of [
432
+ "posts.ts",
433
+ "authors.ts",
434
+ "tags.ts",
435
+ "index.ts"
436
+ ]) {
437
+ const filePath = path.join(collectionsDir, file);
438
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
439
+ }
440
+ const presetFiles = fs.readdirSync(presetDir).filter((f) => f.endsWith(".ts"));
441
+ for (const file of presetFiles) fs.copyFileSync(path.join(presetDir, file), path.join(collectionsDir, file));
442
+ }
443
+ cleanupPresets(presetsDir);
347
444
  }
348
445
  function cleanupPresets(presetsDir) {
349
- if (fs.existsSync(presetsDir)) {
350
- fs.rmSync(presetsDir, { recursive: true, force: true });
351
- }
446
+ if (fs.existsSync(presetsDir)) fs.rmSync(presetsDir, {
447
+ recursive: true,
448
+ force: true
449
+ });
352
450
  }
353
451
  async function replacePlaceholders(options) {
354
- const filesToProcess = [
355
- "package.json",
356
- "frontend/package.json",
357
- "backend/package.json",
358
- "config/package.json",
359
- "frontend/index.html",
360
- "pnpm-workspace.yaml",
361
- "README.md"
362
- ];
363
- const packageJsonPath = path.resolve(cliRoot, "package.json");
364
- let cliVersion = "latest";
365
- if (fs.existsSync(packageJsonPath)) {
366
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
367
- cliVersion = pkg.version || "latest";
368
- }
369
- const versionCache = /* @__PURE__ */ new Map();
370
- const viewBin = "npm";
371
- const getPackageVersion = async (pkgName) => {
372
- if (versionCache.has(pkgName)) return versionCache.get(pkgName);
373
- if (process.env.REBASE_E2E === "true") {
374
- versionCache.set(pkgName, cliVersion);
375
- return cliVersion;
376
- }
377
- let versionToUse = cliVersion;
378
- try {
379
- const { stdout } = await execa(viewBin, ["view", `${pkgName}@${cliVersion}`, "version"]);
380
- if (!stdout.trim()) throw new Error("Not found");
381
- versionToUse = stdout.trim();
382
- } catch {
383
- try {
384
- const tag = cliVersion.includes("canary") ? "canary" : "latest";
385
- const { stdout } = await execa(viewBin, ["view", `${pkgName}@${tag}`, "version"]);
386
- if (!stdout.trim()) throw new Error("Not found");
387
- versionToUse = stdout.trim();
388
- } catch {
389
- try {
390
- const { stdout } = await execa(viewBin, ["view", pkgName, "version"]);
391
- versionToUse = stdout.trim() || "latest";
392
- } catch {
393
- versionToUse = "latest";
394
- }
395
- }
396
- }
397
- versionCache.set(pkgName, versionToUse);
398
- return versionToUse;
399
- };
400
- const allPackages = /* @__PURE__ */ new Set();
401
- const fileContents = /* @__PURE__ */ new Map();
402
- for (const file of filesToProcess) {
403
- const fullPath = path.resolve(options.targetDirectory, file);
404
- if (!fs.existsSync(fullPath)) continue;
405
- const content = fs.readFileSync(fullPath, "utf-8");
406
- fileContents.set(fullPath, content);
407
- const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
408
- for (const match of matches) {
409
- allPackages.add(match[1]);
410
- }
411
- }
412
- console.log(chalk.gray(" Resolving package versions..."));
413
- await Promise.all(Array.from(allPackages).map(getPackageVersion));
414
- for (const [fullPath, originalContent] of fileContents.entries()) {
415
- let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
416
- const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
417
- for (const match of matches) {
418
- const pkgName = match[1];
419
- const resolvedVersion = versionCache.get(pkgName) || "latest";
420
- content = content.replace(new RegExp(`"${pkgName}":\\s*"workspace:\\*"`, "g"), `"${pkgName}": "${resolvedVersion}"`);
421
- }
422
- fs.writeFileSync(fullPath, content, "utf-8");
423
- }
452
+ const filesToProcess = [
453
+ "package.json",
454
+ "frontend/package.json",
455
+ "backend/package.json",
456
+ "config/package.json",
457
+ "frontend/index.html",
458
+ "pnpm-workspace.yaml",
459
+ "README.md"
460
+ ];
461
+ const packageJsonPath = path.resolve(cliRoot, "package.json");
462
+ let cliVersion = "latest";
463
+ if (fs.existsSync(packageJsonPath)) cliVersion = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).version || "latest";
464
+ const versionCache = /* @__PURE__ */ new Map();
465
+ const viewBin = "npm";
466
+ const getPackageVersion = async (pkgName) => {
467
+ if (versionCache.has(pkgName)) return versionCache.get(pkgName);
468
+ if (process.env.REBASE_E2E === "true") {
469
+ versionCache.set(pkgName, cliVersion);
470
+ return cliVersion;
471
+ }
472
+ let versionToUse = cliVersion;
473
+ try {
474
+ const { stdout } = await execa(viewBin, [
475
+ "view",
476
+ `${pkgName}@${cliVersion}`,
477
+ "version"
478
+ ]);
479
+ if (!stdout.trim()) throw new Error("Not found");
480
+ versionToUse = stdout.trim();
481
+ } catch {
482
+ try {
483
+ const { stdout } = await execa(viewBin, [
484
+ "view",
485
+ `${pkgName}@${cliVersion.includes("canary") ? "canary" : "latest"}`,
486
+ "version"
487
+ ]);
488
+ if (!stdout.trim()) throw new Error("Not found");
489
+ versionToUse = stdout.trim();
490
+ } catch {
491
+ try {
492
+ const { stdout } = await execa(viewBin, [
493
+ "view",
494
+ pkgName,
495
+ "version"
496
+ ]);
497
+ versionToUse = stdout.trim() || "latest";
498
+ } catch {
499
+ versionToUse = "latest";
500
+ }
501
+ }
502
+ }
503
+ versionCache.set(pkgName, versionToUse);
504
+ return versionToUse;
505
+ };
506
+ const allPackages = /* @__PURE__ */ new Set();
507
+ const fileContents = /* @__PURE__ */ new Map();
508
+ for (const file of filesToProcess) {
509
+ const fullPath = path.resolve(options.targetDirectory, file);
510
+ if (!fs.existsSync(fullPath)) continue;
511
+ const content = fs.readFileSync(fullPath, "utf-8");
512
+ fileContents.set(fullPath, content);
513
+ const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
514
+ for (const match of matches) allPackages.add(match[1]);
515
+ }
516
+ console.log(chalk.gray(" Resolving package versions..."));
517
+ await Promise.all(Array.from(allPackages).map(getPackageVersion));
518
+ for (const [fullPath, originalContent] of fileContents.entries()) {
519
+ let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
520
+ const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
521
+ for (const match of matches) {
522
+ const pkgName = match[1];
523
+ const resolvedVersion = versionCache.get(pkgName) || "latest";
524
+ content = content.replace(new RegExp(`"${pkgName}":\\s*"workspace:\\*"`, "g"), `"${pkgName}": "${resolvedVersion}"`);
525
+ }
526
+ fs.writeFileSync(fullPath, content, "utf-8");
527
+ }
424
528
  }
425
529
  async function isPortAvailable(port) {
426
- return new Promise((resolve) => {
427
- const server = net.createServer();
428
- server.once("error", () => {
429
- resolve(false);
430
- });
431
- server.once("listening", () => {
432
- server.close(() => resolve(true));
433
- });
434
- server.listen(port);
435
- });
530
+ return new Promise((resolve) => {
531
+ const server = net.createServer();
532
+ server.once("error", () => {
533
+ resolve(false);
534
+ });
535
+ server.once("listening", () => {
536
+ server.close(() => resolve(true));
537
+ });
538
+ server.listen(port);
539
+ });
436
540
  }
437
541
  async function findAvailablePort(startPort) {
438
- let port = startPort;
439
- while (!await isPortAvailable(port)) {
440
- port++;
441
- }
442
- return port;
542
+ let port = startPort;
543
+ while (!await isPortAvailable(port)) port++;
544
+ return port;
443
545
  }
444
546
  async function configureEnvFile(targetDirectory, databaseUrl) {
445
- const envExamplePath = path.join(targetDirectory, ".env.example");
446
- const envPath = path.join(targetDirectory, ".env");
447
- if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
448
- fs.copyFileSync(envExamplePath, envPath);
449
- const jwtSecret = crypto.randomBytes(32).toString("hex");
450
- const dbPassword = crypto.randomBytes(16).toString("hex");
451
- let envContent = fs.readFileSync(envPath, "utf-8");
452
- envContent = envContent.replace(
453
- /^JWT_SECRET=.*$/m,
454
- `JWT_SECRET=${jwtSecret}`
455
- );
456
- if (databaseUrl) {
457
- if (/[\r\n]/.test(databaseUrl)) {
458
- throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
459
- }
460
- envContent = envContent.replace(
461
- /^DATABASE_URL=.*$/m,
462
- `DATABASE_URL=${databaseUrl}`
463
- );
464
- } else {
465
- const dbPort = await findAvailablePort(5432);
466
- envContent = envContent.replace(
467
- /^DATABASE_URL=.*$/m,
468
- `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public
469
- DATABASE_PASSWORD=${dbPassword}`
470
- );
471
- const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
472
- if (fs.existsSync(dockerComposePath)) {
473
- let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
474
- dockerComposeContent = dockerComposeContent.replace(
475
- /-\s*"5432:5432"/g,
476
- `- "${dbPort}:5432"`
477
- );
478
- fs.writeFileSync(dockerComposePath, dockerComposeContent, "utf-8");
479
- }
480
- }
481
- fs.writeFileSync(envPath, envContent, "utf-8");
482
- }
547
+ const envExamplePath = path.join(targetDirectory, ".env.example");
548
+ const envPath = path.join(targetDirectory, ".env");
549
+ if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
550
+ fs.copyFileSync(envExamplePath, envPath);
551
+ const jwtSecret = crypto.randomBytes(32).toString("hex");
552
+ const dbPassword = crypto.randomBytes(16).toString("hex");
553
+ let envContent = fs.readFileSync(envPath, "utf-8");
554
+ envContent = envContent.replace(/^JWT_SECRET=.*$/m, `JWT_SECRET=${jwtSecret}`);
555
+ if (databaseUrl) {
556
+ if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
557
+ envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${databaseUrl}`);
558
+ } else {
559
+ const dbPort = await findAvailablePort(5432);
560
+ envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public\nDATABASE_PASSWORD=${dbPassword}`);
561
+ const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
562
+ if (fs.existsSync(dockerComposePath)) {
563
+ let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
564
+ dockerComposeContent = dockerComposeContent.replace(/-\s*"5432:5432"/g, `- "${dbPort}:5432"`);
565
+ fs.writeFileSync(dockerComposePath, dockerComposeContent, "utf-8");
566
+ }
567
+ }
568
+ fs.writeFileSync(envPath, envContent, "utf-8");
569
+ }
483
570
  }
571
+ //#endregion
572
+ //#region src/commands/generate_sdk.ts
573
+ /**
574
+ * CLI command: generate-sdk
575
+ *
576
+ * Reads collection definitions from a specified directory (default: ./config/collections),
577
+ * generates a typed JS SDK, and writes it to the output directory (default: ./generated/sdk).
578
+ *
579
+ * Uses jiti for dynamic TypeScript import of collection files.
580
+ */
581
+ /**
582
+ * Dynamically load collection definitions from a directory.
583
+ *
584
+ * Expects the directory to have an index.ts/index.js that exports a default
585
+ * array of EntityCollection objects (matching the app/config/collections pattern).
586
+ */
484
587
  async function loadCollections(collectionsDir) {
485
- const absDir = path.resolve(collectionsDir);
486
- if (!fs.existsSync(absDir)) {
487
- throw new Error(`Collections directory not found: ${absDir}`);
488
- }
489
- let jiti;
490
- try {
491
- const jitiModule = await import("jiti");
492
- jiti = jitiModule.default || jitiModule;
493
- } catch {
494
- throw new Error(
495
- "Could not load 'jiti'. Install it with: pnpm add -D jiti\njiti is required to dynamically import TypeScript collection definitions."
496
- );
497
- }
498
- const jitiInstance = jiti(absDir, {
499
- interopDefault: true,
500
- esmResolve: true
501
- });
502
- const indexCandidates = ["index.ts", "index.js", "index.mjs"];
503
- let indexPath = null;
504
- for (const candidate of indexCandidates) {
505
- const p = path.join(absDir, candidate);
506
- if (fs.existsSync(p)) {
507
- indexPath = p;
508
- break;
509
- }
510
- }
511
- if (!indexPath) {
512
- console.log(chalk.yellow(" No index file found, scanning individual collection files..."));
513
- const collections = [];
514
- const files = fs.readdirSync(absDir).filter(
515
- (f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith(".")
516
- );
517
- for (const file of files) {
518
- try {
519
- const mod2 = jitiInstance(path.join(absDir, file));
520
- const exported2 = mod2.default || mod2;
521
- if (exported2 && typeof exported2 === "object" && "slug" in exported2) {
522
- collections.push(exported2);
523
- } else if (Array.isArray(exported2)) {
524
- collections.push(...exported2);
525
- }
526
- } catch (err) {
527
- console.warn(chalk.yellow(` ⚠ Skipping ${file}: ${err.message}`));
528
- }
529
- }
530
- return collections;
531
- }
532
- const mod = jitiInstance(indexPath);
533
- const exported = mod.default || mod;
534
- if (Array.isArray(exported)) {
535
- return exported;
536
- } else if (typeof exported === "object" && exported !== null) {
537
- if ("collections" in exported && Array.isArray(exported.collections)) {
538
- return exported.collections;
539
- }
540
- const collections = [];
541
- for (const value of Object.values(exported)) {
542
- if (value && typeof value === "object" && "slug" in value) {
543
- collections.push(value);
544
- }
545
- }
546
- if (collections.length > 0) return collections;
547
- }
548
- throw new Error(
549
- `Could not extract collections from ${indexPath}.
550
- Expected a default export of EntityCollection[] or an object with named collection exports.`
551
- );
588
+ const absDir = path.resolve(collectionsDir);
589
+ if (!fs.existsSync(absDir)) throw new Error(`Collections directory not found: ${absDir}`);
590
+ let jiti;
591
+ try {
592
+ const jitiModule = await import("jiti");
593
+ jiti = jitiModule.default || jitiModule;
594
+ } catch {
595
+ const installCmd = [
596
+ ...getPMCommands(detectPackageManager()).install,
597
+ "-D",
598
+ "jiti"
599
+ ].join(" ");
600
+ throw new Error(`Could not load 'jiti'. Install it with: ${installCmd}\njiti is required to dynamically import TypeScript collection definitions.`);
601
+ }
602
+ const jitiInstance = jiti(absDir, {
603
+ interopDefault: true,
604
+ esmResolve: true
605
+ });
606
+ const indexCandidates = [
607
+ "index.ts",
608
+ "index.js",
609
+ "index.mjs"
610
+ ];
611
+ let indexPath = null;
612
+ for (const candidate of indexCandidates) {
613
+ const p = path.join(absDir, candidate);
614
+ if (fs.existsSync(p)) {
615
+ indexPath = p;
616
+ break;
617
+ }
618
+ }
619
+ if (!indexPath) {
620
+ console.log(chalk.yellow(" No index file found, scanning individual collection files..."));
621
+ const collections = [];
622
+ const files = fs.readdirSync(absDir).filter((f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith("."));
623
+ for (const file of files) try {
624
+ const mod = jitiInstance(path.join(absDir, file));
625
+ const exported = mod.default || mod;
626
+ if (exported && typeof exported === "object" && "slug" in exported) collections.push(exported);
627
+ else if (Array.isArray(exported)) collections.push(...exported);
628
+ } catch (err) {
629
+ console.warn(chalk.yellow(` ⚠ Skipping ${file}: ${err.message}`));
630
+ }
631
+ return collections;
632
+ }
633
+ const mod = jitiInstance(indexPath);
634
+ const exported = mod.default || mod;
635
+ if (Array.isArray(exported)) return exported;
636
+ else if (typeof exported === "object" && exported !== null) {
637
+ if ("collections" in exported && Array.isArray(exported.collections)) return exported.collections;
638
+ const collections = [];
639
+ for (const value of Object.values(exported)) if (value && typeof value === "object" && "slug" in value) collections.push(value);
640
+ if (collections.length > 0) return collections;
641
+ }
642
+ throw new Error(`Could not extract collections from ${indexPath}.\nExpected a default export of EntityCollection[] or an object with named collection exports.`);
552
643
  }
644
+ /**
645
+ * Write generated files to the output directory.
646
+ */
553
647
  function writeFiles(outputDir, files) {
554
- const absOutput = path.resolve(outputDir);
555
- fs.mkdirSync(absOutput, { recursive: true });
556
- for (const file of files) {
557
- const filePath = path.join(absOutput, file.path);
558
- const dir = path.dirname(filePath);
559
- if (!fs.existsSync(dir)) {
560
- fs.mkdirSync(dir, { recursive: true });
561
- }
562
- fs.writeFileSync(filePath, file.content, "utf-8");
563
- }
648
+ const absOutput = path.resolve(outputDir);
649
+ fs.mkdirSync(absOutput, { recursive: true });
650
+ for (const file of files) {
651
+ const filePath = path.join(absOutput, file.path);
652
+ const dir = path.dirname(filePath);
653
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
654
+ fs.writeFileSync(filePath, file.content, "utf-8");
655
+ }
564
656
  }
657
+ /**
658
+ * Main entry point for the generate-sdk command.
659
+ */
565
660
  async function generateSdkCommand(args) {
566
- const { collectionsDir, output, cwd } = args;
567
- const resolvedCollectionsDir = path.isAbsolute(collectionsDir) ? collectionsDir : path.join(cwd, collectionsDir);
568
- const resolvedOutput = path.isAbsolute(output) ? output : path.join(cwd, output);
569
- console.log("");
570
- console.log(chalk.bold(" 🔧 Rebase SDK Generator"));
571
- console.log("");
572
- console.log(` ${chalk.gray("Collections:")} ${resolvedCollectionsDir}`);
573
- console.log(` ${chalk.gray("Output:")} ${resolvedOutput}`);
574
- console.log("");
575
- console.log(chalk.cyan(" → Loading collection definitions..."));
576
- const collections = await loadCollections(resolvedCollectionsDir);
577
- collections.sort((a, b) => a.slug.localeCompare(b.slug));
578
- if (collections.length === 0) {
579
- console.log(chalk.red(" ✗ No collections found. Nothing to generate."));
580
- process.exit(1);
581
- }
582
- console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
583
- console.log("");
584
- console.log(chalk.cyan(" → Generating SDK files..."));
585
- const files = generateSDK(collections);
586
- console.log(chalk.green(` ✓ Generated ${files.length} file(s)`));
587
- console.log(chalk.cyan(` → Writing to ${resolvedOutput}...`));
588
- writeFiles(resolvedOutput, files);
589
- console.log("");
590
- console.log(chalk.green.bold(" ✓ SDK generated successfully!"));
591
- console.log("");
592
- console.log(chalk.gray(" Usage:"));
593
- console.log(chalk.gray(" import { createRebaseClient } from '@rebasepro/client';"));
594
- console.log(chalk.gray(` import type { Database } from './${path.relative(cwd, path.join(resolvedOutput, "database.types"))}';`));
595
- console.log("");
596
- console.log(chalk.gray(" const rebase = createRebaseClient<Database>({"));
597
- console.log(chalk.gray(" baseUrl: 'http://localhost:3001',"));
598
- console.log(chalk.gray(" // token: 'your-jwt-token',"));
599
- console.log(chalk.gray(" });"));
600
- console.log("");
601
- console.log(chalk.gray(` const { data } = await rebase.collection('${collections[0]?.slug || "my_collection"}').find();`));
602
- console.log("");
661
+ const { collectionsDir, output, cwd } = args;
662
+ const resolvedCollectionsDir = path.isAbsolute(collectionsDir) ? collectionsDir : path.join(cwd, collectionsDir);
663
+ const resolvedOutput = path.isAbsolute(output) ? output : path.join(cwd, output);
664
+ console.log("");
665
+ console.log(chalk.bold(" 🔧 Rebase SDK Generator"));
666
+ console.log("");
667
+ console.log(` ${chalk.gray("Collections:")} ${resolvedCollectionsDir}`);
668
+ console.log(` ${chalk.gray("Output:")} ${resolvedOutput}`);
669
+ console.log("");
670
+ console.log(chalk.cyan(" → Loading collection definitions..."));
671
+ const collections = await loadCollections(resolvedCollectionsDir);
672
+ collections.sort((a, b) => a.slug.localeCompare(b.slug));
673
+ if (collections.length === 0) {
674
+ console.log(chalk.red(" ✗ No collections found. Nothing to generate."));
675
+ process.exit(1);
676
+ }
677
+ console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
678
+ console.log("");
679
+ console.log(chalk.cyan(" → Generating SDK files..."));
680
+ const files = generateSDK(collections);
681
+ console.log(chalk.green(` ✓ Generated ${files.length} file(s)`));
682
+ console.log(chalk.cyan(` → Writing to ${resolvedOutput}...`));
683
+ writeFiles(resolvedOutput, files);
684
+ console.log("");
685
+ console.log(chalk.green.bold(" ✓ SDK generated successfully!"));
686
+ console.log("");
687
+ console.log(chalk.gray(" Usage:"));
688
+ console.log(chalk.gray(" import { createRebaseClient } from '@rebasepro/client';"));
689
+ console.log(chalk.gray(` import type { Database } from './${path.relative(cwd, path.join(resolvedOutput, "database.types"))}';`));
690
+ console.log("");
691
+ console.log(chalk.gray(" const rebase = createRebaseClient<Database>({"));
692
+ console.log(chalk.gray(" baseUrl: 'http://localhost:3001',"));
693
+ console.log(chalk.gray(" // token: 'your-jwt-token',"));
694
+ console.log(chalk.gray(" });"));
695
+ console.log("");
696
+ console.log(chalk.gray(` const { data } = await rebase.collection('${collections[0]?.slug || "my_collection"}').find();`));
697
+ console.log("");
603
698
  }
699
+ //#endregion
700
+ //#region src/utils/project.ts
701
+ /**
702
+ * Project discovery utilities for the Rebase CLI.
703
+ *
704
+ * These helpers locate the project root, backend directory, .env file,
705
+ * and local binaries — used by all CLI command modules.
706
+ */
707
+ /**
708
+ * Walk up from `startDir` to find the Rebase project root.
709
+ *
710
+ * The root is identified by a `package.json` that either:
711
+ * - has `workspaces` containing "backend" or "frontend", OR
712
+ * - has a sibling `backend/` directory
713
+ */
604
714
  function findProjectRoot(startDir = process.cwd()) {
605
- let dir = path.resolve(startDir);
606
- const root = path.parse(dir).root;
607
- while (dir !== root) {
608
- const pkgPath = path.join(dir, "package.json");
609
- if (fs.existsSync(pkgPath)) {
610
- try {
611
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
612
- if (pkg.workspaces && Array.isArray(pkg.workspaces)) {
613
- const hasBackend = pkg.workspaces.some(
614
- (w) => w === "backend" || w.includes("backend")
615
- );
616
- if (hasBackend) return dir;
617
- }
618
- } catch {
619
- }
620
- if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) {
621
- return dir;
622
- }
623
- }
624
- dir = path.dirname(dir);
625
- }
626
- return null;
715
+ let dir = path.resolve(startDir);
716
+ const root = path.parse(dir).root;
717
+ while (dir !== root) {
718
+ const pkgPath = path.join(dir, "package.json");
719
+ if (fs.existsSync(pkgPath)) {
720
+ try {
721
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
722
+ if (pkg.workspaces && Array.isArray(pkg.workspaces)) {
723
+ if (pkg.workspaces.some((w) => w === "backend" || w.includes("backend"))) return dir;
724
+ }
725
+ } catch {}
726
+ if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) return dir;
727
+ }
728
+ dir = path.dirname(dir);
729
+ }
730
+ return null;
627
731
  }
732
+ /**
733
+ * Locate the backend directory within the project root.
734
+ */
628
735
  function findBackendDir(projectRoot) {
629
- const backendDir = path.join(projectRoot, "backend");
630
- return fs.existsSync(backendDir) ? backendDir : null;
736
+ const backendDir = path.join(projectRoot, "backend");
737
+ return fs.existsSync(backendDir) ? backendDir : null;
631
738
  }
739
+ /**
740
+ * Detect the active backend plugin (e.g. @rebasepro/server-postgresql) from the backend's package.json.
741
+ */
632
742
  function getActiveBackendPlugin(backendDir) {
633
- const pkgPath = path.join(backendDir, "package.json");
634
- if (!fs.existsSync(pkgPath)) return null;
635
- try {
636
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
637
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
638
- const candidates = Object.keys(deps).filter(
639
- (dep) => dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core"
640
- );
641
- if (candidates.length === 0) return null;
642
- if (candidates.includes("@rebasepro/server-postgresql")) {
643
- return "@rebasepro/server-postgresql";
644
- }
645
- for (const candidate of candidates) {
646
- if (resolvePluginCliScript(backendDir, candidate)) {
647
- return candidate;
648
- }
649
- }
650
- return candidates[0];
651
- } catch {
652
- }
653
- return null;
743
+ const pkgPath = path.join(backendDir, "package.json");
744
+ if (!fs.existsSync(pkgPath)) return null;
745
+ try {
746
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
747
+ const deps = {
748
+ ...pkg.dependencies,
749
+ ...pkg.devDependencies
750
+ };
751
+ const candidates = Object.keys(deps).filter((dep) => dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core");
752
+ if (candidates.length === 0) return null;
753
+ if (candidates.includes("@rebasepro/server-postgresql")) return "@rebasepro/server-postgresql";
754
+ for (const candidate of candidates) if (resolvePluginCliScript(backendDir, candidate)) return candidate;
755
+ return candidates[0];
756
+ } catch {}
757
+ return null;
654
758
  }
759
+ /**
760
+ * Resolve the active plugin's CLI script.
761
+ */
655
762
  function resolvePluginCliScript(backendDir, pluginName) {
656
- const candidates = [
657
- path.join(backendDir, "node_modules", pluginName, "src", "cli.ts"),
658
- path.join(backendDir, "node_modules", pluginName, "dist", "cli.js"),
659
- // For monorepo dev mode:
660
- path.resolve(backendDir, "..", "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
661
- path.resolve(backendDir, "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
662
- path.resolve(backendDir, "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts")
663
- ];
664
- for (const candidate of candidates) {
665
- if (fs.existsSync(candidate)) return candidate;
666
- }
667
- return null;
763
+ const candidates = [
764
+ path.join(backendDir, "node_modules", pluginName, "src", "cli.ts"),
765
+ path.join(backendDir, "node_modules", pluginName, "dist", "cli.js"),
766
+ path.resolve(backendDir, "..", "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
767
+ path.resolve(backendDir, "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
768
+ path.resolve(backendDir, "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts")
769
+ ];
770
+ for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
771
+ return null;
668
772
  }
773
+ /**
774
+ * Locate the frontend directory within the project root.
775
+ */
669
776
  function findFrontendDir(projectRoot) {
670
- const frontendDir = path.join(projectRoot, "frontend");
671
- return fs.existsSync(frontendDir) ? frontendDir : null;
777
+ const frontendDir = path.join(projectRoot, "frontend");
778
+ return fs.existsSync(frontendDir) ? frontendDir : null;
672
779
  }
780
+ /**
781
+ * Find the .env file. Checks the project root first, then backend.
782
+ */
673
783
  function findEnvFile(projectRoot) {
674
- const candidates = [
675
- path.join(projectRoot, ".env"),
676
- path.join(projectRoot, "backend", ".env")
677
- ];
678
- for (const candidate of candidates) {
679
- if (fs.existsSync(candidate)) return candidate;
680
- }
681
- return null;
784
+ const candidates = [path.join(projectRoot, ".env"), path.join(projectRoot, "backend", ".env")];
785
+ for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
786
+ return null;
682
787
  }
788
+ /**
789
+ * Resolve a binary from the project's node_modules/.bin.
790
+ * Checks backend, root, parent monorepo root, then falls back to PATH.
791
+ */
683
792
  function resolveLocalBin(projectRoot, binName) {
684
- const candidates = [
685
- path.join(projectRoot, "backend", "node_modules", ".bin", binName),
686
- path.join(projectRoot, "node_modules", ".bin", binName)
687
- ];
688
- let parent = path.dirname(projectRoot);
689
- const rootDir = path.parse(parent).root;
690
- while (parent !== rootDir) {
691
- candidates.push(path.join(parent, "node_modules", ".bin", binName));
692
- parent = path.dirname(parent);
693
- }
694
- for (const candidate of candidates) {
695
- if (fs.existsSync(candidate)) return candidate;
696
- }
697
- try {
698
- const globalPath = execSync(`which ${binName}`, { encoding: "utf-8" }).trim();
699
- if (globalPath && fs.existsSync(globalPath)) return globalPath;
700
- } catch {
701
- }
702
- return null;
793
+ const candidates = [path.join(projectRoot, "backend", "node_modules", ".bin", binName), path.join(projectRoot, "node_modules", ".bin", binName)];
794
+ let parent = path.dirname(projectRoot);
795
+ const rootDir = path.parse(parent).root;
796
+ while (parent !== rootDir) {
797
+ candidates.push(path.join(parent, "node_modules", ".bin", binName));
798
+ parent = path.dirname(parent);
799
+ }
800
+ for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
801
+ try {
802
+ const globalPath = execSync(`which ${binName}`, { encoding: "utf-8" }).trim();
803
+ if (globalPath && fs.existsSync(globalPath)) return globalPath;
804
+ } catch {}
805
+ return null;
703
806
  }
807
+ /**
808
+ * Resolve the tsx binary. Checks backend node_modules first, then root.
809
+ */
704
810
  function resolveTsx(projectRoot) {
705
- return resolveLocalBin(projectRoot, "tsx");
811
+ return resolveLocalBin(projectRoot, "tsx");
706
812
  }
813
+ /**
814
+ * Require the project root or exit with a helpful error.
815
+ */
707
816
  function requireProjectRoot() {
708
- const root = findProjectRoot();
709
- if (!root) {
710
- console.error(chalk.red("✗ Could not find a Rebase project root."));
711
- console.error(chalk.gray(" Make sure you are inside a Rebase project directory"));
712
- console.error(chalk.gray(" (one with backend/, frontend/, and config/ directories)."));
713
- process.exit(1);
714
- }
715
- return root;
817
+ const root = findProjectRoot();
818
+ if (!root) {
819
+ console.error(chalk.red("✗ Could not find a Rebase project root."));
820
+ console.error(chalk.gray(" Make sure you are inside a Rebase project directory"));
821
+ console.error(chalk.gray(" (one with backend/, frontend/, and config/ directories)."));
822
+ process.exit(1);
823
+ }
824
+ return root;
716
825
  }
826
+ /**
827
+ * Require the backend directory or exit with a helpful error.
828
+ */
717
829
  function requireBackendDir(projectRoot) {
718
- const backendDir = findBackendDir(projectRoot);
719
- if (!backendDir) {
720
- console.error(chalk.red("✗ Could not find a backend/ directory."));
721
- console.error(chalk.gray(` Expected at: ${path.join(projectRoot, "backend")}`));
722
- process.exit(1);
723
- }
724
- return backendDir;
830
+ const backendDir = findBackendDir(projectRoot);
831
+ if (!backendDir) {
832
+ console.error(chalk.red("✗ Could not find a backend/ directory."));
833
+ console.error(chalk.gray(` Expected at: ${path.join(projectRoot, "backend")}`));
834
+ process.exit(1);
835
+ }
836
+ return backendDir;
725
837
  }
838
+ //#endregion
839
+ //#region src/commands/schema.ts
840
+ /**
841
+ * CLI command: rebase schema <action>
842
+ */
726
843
  async function schemaCommand(subcommand, rawArgs) {
727
- if (!subcommand || subcommand === "--help") {
728
- printSchemaHelp();
729
- return;
730
- }
731
- const projectRoot = requireProjectRoot();
732
- const backendDir = requireBackendDir(projectRoot);
733
- const activePlugin = getActiveBackendPlugin(backendDir);
734
- if (!activePlugin) {
735
- console.error(chalk.red("✗ Could not detect an active database plugin."));
736
- console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
737
- process.exit(1);
738
- }
739
- const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
740
- if (!pluginCli) {
741
- console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
742
- process.exit(1);
743
- }
744
- const envFile = findEnvFile(projectRoot);
745
- const env = { ...process.env };
746
- if (envFile) {
747
- env.DOTENV_CONFIG_PATH = envFile;
748
- }
749
- try {
750
- const isTs = pluginCli.endsWith(".ts");
751
- if (isTs) {
752
- const tsxBin = resolveTsx(projectRoot);
753
- if (!tsxBin) {
754
- console.error(chalk.red("✗ Could not find tsx binary."));
755
- process.exit(1);
756
- }
757
- await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
758
- cwd: backendDir,
759
- stdio: "inherit",
760
- env
761
- });
762
- } else {
763
- await execa("node", [pluginCli, ...rawArgs.slice(2)], {
764
- cwd: backendDir,
765
- stdio: "inherit",
766
- env
767
- });
768
- }
769
- } catch {
770
- process.exit(1);
771
- }
844
+ if (!subcommand || subcommand === "--help") {
845
+ printSchemaHelp();
846
+ return;
847
+ }
848
+ const projectRoot = requireProjectRoot();
849
+ const backendDir = requireBackendDir(projectRoot);
850
+ const activePlugin = getActiveBackendPlugin(backendDir);
851
+ if (!activePlugin) {
852
+ console.error(chalk.red("✗ Could not detect an active database plugin."));
853
+ console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
854
+ process.exit(1);
855
+ }
856
+ const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
857
+ if (!pluginCli) {
858
+ console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
859
+ process.exit(1);
860
+ }
861
+ const envFile = findEnvFile(projectRoot);
862
+ const env = { ...process.env };
863
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
864
+ try {
865
+ if (pluginCli.endsWith(".ts")) {
866
+ const tsxBin = resolveTsx(projectRoot);
867
+ if (!tsxBin) {
868
+ console.error(chalk.red("✗ Could not find tsx binary."));
869
+ process.exit(1);
870
+ }
871
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
872
+ cwd: backendDir,
873
+ stdio: "inherit",
874
+ env
875
+ });
876
+ } else await execa("node", [pluginCli, ...rawArgs.slice(2)], {
877
+ cwd: backendDir,
878
+ stdio: "inherit",
879
+ env
880
+ });
881
+ } catch {
882
+ process.exit(1);
883
+ }
772
884
  }
773
885
  function printSchemaHelp() {
774
- console.log(`
886
+ console.log(`
775
887
  ${chalk.bold("rebase schema")} — Schema management commands
776
888
 
777
889
  ${chalk.green.bold("Usage")}
@@ -791,55 +903,55 @@ ${chalk.green.bold("introspect Options")}
791
903
  ${chalk.blue("--output, -o")} Output directory for generated collection files
792
904
  `);
793
905
  }
906
+ //#endregion
907
+ //#region src/commands/db.ts
908
+ /**
909
+ * CLI command: rebase db <action>
910
+ */
794
911
  async function dbCommand(subcommand, rawArgs) {
795
- if (!subcommand || subcommand === "--help") {
796
- printDbHelp();
797
- return;
798
- }
799
- const projectRoot = requireProjectRoot();
800
- const backendDir = requireBackendDir(projectRoot);
801
- const activePlugin = getActiveBackendPlugin(backendDir);
802
- if (!activePlugin) {
803
- console.error(chalk.red("✗ Could not detect an active database plugin."));
804
- console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
805
- process.exit(1);
806
- }
807
- const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
808
- if (!pluginCli) {
809
- console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
810
- process.exit(1);
811
- }
812
- const envFile = findEnvFile(projectRoot);
813
- const env = { ...process.env };
814
- if (envFile) {
815
- env.DOTENV_CONFIG_PATH = envFile;
816
- }
817
- try {
818
- const isTs = pluginCli.endsWith(".ts");
819
- if (isTs) {
820
- const tsxBin = resolveTsx(projectRoot);
821
- if (!tsxBin) {
822
- console.error(chalk.red("✗ Could not find tsx binary."));
823
- process.exit(1);
824
- }
825
- await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
826
- cwd: backendDir,
827
- stdio: "inherit",
828
- env
829
- });
830
- } else {
831
- await execa("node", [pluginCli, ...rawArgs.slice(2)], {
832
- cwd: backendDir,
833
- stdio: "inherit",
834
- env
835
- });
836
- }
837
- } catch {
838
- process.exit(1);
839
- }
912
+ if (!subcommand || subcommand === "--help") {
913
+ printDbHelp();
914
+ return;
915
+ }
916
+ const projectRoot = requireProjectRoot();
917
+ const backendDir = requireBackendDir(projectRoot);
918
+ const activePlugin = getActiveBackendPlugin(backendDir);
919
+ if (!activePlugin) {
920
+ console.error(chalk.red("✗ Could not detect an active database plugin."));
921
+ console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
922
+ process.exit(1);
923
+ }
924
+ const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
925
+ if (!pluginCli) {
926
+ console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
927
+ process.exit(1);
928
+ }
929
+ const envFile = findEnvFile(projectRoot);
930
+ const env = { ...process.env };
931
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
932
+ try {
933
+ if (pluginCli.endsWith(".ts")) {
934
+ const tsxBin = resolveTsx(projectRoot);
935
+ if (!tsxBin) {
936
+ console.error(chalk.red("✗ Could not find tsx binary."));
937
+ process.exit(1);
938
+ }
939
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
940
+ cwd: backendDir,
941
+ stdio: "inherit",
942
+ env
943
+ });
944
+ } else await execa("node", [pluginCli, ...rawArgs.slice(2)], {
945
+ cwd: backendDir,
946
+ stdio: "inherit",
947
+ env
948
+ });
949
+ } catch {
950
+ process.exit(1);
951
+ }
840
952
  }
841
953
  function printDbHelp() {
842
- console.log(`
954
+ console.log(`
843
955
  ${chalk.bold("rebase db")} — Database management commands
844
956
 
845
957
  ${chalk.green.bold("Usage")}
@@ -865,300 +977,337 @@ ${chalk.green.bold("Examples")}
865
977
  rebase db branch create feature_auth
866
978
  `);
867
979
  }
868
- const DEV_PORT_FILENAME = ".rebase-dev-port";
980
+ //#endregion
981
+ //#region src/commands/dev.ts
982
+ /**
983
+ * CLI command: rebase dev
984
+ *
985
+ * Starts the full development environment:
986
+ * - Backend: tsx watch with auto-reload
987
+ * - Frontend: vite dev server
988
+ *
989
+ * Both processes stream output with color-coded prefixes.
990
+ *
991
+ * When the backend uses port-retry (i.e. the configured port is busy and it
992
+ * binds to the next free one), the CLI detects the actual port from stdout
993
+ * and injects VITE_API_URL into the frontend so it connects automatically.
994
+ *
995
+ * Each project gets a deterministic default port derived from the project
996
+ * root path, so multiple Rebase instances never collide.
997
+ */
998
+ /** Well-known filename the backend writes its actual port to. */
999
+ var DEV_PORT_FILENAME = ".rebase-dev-port";
1000
+ /**
1001
+ * Compute a deterministic port from the project root path.
1002
+ * Range: 3001–3999 (avoids privileged ports and common services).
1003
+ * Two different project directories will almost always get different ports.
1004
+ */
869
1005
  function getProjectPort(projectRoot) {
870
- let hash = 0;
871
- for (let i = 0; i < projectRoot.length; i++) {
872
- hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
873
- }
874
- return 3001 + Math.abs(hash) % 999;
1006
+ let hash = 0;
1007
+ for (let i = 0; i < projectRoot.length; i++) hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
1008
+ return 3001 + Math.abs(hash) % 999;
875
1009
  }
1010
+ /**
1011
+ * Resolve the best starting port for this project:
1012
+ * 1. Explicit --port flag (highest priority)
1013
+ * 2. PORT env var
1014
+ * 3. Previously used port from .rebase-dev-port (port affinity across restarts)
1015
+ * 4. Deterministic hash from project path (unique per project)
1016
+ */
876
1017
  function resolveStartPort(projectRoot, explicitPort) {
877
- if (explicitPort) return explicitPort;
878
- if (process.env.PORT) return parseInt(process.env.PORT, 10);
879
- try {
880
- const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
881
- if (fs.existsSync(portFile)) {
882
- const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
883
- if (saved > 0 && saved < 65536) return saved;
884
- }
885
- } catch {
886
- }
887
- return getProjectPort(projectRoot);
1018
+ if (explicitPort) return explicitPort;
1019
+ if (process.env.PORT) return parseInt(process.env.PORT, 10);
1020
+ try {
1021
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
1022
+ if (fs.existsSync(portFile)) {
1023
+ const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
1024
+ if (saved > 0 && saved < 65536) return saved;
1025
+ }
1026
+ } catch {}
1027
+ return getProjectPort(projectRoot);
888
1028
  }
889
1029
  async function devCommand(rawArgs) {
890
- const args = arg(
891
- {
892
- "--backend-only": Boolean,
893
- "--frontend-only": Boolean,
894
- "--port": Number,
895
- "--generate": Boolean,
896
- "--help": Boolean,
897
- "-b": "--backend-only",
898
- "-f": "--frontend-only",
899
- "-p": "--port",
900
- "-g": "--generate",
901
- "-h": "--help"
902
- },
903
- {
904
- argv: rawArgs.slice(3),
905
- // skip "node rebase dev"
906
- permissive: true
907
- }
908
- );
909
- if (args["--help"]) {
910
- printDevHelp();
911
- return;
912
- }
913
- const projectRoot = requireProjectRoot();
914
- const backendDir = findBackendDir(projectRoot);
915
- const frontendDir = findFrontendDir(projectRoot);
916
- const backendOnly = args["--backend-only"] || false;
917
- const frontendOnly = args["--frontend-only"] || false;
918
- const shouldGenerate = args["--generate"] || process.env.REBASE_AUTO_GENERATE === "true" || process.env.REBASE_GENERATE === "true";
919
- const startPort = resolveStartPort(projectRoot, args["--port"]);
920
- console.log("");
921
- console.log(chalk.bold(" 🚀 Rebase Dev Server"));
922
- console.log("");
923
- const children = [];
924
- let frontendUrl = "";
925
- let backendUrl = "";
926
- let debounceSummary = null;
927
- let bannerPrinted = false;
928
- let resolvedBackendPort = null;
929
- const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
930
- function printSummary() {
931
- if (!frontendUrl || !backendUrl) return;
932
- if (debounceSummary) clearTimeout(debounceSummary);
933
- debounceSummary = setTimeout(() => {
934
- if (bannerPrinted) return;
935
- console.log("");
936
- console.log(chalk.cyan("┌────────────────────────────────────────────────────────────┐"));
937
- console.log(chalk.cyan("│ │"));
938
- console.log(chalk.cyan("│ ✦ Rebase Admin App is ready! │"));
939
- const cleanUrl = stripAnsi(frontendUrl);
940
- const paddedUrl = cleanUrl.padEnd(41);
941
- console.log(chalk.cyan("│ ➜ Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
942
- console.log(chalk.cyan("│ │"));
943
- console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
944
- console.log("");
945
- bannerPrinted = true;
946
- }, 500);
947
- }
948
- const cleanup = () => {
949
- try {
950
- const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
951
- if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
952
- const urlFile = path.join(projectRoot, ".rebase-dev-url");
953
- if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
954
- } catch {
955
- }
956
- children.forEach((child) => {
957
- if (child.pid && !child.killed) {
958
- try {
959
- if (process.platform === "win32") {
960
- execaCommandSync(`taskkill /pid ${child.pid} /T /F`);
961
- } else {
962
- process.kill(-child.pid, "SIGKILL");
963
- }
964
- } catch (e) {
965
- try {
966
- child.kill("SIGKILL");
967
- } catch (err) {
968
- }
969
- }
970
- }
971
- });
972
- process.exit(0);
973
- };
974
- process.on("SIGINT", cleanup);
975
- process.on("SIGTERM", cleanup);
976
- function startFrontend(backendPort) {
977
- if (!frontendDir) return;
978
- console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
979
- const frontendEnv = { ...process.env };
980
- if (backendPort) {
981
- frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
982
- console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
983
- }
984
- const pm = detectPackageManager(projectRoot);
985
- const pmCmds = getPMCommands(pm);
986
- const runDevCmd = pmCmds.run("dev");
987
- const frontendChild = execa(
988
- runDevCmd[0],
989
- runDevCmd.slice(1),
990
- {
991
- cwd: frontendDir,
992
- stdio: ["inherit", "pipe", "pipe"],
993
- env: frontendEnv,
994
- shell: true,
995
- detached: process.platform !== "win32"
996
- }
997
- );
998
- frontendChild.catch(() => {
999
- });
1000
- frontendChild.stdout?.on("data", (data) => {
1001
- const lines = data.toString().split("\n").filter(Boolean);
1002
- lines.forEach((line) => {
1003
- console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
1004
- const cleanLine = stripAnsi(line);
1005
- const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
1006
- if (cleanLine.includes("Local:") && urlMatch) {
1007
- frontendUrl = urlMatch[1];
1008
- printSummary();
1009
- }
1010
- });
1011
- });
1012
- frontendChild.stderr?.on("data", (data) => {
1013
- const lines = data.toString().split("\n").filter(Boolean);
1014
- lines.forEach((line) => {
1015
- console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
1016
- });
1017
- });
1018
- children.push(frontendChild);
1019
- }
1020
- if (!frontendOnly && backendDir) {
1021
- const tsxBin = resolveTsx(projectRoot);
1022
- if (!tsxBin) {
1023
- const pmName = detectPackageManager(projectRoot);
1024
- const addCmd = pmName === "npm" ? "npm install -D tsx" : "pnpm add -D tsx";
1025
- console.error(chalk.red(" ✗ Could not find tsx binary for backend."));
1026
- console.error(chalk.gray(` Install it with: ${addCmd}`));
1027
- process.exit(1);
1028
- }
1029
- const envFile = findEnvFile(projectRoot);
1030
- const env = { ...process.env };
1031
- if (envFile) {
1032
- env.DOTENV_CONFIG_PATH = envFile;
1033
- }
1034
- env.PORT = String(startPort);
1035
- console.log(` ${chalk.cyan("")} Backend: ${chalk.gray(backendDir)}`);
1036
- console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
1037
- let frontendLaunched = false;
1038
- if (shouldGenerate) {
1039
- console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
1040
- try {
1041
- const activePlugin = getActiveBackendPlugin(backendDir);
1042
- const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
1043
- if (pluginCli) {
1044
- await execa(tsxBin, [pluginCli, "schema", "generate"], {
1045
- cwd: backendDir,
1046
- stdio: "inherit",
1047
- env
1048
- });
1049
- }
1050
- await execa("npx", ["rebase", "generate-sdk"], {
1051
- cwd: projectRoot,
1052
- stdio: "inherit",
1053
- env
1054
- });
1055
- console.log(chalk.green(" Initial schema and SDK generated successfully.\n"));
1056
- } catch (err) {
1057
- console.error(chalk.red(` ✗ Initial schema/SDK generation failed: ${err instanceof Error ? err.message : err}
1058
- `));
1059
- }
1060
- const collectionsDir = path.join(projectRoot, "config", "collections");
1061
- if (fs.existsSync(collectionsDir)) {
1062
- let watchDebounce = null;
1063
- fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
1064
- if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
1065
- if (watchDebounce) clearTimeout(watchDebounce);
1066
- watchDebounce = setTimeout(async () => {
1067
- console.log(chalk.yellow(`
1068
- 🔄 Collection change detected (${filename}). Regenerating schema & SDK...`));
1069
- try {
1070
- const activePlugin = getActiveBackendPlugin(backendDir);
1071
- const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
1072
- if (pluginCli) {
1073
- await execa(tsxBin, [pluginCli, "schema", "generate"], {
1074
- cwd: backendDir,
1075
- stdio: "inherit",
1076
- env
1077
- });
1078
- }
1079
- await execa("npx", ["rebase", "generate-sdk"], {
1080
- cwd: projectRoot,
1081
- stdio: "inherit",
1082
- env
1083
- });
1084
- console.log(chalk.green(" ✓ Schema & SDK regenerated successfully. Hono will reload."));
1085
- } catch (err) {
1086
- console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err instanceof Error ? err.message : err}`));
1087
- }
1088
- }, 300);
1089
- });
1090
- }
1091
- }
1092
- const watchArgs = ["watch", "--conditions", "development", "src/index.ts"];
1093
- if (!shouldGenerate) {
1094
- watchArgs.splice(1, 0, `--watch="${path.join("..", "config", "**", "*")}"`);
1095
- }
1096
- const backendChild = execa(
1097
- tsxBin,
1098
- watchArgs,
1099
- {
1100
- cwd: backendDir,
1101
- stdio: ["inherit", "pipe", "pipe"],
1102
- env,
1103
- shell: true,
1104
- detached: process.platform !== "win32"
1105
- }
1106
- );
1107
- backendChild.catch(() => {
1108
- });
1109
- backendChild.stdout?.on("data", (data) => {
1110
- const lines = data.toString().split("\n").filter(Boolean);
1111
- lines.forEach((line) => {
1112
- console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
1113
- const cleanLine = stripAnsi(line);
1114
- const serverMatch = cleanLine.match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
1115
- if (serverMatch) {
1116
- resolvedBackendPort = parseInt(serverMatch[1], 10);
1117
- backendUrl = "started";
1118
- printSummary();
1119
- const urlFile = path.join(projectRoot, ".rebase-dev-url");
1120
- fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
1121
- const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
1122
- fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
1123
- if (!backendOnly && frontendDir && !frontendLaunched) {
1124
- frontendLaunched = true;
1125
- startFrontend(resolvedBackendPort);
1126
- }
1127
- }
1128
- });
1129
- });
1130
- backendChild.stderr?.on("data", (data) => {
1131
- const lines = data.toString().split("\n").filter(Boolean);
1132
- lines.forEach((line) => {
1133
- console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
1134
- });
1135
- });
1136
- children.push(backendChild);
1137
- } else if (!frontendOnly && !backendDir) {
1138
- console.warn(chalk.yellow(" ⚠ No backend/ directory found, skipping backend."));
1139
- }
1140
- if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) {
1141
- startFrontend(null);
1142
- } else if (!backendOnly && !frontendDir) {
1143
- console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
1144
- }
1145
- if (children.length === 0) {
1146
- console.error(chalk.red(" ✗ Nothing to start. Check your project structure."));
1147
- process.exit(1);
1148
- }
1149
- console.log("");
1150
- console.log(chalk.gray(" Press Ctrl+C to stop all servers."));
1151
- console.log("");
1152
- await Promise.all(
1153
- children.map(
1154
- (child) => new Promise((resolve) => {
1155
- child.finally(() => resolve());
1156
- })
1157
- )
1158
- );
1030
+ const args = arg({
1031
+ "--backend-only": Boolean,
1032
+ "--frontend-only": Boolean,
1033
+ "--port": Number,
1034
+ "--generate": Boolean,
1035
+ "--help": Boolean,
1036
+ "-b": "--backend-only",
1037
+ "-f": "--frontend-only",
1038
+ "-p": "--port",
1039
+ "-g": "--generate",
1040
+ "-h": "--help"
1041
+ }, {
1042
+ argv: rawArgs.slice(3),
1043
+ permissive: true
1044
+ });
1045
+ if (args["--help"]) {
1046
+ printDevHelp();
1047
+ return;
1048
+ }
1049
+ const projectRoot = requireProjectRoot();
1050
+ const backendDir = findBackendDir(projectRoot);
1051
+ const frontendDir = findFrontendDir(projectRoot);
1052
+ const backendOnly = args["--backend-only"] || false;
1053
+ const frontendOnly = args["--frontend-only"] || false;
1054
+ const shouldGenerate = args["--generate"] || process.env.REBASE_AUTO_GENERATE === "true" || process.env.REBASE_GENERATE === "true";
1055
+ const startPort = resolveStartPort(projectRoot, args["--port"]);
1056
+ console.log("");
1057
+ console.log(chalk.bold(" 🚀 Rebase Dev Server"));
1058
+ console.log("");
1059
+ const children = [];
1060
+ let frontendUrl = "";
1061
+ let backendUrl = "";
1062
+ let debounceSummary = null;
1063
+ let bannerPrinted = false;
1064
+ /** Actual backend port, resolved once the server prints its URL. */
1065
+ let resolvedBackendPort = null;
1066
+ const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
1067
+ function printSummary() {
1068
+ if (!frontendUrl || !backendUrl) return;
1069
+ if (debounceSummary) clearTimeout(debounceSummary);
1070
+ debounceSummary = setTimeout(() => {
1071
+ if (bannerPrinted) return;
1072
+ console.log("");
1073
+ console.log(chalk.cyan("┌────────────────────────────────────────────────────────────┐"));
1074
+ console.log(chalk.cyan("│ │"));
1075
+ console.log(chalk.cyan("│ ✦ Rebase Admin App is ready! │"));
1076
+ const paddedUrl = stripAnsi(frontendUrl).padEnd(41);
1077
+ console.log(chalk.cyan("│ ➜ Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
1078
+ console.log(chalk.cyan("│ │"));
1079
+ console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
1080
+ console.log("");
1081
+ bannerPrinted = true;
1082
+ }, 500);
1083
+ }
1084
+ const cleanup = () => {
1085
+ try {
1086
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
1087
+ if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
1088
+ const urlFile = path.join(projectRoot, ".rebase-dev-url");
1089
+ if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
1090
+ } catch {}
1091
+ children.forEach((child) => {
1092
+ if (child.pid && !child.killed) try {
1093
+ if (process.platform === "win32") execaCommandSync(`taskkill /pid ${child.pid} /T /F`);
1094
+ else process.kill(-child.pid, "SIGKILL");
1095
+ } catch (e) {
1096
+ try {
1097
+ child.kill("SIGKILL");
1098
+ } catch (err) {}
1099
+ }
1100
+ });
1101
+ process.exit(0);
1102
+ };
1103
+ process.on("SIGINT", cleanup);
1104
+ process.on("SIGTERM", cleanup);
1105
+ /**
1106
+ * Start the Vite frontend, optionally injecting the backend port.
1107
+ */
1108
+ function startFrontend(backendPort) {
1109
+ if (!frontendDir) return;
1110
+ console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
1111
+ const frontendEnv = { ...process.env };
1112
+ if (backendPort) {
1113
+ frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
1114
+ console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
1115
+ }
1116
+ const runDevCmd = getPMCommands(detectPackageManager(projectRoot)).run("dev");
1117
+ const frontendChild = execa(runDevCmd[0], runDevCmd.slice(1), {
1118
+ cwd: frontendDir,
1119
+ stdio: [
1120
+ "inherit",
1121
+ "pipe",
1122
+ "pipe"
1123
+ ],
1124
+ env: frontendEnv,
1125
+ shell: true,
1126
+ detached: process.platform !== "win32"
1127
+ });
1128
+ frontendChild.catch(() => {});
1129
+ frontendChild.stdout?.on("data", (data) => {
1130
+ data.toString().split("\n").filter(Boolean).forEach((line) => {
1131
+ console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
1132
+ const cleanLine = stripAnsi(line);
1133
+ const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
1134
+ if (cleanLine.includes("Local:") && urlMatch) {
1135
+ frontendUrl = urlMatch[1];
1136
+ printSummary();
1137
+ }
1138
+ });
1139
+ });
1140
+ frontendChild.stderr?.on("data", (data) => {
1141
+ data.toString().split("\n").filter(Boolean).forEach((line) => {
1142
+ console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
1143
+ });
1144
+ });
1145
+ children.push(frontendChild);
1146
+ }
1147
+ if (!frontendOnly && backendDir) {
1148
+ const tsxBin = resolveTsx(projectRoot);
1149
+ if (!tsxBin) {
1150
+ const addCmd = [
1151
+ ...getPMCommands(detectPackageManager(projectRoot)).install,
1152
+ "-D",
1153
+ "tsx"
1154
+ ].join(" ");
1155
+ console.error(chalk.red(" ✗ Could not find tsx binary for backend."));
1156
+ console.error(chalk.gray(` Install it with: ${addCmd}`));
1157
+ process.exit(1);
1158
+ }
1159
+ const envFile = findEnvFile(projectRoot);
1160
+ const env = { ...process.env };
1161
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1162
+ env.PORT = String(startPort);
1163
+ console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
1164
+ console.log(` ${chalk.gray(" PORT")} = ${chalk.white(String(startPort))}`);
1165
+ /** Whether the frontend has been launched (we only launch it once). */
1166
+ let frontendLaunched = false;
1167
+ if (shouldGenerate) {
1168
+ console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
1169
+ try {
1170
+ const activePlugin = getActiveBackendPlugin(backendDir);
1171
+ const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
1172
+ if (pluginCli) await execa(tsxBin, [
1173
+ pluginCli,
1174
+ "schema",
1175
+ "generate"
1176
+ ], {
1177
+ cwd: backendDir,
1178
+ stdio: "inherit",
1179
+ env
1180
+ });
1181
+ const sdkCmd = getPMCommands(detectPackageManager(projectRoot)).exec("rebase", ["generate-sdk"]);
1182
+ await execa(sdkCmd[0], sdkCmd.slice(1), {
1183
+ cwd: projectRoot,
1184
+ stdio: "inherit",
1185
+ env
1186
+ });
1187
+ console.log(chalk.green(" ✓ Initial schema and SDK generated successfully.\n"));
1188
+ } catch (err) {
1189
+ console.error(chalk.red(` ✗ Initial schema/SDK generation failed: ${err instanceof Error ? err.message : err}\n`));
1190
+ }
1191
+ const collectionsDir = path.join(projectRoot, "config", "collections");
1192
+ if (fs.existsSync(collectionsDir)) {
1193
+ let watchDebounce = null;
1194
+ fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
1195
+ if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
1196
+ if (watchDebounce) clearTimeout(watchDebounce);
1197
+ watchDebounce = setTimeout(async () => {
1198
+ console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating schema & SDK...`));
1199
+ try {
1200
+ const activePlugin = getActiveBackendPlugin(backendDir);
1201
+ const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
1202
+ if (pluginCli) await execa(tsxBin, [
1203
+ pluginCli,
1204
+ "schema",
1205
+ "generate"
1206
+ ], {
1207
+ cwd: backendDir,
1208
+ stdio: "inherit",
1209
+ env
1210
+ });
1211
+ const sdkCmd = getPMCommands(detectPackageManager(projectRoot)).exec("rebase", ["generate-sdk"]);
1212
+ await execa(sdkCmd[0], sdkCmd.slice(1), {
1213
+ cwd: projectRoot,
1214
+ stdio: "inherit",
1215
+ env
1216
+ });
1217
+ console.log(chalk.green(" ✓ Schema & SDK regenerated successfully. Hono will reload."));
1218
+ } catch (err) {
1219
+ console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err instanceof Error ? err.message : err}`));
1220
+ }
1221
+ }, 300);
1222
+ });
1223
+ }
1224
+ }
1225
+ const watchArgs = [
1226
+ "watch",
1227
+ "--conditions",
1228
+ "development",
1229
+ "src/index.ts"
1230
+ ];
1231
+ if (!shouldGenerate) {
1232
+ watchArgs.splice(1, 0, `--watch="${path.join("..", "config", "**", "*")}"`);
1233
+ const collectionsDir = path.join(projectRoot, "config", "collections");
1234
+ if (fs.existsSync(collectionsDir)) {
1235
+ let driftDebounce = null;
1236
+ fs.watch(collectionsDir, { recursive: true }, (_eventType, filename) => {
1237
+ if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
1238
+ if (driftDebounce) clearTimeout(driftDebounce);
1239
+ driftDebounce = setTimeout(() => {
1240
+ console.log([
1241
+ "",
1242
+ chalk.yellow(" ┌──────────────────────────────────────────────────────────────┐"),
1243
+ chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(filename.padEnd(31)) + chalk.yellow("│"),
1244
+ chalk.yellow(" │ │"),
1245
+ chalk.yellow(" │ Your schema may be out of sync. Run: │"),
1246
+ chalk.yellow(" │ ") + chalk.cyan("rebase schema generate") + chalk.yellow(" regenerate Drizzle schema │"),
1247
+ chalk.yellow(" │ ") + chalk.cyan("rebase db push ") + chalk.yellow(" sync schema to database │"),
1248
+ chalk.yellow(" │ ") + chalk.cyan("rebase doctor ") + chalk.yellow(" check for drift │"),
1249
+ chalk.yellow(" │ │"),
1250
+ chalk.yellow(" │ TIP: Use ") + chalk.bold("rebase dev --generate") + chalk.yellow(" for auto-regeneration │"),
1251
+ chalk.yellow(" └──────────────────────────────────────────────────────────────┘"),
1252
+ ""
1253
+ ].join("\n"));
1254
+ }, 500);
1255
+ });
1256
+ }
1257
+ }
1258
+ const backendChild = execa(tsxBin, watchArgs, {
1259
+ cwd: backendDir,
1260
+ stdio: [
1261
+ "inherit",
1262
+ "pipe",
1263
+ "pipe"
1264
+ ],
1265
+ env,
1266
+ shell: true,
1267
+ detached: process.platform !== "win32"
1268
+ });
1269
+ backendChild.catch(() => {});
1270
+ backendChild.stdout?.on("data", (data) => {
1271
+ data.toString().split("\n").filter(Boolean).forEach((line) => {
1272
+ console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
1273
+ const serverMatch = stripAnsi(line).match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
1274
+ if (serverMatch) {
1275
+ resolvedBackendPort = parseInt(serverMatch[1], 10);
1276
+ backendUrl = "started";
1277
+ printSummary();
1278
+ const urlFile = path.join(projectRoot, ".rebase-dev-url");
1279
+ fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
1280
+ const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
1281
+ fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
1282
+ if (!backendOnly && frontendDir && !frontendLaunched) {
1283
+ frontendLaunched = true;
1284
+ startFrontend(resolvedBackendPort);
1285
+ }
1286
+ }
1287
+ });
1288
+ });
1289
+ backendChild.stderr?.on("data", (data) => {
1290
+ data.toString().split("\n").filter(Boolean).forEach((line) => {
1291
+ console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
1292
+ });
1293
+ });
1294
+ children.push(backendChild);
1295
+ } else if (!frontendOnly && !backendDir) console.warn(chalk.yellow(" ⚠ No backend/ directory found, skipping backend."));
1296
+ if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) startFrontend(null);
1297
+ else if (!backendOnly && !frontendDir) console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
1298
+ if (children.length === 0) {
1299
+ console.error(chalk.red(" ✗ Nothing to start. Check your project structure."));
1300
+ process.exit(1);
1301
+ }
1302
+ console.log("");
1303
+ console.log(chalk.gray(" Press Ctrl+C to stop all servers."));
1304
+ console.log("");
1305
+ await Promise.all(children.map((child) => new Promise((resolve) => {
1306
+ child.finally(() => resolve());
1307
+ })));
1159
1308
  }
1160
1309
  function printDevHelp() {
1161
- console.log(`
1310
+ console.log(`
1162
1311
  ${chalk.bold("rebase dev")} — Start the development server
1163
1312
 
1164
1313
  ${chalk.green.bold("Usage")}
@@ -1187,101 +1336,114 @@ ${chalk.green.bold("Description")}
1187
1336
  in your environment to enable it.
1188
1337
  `);
1189
1338
  }
1339
+ //#endregion
1340
+ //#region src/commands/build.ts
1341
+ /**
1342
+ * CLI command: rebase build
1343
+ *
1344
+ * Runs the build script in all workspace packages.
1345
+ * Automatically detects the package manager (pnpm or npm) and
1346
+ * uses the correct workspace-aware command.
1347
+ */
1190
1348
  async function buildCommand() {
1191
- const projectRoot = requireProjectRoot();
1192
- const pm = detectPackageManager(projectRoot);
1193
- const cmds = getPMCommands(pm);
1194
- const buildCmd = cmds.runAll("build");
1195
- console.log(`${chalk.bold("Rebase")} — Building all workspaces with ${chalk.cyan(pm)}...
1196
- `);
1197
- try {
1198
- await execa(buildCmd[0], buildCmd.slice(1), {
1199
- cwd: projectRoot,
1200
- stdio: "inherit"
1201
- });
1202
- } catch {
1203
- console.error(chalk.red("\n✗ Build failed."));
1204
- process.exit(1);
1205
- }
1349
+ const projectRoot = requireProjectRoot();
1350
+ const pm = detectPackageManager(projectRoot);
1351
+ const buildCmd = getPMCommands(pm).runAll("build");
1352
+ console.log(`${chalk.bold("Rebase")} Building all workspaces with ${chalk.cyan(pm)}...\n`);
1353
+ try {
1354
+ await execa(buildCmd[0], buildCmd.slice(1), {
1355
+ cwd: projectRoot,
1356
+ stdio: "inherit"
1357
+ });
1358
+ } catch {
1359
+ console.error(chalk.red("\n✗ Build failed."));
1360
+ process.exit(1);
1361
+ }
1206
1362
  }
1363
+ //#endregion
1364
+ //#region src/commands/start.ts
1365
+ /**
1366
+ * CLI command: rebase start
1367
+ *
1368
+ * Starts the backend server in production mode.
1369
+ * Automatically detects the package manager (pnpm or npm) and
1370
+ * runs the start script in the backend workspace.
1371
+ */
1207
1372
  async function startCommand() {
1208
- const projectRoot = requireProjectRoot();
1209
- const pm = detectPackageManager(projectRoot);
1210
- const cmds = getPMCommands(pm);
1211
- const startCmd = cmds.runWorkspace("backend", "start");
1212
- const envFile = findEnvFile(projectRoot);
1213
- const env = { ...process.env };
1214
- if (envFile) {
1215
- env.DOTENV_CONFIG_PATH = envFile;
1216
- }
1217
- console.log(`${chalk.bold("Rebase")} — Starting backend server...
1218
- `);
1219
- try {
1220
- await execa(startCmd[0], startCmd.slice(1), {
1221
- cwd: projectRoot,
1222
- stdio: "inherit",
1223
- env
1224
- });
1225
- } catch {
1226
- console.error(chalk.red("\n✗ Failed to start server."));
1227
- process.exit(1);
1228
- }
1373
+ const projectRoot = requireProjectRoot();
1374
+ const startCmd = getPMCommands(detectPackageManager(projectRoot)).runWorkspace("backend", "start");
1375
+ const envFile = findEnvFile(projectRoot);
1376
+ const env = { ...process.env };
1377
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1378
+ console.log(`${chalk.bold("Rebase")} Starting backend server...\n`);
1379
+ try {
1380
+ await execa(startCmd[0], startCmd.slice(1), {
1381
+ cwd: projectRoot,
1382
+ stdio: "inherit",
1383
+ env
1384
+ });
1385
+ } catch {
1386
+ console.error(chalk.red("\n✗ Failed to start server."));
1387
+ process.exit(1);
1388
+ }
1229
1389
  }
1390
+ //#endregion
1391
+ //#region src/commands/auth.ts
1392
+ /**
1393
+ * CLI command: rebase auth <action>
1394
+ *
1395
+ * Subcommands:
1396
+ * reset-password — Reset a user's password
1397
+ */
1230
1398
  async function authCommand(subcommand, rawArgs) {
1231
- if (!subcommand || subcommand === "--help") {
1232
- printAuthHelp();
1233
- return;
1234
- }
1235
- switch (subcommand) {
1236
- case "reset-password":
1237
- await resetPassword(rawArgs);
1238
- break;
1239
- default:
1240
- console.error(chalk.red(`Unknown auth command: ${subcommand}`));
1241
- console.log("");
1242
- printAuthHelp();
1243
- process.exit(1);
1244
- }
1399
+ if (!subcommand || subcommand === "--help") {
1400
+ printAuthHelp();
1401
+ return;
1402
+ }
1403
+ switch (subcommand) {
1404
+ case "reset-password":
1405
+ await resetPassword(rawArgs);
1406
+ break;
1407
+ default:
1408
+ console.error(chalk.red(`Unknown auth command: ${subcommand}`));
1409
+ console.log("");
1410
+ printAuthHelp();
1411
+ process.exit(1);
1412
+ }
1245
1413
  }
1246
1414
  async function resetPassword(rawArgs) {
1247
- const args = arg(
1248
- {
1249
- "--email": String,
1250
- "--password": String,
1251
- "-e": "--email",
1252
- "-p": "--password"
1253
- },
1254
- {
1255
- argv: rawArgs.slice(4),
1256
- // skip "node rebase auth reset-password"
1257
- permissive: true
1258
- }
1259
- );
1260
- const email = args["--email"] || args._[0];
1261
- const newPassword = args["--password"] || args._[1];
1262
- if (!email) {
1263
- console.error(chalk.red("✗ Email is required."));
1264
- console.log("");
1265
- console.log(chalk.gray(" Usage: rebase auth reset-password <email> [new-password]"));
1266
- console.log(chalk.gray(" rebase auth reset-password --email user@example.com --password NewPass123!"));
1267
- process.exit(1);
1268
- }
1269
- const projectRoot = requireProjectRoot();
1270
- const backendDir = requireBackendDir(projectRoot);
1271
- const tsxBin = resolveTsx(projectRoot);
1272
- if (!tsxBin) {
1273
- console.error(chalk.red("✗ Could not find tsx binary."));
1274
- process.exit(1);
1275
- }
1276
- const envFile = findEnvFile(projectRoot);
1277
- const env = { ...process.env };
1278
- if (envFile) {
1279
- env.DOTENV_CONFIG_PATH = envFile;
1280
- }
1281
- env.REBASE_RESET_EMAIL = email;
1282
- env.REBASE_RESET_PASSWORD = newPassword || "NewPassword123!";
1283
- env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
1284
- const scriptContent = `
1415
+ const args = arg({
1416
+ "--email": String,
1417
+ "--password": String,
1418
+ "-e": "--email",
1419
+ "-p": "--password"
1420
+ }, {
1421
+ argv: rawArgs.slice(4),
1422
+ permissive: true
1423
+ });
1424
+ const email = args["--email"] || args._[0];
1425
+ const newPassword = args["--password"] || args._[1];
1426
+ if (!email) {
1427
+ console.error(chalk.red("✗ Email is required."));
1428
+ console.log("");
1429
+ console.log(chalk.gray(" Usage: rebase auth reset-password <email> [new-password]"));
1430
+ console.log(chalk.gray(" rebase auth reset-password --email user@example.com --password NewPass123!"));
1431
+ process.exit(1);
1432
+ }
1433
+ const projectRoot = requireProjectRoot();
1434
+ const backendDir = requireBackendDir(projectRoot);
1435
+ const tsxBin = resolveTsx(projectRoot);
1436
+ if (!tsxBin) {
1437
+ console.error(chalk.red("✗ Could not find tsx binary."));
1438
+ process.exit(1);
1439
+ }
1440
+ const envFile = findEnvFile(projectRoot);
1441
+ const env = { ...process.env };
1442
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1443
+ env.REBASE_RESET_EMAIL = email;
1444
+ env.REBASE_RESET_PASSWORD = newPassword || "NewPassword123!";
1445
+ env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
1446
+ const scriptContent = `
1285
1447
  import { createPostgresDatabaseConnection } from "@rebasepro/server-postgresql";
1286
1448
  import { hashPassword } from "@rebasepro/server-core";
1287
1449
  import { eq } from "drizzle-orm";
@@ -1326,7 +1488,7 @@ async function resetPassword() {
1326
1488
 
1327
1489
  if (result.length > 0) {
1328
1490
  console.log("✅ Password reset for: " + result[0].email);
1329
- ${!newPassword ? 'console.log(" New password: " + newPassword);' : ""}
1491
+ ${!newPassword ? "console.log(\" New password: \" + newPassword);" : ""}
1330
1492
  } else {
1331
1493
  console.log("✗ User not found: " + email);
1332
1494
  }
@@ -1335,36 +1497,31 @@ async function resetPassword() {
1335
1497
 
1336
1498
  resetPassword().catch(console.error);
1337
1499
  `;
1338
- const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
1339
- fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
1340
- console.log("");
1341
- console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password"));
1342
- console.log("");
1343
- console.log(` ${chalk.gray("Email:")} ${email}`);
1344
- if (newPassword) {
1345
- console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
1346
- }
1347
- console.log("");
1348
- const child = spawn(tsxBin, [tmpScriptPath], {
1349
- cwd: backendDir,
1350
- stdio: "inherit",
1351
- env
1352
- });
1353
- return new Promise((resolve) => {
1354
- child.on("close", (code) => {
1355
- try {
1356
- fs.unlinkSync(tmpScriptPath);
1357
- } catch {
1358
- }
1359
- if (code !== 0) {
1360
- process.exit(code ?? 1);
1361
- }
1362
- resolve();
1363
- });
1364
- });
1500
+ const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
1501
+ fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
1502
+ console.log("");
1503
+ console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password"));
1504
+ console.log("");
1505
+ console.log(` ${chalk.gray("Email:")} ${email}`);
1506
+ if (newPassword) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
1507
+ console.log("");
1508
+ const child = spawn(tsxBin, [tmpScriptPath], {
1509
+ cwd: backendDir,
1510
+ stdio: "inherit",
1511
+ env
1512
+ });
1513
+ return new Promise((resolve) => {
1514
+ child.on("close", (code) => {
1515
+ try {
1516
+ fs.unlinkSync(tmpScriptPath);
1517
+ } catch {}
1518
+ if (code !== 0) process.exit(code ?? 1);
1519
+ resolve();
1520
+ });
1521
+ });
1365
1522
  }
1366
1523
  function printAuthHelp() {
1367
- console.log(`
1524
+ console.log(`
1368
1525
  ${chalk.bold("rebase auth")} — Authentication management commands
1369
1526
 
1370
1527
  ${chalk.green.bold("Usage")}
@@ -1382,139 +1539,324 @@ ${chalk.green.bold("Examples")}
1382
1539
  rebase auth reset-password --email user@example.com --password MyNewPass!
1383
1540
  `);
1384
1541
  }
1542
+ //#endregion
1543
+ //#region src/commands/doctor.ts
1544
+ /**
1545
+ * CLI command: rebase doctor
1546
+ *
1547
+ * Detects three-way schema drift between collection definitions,
1548
+ * the generated Drizzle schema, and the live PostgreSQL database.
1549
+ */
1385
1550
  async function doctorCommand(rawArgs) {
1386
- const projectRoot = requireProjectRoot();
1387
- const backendDir = requireBackendDir(projectRoot);
1388
- const activePlugin = getActiveBackendPlugin(backendDir);
1389
- if (!activePlugin) {
1390
- console.error(chalk.red("✗ Could not detect an active database plugin."));
1391
- console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
1392
- process.exit(1);
1393
- }
1394
- const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
1395
- if (!pluginCli) {
1396
- console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
1397
- process.exit(1);
1398
- }
1399
- const envFile = findEnvFile(projectRoot);
1400
- const env = { ...process.env };
1401
- if (envFile) {
1402
- env.DOTENV_CONFIG_PATH = envFile;
1403
- }
1404
- try {
1405
- const isTs = pluginCli.endsWith(".ts");
1406
- if (isTs) {
1407
- const tsxBin = resolveTsx(projectRoot);
1408
- if (!tsxBin) {
1409
- console.error(chalk.red("✗ Could not find tsx binary."));
1410
- process.exit(1);
1411
- }
1412
- await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
1413
- cwd: backendDir,
1414
- stdio: "inherit",
1415
- env
1416
- });
1417
- } else {
1418
- await execa("node", [pluginCli, ...rawArgs.slice(2)], {
1419
- cwd: backendDir,
1420
- stdio: "inherit",
1421
- env
1422
- });
1423
- }
1424
- } catch {
1425
- process.exit(1);
1426
- }
1551
+ const projectRoot = requireProjectRoot();
1552
+ const backendDir = requireBackendDir(projectRoot);
1553
+ const activePlugin = getActiveBackendPlugin(backendDir);
1554
+ if (!activePlugin) {
1555
+ console.error(chalk.red("✗ Could not detect an active database plugin."));
1556
+ console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
1557
+ process.exit(1);
1558
+ }
1559
+ const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
1560
+ if (!pluginCli) {
1561
+ console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
1562
+ process.exit(1);
1563
+ }
1564
+ const envFile = findEnvFile(projectRoot);
1565
+ const env = { ...process.env };
1566
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
1567
+ try {
1568
+ if (pluginCli.endsWith(".ts")) {
1569
+ const tsxBin = resolveTsx(projectRoot);
1570
+ if (!tsxBin) {
1571
+ console.error(chalk.red("✗ Could not find tsx binary."));
1572
+ process.exit(1);
1573
+ }
1574
+ await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
1575
+ cwd: backendDir,
1576
+ stdio: "inherit",
1577
+ env
1578
+ });
1579
+ } else await execa("node", [pluginCli, ...rawArgs.slice(2)], {
1580
+ cwd: backendDir,
1581
+ stdio: "inherit",
1582
+ env
1583
+ });
1584
+ } catch {
1585
+ process.exit(1);
1586
+ }
1587
+ }
1588
+ //#endregion
1589
+ //#region src/commands/skills.ts
1590
+ var __filename$1 = fileURLToPath(import.meta.url);
1591
+ var __dirname$1 = path.dirname(__filename$1);
1592
+ /** Supported agent environments and their target directories. */
1593
+ var AGENTS = {
1594
+ cursor: {
1595
+ label: "Cursor",
1596
+ detectDir: ".cursor",
1597
+ targetDir: ".cursor/rules",
1598
+ /** Cursor uses .mdc files (Markdown with Context). */
1599
+ transformFile: (skillName, content) => ({
1600
+ fileName: `${skillName}.mdc`,
1601
+ content
1602
+ })
1603
+ },
1604
+ claude: {
1605
+ label: "Claude Code",
1606
+ detectDir: ".claude",
1607
+ targetDir: ".claude/skills",
1608
+ /** Claude Code uses the standard SKILL.md format in subdirectories. */
1609
+ transformFile: (skillName, content) => ({
1610
+ fileName: path.join(skillName, "SKILL.md"),
1611
+ content
1612
+ })
1613
+ },
1614
+ windsurf: {
1615
+ label: "Windsurf",
1616
+ detectDir: ".windsurf",
1617
+ targetDir: ".windsurf/rules",
1618
+ /** Windsurf uses plain .md files. */
1619
+ transformFile: (skillName, content) => ({
1620
+ fileName: `${skillName}.md`,
1621
+ content
1622
+ })
1623
+ },
1624
+ gemini: {
1625
+ label: "Gemini CLI / Antigravity",
1626
+ detectDir: ".agents",
1627
+ targetDir: ".agents/skills",
1628
+ /** Gemini uses the standard SKILL.md format in subdirectories. */
1629
+ transformFile: (skillName, content) => ({
1630
+ fileName: path.join(skillName, "SKILL.md"),
1631
+ content
1632
+ })
1633
+ }
1634
+ };
1635
+ function findParentDir(currentDir, targetName) {
1636
+ const root = path.parse(currentDir).root;
1637
+ while (currentDir && currentDir !== root) {
1638
+ if (path.basename(currentDir) === targetName) return currentDir;
1639
+ currentDir = path.dirname(currentDir);
1640
+ }
1641
+ return null;
1642
+ }
1643
+ /** Resolve the path to the bundled skills directory. */
1644
+ function getSkillsSourceDir() {
1645
+ const cliRoot = findParentDir(__dirname$1, "cli");
1646
+ if (cliRoot) {
1647
+ const dir = path.join(cliRoot, "skills");
1648
+ if (fs.existsSync(dir)) return dir;
1649
+ }
1650
+ const distSkills = path.resolve(__dirname$1, "../../skills");
1651
+ if (fs.existsSync(distSkills)) return distSkills;
1652
+ throw new Error("Could not find bundled skills directory. Make sure the CLI was built with `pnpm build`.");
1653
+ }
1654
+ /** Read all skill directories and return their names + content. */
1655
+ function loadSkills(skillsDir) {
1656
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
1657
+ const skills = [];
1658
+ for (const entry of entries) {
1659
+ if (!entry.isDirectory()) continue;
1660
+ const skillMdPath = path.join(skillsDir, entry.name, "SKILL.md");
1661
+ if (!fs.existsSync(skillMdPath)) continue;
1662
+ skills.push({
1663
+ name: entry.name,
1664
+ content: fs.readFileSync(skillMdPath, "utf-8")
1665
+ });
1666
+ }
1667
+ return skills;
1668
+ }
1669
+ /** Detect which agent environments already exist in the project. */
1670
+ function detectAgents(projectDir) {
1671
+ const detected = [];
1672
+ for (const [key, agent] of Object.entries(AGENTS)) if (fs.existsSync(path.join(projectDir, agent.detectDir))) detected.push(key);
1673
+ return detected;
1674
+ }
1675
+ /** Install skills for a specific agent into the project directory. */
1676
+ function installForAgent(agentKey, skills, projectDir) {
1677
+ const agent = AGENTS[agentKey];
1678
+ const targetBase = path.join(projectDir, agent.targetDir);
1679
+ fs.mkdirSync(targetBase, { recursive: true });
1680
+ let count = 0;
1681
+ for (const skill of skills) {
1682
+ const { fileName, content } = agent.transformFile(skill.name, skill.content);
1683
+ const targetPath = path.join(targetBase, fileName);
1684
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
1685
+ fs.writeFileSync(targetPath, content, "utf-8");
1686
+ count++;
1687
+ }
1688
+ return count;
1689
+ }
1690
+ async function skillsCommand(subcommand, _args) {
1691
+ switch (subcommand) {
1692
+ case "install":
1693
+ await skillsInstall();
1694
+ break;
1695
+ case "--help":
1696
+ case void 0:
1697
+ printSkillsHelp();
1698
+ break;
1699
+ default:
1700
+ console.log(chalk.red(`Unknown skills subcommand: ${subcommand}`));
1701
+ console.log("");
1702
+ printSkillsHelp();
1703
+ }
1427
1704
  }
1428
- const __filename$1 = fileURLToPath(import.meta.url);
1429
- const __dirname$1 = path.dirname(__filename$1);
1705
+ async function skillsInstall() {
1706
+ const projectDir = process.cwd();
1707
+ let skillsDir;
1708
+ try {
1709
+ skillsDir = getSkillsSourceDir();
1710
+ } catch (err) {
1711
+ console.error(`${chalk.red.bold("ERROR")} ${err instanceof Error ? err.message : String(err)}`);
1712
+ process.exit(1);
1713
+ }
1714
+ const skills = loadSkills(skillsDir);
1715
+ if (skills.length === 0) {
1716
+ console.error(`${chalk.red.bold("ERROR")} No skills found in ${skillsDir}`);
1717
+ process.exit(1);
1718
+ }
1719
+ let agents = detectAgents(projectDir);
1720
+ if (agents.length === 0) {
1721
+ const choices = Object.entries(AGENTS).map(([key, agent]) => ({
1722
+ name: agent.label,
1723
+ value: key,
1724
+ checked: false
1725
+ }));
1726
+ const { selectedAgents } = await inquirer.prompt([{
1727
+ type: "checkbox",
1728
+ name: "selectedAgents",
1729
+ message: "No AI agent configuration detected. Which agents do you use?",
1730
+ choices,
1731
+ validate: (input) => {
1732
+ if (input.length === 0) return "Please select at least one agent.";
1733
+ return true;
1734
+ }
1735
+ }]);
1736
+ agents = selectedAgents;
1737
+ }
1738
+ console.log("");
1739
+ console.log(chalk.gray(` Found ${chalk.white(skills.length)} Rebase skills`));
1740
+ console.log("");
1741
+ for (const agentKey of agents) {
1742
+ const agent = AGENTS[agentKey];
1743
+ const count = installForAgent(agentKey, skills, projectDir);
1744
+ console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} — ${count} skills installed to ${chalk.gray(agent.targetDir)}`);
1745
+ }
1746
+ console.log("");
1747
+ console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
1748
+ console.log(chalk.gray(" Re-run this command anytime to update to the latest skills."));
1749
+ console.log("");
1750
+ }
1751
+ function printSkillsHelp() {
1752
+ console.log(`
1753
+ ${chalk.bold("rebase skills")} — Manage AI agent skills
1754
+
1755
+ ${chalk.green.bold("Usage")}
1756
+ rebase skills ${chalk.blue("<subcommand>")}
1757
+
1758
+ ${chalk.green.bold("Subcommands")}
1759
+ ${chalk.blue.bold("install")} Install Rebase agent skills for your AI coding assistant
1760
+ Supports: Cursor, Claude Code, Windsurf, Gemini CLI, Antigravity
1761
+
1762
+ ${chalk.green.bold("Examples")}
1763
+ ${chalk.cyan("rebase skills install")}
1764
+ `);
1765
+ }
1766
+ //#endregion
1767
+ //#region src/cli.ts
1768
+ var __filename = fileURLToPath(import.meta.url);
1769
+ var __dirname = path.dirname(__filename);
1430
1770
  function getVersion() {
1431
- try {
1432
- const pkgPath = path.resolve(__dirname$1, "../package.json");
1433
- if (fs.existsSync(pkgPath)) {
1434
- return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
1435
- }
1436
- } catch {
1437
- }
1438
- return "unknown";
1771
+ try {
1772
+ const pkgPath = path.resolve(__dirname, "../package.json");
1773
+ if (fs.existsSync(pkgPath)) return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
1774
+ } catch {}
1775
+ return "unknown";
1439
1776
  }
1440
1777
  async function entry(args) {
1441
- const parsedArgs = arg(
1442
- {
1443
- "--version": Boolean,
1444
- "--help": Boolean,
1445
- "-v": "--version",
1446
- "-h": "--help"
1447
- },
1448
- {
1449
- argv: args.slice(2),
1450
- permissive: true
1451
- }
1452
- );
1453
- if (parsedArgs["--version"]) {
1454
- console.log(getVersion());
1455
- return;
1456
- }
1457
- const command = parsedArgs._[0];
1458
- const subcommand = parsedArgs._[1];
1459
- const namespacedCommands = ["schema", "db", "dev", "build", "start", "auth", "doctor"];
1460
- if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
1461
- printHelp();
1462
- return;
1463
- }
1464
- const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
1465
- switch (command) {
1466
- case "init":
1467
- await createRebaseApp(args);
1468
- break;
1469
- case "generate-sdk": {
1470
- const sdkArgs = arg(
1471
- {
1472
- "--collections-dir": String,
1473
- "--output": String,
1474
- "-c": "--collections-dir",
1475
- "-o": "--output"
1476
- },
1477
- {
1478
- argv: args.slice(3),
1479
- permissive: true
1480
- }
1481
- );
1482
- await generateSdkCommand({
1483
- collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
1484
- output: sdkArgs["--output"] || "./generated/sdk",
1485
- cwd: process.cwd()
1486
- });
1487
- break;
1488
- }
1489
- case "schema":
1490
- await schemaCommand(effectiveSubcommand, args);
1491
- break;
1492
- case "db":
1493
- await dbCommand(effectiveSubcommand, args);
1494
- break;
1495
- case "dev":
1496
- await devCommand(args);
1497
- break;
1498
- case "build":
1499
- await buildCommand();
1500
- break;
1501
- case "start":
1502
- await startCommand();
1503
- break;
1504
- case "auth":
1505
- await authCommand(effectiveSubcommand, args);
1506
- break;
1507
- case "doctor":
1508
- await doctorCommand(args);
1509
- break;
1510
- default:
1511
- console.log(chalk.red(`Unknown command: ${command}`));
1512
- console.log("");
1513
- printHelp();
1514
- }
1778
+ const parsedArgs = arg({
1779
+ "--version": Boolean,
1780
+ "--help": Boolean,
1781
+ "-v": "--version",
1782
+ "-h": "--help"
1783
+ }, {
1784
+ argv: args.slice(2),
1785
+ permissive: true
1786
+ });
1787
+ if (parsedArgs["--version"]) {
1788
+ console.log(getVersion());
1789
+ return;
1790
+ }
1791
+ const command = parsedArgs._[0];
1792
+ const subcommand = parsedArgs._[1];
1793
+ if (!command || parsedArgs["--help"] && ![
1794
+ "schema",
1795
+ "db",
1796
+ "dev",
1797
+ "build",
1798
+ "start",
1799
+ "auth",
1800
+ "doctor",
1801
+ "skills"
1802
+ ].includes(command)) {
1803
+ printHelp();
1804
+ return;
1805
+ }
1806
+ const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
1807
+ switch (command) {
1808
+ case "init":
1809
+ await createRebaseApp(args);
1810
+ break;
1811
+ case "generate-sdk": {
1812
+ const sdkArgs = arg({
1813
+ "--collections-dir": String,
1814
+ "--output": String,
1815
+ "-c": "--collections-dir",
1816
+ "-o": "--output"
1817
+ }, {
1818
+ argv: args.slice(3),
1819
+ permissive: true
1820
+ });
1821
+ await generateSdkCommand({
1822
+ collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
1823
+ output: sdkArgs["--output"] || "./generated/sdk",
1824
+ cwd: process.cwd()
1825
+ });
1826
+ break;
1827
+ }
1828
+ case "schema":
1829
+ await schemaCommand(effectiveSubcommand, args);
1830
+ break;
1831
+ case "db":
1832
+ await dbCommand(effectiveSubcommand, args);
1833
+ break;
1834
+ case "dev":
1835
+ await devCommand(args);
1836
+ break;
1837
+ case "build":
1838
+ await buildCommand();
1839
+ break;
1840
+ case "start":
1841
+ await startCommand();
1842
+ break;
1843
+ case "auth":
1844
+ await authCommand(effectiveSubcommand, args);
1845
+ break;
1846
+ case "doctor":
1847
+ await doctorCommand(args);
1848
+ break;
1849
+ case "skills":
1850
+ await skillsCommand(effectiveSubcommand, args);
1851
+ break;
1852
+ default:
1853
+ console.log(chalk.red(`Unknown command: ${command}`));
1854
+ console.log("");
1855
+ printHelp();
1856
+ }
1515
1857
  }
1516
1858
  function printHelp() {
1517
- console.log(`
1859
+ console.log(`
1518
1860
  ${chalk.bold("Rebase CLI")} — Developer tools for Rebase projects
1519
1861
 
1520
1862
  ${chalk.green.bold("Usage")}
@@ -1548,6 +1890,9 @@ ${chalk.green.bold("Auth")}
1548
1890
  ${chalk.green.bold("Diagnostics")}
1549
1891
  ${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
1550
1892
 
1893
+ ${chalk.green.bold("AI Agent Skills")}
1894
+ ${chalk.blue.bold("skills install")} Install Rebase agent skills for your AI coding assistant
1895
+
1551
1896
  ${chalk.green.bold("Options")}
1552
1897
  ${chalk.blue("--version, -v")} Show version number
1553
1898
  ${chalk.blue("--help, -h")} Show this help message
@@ -1555,29 +1900,7 @@ ${chalk.green.bold("Options")}
1555
1900
  ${chalk.gray("Documentation: https://rebase.pro/docs")}
1556
1901
  `);
1557
1902
  }
1558
- export {
1559
- authCommand,
1560
- buildCommand,
1561
- configureEnvFile,
1562
- createRebaseApp,
1563
- dbCommand,
1564
- detectPackageManager,
1565
- devCommand,
1566
- doctorCommand,
1567
- entry,
1568
- findBackendDir,
1569
- findEnvFile,
1570
- findFrontendDir,
1571
- findProjectRoot,
1572
- generateSdkCommand,
1573
- getActiveBackendPlugin,
1574
- getPMCommands,
1575
- requireBackendDir,
1576
- requireProjectRoot,
1577
- resolveLocalBin,
1578
- resolvePluginCliScript,
1579
- resolveTsx,
1580
- schemaCommand,
1581
- startCommand
1582
- };
1583
- //# sourceMappingURL=index.es.js.map
1903
+ //#endregion
1904
+ export { authCommand, buildCommand, buildInitQuestions, configureEnvFile, createRebaseApp, dbCommand, detectPackageManager, devCommand, doctorCommand, entry, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, generateSdkCommand, getActiveBackendPlugin, getPMCommands, requireBackendDir, requireProjectRoot, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand };
1905
+
1906
+ //# sourceMappingURL=index.es.js.map