@rebasepro/cli 0.0.1-canary.4d4fb3e → 0.0.1-canary.5584634
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/cli.test.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 +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/index.cjs +295 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.es.js +295 -73
- package/dist/index.es.js.map +1 -1
- package/dist/utils/project.test.d.ts +1 -0
- package/package.json +67 -66
- package/templates/template/.env.template +22 -1
- package/templates/template/README.md +2 -2
- package/templates/template/backend/Dockerfile +6 -6
- package/templates/template/backend/drizzle.config.ts +15 -3
- package/templates/template/backend/functions/hello.ts +24 -6
- package/templates/template/backend/package.json +2 -2
- package/templates/template/backend/src/env.ts +52 -0
- package/templates/template/backend/src/index.ts +86 -34
- 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/{shared → config}/collections/posts.ts +39 -1
- package/templates/template/config/collections/tags.ts +27 -0
- package/templates/template/{shared → config}/package.json +1 -1
- package/templates/template/docker-compose.yml +12 -0
- package/templates/template/frontend/Dockerfile +3 -3
- package/templates/template/frontend/package.json +2 -2
- package/templates/template/frontend/src/App.tsx +21 -101
- package/templates/template/frontend/src/main.tsx +2 -2
- package/templates/template/frontend/vite.config.ts +32 -3
- package/templates/template/package.json +5 -4
- package/templates/template/pnpm-workspace.yaml +1 -1
- package/templates/template/scripts/example.ts +91 -0
- package/templates/template/shared/collections/index.ts +0 -3
- /package/templates/template/{shared → config}/index.ts +0 -0
- /package/templates/template/{shared → config}/tsconfig.json +0 -0
package/dist/index.es.js
CHANGED
|
@@ -80,16 +80,31 @@ async function promptForOptions(rawArgs) {
|
|
|
80
80
|
default: true
|
|
81
81
|
});
|
|
82
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
|
+
});
|
|
83
96
|
const answers = await inquirer.prompt(questions);
|
|
84
|
-
const
|
|
85
|
-
const
|
|
97
|
+
const targetDirectory = path.resolve(process.cwd(), nameArg || answers.projectName);
|
|
98
|
+
const projectName = path.basename(targetDirectory);
|
|
86
99
|
const templateDirectory = path.resolve(cliRoot, "templates", "template");
|
|
87
100
|
return {
|
|
88
101
|
projectName,
|
|
89
102
|
git: args["--git"] || answers.git || false,
|
|
90
103
|
installDeps: args["--install"] || answers.installDeps || false,
|
|
91
104
|
targetDirectory,
|
|
92
|
-
templateDirectory
|
|
105
|
+
templateDirectory,
|
|
106
|
+
databaseUrl: answers.databaseUrl?.trim() || void 0,
|
|
107
|
+
introspect: answers.introspect || false
|
|
93
108
|
};
|
|
94
109
|
}
|
|
95
110
|
async function createProject(options) {
|
|
@@ -127,20 +142,27 @@ async function createProject(options) {
|
|
|
127
142
|
if (fs.existsSync(envTemplatePath) && !fs.existsSync(envPath)) {
|
|
128
143
|
fs.renameSync(envTemplatePath, envPath);
|
|
129
144
|
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
130
|
-
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
131
145
|
let envContent = fs.readFileSync(envPath, "utf-8");
|
|
132
|
-
envContent = envContent.replace(
|
|
133
|
-
"postgresql://rebase:password@localhost:5432/rebase",
|
|
134
|
-
`postgresql://rebase:${dbPassword}@localhost:5432/rebase`
|
|
135
|
-
);
|
|
136
146
|
envContent = envContent.replace(
|
|
137
147
|
"change-this-to-a-secure-random-string",
|
|
138
148
|
jwtSecret
|
|
139
149
|
);
|
|
140
|
-
|
|
150
|
+
if (options.databaseUrl) {
|
|
151
|
+
envContent = envContent.replace(
|
|
152
|
+
"postgresql://rebase:password@localhost:5432/rebase",
|
|
153
|
+
options.databaseUrl
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
157
|
+
envContent = envContent.replace(
|
|
158
|
+
"postgresql://rebase:password@localhost:5432/rebase",
|
|
159
|
+
`postgresql://rebase:${dbPassword}@localhost:5432/rebase`
|
|
160
|
+
);
|
|
161
|
+
envContent += `
|
|
141
162
|
# Docker Compose Database Password
|
|
142
163
|
POSTGRES_PASSWORD=${dbPassword}
|
|
143
164
|
`;
|
|
165
|
+
}
|
|
144
166
|
fs.writeFileSync(envPath, envContent, "utf-8");
|
|
145
167
|
}
|
|
146
168
|
if (options.git) {
|
|
@@ -164,6 +186,26 @@ POSTGRES_PASSWORD=${dbPassword}
|
|
|
164
186
|
console.warn(chalk.yellow(" Warning: Failed to install dependencies. You may need to run `pnpm install` manually."));
|
|
165
187
|
}
|
|
166
188
|
}
|
|
189
|
+
if (options.introspect) {
|
|
190
|
+
console.log("");
|
|
191
|
+
if (options.installDeps) {
|
|
192
|
+
console.log(chalk.gray(" Introspecting database and generating collections..."));
|
|
193
|
+
console.log("");
|
|
194
|
+
try {
|
|
195
|
+
await execa("pnpm", ["exec", "rebase", "schema", "introspect"], {
|
|
196
|
+
cwd: options.targetDirectory,
|
|
197
|
+
stdio: "inherit"
|
|
198
|
+
});
|
|
199
|
+
console.log(chalk.green(" Database successfully introspected!"));
|
|
200
|
+
} catch {
|
|
201
|
+
console.warn(chalk.yellow(" Warning: Failed to introspect database automatically."));
|
|
202
|
+
console.warn(chalk.yellow(" You can run `pnpm exec rebase schema introspect` manually after setup."));
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
console.warn(chalk.yellow(" Skipping introspection because dependencies were not installed."));
|
|
206
|
+
console.warn(chalk.yellow(" Run `pnpm install` then `pnpm exec rebase schema introspect` manually."));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
167
209
|
console.log("");
|
|
168
210
|
console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
|
|
169
211
|
console.log("");
|
|
@@ -174,12 +216,19 @@ POSTGRES_PASSWORD=${dbPassword}
|
|
|
174
216
|
console.log(` ${chalk.cyan("pnpm install")}`);
|
|
175
217
|
}
|
|
176
218
|
console.log("");
|
|
177
|
-
|
|
178
|
-
|
|
219
|
+
if (options.databaseUrl) {
|
|
220
|
+
console.log(chalk.gray(" # Your database is configured! Start the dev server:"));
|
|
221
|
+
} else {
|
|
222
|
+
console.log(chalk.gray(" # A local database configuration has been generated in .env"));
|
|
223
|
+
console.log(chalk.gray(" # If using the included docker-compose.yml, start it with:"));
|
|
224
|
+
console.log(` ${chalk.cyan("docker compose up -d")}`);
|
|
225
|
+
console.log("");
|
|
226
|
+
console.log(chalk.gray(" # Then start the dev server:"));
|
|
227
|
+
}
|
|
179
228
|
console.log("");
|
|
180
229
|
console.log(` ${chalk.cyan("pnpm dev")}`);
|
|
181
230
|
console.log("");
|
|
182
|
-
console.log(chalk.gray("This starts both the backend (
|
|
231
|
+
console.log(chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
|
|
183
232
|
console.log("");
|
|
184
233
|
console.log(chalk.gray("Docs: https://rebase.pro/docs"));
|
|
185
234
|
console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
|
|
@@ -190,14 +239,63 @@ async function replacePlaceholders(options) {
|
|
|
190
239
|
"package.json",
|
|
191
240
|
"frontend/package.json",
|
|
192
241
|
"backend/package.json",
|
|
193
|
-
"
|
|
242
|
+
"config/package.json",
|
|
194
243
|
"frontend/index.html"
|
|
195
244
|
];
|
|
245
|
+
const packageJsonPath = path.resolve(cliRoot, "package.json");
|
|
246
|
+
let cliVersion = "latest";
|
|
247
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
248
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
249
|
+
cliVersion = pkg.version || "latest";
|
|
250
|
+
}
|
|
251
|
+
const versionCache = /* @__PURE__ */ new Map();
|
|
252
|
+
const getPackageVersion = async (pkgName) => {
|
|
253
|
+
if (versionCache.has(pkgName)) return versionCache.get(pkgName);
|
|
254
|
+
let versionToUse = cliVersion;
|
|
255
|
+
try {
|
|
256
|
+
const { stdout } = await execa("npm", ["--loglevel", "error", "info", `${pkgName}@${cliVersion}`, "version"]);
|
|
257
|
+
if (!stdout.trim()) throw new Error("Not found");
|
|
258
|
+
versionToUse = stdout.trim();
|
|
259
|
+
} catch {
|
|
260
|
+
try {
|
|
261
|
+
const tag = cliVersion.includes("canary") ? "canary" : "latest";
|
|
262
|
+
const { stdout } = await execa("npm", ["--loglevel", "error", "info", `${pkgName}@${tag}`, "version"]);
|
|
263
|
+
if (!stdout.trim()) throw new Error("Not found");
|
|
264
|
+
versionToUse = stdout.trim();
|
|
265
|
+
} catch {
|
|
266
|
+
try {
|
|
267
|
+
const { stdout } = await execa("npm", ["--loglevel", "error", "info", pkgName, "version"]);
|
|
268
|
+
versionToUse = stdout.trim() || "latest";
|
|
269
|
+
} catch {
|
|
270
|
+
versionToUse = "latest";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
versionCache.set(pkgName, versionToUse);
|
|
275
|
+
return versionToUse;
|
|
276
|
+
};
|
|
277
|
+
const allPackages = /* @__PURE__ */ new Set();
|
|
278
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
196
279
|
for (const file of filesToProcess) {
|
|
197
280
|
const fullPath = path.resolve(options.targetDirectory, file);
|
|
198
281
|
if (!fs.existsSync(fullPath)) continue;
|
|
199
|
-
|
|
200
|
-
|
|
282
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
283
|
+
fileContents.set(fullPath, content);
|
|
284
|
+
const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
|
|
285
|
+
for (const match of matches) {
|
|
286
|
+
allPackages.add(match[1]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
console.log(chalk.gray(" Resolving package versions..."));
|
|
290
|
+
await Promise.all(Array.from(allPackages).map(getPackageVersion));
|
|
291
|
+
for (const [fullPath, originalContent] of fileContents.entries()) {
|
|
292
|
+
let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
|
|
293
|
+
const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
|
|
294
|
+
for (const match of matches) {
|
|
295
|
+
const pkgName = match[1];
|
|
296
|
+
const resolvedVersion = versionCache.get(pkgName) || "latest";
|
|
297
|
+
content = content.replace(new RegExp(`"${pkgName}":\\s*"workspace:\\*"`, "g"), `"${pkgName}": "${resolvedVersion}"`);
|
|
298
|
+
}
|
|
201
299
|
fs.writeFileSync(fullPath, content, "utf-8");
|
|
202
300
|
}
|
|
203
301
|
}
|
|
@@ -309,7 +407,7 @@ async function generateSdkCommand(args) {
|
|
|
309
407
|
console.log(chalk.green.bold(" ✓ SDK generated successfully!"));
|
|
310
408
|
console.log("");
|
|
311
409
|
console.log(chalk.gray(" Usage:"));
|
|
312
|
-
console.log(chalk.gray(
|
|
410
|
+
console.log(chalk.gray(" import { createRebaseClient } from '@rebasepro/client';"));
|
|
313
411
|
console.log(chalk.gray(` import type { Database } from './${path.relative(cwd, path.join(resolvedOutput, "database.types"))}';`));
|
|
314
412
|
console.log("");
|
|
315
413
|
console.log(chalk.gray(" const rebase = createRebaseClient<Database>({"));
|
|
@@ -336,7 +434,7 @@ function findProjectRoot(startDir = process.cwd()) {
|
|
|
336
434
|
}
|
|
337
435
|
} catch {
|
|
338
436
|
}
|
|
339
|
-
if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "
|
|
437
|
+
if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) {
|
|
340
438
|
return dir;
|
|
341
439
|
}
|
|
342
440
|
}
|
|
@@ -353,7 +451,10 @@ function getActiveBackendPlugin(backendDir) {
|
|
|
353
451
|
if (!fs.existsSync(pkgPath)) return null;
|
|
354
452
|
try {
|
|
355
453
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
356
|
-
const deps = {
|
|
454
|
+
const deps = {
|
|
455
|
+
...pkg.dependencies,
|
|
456
|
+
...pkg.devDependencies
|
|
457
|
+
};
|
|
357
458
|
for (const dep of Object.keys(deps)) {
|
|
358
459
|
if (dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server-core") {
|
|
359
460
|
return dep;
|
|
@@ -368,6 +469,7 @@ function resolvePluginCliScript(backendDir, pluginName) {
|
|
|
368
469
|
path.join(backendDir, "node_modules", pluginName, "src", "cli.ts"),
|
|
369
470
|
path.join(backendDir, "node_modules", pluginName, "dist", "cli.js"),
|
|
370
471
|
// For monorepo dev mode:
|
|
472
|
+
path.resolve(backendDir, "..", "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
|
|
371
473
|
path.resolve(backendDir, "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
|
|
372
474
|
path.resolve(backendDir, "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts")
|
|
373
475
|
];
|
|
@@ -464,13 +566,13 @@ async function schemaCommand(subcommand, rawArgs) {
|
|
|
464
566
|
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
465
567
|
process.exit(1);
|
|
466
568
|
}
|
|
467
|
-
await execa(tsxBin, [pluginCli,
|
|
569
|
+
await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
|
|
468
570
|
cwd: backendDir,
|
|
469
571
|
stdio: "inherit",
|
|
470
572
|
env
|
|
471
573
|
});
|
|
472
574
|
} else {
|
|
473
|
-
await execa("node", [pluginCli,
|
|
575
|
+
await execa("node", [pluginCli, ...rawArgs.slice(2)], {
|
|
474
576
|
cwd: backendDir,
|
|
475
577
|
stdio: "inherit",
|
|
476
578
|
env
|
|
@@ -490,11 +592,15 @@ ${chalk.green.bold("Usage")}
|
|
|
490
592
|
${chalk.green.bold("Commands")}
|
|
491
593
|
${chalk.gray("(Commands are provided by your active database driver plugin)")}
|
|
492
594
|
${chalk.blue.bold("generate")} Generate Schema from collection definitions
|
|
595
|
+
${chalk.blue.bold("introspect")} Introspect an existing database to generate collection definitions
|
|
493
596
|
|
|
494
597
|
${chalk.green.bold("generate Options")}
|
|
495
598
|
${chalk.blue("--collections, -c")} Path to collections directory
|
|
496
599
|
${chalk.blue("--output, -o")} Output path for generated schema
|
|
497
600
|
${chalk.blue("--watch, -w")} Watch for changes and regenerate automatically
|
|
601
|
+
|
|
602
|
+
${chalk.green.bold("introspect Options")}
|
|
603
|
+
${chalk.blue("--output, -o")} Output directory for generated collection files
|
|
498
604
|
`);
|
|
499
605
|
}
|
|
500
606
|
async function dbCommand(subcommand, rawArgs) {
|
|
@@ -528,13 +634,13 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
528
634
|
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
529
635
|
process.exit(1);
|
|
530
636
|
}
|
|
531
|
-
await execa(tsxBin, [pluginCli,
|
|
637
|
+
await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
|
|
532
638
|
cwd: backendDir,
|
|
533
639
|
stdio: "inherit",
|
|
534
640
|
env
|
|
535
641
|
});
|
|
536
642
|
} else {
|
|
537
|
-
await execa("node", [pluginCli,
|
|
643
|
+
await execa("node", [pluginCli, ...rawArgs.slice(2)], {
|
|
538
644
|
cwd: backendDir,
|
|
539
645
|
stdio: "inherit",
|
|
540
646
|
env
|
|
@@ -572,6 +678,27 @@ ${chalk.green.bold("Examples")}
|
|
|
572
678
|
rebase db branch create feature_auth
|
|
573
679
|
`);
|
|
574
680
|
}
|
|
681
|
+
const DEV_PORT_FILENAME = ".rebase-dev-port";
|
|
682
|
+
function getProjectPort(projectRoot) {
|
|
683
|
+
let hash = 0;
|
|
684
|
+
for (let i = 0; i < projectRoot.length; i++) {
|
|
685
|
+
hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
|
|
686
|
+
}
|
|
687
|
+
return 3001 + Math.abs(hash) % 999;
|
|
688
|
+
}
|
|
689
|
+
function resolveStartPort(projectRoot, explicitPort) {
|
|
690
|
+
if (explicitPort) return explicitPort;
|
|
691
|
+
if (process.env.PORT) return parseInt(process.env.PORT, 10);
|
|
692
|
+
try {
|
|
693
|
+
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
694
|
+
if (fs.existsSync(portFile)) {
|
|
695
|
+
const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
|
|
696
|
+
if (saved > 0 && saved < 65536) return saved;
|
|
697
|
+
}
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
return getProjectPort(projectRoot);
|
|
701
|
+
}
|
|
575
702
|
async function devCommand(rawArgs) {
|
|
576
703
|
const args = arg(
|
|
577
704
|
{
|
|
@@ -599,6 +726,7 @@ async function devCommand(rawArgs) {
|
|
|
599
726
|
const frontendDir = findFrontendDir(projectRoot);
|
|
600
727
|
const backendOnly = args["--backend-only"] || false;
|
|
601
728
|
const frontendOnly = args["--frontend-only"] || false;
|
|
729
|
+
const startPort = resolveStartPort(projectRoot, args["--port"]);
|
|
602
730
|
console.log("");
|
|
603
731
|
console.log(chalk.bold(" 🚀 Rebase Dev Server"));
|
|
604
732
|
console.log("");
|
|
@@ -607,6 +735,7 @@ async function devCommand(rawArgs) {
|
|
|
607
735
|
let backendUrl = "";
|
|
608
736
|
let debounceSummary = null;
|
|
609
737
|
let bannerPrinted = false;
|
|
738
|
+
let resolvedBackendPort = null;
|
|
610
739
|
const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
|
|
611
740
|
function printSummary() {
|
|
612
741
|
if (!frontendUrl || !backendUrl) return;
|
|
@@ -619,7 +748,7 @@ async function devCommand(rawArgs) {
|
|
|
619
748
|
console.log(chalk.cyan("│ ✨ Rebase Admin App is ready! │"));
|
|
620
749
|
const cleanUrl = stripAnsi(frontendUrl);
|
|
621
750
|
const paddedUrl = cleanUrl.padEnd(40);
|
|
622
|
-
console.log(chalk.cyan(
|
|
751
|
+
console.log(chalk.cyan("│ 👉 Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
|
|
623
752
|
console.log(chalk.cyan("│ │"));
|
|
624
753
|
console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
|
|
625
754
|
console.log("");
|
|
@@ -627,15 +756,74 @@ async function devCommand(rawArgs) {
|
|
|
627
756
|
}, 500);
|
|
628
757
|
}
|
|
629
758
|
const cleanup = () => {
|
|
759
|
+
try {
|
|
760
|
+
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
761
|
+
if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
|
|
762
|
+
const urlFile = path.join(projectRoot, ".rebase-dev-url");
|
|
763
|
+
if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
|
|
764
|
+
} catch {
|
|
765
|
+
}
|
|
630
766
|
children.forEach((child) => {
|
|
631
|
-
if (!child.killed) {
|
|
632
|
-
|
|
767
|
+
if (child.pid && !child.killed) {
|
|
768
|
+
try {
|
|
769
|
+
if (process.platform === "win32") {
|
|
770
|
+
execa.commandSync(`taskkill /pid ${child.pid} /T /F`);
|
|
771
|
+
} else {
|
|
772
|
+
process.kill(-child.pid, "SIGKILL");
|
|
773
|
+
}
|
|
774
|
+
} catch (e) {
|
|
775
|
+
try {
|
|
776
|
+
child.kill("SIGKILL");
|
|
777
|
+
} catch (err) {
|
|
778
|
+
}
|
|
779
|
+
}
|
|
633
780
|
}
|
|
634
781
|
});
|
|
635
782
|
process.exit(0);
|
|
636
783
|
};
|
|
637
784
|
process.on("SIGINT", cleanup);
|
|
638
785
|
process.on("SIGTERM", cleanup);
|
|
786
|
+
function startFrontend(backendPort) {
|
|
787
|
+
if (!frontendDir) return;
|
|
788
|
+
console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
|
|
789
|
+
const frontendEnv = { ...process.env };
|
|
790
|
+
if (backendPort) {
|
|
791
|
+
frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
|
|
792
|
+
console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
|
|
793
|
+
}
|
|
794
|
+
const frontendChild = execa(
|
|
795
|
+
"pnpm",
|
|
796
|
+
["run", "dev"],
|
|
797
|
+
{
|
|
798
|
+
cwd: frontendDir,
|
|
799
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
800
|
+
env: frontendEnv,
|
|
801
|
+
shell: true,
|
|
802
|
+
detached: process.platform !== "win32"
|
|
803
|
+
}
|
|
804
|
+
);
|
|
805
|
+
frontendChild.catch(() => {
|
|
806
|
+
});
|
|
807
|
+
frontendChild.stdout?.on("data", (data) => {
|
|
808
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
809
|
+
lines.forEach((line) => {
|
|
810
|
+
console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
|
|
811
|
+
const cleanLine = stripAnsi(line);
|
|
812
|
+
const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
|
|
813
|
+
if (cleanLine.includes("Local:") && urlMatch) {
|
|
814
|
+
frontendUrl = urlMatch[1];
|
|
815
|
+
printSummary();
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
frontendChild.stderr?.on("data", (data) => {
|
|
820
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
821
|
+
lines.forEach((line) => {
|
|
822
|
+
console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
children.push(frontendChild);
|
|
826
|
+
}
|
|
639
827
|
if (!frontendOnly && backendDir) {
|
|
640
828
|
const tsxBin = resolveTsx(projectRoot);
|
|
641
829
|
if (!tsxBin) {
|
|
@@ -648,22 +836,19 @@ async function devCommand(rawArgs) {
|
|
|
648
836
|
if (envFile) {
|
|
649
837
|
env.DOTENV_CONFIG_PATH = envFile;
|
|
650
838
|
}
|
|
651
|
-
|
|
652
|
-
env.PORT = String(args["--port"]);
|
|
653
|
-
}
|
|
654
|
-
path.join(backendDir, "src", "index.ts");
|
|
655
|
-
const watchDirs = [
|
|
656
|
-
`--watch="${path.join("..", "shared", "**", "*")}"`
|
|
657
|
-
];
|
|
839
|
+
env.PORT = String(startPort);
|
|
658
840
|
console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
|
|
841
|
+
console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
|
|
842
|
+
let frontendLaunched = false;
|
|
659
843
|
const backendChild = execa(
|
|
660
844
|
tsxBin,
|
|
661
|
-
["watch",
|
|
845
|
+
["watch", `--watch="${path.join("..", "config", "**", "*")}"`, "--conditions", "development", "src/index.ts"],
|
|
662
846
|
{
|
|
663
847
|
cwd: backendDir,
|
|
664
848
|
stdio: ["inherit", "pipe", "pipe"],
|
|
665
849
|
env,
|
|
666
|
-
shell: true
|
|
850
|
+
shell: true,
|
|
851
|
+
detached: process.platform !== "win32"
|
|
667
852
|
}
|
|
668
853
|
);
|
|
669
854
|
backendChild.catch(() => {
|
|
@@ -673,9 +858,19 @@ async function devCommand(rawArgs) {
|
|
|
673
858
|
lines.forEach((line) => {
|
|
674
859
|
console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
|
|
675
860
|
const cleanLine = stripAnsi(line);
|
|
676
|
-
|
|
861
|
+
const serverMatch = cleanLine.match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
|
|
862
|
+
if (serverMatch) {
|
|
863
|
+
resolvedBackendPort = parseInt(serverMatch[1], 10);
|
|
677
864
|
backendUrl = "started";
|
|
678
865
|
printSummary();
|
|
866
|
+
const urlFile = path.join(projectRoot, ".rebase-dev-url");
|
|
867
|
+
fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
|
|
868
|
+
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
869
|
+
fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
|
|
870
|
+
if (!backendOnly && frontendDir && !frontendLaunched) {
|
|
871
|
+
frontendLaunched = true;
|
|
872
|
+
startFrontend(resolvedBackendPort);
|
|
873
|
+
}
|
|
679
874
|
}
|
|
680
875
|
});
|
|
681
876
|
});
|
|
@@ -689,39 +884,8 @@ async function devCommand(rawArgs) {
|
|
|
689
884
|
} else if (!frontendOnly && !backendDir) {
|
|
690
885
|
console.warn(chalk.yellow(" ⚠ No backend/ directory found, skipping backend."));
|
|
691
886
|
}
|
|
692
|
-
if (!backendOnly && frontendDir) {
|
|
693
|
-
|
|
694
|
-
const frontendChild = execa(
|
|
695
|
-
"pnpm",
|
|
696
|
-
["run", "dev"],
|
|
697
|
-
{
|
|
698
|
-
cwd: frontendDir,
|
|
699
|
-
stdio: ["inherit", "pipe", "pipe"],
|
|
700
|
-
env: process.env,
|
|
701
|
-
shell: true
|
|
702
|
-
}
|
|
703
|
-
);
|
|
704
|
-
frontendChild.catch(() => {
|
|
705
|
-
});
|
|
706
|
-
frontendChild.stdout?.on("data", (data) => {
|
|
707
|
-
const lines = data.toString().split("\n").filter(Boolean);
|
|
708
|
-
lines.forEach((line) => {
|
|
709
|
-
console.log(`${chalk.magenta.bold("[frontend]")} ${line}`);
|
|
710
|
-
const cleanLine = stripAnsi(line);
|
|
711
|
-
const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
|
|
712
|
-
if (cleanLine.includes("Local:") && urlMatch) {
|
|
713
|
-
frontendUrl = urlMatch[1];
|
|
714
|
-
printSummary();
|
|
715
|
-
}
|
|
716
|
-
});
|
|
717
|
-
});
|
|
718
|
-
frontendChild.stderr?.on("data", (data) => {
|
|
719
|
-
const lines = data.toString().split("\n").filter(Boolean);
|
|
720
|
-
lines.forEach((line) => {
|
|
721
|
-
console.log(`${chalk.magenta.bold("[frontend]")} ${line}`);
|
|
722
|
-
});
|
|
723
|
-
});
|
|
724
|
-
children.push(frontendChild);
|
|
887
|
+
if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) {
|
|
888
|
+
startFrontend(null);
|
|
725
889
|
} else if (!backendOnly && !frontendDir) {
|
|
726
890
|
console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
|
|
727
891
|
}
|
|
@@ -750,11 +914,19 @@ ${chalk.green.bold("Usage")}
|
|
|
750
914
|
${chalk.green.bold("Options")}
|
|
751
915
|
${chalk.blue("--backend-only, -b")} Only start the backend server
|
|
752
916
|
${chalk.blue("--frontend-only, -f")} Only start the frontend server
|
|
753
|
-
${chalk.blue("--port, -p")} Backend port (default:
|
|
917
|
+
${chalk.blue("--port, -p")} Backend port (default: auto-detected per project)
|
|
754
918
|
|
|
755
919
|
${chalk.green.bold("Description")}
|
|
756
|
-
Starts both the backend (tsx watch +
|
|
920
|
+
Starts both the backend (tsx watch + Hono) and frontend (Vite)
|
|
757
921
|
dev servers concurrently with color-coded output prefixes.
|
|
922
|
+
|
|
923
|
+
Each project automatically receives a unique default port derived
|
|
924
|
+
from its directory path, preventing collisions when running multiple
|
|
925
|
+
Rebase instances simultaneously.
|
|
926
|
+
|
|
927
|
+
If the assigned port is already in use, the server will automatically
|
|
928
|
+
try the next available port. The frontend is started only after the
|
|
929
|
+
backend is ready, and VITE_API_URL is injected automatically.
|
|
758
930
|
`);
|
|
759
931
|
}
|
|
760
932
|
async function authCommand(subcommand, rawArgs) {
|
|
@@ -891,6 +1063,49 @@ ${chalk.green.bold("Examples")}
|
|
|
891
1063
|
rebase auth reset-password --email user@example.com --password MyNewPass!
|
|
892
1064
|
`);
|
|
893
1065
|
}
|
|
1066
|
+
async function doctorCommand(rawArgs) {
|
|
1067
|
+
const projectRoot = requireProjectRoot();
|
|
1068
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
1069
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
1070
|
+
if (!activePlugin) {
|
|
1071
|
+
console.error(chalk.red("✗ Could not detect an active database plugin."));
|
|
1072
|
+
console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgresql is installed in backend/package.json."));
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
|
|
1076
|
+
if (!pluginCli) {
|
|
1077
|
+
console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
|
|
1078
|
+
process.exit(1);
|
|
1079
|
+
}
|
|
1080
|
+
const envFile = findEnvFile(projectRoot);
|
|
1081
|
+
const env = { ...process.env };
|
|
1082
|
+
if (envFile) {
|
|
1083
|
+
env.DOTENV_CONFIG_PATH = envFile;
|
|
1084
|
+
}
|
|
1085
|
+
try {
|
|
1086
|
+
const isTs = pluginCli.endsWith(".ts");
|
|
1087
|
+
if (isTs) {
|
|
1088
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
1089
|
+
if (!tsxBin) {
|
|
1090
|
+
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
1091
|
+
process.exit(1);
|
|
1092
|
+
}
|
|
1093
|
+
await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
|
|
1094
|
+
cwd: backendDir,
|
|
1095
|
+
stdio: "inherit",
|
|
1096
|
+
env
|
|
1097
|
+
});
|
|
1098
|
+
} else {
|
|
1099
|
+
await execa("node", [pluginCli, ...rawArgs.slice(2)], {
|
|
1100
|
+
cwd: backendDir,
|
|
1101
|
+
stdio: "inherit",
|
|
1102
|
+
env
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
} catch {
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
894
1109
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
895
1110
|
const __dirname$1 = path.dirname(__filename$1);
|
|
896
1111
|
function getVersion() {
|
|
@@ -922,7 +1137,7 @@ async function entry(args) {
|
|
|
922
1137
|
}
|
|
923
1138
|
const command = parsedArgs._[0];
|
|
924
1139
|
const subcommand = parsedArgs._[1];
|
|
925
|
-
const namespacedCommands = ["schema", "db", "dev", "auth"];
|
|
1140
|
+
const namespacedCommands = ["schema", "db", "dev", "auth", "doctor"];
|
|
926
1141
|
if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
|
|
927
1142
|
printHelp();
|
|
928
1143
|
return;
|
|
@@ -932,7 +1147,7 @@ async function entry(args) {
|
|
|
932
1147
|
case "init":
|
|
933
1148
|
await createRebaseApp(args);
|
|
934
1149
|
break;
|
|
935
|
-
case "generate-sdk":
|
|
1150
|
+
case "generate-sdk": {
|
|
936
1151
|
const sdkArgs = arg(
|
|
937
1152
|
{
|
|
938
1153
|
"--collections-dir": String,
|
|
@@ -946,11 +1161,12 @@ async function entry(args) {
|
|
|
946
1161
|
}
|
|
947
1162
|
);
|
|
948
1163
|
await generateSdkCommand({
|
|
949
|
-
collectionsDir: sdkArgs["--collections-dir"] || "./
|
|
1164
|
+
collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
|
|
950
1165
|
output: sdkArgs["--output"] || "./generated/sdk",
|
|
951
1166
|
cwd: process.cwd()
|
|
952
1167
|
});
|
|
953
1168
|
break;
|
|
1169
|
+
}
|
|
954
1170
|
case "schema":
|
|
955
1171
|
await schemaCommand(effectiveSubcommand, args);
|
|
956
1172
|
break;
|
|
@@ -963,6 +1179,9 @@ async function entry(args) {
|
|
|
963
1179
|
case "auth":
|
|
964
1180
|
await authCommand(effectiveSubcommand, args);
|
|
965
1181
|
break;
|
|
1182
|
+
case "doctor":
|
|
1183
|
+
await doctorCommand(args);
|
|
1184
|
+
break;
|
|
966
1185
|
default:
|
|
967
1186
|
console.log(chalk.red(`Unknown command: ${command}`));
|
|
968
1187
|
console.log("");
|
|
@@ -999,6 +1218,9 @@ ${chalk.green.bold("Auth")}
|
|
|
999
1218
|
${chalk.blue.bold("auth reset-password")} Reset a user's password
|
|
1000
1219
|
${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
|
|
1001
1220
|
|
|
1221
|
+
${chalk.green.bold("Diagnostics")}
|
|
1222
|
+
${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
|
|
1223
|
+
|
|
1002
1224
|
${chalk.green.bold("Options")}
|
|
1003
1225
|
${chalk.blue("--version, -v")} Show version number
|
|
1004
1226
|
${chalk.blue("--help, -h")} Show this help message
|