@rebasepro/cli 0.20.1-canary.g4d882ca → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/build.d.ts +26 -0
- package/dist/commands/cloud/bundle-deploy.d.ts +37 -0
- package/dist/commands/cloud/context.d.ts +54 -0
- package/dist/commands/cloud/databases.d.ts +2 -0
- package/dist/commands/cloud/db-connect.d.ts +70 -0
- package/dist/commands/cloud/projects.d.ts +2 -3
- package/dist/commands/init.d.ts +36 -1
- package/dist/index.es.js +576 -85
- package/dist/index.es.js.map +1 -1
- package/package.json +10 -8
package/dist/index.es.js
CHANGED
|
@@ -18,14 +18,15 @@ import { execa, execaCommandSync } from "execa";
|
|
|
18
18
|
import { cp } from "fs/promises";
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
20
20
|
import crypto from "crypto";
|
|
21
|
-
import { spawn, spawnSync } from "child_process";
|
|
21
|
+
import { execFileSync, spawn, spawnSync } from "child_process";
|
|
22
22
|
import os from "os";
|
|
23
23
|
import { createRebaseClient } from "@rebasepro/client";
|
|
24
24
|
import dotenv from "dotenv";
|
|
25
|
-
import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, DEFAULT_RESOURCE_KEY, RUNTIME_CONTRACT_VERSION, buildResourceGraph, computeSchemaVersion, declareFunction, declaredQueueConsumers, declaredResources, declaredSubscriptions, deserializeCollections, envBasesForResource, findEnvSuffixCollision, findStorageSuffixCollision, getDataSourceCapabilities, isResourceHandle, reservedPrefixFor, resetDeclaredQueueConsumers, resetDeclaredResources, resetDeclaredSubscriptions, resolveResourceRefs, resourceEnvSuffix, resourceId, resourceKeyOf, resourceToDataSource } from "@rebasepro/types";
|
|
25
|
+
import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, DEFAULT_RESOURCE_KEY, DEFAULT_STORAGE_SOURCE_KEY, RUNTIME_CONTRACT_VERSION, buildResourceGraph, computeSchemaVersion, declareFunction, declaredQueueConsumers, declaredResources, declaredSubscriptions, deserializeCollections, envBasesForResource, findEnvSuffixCollision, findStorageSuffixCollision, getDataSourceCapabilities, isResourceHandle, reservedPrefixFor, resetDeclaredQueueConsumers, resetDeclaredResources, resetDeclaredSubscriptions, resolveResourceRefs, resourceEnvSuffix, resourceId, resourceKeyOf, resourceToDataSource } from "@rebasepro/types";
|
|
26
26
|
import { CodegenError, generateSDK, toSafeIdentifier } from "@rebasepro/codegen";
|
|
27
27
|
import { createRequire } from "module";
|
|
28
28
|
import { randomBytes as randomBytes$1 } from "node:crypto";
|
|
29
|
+
import net$1 from "node:net";
|
|
29
30
|
//#region src/utils/version.ts
|
|
30
31
|
/**
|
|
31
32
|
* This CLI's own version, and the User-Agent built from it.
|
|
@@ -337,6 +338,27 @@ function credentialsPath() {
|
|
|
337
338
|
return path.join(os.homedir(), ".rebase", "credentials.json");
|
|
338
339
|
}
|
|
339
340
|
/** Project-local link file: <project>/.rebase/cloud.json */
|
|
341
|
+
/**
|
|
342
|
+
* The billing account an organization row points at, whichever key it arrived under.
|
|
343
|
+
*
|
|
344
|
+
* REST serves every column under its own property name, so the relation comes
|
|
345
|
+
* back as `billingAccountId`. Both readers in this CLI checked
|
|
346
|
+
* `billing_account_id` and `billingAccount` — neither is what arrives — so
|
|
347
|
+
* `rebase cloud billing` answered "no billing account" for every organization,
|
|
348
|
+
* and the deploy pre-check never saw an internal plan. The other two spellings
|
|
349
|
+
* are still accepted: older servers and fixtures send them.
|
|
350
|
+
*/
|
|
351
|
+
function billingAccountIdOf(org) {
|
|
352
|
+
if (typeof org !== "object" || org === null) return void 0;
|
|
353
|
+
for (const key of [
|
|
354
|
+
"billingAccountId",
|
|
355
|
+
"billing_account_id",
|
|
356
|
+
"billingAccount"
|
|
357
|
+
]) {
|
|
358
|
+
const value = org[key];
|
|
359
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
340
362
|
function projectLinkPath(cwd = process.cwd()) {
|
|
341
363
|
const root = findProjectRoot(cwd) || cwd;
|
|
342
364
|
return path.join(root, ".rebase", "cloud.json");
|
|
@@ -1175,6 +1197,45 @@ function openUrl(target, label = "Opening") {
|
|
|
1175
1197
|
child.unref();
|
|
1176
1198
|
} catch {}
|
|
1177
1199
|
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Read the control plane's rows as a subcommand's row shape.
|
|
1202
|
+
*
|
|
1203
|
+
* `client.data.collection(...)` is typed against the *generated* schema of the
|
|
1204
|
+
* project the SDK is pointed at, and the control-plane collections this CLI
|
|
1205
|
+
* reads are not in one — so every row arrives as an open
|
|
1206
|
+
* `Record<string, unknown>`. A declared `interface` gets no implicit index
|
|
1207
|
+
* signature, so it does not overlap that, and a direct `as` is refused: which
|
|
1208
|
+
* is how ten call sites came to write `as unknown as XRow[]`, an assertion
|
|
1209
|
+
* about a wire payload with nothing checking it in either direction.
|
|
1210
|
+
*
|
|
1211
|
+
* There is exactly one invariant to check and this checks it. Everything else
|
|
1212
|
+
* the shapes declare is optional and already read as such, so there is nothing
|
|
1213
|
+
* further to verify — but a row with no usable `id` is not a row any of these
|
|
1214
|
+
* commands can act on, and passing it through was how a listing came to print
|
|
1215
|
+
* `[undefined]` and a lookup came to compare against the string `"undefined"`.
|
|
1216
|
+
*/
|
|
1217
|
+
function cloudRows(rows) {
|
|
1218
|
+
return (rows ?? []).filter((row) => typeof row?.id === "string" || typeof row?.id === "number");
|
|
1219
|
+
}
|
|
1220
|
+
/** {@link cloudRows} for an endpoint that returns a single row. */
|
|
1221
|
+
function cloudRow(row) {
|
|
1222
|
+
return cloudRows(row ? [row] : [])[0];
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* {@link cloudRows} for a write that must have produced a row.
|
|
1226
|
+
*
|
|
1227
|
+
* `create()` returning something with no usable `id` means the control plane
|
|
1228
|
+
* accepted the write and then described it in a way this CLI cannot act on.
|
|
1229
|
+
* The callers all go straight on to use that id — `setContextOrg(url,
|
|
1230
|
+
* String(created.id))` — so asserting the shape, which is what stood here,
|
|
1231
|
+
* turned a control-plane fault into an organization whose active id is the
|
|
1232
|
+
* seven-letter string `"undefined"`, stored in the user's config file.
|
|
1233
|
+
*/
|
|
1234
|
+
function requireCloudRow(row, what) {
|
|
1235
|
+
const parsed = cloudRow(row);
|
|
1236
|
+
if (!parsed) throw new Error(`The control plane accepted the ${what} but returned no id for it.`);
|
|
1237
|
+
return parsed;
|
|
1238
|
+
}
|
|
1178
1239
|
/** Duration in coarse bands — enough to see "slow", not enough to fingerprint. */
|
|
1179
1240
|
function durationBucket(ms) {
|
|
1180
1241
|
if (!Number.isFinite(ms) || ms < 0) return "unknown";
|
|
@@ -1184,6 +1245,24 @@ function durationBucket(ms) {
|
|
|
1184
1245
|
return "120s+";
|
|
1185
1246
|
}
|
|
1186
1247
|
/**
|
|
1248
|
+
* An error reduced to something safe to transmit.
|
|
1249
|
+
*
|
|
1250
|
+
* The message and the stack are discarded, always. What survives is the
|
|
1251
|
+
* constructor name and, for the errors that carry one, a `code` — both of which
|
|
1252
|
+
* come from the program rather than from anything the user typed or named.
|
|
1253
|
+
* `EACCES` is useful and safe; "cannot write /Users/francesco/clients/acme" is
|
|
1254
|
+
* neither.
|
|
1255
|
+
*/
|
|
1256
|
+
function errorClass(error) {
|
|
1257
|
+
if (error && typeof error === "object") {
|
|
1258
|
+
const code = error.code;
|
|
1259
|
+
if (typeof code === "string" && /^[A-Z][A-Z0-9_]{1,31}$/.test(code)) return code;
|
|
1260
|
+
const name = error.name;
|
|
1261
|
+
if (typeof name === "string" && /^[A-Za-z][A-Za-z0-9]{0,31}$/.test(name)) return name;
|
|
1262
|
+
}
|
|
1263
|
+
return "Unknown";
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1187
1266
|
* Drop anything that is not a permitted value type, and clamp strings.
|
|
1188
1267
|
*
|
|
1189
1268
|
* The last line of defence rather than the first. Every call site is supposed
|
|
@@ -1623,11 +1702,6 @@ var PRESET_CHOICES = [
|
|
|
1623
1702
|
short: "Blank"
|
|
1624
1703
|
}
|
|
1625
1704
|
];
|
|
1626
|
-
/**
|
|
1627
|
-
* Builds the interactive prompt questions for `rebase init`.
|
|
1628
|
-
* Exported for testability — all prompt `type` values must match
|
|
1629
|
-
* types registered by the installed version of inquirer.
|
|
1630
|
-
*/
|
|
1631
1705
|
function buildInitQuestions(params) {
|
|
1632
1706
|
const { nameArg, templateArg, headlessArg, hasGitFlag, hasInstallFlag, pm } = params;
|
|
1633
1707
|
const questions = [];
|
|
@@ -4441,8 +4515,25 @@ function isRecord(value) {
|
|
|
4441
4515
|
* A manifest is committed and reviewed, so this is not a security boundary so
|
|
4442
4516
|
* much as a guard against `../../` typos that would otherwise have `rebase build`
|
|
4443
4517
|
* writing outside the project.
|
|
4444
|
-
|
|
4445
|
-
|
|
4518
|
+
*
|
|
4519
|
+
* `mayEscape` is for `context`, and only for `context`. Every other path here
|
|
4520
|
+
* names something Rebase *reads* — collections, functions, the generated
|
|
4521
|
+
* schema, the built assets to serve — and those have to be inside the project
|
|
4522
|
+
* or the bundle cannot carry them. A Docker build context is the one field that
|
|
4523
|
+
* names something Rebase never opens: it is handed to `docker build`, and in
|
|
4524
|
+
* any workspace repository the thing it has to name is the workspace root,
|
|
4525
|
+
* above the app. That is not exotic. It is what pnpm, turbo and nx layouts all
|
|
4526
|
+
* look like, and it is what this repository's own reference project needs — its
|
|
4527
|
+
* Dockerfile's first instruction copies `pnpm-lock.yaml`, which does not exist
|
|
4528
|
+
* beside `rebase.json` and never will.
|
|
4529
|
+
*
|
|
4530
|
+
* Refusing it did not prevent the escape; it only stopped anyone declaring it.
|
|
4531
|
+
* `app/rebase.json` built from the monorepo root the whole time — via
|
|
4532
|
+
* `infra/cloudbuild.yaml`, which says `-f app/backend/Dockerfile .` — while the
|
|
4533
|
+
* manifest said the context was `app/` and `rebase build` printed a command
|
|
4534
|
+
* that dies on its first `COPY`.
|
|
4535
|
+
*/
|
|
4536
|
+
function checkRelativePath(value, fieldPath, issues, { required, mayEscape = false }) {
|
|
4446
4537
|
if (value === void 0) {
|
|
4447
4538
|
if (required) issues.push({
|
|
4448
4539
|
path: fieldPath,
|
|
@@ -4465,7 +4556,7 @@ function checkRelativePath(value, fieldPath, issues, { required }) {
|
|
|
4465
4556
|
return;
|
|
4466
4557
|
}
|
|
4467
4558
|
const normalized = path.normalize(value);
|
|
4468
|
-
if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) {
|
|
4559
|
+
if (!mayEscape && (normalized === ".." || normalized.startsWith(`..${path.sep}`))) {
|
|
4469
4560
|
issues.push({
|
|
4470
4561
|
path: fieldPath,
|
|
4471
4562
|
message: "must stay inside the project directory"
|
|
@@ -4600,12 +4691,26 @@ function validateApp(name, raw, issues) {
|
|
|
4600
4691
|
message: "only applies to a custom runtime — set \"runtime\": \"custom\" to build your own image"
|
|
4601
4692
|
});
|
|
4602
4693
|
checkRelativePath(raw.dockerfile, `${base}.dockerfile`, issues, { required: false });
|
|
4603
|
-
checkRelativePath(raw.context, `${base}.context`, issues, {
|
|
4694
|
+
checkRelativePath(raw.context, `${base}.context`, issues, {
|
|
4695
|
+
required: false,
|
|
4696
|
+
mayEscape: true
|
|
4697
|
+
});
|
|
4604
4698
|
if (raw.port !== void 0 && (typeof raw.port !== "number" || !Number.isInteger(raw.port))) issues.push({
|
|
4605
4699
|
path: `${base}.port`,
|
|
4606
4700
|
message: "must be an integer"
|
|
4607
4701
|
});
|
|
4608
|
-
return
|
|
4702
|
+
return {
|
|
4703
|
+
type: "backend",
|
|
4704
|
+
runtime: custom ? "custom" : "managed",
|
|
4705
|
+
config: raw.config,
|
|
4706
|
+
functions: raw.functions,
|
|
4707
|
+
crons: raw.crons,
|
|
4708
|
+
schema: raw.schema,
|
|
4709
|
+
usersCollection: raw.usersCollection,
|
|
4710
|
+
dockerfile: raw.dockerfile,
|
|
4711
|
+
context: raw.context,
|
|
4712
|
+
port: raw.port
|
|
4713
|
+
};
|
|
4609
4714
|
}
|
|
4610
4715
|
case "static": {
|
|
4611
4716
|
checkRelativePath(raw.root, `${base}.root`, issues, { required: true });
|
|
@@ -4620,7 +4725,15 @@ function validateApp(name, raw, issues) {
|
|
|
4620
4725
|
});
|
|
4621
4726
|
const appPath = checkAppPath(raw.path, `${base}.path`, issues);
|
|
4622
4727
|
checkCmsPath(raw.cms, appPath ?? "/", `${base}.cms`, issues);
|
|
4623
|
-
return
|
|
4728
|
+
return {
|
|
4729
|
+
type: "static",
|
|
4730
|
+
root: raw.root,
|
|
4731
|
+
build: raw.build,
|
|
4732
|
+
output: raw.output,
|
|
4733
|
+
path: appPath,
|
|
4734
|
+
spa: raw.spa,
|
|
4735
|
+
cms: raw.cms
|
|
4736
|
+
};
|
|
4624
4737
|
}
|
|
4625
4738
|
default: return;
|
|
4626
4739
|
}
|
|
@@ -8696,6 +8809,42 @@ ${chalk.bold("Examples")}
|
|
|
8696
8809
|
rebase build web Build only the "web" static app
|
|
8697
8810
|
`.trim());
|
|
8698
8811
|
}
|
|
8812
|
+
/**
|
|
8813
|
+
* The two commands that turn a custom-runtime app into an image.
|
|
8814
|
+
*
|
|
8815
|
+
* Exported for the test, because the failure this replaced was in the printed
|
|
8816
|
+
* text and nowhere else. It said:
|
|
8817
|
+
*
|
|
8818
|
+
* docker build -f backend/Dockerfile .
|
|
8819
|
+
*
|
|
8820
|
+
* for every custom backend, and the `.` was a guess — `context` was validated,
|
|
8821
|
+
* stored on the config, and read by nothing. For the reference project that
|
|
8822
|
+
* guess is wrong: `app/backend/Dockerfile` opens by copying `pnpm-lock.yaml`
|
|
8823
|
+
* and `pnpm-workspace.yaml`, which live at the monorepo root, so the command
|
|
8824
|
+
* `rebase build` handed you died on its first instruction. The deploy that
|
|
8825
|
+
* actually works, `infra/cloudbuild.yaml`, has always said
|
|
8826
|
+
* `-f app/backend/Dockerfile .` from the root.
|
|
8827
|
+
*
|
|
8828
|
+
* `dockerfile` is relative to `rebase.json`; `context` is too, and may point
|
|
8829
|
+
* above it. Docker resolves `-f` against the working directory, not the
|
|
8830
|
+
* context, so the path has to be re-expressed against wherever the command
|
|
8831
|
+
* runs — which is what the old line never did and is the whole reason it
|
|
8832
|
+
* could not be right for both.
|
|
8833
|
+
*/
|
|
8834
|
+
function dockerBuildHint(projectRoot, name, app) {
|
|
8835
|
+
const dockerfile = app.dockerfile ?? "Dockerfile";
|
|
8836
|
+
const context = app.context ?? ".";
|
|
8837
|
+
const contextDir = path.resolve(projectRoot, context);
|
|
8838
|
+
const fromContext = path.relative(contextDir, path.resolve(projectRoot, dockerfile));
|
|
8839
|
+
const build = `${chalk.cyan(`npm run build --workspace ${name}`)}`;
|
|
8840
|
+
if (fromContext.startsWith("..")) return [
|
|
8841
|
+
chalk.dim(` ${build}`),
|
|
8842
|
+
chalk.yellow(` ⚠ ${dockerfile} is outside the build context (${context}) — nothing it COPYs is reachable.`),
|
|
8843
|
+
chalk.dim(` Widen "context" in rebase.json, or move the Dockerfile inside it.`)
|
|
8844
|
+
];
|
|
8845
|
+
const docker = chalk.cyan(`docker build -f ${fromContext} .`);
|
|
8846
|
+
return context === "." ? [chalk.dim(` ${build} then ${docker}`)] : [chalk.dim(` ${build}`), chalk.dim(` ${chalk.cyan(`cd ${context}`)} && ${docker}`)];
|
|
8847
|
+
}
|
|
8699
8848
|
async function buildCommand(rawArgs = []) {
|
|
8700
8849
|
if (wantsHelp(rawArgs)) {
|
|
8701
8850
|
printHelp$5();
|
|
@@ -8759,7 +8908,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
8759
8908
|
console.log(chalk.cyan(`▸ ${name}`) + chalk.dim(` (${app.type})`));
|
|
8760
8909
|
if (app.type === "backend" && app.runtime === "custom") {
|
|
8761
8910
|
console.log(chalk.dim(" custom runtime — this project builds its own image, not a bundle"));
|
|
8762
|
-
|
|
8911
|
+
for (const line of dockerBuildHint(projectRoot, name, app)) console.log(line);
|
|
8763
8912
|
console.log("");
|
|
8764
8913
|
continue;
|
|
8765
8914
|
}
|
|
@@ -8951,8 +9100,19 @@ function findCliRoot(from) {
|
|
|
8951
9100
|
}
|
|
8952
9101
|
return null;
|
|
8953
9102
|
}
|
|
8954
|
-
/**
|
|
9103
|
+
/**
|
|
9104
|
+
* The block names the payload may switch on. A typo has to be an error.
|
|
9105
|
+
*
|
|
9106
|
+
* Typed as keys of {@link ProjectShape}, which is what makes that true: `as
|
|
9107
|
+
* const` alone gave these two string literals and tied them to nothing, so a
|
|
9108
|
+
* flag renamed on the interface left this list naming a block that no longer
|
|
9109
|
+
* exists, and the template kept switching on it.
|
|
9110
|
+
*/
|
|
8955
9111
|
var SHAPE_FLAGS = ["collections", "frontend"];
|
|
9112
|
+
/** Narrow a marker name to a flag, having checked it is one. */
|
|
9113
|
+
function isShapeFlag(name) {
|
|
9114
|
+
return SHAPE_FLAGS.includes(name);
|
|
9115
|
+
}
|
|
8956
9116
|
/**
|
|
8957
9117
|
* Render one payload file for this project.
|
|
8958
9118
|
*
|
|
@@ -8976,7 +9136,6 @@ var SHAPE_FLAGS = ["collections", "frontend"];
|
|
|
8976
9136
|
* typechecker would see.
|
|
8977
9137
|
*/
|
|
8978
9138
|
function renderPayload(contents, shape, projectName) {
|
|
8979
|
-
const flags = shape;
|
|
8980
9139
|
const out = [];
|
|
8981
9140
|
let open = null;
|
|
8982
9141
|
for (const line of contents.split("\n")) {
|
|
@@ -8992,10 +9151,10 @@ function renderPayload(contents, shape, projectName) {
|
|
|
8992
9151
|
continue;
|
|
8993
9152
|
}
|
|
8994
9153
|
if (open) throw new Error(`Eject template: {{${kind}${name}}} inside an open ${open.name} block.`);
|
|
8995
|
-
if (!
|
|
9154
|
+
if (!isShapeFlag(name)) throw new Error(`Eject template: unknown block {{${kind}${name}}}.`);
|
|
8996
9155
|
open = {
|
|
8997
9156
|
name,
|
|
8998
|
-
keep: kind === "#" ?
|
|
9157
|
+
keep: kind === "#" ? shape[name] === true : shape[name] !== true
|
|
8999
9158
|
};
|
|
9000
9159
|
}
|
|
9001
9160
|
if (open) throw new Error(`Eject template: {{#${open.name}}} was never closed.`);
|
|
@@ -12111,10 +12270,10 @@ async function linkCommand(rawArgs) {
|
|
|
12111
12270
|
} else {
|
|
12112
12271
|
requireInteractive("a project to link", "--project <slug>");
|
|
12113
12272
|
const org = getContextOrg(url);
|
|
12114
|
-
const projects = (await client.data.collection("projects").find({
|
|
12273
|
+
const projects = cloudRows((await client.data.collection("projects").find({
|
|
12115
12274
|
where: org ? { organization: ["==", org] } : void 0,
|
|
12116
12275
|
limit: 100
|
|
12117
|
-
})).data;
|
|
12276
|
+
})).data);
|
|
12118
12277
|
if (projects.length === 0) fail("No projects found for your account.", `Create one with ${chalk.bold("rebase cloud projects create")}.`, "no_projects");
|
|
12119
12278
|
const { picked } = await inquirer.prompt([{
|
|
12120
12279
|
type: "select",
|
|
@@ -12578,14 +12737,14 @@ async function webhooksCommand(subcommand, rawArgs) {
|
|
|
12578
12737
|
const table = args["--table"] || fail("--table is required.", void 0, "usage");
|
|
12579
12738
|
const url = args["--endpoint"] || fail("--endpoint is required.", "Where the POST goes.", "usage");
|
|
12580
12739
|
const events = (args["--events"] || "insert,update,delete").split(",").map((s) => s.trim());
|
|
12581
|
-
const created = await client.data.collection("webhooks").create({
|
|
12740
|
+
const created = requireCloudRow(await client.data.collection("webhooks").create({
|
|
12582
12741
|
project: projectId,
|
|
12583
12742
|
name,
|
|
12584
12743
|
table,
|
|
12585
12744
|
url,
|
|
12586
12745
|
events,
|
|
12587
12746
|
enabled: true
|
|
12588
|
-
});
|
|
12747
|
+
}), "webhook");
|
|
12589
12748
|
success(`Created webhook ${chalk.bold(name)} [${created.id}]`);
|
|
12590
12749
|
emit(() => {}, {
|
|
12591
12750
|
success: true,
|
|
@@ -12678,15 +12837,22 @@ async function storageCommand(action, rawArgs) {
|
|
|
12678
12837
|
return;
|
|
12679
12838
|
}
|
|
12680
12839
|
for (const s of stores) {
|
|
12681
|
-
|
|
12682
|
-
|
|
12840
|
+
const name = s.bucketName || s.s3Bucket || "(no bucket)";
|
|
12841
|
+
const key = s.sourceKey || DEFAULT_STORAGE_SOURCE_KEY;
|
|
12842
|
+
console.log(` ${chalk.bold(name)} ${chalk.gray(`[${s.id}]`)} ${colorStatus(s.status)}`);
|
|
12843
|
+
keyValues([
|
|
12844
|
+
["Source", key],
|
|
12845
|
+
["Provider", s.provider],
|
|
12846
|
+
["Type", s.type]
|
|
12847
|
+
]);
|
|
12683
12848
|
}
|
|
12684
12849
|
console.log("");
|
|
12685
12850
|
}, {
|
|
12686
12851
|
projectId,
|
|
12687
12852
|
stores: stores.map((s) => ({
|
|
12688
12853
|
id: String(s.id),
|
|
12689
|
-
|
|
12854
|
+
sourceKey: s.sourceKey || DEFAULT_STORAGE_SOURCE_KEY,
|
|
12855
|
+
bucketName: s.bucketName || s.s3Bucket || null,
|
|
12690
12856
|
type: s.type ?? null,
|
|
12691
12857
|
provider: s.provider ?? null,
|
|
12692
12858
|
status: s.status ?? null
|
|
@@ -12718,7 +12884,8 @@ function printStorageHelp() {
|
|
|
12718
12884
|
["--secret-access-key <s>", "Secret access key. Required"],
|
|
12719
12885
|
["--endpoint <url>", "S3 endpoint. Omit for AWS"],
|
|
12720
12886
|
["--region <region>", "Region"],
|
|
12721
|
-
["--force-path-style", "Required by MinIO and some gateways"]
|
|
12887
|
+
["--force-path-style", "Required by MinIO and some gateways"],
|
|
12888
|
+
["--source <key>", "Which declared bucket. Default: the default one"]
|
|
12722
12889
|
]
|
|
12723
12890
|
}
|
|
12724
12891
|
],
|
|
@@ -12743,11 +12910,10 @@ async function storageCreateCommand(rawArgs) {
|
|
|
12743
12910
|
try {
|
|
12744
12911
|
noteBlank();
|
|
12745
12912
|
note(chalk.gray("Provisioning managed storage — this creates a bucket and its credentials..."));
|
|
12746
|
-
const
|
|
12913
|
+
const info = (await client.functions.invoke("storage-provision", void 0, {
|
|
12747
12914
|
method: "POST",
|
|
12748
12915
|
path: projectId
|
|
12749
|
-
});
|
|
12750
|
-
const info = res.data ?? res.data;
|
|
12916
|
+
})).data;
|
|
12751
12917
|
success(`Managed storage provisioned for ${displayProjectRef(rawArgs)}.`);
|
|
12752
12918
|
emit(() => {
|
|
12753
12919
|
keyValues([
|
|
@@ -12782,7 +12948,8 @@ async function storageAttachCommand(rawArgs) {
|
|
|
12782
12948
|
"--secret-access-key": String,
|
|
12783
12949
|
"--endpoint": String,
|
|
12784
12950
|
"--region": String,
|
|
12785
|
-
"--force-path-style": Boolean
|
|
12951
|
+
"--force-path-style": Boolean,
|
|
12952
|
+
"--source": String
|
|
12786
12953
|
},
|
|
12787
12954
|
rawArgs,
|
|
12788
12955
|
commandWords: 3,
|
|
@@ -12800,13 +12967,18 @@ async function storageAttachCommand(rawArgs) {
|
|
|
12800
12967
|
if (missing.length > 0) fail(`Missing ${missing.join(", ")}.`, "A bucket without credentials cannot be used, and would be stored as though it could. Run `rebase cloud storage --help` for the full list.", "usage");
|
|
12801
12968
|
const { client } = await requireClient(rawArgs);
|
|
12802
12969
|
const projectId = await requireProject(rawArgs, client);
|
|
12970
|
+
const sourceKey = (parsed["--source"] ?? "").trim() || DEFAULT_STORAGE_SOURCE_KEY;
|
|
12803
12971
|
try {
|
|
12804
12972
|
const existing = (await client.data.collection("storages").find({
|
|
12805
12973
|
where: { project: ["==", projectId] },
|
|
12806
|
-
limit:
|
|
12807
|
-
})).data
|
|
12974
|
+
limit: 50
|
|
12975
|
+
})).data.find((r) => {
|
|
12976
|
+
const key = r.sourceKey;
|
|
12977
|
+
return (typeof key === "string" && key ? key : DEFAULT_STORAGE_SOURCE_KEY) === sourceKey;
|
|
12978
|
+
});
|
|
12808
12979
|
const row = {
|
|
12809
12980
|
project: projectId,
|
|
12981
|
+
sourceKey,
|
|
12810
12982
|
type: "byos",
|
|
12811
12983
|
status: "active",
|
|
12812
12984
|
s3Bucket: bucket,
|
|
@@ -13137,8 +13309,7 @@ async function billingCommand(rawArgs) {
|
|
|
13137
13309
|
}
|
|
13138
13310
|
if (!org) fail("No active organization.", "Run `rebase cloud use` first.", "no_org");
|
|
13139
13311
|
try {
|
|
13140
|
-
const
|
|
13141
|
-
const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
|
|
13312
|
+
const billingId = billingAccountIdOf(await client.data.collection("organizations").findById(org));
|
|
13142
13313
|
if (!billingId) {
|
|
13143
13314
|
emit(() => {
|
|
13144
13315
|
console.log("");
|
|
@@ -13226,7 +13397,6 @@ var DIAL_FLAGS = {
|
|
|
13226
13397
|
"--replicas": "replicaCount",
|
|
13227
13398
|
"--spot": "preemptible",
|
|
13228
13399
|
"--scale-to-zero": "scaleToZero",
|
|
13229
|
-
"--db-mode": "databaseMode",
|
|
13230
13400
|
"--db-instances": "databaseInstances",
|
|
13231
13401
|
"--db-cpu": "databaseCpu",
|
|
13232
13402
|
"--db-memory": "databaseMemory",
|
|
@@ -13318,7 +13488,6 @@ async function computeCommand(action, rawArgs) {
|
|
|
13318
13488
|
true: "scale to zero",
|
|
13319
13489
|
false: "stay warm"
|
|
13320
13490
|
})],
|
|
13321
|
-
["Database", dialLine(project.databaseMode)],
|
|
13322
13491
|
["Database instances", dialLine(project.databaseInstances)],
|
|
13323
13492
|
["Database CPU", dialLine(project.databaseCpu)],
|
|
13324
13493
|
["Database memory", dialLine(project.databaseMemory)],
|
|
@@ -13345,7 +13514,6 @@ async function computeCommand(action, rawArgs) {
|
|
|
13345
13514
|
replicaCount: project.replicaCount ?? null,
|
|
13346
13515
|
preemptible: project.preemptible ?? null,
|
|
13347
13516
|
scaleToZero: project.scaleToZero ?? null,
|
|
13348
|
-
databaseMode: project.databaseMode ?? null,
|
|
13349
13517
|
databaseInstances: project.databaseInstances ?? null,
|
|
13350
13518
|
databaseCpu: project.databaseCpu ?? null,
|
|
13351
13519
|
databaseMemory: project.databaseMemory ?? null,
|
|
@@ -13427,6 +13595,227 @@ function buildDialPatch(rawArgs, opts) {
|
|
|
13427
13595
|
return { patch };
|
|
13428
13596
|
}
|
|
13429
13597
|
//#endregion
|
|
13598
|
+
//#region src/commands/cloud/db-connect.ts
|
|
13599
|
+
/**
|
|
13600
|
+
* `rebase cloud db connect` — a local port that is your cloud database.
|
|
13601
|
+
*
|
|
13602
|
+
* ## What this replaces
|
|
13603
|
+
*
|
|
13604
|
+
* A managed database lives in a namespace of the platform's cluster, and its
|
|
13605
|
+
* address (`postgres-rw.rebase-tenant-….svc.cluster.local`) resolves to nothing
|
|
13606
|
+
* on a developer's machine. The console used to bridge that gap by printing
|
|
13607
|
+
*
|
|
13608
|
+
* kubectl port-forward svc/postgres-rw -n rebase-tenant-… 5432:5432
|
|
13609
|
+
*
|
|
13610
|
+
* which nobody outside the platform can run: a tenant of Rebase Cloud has no
|
|
13611
|
+
* kubeconfig for our cluster, and there is no product that sells one. So the
|
|
13612
|
+
* platform reaches into the cluster instead, and this command is the local end
|
|
13613
|
+
* of that reach.
|
|
13614
|
+
*
|
|
13615
|
+
* ## How it works
|
|
13616
|
+
*
|
|
13617
|
+
* A listener on 127.0.0.1. Every TCP connection it accepts opens its own
|
|
13618
|
+
* WebSocket to the control plane, authenticates in-band with the console session
|
|
13619
|
+
* this CLI already holds, and from `ready` onwards the two are a byte pipe. The
|
|
13620
|
+
* database still asks for a password — the tunnel is a network path, not a
|
|
13621
|
+
* credential — so `psql` behaves exactly as it would against a local Postgres.
|
|
13622
|
+
*
|
|
13623
|
+
* One WebSocket per connection rather than one multiplexed socket: `psql` is one
|
|
13624
|
+
* connection and a pool is a handful, and per-connection sockets keep the
|
|
13625
|
+
* framing at "these bytes are those bytes" with no stream ids to get wrong.
|
|
13626
|
+
*
|
|
13627
|
+
* Node's global `WebSocket` is used rather than `ws`, which is why the token
|
|
13628
|
+
* goes in the first frame instead of an `Authorization` header — the WHATWG
|
|
13629
|
+
* client cannot set request headers, and a header-only endpoint would be
|
|
13630
|
+
* unreachable from a browser too.
|
|
13631
|
+
*/
|
|
13632
|
+
/** Documented in `action-help.ts`, and paired with it by `action-help.test.ts`. */
|
|
13633
|
+
var DB_CONNECT_FLAGS = {
|
|
13634
|
+
"--port": Number,
|
|
13635
|
+
"--reveal": Boolean
|
|
13636
|
+
};
|
|
13637
|
+
/** The default, because it is what every Postgres client assumes. */
|
|
13638
|
+
var DEFAULT_LOCAL_PORT = 5432;
|
|
13639
|
+
/** `https://app.rebase.pro` → `wss://app.rebase.pro/api/db-tunnel/p1`. */
|
|
13640
|
+
function tunnelUrl(cloudUrl, projectId) {
|
|
13641
|
+
const url = new URL(cloudUrl);
|
|
13642
|
+
url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
|
|
13643
|
+
url.pathname = `/api/db-tunnel/${encodeURIComponent(projectId)}`;
|
|
13644
|
+
url.search = "";
|
|
13645
|
+
return url.toString();
|
|
13646
|
+
}
|
|
13647
|
+
/**
|
|
13648
|
+
* A local DSN for the tunnel, with the password only if the caller asked.
|
|
13649
|
+
*
|
|
13650
|
+
* Built here and nowhere else. The server reports the *cluster's* URI, which is
|
|
13651
|
+
* the one thing that must not be printed as the way to connect — it is exactly
|
|
13652
|
+
* the address that does not work from here.
|
|
13653
|
+
*/
|
|
13654
|
+
function localDsn(opts) {
|
|
13655
|
+
const user = opts.username ? encodeURIComponent(opts.username) : "postgres";
|
|
13656
|
+
const auth = opts.password ? `${user}:${encodeURIComponent(opts.password)}` : user;
|
|
13657
|
+
const database = opts.database ?? "rebase";
|
|
13658
|
+
return `postgresql://${auth}@127.0.0.1:${opts.port}/${database}`;
|
|
13659
|
+
}
|
|
13660
|
+
async function dbConnect(rawArgs) {
|
|
13661
|
+
const { flags: args } = parseCloudArgs({
|
|
13662
|
+
spec: DB_CONNECT_FLAGS,
|
|
13663
|
+
rawArgs,
|
|
13664
|
+
commandWords: 3,
|
|
13665
|
+
command: "cloud db connect",
|
|
13666
|
+
maxPositionals: 0
|
|
13667
|
+
});
|
|
13668
|
+
const { client, url } = await requireClient(rawArgs);
|
|
13669
|
+
const projectId = await requireProject(rawArgs, client);
|
|
13670
|
+
const projectRef = displayProjectRef(rawArgs);
|
|
13671
|
+
let info;
|
|
13672
|
+
try {
|
|
13673
|
+
info = await client.functions.invoke("db-info", void 0, {
|
|
13674
|
+
method: "GET",
|
|
13675
|
+
path: projectId
|
|
13676
|
+
});
|
|
13677
|
+
} catch (e) {
|
|
13678
|
+
return reportError(e, "Failed to load database info");
|
|
13679
|
+
}
|
|
13680
|
+
if (info.type === "byodb") fail("This project uses your own database.", "Connect to it directly — there is nothing for the platform to tunnel.", "byodb");
|
|
13681
|
+
if (!info.directAccess) fail("The platform could not resolve this project's database, so it cannot open a path to it.", info.unavailableReason ?? "A managed database is provisioned at the project's first deploy; before then there is nothing to connect to.", "db_unavailable");
|
|
13682
|
+
let password;
|
|
13683
|
+
if (args["--reveal"]) {
|
|
13684
|
+
if (!info.passwordAvailable) fail("No password is available to reveal for this database.", info.unavailableReason ?? void 0, "password_unavailable");
|
|
13685
|
+
try {
|
|
13686
|
+
password = (await client.functions.invoke("db-info", { projectId }, { path: "reveal" })).password;
|
|
13687
|
+
} catch (e) {
|
|
13688
|
+
return reportError(e, "Failed to reveal the database password");
|
|
13689
|
+
}
|
|
13690
|
+
}
|
|
13691
|
+
const requested = args["--port"] ?? DEFAULT_LOCAL_PORT;
|
|
13692
|
+
if (!Number.isInteger(requested) || requested < 0 || requested > 65535) fail(`--port must be a port number, not ${requested}.`, void 0, "bad_port");
|
|
13693
|
+
const endpoint = tunnelUrl(url, projectId);
|
|
13694
|
+
const server = net$1.createServer((socket) => {
|
|
13695
|
+
pipeThroughTunnel(socket, endpoint, client.auth.getSession()?.accessToken ?? "");
|
|
13696
|
+
});
|
|
13697
|
+
server.on("error", (err) => {
|
|
13698
|
+
if (err.code === "EADDRINUSE") fail(`Port ${requested} on 127.0.0.1 is already in use.`, "Pass --port to choose another, or stop whatever is listening there.", "port_in_use");
|
|
13699
|
+
fail(`Could not open a local listener: ${err.message}`, void 0, "listen_failed");
|
|
13700
|
+
});
|
|
13701
|
+
await new Promise((resolve) => {
|
|
13702
|
+
server.listen(requested, "127.0.0.1", () => resolve());
|
|
13703
|
+
});
|
|
13704
|
+
const address = server.address();
|
|
13705
|
+
const port = address && typeof address !== "string" ? address.port : requested;
|
|
13706
|
+
const dsn = localDsn({
|
|
13707
|
+
port,
|
|
13708
|
+
username: info.username,
|
|
13709
|
+
database: info.database,
|
|
13710
|
+
password
|
|
13711
|
+
});
|
|
13712
|
+
emit(() => {
|
|
13713
|
+
console.log("");
|
|
13714
|
+
console.log(chalk.bold(` 🔌 Tunnel open — project ${projectRef}`));
|
|
13715
|
+
console.log("");
|
|
13716
|
+
console.log(` ${chalk.green(dsn)}`);
|
|
13717
|
+
console.log("");
|
|
13718
|
+
if (!password) {
|
|
13719
|
+
console.log(chalk.gray(" The password is hidden. Re-run with --reveal to print it in the URL,"));
|
|
13720
|
+
console.log(chalk.gray(" or read it with `rebase cloud db info --reveal`."));
|
|
13721
|
+
console.log("");
|
|
13722
|
+
}
|
|
13723
|
+
console.log(chalk.gray(" Leave this running, and point any Postgres client at it."));
|
|
13724
|
+
console.log(chalk.gray(" Ctrl-C closes the tunnel."));
|
|
13725
|
+
console.log("");
|
|
13726
|
+
}, {
|
|
13727
|
+
projectId,
|
|
13728
|
+
host: "127.0.0.1",
|
|
13729
|
+
port,
|
|
13730
|
+
database: info.database,
|
|
13731
|
+
username: info.username,
|
|
13732
|
+
connectionString: dsn,
|
|
13733
|
+
...password ? { password } : {}
|
|
13734
|
+
});
|
|
13735
|
+
await new Promise((resolve) => {
|
|
13736
|
+
process.once("SIGINT", () => {
|
|
13737
|
+
noteBlank();
|
|
13738
|
+
note(chalk.gray("Tunnel closed."));
|
|
13739
|
+
server.close();
|
|
13740
|
+
resolve();
|
|
13741
|
+
});
|
|
13742
|
+
});
|
|
13743
|
+
}
|
|
13744
|
+
/**
|
|
13745
|
+
* One accepted connection, carried over one WebSocket.
|
|
13746
|
+
*
|
|
13747
|
+
* The local socket is paused until the tunnel says `ready`, so a client that
|
|
13748
|
+
* sends its startup packet the instant it connects — which every Postgres
|
|
13749
|
+
* client does — cannot have those bytes arrive before there is a database to
|
|
13750
|
+
* send them to.
|
|
13751
|
+
*
|
|
13752
|
+
* Exported for `db-connect.pipe.test.ts`, which drives it with a real socket
|
|
13753
|
+
* against a real WebSocket: this is the half a developer's client actually
|
|
13754
|
+
* talks to, and a pipe is the one component whose bugs are silent — a dropped
|
|
13755
|
+
* or reordered chunk does not throw, it corrupts a Postgres message and
|
|
13756
|
+
* surfaces as a protocol error nowhere near here.
|
|
13757
|
+
*/
|
|
13758
|
+
function pipeThroughTunnel(socket, endpoint, token) {
|
|
13759
|
+
socket.pause();
|
|
13760
|
+
socket.setNoDelay(true);
|
|
13761
|
+
const ws = new WebSocket(endpoint);
|
|
13762
|
+
ws.binaryType = "arraybuffer";
|
|
13763
|
+
let ready = false;
|
|
13764
|
+
const closeBoth = (reason) => {
|
|
13765
|
+
if (reason && !ready) note(chalk.red(`✗ ${reason}`));
|
|
13766
|
+
try {
|
|
13767
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
|
|
13768
|
+
} catch {}
|
|
13769
|
+
socket.destroy();
|
|
13770
|
+
};
|
|
13771
|
+
ws.onopen = () => {
|
|
13772
|
+
ws.send(JSON.stringify({
|
|
13773
|
+
type: "authenticate",
|
|
13774
|
+
token
|
|
13775
|
+
}));
|
|
13776
|
+
};
|
|
13777
|
+
ws.onmessage = (event) => {
|
|
13778
|
+
if (typeof event.data === "string") {
|
|
13779
|
+
let frame = null;
|
|
13780
|
+
try {
|
|
13781
|
+
frame = JSON.parse(event.data);
|
|
13782
|
+
} catch {
|
|
13783
|
+
return closeBoth(`the tunnel endpoint answered with ${event.data.slice(0, 80)}`);
|
|
13784
|
+
}
|
|
13785
|
+
if (frame.type === "ready") {
|
|
13786
|
+
ready = true;
|
|
13787
|
+
socket.resume();
|
|
13788
|
+
return;
|
|
13789
|
+
}
|
|
13790
|
+
const refusal = frame.type === "error" ? frame : null;
|
|
13791
|
+
return closeBoth(refusal?.message ?? `the tunnel was refused (${refusal?.code ?? "unknown"})`);
|
|
13792
|
+
}
|
|
13793
|
+
socket.write(Buffer.from(event.data));
|
|
13794
|
+
};
|
|
13795
|
+
ws.onerror = () => {
|
|
13796
|
+
closeBoth("could not reach the control plane's tunnel endpoint");
|
|
13797
|
+
};
|
|
13798
|
+
ws.onclose = () => {
|
|
13799
|
+
socket.destroy();
|
|
13800
|
+
};
|
|
13801
|
+
socket.on("data", (chunk) => {
|
|
13802
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
13803
|
+
ws.send(new Uint8Array(chunk));
|
|
13804
|
+
if (ws.bufferedAmount > 1024 * 1024) {
|
|
13805
|
+
socket.pause();
|
|
13806
|
+
const drain = setInterval(() => {
|
|
13807
|
+
if (ws.readyState !== WebSocket.OPEN) return clearInterval(drain);
|
|
13808
|
+
if (ws.bufferedAmount < 256 * 1024) {
|
|
13809
|
+
clearInterval(drain);
|
|
13810
|
+
socket.resume();
|
|
13811
|
+
}
|
|
13812
|
+
}, 5);
|
|
13813
|
+
}
|
|
13814
|
+
});
|
|
13815
|
+
socket.on("close", () => closeBoth());
|
|
13816
|
+
socket.on("error", () => closeBoth());
|
|
13817
|
+
}
|
|
13818
|
+
//#endregion
|
|
13430
13819
|
//#region src/commands/cloud/databases.ts
|
|
13431
13820
|
async function dbCommand(subcommand, rawArgs) {
|
|
13432
13821
|
switch (subcommand) {
|
|
@@ -13440,6 +13829,9 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
13440
13829
|
case "info":
|
|
13441
13830
|
await dbInfo(rawArgs);
|
|
13442
13831
|
break;
|
|
13832
|
+
case "connect":
|
|
13833
|
+
await dbConnect(rawArgs);
|
|
13834
|
+
break;
|
|
13443
13835
|
case "test":
|
|
13444
13836
|
await testDatabase(rawArgs);
|
|
13445
13837
|
break;
|
|
@@ -13467,10 +13859,10 @@ async function listDatabases(rawArgs) {
|
|
|
13467
13859
|
const projectId = await requireProject(rawArgs, client);
|
|
13468
13860
|
const projectRef = displayProjectRef(rawArgs);
|
|
13469
13861
|
try {
|
|
13470
|
-
const dbs = (await client.data.collection("databases").find({
|
|
13862
|
+
const dbs = cloudRows((await client.data.collection("databases").find({
|
|
13471
13863
|
where: { project: ["==", projectId] },
|
|
13472
13864
|
limit: 50
|
|
13473
|
-
})).data;
|
|
13865
|
+
})).data);
|
|
13474
13866
|
emit(() => {
|
|
13475
13867
|
console.log("");
|
|
13476
13868
|
console.log(chalk.bold(` 🗄 Databases — project ${projectRef}`));
|
|
@@ -13507,10 +13899,10 @@ async function listDatabases(rawArgs) {
|
|
|
13507
13899
|
* point, since the answer decides which row a deploy will actually use.
|
|
13508
13900
|
*/
|
|
13509
13901
|
async function firstAttachedDatabase(client, projectId) {
|
|
13510
|
-
return (await client.data.collection("databases").find({
|
|
13902
|
+
return cloudRows((await client.data.collection("databases").find({
|
|
13511
13903
|
where: { project: ["==", projectId] },
|
|
13512
13904
|
limit: 1
|
|
13513
|
-
})).data[0];
|
|
13905
|
+
})).data)[0];
|
|
13514
13906
|
}
|
|
13515
13907
|
/**
|
|
13516
13908
|
* Attach a database row to a project.
|
|
@@ -13521,12 +13913,12 @@ async function firstAttachedDatabase(client, projectId) {
|
|
|
13521
13913
|
* afterwards".
|
|
13522
13914
|
*/
|
|
13523
13915
|
async function attachDatabaseRow(client, input) {
|
|
13524
|
-
return await client.data.collection("databases").create({
|
|
13916
|
+
return requireCloudRow(await client.data.collection("databases").create({
|
|
13525
13917
|
project: input.projectId,
|
|
13526
13918
|
type: input.type,
|
|
13527
13919
|
connectionString: input.type === "byodb" ? input.connectionString : void 0,
|
|
13528
13920
|
connectionStatus: "untested"
|
|
13529
|
-
});
|
|
13921
|
+
}), "database");
|
|
13530
13922
|
}
|
|
13531
13923
|
/** What `rebase cloud db create` parses. Exported so its help page cannot drift. */
|
|
13532
13924
|
var CREATE_DATABASE_FLAGS = {
|
|
@@ -13713,10 +14105,19 @@ async function dbInfo(rawArgs) {
|
|
|
13713
14105
|
["Connection", connectionString]
|
|
13714
14106
|
]);
|
|
13715
14107
|
if (info.unavailableReason) console.log(chalk.gray(` ${info.unavailableReason}`));
|
|
13716
|
-
if (info.
|
|
13717
|
-
|
|
14108
|
+
if (info.directAccess) {
|
|
14109
|
+
console.log("");
|
|
14110
|
+
console.log(chalk.gray(" The host above is inside the platform's cluster: it is the address your"));
|
|
14111
|
+
console.log(chalk.gray(" deployed backend uses, and it resolves to nothing from here. To reach this"));
|
|
14112
|
+
console.log(chalk.gray(" database from this machine, open a tunnel:"));
|
|
13718
14113
|
console.log("");
|
|
13719
|
-
console.log(
|
|
14114
|
+
console.log(` ${chalk.cyan("rebase cloud db connect")}`);
|
|
14115
|
+
const pf = info.directAccess.kubectl;
|
|
14116
|
+
if (pf) {
|
|
14117
|
+
console.log("");
|
|
14118
|
+
console.log(chalk.gray(" Or, on your own cluster:"));
|
|
14119
|
+
console.log(chalk.gray(` kubectl -n ${pf.namespace} port-forward svc/${pf.service} ${pf.localPort}:${pf.remotePort}`));
|
|
14120
|
+
}
|
|
13720
14121
|
}
|
|
13721
14122
|
console.log("");
|
|
13722
14123
|
}, {
|
|
@@ -13727,7 +14128,7 @@ async function dbInfo(rawArgs) {
|
|
|
13727
14128
|
database: info.database,
|
|
13728
14129
|
username: info.username,
|
|
13729
14130
|
passwordAvailable: info.passwordAvailable,
|
|
13730
|
-
|
|
14131
|
+
directAccess: info.directAccess,
|
|
13731
14132
|
unavailableReason: info.unavailableReason,
|
|
13732
14133
|
...args["--reveal"] ? {
|
|
13733
14134
|
password,
|
|
@@ -14013,7 +14414,13 @@ function printDbHelp() {
|
|
|
14013
14414
|
action: "info",
|
|
14014
14415
|
section: "Database",
|
|
14015
14416
|
description: "Connection details",
|
|
14016
|
-
flags: [["--reveal", "Include the password. Without it, the value is masked"]]
|
|
14417
|
+
flags: [["--reveal", "Include the password (owner or admin). Without it, the value is masked"]]
|
|
14418
|
+
},
|
|
14419
|
+
{
|
|
14420
|
+
action: "connect",
|
|
14421
|
+
section: "Database",
|
|
14422
|
+
description: "Open a local port that IS the project's database",
|
|
14423
|
+
flags: [["--port <n>", "Local port to listen on. Default: 5432"], ["--reveal", "Print the password in the connection URL"]]
|
|
14017
14424
|
},
|
|
14018
14425
|
{
|
|
14019
14426
|
action: "test",
|
|
@@ -14073,7 +14480,9 @@ function printDbHelp() {
|
|
|
14073
14480
|
"A project has exactly one database: `create` refuses rather than attaching a second, because",
|
|
14074
14481
|
"which of two rows a deploy uses is undefined.",
|
|
14075
14482
|
"A managed database is provisioned at the project's FIRST DEPLOY, so `test` failing before then",
|
|
14076
|
-
"is not a fault."
|
|
14483
|
+
"is not a fault.",
|
|
14484
|
+
"The host `info` reports is inside the platform's cluster — your backend's address for it, not",
|
|
14485
|
+
"one your laptop can resolve. `connect` is what makes it reachable from here."
|
|
14077
14486
|
]
|
|
14078
14487
|
});
|
|
14079
14488
|
}
|
|
@@ -14097,7 +14506,7 @@ async function listProjects(rawArgs) {
|
|
|
14097
14506
|
where: org ? { organization: ["==", org] } : void 0,
|
|
14098
14507
|
orderBy: ["name", "asc"],
|
|
14099
14508
|
limit: 100
|
|
14100
|
-
}).then((res) => res.data), fetchTenantBaseDomain(client, url)]);
|
|
14509
|
+
}).then((res) => cloudRows(res.data)), fetchTenantBaseDomain(client, url)]);
|
|
14101
14510
|
const linkedId = readLink()?.projectId;
|
|
14102
14511
|
emit(() => {
|
|
14103
14512
|
console.log("");
|
|
@@ -14216,8 +14625,8 @@ var CREATE_PROJECT_FLAGS = {
|
|
|
14216
14625
|
* means the two-command sequence that every project needs is one command,
|
|
14217
14626
|
* and `--db none` is there for the case that genuinely wants to decide later.
|
|
14218
14627
|
*
|
|
14219
|
-
* Distinct from `--db-
|
|
14220
|
-
* on a database that exists. This is whether there is one.
|
|
14628
|
+
* Distinct from `--db-cpu`/`--db-instances` next to it, which are resource
|
|
14629
|
+
* dials on a database that exists. This is whether there is one.
|
|
14221
14630
|
*/
|
|
14222
14631
|
"--db": String,
|
|
14223
14632
|
/** For `--db byodb`. Same spelling as `rebase cloud db create` uses. */
|
|
@@ -14228,7 +14637,6 @@ var CREATE_PROJECT_FLAGS = {
|
|
|
14228
14637
|
"--replicas": String,
|
|
14229
14638
|
"--spot": String,
|
|
14230
14639
|
"--scale-to-zero": String,
|
|
14231
|
-
"--db-mode": String,
|
|
14232
14640
|
"--db-instances": String,
|
|
14233
14641
|
"--db-cpu": String,
|
|
14234
14642
|
"--db-memory": String,
|
|
@@ -14282,7 +14690,7 @@ async function createProject(rawArgs) {
|
|
|
14282
14690
|
try {
|
|
14283
14691
|
const user = await client.auth.getUser();
|
|
14284
14692
|
if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.", "session_invalid");
|
|
14285
|
-
const created = await client.data.collection("projects").create({
|
|
14693
|
+
const created = requireCloudRow(await client.data.collection("projects").create({
|
|
14286
14694
|
name,
|
|
14287
14695
|
subdomain,
|
|
14288
14696
|
gitRepoUrl,
|
|
@@ -14293,7 +14701,7 @@ async function createProject(rawArgs) {
|
|
|
14293
14701
|
organization: org,
|
|
14294
14702
|
createdById: user.uid,
|
|
14295
14703
|
status: "provisioning"
|
|
14296
|
-
});
|
|
14704
|
+
}), "project");
|
|
14297
14705
|
const host = projectHost(created, await fetchTenantBaseDomain(client, url));
|
|
14298
14706
|
const linked = Boolean(args["--link"]);
|
|
14299
14707
|
if (linked) writeLink({
|
|
@@ -14603,12 +15011,63 @@ function packBundle(bundleDir, outPath) {
|
|
|
14603
15011
|
});
|
|
14604
15012
|
}
|
|
14605
15013
|
/**
|
|
14606
|
-
*
|
|
15014
|
+
* The commit HEAD is on, read here because here is the only place it exists.
|
|
15015
|
+
*
|
|
15016
|
+
* A bundle deploy has no repository anywhere near the control plane: the CLI
|
|
15017
|
+
* builds a tarball and uploads it, so the three paths `deploy.ts` documents for
|
|
15018
|
+
* learning a commit — clone, `ls-remote`, or "there is no repo at all" — all
|
|
15019
|
+
* resolve to the third. Every bundle deployment therefore recorded an empty
|
|
15020
|
+
* hash, which is 291 of the 305 rows in production: a Deployments list where
|
|
15021
|
+
* almost nothing says what it shipped.
|
|
15022
|
+
*
|
|
15023
|
+
* But the CLI is standing IN the repository. `git -C <dir> log -1` answers
|
|
15024
|
+
* exactly, message included — the one thing even the git-build path cannot get
|
|
15025
|
+
* from `ls-remote`.
|
|
14607
15026
|
*
|
|
14608
|
-
*
|
|
14609
|
-
*
|
|
14610
|
-
*
|
|
15027
|
+
* Returns null rather than guessing, for every reason it can fail: no git, not a
|
|
15028
|
+
* repository, no commits yet. The server records what it is given and nothing
|
|
15029
|
+
* more, so null here stays `UNKNOWN_COMMIT_HASH` there.
|
|
15030
|
+
*
|
|
15031
|
+
* A dirty tree is NOT reported as a different commit. The bundle may contain
|
|
15032
|
+
* uncommitted work, and the honest statement about that is "built from a tree at
|
|
15033
|
+
* <hash>", not a fabricated identifier — the same rule the rest of this file
|
|
15034
|
+
* follows about inventing values.
|
|
14611
15035
|
*/
|
|
15036
|
+
function bundleCommit(cwd, run = gitIn(cwd)) {
|
|
15037
|
+
try {
|
|
15038
|
+
const hash = run([
|
|
15039
|
+
"rev-parse",
|
|
15040
|
+
"--short=7",
|
|
15041
|
+
"HEAD"
|
|
15042
|
+
]).trim();
|
|
15043
|
+
if (!/^[0-9a-f]{7,40}$/.test(hash)) return null;
|
|
15044
|
+
return {
|
|
15045
|
+
hash,
|
|
15046
|
+
message: run([
|
|
15047
|
+
"log",
|
|
15048
|
+
"-1",
|
|
15049
|
+
"--pretty=%s"
|
|
15050
|
+
]).trim()
|
|
15051
|
+
};
|
|
15052
|
+
} catch {
|
|
15053
|
+
return null;
|
|
15054
|
+
}
|
|
15055
|
+
}
|
|
15056
|
+
/** `git -C <cwd> …`, as a function, so `bundleCommit` is testable without a repo. */
|
|
15057
|
+
function gitIn(cwd) {
|
|
15058
|
+
return (args) => execFileSync("git", [
|
|
15059
|
+
"-C",
|
|
15060
|
+
cwd,
|
|
15061
|
+
...args
|
|
15062
|
+
], {
|
|
15063
|
+
encoding: "utf8",
|
|
15064
|
+
stdio: [
|
|
15065
|
+
"ignore",
|
|
15066
|
+
"pipe",
|
|
15067
|
+
"ignore"
|
|
15068
|
+
]
|
|
15069
|
+
});
|
|
15070
|
+
}
|
|
14612
15071
|
function bundleDeployBody(input) {
|
|
14613
15072
|
return {
|
|
14614
15073
|
projectId: input.projectId,
|
|
@@ -14618,7 +15077,11 @@ function bundleDeployBody(input) {
|
|
|
14618
15077
|
client: "cli",
|
|
14619
15078
|
frameworkVersion: input.manifest.runtime?.builtAgainst,
|
|
14620
15079
|
...input.declaredApps?.length ? { declaredApps: input.declaredApps } : {},
|
|
14621
|
-
...input.message ? { message: input.message } : {}
|
|
15080
|
+
...input.message ? { message: input.message } : {},
|
|
15081
|
+
...input.commit ? {
|
|
15082
|
+
gitCommitHash: input.commit.hash,
|
|
15083
|
+
gitCommitMessage: input.commit.message
|
|
15084
|
+
} : {}
|
|
14622
15085
|
};
|
|
14623
15086
|
}
|
|
14624
15087
|
/**
|
|
@@ -14885,7 +15348,8 @@ async function uploadAndTrigger(opts) {
|
|
|
14885
15348
|
manifest,
|
|
14886
15349
|
app: opts.appName,
|
|
14887
15350
|
message: opts.message,
|
|
14888
|
-
declaredApps
|
|
15351
|
+
declaredApps,
|
|
15352
|
+
commit: bundleCommit(process.cwd())
|
|
14889
15353
|
});
|
|
14890
15354
|
let deploymentId;
|
|
14891
15355
|
let managed;
|
|
@@ -15130,8 +15594,7 @@ async function readBillingState(client, projectId) {
|
|
|
15130
15594
|
if (!card) return unknown;
|
|
15131
15595
|
let plan = null;
|
|
15132
15596
|
try {
|
|
15133
|
-
const
|
|
15134
|
-
const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
|
|
15597
|
+
const billingId = billingAccountIdOf(await client.data.collection("organizations").findById(org));
|
|
15135
15598
|
if (billingId != null) {
|
|
15136
15599
|
const acct = await client.data.collection("billing-accounts").findById(billingId);
|
|
15137
15600
|
plan = typeof acct?.plan === "string" ? acct.plan : null;
|
|
@@ -15572,7 +16035,7 @@ async function listOrgs(rawArgs) {
|
|
|
15572
16035
|
});
|
|
15573
16036
|
const { client, url } = await requireClient(rawArgs);
|
|
15574
16037
|
try {
|
|
15575
|
-
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
16038
|
+
const orgs = cloudRows((await client.data.collection("organizations").find({ limit: 100 })).data);
|
|
15576
16039
|
const active = getContextOrg(url);
|
|
15577
16040
|
emit(() => {
|
|
15578
16041
|
console.log("");
|
|
@@ -15632,11 +16095,11 @@ async function createOrg(rawArgs) {
|
|
|
15632
16095
|
if (!name) fail("Organization name is required.", "Pass `--name <name>`.", "input_required");
|
|
15633
16096
|
const slug = (args["--slug"] || slugify(name)).trim();
|
|
15634
16097
|
try {
|
|
15635
|
-
const created = await client.data.collection("organizations").create({
|
|
16098
|
+
const created = requireCloudRow(await client.data.collection("organizations").create({
|
|
15636
16099
|
name,
|
|
15637
16100
|
slug,
|
|
15638
16101
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15639
|
-
});
|
|
16102
|
+
}), "organization");
|
|
15640
16103
|
setContextOrg(url, String(created.id));
|
|
15641
16104
|
success(`Created organization ${chalk.bold(name)} and set it active`);
|
|
15642
16105
|
emit(() => {}, {
|
|
@@ -16817,11 +17280,11 @@ function cancelView(res) {
|
|
|
16817
17280
|
};
|
|
16818
17281
|
}
|
|
16819
17282
|
async function fetchDeployments(client, projectId, limit = 100) {
|
|
16820
|
-
return (await client.data.collection("deployments").find({
|
|
17283
|
+
return cloudRows((await client.data.collection("deployments").find({
|
|
16821
17284
|
where: { project: ["==", projectId] },
|
|
16822
17285
|
orderBy: ["createdAt", "desc"],
|
|
16823
17286
|
limit
|
|
16824
|
-
})).data;
|
|
17287
|
+
})).data);
|
|
16825
17288
|
}
|
|
16826
17289
|
/** Hard ceiling on `--limit`, matching the backend's own page size. */
|
|
16827
17290
|
var MAX_DEPLOYMENTS_LIMIT = 100;
|
|
@@ -17666,9 +18129,10 @@ async function dbDebugCommand(rawArgs) {
|
|
|
17666
18129
|
} catch (e) {
|
|
17667
18130
|
reportError(e, "Failed to read database connection info");
|
|
17668
18131
|
}
|
|
17669
|
-
const
|
|
17670
|
-
const
|
|
17671
|
-
const psqlCmd =
|
|
18132
|
+
const access = info.directAccess;
|
|
18133
|
+
const connectCmd = access ? "rebase cloud db connect" : null;
|
|
18134
|
+
const psqlCmd = access && info.username && info.database ? `psql -h 127.0.0.1 -p 5432 -U ${info.username} -d ${info.database}` : null;
|
|
18135
|
+
const kubectlCmd = access?.kubectl ? `kubectl port-forward -n ${access.kubectl.namespace} svc/${access.kubectl.service} ${access.kubectl.localPort}:${access.kubectl.remotePort}` : null;
|
|
17672
18136
|
emit(() => {
|
|
17673
18137
|
console.log("");
|
|
17674
18138
|
console.log(chalk.bold(` 🐘 Database — ${displayProjectRef(rawArgs)}`));
|
|
@@ -17687,12 +18151,18 @@ async function dbDebugCommand(rawArgs) {
|
|
|
17687
18151
|
["Password", info.passwordAvailable ? chalk.gray("stored — not shown here") : chalk.yellow("none stored")]
|
|
17688
18152
|
]);
|
|
17689
18153
|
console.log("");
|
|
17690
|
-
if (
|
|
17691
|
-
console.log(chalk.gray("
|
|
18154
|
+
if (connectCmd) {
|
|
18155
|
+
console.log(chalk.gray(" That host is inside the platform's cluster and does not resolve here."));
|
|
18156
|
+
console.log(chalk.gray(" To reach the database from this machine:"));
|
|
17692
18157
|
console.log("");
|
|
17693
|
-
console.log(` ${
|
|
18158
|
+
console.log(` ${connectCmd}`);
|
|
17694
18159
|
if (psqlCmd) console.log(` ${psqlCmd}`);
|
|
17695
18160
|
console.log("");
|
|
18161
|
+
if (kubectlCmd) {
|
|
18162
|
+
console.log(chalk.gray(" Or, on your own cluster:"));
|
|
18163
|
+
console.log(` ${kubectlCmd}`);
|
|
18164
|
+
console.log("");
|
|
18165
|
+
}
|
|
17696
18166
|
if (info.passwordAvailable) {
|
|
17697
18167
|
console.log(chalk.gray(" Get the password with: ") + chalk.bold("rebase cloud db info --reveal"));
|
|
17698
18168
|
console.log("");
|
|
@@ -17705,7 +18175,8 @@ async function dbDebugCommand(rawArgs) {
|
|
|
17705
18175
|
database: info.database ?? null,
|
|
17706
18176
|
username: info.username ?? null,
|
|
17707
18177
|
passwordAvailable: Boolean(info.passwordAvailable),
|
|
17708
|
-
|
|
18178
|
+
connectCommand: connectCmd,
|
|
18179
|
+
kubectlCommand: kubectlCmd,
|
|
17709
18180
|
psqlCommand: psqlCmd
|
|
17710
18181
|
});
|
|
17711
18182
|
}
|
|
@@ -18004,7 +18475,6 @@ var ACTION_HELP = {
|
|
|
18004
18475
|
["--replicas <n>", "Instance count"],
|
|
18005
18476
|
["--spot <true|false>", "Run on preemptible capacity"],
|
|
18006
18477
|
["--scale-to-zero <true|false>", "Stop the instances when idle"],
|
|
18007
|
-
["--db-mode <mode>", "Database topology dial"],
|
|
18008
18478
|
["--db-instances <n>", "Database instance count"],
|
|
18009
18479
|
["--db-cpu <n>", "vCPU per database instance"],
|
|
18010
18480
|
["--db-memory <size>", "Memory per database instance"],
|
|
@@ -18033,11 +18503,28 @@ var ACTION_HELP = {
|
|
|
18033
18503
|
],
|
|
18034
18504
|
examples: ["rebase cloud db create --type managed", "rebase cloud db create --type byodb --connection-string \"$DATABASE_URL\" --wait"],
|
|
18035
18505
|
notes: [
|
|
18036
|
-
"A managed database is CloudNativePG
|
|
18506
|
+
"A managed database is a CloudNativePG cluster of the project's own, in the project's own namespace and backed up on its own schedule. It is created at the project's first deploy: there is nothing to poll before then, so --wait says so and returns rather than looping.",
|
|
18037
18507
|
"`rebase cloud db test` legitimately fails before the first deploy.",
|
|
18038
18508
|
"A project has exactly one database — attaching a second is refused, because the platform reads one row and it becomes undefined which it deploys against."
|
|
18039
18509
|
]
|
|
18040
18510
|
},
|
|
18511
|
+
"db connect": {
|
|
18512
|
+
command: "cloud db connect",
|
|
18513
|
+
usage: "cloud db connect [--port <n>] [--reveal]",
|
|
18514
|
+
summary: "Open a local port that is the project's managed database, and hold it open until Ctrl-C. A managed database lives inside the platform's cluster, so the host `db info` reports is your backend's address for it and resolves to nothing on your machine; this is what makes it reachable from here. Point psql, TablePlus, Drizzle Studio or pg_dump at the URL it prints. The database still asks for its password — the tunnel is a network path, not a credential.",
|
|
18515
|
+
flags: [["--port <n>", "Local port to listen on. Default: 5432"], ["--reveal", "Print the password inside the connection URL"]],
|
|
18516
|
+
examples: [
|
|
18517
|
+
"rebase cloud db connect",
|
|
18518
|
+
"rebase cloud db connect --port 6543",
|
|
18519
|
+
"rebase cloud db connect --reveal --project shop"
|
|
18520
|
+
],
|
|
18521
|
+
notes: [
|
|
18522
|
+
"Requires the organization's owner or admin role — the same gate as the console's SQL console, because it is the same capability.",
|
|
18523
|
+
"Piped or with --json it prints one object — host, port, database, username, connectionString — and keeps serving, so a script can read the URL and connect.",
|
|
18524
|
+
"Connections go through the control plane, so they count against the project's own database connection limit like any other client.",
|
|
18525
|
+
"Bring-your-own databases are refused: that host is already yours to reach, and there is nothing for the platform to tunnel."
|
|
18526
|
+
]
|
|
18527
|
+
},
|
|
18041
18528
|
deploy: {
|
|
18042
18529
|
command: "cloud deploy",
|
|
18043
18530
|
usage: "cloud deploy [app] [options]",
|
|
@@ -18139,7 +18626,7 @@ var ACTION_HELP = {
|
|
|
18139
18626
|
"rebase cloud db backup restore base-20260831 --yes",
|
|
18140
18627
|
"rebase cloud db backup download base-20260831"
|
|
18141
18628
|
],
|
|
18142
|
-
notes: ["`restore` replaces the live database. Nothing about it is undoable from here — `--yes`, the global flag listed below, is what skips that confirmation.", "
|
|
18629
|
+
notes: ["`restore` replaces the live database. Nothing about it is undoable from here — `--yes`, the global flag listed below, is what skips that confirmation.", "Backups are the project's own: a base backup on a schedule, plus continuous WAL archiving for point-in-time recovery."]
|
|
18143
18630
|
},
|
|
18144
18631
|
"db pitr": {
|
|
18145
18632
|
command: "cloud db pitr",
|
|
@@ -18214,7 +18701,7 @@ var ACTION_HELP = {
|
|
|
18214
18701
|
},
|
|
18215
18702
|
"storage attach": {
|
|
18216
18703
|
command: "cloud storage attach",
|
|
18217
|
-
usage: "cloud storage attach --bucket <name> --access-key-id <id> --secret-access-key <secret> [--endpoint <url>] [--region <region>] [--force-path-style]",
|
|
18704
|
+
usage: "cloud storage attach --bucket <name> --access-key-id <id> --secret-access-key <secret> [--endpoint <url>] [--region <region>] [--force-path-style] [--source <key>]",
|
|
18218
18705
|
summary: "Point the project at storage you already own — S3, R2, MinIO, any S3-compatible bucket. The alternative to `storage create`, for a bucket the platform does not manage.",
|
|
18219
18706
|
flags: [
|
|
18220
18707
|
["--bucket <name>", "The bucket name. Required"],
|
|
@@ -18222,9 +18709,14 @@ var ACTION_HELP = {
|
|
|
18222
18709
|
["--secret-access-key <secret>", "Required. Stored encrypted and never returned"],
|
|
18223
18710
|
["--endpoint <url>", "For anything that is not AWS S3 — R2, MinIO, Backblaze"],
|
|
18224
18711
|
["--region <region>", "Bucket region. Default: the provider's own default"],
|
|
18225
|
-
["--force-path-style", "Address as endpoint/bucket rather than bucket.endpoint. Needed by MinIO"]
|
|
18712
|
+
["--force-path-style", "Address as endpoint/bucket rather than bucket.endpoint. Needed by MinIO"],
|
|
18713
|
+
["--source <key>", "Which declared bucket to configure. Default: the project's default bucket"]
|
|
18714
|
+
],
|
|
18715
|
+
examples: [
|
|
18716
|
+
"rebase cloud storage attach --bucket assets --access-key-id … --secret-access-key …",
|
|
18717
|
+
"rebase cloud storage attach --source media --bucket media-eu --access-key-id … --secret-access-key …",
|
|
18718
|
+
"rebase cloud storage attach --bucket assets --access-key-id … --secret-access-key … \\\n --endpoint https://<account>.r2.cloudflarestorage.com --region auto"
|
|
18226
18719
|
],
|
|
18227
|
-
examples: ["rebase cloud storage attach --bucket assets --access-key-id AKIA… --secret-access-key …", "rebase cloud storage attach --bucket assets --access-key-id … --secret-access-key … \\\n --endpoint https://<account>.r2.cloudflarestorage.com --region auto"],
|
|
18228
18720
|
notes: ["All three of --bucket, --access-key-id and --secret-access-key, or none: a bucket with no credentials reads as configured and fails on the first upload.", "Redeploy for the tenant to pick the credentials up."]
|
|
18229
18721
|
},
|
|
18230
18722
|
"webhooks create": {
|
|
@@ -18311,7 +18803,6 @@ var ACTION_HELP = {
|
|
|
18311
18803
|
["--replicas <n>", "Instances that always exist — the autoscaler's floor, and what is billed at rest"],
|
|
18312
18804
|
["--spot <true|false>", "Preemptible capacity: cheaper, and restarted without notice"],
|
|
18313
18805
|
["--scale-to-zero <true|false>", "Request-billed compute that stops when idle, at the cost of a cold start"],
|
|
18314
|
-
["--db-mode <shared|dedicated>", "Pooled cluster, or one of this project's own"],
|
|
18315
18806
|
["--db-instances <n>", "1–3. 1 is a single instance with no failover; 2 adds an automatic standby"],
|
|
18316
18807
|
["--db-cpu <n>", "Database CPU request per instance. Default: 500m"],
|
|
18317
18808
|
["--db-memory <size>", "Database memory request per instance. Default: 2Gi"],
|
|
@@ -18323,7 +18814,7 @@ var ACTION_HELP = {
|
|
|
18323
18814
|
examples: [
|
|
18324
18815
|
"rebase cloud compute set --cpu 500m --memory 2Gi",
|
|
18325
18816
|
"rebase cloud compute set --replicas 2 --autoscale-max 6",
|
|
18326
|
-
"rebase cloud compute set --db-
|
|
18817
|
+
"rebase cloud compute set --db-instances 2 --db-memory 4Gi"
|
|
18327
18818
|
],
|
|
18328
18819
|
notes: [
|
|
18329
18820
|
"Run `rebase cloud compute` first — it prints the current dials and the €/month this project is quoted.",
|
|
@@ -18965,7 +19456,7 @@ async function appsCommand(subcommand, rawArgs = []) {
|
|
|
18965
19456
|
}
|
|
18966
19457
|
function describeApp(app) {
|
|
18967
19458
|
switch (app.type) {
|
|
18968
|
-
case "backend": return app.runtime === "custom" ? `custom runtime — ${app.dockerfile ?? "Dockerfile"}` : `managed runtime, config: ${app.config ?? "config"}`;
|
|
19459
|
+
case "backend": return app.runtime === "custom" ? `custom runtime — ${app.dockerfile ?? "Dockerfile"}` + (app.context && app.context !== "." ? ` (context: ${app.context})` : "") : `managed runtime, config: ${app.config ?? "config"}`;
|
|
18969
19460
|
case "static": return `${app.root} → ${app.output} @ ${app.path ?? "/"}` + (app.cms ? ` ${chalk.magenta(`CMS at ${app.cms}`)}` : "");
|
|
18970
19461
|
default: return "";
|
|
18971
19462
|
}
|
|
@@ -19180,7 +19671,7 @@ async function entry(args) {
|
|
|
19180
19671
|
await recordEvent("cli.error", {
|
|
19181
19672
|
command: command ?? "none",
|
|
19182
19673
|
subcommand: effectiveSubcommand ?? "none",
|
|
19183
|
-
error_type: error
|
|
19674
|
+
error_type: errorClass(error),
|
|
19184
19675
|
usage: Boolean(error && typeof error === "object" && error.isUsageError)
|
|
19185
19676
|
}, { projectRoot: process.cwd() });
|
|
19186
19677
|
throw error;
|