@rebasepro/cli 0.0.1-canary.eae7889 → 0.0.1-canary.eb08332
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/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/generate_sdk.d.ts +1 -1
- package/dist/commands/init.d.ts +39 -0
- package/dist/commands/skills.d.ts +1 -0
- package/dist/commands/start.d.ts +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.es.js +3785 -990
- package/dist/index.es.js.map +1 -1
- package/dist/utils/package-manager.d.ts +33 -0
- package/dist/utils/project.d.ts +16 -1
- package/package.json +26 -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 +1 -1
- 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 +20 -10
- package/templates/template/ai-instructions.md +17 -0
- package/templates/template/backend/Dockerfile +5 -4
- package/templates/template/backend/functions/hello.ts +36 -32
- package/templates/template/backend/package.json +11 -11
- package/templates/template/backend/src/env.ts +13 -42
- package/templates/template/backend/src/index.ts +54 -28
- package/templates/template/config/collections/authors.ts +2 -2
- package/templates/template/config/collections/index.ts +25 -4
- package/templates/template/config/collections/posts.ts +15 -35
- 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 +3 -5
- package/templates/template/config/collections/users.ts +142 -0
- package/templates/template/config/index.ts +1 -1
- package/templates/template/docker-compose.yml +14 -39
- package/templates/template/frontend/Dockerfile +5 -3
- package/templates/template/frontend/package.json +8 -7
- package/templates/template/frontend/src/App.tsx +26 -105
- package/templates/template/frontend/src/main.tsx +4 -0
- 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 +47 -4
- package/templates/template/package.json +22 -8
- package/templates/template/pnpm-workspace.yaml +9 -0
- package/templates/template/scripts/example.ts +5 -2
- package/dist/auth.d.ts +0 -5
- package/dist/commands/cli.test.d.ts +0 -1
- package/dist/commands/dev.test.d.ts +0 -1
- package/dist/commands/init.test.d.ts +0 -1
- package/dist/index.cjs +0 -1203
- package/dist/index.cjs.map +0 -1
- package/dist/utils/project.test.d.ts +0 -1
- package/templates/template/.env.template +0 -62
- package/templates/template/backend/drizzle.config.ts +0 -22
package/dist/index.es.js
CHANGED
|
@@ -3,488 +3,991 @@ 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 { generateSDK } from "@rebasepro/
|
|
12
|
+
import { generateSDK } from "@rebasepro/codegen";
|
|
12
13
|
import { execSync, spawn } from "child_process";
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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);
|
|
18
138
|
function findParentDir(currentDir, targetName) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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;
|
|
27
237
|
}
|
|
28
|
-
const cliRoot = findParentDir(__dirname$2, "cli");
|
|
29
238
|
async function createRebaseApp(rawArgs) {
|
|
30
|
-
|
|
239
|
+
console.log(`
|
|
31
240
|
${chalk.bold("Rebase")} — Create a new project 🚀
|
|
32
241
|
`);
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
});
|
|
187
519
|
}
|
|
188
520
|
async function replacePlaceholders(options) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
});
|
|
203
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
|
+
*/
|
|
204
656
|
async function loadCollections(collectionsDir) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const collections = [];
|
|
261
|
-
for (const value of Object.values(exported)) {
|
|
262
|
-
if (value && typeof value === "object" && "slug" in value) {
|
|
263
|
-
collections.push(value);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (collections.length > 0) return collections;
|
|
267
|
-
}
|
|
268
|
-
throw new Error(
|
|
269
|
-
`Could not extract collections from ${indexPath}.
|
|
270
|
-
Expected a default export of EntityCollection[] or an object with named collection exports.`
|
|
271
|
-
);
|
|
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.`);
|
|
272
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* Write generated files to the output directory.
|
|
715
|
+
*/
|
|
273
716
|
function writeFiles(outputDir, files) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
fs.writeFileSync(filePath, file.content, "utf-8");
|
|
283
|
-
}
|
|
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
|
+
}
|
|
284
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* Main entry point for the generate-sdk command.
|
|
728
|
+
*/
|
|
285
729
|
async function generateSdkCommand(args) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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("");
|
|
322
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
|
+
*/
|
|
323
783
|
function findProjectRoot(startDir = process.cwd()) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return dir;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
dir = path.dirname(dir);
|
|
344
|
-
}
|
|
345
|
-
return null;
|
|
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;
|
|
346
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* Locate the backend directory within the project root.
|
|
803
|
+
*/
|
|
347
804
|
function findBackendDir(projectRoot) {
|
|
348
|
-
|
|
349
|
-
|
|
805
|
+
const backendDir = path.join(projectRoot, "backend");
|
|
806
|
+
return fs.existsSync(backendDir) ? backendDir : null;
|
|
350
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* Detect the active backend plugin (e.g. @rebasepro/server-postgres) from the backend's package.json.
|
|
810
|
+
*/
|
|
351
811
|
function getActiveBackendPlugin(backendDir) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return null;
|
|
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;
|
|
368
827
|
}
|
|
828
|
+
/**
|
|
829
|
+
* Resolve the active plugin's CLI script.
|
|
830
|
+
*/
|
|
369
831
|
function resolvePluginCliScript(backendDir, pluginName) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
return null;
|
|
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;
|
|
381
841
|
}
|
|
842
|
+
/**
|
|
843
|
+
* Locate the frontend directory within the project root.
|
|
844
|
+
*/
|
|
382
845
|
function findFrontendDir(projectRoot) {
|
|
383
|
-
|
|
384
|
-
|
|
846
|
+
const frontendDir = path.join(projectRoot, "frontend");
|
|
847
|
+
return fs.existsSync(frontendDir) ? frontendDir : null;
|
|
385
848
|
}
|
|
849
|
+
/**
|
|
850
|
+
* Find the .env file. Checks the project root first, then backend.
|
|
851
|
+
*/
|
|
386
852
|
function findEnvFile(projectRoot) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
];
|
|
391
|
-
for (const candidate of candidates) {
|
|
392
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
393
|
-
}
|
|
394
|
-
return null;
|
|
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;
|
|
395
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
|
+
*/
|
|
396
861
|
function resolveLocalBin(projectRoot, binName) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
const globalPath = execSync(`which ${binName}`, { encoding: "utf-8" }).trim();
|
|
412
|
-
if (globalPath && fs.existsSync(globalPath)) return globalPath;
|
|
413
|
-
} catch {
|
|
414
|
-
}
|
|
415
|
-
return null;
|
|
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;
|
|
416
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Resolve the tsx binary. Checks backend node_modules first, then root.
|
|
878
|
+
*/
|
|
417
879
|
function resolveTsx(projectRoot) {
|
|
418
|
-
|
|
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
|
+
}
|
|
419
916
|
}
|
|
917
|
+
/**
|
|
918
|
+
* Require the project root or exit with a helpful error.
|
|
919
|
+
*/
|
|
420
920
|
function requireProjectRoot() {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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;
|
|
429
929
|
}
|
|
930
|
+
/**
|
|
931
|
+
* Require the backend directory or exit with a helpful error.
|
|
932
|
+
*/
|
|
430
933
|
function requireBackendDir(projectRoot) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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;
|
|
438
941
|
}
|
|
942
|
+
//#endregion
|
|
943
|
+
//#region src/commands/schema.ts
|
|
944
|
+
/**
|
|
945
|
+
* CLI command: rebase schema <action>
|
|
946
|
+
*/
|
|
439
947
|
async function schemaCommand(subcommand, rawArgs) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
} catch (err) {
|
|
483
|
-
process.exit(1);
|
|
484
|
-
}
|
|
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
|
+
}
|
|
485
988
|
}
|
|
486
989
|
function printSchemaHelp() {
|
|
487
|
-
|
|
990
|
+
console.log(`
|
|
488
991
|
${chalk.bold("rebase schema")} — Schema management commands
|
|
489
992
|
|
|
490
993
|
${chalk.green.bold("Usage")}
|
|
@@ -493,62 +996,66 @@ ${chalk.green.bold("Usage")}
|
|
|
493
996
|
${chalk.green.bold("Commands")}
|
|
494
997
|
${chalk.gray("(Commands are provided by your active database driver plugin)")}
|
|
495
998
|
${chalk.blue.bold("generate")} Generate Schema from collection definitions
|
|
999
|
+
${chalk.blue.bold("introspect")} Introspect an existing database to generate collection definitions
|
|
496
1000
|
|
|
497
1001
|
${chalk.green.bold("generate Options")}
|
|
498
1002
|
${chalk.blue("--collections, -c")} Path to collections directory
|
|
499
1003
|
${chalk.blue("--output, -o")} Output path for generated schema
|
|
500
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
|
|
501
1008
|
`);
|
|
502
1009
|
}
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/commands/db.ts
|
|
1012
|
+
/**
|
|
1013
|
+
* CLI command: rebase db <action>
|
|
1014
|
+
*/
|
|
503
1015
|
async function dbCommand(subcommand, rawArgs) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
} catch (err) {
|
|
547
|
-
process.exit(1);
|
|
548
|
-
}
|
|
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
|
+
}
|
|
549
1056
|
}
|
|
550
|
-
function printDbHelp() {
|
|
551
|
-
|
|
1057
|
+
function printDbHelp$1() {
|
|
1058
|
+
console.log(`
|
|
552
1059
|
${chalk.bold("rebase db")} — Database management commands
|
|
553
1060
|
|
|
554
1061
|
${chalk.green.bold("Usage")}
|
|
@@ -557,11 +1064,12 @@ ${chalk.green.bold("Usage")}
|
|
|
557
1064
|
${chalk.green.bold("Commands")}
|
|
558
1065
|
${chalk.gray("(Commands are provided by your active database driver plugin)")}
|
|
559
1066
|
${chalk.blue.bold("push")} Apply schema directly to database (development)
|
|
560
|
-
${chalk.blue.bold("pull")} Introspect database → Schema
|
|
561
1067
|
${chalk.blue.bold("generate")} Generate migration files
|
|
562
1068
|
${chalk.blue.bold("migrate")} Run pending migrations
|
|
563
|
-
${chalk.blue.bold("studio")} Open Studio viewer
|
|
564
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)
|
|
565
1073
|
|
|
566
1074
|
${chalk.green.bold("Examples")}
|
|
567
1075
|
${chalk.gray("# Quick development workflow")}
|
|
@@ -573,236 +1081,388 @@ ${chalk.green.bold("Examples")}
|
|
|
573
1081
|
|
|
574
1082
|
${chalk.gray("# Create a database branch")}
|
|
575
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
|
|
576
1091
|
`);
|
|
577
1092
|
}
|
|
578
|
-
|
|
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
|
+
*/
|
|
579
1118
|
function getProjectPort(projectRoot) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
return 3001 + Math.abs(hash) % 999;
|
|
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;
|
|
585
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
|
+
*/
|
|
586
1130
|
function resolveStartPort(projectRoot, explicitPort) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
return getProjectPort(projectRoot);
|
|
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);
|
|
598
1141
|
}
|
|
599
1142
|
async function devCommand(rawArgs) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
+
})));
|
|
803
1463
|
}
|
|
804
1464
|
function printDevHelp() {
|
|
805
|
-
|
|
1465
|
+
console.log(`
|
|
806
1466
|
${chalk.bold("rebase dev")} — Start the development server
|
|
807
1467
|
|
|
808
1468
|
${chalk.green.bold("Usage")}
|
|
@@ -812,6 +1472,7 @@ ${chalk.green.bold("Options")}
|
|
|
812
1472
|
${chalk.blue("--backend-only, -b")} Only start the backend server
|
|
813
1473
|
${chalk.blue("--frontend-only, -f")} Only start the frontend server
|
|
814
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
|
|
815
1476
|
|
|
816
1477
|
${chalk.green.bold("Description")}
|
|
817
1478
|
Starts both the backend (tsx watch + Hono) and frontend (Vite)
|
|
@@ -824,87 +1485,236 @@ ${chalk.green.bold("Description")}
|
|
|
824
1485
|
If the assigned port is already in use, the server will automatically
|
|
825
1486
|
try the next available port. The frontend is started only after the
|
|
826
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.
|
|
827
1492
|
`);
|
|
828
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
|
+
*/
|
|
829
1553
|
async function authCommand(subcommand, rawArgs) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
+
}
|
|
844
1568
|
}
|
|
845
1569
|
async function resetPassword(rawArgs) {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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";
|
|
883
1675
|
import { eq } from "drizzle-orm";
|
|
884
|
-
import { users } from "@rebasepro/server-core/src/db/auth-schema";
|
|
885
1676
|
import * as dotenv from "dotenv";
|
|
886
1677
|
import path from "path";
|
|
1678
|
+
import fs from "fs";
|
|
887
1679
|
|
|
888
|
-
dotenv.config({ path:
|
|
1680
|
+
dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH });
|
|
889
1681
|
|
|
890
|
-
const email =
|
|
891
|
-
const newPassword =
|
|
1682
|
+
const email = process.env.REBASE_RESET_EMAIL!;
|
|
1683
|
+
const newPassword = process.env.REBASE_RESET_PASSWORD!;
|
|
892
1684
|
|
|
893
1685
|
async function resetPassword() {
|
|
894
1686
|
const { db } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);
|
|
895
1687
|
const hash = await hashPassword(newPassword);
|
|
896
1688
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
.
|
|
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;
|
|
1695
|
+
}
|
|
1696
|
+
} catch (e) {
|
|
1697
|
+
// ignore and fallback
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
if (!usersTable) {
|
|
1701
|
+
const pgServer = await import("@rebasepro/server-postgres");
|
|
1702
|
+
usersTable = pgServer.users;
|
|
1703
|
+
}
|
|
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))
|
|
900
1710
|
.returning({
|
|
901
|
-
id:
|
|
902
|
-
email:
|
|
1711
|
+
id: usersTable.id,
|
|
1712
|
+
email: usersTable.email
|
|
903
1713
|
});
|
|
904
1714
|
|
|
905
1715
|
if (result.length > 0) {
|
|
906
1716
|
console.log("✅ Password reset for: " + result[0].email);
|
|
907
|
-
${!newPassword ?
|
|
1717
|
+
${!newPassword ? "console.log(\" New password: \" + newPassword);" : ""}
|
|
908
1718
|
} else {
|
|
909
1719
|
console.log("✗ User not found: " + email);
|
|
910
1720
|
}
|
|
@@ -913,36 +1723,36 @@ async function resetPassword() {
|
|
|
913
1723
|
|
|
914
1724
|
resetPassword().catch(console.error);
|
|
915
1725
|
`;
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
+
}
|
|
943
1753
|
}
|
|
944
1754
|
function printAuthHelp() {
|
|
945
|
-
|
|
1755
|
+
console.log(`
|
|
946
1756
|
${chalk.bold("rebase auth")} — Authentication management commands
|
|
947
1757
|
|
|
948
1758
|
${chalk.green.bold("Usage")}
|
|
@@ -960,133 +1770,2165 @@ ${chalk.green.bold("Examples")}
|
|
|
960
1770
|
rebase auth reset-password --email user@example.com --password MyNewPass!
|
|
961
1771
|
`);
|
|
962
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
|
+
*/
|
|
963
1781
|
async function doctorCommand(rawArgs) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
+
}
|
|
1005
2293
|
}
|
|
1006
|
-
|
|
1007
|
-
const
|
|
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);
|
|
1008
3834
|
function getVersion() {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
} catch {
|
|
1015
|
-
}
|
|
1016
|
-
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";
|
|
1017
3840
|
}
|
|
1018
3841
|
async function entry(args) {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
+
}
|
|
1087
3929
|
}
|
|
1088
3930
|
function printHelp() {
|
|
1089
|
-
|
|
3931
|
+
console.log(`
|
|
1090
3932
|
${chalk.bold("Rebase CLI")} — Developer tools for Rebase projects
|
|
1091
3933
|
|
|
1092
3934
|
${chalk.green.bold("Usage")}
|
|
@@ -1095,21 +3937,22 @@ ${chalk.green.bold("Usage")}
|
|
|
1095
3937
|
${chalk.green.bold("Commands")}
|
|
1096
3938
|
${chalk.blue.bold("init")} Create a new Rebase project
|
|
1097
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)")}
|
|
1098
3942
|
|
|
1099
3943
|
${chalk.green.bold("Schema")}
|
|
1100
3944
|
${chalk.blue.bold("schema generate")} Generate Drizzle schema from collections
|
|
3945
|
+
${chalk.blue.bold("schema introspect")} Introspect database → Rebase collections
|
|
1101
3946
|
${chalk.blue.bold("schema")} ${chalk.gray("--help")} Show schema command help
|
|
1102
3947
|
|
|
1103
3948
|
${chalk.green.bold("Database")}
|
|
1104
3949
|
${chalk.blue.bold("db push")} Apply schema directly to database ${chalk.gray("(dev)")}
|
|
1105
|
-
${chalk.blue.bold("db pull")} Introspect database → Drizzle schema
|
|
1106
3950
|
${chalk.blue.bold("db generate")} Generate SQL migration files
|
|
1107
3951
|
${chalk.blue.bold("db migrate")} Run pending migrations
|
|
1108
|
-
${chalk.blue.bold("db studio")} Open Drizzle Studio
|
|
1109
3952
|
${chalk.blue.bold("db")} ${chalk.gray("--help")} Show database command help
|
|
1110
3953
|
|
|
1111
3954
|
${chalk.green.bold("SDK")}
|
|
1112
|
-
${chalk.blue.bold("generate-sdk")} Generate a typed
|
|
3955
|
+
${chalk.blue.bold("generate-sdk")} Generate a typed TypeScript SDK from collections
|
|
1113
3956
|
|
|
1114
3957
|
${chalk.green.bold("Auth")}
|
|
1115
3958
|
${chalk.blue.bold("auth reset-password")} Reset a user's password
|
|
@@ -1118,6 +3961,21 @@ ${chalk.green.bold("Auth")}
|
|
|
1118
3961
|
${chalk.green.bold("Diagnostics")}
|
|
1119
3962
|
${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
|
|
1120
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
|
|
3978
|
+
|
|
1121
3979
|
${chalk.green.bold("Options")}
|
|
1122
3980
|
${chalk.blue("--version, -v")} Show version number
|
|
1123
3981
|
${chalk.blue("--help, -h")} Show this help message
|
|
@@ -1125,70 +3983,7 @@ ${chalk.green.bold("Options")}
|
|
|
1125
3983
|
${chalk.gray("Documentation: https://rebase.pro/docs")}
|
|
1126
3984
|
`);
|
|
1127
3985
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
async function getTokens(env, _debug) {
|
|
1133
|
-
const fp = tokenPath(env);
|
|
1134
|
-
if (!fs.existsSync(fp)) return null;
|
|
1135
|
-
try {
|
|
1136
|
-
const data = fs.readFileSync(fp, "utf-8");
|
|
1137
|
-
return JSON.parse(data);
|
|
1138
|
-
} catch {
|
|
1139
|
-
return null;
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
function parseJwt(token) {
|
|
1143
|
-
if (!token) throw new Error("No JWT token");
|
|
1144
|
-
const base64Url = token.split(".")[1];
|
|
1145
|
-
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
1146
|
-
const buffer = Buffer.from(base64, "base64");
|
|
1147
|
-
return JSON.parse(buffer.toString());
|
|
1148
|
-
}
|
|
1149
|
-
async function refreshCredentials(env, credentials, _onErr) {
|
|
1150
|
-
if (!credentials) return null;
|
|
1151
|
-
const expiryDate = new Date(credentials["expiry_date"]);
|
|
1152
|
-
if (expiryDate.getTime() > Date.now()) {
|
|
1153
|
-
return credentials;
|
|
1154
|
-
}
|
|
1155
|
-
return null;
|
|
1156
|
-
}
|
|
1157
|
-
async function login(env, _debug) {
|
|
1158
|
-
console.log(
|
|
1159
|
-
"Interactive login is not yet implemented in @rebasepro/cli.\nPlease authenticate via the Rebase dashboard and copy your tokens to " + tokenPath(env)
|
|
1160
|
-
);
|
|
1161
|
-
}
|
|
1162
|
-
async function logout(env, _debug) {
|
|
1163
|
-
const fp = tokenPath(env);
|
|
1164
|
-
if (fs.existsSync(fp)) {
|
|
1165
|
-
fs.unlinkSync(fp);
|
|
1166
|
-
console.log("You have been logged out.");
|
|
1167
|
-
} else {
|
|
1168
|
-
console.log("You are not logged in.");
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
export {
|
|
1172
|
-
authCommand,
|
|
1173
|
-
createRebaseApp,
|
|
1174
|
-
dbCommand,
|
|
1175
|
-
devCommand,
|
|
1176
|
-
entry,
|
|
1177
|
-
findBackendDir,
|
|
1178
|
-
findEnvFile,
|
|
1179
|
-
findFrontendDir,
|
|
1180
|
-
findProjectRoot,
|
|
1181
|
-
getActiveBackendPlugin,
|
|
1182
|
-
getTokens,
|
|
1183
|
-
login,
|
|
1184
|
-
logout,
|
|
1185
|
-
parseJwt,
|
|
1186
|
-
refreshCredentials,
|
|
1187
|
-
requireBackendDir,
|
|
1188
|
-
requireProjectRoot,
|
|
1189
|
-
resolveLocalBin,
|
|
1190
|
-
resolvePluginCliScript,
|
|
1191
|
-
resolveTsx,
|
|
1192
|
-
schemaCommand
|
|
1193
|
-
};
|
|
1194
|
-
//# 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
|