@rebasepro/cli 0.0.1-canary.2 → 0.0.1-canary.3263433
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/dist/commands/auth.d.ts +1 -0
- package/dist/commands/cli.test.d.ts +1 -0
- package/dist/commands/db.d.ts +1 -0
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.test.d.ts +1 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/generate_sdk.d.ts +18 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/schema.d.ts +1 -0
- package/dist/index.cjs +1064 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.es.js +1065 -50
- package/dist/index.es.js.map +1 -1
- package/dist/utils/project.d.ts +45 -0
- package/dist/utils/project.test.d.ts +1 -0
- package/package.json +19 -11
- package/templates/template/.dockerignore +28 -0
- package/templates/template/.env.example +96 -0
- package/templates/template/README.md +84 -17
- package/templates/template/backend/Dockerfile +55 -10
- package/templates/template/backend/drizzle.config.ts +24 -4
- package/templates/template/backend/functions/hello.ts +52 -0
- package/templates/template/backend/package.json +30 -34
- package/templates/template/backend/src/env.ts +52 -0
- package/templates/template/backend/src/index.ts +113 -99
- package/templates/template/backend/tsconfig.json +1 -1
- package/templates/template/config/collections/authors.ts +45 -0
- package/templates/template/config/collections/index.ts +5 -0
- package/templates/template/config/collections/posts.ts +91 -0
- package/templates/template/config/collections/tags.ts +27 -0
- package/templates/template/config/package.json +28 -0
- package/templates/template/docker-compose.yml +49 -13
- package/templates/template/frontend/Dockerfile +36 -14
- package/templates/template/frontend/nginx.conf +40 -0
- package/templates/template/frontend/package.json +9 -10
- package/templates/template/frontend/src/App.tsx +24 -110
- package/templates/template/frontend/src/index.css +15 -1
- package/templates/template/frontend/src/main.tsx +2 -2
- package/templates/template/frontend/vite.config.ts +33 -3
- package/templates/template/package.json +12 -16
- package/templates/template/pnpm-workspace.yaml +4 -0
- package/templates/template/scripts/example.ts +91 -0
- package/templates/template/.env.template +0 -31
- package/templates/template/backend/scripts/db-generate.ts +0 -60
- package/templates/template/shared/collections/index.ts +0 -3
- package/templates/template/shared/collections/posts.ts +0 -53
- package/templates/template/shared/package.json +0 -28
- /package/templates/template/{shared → config}/index.ts +0 -0
- /package/templates/template/{shared → config}/tsconfig.json +0 -0
package/dist/index.es.js
CHANGED
|
@@ -8,6 +8,8 @@ import execa from "execa";
|
|
|
8
8
|
import ncp from "ncp";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import crypto from "crypto";
|
|
11
|
+
import { generateSDK } from "@rebasepro/sdk-generator";
|
|
12
|
+
import { execSync, spawn } from "child_process";
|
|
11
13
|
import * as os from "os";
|
|
12
14
|
const access = promisify(fs.access);
|
|
13
15
|
const copy = promisify(ncp);
|
|
@@ -35,7 +37,9 @@ async function promptForOptions(rawArgs) {
|
|
|
35
37
|
const args = arg(
|
|
36
38
|
{
|
|
37
39
|
"--git": Boolean,
|
|
38
|
-
"
|
|
40
|
+
"--install": Boolean,
|
|
41
|
+
"-g": "--git",
|
|
42
|
+
"-i": "--install"
|
|
39
43
|
},
|
|
40
44
|
{
|
|
41
45
|
argv: rawArgs.slice(3),
|
|
@@ -68,15 +72,39 @@ async function promptForOptions(rawArgs) {
|
|
|
68
72
|
default: true
|
|
69
73
|
});
|
|
70
74
|
}
|
|
75
|
+
if (!args["--install"]) {
|
|
76
|
+
questions.push({
|
|
77
|
+
type: "confirm",
|
|
78
|
+
name: "installDeps",
|
|
79
|
+
message: "Install dependencies with pnpm?",
|
|
80
|
+
default: true
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
questions.push({
|
|
84
|
+
type: "input",
|
|
85
|
+
name: "databaseUrl",
|
|
86
|
+
message: "Enter your PostgreSQL database connection string (leave blank to use a local default):",
|
|
87
|
+
default: ""
|
|
88
|
+
});
|
|
89
|
+
questions.push({
|
|
90
|
+
type: "confirm",
|
|
91
|
+
name: "introspect",
|
|
92
|
+
message: "Would you like to introspect this database to automatically generate collections?",
|
|
93
|
+
default: true,
|
|
94
|
+
when: (answers2) => !!answers2.databaseUrl?.trim()
|
|
95
|
+
});
|
|
71
96
|
const answers = await inquirer.prompt(questions);
|
|
72
|
-
const
|
|
73
|
-
const
|
|
97
|
+
const targetDirectory = path.resolve(process.cwd(), nameArg || answers.projectName);
|
|
98
|
+
const projectName = path.basename(targetDirectory);
|
|
74
99
|
const templateDirectory = path.resolve(cliRoot, "templates", "template");
|
|
75
100
|
return {
|
|
76
101
|
projectName,
|
|
77
102
|
git: args["--git"] || answers.git || false,
|
|
103
|
+
installDeps: args["--install"] || answers.installDeps || false,
|
|
78
104
|
targetDirectory,
|
|
79
|
-
templateDirectory
|
|
105
|
+
templateDirectory,
|
|
106
|
+
databaseUrl: answers.databaseUrl?.trim() || void 0,
|
|
107
|
+
introspect: answers.introspect || false
|
|
80
108
|
};
|
|
81
109
|
}
|
|
82
110
|
async function createProject(options) {
|
|
@@ -95,36 +123,21 @@ async function createProject(options) {
|
|
|
95
123
|
process.exit(1);
|
|
96
124
|
}
|
|
97
125
|
console.log(chalk.gray(" Copying project files..."));
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
fs.renameSync(envTemplatePath, envPath);
|
|
111
|
-
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
112
|
-
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
113
|
-
let envContent = fs.readFileSync(envPath, "utf-8");
|
|
114
|
-
envContent = envContent.replace(
|
|
115
|
-
"postgresql://rebase:password@localhost:5432/rebase",
|
|
116
|
-
`postgresql://rebase:${dbPassword}@localhost:5432/rebase`
|
|
117
|
-
);
|
|
118
|
-
envContent = envContent.replace(
|
|
119
|
-
"change-this-to-a-secure-random-string",
|
|
120
|
-
jwtSecret
|
|
121
|
-
);
|
|
122
|
-
envContent += `
|
|
123
|
-
# Docker Compose Database Password
|
|
124
|
-
POSTGRES_PASSWORD=${dbPassword}
|
|
125
|
-
`;
|
|
126
|
-
fs.writeFileSync(envPath, envContent, "utf-8");
|
|
126
|
+
try {
|
|
127
|
+
await copy(options.templateDirectory, options.targetDirectory, {
|
|
128
|
+
clobber: false,
|
|
129
|
+
dot: true,
|
|
130
|
+
filter: (source) => {
|
|
131
|
+
const basename = path.basename(source);
|
|
132
|
+
return basename !== "node_modules" && basename !== ".DS_Store";
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error(`${chalk.red.bold("ERROR")} Failed to copy template files: ${err instanceof Error ? err.message : String(err)}`);
|
|
137
|
+
process.exit(1);
|
|
127
138
|
}
|
|
139
|
+
await replacePlaceholders(options);
|
|
140
|
+
configureEnvFile(options.targetDirectory, options.databaseUrl);
|
|
128
141
|
if (options.git) {
|
|
129
142
|
console.log(chalk.gray(" Initializing git repository..."));
|
|
130
143
|
try {
|
|
@@ -133,23 +146,65 @@ POSTGRES_PASSWORD=${dbPassword}
|
|
|
133
146
|
console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
|
|
134
147
|
}
|
|
135
148
|
}
|
|
149
|
+
if (options.installDeps) {
|
|
150
|
+
console.log("");
|
|
151
|
+
console.log(chalk.gray(" Installing dependencies with pnpm..."));
|
|
152
|
+
console.log("");
|
|
153
|
+
try {
|
|
154
|
+
await execa("pnpm", ["install"], {
|
|
155
|
+
cwd: options.targetDirectory,
|
|
156
|
+
stdio: "inherit"
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
console.warn(chalk.yellow(" Warning: Failed to install dependencies. You may need to run `pnpm install` manually."));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (options.introspect) {
|
|
163
|
+
console.log("");
|
|
164
|
+
if (options.installDeps) {
|
|
165
|
+
console.log(chalk.gray(" Introspecting database and generating collections..."));
|
|
166
|
+
console.log("");
|
|
167
|
+
try {
|
|
168
|
+
await execa("pnpm", ["exec", "rebase", "schema", "introspect", "--force"], {
|
|
169
|
+
cwd: options.targetDirectory,
|
|
170
|
+
stdio: "inherit"
|
|
171
|
+
});
|
|
172
|
+
console.log(chalk.green(" Database successfully introspected!"));
|
|
173
|
+
} catch {
|
|
174
|
+
console.warn(chalk.yellow(" Warning: Failed to introspect database automatically."));
|
|
175
|
+
console.warn(chalk.yellow(" You can run `pnpm exec rebase schema introspect` manually after setup."));
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
console.warn(chalk.yellow(" Skipping introspection because dependencies were not installed."));
|
|
179
|
+
console.warn(chalk.yellow(" Run `pnpm install` then `pnpm exec rebase schema introspect` manually."));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
136
182
|
console.log("");
|
|
137
183
|
console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
|
|
138
184
|
console.log("");
|
|
139
185
|
console.log(chalk.bold("Next steps:"));
|
|
140
186
|
console.log("");
|
|
141
187
|
console.log(` ${chalk.cyan("cd")} ${options.projectName}`);
|
|
142
|
-
|
|
188
|
+
if (!options.installDeps) {
|
|
189
|
+
console.log(` ${chalk.cyan("pnpm install")}`);
|
|
190
|
+
}
|
|
143
191
|
console.log("");
|
|
144
|
-
|
|
145
|
-
|
|
192
|
+
if (options.databaseUrl) {
|
|
193
|
+
console.log(chalk.gray(" # Your database is configured! Start the dev server:"));
|
|
194
|
+
} else {
|
|
195
|
+
console.log(chalk.gray(" # A local database configuration has been generated in .env."));
|
|
196
|
+
console.log(chalk.gray(" # If using the included docker-compose.yml, start it with:"));
|
|
197
|
+
console.log(` ${chalk.cyan("docker compose up -d")}`);
|
|
198
|
+
console.log("");
|
|
199
|
+
console.log(chalk.gray(" # Then start the dev server:"));
|
|
200
|
+
}
|
|
146
201
|
console.log("");
|
|
147
202
|
console.log(` ${chalk.cyan("pnpm dev")}`);
|
|
148
203
|
console.log("");
|
|
149
|
-
console.log(chalk.gray("This starts both the backend (
|
|
204
|
+
console.log(chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
|
|
150
205
|
console.log("");
|
|
151
206
|
console.log(chalk.gray("Docs: https://rebase.pro/docs"));
|
|
152
|
-
console.log(chalk.gray("GitHub: https://github.com/
|
|
207
|
+
console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
|
|
153
208
|
console.log("");
|
|
154
209
|
}
|
|
155
210
|
async function replacePlaceholders(options) {
|
|
@@ -157,17 +212,899 @@ async function replacePlaceholders(options) {
|
|
|
157
212
|
"package.json",
|
|
158
213
|
"frontend/package.json",
|
|
159
214
|
"backend/package.json",
|
|
160
|
-
"
|
|
215
|
+
"config/package.json",
|
|
161
216
|
"frontend/index.html"
|
|
162
217
|
];
|
|
218
|
+
const packageJsonPath = path.resolve(cliRoot, "package.json");
|
|
219
|
+
let cliVersion = "latest";
|
|
220
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
221
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
222
|
+
cliVersion = pkg.version || "latest";
|
|
223
|
+
}
|
|
224
|
+
const versionCache = /* @__PURE__ */ new Map();
|
|
225
|
+
const getPackageVersion = async (pkgName) => {
|
|
226
|
+
if (versionCache.has(pkgName)) return versionCache.get(pkgName);
|
|
227
|
+
let versionToUse = cliVersion;
|
|
228
|
+
try {
|
|
229
|
+
const { stdout } = await execa("pnpm", ["view", `${pkgName}@${cliVersion}`, "version"]);
|
|
230
|
+
if (!stdout.trim()) throw new Error("Not found");
|
|
231
|
+
versionToUse = stdout.trim();
|
|
232
|
+
} catch {
|
|
233
|
+
try {
|
|
234
|
+
const tag = cliVersion.includes("canary") ? "canary" : "latest";
|
|
235
|
+
const { stdout } = await execa("pnpm", ["view", `${pkgName}@${tag}`, "version"]);
|
|
236
|
+
if (!stdout.trim()) throw new Error("Not found");
|
|
237
|
+
versionToUse = stdout.trim();
|
|
238
|
+
} catch {
|
|
239
|
+
try {
|
|
240
|
+
const { stdout } = await execa("pnpm", ["view", pkgName, "version"]);
|
|
241
|
+
versionToUse = stdout.trim() || "latest";
|
|
242
|
+
} catch {
|
|
243
|
+
versionToUse = "latest";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
versionCache.set(pkgName, versionToUse);
|
|
248
|
+
return versionToUse;
|
|
249
|
+
};
|
|
250
|
+
const allPackages = /* @__PURE__ */ new Set();
|
|
251
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
163
252
|
for (const file of filesToProcess) {
|
|
164
253
|
const fullPath = path.resolve(options.targetDirectory, file);
|
|
165
254
|
if (!fs.existsSync(fullPath)) continue;
|
|
166
|
-
|
|
167
|
-
|
|
255
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
256
|
+
fileContents.set(fullPath, content);
|
|
257
|
+
const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
|
|
258
|
+
for (const match of matches) {
|
|
259
|
+
allPackages.add(match[1]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
console.log(chalk.gray(" Resolving package versions..."));
|
|
263
|
+
await Promise.all(Array.from(allPackages).map(getPackageVersion));
|
|
264
|
+
for (const [fullPath, originalContent] of fileContents.entries()) {
|
|
265
|
+
let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
|
|
266
|
+
const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
|
|
267
|
+
for (const match of matches) {
|
|
268
|
+
const pkgName = match[1];
|
|
269
|
+
const resolvedVersion = versionCache.get(pkgName) || "latest";
|
|
270
|
+
content = content.replace(new RegExp(`"${pkgName}":\\s*"workspace:\\*"`, "g"), `"${pkgName}": "${resolvedVersion}"`);
|
|
271
|
+
}
|
|
168
272
|
fs.writeFileSync(fullPath, content, "utf-8");
|
|
169
273
|
}
|
|
170
274
|
}
|
|
275
|
+
function configureEnvFile(targetDirectory, databaseUrl) {
|
|
276
|
+
const envExamplePath = path.join(targetDirectory, ".env.example");
|
|
277
|
+
const envPath = path.join(targetDirectory, ".env");
|
|
278
|
+
if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
|
|
279
|
+
fs.copyFileSync(envExamplePath, envPath);
|
|
280
|
+
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
281
|
+
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
282
|
+
let envContent = fs.readFileSync(envPath, "utf-8");
|
|
283
|
+
envContent = envContent.replace(
|
|
284
|
+
/^JWT_SECRET=.*$/m,
|
|
285
|
+
`JWT_SECRET=${jwtSecret}`
|
|
286
|
+
);
|
|
287
|
+
if (databaseUrl) {
|
|
288
|
+
envContent = envContent.replace(
|
|
289
|
+
/^DATABASE_URL=.*$/m,
|
|
290
|
+
`DATABASE_URL=${databaseUrl}`
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
envContent = envContent.replace(
|
|
294
|
+
/^DATABASE_URL=.*$/m,
|
|
295
|
+
`DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:5432/rebase`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
fs.writeFileSync(envPath, envContent, "utf-8");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function loadCollections(collectionsDir) {
|
|
302
|
+
const absDir = path.resolve(collectionsDir);
|
|
303
|
+
if (!fs.existsSync(absDir)) {
|
|
304
|
+
throw new Error(`Collections directory not found: ${absDir}`);
|
|
305
|
+
}
|
|
306
|
+
let jiti;
|
|
307
|
+
try {
|
|
308
|
+
const jitiModule = await import("jiti");
|
|
309
|
+
jiti = jitiModule.default || jitiModule;
|
|
310
|
+
} catch {
|
|
311
|
+
throw new Error(
|
|
312
|
+
"Could not load 'jiti'. Install it with: pnpm add -D jiti\njiti is required to dynamically import TypeScript collection definitions."
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const jitiInstance = jiti(absDir, {
|
|
316
|
+
interopDefault: true,
|
|
317
|
+
esmResolve: true
|
|
318
|
+
});
|
|
319
|
+
const indexCandidates = ["index.ts", "index.js", "index.mjs"];
|
|
320
|
+
let indexPath = null;
|
|
321
|
+
for (const candidate of indexCandidates) {
|
|
322
|
+
const p = path.join(absDir, candidate);
|
|
323
|
+
if (fs.existsSync(p)) {
|
|
324
|
+
indexPath = p;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!indexPath) {
|
|
329
|
+
console.log(chalk.yellow(" No index file found, scanning individual collection files..."));
|
|
330
|
+
const collections = [];
|
|
331
|
+
const files = fs.readdirSync(absDir).filter(
|
|
332
|
+
(f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith(".")
|
|
333
|
+
);
|
|
334
|
+
for (const file of files) {
|
|
335
|
+
try {
|
|
336
|
+
const mod2 = jitiInstance(path.join(absDir, file));
|
|
337
|
+
const exported2 = mod2.default || mod2;
|
|
338
|
+
if (exported2 && typeof exported2 === "object" && "slug" in exported2) {
|
|
339
|
+
collections.push(exported2);
|
|
340
|
+
} else if (Array.isArray(exported2)) {
|
|
341
|
+
collections.push(...exported2);
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
console.warn(chalk.yellow(` ⚠ Skipping ${file}: ${err.message}`));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return collections;
|
|
348
|
+
}
|
|
349
|
+
const mod = jitiInstance(indexPath);
|
|
350
|
+
const exported = mod.default || mod;
|
|
351
|
+
if (Array.isArray(exported)) {
|
|
352
|
+
return exported;
|
|
353
|
+
} else if (typeof exported === "object" && exported !== null) {
|
|
354
|
+
if ("collections" in exported && Array.isArray(exported.collections)) {
|
|
355
|
+
return exported.collections;
|
|
356
|
+
}
|
|
357
|
+
const collections = [];
|
|
358
|
+
for (const value of Object.values(exported)) {
|
|
359
|
+
if (value && typeof value === "object" && "slug" in value) {
|
|
360
|
+
collections.push(value);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (collections.length > 0) return collections;
|
|
364
|
+
}
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Could not extract collections from ${indexPath}.
|
|
367
|
+
Expected a default export of EntityCollection[] or an object with named collection exports.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
function writeFiles(outputDir, files) {
|
|
371
|
+
const absOutput = path.resolve(outputDir);
|
|
372
|
+
fs.mkdirSync(absOutput, { recursive: true });
|
|
373
|
+
for (const file of files) {
|
|
374
|
+
const filePath = path.join(absOutput, file.path);
|
|
375
|
+
const dir = path.dirname(filePath);
|
|
376
|
+
if (!fs.existsSync(dir)) {
|
|
377
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
378
|
+
}
|
|
379
|
+
fs.writeFileSync(filePath, file.content, "utf-8");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function generateSdkCommand(args) {
|
|
383
|
+
const { collectionsDir, output, cwd } = args;
|
|
384
|
+
const resolvedCollectionsDir = path.isAbsolute(collectionsDir) ? collectionsDir : path.join(cwd, collectionsDir);
|
|
385
|
+
const resolvedOutput = path.isAbsolute(output) ? output : path.join(cwd, output);
|
|
386
|
+
console.log("");
|
|
387
|
+
console.log(chalk.bold(" 🔧 Rebase SDK Generator"));
|
|
388
|
+
console.log("");
|
|
389
|
+
console.log(` ${chalk.gray("Collections:")} ${resolvedCollectionsDir}`);
|
|
390
|
+
console.log(` ${chalk.gray("Output:")} ${resolvedOutput}`);
|
|
391
|
+
console.log("");
|
|
392
|
+
console.log(chalk.cyan(" → Loading collection definitions..."));
|
|
393
|
+
const collections = await loadCollections(resolvedCollectionsDir);
|
|
394
|
+
if (collections.length === 0) {
|
|
395
|
+
console.log(chalk.red(" ✗ No collections found. Nothing to generate."));
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
|
|
399
|
+
console.log("");
|
|
400
|
+
console.log(chalk.cyan(" → Generating SDK files..."));
|
|
401
|
+
const files = generateSDK(collections);
|
|
402
|
+
console.log(chalk.green(` ✓ Generated ${files.length} file(s)`));
|
|
403
|
+
console.log(chalk.cyan(` → Writing to ${resolvedOutput}...`));
|
|
404
|
+
writeFiles(resolvedOutput, files);
|
|
405
|
+
console.log("");
|
|
406
|
+
console.log(chalk.green.bold(" ✓ SDK generated successfully!"));
|
|
407
|
+
console.log("");
|
|
408
|
+
console.log(chalk.gray(" Usage:"));
|
|
409
|
+
console.log(chalk.gray(" import { createRebaseClient } from '@rebasepro/client';"));
|
|
410
|
+
console.log(chalk.gray(` import type { Database } from './${path.relative(cwd, path.join(resolvedOutput, "database.types"))}';`));
|
|
411
|
+
console.log("");
|
|
412
|
+
console.log(chalk.gray(" const rebase = createRebaseClient<Database>({"));
|
|
413
|
+
console.log(chalk.gray(" baseUrl: 'http://localhost:3001',"));
|
|
414
|
+
console.log(chalk.gray(" // token: 'your-jwt-token',"));
|
|
415
|
+
console.log(chalk.gray(" });"));
|
|
416
|
+
console.log("");
|
|
417
|
+
console.log(chalk.gray(` const { data } = await rebase.collection('${collections[0]?.slug || "my_collection"}').find();`));
|
|
418
|
+
console.log("");
|
|
419
|
+
}
|
|
420
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
421
|
+
let dir = path.resolve(startDir);
|
|
422
|
+
const root = path.parse(dir).root;
|
|
423
|
+
while (dir !== root) {
|
|
424
|
+
const pkgPath = path.join(dir, "package.json");
|
|
425
|
+
if (fs.existsSync(pkgPath)) {
|
|
426
|
+
try {
|
|
427
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
428
|
+
if (pkg.workspaces && Array.isArray(pkg.workspaces)) {
|
|
429
|
+
const hasBackend = pkg.workspaces.some(
|
|
430
|
+
(w) => w === "backend" || w.includes("backend")
|
|
431
|
+
);
|
|
432
|
+
if (hasBackend) return dir;
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) {
|
|
437
|
+
return dir;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
dir = path.dirname(dir);
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
function findBackendDir(projectRoot) {
|
|
445
|
+
const backendDir = path.join(projectRoot, "backend");
|
|
446
|
+
return fs.existsSync(backendDir) ? backendDir : null;
|
|
447
|
+
}
|
|
448
|
+
function getActiveBackendPlugin(backendDir) {
|
|
449
|
+
const pkgPath = path.join(backendDir, "package.json");
|
|
450
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
451
|
+
try {
|
|
452
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
453
|
+
const deps = {
|
|
454
|
+
...pkg.dependencies,
|
|
455
|
+
...pkg.devDependencies
|
|
456
|
+
};
|
|
457
|
+
for (const dep of Object.keys(deps)) {
|
|
458
|
+
if (dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core") {
|
|
459
|
+
return dep;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
function resolvePluginCliScript(backendDir, pluginName) {
|
|
467
|
+
const candidates = [
|
|
468
|
+
path.join(backendDir, "node_modules", pluginName, "src", "cli.ts"),
|
|
469
|
+
path.join(backendDir, "node_modules", pluginName, "dist", "cli.js"),
|
|
470
|
+
// For monorepo dev mode:
|
|
471
|
+
path.resolve(backendDir, "..", "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
|
|
472
|
+
path.resolve(backendDir, "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
|
|
473
|
+
path.resolve(backendDir, "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts")
|
|
474
|
+
];
|
|
475
|
+
for (const candidate of candidates) {
|
|
476
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
function findFrontendDir(projectRoot) {
|
|
481
|
+
const frontendDir = path.join(projectRoot, "frontend");
|
|
482
|
+
return fs.existsSync(frontendDir) ? frontendDir : null;
|
|
483
|
+
}
|
|
484
|
+
function findEnvFile(projectRoot) {
|
|
485
|
+
const candidates = [
|
|
486
|
+
path.join(projectRoot, ".env"),
|
|
487
|
+
path.join(projectRoot, "backend", ".env")
|
|
488
|
+
];
|
|
489
|
+
for (const candidate of candidates) {
|
|
490
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
function resolveLocalBin(projectRoot, binName) {
|
|
495
|
+
const candidates = [
|
|
496
|
+
path.join(projectRoot, "backend", "node_modules", ".bin", binName),
|
|
497
|
+
path.join(projectRoot, "node_modules", ".bin", binName)
|
|
498
|
+
];
|
|
499
|
+
let parent = path.dirname(projectRoot);
|
|
500
|
+
const rootDir = path.parse(parent).root;
|
|
501
|
+
while (parent !== rootDir) {
|
|
502
|
+
candidates.push(path.join(parent, "node_modules", ".bin", binName));
|
|
503
|
+
parent = path.dirname(parent);
|
|
504
|
+
}
|
|
505
|
+
for (const candidate of candidates) {
|
|
506
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
const globalPath = execSync(`which ${binName}`, { encoding: "utf-8" }).trim();
|
|
510
|
+
if (globalPath && fs.existsSync(globalPath)) return globalPath;
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
function resolveTsx(projectRoot) {
|
|
516
|
+
return resolveLocalBin(projectRoot, "tsx");
|
|
517
|
+
}
|
|
518
|
+
function requireProjectRoot() {
|
|
519
|
+
const root = findProjectRoot();
|
|
520
|
+
if (!root) {
|
|
521
|
+
console.error(chalk.red("✗ Could not find a Rebase project root."));
|
|
522
|
+
console.error(chalk.gray(" Make sure you are inside a Rebase project directory"));
|
|
523
|
+
console.error(chalk.gray(" (one with backend/, frontend/, and shared/ directories)."));
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
return root;
|
|
527
|
+
}
|
|
528
|
+
function requireBackendDir(projectRoot) {
|
|
529
|
+
const backendDir = findBackendDir(projectRoot);
|
|
530
|
+
if (!backendDir) {
|
|
531
|
+
console.error(chalk.red("✗ Could not find a backend/ directory."));
|
|
532
|
+
console.error(chalk.gray(` Expected at: ${path.join(projectRoot, "backend")}`));
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
return backendDir;
|
|
536
|
+
}
|
|
537
|
+
async function schemaCommand(subcommand, rawArgs) {
|
|
538
|
+
if (!subcommand || subcommand === "--help") {
|
|
539
|
+
printSchemaHelp();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const projectRoot = requireProjectRoot();
|
|
543
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
544
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
545
|
+
if (!activePlugin) {
|
|
546
|
+
console.error(chalk.red("✗ Could not detect an active database plugin."));
|
|
547
|
+
console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
|
|
551
|
+
if (!pluginCli) {
|
|
552
|
+
console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
const envFile = findEnvFile(projectRoot);
|
|
556
|
+
const env = { ...process.env };
|
|
557
|
+
if (envFile) {
|
|
558
|
+
env.DOTENV_CONFIG_PATH = envFile;
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
const isTs = pluginCli.endsWith(".ts");
|
|
562
|
+
if (isTs) {
|
|
563
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
564
|
+
if (!tsxBin) {
|
|
565
|
+
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
|
|
569
|
+
cwd: backendDir,
|
|
570
|
+
stdio: "inherit",
|
|
571
|
+
env
|
|
572
|
+
});
|
|
573
|
+
} else {
|
|
574
|
+
await execa("node", [pluginCli, ...rawArgs.slice(2)], {
|
|
575
|
+
cwd: backendDir,
|
|
576
|
+
stdio: "inherit",
|
|
577
|
+
env
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
} catch (err) {
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function printSchemaHelp() {
|
|
585
|
+
console.log(`
|
|
586
|
+
${chalk.bold("rebase schema")} — Schema management commands
|
|
587
|
+
|
|
588
|
+
${chalk.green.bold("Usage")}
|
|
589
|
+
rebase schema ${chalk.blue("<command>")} [options]
|
|
590
|
+
|
|
591
|
+
${chalk.green.bold("Commands")}
|
|
592
|
+
${chalk.gray("(Commands are provided by your active database driver plugin)")}
|
|
593
|
+
${chalk.blue.bold("generate")} Generate Schema from collection definitions
|
|
594
|
+
${chalk.blue.bold("introspect")} Introspect an existing database to generate collection definitions
|
|
595
|
+
|
|
596
|
+
${chalk.green.bold("generate Options")}
|
|
597
|
+
${chalk.blue("--collections, -c")} Path to collections directory
|
|
598
|
+
${chalk.blue("--output, -o")} Output path for generated schema
|
|
599
|
+
${chalk.blue("--watch, -w")} Watch for changes and regenerate automatically
|
|
600
|
+
|
|
601
|
+
${chalk.green.bold("introspect Options")}
|
|
602
|
+
${chalk.blue("--output, -o")} Output directory for generated collection files
|
|
603
|
+
`);
|
|
604
|
+
}
|
|
605
|
+
async function dbCommand(subcommand, rawArgs) {
|
|
606
|
+
if (!subcommand || subcommand === "--help") {
|
|
607
|
+
printDbHelp();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const projectRoot = requireProjectRoot();
|
|
611
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
612
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
613
|
+
if (!activePlugin) {
|
|
614
|
+
console.error(chalk.red("✗ Could not detect an active database plugin."));
|
|
615
|
+
console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
|
|
619
|
+
if (!pluginCli) {
|
|
620
|
+
console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
const envFile = findEnvFile(projectRoot);
|
|
624
|
+
const env = { ...process.env };
|
|
625
|
+
if (envFile) {
|
|
626
|
+
env.DOTENV_CONFIG_PATH = envFile;
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const isTs = pluginCli.endsWith(".ts");
|
|
630
|
+
if (isTs) {
|
|
631
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
632
|
+
if (!tsxBin) {
|
|
633
|
+
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
|
|
637
|
+
cwd: backendDir,
|
|
638
|
+
stdio: "inherit",
|
|
639
|
+
env
|
|
640
|
+
});
|
|
641
|
+
} else {
|
|
642
|
+
await execa("node", [pluginCli, ...rawArgs.slice(2)], {
|
|
643
|
+
cwd: backendDir,
|
|
644
|
+
stdio: "inherit",
|
|
645
|
+
env
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
} catch (err) {
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function printDbHelp() {
|
|
653
|
+
console.log(`
|
|
654
|
+
${chalk.bold("rebase db")} — Database management commands
|
|
655
|
+
|
|
656
|
+
${chalk.green.bold("Usage")}
|
|
657
|
+
rebase db ${chalk.blue("<command>")} [options]
|
|
658
|
+
|
|
659
|
+
${chalk.green.bold("Commands")}
|
|
660
|
+
${chalk.gray("(Commands are provided by your active database driver plugin)")}
|
|
661
|
+
${chalk.blue.bold("push")} Apply schema directly to database (development)
|
|
662
|
+
${chalk.blue.bold("pull")} Introspect database → Schema
|
|
663
|
+
${chalk.blue.bold("generate")} Generate migration files
|
|
664
|
+
${chalk.blue.bold("migrate")} Run pending migrations
|
|
665
|
+
${chalk.blue.bold("studio")} Open Studio viewer
|
|
666
|
+
${chalk.blue.bold("branch")} Database branching (create, list, delete, info)
|
|
667
|
+
|
|
668
|
+
${chalk.green.bold("Examples")}
|
|
669
|
+
${chalk.gray("# Quick development workflow")}
|
|
670
|
+
rebase schema generate && rebase db push
|
|
671
|
+
|
|
672
|
+
${chalk.gray("# Production migration workflow")}
|
|
673
|
+
rebase db generate
|
|
674
|
+
rebase db migrate
|
|
675
|
+
|
|
676
|
+
${chalk.gray("# Create a database branch")}
|
|
677
|
+
rebase db branch create feature_auth
|
|
678
|
+
`);
|
|
679
|
+
}
|
|
680
|
+
const DEV_PORT_FILENAME = ".rebase-dev-port";
|
|
681
|
+
function getProjectPort(projectRoot) {
|
|
682
|
+
let hash = 0;
|
|
683
|
+
for (let i = 0; i < projectRoot.length; i++) {
|
|
684
|
+
hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
|
|
685
|
+
}
|
|
686
|
+
return 3001 + Math.abs(hash) % 999;
|
|
687
|
+
}
|
|
688
|
+
function resolveStartPort(projectRoot, explicitPort) {
|
|
689
|
+
if (explicitPort) return explicitPort;
|
|
690
|
+
if (process.env.PORT) return parseInt(process.env.PORT, 10);
|
|
691
|
+
try {
|
|
692
|
+
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
693
|
+
if (fs.existsSync(portFile)) {
|
|
694
|
+
const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
|
|
695
|
+
if (saved > 0 && saved < 65536) return saved;
|
|
696
|
+
}
|
|
697
|
+
} catch {
|
|
698
|
+
}
|
|
699
|
+
return getProjectPort(projectRoot);
|
|
700
|
+
}
|
|
701
|
+
async function devCommand(rawArgs) {
|
|
702
|
+
const args = arg(
|
|
703
|
+
{
|
|
704
|
+
"--backend-only": Boolean,
|
|
705
|
+
"--frontend-only": Boolean,
|
|
706
|
+
"--port": Number,
|
|
707
|
+
"--help": Boolean,
|
|
708
|
+
"-b": "--backend-only",
|
|
709
|
+
"-f": "--frontend-only",
|
|
710
|
+
"-p": "--port",
|
|
711
|
+
"-h": "--help"
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
argv: rawArgs.slice(3),
|
|
715
|
+
// skip "node rebase dev"
|
|
716
|
+
permissive: true
|
|
717
|
+
}
|
|
718
|
+
);
|
|
719
|
+
if (args["--help"]) {
|
|
720
|
+
printDevHelp();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const projectRoot = requireProjectRoot();
|
|
724
|
+
const backendDir = findBackendDir(projectRoot);
|
|
725
|
+
const frontendDir = findFrontendDir(projectRoot);
|
|
726
|
+
const backendOnly = args["--backend-only"] || false;
|
|
727
|
+
const frontendOnly = args["--frontend-only"] || false;
|
|
728
|
+
const startPort = resolveStartPort(projectRoot, args["--port"]);
|
|
729
|
+
console.log("");
|
|
730
|
+
console.log(chalk.bold(" 🚀 Rebase Dev Server"));
|
|
731
|
+
console.log("");
|
|
732
|
+
const children = [];
|
|
733
|
+
let frontendUrl = "";
|
|
734
|
+
let backendUrl = "";
|
|
735
|
+
let debounceSummary = null;
|
|
736
|
+
let bannerPrinted = false;
|
|
737
|
+
let resolvedBackendPort = null;
|
|
738
|
+
const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
|
|
739
|
+
function printSummary() {
|
|
740
|
+
if (!frontendUrl || !backendUrl) return;
|
|
741
|
+
if (debounceSummary) clearTimeout(debounceSummary);
|
|
742
|
+
debounceSummary = setTimeout(() => {
|
|
743
|
+
if (bannerPrinted) return;
|
|
744
|
+
console.log("");
|
|
745
|
+
console.log(chalk.cyan("┌────────────────────────────────────────────────────────────┐"));
|
|
746
|
+
console.log(chalk.cyan("│ │"));
|
|
747
|
+
console.log(chalk.cyan("│ ✨ Rebase Admin App is ready! │"));
|
|
748
|
+
const cleanUrl = stripAnsi(frontendUrl);
|
|
749
|
+
const paddedUrl = cleanUrl.padEnd(40);
|
|
750
|
+
console.log(chalk.cyan("│ 👉 Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
|
|
751
|
+
console.log(chalk.cyan("│ │"));
|
|
752
|
+
console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
|
|
753
|
+
console.log("");
|
|
754
|
+
bannerPrinted = true;
|
|
755
|
+
}, 500);
|
|
756
|
+
}
|
|
757
|
+
const cleanup = () => {
|
|
758
|
+
try {
|
|
759
|
+
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
760
|
+
if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
|
|
761
|
+
const urlFile = path.join(projectRoot, ".rebase-dev-url");
|
|
762
|
+
if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
|
|
763
|
+
} catch {
|
|
764
|
+
}
|
|
765
|
+
children.forEach((child) => {
|
|
766
|
+
if (child.pid && !child.killed) {
|
|
767
|
+
try {
|
|
768
|
+
if (process.platform === "win32") {
|
|
769
|
+
execa.commandSync(`taskkill /pid ${child.pid} /T /F`);
|
|
770
|
+
} else {
|
|
771
|
+
process.kill(-child.pid, "SIGKILL");
|
|
772
|
+
}
|
|
773
|
+
} catch (e) {
|
|
774
|
+
try {
|
|
775
|
+
child.kill("SIGKILL");
|
|
776
|
+
} catch (err) {
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
process.exit(0);
|
|
782
|
+
};
|
|
783
|
+
process.on("SIGINT", cleanup);
|
|
784
|
+
process.on("SIGTERM", cleanup);
|
|
785
|
+
function startFrontend(backendPort) {
|
|
786
|
+
if (!frontendDir) return;
|
|
787
|
+
console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
|
|
788
|
+
const frontendEnv = { ...process.env };
|
|
789
|
+
if (backendPort) {
|
|
790
|
+
frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
|
|
791
|
+
console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
|
|
792
|
+
}
|
|
793
|
+
const frontendChild = execa(
|
|
794
|
+
"pnpm",
|
|
795
|
+
["run", "dev"],
|
|
796
|
+
{
|
|
797
|
+
cwd: frontendDir,
|
|
798
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
799
|
+
env: frontendEnv,
|
|
800
|
+
shell: true,
|
|
801
|
+
detached: process.platform !== "win32"
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
frontendChild.catch(() => {
|
|
805
|
+
});
|
|
806
|
+
frontendChild.stdout?.on("data", (data) => {
|
|
807
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
808
|
+
lines.forEach((line) => {
|
|
809
|
+
console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
|
|
810
|
+
const cleanLine = stripAnsi(line);
|
|
811
|
+
const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
|
|
812
|
+
if (cleanLine.includes("Local:") && urlMatch) {
|
|
813
|
+
frontendUrl = urlMatch[1];
|
|
814
|
+
printSummary();
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
frontendChild.stderr?.on("data", (data) => {
|
|
819
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
820
|
+
lines.forEach((line) => {
|
|
821
|
+
console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
children.push(frontendChild);
|
|
825
|
+
}
|
|
826
|
+
if (!frontendOnly && backendDir) {
|
|
827
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
828
|
+
if (!tsxBin) {
|
|
829
|
+
console.error(chalk.red(" ✗ Could not find tsx binary for backend."));
|
|
830
|
+
console.error(chalk.gray(" Install it with: pnpm add -D tsx"));
|
|
831
|
+
process.exit(1);
|
|
832
|
+
}
|
|
833
|
+
const envFile = findEnvFile(projectRoot);
|
|
834
|
+
const env = { ...process.env };
|
|
835
|
+
if (envFile) {
|
|
836
|
+
env.DOTENV_CONFIG_PATH = envFile;
|
|
837
|
+
}
|
|
838
|
+
env.PORT = String(startPort);
|
|
839
|
+
console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
|
|
840
|
+
console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
|
|
841
|
+
let frontendLaunched = false;
|
|
842
|
+
const backendChild = execa(
|
|
843
|
+
tsxBin,
|
|
844
|
+
["watch", `--watch="${path.join("..", "config", "**", "*")}"`, "--conditions", "development", "src/index.ts"],
|
|
845
|
+
{
|
|
846
|
+
cwd: backendDir,
|
|
847
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
848
|
+
env,
|
|
849
|
+
shell: true,
|
|
850
|
+
detached: process.platform !== "win32"
|
|
851
|
+
}
|
|
852
|
+
);
|
|
853
|
+
backendChild.catch(() => {
|
|
854
|
+
});
|
|
855
|
+
backendChild.stdout?.on("data", (data) => {
|
|
856
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
857
|
+
lines.forEach((line) => {
|
|
858
|
+
console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
|
|
859
|
+
const cleanLine = stripAnsi(line);
|
|
860
|
+
const serverMatch = cleanLine.match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
|
|
861
|
+
if (serverMatch) {
|
|
862
|
+
resolvedBackendPort = parseInt(serverMatch[1], 10);
|
|
863
|
+
backendUrl = "started";
|
|
864
|
+
printSummary();
|
|
865
|
+
const urlFile = path.join(projectRoot, ".rebase-dev-url");
|
|
866
|
+
fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
|
|
867
|
+
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
868
|
+
fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
|
|
869
|
+
if (!backendOnly && frontendDir && !frontendLaunched) {
|
|
870
|
+
frontendLaunched = true;
|
|
871
|
+
startFrontend(resolvedBackendPort);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
backendChild.stderr?.on("data", (data) => {
|
|
877
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
878
|
+
lines.forEach((line) => {
|
|
879
|
+
console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
children.push(backendChild);
|
|
883
|
+
} else if (!frontendOnly && !backendDir) {
|
|
884
|
+
console.warn(chalk.yellow(" ⚠ No backend/ directory found, skipping backend."));
|
|
885
|
+
}
|
|
886
|
+
if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) {
|
|
887
|
+
startFrontend(null);
|
|
888
|
+
} else if (!backendOnly && !frontendDir) {
|
|
889
|
+
console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
|
|
890
|
+
}
|
|
891
|
+
if (children.length === 0) {
|
|
892
|
+
console.error(chalk.red(" ✗ Nothing to start. Check your project structure."));
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
console.log("");
|
|
896
|
+
console.log(chalk.gray(" Press Ctrl+C to stop all servers."));
|
|
897
|
+
console.log("");
|
|
898
|
+
await Promise.all(
|
|
899
|
+
children.map(
|
|
900
|
+
(child) => new Promise((resolve) => {
|
|
901
|
+
child.finally(() => resolve());
|
|
902
|
+
})
|
|
903
|
+
)
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
function printDevHelp() {
|
|
907
|
+
console.log(`
|
|
908
|
+
${chalk.bold("rebase dev")} — Start the development server
|
|
909
|
+
|
|
910
|
+
${chalk.green.bold("Usage")}
|
|
911
|
+
rebase dev [options]
|
|
912
|
+
|
|
913
|
+
${chalk.green.bold("Options")}
|
|
914
|
+
${chalk.blue("--backend-only, -b")} Only start the backend server
|
|
915
|
+
${chalk.blue("--frontend-only, -f")} Only start the frontend server
|
|
916
|
+
${chalk.blue("--port, -p")} Backend port (default: auto-detected per project)
|
|
917
|
+
|
|
918
|
+
${chalk.green.bold("Description")}
|
|
919
|
+
Starts both the backend (tsx watch + Hono) and frontend (Vite)
|
|
920
|
+
dev servers concurrently with color-coded output prefixes.
|
|
921
|
+
|
|
922
|
+
Each project automatically receives a unique default port derived
|
|
923
|
+
from its directory path, preventing collisions when running multiple
|
|
924
|
+
Rebase instances simultaneously.
|
|
925
|
+
|
|
926
|
+
If the assigned port is already in use, the server will automatically
|
|
927
|
+
try the next available port. The frontend is started only after the
|
|
928
|
+
backend is ready, and VITE_API_URL is injected automatically.
|
|
929
|
+
`);
|
|
930
|
+
}
|
|
931
|
+
async function authCommand(subcommand, rawArgs) {
|
|
932
|
+
if (!subcommand || subcommand === "--help") {
|
|
933
|
+
printAuthHelp();
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
switch (subcommand) {
|
|
937
|
+
case "reset-password":
|
|
938
|
+
await resetPassword(rawArgs);
|
|
939
|
+
break;
|
|
940
|
+
default:
|
|
941
|
+
console.error(chalk.red(`Unknown auth command: ${subcommand}`));
|
|
942
|
+
console.log("");
|
|
943
|
+
printAuthHelp();
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
async function resetPassword(rawArgs) {
|
|
948
|
+
const args = arg(
|
|
949
|
+
{
|
|
950
|
+
"--email": String,
|
|
951
|
+
"--password": String,
|
|
952
|
+
"-e": "--email",
|
|
953
|
+
"-p": "--password"
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
argv: rawArgs.slice(4),
|
|
957
|
+
// skip "node rebase auth reset-password"
|
|
958
|
+
permissive: true
|
|
959
|
+
}
|
|
960
|
+
);
|
|
961
|
+
const email = args["--email"] || args._[0];
|
|
962
|
+
const newPassword = args["--password"] || args._[1];
|
|
963
|
+
if (!email) {
|
|
964
|
+
console.error(chalk.red("✗ Email is required."));
|
|
965
|
+
console.log("");
|
|
966
|
+
console.log(chalk.gray(" Usage: rebase auth reset-password <email> [new-password]"));
|
|
967
|
+
console.log(chalk.gray(" rebase auth reset-password --email user@example.com --password NewPass123!"));
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
const projectRoot = requireProjectRoot();
|
|
971
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
972
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
973
|
+
if (!tsxBin) {
|
|
974
|
+
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
975
|
+
process.exit(1);
|
|
976
|
+
}
|
|
977
|
+
const envFile = findEnvFile(projectRoot);
|
|
978
|
+
const env = { ...process.env };
|
|
979
|
+
if (envFile) {
|
|
980
|
+
env.DOTENV_CONFIG_PATH = envFile;
|
|
981
|
+
}
|
|
982
|
+
const scriptContent = `
|
|
983
|
+
import { createPostgresDatabaseConnection } from "@rebasepro/server-core";
|
|
984
|
+
import { hashPassword } from "@rebasepro/server-core/src/auth/password";
|
|
985
|
+
import { eq } from "drizzle-orm";
|
|
986
|
+
import { users } from "@rebasepro/server-core/src/db/auth-schema";
|
|
987
|
+
import * as dotenv from "dotenv";
|
|
988
|
+
import path from "path";
|
|
989
|
+
|
|
990
|
+
dotenv.config({ path: "${envFile || path.join(projectRoot, ".env")}" });
|
|
991
|
+
|
|
992
|
+
const email = "${email}";
|
|
993
|
+
const newPassword = "${newPassword || "NewPassword123!"}";
|
|
994
|
+
|
|
995
|
+
async function resetPassword() {
|
|
996
|
+
const { db } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);
|
|
997
|
+
const hash = await hashPassword(newPassword);
|
|
998
|
+
|
|
999
|
+
const result = await db.update(users)
|
|
1000
|
+
.set({ passwordHash: hash })
|
|
1001
|
+
.where(eq(users.email, email))
|
|
1002
|
+
.returning({
|
|
1003
|
+
id: users.id,
|
|
1004
|
+
email: users.email
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
if (result.length > 0) {
|
|
1008
|
+
console.log("✅ Password reset for: " + result[0].email);
|
|
1009
|
+
${!newPassword ? 'console.log(" New password: " + newPassword);' : ""}
|
|
1010
|
+
} else {
|
|
1011
|
+
console.log("✗ User not found: " + email);
|
|
1012
|
+
}
|
|
1013
|
+
process.exit(0);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
resetPassword().catch(console.error);
|
|
1017
|
+
`;
|
|
1018
|
+
const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
|
|
1019
|
+
fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
|
|
1020
|
+
console.log("");
|
|
1021
|
+
console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password"));
|
|
1022
|
+
console.log("");
|
|
1023
|
+
console.log(` ${chalk.gray("Email:")} ${email}`);
|
|
1024
|
+
if (newPassword) {
|
|
1025
|
+
console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
|
|
1026
|
+
}
|
|
1027
|
+
console.log("");
|
|
1028
|
+
const child = spawn(tsxBin, [tmpScriptPath], {
|
|
1029
|
+
cwd: backendDir,
|
|
1030
|
+
stdio: "inherit",
|
|
1031
|
+
env
|
|
1032
|
+
});
|
|
1033
|
+
return new Promise((resolve) => {
|
|
1034
|
+
child.on("close", (code) => {
|
|
1035
|
+
try {
|
|
1036
|
+
fs.unlinkSync(tmpScriptPath);
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
1039
|
+
if (code !== 0) {
|
|
1040
|
+
process.exit(code ?? 1);
|
|
1041
|
+
}
|
|
1042
|
+
resolve();
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
function printAuthHelp() {
|
|
1047
|
+
console.log(`
|
|
1048
|
+
${chalk.bold("rebase auth")} — Authentication management commands
|
|
1049
|
+
|
|
1050
|
+
${chalk.green.bold("Usage")}
|
|
1051
|
+
rebase auth ${chalk.blue("<command>")} [options]
|
|
1052
|
+
|
|
1053
|
+
${chalk.green.bold("Commands")}
|
|
1054
|
+
${chalk.blue.bold("reset-password")} Reset a user's password
|
|
1055
|
+
|
|
1056
|
+
${chalk.green.bold("reset-password Options")}
|
|
1057
|
+
${chalk.blue("--email, -e")} User's email address
|
|
1058
|
+
${chalk.blue("--password, -p")} New password (default: NewPassword123!)
|
|
1059
|
+
|
|
1060
|
+
${chalk.green.bold("Examples")}
|
|
1061
|
+
rebase auth reset-password user@example.com
|
|
1062
|
+
rebase auth reset-password --email user@example.com --password MyNewPass!
|
|
1063
|
+
`);
|
|
1064
|
+
}
|
|
1065
|
+
async function doctorCommand(rawArgs) {
|
|
1066
|
+
const projectRoot = requireProjectRoot();
|
|
1067
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
1068
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
1069
|
+
if (!activePlugin) {
|
|
1070
|
+
console.error(chalk.red("✗ Could not detect an active database plugin."));
|
|
1071
|
+
console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
|
|
1075
|
+
if (!pluginCli) {
|
|
1076
|
+
console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
const envFile = findEnvFile(projectRoot);
|
|
1080
|
+
const env = { ...process.env };
|
|
1081
|
+
if (envFile) {
|
|
1082
|
+
env.DOTENV_CONFIG_PATH = envFile;
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
const isTs = pluginCli.endsWith(".ts");
|
|
1086
|
+
if (isTs) {
|
|
1087
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
1088
|
+
if (!tsxBin) {
|
|
1089
|
+
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
}
|
|
1092
|
+
await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
|
|
1093
|
+
cwd: backendDir,
|
|
1094
|
+
stdio: "inherit",
|
|
1095
|
+
env
|
|
1096
|
+
});
|
|
1097
|
+
} else {
|
|
1098
|
+
await execa("node", [pluginCli, ...rawArgs.slice(2)], {
|
|
1099
|
+
cwd: backendDir,
|
|
1100
|
+
stdio: "inherit",
|
|
1101
|
+
env
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
} catch {
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
171
1108
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
172
1109
|
const __dirname$1 = path.dirname(__filename$1);
|
|
173
1110
|
function getVersion() {
|
|
@@ -198,16 +1135,56 @@ async function entry(args) {
|
|
|
198
1135
|
return;
|
|
199
1136
|
}
|
|
200
1137
|
const command = parsedArgs._[0];
|
|
201
|
-
|
|
1138
|
+
const subcommand = parsedArgs._[1];
|
|
1139
|
+
const namespacedCommands = ["schema", "db", "dev", "auth", "doctor"];
|
|
1140
|
+
if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
|
|
202
1141
|
printHelp();
|
|
203
1142
|
return;
|
|
204
1143
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1144
|
+
const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
|
|
1145
|
+
switch (command) {
|
|
1146
|
+
case "init":
|
|
1147
|
+
await createRebaseApp(args);
|
|
1148
|
+
break;
|
|
1149
|
+
case "generate-sdk": {
|
|
1150
|
+
const sdkArgs = arg(
|
|
1151
|
+
{
|
|
1152
|
+
"--collections-dir": String,
|
|
1153
|
+
"--output": String,
|
|
1154
|
+
"-c": "--collections-dir",
|
|
1155
|
+
"-o": "--output"
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
argv: args.slice(3),
|
|
1159
|
+
permissive: true
|
|
1160
|
+
}
|
|
1161
|
+
);
|
|
1162
|
+
await generateSdkCommand({
|
|
1163
|
+
collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
|
|
1164
|
+
output: sdkArgs["--output"] || "./generated/sdk",
|
|
1165
|
+
cwd: process.cwd()
|
|
1166
|
+
});
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
case "schema":
|
|
1170
|
+
await schemaCommand(effectiveSubcommand, args);
|
|
1171
|
+
break;
|
|
1172
|
+
case "db":
|
|
1173
|
+
await dbCommand(effectiveSubcommand, args);
|
|
1174
|
+
break;
|
|
1175
|
+
case "dev":
|
|
1176
|
+
await devCommand(args);
|
|
1177
|
+
break;
|
|
1178
|
+
case "auth":
|
|
1179
|
+
await authCommand(effectiveSubcommand, args);
|
|
1180
|
+
break;
|
|
1181
|
+
case "doctor":
|
|
1182
|
+
await doctorCommand(args);
|
|
1183
|
+
break;
|
|
1184
|
+
default:
|
|
1185
|
+
console.log(chalk.red(`Unknown command: ${command}`));
|
|
1186
|
+
console.log("");
|
|
1187
|
+
printHelp();
|
|
211
1188
|
}
|
|
212
1189
|
}
|
|
213
1190
|
function printHelp() {
|
|
@@ -218,7 +1195,30 @@ ${chalk.green.bold("Usage")}
|
|
|
218
1195
|
rebase ${chalk.blue("<command>")} [options]
|
|
219
1196
|
|
|
220
1197
|
${chalk.green.bold("Commands")}
|
|
221
|
-
${chalk.blue.bold("init")}
|
|
1198
|
+
${chalk.blue.bold("init")} Create a new Rebase project
|
|
1199
|
+
${chalk.blue.bold("dev")} Start the development server
|
|
1200
|
+
|
|
1201
|
+
${chalk.green.bold("Schema")}
|
|
1202
|
+
${chalk.blue.bold("schema generate")} Generate Drizzle schema from collections
|
|
1203
|
+
${chalk.blue.bold("schema")} ${chalk.gray("--help")} Show schema command help
|
|
1204
|
+
|
|
1205
|
+
${chalk.green.bold("Database")}
|
|
1206
|
+
${chalk.blue.bold("db push")} Apply schema directly to database ${chalk.gray("(dev)")}
|
|
1207
|
+
${chalk.blue.bold("db pull")} Introspect database → Drizzle schema
|
|
1208
|
+
${chalk.blue.bold("db generate")} Generate SQL migration files
|
|
1209
|
+
${chalk.blue.bold("db migrate")} Run pending migrations
|
|
1210
|
+
${chalk.blue.bold("db studio")} Open Drizzle Studio
|
|
1211
|
+
${chalk.blue.bold("db")} ${chalk.gray("--help")} Show database command help
|
|
1212
|
+
|
|
1213
|
+
${chalk.green.bold("SDK")}
|
|
1214
|
+
${chalk.blue.bold("generate-sdk")} Generate a typed JS SDK from collections
|
|
1215
|
+
|
|
1216
|
+
${chalk.green.bold("Auth")}
|
|
1217
|
+
${chalk.blue.bold("auth reset-password")} Reset a user's password
|
|
1218
|
+
${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
|
|
1219
|
+
|
|
1220
|
+
${chalk.green.bold("Diagnostics")}
|
|
1221
|
+
${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
|
|
222
1222
|
|
|
223
1223
|
${chalk.green.bold("Options")}
|
|
224
1224
|
${chalk.blue("--version, -v")} Show version number
|
|
@@ -271,12 +1271,27 @@ async function logout(env, _debug) {
|
|
|
271
1271
|
}
|
|
272
1272
|
}
|
|
273
1273
|
export {
|
|
1274
|
+
authCommand,
|
|
1275
|
+
configureEnvFile,
|
|
274
1276
|
createRebaseApp,
|
|
1277
|
+
dbCommand,
|
|
1278
|
+
devCommand,
|
|
275
1279
|
entry,
|
|
1280
|
+
findBackendDir,
|
|
1281
|
+
findEnvFile,
|
|
1282
|
+
findFrontendDir,
|
|
1283
|
+
findProjectRoot,
|
|
1284
|
+
getActiveBackendPlugin,
|
|
276
1285
|
getTokens,
|
|
277
1286
|
login,
|
|
278
1287
|
logout,
|
|
279
1288
|
parseJwt,
|
|
280
|
-
refreshCredentials
|
|
1289
|
+
refreshCredentials,
|
|
1290
|
+
requireBackendDir,
|
|
1291
|
+
requireProjectRoot,
|
|
1292
|
+
resolveLocalBin,
|
|
1293
|
+
resolvePluginCliScript,
|
|
1294
|
+
resolveTsx,
|
|
1295
|
+
schemaCommand
|
|
281
1296
|
};
|
|
282
1297
|
//# sourceMappingURL=index.es.js.map
|