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