@rebasepro/cli 0.12.0 → 0.12.1-canary.g009ed95

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
@@ -355,8 +355,9 @@ function requireProjectRoot() {
355
355
  const root = findProjectRoot();
356
356
  if (!root) {
357
357
  console.error(chalk.red("✗ Could not find a Rebase project root."));
358
- console.error(chalk.gray(" Make sure you are inside a Rebase project directory"));
359
- console.error(chalk.gray(" (one with backend/, frontend/, and config/ directories)."));
358
+ console.error(chalk.gray(` Looked in this directory and every parent for a ${MANIFEST_FILENAME},`));
359
+ console.error(chalk.gray(" a package.json with a \"backend\" workspace, or a backend/ next to a config/."));
360
+ console.error(chalk.gray(" Run this from inside a project, or create one with `rebase init`."));
360
361
  process.exit(1);
361
362
  }
362
363
  return root;
@@ -532,7 +533,7 @@ async function requireClient(rawArgs) {
532
533
  * the ingress and the console read — see saas/backend/src/utils/tenant-domain.ts).
533
534
  *
534
535
  * The CLI cannot know this value: it is per-deployment configuration (production
535
- * serves tenants at `apps.rebase.pro`, a dev control plane at `localhost`). It
536
+ * serves tenants at `rebase.website`, a dev control plane at `localhost`). It
536
537
  * used to be hardcoded to `rebase.pro`, so `cloud projects create` congratulated
537
538
  * the user with a URL that resolves nowhere near their app.
538
539
  *
@@ -602,6 +603,39 @@ function removeLink(cwd = process.cwd()) {
602
603
  }
603
604
  return false;
604
605
  }
606
+ /**
607
+ * Flags that may appear anywhere on a `rebase cloud` line, including *before*
608
+ * the resource group.
609
+ *
610
+ * They have to be declared wherever positionals are resolved, because `arg`'s
611
+ * `permissive: true` does not merely tolerate an undeclared flag — it pushes it
612
+ * into `_` alongside the positionals, and for a flag that takes a value it
613
+ * pushes the value in too. So `cloud --project acme storage create` parsed
614
+ * without this spec yields `_` of `["--project", "acme", "storage", "create"]`,
615
+ * and the group reads as `"acme"`: a real project name, in the group position,
616
+ * dispatching to nothing. Skipping tokens that start with `-` does not save you
617
+ * there — the damage is the orphaned value, which looks exactly like a
618
+ * positional.
619
+ *
620
+ * Only genuinely global flags belong here. Group-specific ones (`--bucket`,
621
+ * `--region`, …) are declared by the handler that owns them and always follow
622
+ * the group, so they cannot shift the group or action.
623
+ *
624
+ * `-p` is `--project` in eighteen places and `--password` in `login`. That
625
+ * ambiguity does not matter to the one caller that reads this spec: it resolves
626
+ * positionals and never looks at a flag's value, so all it needs to know is
627
+ * that `-p` takes one. Anything that wants the value must keep declaring it
628
+ * itself, with the meaning its own command gives it.
629
+ */
630
+ var GLOBAL_CLOUD_FLAGS = {
631
+ "--json": Boolean,
632
+ "--yes": Boolean,
633
+ "--help": Boolean,
634
+ "--project": String,
635
+ "-p": "--project",
636
+ "-y": "--yes",
637
+ "-h": "--help"
638
+ };
605
639
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
606
640
  /**
607
641
  * The raw project reference to operate on: explicit `--project` flag wins,
@@ -707,6 +741,35 @@ function emit(human, json) {
707
741
  if (JSON_MODE) printJson(json);
708
742
  else human();
709
743
  }
744
+ /**
745
+ * Print a warning (+ optional hint) — in every output mode, always to stderr.
746
+ *
747
+ * `emit` is for a command's *result*, and JSON mode legitimately replaces the
748
+ * human rendering of one. A warning is not a result: it says the command is
749
+ * about to do something the caller may not have meant, and that is exactly as
750
+ * true when the output is piped. Gating one on `!isJsonMode()` deleted it
751
+ * precisely where nobody was watching the terminal — a `--source` deploy ejected
752
+ * a live project off the managed runtime and said so only to a TTY that wasn't
753
+ * there.
754
+ *
755
+ * stdout carries the JSON value and nothing else, so warnings go to stderr:
756
+ * a machine parser reading stdout cannot be corrupted by one. Only the
757
+ * *formatting* may depend on the mode — colour and indentation for a terminal,
758
+ * plain ASCII otherwise. Whether a warning is emitted at all may not.
759
+ *
760
+ * Anything a caller might branch on belongs in the JSON payload as well; stderr
761
+ * is for whoever reads the transcript afterwards.
762
+ */
763
+ function warn(message, hint) {
764
+ if (JSON_MODE) {
765
+ process.stderr.write(`warning: ${stripAnsi(message)}\n`);
766
+ if (hint) process.stderr.write(` ${stripAnsi(hint)}\n`);
767
+ return;
768
+ }
769
+ console.error("");
770
+ console.error(chalk.yellow(` ⚠ ${message}`));
771
+ if (hint) console.error(chalk.gray(` ${hint}`));
772
+ }
710
773
  /** Print an error (+ optional hint) and exit non-zero. Never returns. */
711
774
  function fail(message, hint, code) {
712
775
  if (JSON_MODE) {
@@ -1461,9 +1524,10 @@ async function replacePlaceholders(options) {
1461
1524
  versionToUse = stdout.trim();
1462
1525
  } catch {
1463
1526
  try {
1527
+ const tag = cliVersion.includes("canary") ? "canary" : "latest";
1464
1528
  const { stdout } = await execa(viewBin, [
1465
1529
  "view",
1466
- `${pkgName}@${cliVersion.includes("canary") ? "canary" : "latest"}`,
1530
+ `${pkgName}@${tag}`,
1467
1531
  "version"
1468
1532
  ]);
1469
1533
  if (!stdout.trim()) throw new Error("Not found");
@@ -1560,13 +1624,22 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
1560
1624
  const dbPassword = crypto.randomBytes(16).toString("hex");
1561
1625
  const serviceKey = crypto.randomBytes(48).toString("base64");
1562
1626
  let envContent = fs.readFileSync(envPath, "utf-8");
1627
+ envContent = envContent.replace(/^(# ║ {2})(Copy this file to \.env and fill in the values)( +║)$/m, (_match, open, old, close) => {
1628
+ const replacement = "Generated by `rebase init` — the secrets below are already set";
1629
+ const width = old.length + close.length - 1;
1630
+ return open + replacement.slice(0, width).padEnd(width, " ") + "║";
1631
+ });
1563
1632
  envContent = envContent.replace(/^JWT_SECRET=.*$/m, `JWT_SECRET=${jwtSecret}`);
1564
1633
  envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
1634
+ const composeApiPort = /^PORT=(\d+)/m.exec(envContent)?.[1] ?? "3001";
1635
+ envContent = envContent.replace(/^#\s*CORS_ORIGINS=.*$/m, `CORS_ORIGINS=http://localhost:${composeApiPort}`);
1565
1636
  const runtimeVersion = readCliVersion();
1566
1637
  envContent = /^#?\s*REBASE_VERSION=.*$/m.test(envContent) ? envContent.replace(/^#?\s*REBASE_VERSION=.*$/m, `REBASE_VERSION=${runtimeVersion}`) : `${envContent.trimEnd()}\n\n# The Rebase runtime image tag docker-compose.yml pulls.\n# Change this and restart to upgrade; your project bundle is untouched.\nREBASE_VERSION=${runtimeVersion}\n`;
1567
1638
  if (databaseUrl) {
1568
1639
  if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
1569
- envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${databaseUrl}\nDATABASE_PASSWORD=${dbPassword}`);
1640
+ const { pinSearchPath } = await import("@rebasepro/server-postgres");
1641
+ const pinnedUrl = pinSearchPath(databaseUrl);
1642
+ envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
1570
1643
  } else {
1571
1644
  const dbPort = await findAvailablePort(5432);
1572
1645
  envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public&sslmode=disable\nDATABASE_PASSWORD=${dbPassword}`);
@@ -2084,7 +2157,7 @@ var REMOVED_APP_TYPES = {
2084
2157
  mobile: "mobile apps are no longer declared in the manifest — nothing consumed this type. Remove the entry"
2085
2158
  };
2086
2159
  /** Reserved because they name things in URLs and CLI output. */
2087
- var RESERVED_APP_NAMES = new Set([
2160
+ var RESERVED_APP_NAMES = /* @__PURE__ */ new Set([
2088
2161
  "api",
2089
2162
  "health",
2090
2163
  "metrics",
@@ -3081,7 +3154,7 @@ ${chalk.green.bold("Description")}
3081
3154
  */
3082
3155
  var DEFAULT_BUNDLE_DIR = "dist-bundle";
3083
3156
  /** Packages whose presence means the bundle cannot run on a stock runtime image. */
3084
- var KNOWN_NATIVE_PACKAGES = new Set([
3157
+ var KNOWN_NATIVE_PACKAGES = /* @__PURE__ */ new Set([
3085
3158
  "sharp",
3086
3159
  "canvas",
3087
3160
  "bcrypt",
@@ -3097,7 +3170,7 @@ var KNOWN_NATIVE_PACKAGES = new Set([
3097
3170
  "pg-native"
3098
3171
  ]);
3099
3172
  /** Dependencies supplied by the runtime image itself, not by the bundle. */
3100
- var RUNTIME_PROVIDED = new Set([
3173
+ var RUNTIME_PROVIDED = /* @__PURE__ */ new Set([
3101
3174
  "@rebasepro/server",
3102
3175
  "@rebasepro/types",
3103
3176
  "@rebasepro/client",
@@ -3506,6 +3579,139 @@ function collectDeclaredDependencies(projectRoot) {
3506
3579
  return declared;
3507
3580
  }
3508
3581
  /**
3582
+ * Lowest version a range could resolve to, or null if it is not a range.
3583
+ *
3584
+ * Kept deliberately tiny and local. The published range grammar here is a caret,
3585
+ * a tilde, an exact version or a `>=` floor, and the alternative — a semver
3586
+ * dependency in the CLI — buys breadth this does not need.
3587
+ */
3588
+ function lowerBoundOf(range) {
3589
+ const raw = range.trim().replace(/^[\^~]/, "").replace(/^>=\s*/, "").replace(/^v/, "");
3590
+ if (!/^\d+(\.\d+){0,2}$/.test(raw)) return null;
3591
+ const [major, minor = 0, patch = 0] = raw.split(".").map(Number);
3592
+ return [
3593
+ major,
3594
+ minor,
3595
+ patch
3596
+ ];
3597
+ }
3598
+ /** Highest version a range could resolve to (exclusive), or null. */
3599
+ function upperBoundOf(range) {
3600
+ const trimmed = range.trim();
3601
+ const min = lowerBoundOf(trimmed);
3602
+ if (!min) return null;
3603
+ if (trimmed.startsWith(">=")) return null;
3604
+ const [major, minor, patch] = min;
3605
+ if (trimmed.startsWith("^")) {
3606
+ if (major > 0) return [
3607
+ major + 1,
3608
+ 0,
3609
+ 0
3610
+ ];
3611
+ if (minor > 0) return [
3612
+ 0,
3613
+ minor + 1,
3614
+ 0
3615
+ ];
3616
+ return [
3617
+ 0,
3618
+ 0,
3619
+ patch + 1
3620
+ ];
3621
+ }
3622
+ if (trimmed.startsWith("~")) return trimmed.replace(/^~v?/, "").split(".").length >= 2 ? [
3623
+ major,
3624
+ minor + 1,
3625
+ 0
3626
+ ] : [
3627
+ major + 1,
3628
+ 0,
3629
+ 0
3630
+ ];
3631
+ const parts = trimmed.replace(/^v/, "").split(".").length;
3632
+ if (parts === 1) return [
3633
+ major + 1,
3634
+ 0,
3635
+ 0
3636
+ ];
3637
+ if (parts === 2) return [
3638
+ major,
3639
+ minor + 1,
3640
+ 0
3641
+ ];
3642
+ return [
3643
+ major,
3644
+ minor,
3645
+ patch + 1
3646
+ ];
3647
+ }
3648
+ function compareTriples(a, b) {
3649
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
3650
+ return 0;
3651
+ }
3652
+ /**
3653
+ * Whether a declared range could EVER resolve at or above `target`.
3654
+ *
3655
+ * The same question the control plane asks at intake, asked here first. Only a
3656
+ * range whose entire span sits below the target is reported — `^0.10.0` can
3657
+ * never cross to 0.12 whatever npm publishes — because a false alarm on a build
3658
+ * that would have worked trains people to ignore the warning that matters.
3659
+ */
3660
+ function canReach(range, target) {
3661
+ const ceiling = upperBoundOf(range);
3662
+ const floor = lowerBoundOf(target);
3663
+ if (!floor || !lowerBoundOf(range)) return null;
3664
+ if (!ceiling) return true;
3665
+ return compareTriples(ceiling, floor) > 0;
3666
+ }
3667
+ /**
3668
+ * Find `@rebasepro/*` dependencies pinned to a version older than this CLI.
3669
+ *
3670
+ * This is the only place a developer can be told. In development, every
3671
+ * `@rebasepro/*` resolves through pnpm's `link:`/`workspace:` overrides to the
3672
+ * checkout, so the version STRINGS in package.json are never exercised — the
3673
+ * project runs fine locally on whatever is on disk, and the declared numbers are
3674
+ * first honoured when the runtime npm-installs them from a bundle in the cloud.
3675
+ * A project scaffolded at 0.10.0 therefore keeps working on a developer's
3676
+ * machine indefinitely while being, in the cloud, a 0.10.0 driver.
3677
+ *
3678
+ * That matters because the image supplies only `@rebasepro/server`; the database
3679
+ * driver comes from these declarations and a newer runtime never updates it.
3680
+ * Every package.json is scanned, `dependencies` and `devDependencies` both,
3681
+ * because they have to be bumped together and the one that gets forgotten is the
3682
+ * one nobody looks at.
3683
+ */
3684
+ function detectFrameworkDepDrift(projectRoot, cliVersion) {
3685
+ const found = [];
3686
+ for (const relative of [
3687
+ "package.json",
3688
+ "backend/package.json",
3689
+ "config/package.json",
3690
+ "frontend/package.json"
3691
+ ]) {
3692
+ const file = path.join(projectRoot, relative);
3693
+ if (!fs.existsSync(file)) continue;
3694
+ try {
3695
+ const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
3696
+ for (const block of [pkg.dependencies, pkg.devDependencies]) for (const [name, range] of Object.entries(block ?? {})) {
3697
+ if (!name.startsWith("@rebasepro/")) continue;
3698
+ if (typeof range !== "string") continue;
3699
+ found.push({
3700
+ name,
3701
+ range,
3702
+ file: relative
3703
+ });
3704
+ }
3705
+ } catch {}
3706
+ }
3707
+ const behind = found.filter((d) => canReach(d.range, cliVersion) === false);
3708
+ const bounds = new Set(found.map((d) => lowerBoundOf(d.range)).filter((b) => b != null).map((b) => b.join(".")));
3709
+ return {
3710
+ behind,
3711
+ disagreeing: bounds.size > 1 ? [...bounds].sort() : []
3712
+ };
3713
+ }
3714
+ /**
3509
3715
  * Rewrite relative import specifiers in emitted JavaScript so Node can resolve them.
3510
3716
  *
3511
3717
  * TypeScript deliberately does not touch specifiers: `moduleResolution: "bundler"`
@@ -4203,6 +4409,16 @@ async function buildCommand(rawArgs = []) {
4203
4409
  console.log(chalk.yellow(` ⚠ native dependencies detected: ${names}`));
4204
4410
  console.log(chalk.dim(" These cannot run on the managed runtime. See `rebase doctor`."));
4205
4411
  }
4412
+ const drift = detectFrameworkDepDrift(projectRoot, resolveCliVersion());
4413
+ if (drift.behind.length > 0) {
4414
+ console.log(chalk.yellow(` ⚠ framework dependencies older than this CLI (${resolveCliVersion()}):`));
4415
+ for (const dep of drift.behind) console.log(chalk.dim(` ${dep.name}@${dep.range} (${dep.file})`));
4416
+ console.log(chalk.dim(" The image supplies the server, but your bundle supplies the database"));
4417
+ console.log(chalk.dim(" driver — a newer runtime does not update it. Bump these and rebuild."));
4418
+ } else if (drift.disagreeing.length > 0) {
4419
+ console.log(chalk.yellow(` ⚠ mixed @rebasepro versions declared: ${drift.disagreeing.join(", ")}`));
4420
+ console.log(chalk.dim(" These are published together and expect to run together; pin them alike."));
4421
+ }
4206
4422
  if (!args["--no-static"]) {
4207
4423
  const folded = await foldFrontendIntoBundle({
4208
4424
  projectRoot,
@@ -5061,7 +5277,7 @@ function parseAgentFlags(rawArgs) {
5061
5277
  return requested;
5062
5278
  }
5063
5279
  async function skillsInstall(rawArgs = []) {
5064
- const projectDir = process.cwd();
5280
+ const projectDir = findProjectRoot() ?? process.cwd();
5065
5281
  let skillsDir;
5066
5282
  try {
5067
5283
  skillsDir = getSkillsSourceDir();
@@ -5105,7 +5321,8 @@ async function skillsInstall(rawArgs = []) {
5105
5321
  for (const agentKey of agents) {
5106
5322
  const agent = AGENTS[agentKey];
5107
5323
  const count = installForAgent(agentKey, skills, projectDir);
5108
- console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} ${count} skills installed to ${chalk.gray(agent.targetDir)}`);
5324
+ const shown = path.relative(process.cwd(), path.join(projectDir, agent.targetDir)) || agent.targetDir;
5325
+ console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} — ${count} skills installed to ${chalk.gray(shown)}`);
5109
5326
  }
5110
5327
  console.log("");
5111
5328
  console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
@@ -6085,12 +6302,24 @@ function resolveFrameworkVersion(sourceDir) {
6085
6302
  dir = parent;
6086
6303
  }
6087
6304
  }
6305
+ /**
6306
+ * A progress line for a human — dropped entirely in JSON mode.
6307
+ *
6308
+ * Progress is not a result. In JSON mode stdout carries the one result value
6309
+ * and nothing else, so every unguarded `console.log` on a deploy path was a
6310
+ * line printed in front of the JSON, breaking the parser meant to read it.
6311
+ * Warnings are the other half of this rule and go the other way: they are
6312
+ * `warn`, which prints in every mode, to stderr. See `warn` in `context.ts`.
6313
+ */
6314
+ function progress(line) {
6315
+ if (!isJsonMode()) console.log(line);
6316
+ }
6088
6317
  /** Upload a build-context tarball; returns the opaque `source` ref for deploy. */
6089
6318
  async function uploadSource(url, token, projectId, tarPath) {
6090
6319
  const bytes = fs.readFileSync(tarPath);
6091
6320
  const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
6092
6321
  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.");
6093
- console.log(chalk.gray(` Uploading source (${sizeMb} MB)...`));
6322
+ progress(chalk.gray(` Uploading source (${sizeMb} MB)...`));
6094
6323
  const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
6095
6324
  method: "POST",
6096
6325
  headers: {
@@ -6122,7 +6351,7 @@ async function deployBundle(opts) {
6122
6351
  const loaded = loadManifest(projectRoot);
6123
6352
  const backend = findBackendApp(loaded.manifest);
6124
6353
  if (!backend) fail("This repository declares no backend app to deploy as a bundle.", "A managed deploy runs the backend; declare one in rebase.json, or deploy from the backend's repository.");
6125
- console.log(chalk.gray(" Building bundle..."));
6354
+ progress(chalk.gray(" Building bundle..."));
6126
6355
  bundleDir = (await buildBundle({
6127
6356
  projectRoot,
6128
6357
  appName: backend.name,
@@ -6130,16 +6359,16 @@ async function deployBundle(opts) {
6130
6359
  runtimeRange: loaded.manifest.rebase,
6131
6360
  storage: loaded.manifest.storage,
6132
6361
  skipTypeCheck: opts.skipTypeCheck,
6133
- log: (m) => console.log(chalk.gray(m))
6362
+ log: (m) => progress(chalk.gray(m))
6134
6363
  })).outDir;
6135
6364
  try {
6136
6365
  const folded = await foldFrontendIntoBundle({
6137
6366
  projectRoot,
6138
6367
  manifest: loaded.manifest,
6139
6368
  bundleDir,
6140
- log: (m) => console.log(m)
6369
+ log: (m) => progress(m)
6141
6370
  });
6142
- for (const outcome of folded) console.log(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
6371
+ for (const outcome of folded) progress(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
6143
6372
  } catch (err) {
6144
6373
  fail(err instanceof Error ? err.message : String(err), "Fix the frontend build, or pass --no-static to deploy the API alone.");
6145
6374
  }
@@ -6156,7 +6385,7 @@ async function deployBundle(opts) {
6156
6385
  try {
6157
6386
  await packBundle(bundleDir, tarPath);
6158
6387
  const sizeMb = (fs.statSync(tarPath).size / 1024 / 1024).toFixed(1);
6159
- console.log(chalk.gray(` Uploading bundle (${sizeMb} MB)...`));
6388
+ progress(chalk.gray(` Uploading bundle (${sizeMb} MB)...`));
6160
6389
  bundleId = await uploadBundle(url, token, projectId, tarPath);
6161
6390
  } catch (e) {
6162
6391
  fail(e instanceof Error ? e.message : String(e));
@@ -6164,8 +6393,10 @@ async function deployBundle(opts) {
6164
6393
  } finally {
6165
6394
  fs.rmSync(tarPath, { force: true });
6166
6395
  }
6167
- console.log("");
6168
- console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
6396
+ if (!isJsonMode()) {
6397
+ console.log("");
6398
+ console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
6399
+ }
6169
6400
  let declaredApps = [];
6170
6401
  try {
6171
6402
  declaredApps = declaredAppsFrom(loadManifest(process.cwd()).manifest);
@@ -6253,9 +6484,75 @@ function planBareDeploy(project, latest, now) {
6253
6484
  lines: ["This project has no git repository configured and no stored source archive to rebuild.", "Upload this directory with `--source .`, or set a repository URL in the project settings."]
6254
6485
  };
6255
6486
  }
6487
+ /** `code` of the warning below, and the field name it sets in the payload. */
6488
+ var EJECTS_MANAGED_RUNTIME = "ejects_managed_runtime";
6256
6489
  /** The one sentence that says a source build undoes `runtimeMode: managed`. */
6257
6490
  function ejectWarning(projectRef) {
6258
- return `⚠ ${projectRef} runs on the managed runtime — a source build ejects it to a custom container.`;
6491
+ return {
6492
+ code: EJECTS_MANAGED_RUNTIME,
6493
+ message: `${projectRef} runs on the managed runtime — this build ejects it to a custom container.`,
6494
+ hint: "Use `rebase cloud deploy --bundle` to stay on managed."
6495
+ };
6496
+ }
6497
+ /**
6498
+ * Why a container-image deploy of a managed project is refused — or `undefined`
6499
+ * to let it through.
6500
+ *
6501
+ * Every path below this point builds a container image, and a successful one
6502
+ * sets `runtimeMode: "custom"` server-side. So the question is never "which flag
6503
+ * was used" but "did the caller ask to leave the managed runtime", and only
6504
+ * `--force` answers it.
6505
+ *
6506
+ * `--source` used to be read as answering it too, on the theory that uploading a
6507
+ * build context is self-evidently a deliberate eject. It is not: `--source`
6508
+ * picks *which source* gets built — this directory, rather than the stale
6509
+ * archive the control plane is holding — and the eject is a side effect of the
6510
+ * answer. That is exactly how a live project got flipped to `custom` by someone
6511
+ * whose actual intent was "deploy what I have here", and it is the same
6512
+ * ignorance the bare form is refused for. Same ignorance, same refusal.
6513
+ */
6514
+ function ejectRefusal(opts, projectRef) {
6515
+ if (!opts.managed || opts.force) return void 0;
6516
+ const eject = "To eject on purpose, add `--force`.";
6517
+ if (opts.source) return {
6518
+ message: `${projectRef} runs on the managed runtime, and \`--source\` builds a container image from this directory — which ejects it from managed. Picking a build method is not the same as asking to leave the runtime.`,
6519
+ hint: `Deploy this directory to the managed runtime with \`rebase cloud deploy --bundle\`. ${eject}`,
6520
+ code: "managed_project"
6521
+ };
6522
+ return {
6523
+ message: `${projectRef} runs on the managed runtime, and a plain \`rebase cloud deploy\` builds a container image instead — ejecting it from managed, from source the control plane already holds rather than this directory.`,
6524
+ hint: `Redeploy it with \`rebase cloud deploy --bundle\`. ${eject} \`--source . --force\` builds this directory; \`--force\` alone builds what the control plane holds.`,
6525
+ code: "managed_project"
6526
+ };
6527
+ }
6528
+ /**
6529
+ * Which warnings a container-image deploy has earned.
6530
+ *
6531
+ * Pure, and separate from the printing, because the printing is what went
6532
+ * wrong: the eject warning used to be written inline behind `!isJsonMode()`, so
6533
+ * the fact that a deploy ejects a managed project existed only as a side effect
6534
+ * of a TTY being attached. Deciding here, emitting once at the call site, means
6535
+ * the decision cannot be output-mode-dependent again.
6536
+ *
6537
+ * The condition is just `managed`: anything reaching this point is a container
6538
+ * image build that `ejectRefusal` has already let through, and on a managed
6539
+ * project that is an eject however it was spelled. A caller who passed `--force`
6540
+ * knows — the warning is for the transcript and the payload, which is what
6541
+ * anyone reviewing the deploy afterwards actually reads.
6542
+ */
6543
+ function deployWarnings(opts, projectRef) {
6544
+ return opts.managed ? [ejectWarning(projectRef)] : [];
6545
+ }
6546
+ /** The warning half of a deploy's JSON payload — merged into whatever it emits. */
6547
+ function warningPayload(warnings) {
6548
+ return {
6549
+ warnings: warnings.map((w) => ({
6550
+ code: w.code,
6551
+ message: w.message,
6552
+ hint: w.hint ?? null
6553
+ })),
6554
+ ejectsManagedRuntime: warnings.some((w) => w.code === EJECTS_MANAGED_RUNTIME)
6555
+ };
6259
6556
  }
6260
6557
  /**
6261
6558
  * Read the two rows the preflight needs.
@@ -6326,17 +6623,18 @@ async function deployCommand(rawArgs, projectRef) {
6326
6623
  }
6327
6624
  const { project, latest } = await readDeployContext(client, projectId);
6328
6625
  const plan = planBareDeploy(project, latest, /* @__PURE__ */ new Date());
6329
- if (!args["--source"]) {
6330
- if (plan.managed && args["--force"] !== true) fail(`${projectRef} runs on the managed runtime, and a plain \`rebase cloud deploy\` builds a container image instead — ejecting it from managed, from source the control plane already holds rather than this directory.`, "Redeploy it with `rebase cloud deploy --bundle`. To eject on purpose, pass `--source .` to build this directory, or `--force` to build what the control plane holds.", "managed_project");
6331
- if (!isJsonMode()) {
6332
- console.log("");
6333
- if (plan.managed) console.log(chalk.yellow(` ${ejectWarning(projectRef)}`));
6334
- for (const line of plan.lines) console.log(chalk.gray(` ${line}`));
6335
- }
6336
- } else if (plan.managed && !isJsonMode()) {
6626
+ const eject = {
6627
+ managed: plan.managed,
6628
+ source: Boolean(args["--source"]),
6629
+ force: args["--force"] === true
6630
+ };
6631
+ const refusal = ejectRefusal(eject, projectRef);
6632
+ if (refusal) fail(refusal.message, refusal.hint, refusal.code);
6633
+ const warnings = deployWarnings(eject, projectRef);
6634
+ for (const w of warnings) warn(w.message, w.hint);
6635
+ if (!args["--source"] && !isJsonMode()) {
6337
6636
  console.log("");
6338
- console.log(chalk.yellow(` ${ejectWarning(projectRef)}`));
6339
- console.log(chalk.gray(" Use `rebase cloud deploy --bundle` to stay on managed."));
6637
+ for (const line of plan.lines) console.log(chalk.gray(` ${line}`));
6340
6638
  }
6341
6639
  let source;
6342
6640
  if (args["--source"]) {
@@ -6349,8 +6647,10 @@ async function deployCommand(rawArgs, projectRef) {
6349
6647
  fs.rmSync(tarPath, { force: true });
6350
6648
  }
6351
6649
  }
6352
- console.log("");
6353
- console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectRef)}${source ? " from uploaded source" : ""}...`);
6650
+ if (!isJsonMode()) {
6651
+ console.log("");
6652
+ console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectRef)}${source ? " from uploaded source" : ""}...`);
6653
+ }
6354
6654
  const body = { projectId };
6355
6655
  if (source) body.source = source;
6356
6656
  if (args["--message"]) body.message = args["--message"];
@@ -6378,7 +6678,8 @@ async function deployCommand(rawArgs, projectRef) {
6378
6678
  deploymentId,
6379
6679
  deduplicated,
6380
6680
  frameworkVersion: frameworkVersion ?? null,
6381
- following: false
6681
+ following: false,
6682
+ ...warningPayload(warnings)
6382
6683
  });
6383
6684
  return;
6384
6685
  }
@@ -6392,7 +6693,8 @@ async function deployCommand(rawArgs, projectRef) {
6392
6693
  deduplicated,
6393
6694
  frameworkVersion: frameworkVersion ?? null,
6394
6695
  following: true,
6395
- status
6696
+ status,
6697
+ ...warningPayload(warnings)
6396
6698
  });
6397
6699
  }
6398
6700
  /**
@@ -9006,6 +9308,7 @@ function describeDatabaseState(db) {
9006
9308
  * So both are printed, rather than leaving anyone to infer one from a Docker tag.
9007
9309
  */
9008
9310
  function describeRuntime(project) {
9311
+ if (project.runtimeMode == null || project.runtimeMode.trim() === "") return `not deployed yet ${chalk.gray("· the first deploy decides (`--bundle` keeps it managed)")}`;
9009
9312
  if (project.runtimeMode !== "managed") return `custom ${chalk.gray("· your own image")}`;
9010
9313
  const version = project.runtimeVersion ?? "unknown";
9011
9314
  const framework = project.runtimeFrameworkVersion;
@@ -9404,17 +9707,41 @@ async function billingCommand(rawArgs) {
9404
9707
  * dispatched from here. Individual groups live in sibling modules; this file
9405
9708
  * only routes and prints help.
9406
9709
  */
9407
- /** Positional tokens after `rebase cloud` (group, action, …). */
9710
+ /**
9711
+ * Positional tokens after `rebase cloud` (group, action, …).
9712
+ *
9713
+ * Two things stop a flag being mistaken for the group. `GLOBAL_CLOUD_FLAGS` is
9714
+ * declared so `arg` *consumes* the flags that may precede it — critically
9715
+ * together with their values, which is the half that filtering cannot do. The
9716
+ * leading-`-` skip then covers a flag nobody declared, so an unrecognised
9717
+ * boolean shifts nothing.
9718
+ *
9719
+ * Only leading tokens are skipped: past the group and action, an undeclared
9720
+ * flag and its value are somebody else's positionals and none of our business.
9721
+ * A flag this file has never heard of, that takes a value, placed before the
9722
+ * group, is the one shape still unresolvable here — there is no way to know
9723
+ * whether the token after it is its value or the group, and guessing either way
9724
+ * is worse than the handler reporting an unknown group.
9725
+ *
9726
+ * Exported so its tests can drive the real thing. The dispatch test used to
9727
+ * re-implement it locally as `slice(3).filter(a => !a.startsWith("-"))` — which
9728
+ * filtered flags, while this function did not — so the test asserted the
9729
+ * behaviour we wanted against a copy that had it, and stayed green for as long
9730
+ * as the real dispatcher was broken.
9731
+ */
9408
9732
  function positionals(rawArgs) {
9409
- return arg({}, {
9733
+ const rest = arg(GLOBAL_CLOUD_FLAGS, {
9410
9734
  argv: rawArgs.slice(3),
9411
9735
  permissive: true
9412
9736
  })._;
9737
+ let i = 0;
9738
+ while (i < rest.length && rest[i].startsWith("-")) i++;
9739
+ return rest.slice(i);
9413
9740
  }
9414
9741
  async function cloudCommand(subcommand, rawArgs) {
9415
9742
  initOutputMode(rawArgs);
9416
9743
  const pos = positionals(rawArgs);
9417
- const group = subcommand && subcommand !== "--help" ? subcommand : pos[0];
9744
+ const group = pos[0] ?? (subcommand !== "--help" ? subcommand : void 0);
9418
9745
  const action = pos[1];
9419
9746
  if (!group || subcommand === "--help") {
9420
9747
  printCloudHelp();
@@ -9836,8 +10163,9 @@ async function entry(args) {
9836
10163
  console.log(getVersion());
9837
10164
  return;
9838
10165
  }
9839
- const command = parsedArgs._[0];
9840
- const subcommand = parsedArgs._[1];
10166
+ const words = parsedArgs._.filter((a) => !a.startsWith("-"));
10167
+ const command = words[0];
10168
+ const subcommand = words[1];
9841
10169
  if (!command || parsedArgs["--help"] && ![
9842
10170
  "init",
9843
10171
  "schema",
@@ -9876,9 +10204,10 @@ async function entry(args) {
9876
10204
  argv: args.slice(3),
9877
10205
  permissive: true
9878
10206
  });
10207
+ const sdkRoot = sdkArgs["--help"] ? process.cwd() : requireProjectRoot();
9879
10208
  await generateSdkCommand({
9880
- collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
9881
- output: sdkArgs["--output"] || "./generated/sdk",
10209
+ collectionsDir: sdkArgs["--collections-dir"] || path.join(sdkRoot, "config/collections"),
10210
+ output: sdkArgs["--output"] || path.join(sdkRoot, "generated/sdk"),
9882
10211
  from: sdkArgs["--from"],
9883
10212
  token: sdkArgs["--token"],
9884
10213
  help: sdkArgs["--help"],
@@ -9988,6 +10317,6 @@ ${chalk.gray("Documentation: https://rebase.pro/docs")}
9988
10317
  `);
9989
10318
  }
9990
10319
  //#endregion
9991
- export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, MANIFEST_FILENAME, ManifestError, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
10320
+ export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_PORT_FILENAME, MANIFEST_FILENAME, ManifestError, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectFrameworkDepDrift, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, getProjectPort, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveStartPort, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
9992
10321
 
9993
10322
  //# sourceMappingURL=index.es.js.map