@rebasepro/cli 0.20.0 → 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/bundle.d.ts +4 -0
- 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/fold-static.d.ts +3 -0
- package/dist/index.es.js +685 -122
- package/dist/index.es.js.map +1 -1
- package/dist/manifest.d.ts +20 -1
- package/dist/telemetry/consent.d.ts +1 -1
- package/package.json +10 -8
- package/templates/template/rebase.json +2 -1
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
|
|
@@ -1495,33 +1574,30 @@ function renderPreview(event, properties) {
|
|
|
1495
1574
|
* CI must behave exactly as it does today, which means not asking and not
|
|
1496
1575
|
* sending.
|
|
1497
1576
|
*/
|
|
1498
|
-
async function promptForConsent(
|
|
1577
|
+
async function promptForConsent(options = {}) {
|
|
1499
1578
|
if (!shouldPrompt(process.env, options)) return false;
|
|
1500
1579
|
const askedBefore = suppressionReason(process.env) === "declined";
|
|
1501
1580
|
try {
|
|
1502
1581
|
console.log("");
|
|
1503
1582
|
console.log(chalk.bold("Help improve Rebase?"));
|
|
1504
1583
|
console.log("");
|
|
1505
|
-
console.log(chalk.gray(" Rebase is self-hosted, so we
|
|
1506
|
-
console.log(chalk.gray("
|
|
1507
|
-
console.log("");
|
|
1508
|
-
console.log(chalk.gray("
|
|
1509
|
-
console.log("");
|
|
1510
|
-
console.log(renderPreview(event, properties).split("\n").map((line) => chalk.gray(" " + line)).join("\n"));
|
|
1584
|
+
console.log(chalk.gray(" Rebase is self-hosted, so we only learn what works if you tell us."));
|
|
1585
|
+
console.log(chalk.gray(" Anonymous: random ids, the CLI version, your OS, and which template and"));
|
|
1586
|
+
console.log(chalk.gray(" package manager you used. Never project names, paths, schemas, URLs or"));
|
|
1587
|
+
console.log(chalk.gray(" error messages."));
|
|
1511
1588
|
console.log("");
|
|
1512
|
-
console.log(chalk.gray(
|
|
1513
|
-
console.log(chalk.gray(`
|
|
1589
|
+
console.log(chalk.gray(` Print the exact payload with ${chalk.cyan("rebase telemetry show")}; change your mind`));
|
|
1590
|
+
console.log(chalk.gray(` any time with ${chalk.cyan("rebase telemetry disable")}.`));
|
|
1514
1591
|
if (askedBefore) {
|
|
1515
1592
|
console.log("");
|
|
1516
|
-
console.log(chalk.gray(" You declined before
|
|
1517
|
-
console.log(chalk.gray(" here. Asked once per new project; never during ordinary work."));
|
|
1593
|
+
console.log(chalk.gray(" You declined before; that stands unless you change it here."));
|
|
1518
1594
|
}
|
|
1519
1595
|
console.log("");
|
|
1520
1596
|
const { accepted } = await inquirer.prompt([{
|
|
1521
1597
|
type: "confirm",
|
|
1522
1598
|
name: "accepted",
|
|
1523
1599
|
message: "Share anonymous usage data?",
|
|
1524
|
-
default:
|
|
1600
|
+
default: true
|
|
1525
1601
|
}]);
|
|
1526
1602
|
setConsent(Boolean(accepted));
|
|
1527
1603
|
console.log(accepted ? chalk.green(" Thank you — sharing enabled.") : chalk.gray(" Nothing will be sent." + (options.reAskDeclined ? " You will be asked again the next time you scaffold a project." : " You will not be asked again.")));
|
|
@@ -1626,11 +1702,6 @@ var PRESET_CHOICES = [
|
|
|
1626
1702
|
short: "Blank"
|
|
1627
1703
|
}
|
|
1628
1704
|
];
|
|
1629
|
-
/**
|
|
1630
|
-
* Builds the interactive prompt questions for `rebase init`.
|
|
1631
|
-
* Exported for testability — all prompt `type` values must match
|
|
1632
|
-
* types registered by the installed version of inquirer.
|
|
1633
|
-
*/
|
|
1634
1705
|
function buildInitQuestions(params) {
|
|
1635
1706
|
const { nameArg, templateArg, headlessArg, hasGitFlag, hasInstallFlag, pm } = params;
|
|
1636
1707
|
const questions = [];
|
|
@@ -2124,7 +2195,7 @@ async function createProject$1(options) {
|
|
|
2124
2195
|
git: Boolean(options.git),
|
|
2125
2196
|
duration: durationBucket(Date.now() - startedAt)
|
|
2126
2197
|
};
|
|
2127
|
-
if (await promptForConsent(
|
|
2198
|
+
if (await promptForConsent({ reAskDeclined: true })) await recordEvent("cli.init", initProperties, { projectRoot: options.targetDirectory });
|
|
2128
2199
|
}
|
|
2129
2200
|
/**
|
|
2130
2201
|
* Apply a template preset by replacing the default collection files.
|
|
@@ -4403,6 +4474,38 @@ function checkAppPath(value, fieldPath, issues) {
|
|
|
4403
4474
|
}
|
|
4404
4475
|
return value;
|
|
4405
4476
|
}
|
|
4477
|
+
/**
|
|
4478
|
+
* Is `candidate` the path `parent`, or something beneath it?
|
|
4479
|
+
*
|
|
4480
|
+
* Segment-aware for the same reason `isForbiddenStaticPath` is: a plain
|
|
4481
|
+
* `startsWith` reads `/admin` as living under `/adm`.
|
|
4482
|
+
*/
|
|
4483
|
+
function isUnderPath(candidate, parent) {
|
|
4484
|
+
if (parent === "/") return true;
|
|
4485
|
+
return candidate === parent || candidate.startsWith(`${parent}/`);
|
|
4486
|
+
}
|
|
4487
|
+
/**
|
|
4488
|
+
* Validate where an app says it mounts the Rebase CMS.
|
|
4489
|
+
*
|
|
4490
|
+
* The same shape as {@link checkAppPath} — it is a URL path and ends up in the
|
|
4491
|
+
* same places — plus one rule of its own: it has to be inside the app declaring
|
|
4492
|
+
* it. The app's SPA fallback is what answers that URL, so a `cms` outside its
|
|
4493
|
+
* `path` names an address this app will never serve. That is a link the console
|
|
4494
|
+
* would then offer to a 404, which is worse than the missing link it replaces.
|
|
4495
|
+
*/
|
|
4496
|
+
function checkCmsPath(value, appPath, fieldPath, issues) {
|
|
4497
|
+
if (value === void 0) return void 0;
|
|
4498
|
+
const cms = checkAppPath(value, fieldPath, issues);
|
|
4499
|
+
if (cms === void 0) return void 0;
|
|
4500
|
+
if (!isUnderPath(cms, appPath)) {
|
|
4501
|
+
issues.push({
|
|
4502
|
+
path: fieldPath,
|
|
4503
|
+
message: `must be inside this app's path — it is at "${appPath}", so it cannot serve "${cms}". The CMS is a route of this app, not a separate deployment; if it really lives elsewhere, declare that app and put \`cms\` on it instead`
|
|
4504
|
+
});
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
return cms;
|
|
4508
|
+
}
|
|
4406
4509
|
function isRecord(value) {
|
|
4407
4510
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4408
4511
|
}
|
|
@@ -4412,8 +4515,25 @@ function isRecord(value) {
|
|
|
4412
4515
|
* A manifest is committed and reviewed, so this is not a security boundary so
|
|
4413
4516
|
* much as a guard against `../../` typos that would otherwise have `rebase build`
|
|
4414
4517
|
* writing outside the project.
|
|
4415
|
-
|
|
4416
|
-
|
|
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 }) {
|
|
4417
4537
|
if (value === void 0) {
|
|
4418
4538
|
if (required) issues.push({
|
|
4419
4539
|
path: fieldPath,
|
|
@@ -4436,7 +4556,7 @@ function checkRelativePath(value, fieldPath, issues, { required }) {
|
|
|
4436
4556
|
return;
|
|
4437
4557
|
}
|
|
4438
4558
|
const normalized = path.normalize(value);
|
|
4439
|
-
if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) {
|
|
4559
|
+
if (!mayEscape && (normalized === ".." || normalized.startsWith(`..${path.sep}`))) {
|
|
4440
4560
|
issues.push({
|
|
4441
4561
|
path: fieldPath,
|
|
4442
4562
|
message: "must stay inside the project directory"
|
|
@@ -4465,7 +4585,8 @@ var KNOWN_APP_FIELDS = {
|
|
|
4465
4585
|
"build",
|
|
4466
4586
|
"output",
|
|
4467
4587
|
"path",
|
|
4468
|
-
"spa"
|
|
4588
|
+
"spa",
|
|
4589
|
+
"cms"
|
|
4469
4590
|
]
|
|
4470
4591
|
};
|
|
4471
4592
|
/**
|
|
@@ -4570,14 +4691,28 @@ function validateApp(name, raw, issues) {
|
|
|
4570
4691
|
message: "only applies to a custom runtime — set \"runtime\": \"custom\" to build your own image"
|
|
4571
4692
|
});
|
|
4572
4693
|
checkRelativePath(raw.dockerfile, `${base}.dockerfile`, issues, { required: false });
|
|
4573
|
-
checkRelativePath(raw.context, `${base}.context`, issues, {
|
|
4694
|
+
checkRelativePath(raw.context, `${base}.context`, issues, {
|
|
4695
|
+
required: false,
|
|
4696
|
+
mayEscape: true
|
|
4697
|
+
});
|
|
4574
4698
|
if (raw.port !== void 0 && (typeof raw.port !== "number" || !Number.isInteger(raw.port))) issues.push({
|
|
4575
4699
|
path: `${base}.port`,
|
|
4576
4700
|
message: "must be an integer"
|
|
4577
4701
|
});
|
|
4578
|
-
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
|
+
};
|
|
4579
4714
|
}
|
|
4580
|
-
case "static":
|
|
4715
|
+
case "static": {
|
|
4581
4716
|
checkRelativePath(raw.root, `${base}.root`, issues, { required: true });
|
|
4582
4717
|
checkRelativePath(raw.output, `${base}.output`, issues, { required: true });
|
|
4583
4718
|
if (raw.build !== void 0 && typeof raw.build !== "string") issues.push({
|
|
@@ -4588,8 +4723,18 @@ function validateApp(name, raw, issues) {
|
|
|
4588
4723
|
path: `${base}.spa`,
|
|
4589
4724
|
message: "must be a boolean"
|
|
4590
4725
|
});
|
|
4591
|
-
checkAppPath(raw.path, `${base}.path`, issues);
|
|
4592
|
-
|
|
4726
|
+
const appPath = checkAppPath(raw.path, `${base}.path`, issues);
|
|
4727
|
+
checkCmsPath(raw.cms, appPath ?? "/", `${base}.cms`, issues);
|
|
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
|
+
};
|
|
4737
|
+
}
|
|
4593
4738
|
default: return;
|
|
4594
4739
|
}
|
|
4595
4740
|
}
|
|
@@ -4656,6 +4801,11 @@ function validateManifest(raw) {
|
|
|
4656
4801
|
}
|
|
4657
4802
|
byPath.set(at, name);
|
|
4658
4803
|
}
|
|
4804
|
+
const withCms = Object.entries(apps).filter(([, app]) => app.type === "static" && typeof app.cms === "string");
|
|
4805
|
+
if (withCms.length > 1) for (const [name] of withCms.slice(1)) issues.push({
|
|
4806
|
+
path: `apps.${name}.cms`,
|
|
4807
|
+
message: `a project has one CMS — "${withCms[0][0]}" already declares one`
|
|
4808
|
+
});
|
|
4659
4809
|
refuseStorageBlock(raw.storage, issues);
|
|
4660
4810
|
let telemetry;
|
|
4661
4811
|
if (raw.telemetry !== void 0) if (typeof raw.telemetry === "boolean") telemetry = raw.telemetry;
|
|
@@ -4838,6 +4988,30 @@ function findBackendApp(manifest) {
|
|
|
4838
4988
|
};
|
|
4839
4989
|
}
|
|
4840
4990
|
/**
|
|
4991
|
+
* Where this project mounts the Rebase CMS, if it says.
|
|
4992
|
+
*
|
|
4993
|
+
* The CMS is a component inside a developer's own app, so its address is a
|
|
4994
|
+
* client-side route: no build artifact, no running server and no control plane
|
|
4995
|
+
* can observe it. {@link RebaseStaticAppConfig.cms} is the only place that fact
|
|
4996
|
+
* is ever written down, and this is the one reader of it — so that "the
|
|
4997
|
+
* project's CMS" means the same thing to `rebase dev`, `rebase apps list`, the
|
|
4998
|
+
* bundle it builds and the console that shows it.
|
|
4999
|
+
*
|
|
5000
|
+
* Returns the *serving* app alongside the path, because a caller with a base URL
|
|
5001
|
+
* needs to know which app answers there — and because the path alone cannot say
|
|
5002
|
+
* whether the CMS is the whole of an app or one route of it.
|
|
5003
|
+
*/
|
|
5004
|
+
function cmsMountOf(manifest) {
|
|
5005
|
+
for (const [name, app] of Object.entries(manifest.apps)) {
|
|
5006
|
+
if (app.type !== "static" || typeof app.cms !== "string") continue;
|
|
5007
|
+
return {
|
|
5008
|
+
appName: name,
|
|
5009
|
+
app,
|
|
5010
|
+
path: app.cms
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
/**
|
|
4841
5015
|
* The app a deploy targets: the one named, or the obvious one.
|
|
4842
5016
|
*
|
|
4843
5017
|
* A repository declares apps; a project owns them. So "which app" is a question
|
|
@@ -5634,14 +5808,21 @@ async function devCommand(rawArgs) {
|
|
|
5634
5808
|
* a shape that has no such directory reads as a broken scaffold on the
|
|
5635
5809
|
* very first run of the very command the headless quickstart names.
|
|
5636
5810
|
*/
|
|
5637
|
-
const
|
|
5811
|
+
const staticShape = (() => {
|
|
5638
5812
|
try {
|
|
5639
5813
|
const { manifest } = loadManifest(projectRoot);
|
|
5640
|
-
return
|
|
5814
|
+
return {
|
|
5815
|
+
declaresStaticApp: Object.values(manifest.apps ?? {}).some((app) => app.type === "static"),
|
|
5816
|
+
cmsPath: cmsMountOf(manifest)?.path
|
|
5817
|
+
};
|
|
5641
5818
|
} catch {
|
|
5642
|
-
return
|
|
5819
|
+
return {
|
|
5820
|
+
declaresStaticApp: Boolean(frontendDir),
|
|
5821
|
+
cmsPath: void 0
|
|
5822
|
+
};
|
|
5643
5823
|
}
|
|
5644
5824
|
})();
|
|
5825
|
+
const declaresStaticApp = staticShape.declaresStaticApp;
|
|
5645
5826
|
let frontendUrl = "";
|
|
5646
5827
|
let backendUrl = "";
|
|
5647
5828
|
let debounceSummary = null;
|
|
@@ -5681,6 +5862,7 @@ async function devCommand(rawArgs) {
|
|
|
5681
5862
|
["✦ Rebase API is ready!", ""],
|
|
5682
5863
|
["➜ API: ", api]
|
|
5683
5864
|
];
|
|
5865
|
+
if (declaresStaticApp && staticShape.cmsPath && staticShape.cmsPath !== "/") lines.push(["➜ CMS: ", `${stripAnsi(frontendUrl).replace(/\/$/, "")}${staticShape.cmsPath}`]);
|
|
5684
5866
|
if (!declaresStaticApp) {
|
|
5685
5867
|
if (swaggerPath) lines.push(["➜ Swagger: ", `${api}${swaggerPath}`]);
|
|
5686
5868
|
else lines.push([" ", "(no tables served yet, so no data API and no docs)"]);
|
|
@@ -7797,7 +7979,7 @@ function vendorDependencies(options) {
|
|
|
7797
7979
|
* in one image already, that is exactly what it had.
|
|
7798
7980
|
*/
|
|
7799
7981
|
function foldStaticIntoBundle(options) {
|
|
7800
|
-
const { bundleDir, assetsDir, appName, path: basePath, spa } = options;
|
|
7982
|
+
const { bundleDir, assetsDir, appName, path: basePath, spa, cms } = options;
|
|
7801
7983
|
const manifestPath = path.join(bundleDir, "manifest.json");
|
|
7802
7984
|
if (!fs.existsSync(manifestPath)) throw new Error(`No manifest at ${manifestPath} — build the backend bundle first.`);
|
|
7803
7985
|
if (!fs.existsSync(assetsDir)) throw new Error(`No built assets at ${assetsDir}.`);
|
|
@@ -7822,7 +8004,9 @@ function foldStaticIntoBundle(options) {
|
|
|
7822
8004
|
static: [...existing, {
|
|
7823
8005
|
path: basePath,
|
|
7824
8006
|
dir,
|
|
7825
|
-
spa
|
|
8007
|
+
spa,
|
|
8008
|
+
name: appName,
|
|
8009
|
+
...cms ? { cms } : {}
|
|
7826
8010
|
}]
|
|
7827
8011
|
};
|
|
7828
8012
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
@@ -7857,7 +8041,9 @@ function buildStaticBundle(options) {
|
|
|
7857
8041
|
entry: { static: [{
|
|
7858
8042
|
path: basePath,
|
|
7859
8043
|
dir: "static",
|
|
7860
|
-
spa: options.spa ?? true
|
|
8044
|
+
spa: options.spa ?? true,
|
|
8045
|
+
name: appName,
|
|
8046
|
+
...options.cms ? { cms: options.cms } : {}
|
|
7861
8047
|
}] },
|
|
7862
8048
|
hooks: { native: false },
|
|
7863
8049
|
deps: { declared: {} },
|
|
@@ -7989,7 +8175,8 @@ function foldableApps(manifest) {
|
|
|
7989
8175
|
build: app.build,
|
|
7990
8176
|
output: app.output,
|
|
7991
8177
|
path: app.path ?? "/",
|
|
7992
|
-
spa: app.spa ?? true
|
|
8178
|
+
spa: app.spa ?? true,
|
|
8179
|
+
cms: app.cms
|
|
7993
8180
|
});
|
|
7994
8181
|
}
|
|
7995
8182
|
apps.sort((a, b) => b.path.length - a.path.length);
|
|
@@ -8109,7 +8296,8 @@ async function foldFrontendIntoBundle(options) {
|
|
|
8109
8296
|
assetsDir,
|
|
8110
8297
|
appName: app.name,
|
|
8111
8298
|
path: app.path,
|
|
8112
|
-
spa: app.spa
|
|
8299
|
+
spa: app.spa,
|
|
8300
|
+
cms: app.cms
|
|
8113
8301
|
});
|
|
8114
8302
|
outcomes.push({
|
|
8115
8303
|
appName: app.name,
|
|
@@ -8621,6 +8809,42 @@ ${chalk.bold("Examples")}
|
|
|
8621
8809
|
rebase build web Build only the "web" static app
|
|
8622
8810
|
`.trim());
|
|
8623
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
|
+
}
|
|
8624
8848
|
async function buildCommand(rawArgs = []) {
|
|
8625
8849
|
if (wantsHelp(rawArgs)) {
|
|
8626
8850
|
printHelp$5();
|
|
@@ -8684,7 +8908,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
8684
8908
|
console.log(chalk.cyan(`▸ ${name}`) + chalk.dim(` (${app.type})`));
|
|
8685
8909
|
if (app.type === "backend" && app.runtime === "custom") {
|
|
8686
8910
|
console.log(chalk.dim(" custom runtime — this project builds its own image, not a bundle"));
|
|
8687
|
-
|
|
8911
|
+
for (const line of dockerBuildHint(projectRoot, name, app)) console.log(line);
|
|
8688
8912
|
console.log("");
|
|
8689
8913
|
continue;
|
|
8690
8914
|
}
|
|
@@ -8821,7 +9045,8 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
8821
9045
|
outDir: outOverride ? path.resolve(process.cwd(), outOverride) : path.join(projectRoot, `dist-bundle-${name}`),
|
|
8822
9046
|
runtimeRange,
|
|
8823
9047
|
path: basePath,
|
|
8824
|
-
spa: asset.spa ?? true
|
|
9048
|
+
spa: asset.spa ?? true,
|
|
9049
|
+
cms: asset.cms
|
|
8825
9050
|
});
|
|
8826
9051
|
const rel = path.relative(projectRoot, result.outDir);
|
|
8827
9052
|
console.log(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s) → served at ${basePath})`));
|
|
@@ -8875,8 +9100,19 @@ function findCliRoot(from) {
|
|
|
8875
9100
|
}
|
|
8876
9101
|
return null;
|
|
8877
9102
|
}
|
|
8878
|
-
/**
|
|
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
|
+
*/
|
|
8879
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
|
+
}
|
|
8880
9116
|
/**
|
|
8881
9117
|
* Render one payload file for this project.
|
|
8882
9118
|
*
|
|
@@ -8900,7 +9136,6 @@ var SHAPE_FLAGS = ["collections", "frontend"];
|
|
|
8900
9136
|
* typechecker would see.
|
|
8901
9137
|
*/
|
|
8902
9138
|
function renderPayload(contents, shape, projectName) {
|
|
8903
|
-
const flags = shape;
|
|
8904
9139
|
const out = [];
|
|
8905
9140
|
let open = null;
|
|
8906
9141
|
for (const line of contents.split("\n")) {
|
|
@@ -8916,10 +9151,10 @@ function renderPayload(contents, shape, projectName) {
|
|
|
8916
9151
|
continue;
|
|
8917
9152
|
}
|
|
8918
9153
|
if (open) throw new Error(`Eject template: {{${kind}${name}}} inside an open ${open.name} block.`);
|
|
8919
|
-
if (!
|
|
9154
|
+
if (!isShapeFlag(name)) throw new Error(`Eject template: unknown block {{${kind}${name}}}.`);
|
|
8920
9155
|
open = {
|
|
8921
9156
|
name,
|
|
8922
|
-
keep: kind === "#" ?
|
|
9157
|
+
keep: kind === "#" ? shape[name] === true : shape[name] !== true
|
|
8923
9158
|
};
|
|
8924
9159
|
}
|
|
8925
9160
|
if (open) throw new Error(`Eject template: {{#${open.name}}} was never closed.`);
|
|
@@ -9004,13 +9239,6 @@ async function ejectCommand(rawArgs = []) {
|
|
|
9004
9239
|
const dryRun = Boolean(args["--dry-run"]);
|
|
9005
9240
|
const force = Boolean(args["--force"]);
|
|
9006
9241
|
const requested = positionals[0];
|
|
9007
|
-
if (requested === "infra") {
|
|
9008
|
-
console.error(chalk.red(" ✗ `rebase eject infra` has been removed."));
|
|
9009
|
-
console.error(chalk.gray(" It wrote rebase.infra.json, which nothing ever read. Resources bind"));
|
|
9010
|
-
console.error(chalk.gray(" from the environment on the <BASE>__<KEY> convention; declare them in"));
|
|
9011
|
-
console.error(chalk.gray(" config/resources.ts and see `rebase resources` for the names."));
|
|
9012
|
-
process.exit(1);
|
|
9013
|
-
}
|
|
9014
9242
|
let loaded;
|
|
9015
9243
|
try {
|
|
9016
9244
|
loaded = loadManifest(projectRoot);
|
|
@@ -11735,8 +11963,11 @@ function printPayload() {
|
|
|
11735
11963
|
console.log("");
|
|
11736
11964
|
console.log(` ${describeState()}`);
|
|
11737
11965
|
console.log("");
|
|
11738
|
-
console.log(chalk.gray(" Nothing is being sent
|
|
11739
|
-
console.log(
|
|
11966
|
+
console.log(chalk.gray(" Nothing is being sent. This is what WOULD be sent if you said yes:"));
|
|
11967
|
+
console.log("");
|
|
11968
|
+
console.log(renderPreview("cli.dev", { first_run: false }).split("\n").map((l) => " " + l).join("\n"));
|
|
11969
|
+
console.log("");
|
|
11970
|
+
console.log(chalk.gray(` Start sharing with ${chalk.cyan("rebase telemetry enable")}.`));
|
|
11740
11971
|
console.log("");
|
|
11741
11972
|
return;
|
|
11742
11973
|
}
|
|
@@ -11752,7 +11983,7 @@ function printPayload() {
|
|
|
11752
11983
|
}
|
|
11753
11984
|
function printHelp$2() {
|
|
11754
11985
|
console.log(`
|
|
11755
|
-
${chalk.bold("rebase telemetry")} — anonymous usage sharing (
|
|
11986
|
+
${chalk.bold("rebase telemetry")} — anonymous usage sharing (asked once per project)
|
|
11756
11987
|
|
|
11757
11988
|
${chalk.bold("Commands")}
|
|
11758
11989
|
${chalk.blue("status")} Whether anything is being shared, and why ${chalk.gray("(default)")}
|
|
@@ -12039,10 +12270,10 @@ async function linkCommand(rawArgs) {
|
|
|
12039
12270
|
} else {
|
|
12040
12271
|
requireInteractive("a project to link", "--project <slug>");
|
|
12041
12272
|
const org = getContextOrg(url);
|
|
12042
|
-
const projects = (await client.data.collection("projects").find({
|
|
12273
|
+
const projects = cloudRows((await client.data.collection("projects").find({
|
|
12043
12274
|
where: org ? { organization: ["==", org] } : void 0,
|
|
12044
12275
|
limit: 100
|
|
12045
|
-
})).data;
|
|
12276
|
+
})).data);
|
|
12046
12277
|
if (projects.length === 0) fail("No projects found for your account.", `Create one with ${chalk.bold("rebase cloud projects create")}.`, "no_projects");
|
|
12047
12278
|
const { picked } = await inquirer.prompt([{
|
|
12048
12279
|
type: "select",
|
|
@@ -12506,14 +12737,14 @@ async function webhooksCommand(subcommand, rawArgs) {
|
|
|
12506
12737
|
const table = args["--table"] || fail("--table is required.", void 0, "usage");
|
|
12507
12738
|
const url = args["--endpoint"] || fail("--endpoint is required.", "Where the POST goes.", "usage");
|
|
12508
12739
|
const events = (args["--events"] || "insert,update,delete").split(",").map((s) => s.trim());
|
|
12509
|
-
const created = await client.data.collection("webhooks").create({
|
|
12740
|
+
const created = requireCloudRow(await client.data.collection("webhooks").create({
|
|
12510
12741
|
project: projectId,
|
|
12511
12742
|
name,
|
|
12512
12743
|
table,
|
|
12513
12744
|
url,
|
|
12514
12745
|
events,
|
|
12515
12746
|
enabled: true
|
|
12516
|
-
});
|
|
12747
|
+
}), "webhook");
|
|
12517
12748
|
success(`Created webhook ${chalk.bold(name)} [${created.id}]`);
|
|
12518
12749
|
emit(() => {}, {
|
|
12519
12750
|
success: true,
|
|
@@ -12606,15 +12837,22 @@ async function storageCommand(action, rawArgs) {
|
|
|
12606
12837
|
return;
|
|
12607
12838
|
}
|
|
12608
12839
|
for (const s of stores) {
|
|
12609
|
-
|
|
12610
|
-
|
|
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
|
+
]);
|
|
12611
12848
|
}
|
|
12612
12849
|
console.log("");
|
|
12613
12850
|
}, {
|
|
12614
12851
|
projectId,
|
|
12615
12852
|
stores: stores.map((s) => ({
|
|
12616
12853
|
id: String(s.id),
|
|
12617
|
-
|
|
12854
|
+
sourceKey: s.sourceKey || DEFAULT_STORAGE_SOURCE_KEY,
|
|
12855
|
+
bucketName: s.bucketName || s.s3Bucket || null,
|
|
12618
12856
|
type: s.type ?? null,
|
|
12619
12857
|
provider: s.provider ?? null,
|
|
12620
12858
|
status: s.status ?? null
|
|
@@ -12646,7 +12884,8 @@ function printStorageHelp() {
|
|
|
12646
12884
|
["--secret-access-key <s>", "Secret access key. Required"],
|
|
12647
12885
|
["--endpoint <url>", "S3 endpoint. Omit for AWS"],
|
|
12648
12886
|
["--region <region>", "Region"],
|
|
12649
|
-
["--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"]
|
|
12650
12889
|
]
|
|
12651
12890
|
}
|
|
12652
12891
|
],
|
|
@@ -12671,11 +12910,10 @@ async function storageCreateCommand(rawArgs) {
|
|
|
12671
12910
|
try {
|
|
12672
12911
|
noteBlank();
|
|
12673
12912
|
note(chalk.gray("Provisioning managed storage — this creates a bucket and its credentials..."));
|
|
12674
|
-
const
|
|
12913
|
+
const info = (await client.functions.invoke("storage-provision", void 0, {
|
|
12675
12914
|
method: "POST",
|
|
12676
12915
|
path: projectId
|
|
12677
|
-
});
|
|
12678
|
-
const info = res.data ?? res.data;
|
|
12916
|
+
})).data;
|
|
12679
12917
|
success(`Managed storage provisioned for ${displayProjectRef(rawArgs)}.`);
|
|
12680
12918
|
emit(() => {
|
|
12681
12919
|
keyValues([
|
|
@@ -12710,7 +12948,8 @@ async function storageAttachCommand(rawArgs) {
|
|
|
12710
12948
|
"--secret-access-key": String,
|
|
12711
12949
|
"--endpoint": String,
|
|
12712
12950
|
"--region": String,
|
|
12713
|
-
"--force-path-style": Boolean
|
|
12951
|
+
"--force-path-style": Boolean,
|
|
12952
|
+
"--source": String
|
|
12714
12953
|
},
|
|
12715
12954
|
rawArgs,
|
|
12716
12955
|
commandWords: 3,
|
|
@@ -12728,13 +12967,18 @@ async function storageAttachCommand(rawArgs) {
|
|
|
12728
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");
|
|
12729
12968
|
const { client } = await requireClient(rawArgs);
|
|
12730
12969
|
const projectId = await requireProject(rawArgs, client);
|
|
12970
|
+
const sourceKey = (parsed["--source"] ?? "").trim() || DEFAULT_STORAGE_SOURCE_KEY;
|
|
12731
12971
|
try {
|
|
12732
12972
|
const existing = (await client.data.collection("storages").find({
|
|
12733
12973
|
where: { project: ["==", projectId] },
|
|
12734
|
-
limit:
|
|
12735
|
-
})).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
|
+
});
|
|
12736
12979
|
const row = {
|
|
12737
12980
|
project: projectId,
|
|
12981
|
+
sourceKey,
|
|
12738
12982
|
type: "byos",
|
|
12739
12983
|
status: "active",
|
|
12740
12984
|
s3Bucket: bucket,
|
|
@@ -13065,8 +13309,7 @@ async function billingCommand(rawArgs) {
|
|
|
13065
13309
|
}
|
|
13066
13310
|
if (!org) fail("No active organization.", "Run `rebase cloud use` first.", "no_org");
|
|
13067
13311
|
try {
|
|
13068
|
-
const
|
|
13069
|
-
const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
|
|
13312
|
+
const billingId = billingAccountIdOf(await client.data.collection("organizations").findById(org));
|
|
13070
13313
|
if (!billingId) {
|
|
13071
13314
|
emit(() => {
|
|
13072
13315
|
console.log("");
|
|
@@ -13154,7 +13397,6 @@ var DIAL_FLAGS = {
|
|
|
13154
13397
|
"--replicas": "replicaCount",
|
|
13155
13398
|
"--spot": "preemptible",
|
|
13156
13399
|
"--scale-to-zero": "scaleToZero",
|
|
13157
|
-
"--db-mode": "databaseMode",
|
|
13158
13400
|
"--db-instances": "databaseInstances",
|
|
13159
13401
|
"--db-cpu": "databaseCpu",
|
|
13160
13402
|
"--db-memory": "databaseMemory",
|
|
@@ -13246,7 +13488,6 @@ async function computeCommand(action, rawArgs) {
|
|
|
13246
13488
|
true: "scale to zero",
|
|
13247
13489
|
false: "stay warm"
|
|
13248
13490
|
})],
|
|
13249
|
-
["Database", dialLine(project.databaseMode)],
|
|
13250
13491
|
["Database instances", dialLine(project.databaseInstances)],
|
|
13251
13492
|
["Database CPU", dialLine(project.databaseCpu)],
|
|
13252
13493
|
["Database memory", dialLine(project.databaseMemory)],
|
|
@@ -13273,7 +13514,6 @@ async function computeCommand(action, rawArgs) {
|
|
|
13273
13514
|
replicaCount: project.replicaCount ?? null,
|
|
13274
13515
|
preemptible: project.preemptible ?? null,
|
|
13275
13516
|
scaleToZero: project.scaleToZero ?? null,
|
|
13276
|
-
databaseMode: project.databaseMode ?? null,
|
|
13277
13517
|
databaseInstances: project.databaseInstances ?? null,
|
|
13278
13518
|
databaseCpu: project.databaseCpu ?? null,
|
|
13279
13519
|
databaseMemory: project.databaseMemory ?? null,
|
|
@@ -13355,6 +13595,227 @@ function buildDialPatch(rawArgs, opts) {
|
|
|
13355
13595
|
return { patch };
|
|
13356
13596
|
}
|
|
13357
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
|
|
13358
13819
|
//#region src/commands/cloud/databases.ts
|
|
13359
13820
|
async function dbCommand(subcommand, rawArgs) {
|
|
13360
13821
|
switch (subcommand) {
|
|
@@ -13368,6 +13829,9 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
13368
13829
|
case "info":
|
|
13369
13830
|
await dbInfo(rawArgs);
|
|
13370
13831
|
break;
|
|
13832
|
+
case "connect":
|
|
13833
|
+
await dbConnect(rawArgs);
|
|
13834
|
+
break;
|
|
13371
13835
|
case "test":
|
|
13372
13836
|
await testDatabase(rawArgs);
|
|
13373
13837
|
break;
|
|
@@ -13395,10 +13859,10 @@ async function listDatabases(rawArgs) {
|
|
|
13395
13859
|
const projectId = await requireProject(rawArgs, client);
|
|
13396
13860
|
const projectRef = displayProjectRef(rawArgs);
|
|
13397
13861
|
try {
|
|
13398
|
-
const dbs = (await client.data.collection("databases").find({
|
|
13862
|
+
const dbs = cloudRows((await client.data.collection("databases").find({
|
|
13399
13863
|
where: { project: ["==", projectId] },
|
|
13400
13864
|
limit: 50
|
|
13401
|
-
})).data;
|
|
13865
|
+
})).data);
|
|
13402
13866
|
emit(() => {
|
|
13403
13867
|
console.log("");
|
|
13404
13868
|
console.log(chalk.bold(` 🗄 Databases — project ${projectRef}`));
|
|
@@ -13435,10 +13899,10 @@ async function listDatabases(rawArgs) {
|
|
|
13435
13899
|
* point, since the answer decides which row a deploy will actually use.
|
|
13436
13900
|
*/
|
|
13437
13901
|
async function firstAttachedDatabase(client, projectId) {
|
|
13438
|
-
return (await client.data.collection("databases").find({
|
|
13902
|
+
return cloudRows((await client.data.collection("databases").find({
|
|
13439
13903
|
where: { project: ["==", projectId] },
|
|
13440
13904
|
limit: 1
|
|
13441
|
-
})).data[0];
|
|
13905
|
+
})).data)[0];
|
|
13442
13906
|
}
|
|
13443
13907
|
/**
|
|
13444
13908
|
* Attach a database row to a project.
|
|
@@ -13449,12 +13913,12 @@ async function firstAttachedDatabase(client, projectId) {
|
|
|
13449
13913
|
* afterwards".
|
|
13450
13914
|
*/
|
|
13451
13915
|
async function attachDatabaseRow(client, input) {
|
|
13452
|
-
return await client.data.collection("databases").create({
|
|
13916
|
+
return requireCloudRow(await client.data.collection("databases").create({
|
|
13453
13917
|
project: input.projectId,
|
|
13454
13918
|
type: input.type,
|
|
13455
13919
|
connectionString: input.type === "byodb" ? input.connectionString : void 0,
|
|
13456
13920
|
connectionStatus: "untested"
|
|
13457
|
-
});
|
|
13921
|
+
}), "database");
|
|
13458
13922
|
}
|
|
13459
13923
|
/** What `rebase cloud db create` parses. Exported so its help page cannot drift. */
|
|
13460
13924
|
var CREATE_DATABASE_FLAGS = {
|
|
@@ -13641,10 +14105,19 @@ async function dbInfo(rawArgs) {
|
|
|
13641
14105
|
["Connection", connectionString]
|
|
13642
14106
|
]);
|
|
13643
14107
|
if (info.unavailableReason) console.log(chalk.gray(` ${info.unavailableReason}`));
|
|
13644
|
-
if (info.
|
|
13645
|
-
|
|
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:"));
|
|
13646
14113
|
console.log("");
|
|
13647
|
-
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
|
+
}
|
|
13648
14121
|
}
|
|
13649
14122
|
console.log("");
|
|
13650
14123
|
}, {
|
|
@@ -13655,7 +14128,7 @@ async function dbInfo(rawArgs) {
|
|
|
13655
14128
|
database: info.database,
|
|
13656
14129
|
username: info.username,
|
|
13657
14130
|
passwordAvailable: info.passwordAvailable,
|
|
13658
|
-
|
|
14131
|
+
directAccess: info.directAccess,
|
|
13659
14132
|
unavailableReason: info.unavailableReason,
|
|
13660
14133
|
...args["--reveal"] ? {
|
|
13661
14134
|
password,
|
|
@@ -13941,7 +14414,13 @@ function printDbHelp() {
|
|
|
13941
14414
|
action: "info",
|
|
13942
14415
|
section: "Database",
|
|
13943
14416
|
description: "Connection details",
|
|
13944
|
-
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"]]
|
|
13945
14424
|
},
|
|
13946
14425
|
{
|
|
13947
14426
|
action: "test",
|
|
@@ -14001,7 +14480,9 @@ function printDbHelp() {
|
|
|
14001
14480
|
"A project has exactly one database: `create` refuses rather than attaching a second, because",
|
|
14002
14481
|
"which of two rows a deploy uses is undefined.",
|
|
14003
14482
|
"A managed database is provisioned at the project's FIRST DEPLOY, so `test` failing before then",
|
|
14004
|
-
"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."
|
|
14005
14486
|
]
|
|
14006
14487
|
});
|
|
14007
14488
|
}
|
|
@@ -14025,7 +14506,7 @@ async function listProjects(rawArgs) {
|
|
|
14025
14506
|
where: org ? { organization: ["==", org] } : void 0,
|
|
14026
14507
|
orderBy: ["name", "asc"],
|
|
14027
14508
|
limit: 100
|
|
14028
|
-
}).then((res) => res.data), fetchTenantBaseDomain(client, url)]);
|
|
14509
|
+
}).then((res) => cloudRows(res.data)), fetchTenantBaseDomain(client, url)]);
|
|
14029
14510
|
const linkedId = readLink()?.projectId;
|
|
14030
14511
|
emit(() => {
|
|
14031
14512
|
console.log("");
|
|
@@ -14144,8 +14625,8 @@ var CREATE_PROJECT_FLAGS = {
|
|
|
14144
14625
|
* means the two-command sequence that every project needs is one command,
|
|
14145
14626
|
* and `--db none` is there for the case that genuinely wants to decide later.
|
|
14146
14627
|
*
|
|
14147
|
-
* Distinct from `--db-
|
|
14148
|
-
* 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.
|
|
14149
14630
|
*/
|
|
14150
14631
|
"--db": String,
|
|
14151
14632
|
/** For `--db byodb`. Same spelling as `rebase cloud db create` uses. */
|
|
@@ -14156,7 +14637,6 @@ var CREATE_PROJECT_FLAGS = {
|
|
|
14156
14637
|
"--replicas": String,
|
|
14157
14638
|
"--spot": String,
|
|
14158
14639
|
"--scale-to-zero": String,
|
|
14159
|
-
"--db-mode": String,
|
|
14160
14640
|
"--db-instances": String,
|
|
14161
14641
|
"--db-cpu": String,
|
|
14162
14642
|
"--db-memory": String,
|
|
@@ -14210,7 +14690,7 @@ async function createProject(rawArgs) {
|
|
|
14210
14690
|
try {
|
|
14211
14691
|
const user = await client.auth.getUser();
|
|
14212
14692
|
if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.", "session_invalid");
|
|
14213
|
-
const created = await client.data.collection("projects").create({
|
|
14693
|
+
const created = requireCloudRow(await client.data.collection("projects").create({
|
|
14214
14694
|
name,
|
|
14215
14695
|
subdomain,
|
|
14216
14696
|
gitRepoUrl,
|
|
@@ -14221,7 +14701,7 @@ async function createProject(rawArgs) {
|
|
|
14221
14701
|
organization: org,
|
|
14222
14702
|
createdById: user.uid,
|
|
14223
14703
|
status: "provisioning"
|
|
14224
|
-
});
|
|
14704
|
+
}), "project");
|
|
14225
14705
|
const host = projectHost(created, await fetchTenantBaseDomain(client, url));
|
|
14226
14706
|
const linked = Boolean(args["--link"]);
|
|
14227
14707
|
if (linked) writeLink({
|
|
@@ -14531,12 +15011,63 @@ function packBundle(bundleDir, outPath) {
|
|
|
14531
15011
|
});
|
|
14532
15012
|
}
|
|
14533
15013
|
/**
|
|
14534
|
-
*
|
|
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`.
|
|
14535
15026
|
*
|
|
14536
|
-
*
|
|
14537
|
-
*
|
|
14538
|
-
*
|
|
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.
|
|
14539
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
|
+
}
|
|
14540
15071
|
function bundleDeployBody(input) {
|
|
14541
15072
|
return {
|
|
14542
15073
|
projectId: input.projectId,
|
|
@@ -14546,7 +15077,11 @@ function bundleDeployBody(input) {
|
|
|
14546
15077
|
client: "cli",
|
|
14547
15078
|
frameworkVersion: input.manifest.runtime?.builtAgainst,
|
|
14548
15079
|
...input.declaredApps?.length ? { declaredApps: input.declaredApps } : {},
|
|
14549
|
-
...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
|
+
} : {}
|
|
14550
15085
|
};
|
|
14551
15086
|
}
|
|
14552
15087
|
/**
|
|
@@ -14813,7 +15348,8 @@ async function uploadAndTrigger(opts) {
|
|
|
14813
15348
|
manifest,
|
|
14814
15349
|
app: opts.appName,
|
|
14815
15350
|
message: opts.message,
|
|
14816
|
-
declaredApps
|
|
15351
|
+
declaredApps,
|
|
15352
|
+
commit: bundleCommit(process.cwd())
|
|
14817
15353
|
});
|
|
14818
15354
|
let deploymentId;
|
|
14819
15355
|
let managed;
|
|
@@ -15058,8 +15594,7 @@ async function readBillingState(client, projectId) {
|
|
|
15058
15594
|
if (!card) return unknown;
|
|
15059
15595
|
let plan = null;
|
|
15060
15596
|
try {
|
|
15061
|
-
const
|
|
15062
|
-
const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
|
|
15597
|
+
const billingId = billingAccountIdOf(await client.data.collection("organizations").findById(org));
|
|
15063
15598
|
if (billingId != null) {
|
|
15064
15599
|
const acct = await client.data.collection("billing-accounts").findById(billingId);
|
|
15065
15600
|
plan = typeof acct?.plan === "string" ? acct.plan : null;
|
|
@@ -15500,7 +16035,7 @@ async function listOrgs(rawArgs) {
|
|
|
15500
16035
|
});
|
|
15501
16036
|
const { client, url } = await requireClient(rawArgs);
|
|
15502
16037
|
try {
|
|
15503
|
-
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
16038
|
+
const orgs = cloudRows((await client.data.collection("organizations").find({ limit: 100 })).data);
|
|
15504
16039
|
const active = getContextOrg(url);
|
|
15505
16040
|
emit(() => {
|
|
15506
16041
|
console.log("");
|
|
@@ -15560,11 +16095,11 @@ async function createOrg(rawArgs) {
|
|
|
15560
16095
|
if (!name) fail("Organization name is required.", "Pass `--name <name>`.", "input_required");
|
|
15561
16096
|
const slug = (args["--slug"] || slugify(name)).trim();
|
|
15562
16097
|
try {
|
|
15563
|
-
const created = await client.data.collection("organizations").create({
|
|
16098
|
+
const created = requireCloudRow(await client.data.collection("organizations").create({
|
|
15564
16099
|
name,
|
|
15565
16100
|
slug,
|
|
15566
16101
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15567
|
-
});
|
|
16102
|
+
}), "organization");
|
|
15568
16103
|
setContextOrg(url, String(created.id));
|
|
15569
16104
|
success(`Created organization ${chalk.bold(name)} and set it active`);
|
|
15570
16105
|
emit(() => {}, {
|
|
@@ -16745,11 +17280,11 @@ function cancelView(res) {
|
|
|
16745
17280
|
};
|
|
16746
17281
|
}
|
|
16747
17282
|
async function fetchDeployments(client, projectId, limit = 100) {
|
|
16748
|
-
return (await client.data.collection("deployments").find({
|
|
17283
|
+
return cloudRows((await client.data.collection("deployments").find({
|
|
16749
17284
|
where: { project: ["==", projectId] },
|
|
16750
17285
|
orderBy: ["createdAt", "desc"],
|
|
16751
17286
|
limit
|
|
16752
|
-
})).data;
|
|
17287
|
+
})).data);
|
|
16753
17288
|
}
|
|
16754
17289
|
/** Hard ceiling on `--limit`, matching the backend's own page size. */
|
|
16755
17290
|
var MAX_DEPLOYMENTS_LIMIT = 100;
|
|
@@ -17594,9 +18129,10 @@ async function dbDebugCommand(rawArgs) {
|
|
|
17594
18129
|
} catch (e) {
|
|
17595
18130
|
reportError(e, "Failed to read database connection info");
|
|
17596
18131
|
}
|
|
17597
|
-
const
|
|
17598
|
-
const
|
|
17599
|
-
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;
|
|
17600
18136
|
emit(() => {
|
|
17601
18137
|
console.log("");
|
|
17602
18138
|
console.log(chalk.bold(` 🐘 Database — ${displayProjectRef(rawArgs)}`));
|
|
@@ -17615,12 +18151,18 @@ async function dbDebugCommand(rawArgs) {
|
|
|
17615
18151
|
["Password", info.passwordAvailable ? chalk.gray("stored — not shown here") : chalk.yellow("none stored")]
|
|
17616
18152
|
]);
|
|
17617
18153
|
console.log("");
|
|
17618
|
-
if (
|
|
17619
|
-
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:"));
|
|
17620
18157
|
console.log("");
|
|
17621
|
-
console.log(` ${
|
|
18158
|
+
console.log(` ${connectCmd}`);
|
|
17622
18159
|
if (psqlCmd) console.log(` ${psqlCmd}`);
|
|
17623
18160
|
console.log("");
|
|
18161
|
+
if (kubectlCmd) {
|
|
18162
|
+
console.log(chalk.gray(" Or, on your own cluster:"));
|
|
18163
|
+
console.log(` ${kubectlCmd}`);
|
|
18164
|
+
console.log("");
|
|
18165
|
+
}
|
|
17624
18166
|
if (info.passwordAvailable) {
|
|
17625
18167
|
console.log(chalk.gray(" Get the password with: ") + chalk.bold("rebase cloud db info --reveal"));
|
|
17626
18168
|
console.log("");
|
|
@@ -17633,7 +18175,8 @@ async function dbDebugCommand(rawArgs) {
|
|
|
17633
18175
|
database: info.database ?? null,
|
|
17634
18176
|
username: info.username ?? null,
|
|
17635
18177
|
passwordAvailable: Boolean(info.passwordAvailable),
|
|
17636
|
-
|
|
18178
|
+
connectCommand: connectCmd,
|
|
18179
|
+
kubectlCommand: kubectlCmd,
|
|
17637
18180
|
psqlCommand: psqlCmd
|
|
17638
18181
|
});
|
|
17639
18182
|
}
|
|
@@ -17932,7 +18475,6 @@ var ACTION_HELP = {
|
|
|
17932
18475
|
["--replicas <n>", "Instance count"],
|
|
17933
18476
|
["--spot <true|false>", "Run on preemptible capacity"],
|
|
17934
18477
|
["--scale-to-zero <true|false>", "Stop the instances when idle"],
|
|
17935
|
-
["--db-mode <mode>", "Database topology dial"],
|
|
17936
18478
|
["--db-instances <n>", "Database instance count"],
|
|
17937
18479
|
["--db-cpu <n>", "vCPU per database instance"],
|
|
17938
18480
|
["--db-memory <size>", "Memory per database instance"],
|
|
@@ -17961,11 +18503,28 @@ var ACTION_HELP = {
|
|
|
17961
18503
|
],
|
|
17962
18504
|
examples: ["rebase cloud db create --type managed", "rebase cloud db create --type byodb --connection-string \"$DATABASE_URL\" --wait"],
|
|
17963
18505
|
notes: [
|
|
17964
|
-
"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.",
|
|
17965
18507
|
"`rebase cloud db test` legitimately fails before the first deploy.",
|
|
17966
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."
|
|
17967
18509
|
]
|
|
17968
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
|
+
},
|
|
17969
18528
|
deploy: {
|
|
17970
18529
|
command: "cloud deploy",
|
|
17971
18530
|
usage: "cloud deploy [app] [options]",
|
|
@@ -18067,7 +18626,7 @@ var ACTION_HELP = {
|
|
|
18067
18626
|
"rebase cloud db backup restore base-20260831 --yes",
|
|
18068
18627
|
"rebase cloud db backup download base-20260831"
|
|
18069
18628
|
],
|
|
18070
|
-
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."]
|
|
18071
18630
|
},
|
|
18072
18631
|
"db pitr": {
|
|
18073
18632
|
command: "cloud db pitr",
|
|
@@ -18142,7 +18701,7 @@ var ACTION_HELP = {
|
|
|
18142
18701
|
},
|
|
18143
18702
|
"storage attach": {
|
|
18144
18703
|
command: "cloud storage attach",
|
|
18145
|
-
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>]",
|
|
18146
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.",
|
|
18147
18706
|
flags: [
|
|
18148
18707
|
["--bucket <name>", "The bucket name. Required"],
|
|
@@ -18150,9 +18709,14 @@ var ACTION_HELP = {
|
|
|
18150
18709
|
["--secret-access-key <secret>", "Required. Stored encrypted and never returned"],
|
|
18151
18710
|
["--endpoint <url>", "For anything that is not AWS S3 — R2, MinIO, Backblaze"],
|
|
18152
18711
|
["--region <region>", "Bucket region. Default: the provider's own default"],
|
|
18153
|
-
["--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"
|
|
18154
18719
|
],
|
|
18155
|
-
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"],
|
|
18156
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."]
|
|
18157
18721
|
},
|
|
18158
18722
|
"webhooks create": {
|
|
@@ -18239,7 +18803,6 @@ var ACTION_HELP = {
|
|
|
18239
18803
|
["--replicas <n>", "Instances that always exist — the autoscaler's floor, and what is billed at rest"],
|
|
18240
18804
|
["--spot <true|false>", "Preemptible capacity: cheaper, and restarted without notice"],
|
|
18241
18805
|
["--scale-to-zero <true|false>", "Request-billed compute that stops when idle, at the cost of a cold start"],
|
|
18242
|
-
["--db-mode <shared|dedicated>", "Pooled cluster, or one of this project's own"],
|
|
18243
18806
|
["--db-instances <n>", "1–3. 1 is a single instance with no failover; 2 adds an automatic standby"],
|
|
18244
18807
|
["--db-cpu <n>", "Database CPU request per instance. Default: 500m"],
|
|
18245
18808
|
["--db-memory <size>", "Database memory request per instance. Default: 2Gi"],
|
|
@@ -18251,7 +18814,7 @@ var ACTION_HELP = {
|
|
|
18251
18814
|
examples: [
|
|
18252
18815
|
"rebase cloud compute set --cpu 500m --memory 2Gi",
|
|
18253
18816
|
"rebase cloud compute set --replicas 2 --autoscale-max 6",
|
|
18254
|
-
"rebase cloud compute set --db-
|
|
18817
|
+
"rebase cloud compute set --db-instances 2 --db-memory 4Gi"
|
|
18255
18818
|
],
|
|
18256
18819
|
notes: [
|
|
18257
18820
|
"Run `rebase cloud compute` first — it prints the current dials and the €/month this project is quoted.",
|
|
@@ -18893,8 +19456,8 @@ async function appsCommand(subcommand, rawArgs = []) {
|
|
|
18893
19456
|
}
|
|
18894
19457
|
function describeApp(app) {
|
|
18895
19458
|
switch (app.type) {
|
|
18896
|
-
case "backend": return app.runtime === "custom" ? `custom runtime — ${app.dockerfile ?? "Dockerfile"}` : `managed runtime, config: ${app.config ?? "config"}`;
|
|
18897
|
-
case "static": return `${app.root} → ${app.output} @ ${app.path ?? "/"}
|
|
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"}`;
|
|
19460
|
+
case "static": return `${app.root} → ${app.output} @ ${app.path ?? "/"}` + (app.cms ? ` ${chalk.magenta(`CMS at ${app.cms}`)}` : "");
|
|
18898
19461
|
default: return "";
|
|
18899
19462
|
}
|
|
18900
19463
|
}
|
|
@@ -19108,7 +19671,7 @@ async function entry(args) {
|
|
|
19108
19671
|
await recordEvent("cli.error", {
|
|
19109
19672
|
command: command ?? "none",
|
|
19110
19673
|
subcommand: effectiveSubcommand ?? "none",
|
|
19111
|
-
error_type: error
|
|
19674
|
+
error_type: errorClass(error),
|
|
19112
19675
|
usage: Boolean(error && typeof error === "object" && error.isUsageError)
|
|
19113
19676
|
}, { projectRoot: process.cwd() });
|
|
19114
19677
|
throw error;
|
|
@@ -19297,6 +19860,6 @@ function telemetryNotice() {
|
|
|
19297
19860
|
return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
|
|
19298
19861
|
}
|
|
19299
19862
|
//#endregion
|
|
19300
|
-
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, ManifestError, VENDOR_SIZE_MAX_BYTES, VENDOR_SIZE_WARN_BYTES, VENDOR_TARGET_CPU, VENDOR_TARGET_OS, assessManagedCompatibility, buildBundle, buildStaticBundle, buildableApps, collectDeclaredDependencies, commentSpans, composeBundleManifest, detectDeclaredDepConflicts, detectFrameworkDepDrift, detectNativeDependencies, detectStorageAuthorize, entry, findBackendApp, findUnusedServerEntry, foldStaticIntoBundle, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, resolveBackendPaths, selectDeployApp, synthesizeManifest, validateManifest, vendorDependencies, writeManifest };
|
|
19863
|
+
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, ManifestError, VENDOR_SIZE_MAX_BYTES, VENDOR_SIZE_WARN_BYTES, VENDOR_TARGET_CPU, VENDOR_TARGET_OS, assessManagedCompatibility, buildBundle, buildStaticBundle, buildableApps, cmsMountOf, collectDeclaredDependencies, commentSpans, composeBundleManifest, detectDeclaredDepConflicts, detectFrameworkDepDrift, detectNativeDependencies, detectStorageAuthorize, entry, findBackendApp, findUnusedServerEntry, foldStaticIntoBundle, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, resolveBackendPaths, selectDeployApp, synthesizeManifest, validateManifest, vendorDependencies, writeManifest };
|
|
19301
19864
|
|
|
19302
19865
|
//# sourceMappingURL=index.es.js.map
|