@rebasepro/cli 0.22.0 → 0.23.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 +10 -0
- package/dist/cli.d.ts +18 -0
- package/dist/commands/api-keys.d.ts +2 -0
- package/dist/commands/apps.d.ts +2 -0
- package/dist/commands/auth.d.ts +12 -0
- package/dist/commands/build.d.ts +9 -1
- package/dist/commands/cloud/bundle-deploy.d.ts +34 -5
- package/dist/commands/cloud/context.d.ts +43 -0
- package/dist/commands/cloud/db-connect.d.ts +15 -0
- package/dist/commands/cloud/deploy.d.ts +55 -2
- package/dist/commands/cloud/domains.d.ts +2 -2
- package/dist/commands/cloud/env.d.ts +14 -0
- package/dist/commands/cloud/rebuild-source.d.ts +24 -2
- package/dist/commands/db.d.ts +2 -0
- package/dist/commands/schema.d.ts +2 -0
- package/dist/commands/skills.d.ts +2 -0
- package/dist/commands/telemetry.d.ts +10 -0
- package/dist/dev-db/pull.d.ts +31 -2
- package/dist/dev-db/resolve.d.ts +2 -4
- package/dist/fold-static.d.ts +2 -0
- package/dist/index.es.js +1406 -429
- package/dist/index.es.js.map +1 -1
- package/dist/{pull-Bj0XnH5s.js → pull-Dywm5FWP.js} +41 -5
- package/dist/{pull-Bj0XnH5s.js.map → pull-Dywm5FWP.js.map} +1 -1
- package/dist/{resolve-Y56osuQH.js → resolve-BAeRUBhg.js} +2 -2
- package/dist/resolve-BAeRUBhg.js.map +1 -0
- package/dist/upgrade.d.ts +10 -1
- package/dist/utils/tool-stdio.d.ts +11 -0
- package/package.json +8 -8
- package/dist/resolve-Y56osuQH.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -4,7 +4,7 @@ import { i as probeTcp, r as ensureDevDatabase, t as composeDatabaseUrl } from "
|
|
|
4
4
|
import { r as readActiveBranch, t as branchUrl } from "./branch-pointer-dBiClJ0p.js";
|
|
5
5
|
import { t as MANAGED_LIMITATIONS } from "./constraints-DKGbNfUW.js";
|
|
6
6
|
import { i as managedUrl, n as ensureManagedDatabase, r as findRunningDaemon } from "./daemon-DR9Tk0La.js";
|
|
7
|
-
import { n as resolveDevDatabase, t as describeDevDatabase } from "./resolve-
|
|
7
|
+
import { n as resolveDevDatabase, t as describeDevDatabase } from "./resolve-BAeRUBhg.js";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import arg from "arg";
|
|
10
10
|
import inquirer from "inquirer";
|
|
@@ -459,16 +459,47 @@ function setCurrentContext(url) {
|
|
|
459
459
|
writeCredentials(creds);
|
|
460
460
|
}
|
|
461
461
|
function resolveCloudUrl(rawArgs) {
|
|
462
|
-
|
|
462
|
+
return resolveCloudTarget(rawArgs).url;
|
|
463
|
+
}
|
|
464
|
+
/** The control-plane URL, and which rung of the ladder above supplied it. */
|
|
465
|
+
function resolveCloudTarget(rawArgs) {
|
|
466
|
+
const parsed = arg({ "--url": String }, {
|
|
463
467
|
argv: rawArgs.slice(2),
|
|
464
468
|
permissive: true
|
|
465
|
-
})
|
|
466
|
-
if (
|
|
469
|
+
});
|
|
470
|
+
if (parsed["--url"]) return {
|
|
471
|
+
url: normalizeUrl(parsed["--url"]),
|
|
472
|
+
source: "flag"
|
|
473
|
+
};
|
|
474
|
+
if (process.env.REBASE_CLOUD_URL) return {
|
|
475
|
+
url: normalizeUrl(process.env.REBASE_CLOUD_URL),
|
|
476
|
+
source: "env"
|
|
477
|
+
};
|
|
467
478
|
const link = readLink();
|
|
468
|
-
if (link?.url && link.mode !== "direct") return
|
|
479
|
+
if (link?.url && link.mode !== "direct") return {
|
|
480
|
+
url: normalizeUrl(link.url),
|
|
481
|
+
source: "link"
|
|
482
|
+
};
|
|
469
483
|
const current = currentContextUrl();
|
|
470
|
-
if (current) return
|
|
471
|
-
|
|
484
|
+
if (current) return {
|
|
485
|
+
url: normalizeUrl(current),
|
|
486
|
+
source: "context"
|
|
487
|
+
};
|
|
488
|
+
return {
|
|
489
|
+
url: DEFAULT_CLOUD_URL,
|
|
490
|
+
source: "default"
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Whether a control-plane host is one the user chose: the platform's own, or a
|
|
495
|
+
* host already in their credentials file because they signed in to it.
|
|
496
|
+
*
|
|
497
|
+
* A link file is part of the repository, and a cloned repository is not the
|
|
498
|
+
* user's to trust with their password. `login` asks before sending one to a
|
|
499
|
+
* linked host that is neither.
|
|
500
|
+
*/
|
|
501
|
+
function isKnownControlPlane(url) {
|
|
502
|
+
return url === DEFAULT_CLOUD_URL || Object.hasOwn(readCredentials().contexts, url);
|
|
472
503
|
}
|
|
473
504
|
/**
|
|
474
505
|
* Refuse to run a control-plane command in a directory linked straight at a
|
|
@@ -522,6 +553,22 @@ function createCloudClient(url) {
|
|
|
522
553
|
/** Two minutes of head-room before a token is treated as expired. */
|
|
523
554
|
var EXPIRY_BUFFER_MS = 12e4;
|
|
524
555
|
/**
|
|
556
|
+
* An access token with time left on it, refreshed first if it is close to
|
|
557
|
+
* expiry. Rejects when there is no session, or it cannot be refreshed.
|
|
558
|
+
*
|
|
559
|
+
* For the requests the SDK does not make itself. Its own calls refresh and
|
|
560
|
+
* retry on a 401; a raw `fetch` or a tunnel handshake presents whatever token
|
|
561
|
+
* it was handed, and `createCloudClient` never refreshes in the background. A
|
|
562
|
+
* token read once at the start of a command that runs for longer than it
|
|
563
|
+
* lives — a deploy's build, a `db connect` left open — is refused by the time
|
|
564
|
+
* it is used, so it is read here at the moment it is sent.
|
|
565
|
+
*/
|
|
566
|
+
async function freshAccessToken(client) {
|
|
567
|
+
const session = client.auth.getSession();
|
|
568
|
+
if (session?.accessToken && session.expiresAt > Date.now() + EXPIRY_BUFFER_MS) return session.accessToken;
|
|
569
|
+
return (await client.auth.refreshSession()).accessToken;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
525
572
|
* Return an authenticated client for the resolved host, refreshing the access
|
|
526
573
|
* token if it is close to expiry. Exits with a helpful message when there is no
|
|
527
574
|
* usable session (never logged in, or the refresh token was revoked).
|
|
@@ -2537,7 +2584,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2537
2584
|
if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
|
|
2538
2585
|
const { pinSearchPath } = await import("@rebasepro/server-postgres");
|
|
2539
2586
|
const pinnedUrl = pinSearchPath(databaseUrl);
|
|
2540
|
-
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2587
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, () => `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2541
2588
|
} else {
|
|
2542
2589
|
const dbPort = await findAvailablePort(5432);
|
|
2543
2590
|
envContent = envContent.replace(/^DATABASE_URL=.*$/m, [
|
|
@@ -3637,7 +3684,7 @@ async function runDriverDbCommand(rawArgs, options = {}) {
|
|
|
3637
3684
|
const env = { ...process.env };
|
|
3638
3685
|
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
3639
3686
|
env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? "";
|
|
3640
|
-
const { resolveDevDatabase } = await import("./resolve-
|
|
3687
|
+
const { resolveDevDatabase } = await import("./resolve-BAeRUBhg.js").then((n) => n.r);
|
|
3641
3688
|
const { readEnvFile } = await import("./project-3WCbrrfW.js").then((n) => n.u);
|
|
3642
3689
|
refuseBranchOnManagedDatabase(rawArgs, resolveDevDatabase({
|
|
3643
3690
|
flagUrl: readFlagValue(rawArgs, "--database-url"),
|
|
@@ -3807,7 +3854,7 @@ async function manageLocalDatabase(subcommand, projectRoot, rawArgs) {
|
|
|
3807
3854
|
* and the wrong one here means overwriting production with a laptop.
|
|
3808
3855
|
*/
|
|
3809
3856
|
async function pullIntoLocal(projectRoot, rawArgs) {
|
|
3810
|
-
const { anonymizeStatements, describeTarget, dumpArgs,
|
|
3857
|
+
const { anonymizeStatements, describeTarget, dumpArgs, findPgTool, restoreArgs, restoreOutcome } = await import("./pull-Dywm5FWP.js");
|
|
3811
3858
|
const { prepareDatabaseEnv } = await Promise.resolve().then(() => prepare_exports);
|
|
3812
3859
|
const { readEnvFile } = await import("./project-3WCbrrfW.js").then((n) => n.u);
|
|
3813
3860
|
const source = readFlagValue(rawArgs, "--from");
|
|
@@ -3818,8 +3865,8 @@ async function pullIntoLocal(projectRoot, rawArgs) {
|
|
|
3818
3865
|
}
|
|
3819
3866
|
const schemas = rawArgs.flatMap((arg, index) => arg === "--schema" && rawArgs[index + 1] ? [rawArgs[index + 1]] : []);
|
|
3820
3867
|
const anonymize = rawArgs.includes("--anonymize");
|
|
3821
|
-
if (!await
|
|
3822
|
-
console.error(chalk.red(
|
|
3868
|
+
for (const tool of ["pg_dump", "pg_restore"]) if (!await findPgTool(tool)) {
|
|
3869
|
+
console.error(chalk.red(`✗ \`${tool}\` is not on PATH, and this command cannot run without it.`));
|
|
3823
3870
|
console.error(chalk.gray(" Install the PostgreSQL client tools, e.g. `brew install libpq`."));
|
|
3824
3871
|
process.exit(1);
|
|
3825
3872
|
}
|
|
@@ -3876,6 +3923,7 @@ async function pullIntoLocal(projectRoot, rawArgs) {
|
|
|
3876
3923
|
}
|
|
3877
3924
|
}
|
|
3878
3925
|
const dumpFile = path.join(os.tmpdir(), `rebase-pull-${process.pid}.dump`);
|
|
3926
|
+
let failure = null;
|
|
3879
3927
|
try {
|
|
3880
3928
|
console.log(chalk.gray(" Dumping…"));
|
|
3881
3929
|
await execa("pg_dump", [
|
|
@@ -3888,12 +3936,14 @@ async function pullIntoLocal(projectRoot, rawArgs) {
|
|
|
3888
3936
|
"inherit"
|
|
3889
3937
|
] });
|
|
3890
3938
|
console.log(chalk.gray(" Restoring…"));
|
|
3891
|
-
await execa("pg_restore", restoreArgs(plan, dumpFile), {
|
|
3892
|
-
"ignore",
|
|
3893
|
-
"inherit",
|
|
3894
|
-
"inherit"
|
|
3895
|
-
|
|
3896
|
-
|
|
3939
|
+
const restored = restoreOutcome(await execa("pg_restore", restoreArgs(plan, dumpFile), {
|
|
3940
|
+
stdin: "ignore",
|
|
3941
|
+
stdout: "inherit",
|
|
3942
|
+
stderr: ["pipe", "inherit"],
|
|
3943
|
+
reject: false
|
|
3944
|
+
}));
|
|
3945
|
+
failure = restored.failure;
|
|
3946
|
+
if (anonymize && restored.started) {
|
|
3897
3947
|
const { Client } = await import("pg");
|
|
3898
3948
|
const client = new Client({ connectionString: target });
|
|
3899
3949
|
await client.connect();
|
|
@@ -3910,11 +3960,17 @@ async function pullIntoLocal(projectRoot, rawArgs) {
|
|
|
3910
3960
|
await client.end();
|
|
3911
3961
|
}
|
|
3912
3962
|
}
|
|
3913
|
-
await restoreAppRole(target);
|
|
3914
|
-
console.log(chalk.green("✓ Local database now holds a copy of " + describeTarget(source)));
|
|
3963
|
+
if (!failure) await restoreAppRole(target);
|
|
3915
3964
|
} finally {
|
|
3916
3965
|
fs.rmSync(dumpFile, { force: true });
|
|
3917
3966
|
}
|
|
3967
|
+
if (failure) {
|
|
3968
|
+
console.error(chalk.red(`✗ ${failure}`));
|
|
3969
|
+
console.error(chalk.yellow(` The local database holds an incomplete copy of ${describeTarget(source)}.`));
|
|
3970
|
+
console.error(chalk.gray(" Fix what the errors above name — often an extension or a role the source has — and pull again."));
|
|
3971
|
+
process.exit(1);
|
|
3972
|
+
}
|
|
3973
|
+
console.log(chalk.green("✓ Local database now holds a copy of " + describeTarget(source)));
|
|
3918
3974
|
}
|
|
3919
3975
|
/**
|
|
3920
3976
|
* Give the restored copy back the privileges `pg_dump` stripped.
|
|
@@ -3938,7 +3994,7 @@ async function pullIntoLocal(projectRoot, rawArgs) {
|
|
|
3938
3994
|
async function restoreAppRole(target) {
|
|
3939
3995
|
const { Client } = await import("pg");
|
|
3940
3996
|
const { detectConnectionPosture, ensureAppRole, REBASE_USER_ROLE } = await import("@rebasepro/server-postgres");
|
|
3941
|
-
const { provisionableSchemas } = await import("./pull-
|
|
3997
|
+
const { provisionableSchemas } = await import("./pull-Dywm5FWP.js");
|
|
3942
3998
|
const client = new Client({ connectionString: target });
|
|
3943
3999
|
await client.connect();
|
|
3944
4000
|
try {
|
|
@@ -4044,7 +4100,7 @@ async function listFromParentDatabase(projectRoot, rawArgs) {
|
|
|
4044
4100
|
const active = readActiveBranch(projectRoot);
|
|
4045
4101
|
if (!active) return rawArgs;
|
|
4046
4102
|
const { readEnvFile } = await import("./project-3WCbrrfW.js").then((n) => n.u);
|
|
4047
|
-
const { resolveDevDatabase } = await import("./resolve-
|
|
4103
|
+
const { resolveDevDatabase } = await import("./resolve-BAeRUBhg.js").then((n) => n.r);
|
|
4048
4104
|
const { resolveComposeUrl } = await Promise.resolve().then(() => prepare_exports);
|
|
4049
4105
|
const envFile = readEnvFile(projectRoot);
|
|
4050
4106
|
const parent = resolveDevDatabase({
|
|
@@ -4090,7 +4146,7 @@ async function listFromParentDatabase(projectRoot, rawArgs) {
|
|
|
4090
4146
|
async function switchBranch(projectRoot, rawArgs) {
|
|
4091
4147
|
const { branchDatabaseName, clearActiveBranch, databaseNameOf, readActiveBranch, writeActiveBranch } = await import("./branch-pointer-dBiClJ0p.js").then((n) => n.n);
|
|
4092
4148
|
const { readEnvFile } = await import("./project-3WCbrrfW.js").then((n) => n.u);
|
|
4093
|
-
const { resolveDevDatabase } = await import("./resolve-
|
|
4149
|
+
const { resolveDevDatabase } = await import("./resolve-BAeRUBhg.js").then((n) => n.r);
|
|
4094
4150
|
const { resolveComposeUrl } = await Promise.resolve().then(() => prepare_exports);
|
|
4095
4151
|
const goingOff = rawArgs.includes("--off") || rawArgs.includes("--main");
|
|
4096
4152
|
const name = commandWords(rawArgs, "db")[3];
|
|
@@ -4249,6 +4305,8 @@ var DB_ACTION_HELP = {
|
|
|
4249
4305
|
notes: ["Destructive, and the data exists only on this machine."]
|
|
4250
4306
|
}
|
|
4251
4307
|
};
|
|
4308
|
+
/** Every subcommand the family dispatches — the help table's keys. */
|
|
4309
|
+
var DB_SUBCOMMANDS = Object.keys(DB_ACTION_HELP);
|
|
4252
4310
|
function printDbHelp$1(action) {
|
|
4253
4311
|
const entry = action ? DB_ACTION_HELP[action] : void 0;
|
|
4254
4312
|
if (entry) {
|
|
@@ -4385,6 +4443,8 @@ var SCHEMA_ACTION_HELP = {
|
|
|
4385
4443
|
notes: ["--fix regenerates them instead of only reporting."]
|
|
4386
4444
|
}
|
|
4387
4445
|
};
|
|
4446
|
+
/** Every subcommand the family dispatches — the help table's keys. */
|
|
4447
|
+
var SCHEMA_SUBCOMMANDS = Object.keys(SCHEMA_ACTION_HELP);
|
|
4388
4448
|
function printSchemaHelp(action) {
|
|
4389
4449
|
const entry = action ? SCHEMA_ACTION_HELP[action] : void 0;
|
|
4390
4450
|
if (entry) {
|
|
@@ -6421,7 +6481,7 @@ ${chalk.green.bold("Which database")}
|
|
|
6421
6481
|
2. ${chalk.gray("DATABASE_URL")} in the shell environment
|
|
6422
6482
|
3. the branch this checkout is switched to ${chalk.gray("(rebase db branch switch)")}
|
|
6423
6483
|
4. ${chalk.gray("DATABASE_URL")} in the project's .env
|
|
6424
|
-
5. ${chalk.blue("--docker")}
|
|
6484
|
+
5. ${chalk.blue("--docker")} on this command line
|
|
6425
6485
|
6. the managed development database ${chalk.gray("(PGlite, data in .rebase/)")}
|
|
6426
6486
|
|
|
6427
6487
|
A scaffolded project sets none of 1–5, so it lands on (6): no Docker,
|
|
@@ -6470,6 +6530,25 @@ ${chalk.green.bold("Description")}
|
|
|
6470
6530
|
`);
|
|
6471
6531
|
}
|
|
6472
6532
|
//#endregion
|
|
6533
|
+
//#region src/utils/tool-stdio.ts
|
|
6534
|
+
/**
|
|
6535
|
+
* The stdio a build tool the CLI runs in the foreground gets: a static app's
|
|
6536
|
+
* build command, `tsc`, `tar`.
|
|
6537
|
+
*
|
|
6538
|
+
* Inherited, so its output streams as it happens. With `quietStdout` its stdout
|
|
6539
|
+
* goes to stderr instead, for a command whose own stdout carries a result:
|
|
6540
|
+
* `rebase cloud deploy` in JSON mode — every piped run — owes stdout exactly one
|
|
6541
|
+
* JSON value, and a `vite v6 building…` line in front of it is a result no
|
|
6542
|
+
* parser can read. The tool's output still reaches the person watching.
|
|
6543
|
+
*/
|
|
6544
|
+
function toolStdio(quietStdout) {
|
|
6545
|
+
return quietStdout ? [
|
|
6546
|
+
"ignore",
|
|
6547
|
+
2,
|
|
6548
|
+
"inherit"
|
|
6549
|
+
] : "inherit";
|
|
6550
|
+
}
|
|
6551
|
+
//#endregion
|
|
6473
6552
|
//#region src/function-portability.ts
|
|
6474
6553
|
/**
|
|
6475
6554
|
* What each of a project's custom functions depends on the *host* for.
|
|
@@ -6780,6 +6859,15 @@ function log(options, message) {
|
|
|
6780
6859
|
(options.log ?? ((m) => console.log(m)))(message);
|
|
6781
6860
|
}
|
|
6782
6861
|
/**
|
|
6862
|
+
* A line the build says to the person running it — a warning, or what it left
|
|
6863
|
+
* out — which progress callbacks may drop and this does not: stdout, or stderr
|
|
6864
|
+
* when stdout carries the caller's result.
|
|
6865
|
+
*/
|
|
6866
|
+
function say(options, line) {
|
|
6867
|
+
if (options.quietStdout) console.error(line);
|
|
6868
|
+
else console.log(line);
|
|
6869
|
+
}
|
|
6870
|
+
/**
|
|
6783
6871
|
* Every `node_modules/@types` directory the project can see.
|
|
6784
6872
|
*
|
|
6785
6873
|
* Type roots normally resolve by walking up from the tsconfig's own directory,
|
|
@@ -6873,7 +6961,8 @@ function filterProjectPaths(baseDir, projectRoot, paths, baseUrl) {
|
|
|
6873
6961
|
* `baseUrl: "../../"` into a config in a different directory silently repoints
|
|
6874
6962
|
* it at the wrong place.
|
|
6875
6963
|
*/
|
|
6876
|
-
async function writeBundleTsconfig(projectRoot, outDir, includes,
|
|
6964
|
+
async function writeBundleTsconfig(projectRoot, outDir, includes, options) {
|
|
6965
|
+
const skipTypeCheck = options.skipTypeCheck === true;
|
|
6877
6966
|
const tsconfigDir = path.join(projectRoot, ".rebase");
|
|
6878
6967
|
const fromTsconfig = (target) => {
|
|
6879
6968
|
return path.relative(tsconfigDir, path.resolve(projectRoot, target)).split(path.sep).join("/");
|
|
@@ -6890,7 +6979,7 @@ async function writeBundleTsconfig(projectRoot, outDir, includes, skipTypeCheck)
|
|
|
6890
6979
|
baseUrl: fromTsconfig("."),
|
|
6891
6980
|
paths: kept
|
|
6892
6981
|
};
|
|
6893
|
-
if (dropped.length > 0)
|
|
6982
|
+
if (dropped.length > 0) say(options, chalk.dim(` ignoring ${dropped.length} path alias(es) pointing outside the project (${dropped.join(", ")}) — resolving those from node_modules instead`));
|
|
6894
6983
|
}
|
|
6895
6984
|
const compilerOptions = {
|
|
6896
6985
|
target: "ES2022",
|
|
@@ -7668,8 +7757,20 @@ function composeBundleManifest(input) {
|
|
|
7668
7757
|
build: input.build
|
|
7669
7758
|
};
|
|
7670
7759
|
}
|
|
7760
|
+
/**
|
|
7761
|
+
* What a build refusing on {@link detectDeclaredDepConflicts} says: every name,
|
|
7762
|
+
* both ranges and where each is declared.
|
|
7763
|
+
*/
|
|
7764
|
+
function describeDepConflicts(conflicts) {
|
|
7765
|
+
const lines = [`${conflicts.length} dependency range(s) no single version satisfies:`];
|
|
7766
|
+
for (const { name, declarations: [first, second] } of conflicts) lines.push(` ${name}`, ` ${first.range} (${first.file})`, ` ${second.range} (${second.file})`);
|
|
7767
|
+
lines.push(" The bundle installs one range; the other half of the project was built against the other.", " Declare one range, in the app that uses it.");
|
|
7768
|
+
return lines.join("\n");
|
|
7769
|
+
}
|
|
7671
7770
|
async function buildBundle(options) {
|
|
7672
7771
|
const { projectRoot, app, appName } = options;
|
|
7772
|
+
const conflicts = detectDeclaredDepConflicts(projectRoot);
|
|
7773
|
+
if (conflicts.length > 0) throw new Error(describeDepConflicts(conflicts));
|
|
7673
7774
|
const paths = resolveBackendPaths(app, projectRoot);
|
|
7674
7775
|
const outDir = path.resolve(projectRoot, options.outDir ?? "dist-bundle");
|
|
7675
7776
|
const includes = [];
|
|
@@ -7690,22 +7791,22 @@ async function buildBundle(options) {
|
|
|
7690
7791
|
"the schema"
|
|
7691
7792
|
];
|
|
7692
7793
|
const compiled = `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7794
|
+
say(options, chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
|
|
7795
|
+
say(options, chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
|
|
7796
|
+
say(options, chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
|
|
7797
|
+
say(options, chalk.dim(" or run `rebase eject`, which writes an entrypoint of its own and owns"));
|
|
7798
|
+
say(options, chalk.dim(" the image — it does not adopt this file, and will not replace it"));
|
|
7799
|
+
say(options, chalk.dim(" without --force."));
|
|
7699
7800
|
}
|
|
7700
7801
|
log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
|
|
7701
7802
|
cleanOutDir(projectRoot, outDir);
|
|
7702
|
-
const tsconfigPath = await writeBundleTsconfig(projectRoot, outDir, includes, options
|
|
7803
|
+
const tsconfigPath = await writeBundleTsconfig(projectRoot, outDir, includes, options);
|
|
7703
7804
|
const tsc = resolveLocalBin(projectRoot, "tsc");
|
|
7704
7805
|
if (!tsc) throw new Error("TypeScript is not installed in this project. Run your package manager's install first.");
|
|
7705
7806
|
try {
|
|
7706
7807
|
await execa(tsc, ["-p", tsconfigPath], {
|
|
7707
7808
|
cwd: projectRoot,
|
|
7708
|
-
stdio:
|
|
7809
|
+
stdio: toolStdio(options.quietStdout)
|
|
7709
7810
|
});
|
|
7710
7811
|
} catch {
|
|
7711
7812
|
throw new Error("TypeScript compilation failed — the bundle was not written.");
|
|
@@ -7713,9 +7814,9 @@ async function buildBundle(options) {
|
|
|
7713
7814
|
const normalized = normalizeEsmSpecifiers(outDir);
|
|
7714
7815
|
if (normalized.rewritten > 0) log(options, chalk.dim(` resolved ${normalized.rewritten} relative import(s) for Node ESM`));
|
|
7715
7816
|
if (normalized.unresolved.length > 0) {
|
|
7716
|
-
|
|
7717
|
-
for (const item of normalized.unresolved.slice(0, 5))
|
|
7718
|
-
if (normalized.unresolved.length > 5)
|
|
7817
|
+
say(options, chalk.yellow(` ⚠ ${normalized.unresolved.length} import(s) could not be resolved to a file:`));
|
|
7818
|
+
for (const item of normalized.unresolved.slice(0, 5)) say(options, chalk.dim(` ${item}`));
|
|
7819
|
+
if (normalized.unresolved.length > 5) say(options, chalk.dim(` … and ${normalized.unresolved.length - 5} more`));
|
|
7719
7820
|
}
|
|
7720
7821
|
const compiledConfigDir = path.join(outDir, paths.config);
|
|
7721
7822
|
const compiledCollectionsDir = path.join(compiledConfigDir, "collections");
|
|
@@ -7751,7 +7852,7 @@ async function buildBundle(options) {
|
|
|
7751
7852
|
...requires.length > 0 ? { requires } : {}
|
|
7752
7853
|
};
|
|
7753
7854
|
});
|
|
7754
|
-
for (const line of summarisePortability(functionReports))
|
|
7855
|
+
for (const line of summarisePortability(functionReports)) say(options, line.trimStart().startsWith("⚠") ? chalk.yellow(line) : chalk.dim(line));
|
|
7755
7856
|
const manifest = composeBundleManifest({
|
|
7756
7857
|
runtimeRange: options.runtimeRange,
|
|
7757
7858
|
builtAgainst: resolveServerVersion(projectRoot),
|
|
@@ -8322,7 +8423,7 @@ async function foldFrontendIntoBundle(options) {
|
|
|
8322
8423
|
for (const app of apps) {
|
|
8323
8424
|
if (app.build && !skipBuild) await execa(app.build, {
|
|
8324
8425
|
cwd: projectRoot,
|
|
8325
|
-
stdio:
|
|
8426
|
+
stdio: toolStdio(options.quietStdout),
|
|
8326
8427
|
shell: true,
|
|
8327
8428
|
env: staticBuildEnv(app.path, app.name)
|
|
8328
8429
|
});
|
|
@@ -8942,6 +9043,9 @@ async function buildCommand(rawArgs = []) {
|
|
|
8942
9043
|
await runWorkspaceBuilds(projectRoot);
|
|
8943
9044
|
return;
|
|
8944
9045
|
}
|
|
9046
|
+
const output = args["--output"] ? path.resolve(process.cwd(), args["--output"]) : void 0;
|
|
9047
|
+
const bundled = targets.filter(({ app }) => app.type === "backend" && app.runtime !== "custom" || app.type === "static" && Boolean(app.build) && Boolean(app.output));
|
|
9048
|
+
if (output && bundled.length > 1) throw new UsageError(`--output names one bundle directory, and this build produces ${bundled.length}: ${bundled.map((t) => t.name).join(", ")}. Build one app into it: rebase build ${bundled[0].name} --output ${args["--output"]}`);
|
|
8945
9049
|
console.log(`${chalk.bold("Rebase")} — building ${targets.length} app(s)\n`);
|
|
8946
9050
|
for (const { name, app } of targets) {
|
|
8947
9051
|
console.log(chalk.cyan(`▸ ${name}`) + chalk.dim(` (${app.type})`));
|
|
@@ -8962,25 +9066,11 @@ async function buildCommand(rawArgs = []) {
|
|
|
8962
9066
|
}
|
|
8963
9067
|
writeResourceGraphFile(projectRoot, resourceGraph);
|
|
8964
9068
|
const drift = detectFrameworkDepDrift(projectRoot, cliVersion$1());
|
|
8965
|
-
if (drift.conflicting.length > 0) {
|
|
8966
|
-
console.error(chalk.red(`\n ✗ ${drift.conflicting.length} dependency range(s) no single version satisfies:\n`));
|
|
8967
|
-
for (const conflict of drift.conflicting) {
|
|
8968
|
-
const [first, second] = conflict.declarations;
|
|
8969
|
-
console.error(` ${chalk.bold(conflict.name)}`);
|
|
8970
|
-
console.error(chalk.dim(` ${first.range} (${first.file})`));
|
|
8971
|
-
console.error(chalk.dim(` ${second.range} (${second.file})`));
|
|
8972
|
-
}
|
|
8973
|
-
console.error("");
|
|
8974
|
-
console.error(chalk.dim(" The bundle installs one range; the other half of the project was built"));
|
|
8975
|
-
console.error(chalk.dim(" against the other. Declare one range, in the app that uses it.\n"));
|
|
8976
|
-
process.exitCode = 1;
|
|
8977
|
-
return;
|
|
8978
|
-
}
|
|
8979
9069
|
const result = await buildBundle({
|
|
8980
9070
|
projectRoot,
|
|
8981
9071
|
appName: name,
|
|
8982
9072
|
app,
|
|
8983
|
-
outDir:
|
|
9073
|
+
outDir: output,
|
|
8984
9074
|
runtimeRange: manifest.rebase,
|
|
8985
9075
|
resources: resourceGraph,
|
|
8986
9076
|
skipTypeCheck: args["--skip-type-check"],
|
|
@@ -9032,7 +9122,10 @@ async function buildCommand(rawArgs = []) {
|
|
|
9032
9122
|
});
|
|
9033
9123
|
for (const outcome of folded ?? []) console.log(chalk.green(` ✓ ${outcome.appName} folded in`) + chalk.dim(` (${outcome.fileCount} file(s) → served at ${outcome.path})`));
|
|
9034
9124
|
}
|
|
9035
|
-
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase,
|
|
9125
|
+
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, output).catch((err) => {
|
|
9126
|
+
console.error(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
|
|
9127
|
+
process.exit(1);
|
|
9128
|
+
});
|
|
9036
9129
|
console.log("");
|
|
9037
9130
|
}
|
|
9038
9131
|
console.log(chalk.green("✓ Build complete."));
|
|
@@ -9044,41 +9137,40 @@ async function buildCommand(rawArgs = []) {
|
|
|
9044
9137
|
* packages that output into a `static`-kind bundle — the same deployable shape as
|
|
9045
9138
|
* a backend bundle, so a frontend or admin app deploys through the identical
|
|
9046
9139
|
* path and runs on the identical image, just serving files instead of an API.
|
|
9140
|
+
*
|
|
9141
|
+
* Throws when the build fails or produces nothing usable, so the caller answers
|
|
9142
|
+
* in its own way: `rebase build` with a red line, `rebase cloud deploy --json`
|
|
9143
|
+
* with its JSON error.
|
|
9047
9144
|
*/
|
|
9048
|
-
async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride) {
|
|
9145
|
+
async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride, options = {}) {
|
|
9049
9146
|
const asset = app;
|
|
9050
9147
|
const basePath = asset.path ?? "/";
|
|
9148
|
+
const say = (line) => {
|
|
9149
|
+
if (options.quietStdout) console.error(line);
|
|
9150
|
+
else console.log(line);
|
|
9151
|
+
};
|
|
9051
9152
|
if (!asset.build) {
|
|
9052
|
-
|
|
9153
|
+
say(chalk.dim(" no build command declared — skipping"));
|
|
9053
9154
|
return;
|
|
9054
9155
|
}
|
|
9055
9156
|
try {
|
|
9056
9157
|
await execa(asset.build, {
|
|
9057
9158
|
cwd: projectRoot,
|
|
9058
|
-
stdio:
|
|
9159
|
+
stdio: toolStdio(options.quietStdout),
|
|
9059
9160
|
shell: true,
|
|
9060
9161
|
env: staticBuildEnv(basePath, name)
|
|
9061
9162
|
});
|
|
9062
9163
|
} catch {
|
|
9063
|
-
|
|
9064
|
-
process.exit(1);
|
|
9164
|
+
throw new Error(`build command failed for "${name}"`);
|
|
9065
9165
|
}
|
|
9066
9166
|
if (!asset.output) {
|
|
9067
|
-
|
|
9167
|
+
say(chalk.yellow(" no output directory declared — built, but nothing to bundle"));
|
|
9068
9168
|
return;
|
|
9069
9169
|
}
|
|
9070
9170
|
const outputPath = path.join(projectRoot, asset.output);
|
|
9071
|
-
if (!fs.existsSync(outputPath)) {
|
|
9072
|
-
console.error(chalk.red(` ✗ declared output "${asset.output}" does not exist after building`));
|
|
9073
|
-
process.exit(1);
|
|
9074
|
-
}
|
|
9171
|
+
if (!fs.existsSync(outputPath)) throw new Error(`declared output "${asset.output}" does not exist after building`);
|
|
9075
9172
|
const indexHtml = path.join(outputPath, "index.html");
|
|
9076
|
-
if (fs.existsSync(indexHtml))
|
|
9077
|
-
assertBuiltForPath(fs.readFileSync(indexHtml, "utf8"), basePath, name);
|
|
9078
|
-
} catch (err) {
|
|
9079
|
-
console.error(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
|
|
9080
|
-
process.exit(1);
|
|
9081
|
-
}
|
|
9173
|
+
if (fs.existsSync(indexHtml)) assertBuiltForPath(fs.readFileSync(indexHtml, "utf8"), basePath, name);
|
|
9082
9174
|
const result = buildStaticBundle({
|
|
9083
9175
|
projectRoot,
|
|
9084
9176
|
appName: name,
|
|
@@ -9090,7 +9182,7 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
9090
9182
|
cms: asset.cms
|
|
9091
9183
|
});
|
|
9092
9184
|
const rel = path.relative(projectRoot, result.outDir);
|
|
9093
|
-
|
|
9185
|
+
say(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s) → served at ${basePath})`));
|
|
9094
9186
|
return result.outDir;
|
|
9095
9187
|
}
|
|
9096
9188
|
/**
|
|
@@ -9210,7 +9302,7 @@ function renderPayload(contents, shape, projectName) {
|
|
|
9210
9302
|
};
|
|
9211
9303
|
}
|
|
9212
9304
|
if (open) throw new Error(`Eject template: {{#${open.name}}} was never closed.`);
|
|
9213
|
-
return out.join("\n").replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
9305
|
+
return out.join("\n").replace(/\{\{PROJECT_NAME\}\}/g, () => projectName);
|
|
9214
9306
|
}
|
|
9215
9307
|
/** Files the eject payload contributes, as `<source> → <destination>`. */
|
|
9216
9308
|
var PAYLOAD = [
|
|
@@ -9781,7 +9873,59 @@ async function resetPassword(rawArgs) {
|
|
|
9781
9873
|
env.REBASE_RESET_EMAIL = email;
|
|
9782
9874
|
env.REBASE_RESET_PASSWORD = newPassword;
|
|
9783
9875
|
env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, ".env");
|
|
9784
|
-
const
|
|
9876
|
+
const { prepareDatabaseEnv } = await Promise.resolve().then(() => prepare_exports);
|
|
9877
|
+
const prepared = await prepareDatabaseEnv(projectRoot, { onProgress: (message) => console.log(chalk.gray(` ${message}`)) });
|
|
9878
|
+
Object.assign(env, prepared.env);
|
|
9879
|
+
const scriptContent = resetPasswordScript(wasGenerated);
|
|
9880
|
+
const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
|
|
9881
|
+
fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
|
|
9882
|
+
console.log("");
|
|
9883
|
+
console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)"));
|
|
9884
|
+
console.log("");
|
|
9885
|
+
console.log(` ${chalk.gray("Email:")} ${email}`);
|
|
9886
|
+
console.log(` ${chalk.gray("Database:")} ${prepared.description}`);
|
|
9887
|
+
if (!wasGenerated) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
|
|
9888
|
+
console.log("");
|
|
9889
|
+
const child = spawn(tsxBin, [tmpScriptPath], {
|
|
9890
|
+
cwd: backendDir,
|
|
9891
|
+
stdio: "inherit",
|
|
9892
|
+
env
|
|
9893
|
+
});
|
|
9894
|
+
const cleanup = () => {
|
|
9895
|
+
try {
|
|
9896
|
+
fs.unlinkSync(tmpScriptPath);
|
|
9897
|
+
} catch {}
|
|
9898
|
+
};
|
|
9899
|
+
return new Promise((resolve) => {
|
|
9900
|
+
child.on("error", (err) => {
|
|
9901
|
+
cleanup();
|
|
9902
|
+
console.error(chalk.red("✗ Could not run the reset script."));
|
|
9903
|
+
console.error(chalk.gray(` ${err.message}`));
|
|
9904
|
+
process.exit(1);
|
|
9905
|
+
});
|
|
9906
|
+
child.on("close", (code) => {
|
|
9907
|
+
cleanup();
|
|
9908
|
+
if (code !== 0) process.exit(code ?? 1);
|
|
9909
|
+
resolve();
|
|
9910
|
+
});
|
|
9911
|
+
});
|
|
9912
|
+
} catch (err) {
|
|
9913
|
+
console.error(chalk.red("✗ Direct database update failed."));
|
|
9914
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
9915
|
+
process.exit(1);
|
|
9916
|
+
}
|
|
9917
|
+
}
|
|
9918
|
+
/**
|
|
9919
|
+
* The script the direct-database fallback runs under the project's own tsx, so
|
|
9920
|
+
* it resolves the project's `@rebasepro/server-postgres` and schema.
|
|
9921
|
+
*
|
|
9922
|
+
* It exits 0 only when a row was updated: a missing user and a thrown error are
|
|
9923
|
+
* both exit 1. `echoPassword` prints the new password, for a generated one.
|
|
9924
|
+
*
|
|
9925
|
+
* Exported so its tests can run it.
|
|
9926
|
+
*/
|
|
9927
|
+
function resetPasswordScript(echoPassword) {
|
|
9928
|
+
return `
|
|
9785
9929
|
import { createPostgresDatabaseConnection } from "@rebasepro/server-postgres";
|
|
9786
9930
|
import { hashPassword } from "@rebasepro/server";
|
|
9787
9931
|
import { eq } from "drizzle-orm";
|
|
@@ -9826,7 +9970,7 @@ async function resetPassword() {
|
|
|
9826
9970
|
|
|
9827
9971
|
if (result.length > 0) {
|
|
9828
9972
|
console.log("✅ Password reset for: " + result[0].email);
|
|
9829
|
-
${
|
|
9973
|
+
${echoPassword ? "console.log(\" New password: \" + newPassword);" : ""}
|
|
9830
9974
|
process.exit(0);
|
|
9831
9975
|
}
|
|
9832
9976
|
// Nothing was updated, so nothing was reset. Exiting 0 here reported
|
|
@@ -9835,44 +9979,13 @@ async function resetPassword() {
|
|
|
9835
9979
|
process.exit(1);
|
|
9836
9980
|
}
|
|
9837
9981
|
|
|
9838
|
-
|
|
9982
|
+
// A thrown error is a failed reset, and exits like one — a refused connection
|
|
9983
|
+
// that exits 0 reads as a reset to anything checking the exit code.
|
|
9984
|
+
resetPassword().catch((error) => {
|
|
9985
|
+
console.error(error);
|
|
9986
|
+
process.exit(1);
|
|
9987
|
+
});
|
|
9839
9988
|
`;
|
|
9840
|
-
const tmpScriptPath = path.join(backendDir, ".tmp-reset-password.ts");
|
|
9841
|
-
fs.writeFileSync(tmpScriptPath, scriptContent, "utf-8");
|
|
9842
|
-
console.log("");
|
|
9843
|
-
console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)"));
|
|
9844
|
-
console.log("");
|
|
9845
|
-
console.log(` ${chalk.gray("Email:")} ${email}`);
|
|
9846
|
-
if (!wasGenerated) console.log(` ${chalk.gray("Password:")} ${"*".repeat(newPassword.length)}`);
|
|
9847
|
-
console.log("");
|
|
9848
|
-
const child = spawn(tsxBin, [tmpScriptPath], {
|
|
9849
|
-
cwd: backendDir,
|
|
9850
|
-
stdio: "inherit",
|
|
9851
|
-
env
|
|
9852
|
-
});
|
|
9853
|
-
const cleanup = () => {
|
|
9854
|
-
try {
|
|
9855
|
-
fs.unlinkSync(tmpScriptPath);
|
|
9856
|
-
} catch {}
|
|
9857
|
-
};
|
|
9858
|
-
return new Promise((resolve) => {
|
|
9859
|
-
child.on("error", (err) => {
|
|
9860
|
-
cleanup();
|
|
9861
|
-
console.error(chalk.red("✗ Could not run the reset script."));
|
|
9862
|
-
console.error(chalk.gray(` ${err.message}`));
|
|
9863
|
-
process.exit(1);
|
|
9864
|
-
});
|
|
9865
|
-
child.on("close", (code) => {
|
|
9866
|
-
cleanup();
|
|
9867
|
-
if (code !== 0) process.exit(code ?? 1);
|
|
9868
|
-
resolve();
|
|
9869
|
-
});
|
|
9870
|
-
});
|
|
9871
|
-
} catch (err) {
|
|
9872
|
-
console.error(chalk.red("✗ Direct database update failed."));
|
|
9873
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
9874
|
-
process.exit(1);
|
|
9875
|
-
}
|
|
9876
9989
|
}
|
|
9877
9990
|
function printAuthHelp() {
|
|
9878
9991
|
console.log(`
|
|
@@ -10259,7 +10372,8 @@ function findLibpqUrlProblems(projectRoot, envFile) {
|
|
|
10259
10372
|
return findings;
|
|
10260
10373
|
}
|
|
10261
10374
|
/**
|
|
10262
|
-
* Report unparseable connection strings, if any.
|
|
10375
|
+
* Report unparseable connection strings, if any. Returns true when there were,
|
|
10376
|
+
* so the finding reaches the exit code like the environment errors do.
|
|
10263
10377
|
*
|
|
10264
10378
|
* Runs before the plugin's drift check and never blocks it. The plugin connects
|
|
10265
10379
|
* through node-postgres, which parses these URLs happily — so it cannot see this
|
|
@@ -10267,7 +10381,7 @@ function findLibpqUrlProblems(projectRoot, envFile) {
|
|
|
10267
10381
|
* `rebase db backup` fails.
|
|
10268
10382
|
*/
|
|
10269
10383
|
function reportLibpqUrlProblems(findings) {
|
|
10270
|
-
if (findings.length === 0) return;
|
|
10384
|
+
if (findings.length === 0) return false;
|
|
10271
10385
|
console.log("");
|
|
10272
10386
|
console.log(chalk.red.bold(" ✗ Connection string that PostgreSQL's own tools cannot parse"));
|
|
10273
10387
|
console.log("");
|
|
@@ -10287,6 +10401,7 @@ function reportLibpqUrlProblems(findings) {
|
|
|
10287
10401
|
console.log(chalk.gray(" is why a project can look healthy and still have no working backups."));
|
|
10288
10402
|
console.log(chalk.gray(" Projects scaffolded before 2026-08-18 all carry it."));
|
|
10289
10403
|
console.log("");
|
|
10404
|
+
return true;
|
|
10290
10405
|
}
|
|
10291
10406
|
/**
|
|
10292
10407
|
* `--help` is answered before the project guard, not after.
|
|
@@ -10592,9 +10707,10 @@ async function doctorCommand(rawArgs) {
|
|
|
10592
10707
|
const env = { ...process.env };
|
|
10593
10708
|
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
10594
10709
|
env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? "";
|
|
10595
|
-
reportLibpqUrlProblems(findLibpqUrlProblems(projectRoot, envFile));
|
|
10710
|
+
const connectionFailed = reportLibpqUrlProblems(findLibpqUrlProblems(projectRoot, envFile));
|
|
10596
10711
|
reportFunctionPortability(projectRoot);
|
|
10597
10712
|
const environmentFailed = reportEnvironmentFindings(collectEnvironmentFindings(projectRoot, envFile));
|
|
10713
|
+
const failed = connectionFailed || environmentFailed;
|
|
10598
10714
|
try {
|
|
10599
10715
|
const prepared = await prepareDatabaseEnv(projectRoot, { onProgress: (message) => console.log(chalk.gray(` ${message}`)) });
|
|
10600
10716
|
Object.assign(env, prepared.env);
|
|
@@ -10607,7 +10723,7 @@ async function doctorCommand(rawArgs) {
|
|
|
10607
10723
|
if (!hasDeclaredCollections(projectRoot)) {
|
|
10608
10724
|
console.log("");
|
|
10609
10725
|
console.log(chalk.gray(" ○ Schema drift not checked: this project derives its API from the database — run `rebase schema introspect` first."));
|
|
10610
|
-
if (
|
|
10726
|
+
if (failed) process.exit(1);
|
|
10611
10727
|
return;
|
|
10612
10728
|
}
|
|
10613
10729
|
try {
|
|
@@ -10628,7 +10744,7 @@ async function doctorCommand(rawArgs) {
|
|
|
10628
10744
|
reportSpawnFailure(error);
|
|
10629
10745
|
process.exit(1);
|
|
10630
10746
|
}
|
|
10631
|
-
if (
|
|
10747
|
+
if (failed) process.exit(1);
|
|
10632
10748
|
}
|
|
10633
10749
|
//#endregion
|
|
10634
10750
|
//#region src/commands/resources.ts
|
|
@@ -12108,7 +12224,8 @@ async function loginCommand(rawArgs) {
|
|
|
12108
12224
|
maxPositionals: 0
|
|
12109
12225
|
});
|
|
12110
12226
|
refuseDirectLink(rawArgs);
|
|
12111
|
-
const url =
|
|
12227
|
+
const { url, source } = resolveCloudTarget(rawArgs);
|
|
12228
|
+
if (source === "link" && !isKnownControlPlane(url)) await confirmLinkedControlPlane(url);
|
|
12112
12229
|
noteBlank();
|
|
12113
12230
|
note(`Signing in to ${chalk.cyan(url)}`);
|
|
12114
12231
|
noteBlank();
|
|
@@ -12161,6 +12278,31 @@ async function loginCommand(rawArgs) {
|
|
|
12161
12278
|
reportError(e, "Login failed");
|
|
12162
12279
|
}
|
|
12163
12280
|
}
|
|
12281
|
+
/**
|
|
12282
|
+
* Ask before sending a password to a control plane a repository named.
|
|
12283
|
+
*
|
|
12284
|
+
* `.rebase/cloud.json` is committed with the code, so a cloned repository can
|
|
12285
|
+
* point its cloud commands at any host — and "Not logged in to <host>" invites
|
|
12286
|
+
* the login that would hand that host the user's email and password. A host
|
|
12287
|
+
* the user has signed in to before, or the platform's own, is theirs; any other
|
|
12288
|
+
* one is named and confirmed at a terminal, and refused without one: `--url`
|
|
12289
|
+
* is how a script says it means that host.
|
|
12290
|
+
*/
|
|
12291
|
+
async function confirmLinkedControlPlane(url) {
|
|
12292
|
+
const message = `This directory's .rebase/cloud.json names the control plane ${url}, which you have not signed in to before.`;
|
|
12293
|
+
if (isJsonMode() || process.stdin.isTTY !== true) fail(message, `Your email and password would be sent there. To sign in to it on purpose, pass --url ${url}.`, "unknown_control_plane");
|
|
12294
|
+
warn(message, "Your Rebase Cloud email and password will be sent to it.");
|
|
12295
|
+
const { confirmed } = await inquirer.prompt([{
|
|
12296
|
+
type: "confirm",
|
|
12297
|
+
name: "confirmed",
|
|
12298
|
+
default: false,
|
|
12299
|
+
message: `Sign in to ${url}?`
|
|
12300
|
+
}]);
|
|
12301
|
+
if (!confirmed) {
|
|
12302
|
+
console.error(chalk.gray(" Aborted."));
|
|
12303
|
+
process.exit(0);
|
|
12304
|
+
}
|
|
12305
|
+
}
|
|
12164
12306
|
async function logoutCommand(rawArgs) {
|
|
12165
12307
|
parseCloudArgs({
|
|
12166
12308
|
spec: {},
|
|
@@ -13742,10 +13884,7 @@ async function dbConnect(rawArgs) {
|
|
|
13742
13884
|
}
|
|
13743
13885
|
const requested = args["--port"] ?? DEFAULT_LOCAL_PORT;
|
|
13744
13886
|
if (!Number.isInteger(requested) || requested < 0 || requested > 65535) fail(`--port must be a port number, not ${requested}.`, void 0, "bad_port");
|
|
13745
|
-
const
|
|
13746
|
-
const server = net$1.createServer((socket) => {
|
|
13747
|
-
pipeThroughTunnel(socket, endpoint, client.auth.getSession()?.accessToken ?? "");
|
|
13748
|
-
});
|
|
13887
|
+
const server = tunnelListener(client, tunnelUrl(url, projectId));
|
|
13749
13888
|
server.on("error", (err) => {
|
|
13750
13889
|
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");
|
|
13751
13890
|
fail(`Could not open a local listener: ${err.message}`, void 0, "listen_failed");
|
|
@@ -13794,6 +13933,30 @@ async function dbConnect(rawArgs) {
|
|
|
13794
13933
|
});
|
|
13795
13934
|
}
|
|
13796
13935
|
/**
|
|
13936
|
+
* The local listener: each connection it accepts gets its own tunnel, signed in
|
|
13937
|
+
* with a token that is current when the connection arrives.
|
|
13938
|
+
*
|
|
13939
|
+
* The command is left running for as long as the developer works, and the
|
|
13940
|
+
* access token it started with lapses within the hour. Every connection is
|
|
13941
|
+
* authenticated on its own, so each one reads the token then, refreshing it
|
|
13942
|
+
* first when it is close to expiry — rather than the one read at startup, which
|
|
13943
|
+
* the control plane refuses for every connection opened after it expires.
|
|
13944
|
+
*
|
|
13945
|
+
* The socket is paused while the token is read, so a client's first bytes wait
|
|
13946
|
+
* for the tunnel exactly as they do once it is being established.
|
|
13947
|
+
*/
|
|
13948
|
+
function tunnelListener(client, endpoint) {
|
|
13949
|
+
return net$1.createServer((socket) => {
|
|
13950
|
+
socket.pause();
|
|
13951
|
+
freshAccessToken(client).then((token) => {
|
|
13952
|
+
if (!socket.destroyed) pipeThroughTunnel(socket, endpoint, token);
|
|
13953
|
+
}, () => {
|
|
13954
|
+
warn("A connection was closed: the session this tunnel signs in with has expired and could not be refreshed.", "Run `rebase cloud login`, then connect again.");
|
|
13955
|
+
socket.destroy();
|
|
13956
|
+
});
|
|
13957
|
+
});
|
|
13958
|
+
}
|
|
13959
|
+
/**
|
|
13797
13960
|
* One accepted connection, carried over one WebSocket.
|
|
13798
13961
|
*
|
|
13799
13962
|
* The local socket is paused until the tunnel says `ready`, so a client that
|
|
@@ -13814,7 +13977,7 @@ function pipeThroughTunnel(socket, endpoint, token) {
|
|
|
13814
13977
|
ws.binaryType = "arraybuffer";
|
|
13815
13978
|
let ready = false;
|
|
13816
13979
|
const closeBoth = (reason) => {
|
|
13817
|
-
if (reason && !ready)
|
|
13980
|
+
if (reason && !ready) warn(`A connection was refused: ${reason}`);
|
|
13818
13981
|
try {
|
|
13819
13982
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
|
|
13820
13983
|
} catch {}
|
|
@@ -13869,6 +14032,15 @@ function pipeThroughTunnel(socket, endpoint, token) {
|
|
|
13869
14032
|
}
|
|
13870
14033
|
//#endregion
|
|
13871
14034
|
//#region src/commands/cloud/databases.ts
|
|
14035
|
+
/** The filename a backup is restored and downloaded by. */
|
|
14036
|
+
function backupName(row) {
|
|
14037
|
+
return row?.name ?? row?.filename;
|
|
14038
|
+
}
|
|
14039
|
+
/** A backup's size for a person: the control plane's own string, or bytes as MB. */
|
|
14040
|
+
function backupSize(row) {
|
|
14041
|
+
if (typeof row.size === "string") return row.size;
|
|
14042
|
+
if (typeof row.size === "number" && Number.isFinite(row.size)) return `${(row.size / 1024 / 1024).toFixed(1)} MB`;
|
|
14043
|
+
}
|
|
13872
14044
|
async function dbCommand(subcommand, rawArgs) {
|
|
13873
14045
|
switch (subcommand) {
|
|
13874
14046
|
case "list":
|
|
@@ -14260,7 +14432,7 @@ async function backupCommand(rawArgs) {
|
|
|
14260
14432
|
type: "manual"
|
|
14261
14433
|
}, { path: "create" });
|
|
14262
14434
|
if (!res.success) fail(res.error || "Backup failed.");
|
|
14263
|
-
emit(() => success(`Backup created: ${res.backup
|
|
14435
|
+
emit(() => success(`Backup created: ${backupName(res.backup) ?? "(unnamed)"}`), {
|
|
14264
14436
|
success: true,
|
|
14265
14437
|
backup: res.backup ?? null
|
|
14266
14438
|
});
|
|
@@ -14339,8 +14511,12 @@ async function backupCommand(rawArgs) {
|
|
|
14339
14511
|
return;
|
|
14340
14512
|
}
|
|
14341
14513
|
for (const b of res.backups) {
|
|
14342
|
-
const
|
|
14343
|
-
|
|
14514
|
+
const detail = [
|
|
14515
|
+
b.type,
|
|
14516
|
+
backupSize(b),
|
|
14517
|
+
b.date
|
|
14518
|
+
].filter(Boolean).join(" ");
|
|
14519
|
+
console.log(` ${chalk.bold(backupName(b) ?? "(unnamed)")} ${chalk.gray(detail)}`);
|
|
14344
14520
|
}
|
|
14345
14521
|
console.log("");
|
|
14346
14522
|
}, {
|
|
@@ -15033,26 +15209,29 @@ function readBundleManifest(bundleDir) {
|
|
|
15033
15209
|
return manifest;
|
|
15034
15210
|
}
|
|
15035
15211
|
/**
|
|
15212
|
+
* The control plane's cap on an uploaded bundle: its `MAX_BUNDLE_BYTES`, which
|
|
15213
|
+
* it checks before reading the body.
|
|
15214
|
+
*/
|
|
15215
|
+
var MAX_BUNDLE_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
15216
|
+
/**
|
|
15036
15217
|
* Tar a built bundle into a gzipped archive.
|
|
15037
15218
|
*
|
|
15038
|
-
* `node_modules`
|
|
15039
|
-
* the
|
|
15040
|
-
*
|
|
15041
|
-
* platform-specific build that will not run on the runtime image.
|
|
15219
|
+
* `node_modules` goes in only with `withModules`: a tree the build vendored for
|
|
15220
|
+
* the runtime's platform is the point of vendoring, while one installed into a
|
|
15221
|
+
* prebuilt bundle by hand was installed for whatever machine ran it.
|
|
15042
15222
|
*/
|
|
15043
|
-
function packBundle(bundleDir, outPath) {
|
|
15223
|
+
function packBundle(bundleDir, outPath, options = {}) {
|
|
15044
15224
|
return new Promise((resolve, reject) => {
|
|
15045
15225
|
const child = spawn("tar", [
|
|
15046
15226
|
"-czf",
|
|
15047
15227
|
outPath,
|
|
15048
15228
|
"--no-xattrs",
|
|
15049
|
-
"--exclude",
|
|
15050
|
-
"node_modules",
|
|
15229
|
+
...options.withModules ? [] : ["--exclude", "node_modules"],
|
|
15051
15230
|
"-C",
|
|
15052
15231
|
bundleDir,
|
|
15053
15232
|
"."
|
|
15054
15233
|
], {
|
|
15055
|
-
stdio:
|
|
15234
|
+
stdio: toolStdio(true),
|
|
15056
15235
|
env: {
|
|
15057
15236
|
...process.env,
|
|
15058
15237
|
COPYFILE_DISABLE: "1"
|
|
@@ -15063,6 +15242,35 @@ function packBundle(bundleDir, outPath) {
|
|
|
15063
15242
|
});
|
|
15064
15243
|
}
|
|
15065
15244
|
/**
|
|
15245
|
+
* Pack a bundle for upload — without `node_modules`, even a vendored tree.
|
|
15246
|
+
*
|
|
15247
|
+
* Vendoring exists so a pod untars and boots rather than spending 35-55s of
|
|
15248
|
+
* every start in `npm install`, and a deploy did carry the tree for one night
|
|
15249
|
+
* (c36ea4637). It is left out again because of what the control plane does
|
|
15250
|
+
* with the archive: `GET /bundle/:projectId/:bundleId` reads the whole object
|
|
15251
|
+
* into memory on every pod start, and a runtime rollout restarts every tenant's
|
|
15252
|
+
* pods at once. Bundles it was sized against are a few hundred kB; a vendored
|
|
15253
|
+
* one is tens of MB, up to the 100 MB cap — N pods starting together would hold
|
|
15254
|
+
* N of them. A slower cold start per pod is the status quo every deploy has
|
|
15255
|
+
* shipped with; a control plane out of memory is every tenant's outage. The
|
|
15256
|
+
* platform's own rebuilds build with `--no-vendor` for the same shape of
|
|
15257
|
+
* reason. When the control plane streams bundles, this is the one switch to
|
|
15258
|
+
* flip, and `rebase cloud deploy` builds with `vendor: false` until then.
|
|
15259
|
+
*
|
|
15260
|
+
* `modulesLeftOut` says a tree the build DID vendor was not uploaded — a
|
|
15261
|
+
* prebuilt `--bundle-dir` from a plain `rebase build`, which vendors by default.
|
|
15262
|
+
*/
|
|
15263
|
+
async function packBundleForUpload(bundleDir, outPath, manifest, maxBytes = MAX_BUNDLE_UPLOAD_BYTES) {
|
|
15264
|
+
const vendored = manifest.deps?.vendored === true;
|
|
15265
|
+
await packBundle(bundleDir, outPath, { withModules: false });
|
|
15266
|
+
const bytes = fs.statSync(outPath).size;
|
|
15267
|
+
if (bytes > maxBytes) throw new Error(`The bundle is ${(bytes / 1024 / 1024).toFixed(1)} MB without its dependencies, over the ${Math.round(maxBytes / 1024 / 1024)} MB upload limit.`);
|
|
15268
|
+
return {
|
|
15269
|
+
bytes,
|
|
15270
|
+
modulesLeftOut: vendored
|
|
15271
|
+
};
|
|
15272
|
+
}
|
|
15273
|
+
/**
|
|
15066
15274
|
* The commit HEAD is on, read here because here is the only place it exists.
|
|
15067
15275
|
*
|
|
15068
15276
|
* A bundle deploy has no repository anywhere near the control plane: the CLI
|
|
@@ -15198,7 +15406,8 @@ async function uploadBundle(url, token, projectId, tarPath) {
|
|
|
15198
15406
|
* the developer considers source: `.gitignore` honoured the way git honours
|
|
15199
15407
|
* it, and a sibling package the project links (`link:../../packages/editor`)
|
|
15200
15408
|
* included, because it is outside the project directory but inside the
|
|
15201
|
-
* repository.
|
|
15409
|
+
* repository. Outside a repository — or inside one that ignores the project —
|
|
15410
|
+
* the project is walked, and its `.gitignore` files still decide.
|
|
15202
15411
|
* - **Secrets never leave the machine, whatever git says.** A committed `.env`
|
|
15203
15412
|
* is still a `.env`. {@link neverUploaded} is applied after git, not instead
|
|
15204
15413
|
* of it.
|
|
@@ -15227,12 +15436,18 @@ var ENV_TEMPLATES = /* @__PURE__ */ new Set([
|
|
|
15227
15436
|
* bundles are never source. Every `.env` and `.env.*` is excluded except the
|
|
15228
15437
|
* three template names, and so is direnv's `.envrc`, which is the same thing
|
|
15229
15438
|
* under another name.
|
|
15439
|
+
*
|
|
15440
|
+
* So is what the CLI keeps on this machine: `.rebase/`, which holds the
|
|
15441
|
+
* development database, and the `.rebase-dev-*` files beside it, one of which
|
|
15442
|
+
* is the development signing secrets. And so is a `*.dump`, which is what
|
|
15443
|
+
* `rebase db backup` writes: every row of the database, `rebase.users`
|
|
15444
|
+
* password hashes included.
|
|
15230
15445
|
*/
|
|
15231
15446
|
function neverUploaded(relativePath) {
|
|
15232
15447
|
const segments = relativePath.split("/");
|
|
15233
|
-
if (segments.some((segment) => segment === "node_modules" || segment === ".git" || segment.startsWith("dist-bundle"))) return true;
|
|
15448
|
+
if (segments.some((segment) => segment === "node_modules" || segment === ".git" || segment === ".rebase" || segment.startsWith("dist-bundle"))) return true;
|
|
15234
15449
|
const base = segments[segments.length - 1];
|
|
15235
|
-
if (base === ".envrc") return true;
|
|
15450
|
+
if (base === ".envrc" || base.startsWith(".rebase-dev-") || base.endsWith(".dump")) return true;
|
|
15236
15451
|
if ((base === ".env" || base.startsWith(".env.")) && !ENV_TEMPLATES.has(base)) return true;
|
|
15237
15452
|
return false;
|
|
15238
15453
|
}
|
|
@@ -15245,6 +15460,10 @@ var WALK_SKIP = /* @__PURE__ */ new Set([
|
|
|
15245
15460
|
".next",
|
|
15246
15461
|
"coverage"
|
|
15247
15462
|
]);
|
|
15463
|
+
/** The same directories as `ls-files` excludes, plus every `dist*` build directory. */
|
|
15464
|
+
var WALK_EXCLUDES = [...[...WALK_SKIP].map((name) => `--exclude=${name}/`), "--exclude=dist*/"];
|
|
15465
|
+
/** The file that says what a directory's source is not, which a walk has to honour. */
|
|
15466
|
+
var IGNORE_FILE = ".gitignore";
|
|
15248
15467
|
var runGit = (cwd, args) => execFileSync("git", [
|
|
15249
15468
|
"-C",
|
|
15250
15469
|
cwd,
|
|
@@ -15269,29 +15488,106 @@ function isRegularFile(absolute) {
|
|
|
15269
15488
|
return false;
|
|
15270
15489
|
}
|
|
15271
15490
|
}
|
|
15491
|
+
/** Split `-z` output into its entries. */
|
|
15492
|
+
function nulSeparated(output) {
|
|
15493
|
+
return [...new Set(output.split("\0").filter(Boolean))];
|
|
15494
|
+
}
|
|
15495
|
+
/**
|
|
15496
|
+
* Run `fn` with the git directory of a throwaway, empty repository.
|
|
15497
|
+
*
|
|
15498
|
+
* `git ls-files` reads `.gitignore` files only inside a repository. Pointed at
|
|
15499
|
+
* an empty one with `--git-dir`, and at a directory with `--work-tree`, it lists
|
|
15500
|
+
* that directory's files the way git would — nested ignore files, negations and
|
|
15501
|
+
* the user's global excludes included — without the directory being a
|
|
15502
|
+
* repository, or `git init` touching it.
|
|
15503
|
+
*/
|
|
15504
|
+
function withScratchRepository(git, fn) {
|
|
15505
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "rebase-ignore-"));
|
|
15506
|
+
try {
|
|
15507
|
+
git(scratch, ["init", "-q"]);
|
|
15508
|
+
return fn(path.join(scratch, ".git"));
|
|
15509
|
+
} finally {
|
|
15510
|
+
fs.rmSync(scratch, {
|
|
15511
|
+
recursive: true,
|
|
15512
|
+
force: true
|
|
15513
|
+
});
|
|
15514
|
+
}
|
|
15515
|
+
}
|
|
15272
15516
|
/**
|
|
15273
15517
|
* The files of the repository `dir` belongs to — tracked and unignored, the way
|
|
15274
|
-
* git sees them — or,
|
|
15518
|
+
* git sees them — or, when no repository holds it, `dir` walked.
|
|
15519
|
+
*
|
|
15520
|
+
* A repository that lists nothing under `dir` does not hold it: a project in a
|
|
15521
|
+
* directory the surrounding repository ignores (`tmp/my-app` under a repository
|
|
15522
|
+
* ignoring `tmp/`) has no repository of its own. Uploading that repository sent
|
|
15523
|
+
* everything except the project, so the project is walked instead.
|
|
15275
15524
|
*/
|
|
15276
15525
|
function listUnit(dir, git) {
|
|
15277
15526
|
try {
|
|
15278
15527
|
const toplevel = git(dir, ["rev-parse", "--show-toplevel"]).trim();
|
|
15279
15528
|
if (toplevel) {
|
|
15280
15529
|
const root = fs.realpathSync(toplevel);
|
|
15281
|
-
const
|
|
15530
|
+
const files = nulSeparated(git(root, [
|
|
15282
15531
|
"ls-files",
|
|
15283
15532
|
"-z",
|
|
15284
15533
|
"--cached",
|
|
15285
15534
|
"--others",
|
|
15286
15535
|
"--exclude-standard"
|
|
15287
|
-
]);
|
|
15288
|
-
|
|
15536
|
+
])).filter((relative) => !neverUploaded(relative) && isRegularFile(path.join(root, relative)));
|
|
15537
|
+
const inside = toPosix(path.relative(root, dir));
|
|
15538
|
+
if (inside === "" || files.some((file) => file.startsWith(`${inside}/`))) return {
|
|
15289
15539
|
root,
|
|
15290
|
-
files
|
|
15540
|
+
files,
|
|
15291
15541
|
fromGit: true
|
|
15292
15542
|
};
|
|
15293
15543
|
}
|
|
15294
15544
|
} catch {}
|
|
15545
|
+
return {
|
|
15546
|
+
root: dir,
|
|
15547
|
+
files: walkFiles(dir, git),
|
|
15548
|
+
fromGit: false
|
|
15549
|
+
};
|
|
15550
|
+
}
|
|
15551
|
+
/**
|
|
15552
|
+
* `dir`'s files, walked, with its `.gitignore` files honoured.
|
|
15553
|
+
*
|
|
15554
|
+
* A scaffold made without `git init` still carries the stock `.gitignore`, and
|
|
15555
|
+
* it is the only statement of what in the directory is not source: `backups/`
|
|
15556
|
+
* with its database dumps, `uploads/` with the users' files. Git reads it,
|
|
15557
|
+
* through a throwaway repository (see {@link withScratchRepository}); a
|
|
15558
|
+
* repository nested in `dir` lists as one directory entry, and is listed as the
|
|
15559
|
+
* repository it is.
|
|
15560
|
+
*
|
|
15561
|
+
* Without git the walk has fixed excludes alone, and those cannot read an
|
|
15562
|
+
* ignore file. So a directory holding one is refused rather than walked past
|
|
15563
|
+
* it — the developer wrote down what must stay on this machine.
|
|
15564
|
+
*/
|
|
15565
|
+
function walkFiles(dir, git) {
|
|
15566
|
+
let listed;
|
|
15567
|
+
try {
|
|
15568
|
+
listed = withScratchRepository(git, (gitDir) => nulSeparated(git(dir, [
|
|
15569
|
+
`--git-dir=${gitDir}`,
|
|
15570
|
+
`--work-tree=${dir}`,
|
|
15571
|
+
"ls-files",
|
|
15572
|
+
"-z",
|
|
15573
|
+
"--others",
|
|
15574
|
+
"--exclude-standard",
|
|
15575
|
+
...WALK_EXCLUDES
|
|
15576
|
+
])));
|
|
15577
|
+
} catch {
|
|
15578
|
+
return walkWithoutGit(dir);
|
|
15579
|
+
}
|
|
15580
|
+
const files = [];
|
|
15581
|
+
for (const relative of listed) if (relative.endsWith("/")) {
|
|
15582
|
+
const nested = listUnit(path.join(dir, relative), git);
|
|
15583
|
+
const prefix = toPosix(path.relative(dir, nested.root));
|
|
15584
|
+
if (prefix.startsWith("..") || path.isAbsolute(prefix)) continue;
|
|
15585
|
+
for (const file of nested.files) files.push(prefix ? `${prefix}/${file}` : file);
|
|
15586
|
+
} else if (!neverUploaded(relative) && isRegularFile(path.join(dir, relative))) files.push(relative);
|
|
15587
|
+
return files;
|
|
15588
|
+
}
|
|
15589
|
+
/** The walk with fixed excludes, for a machine with no git. Refuses a tree with an ignore file in it. */
|
|
15590
|
+
function walkWithoutGit(dir) {
|
|
15295
15591
|
const files = [];
|
|
15296
15592
|
const walk = (current, prefix) => {
|
|
15297
15593
|
let entries;
|
|
@@ -15305,15 +15601,12 @@ function listUnit(dir, git) {
|
|
|
15305
15601
|
if (entry.isDirectory()) {
|
|
15306
15602
|
if (WALK_SKIP.has(entry.name) || entry.name.startsWith("dist")) continue;
|
|
15307
15603
|
walk(path.join(current, entry.name), relative);
|
|
15308
|
-
} else if (entry.isFile() &&
|
|
15604
|
+
} else if (entry.isFile() && entry.name === IGNORE_FILE) throw new Error(`${relative} says what is not source, and reading it takes git, which is not available here`);
|
|
15605
|
+
else if (entry.isFile() && !neverUploaded(relative)) files.push(relative);
|
|
15309
15606
|
}
|
|
15310
15607
|
};
|
|
15311
15608
|
walk(dir, "");
|
|
15312
|
-
return
|
|
15313
|
-
root: dir,
|
|
15314
|
-
files,
|
|
15315
|
-
fromGit: false
|
|
15316
|
-
};
|
|
15609
|
+
return files;
|
|
15317
15610
|
}
|
|
15318
15611
|
/** Whether `candidate` is `dir` or inside it. */
|
|
15319
15612
|
function isWithin(dir, candidate) {
|
|
@@ -15408,6 +15701,42 @@ function listSourceFiles(projectRoot, git = runGit) {
|
|
|
15408
15701
|
fromGit: units.every((u) => u.fromGit)
|
|
15409
15702
|
};
|
|
15410
15703
|
}
|
|
15704
|
+
/** The per-directory file of patterns a `--source` build context leaves out, beside `.gitignore`. */
|
|
15705
|
+
var CONTEXT_IGNORE_FILE = ".rebaseignore";
|
|
15706
|
+
/**
|
|
15707
|
+
* The files of a `--source` build context: `dir` and what is under it, relative
|
|
15708
|
+
* to it, POSIX, sorted.
|
|
15709
|
+
*
|
|
15710
|
+
* The same rules as the rebuild source — git decides, and {@link neverUploaded}
|
|
15711
|
+
* after it — restricted to `dir`, which is the context's root, so a Dockerfile
|
|
15712
|
+
* in it still reads its paths from there. A `dir` that is a subfolder of a
|
|
15713
|
+
* repository is held to that repository's `.gitignore`, which is where a
|
|
15714
|
+
* monorepo keeps it.
|
|
15715
|
+
*
|
|
15716
|
+
* `.rebaseignore` in `dir` leaves out more, read with `.gitignore` syntax and
|
|
15717
|
+
* anchored at `dir`. It needs git to be read, like `.gitignore` outside a
|
|
15718
|
+
* repository; without git, a context that has one is refused.
|
|
15719
|
+
*/
|
|
15720
|
+
function listContextFiles(dir, git = runGit) {
|
|
15721
|
+
const root = fs.realpathSync(dir);
|
|
15722
|
+
const unit = listUnit(root, git);
|
|
15723
|
+
const prefix = toPosix(path.relative(unit.root, root));
|
|
15724
|
+
let files = prefix === "" ? unit.files : unit.files.filter((file) => file.startsWith(`${prefix}/`)).map((file) => file.slice(prefix.length + 1));
|
|
15725
|
+
const contextIgnore = path.join(root, CONTEXT_IGNORE_FILE);
|
|
15726
|
+
if (fs.existsSync(contextIgnore)) {
|
|
15727
|
+
const ignored = new Set(withScratchRepository(git, (gitDir) => nulSeparated(git(root, [
|
|
15728
|
+
`--git-dir=${gitDir}`,
|
|
15729
|
+
`--work-tree=${root}`,
|
|
15730
|
+
"ls-files",
|
|
15731
|
+
"-z",
|
|
15732
|
+
"--others",
|
|
15733
|
+
"--ignored",
|
|
15734
|
+
`--exclude-from=${contextIgnore}`
|
|
15735
|
+
]))));
|
|
15736
|
+
files = files.filter((file) => !ignored.has(file));
|
|
15737
|
+
}
|
|
15738
|
+
return [...files].sort();
|
|
15739
|
+
}
|
|
15411
15740
|
/**
|
|
15412
15741
|
* Pack a listing into a gzipped tarball at `outPath`.
|
|
15413
15742
|
*
|
|
@@ -15552,7 +15881,7 @@ async function prepareRebuildSource(opts) {
|
|
|
15552
15881
|
}
|
|
15553
15882
|
opts.progress(` Uploading source (${listing.files.length} files, ${mb(size)} MB)...`);
|
|
15554
15883
|
return {
|
|
15555
|
-
sourceId: await steps.upload(opts.url, opts.token, opts.projectId, tarPath),
|
|
15884
|
+
sourceId: await steps.upload(opts.url, await opts.token(), opts.projectId, tarPath),
|
|
15556
15885
|
projectPath: listing.projectPath,
|
|
15557
15886
|
buildEnv: collectBuildEnv(opts.projectRoot, staticAppRoots(opts.manifest))
|
|
15558
15887
|
};
|
|
@@ -15567,48 +15896,31 @@ async function prepareRebuildSource(opts) {
|
|
|
15567
15896
|
//#region src/commands/cloud/deploy.ts
|
|
15568
15897
|
var POLL_INTERVAL_MS = 1500;
|
|
15569
15898
|
var POLL_TIMEOUT_MS = 900 * 1e3;
|
|
15899
|
+
/** How long a follow keeps retrying a status it cannot read before it says so. */
|
|
15900
|
+
var UNREADABLE_STATUS_MS = 60 * 1e3;
|
|
15570
15901
|
function sleep(ms) {
|
|
15571
15902
|
return new Promise((r) => setTimeout(r, ms));
|
|
15572
15903
|
}
|
|
15573
|
-
function run(cmd, cmdArgs, cwd, env) {
|
|
15574
|
-
return new Promise((resolve, reject) => {
|
|
15575
|
-
const child = spawn(cmd, cmdArgs, {
|
|
15576
|
-
cwd,
|
|
15577
|
-
env: env ? {
|
|
15578
|
-
...process.env,
|
|
15579
|
-
...env
|
|
15580
|
-
} : void 0,
|
|
15581
|
-
stdio: [
|
|
15582
|
-
"ignore",
|
|
15583
|
-
"ignore",
|
|
15584
|
-
"pipe"
|
|
15585
|
-
]
|
|
15586
|
-
});
|
|
15587
|
-
let stderr = "";
|
|
15588
|
-
child.stderr.on("data", (d) => stderr += d.toString());
|
|
15589
|
-
child.on("error", reject);
|
|
15590
|
-
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr || `${cmd} exited ${code}`)));
|
|
15591
|
-
});
|
|
15592
|
-
}
|
|
15593
15904
|
/**
|
|
15594
|
-
* Package `sourceDir` into a gzipped tarball
|
|
15595
|
-
*
|
|
15905
|
+
* Package `sourceDir` into a gzipped tarball; returns the temp archive path.
|
|
15906
|
+
*
|
|
15907
|
+
* What goes in is {@link listContextFiles}: `.gitignore` read the way git reads
|
|
15908
|
+
* it, `.rebaseignore` beside it, and never an env file, a database dump or the
|
|
15909
|
+
* CLI's local state, whatever either says. The control plane keeps the archive
|
|
15910
|
+
* as the project's source, so this is the last point anything can be kept back.
|
|
15596
15911
|
*/
|
|
15597
15912
|
async function createSourceTarball(sourceDir) {
|
|
15598
15913
|
const dir = path.resolve(sourceDir);
|
|
15599
15914
|
if (!fs.existsSync(dir)) fail(`Source directory not found: ${dir}`);
|
|
15600
15915
|
const tarPath = path.join(os.tmpdir(), `rebase-src-${Date.now()}.tar.gz`);
|
|
15601
|
-
const tarArgs = [
|
|
15602
|
-
"-czf",
|
|
15603
|
-
tarPath,
|
|
15604
|
-
"--exclude=.git",
|
|
15605
|
-
"--exclude=node_modules"
|
|
15606
|
-
];
|
|
15607
|
-
for (const ignore of [".gitignore", ".rebaseignore"]) if (fs.existsSync(path.join(dir, ignore))) tarArgs.push(`--exclude-from=${ignore}`);
|
|
15608
|
-
tarArgs.push(".");
|
|
15609
15916
|
try {
|
|
15610
|
-
|
|
15917
|
+
const files = listContextFiles(dir);
|
|
15918
|
+
await packSource({
|
|
15919
|
+
root: fs.realpathSync(dir),
|
|
15920
|
+
files
|
|
15921
|
+
}, tarPath);
|
|
15611
15922
|
} catch (e) {
|
|
15923
|
+
fs.rmSync(tarPath, { force: true });
|
|
15612
15924
|
fail(`Failed to package source: ${e instanceof Error ? e.message : String(e)}`);
|
|
15613
15925
|
}
|
|
15614
15926
|
return tarPath;
|
|
@@ -15654,6 +15966,22 @@ function resolveFrameworkVersion(sourceDir) {
|
|
|
15654
15966
|
function progress(line) {
|
|
15655
15967
|
if (!isJsonMode()) console.log(line);
|
|
15656
15968
|
}
|
|
15969
|
+
/**
|
|
15970
|
+
* The token for one of the raw uploads below, read as it is sent.
|
|
15971
|
+
*
|
|
15972
|
+
* They are plain `fetch` calls, which never refresh a session the way the
|
|
15973
|
+
* SDK's own requests do, and each is sent after work that can outlive a token:
|
|
15974
|
+
* a type check and a build, or packing a large directory. Read once at the
|
|
15975
|
+
* start, the token had lapsed by then, the upload was refused with a 401, and
|
|
15976
|
+
* the build was thrown away.
|
|
15977
|
+
*/
|
|
15978
|
+
async function uploadToken(client, url) {
|
|
15979
|
+
try {
|
|
15980
|
+
return await freshAccessToken(client);
|
|
15981
|
+
} catch {
|
|
15982
|
+
fail(`Your session for ${url} has expired.`, "Run `rebase cloud login` to sign in again, then deploy again.", "session_expired");
|
|
15983
|
+
}
|
|
15984
|
+
}
|
|
15657
15985
|
/** Upload a build-context tarball; returns the opaque `source` ref for deploy. */
|
|
15658
15986
|
async function uploadSource(url, token, projectId, tarPath) {
|
|
15659
15987
|
const bytes = fs.readFileSync(tarPath);
|
|
@@ -15680,14 +16008,20 @@ async function uploadSource(url, token, projectId, tarPath) {
|
|
|
15680
16008
|
* Build, upload and deploy a project as a managed bundle.
|
|
15681
16009
|
*
|
|
15682
16010
|
* Builds the backend app into `dist-bundle` (unless one is pointed at with
|
|
15683
|
-
* `--bundle-dir`), packs it
|
|
15684
|
-
* deploy carrying the manifest so the control plane
|
|
16011
|
+
* `--bundle-dir`), packs it with the dependency tree the build vendored into it,
|
|
16012
|
+
* uploads it, and triggers a deploy carrying the manifest so the control plane
|
|
16013
|
+
* can validate intake fast.
|
|
15685
16014
|
*/
|
|
15686
16015
|
async function deployBundle(opts) {
|
|
15687
16016
|
const projectRoot = requireProjectRoot();
|
|
15688
16017
|
let bundleDir = opts.bundleDir ? path.resolve(process.cwd(), opts.bundleDir) : path.join(projectRoot, "dist-bundle");
|
|
15689
16018
|
if (!opts.bundleDir) {
|
|
15690
|
-
|
|
16019
|
+
let loaded;
|
|
16020
|
+
try {
|
|
16021
|
+
loaded = loadManifest(projectRoot);
|
|
16022
|
+
} catch (err) {
|
|
16023
|
+
fail(err instanceof Error ? err.message : String(err), void 0, "invalid_manifest");
|
|
16024
|
+
}
|
|
15691
16025
|
let target;
|
|
15692
16026
|
try {
|
|
15693
16027
|
target = selectDeployApp(loaded.manifest, opts.appName);
|
|
@@ -15695,9 +16029,15 @@ async function deployBundle(opts) {
|
|
|
15695
16029
|
fail(err instanceof Error ? err.message : String(err));
|
|
15696
16030
|
return;
|
|
15697
16031
|
}
|
|
16032
|
+
if (target.app.type === "static" && opts.noStatic) fail(`--no-static leaves a backend's frontend out of its bundle, and "${target.name}" is a static app: its bundle is nothing but its frontend.`, "Deploy it without --no-static.", "usage");
|
|
15698
16033
|
if (target.app.type === "static") {
|
|
15699
16034
|
progress(chalk.gray(` Building static app "${target.name}"...`));
|
|
15700
|
-
|
|
16035
|
+
let staticDir;
|
|
16036
|
+
try {
|
|
16037
|
+
staticDir = await buildAssetApp(projectRoot, target.name, target.app, loaded.manifest.rebase, void 0, { quietStdout: isJsonMode() });
|
|
16038
|
+
} catch (err) {
|
|
16039
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
16040
|
+
}
|
|
15701
16041
|
if (!staticDir) {
|
|
15702
16042
|
fail(`App "${target.name}" produced no bundle.`, "A static app needs both a `build` command and an `output` directory in rebase.json.");
|
|
15703
16043
|
return;
|
|
@@ -15717,22 +16057,32 @@ async function deployBundle(opts) {
|
|
|
15717
16057
|
app: target.app
|
|
15718
16058
|
};
|
|
15719
16059
|
const { graph: resourceGraph, issues: resourceIssues } = await deriveResourceGraph(deriveOptionsFor(projectRoot, backend.app));
|
|
15720
|
-
if (resourceIssues.length > 0)
|
|
15721
|
-
|
|
15722
|
-
projectRoot,
|
|
15723
|
-
appName: backend.name,
|
|
15724
|
-
app: backend.app,
|
|
15725
|
-
runtimeRange: loaded.manifest.rebase,
|
|
15726
|
-
resources: resourceGraph,
|
|
15727
|
-
skipTypeCheck: opts.skipTypeCheck,
|
|
15728
|
-
log: (m) => progress(chalk.gray(m))
|
|
15729
|
-
})).outDir;
|
|
16060
|
+
if (resourceIssues.length > 0) fail(`${resourceIssues.length} problem(s) in the declared resources:\n` + resourceIssues.map((i) => ` ${i.path} ${i.message}`).join("\n"), void 0, "build_failed");
|
|
16061
|
+
let result;
|
|
15730
16062
|
try {
|
|
16063
|
+
result = await buildBundle({
|
|
16064
|
+
projectRoot,
|
|
16065
|
+
appName: backend.name,
|
|
16066
|
+
app: backend.app,
|
|
16067
|
+
runtimeRange: loaded.manifest.rebase,
|
|
16068
|
+
resources: resourceGraph,
|
|
16069
|
+
skipTypeCheck: opts.skipTypeCheck,
|
|
16070
|
+
skipSchema: opts.skipSchema,
|
|
16071
|
+
vendor: false,
|
|
16072
|
+
log: (m) => progress(chalk.gray(m)),
|
|
16073
|
+
quietStdout: isJsonMode()
|
|
16074
|
+
});
|
|
16075
|
+
} catch (err) {
|
|
16076
|
+
fail(err instanceof Error ? err.message : String(err), void 0, "build_failed");
|
|
16077
|
+
}
|
|
16078
|
+
bundleDir = result.outDir;
|
|
16079
|
+
if (!opts.noStatic) try {
|
|
15731
16080
|
const folded = await foldFrontendIntoBundle({
|
|
15732
16081
|
projectRoot,
|
|
15733
16082
|
manifest: loaded.manifest,
|
|
15734
16083
|
bundleDir,
|
|
15735
|
-
log: (m) => progress(m)
|
|
16084
|
+
log: (m) => progress(m),
|
|
16085
|
+
quietStdout: isJsonMode()
|
|
15736
16086
|
});
|
|
15737
16087
|
for (const outcome of folded) progress(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
|
|
15738
16088
|
} catch (err) {
|
|
@@ -15757,20 +16107,32 @@ async function deployBundle(opts) {
|
|
|
15757
16107
|
*/
|
|
15758
16108
|
async function uploadAndTrigger(opts) {
|
|
15759
16109
|
const { client, url, projectId, projectRef, projectRoot, bundleDir } = opts;
|
|
15760
|
-
|
|
16110
|
+
let manifest;
|
|
16111
|
+
try {
|
|
16112
|
+
manifest = readBundleManifest(bundleDir);
|
|
16113
|
+
} catch (err) {
|
|
16114
|
+
fail(err instanceof Error ? err.message : String(err), void 0, "invalid_bundle");
|
|
16115
|
+
}
|
|
16116
|
+
const record = (followed, status) => recordDeploy({
|
|
16117
|
+
followed,
|
|
16118
|
+
status,
|
|
16119
|
+
deduplicated: false,
|
|
16120
|
+
frameworkVersion: manifest.runtime?.builtAgainst,
|
|
16121
|
+
startedAt: opts.startedAt,
|
|
16122
|
+
projectRoot
|
|
16123
|
+
});
|
|
15761
16124
|
if (manifest.hooks?.native) {
|
|
15762
16125
|
const names = (manifest.hooks.nativeModules ?? []).map((m) => m.name).join(", ");
|
|
15763
16126
|
fail(`This bundle depends on native modules${names ? ` (${names})` : ""}, which the managed runtime cannot run.`, "Remove the native dependency, or deploy on the custom runtime.");
|
|
15764
16127
|
}
|
|
15765
16128
|
const tarPath = path.join(os.tmpdir(), `rebase-bundle-${Date.now()}.tar.gz`);
|
|
15766
|
-
const token = client.auth.getSession()?.accessToken;
|
|
15767
|
-
if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
|
|
15768
16129
|
let bundleId;
|
|
15769
16130
|
try {
|
|
15770
|
-
await
|
|
15771
|
-
|
|
16131
|
+
const packed = await packBundleForUpload(bundleDir, tarPath, manifest);
|
|
16132
|
+
if (packed.modulesLeftOut) warn("This bundle's installed dependencies are not uploaded: a managed pod installs them when it starts.", "Build it with `rebase build --no-vendor` to skip installing a tree the upload leaves out.");
|
|
16133
|
+
const sizeMb = (packed.bytes / 1024 / 1024).toFixed(1);
|
|
15772
16134
|
progress(chalk.gray(` Uploading bundle (${sizeMb} MB)...`));
|
|
15773
|
-
bundleId = await uploadBundle(url,
|
|
16135
|
+
bundleId = await uploadBundle(url, await uploadToken(client, url), projectId, tarPath);
|
|
15774
16136
|
} catch (e) {
|
|
15775
16137
|
fail(e instanceof Error ? e.message : String(e));
|
|
15776
16138
|
return;
|
|
@@ -15791,7 +16153,7 @@ async function uploadAndTrigger(opts) {
|
|
|
15791
16153
|
else if (opts.uploadSource && manifest.kind !== "static") rebuildSource = await prepareRebuildSource({
|
|
15792
16154
|
projectRoot,
|
|
15793
16155
|
url,
|
|
15794
|
-
token,
|
|
16156
|
+
token: () => freshAccessToken(client),
|
|
15795
16157
|
projectId,
|
|
15796
16158
|
manifest: projectManifest,
|
|
15797
16159
|
progress: (line) => progress(chalk.gray(line)),
|
|
@@ -15831,6 +16193,7 @@ async function uploadAndTrigger(opts) {
|
|
|
15831
16193
|
sourceUploaded: rebuildSource !== null,
|
|
15832
16194
|
following: false
|
|
15833
16195
|
});
|
|
16196
|
+
await record(false, "not_followed");
|
|
15834
16197
|
return;
|
|
15835
16198
|
}
|
|
15836
16199
|
if (!isJsonMode()) {
|
|
@@ -15838,11 +16201,12 @@ async function uploadAndTrigger(opts) {
|
|
|
15838
16201
|
console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
|
|
15839
16202
|
console.log("");
|
|
15840
16203
|
}
|
|
15841
|
-
const status = await streamBuildLogs(client, deploymentId, {
|
|
16204
|
+
const { status } = await streamBuildLogs(client, deploymentId, {
|
|
15842
16205
|
quiet: isJsonMode(),
|
|
15843
16206
|
timeoutMs: opts.timeoutMs,
|
|
15844
16207
|
projectId,
|
|
15845
|
-
url
|
|
16208
|
+
url,
|
|
16209
|
+
onTerminal: (ended) => record(true, ended)
|
|
15846
16210
|
});
|
|
15847
16211
|
emit(() => {}, {
|
|
15848
16212
|
success: true,
|
|
@@ -15885,10 +16249,42 @@ function isManagedProject(project, latest) {
|
|
|
15885
16249
|
if (pick(project, "runtimeMode", "runtime_mode") === "managed") return true;
|
|
15886
16250
|
return latest?.status === "success" && pick(latest, "bundleId", "bundle_id") !== void 0;
|
|
15887
16251
|
}
|
|
15888
|
-
/**
|
|
15889
|
-
function
|
|
16252
|
+
/** A row's own `createdAt`, in ms; 0 when it has none that parses. */
|
|
16253
|
+
function createdAtMs(row) {
|
|
16254
|
+
const raw = row.createdAt ?? row.created_at;
|
|
16255
|
+
const at = raw instanceof Date ? raw.getTime() : typeof raw === "string" ? new Date(raw).getTime() : NaN;
|
|
16256
|
+
return Number.isNaN(at) ? 0 : at;
|
|
16257
|
+
}
|
|
16258
|
+
/**
|
|
16259
|
+
* The deployment whose uploaded archive a bare deploy rebuilds, if any.
|
|
16260
|
+
*
|
|
16261
|
+
* The control plane's own rule (`reusableSourceRef`, over the newest 25 rows):
|
|
16262
|
+
* the newest row, by its own `createdAt`, carrying a build-context ref. Not the
|
|
16263
|
+
* newest row — a rollback or a bundle deploy carries none, and the upload
|
|
16264
|
+
* rebuilt then is the one before it.
|
|
16265
|
+
*/
|
|
16266
|
+
function snapshotDeployment(recent) {
|
|
16267
|
+
let best;
|
|
16268
|
+
for (const row of recent) {
|
|
16269
|
+
const ref = pick(row, "sourceRef", "source_ref");
|
|
16270
|
+
if (!ref || ref.includes("..") || !ref.endsWith(".tar.gz")) continue;
|
|
16271
|
+
const at = createdAtMs(row);
|
|
16272
|
+
if (!best || at > best.at) best = {
|
|
16273
|
+
row,
|
|
16274
|
+
at
|
|
16275
|
+
};
|
|
16276
|
+
}
|
|
16277
|
+
return best?.row;
|
|
16278
|
+
}
|
|
16279
|
+
/**
|
|
16280
|
+
* What a `deploy` with nothing attached will build, in the words to print.
|
|
16281
|
+
*
|
|
16282
|
+
* `recent` is the project's newest deployments, newest first — the same page
|
|
16283
|
+
* the control plane reads to choose an archive.
|
|
16284
|
+
*/
|
|
16285
|
+
function planBareDeploy(project, recent, now) {
|
|
15890
16286
|
const projectRow = project;
|
|
15891
|
-
const
|
|
16287
|
+
const latest = recent[0];
|
|
15892
16288
|
const managed = isManagedProject(project, latest);
|
|
15893
16289
|
const repo = pick(projectRow, "gitRepoUrl", "git_repo_url");
|
|
15894
16290
|
if (repo) {
|
|
@@ -15899,12 +16295,16 @@ function planBareDeploy(project, latest, now) {
|
|
|
15899
16295
|
lines: [`Building from git: ${repo}${branch ? ` (${branch})` : ""}.`]
|
|
15900
16296
|
};
|
|
15901
16297
|
}
|
|
15902
|
-
|
|
15903
|
-
|
|
16298
|
+
const snapshot = snapshotDeployment(recent);
|
|
16299
|
+
if (snapshot) {
|
|
16300
|
+
const age = timeAgo(snapshot.createdAt ?? snapshot.created_at, now);
|
|
16301
|
+
const lines = [`Rebuilding the stored source archive${snapshot.id !== void 0 ? ` from deployment ${snapshot.id}` : ""}${age ? `, uploaded ${age}` : ""}.`];
|
|
16302
|
+
if (latest && snapshot !== latest && latest.id !== void 0) lines.push(`Deployment ${latest.id}, the newest, carried no archive of its own, so this is not what it deployed.`);
|
|
16303
|
+
lines.push("This directory is NOT uploaded — pass `--source .` to build what is on disk.");
|
|
15904
16304
|
return {
|
|
15905
16305
|
managed,
|
|
15906
16306
|
source: "snapshot",
|
|
15907
|
-
lines
|
|
16307
|
+
lines
|
|
15908
16308
|
};
|
|
15909
16309
|
}
|
|
15910
16310
|
return {
|
|
@@ -15992,22 +16392,28 @@ function warningPayload(warnings) {
|
|
|
15992
16392
|
};
|
|
15993
16393
|
}
|
|
15994
16394
|
/**
|
|
15995
|
-
* Read the
|
|
16395
|
+
* Read what the preflight needs: the project row and its newest deployments.
|
|
15996
16396
|
*
|
|
15997
|
-
*
|
|
15998
|
-
*
|
|
15999
|
-
* read, so an unreadable project falls through to the old behaviour rather than
|
|
16000
|
-
* blocking a deploy on a lookup.
|
|
16397
|
+
* `readable: false` when either read failed. The plan lines are best effort and
|
|
16398
|
+
* say nothing then; the managed refusal is not — see the caller.
|
|
16001
16399
|
*/
|
|
16002
16400
|
async function readDeployContext(client, projectId) {
|
|
16003
16401
|
try {
|
|
16004
|
-
const [project,
|
|
16402
|
+
const [project, recent] = await Promise.all([client.data.collection("projects").findById(projectId), client.data.collection("deployments").find({
|
|
16403
|
+
where: { project: ["==", projectId] },
|
|
16404
|
+
orderBy: ["createdAt", "desc"],
|
|
16405
|
+
limit: 25
|
|
16406
|
+
})]);
|
|
16005
16407
|
return {
|
|
16006
16408
|
project,
|
|
16007
|
-
|
|
16409
|
+
recent: recent.data,
|
|
16410
|
+
readable: true
|
|
16008
16411
|
};
|
|
16009
16412
|
} catch {
|
|
16010
|
-
return {
|
|
16413
|
+
return {
|
|
16414
|
+
recent: [],
|
|
16415
|
+
readable: false
|
|
16416
|
+
};
|
|
16011
16417
|
}
|
|
16012
16418
|
}
|
|
16013
16419
|
/**
|
|
@@ -16117,6 +16523,8 @@ var DEPLOY_FLAGS = {
|
|
|
16117
16523
|
"--bundle": Boolean,
|
|
16118
16524
|
"--bundle-dir": String,
|
|
16119
16525
|
"--skip-type-check": Boolean,
|
|
16526
|
+
"--no-static": Boolean,
|
|
16527
|
+
"--skip-schema": Boolean,
|
|
16120
16528
|
"--eject": Boolean,
|
|
16121
16529
|
"--no-source": Boolean,
|
|
16122
16530
|
"--allow-downgrade": Boolean,
|
|
@@ -16178,7 +16586,7 @@ async function recordDeploy(o) {
|
|
|
16178
16586
|
deduplicated: o.deduplicated,
|
|
16179
16587
|
framework_version: o.frameworkVersion ?? null,
|
|
16180
16588
|
duration: durationBucket(Date.now() - o.startedAt)
|
|
16181
|
-
}, { projectRoot: process.cwd() });
|
|
16589
|
+
}, { projectRoot: o.projectRoot ?? process.cwd() });
|
|
16182
16590
|
}
|
|
16183
16591
|
async function deployCommand(rawArgs, projectRef) {
|
|
16184
16592
|
const startedAt = Date.now();
|
|
@@ -16201,15 +16609,19 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
16201
16609
|
message: args["--message"],
|
|
16202
16610
|
appName,
|
|
16203
16611
|
skipTypeCheck: args["--skip-type-check"] === true,
|
|
16612
|
+
skipSchema: args["--skip-schema"] === true,
|
|
16613
|
+
noStatic: args["--no-static"] === true,
|
|
16204
16614
|
follow: args["--no-follow"] !== true,
|
|
16205
16615
|
timeoutMs: resolveDeployTimeout(args["--timeout"]),
|
|
16206
16616
|
uploadSource: args["--no-source"] !== true,
|
|
16207
|
-
allowDowngrade: args["--allow-downgrade"] === true
|
|
16617
|
+
allowDowngrade: args["--allow-downgrade"] === true,
|
|
16618
|
+
startedAt
|
|
16208
16619
|
});
|
|
16209
16620
|
return;
|
|
16210
16621
|
}
|
|
16211
|
-
const { project,
|
|
16212
|
-
const plan = planBareDeploy(project,
|
|
16622
|
+
const { project, recent, readable } = await readDeployContext(client, projectId);
|
|
16623
|
+
const plan = planBareDeploy(project, recent, /* @__PURE__ */ new Date());
|
|
16624
|
+
if (!readable && args["--eject"] !== true) fail(`Could not read ${projectRef}, so this deploy cannot tell whether it would eject the project from the managed runtime.`, "Try again in a moment. If leaving the managed runtime is intended, pass `--eject`.", "preflight_unreadable");
|
|
16213
16625
|
const eject = {
|
|
16214
16626
|
managed: plan.managed,
|
|
16215
16627
|
source: Boolean(args["--source"]),
|
|
@@ -16227,9 +16639,7 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
16227
16639
|
if (args["--source"]) {
|
|
16228
16640
|
const tarPath = await createSourceTarball(args["--source"]);
|
|
16229
16641
|
try {
|
|
16230
|
-
|
|
16231
|
-
if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
|
|
16232
|
-
source = await uploadSource(url, token, projectId, tarPath);
|
|
16642
|
+
source = await uploadSource(url, await uploadToken(client, url), projectId, tarPath);
|
|
16233
16643
|
} finally {
|
|
16234
16644
|
fs.rmSync(tarPath, { force: true });
|
|
16235
16645
|
}
|
|
@@ -16253,7 +16663,7 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
16253
16663
|
deduplicated: res.deduplicated === true
|
|
16254
16664
|
};
|
|
16255
16665
|
} catch (e) {
|
|
16256
|
-
|
|
16666
|
+
resolveTriggerFailure(e);
|
|
16257
16667
|
}
|
|
16258
16668
|
const { deploymentId, deduplicated } = triggered;
|
|
16259
16669
|
if (!isJsonMode()) console.log(chalk.gray(deduplicated ? ` Deployment ${deploymentId} is already running — following it.` : ` Deployment ${deploymentId} created.${frameworkVersion ? ` (@rebasepro/* ${frameworkVersion})` : ""}`));
|
|
@@ -16281,18 +16691,18 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
16281
16691
|
console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
|
|
16282
16692
|
console.log("");
|
|
16283
16693
|
}
|
|
16284
|
-
const status = await streamBuildLogs(client, deploymentId, {
|
|
16694
|
+
const { status } = await streamBuildLogs(client, deploymentId, {
|
|
16285
16695
|
quiet: isJsonMode(),
|
|
16286
16696
|
timeoutMs: resolveDeployTimeout(args["--timeout"]),
|
|
16287
16697
|
projectId,
|
|
16288
|
-
url
|
|
16289
|
-
|
|
16290
|
-
|
|
16291
|
-
|
|
16292
|
-
|
|
16293
|
-
|
|
16294
|
-
|
|
16295
|
-
|
|
16698
|
+
url,
|
|
16699
|
+
onTerminal: (ended) => recordDeploy({
|
|
16700
|
+
followed: true,
|
|
16701
|
+
status: ended,
|
|
16702
|
+
deduplicated,
|
|
16703
|
+
frameworkVersion,
|
|
16704
|
+
startedAt
|
|
16705
|
+
})
|
|
16296
16706
|
});
|
|
16297
16707
|
emit(() => {}, {
|
|
16298
16708
|
deploymentId,
|
|
@@ -16311,29 +16721,26 @@ function resolveDeployTimeout(value) {
|
|
|
16311
16721
|
});
|
|
16312
16722
|
}
|
|
16313
16723
|
/**
|
|
16314
|
-
* Turn a failed trigger into
|
|
16724
|
+
* Turn a failed trigger into an exit that says why.
|
|
16315
16725
|
*
|
|
16316
|
-
*
|
|
16317
|
-
*
|
|
16318
|
-
*
|
|
16319
|
-
*
|
|
16320
|
-
*
|
|
16321
|
-
*
|
|
16322
|
-
*
|
|
16726
|
+
* A 409 is a refusal, whoever holds the lock. The control plane answers a
|
|
16727
|
+
* replay of this very trigger — the same user sending the same uploaded
|
|
16728
|
+
* archive — with a 200 carrying `deduplicated: true`, which the success path
|
|
16729
|
+
* follows. So the deployment a 409 names is always a different one, and
|
|
16730
|
+
* `mine` means only that the same user started it: an earlier deploy, building
|
|
16731
|
+
* earlier code. Following it reported that deploy's verdict as this one's, and
|
|
16732
|
+
* the code just uploaded was never built.
|
|
16323
16733
|
*
|
|
16324
|
-
*
|
|
16325
|
-
*
|
|
16326
|
-
* where" is the difference between an actionable refusal and a dead end.
|
|
16734
|
+
* The refusal names the blocker, because "which one, since when, from where"
|
|
16735
|
+
* is the difference between an actionable refusal and a dead end.
|
|
16327
16736
|
*/
|
|
16328
16737
|
function resolveTriggerFailure(e) {
|
|
16329
16738
|
const err = e;
|
|
16330
16739
|
if (err?.status === 409) {
|
|
16331
16740
|
const blocking = err.details?.deployment;
|
|
16332
|
-
|
|
16333
|
-
|
|
16334
|
-
|
|
16335
|
-
};
|
|
16336
|
-
fail(blocking?.id ? `Deployment ${blocking.id} is already in progress for this project${blocking.triggerSource && blocking.triggerSource !== "unknown" ? `, triggered from the ${blocking.triggerSource}` : ""}${blocking.createdAt ? ` at ${fmtDate(blocking.createdAt)}` : ""}.` : "A deployment is already in progress for this project.", blocking?.id ? `Follow it with \`rebase cloud logs -f\`, or stop it with \`rebase cloud cancel ${blocking.id}\`.` : "Follow it with `rebase cloud logs -f`.", "deploy_in_progress");
|
|
16741
|
+
const where = blocking?.triggerSource && blocking.triggerSource !== "unknown" ? ` from the ${blocking.triggerSource}` : "";
|
|
16742
|
+
const who = blocking?.mine ? ", triggered by you" : where ? ", triggered" : "";
|
|
16743
|
+
fail(blocking?.id ? `Deployment ${blocking.id} is already in progress for this project${who}${where}${blocking.createdAt ? ` at ${fmtDate(blocking.createdAt)}` : ""}.` : "A deployment is already in progress for this project.", blocking?.id ? `Nothing was deployed. Wait for it (\`rebase cloud logs -f\`) or stop it with \`rebase cloud cancel ${blocking.id}\`, then deploy again.` : "Nothing was deployed. Wait for it (`rebase cloud logs -f`), then deploy again.", "deploy_in_progress");
|
|
16337
16744
|
}
|
|
16338
16745
|
if (err?.status === 402) fail(err.message || "Payment required before deploying.", "Attach a card once with `rebase cloud billing setup`, then deploy again.", "payment_required");
|
|
16339
16746
|
const refusal = intakeRefusal(e);
|
|
@@ -16416,23 +16823,95 @@ async function deployedUrl(client, opts) {
|
|
|
16416
16823
|
return;
|
|
16417
16824
|
}
|
|
16418
16825
|
}
|
|
16826
|
+
/**
|
|
16827
|
+
* The line an in-progress build keeps rewriting at the end of its log, to prove
|
|
16828
|
+
* to the control plane that it is still alive.
|
|
16829
|
+
*
|
|
16830
|
+
* The control plane's own format — `HEARTBEAT_MARKER` and `heartbeatSuffix` in
|
|
16831
|
+
* its `functions/deploy.ts` — which appends `\n<marker><ISO time>\n` to the log
|
|
16832
|
+
* on every write and every 30 seconds, and drops it on the terminal write.
|
|
16833
|
+
*/
|
|
16834
|
+
var BUILD_HEARTBEAT_MARKER = "⏳ Build in progress — control-plane heartbeat ";
|
|
16835
|
+
/**
|
|
16836
|
+
* A deployment's log without its trailing heartbeat line: the build log itself.
|
|
16837
|
+
*
|
|
16838
|
+
* Only a heartbeat that is the last line is one. The same text anywhere else is
|
|
16839
|
+
* the build quoting it.
|
|
16840
|
+
*/
|
|
16841
|
+
function withoutHeartbeat(logs) {
|
|
16842
|
+
const at = logs.lastIndexOf(`\n${BUILD_HEARTBEAT_MARKER}`);
|
|
16843
|
+
if (at < 0) return logs;
|
|
16844
|
+
const newline = logs.indexOf("\n", at + 1);
|
|
16845
|
+
return newline === -1 || newline === logs.length - 1 ? logs.slice(0, at) : logs;
|
|
16846
|
+
}
|
|
16847
|
+
/**
|
|
16848
|
+
* What to print of a deployment's log, given everything printed so far.
|
|
16849
|
+
*
|
|
16850
|
+
* The `logs` column is not append-only. Its heartbeat line is rewritten at the
|
|
16851
|
+
* end on every write, and a failed static deploy replaces its whole log with
|
|
16852
|
+
* one line. Diffing the raw column by length therefore cut the start off every
|
|
16853
|
+
* new chunk — as many characters as the heartbeat that had been there — and
|
|
16854
|
+
* printed heartbeat fragments in their place: a failed managed deploy lost the
|
|
16855
|
+
* first half of its reason.
|
|
16856
|
+
*
|
|
16857
|
+
* So the heartbeat is stripped first, and the log printed so far is compared
|
|
16858
|
+
* with the log now. When it has only grown, the new text is printed. When it
|
|
16859
|
+
* was rewritten, printing resumes from the start of the first line that
|
|
16860
|
+
* differs, on a line of its own.
|
|
16861
|
+
*/
|
|
16862
|
+
function nextLogChunk(printed, rowLogs) {
|
|
16863
|
+
const logs = withoutHeartbeat(rowLogs);
|
|
16864
|
+
if (logs.startsWith(printed)) return {
|
|
16865
|
+
chunk: logs.slice(printed.length),
|
|
16866
|
+
printed: logs
|
|
16867
|
+
};
|
|
16868
|
+
if (printed.startsWith(logs)) return {
|
|
16869
|
+
chunk: "",
|
|
16870
|
+
printed
|
|
16871
|
+
};
|
|
16872
|
+
let same = 0;
|
|
16873
|
+
while (same < logs.length && logs[same] === printed[same]) same++;
|
|
16874
|
+
const lineStart = logs.lastIndexOf("\n", same - 1) + 1;
|
|
16875
|
+
return {
|
|
16876
|
+
chunk: `${printed.endsWith("\n") ? "" : "\n"}${logs.slice(lineStart)}`,
|
|
16877
|
+
printed: logs
|
|
16878
|
+
};
|
|
16879
|
+
}
|
|
16419
16880
|
async function streamBuildLogs(client, deploymentId, opts = {}) {
|
|
16420
16881
|
const quiet = opts.quiet === true;
|
|
16421
16882
|
const timeoutMs = opts.timeoutMs ?? POLL_TIMEOUT_MS;
|
|
16422
|
-
let printed =
|
|
16883
|
+
let printed = "";
|
|
16423
16884
|
const started = Date.now();
|
|
16885
|
+
/** When the current run of failed status reads began. */
|
|
16886
|
+
let unreadableSince;
|
|
16887
|
+
const stopIfTimedOut = async () => {
|
|
16888
|
+
if (Date.now() - started <= timeoutMs) return;
|
|
16889
|
+
await opts.onTerminal?.("timeout");
|
|
16890
|
+
if (!quiet) console.log("");
|
|
16891
|
+
fail(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the build to finish.`, "The deployment may still be running — check `rebase cloud logs`.", "timeout");
|
|
16892
|
+
};
|
|
16424
16893
|
for (;;) {
|
|
16425
16894
|
let dep;
|
|
16426
16895
|
try {
|
|
16427
16896
|
dep = await client.data.collection("deployments").findById(deploymentId);
|
|
16897
|
+
unreadableSince = void 0;
|
|
16428
16898
|
} catch (e) {
|
|
16429
|
-
reportError(e, "Failed to read deployment status");
|
|
16899
|
+
if (!transientReadFailure(e)) reportError(e, "Failed to read deployment status");
|
|
16900
|
+
unreadableSince ??= Date.now();
|
|
16901
|
+
if (Date.now() - unreadableSince > UNREADABLE_STATUS_MS) {
|
|
16902
|
+
await opts.onTerminal?.("status_unreadable");
|
|
16903
|
+
fail(`Could not read deployment ${deploymentId}'s status for over a minute: ${e instanceof Error ? e.message : String(e)}`, "The deployment may still be running — check `rebase cloud logs`.", "status_unreadable");
|
|
16904
|
+
}
|
|
16905
|
+
await stopIfTimedOut();
|
|
16906
|
+
await sleep(POLL_INTERVAL_MS);
|
|
16907
|
+
continue;
|
|
16430
16908
|
}
|
|
16431
16909
|
if (!dep) fail(`Deployment ${deploymentId} disappeared.`, void 0, "not_found");
|
|
16432
|
-
const
|
|
16433
|
-
if (!quiet &&
|
|
16434
|
-
printed =
|
|
16910
|
+
const next = nextLogChunk(printed, dep.logs ?? "");
|
|
16911
|
+
if (!quiet && next.chunk !== "") process.stdout.write(next.chunk);
|
|
16912
|
+
printed = next.printed;
|
|
16435
16913
|
if (dep.status && dep.status !== "deploying") {
|
|
16914
|
+
await opts.onTerminal?.(dep.status);
|
|
16436
16915
|
if (dep.status !== "success") {
|
|
16437
16916
|
if (quiet) {
|
|
16438
16917
|
printJson({ error: {
|
|
@@ -16440,7 +16919,7 @@ async function streamBuildLogs(client, deploymentId, opts = {}) {
|
|
|
16440
16919
|
code: "deploy_failed",
|
|
16441
16920
|
status: null,
|
|
16442
16921
|
deploymentId,
|
|
16443
|
-
logs
|
|
16922
|
+
logs: withoutHeartbeat(dep.logs ?? "")
|
|
16444
16923
|
} });
|
|
16445
16924
|
process.exit(1);
|
|
16446
16925
|
}
|
|
@@ -16456,15 +16935,24 @@ async function streamBuildLogs(client, deploymentId, opts = {}) {
|
|
|
16456
16935
|
if (url) console.log(` ${chalk.cyan(`https://${url}`)}`);
|
|
16457
16936
|
console.log("");
|
|
16458
16937
|
}
|
|
16459
|
-
return
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
|
|
16463
|
-
fail(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the build to finish.`, "The deployment may still be running — check `rebase cloud logs`.", "timeout");
|
|
16938
|
+
return {
|
|
16939
|
+
status: dep.status,
|
|
16940
|
+
logs: withoutHeartbeat(dep.logs ?? "")
|
|
16941
|
+
};
|
|
16464
16942
|
}
|
|
16943
|
+
await stopIfTimedOut();
|
|
16465
16944
|
await sleep(POLL_INTERVAL_MS);
|
|
16466
16945
|
}
|
|
16467
16946
|
}
|
|
16947
|
+
/**
|
|
16948
|
+
* Whether a failed status read is worth reading again: no answer at all (the
|
|
16949
|
+
* connection failed), or one saying the control plane could not answer now.
|
|
16950
|
+
* Anything else — a 401, a 403, a 404 — will say the same next time.
|
|
16951
|
+
*/
|
|
16952
|
+
function transientReadFailure(e) {
|
|
16953
|
+
const status = typeof e === "object" && e !== null && "status" in e && typeof e.status === "number" ? e.status : void 0;
|
|
16954
|
+
return status === void 0 || status === 408 || status === 429 || status >= 500;
|
|
16955
|
+
}
|
|
16468
16956
|
/** What `rebase cloud logs` parses. Its page pairs against this. */
|
|
16469
16957
|
var LOGS_FLAGS = {
|
|
16470
16958
|
"--runtime": Boolean,
|
|
@@ -16487,11 +16975,16 @@ async function logsCommand(rawArgs, projectRef) {
|
|
|
16487
16975
|
method: "GET",
|
|
16488
16976
|
path: projectId
|
|
16489
16977
|
});
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
|
|
16493
|
-
|
|
16494
|
-
|
|
16978
|
+
emit(() => {
|
|
16979
|
+
console.log("");
|
|
16980
|
+
console.log(chalk.bold(` 📄 Runtime logs — project ${projectRef}`));
|
|
16981
|
+
console.log("");
|
|
16982
|
+
console.log(res.logs ?? chalk.gray(" (no logs)"));
|
|
16983
|
+
console.log("");
|
|
16984
|
+
}, {
|
|
16985
|
+
runtime: true,
|
|
16986
|
+
logs: res.logs ?? null
|
|
16987
|
+
});
|
|
16495
16988
|
} catch (e) {
|
|
16496
16989
|
reportError(e, "Failed to fetch runtime logs");
|
|
16497
16990
|
}
|
|
@@ -16500,22 +16993,48 @@ async function logsCommand(rawArgs, projectRef) {
|
|
|
16500
16993
|
try {
|
|
16501
16994
|
const dep = await latestDeployment(client, projectId);
|
|
16502
16995
|
if (!dep) {
|
|
16503
|
-
|
|
16504
|
-
|
|
16505
|
-
|
|
16996
|
+
emit(() => {
|
|
16997
|
+
console.log("");
|
|
16998
|
+
console.log(chalk.gray(" No deployments yet for this project."));
|
|
16999
|
+
console.log("");
|
|
17000
|
+
}, {
|
|
17001
|
+
deploymentId: null,
|
|
17002
|
+
status: null,
|
|
17003
|
+
logs: null
|
|
17004
|
+
});
|
|
16506
17005
|
return;
|
|
16507
17006
|
}
|
|
16508
|
-
|
|
16509
|
-
|
|
16510
|
-
|
|
16511
|
-
|
|
16512
|
-
|
|
16513
|
-
|
|
16514
|
-
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
17007
|
+
const deploymentId = String(dep.id);
|
|
17008
|
+
if (args["--follow"] && dep.status === "deploying") {
|
|
17009
|
+
if (!isJsonMode()) {
|
|
17010
|
+
console.log("");
|
|
17011
|
+
console.log(chalk.bold(` 📄 Build logs — deployment ${dep.id}`) + ` ${colorStatus(dep.status)}`);
|
|
17012
|
+
console.log("");
|
|
17013
|
+
}
|
|
17014
|
+
const { status, logs } = await streamBuildLogs(client, deploymentId, {
|
|
17015
|
+
quiet: isJsonMode(),
|
|
17016
|
+
projectId,
|
|
17017
|
+
url
|
|
17018
|
+
});
|
|
17019
|
+
emit(() => {}, {
|
|
17020
|
+
deploymentId,
|
|
17021
|
+
status,
|
|
17022
|
+
logs
|
|
17023
|
+
});
|
|
17024
|
+
return;
|
|
16518
17025
|
}
|
|
17026
|
+
const logs = dep.logs == null ? null : withoutHeartbeat(dep.logs);
|
|
17027
|
+
emit(() => {
|
|
17028
|
+
console.log("");
|
|
17029
|
+
console.log(chalk.bold(` 📄 Build logs — deployment ${dep.id}`) + ` ${colorStatus(dep.status)}`);
|
|
17030
|
+
console.log("");
|
|
17031
|
+
console.log(logs ?? chalk.gray(" (no logs)"));
|
|
17032
|
+
console.log("");
|
|
17033
|
+
}, {
|
|
17034
|
+
deploymentId,
|
|
17035
|
+
status: dep.status ?? null,
|
|
17036
|
+
logs
|
|
17037
|
+
});
|
|
16519
17038
|
} catch (e) {
|
|
16520
17039
|
reportError(e, "Failed to fetch build logs");
|
|
16521
17040
|
}
|
|
@@ -16884,8 +17403,7 @@ async function setEnv(rawArgs) {
|
|
|
16884
17403
|
const projectId = await requireProject(rawArgs, client);
|
|
16885
17404
|
displayProjectRef(rawArgs);
|
|
16886
17405
|
if (!parsed || !parsed.key) fail("Usage: rebase cloud env set KEY=VALUE [--secret]", void 0, "usage");
|
|
16887
|
-
|
|
16888
|
-
if (buildTimePrefix && !args["--force"]) fail(`${parsed.key} is read by your bundler at BUILD time, and project variables are applied at rollout — after the image is built. Setting it here would not reach the bundle.`, `Put ${buildTimePrefix}* variables in the source you deploy (a committed .env, or your build config), then \`rebase cloud deploy\`. Pass --force if your build genuinely reads this at run time.`, "build_time_variable");
|
|
17406
|
+
if (buildTimeEnvPrefix(parsed.key) && !args["--force"]) fail(`${parsed.key} is read by your bundler at BUILD time, and project variables are applied at rollout — after the image is built. Setting it here would not reach the bundle.`, `A managed deploy builds the frontend on this machine: set it in your shell when you run \`rebase cloud deploy\`${parsed.key === "VITE_API_URL" ? "" : ", or in the app's .env.production"}. A --source build runs on the platform and never receives .env files: set it in the build itself — an ENV line in your Dockerfile, or \`define\` in your bundler config. Pass --force if your build genuinely reads this at run time.`, "build_time_variable");
|
|
16889
17407
|
const body = {
|
|
16890
17408
|
key: parsed.key,
|
|
16891
17409
|
value: parsed.value
|
|
@@ -16983,6 +17501,28 @@ async function revealEnv(rawArgs) {
|
|
|
16983
17501
|
reportError(e, "Failed to reveal environment variable");
|
|
16984
17502
|
}
|
|
16985
17503
|
}
|
|
17504
|
+
/**
|
|
17505
|
+
* One `KEY=value` line that dotenv reads back as exactly `value`, or null when
|
|
17506
|
+
* no quoting can carry it.
|
|
17507
|
+
*
|
|
17508
|
+
* dotenv's quoting is not JSON's, and `JSON.stringify` was what this used: a
|
|
17509
|
+
* double-quoted value has only `\n` and `\r` unescaped, so every other
|
|
17510
|
+
* backslash stayed and `{"a":1}` came back as `{\"a\":1}`. Single quotes and
|
|
17511
|
+
* backticks are literal — nothing inside is unescaped, and either may span
|
|
17512
|
+
* lines — so they carry any value without their own quote character. Double
|
|
17513
|
+
* quotes come last: they cannot hold a `"` or a literal backslash-n, and they
|
|
17514
|
+
* are the only way to carry a carriage return, which dotenv folds into a
|
|
17515
|
+
* newline everywhere else.
|
|
17516
|
+
*/
|
|
17517
|
+
function dotenvLine(key, value) {
|
|
17518
|
+
if (!/[\s#'"`\\]/.test(value)) return `${key}=${value}`;
|
|
17519
|
+
if (!value.includes("\r")) {
|
|
17520
|
+
if (!value.includes("'")) return `${key}='${value}'`;
|
|
17521
|
+
if (!value.includes("`")) return `${key}=\`${value}\``;
|
|
17522
|
+
}
|
|
17523
|
+
if (!value.includes("\"") && !/\\[nr]/.test(value)) return `${key}="${value.replace(/\r/g, "\\r").replace(/\n/g, "\\n")}"`;
|
|
17524
|
+
return null;
|
|
17525
|
+
}
|
|
16986
17526
|
async function pullEnv(rawArgs) {
|
|
16987
17527
|
const { flags: args } = parseCloudArgs({
|
|
16988
17528
|
spec: {
|
|
@@ -17024,15 +17564,22 @@ async function pullEnv(rawArgs) {
|
|
|
17024
17564
|
projectId,
|
|
17025
17565
|
key: v.key
|
|
17026
17566
|
}, { path: "reveal" });
|
|
17027
|
-
const
|
|
17028
|
-
|
|
17567
|
+
const line = dotenvLine(v.key, revealed.value);
|
|
17568
|
+
if (line === null) {
|
|
17569
|
+
skipped.push({
|
|
17570
|
+
key: v.key,
|
|
17571
|
+
reason: "no .env quoting carries this value unchanged"
|
|
17572
|
+
});
|
|
17573
|
+
continue;
|
|
17574
|
+
}
|
|
17575
|
+
lines.push(line);
|
|
17029
17576
|
written.push(v.key);
|
|
17030
17577
|
}
|
|
17031
17578
|
fs.writeFileSync(outPath, lines.length ? lines.join("\n") + "\n" : "", { mode: 384 });
|
|
17032
17579
|
emit(() => {
|
|
17033
17580
|
success(`Wrote ${written.length} variable${written.length === 1 ? "" : "s"} to ${outPath}`);
|
|
17034
17581
|
if (skipped.length) {
|
|
17035
|
-
console.log(chalk.gray(` Skipped ${
|
|
17582
|
+
for (const s of skipped) console.log(chalk.gray(` Skipped ${s.key}: ${s.reason}`));
|
|
17036
17583
|
console.log("");
|
|
17037
17584
|
}
|
|
17038
17585
|
}, {
|
|
@@ -17086,27 +17633,129 @@ function printEnvHelp() {
|
|
|
17086
17633
|
//#endregion
|
|
17087
17634
|
//#region src/commands/cloud/domains.ts
|
|
17088
17635
|
/**
|
|
17089
|
-
* `rebase cloud domains` — a project
|
|
17090
|
-
*
|
|
17091
|
-
* domains list
|
|
17092
|
-
* domains add <domain>
|
|
17093
|
-
* domains verify
|
|
17094
|
-
* domains remove
|
|
17095
|
-
*
|
|
17096
|
-
*
|
|
17097
|
-
*
|
|
17098
|
-
*
|
|
17099
|
-
*
|
|
17100
|
-
*
|
|
17636
|
+
* `rebase cloud domains` — the hostnames a project is served on.
|
|
17637
|
+
*
|
|
17638
|
+
* domains list Every host, its status, and the DNS records it needs
|
|
17639
|
+
* domains add <domain> Register a host (starts, does not finish, setup)
|
|
17640
|
+
* domains verify [domain] Check the host's published DNS now; live only if it passes
|
|
17641
|
+
* domains remove <domain> Stop serving a host
|
|
17642
|
+
*
|
|
17643
|
+
* A project's hosts are rows of the control plane's `project_domains` table,
|
|
17644
|
+
* managed through its `domains` function — the same one the console uses. The
|
|
17645
|
+
* Ingress is built from those rows, one verified host at a time, so every
|
|
17646
|
+
* action here addresses a row. The single `projects.customDomain` column is a
|
|
17647
|
+
* projection the control plane keeps in step with them; writing it directly
|
|
17648
|
+
* changes nothing that is served.
|
|
17649
|
+
*
|
|
17650
|
+
* Two cases still go through that column, because the control plane still
|
|
17651
|
+
* serves them from it: a project with no rows at all, whose one domain lives
|
|
17652
|
+
* only in the column, and a control plane that has no `domains` function.
|
|
17653
|
+
*
|
|
17654
|
+
* The DNS record set comes from the server, never composed here: whether to
|
|
17655
|
+
* publish an A or a CNAME depends on apex-vs-subdomain and on the ingress
|
|
17656
|
+
* address behind the tenant host, which the CLI cannot know. Adding a host only
|
|
17657
|
+
* registers it — it is unverified until the records are published and `verify`
|
|
17658
|
+
* passes.
|
|
17659
|
+
*/
|
|
17660
|
+
/** The column's one domain as an entry, or none. */
|
|
17661
|
+
function columnEntry(setup) {
|
|
17662
|
+
if (!setup.domain) return [];
|
|
17663
|
+
return [{
|
|
17664
|
+
id: null,
|
|
17665
|
+
host: setup.domain,
|
|
17666
|
+
primary: true,
|
|
17667
|
+
status: setup.status === "verified" ? "verified" : "pending",
|
|
17668
|
+
verifiedAt: setup.verifiedAt ?? null,
|
|
17669
|
+
isApex: setup.isApex,
|
|
17670
|
+
tenantHost: setup.tenantHost,
|
|
17671
|
+
instructions: setup.instructions
|
|
17672
|
+
}];
|
|
17673
|
+
}
|
|
17674
|
+
/**
|
|
17675
|
+
* A row, or null for one with no id or host — it cannot be verified or removed,
|
|
17676
|
+
* so it is not offered as a domain. Anything but `verified` is pending: an
|
|
17677
|
+
* unreadable status must not read as live.
|
|
17678
|
+
*/
|
|
17679
|
+
function rowEntry(row) {
|
|
17680
|
+
if (typeof row?.id !== "string" || typeof row.host !== "string") return null;
|
|
17681
|
+
return {
|
|
17682
|
+
id: row.id,
|
|
17683
|
+
host: row.host,
|
|
17684
|
+
primary: row.primary === true,
|
|
17685
|
+
status: row.status === "verified" ? "verified" : "pending",
|
|
17686
|
+
verifiedAt: row.verifiedAt ?? null,
|
|
17687
|
+
isApex: row.isApex,
|
|
17688
|
+
tenantHost: row.tenantHost,
|
|
17689
|
+
instructions: row.instructions
|
|
17690
|
+
};
|
|
17691
|
+
}
|
|
17692
|
+
/**
|
|
17693
|
+
* Whether a failed `domains` call means the control plane has no such function
|
|
17694
|
+
* — as opposed to refusing this project, which the function answers `not_found`.
|
|
17101
17695
|
*/
|
|
17102
|
-
|
|
17696
|
+
function noDomainsFunction(e) {
|
|
17697
|
+
if (typeof e !== "object" || e === null) return false;
|
|
17698
|
+
const status = "status" in e ? e.status : void 0;
|
|
17699
|
+
const code = "code" in e ? e.code : void 0;
|
|
17700
|
+
return status === 404 && code !== "not_found";
|
|
17701
|
+
}
|
|
17702
|
+
async function fetchColumnSetup(client, projectId) {
|
|
17103
17703
|
return client.functions.invoke("verify-domain", void 0, {
|
|
17104
17704
|
method: "GET",
|
|
17105
17705
|
path: projectId
|
|
17106
17706
|
});
|
|
17107
17707
|
}
|
|
17108
|
-
|
|
17109
|
-
|
|
17708
|
+
/** Every host this project is served on, or may be once verified. */
|
|
17709
|
+
async function loadDomains(client, projectId) {
|
|
17710
|
+
let listed;
|
|
17711
|
+
try {
|
|
17712
|
+
listed = await client.functions.invoke("domains", void 0, {
|
|
17713
|
+
method: "GET",
|
|
17714
|
+
path: projectId
|
|
17715
|
+
});
|
|
17716
|
+
} catch (e) {
|
|
17717
|
+
if (!noDomainsFunction(e)) throw e;
|
|
17718
|
+
const setup = await fetchColumnSetup(client, projectId);
|
|
17719
|
+
return {
|
|
17720
|
+
model: "column",
|
|
17721
|
+
tenantHost: setup.tenantHost,
|
|
17722
|
+
domains: columnEntry(setup)
|
|
17723
|
+
};
|
|
17724
|
+
}
|
|
17725
|
+
const domains = (listed.domains ?? []).map(rowEntry).filter((d) => d !== null);
|
|
17726
|
+
if (domains.length > 0) return {
|
|
17727
|
+
model: "rows",
|
|
17728
|
+
tenantHost: listed.tenantHost,
|
|
17729
|
+
domains
|
|
17730
|
+
};
|
|
17731
|
+
const setup = await fetchColumnSetup(client, projectId);
|
|
17732
|
+
return {
|
|
17733
|
+
model: "rows",
|
|
17734
|
+
tenantHost: listed.tenantHost ?? setup.tenantHost,
|
|
17735
|
+
domains: columnEntry(setup)
|
|
17736
|
+
};
|
|
17737
|
+
}
|
|
17738
|
+
/**
|
|
17739
|
+
* The host an action names, or the only one there is to name.
|
|
17740
|
+
*
|
|
17741
|
+
* Refuses to guess among several: removing or verifying the wrong host of a
|
|
17742
|
+
* live site is not a mistake worth saving a word over.
|
|
17743
|
+
*/
|
|
17744
|
+
function pickDomain(set, named, action) {
|
|
17745
|
+
if (named) {
|
|
17746
|
+
const wanted = named.trim().toLowerCase().replace(/\.$/, "");
|
|
17747
|
+
const found = set.domains.find((d) => d.host.toLowerCase() === wanted);
|
|
17748
|
+
if (!found) fail(`This project has no domain ${named}.`, set.domains.length ? `Its domains: ${set.domains.map((d) => d.host).join(", ")}.` : "It has no custom domains. Add one with `rebase cloud domains add <domain>`.", "not_found");
|
|
17749
|
+
return found;
|
|
17750
|
+
}
|
|
17751
|
+
if (set.domains.length === 1) return set.domains[0];
|
|
17752
|
+
if (set.domains.length === 0) fail("This project has no custom domains.", "Add one with `rebase cloud domains add <domain>`.", "not_found");
|
|
17753
|
+
const pending = set.domains.filter((d) => d.status !== "verified");
|
|
17754
|
+
if (action === "verify" && pending.length === 1) return pending[0];
|
|
17755
|
+
fail(`This project has ${set.domains.length} domains; name the one to ${action}.`, `Run \`rebase cloud domains ${action} <domain>\` with one of: ${set.domains.map((d) => d.host).join(", ")}.`, "usage");
|
|
17756
|
+
}
|
|
17757
|
+
function printRecords(instructions) {
|
|
17758
|
+
const recs = [instructions?.pointing, instructions?.ownership].filter(Boolean);
|
|
17110
17759
|
if (!recs.length) return;
|
|
17111
17760
|
console.log(chalk.bold(" DNS records to publish:"));
|
|
17112
17761
|
for (const r of recs) console.log(` ${chalk.cyan(r.type)} ${r.name} → ${r.values.join(", ")}`);
|
|
@@ -17124,7 +17773,7 @@ async function domainsCommand(action, rawArgs) {
|
|
|
17124
17773
|
await addDomain(rawArgs);
|
|
17125
17774
|
break;
|
|
17126
17775
|
case "verify":
|
|
17127
|
-
await
|
|
17776
|
+
await verifyDomain(rawArgs);
|
|
17128
17777
|
break;
|
|
17129
17778
|
case "remove":
|
|
17130
17779
|
case "rm":
|
|
@@ -17149,47 +17798,50 @@ async function listDomains(rawArgs) {
|
|
|
17149
17798
|
const projectId = await requireProject(rawArgs, client);
|
|
17150
17799
|
const projectRef = displayProjectRef(rawArgs);
|
|
17151
17800
|
try {
|
|
17152
|
-
const
|
|
17801
|
+
const set = await loadDomains(client, projectId);
|
|
17153
17802
|
emit(() => {
|
|
17154
17803
|
console.log("");
|
|
17155
|
-
console.log(chalk.bold(` 🌐 Custom
|
|
17804
|
+
console.log(chalk.bold(` 🌐 Custom domains — project ${projectRef}`));
|
|
17156
17805
|
console.log("");
|
|
17157
|
-
if (!
|
|
17158
|
-
console.log(chalk.gray(" No custom
|
|
17806
|
+
if (!set.domains.length) {
|
|
17807
|
+
console.log(chalk.gray(" No custom domains. Add one with `rebase cloud domains add <domain>`."));
|
|
17159
17808
|
console.log("");
|
|
17160
17809
|
return;
|
|
17161
17810
|
}
|
|
17162
|
-
|
|
17163
|
-
["
|
|
17164
|
-
|
|
17165
|
-
|
|
17166
|
-
|
|
17167
|
-
|
|
17168
|
-
|
|
17811
|
+
if (set.tenantHost) {
|
|
17812
|
+
keyValues([["Tenant host", set.tenantHost]]);
|
|
17813
|
+
console.log("");
|
|
17814
|
+
}
|
|
17815
|
+
for (const d of set.domains) {
|
|
17816
|
+
const status = d.status === "verified" ? chalk.green(d.status) : chalk.yellow(d.status);
|
|
17817
|
+
console.log(` ${chalk.bold(d.host)} ${status}${d.primary && set.domains.length > 1 ? chalk.gray(" primary") : ""}`);
|
|
17818
|
+
if (d.verifiedAt) console.log(chalk.gray(` verified ${d.verifiedAt}`));
|
|
17819
|
+
if (d.status !== "verified") printRecords(d.instructions);
|
|
17820
|
+
}
|
|
17169
17821
|
console.log("");
|
|
17170
|
-
if (setup.status !== "verified") printRecords(setup);
|
|
17171
17822
|
}, {
|
|
17172
17823
|
projectId,
|
|
17173
|
-
|
|
17824
|
+
tenantHost: set.tenantHost ?? null,
|
|
17825
|
+
domains: set.domains
|
|
17174
17826
|
});
|
|
17175
17827
|
} catch (e) {
|
|
17176
|
-
reportError(e, "Failed to load custom
|
|
17828
|
+
reportError(e, "Failed to load custom domains");
|
|
17177
17829
|
}
|
|
17178
17830
|
}
|
|
17179
17831
|
/**
|
|
17180
|
-
* The domain
|
|
17832
|
+
* The domain an action was given, or undefined.
|
|
17181
17833
|
*
|
|
17182
17834
|
* Exported so its tests drive the real parser. Under the old operand filter
|
|
17183
17835
|
* `rebase cloud domains add -p acme` registered a domain called "acme" — the
|
|
17184
17836
|
* project slug, read out of `--project`'s own value — and a registered domain
|
|
17185
17837
|
* is a project-record write, not a no-op.
|
|
17186
17838
|
*/
|
|
17187
|
-
function resolveDomainArg(rawArgs) {
|
|
17839
|
+
function resolveDomainArg(rawArgs, action = "add") {
|
|
17188
17840
|
return parseCloudArgs({
|
|
17189
17841
|
spec: {},
|
|
17190
17842
|
rawArgs,
|
|
17191
17843
|
commandWords: 3,
|
|
17192
|
-
command:
|
|
17844
|
+
command: `cloud domains ${action}`,
|
|
17193
17845
|
maxPositionals: 1
|
|
17194
17846
|
}).positionals[0];
|
|
17195
17847
|
}
|
|
@@ -17198,41 +17850,51 @@ async function addDomain(rawArgs) {
|
|
|
17198
17850
|
if (!domain) fail("Usage: rebase cloud domains add <domain>", void 0, "usage");
|
|
17199
17851
|
const { client } = await requireClient(rawArgs);
|
|
17200
17852
|
const projectId = await requireProject(rawArgs, client);
|
|
17853
|
+
let added;
|
|
17201
17854
|
try {
|
|
17202
|
-
await client
|
|
17203
|
-
|
|
17204
|
-
|
|
17205
|
-
|
|
17206
|
-
|
|
17207
|
-
|
|
17208
|
-
|
|
17209
|
-
|
|
17210
|
-
|
|
17211
|
-
projectId,
|
|
17212
|
-
...setup
|
|
17213
|
-
});
|
|
17855
|
+
const set = await loadDomains(client, projectId);
|
|
17856
|
+
if (set.model === "column") {
|
|
17857
|
+
await client.data.collection("projects").update(projectId, { customDomain: domain });
|
|
17858
|
+
added = columnEntry(await fetchColumnSetup(client, projectId))[0];
|
|
17859
|
+
} else {
|
|
17860
|
+
const legacy = set.domains.find((d) => d.id === null);
|
|
17861
|
+
if (legacy && legacy.host.toLowerCase() !== domain.trim().toLowerCase()) warn(`${legacy.host} is held outside this project's host list, and stops being served once another host is verified.`, `Keep it by adding it too: \`rebase cloud domains add ${legacy.host}\`, then \`rebase cloud domains verify ${legacy.host}\`.`);
|
|
17862
|
+
added = rowEntry(await client.functions.invoke("domains", { host: domain }, { path: projectId })) ?? void 0;
|
|
17863
|
+
}
|
|
17214
17864
|
} catch (e) {
|
|
17215
17865
|
reportError(e, "Failed to register domain");
|
|
17216
17866
|
}
|
|
17217
|
-
}
|
|
17218
|
-
|
|
17219
|
-
|
|
17220
|
-
|
|
17221
|
-
|
|
17222
|
-
|
|
17223
|
-
|
|
17224
|
-
|
|
17867
|
+
if (!added) fail(`The control plane did not return ${domain}, so whether it was registered is unknown.`, "Run `rebase cloud domains list`.");
|
|
17868
|
+
const registered = added;
|
|
17869
|
+
emit(() => {
|
|
17870
|
+
success(`Registered ${chalk.bold(registered.host)} — not yet verified`);
|
|
17871
|
+
printRecords(registered.instructions);
|
|
17872
|
+
console.log(chalk.gray(` Publish the records above, then run \`rebase cloud domains verify ${registered.host}\`.`));
|
|
17873
|
+
console.log("");
|
|
17874
|
+
}, {
|
|
17875
|
+
success: true,
|
|
17876
|
+
projectId,
|
|
17877
|
+
...registered
|
|
17225
17878
|
});
|
|
17879
|
+
}
|
|
17880
|
+
async function verifyDomain(rawArgs) {
|
|
17881
|
+
const named = resolveDomainArg(rawArgs, "verify");
|
|
17226
17882
|
const { client } = await requireClient(rawArgs);
|
|
17227
17883
|
const projectId = await requireProject(rawArgs, client);
|
|
17228
|
-
|
|
17884
|
+
let set;
|
|
17229
17885
|
try {
|
|
17230
|
-
|
|
17886
|
+
set = await loadDomains(client, projectId);
|
|
17887
|
+
} catch (e) {
|
|
17888
|
+
reportError(e, "Failed to load custom domains");
|
|
17889
|
+
}
|
|
17890
|
+
const target = pickDomain(set, named, "verify");
|
|
17891
|
+
try {
|
|
17892
|
+
const res = verifyOutcome(target.id === null ? await client.functions.invoke("verify-domain", {}, { path: projectId }) : await client.functions.invoke("domains", {}, { path: `${projectId}/${target.id}/verify` }), target);
|
|
17231
17893
|
emit(() => {
|
|
17232
17894
|
console.log("");
|
|
17233
|
-
if (res.verified) success(`${
|
|
17895
|
+
if (res.verified) success(`${target.host} is verified and live`);
|
|
17234
17896
|
else {
|
|
17235
|
-
console.log(chalk.yellow(` ⚠ ${
|
|
17897
|
+
console.log(chalk.yellow(` ⚠ ${target.host} is not verified yet`));
|
|
17236
17898
|
console.log("");
|
|
17237
17899
|
const rows = [["Ownership", res.checks.ownership], ["Pointing", res.checks.pointing]];
|
|
17238
17900
|
for (const [label, check] of rows) {
|
|
@@ -17243,41 +17905,78 @@ async function verifyDomains(rawArgs) {
|
|
|
17243
17905
|
if (check.error) console.log(chalk.gray(` error: ${check.error}`));
|
|
17244
17906
|
}
|
|
17245
17907
|
console.log("");
|
|
17246
|
-
printRecords(res);
|
|
17908
|
+
printRecords(res.entry.instructions);
|
|
17247
17909
|
}
|
|
17248
17910
|
}, {
|
|
17249
17911
|
projectId,
|
|
17912
|
+
id: res.entry.id,
|
|
17913
|
+
host: res.entry.host,
|
|
17250
17914
|
verified: res.verified,
|
|
17251
|
-
status: res.status,
|
|
17252
|
-
domain: res.domain,
|
|
17915
|
+
status: res.entry.status,
|
|
17253
17916
|
checks: res.checks,
|
|
17254
|
-
instructions: res.instructions
|
|
17917
|
+
instructions: res.entry.instructions ?? null
|
|
17255
17918
|
});
|
|
17256
17919
|
if (!res.verified) process.exit(1);
|
|
17257
17920
|
} catch (e) {
|
|
17258
17921
|
reportError(e, "Failed to verify domain");
|
|
17259
17922
|
}
|
|
17260
17923
|
}
|
|
17924
|
+
/** A verify answer from either door, as one shape. */
|
|
17925
|
+
function verifyOutcome(res, target) {
|
|
17926
|
+
const verified = res.verified === true;
|
|
17927
|
+
const unchecked = {
|
|
17928
|
+
ok: false,
|
|
17929
|
+
expected: [],
|
|
17930
|
+
observed: []
|
|
17931
|
+
};
|
|
17932
|
+
return {
|
|
17933
|
+
verified,
|
|
17934
|
+
checks: res.checks ?? {
|
|
17935
|
+
ownership: unchecked,
|
|
17936
|
+
pointing: unchecked
|
|
17937
|
+
},
|
|
17938
|
+
entry: {
|
|
17939
|
+
...target,
|
|
17940
|
+
status: verified || res.status === "verified" ? "verified" : "pending",
|
|
17941
|
+
verifiedAt: res.verifiedAt ?? target.verifiedAt,
|
|
17942
|
+
instructions: res.instructions ?? target.instructions
|
|
17943
|
+
}
|
|
17944
|
+
};
|
|
17945
|
+
}
|
|
17261
17946
|
async function removeDomain(rawArgs) {
|
|
17262
|
-
const { flags: args } = parseCloudArgs({
|
|
17947
|
+
const { flags: args, positionals } = parseCloudArgs({
|
|
17263
17948
|
spec: {},
|
|
17264
17949
|
rawArgs,
|
|
17265
17950
|
commandWords: 3,
|
|
17266
17951
|
command: "cloud domains remove",
|
|
17267
|
-
maxPositionals:
|
|
17952
|
+
maxPositionals: 1
|
|
17268
17953
|
});
|
|
17269
17954
|
const { client } = await requireClient(rawArgs);
|
|
17270
17955
|
const projectId = await requireProject(rawArgs, client);
|
|
17271
17956
|
const projectRef = displayProjectRef(rawArgs);
|
|
17957
|
+
let set;
|
|
17958
|
+
try {
|
|
17959
|
+
set = await loadDomains(client, projectId);
|
|
17960
|
+
} catch (e) {
|
|
17961
|
+
reportError(e, "Failed to load custom domains");
|
|
17962
|
+
}
|
|
17963
|
+
const target = pickDomain(set, positionals[0], "remove");
|
|
17272
17964
|
await confirmDestructive({
|
|
17273
17965
|
yes: Boolean(args["--yes"]),
|
|
17274
|
-
prompt: `
|
|
17966
|
+
prompt: `Stop serving ${target.host} on project ${projectRef}?`
|
|
17275
17967
|
});
|
|
17276
17968
|
try {
|
|
17277
|
-
|
|
17278
|
-
|
|
17969
|
+
let removed = true;
|
|
17970
|
+
if (target.id === null) await client.data.collection("projects").update(projectId, { customDomain: "" });
|
|
17971
|
+
else removed = (await client.functions.invoke("domains", void 0, {
|
|
17972
|
+
method: "DELETE",
|
|
17973
|
+
path: `${projectId}/${target.id}`
|
|
17974
|
+
}))?.removed !== false;
|
|
17975
|
+
emit(() => success(removed ? `${target.host} is no longer served by project ${projectRef}` : `${target.host} was already gone from project ${projectRef}`), {
|
|
17279
17976
|
success: true,
|
|
17280
|
-
projectId
|
|
17977
|
+
projectId,
|
|
17978
|
+
host: target.host,
|
|
17979
|
+
removed
|
|
17281
17980
|
});
|
|
17282
17981
|
} catch (e) {
|
|
17283
17982
|
reportError(e, "Failed to remove domain");
|
|
@@ -17286,27 +17985,33 @@ async function removeDomain(rawArgs) {
|
|
|
17286
17985
|
function printDomainsHelp() {
|
|
17287
17986
|
printGroupHelp({
|
|
17288
17987
|
command: "cloud domains",
|
|
17289
|
-
title: "Custom
|
|
17988
|
+
title: "Custom domains",
|
|
17290
17989
|
actions: [
|
|
17291
17990
|
{
|
|
17292
17991
|
action: "list",
|
|
17293
|
-
description: "
|
|
17992
|
+
description: "Every host, its status, and the DNS records it needs"
|
|
17294
17993
|
},
|
|
17295
17994
|
{
|
|
17296
17995
|
action: "add",
|
|
17297
17996
|
args: "<domain>",
|
|
17298
|
-
description: "Register a
|
|
17997
|
+
description: "Register a host"
|
|
17299
17998
|
},
|
|
17300
17999
|
{
|
|
17301
18000
|
action: "verify",
|
|
17302
|
-
|
|
18001
|
+
args: "[domain]",
|
|
18002
|
+
description: "Check a host's DNS and put it live"
|
|
17303
18003
|
},
|
|
17304
18004
|
{
|
|
17305
18005
|
action: "remove",
|
|
17306
|
-
|
|
18006
|
+
args: "<domain>",
|
|
18007
|
+
description: "Stop serving a host"
|
|
17307
18008
|
}
|
|
17308
18009
|
],
|
|
17309
|
-
notes: [
|
|
18010
|
+
notes: [
|
|
18011
|
+
"A project can be served on several hosts; each is verified on its own.",
|
|
18012
|
+
"A CNAME is invalid at a zone apex: use an A record there and a CNAME below it.",
|
|
18013
|
+
"`list` prints the exact records to add — copy them rather than deriving them."
|
|
18014
|
+
]
|
|
17310
18015
|
});
|
|
17311
18016
|
}
|
|
17312
18017
|
//#endregion
|
|
@@ -17904,6 +18609,25 @@ function resolveDeploymentIdArg(rawArgs, command) {
|
|
|
17904
18609
|
id: positionals[0]
|
|
17905
18610
|
};
|
|
17906
18611
|
}
|
|
18612
|
+
/**
|
|
18613
|
+
* One deployment by id, when it belongs to `projectId`; undefined otherwise.
|
|
18614
|
+
*
|
|
18615
|
+
* A read that fails is "not found" too: the caller's refusal says so, and the
|
|
18616
|
+
* control plane checks the same ownership again before rolling back.
|
|
18617
|
+
*/
|
|
18618
|
+
async function deploymentOfProject(client, deploymentId, projectId) {
|
|
18619
|
+
let row;
|
|
18620
|
+
try {
|
|
18621
|
+
row = await client.data.collection("deployments").findById(deploymentId);
|
|
18622
|
+
} catch {
|
|
18623
|
+
return;
|
|
18624
|
+
}
|
|
18625
|
+
if (!row) return void 0;
|
|
18626
|
+
const owner = row.project ?? row.projectId ?? row.project_id;
|
|
18627
|
+
const ownerId = typeof owner === "object" && owner !== null && "id" in owner ? owner.id : owner;
|
|
18628
|
+
if (ownerId !== void 0 && ownerId !== null && String(ownerId) !== projectId) return void 0;
|
|
18629
|
+
return cloudRow(row);
|
|
18630
|
+
}
|
|
17907
18631
|
async function rollbackCommand(rawArgs) {
|
|
17908
18632
|
const { flags: args, id: explicitId } = resolveDeploymentIdArg(rawArgs, "cloud rollback");
|
|
17909
18633
|
const { client } = await requireClient(rawArgs);
|
|
@@ -17918,7 +18642,7 @@ async function rollbackCommand(rawArgs) {
|
|
|
17918
18642
|
if (!rows.length) fail("No deployments to roll back to.", void 0, "no_deployments");
|
|
17919
18643
|
let target;
|
|
17920
18644
|
if (explicitId) {
|
|
17921
|
-
target = rows.find((d) => String(d.id) === explicitId);
|
|
18645
|
+
target = rows.find((d) => String(d.id) === explicitId) ?? await deploymentOfProject(client, explicitId, projectId);
|
|
17922
18646
|
if (!target) fail(`Deployment ${explicitId} not found for project ${projectRef}.`, void 0, "not_found");
|
|
17923
18647
|
if (!isRollbackable(target)) fail(`Deployment ${explicitId} is not rollbackable (${ROLLBACKABLE_RULE}).`, "List candidates with `rebase cloud deployments list`.", "deploy_not_rollbackable");
|
|
17924
18648
|
} else {
|
|
@@ -19075,6 +19799,8 @@ var ACTION_HELP = {
|
|
|
19075
19799
|
["--bundle-dir <path>", "Deploy a bundle that is already built"],
|
|
19076
19800
|
["--source <path>", "Upload this directory and build a container image from it"],
|
|
19077
19801
|
["--skip-type-check", "Compile without type checking, as `rebase build` does"],
|
|
19802
|
+
["--skip-schema", "Keep the committed database schema instead of regenerating it"],
|
|
19803
|
+
["--no-static", "Deploy the backend without folding the static apps into its bundle"],
|
|
19078
19804
|
["--no-source", "Do not upload the project's source beside a backend bundle"],
|
|
19079
19805
|
["--allow-downgrade", "Deploy a bundle built on an older framework release than the project runs"],
|
|
19080
19806
|
["--eject", "Leave the managed runtime on purpose (ejects to a container image)"]
|
|
@@ -19199,7 +19925,7 @@ var ACTION_HELP = {
|
|
|
19199
19925
|
"domains add": {
|
|
19200
19926
|
command: "cloud domains add",
|
|
19201
19927
|
usage: "cloud domains add <domain>",
|
|
19202
|
-
summary: "Register a custom domain on this project and print the DNS records to publish. Registering is not verifying — run `domains verify
|
|
19928
|
+
summary: "Register a custom domain on this project, beside any it already has, and print the DNS records to publish. Registering is not verifying — run `domains verify <domain>` once the records are live.",
|
|
19203
19929
|
flags: [],
|
|
19204
19930
|
examples: ["rebase cloud domains add app.example.com"],
|
|
19205
19931
|
notes: ["At a zone apex use the A record this prints, not a CNAME: a CNAME is invalid at an apex.", "Verification is a separate command because DNS propagation is not instant."]
|
|
@@ -19207,7 +19933,7 @@ var ACTION_HELP = {
|
|
|
19207
19933
|
"domains remove": {
|
|
19208
19934
|
command: "cloud domains remove",
|
|
19209
19935
|
usage: "cloud domains remove <domain>",
|
|
19210
|
-
summary: "
|
|
19936
|
+
summary: "Stop serving one custom domain. The project keeps its other domains and its <slug>.rebase.website host. The domain may be left out when the project has only one. Aliases: `rm`, `delete`.",
|
|
19211
19937
|
flags: [],
|
|
19212
19938
|
examples: ["rebase cloud domains remove app.example.com"]
|
|
19213
19939
|
},
|
|
@@ -19215,7 +19941,7 @@ var ACTION_HELP = {
|
|
|
19215
19941
|
command: "cloud env set",
|
|
19216
19942
|
usage: "cloud env set KEY=VALUE | KEY VALUE [--secret] [--force]",
|
|
19217
19943
|
summary: "Set one environment variable on the project. Takes effect on the next deploy — a running instance does not pick it up.",
|
|
19218
|
-
flags: [["--secret", "Store it encrypted and never return it in a listing"], ["--force", "
|
|
19944
|
+
flags: [["--secret", "Store it encrypted and never return it in a listing"], ["--force", "Set a build-time key (VITE_*, NEXT_PUBLIC_*, …) anyway, for a build that reads it at run time"]],
|
|
19219
19945
|
examples: [
|
|
19220
19946
|
"rebase cloud env set STRIPE_KEY=sk_live_… --secret",
|
|
19221
19947
|
"rebase cloud env set LOG_LEVEL debug",
|
|
@@ -19376,7 +20102,11 @@ var ACTION_HELP = {
|
|
|
19376
20102
|
"rebase cloud login --email me@example.com",
|
|
19377
20103
|
"rebase cloud login --url https://cloud.example.com"
|
|
19378
20104
|
],
|
|
19379
|
-
notes: [
|
|
20105
|
+
notes: [
|
|
20106
|
+
"--password warns, once, before the request: by the time a login succeeds the password is already in the history file.",
|
|
20107
|
+
"There is no machine token yet, so CI genuinely needs a human's credentials. REBASE_CLOUD_EMAIL and REBASE_CLOUD_PASSWORD are how a secret store hands them over without them appearing on a command line.",
|
|
20108
|
+
"In a directory whose .rebase/cloud.json names a control plane you have not signed in to before, login names it and asks first, and refuses without a terminal: a cloned repository does not choose where your password goes. Pass --url <that host> to sign in there on purpose."
|
|
20109
|
+
]
|
|
19380
20110
|
},
|
|
19381
20111
|
logout: {
|
|
19382
20112
|
command: "cloud logout",
|
|
@@ -19868,7 +20598,7 @@ var CLOUD_GROUPS = [
|
|
|
19868
20598
|
{
|
|
19869
20599
|
action: "domains",
|
|
19870
20600
|
section: "Config",
|
|
19871
|
-
description: "Custom
|
|
20601
|
+
description: "Custom domains, their DNS records, and verification"
|
|
19872
20602
|
},
|
|
19873
20603
|
{
|
|
19874
20604
|
action: "extensions",
|
|
@@ -19921,7 +20651,8 @@ var CLOUD_GROUPS = [
|
|
|
19921
20651
|
description: "The organization's billing account and card on file"
|
|
19922
20652
|
}
|
|
19923
20653
|
];
|
|
19924
|
-
|
|
20654
|
+
/** The dispatch words alone — what `--help` routing and the tests iterate. */
|
|
20655
|
+
var CLOUD_GROUP_NAMES = CLOUD_GROUPS.map((g) => g.action);
|
|
19925
20656
|
function printCloudHelp() {
|
|
19926
20657
|
printGroupHelp({
|
|
19927
20658
|
command: "cloud",
|
|
@@ -20210,11 +20941,22 @@ function classifySpec(spec) {
|
|
|
20210
20941
|
kind: "local",
|
|
20211
20942
|
protocol
|
|
20212
20943
|
};
|
|
20944
|
+
const catalog = /^catalog:(.*)$/.exec(spec.trim());
|
|
20945
|
+
if (catalog) return {
|
|
20946
|
+
kind: "catalog",
|
|
20947
|
+
catalog: catalog[1].trim() || "default"
|
|
20948
|
+
};
|
|
20213
20949
|
return {
|
|
20214
20950
|
kind: "other",
|
|
20215
20951
|
reason: otherReason(spec)
|
|
20216
20952
|
};
|
|
20217
20953
|
}
|
|
20954
|
+
/** Why a spec that does not move is left alone, as the report says it. */
|
|
20955
|
+
function leftAloneReason(cls) {
|
|
20956
|
+
if (cls.kind === "local") return `a local ${cls.protocol} path, which points at a checkout on this machine rather than a release`;
|
|
20957
|
+
if (cls.kind === "catalog") return "a catalog: reference inside a catalog, which pnpm does not resolve";
|
|
20958
|
+
return cls.reason;
|
|
20959
|
+
}
|
|
20218
20960
|
function otherReason(spec) {
|
|
20219
20961
|
const trimmed = spec.trim();
|
|
20220
20962
|
if (trimmed.startsWith("workspace:")) return "a workspace: link, resolved inside this repository rather than from the registry";
|
|
@@ -20274,6 +21016,9 @@ function discoverProjectFiles(projectRoot) {
|
|
|
20274
21016
|
walk(projectRoot);
|
|
20275
21017
|
return found;
|
|
20276
21018
|
}
|
|
21019
|
+
function catalogKey(catalog, pkg) {
|
|
21020
|
+
return `${catalog}\u0000${pkg}`;
|
|
21021
|
+
}
|
|
20277
21022
|
/** Project-relative, POSIX. What every file path in the output looks like. */
|
|
20278
21023
|
function relativeTo(projectRoot, absolute) {
|
|
20279
21024
|
return path.relative(projectRoot, absolute).split(path.sep).join("/");
|
|
@@ -20291,13 +21036,18 @@ function planUpgrade(projectRoot, target, options = {}) {
|
|
|
20291
21036
|
skipped: [],
|
|
20292
21037
|
overrides: [],
|
|
20293
21038
|
unreadable: [],
|
|
20294
|
-
writes: []
|
|
21039
|
+
writes: [],
|
|
21040
|
+
unchanged: 0
|
|
21041
|
+
};
|
|
21042
|
+
const ledger = {
|
|
21043
|
+
refs: [],
|
|
21044
|
+
defined: /* @__PURE__ */ new Set()
|
|
20295
21045
|
};
|
|
20296
21046
|
const files = discoverProjectFiles(projectRoot);
|
|
20297
21047
|
for (const absolute of files.packageJsons) {
|
|
20298
21048
|
const file = relativeTo(projectRoot, absolute);
|
|
20299
21049
|
const original = fs.readFileSync(absolute, "utf8");
|
|
20300
|
-
const content = planPackageJson(file, original, target, options, plan);
|
|
21050
|
+
const content = planPackageJson(file, original, target, options, plan, ledger);
|
|
20301
21051
|
if (content !== null && content !== original) plan.writes.push({
|
|
20302
21052
|
file,
|
|
20303
21053
|
absolute,
|
|
@@ -20307,13 +21057,23 @@ function planUpgrade(projectRoot, target, options = {}) {
|
|
|
20307
21057
|
for (const absolute of files.workspaceYamls) {
|
|
20308
21058
|
const file = relativeTo(projectRoot, absolute);
|
|
20309
21059
|
const original = fs.readFileSync(absolute, "utf8");
|
|
20310
|
-
const content = planWorkspaceYaml(file, original, target, options, plan);
|
|
21060
|
+
const content = planWorkspaceYaml(file, original, target, options, plan, ledger);
|
|
20311
21061
|
if (content !== original) plan.writes.push({
|
|
20312
21062
|
file,
|
|
20313
21063
|
absolute,
|
|
20314
21064
|
content
|
|
20315
21065
|
});
|
|
20316
21066
|
}
|
|
21067
|
+
for (const ref of ledger.refs) {
|
|
21068
|
+
if (ledger.defined.has(catalogKey(ref.catalog, ref.pkg))) continue;
|
|
21069
|
+
plan.skipped.push({
|
|
21070
|
+
file: ref.file,
|
|
21071
|
+
name: ref.name,
|
|
21072
|
+
field: ref.field,
|
|
21073
|
+
spec: ref.spec,
|
|
21074
|
+
reason: `a pnpm catalog: reference, and no catalog under this project defines it — move the version where the "${ref.catalog}" catalog is declared`
|
|
21075
|
+
});
|
|
21076
|
+
}
|
|
20317
21077
|
return plan;
|
|
20318
21078
|
}
|
|
20319
21079
|
/** Write what the plan changes. Nothing else on disk is touched. */
|
|
@@ -20326,7 +21086,7 @@ var JSON_OVERRIDE_BLOCKS = [
|
|
|
20326
21086
|
["overrides"],
|
|
20327
21087
|
["resolutions"]
|
|
20328
21088
|
];
|
|
20329
|
-
function planPackageJson(file, original, target, options, plan) {
|
|
21089
|
+
function planPackageJson(file, original, target, options, plan, ledger) {
|
|
20330
21090
|
const bom = original.startsWith("") ? "" : "";
|
|
20331
21091
|
const text = original.slice(bom.length);
|
|
20332
21092
|
let parsed;
|
|
@@ -20371,7 +21131,10 @@ function planPackageJson(file, original, target, options, plan) {
|
|
|
20371
21131
|
const cls = classifySpec(spec);
|
|
20372
21132
|
if (cls.kind === "movable") {
|
|
20373
21133
|
const to = `${cls.prefix}${target}`;
|
|
20374
|
-
if (to === spec)
|
|
21134
|
+
if (to === spec) {
|
|
21135
|
+
plan.unchanged++;
|
|
21136
|
+
continue;
|
|
21137
|
+
}
|
|
20375
21138
|
edits.push({
|
|
20376
21139
|
start: member.value.start,
|
|
20377
21140
|
end: member.value.end,
|
|
@@ -20385,16 +21148,21 @@ function planPackageJson(file, original, target, options, plan) {
|
|
|
20385
21148
|
from: spec,
|
|
20386
21149
|
to
|
|
20387
21150
|
});
|
|
20388
|
-
} else {
|
|
20389
|
-
|
|
20390
|
-
|
|
20391
|
-
|
|
20392
|
-
|
|
20393
|
-
|
|
20394
|
-
|
|
20395
|
-
|
|
20396
|
-
|
|
20397
|
-
|
|
21151
|
+
} else if (cls.kind === "catalog") ledger.refs.push({
|
|
21152
|
+
file,
|
|
21153
|
+
name: member.key,
|
|
21154
|
+
field,
|
|
21155
|
+
spec,
|
|
21156
|
+
catalog: cls.catalog,
|
|
21157
|
+
pkg: member.key
|
|
21158
|
+
});
|
|
21159
|
+
else plan.skipped.push({
|
|
21160
|
+
file,
|
|
21161
|
+
name: member.key,
|
|
21162
|
+
field,
|
|
21163
|
+
spec,
|
|
21164
|
+
reason: leftAloneReason(cls)
|
|
21165
|
+
});
|
|
20398
21166
|
}
|
|
20399
21167
|
}
|
|
20400
21168
|
for (const blockPath of JSON_OVERRIDE_BLOCKS) {
|
|
@@ -20415,9 +21183,13 @@ function planPackageJson(file, original, target, options, plan) {
|
|
|
20415
21183
|
}
|
|
20416
21184
|
const spec = member.value.value;
|
|
20417
21185
|
const cls = classifySpec(spec);
|
|
21186
|
+
const pkg = overrideTarget(member.key);
|
|
20418
21187
|
if (cls.kind === "movable") {
|
|
20419
21188
|
const to = `${cls.prefix}${target}`;
|
|
20420
|
-
if (to === spec)
|
|
21189
|
+
if (to === spec) {
|
|
21190
|
+
plan.unchanged++;
|
|
21191
|
+
continue;
|
|
21192
|
+
}
|
|
20421
21193
|
edits.push({
|
|
20422
21194
|
start: member.value.start,
|
|
20423
21195
|
end: member.value.end,
|
|
@@ -20431,7 +21203,15 @@ function planPackageJson(file, original, target, options, plan) {
|
|
|
20431
21203
|
action: "bumped",
|
|
20432
21204
|
to
|
|
20433
21205
|
});
|
|
20434
|
-
} else if (cls.kind === "
|
|
21206
|
+
} else if (cls.kind === "catalog" && pkg) ledger.refs.push({
|
|
21207
|
+
file,
|
|
21208
|
+
name: member.key,
|
|
21209
|
+
field,
|
|
21210
|
+
spec,
|
|
21211
|
+
catalog: cls.catalog,
|
|
21212
|
+
pkg
|
|
21213
|
+
});
|
|
21214
|
+
else if (cls.kind === "local") if (options.dropLocalOverrides) {
|
|
20435
21215
|
removals.push([blockPath, member.key]);
|
|
20436
21216
|
deletePath(expected, [...blockPath, member.key]);
|
|
20437
21217
|
plan.overrides.push({
|
|
@@ -20451,7 +21231,7 @@ function planPackageJson(file, original, target, options, plan) {
|
|
|
20451
21231
|
name: member.key,
|
|
20452
21232
|
field,
|
|
20453
21233
|
spec,
|
|
20454
|
-
reason: cls
|
|
21234
|
+
reason: leftAloneReason(cls)
|
|
20455
21235
|
});
|
|
20456
21236
|
}
|
|
20457
21237
|
}
|
|
@@ -20694,7 +21474,8 @@ function readYamlScalar(rest) {
|
|
|
20694
21474
|
};
|
|
20695
21475
|
}
|
|
20696
21476
|
/**
|
|
20697
|
-
*
|
|
21477
|
+
* A top-level block mapping of a `pnpm-workspace.yaml` — `overrides:`,
|
|
21478
|
+
* `catalog:` — read line by line.
|
|
20698
21479
|
*
|
|
20699
21480
|
* Deliberately narrow. The file is YAML, and the CLI carries no YAML parser; the
|
|
20700
21481
|
* shape pnpm documents and every project writes is a block mapping of one
|
|
@@ -20702,8 +21483,8 @@ function readYamlScalar(rest) {
|
|
|
20702
21483
|
* block is counted rather than guessed at, and an inline `overrides: { … }` is
|
|
20703
21484
|
* reported as not handled.
|
|
20704
21485
|
*/
|
|
20705
|
-
function
|
|
20706
|
-
const keyLine = lines.findIndex((line) =>
|
|
21486
|
+
function readYamlBlock(lines, key) {
|
|
21487
|
+
const keyLine = lines.findIndex((line) => new RegExp(`^${key}:\\s*(#.*)?$`).test(line));
|
|
20707
21488
|
if (keyLine === -1) return null;
|
|
20708
21489
|
const block = {
|
|
20709
21490
|
keyLine,
|
|
@@ -20716,29 +21497,142 @@ function readYamlOverrides(lines) {
|
|
|
20716
21497
|
if (line.trim() !== "" && !/^\s/.test(line) && !line.startsWith("#")) break;
|
|
20717
21498
|
block.lines.push(index);
|
|
20718
21499
|
if (line.trim() === "" || /^\s*#/.test(line)) continue;
|
|
20719
|
-
const entry =
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
|
|
20723
|
-
|
|
21500
|
+
const entry = readYamlEntry(line, index);
|
|
21501
|
+
if (entry) block.entries.push(entry);
|
|
21502
|
+
else block.otherContent++;
|
|
21503
|
+
}
|
|
21504
|
+
return block;
|
|
21505
|
+
}
|
|
21506
|
+
/** One `key: scalar` line of a block mapping, or null when it is anything else. */
|
|
21507
|
+
function readYamlEntry(line, index) {
|
|
21508
|
+
const entry = /^(\s+)("[^"]*"|'[^']*'|[^\s"'#][^:#]*?)\s*:(\s+|$)/.exec(line);
|
|
21509
|
+
const valueOffset = entry ? entry[0].length : -1;
|
|
21510
|
+
const scalar = entry ? readYamlScalar(line.slice(valueOffset)) : null;
|
|
21511
|
+
if (!entry || !scalar) return null;
|
|
21512
|
+
const rawKey = entry[2];
|
|
21513
|
+
return {
|
|
21514
|
+
line: index,
|
|
21515
|
+
key: /^["']/.test(rawKey) ? rawKey.slice(1, -1) : rawKey,
|
|
21516
|
+
value: scalar.value,
|
|
21517
|
+
valueStart: valueOffset + scalar.start,
|
|
21518
|
+
valueEnd: valueOffset + scalar.end
|
|
21519
|
+
};
|
|
21520
|
+
}
|
|
21521
|
+
/**
|
|
21522
|
+
* Every catalog a `pnpm-workspace.yaml` declares: the top-level `catalog:`
|
|
21523
|
+
* block, and each named block under `catalogs:`.
|
|
21524
|
+
*
|
|
21525
|
+
* `catalogs:` is one level deeper — a header line per catalog, its entries
|
|
21526
|
+
* indented under it — and is read with the same one-scalar-per-line rule.
|
|
21527
|
+
*/
|
|
21528
|
+
function readYamlCatalogs(lines) {
|
|
21529
|
+
const catalogs = [];
|
|
21530
|
+
const single = readYamlBlock(lines, "catalog");
|
|
21531
|
+
if (single) catalogs.push({
|
|
21532
|
+
name: "default",
|
|
21533
|
+
field: "catalog",
|
|
21534
|
+
entries: single.entries
|
|
21535
|
+
});
|
|
21536
|
+
const keyLine = lines.findIndex((line) => /^catalogs:\s*(#.*)?$/.test(line));
|
|
21537
|
+
if (keyLine === -1) return catalogs;
|
|
21538
|
+
let current = null;
|
|
21539
|
+
let headerIndent = -1;
|
|
21540
|
+
for (let index = keyLine + 1; index < lines.length; index++) {
|
|
21541
|
+
const line = lines[index];
|
|
21542
|
+
if (line.trim() !== "" && !/^\s/.test(line) && !line.startsWith("#")) break;
|
|
21543
|
+
if (line.trim() === "" || /^\s*#/.test(line)) continue;
|
|
21544
|
+
const indent = line.length - line.trimStart().length;
|
|
21545
|
+
const header = /^\s+("[^"]*"|'[^']*'|[^\s"'#][^:#]*?)\s*:\s*(#.*)?$/.exec(line);
|
|
21546
|
+
if (header && (headerIndent === -1 || indent === headerIndent)) {
|
|
21547
|
+
headerIndent = indent;
|
|
21548
|
+
const name = /^["']/.test(header[1]) ? header[1].slice(1, -1) : header[1];
|
|
21549
|
+
current = {
|
|
21550
|
+
name,
|
|
21551
|
+
field: `catalogs.${name}`,
|
|
21552
|
+
entries: []
|
|
21553
|
+
};
|
|
21554
|
+
catalogs.push(current);
|
|
20724
21555
|
continue;
|
|
20725
21556
|
}
|
|
20726
|
-
|
|
20727
|
-
const
|
|
20728
|
-
|
|
20729
|
-
|
|
20730
|
-
|
|
20731
|
-
|
|
20732
|
-
|
|
20733
|
-
|
|
21557
|
+
if (!current || indent <= headerIndent) continue;
|
|
21558
|
+
const entry = readYamlEntry(line, index);
|
|
21559
|
+
if (entry) current.entries.push(entry);
|
|
21560
|
+
}
|
|
21561
|
+
return catalogs;
|
|
21562
|
+
}
|
|
21563
|
+
/** A `pnpm-workspace.yaml`, with its overrides and then its catalogs moved. */
|
|
21564
|
+
function planWorkspaceYaml(file, original, target, options, plan, ledger) {
|
|
21565
|
+
return planYamlCatalogs(file, planYamlOverrides(file, original, target, options, plan, ledger), target, plan, ledger);
|
|
21566
|
+
}
|
|
21567
|
+
/**
|
|
21568
|
+
* Move the framework entries of every catalog. Entries are rewritten in place
|
|
21569
|
+
* — a catalog loses no line — and the result is read back and held to what was
|
|
21570
|
+
* meant, like every other rewrite here.
|
|
21571
|
+
*/
|
|
21572
|
+
function planYamlCatalogs(file, original, target, plan, ledger) {
|
|
21573
|
+
const lines = splitLines(original);
|
|
21574
|
+
const catalogs = readYamlCatalogs(lines);
|
|
21575
|
+
for (const key of ["catalog", "catalogs"]) {
|
|
21576
|
+
const inline = lines.find((line) => new RegExp(`^${key}:\\s*[^\\s#]`).test(line));
|
|
21577
|
+
if (inline && inline.includes("@rebasepro/")) plan.skipped.push({
|
|
21578
|
+
file,
|
|
21579
|
+
name: key,
|
|
21580
|
+
field: key,
|
|
21581
|
+
spec: inline.slice(key.length + 1).trim(),
|
|
21582
|
+
reason: `an inline ${key} map, which this does not rewrite; write it as a block, or edit it by hand`
|
|
20734
21583
|
});
|
|
20735
21584
|
}
|
|
20736
|
-
return
|
|
21585
|
+
if (catalogs.length === 0) return original;
|
|
21586
|
+
const next = [...lines];
|
|
21587
|
+
const expected = [];
|
|
21588
|
+
for (const catalog of catalogs) for (const entry of catalog.entries) {
|
|
21589
|
+
const meant = [
|
|
21590
|
+
catalog.field,
|
|
21591
|
+
entry.key,
|
|
21592
|
+
entry.value
|
|
21593
|
+
];
|
|
21594
|
+
expected.push(meant);
|
|
21595
|
+
if (!entry.key.startsWith("@rebasepro/")) continue;
|
|
21596
|
+
ledger.defined.add(catalogKey(catalog.name, entry.key));
|
|
21597
|
+
const cls = classifySpec(entry.value);
|
|
21598
|
+
if (cls.kind !== "movable") {
|
|
21599
|
+
plan.skipped.push({
|
|
21600
|
+
file,
|
|
21601
|
+
name: entry.key,
|
|
21602
|
+
field: catalog.field,
|
|
21603
|
+
spec: entry.value,
|
|
21604
|
+
reason: leftAloneReason(cls)
|
|
21605
|
+
});
|
|
21606
|
+
continue;
|
|
21607
|
+
}
|
|
21608
|
+
const to = `${cls.prefix}${target}`;
|
|
21609
|
+
if (to === entry.value) {
|
|
21610
|
+
plan.unchanged++;
|
|
21611
|
+
continue;
|
|
21612
|
+
}
|
|
21613
|
+
meant[2] = to;
|
|
21614
|
+
const line = next[entry.line];
|
|
21615
|
+
next[entry.line] = line.slice(0, entry.valueStart) + to + line.slice(entry.valueEnd);
|
|
21616
|
+
plan.changed.push({
|
|
21617
|
+
file,
|
|
21618
|
+
name: entry.key,
|
|
21619
|
+
field: catalog.field,
|
|
21620
|
+
from: entry.value,
|
|
21621
|
+
to
|
|
21622
|
+
});
|
|
21623
|
+
}
|
|
21624
|
+
const content = next.join(original.includes("\r\n") ? "\r\n" : "\n");
|
|
21625
|
+
if (!isDeepStrictEqual(readYamlCatalogs(splitLines(content)).flatMap((catalog) => catalog.entries.map((e) => [
|
|
21626
|
+
catalog.field,
|
|
21627
|
+
e.key,
|
|
21628
|
+
e.value
|
|
21629
|
+
])), expected)) throw new UpgradeError(`Could not rewrite ${file} without disturbing it; nothing was written.`, "rewrite_failed", "Move its @rebasepro catalog entries by hand, then run `rebase upgrade` again.");
|
|
21630
|
+
return content;
|
|
20737
21631
|
}
|
|
20738
|
-
function
|
|
21632
|
+
function planYamlOverrides(file, original, target, options, plan, ledger) {
|
|
20739
21633
|
const lines = splitLines(original);
|
|
20740
21634
|
const crlf = original.includes("\r\n");
|
|
20741
|
-
const block =
|
|
21635
|
+
const block = readYamlBlock(lines, "overrides");
|
|
20742
21636
|
if (!block) {
|
|
20743
21637
|
const inline = lines.find((line) => /^overrides:\s*[^\s#]/.test(line));
|
|
20744
21638
|
if (inline && inline.includes("@rebasepro/")) plan.skipped.push({
|
|
@@ -20759,10 +21653,14 @@ function planWorkspaceYaml(file, original, target, options, plan) {
|
|
|
20759
21653
|
continue;
|
|
20760
21654
|
}
|
|
20761
21655
|
const cls = classifySpec(entry.value);
|
|
21656
|
+
const pkg = overrideTarget(entry.key);
|
|
20762
21657
|
if (cls.kind === "movable") {
|
|
20763
21658
|
const to = `${cls.prefix}${target}`;
|
|
20764
21659
|
expected.set(entry.key, to);
|
|
20765
|
-
if (to === entry.value)
|
|
21660
|
+
if (to === entry.value) {
|
|
21661
|
+
plan.unchanged++;
|
|
21662
|
+
continue;
|
|
21663
|
+
}
|
|
20766
21664
|
const line = lines[entry.line];
|
|
20767
21665
|
next[entry.line] = line.slice(0, entry.valueStart) + to + line.slice(entry.valueEnd);
|
|
20768
21666
|
plan.overrides.push({
|
|
@@ -20772,6 +21670,16 @@ function planWorkspaceYaml(file, original, target, options, plan) {
|
|
|
20772
21670
|
action: "bumped",
|
|
20773
21671
|
to
|
|
20774
21672
|
});
|
|
21673
|
+
} else if (cls.kind === "catalog" && pkg) {
|
|
21674
|
+
expected.set(entry.key, entry.value);
|
|
21675
|
+
ledger.refs.push({
|
|
21676
|
+
file,
|
|
21677
|
+
name: entry.key,
|
|
21678
|
+
field: "overrides",
|
|
21679
|
+
spec: entry.value,
|
|
21680
|
+
catalog: cls.catalog,
|
|
21681
|
+
pkg
|
|
21682
|
+
});
|
|
20775
21683
|
} else if (cls.kind === "local") if (options.dropLocalOverrides) {
|
|
20776
21684
|
removed.add(entry.line);
|
|
20777
21685
|
plan.overrides.push({
|
|
@@ -20796,14 +21704,14 @@ function planWorkspaceYaml(file, original, target, options, plan) {
|
|
|
20796
21704
|
name: entry.key,
|
|
20797
21705
|
field: "overrides",
|
|
20798
21706
|
spec: entry.value,
|
|
20799
|
-
reason: cls
|
|
21707
|
+
reason: leftAloneReason(cls)
|
|
20800
21708
|
});
|
|
20801
21709
|
}
|
|
20802
21710
|
}
|
|
20803
21711
|
const survivors = block.entries.filter((entry) => !removed.has(entry.line)).length + block.otherContent;
|
|
20804
21712
|
if (removed.size > 0 && survivors === 0) removed.add(block.keyLine);
|
|
20805
21713
|
const content = next.filter((_, index) => !removed.has(index)).join(crlf ? "\r\n" : "\n");
|
|
20806
|
-
const after =
|
|
21714
|
+
const after = readYamlBlock(splitLines(content), "overrides");
|
|
20807
21715
|
if (!isDeepStrictEqual(after ? after.entries.map((e) => [e.key, e.value]) : [], [...expected.entries()])) throw new UpgradeError(`Could not rewrite ${file} without disturbing it; nothing was written.`, "rewrite_failed", "Move its @rebasepro overrides by hand, then run `rebase upgrade` again.");
|
|
20808
21716
|
return content;
|
|
20809
21717
|
}
|
|
@@ -20947,7 +21855,9 @@ ${chalk.bold("What it changes")}
|
|
|
20947
21855
|
in every package.json under the project (node_modules, dist* and hidden
|
|
20948
21856
|
directories excepted). ^ and ~ are kept. peerDependencies are left alone, and so
|
|
20949
21857
|
are workspace:, link:, file:, git and tag specs, which are listed with the reason.
|
|
20950
|
-
Overrides in pnpm-workspace.yaml and package.json are bumped the same way
|
|
21858
|
+
Overrides in pnpm-workspace.yaml and package.json are bumped the same way, and so
|
|
21859
|
+
are the entries of pnpm-workspace.yaml's catalog: and catalogs: blocks, which is
|
|
21860
|
+
where a catalog: pin's version lives.
|
|
20951
21861
|
|
|
20952
21862
|
${chalk.bold("Examples")}
|
|
20953
21863
|
rebase upgrade Move to the latest release and install
|
|
@@ -21111,6 +22021,15 @@ function printSummary(plan, dryRun) {
|
|
|
21111
22021
|
}
|
|
21112
22022
|
function printOutcome(plan, outcome) {
|
|
21113
22023
|
const moved = plan.changed.length + plan.overrides.filter((o) => o.action !== "kept-local").length;
|
|
22024
|
+
const leftAlone = plan.skipped.length + plan.overrides.filter((o) => o.action === "kept-local").length;
|
|
22025
|
+
const entries = (n) => `${n} entr${n === 1 ? "y" : "ies"}`;
|
|
22026
|
+
if (moved === 0 && leftAlone > 0) {
|
|
22027
|
+
if (plan.unchanged > 0) console.log(chalk.green(`✓ Every pin this can move is already on ${plan.target}; ${entries(leftAlone)} ${leftAlone === 1 ? "was" : "were"} left alone, above.`));
|
|
22028
|
+
else if (outcome.dryRun) console.log(chalk.yellow(` Nothing to move to ${plan.target}: ${entries(leftAlone)} ${leftAlone === 1 ? "is" : "are"} left alone, above.`));
|
|
22029
|
+
else console.log(chalk.yellow(`⚠ Nothing moved to ${plan.target}: ${entries(leftAlone)} ${leftAlone === 1 ? "was" : "were"} left alone, above.`));
|
|
22030
|
+
console.log("");
|
|
22031
|
+
return;
|
|
22032
|
+
}
|
|
21114
22033
|
if (outcome.dryRun) {
|
|
21115
22034
|
console.log(chalk.gray(moved === 0 ? ` Nothing to change: this project is already on ${plan.target}.` : " Dry run — nothing was written. Run it without --dry-run to apply."));
|
|
21116
22035
|
console.log("");
|
|
@@ -21166,6 +22085,85 @@ var ROOT_FLAGS = {
|
|
|
21166
22085
|
"-v": "--version",
|
|
21167
22086
|
"-h": "--help"
|
|
21168
22087
|
};
|
|
22088
|
+
/**
|
|
22089
|
+
* Every command the dispatch below answers and the help lists. Declared as a
|
|
22090
|
+
* literal under this name: `help-coverage.test.ts`, `printed-commands.test.ts`
|
|
22091
|
+
* and the docs verifier read it out of this file's source.
|
|
22092
|
+
*/
|
|
22093
|
+
var namespacedCommands = [
|
|
22094
|
+
"init",
|
|
22095
|
+
"schema",
|
|
22096
|
+
"db",
|
|
22097
|
+
"dev",
|
|
22098
|
+
"build",
|
|
22099
|
+
"start",
|
|
22100
|
+
"auth",
|
|
22101
|
+
"doctor",
|
|
22102
|
+
"skills",
|
|
22103
|
+
"api-keys",
|
|
22104
|
+
"cloud",
|
|
22105
|
+
"apps",
|
|
22106
|
+
"eject",
|
|
22107
|
+
"generate-sdk",
|
|
22108
|
+
"telemetry",
|
|
22109
|
+
"resources",
|
|
22110
|
+
"status",
|
|
22111
|
+
"upgrade"
|
|
22112
|
+
];
|
|
22113
|
+
/**
|
|
22114
|
+
* The subcommands each command group dispatches, from the lists those groups
|
|
22115
|
+
* dispatch on. A command absent from here takes no subcommand.
|
|
22116
|
+
*/
|
|
22117
|
+
function knownSubcommands(command) {
|
|
22118
|
+
switch (command) {
|
|
22119
|
+
case "schema": return SCHEMA_SUBCOMMANDS;
|
|
22120
|
+
case "db": return DB_SUBCOMMANDS;
|
|
22121
|
+
case "auth": return AUTH_SUBCOMMANDS;
|
|
22122
|
+
case "skills": return SKILLS_SUBCOMMANDS;
|
|
22123
|
+
case "api-keys": return API_KEYS_SUBCOMMANDS;
|
|
22124
|
+
case "apps": return APPS_SUBCOMMANDS;
|
|
22125
|
+
case "telemetry": return TELEMETRY_SUBCOMMANDS;
|
|
22126
|
+
case "cloud": return CLOUD_GROUP_NAMES;
|
|
22127
|
+
default: return [];
|
|
22128
|
+
}
|
|
22129
|
+
}
|
|
22130
|
+
/**
|
|
22131
|
+
* The command and subcommand words as `cli.error` may send them: each one is a
|
|
22132
|
+
* word this CLI dispatches, `"none"` when nothing was typed, or `"other"`.
|
|
22133
|
+
*
|
|
22134
|
+
* The words come from what was typed, and what is typed after a command is
|
|
22135
|
+
* only sometimes a subcommand. `rebase init acme-payroll-internal`, `rebase
|
|
22136
|
+
* cloud --project acme-prod deploy` and `rebase db psuh` put a project name, a
|
|
22137
|
+
* slug and a typo in the subcommand's position — the consent screen promises
|
|
22138
|
+
* "never project names", and `sanitize` cannot tell one of those from a
|
|
22139
|
+
* subcommand because neither has a separator in it. So the words are held
|
|
22140
|
+
* against the vocabulary here, where it is known, and nothing else survives.
|
|
22141
|
+
*
|
|
22142
|
+
* Exported for `cli.test.ts`.
|
|
22143
|
+
*/
|
|
22144
|
+
function telemetryCommandWords(command, subcommand) {
|
|
22145
|
+
if (!command) return {
|
|
22146
|
+
command: "none",
|
|
22147
|
+
subcommand: "none"
|
|
22148
|
+
};
|
|
22149
|
+
if (!(namespacedCommands.includes(command) || command === "normalize-imports" || command === "__dev-db-daemon")) return {
|
|
22150
|
+
command: "other",
|
|
22151
|
+
subcommand: subcommand ? "other" : "none"
|
|
22152
|
+
};
|
|
22153
|
+
if (!subcommand) return {
|
|
22154
|
+
command,
|
|
22155
|
+
subcommand: "none"
|
|
22156
|
+
};
|
|
22157
|
+
if (subcommand === "--help") return {
|
|
22158
|
+
command,
|
|
22159
|
+
subcommand
|
|
22160
|
+
};
|
|
22161
|
+
const canonical = command === "cloud" ? GROUP_ALIASES[subcommand] ?? subcommand : subcommand;
|
|
22162
|
+
return {
|
|
22163
|
+
command,
|
|
22164
|
+
subcommand: knownSubcommands(command).includes(canonical) ? canonical : "other"
|
|
22165
|
+
};
|
|
22166
|
+
}
|
|
21169
22167
|
async function entry(args) {
|
|
21170
22168
|
silenceDotenvBanner();
|
|
21171
22169
|
const parsedArgs = arg({ ...ROOT_FLAGS }, {
|
|
@@ -21179,26 +22177,6 @@ async function entry(args) {
|
|
|
21179
22177
|
const words = parsedArgs._.filter((a) => !a.startsWith("-"));
|
|
21180
22178
|
const command = words[0];
|
|
21181
22179
|
const subcommand = words[1];
|
|
21182
|
-
const namespacedCommands = [
|
|
21183
|
-
"init",
|
|
21184
|
-
"schema",
|
|
21185
|
-
"db",
|
|
21186
|
-
"dev",
|
|
21187
|
-
"build",
|
|
21188
|
-
"start",
|
|
21189
|
-
"auth",
|
|
21190
|
-
"doctor",
|
|
21191
|
-
"skills",
|
|
21192
|
-
"api-keys",
|
|
21193
|
-
"cloud",
|
|
21194
|
-
"apps",
|
|
21195
|
-
"eject",
|
|
21196
|
-
"generate-sdk",
|
|
21197
|
-
"telemetry",
|
|
21198
|
-
"resources",
|
|
21199
|
-
"status",
|
|
21200
|
-
"upgrade"
|
|
21201
|
-
];
|
|
21202
22180
|
if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
|
|
21203
22181
|
printHelp();
|
|
21204
22182
|
return;
|
|
@@ -21208,8 +22186,7 @@ async function entry(args) {
|
|
|
21208
22186
|
await dispatch(command, effectiveSubcommand, args, parsedArgs, namespacedCommands);
|
|
21209
22187
|
} catch (error) {
|
|
21210
22188
|
await recordEvent("cli.error", {
|
|
21211
|
-
command
|
|
21212
|
-
subcommand: effectiveSubcommand ?? "none",
|
|
22189
|
+
...telemetryCommandWords(command, effectiveSubcommand),
|
|
21213
22190
|
error_type: errorClass(error),
|
|
21214
22191
|
usage: Boolean(error && typeof error === "object" && error.isUsageError)
|
|
21215
22192
|
}, { projectRoot: process.cwd() });
|
|
@@ -21403,6 +22380,6 @@ function telemetryNotice() {
|
|
|
21403
22380
|
return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
|
|
21404
22381
|
}
|
|
21405
22382
|
//#endregion
|
|
21406
|
-
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 };
|
|
22383
|
+
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, describeDepConflicts, detectDeclaredDepConflicts, detectFrameworkDepDrift, detectNativeDependencies, detectStorageAuthorize, entry, findBackendApp, findUnusedServerEntry, foldStaticIntoBundle, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, resolveBackendPaths, selectDeployApp, synthesizeManifest, validateManifest, vendorDependencies, writeManifest };
|
|
21407
22384
|
|
|
21408
22385
|
//# sourceMappingURL=index.es.js.map
|