@rebasepro/cli 0.21.1-canary.g8c5a265 → 0.21.1

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/index.es.js CHANGED
@@ -13,7 +13,7 @@ import path from "path";
13
13
  import * as fs$1 from "fs";
14
14
  import fs from "fs";
15
15
  import net from "net";
16
- import { promisify } from "util";
16
+ import { isDeepStrictEqual, promisify } from "util";
17
17
  import { execa, execaCommandSync } from "execa";
18
18
  import { cp } from "fs/promises";
19
19
  import { fileURLToPath, pathToFileURL } from "url";
@@ -2362,11 +2362,11 @@ async function probeRelease(version) {
2362
2362
  };
2363
2363
  return {
2364
2364
  kind: "unreachable",
2365
- reason: firstLine(text) || "the registry could not be reached"
2365
+ reason: firstLine$1(text) || "the registry could not be reached"
2366
2366
  };
2367
2367
  }
2368
2368
  }
2369
- function firstLine(text) {
2369
+ function firstLine$1(text) {
2370
2370
  return text.split("\n").map((l) => l.trim()).filter(Boolean)[0] ?? "";
2371
2371
  }
2372
2372
  async function replacePlaceholders(options) {
@@ -4545,7 +4545,7 @@ function checkCmsPath(value, appPath, fieldPath, issues) {
4545
4545
  }
4546
4546
  return cms;
4547
4547
  }
4548
- function isRecord(value) {
4548
+ function isRecord$1(value) {
4549
4549
  return typeof value === "object" && value !== null && !Array.isArray(value);
4550
4550
  }
4551
4551
  /**
@@ -4682,7 +4682,7 @@ function isNearMiss(a, b) {
4682
4682
  }
4683
4683
  function validateApp(name, raw, issues) {
4684
4684
  const base = `apps.${name}`;
4685
- if (!isRecord(raw)) {
4685
+ if (!isRecord$1(raw)) {
4686
4686
  issues.push({
4687
4687
  path: base,
4688
4688
  message: "must be an object"
@@ -4782,7 +4782,7 @@ function validateApp(name, raw, issues) {
4782
4782
  */
4783
4783
  function validateManifest(raw) {
4784
4784
  const issues = [];
4785
- if (!isRecord(raw)) return { issues: [{
4785
+ if (!isRecord$1(raw)) return { issues: [{
4786
4786
  path: "",
4787
4787
  message: `${MANIFEST_FILENAME} must contain a JSON object`
4788
4788
  }] };
@@ -4793,7 +4793,7 @@ function validateManifest(raw) {
4793
4793
  message
4794
4794
  });
4795
4795
  }
4796
- if (!isRecord(raw.apps)) {
4796
+ if (!isRecord$1(raw.apps)) {
4797
4797
  issues.push({
4798
4798
  path: "apps",
4799
4799
  message: "is required and must be an object"
@@ -4981,7 +4981,7 @@ var MODELLED_MANIFEST_KEYS = [
4981
4981
  function readManifestObject(filePath) {
4982
4982
  try {
4983
4983
  const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
4984
- return isRecord(parsed) ? parsed : {};
4984
+ return isRecord$1(parsed) ? parsed : {};
4985
4985
  } catch {
4986
4986
  return {};
4987
4987
  }
@@ -8824,7 +8824,7 @@ function projectResourceGraph(graph) {
8824
8824
  * entrypoint, falls back to the previous behaviour: run every workspace's own
8825
8825
  * `build` script. Nothing that built before stops building.
8826
8826
  */
8827
- function printHelp$5() {
8827
+ function printHelp$6() {
8828
8828
  console.log(`
8829
8829
  ${chalk.bold("rebase build")} — build the apps declared in rebase.json
8830
8830
 
@@ -8886,7 +8886,7 @@ function dockerBuildHint(projectRoot, name, app) {
8886
8886
  }
8887
8887
  async function buildCommand(rawArgs = []) {
8888
8888
  if (wantsHelp(rawArgs)) {
8889
- printHelp$5();
8889
+ printHelp$6();
8890
8890
  return;
8891
8891
  }
8892
8892
  const { flags: args, positionals: requested } = parseCommandArgs({
@@ -9012,10 +9012,12 @@ async function buildCommand(rawArgs = []) {
9012
9012
  console.log(chalk.yellow(` ⚠ framework dependencies older than this CLI (${cliVersion$1()}):`));
9013
9013
  for (const dep of drift.behind) console.log(chalk.dim(` ${dep.name}@${dep.range} (${dep.file})`));
9014
9014
  console.log(chalk.dim(" The image supplies the server, but your bundle supplies the database"));
9015
- console.log(chalk.dim(" driver — a newer runtime does not update it. Bump these and rebuild."));
9015
+ console.log(chalk.dim(" driver — a newer runtime does not update it."));
9016
+ console.log(chalk.dim(` Run \`${upgradeToThisCli()}\`, then rebuild.`));
9016
9017
  } else if (drift.disagreeing.length > 0) {
9017
9018
  console.log(chalk.yellow(` ⚠ mixed @rebasepro versions declared: ${drift.disagreeing.join(", ")}`));
9018
- console.log(chalk.dim(" These are published together and expect to run together; pin them alike."));
9019
+ console.log(chalk.dim(" These are published together and expect to run together."));
9020
+ console.log(chalk.dim(` Run \`${upgradeToThisCli()}\` to pin them alike, then rebuild.`));
9019
9021
  }
9020
9022
  if (!args["--no-static"]) {
9021
9023
  const folded = await foldFrontendIntoBundle({
@@ -9091,6 +9093,17 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
9091
9093
  console.log(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s) → served at ${basePath})`));
9092
9094
  return result.outDir;
9093
9095
  }
9096
+ /**
9097
+ * The `rebase upgrade` line the drift warnings print.
9098
+ *
9099
+ * Aimed at this CLI's own version, because that is what the warning measured
9100
+ * against: "older than this CLI" is answered by moving to exactly this CLI, not
9101
+ * to whatever `latest` happens to be — which, on a canary, is older still.
9102
+ */
9103
+ function upgradeToThisCli() {
9104
+ const version = cliVersion$1();
9105
+ return version === "unknown" ? "rebase upgrade" : `rebase upgrade --to ${version}`;
9106
+ }
9094
9107
  /** The pre-manifest behaviour: build every workspace package. */
9095
9108
  async function runWorkspaceBuilds(projectRoot) {
9096
9109
  const pm = detectPackageManager(projectRoot);
@@ -9240,7 +9253,7 @@ function projectNameOf(projectRoot) {
9240
9253
  } catch {}
9241
9254
  return path.basename(projectRoot);
9242
9255
  }
9243
- function printHelp$4() {
9256
+ function printHelp$5() {
9244
9257
  console.log(`
9245
9258
  ${chalk.bold("rebase eject")} — take ownership of the server process
9246
9259
 
@@ -9261,7 +9274,7 @@ ${chalk.bold("Options")}
9261
9274
  }
9262
9275
  async function ejectCommand(rawArgs = []) {
9263
9276
  if (wantsHelp(rawArgs)) {
9264
- printHelp$4();
9277
+ printHelp$5();
9265
9278
  return;
9266
9279
  }
9267
9280
  const { flags: args, positionals } = parseCommandArgs({
@@ -9443,7 +9456,7 @@ function restoreBackendScripts(projectRoot) {
9443
9456
  * `rebase.json`) this falls back to the backend workspace's own `start` script,
9444
9457
  * which is what such a project has always used.
9445
9458
  */
9446
- function printHelp$3() {
9459
+ function printHelp$4() {
9447
9460
  console.log(`
9448
9461
  ${chalk.bold("rebase start")} — run a built bundle
9449
9462
 
@@ -9460,7 +9473,7 @@ Build first with ${chalk.cyan("rebase build")}.
9460
9473
  }
9461
9474
  async function startCommand(rawArgs = []) {
9462
9475
  if (wantsHelp(rawArgs)) {
9463
- printHelp$3();
9476
+ printHelp$4();
9464
9477
  return;
9465
9478
  }
9466
9479
  const { flags: args } = parseCommandArgs({
@@ -10056,7 +10069,7 @@ function checkNodeVersion(running, required) {
10056
10069
  }];
10057
10070
  }
10058
10071
  /** The lockfiles a project may have, and the manager each one belongs to. */
10059
- var LOCKFILES = {
10072
+ var LOCKFILES$1 = {
10060
10073
  "pnpm-lock.yaml": "pnpm",
10061
10074
  "package-lock.json": "npm",
10062
10075
  "yarn.lock": "yarn",
@@ -10073,18 +10086,18 @@ var LOCKFILES = {
10073
10086
  * at it.
10074
10087
  */
10075
10088
  function checkPackageManager(lockfilesPresent, declared) {
10076
- const known = lockfilesPresent.filter((file) => file in LOCKFILES);
10089
+ const known = lockfilesPresent.filter((file) => file in LOCKFILES$1);
10077
10090
  if (known.length < 2) {
10078
10091
  const declaredName = declared?.split("@")[0];
10079
- if (known.length === 1 && declaredName && LOCKFILES[known[0]] !== declaredName) return [{
10092
+ if (known.length === 1 && declaredName && LOCKFILES$1[known[0]] !== declaredName) return [{
10080
10093
  check: "package-manager",
10081
10094
  severity: "warning",
10082
- message: `This project declares packageManager "${declared}" and has a ${LOCKFILES[known[0]]} lockfile (${known[0]}).`,
10095
+ message: `This project declares packageManager "${declared}" and has a ${LOCKFILES$1[known[0]]} lockfile (${known[0]}).`,
10083
10096
  fix: `Delete ${known[0]} and node_modules, then install with ${declaredName}.`
10084
10097
  }];
10085
10098
  return [];
10086
10099
  }
10087
- const managers = known.map((file) => LOCKFILES[file]);
10100
+ const managers = known.map((file) => LOCKFILES$1[file]);
10088
10101
  return [{
10089
10102
  check: "package-manager",
10090
10103
  severity: "error",
@@ -10192,7 +10205,7 @@ function checkVersionSkew(declared) {
10192
10205
  check: "versions",
10193
10206
  severity: "error",
10194
10207
  message: `${name} is pinned to different versions in this project: ${described}.`,
10195
- fix: "Pin one version everywhere and reinstall. Two copies of a Rebase package break `instanceof` between them, which fails as a type guard rejecting its own type."
10208
+ fix: "Run `rebase upgrade` to pin every @rebasepro package to one release and reinstall. Two copies of a Rebase package break `instanceof` between them, which fails as a type guard rejecting its own type."
10196
10209
  });
10197
10210
  }
10198
10211
  return findings;
@@ -10318,7 +10331,7 @@ function collectEnvironmentFindings(projectRoot, envFile) {
10318
10331
  const findings = [];
10319
10332
  findings.push(...checkNodeVersion(process.versions.node, readCliEngines()));
10320
10333
  const rootPackage = readJson(path.join(projectRoot, "package.json"));
10321
- findings.push(...checkPackageManager(Object.keys(LOCKFILES).filter((file) => fs.existsSync(path.join(projectRoot, file))), typeof rootPackage?.packageManager === "string" ? rootPackage.packageManager : void 0));
10334
+ findings.push(...checkPackageManager(Object.keys(LOCKFILES$1).filter((file) => fs.existsSync(path.join(projectRoot, file))), typeof rootPackage?.packageManager === "string" ? rootPackage.packageManager : void 0));
10322
10335
  findings.push(...checkDuplicateSlugs(readDeclaredSlugs(projectRoot)));
10323
10336
  if (envFile && fs.existsSync(envFile)) try {
10324
10337
  findings.push(...checkEnvSanity(parseEnvFile(fs.readFileSync(envFile, "utf-8"))));
@@ -10385,7 +10398,7 @@ function readAtlasBinaryState(projectRoot) {
10385
10398
  }
10386
10399
  /** The reader's own package manager, from the lockfile beside their project. */
10387
10400
  function readPackageManagerName(projectRoot) {
10388
- for (const [file, manager] of Object.entries(LOCKFILES)) if (fs.existsSync(path.join(projectRoot, file))) return manager;
10401
+ for (const [file, manager] of Object.entries(LOCKFILES$1)) if (fs.existsSync(path.join(projectRoot, file))) return manager;
10389
10402
  return "pnpm";
10390
10403
  }
10391
10404
  /** The `engines.node` range of the CLI actually running. */
@@ -11942,7 +11955,7 @@ var TELEMETRY_SUBCOMMANDS = [
11942
11955
  ];
11943
11956
  async function telemetryCommand(rawArgs) {
11944
11957
  if (wantsHelp(rawArgs)) {
11945
- printHelp$2();
11958
+ printHelp$3();
11946
11959
  return;
11947
11960
  }
11948
11961
  const { positionals } = parseCommandArgs({
@@ -12020,7 +12033,7 @@ function printPayload() {
12020
12033
  console.log(chalk.gray(" hostname, your project name or anything you have typed."));
12021
12034
  console.log("");
12022
12035
  }
12023
- function printHelp$2() {
12036
+ function printHelp$3() {
12024
12037
  console.log(`
12025
12038
  ${chalk.bold("rebase telemetry")} — anonymous usage sharing (asked once per project)
12026
12039
 
@@ -15120,7 +15133,13 @@ function bundleDeployBody(input) {
15120
15133
  ...input.commit ? {
15121
15134
  gitCommitHash: input.commit.hash,
15122
15135
  gitCommitMessage: input.commit.message
15123
- } : {}
15136
+ } : {},
15137
+ ...input.rebuildSource ? { rebuildSource: {
15138
+ sourceId: input.rebuildSource.sourceId,
15139
+ projectPath: input.rebuildSource.projectPath,
15140
+ buildEnv: input.rebuildSource.buildEnv
15141
+ } } : {},
15142
+ ...input.allowFrameworkDowngrade ? { allowFrameworkDowngrade: true } : {}
15124
15143
  };
15125
15144
  }
15126
15145
  /**
@@ -15161,10 +15180,393 @@ async function uploadBundle(url, token, projectId, tarPath) {
15161
15180
  return data.bundleId;
15162
15181
  }
15163
15182
  //#endregion
15183
+ //#region src/commands/cloud/rebuild-source.ts
15184
+ /**
15185
+ * The project's source, uploaded beside its bundle so the platform can rebuild it.
15186
+ *
15187
+ * A managed deploy ships a built bundle, and a bundle is fixed to the framework
15188
+ * release it was built on. When the platform rolls a newer release across the
15189
+ * fleet it has to rebuild each project from source — and the bundle path never
15190
+ * uploaded any: the control plane held a tarball of compiled output and
15191
+ * nothing it could compile again. So every bundle deploy now also packs the
15192
+ * source the bundle was built from, uploads it, and names it in the trigger.
15193
+ *
15194
+ * Three rules shape what goes in:
15195
+ *
15196
+ * - **Git decides, when there is a repository.** `git ls-files --cached
15197
+ * --others --exclude-standard` at the repository's top level is exactly what
15198
+ * the developer considers source: `.gitignore` honoured the way git honours
15199
+ * it, and a sibling package the project links (`link:../../packages/editor`)
15200
+ * included, because it is outside the project directory but inside the
15201
+ * repository.
15202
+ * - **Secrets never leave the machine, whatever git says.** A committed `.env`
15203
+ * is still a `.env`. {@link neverUploaded} is applied after git, not instead
15204
+ * of it.
15205
+ * - **It never costs the deploy.** The bundle is what runs; the source only
15206
+ * lets a later platform upgrade rebuild it. Too large, unpackable, refused by
15207
+ * the control plane — each is a warning, and the deploy goes on without it.
15208
+ */
15209
+ /**
15210
+ * The control plane's cap on an uploaded source archive.
15211
+ *
15212
+ * Keep in sync with its build-context cap (deploy/upload `MAX_BYTES` and the
15213
+ * backend's `maxBodySize`). Checked before uploading, so an oversized archive is
15214
+ * a message in milliseconds rather than a bare 413 after the upload.
15215
+ */
15216
+ var MAX_SOURCE_UPLOAD_BYTES = 100 * 1024 * 1024;
15217
+ /** Env files that are templates by convention, and safe to ship. */
15218
+ var ENV_TEMPLATES = /* @__PURE__ */ new Set([
15219
+ ".env.example",
15220
+ ".env.sample",
15221
+ ".env.template"
15222
+ ]);
15223
+ /**
15224
+ * Whether a file must never be uploaded, whatever the ignore rules said.
15225
+ *
15226
+ * `relativePath` is POSIX. Installed packages, git's own directory and built
15227
+ * bundles are never source. Every `.env` and `.env.*` is excluded except the
15228
+ * three template names, and so is direnv's `.envrc`, which is the same thing
15229
+ * under another name.
15230
+ */
15231
+ function neverUploaded(relativePath) {
15232
+ const segments = relativePath.split("/");
15233
+ if (segments.some((segment) => segment === "node_modules" || segment === ".git" || segment.startsWith("dist-bundle"))) return true;
15234
+ const base = segments[segments.length - 1];
15235
+ if (base === ".envrc") return true;
15236
+ if ((base === ".env" || base.startsWith(".env.")) && !ENV_TEMPLATES.has(base)) return true;
15237
+ return false;
15238
+ }
15239
+ /** Directories the walk outside a repository never enters. */
15240
+ var WALK_SKIP = /* @__PURE__ */ new Set([
15241
+ "node_modules",
15242
+ ".git",
15243
+ ".rebase",
15244
+ ".turbo",
15245
+ ".next",
15246
+ "coverage"
15247
+ ]);
15248
+ var runGit = (cwd, args) => execFileSync("git", [
15249
+ "-C",
15250
+ cwd,
15251
+ ...args
15252
+ ], {
15253
+ encoding: "utf8",
15254
+ stdio: [
15255
+ "ignore",
15256
+ "pipe",
15257
+ "ignore"
15258
+ ],
15259
+ maxBuffer: 256 * 1024 * 1024
15260
+ });
15261
+ function toPosix(relative) {
15262
+ return relative.split(path.sep).join("/");
15263
+ }
15264
+ /** A regular file on disk: not missing, not a directory or submodule, not a symlink. */
15265
+ function isRegularFile(absolute) {
15266
+ try {
15267
+ return fs.lstatSync(absolute).isFile();
15268
+ } catch {
15269
+ return false;
15270
+ }
15271
+ }
15272
+ /**
15273
+ * The files of the repository `dir` belongs to — tracked and unignored, the way
15274
+ * git sees them — or, outside any repository, `dir` walked with fixed excludes.
15275
+ */
15276
+ function listUnit(dir, git) {
15277
+ try {
15278
+ const toplevel = git(dir, ["rev-parse", "--show-toplevel"]).trim();
15279
+ if (toplevel) {
15280
+ const root = fs.realpathSync(toplevel);
15281
+ const listed = git(root, [
15282
+ "ls-files",
15283
+ "-z",
15284
+ "--cached",
15285
+ "--others",
15286
+ "--exclude-standard"
15287
+ ]);
15288
+ return {
15289
+ root,
15290
+ files: [...new Set(listed.split("\0").filter(Boolean))].filter((relative) => !neverUploaded(relative) && isRegularFile(path.join(root, relative))),
15291
+ fromGit: true
15292
+ };
15293
+ }
15294
+ } catch {}
15295
+ const files = [];
15296
+ const walk = (current, prefix) => {
15297
+ let entries;
15298
+ try {
15299
+ entries = fs.readdirSync(current, { withFileTypes: true });
15300
+ } catch {
15301
+ return;
15302
+ }
15303
+ for (const entry of entries) {
15304
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
15305
+ if (entry.isDirectory()) {
15306
+ if (WALK_SKIP.has(entry.name) || entry.name.startsWith("dist")) continue;
15307
+ walk(path.join(current, entry.name), relative);
15308
+ } else if (entry.isFile() && !neverUploaded(relative)) files.push(relative);
15309
+ }
15310
+ };
15311
+ walk(dir, "");
15312
+ return {
15313
+ root: dir,
15314
+ files,
15315
+ fromGit: false
15316
+ };
15317
+ }
15318
+ /** Whether `candidate` is `dir` or inside it. */
15319
+ function isWithin(dir, candidate) {
15320
+ const relative = path.relative(dir, candidate);
15321
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
15322
+ }
15323
+ /** The deepest directory containing every path given. */
15324
+ function commonAncestor(paths) {
15325
+ let ancestor = paths[0];
15326
+ for (const p of paths.slice(1)) while (!isWithin(ancestor, p)) ancestor = path.dirname(ancestor);
15327
+ return ancestor;
15328
+ }
15329
+ var DEPENDENCY_FIELDS = [
15330
+ "dependencies",
15331
+ "devDependencies",
15332
+ "optionalDependencies"
15333
+ ];
15334
+ /**
15335
+ * Directories outside every unit that a unit's `package.json` files link to
15336
+ * with `link:` or `file:`.
15337
+ *
15338
+ * `@rebasepro/*` links are not followed. They point at a local framework
15339
+ * checkout, which is not the project's source and would drag a whole monorepo
15340
+ * into the archive; a rebuild moves those packages to a published release
15341
+ * anyway.
15342
+ */
15343
+ function externalLinkTargets(units) {
15344
+ const targets = /* @__PURE__ */ new Set();
15345
+ for (const unit of units) for (const relative of unit.files) {
15346
+ if (path.posix.basename(relative) !== "package.json") continue;
15347
+ let manifest;
15348
+ try {
15349
+ manifest = JSON.parse(fs.readFileSync(path.join(unit.root, relative), "utf8"));
15350
+ } catch {
15351
+ continue;
15352
+ }
15353
+ if (typeof manifest !== "object" || manifest === null) continue;
15354
+ for (const field of DEPENDENCY_FIELDS) {
15355
+ const deps = manifest[field];
15356
+ if (typeof deps !== "object" || deps === null) continue;
15357
+ for (const [name, spec] of Object.entries(deps)) {
15358
+ if (name.startsWith("@rebasepro/") || typeof spec !== "string") continue;
15359
+ const match = /^(?:link|file):(.+)$/.exec(spec);
15360
+ if (!match) continue;
15361
+ const target = path.resolve(unit.root, path.posix.dirname(relative), match[1]);
15362
+ let real;
15363
+ try {
15364
+ real = fs.realpathSync(target);
15365
+ if (!fs.statSync(real).isDirectory()) continue;
15366
+ } catch {
15367
+ continue;
15368
+ }
15369
+ if (!units.some((u) => isWithin(u.root, real))) targets.add(real);
15370
+ }
15371
+ }
15372
+ }
15373
+ return [...targets];
15374
+ }
15375
+ /**
15376
+ * The files a source upload carries, and where they are rooted.
15377
+ *
15378
+ * The project's repository first — or the project walked, outside git. Then
15379
+ * every repository a local `link:`/`file:` dependency reaches outside it, until
15380
+ * nothing new is reached, rooted together at their deepest common directory.
15381
+ * That is dadaki's shape: its Rebase project is a repository of its own, nested
15382
+ * inside the editor's repository (which ignores it), and its frontend links
15383
+ * `../../packages/editor` from there. A listing of the project's repository
15384
+ * alone rebuilt into "Rollup failed to resolve import @dadaki/editor".
15385
+ *
15386
+ * Paths are resolved through `realpath` first: git reports its top level with
15387
+ * symlinks resolved (`/private/var/…` for a macOS temp directory), and a
15388
+ * project path computed against the unresolved one would climb out of the
15389
+ * archive with `../`.
15390
+ */
15391
+ function listSourceFiles(projectRoot, git = runGit) {
15392
+ const realRoot = fs.realpathSync(projectRoot);
15393
+ const units = [listUnit(realRoot, git)];
15394
+ for (let reached = externalLinkTargets(units); reached.length > 0; reached = externalLinkTargets(units)) for (const target of reached) {
15395
+ if (units.some((u) => isWithin(u.root, target))) continue;
15396
+ units.push(listUnit(target, git));
15397
+ }
15398
+ const root = commonAncestor(units.map((u) => u.root));
15399
+ const files = /* @__PURE__ */ new Set();
15400
+ for (const unit of units) {
15401
+ const prefix = toPosix(path.relative(root, unit.root));
15402
+ for (const file of unit.files) files.add(prefix ? `${prefix}/${file}` : file);
15403
+ }
15404
+ return {
15405
+ root,
15406
+ files: [...files].sort(),
15407
+ projectPath: toPosix(path.relative(root, realRoot)),
15408
+ fromGit: units.every((u) => u.fromGit)
15409
+ };
15410
+ }
15411
+ /**
15412
+ * Pack a listing into a gzipped tarball at `outPath`.
15413
+ *
15414
+ * The list goes to tar as a NUL-separated file (`--null -T`), so a path with a
15415
+ * space or a newline in it is one path. `--no-xattrs` and `COPYFILE_DISABLE`
15416
+ * keep macOS's extended attributes and AppleDouble `._*` sidecars out of the
15417
+ * archive, as `packBundle` does; GNU tar accepts both. Every path handed to tar
15418
+ * is absolute, because GNU tar resolves a `-T` file after it has applied `-C`.
15419
+ */
15420
+ function packSource(listing, outPath) {
15421
+ const listPath = `${outPath}.files`;
15422
+ fs.writeFileSync(listPath, listing.files.map((file) => `${file}\0`).join(""));
15423
+ return new Promise((resolve, reject) => {
15424
+ const child = spawn("tar", [
15425
+ "-czf",
15426
+ path.resolve(outPath),
15427
+ "--no-xattrs",
15428
+ "--null",
15429
+ "-C",
15430
+ listing.root,
15431
+ "-T",
15432
+ path.resolve(listPath)
15433
+ ], {
15434
+ stdio: [
15435
+ "ignore",
15436
+ "ignore",
15437
+ "pipe"
15438
+ ],
15439
+ env: {
15440
+ ...process.env,
15441
+ COPYFILE_DISABLE: "1"
15442
+ }
15443
+ });
15444
+ let stderr = "";
15445
+ child.stderr.on("data", (chunk) => stderr += String(chunk));
15446
+ child.on("error", reject);
15447
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr.trim() || `tar exited ${code}`)));
15448
+ }).finally(() => fs.rmSync(listPath, { force: true }));
15449
+ }
15450
+ /** Upload a source archive; returns the control plane's id for it. */
15451
+ async function uploadRebuildSource(url, token, projectId, tarPath) {
15452
+ const bytes = fs.readFileSync(tarPath);
15453
+ const res = await fetch(`${url}/api/functions/deploy/source/upload?projectId=${encodeURIComponent(projectId)}`, {
15454
+ method: "POST",
15455
+ headers: {
15456
+ Authorization: `Bearer ${token}`,
15457
+ "Content-Type": "application/gzip",
15458
+ "User-Agent": cliUserAgent()
15459
+ },
15460
+ body: bytes
15461
+ });
15462
+ if (!res.ok) {
15463
+ const body = await res.text().catch(() => "");
15464
+ throw new Error(`the upload was refused (${res.status}): ${body || res.statusText}`);
15465
+ }
15466
+ const data = await res.json();
15467
+ const sourceId = typeof data === "object" && data !== null && "sourceId" in data ? data.sourceId : void 0;
15468
+ if (typeof sourceId !== "string" || !/^[0-9a-f]{32}$/i.test(sourceId)) throw new Error("the control plane did not return a source id");
15469
+ return sourceId;
15470
+ }
15471
+ /** The env files Vite reads for a production build, lowest precedence first. */
15472
+ var PRODUCTION_ENV_FILES = [
15473
+ ".env",
15474
+ ".env.local",
15475
+ ".env.production",
15476
+ ".env.production.local"
15477
+ ];
15478
+ /**
15479
+ * The `VITE_*` values a rebuild of this project's frontend needs.
15480
+ *
15481
+ * The `.env` files are never uploaded, so the values a static app is built with
15482
+ * have to travel separately — and only the `VITE_*` ones, which Vite inlines
15483
+ * into the client bundle and are public by construction. Nothing else is read.
15484
+ *
15485
+ * Read the way Vite reads them for `vite build`: `.env`, `.env.local`,
15486
+ * `.env.production`, `.env.production.local`, later winning — from the project
15487
+ * root, then from each static app's own `root`, which is where its Vite config
15488
+ * usually points. `process.env` wins over every file, as it does in Vite.
15489
+ *
15490
+ * `VITE_API_URL` is the one exception, taken from `process.env` only.
15491
+ * `staticBuildEnv` sets it to `process.env.VITE_API_URL ?? ""` for every
15492
+ * static build, so a value in a `.env` file never reaches a deployed bundle —
15493
+ * it is the `http://localhost:3001` of local development. Sending it would make
15494
+ * the rebuild bake in exactly the address the local build refuses to.
15495
+ */
15496
+ function collectBuildEnv(projectRoot, appRoots, env = process.env) {
15497
+ const collected = {};
15498
+ const seen = /* @__PURE__ */ new Set();
15499
+ for (const dir of [projectRoot, ...appRoots.map((root) => path.resolve(projectRoot, root))]) {
15500
+ const resolved = path.resolve(dir);
15501
+ if (seen.has(resolved) || !fs.existsSync(resolved)) continue;
15502
+ seen.add(resolved);
15503
+ for (const name of PRODUCTION_ENV_FILES) {
15504
+ let parsed;
15505
+ try {
15506
+ parsed = dotenv.parse(fs.readFileSync(path.join(resolved, name), "utf8"));
15507
+ } catch {
15508
+ continue;
15509
+ }
15510
+ for (const [key, value] of Object.entries(parsed)) if (key.startsWith("VITE_")) collected[key] = value;
15511
+ }
15512
+ }
15513
+ delete collected.VITE_API_URL;
15514
+ for (const [key, value] of Object.entries(env)) if (key.startsWith("VITE_") && value !== void 0) collected[key] = value;
15515
+ return collected;
15516
+ }
15517
+ /** The `root` of every static app a manifest declares, in declaration order. */
15518
+ function staticAppRoots(manifest) {
15519
+ if (!manifest) return [];
15520
+ const roots = [];
15521
+ for (const app of Object.values(manifest.apps)) if (app.type === "static" && typeof app.root === "string" && app.root !== "") roots.push(app.root);
15522
+ return roots;
15523
+ }
15524
+ var DEFAULT_STEPS = {
15525
+ list: (projectRoot) => listSourceFiles(projectRoot),
15526
+ pack: packSource,
15527
+ upload: uploadRebuildSource
15528
+ };
15529
+ /**
15530
+ * List, pack and upload the project's source. Never throws.
15531
+ *
15532
+ * Returns what the trigger should carry, or null — after a warning saying why
15533
+ * and what it costs — when the source could not be sent. The caller deploys
15534
+ * either way.
15535
+ */
15536
+ async function prepareRebuildSource(opts) {
15537
+ const steps = {
15538
+ ...DEFAULT_STEPS,
15539
+ ...opts.steps
15540
+ };
15541
+ const maxBytes = opts.maxBytes ?? 104857600;
15542
+ const tarPath = path.join(os.tmpdir(), `rebase-rebuild-src-${process.pid}-${Date.now()}.tar.gz`);
15543
+ const cost = "Platform upgrades will not rebuild this project until a later deploy uploads its source.";
15544
+ try {
15545
+ const listing = steps.list(opts.projectRoot);
15546
+ await steps.pack(listing, tarPath);
15547
+ const size = fs.statSync(tarPath).size;
15548
+ const mb = (bytes) => (bytes / 1024 / 1024).toFixed(1);
15549
+ if (size > maxBytes) {
15550
+ opts.warn(`The project source is ${mb(size)} MB compressed, over the ${Math.round(maxBytes / 1024 / 1024)} MB upload limit, so it was not uploaded. ${cost}`, listing.fromGit ? "Stop tracking large assets and build output in git, or pass --no-source to skip it on purpose." : "Initialise a git repository with a .gitignore to choose what is source, or pass --no-source.");
15551
+ return null;
15552
+ }
15553
+ opts.progress(` Uploading source (${listing.files.length} files, ${mb(size)} MB)...`);
15554
+ return {
15555
+ sourceId: await steps.upload(opts.url, opts.token, opts.projectId, tarPath),
15556
+ projectPath: listing.projectPath,
15557
+ buildEnv: collectBuildEnv(opts.projectRoot, staticAppRoots(opts.manifest))
15558
+ };
15559
+ } catch (err) {
15560
+ opts.warn(`The project source was not uploaded: ${err instanceof Error ? err.message : String(err)}. ${cost}`, "The deploy continues with the bundle alone.");
15561
+ return null;
15562
+ } finally {
15563
+ fs.rmSync(tarPath, { force: true });
15564
+ }
15565
+ }
15566
+ //#endregion
15164
15567
  //#region src/commands/cloud/deploy.ts
15165
15568
  var POLL_INTERVAL_MS = 1500;
15166
15569
  var POLL_TIMEOUT_MS = 900 * 1e3;
15167
- var MAX_SOURCE_UPLOAD_BYTES = 100 * 1024 * 1024;
15168
15570
  function sleep(ms) {
15169
15571
  return new Promise((r) => setTimeout(r, ms));
15170
15572
  }
@@ -15256,7 +15658,7 @@ function progress(line) {
15256
15658
  async function uploadSource(url, token, projectId, tarPath) {
15257
15659
  const bytes = fs.readFileSync(tarPath);
15258
15660
  const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
15259
- if (bytes.length > MAX_SOURCE_UPLOAD_BYTES) fail(`Source context is ${sizeMb} MB — the upload cap is ${Math.round(MAX_SOURCE_UPLOAD_BYTES / 1024 / 1024)} MB.`, "Trim the build context: exclude sourcemaps (*.map), build output and large assets via .rebaseignore or .gitignore.");
15661
+ if (bytes.length > 104857600) fail(`Source context is ${sizeMb} MB — the upload cap is ${Math.round(MAX_SOURCE_UPLOAD_BYTES / 1024 / 1024)} MB.`, "Trim the build context: exclude sourcemaps (*.map), build output and large assets via .rebaseignore or .gitignore.");
15260
15662
  progress(chalk.gray(` Uploading source (${sizeMb} MB)...`));
15261
15663
  const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
15262
15664
  method: "POST",
@@ -15303,6 +15705,7 @@ async function deployBundle(opts) {
15303
15705
  bundleDir = staticDir;
15304
15706
  await uploadAndTrigger({
15305
15707
  ...opts,
15708
+ projectRoot,
15306
15709
  bundleDir,
15307
15710
  appName: target.name
15308
15711
  });
@@ -15338,6 +15741,7 @@ async function deployBundle(opts) {
15338
15741
  }
15339
15742
  await uploadAndTrigger({
15340
15743
  ...opts,
15744
+ projectRoot,
15341
15745
  bundleDir
15342
15746
  });
15343
15747
  }
@@ -15352,7 +15756,7 @@ async function deployBundle(opts) {
15352
15756
  * bundle with no site in it.
15353
15757
  */
15354
15758
  async function uploadAndTrigger(opts) {
15355
- const { client, url, projectId, projectRef, bundleDir } = opts;
15759
+ const { client, url, projectId, projectRef, projectRoot, bundleDir } = opts;
15356
15760
  const manifest = readBundleManifest(bundleDir);
15357
15761
  if (manifest.hooks?.native) {
15358
15762
  const names = (manifest.hooks.nativeModules ?? []).map((m) => m.name).join(", ");
@@ -15377,10 +15781,22 @@ async function uploadAndTrigger(opts) {
15377
15781
  console.log("");
15378
15782
  console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
15379
15783
  }
15380
- let declaredApps = [];
15784
+ let projectManifest;
15381
15785
  try {
15382
- declaredApps = declaredAppsFrom(loadManifest(process.cwd()).manifest);
15786
+ projectManifest = loadManifest(projectRoot).manifest;
15383
15787
  } catch {}
15788
+ const declaredApps = declaredAppsFrom(projectManifest);
15789
+ let rebuildSource = null;
15790
+ if (opts.uploadSource && manifest.kind !== "static" && await platformRebuildsOff(client, projectId)) progress(chalk.gray(" Platform rebuilds are off for this project, so its source is not uploaded (turn them on with `rebase cloud settings set --platform-rebuilds on`)."));
15791
+ else if (opts.uploadSource && manifest.kind !== "static") rebuildSource = await prepareRebuildSource({
15792
+ projectRoot,
15793
+ url,
15794
+ token,
15795
+ projectId,
15796
+ manifest: projectManifest,
15797
+ progress: (line) => progress(chalk.gray(line)),
15798
+ warn
15799
+ });
15384
15800
  const body = bundleDeployBody({
15385
15801
  projectId,
15386
15802
  bundleId,
@@ -15388,7 +15804,9 @@ async function uploadAndTrigger(opts) {
15388
15804
  app: opts.appName,
15389
15805
  message: opts.message,
15390
15806
  declaredApps,
15391
- commit: bundleCommit(process.cwd())
15807
+ commit: bundleCommit(projectRoot),
15808
+ rebuildSource,
15809
+ allowFrameworkDowngrade: opts.allowDowngrade
15392
15810
  });
15393
15811
  let deploymentId;
15394
15812
  let managed;
@@ -15398,6 +15816,8 @@ async function uploadAndTrigger(opts) {
15398
15816
  deploymentId = String(res.deployment.id);
15399
15817
  managed = res.managed === true;
15400
15818
  } catch (e) {
15819
+ const refusal = intakeRefusal(e);
15820
+ if (refusal) fail(refusal.message, refusal.hint, refusal.code);
15401
15821
  reportError(e, "Managed deploy failed to start");
15402
15822
  }
15403
15823
  if (!opts.follow) {
@@ -15408,6 +15828,7 @@ async function uploadAndTrigger(opts) {
15408
15828
  success: true,
15409
15829
  deploymentId,
15410
15830
  managed,
15831
+ sourceUploaded: rebuildSource !== null,
15411
15832
  following: false
15412
15833
  });
15413
15834
  return;
@@ -15427,6 +15848,7 @@ async function uploadAndTrigger(opts) {
15427
15848
  success: true,
15428
15849
  deploymentId,
15429
15850
  managed,
15851
+ sourceUploaded: rebuildSource !== null,
15430
15852
  following: true,
15431
15853
  status
15432
15854
  });
@@ -15696,6 +16118,8 @@ var DEPLOY_FLAGS = {
15696
16118
  "--bundle-dir": String,
15697
16119
  "--skip-type-check": Boolean,
15698
16120
  "--eject": Boolean,
16121
+ "--no-source": Boolean,
16122
+ "--allow-downgrade": Boolean,
15699
16123
  "-m": "--message"
15700
16124
  };
15701
16125
  /**
@@ -15760,6 +16184,7 @@ async function deployCommand(rawArgs, projectRef) {
15760
16184
  const startedAt = Date.now();
15761
16185
  const { flags: args, appName } = resolveDeployArgs(rawArgs);
15762
16186
  if (args["--wait"] && args["--no-follow"]) fail("--wait and --no-follow ask for opposite things.", "`deploy` follows by default — pass neither, or `--no-follow` to return as soon as the build is triggered.", "usage");
16187
+ if (args["--source"] && args["--no-source"]) fail("--source and --no-source ask for opposite things.", "`--source <path>` builds a container image from a directory; `--no-source` skips uploading the source beside a managed bundle. Pass one.", "usage");
15763
16188
  const { client, url } = await requireClient(rawArgs);
15764
16189
  const projectId = await resolveProjectRef(projectRef, client);
15765
16190
  await requirePaymentMethod(client, projectId);
@@ -15777,7 +16202,9 @@ async function deployCommand(rawArgs, projectRef) {
15777
16202
  appName,
15778
16203
  skipTypeCheck: args["--skip-type-check"] === true,
15779
16204
  follow: args["--no-follow"] !== true,
15780
- timeoutMs: resolveDeployTimeout(args["--timeout"])
16205
+ timeoutMs: resolveDeployTimeout(args["--timeout"]),
16206
+ uploadSource: args["--no-source"] !== true,
16207
+ allowDowngrade: args["--allow-downgrade"] === true
15781
16208
  });
15782
16209
  return;
15783
16210
  }
@@ -15909,10 +16336,62 @@ function resolveTriggerFailure(e) {
15909
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");
15910
16337
  }
15911
16338
  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");
15912
- if (err?.status === 400 && err.details?.intakeCode) fail(err.message || "This bundle was refused.", err.details.hint ?? "Run `rebase cloud compute` to see what this project reserves.", err.details.intakeCode);
16339
+ const refusal = intakeRefusal(e);
16340
+ if (refusal) fail(refusal.message, refusal.hint, refusal.code);
15913
16341
  reportError(e, "Failed to trigger deployment");
15914
16342
  }
15915
16343
  /**
16344
+ * Whether the project's owner turned platform rebuilds off.
16345
+ *
16346
+ * Only an explicit `false` counts: a project row that predates the switch, or
16347
+ * one that could not be read, is on — which is the default, and the control
16348
+ * plane refuses an upload it should not have anyway.
16349
+ */
16350
+ async function platformRebuildsOff(client, projectId) {
16351
+ try {
16352
+ const row = await client.data.collection("projects").findById(projectId);
16353
+ return typeof row === "object" && row !== null && "platformRebuilds" in row && row.platformRebuilds === false;
16354
+ } catch {
16355
+ return false;
16356
+ }
16357
+ }
16358
+ /**
16359
+ * An intake refusal, which is a decision about what was deployed rather than a
16360
+ * transport error — or undefined for anything else.
16361
+ *
16362
+ * It carries a stable code in `details.intakeCode` and usually a remedy in
16363
+ * `details.hint`, and both used to be discarded: the deploy printed `Failed to
16364
+ * trigger deployment (400): …` and threw the hint away. The managed path, where
16365
+ * bundles are refused, never looked for either. Both paths answer through this
16366
+ * now, in the three-argument shape `fail` takes, so `--json` carries the code
16367
+ * and a person sees the fix.
16368
+ *
16369
+ * A downgrade gets one more line, because its two remedies are both in this
16370
+ * CLI: move the project forward, or say the step back is meant.
16371
+ */
16372
+ function intakeRefusal(e) {
16373
+ if (typeof e !== "object" || e === null) return void 0;
16374
+ const status = "status" in e && typeof e.status === "number" ? e.status : void 0;
16375
+ const details = "details" in e && typeof e.details === "object" && e.details !== null ? e.details : void 0;
16376
+ const code = details && "intakeCode" in details && typeof details.intakeCode === "string" ? details.intakeCode : void 0;
16377
+ if (status === void 0 || status < 400 || status >= 500 || !code) return void 0;
16378
+ const message = "message" in e && typeof e.message === "string" && e.message !== "" ? e.message : "This bundle was refused.";
16379
+ const serverHint = details && "hint" in details && typeof details.hint === "string" && details.hint !== "" ? details.hint : void 0;
16380
+ if (code === "FRAMEWORK_DOWNGRADE") {
16381
+ const remedy = "Run `rebase upgrade` to move this project's @rebasepro packages forward and deploy again, or pass `--allow-downgrade` to deploy the older release on purpose.";
16382
+ return {
16383
+ message,
16384
+ hint: serverHint ? `${serverHint}\n ${remedy}` : remedy,
16385
+ code
16386
+ };
16387
+ }
16388
+ return {
16389
+ message,
16390
+ hint: serverHint ?? "Run `rebase cloud compute` to see what this project reserves.",
16391
+ code
16392
+ };
16393
+ }
16394
+ /**
15916
16395
  * Poll a deployment record and print new log output as it arrives. Returns the
15917
16396
  * terminal status; a non-success still exits non-zero, as it always has.
15918
16397
  *
@@ -17033,7 +17512,7 @@ function printExtensionsHelp() {
17033
17512
  * `rebase cloud settings` — a project's editable configuration.
17034
17513
  *
17035
17514
  * settings Show the current settings
17036
- * settings set [flags] Update name / branch / repo / subdomain
17515
+ * settings set [flags] Update name / branch / repo / subdomain / platform rebuilds
17037
17516
  *
17038
17517
  * These are plain `projects` updates. A subdomain change is validated against
17039
17518
  * `check-subdomain` up front so the CLI fails with the real reason rather than a
@@ -17080,7 +17559,8 @@ async function showSettings(rawArgs) {
17080
17559
  ["Branch", p.gitBranch],
17081
17560
  ["Custom domain", p.customDomain],
17082
17561
  ["Provider", p.provider],
17083
- ["Region", p.region]
17562
+ ["Region", p.region],
17563
+ ["Platform rebuilds", p.platformRebuilds === false ? "off" : "on"]
17084
17564
  ]);
17085
17565
  console.log("");
17086
17566
  }, {
@@ -17091,12 +17571,20 @@ async function showSettings(rawArgs) {
17091
17571
  gitBranch: p.gitBranch ?? null,
17092
17572
  customDomain: p.customDomain ?? null,
17093
17573
  provider: p.provider ?? null,
17094
- region: p.region ?? null
17574
+ region: p.region ?? null,
17575
+ platformRebuilds: p.platformRebuilds !== false
17095
17576
  });
17096
17577
  } catch (e) {
17097
17578
  reportError(e, "Failed to load settings");
17098
17579
  }
17099
17580
  }
17581
+ /** `on`/`off` (or `true`/`false`) as a boolean, or null for anything else. */
17582
+ function parseOnOff(value) {
17583
+ const v = value.trim().toLowerCase();
17584
+ if (v === "on" || v === "true") return true;
17585
+ if (v === "off" || v === "false") return false;
17586
+ return null;
17587
+ }
17100
17588
  /** Build the update patch from the flags actually supplied (pure/testable). */
17101
17589
  function buildSettingsPatch(args) {
17102
17590
  const patch = {};
@@ -17104,6 +17592,7 @@ function buildSettingsPatch(args) {
17104
17592
  if (args.subdomain !== void 0) patch.subdomain = args.subdomain.toLowerCase();
17105
17593
  if (args.repo !== void 0) patch.gitRepoUrl = args.repo;
17106
17594
  if (args.branch !== void 0) patch.gitBranch = args.branch;
17595
+ if (args.platformRebuilds !== void 0) patch.platformRebuilds = args.platformRebuilds;
17107
17596
  return patch;
17108
17597
  }
17109
17598
  /** What `rebase cloud settings set` parses. Its page is the group's own. */
@@ -17111,7 +17600,8 @@ var SET_SETTINGS_FLAGS = {
17111
17600
  "--name": String,
17112
17601
  "--subdomain": String,
17113
17602
  "--repo": String,
17114
- "--branch": String
17603
+ "--branch": String,
17604
+ "--platform-rebuilds": String
17115
17605
  };
17116
17606
  async function setSettings(rawArgs) {
17117
17607
  const { flags: args } = parseCloudArgs({
@@ -17124,15 +17614,22 @@ async function setSettings(rawArgs) {
17124
17614
  const { client } = await requireClient(rawArgs);
17125
17615
  const projectId = await requireProject(rawArgs, client);
17126
17616
  const projectRef = displayProjectRef(rawArgs);
17617
+ let platformRebuilds;
17618
+ if (args["--platform-rebuilds"] !== void 0) {
17619
+ const parsed = parseOnOff(args["--platform-rebuilds"]);
17620
+ if (parsed === null) fail(`--platform-rebuilds takes on or off (got "${args["--platform-rebuilds"]}").`, void 0, "usage");
17621
+ platformRebuilds = parsed;
17622
+ }
17127
17623
  const patch = buildSettingsPatch({
17128
17624
  name: args["--name"],
17129
17625
  subdomain: args["--subdomain"],
17130
17626
  repo: args["--repo"],
17131
- branch: args["--branch"]
17627
+ branch: args["--branch"],
17628
+ platformRebuilds
17132
17629
  });
17133
- if (Object.keys(patch).length === 0) fail("Nothing to update.", "Pass --name, --subdomain, --repo, or --branch.", "usage");
17630
+ if (Object.keys(patch).length === 0) fail("Nothing to update.", "Pass --name, --subdomain, --repo, --branch, or --platform-rebuilds.", "usage");
17134
17631
  try {
17135
- if (patch.subdomain) {
17632
+ if (typeof patch.subdomain === "string" && patch.subdomain) {
17136
17633
  const check = await client.functions.invoke("check-subdomain", { subdomain: patch.subdomain }).catch(() => void 0);
17137
17634
  if (check && !check.available) fail(`Subdomain "${patch.subdomain}" is not available${check.reason ? ` (${check.reason})` : ""}.`, void 0, "subdomain_taken");
17138
17635
  }
@@ -17152,7 +17649,7 @@ function printSettingsHelp() {
17152
17649
  title: "Project configuration",
17153
17650
  actions: [{
17154
17651
  action: "show",
17155
- description: "Name, subdomain, repository and branch as recorded"
17652
+ description: "Name, subdomain, repository, branch and platform rebuilds as recorded"
17156
17653
  }, {
17157
17654
  action: "set",
17158
17655
  description: "Change one or more of them",
@@ -17160,7 +17657,8 @@ function printSettingsHelp() {
17160
17657
  ["--name <name>", "Display name"],
17161
17658
  ["--subdomain <sub>", "The <slug>.rebase.website host"],
17162
17659
  ["--repo <git url>", "Repository to build from"],
17163
- ["--branch <branch>", "Branch to build"]
17660
+ ["--branch <branch>", "Branch to build"],
17661
+ ["--platform-rebuilds <on|off>", "Rebuild this app on new framework releases (default on)"]
17164
17662
  ]
17165
17663
  }]
17166
17664
  });
@@ -18577,6 +19075,8 @@ var ACTION_HELP = {
18577
19075
  ["--bundle-dir <path>", "Deploy a bundle that is already built"],
18578
19076
  ["--source <path>", "Upload this directory and build a container image from it"],
18579
19077
  ["--skip-type-check", "Compile without type checking, as `rebase build` does"],
19078
+ ["--no-source", "Do not upload the project's source beside a backend bundle"],
19079
+ ["--allow-downgrade", "Deploy a bundle built on an older framework release than the project runs"],
18580
19080
  ["--eject", "Leave the managed runtime on purpose (ejects to a container image)"]
18581
19081
  ],
18582
19082
  examples: [
@@ -18584,7 +19084,12 @@ var ACTION_HELP = {
18584
19084
  "rebase cloud deploy web --message \"add search\"",
18585
19085
  "rebase cloud deploy --timeout 300 --json"
18586
19086
  ],
18587
- notes: ["A project whose rebase.json declares runtime: managed deploys a bundle without --bundle.", "--source on a managed project is refused: it would swap the project onto a container image."]
19087
+ notes: [
19088
+ "A project whose rebase.json declares runtime: managed deploys a bundle without --bundle.",
19089
+ "--source on a managed project is refused: it would swap the project onto a container image.",
19090
+ "A backend bundle deploy also uploads the project's source (what git tracks, never a .env), so a platform upgrade can rebuild it on a newer release. If it is over 100 MB or the upload fails, the deploy goes on without it and says so.",
19091
+ "A bundle older than the release the project runs is refused as FRAMEWORK_DOWNGRADE. Run `rebase upgrade` and deploy again, or pass --allow-downgrade."
19092
+ ]
18588
19093
  },
18589
19094
  logs: {
18590
19095
  command: "cloud logs",
@@ -19444,7 +19949,7 @@ function printCloudHelp() {
19444
19949
  * a mobile app with no repo relationship at all — an ordinary thing rather than
19445
19950
  * a special case.
19446
19951
  */
19447
- function printHelp$1() {
19952
+ function printHelp$2() {
19448
19953
  console.log(`
19449
19954
  ${chalk.bold("rebase apps")} — the apps this repository contributes
19450
19955
 
@@ -19467,7 +19972,7 @@ var APPS_SUBCOMMANDS = [
19467
19972
  ];
19468
19973
  async function appsCommand(subcommand, rawArgs = []) {
19469
19974
  if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
19470
- printHelp$1();
19975
+ printHelp$2();
19471
19976
  return;
19472
19977
  }
19473
19978
  const { flags: args, positionals } = parseCommandArgs({
@@ -19629,6 +20134,1000 @@ function loadManifestOrExit(projectRoot, asJson = false) {
19629
20134
  }
19630
20135
  }
19631
20136
  //#endregion
20137
+ //#region src/upgrade.ts
20138
+ /**
20139
+ * Moving a project's `@rebasepro/*` pins to one release.
20140
+ *
20141
+ * Every `@rebasepro/*` package ships at one version, and a project declares them
20142
+ * in several `package.json` files — the root, `backend/`, `frontend/`,
20143
+ * `config/`, and whatever else the repository holds. Bumping them by hand means
20144
+ * finding every one, keeping each range's `^` or `~`, and noticing the override
20145
+ * that quietly wins over all of them. `rebase upgrade` is that job, done the same
20146
+ * way every time; the control plane runs it too, when it rebuilds a managed
20147
+ * project on a newer release.
20148
+ *
20149
+ * Everything here is pure file work, so it can be tested against a temporary
20150
+ * directory: which specs move, the rewrite itself, and what overrides say. The
20151
+ * command in `commands/upgrade.ts` adds the registry lookup, the install and the
20152
+ * printing.
20153
+ *
20154
+ * ## The rewrite is textual
20155
+ *
20156
+ * A `package.json` is authored. Parsing it and writing `JSON.stringify` back
20157
+ * reformats the file, reorders nothing but reindents everything, and drops a
20158
+ * trailing newline or a two-space indent somebody chose — a one-line version
20159
+ * bump that arrives as a whole-file diff. So the file is scanned for the exact
20160
+ * byte range of each value, only those bytes are replaced, and the result is
20161
+ * parsed again and compared against what was intended. A file whose rewrite
20162
+ * does not parse back to exactly the intended values is not written.
20163
+ */
20164
+ /** The dependency blocks a pin is moved in. `peerDependencies` is not one. */
20165
+ var PIN_FIELDS = [
20166
+ "dependencies",
20167
+ "devDependencies",
20168
+ "optionalDependencies"
20169
+ ];
20170
+ /** The package whose published versions stand for the release as a whole. */
20171
+ var RELEASE_PACKAGE = "@rebasepro/cli";
20172
+ /** `MAJOR.MINOR.PATCH`, with an optional prerelease and build, and nothing else. */
20173
+ var SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
20174
+ /** Whether `value` is one exact version — `0.21.0`, `0.21.1-canary.g8c5a265`. */
20175
+ function isExactVersion(value) {
20176
+ return SEMVER.test(value);
20177
+ }
20178
+ /** An error that carries the `--json` envelope's code and a remedy. */
20179
+ var UpgradeError = class extends Error {
20180
+ code;
20181
+ hint;
20182
+ constructor(message, code, hint) {
20183
+ super(message);
20184
+ this.code = code;
20185
+ this.hint = hint;
20186
+ this.name = "UpgradeError";
20187
+ }
20188
+ };
20189
+ /**
20190
+ * What a dependency spec is, as far as moving it goes.
20191
+ *
20192
+ * Only a plain version is moved, because only a plain version has an obvious
20193
+ * answer: `^0.19.1` becomes `^0.21.0`. A `>=` floor, a `||` union or a partial
20194
+ * `^0.19` could each be rewritten several ways, and a tag or a workspace link
20195
+ * already means something other than "this release" — so each is reported with
20196
+ * its reason and left as written.
20197
+ */
20198
+ function classifySpec(spec) {
20199
+ const movable = /^([\^~]?)(.+)$/.exec(spec);
20200
+ if (movable && isExactVersion(movable[2])) return {
20201
+ kind: "movable",
20202
+ prefix: movable[1],
20203
+ version: movable[2]
20204
+ };
20205
+ for (const protocol of [
20206
+ "link:",
20207
+ "file:",
20208
+ "portal:"
20209
+ ]) if (spec.startsWith(protocol)) return {
20210
+ kind: "local",
20211
+ protocol
20212
+ };
20213
+ return {
20214
+ kind: "other",
20215
+ reason: otherReason(spec)
20216
+ };
20217
+ }
20218
+ function otherReason(spec) {
20219
+ const trimmed = spec.trim();
20220
+ if (trimmed.startsWith("workspace:")) return "a workspace: link, resolved inside this repository rather than from the registry";
20221
+ if (trimmed.startsWith("npm:")) return "an npm: alias; move the aliased version by hand";
20222
+ if (/^(git\+|git:|github:|gitlab:|bitbucket:|git@)/.test(trimmed) || /^https?:\/\//.test(trimmed)) return "a git or URL dependency, not a registry version";
20223
+ if (trimmed === "" || trimmed === "*" || /^[xX]$/.test(trimmed)) return "a wildcard, which already takes whatever the registry has";
20224
+ if (trimmed.includes("||")) return "a union of ranges, with no single version to move";
20225
+ if (/^[<>=]/.test(trimmed) || /\s-\s/.test(trimmed)) return "a comparator range, with no single version to move";
20226
+ if (/^[\^~]?v?\d/.test(trimmed)) return "not a plain MAJOR.MINOR.PATCH version";
20227
+ return "a dist-tag, not a version";
20228
+ }
20229
+ /**
20230
+ * The `@rebasepro/*` package an override key targets, or null.
20231
+ *
20232
+ * pnpm and npm key an override by name, optionally narrowed: `@rebasepro/x@<1`,
20233
+ * `parent>@rebasepro/x`; yarn's `resolutions` by a path, `**\/@rebasepro/x`. The
20234
+ * target is the last segment either way.
20235
+ */
20236
+ function overrideTarget(key) {
20237
+ const match = /(?:^|[/>])(@rebasepro\/[^/@>\s]+)(?:@[^/>]*)?$/.exec(key);
20238
+ return match ? match[1] : null;
20239
+ }
20240
+ /**
20241
+ * Directories the walk never enters: installed packages, build output (`dist`,
20242
+ * `dist-bundle`, `dist-bundle-admin`, …) and anything hidden (`.git`,
20243
+ * `.rebase`, an editor's worktrees). None of them hold a manifest anybody
20244
+ * authored for this project, and `dist-bundle/package.json` in particular is a
20245
+ * generated copy that `rebase build` rewrites anyway.
20246
+ */
20247
+ function skipDirectory(name) {
20248
+ return name === "node_modules" || name.startsWith("dist") || name.startsWith(".");
20249
+ }
20250
+ /** Every `package.json` and `pnpm-workspace.yaml` under the project root. */
20251
+ function discoverProjectFiles(projectRoot) {
20252
+ const found = {
20253
+ packageJsons: [],
20254
+ workspaceYamls: []
20255
+ };
20256
+ const walk = (dir) => {
20257
+ let entries;
20258
+ try {
20259
+ entries = fs.readdirSync(dir, { withFileTypes: true });
20260
+ } catch {
20261
+ return;
20262
+ }
20263
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
20264
+ for (const entry of entries) {
20265
+ const full = path.join(dir, entry.name);
20266
+ if (entry.isDirectory()) {
20267
+ if (!skipDirectory(entry.name)) walk(full);
20268
+ } else if (entry.isFile()) {
20269
+ if (entry.name === "package.json") found.packageJsons.push(full);
20270
+ else if (entry.name === "pnpm-workspace.yaml") found.workspaceYamls.push(full);
20271
+ }
20272
+ }
20273
+ };
20274
+ walk(projectRoot);
20275
+ return found;
20276
+ }
20277
+ /** Project-relative, POSIX. What every file path in the output looks like. */
20278
+ function relativeTo(projectRoot, absolute) {
20279
+ return path.relative(projectRoot, absolute).split(path.sep).join("/");
20280
+ }
20281
+ /**
20282
+ * Work out every change the upgrade makes, without writing anything.
20283
+ *
20284
+ * `--dry-run` prints this; a real run writes `writes` and nothing else.
20285
+ */
20286
+ function planUpgrade(projectRoot, target, options = {}) {
20287
+ if (!isExactVersion(target)) throw new UpgradeError(`"${target}" is not an exact version.`, "target_invalid");
20288
+ const plan = {
20289
+ target,
20290
+ changed: [],
20291
+ skipped: [],
20292
+ overrides: [],
20293
+ unreadable: [],
20294
+ writes: []
20295
+ };
20296
+ const files = discoverProjectFiles(projectRoot);
20297
+ for (const absolute of files.packageJsons) {
20298
+ const file = relativeTo(projectRoot, absolute);
20299
+ const original = fs.readFileSync(absolute, "utf8");
20300
+ const content = planPackageJson(file, original, target, options, plan);
20301
+ if (content !== null && content !== original) plan.writes.push({
20302
+ file,
20303
+ absolute,
20304
+ content
20305
+ });
20306
+ }
20307
+ for (const absolute of files.workspaceYamls) {
20308
+ const file = relativeTo(projectRoot, absolute);
20309
+ const original = fs.readFileSync(absolute, "utf8");
20310
+ const content = planWorkspaceYaml(file, original, target, options, plan);
20311
+ if (content !== original) plan.writes.push({
20312
+ file,
20313
+ absolute,
20314
+ content
20315
+ });
20316
+ }
20317
+ return plan;
20318
+ }
20319
+ /** Write what the plan changes. Nothing else on disk is touched. */
20320
+ function applyUpgradePlan(plan) {
20321
+ for (const write of plan.writes) fs.writeFileSync(write.absolute, write.content, "utf8");
20322
+ }
20323
+ /** Override blocks in a `package.json`, as paths from the root object. */
20324
+ var JSON_OVERRIDE_BLOCKS = [
20325
+ ["pnpm", "overrides"],
20326
+ ["overrides"],
20327
+ ["resolutions"]
20328
+ ];
20329
+ function planPackageJson(file, original, target, options, plan) {
20330
+ const bom = original.startsWith("") ? "" : "";
20331
+ const text = original.slice(bom.length);
20332
+ let parsed;
20333
+ let tree;
20334
+ try {
20335
+ parsed = JSON.parse(text);
20336
+ tree = scanJson(text);
20337
+ } catch (err) {
20338
+ plan.unreadable.push({
20339
+ file,
20340
+ reason: `not valid JSON: ${err instanceof Error ? err.message : String(err)}`
20341
+ });
20342
+ return null;
20343
+ }
20344
+ if (!isRecord(parsed) || tree.kind !== "object") {
20345
+ plan.unreadable.push({
20346
+ file,
20347
+ reason: "not a JSON object"
20348
+ });
20349
+ return null;
20350
+ }
20351
+ const expected = structuredClone(parsed);
20352
+ const edits = [];
20353
+ /** Override members to remove, as [block path, key]. */
20354
+ const removals = [];
20355
+ for (const field of PIN_FIELDS) {
20356
+ const block = memberValue(tree, [field]);
20357
+ if (block?.kind !== "object") continue;
20358
+ for (const member of block.members) {
20359
+ if (!member.key.startsWith("@rebasepro/")) continue;
20360
+ if (member.value.kind !== "string") {
20361
+ plan.skipped.push({
20362
+ file,
20363
+ name: member.key,
20364
+ field,
20365
+ spec: text.slice(member.value.start, member.value.end),
20366
+ reason: "not a string"
20367
+ });
20368
+ continue;
20369
+ }
20370
+ const spec = member.value.value;
20371
+ const cls = classifySpec(spec);
20372
+ if (cls.kind === "movable") {
20373
+ const to = `${cls.prefix}${target}`;
20374
+ if (to === spec) continue;
20375
+ edits.push({
20376
+ start: member.value.start,
20377
+ end: member.value.end,
20378
+ replacement: JSON.stringify(to)
20379
+ });
20380
+ setPath(expected, [field, member.key], to);
20381
+ plan.changed.push({
20382
+ file,
20383
+ name: member.key,
20384
+ field,
20385
+ from: spec,
20386
+ to
20387
+ });
20388
+ } else {
20389
+ const reason = cls.kind === "local" ? `a local ${cls.protocol} path, which points at a checkout on this machine rather than a release` : cls.reason;
20390
+ plan.skipped.push({
20391
+ file,
20392
+ name: member.key,
20393
+ field,
20394
+ spec,
20395
+ reason
20396
+ });
20397
+ }
20398
+ }
20399
+ }
20400
+ for (const blockPath of JSON_OVERRIDE_BLOCKS) {
20401
+ const block = memberValue(tree, blockPath);
20402
+ if (block?.kind !== "object") continue;
20403
+ const field = blockPath.join(".");
20404
+ for (const member of block.members) {
20405
+ if (!overrideTarget(member.key)) continue;
20406
+ if (member.value.kind !== "string") {
20407
+ plan.skipped.push({
20408
+ file,
20409
+ name: member.key,
20410
+ field,
20411
+ spec: text.slice(member.value.start, member.value.end),
20412
+ reason: "a nested override, which has no single version to move; edit it by hand"
20413
+ });
20414
+ continue;
20415
+ }
20416
+ const spec = member.value.value;
20417
+ const cls = classifySpec(spec);
20418
+ if (cls.kind === "movable") {
20419
+ const to = `${cls.prefix}${target}`;
20420
+ if (to === spec) continue;
20421
+ edits.push({
20422
+ start: member.value.start,
20423
+ end: member.value.end,
20424
+ replacement: JSON.stringify(to)
20425
+ });
20426
+ setPath(expected, [...blockPath, member.key], to);
20427
+ plan.overrides.push({
20428
+ file,
20429
+ name: member.key,
20430
+ spec,
20431
+ action: "bumped",
20432
+ to
20433
+ });
20434
+ } else if (cls.kind === "local") if (options.dropLocalOverrides) {
20435
+ removals.push([blockPath, member.key]);
20436
+ deletePath(expected, [...blockPath, member.key]);
20437
+ plan.overrides.push({
20438
+ file,
20439
+ name: member.key,
20440
+ spec,
20441
+ action: "removed-local"
20442
+ });
20443
+ } else plan.overrides.push({
20444
+ file,
20445
+ name: member.key,
20446
+ spec,
20447
+ action: "kept-local"
20448
+ });
20449
+ else plan.skipped.push({
20450
+ file,
20451
+ name: member.key,
20452
+ field,
20453
+ spec,
20454
+ reason: cls.reason
20455
+ });
20456
+ }
20457
+ }
20458
+ if (edits.length === 0 && removals.length === 0) return original;
20459
+ let next = applyEdits(text, edits);
20460
+ for (const [blockPath, key] of removals) next = removeJsonMember(next, blockPath, key);
20461
+ for (const blockPath of JSON_OVERRIDE_BLOCKS) {
20462
+ if (!removals.some(([removedFrom]) => removedFrom === blockPath)) continue;
20463
+ for (let depth = blockPath.length; depth > 0; depth--) {
20464
+ const containerPath = blockPath.slice(0, depth);
20465
+ const node = memberValue(scanJson(next), containerPath);
20466
+ if (node?.kind !== "object" || node.members.length > 0) break;
20467
+ next = removeJsonMember(next, containerPath.slice(0, -1), containerPath[containerPath.length - 1]);
20468
+ deletePath(expected, containerPath);
20469
+ }
20470
+ }
20471
+ let reparsed;
20472
+ try {
20473
+ reparsed = JSON.parse(next);
20474
+ } catch {
20475
+ reparsed = void 0;
20476
+ }
20477
+ if (!isDeepStrictEqual(reparsed, expected)) throw new UpgradeError(`Could not rewrite ${file} without disturbing it; nothing was written.`, "rewrite_failed", "Move its @rebasepro versions by hand, then run `rebase upgrade` again.");
20478
+ return bom + next;
20479
+ }
20480
+ /**
20481
+ * The structure of a JSON document, with where each value starts and ends.
20482
+ *
20483
+ * `JSON.parse` answers what a file says; this answers where it says it, which is
20484
+ * what a rewrite that leaves every other byte alone needs. Throws on anything
20485
+ * that is not JSON — callers have already parsed the same text, so that never
20486
+ * happens in practice.
20487
+ */
20488
+ function scanJson(text) {
20489
+ let i = 0;
20490
+ const WHITESPACE = " \n\r";
20491
+ const skipWhitespace = () => {
20492
+ while (i < text.length && WHITESPACE.includes(text[i])) i++;
20493
+ };
20494
+ const unexpected = () => {
20495
+ throw new Error(`unexpected ${i < text.length ? `"${text[i]}"` : "end of input"} at offset ${i}`);
20496
+ };
20497
+ const readString = () => {
20498
+ const start = i;
20499
+ if (text[i] !== "\"") unexpected();
20500
+ i++;
20501
+ while (i < text.length && text[i] !== "\"") i += text[i] === "\\" ? 2 : 1;
20502
+ if (i >= text.length) unexpected();
20503
+ i++;
20504
+ return {
20505
+ kind: "string",
20506
+ start,
20507
+ end: i,
20508
+ value: JSON.parse(text.slice(start, i))
20509
+ };
20510
+ };
20511
+ const readValue = () => {
20512
+ skipWhitespace();
20513
+ const start = i;
20514
+ if (text[i] === "{") {
20515
+ i++;
20516
+ const members = [];
20517
+ skipWhitespace();
20518
+ if (text[i] === "}") {
20519
+ i++;
20520
+ return {
20521
+ kind: "object",
20522
+ start,
20523
+ end: i,
20524
+ members
20525
+ };
20526
+ }
20527
+ for (;;) {
20528
+ skipWhitespace();
20529
+ const key = readString();
20530
+ skipWhitespace();
20531
+ if (text[i] !== ":") unexpected();
20532
+ i++;
20533
+ members.push({
20534
+ key: key.value,
20535
+ keyStart: key.start,
20536
+ value: readValue()
20537
+ });
20538
+ skipWhitespace();
20539
+ if (text[i] === ",") {
20540
+ i++;
20541
+ continue;
20542
+ }
20543
+ if (text[i] === "}") {
20544
+ i++;
20545
+ return {
20546
+ kind: "object",
20547
+ start,
20548
+ end: i,
20549
+ members
20550
+ };
20551
+ }
20552
+ unexpected();
20553
+ }
20554
+ }
20555
+ if (text[i] === "[") {
20556
+ i++;
20557
+ skipWhitespace();
20558
+ if (text[i] === "]") {
20559
+ i++;
20560
+ return {
20561
+ kind: "array",
20562
+ start,
20563
+ end: i
20564
+ };
20565
+ }
20566
+ for (;;) {
20567
+ readValue();
20568
+ skipWhitespace();
20569
+ if (text[i] === ",") {
20570
+ i++;
20571
+ continue;
20572
+ }
20573
+ if (text[i] === "]") {
20574
+ i++;
20575
+ return {
20576
+ kind: "array",
20577
+ start,
20578
+ end: i
20579
+ };
20580
+ }
20581
+ unexpected();
20582
+ }
20583
+ }
20584
+ if (text[i] === "\"") return readString();
20585
+ while (i < text.length && !",:{}[]\"".includes(text[i]) && !WHITESPACE.includes(text[i])) i++;
20586
+ if (i === start) unexpected();
20587
+ return {
20588
+ kind: "scalar",
20589
+ start,
20590
+ end: i
20591
+ };
20592
+ };
20593
+ const root = readValue();
20594
+ skipWhitespace();
20595
+ if (i !== text.length) unexpected();
20596
+ return root;
20597
+ }
20598
+ /** The value at a path of object keys, or undefined. The last duplicate wins, as in `JSON.parse`. */
20599
+ function memberValue(root, keys) {
20600
+ let node = root;
20601
+ for (const key of keys) {
20602
+ if (node?.kind !== "object") return void 0;
20603
+ const members = node.members.filter((m) => m.key === key);
20604
+ node = members[members.length - 1]?.value;
20605
+ }
20606
+ return node;
20607
+ }
20608
+ /** Apply non-overlapping edits, last first so the earlier offsets stay valid. */
20609
+ function applyEdits(text, edits) {
20610
+ let out = text;
20611
+ for (const edit of [...edits].sort((a, b) => b.start - a.start)) out = out.slice(0, edit.start) + edit.replacement + out.slice(edit.end);
20612
+ return out;
20613
+ }
20614
+ /**
20615
+ * Remove one member from the object at `objectPath`, taking its comma with it.
20616
+ *
20617
+ * Rescans rather than trusting offsets from before an earlier edit. A middle or
20618
+ * first member takes everything up to the next key, so the next member inherits
20619
+ * its indentation; the last takes the comma and whitespace after the previous
20620
+ * value, so the closing brace keeps its line; an only member leaves `{}`.
20621
+ */
20622
+ function removeJsonMember(text, objectPath, key) {
20623
+ const container = memberValue(scanJson(text), objectPath);
20624
+ if (container?.kind !== "object") return text;
20625
+ const index = container.members.findIndex((m) => m.key === key);
20626
+ if (index === -1) return text;
20627
+ const members = container.members;
20628
+ const member = members[index];
20629
+ if (members.length === 1) return text.slice(0, container.start + 1) + text.slice(container.end - 1);
20630
+ if (index < members.length - 1) return text.slice(0, member.keyStart) + text.slice(members[index + 1].keyStart);
20631
+ return text.slice(0, members[index - 1].value.end) + text.slice(member.value.end);
20632
+ }
20633
+ function isRecord(value) {
20634
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20635
+ }
20636
+ function setPath(root, keys, value) {
20637
+ let node = root;
20638
+ for (const key of keys.slice(0, -1)) {
20639
+ const next = node[key];
20640
+ if (!isRecord(next)) return;
20641
+ node = next;
20642
+ }
20643
+ node[keys[keys.length - 1]] = value;
20644
+ }
20645
+ function deletePath(root, keys) {
20646
+ let node = root;
20647
+ for (const key of keys.slice(0, -1)) {
20648
+ const next = node[key];
20649
+ if (!isRecord(next)) return;
20650
+ node = next;
20651
+ }
20652
+ delete node[keys[keys.length - 1]];
20653
+ }
20654
+ /** Lines, each without its `\r`, and whether the file used them. */
20655
+ function splitLines(text) {
20656
+ return text.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
20657
+ }
20658
+ /**
20659
+ * A YAML scalar at the start of `rest`: its text, and where that text sits.
20660
+ *
20661
+ * Double- and single-quoted and plain scalars, which is every shape an override
20662
+ * value is written in. A double-quoted one with an escape in it is refused
20663
+ * rather than decoded: no version or path needs one, and getting the decoding
20664
+ * wrong would rewrite a value this does not understand.
20665
+ */
20666
+ function readYamlScalar(rest) {
20667
+ if (rest.startsWith("\"")) {
20668
+ const close = rest.indexOf("\"", 1);
20669
+ if (close === -1 || rest.slice(1, close).includes("\\")) return null;
20670
+ if (!/^\s*(#.*)?$/.test(rest.slice(close + 1))) return null;
20671
+ return {
20672
+ value: rest.slice(1, close),
20673
+ start: 1,
20674
+ end: close
20675
+ };
20676
+ }
20677
+ if (rest.startsWith("'")) {
20678
+ const close = rest.indexOf("'", 1);
20679
+ if (close === -1 || rest[close + 1] === "'") return null;
20680
+ if (!/^\s*(#.*)?$/.test(rest.slice(close + 1))) return null;
20681
+ return {
20682
+ value: rest.slice(1, close),
20683
+ start: 1,
20684
+ end: close
20685
+ };
20686
+ }
20687
+ const comment = rest.search(/\s#/);
20688
+ const plain = (comment === -1 ? rest : rest.slice(0, comment)).trimEnd();
20689
+ if (plain === "" || /^[[{&*!|>%@`]/.test(plain)) return null;
20690
+ return {
20691
+ value: plain,
20692
+ start: 0,
20693
+ end: plain.length
20694
+ };
20695
+ }
20696
+ /**
20697
+ * The top-level `overrides:` block of a `pnpm-workspace.yaml`, read line by line.
20698
+ *
20699
+ * Deliberately narrow. The file is YAML, and the CLI carries no YAML parser; the
20700
+ * shape pnpm documents and every project writes is a block mapping of one
20701
+ * scalar per line, which a line reader handles exactly. Anything else in the
20702
+ * block is counted rather than guessed at, and an inline `overrides: { … }` is
20703
+ * reported as not handled.
20704
+ */
20705
+ function readYamlOverrides(lines) {
20706
+ const keyLine = lines.findIndex((line) => /^overrides:\s*(#.*)?$/.test(line));
20707
+ if (keyLine === -1) return null;
20708
+ const block = {
20709
+ keyLine,
20710
+ lines: [],
20711
+ entries: [],
20712
+ otherContent: 0
20713
+ };
20714
+ for (let index = keyLine + 1; index < lines.length; index++) {
20715
+ const line = lines[index];
20716
+ if (line.trim() !== "" && !/^\s/.test(line) && !line.startsWith("#")) break;
20717
+ block.lines.push(index);
20718
+ if (line.trim() === "" || /^\s*#/.test(line)) continue;
20719
+ const entry = /^(\s+)("[^"]*"|'[^']*'|[^\s"'#][^:#]*?)\s*:(\s+|$)/.exec(line);
20720
+ const valueOffset = entry ? entry[0].length : -1;
20721
+ const scalar = entry ? readYamlScalar(line.slice(valueOffset)) : null;
20722
+ if (!entry || !scalar) {
20723
+ block.otherContent++;
20724
+ continue;
20725
+ }
20726
+ const rawKey = entry[2];
20727
+ const key = /^["']/.test(rawKey) ? rawKey.slice(1, -1) : rawKey;
20728
+ block.entries.push({
20729
+ line: index,
20730
+ key,
20731
+ value: scalar.value,
20732
+ valueStart: valueOffset + scalar.start,
20733
+ valueEnd: valueOffset + scalar.end
20734
+ });
20735
+ }
20736
+ return block;
20737
+ }
20738
+ function planWorkspaceYaml(file, original, target, options, plan) {
20739
+ const lines = splitLines(original);
20740
+ const crlf = original.includes("\r\n");
20741
+ const block = readYamlOverrides(lines);
20742
+ if (!block) {
20743
+ const inline = lines.find((line) => /^overrides:\s*[^\s#]/.test(line));
20744
+ if (inline && inline.includes("@rebasepro/")) plan.skipped.push({
20745
+ file,
20746
+ name: "overrides",
20747
+ field: "overrides",
20748
+ spec: inline.slice(10).trim(),
20749
+ reason: "an inline overrides map, which this does not rewrite; write it as a block, or edit it by hand"
20750
+ });
20751
+ return original;
20752
+ }
20753
+ const next = [...lines];
20754
+ const removed = /* @__PURE__ */ new Set();
20755
+ const expected = /* @__PURE__ */ new Map();
20756
+ for (const entry of block.entries) {
20757
+ if (!overrideTarget(entry.key)) {
20758
+ expected.set(entry.key, entry.value);
20759
+ continue;
20760
+ }
20761
+ const cls = classifySpec(entry.value);
20762
+ if (cls.kind === "movable") {
20763
+ const to = `${cls.prefix}${target}`;
20764
+ expected.set(entry.key, to);
20765
+ if (to === entry.value) continue;
20766
+ const line = lines[entry.line];
20767
+ next[entry.line] = line.slice(0, entry.valueStart) + to + line.slice(entry.valueEnd);
20768
+ plan.overrides.push({
20769
+ file,
20770
+ name: entry.key,
20771
+ spec: entry.value,
20772
+ action: "bumped",
20773
+ to
20774
+ });
20775
+ } else if (cls.kind === "local") if (options.dropLocalOverrides) {
20776
+ removed.add(entry.line);
20777
+ plan.overrides.push({
20778
+ file,
20779
+ name: entry.key,
20780
+ spec: entry.value,
20781
+ action: "removed-local"
20782
+ });
20783
+ } else {
20784
+ expected.set(entry.key, entry.value);
20785
+ plan.overrides.push({
20786
+ file,
20787
+ name: entry.key,
20788
+ spec: entry.value,
20789
+ action: "kept-local"
20790
+ });
20791
+ }
20792
+ else {
20793
+ expected.set(entry.key, entry.value);
20794
+ plan.skipped.push({
20795
+ file,
20796
+ name: entry.key,
20797
+ field: "overrides",
20798
+ spec: entry.value,
20799
+ reason: cls.reason
20800
+ });
20801
+ }
20802
+ }
20803
+ const survivors = block.entries.filter((entry) => !removed.has(entry.line)).length + block.otherContent;
20804
+ if (removed.size > 0 && survivors === 0) removed.add(block.keyLine);
20805
+ const content = next.filter((_, index) => !removed.has(index)).join(crlf ? "\r\n" : "\n");
20806
+ const after = readYamlOverrides(splitLines(content));
20807
+ 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
+ return content;
20809
+ }
20810
+ /** Lockfiles, in the order they are looked for in each directory. */
20811
+ var LOCKFILES = [
20812
+ ["pnpm-lock.yaml", "pnpm"],
20813
+ ["package-lock.json", "npm"],
20814
+ ["yarn.lock", "yarn"],
20815
+ ["bun.lockb", "bun"],
20816
+ ["bun.lock", "bun"]
20817
+ ];
20818
+ /**
20819
+ * The package manager this project installs with, from what is on disk.
20820
+ *
20821
+ * A lockfile is the project's own statement, so it wins. The search starts at
20822
+ * the project root and walks up, because a project inside a workspace has its
20823
+ * lockfile at the workspace root; it stops at the repository's root, so a stray
20824
+ * lockfile in a home directory is never read as this project's choice. With no
20825
+ * lockfile anywhere, a `pnpm-workspace.yaml` still says pnpm; otherwise npm,
20826
+ * the one every Node install has.
20827
+ */
20828
+ function detectInstaller(projectRoot) {
20829
+ let dir = path.resolve(projectRoot);
20830
+ let workspaceYaml = false;
20831
+ for (;;) {
20832
+ for (const [lockfile, installer] of LOCKFILES) if (fs.existsSync(path.join(dir, lockfile))) return installer;
20833
+ if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) workspaceYaml = true;
20834
+ const parent = path.dirname(dir);
20835
+ if (parent === dir || fs.existsSync(path.join(dir, ".git"))) break;
20836
+ dir = parent;
20837
+ }
20838
+ return workspaceYaml ? "pnpm" : "npm";
20839
+ }
20840
+ /**
20841
+ * The install command for each package manager — pnpm's and npm's from the
20842
+ * helpers every other command uses, yarn's and bun's spelled the same way.
20843
+ */
20844
+ function installCommand(installer) {
20845
+ const [bin, ...args] = installer === "pnpm" || installer === "npm" ? getPMCommands(installer).install : [installer, "install"];
20846
+ return [bin, args];
20847
+ }
20848
+ /**
20849
+ * The exact version `--to` names.
20850
+ *
20851
+ * An exact version is used as written, with no network call: the control plane
20852
+ * passes one when it rebuilds a project, and that rebuild must not depend on a
20853
+ * registry lookup succeeding for anything but the install itself. Anything
20854
+ * else is a dist-tag, asked of the registry through `npmView` — which runs npm
20855
+ * in the project, so its `.npmrc` and the user's registry config apply.
20856
+ */
20857
+ async function resolveTarget(requested, npmView) {
20858
+ const trimmed = requested.trim();
20859
+ const bare = trimmed.replace(/^v(?=\d)/, "");
20860
+ if (isExactVersion(bare)) return bare;
20861
+ if (!/^[A-Za-z][\w.-]*$/.test(trimmed)) throw new UpgradeError(`"${requested}" is neither an exact version nor a dist-tag.`, "target_invalid", "Pass an exact version such as 0.21.0, or a dist-tag such as latest or canary.");
20862
+ let answer;
20863
+ try {
20864
+ answer = (await npmView(`${RELEASE_PACKAGE}@${trimmed}`)).trim();
20865
+ } catch (err) {
20866
+ const detail = err instanceof Error ? firstLine(err.message) : String(err);
20867
+ throw new UpgradeError(`Could not resolve "${trimmed}" on the npm registry${detail ? `: ${detail}` : "."}`, "target_unresolved", "Check the tag and your registry access, or pass an exact version with --to.");
20868
+ }
20869
+ if (!isExactVersion(answer)) throw new UpgradeError(`The registry has no single version for ${RELEASE_PACKAGE}@${trimmed}${answer ? ` (it answered "${firstLine(answer)}")` : ""}.`, "target_unresolved", "Check the tag, or pass an exact version with --to.");
20870
+ return answer;
20871
+ }
20872
+ function firstLine(text) {
20873
+ return text.split("\n").map((line) => line.trim()).filter(Boolean)[0] ?? "";
20874
+ }
20875
+ /**
20876
+ * Whether moving from `from` to `to` goes backwards, for the one-word note the
20877
+ * summary prints. Prerelease identifiers compare as SemVer orders them: a
20878
+ * release outranks its own prereleases.
20879
+ */
20880
+ function isDowngrade(from, to) {
20881
+ const a = SEMVER.exec(from.replace(/^[\^~]/, ""));
20882
+ const b = SEMVER.exec(to.replace(/^[\^~]/, ""));
20883
+ if (!a || !b) return false;
20884
+ for (let i = 1; i <= 3; i++) {
20885
+ const diff = Number(a[i]) - Number(b[i]);
20886
+ if (diff !== 0) return diff > 0;
20887
+ }
20888
+ const preA = a[4];
20889
+ const preB = b[4];
20890
+ if (preA === preB) return false;
20891
+ if (preA === void 0) return true;
20892
+ if (preB === void 0) return false;
20893
+ const partsA = preA.split(".");
20894
+ const partsB = preB.split(".");
20895
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
20896
+ const x = partsA[i];
20897
+ const y = partsB[i];
20898
+ if (x === void 0) return false;
20899
+ if (y === void 0) return true;
20900
+ if (x === y) continue;
20901
+ const nx = /^\d+$/.test(x) ? Number(x) : NaN;
20902
+ const ny = /^\d+$/.test(y) ? Number(y) : NaN;
20903
+ if (!Number.isNaN(nx) && !Number.isNaN(ny)) return nx > ny;
20904
+ if (!Number.isNaN(nx)) return false;
20905
+ if (!Number.isNaN(ny)) return true;
20906
+ return x > y;
20907
+ }
20908
+ return false;
20909
+ }
20910
+ //#endregion
20911
+ //#region src/commands/upgrade.ts
20912
+ /**
20913
+ * CLI command: rebase upgrade [--to <version|tag>]
20914
+ *
20915
+ * Moves every `@rebasepro/*` package the project pins to one release, then
20916
+ * installs. The file work — which specs move, the rewrite, what overrides say —
20917
+ * is `../upgrade.ts`; this adds the registry lookup for a tag, the install, and
20918
+ * the two ways of saying what happened.
20919
+ */
20920
+ /** Every flag `rebase upgrade` accepts. Its help page and the docs verifier read this. */
20921
+ var UPGRADE_FLAGS = {
20922
+ "--to": String,
20923
+ "--drop-local-overrides": Boolean,
20924
+ "--no-install": Boolean,
20925
+ "--dry-run": Boolean,
20926
+ "--json": Boolean
20927
+ };
20928
+ function printHelp$1() {
20929
+ console.log(`
20930
+ ${chalk.bold("rebase upgrade")} — move every @rebasepro package this project pins to one release
20931
+
20932
+ ${chalk.bold("Usage")}
20933
+ rebase upgrade [--to <version|tag>] [options]
20934
+
20935
+ ${chalk.bold("Options")}
20936
+ --to <version|tag> The release: an exact version (0.21.0) or a dist-tag
20937
+ (latest, canary). Default: latest
20938
+ --drop-local-overrides Remove link: and file: overrides of @rebasepro packages,
20939
+ which win over every pin
20940
+ --no-install Rewrite the package.json files, but do not install
20941
+ --dry-run Print what would change, and write nothing
20942
+ --json One JSON document on stdout
20943
+ -h, --help Show this help
20944
+
20945
+ ${chalk.bold("What it changes")}
20946
+ Every @rebasepro/* entry in dependencies, devDependencies and optionalDependencies,
20947
+ in every package.json under the project (node_modules, dist* and hidden
20948
+ directories excepted). ^ and ~ are kept. peerDependencies are left alone, and so
20949
+ 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.
20951
+
20952
+ ${chalk.bold("Examples")}
20953
+ rebase upgrade Move to the latest release and install
20954
+ rebase upgrade --to canary --dry-run See what the canary would change
20955
+ rebase upgrade --to 0.21.0 --no-install --json
20956
+ `.trim());
20957
+ }
20958
+ var defaultIo = {
20959
+ npmView: async (spec, cwd) => {
20960
+ try {
20961
+ return (await execa("npm", [
20962
+ "view",
20963
+ spec,
20964
+ "version"
20965
+ ], { cwd })).stdout;
20966
+ } catch (err) {
20967
+ throw new Error(npmFailure(err));
20968
+ }
20969
+ },
20970
+ install: async ([bin, args], cwd, quietStdout) => {
20971
+ await execa(bin, args, {
20972
+ cwd,
20973
+ stdio: quietStdout ? [
20974
+ "ignore",
20975
+ 2,
20976
+ "inherit"
20977
+ ] : "inherit"
20978
+ });
20979
+ }
20980
+ };
20981
+ /**
20982
+ * The one line of a failed `npm view` worth printing.
20983
+ *
20984
+ * execa's own message is the command line that failed, which says nothing the
20985
+ * reader does not know. npm's stderr says why — `404 No match found for version
20986
+ * nosuchtag` — under an `npm error code E404` line that says less.
20987
+ */
20988
+ function npmFailure(err) {
20989
+ return (typeof err === "object" && err !== null && "stderr" in err && typeof err.stderr === "string" ? err.stderr : "").split("\n").map((line) => line.replace(/^npm (?:error|ERR!)\s*/, "").trim()).find((line) => line !== "" && !/^code\s/.test(line) && !/^\d{3}$/.test(line)) ?? (err instanceof Error ? err.message : String(err));
20990
+ }
20991
+ async function upgradeCommand(rawArgs, io = defaultIo) {
20992
+ if (wantsHelp(rawArgs)) {
20993
+ printHelp$1();
20994
+ return;
20995
+ }
20996
+ const { flags } = parseCommandArgs({
20997
+ spec: UPGRADE_FLAGS,
20998
+ rawArgs,
20999
+ commandWords: 1,
21000
+ command: "upgrade",
21001
+ maxPositionals: 0
21002
+ });
21003
+ const json = flags["--json"] === true;
21004
+ const dryRun = flags["--dry-run"] === true;
21005
+ const projectRoot = requireProjectRoot();
21006
+ /** Every refusal, in whichever of the two languages was asked for. */
21007
+ const refuse = (message, code, hint, issues) => {
21008
+ if (json) failAsJson(message, code, hint, issues);
21009
+ console.error(chalk.red(`✗ ${message}`));
21010
+ for (const issue of issues ?? []) console.error(` ${chalk.gray(issue.path ?? "")} ${issue.message}`);
21011
+ if (hint) console.error(chalk.gray(` ${hint}`));
21012
+ process.exit(1);
21013
+ };
21014
+ try {
21015
+ loadManifest(projectRoot);
21016
+ } catch (err) {
21017
+ if (err instanceof ManifestError) refuse(err.message, "manifest_invalid", "Fix rebase.json, then run this again.", err.issues.map((issue) => ({
21018
+ path: issue.path,
21019
+ message: issue.message
21020
+ })));
21021
+ throw err;
21022
+ }
21023
+ let plan;
21024
+ try {
21025
+ plan = planUpgrade(projectRoot, await resolveTarget(flags["--to"] ?? "latest", (spec) => io.npmView(spec, projectRoot)), { dropLocalOverrides: flags["--drop-local-overrides"] === true });
21026
+ } catch (err) {
21027
+ if (err instanceof UpgradeError) refuse(err.message, err.code, err.hint);
21028
+ throw err;
21029
+ }
21030
+ if (!dryRun) applyUpgradePlan(plan);
21031
+ const wrote = !dryRun && plan.writes.length > 0;
21032
+ const install = wrote && flags["--no-install"] !== true;
21033
+ const installer = detectInstaller(projectRoot);
21034
+ const [bin, args] = installCommand(installer);
21035
+ const installLine = [bin, ...args].join(" ");
21036
+ if (!json) printSummary(plan, dryRun);
21037
+ let installed = false;
21038
+ if (install) {
21039
+ if (!json) console.log(chalk.gray(` Installing with ${installer}...\n`));
21040
+ try {
21041
+ await io.install([bin, args], projectRoot, json);
21042
+ installed = true;
21043
+ } catch {
21044
+ refuse(`\`${installLine}\` failed.`, "install_failed", `The package.json edits were kept. Fix what the install reported and run \`${installLine}\` again, or revert the edits with git.`);
21045
+ }
21046
+ }
21047
+ const result = {
21048
+ target: plan.target,
21049
+ changed: plan.changed,
21050
+ skipped: plan.skipped,
21051
+ overrides: plan.overrides,
21052
+ unreadable: plan.unreadable,
21053
+ installed,
21054
+ dryRun
21055
+ };
21056
+ if (json) {
21057
+ console.log(JSON.stringify(result, null, 2));
21058
+ return;
21059
+ }
21060
+ printOutcome(plan, {
21061
+ dryRun,
21062
+ wrote,
21063
+ installed,
21064
+ installLine
21065
+ });
21066
+ }
21067
+ /** Changes grouped by file, then what was left alone and why, then overrides. */
21068
+ function printSummary(plan, dryRun) {
21069
+ console.log("");
21070
+ console.log(`${chalk.bold("Rebase")} — ${dryRun ? "what upgrading" : "upgrading"} @rebasepro packages to ${chalk.cyan(plan.target)}${dryRun ? " would change" : ""}`);
21071
+ const byFile = /* @__PURE__ */ new Map();
21072
+ for (const pin of plan.changed) byFile.set(pin.file, [...byFile.get(pin.file) ?? [], pin]);
21073
+ const nameWidth = Math.max(0, ...plan.changed.map((pin) => pin.name.length));
21074
+ for (const [file, pins] of byFile) {
21075
+ console.log("");
21076
+ console.log(` ${chalk.bold(file)}`);
21077
+ for (const pin of pins) {
21078
+ const field = pin.field === "dependencies" ? "" : chalk.gray(` ${pin.field}`);
21079
+ const older = isDowngrade(pin.from, pin.to) ? chalk.yellow(" (older)") : "";
21080
+ console.log(` ${pin.name.padEnd(nameWidth)} ${chalk.gray(pin.from)} → ${chalk.green(pin.to)}${field}${older}`);
21081
+ }
21082
+ }
21083
+ if (plan.skipped.length > 0) {
21084
+ console.log("");
21085
+ console.log(` ${chalk.bold("Left alone")}`);
21086
+ for (const skip of plan.skipped) {
21087
+ console.log(` ${skip.name} ${chalk.gray(skip.spec)} ${chalk.gray(`(${skip.file}, ${skip.field})`)}`);
21088
+ console.log(chalk.gray(` ${skip.reason}`));
21089
+ }
21090
+ }
21091
+ if (plan.overrides.length > 0) {
21092
+ console.log("");
21093
+ console.log(` ${chalk.bold("Overrides")}`);
21094
+ for (const override of plan.overrides) {
21095
+ const where = chalk.gray(`(${override.file})`);
21096
+ if (override.action === "bumped") console.log(` ${override.name} ${chalk.gray(override.spec)} → ${chalk.green(override.to ?? "")} ${where}`);
21097
+ else if (override.action === "removed-local") console.log(` ${override.name} ${chalk.gray(override.spec)} ${where} ${chalk.green(dryRun ? "would be removed" : "removed")}`);
21098
+ else {
21099
+ console.log(` ${chalk.yellow("⚠")} ${override.name} → ${override.spec} ${where}`);
21100
+ console.log(chalk.yellow(" local override — wins over every pin, the upgrade will not reach these packages"));
21101
+ }
21102
+ }
21103
+ if (plan.overrides.some((o) => o.action === "kept-local")) console.log(chalk.gray(" Run again with --drop-local-overrides to remove them."));
21104
+ }
21105
+ if (plan.unreadable.length > 0) {
21106
+ console.log("");
21107
+ console.log(` ${chalk.bold("Not read")}`);
21108
+ for (const entry of plan.unreadable) console.log(` ${entry.file} ${chalk.gray(entry.reason)}`);
21109
+ }
21110
+ console.log("");
21111
+ }
21112
+ function printOutcome(plan, outcome) {
21113
+ const moved = plan.changed.length + plan.overrides.filter((o) => o.action !== "kept-local").length;
21114
+ if (outcome.dryRun) {
21115
+ 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
+ console.log("");
21117
+ return;
21118
+ }
21119
+ if (!outcome.wrote) {
21120
+ console.log(chalk.green(`✓ This project is already on ${plan.target}.`));
21121
+ console.log("");
21122
+ return;
21123
+ }
21124
+ const files = plan.writes.length;
21125
+ console.log(chalk.green(`✓ Moved ${moved} entr${moved === 1 ? "y" : "ies"} in ${files} file${files === 1 ? "" : "s"} to ${plan.target}${outcome.installed ? " and installed" : ""}.`));
21126
+ if (!outcome.installed) console.log(chalk.gray(` Install with \`${outcome.installLine}\`, then run \`rebase build\`, then \`rebase cloud deploy\`.`));
21127
+ else console.log(chalk.gray(" Run `rebase build`, then `rebase cloud deploy`."));
21128
+ console.log("");
21129
+ }
21130
+ //#endregion
19632
21131
  //#region src/cli.ts
19633
21132
  /**
19634
21133
  * Silence dotenv's own banner, for this process and everything it spawns.
@@ -19697,7 +21196,8 @@ async function entry(args) {
19697
21196
  "generate-sdk",
19698
21197
  "telemetry",
19699
21198
  "resources",
19700
- "status"
21199
+ "status",
21200
+ "upgrade"
19701
21201
  ];
19702
21202
  if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
19703
21203
  printHelp();
@@ -19776,6 +21276,9 @@ async function dispatch(command, effectiveSubcommand, args, parsedArgs, namespac
19776
21276
  case "start":
19777
21277
  await startCommand(args);
19778
21278
  break;
21279
+ case "upgrade":
21280
+ await upgradeCommand(args);
21281
+ break;
19779
21282
  case "apps":
19780
21283
  await appsCommand(effectiveSubcommand, args);
19781
21284
  break;
@@ -19823,6 +21326,7 @@ ${chalk.green.bold("Commands")}
19823
21326
  ${chalk.blue.bold("build")} Build the apps declared in rebase.json into a bundle
19824
21327
  ${chalk.blue.bold("normalize-imports")} Complete compiled output's relative imports for Node ESM
19825
21328
  ${chalk.blue.bold("start")} Start the backend server ${chalk.gray("(production)")}
21329
+ ${chalk.blue.bold("upgrade")} Move every @rebasepro package to one release, then install
19826
21330
  ${chalk.blue.bold("apps list")} Show the apps this repository declares
19827
21331
 
19828
21332
  ${chalk.green.bold("Schema")}