@rebasepro/cli 0.0.1-canary.2 → 0.0.1-canary.4829d6e
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/LICENSE +17 -196
- package/README.md +57 -33
- package/dist/commands/api-keys.d.ts +1 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/build.d.ts +1 -0
- package/dist/commands/cloud/auth.d.ts +3 -0
- package/dist/commands/cloud/context.d.ts +61 -0
- package/dist/commands/cloud/databases.d.ts +1 -0
- package/dist/commands/cloud/deploy.d.ts +2 -0
- package/dist/commands/cloud/index.d.ts +1 -0
- package/dist/commands/cloud/link.d.ts +5 -0
- package/dist/commands/cloud/orgs.d.ts +1 -0
- package/dist/commands/cloud/projects.d.ts +13 -0
- package/dist/commands/cloud/resources.d.ts +6 -0
- package/dist/commands/db.d.ts +1 -0
- package/dist/commands/dev.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 +40 -0
- package/dist/commands/schema.d.ts +1 -0
- package/dist/commands/skills.d.ts +1 -0
- package/dist/commands/start.d.ts +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.es.js +3952 -245
- package/dist/index.es.js.map +1 -1
- package/dist/utils/package-manager.d.ts +33 -0
- package/dist/utils/project.d.ts +60 -0
- package/package.json +35 -23
- package/templates/overlays/baas/README.md +37 -0
- package/templates/overlays/baas/backend/package.json +31 -0
- package/templates/overlays/baas/backend/src/index.ts +172 -0
- package/templates/overlays/baas/backend/tsconfig.json +18 -0
- package/templates/overlays/baas/package.json +42 -0
- package/templates/overlays/baas/pnpm-workspace.yaml +9 -0
- package/templates/template/.cursorrules +2 -0
- package/templates/template/.dockerignore +28 -0
- package/templates/template/.env.example +120 -0
- package/templates/template/.github/copilot-instructions.md +2 -0
- package/templates/template/.windsurfrules +2 -0
- package/templates/template/AGENTS.md +2 -0
- package/templates/template/CLAUDE.md +2 -0
- package/templates/template/README.md +88 -21
- package/templates/template/ai-instructions.md +17 -0
- package/templates/template/backend/Dockerfile +57 -11
- package/templates/template/backend/functions/hello.ts +56 -0
- package/templates/template/backend/package.json +30 -34
- package/templates/template/backend/src/env.ts +23 -0
- package/templates/template/backend/src/index.ts +140 -100
- package/templates/template/backend/tsconfig.json +1 -1
- package/templates/template/config/collections/authors.ts +45 -0
- package/templates/template/config/collections/index.ts +26 -0
- package/templates/template/{shared → config}/collections/posts.ts +28 -10
- package/templates/template/config/collections/presets/blank/index.ts +3 -0
- package/templates/template/config/collections/presets/ecommerce/categories.ts +40 -0
- package/templates/template/config/collections/presets/ecommerce/index.ts +6 -0
- package/templates/template/config/collections/presets/ecommerce/orders.ts +66 -0
- package/templates/template/config/collections/presets/ecommerce/products.ts +64 -0
- package/templates/template/config/collections/tags.ts +25 -0
- package/templates/template/config/collections/users.ts +142 -0
- package/templates/template/{shared → config}/index.ts +1 -1
- package/templates/template/config/package.json +28 -0
- package/templates/template/docker-compose.yml +50 -14
- package/templates/template/frontend/Dockerfile +39 -15
- package/templates/template/frontend/nginx.conf +40 -0
- package/templates/template/frontend/package.json +13 -13
- package/templates/template/frontend/src/App.tsx +30 -116
- package/templates/template/frontend/src/index.css +15 -1
- package/templates/template/frontend/src/main.tsx +6 -2
- package/templates/template/frontend/src/virtual.d.ts +6 -0
- package/templates/template/frontend/tsconfig.json +3 -2
- package/templates/template/frontend/vite.config.ts +48 -5
- package/templates/template/package.json +21 -12
- package/templates/template/pnpm-workspace.yaml +13 -0
- package/templates/template/scripts/example.ts +94 -0
- package/dist/auth.d.ts +0 -5
- package/dist/index.cjs +0 -293
- package/dist/index.cjs.map +0 -1
- package/templates/template/.env.template +0 -31
- package/templates/template/backend/drizzle.config.ts +0 -22
- package/templates/template/backend/scripts/db-generate.ts +0 -60
- package/templates/template/shared/collections/index.ts +0 -3
- package/templates/template/shared/package.json +0 -28
- /package/templates/template/{shared → config}/tsconfig.json +0 -0
package/dist/index.es.js
CHANGED
|
@@ -3,222 +3,3978 @@ import arg from "arg";
|
|
|
3
3
|
import inquirer from "inquirer";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import fs from "fs";
|
|
6
|
+
import net from "net";
|
|
6
7
|
import { promisify } from "util";
|
|
7
|
-
import execa from "execa";
|
|
8
|
-
import
|
|
8
|
+
import { execa, execaCommandSync } from "execa";
|
|
9
|
+
import { cp } from "fs/promises";
|
|
9
10
|
import { fileURLToPath } from "url";
|
|
10
11
|
import crypto from "crypto";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
import { generateSDK } from "@rebasepro/codegen";
|
|
13
|
+
import { execSync, spawn } from "child_process";
|
|
14
|
+
import { createRequire } from "module";
|
|
15
|
+
import os from "os";
|
|
16
|
+
import { createRebaseClient } from "@rebasepro/client";
|
|
17
|
+
//#region src/utils/package-manager.ts
|
|
18
|
+
/**
|
|
19
|
+
* Package manager detection and command abstraction.
|
|
20
|
+
*
|
|
21
|
+
* Detects whether the user is running pnpm or npm and provides
|
|
22
|
+
* a unified interface for common package-manager operations so
|
|
23
|
+
* the rest of the CLI never has to hardcode a specific PM.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Detect the package manager from the environment or the target directory.
|
|
27
|
+
*
|
|
28
|
+
* Detection order:
|
|
29
|
+
* 1. Explicit override (if provided)
|
|
30
|
+
* 2. `npm_config_user_agent` env var (set by npm/pnpm when running via `npx`/`pnpm dlx`)
|
|
31
|
+
* 3. Lock-file presence in the target directory
|
|
32
|
+
* 4. Default to pnpm (Rebase's recommended PM)
|
|
33
|
+
*/
|
|
34
|
+
function detectPackageManager(targetDir) {
|
|
35
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
36
|
+
if (userAgent.startsWith("npm/")) return "npm";
|
|
37
|
+
if (userAgent.startsWith("pnpm/")) return "pnpm";
|
|
38
|
+
if (targetDir) {
|
|
39
|
+
if (fs.existsSync(path.join(targetDir, "package-lock.json"))) return "npm";
|
|
40
|
+
if (fs.existsSync(path.join(targetDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
41
|
+
}
|
|
42
|
+
const cwd = process.cwd();
|
|
43
|
+
if (cwd !== targetDir) {
|
|
44
|
+
if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm";
|
|
45
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
46
|
+
}
|
|
47
|
+
return "pnpm";
|
|
48
|
+
}
|
|
49
|
+
/** Build the command helpers for a given package manager. */
|
|
50
|
+
function getPMCommands(pm) {
|
|
51
|
+
if (pm === "npm") return {
|
|
52
|
+
name: "npm",
|
|
53
|
+
install: ["npm", "install"],
|
|
54
|
+
run: (script) => [
|
|
55
|
+
"npm",
|
|
56
|
+
"run",
|
|
57
|
+
script
|
|
58
|
+
],
|
|
59
|
+
exec: (bin, args) => [
|
|
60
|
+
"npx",
|
|
61
|
+
bin,
|
|
62
|
+
...args
|
|
63
|
+
],
|
|
64
|
+
view: (pkg, field) => [
|
|
65
|
+
"npm",
|
|
66
|
+
"view",
|
|
67
|
+
pkg,
|
|
68
|
+
field
|
|
69
|
+
],
|
|
70
|
+
runAll: (script) => [
|
|
71
|
+
"npm",
|
|
72
|
+
"run",
|
|
73
|
+
script,
|
|
74
|
+
"--workspaces",
|
|
75
|
+
"--if-present"
|
|
76
|
+
],
|
|
77
|
+
runWorkspace: (workspace, script) => [
|
|
78
|
+
"npm",
|
|
79
|
+
"run",
|
|
80
|
+
script,
|
|
81
|
+
"-w",
|
|
82
|
+
workspace
|
|
83
|
+
],
|
|
84
|
+
dlx: (pkg, args) => [
|
|
85
|
+
"npx",
|
|
86
|
+
"-y",
|
|
87
|
+
pkg,
|
|
88
|
+
...args
|
|
89
|
+
],
|
|
90
|
+
workspaceProtocol: "*"
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
name: "pnpm",
|
|
94
|
+
install: ["pnpm", "install"],
|
|
95
|
+
run: (script) => [
|
|
96
|
+
"pnpm",
|
|
97
|
+
"run",
|
|
98
|
+
script
|
|
99
|
+
],
|
|
100
|
+
exec: (bin, args) => [
|
|
101
|
+
"pnpm",
|
|
102
|
+
"exec",
|
|
103
|
+
bin,
|
|
104
|
+
...args
|
|
105
|
+
],
|
|
106
|
+
view: (pkg, field) => [
|
|
107
|
+
"pnpm",
|
|
108
|
+
"view",
|
|
109
|
+
pkg,
|
|
110
|
+
field
|
|
111
|
+
],
|
|
112
|
+
runAll: (script) => [
|
|
113
|
+
"pnpm",
|
|
114
|
+
"-r",
|
|
115
|
+
"run",
|
|
116
|
+
script
|
|
117
|
+
],
|
|
118
|
+
runWorkspace: (workspace, script) => [
|
|
119
|
+
"pnpm",
|
|
120
|
+
"--filter",
|
|
121
|
+
workspace,
|
|
122
|
+
script
|
|
123
|
+
],
|
|
124
|
+
dlx: (pkg, args) => [
|
|
125
|
+
"pnpm",
|
|
126
|
+
"dlx",
|
|
127
|
+
pkg,
|
|
128
|
+
...args
|
|
129
|
+
],
|
|
130
|
+
workspaceProtocol: "workspace:*"
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/commands/init.ts
|
|
135
|
+
var access = promisify(fs.access);
|
|
136
|
+
var __filename$1 = fileURLToPath(import.meta.url);
|
|
137
|
+
var __dirname$1 = path.dirname(__filename$1);
|
|
16
138
|
function findParentDir(currentDir, targetName) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
139
|
+
const root = path.parse(currentDir).root;
|
|
140
|
+
while (currentDir && currentDir !== root) {
|
|
141
|
+
if (path.basename(currentDir) === targetName) return currentDir;
|
|
142
|
+
currentDir = path.dirname(currentDir);
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
var cliRoot = findParentDir(__dirname$1, "cli");
|
|
147
|
+
var FLAVOR_CHOICES = [{
|
|
148
|
+
name: "BaaS + admin — API plus an admin UI, driven by collections you define (like Payload/Directus)",
|
|
149
|
+
value: "cms",
|
|
150
|
+
short: "BaaS + admin"
|
|
151
|
+
}, {
|
|
152
|
+
name: "BaaS only — headless API over your database. No collections, no UI (like Supabase)",
|
|
153
|
+
value: "baas",
|
|
154
|
+
short: "BaaS only"
|
|
155
|
+
}];
|
|
156
|
+
var PRESET_CHOICES = [
|
|
157
|
+
{
|
|
158
|
+
name: "Blog — Posts, Authors, Tags (with markdown editor)",
|
|
159
|
+
value: "blog",
|
|
160
|
+
short: "Blog"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "E-commerce — Products, Categories, Orders",
|
|
164
|
+
value: "ecommerce",
|
|
165
|
+
short: "E-commerce"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "Blank — Empty project, just authentication",
|
|
169
|
+
value: "blank",
|
|
170
|
+
short: "Blank"
|
|
171
|
+
}
|
|
172
|
+
];
|
|
173
|
+
/**
|
|
174
|
+
* Builds the interactive prompt questions for `rebase init`.
|
|
175
|
+
* Exported for testability — all prompt `type` values must match
|
|
176
|
+
* types registered by the installed version of inquirer.
|
|
177
|
+
*/
|
|
178
|
+
function buildInitQuestions(params) {
|
|
179
|
+
const { nameArg, templateArg, flavorArg, hasGitFlag, hasInstallFlag, pm } = params;
|
|
180
|
+
const questions = [];
|
|
181
|
+
if (!nameArg) questions.push({
|
|
182
|
+
type: "input",
|
|
183
|
+
name: "projectName",
|
|
184
|
+
message: "Project name:",
|
|
185
|
+
default: "my-rebase-app",
|
|
186
|
+
validate: (input) => {
|
|
187
|
+
if (!input.trim()) return "Project name is required";
|
|
188
|
+
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";
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
if (!flavorArg) questions.push({
|
|
193
|
+
type: "select",
|
|
194
|
+
name: "flavor",
|
|
195
|
+
message: "What do you want to build?",
|
|
196
|
+
choices: FLAVOR_CHOICES,
|
|
197
|
+
default: "cms"
|
|
198
|
+
});
|
|
199
|
+
if (!templateArg) questions.push({
|
|
200
|
+
type: "select",
|
|
201
|
+
name: "preset",
|
|
202
|
+
message: "Choose a starter template:",
|
|
203
|
+
choices: PRESET_CHOICES,
|
|
204
|
+
default: "blog",
|
|
205
|
+
when: (answers) => (flavorArg ?? answers.flavor) !== "baas"
|
|
206
|
+
});
|
|
207
|
+
if (!hasGitFlag) questions.push({
|
|
208
|
+
type: "confirm",
|
|
209
|
+
name: "git",
|
|
210
|
+
message: "Initialize a git repository?",
|
|
211
|
+
default: true
|
|
212
|
+
});
|
|
213
|
+
if (!hasInstallFlag) questions.push({
|
|
214
|
+
type: "confirm",
|
|
215
|
+
name: "installDeps",
|
|
216
|
+
message: `Install dependencies with ${pm}?`,
|
|
217
|
+
default: true
|
|
218
|
+
});
|
|
219
|
+
questions.push({
|
|
220
|
+
type: "input",
|
|
221
|
+
name: "databaseUrl",
|
|
222
|
+
message: "Enter your PostgreSQL database connection string (leave blank to use a local default):",
|
|
223
|
+
default: "",
|
|
224
|
+
validate: (input) => {
|
|
225
|
+
if (input.trim() && /[\r\n]/.test(input)) return "Database URL cannot contain newline characters.";
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
questions.push({
|
|
230
|
+
type: "confirm",
|
|
231
|
+
name: "introspect",
|
|
232
|
+
message: "Would you like to introspect this database to automatically generate collections?",
|
|
233
|
+
default: true,
|
|
234
|
+
when: (answers) => !!answers.databaseUrl?.trim()
|
|
235
|
+
});
|
|
236
|
+
return questions;
|
|
25
237
|
}
|
|
26
|
-
const cliRoot = findParentDir(__dirname$2, "cli");
|
|
27
238
|
async function createRebaseApp(rawArgs) {
|
|
28
|
-
|
|
239
|
+
console.log(`
|
|
29
240
|
${chalk.bold("Rebase")} — Create a new project 🚀
|
|
30
241
|
`);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
242
|
+
await createProject$1(await promptForOptions(rawArgs, detectPackageManager()));
|
|
243
|
+
}
|
|
244
|
+
async function promptForOptions(rawArgs, pm) {
|
|
245
|
+
const args = arg({
|
|
246
|
+
"--git": Boolean,
|
|
247
|
+
"--install": Boolean,
|
|
248
|
+
"--database-url": String,
|
|
249
|
+
"--introspect": Boolean,
|
|
250
|
+
"--template": String,
|
|
251
|
+
"--flavor": String,
|
|
252
|
+
"--yes": Boolean,
|
|
253
|
+
"-g": "--git",
|
|
254
|
+
"-i": "--install",
|
|
255
|
+
"-t": "--template",
|
|
256
|
+
"-f": "--flavor",
|
|
257
|
+
"-y": "--yes"
|
|
258
|
+
}, {
|
|
259
|
+
argv: rawArgs.slice(3),
|
|
260
|
+
permissive: true
|
|
261
|
+
});
|
|
262
|
+
const nameArg = args._[0];
|
|
263
|
+
const isNonInteractive = args["--yes"] || false;
|
|
264
|
+
const templateArg = args["--template"];
|
|
265
|
+
if (templateArg && !PRESET_CHOICES.some((p) => p.value === templateArg)) {
|
|
266
|
+
console.error(chalk.red(`Unknown template "${templateArg}". Available: ${PRESET_CHOICES.map((p) => p.value).join(", ")}`));
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
const flavorArg = args["--flavor"];
|
|
270
|
+
if (flavorArg && !FLAVOR_CHOICES.some((f) => f.value === flavorArg)) {
|
|
271
|
+
console.error(chalk.red(`Unknown flavor "${flavorArg}". Available: ${FLAVOR_CHOICES.map((f) => f.value).join(", ")}`));
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
if (isNonInteractive) {
|
|
275
|
+
const projectName = nameArg || "my-rebase-app";
|
|
276
|
+
const targetDirectory = path.resolve(process.cwd(), projectName);
|
|
277
|
+
const templateDirectory = path.resolve(cliRoot, "templates", "template");
|
|
278
|
+
const pmCommands = getPMCommands(pm);
|
|
279
|
+
return {
|
|
280
|
+
projectName: path.basename(targetDirectory),
|
|
281
|
+
git: args["--git"] ?? false,
|
|
282
|
+
installDeps: args["--install"] ?? false,
|
|
283
|
+
targetDirectory,
|
|
284
|
+
templateDirectory,
|
|
285
|
+
databaseUrl: args["--database-url"] || void 0,
|
|
286
|
+
introspect: args["--introspect"] || false,
|
|
287
|
+
preset: templateArg || "blog",
|
|
288
|
+
flavor: flavorArg || "cms",
|
|
289
|
+
pm,
|
|
290
|
+
pmCommands
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const questions = buildInitQuestions({
|
|
294
|
+
nameArg,
|
|
295
|
+
templateArg,
|
|
296
|
+
flavorArg,
|
|
297
|
+
hasGitFlag: !!args["--git"],
|
|
298
|
+
hasInstallFlag: !!args["--install"],
|
|
299
|
+
pm
|
|
300
|
+
});
|
|
301
|
+
const answers = await inquirer.prompt(questions);
|
|
302
|
+
const targetDirectory = path.resolve(process.cwd(), nameArg || answers.projectName);
|
|
303
|
+
const projectName = path.basename(targetDirectory);
|
|
304
|
+
const templateDirectory = path.resolve(cliRoot, "templates", "template");
|
|
305
|
+
const pmCommands = getPMCommands(pm);
|
|
306
|
+
return {
|
|
307
|
+
projectName,
|
|
308
|
+
git: args["--git"] || answers.git || false,
|
|
309
|
+
installDeps: args["--install"] || answers.installDeps || false,
|
|
310
|
+
targetDirectory,
|
|
311
|
+
templateDirectory,
|
|
312
|
+
databaseUrl: answers.databaseUrl?.trim() || void 0,
|
|
313
|
+
introspect: answers.introspect || false,
|
|
314
|
+
preset: templateArg || answers.preset || "blog",
|
|
315
|
+
flavor: flavorArg || answers.flavor || "cms",
|
|
316
|
+
pm,
|
|
317
|
+
pmCommands
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async function createProject$1(options) {
|
|
321
|
+
if (fs.existsSync(options.targetDirectory)) {
|
|
322
|
+
if (fs.readdirSync(options.targetDirectory).length !== 0) {
|
|
323
|
+
console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
} else fs.mkdirSync(options.targetDirectory, { recursive: true });
|
|
327
|
+
try {
|
|
328
|
+
await access(options.templateDirectory, fs.constants.R_OK);
|
|
329
|
+
} catch {
|
|
330
|
+
console.error(`${chalk.red.bold("ERROR")} Template not found at ${options.templateDirectory}`);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
console.log(chalk.gray(" Copying project files..."));
|
|
334
|
+
try {
|
|
335
|
+
await cp(options.templateDirectory, options.targetDirectory, {
|
|
336
|
+
recursive: true,
|
|
337
|
+
filter: (source) => {
|
|
338
|
+
const basename = path.basename(source);
|
|
339
|
+
return basename !== "node_modules" && basename !== ".DS_Store";
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.error(`${chalk.red.bold("ERROR")} Failed to copy template files: ${err instanceof Error ? err.message : String(err)}`);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
if (options.flavor !== "baas") await applyPreset(options.targetDirectory, options.preset);
|
|
347
|
+
await applyFlavor(options.targetDirectory, options.flavor);
|
|
348
|
+
await replacePlaceholders(options);
|
|
349
|
+
await configureEnvFile(options.targetDirectory, options.databaseUrl);
|
|
350
|
+
if (options.git) {
|
|
351
|
+
console.log(chalk.gray(" Initializing git repository..."));
|
|
352
|
+
try {
|
|
353
|
+
await execa("git", ["init"], { cwd: options.targetDirectory });
|
|
354
|
+
} catch {
|
|
355
|
+
console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const { pm, pmCommands } = options;
|
|
359
|
+
const installCmd = pmCommands.install;
|
|
360
|
+
const execCmd = pmCommands.exec("rebase", [
|
|
361
|
+
"schema",
|
|
362
|
+
"introspect",
|
|
363
|
+
"--force"
|
|
364
|
+
]);
|
|
365
|
+
if (options.installDeps) {
|
|
366
|
+
console.log("");
|
|
367
|
+
console.log(chalk.gray(` Installing dependencies with ${pm}...`));
|
|
368
|
+
console.log("");
|
|
369
|
+
try {
|
|
370
|
+
await execa(installCmd[0], installCmd.slice(1), {
|
|
371
|
+
cwd: options.targetDirectory,
|
|
372
|
+
stdio: "inherit"
|
|
373
|
+
});
|
|
374
|
+
} catch {
|
|
375
|
+
console.warn(chalk.yellow(` Warning: Failed to install dependencies. You may need to run \`${installCmd.join(" ")}\` manually.`));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (options.introspect) {
|
|
379
|
+
console.log("");
|
|
380
|
+
if (options.installDeps) {
|
|
381
|
+
console.log(chalk.gray(" Introspecting database and generating collections..."));
|
|
382
|
+
console.log("");
|
|
383
|
+
try {
|
|
384
|
+
await execa(execCmd[0], execCmd.slice(1), {
|
|
385
|
+
cwd: options.targetDirectory,
|
|
386
|
+
stdio: "inherit"
|
|
387
|
+
});
|
|
388
|
+
console.log(chalk.green(" Database successfully introspected!"));
|
|
389
|
+
} catch {
|
|
390
|
+
console.warn(chalk.yellow(" Warning: Failed to introspect database automatically."));
|
|
391
|
+
console.warn(chalk.yellow(` You can run \`${execCmd.join(" ")}\` manually after setup.`));
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
console.warn(chalk.yellow(" Skipping introspection because dependencies were not installed."));
|
|
395
|
+
console.warn(chalk.yellow(` Run \`${installCmd.join(" ")}\` then \`${execCmd.join(" ")}\` manually.`));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
console.log("");
|
|
399
|
+
console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
|
|
400
|
+
console.log("");
|
|
401
|
+
console.log(chalk.bold("Next steps:"));
|
|
402
|
+
console.log("");
|
|
403
|
+
const runDev = pmCommands.run("dev");
|
|
404
|
+
const runDbPush = pmCommands.run("db:push");
|
|
405
|
+
const isBaas = options.flavor === "baas";
|
|
406
|
+
console.log(` ${chalk.cyan("cd")} ${options.projectName}`);
|
|
407
|
+
if (!options.installDeps) console.log(` ${chalk.cyan(installCmd.join(" "))}`);
|
|
408
|
+
console.log("");
|
|
409
|
+
if (options.databaseUrl) if (options.introspect) {
|
|
410
|
+
console.log(chalk.gray(" # Database has been introspected & collections generated!"));
|
|
411
|
+
console.log(chalk.gray(" # Start the development server (frontend + backend):"));
|
|
412
|
+
console.log(` ${chalk.cyan(runDev.join(" "))}`);
|
|
413
|
+
} else {
|
|
414
|
+
console.log(chalk.gray(" # Your custom database is configured in .env."));
|
|
415
|
+
console.log(chalk.gray(" # If the database is empty, push the Rebase schema to initialize it:"));
|
|
416
|
+
console.log(` ${chalk.cyan(runDbPush.join(" "))}`);
|
|
417
|
+
console.log("");
|
|
418
|
+
console.log(chalk.gray(" # Then start the development server:"));
|
|
419
|
+
console.log(` ${chalk.cyan(runDev.join(" "))}`);
|
|
420
|
+
}
|
|
421
|
+
else if (isBaas) {
|
|
422
|
+
console.log(chalk.gray(" # A local database configuration has been generated in .env."));
|
|
423
|
+
console.log(chalk.gray(" # 1. Start the PostgreSQL database container:"));
|
|
424
|
+
console.log(` ${chalk.cyan("docker compose up -d db")}`);
|
|
425
|
+
console.log("");
|
|
426
|
+
console.log(chalk.gray(" # 2. Create your tables (migrations, SQL, any tool you like)."));
|
|
427
|
+
console.log("");
|
|
428
|
+
console.log(chalk.gray(" # 3. Start the API — every table is served automatically:"));
|
|
429
|
+
console.log(` ${chalk.cyan(runDev.join(" "))}`);
|
|
430
|
+
} else {
|
|
431
|
+
console.log(chalk.gray(" # A local database configuration has been generated in .env."));
|
|
432
|
+
console.log(chalk.gray(" # 1. Start the PostgreSQL database container:"));
|
|
433
|
+
console.log(` ${chalk.cyan("docker compose up -d db")}`);
|
|
434
|
+
console.log("");
|
|
435
|
+
console.log(chalk.gray(" # 2. Push the Rebase schema to initialize database tables:"));
|
|
436
|
+
console.log(` ${chalk.cyan(runDbPush.join(" "))}`);
|
|
437
|
+
console.log("");
|
|
438
|
+
console.log(chalk.gray(" # 3. Start the development server (frontend + backend):"));
|
|
439
|
+
console.log(` ${chalk.cyan(runDev.join(" "))}`);
|
|
440
|
+
}
|
|
441
|
+
console.log("");
|
|
442
|
+
console.log(isBaas ? chalk.gray("This starts a headless API (Hono + PostgreSQL). There are no collection files: ") + chalk.gray("the API is derived from your database schema. Docs at /api/swagger.") : chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
|
|
443
|
+
console.log("");
|
|
444
|
+
console.log(chalk.gray("Docs: https://rebase.pro/docs"));
|
|
445
|
+
console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
|
|
446
|
+
console.log("");
|
|
447
|
+
console.log(chalk.bold("🤖 AI Agent Skills"));
|
|
448
|
+
console.log("");
|
|
449
|
+
console.log(chalk.gray(" Install Rebase agent skills for your AI coding assistant:"));
|
|
450
|
+
console.log("");
|
|
451
|
+
console.log(` ${chalk.cyan("rebase skills install")} ${chalk.gray("or")} ${chalk.cyan(pmCommands.run("skills:install").join(" "))}`);
|
|
452
|
+
console.log("");
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Apply a template preset by replacing the default collection files.
|
|
456
|
+
*
|
|
457
|
+
* The template ships with blog collections at the top level and
|
|
458
|
+
* preset alternatives under `config/collections/presets/<name>/`.
|
|
459
|
+
* This function swaps the active collection files and removes the
|
|
460
|
+
* presets directory so the final project is clean.
|
|
461
|
+
*/
|
|
462
|
+
/**
|
|
463
|
+
* Reduce the scaffolded project to the chosen flavor.
|
|
464
|
+
*
|
|
465
|
+
* The base template is the full CMS triad. `baas` drops the frontend and the
|
|
466
|
+
* collections config entirely — there is nothing to define, since the server
|
|
467
|
+
* derives its API from the database — and overlays the files that differ.
|
|
468
|
+
*/
|
|
469
|
+
async function applyFlavor(targetDirectory, flavor) {
|
|
470
|
+
if (flavor !== "baas") return;
|
|
471
|
+
for (const dir of ["frontend", "config"]) fs.rmSync(path.join(targetDirectory, dir), {
|
|
472
|
+
recursive: true,
|
|
473
|
+
force: true
|
|
474
|
+
});
|
|
475
|
+
fs.rmSync(path.join(targetDirectory, "backend", "src", "schema.generated.ts"), { force: true });
|
|
476
|
+
const overlayDir = path.resolve(cliRoot, "templates", "overlays", "baas");
|
|
477
|
+
if (!fs.existsSync(overlayDir)) {
|
|
478
|
+
console.error(`${chalk.red.bold("ERROR")} BaaS template overlay not found at ${overlayDir}`);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
await cp(overlayDir, targetDirectory, {
|
|
482
|
+
recursive: true,
|
|
483
|
+
force: true,
|
|
484
|
+
filter: (source) => {
|
|
485
|
+
const basename = path.basename(source);
|
|
486
|
+
return basename !== "node_modules" && basename !== ".DS_Store";
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
async function applyPreset(targetDirectory, preset) {
|
|
491
|
+
const collectionsDir = path.join(targetDirectory, "config", "collections");
|
|
492
|
+
const presetsDir = path.join(collectionsDir, "presets");
|
|
493
|
+
if (preset !== "blog") {
|
|
494
|
+
const presetDir = path.join(presetsDir, preset);
|
|
495
|
+
if (!fs.existsSync(presetDir)) {
|
|
496
|
+
console.warn(chalk.yellow(` Warning: Preset "${preset}" not found, falling back to blog template.`));
|
|
497
|
+
cleanupPresets(presetsDir);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
for (const file of [
|
|
501
|
+
"posts.ts",
|
|
502
|
+
"authors.ts",
|
|
503
|
+
"tags.ts",
|
|
504
|
+
"index.ts"
|
|
505
|
+
]) {
|
|
506
|
+
const filePath = path.join(collectionsDir, file);
|
|
507
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
508
|
+
}
|
|
509
|
+
const presetFiles = fs.readdirSync(presetDir).filter((f) => f.endsWith(".ts"));
|
|
510
|
+
for (const file of presetFiles) fs.copyFileSync(path.join(presetDir, file), path.join(collectionsDir, file));
|
|
511
|
+
}
|
|
512
|
+
cleanupPresets(presetsDir);
|
|
513
|
+
}
|
|
514
|
+
function cleanupPresets(presetsDir) {
|
|
515
|
+
if (fs.existsSync(presetsDir)) fs.rmSync(presetsDir, {
|
|
516
|
+
recursive: true,
|
|
517
|
+
force: true
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
async function replacePlaceholders(options) {
|
|
521
|
+
const filesToProcess = [
|
|
522
|
+
"package.json",
|
|
523
|
+
"frontend/package.json",
|
|
524
|
+
"backend/package.json",
|
|
525
|
+
"config/package.json",
|
|
526
|
+
"frontend/index.html",
|
|
527
|
+
"pnpm-workspace.yaml",
|
|
528
|
+
"README.md"
|
|
529
|
+
];
|
|
530
|
+
const packageJsonPath = path.resolve(cliRoot, "package.json");
|
|
531
|
+
let cliVersion = "latest";
|
|
532
|
+
if (fs.existsSync(packageJsonPath)) cliVersion = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).version || "latest";
|
|
533
|
+
const versionCache = /* @__PURE__ */ new Map();
|
|
534
|
+
const viewBin = "npm";
|
|
535
|
+
const getPackageVersion = async (pkgName) => {
|
|
536
|
+
if (versionCache.has(pkgName)) return versionCache.get(pkgName);
|
|
537
|
+
if (process.env.REBASE_E2E === "true") {
|
|
538
|
+
versionCache.set(pkgName, cliVersion);
|
|
539
|
+
return cliVersion;
|
|
540
|
+
}
|
|
541
|
+
let versionToUse = cliVersion;
|
|
542
|
+
try {
|
|
543
|
+
const { stdout } = await execa(viewBin, [
|
|
544
|
+
"view",
|
|
545
|
+
`${pkgName}@${cliVersion}`,
|
|
546
|
+
"version"
|
|
547
|
+
]);
|
|
548
|
+
if (!stdout.trim()) throw new Error("Not found");
|
|
549
|
+
versionToUse = stdout.trim();
|
|
550
|
+
} catch {
|
|
551
|
+
try {
|
|
552
|
+
const { stdout } = await execa(viewBin, [
|
|
553
|
+
"view",
|
|
554
|
+
`${pkgName}@${cliVersion.includes("canary") ? "canary" : "latest"}`,
|
|
555
|
+
"version"
|
|
556
|
+
]);
|
|
557
|
+
if (!stdout.trim()) throw new Error("Not found");
|
|
558
|
+
versionToUse = stdout.trim();
|
|
559
|
+
} catch {
|
|
560
|
+
try {
|
|
561
|
+
const { stdout } = await execa(viewBin, [
|
|
562
|
+
"view",
|
|
563
|
+
pkgName,
|
|
564
|
+
"version"
|
|
565
|
+
]);
|
|
566
|
+
versionToUse = stdout.trim() || "latest";
|
|
567
|
+
} catch {
|
|
568
|
+
versionToUse = "latest";
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
versionCache.set(pkgName, versionToUse);
|
|
573
|
+
return versionToUse;
|
|
574
|
+
};
|
|
575
|
+
const allPackages = /* @__PURE__ */ new Set();
|
|
576
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
577
|
+
for (const file of filesToProcess) {
|
|
578
|
+
const fullPath = path.resolve(options.targetDirectory, file);
|
|
579
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
580
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
581
|
+
fileContents.set(fullPath, content);
|
|
582
|
+
const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
|
|
583
|
+
for (const match of matches) allPackages.add(match[1]);
|
|
584
|
+
}
|
|
585
|
+
console.log(chalk.gray(" Resolving package versions..."));
|
|
586
|
+
await Promise.all(Array.from(allPackages).map(getPackageVersion));
|
|
587
|
+
for (const [fullPath, originalContent] of fileContents.entries()) {
|
|
588
|
+
let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
|
|
589
|
+
const matches = [...content.matchAll(/"(@rebasepro\/[^"]+)":\s*"workspace:\*"/g)];
|
|
590
|
+
for (const match of matches) {
|
|
591
|
+
const pkgName = match[1];
|
|
592
|
+
const resolvedVersion = versionCache.get(pkgName) || "latest";
|
|
593
|
+
content = content.replace(new RegExp(`"${pkgName}":\\s*"workspace:\\*"`, "g"), `"${pkgName}": "${resolvedVersion}"`);
|
|
594
|
+
}
|
|
595
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async function isPortAvailable(port) {
|
|
599
|
+
return new Promise((resolve) => {
|
|
600
|
+
const server = net.createServer();
|
|
601
|
+
server.once("error", () => {
|
|
602
|
+
resolve(false);
|
|
603
|
+
});
|
|
604
|
+
server.once("listening", () => {
|
|
605
|
+
server.close(() => resolve(true));
|
|
606
|
+
});
|
|
607
|
+
server.listen(port);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
async function findAvailablePort(startPort) {
|
|
611
|
+
let port = startPort;
|
|
612
|
+
while (!await isPortAvailable(port)) port++;
|
|
613
|
+
return port;
|
|
614
|
+
}
|
|
615
|
+
async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
616
|
+
const envExamplePath = path.join(targetDirectory, ".env.example");
|
|
617
|
+
const envPath = path.join(targetDirectory, ".env");
|
|
618
|
+
if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
|
|
619
|
+
fs.copyFileSync(envExamplePath, envPath);
|
|
620
|
+
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
621
|
+
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
622
|
+
let envContent = fs.readFileSync(envPath, "utf-8");
|
|
623
|
+
envContent = envContent.replace(/^JWT_SECRET=.*$/m, `JWT_SECRET=${jwtSecret}`);
|
|
624
|
+
if (databaseUrl) {
|
|
625
|
+
if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
|
|
626
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${databaseUrl}`);
|
|
627
|
+
} else {
|
|
628
|
+
const dbPort = await findAvailablePort(5432);
|
|
629
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public\nDATABASE_PASSWORD=${dbPassword}`);
|
|
630
|
+
const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
|
|
631
|
+
if (fs.existsSync(dockerComposePath)) {
|
|
632
|
+
let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
|
|
633
|
+
dockerComposeContent = dockerComposeContent.replace(/-\s*"5432:5432"/g, `- "${dbPort}:5432"`);
|
|
634
|
+
fs.writeFileSync(dockerComposePath, dockerComposeContent, "utf-8");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
fs.writeFileSync(envPath, envContent, "utf-8");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/commands/generate_sdk.ts
|
|
642
|
+
/**
|
|
643
|
+
* CLI command: generate-sdk
|
|
644
|
+
*
|
|
645
|
+
* Reads collection definitions from a specified directory (default: ./config/collections),
|
|
646
|
+
* generates a typed TypeScript SDK, and writes it to the output directory (default: ./generated/sdk).
|
|
647
|
+
*
|
|
648
|
+
* Uses jiti for dynamic TypeScript import of collection files.
|
|
649
|
+
*/
|
|
650
|
+
/**
|
|
651
|
+
* Dynamically load collection definitions from a directory.
|
|
652
|
+
*
|
|
653
|
+
* Expects the directory to have an index.ts/index.js that exports a default
|
|
654
|
+
* array of CollectionConfig objects (matching the app/config/collections pattern).
|
|
655
|
+
*/
|
|
656
|
+
async function loadCollections(collectionsDir) {
|
|
657
|
+
const absDir = path.resolve(collectionsDir);
|
|
658
|
+
if (!fs.existsSync(absDir)) throw new Error(`Collections directory not found: ${absDir}`);
|
|
659
|
+
let jiti;
|
|
660
|
+
try {
|
|
661
|
+
const jitiModule = await import("jiti");
|
|
662
|
+
jiti = jitiModule.default || jitiModule;
|
|
663
|
+
} catch {
|
|
664
|
+
const installCmd = [
|
|
665
|
+
...getPMCommands(detectPackageManager()).install,
|
|
666
|
+
"-D",
|
|
667
|
+
"jiti"
|
|
668
|
+
].join(" ");
|
|
669
|
+
throw new Error(`Could not load 'jiti'. Install it with: ${installCmd}\njiti is required to dynamically import TypeScript collection definitions.`);
|
|
670
|
+
}
|
|
671
|
+
const jitiInstance = jiti(absDir, {
|
|
672
|
+
interopDefault: true,
|
|
673
|
+
esmResolve: true
|
|
674
|
+
});
|
|
675
|
+
const indexCandidates = [
|
|
676
|
+
"index.ts",
|
|
677
|
+
"index.js",
|
|
678
|
+
"index.mjs"
|
|
679
|
+
];
|
|
680
|
+
let indexPath = null;
|
|
681
|
+
for (const candidate of indexCandidates) {
|
|
682
|
+
const p = path.join(absDir, candidate);
|
|
683
|
+
if (fs.existsSync(p)) {
|
|
684
|
+
indexPath = p;
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (!indexPath) {
|
|
689
|
+
console.log(chalk.yellow(" No index file found, scanning individual collection files..."));
|
|
690
|
+
const collections = [];
|
|
691
|
+
const files = fs.readdirSync(absDir).filter((f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.startsWith("."));
|
|
692
|
+
for (const file of files) try {
|
|
693
|
+
const mod = jitiInstance(path.join(absDir, file));
|
|
694
|
+
const exported = mod.default || mod;
|
|
695
|
+
if (exported && typeof exported === "object" && "slug" in exported) collections.push(exported);
|
|
696
|
+
else if (Array.isArray(exported)) collections.push(...exported);
|
|
697
|
+
} catch (err) {
|
|
698
|
+
console.warn(chalk.yellow(` ⚠ Skipping ${file}: ${err.message}`));
|
|
699
|
+
}
|
|
700
|
+
return collections;
|
|
701
|
+
}
|
|
702
|
+
const mod = jitiInstance(indexPath);
|
|
703
|
+
const exported = mod.default || mod;
|
|
704
|
+
if (Array.isArray(exported)) return exported;
|
|
705
|
+
else if (typeof exported === "object" && exported !== null) {
|
|
706
|
+
if ("collections" in exported && Array.isArray(exported.collections)) return exported.collections;
|
|
707
|
+
const collections = [];
|
|
708
|
+
for (const value of Object.values(exported)) if (value && typeof value === "object" && "slug" in value) collections.push(value);
|
|
709
|
+
if (collections.length > 0) return collections;
|
|
710
|
+
}
|
|
711
|
+
throw new Error(`Could not extract collections from ${indexPath}.\nExpected a default export of CollectionConfig[] or an object with named collection exports.`);
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Write generated files to the output directory.
|
|
715
|
+
*/
|
|
716
|
+
function writeFiles(outputDir, files) {
|
|
717
|
+
const absOutput = path.resolve(outputDir);
|
|
718
|
+
fs.mkdirSync(absOutput, { recursive: true });
|
|
719
|
+
for (const file of files) {
|
|
720
|
+
const filePath = path.join(absOutput, file.path);
|
|
721
|
+
const dir = path.dirname(filePath);
|
|
722
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
723
|
+
fs.writeFileSync(filePath, file.content, "utf-8");
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Main entry point for the generate-sdk command.
|
|
728
|
+
*/
|
|
729
|
+
async function generateSdkCommand(args) {
|
|
730
|
+
const { collectionsDir, output, cwd } = args;
|
|
731
|
+
const resolvedCollectionsDir = path.isAbsolute(collectionsDir) ? collectionsDir : path.join(cwd, collectionsDir);
|
|
732
|
+
const resolvedOutput = path.isAbsolute(output) ? output : path.join(cwd, output);
|
|
733
|
+
console.log("");
|
|
734
|
+
console.log(chalk.bold(" 🔧 Rebase SDK Generator"));
|
|
735
|
+
console.log("");
|
|
736
|
+
console.log(` ${chalk.gray("Collections:")} ${resolvedCollectionsDir}`);
|
|
737
|
+
console.log(` ${chalk.gray("Output:")} ${resolvedOutput}`);
|
|
738
|
+
console.log("");
|
|
739
|
+
console.log(chalk.cyan(" → Loading collection definitions..."));
|
|
740
|
+
const collections = await loadCollections(resolvedCollectionsDir);
|
|
741
|
+
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
742
|
+
if (collections.length === 0) {
|
|
743
|
+
console.log(chalk.red(" ✗ No collections found. Nothing to generate."));
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
|
|
747
|
+
console.log("");
|
|
748
|
+
console.log(chalk.cyan(" → Generating SDK files..."));
|
|
749
|
+
const files = generateSDK(collections);
|
|
750
|
+
console.log(chalk.green(` ✓ Generated ${files.length} file(s)`));
|
|
751
|
+
console.log(chalk.cyan(` → Writing to ${resolvedOutput}...`));
|
|
752
|
+
writeFiles(resolvedOutput, files);
|
|
753
|
+
console.log("");
|
|
754
|
+
console.log(chalk.green.bold(" ✓ SDK generated successfully!"));
|
|
755
|
+
console.log("");
|
|
756
|
+
console.log(chalk.gray(" Usage:"));
|
|
757
|
+
console.log(chalk.gray(" import { createRebaseClient } from '@rebasepro/client';"));
|
|
758
|
+
console.log(chalk.gray(` import type { Database } from './${path.relative(cwd, path.join(resolvedOutput, "database.types"))}';`));
|
|
759
|
+
console.log("");
|
|
760
|
+
console.log(chalk.gray(" const rebase = createRebaseClient<Database>({"));
|
|
761
|
+
console.log(chalk.gray(" baseUrl: 'http://localhost:3001',"));
|
|
762
|
+
console.log(chalk.gray(" // token: 'your-jwt-token',"));
|
|
763
|
+
console.log(chalk.gray(" });"));
|
|
764
|
+
console.log("");
|
|
765
|
+
console.log(chalk.gray(` const { data } = await rebase.collection('${collections[0]?.slug || "my_collection"}').find();`));
|
|
766
|
+
console.log("");
|
|
767
|
+
}
|
|
768
|
+
//#endregion
|
|
769
|
+
//#region src/utils/project.ts
|
|
770
|
+
/**
|
|
771
|
+
* Project discovery utilities for the Rebase CLI.
|
|
772
|
+
*
|
|
773
|
+
* These helpers locate the project root, backend directory, .env file,
|
|
774
|
+
* and local binaries — used by all CLI command modules.
|
|
775
|
+
*/
|
|
776
|
+
/**
|
|
777
|
+
* Walk up from `startDir` to find the Rebase project root.
|
|
778
|
+
*
|
|
779
|
+
* The root is identified by a `package.json` that either:
|
|
780
|
+
* - has `workspaces` containing "backend" or "frontend", OR
|
|
781
|
+
* - has a sibling `backend/` directory
|
|
782
|
+
*/
|
|
783
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
784
|
+
let dir = path.resolve(startDir);
|
|
785
|
+
const root = path.parse(dir).root;
|
|
786
|
+
while (dir !== root) {
|
|
787
|
+
const pkgPath = path.join(dir, "package.json");
|
|
788
|
+
if (fs.existsSync(pkgPath)) {
|
|
789
|
+
try {
|
|
790
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
791
|
+
if (pkg.workspaces && Array.isArray(pkg.workspaces)) {
|
|
792
|
+
if (pkg.workspaces.some((w) => w === "backend")) return dir;
|
|
793
|
+
}
|
|
794
|
+
} catch {}
|
|
795
|
+
if (fs.existsSync(path.join(dir, "backend")) && fs.existsSync(path.join(dir, "config"))) return dir;
|
|
796
|
+
}
|
|
797
|
+
dir = path.dirname(dir);
|
|
798
|
+
}
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Locate the backend directory within the project root.
|
|
803
|
+
*/
|
|
804
|
+
function findBackendDir(projectRoot) {
|
|
805
|
+
const backendDir = path.join(projectRoot, "backend");
|
|
806
|
+
return fs.existsSync(backendDir) ? backendDir : null;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Detect the active backend plugin (e.g. @rebasepro/server-postgres) from the backend's package.json.
|
|
810
|
+
*/
|
|
811
|
+
function getActiveBackendPlugin(backendDir) {
|
|
812
|
+
const pkgPath = path.join(backendDir, "package.json");
|
|
813
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
814
|
+
try {
|
|
815
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
816
|
+
const deps = {
|
|
817
|
+
...pkg.dependencies,
|
|
818
|
+
...pkg.devDependencies
|
|
819
|
+
};
|
|
820
|
+
const candidates = Object.keys(deps).filter((dep) => dep.startsWith("@rebasepro/server-") && dep !== "@rebasepro/server");
|
|
821
|
+
if (candidates.length === 0) return null;
|
|
822
|
+
if (candidates.includes("@rebasepro/server-postgres")) return "@rebasepro/server-postgres";
|
|
823
|
+
for (const candidate of candidates) if (resolvePluginCliScript(backendDir, candidate)) return candidate;
|
|
824
|
+
return candidates[0];
|
|
825
|
+
} catch {}
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Resolve the active plugin's CLI script.
|
|
830
|
+
*/
|
|
831
|
+
function resolvePluginCliScript(backendDir, pluginName) {
|
|
832
|
+
const candidates = [
|
|
833
|
+
path.join(backendDir, "node_modules", pluginName, "src", "cli.ts"),
|
|
834
|
+
path.join(backendDir, "node_modules", pluginName, "dist", "cli.js"),
|
|
835
|
+
path.resolve(backendDir, "..", "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
|
|
836
|
+
path.resolve(backendDir, "..", "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts"),
|
|
837
|
+
path.resolve(backendDir, "..", "packages", pluginName.replace("@rebasepro/", ""), "src", "cli.ts")
|
|
838
|
+
];
|
|
839
|
+
for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Locate the frontend directory within the project root.
|
|
844
|
+
*/
|
|
845
|
+
function findFrontendDir(projectRoot) {
|
|
846
|
+
const frontendDir = path.join(projectRoot, "frontend");
|
|
847
|
+
return fs.existsSync(frontendDir) ? frontendDir : null;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Find the .env file. Checks the project root first, then backend.
|
|
851
|
+
*/
|
|
852
|
+
function findEnvFile(projectRoot) {
|
|
853
|
+
const candidates = [path.join(projectRoot, ".env"), path.join(projectRoot, "backend", ".env")];
|
|
854
|
+
for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Resolve a binary from the project's node_modules/.bin.
|
|
859
|
+
* Checks backend, root, parent monorepo root, then falls back to PATH.
|
|
860
|
+
*/
|
|
861
|
+
function resolveLocalBin(projectRoot, binName) {
|
|
862
|
+
const candidates = [path.join(projectRoot, "backend", "node_modules", ".bin", binName), path.join(projectRoot, "node_modules", ".bin", binName)];
|
|
863
|
+
let parent = path.dirname(projectRoot);
|
|
864
|
+
const rootDir = path.parse(parent).root;
|
|
865
|
+
while (parent !== rootDir) {
|
|
866
|
+
candidates.push(path.join(parent, "node_modules", ".bin", binName));
|
|
867
|
+
parent = path.dirname(parent);
|
|
868
|
+
}
|
|
869
|
+
for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
|
|
870
|
+
try {
|
|
871
|
+
const globalPath = execSync(`which ${binName}`, { encoding: "utf-8" }).trim();
|
|
872
|
+
if (globalPath && fs.existsSync(globalPath)) return globalPath;
|
|
873
|
+
} catch {}
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Resolve the tsx binary. Checks backend node_modules first, then root.
|
|
878
|
+
*/
|
|
879
|
+
function resolveTsx(projectRoot) {
|
|
880
|
+
return resolveLocalBin(projectRoot, "tsx");
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Validate that a resolved tsx binary actually has an intact installation.
|
|
884
|
+
*
|
|
885
|
+
* `resolveLocalBin` only checks whether `node_modules/.bin/tsx` (a symlink)
|
|
886
|
+
* exists. If the pnpm content-addressable store was cleaned or a previous
|
|
887
|
+
* install was interrupted, the symlink can exist while critical files inside
|
|
888
|
+
* the tsx package (e.g. `dist/preflight.cjs`) are missing — causing a
|
|
889
|
+
* confusing MODULE_NOT_FOUND error at runtime.
|
|
890
|
+
*
|
|
891
|
+
* This function follows the symlink, walks up to find the tsx package root
|
|
892
|
+
* (`package.json` with `name: "tsx"`), and verifies that `dist/preflight.cjs`
|
|
893
|
+
* is present. Returns `null` when the installation looks healthy, or an
|
|
894
|
+
* error description string when it appears corrupted.
|
|
895
|
+
*/
|
|
896
|
+
function validateTsxInstallation(tsxBinPath) {
|
|
897
|
+
try {
|
|
898
|
+
const realPath = fs.realpathSync(tsxBinPath);
|
|
899
|
+
let dir = path.dirname(realPath);
|
|
900
|
+
const fsRoot = path.parse(dir).root;
|
|
901
|
+
for (let depth = 0; depth < 10 && dir !== fsRoot; depth++) {
|
|
902
|
+
const pkgPath = path.join(dir, "package.json");
|
|
903
|
+
if (fs.existsSync(pkgPath)) try {
|
|
904
|
+
if (JSON.parse(fs.readFileSync(pkgPath, "utf-8")).name === "tsx") {
|
|
905
|
+
const preflightPath = path.join(dir, "dist", "preflight.cjs");
|
|
906
|
+
if (!fs.existsSync(preflightPath)) return `tsx package at ${dir} is missing dist/preflight.cjs`;
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
} catch {}
|
|
910
|
+
dir = path.dirname(dir);
|
|
911
|
+
}
|
|
912
|
+
return null;
|
|
913
|
+
} catch (err) {
|
|
914
|
+
return `tsx binary symlink is broken: ${err instanceof Error ? err.message : String(err)}`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Require the project root or exit with a helpful error.
|
|
919
|
+
*/
|
|
920
|
+
function requireProjectRoot() {
|
|
921
|
+
const root = findProjectRoot();
|
|
922
|
+
if (!root) {
|
|
923
|
+
console.error(chalk.red("✗ Could not find a Rebase project root."));
|
|
924
|
+
console.error(chalk.gray(" Make sure you are inside a Rebase project directory"));
|
|
925
|
+
console.error(chalk.gray(" (one with backend/, frontend/, and config/ directories)."));
|
|
926
|
+
process.exit(1);
|
|
927
|
+
}
|
|
928
|
+
return root;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Require the backend directory or exit with a helpful error.
|
|
932
|
+
*/
|
|
933
|
+
function requireBackendDir(projectRoot) {
|
|
934
|
+
const backendDir = findBackendDir(projectRoot);
|
|
935
|
+
if (!backendDir) {
|
|
936
|
+
console.error(chalk.red("✗ Could not find a backend/ directory."));
|
|
937
|
+
console.error(chalk.gray(` Expected at: ${path.join(projectRoot, "backend")}`));
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
return backendDir;
|
|
941
|
+
}
|
|
942
|
+
//#endregion
|
|
943
|
+
//#region src/commands/schema.ts
|
|
944
|
+
/**
|
|
945
|
+
* CLI command: rebase schema <action>
|
|
946
|
+
*/
|
|
947
|
+
async function schemaCommand(subcommand, rawArgs) {
|
|
948
|
+
if (!subcommand || subcommand === "--help") {
|
|
949
|
+
printSchemaHelp();
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const projectRoot = requireProjectRoot();
|
|
953
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
954
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
955
|
+
if (!activePlugin) {
|
|
956
|
+
console.error(chalk.red("✗ Could not detect an active database plugin."));
|
|
957
|
+
console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json."));
|
|
958
|
+
process.exit(1);
|
|
959
|
+
}
|
|
960
|
+
const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
|
|
961
|
+
if (!pluginCli) {
|
|
962
|
+
console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
|
|
963
|
+
process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
const envFile = findEnvFile(projectRoot);
|
|
966
|
+
const env = { ...process.env };
|
|
967
|
+
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
968
|
+
try {
|
|
969
|
+
if (pluginCli.endsWith(".ts")) {
|
|
970
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
971
|
+
if (!tsxBin) {
|
|
972
|
+
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
|
|
976
|
+
cwd: backendDir,
|
|
977
|
+
stdio: "inherit",
|
|
978
|
+
env
|
|
979
|
+
});
|
|
980
|
+
} else await execa("node", [pluginCli, ...rawArgs.slice(2)], {
|
|
981
|
+
cwd: backendDir,
|
|
982
|
+
stdio: "inherit",
|
|
983
|
+
env
|
|
984
|
+
});
|
|
985
|
+
} catch {
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function printSchemaHelp() {
|
|
990
|
+
console.log(`
|
|
991
|
+
${chalk.bold("rebase schema")} — Schema management commands
|
|
992
|
+
|
|
993
|
+
${chalk.green.bold("Usage")}
|
|
994
|
+
rebase schema ${chalk.blue("<command>")} [options]
|
|
995
|
+
|
|
996
|
+
${chalk.green.bold("Commands")}
|
|
997
|
+
${chalk.gray("(Commands are provided by your active database driver plugin)")}
|
|
998
|
+
${chalk.blue.bold("generate")} Generate Schema from collection definitions
|
|
999
|
+
${chalk.blue.bold("introspect")} Introspect an existing database to generate collection definitions
|
|
1000
|
+
|
|
1001
|
+
${chalk.green.bold("generate Options")}
|
|
1002
|
+
${chalk.blue("--collections, -c")} Path to collections directory
|
|
1003
|
+
${chalk.blue("--output, -o")} Output path for generated schema
|
|
1004
|
+
${chalk.blue("--watch, -w")} Watch for changes and regenerate automatically
|
|
1005
|
+
|
|
1006
|
+
${chalk.green.bold("introspect Options")}
|
|
1007
|
+
${chalk.blue("--output, -o")} Output directory for generated collection files
|
|
1008
|
+
`);
|
|
1009
|
+
}
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/commands/db.ts
|
|
1012
|
+
/**
|
|
1013
|
+
* CLI command: rebase db <action>
|
|
1014
|
+
*/
|
|
1015
|
+
async function dbCommand(subcommand, rawArgs) {
|
|
1016
|
+
if (!subcommand || subcommand === "--help") {
|
|
1017
|
+
printDbHelp$1();
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const projectRoot = requireProjectRoot();
|
|
1021
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
1022
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
1023
|
+
if (!activePlugin) {
|
|
1024
|
+
console.error(chalk.red("✗ Could not detect an active database plugin."));
|
|
1025
|
+
console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json."));
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
|
|
1029
|
+
if (!pluginCli) {
|
|
1030
|
+
console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
const envFile = findEnvFile(projectRoot);
|
|
1034
|
+
const env = { ...process.env };
|
|
1035
|
+
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
1036
|
+
try {
|
|
1037
|
+
if (pluginCli.endsWith(".ts")) {
|
|
1038
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
1039
|
+
if (!tsxBin) {
|
|
1040
|
+
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
1041
|
+
process.exit(1);
|
|
1042
|
+
}
|
|
1043
|
+
await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
|
|
1044
|
+
cwd: backendDir,
|
|
1045
|
+
stdio: "inherit",
|
|
1046
|
+
env
|
|
1047
|
+
});
|
|
1048
|
+
} else await execa("node", [pluginCli, ...rawArgs.slice(2)], {
|
|
1049
|
+
cwd: backendDir,
|
|
1050
|
+
stdio: "inherit",
|
|
1051
|
+
env
|
|
1052
|
+
});
|
|
1053
|
+
} catch {
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function printDbHelp$1() {
|
|
1058
|
+
console.log(`
|
|
1059
|
+
${chalk.bold("rebase db")} — Database management commands
|
|
1060
|
+
|
|
1061
|
+
${chalk.green.bold("Usage")}
|
|
1062
|
+
rebase db ${chalk.blue("<command>")} [options]
|
|
1063
|
+
|
|
1064
|
+
${chalk.green.bold("Commands")}
|
|
1065
|
+
${chalk.gray("(Commands are provided by your active database driver plugin)")}
|
|
1066
|
+
${chalk.blue.bold("push")} Apply schema directly to database (development)
|
|
1067
|
+
${chalk.blue.bold("generate")} Generate migration files
|
|
1068
|
+
${chalk.blue.bold("migrate")} Run pending migrations
|
|
1069
|
+
${chalk.blue.bold("branch")} Database branching (create, list, delete, info)
|
|
1070
|
+
${chalk.blue.bold("backup")} Create a backup with pg_dump (--out <path|s3://…>)
|
|
1071
|
+
${chalk.blue.bold("restore")} Restore a backup with pg_restore (destructive; needs --yes)
|
|
1072
|
+
${chalk.blue.bold("backups")} List stored backups (backups list)
|
|
1073
|
+
|
|
1074
|
+
${chalk.green.bold("Examples")}
|
|
1075
|
+
${chalk.gray("# Quick development workflow")}
|
|
1076
|
+
rebase schema generate && rebase db push
|
|
1077
|
+
|
|
1078
|
+
${chalk.gray("# Production migration workflow")}
|
|
1079
|
+
rebase db generate
|
|
1080
|
+
rebase db migrate
|
|
1081
|
+
|
|
1082
|
+
${chalk.gray("# Create a database branch")}
|
|
1083
|
+
rebase db branch create feature_auth
|
|
1084
|
+
|
|
1085
|
+
${chalk.gray("# Back up to a local directory, then to object storage")}
|
|
1086
|
+
rebase db backup --out ./backups
|
|
1087
|
+
rebase db backup --out s3://my-private-bucket/backups
|
|
1088
|
+
|
|
1089
|
+
${chalk.gray("# Restore into a fresh database (safe: does not touch the live one)")}
|
|
1090
|
+
rebase db restore ./backups/rebase-app-20260714T030000Z.dump --create-db --target-db app_restored
|
|
1091
|
+
`);
|
|
1092
|
+
}
|
|
1093
|
+
//#endregion
|
|
1094
|
+
//#region src/commands/dev.ts
|
|
1095
|
+
/**
|
|
1096
|
+
* CLI command: rebase dev
|
|
1097
|
+
*
|
|
1098
|
+
* Starts the full development environment:
|
|
1099
|
+
* - Backend: tsx watch with auto-reload
|
|
1100
|
+
* - Frontend: vite dev server
|
|
1101
|
+
*
|
|
1102
|
+
* Both processes stream output with color-coded prefixes.
|
|
1103
|
+
*
|
|
1104
|
+
* When the backend uses port-retry (i.e. the configured port is busy and it
|
|
1105
|
+
* binds to the next free one), the CLI detects the actual port from stdout
|
|
1106
|
+
* and injects VITE_API_URL into the frontend so it connects automatically.
|
|
1107
|
+
*
|
|
1108
|
+
* Each project gets a deterministic default port derived from the project
|
|
1109
|
+
* root path, so multiple Rebase instances never collide.
|
|
1110
|
+
*/
|
|
1111
|
+
/** Well-known filename the backend writes its actual port to. */
|
|
1112
|
+
var DEV_PORT_FILENAME = ".rebase-dev-port";
|
|
1113
|
+
/**
|
|
1114
|
+
* Compute a deterministic port from the project root path.
|
|
1115
|
+
* Range: 3001–3999 (avoids privileged ports and common services).
|
|
1116
|
+
* Two different project directories will almost always get different ports.
|
|
1117
|
+
*/
|
|
1118
|
+
function getProjectPort(projectRoot) {
|
|
1119
|
+
let hash = 0;
|
|
1120
|
+
for (let i = 0; i < projectRoot.length; i++) hash = (hash << 5) - hash + projectRoot.charCodeAt(i) | 0;
|
|
1121
|
+
return 3001 + Math.abs(hash) % 999;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Resolve the best starting port for this project:
|
|
1125
|
+
* 1. Explicit --port flag (highest priority)
|
|
1126
|
+
* 2. PORT env var
|
|
1127
|
+
* 3. Previously used port from .rebase-dev-port (port affinity across restarts)
|
|
1128
|
+
* 4. Deterministic hash from project path (unique per project)
|
|
1129
|
+
*/
|
|
1130
|
+
function resolveStartPort(projectRoot, explicitPort) {
|
|
1131
|
+
if (explicitPort) return explicitPort;
|
|
1132
|
+
if (process.env.PORT) return parseInt(process.env.PORT, 10);
|
|
1133
|
+
try {
|
|
1134
|
+
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
1135
|
+
if (fs.existsSync(portFile)) {
|
|
1136
|
+
const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
|
|
1137
|
+
if (saved > 0 && saved < 65536) return saved;
|
|
1138
|
+
}
|
|
1139
|
+
} catch {}
|
|
1140
|
+
return getProjectPort(projectRoot);
|
|
1141
|
+
}
|
|
1142
|
+
async function devCommand(rawArgs) {
|
|
1143
|
+
const args = arg({
|
|
1144
|
+
"--backend-only": Boolean,
|
|
1145
|
+
"--frontend-only": Boolean,
|
|
1146
|
+
"--port": Number,
|
|
1147
|
+
"--generate": Boolean,
|
|
1148
|
+
"--help": Boolean,
|
|
1149
|
+
"-b": "--backend-only",
|
|
1150
|
+
"-f": "--frontend-only",
|
|
1151
|
+
"-p": "--port",
|
|
1152
|
+
"-g": "--generate",
|
|
1153
|
+
"-h": "--help"
|
|
1154
|
+
}, {
|
|
1155
|
+
argv: rawArgs.slice(3),
|
|
1156
|
+
permissive: true
|
|
1157
|
+
});
|
|
1158
|
+
if (args["--help"]) {
|
|
1159
|
+
printDevHelp();
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const projectRoot = requireProjectRoot();
|
|
1163
|
+
const backendDir = findBackendDir(projectRoot);
|
|
1164
|
+
const frontendDir = findFrontendDir(projectRoot);
|
|
1165
|
+
const backendOnly = args["--backend-only"] || false;
|
|
1166
|
+
const frontendOnly = args["--frontend-only"] || false;
|
|
1167
|
+
const shouldGenerate = args["--generate"] || process.env.REBASE_AUTO_GENERATE === "true" || process.env.REBASE_GENERATE === "true";
|
|
1168
|
+
const startPort = resolveStartPort(projectRoot, args["--port"]);
|
|
1169
|
+
console.log("");
|
|
1170
|
+
console.log(chalk.bold(" 🚀 Rebase Dev Server"));
|
|
1171
|
+
console.log("");
|
|
1172
|
+
const children = [];
|
|
1173
|
+
let frontendUrl = "";
|
|
1174
|
+
let backendUrl = "";
|
|
1175
|
+
let debounceSummary = null;
|
|
1176
|
+
let bannerPrinted = false;
|
|
1177
|
+
/** Actual backend port, resolved once the server prints its URL. */
|
|
1178
|
+
let resolvedBackendPort = null;
|
|
1179
|
+
const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
|
|
1180
|
+
function printSummary() {
|
|
1181
|
+
if (!frontendUrl || !backendUrl) return;
|
|
1182
|
+
if (debounceSummary) clearTimeout(debounceSummary);
|
|
1183
|
+
debounceSummary = setTimeout(() => {
|
|
1184
|
+
if (bannerPrinted) return;
|
|
1185
|
+
console.log("");
|
|
1186
|
+
console.log(chalk.cyan("┌────────────────────────────────────────────────────────────┐"));
|
|
1187
|
+
console.log(chalk.cyan("│ │"));
|
|
1188
|
+
console.log(chalk.cyan("│ ✦ Rebase Admin App is ready! │"));
|
|
1189
|
+
const paddedUrl = stripAnsi(frontendUrl).padEnd(41);
|
|
1190
|
+
console.log(chalk.cyan("│ ➜ Frontend URL: ") + chalk.white(paddedUrl) + chalk.cyan("│"));
|
|
1191
|
+
console.log(chalk.cyan("│ │"));
|
|
1192
|
+
console.log(chalk.cyan("└────────────────────────────────────────────────────────────┘"));
|
|
1193
|
+
console.log("");
|
|
1194
|
+
bannerPrinted = true;
|
|
1195
|
+
}, 500);
|
|
1196
|
+
}
|
|
1197
|
+
const cleanup = () => {
|
|
1198
|
+
try {
|
|
1199
|
+
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
1200
|
+
if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
|
|
1201
|
+
const urlFile = path.join(projectRoot, ".rebase-dev-url");
|
|
1202
|
+
if (fs.existsSync(urlFile)) fs.unlinkSync(urlFile);
|
|
1203
|
+
} catch {}
|
|
1204
|
+
children.forEach((child) => {
|
|
1205
|
+
if (child.pid && !child.killed) try {
|
|
1206
|
+
if (process.platform === "win32") execaCommandSync(`taskkill /pid ${child.pid} /T /F`);
|
|
1207
|
+
else process.kill(-child.pid, "SIGKILL");
|
|
1208
|
+
} catch (e) {
|
|
1209
|
+
try {
|
|
1210
|
+
child.kill("SIGKILL");
|
|
1211
|
+
} catch (err) {}
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
process.exit(0);
|
|
1215
|
+
};
|
|
1216
|
+
process.on("SIGINT", cleanup);
|
|
1217
|
+
process.on("SIGTERM", cleanup);
|
|
1218
|
+
/**
|
|
1219
|
+
* Start the Vite frontend, optionally injecting the backend port.
|
|
1220
|
+
*/
|
|
1221
|
+
function startFrontend(backendPort) {
|
|
1222
|
+
if (!frontendDir) return;
|
|
1223
|
+
console.log(` ${chalk.magenta("▶")} Frontend: ${chalk.gray(frontendDir)}`);
|
|
1224
|
+
const frontendEnv = { ...process.env };
|
|
1225
|
+
if (backendPort) {
|
|
1226
|
+
frontendEnv.VITE_API_URL = `http://localhost:${backendPort}`;
|
|
1227
|
+
console.log(` ${chalk.gray("↳ VITE_API_URL")} = ${chalk.white(`http://localhost:${backendPort}`)}`);
|
|
1228
|
+
}
|
|
1229
|
+
const runDevCmd = getPMCommands(detectPackageManager(projectRoot)).run("dev");
|
|
1230
|
+
const frontendChild = execa(runDevCmd[0], runDevCmd.slice(1), {
|
|
1231
|
+
cwd: frontendDir,
|
|
1232
|
+
stdio: [
|
|
1233
|
+
"inherit",
|
|
1234
|
+
"pipe",
|
|
1235
|
+
"pipe"
|
|
1236
|
+
],
|
|
1237
|
+
env: frontendEnv,
|
|
1238
|
+
shell: true,
|
|
1239
|
+
detached: process.platform !== "win32"
|
|
1240
|
+
});
|
|
1241
|
+
frontendChild.catch(() => {});
|
|
1242
|
+
frontendChild.stdout?.on("data", (data) => {
|
|
1243
|
+
data.toString().split("\n").filter(Boolean).forEach((line) => {
|
|
1244
|
+
console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
|
|
1245
|
+
const cleanLine = stripAnsi(line);
|
|
1246
|
+
const urlMatch = cleanLine.match(/(http:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
|
|
1247
|
+
if (cleanLine.includes("Local:") && urlMatch) {
|
|
1248
|
+
frontendUrl = urlMatch[1];
|
|
1249
|
+
printSummary();
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
1253
|
+
frontendChild.stderr?.on("data", (data) => {
|
|
1254
|
+
data.toString().split("\n").filter(Boolean).forEach((line) => {
|
|
1255
|
+
console.log(`${chalk.magenta.bold("[admin]")} ${line}`);
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
children.push(frontendChild);
|
|
1259
|
+
}
|
|
1260
|
+
if (!frontendOnly && backendDir) {
|
|
1261
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
1262
|
+
if (!tsxBin) {
|
|
1263
|
+
const addCmd = [
|
|
1264
|
+
...getPMCommands(detectPackageManager(projectRoot)).install,
|
|
1265
|
+
"-D",
|
|
1266
|
+
"tsx"
|
|
1267
|
+
].join(" ");
|
|
1268
|
+
console.error(chalk.red(" ✗ Could not find tsx binary for backend."));
|
|
1269
|
+
console.error(chalk.gray(` Install it with: ${addCmd}`));
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
const tsxValidationError = validateTsxInstallation(tsxBin);
|
|
1273
|
+
if (tsxValidationError) {
|
|
1274
|
+
const installCmd = getPMCommands(detectPackageManager(projectRoot)).install.join(" ");
|
|
1275
|
+
console.error(chalk.red(" ✗ tsx installation appears corrupted."));
|
|
1276
|
+
console.error(chalk.gray(` ${tsxValidationError}`));
|
|
1277
|
+
console.error("");
|
|
1278
|
+
console.error(chalk.gray(" To fix, run:"));
|
|
1279
|
+
console.error(chalk.cyan(` rm -rf node_modules && ${installCmd}`));
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
}
|
|
1282
|
+
const envFile = findEnvFile(projectRoot);
|
|
1283
|
+
const env = { ...process.env };
|
|
1284
|
+
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
1285
|
+
env.PORT = String(startPort);
|
|
1286
|
+
console.log(` ${chalk.cyan("▶")} Backend: ${chalk.gray(backendDir)}`);
|
|
1287
|
+
console.log(` ${chalk.gray("↳ PORT")} = ${chalk.white(String(startPort))}`);
|
|
1288
|
+
if (envFile) try {
|
|
1289
|
+
const envText = fs.readFileSync(envFile, "utf-8");
|
|
1290
|
+
const readEnvKey = (key) => {
|
|
1291
|
+
const m = envText.match(new RegExp(`^\\s*${key}\\s*=\\s*(.+?)\\s*$`, "m"));
|
|
1292
|
+
return m ? m[1].replace(/^["']|["']$/g, "") : void 0;
|
|
1293
|
+
};
|
|
1294
|
+
const envPort = readEnvKey("PORT");
|
|
1295
|
+
const envApiUrl = readEnvKey("VITE_API_URL");
|
|
1296
|
+
const overridden = [];
|
|
1297
|
+
if (envPort && envPort !== String(startPort)) overridden.push("PORT");
|
|
1298
|
+
if (envApiUrl && envApiUrl !== `http://localhost:${startPort}`) overridden.push("VITE_API_URL");
|
|
1299
|
+
if (overridden.length > 0) console.log(chalk.yellow(` ⚠ dev uses a derived per-project port (${startPort}); your .env ${overridden.join(" / ")} ${overridden.length > 1 ? "are" : "is"} ignored here (avoids cross-project collisions). Pass ${chalk.white("--port")} to pin a port.`));
|
|
1300
|
+
} catch {}
|
|
1301
|
+
/** Whether the frontend has been launched (we only launch it once). */
|
|
1302
|
+
let frontendLaunched = false;
|
|
1303
|
+
if (shouldGenerate) {
|
|
1304
|
+
console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
|
|
1305
|
+
try {
|
|
1306
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
1307
|
+
const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
1308
|
+
if (pluginCli) await execa(tsxBin, [
|
|
1309
|
+
pluginCli,
|
|
1310
|
+
"schema",
|
|
1311
|
+
"generate"
|
|
1312
|
+
], {
|
|
1313
|
+
cwd: backendDir,
|
|
1314
|
+
stdio: "inherit",
|
|
1315
|
+
env
|
|
1316
|
+
});
|
|
1317
|
+
const sdkCmd = getPMCommands(detectPackageManager(projectRoot)).exec("rebase", ["generate-sdk"]);
|
|
1318
|
+
await execa(sdkCmd[0], sdkCmd.slice(1), {
|
|
1319
|
+
cwd: projectRoot,
|
|
1320
|
+
stdio: "inherit",
|
|
1321
|
+
env
|
|
1322
|
+
});
|
|
1323
|
+
console.log(chalk.green(" ✓ Initial schema and SDK generated successfully.\n"));
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
console.error(chalk.red(` ✗ Initial schema/SDK generation failed: ${err instanceof Error ? err.message : err}\n`));
|
|
1326
|
+
}
|
|
1327
|
+
const collectionsDir = path.join(projectRoot, "config", "collections");
|
|
1328
|
+
if (fs.existsSync(collectionsDir)) {
|
|
1329
|
+
let watchDebounce = null;
|
|
1330
|
+
fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
|
|
1331
|
+
if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
|
|
1332
|
+
if (watchDebounce) clearTimeout(watchDebounce);
|
|
1333
|
+
watchDebounce = setTimeout(async () => {
|
|
1334
|
+
console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating schema & SDK...`));
|
|
1335
|
+
try {
|
|
1336
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
1337
|
+
const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
1338
|
+
if (pluginCli) await execa(tsxBin, [
|
|
1339
|
+
pluginCli,
|
|
1340
|
+
"schema",
|
|
1341
|
+
"generate"
|
|
1342
|
+
], {
|
|
1343
|
+
cwd: backendDir,
|
|
1344
|
+
stdio: "inherit",
|
|
1345
|
+
env
|
|
1346
|
+
});
|
|
1347
|
+
const sdkCmd = getPMCommands(detectPackageManager(projectRoot)).exec("rebase", ["generate-sdk"]);
|
|
1348
|
+
await execa(sdkCmd[0], sdkCmd.slice(1), {
|
|
1349
|
+
cwd: projectRoot,
|
|
1350
|
+
stdio: "inherit",
|
|
1351
|
+
env
|
|
1352
|
+
});
|
|
1353
|
+
console.log(chalk.green(" ✓ Schema & SDK regenerated successfully. Hono will reload."));
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err instanceof Error ? err.message : err}`));
|
|
1356
|
+
}
|
|
1357
|
+
}, 300);
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
const watchArgs = [
|
|
1362
|
+
"watch",
|
|
1363
|
+
"--conditions",
|
|
1364
|
+
"development",
|
|
1365
|
+
"src/index.ts"
|
|
1366
|
+
];
|
|
1367
|
+
if (!shouldGenerate) {
|
|
1368
|
+
watchArgs.splice(1, 0, `--watch="${path.join("..", "config", "**", "*")}"`);
|
|
1369
|
+
const collectionsDir = path.join(projectRoot, "config", "collections");
|
|
1370
|
+
if (fs.existsSync(collectionsDir)) {
|
|
1371
|
+
let driftDebounce = null;
|
|
1372
|
+
fs.watch(collectionsDir, { recursive: true }, (_eventType, filename) => {
|
|
1373
|
+
if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
|
|
1374
|
+
if (driftDebounce) clearTimeout(driftDebounce);
|
|
1375
|
+
driftDebounce = setTimeout(() => {
|
|
1376
|
+
console.log([
|
|
1377
|
+
"",
|
|
1378
|
+
chalk.yellow(" ┌──────────────────────────────────────────────────────────────┐"),
|
|
1379
|
+
chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(filename.padEnd(31)) + chalk.yellow("│"),
|
|
1380
|
+
chalk.yellow(" │ │"),
|
|
1381
|
+
chalk.yellow(" │ Your schema may be out of sync. Run: │"),
|
|
1382
|
+
chalk.yellow(" │ ") + chalk.cyan("rebase schema generate") + chalk.yellow(" regenerate Drizzle schema │"),
|
|
1383
|
+
chalk.yellow(" │ ") + chalk.cyan("rebase db push ") + chalk.yellow(" sync schema to database │"),
|
|
1384
|
+
chalk.yellow(" │ ") + chalk.cyan("rebase doctor ") + chalk.yellow(" check for drift │"),
|
|
1385
|
+
chalk.yellow(" │ │"),
|
|
1386
|
+
chalk.yellow(" │ TIP: Use ") + chalk.bold("rebase dev --generate") + chalk.yellow(" for auto-regeneration │"),
|
|
1387
|
+
chalk.yellow(" └──────────────────────────────────────────────────────────────┘"),
|
|
1388
|
+
""
|
|
1389
|
+
].join("\n"));
|
|
1390
|
+
}, 500);
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
const backendChild = execa(tsxBin, watchArgs, {
|
|
1395
|
+
cwd: backendDir,
|
|
1396
|
+
stdio: [
|
|
1397
|
+
"inherit",
|
|
1398
|
+
"pipe",
|
|
1399
|
+
"pipe"
|
|
1400
|
+
],
|
|
1401
|
+
env,
|
|
1402
|
+
shell: true,
|
|
1403
|
+
detached: process.platform !== "win32"
|
|
1404
|
+
});
|
|
1405
|
+
backendChild.catch(() => {});
|
|
1406
|
+
backendChild.stdout?.on("data", (data) => {
|
|
1407
|
+
data.toString().split("\n").filter(Boolean).forEach((line) => {
|
|
1408
|
+
console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
|
|
1409
|
+
const serverMatch = stripAnsi(line).match(/Server running at http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
|
|
1410
|
+
if (serverMatch) {
|
|
1411
|
+
resolvedBackendPort = parseInt(serverMatch[1], 10);
|
|
1412
|
+
backendUrl = "started";
|
|
1413
|
+
printSummary();
|
|
1414
|
+
const urlFile = path.join(projectRoot, ".rebase-dev-url");
|
|
1415
|
+
fs.writeFileSync(urlFile, `http://localhost:${resolvedBackendPort}`, "utf-8");
|
|
1416
|
+
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
1417
|
+
fs.writeFileSync(portFile, String(resolvedBackendPort), "utf-8");
|
|
1418
|
+
if (!backendOnly && frontendDir && !frontendLaunched) {
|
|
1419
|
+
frontendLaunched = true;
|
|
1420
|
+
startFrontend(resolvedBackendPort);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
});
|
|
1425
|
+
/** Whether we've already shown a corrupted-modules recovery hint. */
|
|
1426
|
+
let corruptedModulesWarned = false;
|
|
1427
|
+
backendChild.stderr?.on("data", (data) => {
|
|
1428
|
+
data.toString().split("\n").filter(Boolean).forEach((line) => {
|
|
1429
|
+
console.log(`${chalk.cyan.bold("[backend]")} ${line}`);
|
|
1430
|
+
if (!corruptedModulesWarned) {
|
|
1431
|
+
const cleanLine = stripAnsi(line);
|
|
1432
|
+
if (cleanLine.includes("Cannot find module") && cleanLine.includes("node_modules/.pnpm/")) {
|
|
1433
|
+
corruptedModulesWarned = true;
|
|
1434
|
+
setTimeout(() => {
|
|
1435
|
+
const installCmd = getPMCommands(detectPackageManager(projectRoot)).install.join(" ");
|
|
1436
|
+
console.error("");
|
|
1437
|
+
console.error(chalk.red(" ✗ node_modules appears corrupted — a required file is missing."));
|
|
1438
|
+
console.error(chalk.gray(" This usually happens when a previous install was interrupted"));
|
|
1439
|
+
console.error(chalk.gray(" or the package manager store was cleaned."));
|
|
1440
|
+
console.error("");
|
|
1441
|
+
console.error(chalk.gray(" To fix, stop the dev server and run:"));
|
|
1442
|
+
console.error(chalk.cyan(` rm -rf node_modules && ${installCmd}`));
|
|
1443
|
+
console.error("");
|
|
1444
|
+
}, 200);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
});
|
|
1449
|
+
children.push(backendChild);
|
|
1450
|
+
} else if (!frontendOnly && !backendDir) console.warn(chalk.yellow(" ⚠ No backend/ directory found, skipping backend."));
|
|
1451
|
+
if (!backendOnly && frontendDir && (frontendOnly || !backendDir)) startFrontend(null);
|
|
1452
|
+
else if (!backendOnly && !frontendDir) console.warn(chalk.yellow(" ⚠ No frontend/ directory found, skipping frontend."));
|
|
1453
|
+
if (children.length === 0) {
|
|
1454
|
+
console.error(chalk.red(" ✗ Nothing to start. Check your project structure."));
|
|
1455
|
+
process.exit(1);
|
|
1456
|
+
}
|
|
1457
|
+
console.log("");
|
|
1458
|
+
console.log(chalk.gray(" Press Ctrl+C to stop all servers."));
|
|
1459
|
+
console.log("");
|
|
1460
|
+
await Promise.all(children.map((child) => new Promise((resolve) => {
|
|
1461
|
+
child.finally(() => resolve());
|
|
1462
|
+
})));
|
|
1463
|
+
}
|
|
1464
|
+
function printDevHelp() {
|
|
1465
|
+
console.log(`
|
|
1466
|
+
${chalk.bold("rebase dev")} — Start the development server
|
|
1467
|
+
|
|
1468
|
+
${chalk.green.bold("Usage")}
|
|
1469
|
+
rebase dev [options]
|
|
1470
|
+
|
|
1471
|
+
${chalk.green.bold("Options")}
|
|
1472
|
+
${chalk.blue("--backend-only, -b")} Only start the backend server
|
|
1473
|
+
${chalk.blue("--frontend-only, -f")} Only start the frontend server
|
|
1474
|
+
${chalk.blue("--port, -p")} Backend port (default: auto-detected per project)
|
|
1475
|
+
${chalk.blue("--generate, -g")} Enable automatic schema and SDK generation on startup and file changes
|
|
1476
|
+
|
|
1477
|
+
${chalk.green.bold("Description")}
|
|
1478
|
+
Starts both the backend (tsx watch + Hono) and frontend (Vite)
|
|
1479
|
+
dev servers concurrently with color-coded output prefixes.
|
|
1480
|
+
|
|
1481
|
+
Each project automatically receives a unique default port derived
|
|
1482
|
+
from its directory path, preventing collisions when running multiple
|
|
1483
|
+
Rebase instances simultaneously.
|
|
1484
|
+
|
|
1485
|
+
If the assigned port is already in use, the server will automatically
|
|
1486
|
+
try the next available port. The frontend is started only after the
|
|
1487
|
+
backend is ready, and VITE_API_URL is injected automatically.
|
|
1488
|
+
|
|
1489
|
+
By default, automatic schema and SDK generation is disabled on startup
|
|
1490
|
+
and file changes. Pass --generate (-g) or set REBASE_AUTO_GENERATE=true
|
|
1491
|
+
in your environment to enable it.
|
|
1492
|
+
`);
|
|
1493
|
+
}
|
|
1494
|
+
//#endregion
|
|
1495
|
+
//#region src/commands/build.ts
|
|
1496
|
+
/**
|
|
1497
|
+
* CLI command: rebase build
|
|
1498
|
+
*
|
|
1499
|
+
* Runs the build script in all workspace packages.
|
|
1500
|
+
* Automatically detects the package manager (pnpm or npm) and
|
|
1501
|
+
* uses the correct workspace-aware command.
|
|
1502
|
+
*/
|
|
1503
|
+
async function buildCommand() {
|
|
1504
|
+
const projectRoot = requireProjectRoot();
|
|
1505
|
+
const pm = detectPackageManager(projectRoot);
|
|
1506
|
+
const buildCmd = getPMCommands(pm).runAll("build");
|
|
1507
|
+
console.log(`${chalk.bold("Rebase")} — Building all workspaces with ${chalk.cyan(pm)}...\n`);
|
|
1508
|
+
try {
|
|
1509
|
+
await execa(buildCmd[0], buildCmd.slice(1), {
|
|
1510
|
+
cwd: projectRoot,
|
|
1511
|
+
stdio: "inherit"
|
|
1512
|
+
});
|
|
1513
|
+
} catch {
|
|
1514
|
+
console.error(chalk.red("\n✗ Build failed."));
|
|
1515
|
+
process.exit(1);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
//#endregion
|
|
1519
|
+
//#region src/commands/start.ts
|
|
1520
|
+
/**
|
|
1521
|
+
* CLI command: rebase start
|
|
1522
|
+
*
|
|
1523
|
+
* Starts the backend server in production mode.
|
|
1524
|
+
* Automatically detects the package manager (pnpm or npm) and
|
|
1525
|
+
* runs the start script in the backend workspace.
|
|
1526
|
+
*/
|
|
1527
|
+
async function startCommand() {
|
|
1528
|
+
const projectRoot = requireProjectRoot();
|
|
1529
|
+
const startCmd = getPMCommands(detectPackageManager(projectRoot)).runWorkspace("backend", "start");
|
|
1530
|
+
const envFile = findEnvFile(projectRoot);
|
|
1531
|
+
const env = { ...process.env };
|
|
1532
|
+
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
1533
|
+
console.log(`${chalk.bold("Rebase")} — Starting backend server...\n`);
|
|
1534
|
+
try {
|
|
1535
|
+
await execa(startCmd[0], startCmd.slice(1), {
|
|
1536
|
+
cwd: projectRoot,
|
|
1537
|
+
stdio: "inherit",
|
|
1538
|
+
env
|
|
1539
|
+
});
|
|
1540
|
+
} catch {
|
|
1541
|
+
console.error(chalk.red("\n✗ Failed to start server."));
|
|
1542
|
+
process.exit(1);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
//#endregion
|
|
1546
|
+
//#region src/commands/auth.ts
|
|
1547
|
+
/**
|
|
1548
|
+
* CLI command: rebase auth <action>
|
|
1549
|
+
*
|
|
1550
|
+
* Subcommands:
|
|
1551
|
+
* reset-password — Reset a user's password
|
|
1552
|
+
*/
|
|
1553
|
+
async function authCommand(subcommand, rawArgs) {
|
|
1554
|
+
if (!subcommand || subcommand === "--help") {
|
|
1555
|
+
printAuthHelp();
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
switch (subcommand) {
|
|
1559
|
+
case "reset-password":
|
|
1560
|
+
await resetPassword(rawArgs);
|
|
1561
|
+
break;
|
|
1562
|
+
default:
|
|
1563
|
+
console.error(chalk.red(`Unknown auth command: ${subcommand}`));
|
|
1564
|
+
console.log("");
|
|
1565
|
+
printAuthHelp();
|
|
1566
|
+
process.exit(1);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
async function resetPassword(rawArgs) {
|
|
1570
|
+
const args = arg({
|
|
1571
|
+
"--email": String,
|
|
1572
|
+
"--password": String,
|
|
1573
|
+
"-e": "--email",
|
|
1574
|
+
"-p": "--password"
|
|
1575
|
+
}, {
|
|
1576
|
+
argv: rawArgs.slice(4),
|
|
1577
|
+
permissive: true
|
|
1578
|
+
});
|
|
1579
|
+
const email = args["--email"] || args._[0];
|
|
1580
|
+
const newPassword = args["--password"] || args._[1];
|
|
1581
|
+
if (!email) {
|
|
1582
|
+
console.error(chalk.red("✗ Email is required."));
|
|
1583
|
+
console.log("");
|
|
1584
|
+
console.log(chalk.gray(" Usage: rebase auth reset-password <email> [new-password]"));
|
|
1585
|
+
console.log(chalk.gray(" rebase auth reset-password --email user@example.com --password NewPass123!"));
|
|
1586
|
+
process.exit(1);
|
|
1587
|
+
}
|
|
1588
|
+
const projectRoot = requireProjectRoot();
|
|
1589
|
+
let envServiceKey;
|
|
1590
|
+
const envFile = findEnvFile(projectRoot);
|
|
1591
|
+
if (envFile && fs.existsSync(envFile)) try {
|
|
1592
|
+
const match = fs.readFileSync(envFile, "utf8").match(/^\s*REBASE_SERVICE_KEY\s*=\s*['"]?(.*?)['"]?\s*$/m);
|
|
1593
|
+
if (match && match[1]) envServiceKey = match[1];
|
|
1594
|
+
} catch {}
|
|
1595
|
+
let baseUrl = process.env.REBASE_BASE_URL;
|
|
1596
|
+
let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;
|
|
1597
|
+
const statePath = path.join(projectRoot, ".rebase", "state.json");
|
|
1598
|
+
if (fs.existsSync(statePath)) try {
|
|
1599
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
1600
|
+
if (state && typeof state === "object") {
|
|
1601
|
+
if (typeof state.baseUrl === "string" && !baseUrl) baseUrl = state.baseUrl;
|
|
1602
|
+
if (typeof state.serviceKey === "string" && !serviceKey) serviceKey = state.serviceKey;
|
|
1603
|
+
}
|
|
1604
|
+
} catch {}
|
|
1605
|
+
const devUrlPath = path.join(projectRoot, ".rebase-dev-url");
|
|
1606
|
+
if (fs.existsSync(devUrlPath) && !baseUrl) try {
|
|
1607
|
+
baseUrl = fs.readFileSync(devUrlPath, "utf8").trim();
|
|
1608
|
+
} catch {}
|
|
1609
|
+
if (baseUrl && serviceKey) {
|
|
1610
|
+
console.log("Trying API-first reset via running backend...");
|
|
1611
|
+
try {
|
|
1612
|
+
const finalPass = newPassword || "NewPassword123!";
|
|
1613
|
+
const cleanBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
1614
|
+
const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=1`;
|
|
1615
|
+
const searchRes = await fetch(searchUrl, { headers: {
|
|
1616
|
+
"Authorization": `Bearer ${serviceKey}`,
|
|
1617
|
+
"Accept": "application/json"
|
|
1618
|
+
} });
|
|
1619
|
+
if (!searchRes.ok) throw new Error(`Failed to list users: ${searchRes.statusText}`);
|
|
1620
|
+
const searchData = await searchRes.json();
|
|
1621
|
+
if (!searchData || typeof searchData !== "object") throw new Error("Invalid response format from user search API.");
|
|
1622
|
+
let userId;
|
|
1623
|
+
if (Array.isArray(searchData)) {
|
|
1624
|
+
const firstUser = searchData[0];
|
|
1625
|
+
if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
|
|
1626
|
+
else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
|
|
1627
|
+
} else if ("users" in searchData && Array.isArray(searchData.users)) {
|
|
1628
|
+
const firstUser = searchData.users[0];
|
|
1629
|
+
if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
|
|
1630
|
+
else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
|
|
1631
|
+
}
|
|
1632
|
+
if (!userId) throw new Error(`User not found with email: ${email}`);
|
|
1633
|
+
const resetUrl = `${cleanBaseUrl}/api/admin/users/${userId}/reset-password`;
|
|
1634
|
+
const resetRes = await fetch(resetUrl, {
|
|
1635
|
+
method: "POST",
|
|
1636
|
+
headers: {
|
|
1637
|
+
"Authorization": `Bearer ${serviceKey}`,
|
|
1638
|
+
"Content-Type": "application/json",
|
|
1639
|
+
"Accept": "application/json"
|
|
1640
|
+
},
|
|
1641
|
+
body: JSON.stringify({ password: finalPass })
|
|
1642
|
+
});
|
|
1643
|
+
if (!resetRes.ok) {
|
|
1644
|
+
const errText = await resetRes.text();
|
|
1645
|
+
throw new Error(`Password reset endpoint failed: ${errText || resetRes.statusText}`);
|
|
1646
|
+
}
|
|
1647
|
+
console.log("API reset successful.");
|
|
1648
|
+
console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (via API)"));
|
|
1649
|
+
console.log("");
|
|
1650
|
+
console.log(` ${chalk.gray("Email:")} ${email}`);
|
|
1651
|
+
console.log(` ${chalk.gray("Password:")} ${finalPass}`);
|
|
1652
|
+
console.log("");
|
|
1653
|
+
return;
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1656
|
+
console.warn(chalk.yellow("API reset failed, falling back to direct database update..."));
|
|
1657
|
+
console.warn(chalk.gray(` Details: ${errMsg}`));
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
1661
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
1662
|
+
if (!tsxBin) {
|
|
1663
|
+
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
1664
|
+
process.exit(1);
|
|
1665
|
+
}
|
|
1666
|
+
try {
|
|
1667
|
+
const env = { ...process.env };
|
|
1668
|
+
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
1669
|
+
env.REBASE_RESET_EMAIL = email;
|
|
1670
|
+
env.REBASE_RESET_PASSWORD = newPassword || "NewPassword123!";
|
|
1671
|
+
env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
|
|
1672
|
+
const scriptContent = `
|
|
1673
|
+
import { createPostgresDatabaseConnection } from "@rebasepro/server-postgres";
|
|
1674
|
+
import { hashPassword } from "@rebasepro/server";
|
|
1675
|
+
import { eq } from "drizzle-orm";
|
|
1676
|
+
import * as dotenv from "dotenv";
|
|
1677
|
+
import path from "path";
|
|
1678
|
+
import fs from "fs";
|
|
1679
|
+
|
|
1680
|
+
dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH });
|
|
1681
|
+
|
|
1682
|
+
const email = process.env.REBASE_RESET_EMAIL!;
|
|
1683
|
+
const newPassword = process.env.REBASE_RESET_PASSWORD!;
|
|
1684
|
+
|
|
1685
|
+
async function resetPassword() {
|
|
1686
|
+
const { db } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);
|
|
1687
|
+
const hash = await hashPassword(newPassword);
|
|
1688
|
+
|
|
1689
|
+
let usersTable;
|
|
1690
|
+
try {
|
|
1691
|
+
const schemaPath = path.resolve("./src/schema.generated.ts");
|
|
1692
|
+
if (fs.existsSync(schemaPath)) {
|
|
1693
|
+
const schema = await import("file://" + schemaPath);
|
|
1694
|
+
usersTable = schema.users || schema.tables?.users;
|
|
58
1695
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
if (!args["--git"]) {
|
|
64
|
-
questions.push({
|
|
65
|
-
type: "confirm",
|
|
66
|
-
name: "git",
|
|
67
|
-
message: "Initialize a git repository?",
|
|
68
|
-
default: true
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
const answers = await inquirer.prompt(questions);
|
|
72
|
-
const projectName = nameArg || answers.projectName;
|
|
73
|
-
const targetDirectory = path.resolve(process.cwd(), projectName);
|
|
74
|
-
const templateDirectory = path.resolve(cliRoot, "templates", "template");
|
|
75
|
-
return {
|
|
76
|
-
projectName,
|
|
77
|
-
git: args["--git"] || answers.git || false,
|
|
78
|
-
targetDirectory,
|
|
79
|
-
templateDirectory
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
async function createProject(options) {
|
|
83
|
-
if (fs.existsSync(options.targetDirectory)) {
|
|
84
|
-
if (fs.readdirSync(options.targetDirectory).length !== 0) {
|
|
85
|
-
console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
|
|
86
|
-
process.exit(1);
|
|
1696
|
+
} catch (e) {
|
|
1697
|
+
// ignore and fallback
|
|
87
1698
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await access(options.templateDirectory, fs.constants.R_OK);
|
|
93
|
-
} catch {
|
|
94
|
-
console.error(`${chalk.red.bold("ERROR")} Template not found at ${options.templateDirectory}`);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
console.log(chalk.gray(" Copying project files..."));
|
|
98
|
-
await copy(options.templateDirectory, options.targetDirectory, {
|
|
99
|
-
clobber: false,
|
|
100
|
-
dot: true,
|
|
101
|
-
filter: (source) => {
|
|
102
|
-
const basename = path.basename(source);
|
|
103
|
-
return basename !== "node_modules" && basename !== ".DS_Store";
|
|
1699
|
+
|
|
1700
|
+
if (!usersTable) {
|
|
1701
|
+
const pgServer = await import("@rebasepro/server-postgres");
|
|
1702
|
+
usersTable = pgServer.users;
|
|
104
1703
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
);
|
|
122
|
-
envContent += `
|
|
123
|
-
# Docker Compose Database Password
|
|
124
|
-
POSTGRES_PASSWORD=${dbPassword}
|
|
125
|
-
`;
|
|
126
|
-
fs.writeFileSync(envPath, envContent, "utf-8");
|
|
127
|
-
}
|
|
128
|
-
if (options.git) {
|
|
129
|
-
console.log(chalk.gray(" Initializing git repository..."));
|
|
130
|
-
try {
|
|
131
|
-
await execa("git", ["init"], { cwd: options.targetDirectory });
|
|
132
|
-
} catch {
|
|
133
|
-
console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
|
|
1704
|
+
|
|
1705
|
+
const passwordHashKey = (usersTable.passwordHash || "passwordHash" in usersTable) ? "passwordHash" : "password_hash";
|
|
1706
|
+
|
|
1707
|
+
const result = await db.update(usersTable)
|
|
1708
|
+
.set({ [passwordHashKey]: hash })
|
|
1709
|
+
.where(eq(usersTable.email, email))
|
|
1710
|
+
.returning({
|
|
1711
|
+
id: usersTable.id,
|
|
1712
|
+
email: usersTable.email
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
if (result.length > 0) {
|
|
1716
|
+
console.log("✅ Password reset for: " + result[0].email);
|
|
1717
|
+
${!newPassword ? "console.log(\" New password: \" + newPassword);" : ""}
|
|
1718
|
+
} else {
|
|
1719
|
+
console.log("✗ User not found: " + email);
|
|
134
1720
|
}
|
|
135
|
-
|
|
136
|
-
console.log("");
|
|
137
|
-
console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
|
|
138
|
-
console.log("");
|
|
139
|
-
console.log(chalk.bold("Next steps:"));
|
|
140
|
-
console.log("");
|
|
141
|
-
console.log(` ${chalk.cyan("cd")} ${options.projectName}`);
|
|
142
|
-
console.log(` ${chalk.cyan("pnpm install")}`);
|
|
143
|
-
console.log("");
|
|
144
|
-
console.log(chalk.gray(" # Set up your database connection in .env"));
|
|
145
|
-
console.log(chalk.gray(" # Then run:"));
|
|
146
|
-
console.log("");
|
|
147
|
-
console.log(` ${chalk.cyan("pnpm dev")}`);
|
|
148
|
-
console.log("");
|
|
149
|
-
console.log(chalk.gray("This starts both the backend (Express + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
|
|
150
|
-
console.log("");
|
|
151
|
-
console.log(chalk.gray("Docs: https://rebase.pro/docs"));
|
|
152
|
-
console.log(chalk.gray("GitHub: https://github.com/rebaseco/rebase"));
|
|
153
|
-
console.log("");
|
|
1721
|
+
process.exit(0);
|
|
154
1722
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
1723
|
+
|
|
1724
|
+
resetPassword().catch(console.error);
|
|
1725
|
+
`;
|
|
1726
|
+
const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
|
|
1727
|
+
fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
|
|
1728
|
+
console.log("");
|
|
1729
|
+
console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)"));
|
|
1730
|
+
console.log("");
|
|
1731
|
+
console.log(` ${chalk.gray("Email:")} ${email}`);
|
|
1732
|
+
if (newPassword) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
|
|
1733
|
+
console.log("");
|
|
1734
|
+
const child = spawn(tsxBin, [tmpScriptPath], {
|
|
1735
|
+
cwd: backendDir,
|
|
1736
|
+
stdio: "inherit",
|
|
1737
|
+
env
|
|
1738
|
+
});
|
|
1739
|
+
return new Promise((resolve) => {
|
|
1740
|
+
child.on("close", (code) => {
|
|
1741
|
+
try {
|
|
1742
|
+
fs.unlinkSync(tmpScriptPath);
|
|
1743
|
+
} catch {}
|
|
1744
|
+
if (code !== 0) process.exit(code ?? 1);
|
|
1745
|
+
resolve();
|
|
1746
|
+
});
|
|
1747
|
+
});
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
console.error(chalk.red("✗ Direct database update failed."));
|
|
1750
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1751
|
+
process.exit(1);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
function printAuthHelp() {
|
|
1755
|
+
console.log(`
|
|
1756
|
+
${chalk.bold("rebase auth")} — Authentication management commands
|
|
1757
|
+
|
|
1758
|
+
${chalk.green.bold("Usage")}
|
|
1759
|
+
rebase auth ${chalk.blue("<command>")} [options]
|
|
1760
|
+
|
|
1761
|
+
${chalk.green.bold("Commands")}
|
|
1762
|
+
${chalk.blue.bold("reset-password")} Reset a user's password
|
|
1763
|
+
|
|
1764
|
+
${chalk.green.bold("reset-password Options")}
|
|
1765
|
+
${chalk.blue("--email, -e")} User's email address
|
|
1766
|
+
${chalk.blue("--password, -p")} New password (default: NewPassword123!)
|
|
1767
|
+
|
|
1768
|
+
${chalk.green.bold("Examples")}
|
|
1769
|
+
rebase auth reset-password user@example.com
|
|
1770
|
+
rebase auth reset-password --email user@example.com --password MyNewPass!
|
|
1771
|
+
`);
|
|
1772
|
+
}
|
|
1773
|
+
//#endregion
|
|
1774
|
+
//#region src/commands/doctor.ts
|
|
1775
|
+
/**
|
|
1776
|
+
* CLI command: rebase doctor
|
|
1777
|
+
*
|
|
1778
|
+
* Detects three-way schema drift between collection definitions,
|
|
1779
|
+
* the generated Drizzle schema, and the live PostgreSQL database.
|
|
1780
|
+
*/
|
|
1781
|
+
async function doctorCommand(rawArgs) {
|
|
1782
|
+
const projectRoot = requireProjectRoot();
|
|
1783
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
1784
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
1785
|
+
if (!activePlugin) {
|
|
1786
|
+
console.error(chalk.red("✗ Could not detect an active database plugin."));
|
|
1787
|
+
console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json."));
|
|
1788
|
+
process.exit(1);
|
|
1789
|
+
}
|
|
1790
|
+
const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
|
|
1791
|
+
if (!pluginCli) {
|
|
1792
|
+
console.error(chalk.red(`✗ Could not find CLI entry point for ${activePlugin}.`));
|
|
1793
|
+
process.exit(1);
|
|
1794
|
+
}
|
|
1795
|
+
const envFile = findEnvFile(projectRoot);
|
|
1796
|
+
const env = { ...process.env };
|
|
1797
|
+
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
1798
|
+
try {
|
|
1799
|
+
if (pluginCli.endsWith(".ts")) {
|
|
1800
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
1801
|
+
if (!tsxBin) {
|
|
1802
|
+
console.error(chalk.red("✗ Could not find tsx binary."));
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
await execa(tsxBin, [pluginCli, ...rawArgs.slice(2)], {
|
|
1806
|
+
cwd: backendDir,
|
|
1807
|
+
stdio: "inherit",
|
|
1808
|
+
env
|
|
1809
|
+
});
|
|
1810
|
+
} else await execa("node", [pluginCli, ...rawArgs.slice(2)], {
|
|
1811
|
+
cwd: backendDir,
|
|
1812
|
+
stdio: "inherit",
|
|
1813
|
+
env
|
|
1814
|
+
});
|
|
1815
|
+
} catch {
|
|
1816
|
+
process.exit(1);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
//#endregion
|
|
1820
|
+
//#region src/commands/skills.ts
|
|
1821
|
+
var require = createRequire(import.meta.url);
|
|
1822
|
+
/** Supported agent environments and their target directories. */
|
|
1823
|
+
var AGENTS = {
|
|
1824
|
+
cursor: {
|
|
1825
|
+
label: "Cursor",
|
|
1826
|
+
detectDir: ".cursor",
|
|
1827
|
+
targetDir: ".cursor/rules",
|
|
1828
|
+
/** Cursor uses .mdc files (Markdown with Context). */
|
|
1829
|
+
transformFile: (skillName, content) => ({
|
|
1830
|
+
fileName: `${skillName}.mdc`,
|
|
1831
|
+
content
|
|
1832
|
+
})
|
|
1833
|
+
},
|
|
1834
|
+
claude: {
|
|
1835
|
+
label: "Claude Code",
|
|
1836
|
+
detectDir: ".claude",
|
|
1837
|
+
targetDir: ".claude/skills",
|
|
1838
|
+
/** Claude Code uses the standard SKILL.md format in subdirectories. */
|
|
1839
|
+
transformFile: (skillName, content) => ({
|
|
1840
|
+
fileName: path.join(skillName, "SKILL.md"),
|
|
1841
|
+
content
|
|
1842
|
+
})
|
|
1843
|
+
},
|
|
1844
|
+
windsurf: {
|
|
1845
|
+
label: "Windsurf",
|
|
1846
|
+
detectDir: ".windsurf",
|
|
1847
|
+
targetDir: ".windsurf/rules",
|
|
1848
|
+
/** Windsurf uses plain .md files. */
|
|
1849
|
+
transformFile: (skillName, content) => ({
|
|
1850
|
+
fileName: `${skillName}.md`,
|
|
1851
|
+
content
|
|
1852
|
+
})
|
|
1853
|
+
},
|
|
1854
|
+
gemini: {
|
|
1855
|
+
label: "Gemini CLI / Antigravity",
|
|
1856
|
+
detectDir: ".agents",
|
|
1857
|
+
targetDir: ".agents/skills",
|
|
1858
|
+
/** Gemini uses the standard SKILL.md format in subdirectories. */
|
|
1859
|
+
transformFile: (skillName, content) => ({
|
|
1860
|
+
fileName: path.join(skillName, "SKILL.md"),
|
|
1861
|
+
content
|
|
1862
|
+
})
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1865
|
+
/**
|
|
1866
|
+
* Resolve the path to the skills directory from @rebasepro/agent-skills.
|
|
1867
|
+
* Works in both workspace (symlink) and published (real files) layouts.
|
|
1868
|
+
*/
|
|
1869
|
+
function getSkillsSourceDir() {
|
|
1870
|
+
const pkgJsonPath = require.resolve("@rebasepro/agent-skills/package.json");
|
|
1871
|
+
const pkgRoot = path.dirname(pkgJsonPath);
|
|
1872
|
+
const skillsDir = path.join(pkgRoot, "skills");
|
|
1873
|
+
if (!fs.existsSync(skillsDir)) throw new Error(`Skills directory not found at ${skillsDir}. Make sure @rebasepro/agent-skills is installed.`);
|
|
1874
|
+
return skillsDir;
|
|
1875
|
+
}
|
|
1876
|
+
/** Read all skill directories and return their names + content. */
|
|
1877
|
+
function loadSkills(skillsDir) {
|
|
1878
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
1879
|
+
const skills = [];
|
|
1880
|
+
for (const entry of entries) {
|
|
1881
|
+
if (!entry.isDirectory()) continue;
|
|
1882
|
+
const skillMdPath = path.join(skillsDir, entry.name, "SKILL.md");
|
|
1883
|
+
if (!fs.existsSync(skillMdPath)) continue;
|
|
1884
|
+
skills.push({
|
|
1885
|
+
name: entry.name,
|
|
1886
|
+
content: fs.readFileSync(skillMdPath, "utf-8")
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
return skills;
|
|
1890
|
+
}
|
|
1891
|
+
/** Detect which agent environments already exist in the project. */
|
|
1892
|
+
function detectAgents(projectDir) {
|
|
1893
|
+
const detected = [];
|
|
1894
|
+
for (const [key, agent] of Object.entries(AGENTS)) if (fs.existsSync(path.join(projectDir, agent.detectDir))) detected.push(key);
|
|
1895
|
+
return detected;
|
|
1896
|
+
}
|
|
1897
|
+
/** Install skills for a specific agent into the project directory. */
|
|
1898
|
+
function installForAgent(agentKey, skills, projectDir) {
|
|
1899
|
+
const agent = AGENTS[agentKey];
|
|
1900
|
+
const targetBase = path.join(projectDir, agent.targetDir);
|
|
1901
|
+
fs.mkdirSync(targetBase, { recursive: true });
|
|
1902
|
+
let count = 0;
|
|
1903
|
+
for (const skill of skills) {
|
|
1904
|
+
const { fileName, content } = agent.transformFile(skill.name, skill.content);
|
|
1905
|
+
const targetPath = path.join(targetBase, fileName);
|
|
1906
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1907
|
+
fs.writeFileSync(targetPath, content, "utf-8");
|
|
1908
|
+
count++;
|
|
1909
|
+
}
|
|
1910
|
+
return count;
|
|
1911
|
+
}
|
|
1912
|
+
async function skillsCommand(subcommand, _args) {
|
|
1913
|
+
switch (subcommand) {
|
|
1914
|
+
case "install":
|
|
1915
|
+
await skillsInstall();
|
|
1916
|
+
break;
|
|
1917
|
+
case "--help":
|
|
1918
|
+
case void 0:
|
|
1919
|
+
printSkillsHelp();
|
|
1920
|
+
break;
|
|
1921
|
+
default:
|
|
1922
|
+
console.log(chalk.red(`Unknown skills subcommand: ${subcommand}`));
|
|
1923
|
+
console.log("");
|
|
1924
|
+
printSkillsHelp();
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
async function skillsInstall() {
|
|
1928
|
+
const projectDir = process.cwd();
|
|
1929
|
+
let skillsDir;
|
|
1930
|
+
try {
|
|
1931
|
+
skillsDir = getSkillsSourceDir();
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
console.error(`${chalk.red.bold("ERROR")} ${err instanceof Error ? err.message : String(err)}`);
|
|
1934
|
+
process.exit(1);
|
|
1935
|
+
}
|
|
1936
|
+
const skills = loadSkills(skillsDir);
|
|
1937
|
+
if (skills.length === 0) {
|
|
1938
|
+
console.error(`${chalk.red.bold("ERROR")} No skills found in ${skillsDir}`);
|
|
1939
|
+
process.exit(1);
|
|
1940
|
+
}
|
|
1941
|
+
let agents = detectAgents(projectDir);
|
|
1942
|
+
if (agents.length === 0) {
|
|
1943
|
+
const choices = Object.entries(AGENTS).map(([key, agent]) => ({
|
|
1944
|
+
name: agent.label,
|
|
1945
|
+
value: key,
|
|
1946
|
+
checked: false
|
|
1947
|
+
}));
|
|
1948
|
+
const { selectedAgents } = await inquirer.prompt([{
|
|
1949
|
+
type: "checkbox",
|
|
1950
|
+
name: "selectedAgents",
|
|
1951
|
+
message: "No AI agent configuration detected. Which agents do you use?",
|
|
1952
|
+
choices,
|
|
1953
|
+
validate: (input) => {
|
|
1954
|
+
if (input.length === 0) return "Please select at least one agent.";
|
|
1955
|
+
return true;
|
|
1956
|
+
}
|
|
1957
|
+
}]);
|
|
1958
|
+
agents = selectedAgents;
|
|
1959
|
+
}
|
|
1960
|
+
console.log("");
|
|
1961
|
+
console.log(chalk.gray(` Found ${chalk.white(skills.length)} Rebase skills`));
|
|
1962
|
+
console.log("");
|
|
1963
|
+
for (const agentKey of agents) {
|
|
1964
|
+
const agent = AGENTS[agentKey];
|
|
1965
|
+
const count = installForAgent(agentKey, skills, projectDir);
|
|
1966
|
+
console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} — ${count} skills installed to ${chalk.gray(agent.targetDir)}`);
|
|
1967
|
+
}
|
|
1968
|
+
console.log("");
|
|
1969
|
+
console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
|
|
1970
|
+
console.log(chalk.gray(" Re-run this command anytime to update to the latest skills."));
|
|
1971
|
+
console.log("");
|
|
1972
|
+
}
|
|
1973
|
+
function printSkillsHelp() {
|
|
1974
|
+
console.log(`
|
|
1975
|
+
${chalk.bold("rebase skills")} — Manage AI agent skills
|
|
1976
|
+
|
|
1977
|
+
${chalk.green.bold("Usage")}
|
|
1978
|
+
rebase skills ${chalk.blue("<subcommand>")}
|
|
1979
|
+
|
|
1980
|
+
${chalk.green.bold("Subcommands")}
|
|
1981
|
+
${chalk.blue.bold("install")} Install Rebase agent skills for your AI coding assistant
|
|
1982
|
+
Supports: Cursor, Claude Code, Windsurf, Gemini CLI, Antigravity
|
|
1983
|
+
|
|
1984
|
+
${chalk.green.bold("Examples")}
|
|
1985
|
+
${chalk.cyan("rebase skills install")}
|
|
1986
|
+
`);
|
|
1987
|
+
}
|
|
1988
|
+
//#endregion
|
|
1989
|
+
//#region src/commands/api-keys.ts
|
|
1990
|
+
/**
|
|
1991
|
+
* CLI command: rebase api-keys <action>
|
|
1992
|
+
*
|
|
1993
|
+
* Subcommands:
|
|
1994
|
+
* list — List all API keys (masked)
|
|
1995
|
+
* create — Create a new API key
|
|
1996
|
+
* revoke — Revoke an existing API key
|
|
1997
|
+
*/
|
|
1998
|
+
function loadEnv(projectRoot) {
|
|
1999
|
+
const envFile = findEnvFile(projectRoot);
|
|
2000
|
+
const env = {};
|
|
2001
|
+
if (envFile && fs.existsSync(envFile)) {
|
|
2002
|
+
const content = fs.readFileSync(envFile, "utf-8");
|
|
2003
|
+
for (const line of content.split("\n")) {
|
|
2004
|
+
const trimmed = line.trim();
|
|
2005
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2006
|
+
const idx = trimmed.indexOf("=");
|
|
2007
|
+
if (idx > 0) {
|
|
2008
|
+
const key = trimmed.slice(0, idx).trim();
|
|
2009
|
+
let value = trimmed.slice(idx + 1).trim();
|
|
2010
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
2011
|
+
env[key] = value;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return env;
|
|
2016
|
+
}
|
|
2017
|
+
function resolveBaseUrl(env) {
|
|
2018
|
+
const port = env.PORT || env.REBASE_PORT || "3001";
|
|
2019
|
+
return env.REBASE_BASE_URL || `http://localhost:${port}`;
|
|
2020
|
+
}
|
|
2021
|
+
async function apiKeysCommand(subcommand, rawArgs) {
|
|
2022
|
+
if (!subcommand || subcommand === "--help") {
|
|
2023
|
+
printApiKeysHelp();
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
switch (subcommand) {
|
|
2027
|
+
case "list":
|
|
2028
|
+
await listKeys(rawArgs);
|
|
2029
|
+
break;
|
|
2030
|
+
case "create":
|
|
2031
|
+
await createKey(rawArgs);
|
|
2032
|
+
break;
|
|
2033
|
+
case "revoke":
|
|
2034
|
+
await revokeKey(rawArgs);
|
|
2035
|
+
break;
|
|
2036
|
+
default:
|
|
2037
|
+
console.error(chalk.red(`Unknown api-keys command: ${subcommand}`));
|
|
2038
|
+
console.log("");
|
|
2039
|
+
printApiKeysHelp();
|
|
2040
|
+
process.exit(1);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
async function listKeys(_rawArgs) {
|
|
2044
|
+
const env = loadEnv(requireProjectRoot());
|
|
2045
|
+
const baseUrl = resolveBaseUrl(env);
|
|
2046
|
+
const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
|
|
2047
|
+
if (!serviceKey) {
|
|
2048
|
+
console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
|
|
2049
|
+
process.exit(1);
|
|
2050
|
+
}
|
|
2051
|
+
try {
|
|
2052
|
+
const res = await fetch(`${baseUrl}/api/admin/api-keys`, { headers: { Authorization: `Bearer ${serviceKey}` } });
|
|
2053
|
+
if (!res.ok) {
|
|
2054
|
+
const body = await res.text();
|
|
2055
|
+
console.error(chalk.red(`✗ Failed to list API keys: ${res.status} ${body}`));
|
|
2056
|
+
process.exit(1);
|
|
2057
|
+
}
|
|
2058
|
+
const { keys } = await res.json();
|
|
2059
|
+
console.log("");
|
|
2060
|
+
console.log(chalk.bold(" 🔑 API Keys"));
|
|
2061
|
+
console.log("");
|
|
2062
|
+
if (keys.length === 0) {
|
|
2063
|
+
console.log(chalk.gray(" No API keys found."));
|
|
2064
|
+
console.log("");
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
for (const key of keys) {
|
|
2068
|
+
const status = key.revoked_at ? chalk.red("revoked") : key.expires_at && new Date(key.expires_at) < /* @__PURE__ */ new Date() ? chalk.yellow("expired") : chalk.green("active");
|
|
2069
|
+
const perms = key.permissions.map((p) => `${p.collection}(${p.operations.join(",")})`).join(", ");
|
|
2070
|
+
console.log(` ${chalk.bold(key.name)} ${chalk.gray(`[${key.key_prefix}•••]`)} ${status}`);
|
|
2071
|
+
console.log(` ${chalk.gray("ID:")} ${key.id}`);
|
|
2072
|
+
console.log(` ${chalk.gray("Permissions:")} ${perms || "none"}`);
|
|
2073
|
+
console.log(` ${chalk.gray("Created:")} ${new Date(key.created_at).toLocaleDateString()}`);
|
|
2074
|
+
if (key.last_used_at) console.log(` ${chalk.gray("Last used:")} ${new Date(key.last_used_at).toLocaleDateString()}`);
|
|
2075
|
+
console.log("");
|
|
2076
|
+
}
|
|
2077
|
+
} catch (e) {
|
|
2078
|
+
console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
|
|
2079
|
+
console.error(chalk.gray(" Is the Rebase server running?"));
|
|
2080
|
+
process.exit(1);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
async function createKey(rawArgs) {
|
|
2084
|
+
const args = arg({
|
|
2085
|
+
"--name": String,
|
|
2086
|
+
"--permissions": String,
|
|
2087
|
+
"--admin": Boolean,
|
|
2088
|
+
"--rate-limit": Number,
|
|
2089
|
+
"--expires": String,
|
|
2090
|
+
"-n": "--name"
|
|
2091
|
+
}, {
|
|
2092
|
+
argv: rawArgs.slice(4),
|
|
2093
|
+
permissive: true
|
|
2094
|
+
});
|
|
2095
|
+
const name = args["--name"] || args._[0];
|
|
2096
|
+
const permissionsRaw = args["--permissions"];
|
|
2097
|
+
if (!name) {
|
|
2098
|
+
console.error(chalk.red("✗ Name is required."));
|
|
2099
|
+
console.log("");
|
|
2100
|
+
console.log(chalk.gray(" Usage: rebase api-keys create --name \"My Key\" --permissions '[{\"collection\":\"*\",\"operations\":[\"read\"]}]'"));
|
|
2101
|
+
process.exit(1);
|
|
2102
|
+
}
|
|
2103
|
+
let permissions;
|
|
2104
|
+
if (permissionsRaw) try {
|
|
2105
|
+
permissions = JSON.parse(permissionsRaw);
|
|
2106
|
+
} catch {
|
|
2107
|
+
console.error(chalk.red("✗ Invalid --permissions JSON."));
|
|
2108
|
+
console.log(chalk.gray(" Example: '[{\"collection\":\"*\",\"operations\":[\"read\",\"write\"]}]'"));
|
|
2109
|
+
process.exit(1);
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
else permissions = [{
|
|
2113
|
+
collection: "*",
|
|
2114
|
+
operations: [
|
|
2115
|
+
"read",
|
|
2116
|
+
"write",
|
|
2117
|
+
"delete"
|
|
2118
|
+
]
|
|
2119
|
+
}];
|
|
2120
|
+
let expires_at = null;
|
|
2121
|
+
const expiresFlag = args["--expires"];
|
|
2122
|
+
if (expiresFlag) {
|
|
2123
|
+
const days = {
|
|
2124
|
+
"7d": 7,
|
|
2125
|
+
"30d": 30,
|
|
2126
|
+
"90d": 90,
|
|
2127
|
+
"1y": 365
|
|
2128
|
+
};
|
|
2129
|
+
if (days[expiresFlag]) expires_at = new Date(Date.now() + days[expiresFlag] * 864e5).toISOString();
|
|
2130
|
+
else {
|
|
2131
|
+
const parsed = new Date(expiresFlag);
|
|
2132
|
+
if (isNaN(parsed.getTime())) {
|
|
2133
|
+
console.error(chalk.red("✗ Invalid --expires value. Use 7d, 30d, 90d, 1y, or an ISO date."));
|
|
2134
|
+
process.exit(1);
|
|
2135
|
+
}
|
|
2136
|
+
expires_at = parsed.toISOString();
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
const env = loadEnv(requireProjectRoot());
|
|
2140
|
+
const baseUrl = resolveBaseUrl(env);
|
|
2141
|
+
const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
|
|
2142
|
+
if (!serviceKey) {
|
|
2143
|
+
console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
|
|
2144
|
+
process.exit(1);
|
|
2145
|
+
}
|
|
2146
|
+
try {
|
|
2147
|
+
const body = {
|
|
2148
|
+
name,
|
|
2149
|
+
permissions,
|
|
2150
|
+
admin: args["--admin"] ?? false,
|
|
2151
|
+
rate_limit: args["--rate-limit"] ?? null,
|
|
2152
|
+
expires_at
|
|
2153
|
+
};
|
|
2154
|
+
const res = await fetch(`${baseUrl}/api/admin/api-keys`, {
|
|
2155
|
+
method: "POST",
|
|
2156
|
+
headers: {
|
|
2157
|
+
Authorization: `Bearer ${serviceKey}`,
|
|
2158
|
+
"Content-Type": "application/json"
|
|
2159
|
+
},
|
|
2160
|
+
body: JSON.stringify(body)
|
|
2161
|
+
});
|
|
2162
|
+
if (!res.ok) {
|
|
2163
|
+
const errBody = await res.text();
|
|
2164
|
+
console.error(chalk.red(`✗ Failed to create API key: ${res.status} ${errBody}`));
|
|
2165
|
+
process.exit(1);
|
|
2166
|
+
}
|
|
2167
|
+
const { key } = await res.json();
|
|
2168
|
+
console.log("");
|
|
2169
|
+
console.log(chalk.bold.green(" ✓ API key created successfully"));
|
|
2170
|
+
console.log("");
|
|
2171
|
+
console.log(` ${chalk.gray("Name:")} ${key.name}`);
|
|
2172
|
+
console.log(` ${chalk.gray("ID:")} ${key.id}`);
|
|
2173
|
+
console.log(` ${chalk.gray("Prefix:")} ${key.key_prefix}`);
|
|
2174
|
+
console.log("");
|
|
2175
|
+
console.log(chalk.bold.yellow(" ⚠ Copy your key now — it won't be shown again:"));
|
|
2176
|
+
console.log("");
|
|
2177
|
+
console.log(` ${chalk.cyan(key.key)}`);
|
|
2178
|
+
console.log("");
|
|
2179
|
+
} catch (e) {
|
|
2180
|
+
console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
|
|
2181
|
+
console.error(chalk.gray(" Is the Rebase server running?"));
|
|
2182
|
+
process.exit(1);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
async function revokeKey(rawArgs) {
|
|
2186
|
+
const args = arg({ "--id": String }, {
|
|
2187
|
+
argv: rawArgs.slice(4),
|
|
2188
|
+
permissive: true
|
|
2189
|
+
});
|
|
2190
|
+
const id = args["--id"] || args._[0];
|
|
2191
|
+
if (!id) {
|
|
2192
|
+
console.error(chalk.red("✗ Key ID is required."));
|
|
2193
|
+
console.log("");
|
|
2194
|
+
console.log(chalk.gray(" Usage: rebase api-keys revoke <key-id>"));
|
|
2195
|
+
process.exit(1);
|
|
2196
|
+
}
|
|
2197
|
+
const env = loadEnv(requireProjectRoot());
|
|
2198
|
+
const baseUrl = resolveBaseUrl(env);
|
|
2199
|
+
const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
|
|
2200
|
+
if (!serviceKey) {
|
|
2201
|
+
console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
|
|
2202
|
+
process.exit(1);
|
|
2203
|
+
}
|
|
2204
|
+
try {
|
|
2205
|
+
const res = await fetch(`${baseUrl}/api/admin/api-keys/${encodeURIComponent(id)}`, {
|
|
2206
|
+
method: "DELETE",
|
|
2207
|
+
headers: { Authorization: `Bearer ${serviceKey}` }
|
|
2208
|
+
});
|
|
2209
|
+
if (!res.ok) {
|
|
2210
|
+
const errBody = await res.text();
|
|
2211
|
+
console.error(chalk.red(`✗ Failed to revoke API key: ${res.status} ${errBody}`));
|
|
2212
|
+
process.exit(1);
|
|
2213
|
+
}
|
|
2214
|
+
console.log("");
|
|
2215
|
+
console.log(chalk.bold.green(" ✓ API key revoked successfully"));
|
|
2216
|
+
console.log(` ${chalk.gray("ID:")} ${id}`);
|
|
2217
|
+
console.log("");
|
|
2218
|
+
} catch (e) {
|
|
2219
|
+
console.error(chalk.red(`✗ ${e instanceof Error ? e.message : String(e)}`));
|
|
2220
|
+
console.error(chalk.gray(" Is the Rebase server running?"));
|
|
2221
|
+
process.exit(1);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
function printApiKeysHelp() {
|
|
2225
|
+
console.log(`
|
|
2226
|
+
${chalk.bold("rebase api-keys")} — Manage Service API Keys
|
|
2227
|
+
|
|
2228
|
+
${chalk.green.bold("Usage")}
|
|
2229
|
+
rebase api-keys ${chalk.blue("<command>")} [options]
|
|
2230
|
+
|
|
2231
|
+
${chalk.green.bold("Commands")}
|
|
2232
|
+
${chalk.blue.bold("list")} List all API keys
|
|
2233
|
+
${chalk.blue.bold("create")} Create a new API key
|
|
2234
|
+
${chalk.blue.bold("revoke")} Revoke an API key
|
|
2235
|
+
|
|
2236
|
+
${chalk.green.bold("create Options")}
|
|
2237
|
+
${chalk.blue("--name, -n")} Key name ${chalk.gray("(required)")}
|
|
2238
|
+
${chalk.blue("--permissions")} JSON array of permissions
|
|
2239
|
+
${chalk.gray("Default: [{\"collection\":\"*\",\"operations\":[\"read\",\"write\",\"delete\"]}]")}
|
|
2240
|
+
${chalk.blue("--admin")} Grant admin role (access to admin routes)
|
|
2241
|
+
${chalk.blue("--rate-limit")} Requests per 15-min window ${chalk.gray("(default: 1000)")}
|
|
2242
|
+
${chalk.blue("--expires")} Expiration: 7d, 30d, 90d, 1y, or ISO date
|
|
2243
|
+
|
|
2244
|
+
${chalk.green.bold("revoke Options")}
|
|
2245
|
+
${chalk.blue("--id")} API key ID to revoke ${chalk.gray("(or positional arg)")}
|
|
2246
|
+
|
|
2247
|
+
${chalk.green.bold("Examples")}
|
|
2248
|
+
rebase api-keys list
|
|
2249
|
+
rebase api-keys create --name "Analytics" --permissions '[{"collection":"events","operations":["read"]}]'
|
|
2250
|
+
rebase api-keys create -n "Full Access" --expires 90d
|
|
2251
|
+
rebase api-keys revoke abc123-def456
|
|
2252
|
+
`);
|
|
2253
|
+
}
|
|
2254
|
+
//#endregion
|
|
2255
|
+
//#region src/commands/cloud/context.ts
|
|
2256
|
+
/**
|
|
2257
|
+
* Shared foundation for the `rebase cloud` command family.
|
|
2258
|
+
*
|
|
2259
|
+
* Everything cloud subcommands need in common lives here:
|
|
2260
|
+
* - credential storage (~/.rebase/credentials.json, keyed per control-plane host)
|
|
2261
|
+
* - project link file (.rebase/cloud.json in the project dir)
|
|
2262
|
+
* - control-plane URL resolution
|
|
2263
|
+
* - an authenticated `@rebasepro/client` instance (createCloudClient / requireClient)
|
|
2264
|
+
* - small output helpers shared across subcommands
|
|
2265
|
+
*
|
|
2266
|
+
* The control plane is itself a Rebase app, so we reuse the same SDK the web
|
|
2267
|
+
* console uses (`@rebasepro/client`). Auth, token refresh, the data REST client
|
|
2268
|
+
* and function invocation all come from the SDK — the CLI only supplies a
|
|
2269
|
+
* file-backed AuthStorage so a login persists across invocations.
|
|
2270
|
+
*/
|
|
2271
|
+
/** Default hosted control plane (the Rebase Cloud console origin). */
|
|
2272
|
+
var DEFAULT_CLOUD_URL = "https://app.rebase.pro";
|
|
2273
|
+
/** The storage key the SDK's auth module reads/writes the session under. */
|
|
2274
|
+
var SDK_SESSION_KEY = "rebase_auth";
|
|
2275
|
+
/** ~/.rebase/credentials.json — one file, many hosts. */
|
|
2276
|
+
function credentialsPath() {
|
|
2277
|
+
return path.join(os.homedir(), ".rebase", "credentials.json");
|
|
2278
|
+
}
|
|
2279
|
+
/** Project-local link file: <project>/.rebase/cloud.json */
|
|
2280
|
+
function projectLinkPath(cwd = process.cwd()) {
|
|
2281
|
+
const root = findProjectRoot(cwd) || cwd;
|
|
2282
|
+
return path.join(root, ".rebase", "cloud.json");
|
|
2283
|
+
}
|
|
2284
|
+
function readCredentials() {
|
|
2285
|
+
try {
|
|
2286
|
+
const raw = fs.readFileSync(credentialsPath(), "utf-8");
|
|
2287
|
+
const parsed = JSON.parse(raw);
|
|
2288
|
+
if (!parsed.contexts) parsed.contexts = {};
|
|
2289
|
+
return parsed;
|
|
2290
|
+
} catch {
|
|
2291
|
+
return { contexts: {} };
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function writeCredentials(data) {
|
|
2295
|
+
const file = credentialsPath();
|
|
2296
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
2297
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), { mode: 384 });
|
|
2298
|
+
try {
|
|
2299
|
+
fs.chmodSync(file, 384);
|
|
2300
|
+
} catch {}
|
|
2301
|
+
}
|
|
2302
|
+
/** Host that a bare `rebase cloud` command should target, if any. */
|
|
2303
|
+
function currentContextUrl() {
|
|
2304
|
+
return readCredentials().current;
|
|
2305
|
+
}
|
|
2306
|
+
/** Persist the active organization id for a host. */
|
|
2307
|
+
function setContextOrg(url, org) {
|
|
2308
|
+
const creds = readCredentials();
|
|
2309
|
+
const entry = creds.contexts[url] || {};
|
|
2310
|
+
if (org) entry.org = org;
|
|
2311
|
+
else delete entry.org;
|
|
2312
|
+
creds.contexts[url] = entry;
|
|
2313
|
+
writeCredentials(creds);
|
|
2314
|
+
}
|
|
2315
|
+
function getContextOrg(url) {
|
|
2316
|
+
return readCredentials().contexts[url]?.org;
|
|
2317
|
+
}
|
|
2318
|
+
function createFileAuthStorage(url) {
|
|
2319
|
+
return {
|
|
2320
|
+
getItem(key) {
|
|
2321
|
+
if (key !== SDK_SESSION_KEY) return null;
|
|
2322
|
+
return readCredentials().contexts[url]?.auth ?? null;
|
|
2323
|
+
},
|
|
2324
|
+
setItem(key, value) {
|
|
2325
|
+
if (key !== SDK_SESSION_KEY) return;
|
|
2326
|
+
const creds = readCredentials();
|
|
2327
|
+
const entry = creds.contexts[url] || {};
|
|
2328
|
+
entry.auth = value;
|
|
2329
|
+
creds.contexts[url] = entry;
|
|
2330
|
+
if (!creds.current) creds.current = url;
|
|
2331
|
+
writeCredentials(creds);
|
|
2332
|
+
},
|
|
2333
|
+
removeItem(key) {
|
|
2334
|
+
if (key !== SDK_SESSION_KEY) return;
|
|
2335
|
+
const creds = readCredentials();
|
|
2336
|
+
if (creds.contexts[url]) {
|
|
2337
|
+
delete creds.contexts[url].auth;
|
|
2338
|
+
delete creds.contexts[url].org;
|
|
2339
|
+
}
|
|
2340
|
+
writeCredentials(creds);
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
/** Mark a host as the active context (called on login). */
|
|
2345
|
+
function setCurrentContext(url) {
|
|
2346
|
+
const creds = readCredentials();
|
|
2347
|
+
creds.current = url;
|
|
2348
|
+
if (!creds.contexts[url]) creds.contexts[url] = {};
|
|
2349
|
+
writeCredentials(creds);
|
|
2350
|
+
}
|
|
2351
|
+
function resolveCloudUrl(rawArgs) {
|
|
2352
|
+
const explicit = arg({ "--url": String }, {
|
|
2353
|
+
argv: rawArgs.slice(2),
|
|
2354
|
+
permissive: true
|
|
2355
|
+
})["--url"] || process.env.REBASE_CLOUD_URL;
|
|
2356
|
+
if (explicit) return normalizeUrl(explicit);
|
|
2357
|
+
const link = readLink();
|
|
2358
|
+
if (link?.url) return normalizeUrl(link.url);
|
|
2359
|
+
const current = currentContextUrl();
|
|
2360
|
+
if (current) return normalizeUrl(current);
|
|
2361
|
+
return DEFAULT_CLOUD_URL;
|
|
2362
|
+
}
|
|
2363
|
+
function normalizeUrl(url) {
|
|
2364
|
+
let u = url.trim().replace(/\/+$/, "");
|
|
2365
|
+
if (!/^https?:\/\//.test(u)) u = `https://${u}`;
|
|
2366
|
+
return u;
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Build an SDK client bound to a control-plane host, backed by the on-disk
|
|
2370
|
+
* credential store. `autoRefresh` is disabled so we never leave a dangling
|
|
2371
|
+
* setTimeout that keeps the CLI process alive; token refresh is done on demand
|
|
2372
|
+
* by `requireClient`.
|
|
2373
|
+
*/
|
|
2374
|
+
function createCloudClient(url) {
|
|
2375
|
+
return createRebaseClient({
|
|
2376
|
+
baseUrl: url,
|
|
2377
|
+
websocketUrl: "",
|
|
2378
|
+
auth: {
|
|
2379
|
+
storage: createFileAuthStorage(url),
|
|
2380
|
+
persistSession: true,
|
|
2381
|
+
autoRefresh: false
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
/** Two minutes of head-room before a token is treated as expired. */
|
|
2386
|
+
var EXPIRY_BUFFER_MS = 12e4;
|
|
2387
|
+
/**
|
|
2388
|
+
* Return an authenticated client for the resolved host, refreshing the access
|
|
2389
|
+
* token if it is close to expiry. Exits with a helpful message when there is no
|
|
2390
|
+
* usable session (never logged in, or the refresh token was revoked).
|
|
2391
|
+
*/
|
|
2392
|
+
async function requireClient(rawArgs) {
|
|
2393
|
+
const url = resolveCloudUrl(rawArgs);
|
|
2394
|
+
const client = createCloudClient(url);
|
|
2395
|
+
const session = client.auth.getSession();
|
|
2396
|
+
if (!session || !session.accessToken) fail(`Not logged in to ${chalk.cyan(url)}.`, `Run ${chalk.bold("rebase cloud login")} first.`);
|
|
2397
|
+
if (session.expiresAt <= Date.now() + EXPIRY_BUFFER_MS) try {
|
|
2398
|
+
await client.auth.refreshSession();
|
|
2399
|
+
} catch {
|
|
2400
|
+
fail(`Your session for ${chalk.cyan(url)} has expired.`, `Run ${chalk.bold("rebase cloud login")} to sign in again.`);
|
|
2401
|
+
}
|
|
2402
|
+
return {
|
|
2403
|
+
client,
|
|
2404
|
+
url
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
function readLink(cwd = process.cwd()) {
|
|
2408
|
+
try {
|
|
2409
|
+
return JSON.parse(fs.readFileSync(projectLinkPath(cwd), "utf-8"));
|
|
2410
|
+
} catch {
|
|
2411
|
+
return null;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
function writeLink(link, cwd = process.cwd()) {
|
|
2415
|
+
const file = projectLinkPath(cwd);
|
|
2416
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
2417
|
+
fs.writeFileSync(file, JSON.stringify(link, null, 2));
|
|
2418
|
+
}
|
|
2419
|
+
function removeLink(cwd = process.cwd()) {
|
|
2420
|
+
const file = projectLinkPath(cwd);
|
|
2421
|
+
if (fs.existsSync(file)) {
|
|
2422
|
+
fs.rmSync(file);
|
|
2423
|
+
return true;
|
|
2424
|
+
}
|
|
2425
|
+
return false;
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Resolve the project id to operate on: explicit `--project` flag wins,
|
|
2429
|
+
* otherwise the linked project. Exits with guidance when neither is present.
|
|
2430
|
+
*/
|
|
2431
|
+
function requireProjectId(rawArgs) {
|
|
2432
|
+
const parsed = arg({
|
|
2433
|
+
"--project": String,
|
|
2434
|
+
"-p": "--project"
|
|
2435
|
+
}, {
|
|
2436
|
+
argv: rawArgs.slice(2),
|
|
2437
|
+
permissive: true
|
|
2438
|
+
});
|
|
2439
|
+
if (parsed["--project"]) return parsed["--project"];
|
|
2440
|
+
const link = readLink();
|
|
2441
|
+
if (link?.projectId) return link.projectId;
|
|
2442
|
+
fail("No project specified and this directory is not linked.", `Pass ${chalk.bold("--project <id>")} or run ${chalk.bold("rebase cloud link")}.`);
|
|
2443
|
+
}
|
|
2444
|
+
/** Print an error (+ optional hint) and exit non-zero. Never returns. */
|
|
2445
|
+
function fail(message, hint) {
|
|
2446
|
+
console.error("");
|
|
2447
|
+
console.error(chalk.red(` ✗ ${message}`));
|
|
2448
|
+
if (hint) console.error(chalk.gray(` ${hint}`));
|
|
2449
|
+
console.error("");
|
|
2450
|
+
process.exit(1);
|
|
2451
|
+
}
|
|
2452
|
+
function success(message) {
|
|
2453
|
+
console.log("");
|
|
2454
|
+
console.log(chalk.bold.green(` ✓ ${message}`));
|
|
2455
|
+
console.log("");
|
|
2456
|
+
}
|
|
2457
|
+
/** Colorize a deployment / resource status token. */
|
|
2458
|
+
function colorStatus(status) {
|
|
2459
|
+
switch (status) {
|
|
2460
|
+
case "active":
|
|
2461
|
+
case "success":
|
|
2462
|
+
case "connected": return chalk.green(status);
|
|
2463
|
+
case "deploying":
|
|
2464
|
+
case "provisioning":
|
|
2465
|
+
case "pending_billing":
|
|
2466
|
+
case "untested": return chalk.yellow(status ?? "");
|
|
2467
|
+
case "failed": return chalk.red(status);
|
|
2468
|
+
case "stopped": return chalk.gray(status);
|
|
2469
|
+
default: return chalk.gray(status ?? "unknown");
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
/** Render a two-column key/value block with aligned keys. */
|
|
2473
|
+
function keyValues(rows) {
|
|
2474
|
+
const width = Math.max(...rows.map(([k]) => k.length));
|
|
2475
|
+
for (const [k, v] of rows) {
|
|
2476
|
+
if (v === void 0 || v === "") continue;
|
|
2477
|
+
console.log(` ${chalk.gray(`${k}:`.padEnd(width + 1))} ${v}`);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Surface an SDK/HTTP error consistently. The SDK throws RebaseApiError with
|
|
2482
|
+
* a `.status` and `.message`; anything else falls back to its string form.
|
|
2483
|
+
*/
|
|
2484
|
+
function reportError(e, context) {
|
|
2485
|
+
const err = e;
|
|
2486
|
+
fail(`${context}${err?.status ? ` (${err.status})` : ""}: ${err?.message ?? String(e)}`);
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Open a URL in the user's default browser (best effort). Always prints the URL
|
|
2490
|
+
* first so it stays usable over SSH or when no browser is available.
|
|
2491
|
+
*/
|
|
2492
|
+
function openUrl(target, label = "Opening") {
|
|
2493
|
+
console.log("");
|
|
2494
|
+
console.log(` ${label} ${chalk.cyan(target)}`);
|
|
2495
|
+
console.log("");
|
|
2496
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2497
|
+
try {
|
|
2498
|
+
const child = spawn(opener, [target], {
|
|
2499
|
+
stdio: "ignore",
|
|
2500
|
+
detached: true,
|
|
2501
|
+
shell: process.platform === "win32"
|
|
2502
|
+
});
|
|
2503
|
+
child.on("error", () => {});
|
|
2504
|
+
child.unref();
|
|
2505
|
+
} catch {}
|
|
2506
|
+
}
|
|
2507
|
+
//#endregion
|
|
2508
|
+
//#region src/commands/cloud/auth.ts
|
|
2509
|
+
/**
|
|
2510
|
+
* `rebase cloud` auth subcommands: login, logout, whoami.
|
|
2511
|
+
*/
|
|
2512
|
+
async function loginCommand(rawArgs) {
|
|
2513
|
+
const args = arg({
|
|
2514
|
+
"--email": String,
|
|
2515
|
+
"--password": String,
|
|
2516
|
+
"-e": "--email",
|
|
2517
|
+
"-p": "--password"
|
|
2518
|
+
}, {
|
|
2519
|
+
argv: rawArgs.slice(3),
|
|
2520
|
+
permissive: true
|
|
2521
|
+
});
|
|
2522
|
+
const url = resolveCloudUrl(rawArgs);
|
|
2523
|
+
console.log("");
|
|
2524
|
+
console.log(` Signing in to ${chalk.cyan(url)}`);
|
|
2525
|
+
console.log("");
|
|
2526
|
+
const prompts = [];
|
|
2527
|
+
if (!args["--email"]) prompts.push({
|
|
2528
|
+
type: "input",
|
|
2529
|
+
name: "email",
|
|
2530
|
+
message: "Email:"
|
|
2531
|
+
});
|
|
2532
|
+
if (!args["--password"]) prompts.push({
|
|
2533
|
+
type: "password",
|
|
2534
|
+
name: "password",
|
|
2535
|
+
message: "Password:",
|
|
2536
|
+
mask: "•"
|
|
2537
|
+
});
|
|
2538
|
+
const answers = prompts.length ? await inquirer.prompt(prompts) : {};
|
|
2539
|
+
const email = (args["--email"] || answers.email || "").trim();
|
|
2540
|
+
const password = args["--password"] || answers.password || "";
|
|
2541
|
+
if (!email || !password) fail("Email and password are required.");
|
|
2542
|
+
const client = createCloudClient(url);
|
|
2543
|
+
try {
|
|
2544
|
+
const { user } = await client.auth.signInWithEmail(email, password);
|
|
2545
|
+
setCurrentContext(url);
|
|
2546
|
+
try {
|
|
2547
|
+
const orgs = await client.data.collection("organizations").find({ limit: 2 });
|
|
2548
|
+
if (orgs.data.length === 1 && !getContextOrg(url)) setContextOrg(url, String(orgs.data[0].id));
|
|
2549
|
+
} catch {}
|
|
2550
|
+
success(`Logged in as ${chalk.bold(user.email ?? email)}`);
|
|
2551
|
+
keyValues([
|
|
2552
|
+
["Host", url],
|
|
2553
|
+
["User", user.email ?? void 0],
|
|
2554
|
+
["Active org", getContextOrg(url)]
|
|
2555
|
+
]);
|
|
2556
|
+
console.log("");
|
|
2557
|
+
} catch (e) {
|
|
2558
|
+
if (e?.status === 401) fail("Invalid email or password.");
|
|
2559
|
+
reportError(e, "Login failed");
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
async function logoutCommand(rawArgs) {
|
|
2563
|
+
const url = resolveCloudUrl(rawArgs);
|
|
2564
|
+
const client = createCloudClient(url);
|
|
2565
|
+
if (!client.auth.getSession()) {
|
|
2566
|
+
console.log("");
|
|
2567
|
+
console.log(chalk.gray(` Not logged in to ${url}.`));
|
|
2568
|
+
console.log("");
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
try {
|
|
2572
|
+
await client.auth.signOut();
|
|
2573
|
+
} catch {}
|
|
2574
|
+
success(`Logged out of ${url}`);
|
|
2575
|
+
}
|
|
2576
|
+
async function whoamiCommand(rawArgs) {
|
|
2577
|
+
const { client, url } = await requireClient(rawArgs);
|
|
2578
|
+
try {
|
|
2579
|
+
const user = await client.auth.getUser();
|
|
2580
|
+
if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.");
|
|
2581
|
+
const link = readLink();
|
|
2582
|
+
console.log("");
|
|
2583
|
+
console.log(chalk.bold(" 🔐 Rebase Cloud session"));
|
|
2584
|
+
console.log("");
|
|
2585
|
+
keyValues([
|
|
2586
|
+
["Host", url],
|
|
2587
|
+
["User", user.email ?? void 0],
|
|
2588
|
+
["User ID", user.uid],
|
|
2589
|
+
["Roles", user.roles?.length ? user.roles.join(", ") : void 0],
|
|
2590
|
+
["Active org", getContextOrg(url)],
|
|
2591
|
+
["Linked project", link ? `${link.projectName ?? ""} (${link.projectId})`.trim() : void 0]
|
|
2592
|
+
]);
|
|
2593
|
+
console.log("");
|
|
2594
|
+
} catch (e) {
|
|
2595
|
+
reportError(e, "Failed to fetch session");
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
//#endregion
|
|
2599
|
+
//#region src/commands/cloud/link.ts
|
|
2600
|
+
/**
|
|
2601
|
+
* `rebase cloud` context subcommands: link, unlink, use, open.
|
|
2602
|
+
*
|
|
2603
|
+
* `link` associates the current directory with a cloud project by writing
|
|
2604
|
+
* `.rebase/cloud.json`; deploy/logs/status then operate on it with no flags.
|
|
2605
|
+
*/
|
|
2606
|
+
async function linkCommand(rawArgs) {
|
|
2607
|
+
const args = arg({
|
|
2608
|
+
"--project": String,
|
|
2609
|
+
"-p": "--project"
|
|
2610
|
+
}, {
|
|
2611
|
+
argv: rawArgs.slice(3),
|
|
2612
|
+
permissive: true
|
|
2613
|
+
});
|
|
2614
|
+
const { client, url } = await requireClient(rawArgs);
|
|
2615
|
+
try {
|
|
2616
|
+
let project;
|
|
2617
|
+
if (args["--project"]) {
|
|
2618
|
+
project = await client.data.collection("projects").findById(args["--project"]);
|
|
2619
|
+
if (!project) fail(`Project ${args["--project"]} not found.`);
|
|
2620
|
+
} else {
|
|
2621
|
+
const org = getContextOrg(url);
|
|
2622
|
+
const projects = (await client.data.collection("projects").find({
|
|
2623
|
+
where: org ? { organization: ["==", org] } : void 0,
|
|
2624
|
+
limit: 100
|
|
2625
|
+
})).data;
|
|
2626
|
+
if (projects.length === 0) fail("No projects found for your account.", `Create one with ${chalk.bold("rebase cloud projects create")}.`);
|
|
2627
|
+
const { picked } = await inquirer.prompt([{
|
|
2628
|
+
type: "list",
|
|
2629
|
+
name: "picked",
|
|
2630
|
+
message: "Select a project to link:",
|
|
2631
|
+
choices: projects.map((p) => ({
|
|
2632
|
+
name: `${p.name ?? "(unnamed)"} ${chalk.gray(`${p.subdomain ?? ""} · ${p.id}`)}`,
|
|
2633
|
+
value: p
|
|
2634
|
+
}))
|
|
2635
|
+
}]);
|
|
2636
|
+
project = picked;
|
|
2637
|
+
}
|
|
2638
|
+
if (!project) fail("No project selected.");
|
|
2639
|
+
writeLink({
|
|
2640
|
+
url,
|
|
2641
|
+
projectId: String(project.id),
|
|
2642
|
+
projectName: project.name,
|
|
2643
|
+
orgId: project.organization !== void 0 ? String(project.organization) : void 0
|
|
2644
|
+
});
|
|
2645
|
+
success(`Linked to ${chalk.bold(project.name ?? project.id)}`);
|
|
2646
|
+
console.log(chalk.gray(` Wrote ${projectLinkPath()}`));
|
|
2647
|
+
console.log("");
|
|
2648
|
+
} catch (e) {
|
|
2649
|
+
reportError(e, "Failed to link project");
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
function unlinkCommand() {
|
|
2653
|
+
if (!readLink()) {
|
|
2654
|
+
console.log("");
|
|
2655
|
+
console.log(chalk.gray(" This directory is not linked to a cloud project."));
|
|
2656
|
+
console.log("");
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
removeLink();
|
|
2660
|
+
success("Unlinked from cloud project");
|
|
2661
|
+
}
|
|
2662
|
+
async function selectOrgCommand(rawArgs) {
|
|
2663
|
+
const target = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
|
|
2664
|
+
const { client, url } = await requireClient(rawArgs);
|
|
2665
|
+
try {
|
|
2666
|
+
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
2667
|
+
if (orgs.length === 0) fail("You are not a member of any organization.");
|
|
2668
|
+
let chosen = target ? orgs.find((o) => String(o.id) === target || o.slug === target) : void 0;
|
|
2669
|
+
if (!chosen && !target) {
|
|
2670
|
+
const { picked } = await inquirer.prompt([{
|
|
2671
|
+
type: "list",
|
|
2672
|
+
name: "picked",
|
|
2673
|
+
message: "Select the active organization:",
|
|
2674
|
+
choices: orgs.map((o) => ({
|
|
2675
|
+
name: `${o.name ?? "(unnamed)"} ${chalk.gray(`${o.slug ?? ""} · ${o.id}`)}`,
|
|
2676
|
+
value: o
|
|
2677
|
+
}))
|
|
2678
|
+
}]);
|
|
2679
|
+
chosen = picked;
|
|
2680
|
+
}
|
|
2681
|
+
if (!chosen) fail(`Organization "${target}" not found.`);
|
|
2682
|
+
setContextOrg(url, String(chosen.id));
|
|
2683
|
+
success(`Active organization set to ${chalk.bold(chosen.name ?? chosen.id)}`);
|
|
2684
|
+
} catch (e) {
|
|
2685
|
+
reportError(e, "Failed to set organization");
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
/** Open the Rebase Cloud dashboard (or the linked project) in a browser. */
|
|
2689
|
+
function openCommand(rawArgs) {
|
|
2690
|
+
const url = resolveCloudUrl(rawArgs);
|
|
2691
|
+
const link = readLink();
|
|
2692
|
+
openUrl(link ? `${url}/projects/${link.projectId}` : url);
|
|
2693
|
+
}
|
|
2694
|
+
//#endregion
|
|
2695
|
+
//#region src/commands/cloud/projects.ts
|
|
2696
|
+
/**
|
|
2697
|
+
* `rebase cloud projects` — list / create / info / delete.
|
|
2698
|
+
*/
|
|
2699
|
+
async function listProjects(rawArgs) {
|
|
2700
|
+
const { client, url } = await requireClient(rawArgs);
|
|
2701
|
+
const org = getContextOrg(url);
|
|
2702
|
+
try {
|
|
2703
|
+
const projects = (await client.data.collection("projects").find({
|
|
2704
|
+
where: org ? { organization: ["==", org] } : void 0,
|
|
2705
|
+
orderBy: ["name", "asc"],
|
|
2706
|
+
limit: 100
|
|
2707
|
+
})).data;
|
|
2708
|
+
console.log("");
|
|
2709
|
+
console.log(chalk.bold(" 📦 Projects") + (org ? chalk.gray(` (org ${org})`) : ""));
|
|
2710
|
+
console.log("");
|
|
2711
|
+
if (projects.length === 0) {
|
|
2712
|
+
console.log(chalk.gray(" No projects yet. Create one with `rebase cloud projects create`."));
|
|
2713
|
+
console.log("");
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
const linkedId = readLink()?.projectId;
|
|
2717
|
+
for (const p of projects) {
|
|
2718
|
+
const marker = String(p.id) === linkedId ? chalk.green(" ●") : " ";
|
|
2719
|
+
console.log(`${marker}${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.id}]`)} ${colorStatus(p.status)}`);
|
|
2720
|
+
console.log(` ${chalk.gray(`${p.subdomain ?? "—"}.rebase.pro`)}${p.provider ? chalk.gray(` · ${p.provider}`) : ""}`);
|
|
2721
|
+
}
|
|
2722
|
+
console.log("");
|
|
2723
|
+
} catch (e) {
|
|
2724
|
+
reportError(e, "Failed to list projects");
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
/** Default region + VM size per provider (both are required on create). */
|
|
2728
|
+
function providerDefaults(provider) {
|
|
2729
|
+
switch (provider) {
|
|
2730
|
+
case "gcp": return {
|
|
2731
|
+
region: "europe-west1",
|
|
2732
|
+
vmSize: "e2-small"
|
|
2733
|
+
};
|
|
2734
|
+
case "aws": return {
|
|
2735
|
+
region: "us-east-1",
|
|
2736
|
+
vmSize: "t3.small"
|
|
2737
|
+
};
|
|
2738
|
+
default: return {
|
|
2739
|
+
region: "nbg1",
|
|
2740
|
+
vmSize: "cx21"
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
async function createProject(rawArgs) {
|
|
2745
|
+
const args = arg({
|
|
2746
|
+
"--name": String,
|
|
2747
|
+
"--subdomain": String,
|
|
2748
|
+
"--repo": String,
|
|
2749
|
+
"--branch": String,
|
|
2750
|
+
"--provider": String,
|
|
2751
|
+
"--region": String,
|
|
2752
|
+
"--vm-size": String,
|
|
2753
|
+
"--org": String,
|
|
2754
|
+
"--link": Boolean,
|
|
2755
|
+
"-n": "--name"
|
|
2756
|
+
}, {
|
|
2757
|
+
argv: rawArgs.slice(4),
|
|
2758
|
+
permissive: true
|
|
2759
|
+
});
|
|
2760
|
+
const { client, url } = await requireClient(rawArgs);
|
|
2761
|
+
const org = args["--org"] || getContextOrg(url);
|
|
2762
|
+
if (!org) fail("No organization selected.", `Pass ${chalk.bold("--org <id>")} or run ${chalk.bold("rebase cloud use")}.`);
|
|
2763
|
+
const prompts = [];
|
|
2764
|
+
if (!args["--name"]) prompts.push({
|
|
2765
|
+
type: "input",
|
|
2766
|
+
name: "name",
|
|
2767
|
+
message: "Project name:"
|
|
2768
|
+
});
|
|
2769
|
+
if (!args["--subdomain"]) prompts.push({
|
|
2770
|
+
type: "input",
|
|
2771
|
+
name: "subdomain",
|
|
2772
|
+
message: "Subdomain:"
|
|
2773
|
+
});
|
|
2774
|
+
const a = prompts.length && process.stdin.isTTY ? await inquirer.prompt(prompts) : {};
|
|
2775
|
+
const name = (args["--name"] || a.name || "").trim();
|
|
2776
|
+
const subdomain = (args["--subdomain"] || a.subdomain || "").trim().toLowerCase();
|
|
2777
|
+
const gitRepoUrl = (args["--repo"] || a.repo || "").trim();
|
|
2778
|
+
const gitBranch = (args["--branch"] || a.branch || "main").trim();
|
|
2779
|
+
const provider = (args["--provider"] || a.provider || "hetzner").trim();
|
|
2780
|
+
const defaults = providerDefaults(provider);
|
|
2781
|
+
const region = (args["--region"] || defaults.region).trim();
|
|
2782
|
+
const vmSize = (args["--vm-size"] || defaults.vmSize).trim();
|
|
2783
|
+
if (!name || !subdomain) fail("Name and subdomain are required.");
|
|
2784
|
+
try {
|
|
2785
|
+
const check = await client.functions.invoke("check-subdomain", { subdomain });
|
|
2786
|
+
if (!check.available) fail(`Subdomain "${subdomain}" is not available${check.reason ? ` (${check.reason})` : ""}.`);
|
|
2787
|
+
} catch {}
|
|
2788
|
+
try {
|
|
2789
|
+
const user = await client.auth.getUser();
|
|
2790
|
+
if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.");
|
|
2791
|
+
const created = await client.data.collection("projects").create({
|
|
2792
|
+
name,
|
|
2793
|
+
subdomain,
|
|
2794
|
+
gitRepoUrl,
|
|
2795
|
+
gitBranch,
|
|
2796
|
+
provider,
|
|
2797
|
+
region,
|
|
2798
|
+
vmSize,
|
|
2799
|
+
organization: org,
|
|
2800
|
+
createdById: user.uid,
|
|
2801
|
+
status: "provisioning"
|
|
2802
|
+
});
|
|
2803
|
+
success(`Created project ${chalk.bold(name)}`);
|
|
2804
|
+
keyValues([
|
|
2805
|
+
["ID", String(created.id)],
|
|
2806
|
+
["URL", `${subdomain}.rebase.pro`],
|
|
2807
|
+
["Provider", provider],
|
|
2808
|
+
["Branch", gitBranch]
|
|
2809
|
+
]);
|
|
2810
|
+
if (args["--link"]) {
|
|
2811
|
+
writeLink({
|
|
2812
|
+
url,
|
|
2813
|
+
projectId: String(created.id),
|
|
2814
|
+
projectName: name,
|
|
2815
|
+
orgId: String(org)
|
|
2816
|
+
});
|
|
2817
|
+
console.log(chalk.gray(" Linked this directory to the new project."));
|
|
2818
|
+
}
|
|
2819
|
+
console.log("");
|
|
2820
|
+
console.log(chalk.gray(` Deploy it with: ${chalk.bold(`rebase cloud deploy --project ${created.id}`)}`));
|
|
2821
|
+
console.log("");
|
|
2822
|
+
} catch (e) {
|
|
2823
|
+
reportError(e, "Failed to create project");
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
async function projectInfo(rawArgs, projectId) {
|
|
2827
|
+
const { client } = await requireClient(rawArgs);
|
|
2828
|
+
try {
|
|
2829
|
+
const p = await client.data.collection("projects").findById(projectId);
|
|
2830
|
+
if (!p) fail(`Project ${projectId} not found.`);
|
|
2831
|
+
const [db, lastDeploy] = await Promise.all([firstRow(client, "databases", projectId), latestDeployment(client, projectId)]);
|
|
2832
|
+
console.log("");
|
|
2833
|
+
console.log(` ${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.id}]`)} ${colorStatus(p.status)}`);
|
|
2834
|
+
console.log("");
|
|
2835
|
+
keyValues([
|
|
2836
|
+
["Subdomain", p.subdomain ? `${p.subdomain}.rebase.pro` : void 0],
|
|
2837
|
+
["Custom domain", p.customDomain],
|
|
2838
|
+
["Repository", p.gitRepoUrl],
|
|
2839
|
+
["Branch", p.gitBranch],
|
|
2840
|
+
["Provider", p.provider],
|
|
2841
|
+
["Region", p.region],
|
|
2842
|
+
["Organization", p.organization !== void 0 ? String(p.organization) : void 0],
|
|
2843
|
+
["Database", db ? `${db.type} (${colorStatus(db.connectionStatus)})` : "none"],
|
|
2844
|
+
["Last deploy", lastDeploy ? `${colorStatus(lastDeploy.status)} · ${fmtDate(lastDeploy.createdAt)}` : "never"]
|
|
2845
|
+
]);
|
|
2846
|
+
console.log("");
|
|
2847
|
+
} catch (e) {
|
|
2848
|
+
reportError(e, "Failed to load project");
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
async function deleteProject(rawArgs, projectId) {
|
|
2852
|
+
const args = arg({
|
|
2853
|
+
"--yes": Boolean,
|
|
2854
|
+
"-y": "--yes"
|
|
2855
|
+
}, {
|
|
2856
|
+
argv: rawArgs.slice(2),
|
|
2857
|
+
permissive: true
|
|
2858
|
+
});
|
|
2859
|
+
const { client } = await requireClient(rawArgs);
|
|
2860
|
+
const p = await client.data.collection("projects").findById(projectId).catch(() => void 0);
|
|
2861
|
+
if (!p) fail(`Project ${projectId} not found.`);
|
|
2862
|
+
if (!args["--yes"]) {
|
|
2863
|
+
const { confirmed } = await inquirer.prompt([{
|
|
2864
|
+
type: "confirm",
|
|
2865
|
+
name: "confirmed",
|
|
2866
|
+
default: false,
|
|
2867
|
+
message: `Permanently delete project "${p.name ?? projectId}" (${projectId})? This tears down its deployment.`
|
|
2868
|
+
}]);
|
|
2869
|
+
if (!confirmed) {
|
|
2870
|
+
console.log(chalk.gray(" Aborted."));
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
try {
|
|
2875
|
+
await client.data.collection("projects").delete(projectId);
|
|
2876
|
+
success(`Deleted project ${chalk.bold(p.name ?? projectId)}`);
|
|
2877
|
+
} catch (e) {
|
|
2878
|
+
reportError(e, "Failed to delete project");
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
async function firstRow(client, collection, projectId) {
|
|
2882
|
+
return (await client.data.collection(collection).find({
|
|
2883
|
+
where: { project: ["==", projectId] },
|
|
2884
|
+
limit: 1
|
|
2885
|
+
})).data[0];
|
|
2886
|
+
}
|
|
2887
|
+
async function latestDeployment(client, projectId) {
|
|
2888
|
+
return (await client.data.collection("deployments").find({
|
|
2889
|
+
where: { project: ["==", projectId] },
|
|
2890
|
+
orderBy: ["createdAt", "desc"],
|
|
2891
|
+
limit: 1
|
|
2892
|
+
})).data[0];
|
|
2893
|
+
}
|
|
2894
|
+
function fmtDate(value) {
|
|
2895
|
+
if (!value) return "—";
|
|
2896
|
+
const d = new Date(value);
|
|
2897
|
+
return isNaN(d.getTime()) ? value : d.toLocaleString();
|
|
2898
|
+
}
|
|
2899
|
+
//#endregion
|
|
2900
|
+
//#region src/commands/cloud/deploy.ts
|
|
2901
|
+
/**
|
|
2902
|
+
* `rebase cloud deploy` and `rebase cloud logs`.
|
|
2903
|
+
*
|
|
2904
|
+
* `deploy` triggers the control-plane `deploy` function, then tails the build
|
|
2905
|
+
* logs from the deployment record until it succeeds or fails. `logs` shows the
|
|
2906
|
+
* latest build log, or runtime logs with `--runtime`.
|
|
2907
|
+
*/
|
|
2908
|
+
var POLL_INTERVAL_MS = 1500;
|
|
2909
|
+
var POLL_TIMEOUT_MS = 900 * 1e3;
|
|
2910
|
+
function sleep(ms) {
|
|
2911
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
2912
|
+
}
|
|
2913
|
+
function run(cmd, cmdArgs, cwd) {
|
|
2914
|
+
return new Promise((resolve, reject) => {
|
|
2915
|
+
const child = spawn(cmd, cmdArgs, {
|
|
2916
|
+
cwd,
|
|
2917
|
+
stdio: [
|
|
2918
|
+
"ignore",
|
|
2919
|
+
"ignore",
|
|
2920
|
+
"pipe"
|
|
2921
|
+
]
|
|
2922
|
+
});
|
|
2923
|
+
let stderr = "";
|
|
2924
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
2925
|
+
child.on("error", reject);
|
|
2926
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr || `${cmd} exited ${code}`)));
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* Package `sourceDir` into a gzipped tarball, honoring `.gitignore`/`.rebaseignore`
|
|
2931
|
+
* and always excluding `.git` and `node_modules`. Returns the temp archive path.
|
|
2932
|
+
*/
|
|
2933
|
+
async function createSourceTarball(sourceDir) {
|
|
2934
|
+
const dir = path.resolve(sourceDir);
|
|
2935
|
+
if (!fs.existsSync(dir)) fail(`Source directory not found: ${dir}`);
|
|
2936
|
+
const tarPath = path.join(os.tmpdir(), `rebase-src-${Date.now()}.tar.gz`);
|
|
2937
|
+
const tarArgs = [
|
|
2938
|
+
"-czf",
|
|
2939
|
+
tarPath,
|
|
2940
|
+
"--exclude=.git",
|
|
2941
|
+
"--exclude=node_modules"
|
|
2942
|
+
];
|
|
2943
|
+
for (const ignore of [".gitignore", ".rebaseignore"]) if (fs.existsSync(path.join(dir, ignore))) tarArgs.push(`--exclude-from=${ignore}`);
|
|
2944
|
+
tarArgs.push(".");
|
|
2945
|
+
try {
|
|
2946
|
+
await run("tar", tarArgs, dir);
|
|
2947
|
+
} catch (e) {
|
|
2948
|
+
fail(`Failed to package source: ${e instanceof Error ? e.message : String(e)}`);
|
|
2949
|
+
}
|
|
2950
|
+
return tarPath;
|
|
2951
|
+
}
|
|
2952
|
+
/** Upload a build-context tarball; returns the opaque `source` ref for deploy. */
|
|
2953
|
+
async function uploadSource(url, token, projectId, tarPath) {
|
|
2954
|
+
const bytes = fs.readFileSync(tarPath);
|
|
2955
|
+
const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
|
|
2956
|
+
console.log(chalk.gray(` Uploading source (${sizeMb} MB)...`));
|
|
2957
|
+
const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
|
|
2958
|
+
method: "POST",
|
|
2959
|
+
headers: {
|
|
2960
|
+
Authorization: `Bearer ${token}`,
|
|
2961
|
+
"Content-Type": "application/gzip"
|
|
2962
|
+
},
|
|
2963
|
+
body: bytes
|
|
2964
|
+
});
|
|
2965
|
+
if (!res.ok) {
|
|
2966
|
+
const body = await res.text().catch(() => "");
|
|
2967
|
+
fail(`Source upload failed (${res.status}): ${body || res.statusText}`);
|
|
2968
|
+
}
|
|
2969
|
+
const data = await res.json();
|
|
2970
|
+
if (!data.source) fail("Upload endpoint did not return a source reference.");
|
|
2971
|
+
return data.source;
|
|
2972
|
+
}
|
|
2973
|
+
async function deployCommand(rawArgs, projectId) {
|
|
2974
|
+
const args = arg({
|
|
2975
|
+
"--no-follow": Boolean,
|
|
2976
|
+
"--source": String
|
|
2977
|
+
}, {
|
|
2978
|
+
argv: rawArgs.slice(2),
|
|
2979
|
+
permissive: true
|
|
2980
|
+
});
|
|
2981
|
+
const { client, url } = await requireClient(rawArgs);
|
|
2982
|
+
let source;
|
|
2983
|
+
if (args["--source"]) {
|
|
2984
|
+
const tarPath = await createSourceTarball(args["--source"]);
|
|
2985
|
+
try {
|
|
2986
|
+
const token = client.auth.getSession()?.accessToken;
|
|
2987
|
+
if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
|
|
2988
|
+
source = await uploadSource(url, token, projectId, tarPath);
|
|
2989
|
+
} finally {
|
|
2990
|
+
fs.rmSync(tarPath, { force: true });
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
console.log("");
|
|
2994
|
+
console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectId)}${source ? " from uploaded source" : ""}...`);
|
|
2995
|
+
let deploymentId;
|
|
2996
|
+
try {
|
|
2997
|
+
const res = await client.functions.invoke("deploy", source ? {
|
|
2998
|
+
projectId,
|
|
2999
|
+
source
|
|
3000
|
+
} : { projectId });
|
|
3001
|
+
if (!res?.deployment?.id) fail("Control plane did not return a deployment id.");
|
|
3002
|
+
deploymentId = String(res.deployment.id);
|
|
3003
|
+
} catch (e) {
|
|
3004
|
+
const err = e;
|
|
3005
|
+
if (err?.status === 409) fail("A deployment is already in progress for this project.");
|
|
3006
|
+
if (err?.status === 402) fail(err.message || "Payment required before deploying.", "Attach a card once with `rebase cloud billing setup`, then deploy again.");
|
|
3007
|
+
reportError(e, "Failed to trigger deployment");
|
|
3008
|
+
}
|
|
3009
|
+
console.log(chalk.gray(` Deployment ${deploymentId} created.`));
|
|
3010
|
+
if (args["--no-follow"]) {
|
|
3011
|
+
console.log(chalk.gray(" Not following logs (--no-follow). Check status with `rebase cloud logs`."));
|
|
3012
|
+
console.log("");
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
|
|
3016
|
+
console.log("");
|
|
3017
|
+
await streamBuildLogs(client, deploymentId);
|
|
3018
|
+
}
|
|
3019
|
+
/** Poll a deployment record and print new log output as it arrives. */
|
|
3020
|
+
async function streamBuildLogs(client, deploymentId) {
|
|
3021
|
+
let printed = 0;
|
|
3022
|
+
const started = Date.now();
|
|
3023
|
+
for (;;) {
|
|
3024
|
+
let dep;
|
|
3025
|
+
try {
|
|
3026
|
+
dep = await client.data.collection("deployments").findById(deploymentId);
|
|
3027
|
+
} catch (e) {
|
|
3028
|
+
reportError(e, "Failed to read deployment status");
|
|
3029
|
+
}
|
|
3030
|
+
if (!dep) fail(`Deployment ${deploymentId} disappeared.`);
|
|
3031
|
+
const logs = dep.logs ?? "";
|
|
3032
|
+
if (logs.length > printed) {
|
|
3033
|
+
process.stdout.write(logs.slice(printed));
|
|
3034
|
+
printed = logs.length;
|
|
3035
|
+
}
|
|
3036
|
+
if (dep.status && dep.status !== "deploying") {
|
|
3037
|
+
console.log("");
|
|
3038
|
+
if (dep.status === "success") console.log(chalk.bold.green(" ✓ Deployment succeeded"));
|
|
3039
|
+
else {
|
|
3040
|
+
console.log(chalk.bold.red(` ✗ Deployment ${dep.status}`));
|
|
3041
|
+
console.log("");
|
|
3042
|
+
process.exit(1);
|
|
3043
|
+
}
|
|
3044
|
+
console.log("");
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
if (Date.now() - started > POLL_TIMEOUT_MS) {
|
|
3048
|
+
console.log("");
|
|
3049
|
+
fail("Timed out waiting for the build to finish.", "The deployment may still be running — check `rebase cloud logs`.");
|
|
3050
|
+
}
|
|
3051
|
+
await sleep(POLL_INTERVAL_MS);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
async function logsCommand(rawArgs, projectId) {
|
|
3055
|
+
const args = arg({
|
|
3056
|
+
"--runtime": Boolean,
|
|
3057
|
+
"--follow": Boolean,
|
|
3058
|
+
"-f": "--follow"
|
|
3059
|
+
}, {
|
|
3060
|
+
argv: rawArgs.slice(2),
|
|
3061
|
+
permissive: true
|
|
3062
|
+
});
|
|
3063
|
+
const { client } = await requireClient(rawArgs);
|
|
3064
|
+
if (args["--runtime"]) {
|
|
3065
|
+
try {
|
|
3066
|
+
const res = await client.functions.invoke("runtime-logs", void 0, {
|
|
3067
|
+
method: "GET",
|
|
3068
|
+
path: projectId
|
|
3069
|
+
});
|
|
3070
|
+
console.log("");
|
|
3071
|
+
console.log(chalk.bold(` 📄 Runtime logs — project ${projectId}`));
|
|
3072
|
+
console.log("");
|
|
3073
|
+
console.log(res.logs ?? chalk.gray(" (no logs)"));
|
|
3074
|
+
console.log("");
|
|
3075
|
+
} catch (e) {
|
|
3076
|
+
reportError(e, "Failed to fetch runtime logs");
|
|
3077
|
+
}
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
try {
|
|
3081
|
+
const dep = await latestDeployment(client, projectId);
|
|
3082
|
+
if (!dep) {
|
|
3083
|
+
console.log("");
|
|
3084
|
+
console.log(chalk.gray(" No deployments yet for this project."));
|
|
3085
|
+
console.log("");
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
console.log("");
|
|
3089
|
+
console.log(chalk.bold(` 📄 Build logs — deployment ${dep.id}`) + ` ${colorStatus(dep.status)}`);
|
|
3090
|
+
console.log("");
|
|
3091
|
+
if (args["--follow"] && dep.status === "deploying") await streamBuildLogs(client, String(dep.id));
|
|
3092
|
+
else {
|
|
3093
|
+
console.log(dep.logs ?? chalk.gray(" (no logs)"));
|
|
3094
|
+
console.log("");
|
|
3095
|
+
}
|
|
3096
|
+
} catch (e) {
|
|
3097
|
+
reportError(e, "Failed to fetch build logs");
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
//#endregion
|
|
3101
|
+
//#region src/commands/cloud/orgs.ts
|
|
3102
|
+
/**
|
|
3103
|
+
* `rebase cloud orgs` — list / create / members.
|
|
3104
|
+
*/
|
|
3105
|
+
async function orgsCommand(subcommand, rawArgs) {
|
|
3106
|
+
switch (subcommand) {
|
|
3107
|
+
case "list":
|
|
3108
|
+
case void 0:
|
|
3109
|
+
await listOrgs(rawArgs);
|
|
3110
|
+
break;
|
|
3111
|
+
case "create":
|
|
3112
|
+
await createOrg(rawArgs);
|
|
3113
|
+
break;
|
|
3114
|
+
case "members":
|
|
3115
|
+
await listMembers(rawArgs);
|
|
3116
|
+
break;
|
|
3117
|
+
case "--help":
|
|
3118
|
+
printOrgsHelp();
|
|
3119
|
+
break;
|
|
3120
|
+
default: fail(`Unknown orgs command: ${subcommand}`);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
async function listOrgs(rawArgs) {
|
|
3124
|
+
const { client, url } = await requireClient(rawArgs);
|
|
3125
|
+
try {
|
|
3126
|
+
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
3127
|
+
const active = getContextOrg(url);
|
|
3128
|
+
console.log("");
|
|
3129
|
+
console.log(chalk.bold(" 🏢 Organizations"));
|
|
3130
|
+
console.log("");
|
|
3131
|
+
if (orgs.length === 0) {
|
|
3132
|
+
console.log(chalk.gray(" You are not a member of any organization."));
|
|
3133
|
+
console.log("");
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
for (const o of orgs) {
|
|
3137
|
+
const marker = String(o.id) === active ? chalk.green(" ●") : " ";
|
|
3138
|
+
console.log(`${marker}${chalk.bold(o.name ?? "(unnamed)")} ${chalk.gray(`[${o.id}]`)}${o.slug ? chalk.gray(` ${o.slug}`) : ""}`);
|
|
3139
|
+
}
|
|
3140
|
+
console.log("");
|
|
3141
|
+
console.log(chalk.gray(" ● = active organization. Switch with `rebase cloud use <id>`."));
|
|
3142
|
+
console.log("");
|
|
3143
|
+
} catch (e) {
|
|
3144
|
+
reportError(e, "Failed to list organizations");
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
async function createOrg(rawArgs) {
|
|
3148
|
+
const args = arg({
|
|
3149
|
+
"--name": String,
|
|
3150
|
+
"--slug": String,
|
|
3151
|
+
"-n": "--name"
|
|
3152
|
+
}, {
|
|
3153
|
+
argv: rawArgs.slice(4),
|
|
3154
|
+
permissive: true
|
|
3155
|
+
});
|
|
3156
|
+
const { client, url } = await requireClient(rawArgs);
|
|
3157
|
+
const prompts = [];
|
|
3158
|
+
if (!args["--name"]) prompts.push({
|
|
3159
|
+
type: "input",
|
|
3160
|
+
name: "name",
|
|
3161
|
+
message: "Organization name:"
|
|
3162
|
+
});
|
|
3163
|
+
const answers = prompts.length ? await inquirer.prompt(prompts) : {};
|
|
3164
|
+
const name = (args["--name"] || answers.name || "").trim();
|
|
3165
|
+
if (!name) fail("Organization name is required.");
|
|
3166
|
+
const slug = (args["--slug"] || slugify(name)).trim();
|
|
3167
|
+
try {
|
|
3168
|
+
const created = await client.data.collection("organizations").create({
|
|
3169
|
+
name,
|
|
3170
|
+
slug,
|
|
3171
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3172
|
+
});
|
|
3173
|
+
setContextOrg(url, String(created.id));
|
|
3174
|
+
success(`Created organization ${chalk.bold(name)} and set it active`);
|
|
3175
|
+
} catch (e) {
|
|
3176
|
+
reportError(e, "Failed to create organization");
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
async function listMembers(rawArgs) {
|
|
3180
|
+
const { client, url } = await requireClient(rawArgs);
|
|
3181
|
+
const org = getContextOrg(url);
|
|
3182
|
+
if (!org) fail("No active organization.", "Run `rebase cloud use` first.");
|
|
3183
|
+
try {
|
|
3184
|
+
const members = (await client.data.collection("organization-members").find({
|
|
3185
|
+
where: { organization: ["==", org] },
|
|
3186
|
+
limit: 200
|
|
3187
|
+
})).data;
|
|
3188
|
+
console.log("");
|
|
3189
|
+
console.log(chalk.bold(` 👥 Members — org ${org}`));
|
|
3190
|
+
console.log("");
|
|
3191
|
+
if (members.length === 0) {
|
|
3192
|
+
console.log(chalk.gray(" No members found."));
|
|
3193
|
+
console.log("");
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
for (const m of members) console.log(` ${chalk.bold(m.userId ?? "?")} ${colorStatus(m.role)}`);
|
|
3197
|
+
console.log("");
|
|
3198
|
+
} catch (e) {
|
|
3199
|
+
reportError(e, "Failed to list members");
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
function slugify(s) {
|
|
3203
|
+
return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3204
|
+
}
|
|
3205
|
+
function printOrgsHelp() {
|
|
3206
|
+
console.log(`
|
|
3207
|
+
${chalk.bold("rebase cloud orgs")} — Manage organizations
|
|
3208
|
+
|
|
3209
|
+
${chalk.green.bold("Commands")}
|
|
3210
|
+
${chalk.blue.bold("list")} List organizations you belong to
|
|
3211
|
+
${chalk.blue.bold("create")} Create a new organization ${chalk.gray("(--name, --slug)")}
|
|
3212
|
+
${chalk.blue.bold("members")} List members of the active organization
|
|
3213
|
+
`);
|
|
3214
|
+
}
|
|
3215
|
+
//#endregion
|
|
3216
|
+
//#region src/commands/cloud/databases.ts
|
|
3217
|
+
/**
|
|
3218
|
+
* `rebase cloud db` — database + backup management for a project.
|
|
3219
|
+
*
|
|
3220
|
+
* db list List databases attached to the project
|
|
3221
|
+
* db create Attach a managed or bring-your-own database
|
|
3222
|
+
* db test Test connectivity to the project's database
|
|
3223
|
+
* db backup list|create|restore
|
|
3224
|
+
*/
|
|
3225
|
+
async function dbCommand$1(subcommand, rawArgs) {
|
|
3226
|
+
switch (subcommand) {
|
|
3227
|
+
case "list":
|
|
3228
|
+
case void 0:
|
|
3229
|
+
await listDatabases(rawArgs);
|
|
3230
|
+
break;
|
|
3231
|
+
case "create":
|
|
3232
|
+
await createDatabase(rawArgs);
|
|
3233
|
+
break;
|
|
3234
|
+
case "test":
|
|
3235
|
+
await testDatabase(rawArgs);
|
|
3236
|
+
break;
|
|
3237
|
+
case "backup":
|
|
3238
|
+
await backupCommand(rawArgs);
|
|
3239
|
+
break;
|
|
3240
|
+
case "--help":
|
|
3241
|
+
printDbHelp();
|
|
3242
|
+
break;
|
|
3243
|
+
default: fail(`Unknown db command: ${subcommand}`);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
async function listDatabases(rawArgs) {
|
|
3247
|
+
const projectId = requireProjectId(rawArgs);
|
|
3248
|
+
const { client } = await requireClient(rawArgs);
|
|
3249
|
+
try {
|
|
3250
|
+
const dbs = (await client.data.collection("databases").find({
|
|
3251
|
+
where: { project: ["==", projectId] },
|
|
3252
|
+
limit: 50
|
|
3253
|
+
})).data;
|
|
3254
|
+
console.log("");
|
|
3255
|
+
console.log(chalk.bold(` 🗄 Databases — project ${projectId}`));
|
|
3256
|
+
console.log("");
|
|
3257
|
+
if (dbs.length === 0) {
|
|
3258
|
+
console.log(chalk.gray(" No database attached. Add one with `rebase cloud db create`."));
|
|
3259
|
+
console.log("");
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
for (const d of dbs) {
|
|
3263
|
+
console.log(` ${chalk.bold(d.type ?? "unknown")} ${chalk.gray(`[${d.id}]`)} ${colorStatus(d.connectionStatus)}`);
|
|
3264
|
+
keyValues([["SSH tunnel", d.useSshTunnel ? "yes" : void 0], ["PITR", d.pitrEnabled ? "enabled" : void 0]]);
|
|
3265
|
+
}
|
|
3266
|
+
console.log("");
|
|
3267
|
+
} catch (e) {
|
|
3268
|
+
reportError(e, "Failed to list databases");
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
async function createDatabase(rawArgs) {
|
|
3272
|
+
const args = arg({
|
|
3273
|
+
"--type": String,
|
|
3274
|
+
"--connection-string": String,
|
|
3275
|
+
"--project": String,
|
|
3276
|
+
"-p": "--project"
|
|
3277
|
+
}, {
|
|
3278
|
+
argv: rawArgs.slice(4),
|
|
3279
|
+
permissive: true
|
|
3280
|
+
});
|
|
3281
|
+
const projectId = requireProjectId(rawArgs);
|
|
3282
|
+
const { client } = await requireClient(rawArgs);
|
|
3283
|
+
let type = args["--type"];
|
|
3284
|
+
if (!type) {
|
|
3285
|
+
const { picked } = await inquirer.prompt([{
|
|
3286
|
+
type: "list",
|
|
3287
|
+
name: "picked",
|
|
3288
|
+
message: "Database type:",
|
|
3289
|
+
choices: [{
|
|
3290
|
+
name: "SaaS Managed (provisioned for you)",
|
|
3291
|
+
value: "managed"
|
|
3292
|
+
}, {
|
|
3293
|
+
name: "Bring Your Own DB (external PostgreSQL)",
|
|
3294
|
+
value: "byodb"
|
|
3295
|
+
}]
|
|
3296
|
+
}]);
|
|
3297
|
+
type = picked;
|
|
3298
|
+
}
|
|
3299
|
+
let connectionString = args["--connection-string"];
|
|
3300
|
+
if (type === "byodb" && !connectionString) {
|
|
3301
|
+
const { cs } = await inquirer.prompt([{
|
|
3302
|
+
type: "input",
|
|
3303
|
+
name: "cs",
|
|
3304
|
+
message: "PostgreSQL connection string:"
|
|
3305
|
+
}]);
|
|
3306
|
+
connectionString = cs?.trim();
|
|
3307
|
+
if (!connectionString) fail("A connection string is required for bring-your-own databases.");
|
|
3308
|
+
}
|
|
3309
|
+
try {
|
|
3310
|
+
const created = await client.data.collection("databases").create({
|
|
3311
|
+
project: projectId,
|
|
3312
|
+
type,
|
|
3313
|
+
connectionString: type === "byodb" ? connectionString : void 0,
|
|
3314
|
+
connectionStatus: "untested"
|
|
3315
|
+
});
|
|
3316
|
+
success(`Attached ${type} database to project ${projectId}`);
|
|
3317
|
+
keyValues([["ID", String(created.id)]]);
|
|
3318
|
+
if (type === "byodb") {
|
|
3319
|
+
console.log(chalk.gray(" Verify it with `rebase cloud db test`."));
|
|
3320
|
+
console.log("");
|
|
3321
|
+
}
|
|
3322
|
+
} catch (e) {
|
|
3323
|
+
reportError(e, "Failed to attach database");
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
async function testDatabase(rawArgs) {
|
|
3327
|
+
const projectId = requireProjectId(rawArgs);
|
|
3328
|
+
const { client } = await requireClient(rawArgs);
|
|
3329
|
+
console.log("");
|
|
3330
|
+
console.log(` Testing database connectivity for project ${chalk.bold(projectId)}...`);
|
|
3331
|
+
try {
|
|
3332
|
+
const res = await client.functions.invoke("db-test", { projectId });
|
|
3333
|
+
console.log("");
|
|
3334
|
+
if (res.logs) console.log(res.logs);
|
|
3335
|
+
if (res.success) success("Database connection succeeded");
|
|
3336
|
+
else fail("Database connection failed. See logs above.");
|
|
3337
|
+
} catch (e) {
|
|
3338
|
+
reportError(e, "Failed to test database");
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
async function backupCommand(rawArgs) {
|
|
3342
|
+
const action = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[2] || "list";
|
|
3343
|
+
const projectId = requireProjectId(rawArgs);
|
|
3344
|
+
const { client } = await requireClient(rawArgs);
|
|
3345
|
+
try {
|
|
3346
|
+
if (action === "create") {
|
|
3347
|
+
console.log("");
|
|
3348
|
+
console.log(` Creating backup for project ${chalk.bold(projectId)}...`);
|
|
3349
|
+
const res = await client.functions.invoke("backup", {
|
|
3350
|
+
projectId,
|
|
3351
|
+
type: "manual"
|
|
3352
|
+
}, { path: "create" });
|
|
3353
|
+
if (res.success) success(`Backup created: ${res.backup?.filename ?? "(unknown)"}`);
|
|
3354
|
+
else fail(res.error || "Backup failed.");
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
if (action === "restore") {
|
|
3358
|
+
const filename = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[3];
|
|
3359
|
+
if (!filename) fail("Usage: rebase cloud db backup restore <filename>");
|
|
3360
|
+
const { confirmed } = await inquirer.prompt([{
|
|
3361
|
+
type: "confirm",
|
|
3362
|
+
name: "confirmed",
|
|
3363
|
+
default: false,
|
|
3364
|
+
message: `Restore "${filename}" over the current database for project ${projectId}?`
|
|
3365
|
+
}]);
|
|
3366
|
+
if (!confirmed) {
|
|
3367
|
+
console.log(chalk.gray(" Aborted."));
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
const res = await client.functions.invoke("backup", {
|
|
3371
|
+
projectId,
|
|
3372
|
+
filename
|
|
3373
|
+
}, { path: "restore" });
|
|
3374
|
+
if (res.success) success(res.message || "Restore complete");
|
|
3375
|
+
else fail(res.error || "Restore failed.");
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
const res = await client.functions.invoke("backup", void 0, {
|
|
3379
|
+
method: "GET",
|
|
3380
|
+
path: `list/${projectId}`
|
|
3381
|
+
});
|
|
3382
|
+
console.log("");
|
|
3383
|
+
console.log(chalk.bold(` 💾 Backups — project ${projectId}`));
|
|
3384
|
+
console.log("");
|
|
3385
|
+
if (!res.backups?.length) {
|
|
3386
|
+
console.log(chalk.gray(" No backups yet. Create one with `rebase cloud db backup create`."));
|
|
3387
|
+
console.log("");
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
for (const b of res.backups) {
|
|
3391
|
+
const size = b.size !== void 0 ? `${(b.size / 1024 / 1024).toFixed(1)} MB` : "";
|
|
3392
|
+
console.log(` ${chalk.bold(b.filename)} ${chalk.gray(`${b.type ?? ""} ${size}`.trim())}`);
|
|
3393
|
+
}
|
|
3394
|
+
console.log("");
|
|
3395
|
+
} catch (e) {
|
|
3396
|
+
reportError(e, "Backup operation failed");
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
function printDbHelp() {
|
|
3400
|
+
console.log(`
|
|
3401
|
+
${chalk.bold("rebase cloud db")} — Database & backups
|
|
3402
|
+
|
|
3403
|
+
${chalk.green.bold("Commands")}
|
|
3404
|
+
${chalk.blue.bold("list")} List databases attached to the project
|
|
3405
|
+
${chalk.blue.bold("create")} Attach a managed or bring-your-own database
|
|
3406
|
+
${chalk.blue.bold("test")} Test database connectivity
|
|
3407
|
+
${chalk.blue.bold("backup list")} List backups
|
|
3408
|
+
${chalk.blue.bold("backup create")} Create a manual backup
|
|
3409
|
+
${chalk.blue.bold("backup restore")} ${chalk.gray("<file>")} Restore a backup
|
|
3410
|
+
|
|
3411
|
+
${chalk.green.bold("Options")}
|
|
3412
|
+
${chalk.blue("--project, -p")} Target project id ${chalk.gray("(defaults to the linked project)")}
|
|
3413
|
+
${chalk.blue("--type")} managed | byodb ${chalk.gray("(create)")}
|
|
3414
|
+
${chalk.blue("--connection-string")} External DB URL ${chalk.gray("(byodb)")}
|
|
3415
|
+
`);
|
|
3416
|
+
}
|
|
3417
|
+
//#endregion
|
|
3418
|
+
//#region src/commands/cloud/resources.ts
|
|
3419
|
+
/**
|
|
3420
|
+
* `rebase cloud` resource subcommands: status, metrics, webhooks, storage,
|
|
3421
|
+
* clusters, billing.
|
|
3422
|
+
*/
|
|
3423
|
+
async function statusCommand(rawArgs) {
|
|
3424
|
+
const projectId = requireProjectId(rawArgs);
|
|
3425
|
+
const { client } = await requireClient(rawArgs);
|
|
3426
|
+
try {
|
|
3427
|
+
const project = await client.data.collection("projects").findById(projectId);
|
|
3428
|
+
if (!project) fail(`Project ${projectId} not found.`);
|
|
3429
|
+
const [db, storage, deploy] = await Promise.all([
|
|
3430
|
+
firstRow(client, "databases", projectId),
|
|
3431
|
+
firstRow(client, "storages", projectId),
|
|
3432
|
+
latestDeployment(client, projectId)
|
|
3433
|
+
]);
|
|
3434
|
+
console.log("");
|
|
3435
|
+
console.log(` ${chalk.bold(project.name ?? projectId)} ${chalk.gray(`[${projectId}]`)} ${colorStatus(project.status)}`);
|
|
3436
|
+
console.log("");
|
|
3437
|
+
keyValues([
|
|
3438
|
+
["URL", project.subdomain ? `${project.subdomain}.rebase.pro` : void 0],
|
|
3439
|
+
["Branch", project.gitBranch],
|
|
3440
|
+
["Last deploy", deploy ? `${colorStatus(deploy.status)} · ${fmtDate(deploy.createdAt)}` : "never"],
|
|
3441
|
+
["Database", db ? `${db.type} (${colorStatus(db.connectionStatus)})` : "none"],
|
|
3442
|
+
["Storage", storage ? `${storage.type} (${colorStatus(storage.status)})` : "none"]
|
|
3443
|
+
]);
|
|
3444
|
+
console.log("");
|
|
3445
|
+
} catch (e) {
|
|
3446
|
+
reportError(e, "Failed to load status");
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
async function metricsCommand(rawArgs) {
|
|
3450
|
+
const projectId = requireProjectId(rawArgs);
|
|
3451
|
+
const { client } = await requireClient(rawArgs);
|
|
3452
|
+
try {
|
|
3453
|
+
const m = await client.functions.invoke("metrics", void 0, {
|
|
3454
|
+
method: "GET",
|
|
3455
|
+
path: projectId
|
|
3456
|
+
});
|
|
3457
|
+
console.log("");
|
|
3458
|
+
console.log(chalk.bold(` 📊 Metrics — project ${projectId}`));
|
|
3459
|
+
console.log("");
|
|
3460
|
+
keyValues([
|
|
3461
|
+
["Status", m.status ? colorStatus(m.status === "running" ? "active" : m.status) : void 0],
|
|
3462
|
+
["CPU", m.cpu],
|
|
3463
|
+
["Memory", m.memory ? `${m.memory}${m.memoryPercent ? ` (${m.memoryPercent})` : ""}` : void 0],
|
|
3464
|
+
["Disk", m.disk]
|
|
3465
|
+
]);
|
|
3466
|
+
console.log("");
|
|
3467
|
+
} catch (e) {
|
|
3468
|
+
reportError(e, "Failed to fetch metrics");
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
async function webhooksCommand(subcommand, rawArgs) {
|
|
3472
|
+
const projectId = requireProjectId(rawArgs);
|
|
3473
|
+
const { client } = await requireClient(rawArgs);
|
|
3474
|
+
try {
|
|
3475
|
+
if (subcommand === "create") {
|
|
3476
|
+
const args = arg({
|
|
3477
|
+
"--name": String,
|
|
3478
|
+
"--table": String,
|
|
3479
|
+
"--url": String,
|
|
3480
|
+
"--events": String
|
|
3481
|
+
}, {
|
|
3482
|
+
argv: rawArgs.slice(4),
|
|
3483
|
+
permissive: true
|
|
3484
|
+
});
|
|
3485
|
+
const name = args["--name"] || fail("--name is required.");
|
|
3486
|
+
const table = args["--table"] || fail("--table is required.");
|
|
3487
|
+
const url = args["--url"] || fail("--url (endpoint) is required.");
|
|
3488
|
+
const events = (args["--events"] || "insert,update,delete").split(",").map((s) => s.trim());
|
|
3489
|
+
const created = await client.data.collection("webhooks").create({
|
|
3490
|
+
project: projectId,
|
|
3491
|
+
name,
|
|
3492
|
+
table,
|
|
3493
|
+
url,
|
|
3494
|
+
events,
|
|
3495
|
+
enabled: true
|
|
3496
|
+
});
|
|
3497
|
+
success(`Created webhook ${chalk.bold(name)} [${created.id}]`);
|
|
3498
|
+
return;
|
|
3499
|
+
}
|
|
3500
|
+
if (subcommand === "delete") {
|
|
3501
|
+
const id = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[2];
|
|
3502
|
+
if (!id) fail("Usage: rebase cloud webhooks delete <id>");
|
|
3503
|
+
await client.data.collection("webhooks").delete(id);
|
|
3504
|
+
success(`Deleted webhook ${id}`);
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
const hooks = (await client.data.collection("webhooks").find({
|
|
3508
|
+
where: { project: ["==", projectId] },
|
|
3509
|
+
limit: 100
|
|
3510
|
+
})).data;
|
|
3511
|
+
console.log("");
|
|
3512
|
+
console.log(chalk.bold(` 🔗 Webhooks — project ${projectId}`));
|
|
3513
|
+
console.log("");
|
|
3514
|
+
if (hooks.length === 0) {
|
|
3515
|
+
console.log(chalk.gray(" No webhooks. Add one with `rebase cloud webhooks create`."));
|
|
3516
|
+
console.log("");
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
for (const h of hooks) {
|
|
3520
|
+
const state = h.enabled ? chalk.green("enabled") : chalk.gray("disabled");
|
|
3521
|
+
console.log(` ${chalk.bold(h.name ?? "(unnamed)")} ${chalk.gray(`[${h.id}]`)} ${state}`);
|
|
3522
|
+
console.log(` ${chalk.gray(`${h.table ?? "?"} → ${h.url ?? "?"} (${(h.events ?? []).join(", ")})`)}`);
|
|
3523
|
+
}
|
|
3524
|
+
console.log("");
|
|
3525
|
+
} catch (e) {
|
|
3526
|
+
reportError(e, "Webhook operation failed");
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
async function storageCommand(rawArgs) {
|
|
3530
|
+
const projectId = requireProjectId(rawArgs);
|
|
3531
|
+
const { client } = await requireClient(rawArgs);
|
|
3532
|
+
try {
|
|
3533
|
+
const stores = (await client.data.collection("storages").find({
|
|
3534
|
+
where: { project: ["==", projectId] },
|
|
3535
|
+
limit: 50
|
|
3536
|
+
})).data;
|
|
3537
|
+
console.log("");
|
|
3538
|
+
console.log(chalk.bold(` 🪣 Storage — project ${projectId}`));
|
|
3539
|
+
console.log("");
|
|
3540
|
+
if (stores.length === 0) {
|
|
3541
|
+
console.log(chalk.gray(" No storage buckets attached."));
|
|
3542
|
+
console.log("");
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
for (const s of stores) {
|
|
3546
|
+
console.log(` ${chalk.bold(s.bucketName ?? s.type ?? "bucket")} ${chalk.gray(`[${s.id}]`)} ${colorStatus(s.status)}`);
|
|
3547
|
+
keyValues([["Provider", s.provider], ["Type", s.type]]);
|
|
3548
|
+
}
|
|
3549
|
+
console.log("");
|
|
3550
|
+
} catch (e) {
|
|
3551
|
+
reportError(e, "Failed to list storage");
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
async function clustersCommand(rawArgs) {
|
|
3555
|
+
const { client } = await requireClient(rawArgs);
|
|
3556
|
+
try {
|
|
3557
|
+
const clusters = (await client.data.collection("clusters").find({ limit: 100 })).data;
|
|
3558
|
+
console.log("");
|
|
3559
|
+
console.log(chalk.bold(" ☸ Clusters"));
|
|
3560
|
+
console.log("");
|
|
3561
|
+
if (clusters.length === 0) {
|
|
3562
|
+
console.log(chalk.gray(" No clusters registered."));
|
|
3563
|
+
console.log("");
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
for (const c of clusters) {
|
|
3567
|
+
console.log(` ${chalk.bold(c.name ?? "(unnamed)")} ${chalk.gray(`[${c.id}]`)} ${colorStatus(c.status)}`);
|
|
3568
|
+
keyValues([["Provider", c.provider], ["Region", c.region]]);
|
|
3569
|
+
}
|
|
3570
|
+
console.log("");
|
|
3571
|
+
} catch (e) {
|
|
3572
|
+
reportError(e, "Failed to list clusters");
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
async function billingCommand(rawArgs) {
|
|
3576
|
+
const { client, url } = await requireClient(rawArgs);
|
|
3577
|
+
const org = getContextOrg(url);
|
|
3578
|
+
const action = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
|
|
3579
|
+
if (action === "setup") {
|
|
3580
|
+
if (!org) fail("No active organization.", "Run `rebase cloud use` first.");
|
|
3581
|
+
try {
|
|
3582
|
+
const res = await client.functions.invoke("stripe-billing", { organizationId: org }, { path: "setup-session" });
|
|
3583
|
+
if (!res.url) fail("Could not start billing setup.");
|
|
3584
|
+
openUrl(res.url, "Add a payment method in your browser:");
|
|
3585
|
+
if (res.simulated) {
|
|
3586
|
+
console.log(chalk.gray(" (dev mode — Stripe not configured; complete setup from the console)"));
|
|
3587
|
+
console.log("");
|
|
3588
|
+
} else {
|
|
3589
|
+
console.log(chalk.gray(" Once you've added a card, `rebase cloud deploy` runs without further prompts."));
|
|
3590
|
+
console.log("");
|
|
3591
|
+
}
|
|
3592
|
+
} catch (e) {
|
|
3593
|
+
reportError(e, "Failed to start billing setup");
|
|
3594
|
+
}
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
if (action === "checkout") {
|
|
3598
|
+
const projectId = requireProjectId(rawArgs);
|
|
3599
|
+
try {
|
|
3600
|
+
const res = await client.functions.invoke("stripe-billing", { projectId }, { path: "session" });
|
|
3601
|
+
if (!res.url) fail("Billing session could not be created.");
|
|
3602
|
+
console.log("");
|
|
3603
|
+
console.log(" Complete checkout in your browser:");
|
|
3604
|
+
console.log(` ${chalk.cyan(res.url)}`);
|
|
3605
|
+
console.log("");
|
|
3606
|
+
} catch (e) {
|
|
3607
|
+
reportError(e, "Failed to start checkout");
|
|
3608
|
+
}
|
|
3609
|
+
return;
|
|
3610
|
+
}
|
|
3611
|
+
if (!org) fail("No active organization.", "Run `rebase cloud use` first.");
|
|
3612
|
+
try {
|
|
3613
|
+
const orgRow = await client.data.collection("organizations").findById(org);
|
|
3614
|
+
const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
|
|
3615
|
+
if (!billingId) {
|
|
3616
|
+
console.log("");
|
|
3617
|
+
console.log(chalk.gray(` Organization ${org} has no billing account yet.`));
|
|
3618
|
+
console.log("");
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
const acct = await client.data.collection("billing-accounts").findById(billingId);
|
|
3622
|
+
let card = {};
|
|
3623
|
+
try {
|
|
3624
|
+
card = await client.functions.invoke("stripe-billing", void 0, {
|
|
3625
|
+
method: "GET",
|
|
3626
|
+
path: `payment-method/${org}`
|
|
3627
|
+
});
|
|
3628
|
+
} catch {}
|
|
3629
|
+
let plan;
|
|
3630
|
+
try {
|
|
3631
|
+
const projectId = arg({
|
|
3632
|
+
"--project": String,
|
|
3633
|
+
"-p": "--project"
|
|
3634
|
+
}, {
|
|
3635
|
+
argv: rawArgs.slice(2),
|
|
3636
|
+
permissive: true
|
|
3637
|
+
})["--project"] || readLink()?.projectId;
|
|
3638
|
+
if (projectId) {
|
|
3639
|
+
const proj = await client.data.collection("projects").findById(projectId);
|
|
3640
|
+
const hasCluster = proj?.cluster_id != null || proj?.cluster != null;
|
|
3641
|
+
plan = hasCluster ? "platform fee (own cluster)" : "managed compute";
|
|
3642
|
+
try {
|
|
3643
|
+
const pricing = await client.functions.invoke("pricing", void 0, { method: "GET" });
|
|
3644
|
+
const key = hasCluster ? "platform_byo" : `compute_${proj?.provider || "hetzner"}_${proj?.vmSize || "cx21"}`;
|
|
3645
|
+
const item = pricing.items?.find((i) => i.lookupKey === key);
|
|
3646
|
+
if (item) plan = `${plan} — €${item.amountEur.toFixed(2)}/mo`;
|
|
3647
|
+
} catch {}
|
|
3648
|
+
}
|
|
3649
|
+
} catch {}
|
|
3650
|
+
console.log("");
|
|
3651
|
+
console.log(chalk.bold(` 💳 Billing — org ${org}`));
|
|
3652
|
+
console.log("");
|
|
3653
|
+
keyValues([
|
|
3654
|
+
["Account", acct ? String(acct.id) : void 0],
|
|
3655
|
+
["Email", acct?.billingEmail],
|
|
3656
|
+
["Status", acct?.status ? colorStatus(acct.status) : void 0],
|
|
3657
|
+
["Plan", plan],
|
|
3658
|
+
["Payment method", card.hasPaymentMethod ? `${card.brand ?? "card"} •••• ${card.last4 ?? "????"}${card.expMonth ? ` (exp ${card.expMonth}/${card.expYear})` : ""}` : chalk.yellow("none — run `rebase cloud billing setup`")]
|
|
3659
|
+
]);
|
|
3660
|
+
console.log("");
|
|
3661
|
+
} catch (e) {
|
|
3662
|
+
reportError(e, "Failed to load billing");
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
//#endregion
|
|
3666
|
+
//#region src/commands/cloud/index.ts
|
|
3667
|
+
/**
|
|
3668
|
+
* CLI command: `rebase cloud <group> [action] [options]`
|
|
3669
|
+
*
|
|
3670
|
+
* A single entry point for everything you do against Rebase Cloud — the hosted
|
|
3671
|
+
* control plane. Auth, project link, deploys, databases, and the rest are all
|
|
3672
|
+
* dispatched from here. Individual groups live in sibling modules; this file
|
|
3673
|
+
* only routes and prints help.
|
|
3674
|
+
*/
|
|
3675
|
+
/** Positional tokens after `rebase cloud` (group, action, …). */
|
|
3676
|
+
function positionals(rawArgs) {
|
|
3677
|
+
return arg({}, {
|
|
3678
|
+
argv: rawArgs.slice(3),
|
|
3679
|
+
permissive: true
|
|
3680
|
+
})._;
|
|
3681
|
+
}
|
|
3682
|
+
async function cloudCommand(subcommand, rawArgs) {
|
|
3683
|
+
const pos = positionals(rawArgs);
|
|
3684
|
+
const group = subcommand && subcommand !== "--help" ? subcommand : pos[0];
|
|
3685
|
+
const action = pos[1];
|
|
3686
|
+
if (!group || subcommand === "--help") {
|
|
3687
|
+
printCloudHelp();
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
switch (group) {
|
|
3691
|
+
case "login":
|
|
3692
|
+
await loginCommand(rawArgs);
|
|
3693
|
+
break;
|
|
3694
|
+
case "logout":
|
|
3695
|
+
await logoutCommand(rawArgs);
|
|
3696
|
+
break;
|
|
3697
|
+
case "whoami":
|
|
3698
|
+
await whoamiCommand(rawArgs);
|
|
3699
|
+
break;
|
|
3700
|
+
case "link":
|
|
3701
|
+
await linkCommand(rawArgs);
|
|
3702
|
+
break;
|
|
3703
|
+
case "unlink":
|
|
3704
|
+
unlinkCommand();
|
|
3705
|
+
break;
|
|
3706
|
+
case "use":
|
|
3707
|
+
await selectOrgCommand(rawArgs);
|
|
3708
|
+
break;
|
|
3709
|
+
case "open":
|
|
3710
|
+
openCommand(rawArgs);
|
|
3711
|
+
break;
|
|
3712
|
+
case "projects":
|
|
3713
|
+
case "project":
|
|
3714
|
+
await projectsGroup(action, rawArgs);
|
|
3715
|
+
break;
|
|
3716
|
+
case "deploy":
|
|
3717
|
+
await deployCommand(rawArgs, requireProjectId(rawArgs));
|
|
3718
|
+
break;
|
|
3719
|
+
case "logs":
|
|
3720
|
+
await logsCommand(rawArgs, requireProjectId(rawArgs));
|
|
3721
|
+
break;
|
|
3722
|
+
case "status":
|
|
3723
|
+
await statusCommand(rawArgs);
|
|
3724
|
+
break;
|
|
3725
|
+
case "metrics":
|
|
3726
|
+
await metricsCommand(rawArgs);
|
|
3727
|
+
break;
|
|
3728
|
+
case "orgs":
|
|
3729
|
+
case "org":
|
|
3730
|
+
await orgsCommand(action, rawArgs);
|
|
3731
|
+
break;
|
|
3732
|
+
case "db":
|
|
3733
|
+
case "database":
|
|
3734
|
+
await dbCommand$1(action, rawArgs);
|
|
3735
|
+
break;
|
|
3736
|
+
case "webhooks":
|
|
3737
|
+
await webhooksCommand(action, rawArgs);
|
|
3738
|
+
break;
|
|
3739
|
+
case "storage":
|
|
3740
|
+
await storageCommand(rawArgs);
|
|
3741
|
+
break;
|
|
3742
|
+
case "clusters":
|
|
3743
|
+
await clustersCommand(rawArgs);
|
|
3744
|
+
break;
|
|
3745
|
+
case "billing":
|
|
3746
|
+
await billingCommand(rawArgs);
|
|
3747
|
+
break;
|
|
3748
|
+
default:
|
|
3749
|
+
console.error(chalk.red(`Unknown cloud command: ${group}`));
|
|
3750
|
+
console.log("");
|
|
3751
|
+
printCloudHelp();
|
|
3752
|
+
process.exit(1);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
async function projectsGroup(action, rawArgs) {
|
|
3756
|
+
switch (action) {
|
|
3757
|
+
case "list":
|
|
3758
|
+
case void 0:
|
|
3759
|
+
await listProjects(rawArgs);
|
|
3760
|
+
break;
|
|
3761
|
+
case "create":
|
|
3762
|
+
await createProject(rawArgs);
|
|
3763
|
+
break;
|
|
3764
|
+
case "info":
|
|
3765
|
+
await projectInfo(rawArgs, positionals(rawArgs)[2] || requireProjectId(rawArgs));
|
|
3766
|
+
break;
|
|
3767
|
+
case "delete":
|
|
3768
|
+
await deleteProject(rawArgs, positionals(rawArgs)[2] || requireProjectId(rawArgs));
|
|
3769
|
+
break;
|
|
3770
|
+
case "--help":
|
|
3771
|
+
printCloudHelp();
|
|
3772
|
+
break;
|
|
3773
|
+
default:
|
|
3774
|
+
console.error(chalk.red(`Unknown projects command: ${action}`));
|
|
3775
|
+
process.exit(1);
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
function printCloudHelp() {
|
|
3779
|
+
console.log(`
|
|
3780
|
+
${chalk.bold("rebase cloud")} — Manage your apps on Rebase Cloud
|
|
3781
|
+
|
|
3782
|
+
${chalk.green.bold("Usage")}
|
|
3783
|
+
rebase cloud ${chalk.blue("<command>")} [options]
|
|
3784
|
+
|
|
3785
|
+
${chalk.green.bold("Auth")}
|
|
3786
|
+
${chalk.blue.bold("login")} Sign in to the control plane
|
|
3787
|
+
${chalk.blue.bold("logout")} Sign out
|
|
3788
|
+
${chalk.blue.bold("whoami")} Show the current session
|
|
3789
|
+
|
|
3790
|
+
${chalk.green.bold("Project link")}
|
|
3791
|
+
${chalk.blue.bold("link")} Link this directory to a cloud project
|
|
3792
|
+
${chalk.blue.bold("unlink")} Remove the link
|
|
3793
|
+
${chalk.blue.bold("use")} ${chalk.gray("[org]")} Select the active organization
|
|
3794
|
+
${chalk.blue.bold("open")} Open the dashboard in a browser
|
|
3795
|
+
|
|
3796
|
+
${chalk.green.bold("Projects")}
|
|
3797
|
+
${chalk.blue.bold("projects list")} List projects
|
|
3798
|
+
${chalk.blue.bold("projects create")} Create a project ${chalk.gray("(--link to link it)")}
|
|
3799
|
+
${chalk.blue.bold("projects info")} ${chalk.gray("[id]")} Show project details
|
|
3800
|
+
${chalk.blue.bold("projects delete")} ${chalk.gray("[id]")} Delete a project
|
|
3801
|
+
|
|
3802
|
+
${chalk.green.bold("Deploy & observe")}
|
|
3803
|
+
${chalk.blue.bold("deploy")} ${chalk.gray("[--source .]")} Deploy the linked project + stream build logs
|
|
3804
|
+
${chalk.blue.bold("logs")} ${chalk.gray("[--runtime] [-f]")} Show build (or runtime) logs
|
|
3805
|
+
${chalk.blue.bold("status")} One-glance project status
|
|
3806
|
+
${chalk.blue.bold("metrics")} Live CPU / memory / disk
|
|
3807
|
+
|
|
3808
|
+
${chalk.green.bold("Organizations")}
|
|
3809
|
+
${chalk.blue.bold("orgs list|create|members")}
|
|
3810
|
+
|
|
3811
|
+
${chalk.green.bold("Databases")}
|
|
3812
|
+
${chalk.blue.bold("db list|create|test")}
|
|
3813
|
+
${chalk.blue.bold("db backup list|create|restore")}
|
|
3814
|
+
|
|
3815
|
+
${chalk.green.bold("Other resources")}
|
|
3816
|
+
${chalk.blue.bold("webhooks list|create|delete")}
|
|
3817
|
+
${chalk.blue.bold("storage")} List storage buckets
|
|
3818
|
+
${chalk.blue.bold("clusters")} List compute clusters
|
|
3819
|
+
${chalk.blue.bold("billing setup")} Attach a card to the org ${chalk.gray("(one-time, opens browser)")}
|
|
3820
|
+
${chalk.blue.bold("billing")} Show billing account + card on file
|
|
3821
|
+
|
|
3822
|
+
${chalk.green.bold("Global options")}
|
|
3823
|
+
${chalk.blue("--url <origin>")} Target a specific control plane ${chalk.gray("(or REBASE_CLOUD_URL)")}
|
|
3824
|
+
${chalk.blue("--project, -p <id>")} Operate on a project without linking
|
|
3825
|
+
|
|
3826
|
+
${chalk.gray("Most commands act on the linked project (.rebase/cloud.json) unless --project is given.")}
|
|
3827
|
+
${chalk.gray("Docs: https://rebase.pro/docs")}
|
|
3828
|
+
`);
|
|
3829
|
+
}
|
|
3830
|
+
//#endregion
|
|
3831
|
+
//#region src/cli.ts
|
|
3832
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
3833
|
+
var __dirname = path.dirname(__filename);
|
|
173
3834
|
function getVersion() {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
} catch {
|
|
180
|
-
}
|
|
181
|
-
return "unknown";
|
|
3835
|
+
try {
|
|
3836
|
+
const pkgPath = path.resolve(__dirname, "../package.json");
|
|
3837
|
+
if (fs.existsSync(pkgPath)) return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
3838
|
+
} catch {}
|
|
3839
|
+
return "unknown";
|
|
182
3840
|
}
|
|
183
3841
|
async function entry(args) {
|
|
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
|
-
|
|
3842
|
+
const parsedArgs = arg({
|
|
3843
|
+
"--version": Boolean,
|
|
3844
|
+
"--help": Boolean,
|
|
3845
|
+
"-v": "--version",
|
|
3846
|
+
"-h": "--help"
|
|
3847
|
+
}, {
|
|
3848
|
+
argv: args.slice(2),
|
|
3849
|
+
permissive: true
|
|
3850
|
+
});
|
|
3851
|
+
if (parsedArgs["--version"]) {
|
|
3852
|
+
console.log(getVersion());
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
const command = parsedArgs._[0];
|
|
3856
|
+
const subcommand = parsedArgs._[1];
|
|
3857
|
+
if (!command || parsedArgs["--help"] && ![
|
|
3858
|
+
"schema",
|
|
3859
|
+
"db",
|
|
3860
|
+
"dev",
|
|
3861
|
+
"build",
|
|
3862
|
+
"start",
|
|
3863
|
+
"auth",
|
|
3864
|
+
"doctor",
|
|
3865
|
+
"skills",
|
|
3866
|
+
"api-keys",
|
|
3867
|
+
"cloud"
|
|
3868
|
+
].includes(command)) {
|
|
3869
|
+
printHelp();
|
|
3870
|
+
return;
|
|
3871
|
+
}
|
|
3872
|
+
const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
|
|
3873
|
+
switch (command) {
|
|
3874
|
+
case "init":
|
|
3875
|
+
await createRebaseApp(args);
|
|
3876
|
+
break;
|
|
3877
|
+
case "generate-sdk": {
|
|
3878
|
+
const sdkArgs = arg({
|
|
3879
|
+
"--collections-dir": String,
|
|
3880
|
+
"--output": String,
|
|
3881
|
+
"-c": "--collections-dir",
|
|
3882
|
+
"-o": "--output"
|
|
3883
|
+
}, {
|
|
3884
|
+
argv: args.slice(3),
|
|
3885
|
+
permissive: true
|
|
3886
|
+
});
|
|
3887
|
+
await generateSdkCommand({
|
|
3888
|
+
collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
|
|
3889
|
+
output: sdkArgs["--output"] || "./generated/sdk",
|
|
3890
|
+
cwd: process.cwd()
|
|
3891
|
+
});
|
|
3892
|
+
break;
|
|
3893
|
+
}
|
|
3894
|
+
case "schema":
|
|
3895
|
+
await schemaCommand(effectiveSubcommand, args);
|
|
3896
|
+
break;
|
|
3897
|
+
case "db":
|
|
3898
|
+
await dbCommand(effectiveSubcommand, args);
|
|
3899
|
+
break;
|
|
3900
|
+
case "dev":
|
|
3901
|
+
await devCommand(args);
|
|
3902
|
+
break;
|
|
3903
|
+
case "build":
|
|
3904
|
+
await buildCommand();
|
|
3905
|
+
break;
|
|
3906
|
+
case "start":
|
|
3907
|
+
await startCommand();
|
|
3908
|
+
break;
|
|
3909
|
+
case "auth":
|
|
3910
|
+
await authCommand(effectiveSubcommand, args);
|
|
3911
|
+
break;
|
|
3912
|
+
case "doctor":
|
|
3913
|
+
await doctorCommand(args);
|
|
3914
|
+
break;
|
|
3915
|
+
case "skills":
|
|
3916
|
+
await skillsCommand(effectiveSubcommand, args);
|
|
3917
|
+
break;
|
|
3918
|
+
case "api-keys":
|
|
3919
|
+
await apiKeysCommand(effectiveSubcommand, args);
|
|
3920
|
+
break;
|
|
3921
|
+
case "cloud":
|
|
3922
|
+
await cloudCommand(effectiveSubcommand, args);
|
|
3923
|
+
break;
|
|
3924
|
+
default:
|
|
3925
|
+
console.log(chalk.red(`Unknown command: ${command}`));
|
|
3926
|
+
console.log("");
|
|
3927
|
+
printHelp();
|
|
3928
|
+
}
|
|
212
3929
|
}
|
|
213
3930
|
function printHelp() {
|
|
214
|
-
|
|
3931
|
+
console.log(`
|
|
215
3932
|
${chalk.bold("Rebase CLI")} — Developer tools for Rebase projects
|
|
216
3933
|
|
|
217
3934
|
${chalk.green.bold("Usage")}
|
|
218
3935
|
rebase ${chalk.blue("<command>")} [options]
|
|
219
3936
|
|
|
220
3937
|
${chalk.green.bold("Commands")}
|
|
221
|
-
${chalk.blue.bold("init")}
|
|
3938
|
+
${chalk.blue.bold("init")} Create a new Rebase project
|
|
3939
|
+
${chalk.blue.bold("dev")} Start the development server
|
|
3940
|
+
${chalk.blue.bold("build")} Build all workspace packages
|
|
3941
|
+
${chalk.blue.bold("start")} Start the backend server ${chalk.gray("(production)")}
|
|
3942
|
+
|
|
3943
|
+
${chalk.green.bold("Schema")}
|
|
3944
|
+
${chalk.blue.bold("schema generate")} Generate Drizzle schema from collections
|
|
3945
|
+
${chalk.blue.bold("schema introspect")} Introspect database → Rebase collections
|
|
3946
|
+
${chalk.blue.bold("schema")} ${chalk.gray("--help")} Show schema command help
|
|
3947
|
+
|
|
3948
|
+
${chalk.green.bold("Database")}
|
|
3949
|
+
${chalk.blue.bold("db push")} Apply schema directly to database ${chalk.gray("(dev)")}
|
|
3950
|
+
${chalk.blue.bold("db generate")} Generate SQL migration files
|
|
3951
|
+
${chalk.blue.bold("db migrate")} Run pending migrations
|
|
3952
|
+
${chalk.blue.bold("db")} ${chalk.gray("--help")} Show database command help
|
|
3953
|
+
|
|
3954
|
+
${chalk.green.bold("SDK")}
|
|
3955
|
+
${chalk.blue.bold("generate-sdk")} Generate a typed TypeScript SDK from collections
|
|
3956
|
+
|
|
3957
|
+
${chalk.green.bold("Auth")}
|
|
3958
|
+
${chalk.blue.bold("auth reset-password")} Reset a user's password
|
|
3959
|
+
${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
|
|
3960
|
+
|
|
3961
|
+
${chalk.green.bold("Diagnostics")}
|
|
3962
|
+
${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
|
|
3963
|
+
|
|
3964
|
+
${chalk.green.bold("AI Agent Skills")}
|
|
3965
|
+
${chalk.blue.bold("skills install")} Install Rebase agent skills for your AI coding assistant
|
|
3966
|
+
|
|
3967
|
+
${chalk.green.bold("API Keys")}
|
|
3968
|
+
${chalk.blue.bold("api-keys list")} List all service API keys
|
|
3969
|
+
${chalk.blue.bold("api-keys create")} Create a new scoped API key
|
|
3970
|
+
${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
|
|
3971
|
+
${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
|
|
3972
|
+
|
|
3973
|
+
${chalk.green.bold("Rebase Cloud")}
|
|
3974
|
+
${chalk.blue.bold("cloud login")} Sign in to the hosted control plane
|
|
3975
|
+
${chalk.blue.bold("cloud link")} Link this directory to a cloud project
|
|
3976
|
+
${chalk.blue.bold("cloud deploy")} Deploy the linked project + stream logs
|
|
3977
|
+
${chalk.blue.bold("cloud")} ${chalk.gray("--help")} Show all cloud commands
|
|
222
3978
|
|
|
223
3979
|
${chalk.green.bold("Options")}
|
|
224
3980
|
${chalk.blue("--version, -v")} Show version number
|
|
@@ -227,56 +3983,7 @@ ${chalk.green.bold("Options")}
|
|
|
227
3983
|
${chalk.gray("Documentation: https://rebase.pro/docs")}
|
|
228
3984
|
`);
|
|
229
3985
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
async function getTokens(env, _debug) {
|
|
235
|
-
const fp = tokenPath(env);
|
|
236
|
-
if (!fs.existsSync(fp)) return null;
|
|
237
|
-
try {
|
|
238
|
-
const data = fs.readFileSync(fp, "utf-8");
|
|
239
|
-
return JSON.parse(data);
|
|
240
|
-
} catch {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
function parseJwt(token) {
|
|
245
|
-
if (!token) throw new Error("No JWT token");
|
|
246
|
-
const base64Url = token.split(".")[1];
|
|
247
|
-
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
248
|
-
const buffer = Buffer.from(base64, "base64");
|
|
249
|
-
return JSON.parse(buffer.toString());
|
|
250
|
-
}
|
|
251
|
-
async function refreshCredentials(env, credentials, _onErr) {
|
|
252
|
-
if (!credentials) return null;
|
|
253
|
-
const expiryDate = new Date(credentials["expiry_date"]);
|
|
254
|
-
if (expiryDate.getTime() > Date.now()) {
|
|
255
|
-
return credentials;
|
|
256
|
-
}
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
async function login(env, _debug) {
|
|
260
|
-
console.log(
|
|
261
|
-
"Interactive login is not yet implemented in @rebasepro/cli.\nPlease authenticate via the Rebase dashboard and copy your tokens to " + tokenPath(env)
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
async function logout(env, _debug) {
|
|
265
|
-
const fp = tokenPath(env);
|
|
266
|
-
if (fs.existsSync(fp)) {
|
|
267
|
-
fs.unlinkSync(fp);
|
|
268
|
-
console.log("You have been logged out.");
|
|
269
|
-
} else {
|
|
270
|
-
console.log("You are not logged in.");
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
export {
|
|
274
|
-
createRebaseApp,
|
|
275
|
-
entry,
|
|
276
|
-
getTokens,
|
|
277
|
-
login,
|
|
278
|
-
logout,
|
|
279
|
-
parseJwt,
|
|
280
|
-
refreshCredentials
|
|
281
|
-
};
|
|
282
|
-
//# sourceMappingURL=index.es.js.map
|
|
3986
|
+
//#endregion
|
|
3987
|
+
export { authCommand, buildCommand, buildInitQuestions, cloudCommand, configureEnvFile, createRebaseApp, dbCommand, detectPackageManager, devCommand, doctorCommand, entry, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, generateSdkCommand, getActiveBackendPlugin, getPMCommands, requireBackendDir, requireProjectRoot, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, validateTsxInstallation };
|
|
3988
|
+
|
|
3989
|
+
//# sourceMappingURL=index.es.js.map
|