@rebasepro/cli 0.12.1-canary.gf5f1d39 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -12,9 +12,9 @@ import crypto from "crypto";
12
12
  import { execSync, spawn, spawnSync } from "child_process";
13
13
  import os from "os";
14
14
  import { createRebaseClient } from "@rebasepro/client";
15
+ import { createRequire } from "module";
15
16
  import { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
16
17
  import { generateSDK } from "@rebasepro/codegen";
17
- import { createRequire } from "module";
18
18
  //#region src/utils/package-manager.ts
19
19
  /**
20
20
  * Package manager detection and command abstraction.
@@ -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;
@@ -527,33 +528,51 @@ async function requireClient(rawArgs) {
527
528
  };
528
529
  }
529
530
  /**
530
- * The base domain tenant projects are served at, as reported by the control
531
- * plane (`platform-config`, which derives it from the same TENANT_BASE_DOMAIN
532
- * the ingress and the console read — see saas/backend/src/utils/tenant-domain.ts).
531
+ * The control plane's public, non-secret self-description (`platform-config`).
533
532
  *
534
- * 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
- * used to be hardcoded to `rebase.pro`, so `cloud projects create` congratulated
537
- * the user with a URL that resolves nowhere near their app.
533
+ * None of it is knowable from the CLI side: it is per-deployment configuration
534
+ * production serves tenants at `rebase.website` on GKE, a dev control plane at
535
+ * `localhost` on Docker. Guessing produced two separate lies: a congratulation
536
+ * URL that resolved nowhere near the app, and a `provider` the project does not
537
+ * run on (see `createProject`).
538
538
  *
539
539
  * Cached per host for the process: it is fixed for a control plane's lifetime,
540
540
  * and `projects list` formats one host per row off a single fetch.
541
541
  *
542
- * @returns the base domain, or `undefined` if the control plane doesn't serve
542
+ * @returns the config, or `undefined` if the control plane doesn't serve
543
543
  * `platform-config` (an older deployment) or the request failed. A failure is
544
- * cached too — the caller renders a subdomain either way, and a short-lived
545
- * CLI should not retry once per row.
546
- */
547
- var tenantBaseDomainCache = /* @__PURE__ */ new Map();
548
- function fetchTenantBaseDomain(client, url) {
549
- let pending = tenantBaseDomainCache.get(url);
544
+ * cached too — a short-lived CLI should not retry once per row. Note the
545
+ * distinction callers rely on: `undefined` means "this control plane cannot
546
+ * tell us", whereas `deployTargets: []` is a control plane stating that it has
547
+ * no infrastructure configured.
548
+ */
549
+ var platformConfigCache = /* @__PURE__ */ new Map();
550
+ function fetchPlatformConfig(client, url) {
551
+ let pending = platformConfigCache.get(url);
550
552
  if (!pending) {
551
- pending = client.functions.invoke("platform-config", void 0, { method: "GET" }).then((cfg) => cfg?.tenantBaseDomain?.trim() || void 0).catch(() => void 0);
552
- tenantBaseDomainCache.set(url, pending);
553
+ pending = client.functions.invoke("platform-config", void 0, { method: "GET" }).then((cfg) => cfg ?? void 0).catch(() => void 0);
554
+ platformConfigCache.set(url, pending);
553
555
  }
554
556
  return pending;
555
557
  }
556
558
  /**
559
+ * The base domain tenant projects are served at, derived from the same
560
+ * TENANT_BASE_DOMAIN the ingress and the console read (see
561
+ * saas/backend/src/utils/tenant-domain.ts).
562
+ */
563
+ function fetchTenantBaseDomain(client, url) {
564
+ return fetchPlatformConfig(client, url).then((cfg) => cfg?.tenantBaseDomain?.trim() || void 0);
565
+ }
566
+ /**
567
+ * The infrastructure a deploy for this control plane would ACTUALLY use, in the
568
+ * resolver's own preference order (saas/backend/src/k8s/resolve.ts).
569
+ *
570
+ * @returns the targets, or `undefined` when the control plane cannot say.
571
+ */
572
+ function fetchDeployTargets(client, url) {
573
+ return fetchPlatformConfig(client, url).then((cfg) => Array.isArray(cfg?.deployTargets) ? cfg.deployTargets : void 0);
574
+ }
575
+ /**
557
576
  * Public host for a project — `<subdomain>.<base>`, or the bare subdomain when
558
577
  * the base domain is unknown.
559
578
  *
@@ -602,6 +621,39 @@ function removeLink(cwd = process.cwd()) {
602
621
  }
603
622
  return false;
604
623
  }
624
+ /**
625
+ * Flags that may appear anywhere on a `rebase cloud` line, including *before*
626
+ * the resource group.
627
+ *
628
+ * They have to be declared wherever positionals are resolved, because `arg`'s
629
+ * `permissive: true` does not merely tolerate an undeclared flag — it pushes it
630
+ * into `_` alongside the positionals, and for a flag that takes a value it
631
+ * pushes the value in too. So `cloud --project acme storage create` parsed
632
+ * without this spec yields `_` of `["--project", "acme", "storage", "create"]`,
633
+ * and the group reads as `"acme"`: a real project name, in the group position,
634
+ * dispatching to nothing. Skipping tokens that start with `-` does not save you
635
+ * there — the damage is the orphaned value, which looks exactly like a
636
+ * positional.
637
+ *
638
+ * Only genuinely global flags belong here. Group-specific ones (`--bucket`,
639
+ * `--region`, …) are declared by the handler that owns them and always follow
640
+ * the group, so they cannot shift the group or action.
641
+ *
642
+ * `-p` is `--project` in eighteen places and `--password` in `login`. That
643
+ * ambiguity does not matter to the one caller that reads this spec: it resolves
644
+ * positionals and never looks at a flag's value, so all it needs to know is
645
+ * that `-p` takes one. Anything that wants the value must keep declaring it
646
+ * itself, with the meaning its own command gives it.
647
+ */
648
+ var GLOBAL_CLOUD_FLAGS = {
649
+ "--json": Boolean,
650
+ "--yes": Boolean,
651
+ "--help": Boolean,
652
+ "--project": String,
653
+ "-p": "--project",
654
+ "-y": "--yes",
655
+ "-h": "--help"
656
+ };
605
657
  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
658
  /**
607
659
  * The raw project reference to operate on: explicit `--project` flag wins,
@@ -707,6 +759,35 @@ function emit(human, json) {
707
759
  if (JSON_MODE) printJson(json);
708
760
  else human();
709
761
  }
762
+ /**
763
+ * Print a warning (+ optional hint) — in every output mode, always to stderr.
764
+ *
765
+ * `emit` is for a command's *result*, and JSON mode legitimately replaces the
766
+ * human rendering of one. A warning is not a result: it says the command is
767
+ * about to do something the caller may not have meant, and that is exactly as
768
+ * true when the output is piped. Gating one on `!isJsonMode()` deleted it
769
+ * precisely where nobody was watching the terminal — a `--source` deploy ejected
770
+ * a live project off the managed runtime and said so only to a TTY that wasn't
771
+ * there.
772
+ *
773
+ * stdout carries the JSON value and nothing else, so warnings go to stderr:
774
+ * a machine parser reading stdout cannot be corrupted by one. Only the
775
+ * *formatting* may depend on the mode — colour and indentation for a terminal,
776
+ * plain ASCII otherwise. Whether a warning is emitted at all may not.
777
+ *
778
+ * Anything a caller might branch on belongs in the JSON payload as well; stderr
779
+ * is for whoever reads the transcript afterwards.
780
+ */
781
+ function warn(message, hint) {
782
+ if (JSON_MODE) {
783
+ process.stderr.write(`warning: ${stripAnsi(message)}\n`);
784
+ if (hint) process.stderr.write(` ${stripAnsi(hint)}\n`);
785
+ return;
786
+ }
787
+ console.error("");
788
+ console.error(chalk.yellow(` ⚠ ${message}`));
789
+ if (hint) console.error(chalk.gray(` ${hint}`));
790
+ }
710
791
  /** Print an error (+ optional hint) and exit non-zero. Never returns. */
711
792
  function fail(message, hint, code) {
712
793
  if (JSON_MODE) {
@@ -825,6 +906,369 @@ function openUrl(target, label = "Opening") {
825
906
  child.unref();
826
907
  } catch {}
827
908
  }
909
+ /** Duration in coarse bands — enough to see "slow", not enough to fingerprint. */
910
+ function durationBucket(ms) {
911
+ if (!Number.isFinite(ms) || ms < 0) return "unknown";
912
+ if (ms < 5e3) return "<5s";
913
+ if (ms < 3e4) return "5-30s";
914
+ if (ms < 12e4) return "30-120s";
915
+ return "120s+";
916
+ }
917
+ /**
918
+ * Drop anything that is not a permitted value type, and clamp strings.
919
+ *
920
+ * The last line of defence rather than the first. Every call site is supposed
921
+ * to pass enumerated values; this is what stops a future one that forgets from
922
+ * turning into an incident. Strings are capped at 64 characters because no
923
+ * legitimate enumerated value is longer, and a path or a message always is.
924
+ */
925
+ /**
926
+ * Key names that shadow an `Object.prototype` member.
927
+ *
928
+ * `__proto__` is already excluded by the pattern (it starts with an
929
+ * underscore), and assigning any of these to an object literal shadows rather
930
+ * than pollutes — so nothing is exploitable here. They are refused because a
931
+ * stored property called `constructor` is a trap for every consumer downstream
932
+ * that reaches for `properties.constructor` and gets a string.
933
+ */
934
+ var RESERVED_KEYS = /* @__PURE__ */ new Set([
935
+ "constructor",
936
+ "prototype",
937
+ "hasownproperty",
938
+ "tostring",
939
+ "valueof"
940
+ ]);
941
+ function sanitize(properties) {
942
+ const out = {};
943
+ for (const [key, value] of Object.entries(properties)) {
944
+ if (!/^[a-z][a-z0-9_]{0,31}$/.test(key) || RESERVED_KEYS.has(key.toLowerCase())) continue;
945
+ if (typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) out[key] = value;
946
+ else if (typeof value === "string" && value.length > 0 && value.length <= 64) {
947
+ if (!/[/\\@:\s]/.test(value)) out[key] = value;
948
+ }
949
+ }
950
+ return out;
951
+ }
952
+ function cliVersion() {
953
+ try {
954
+ const pkg = createRequire(import.meta.url)("../../package.json");
955
+ return typeof pkg?.version === "string" ? pkg.version : "unknown";
956
+ } catch {
957
+ return "unknown";
958
+ }
959
+ }
960
+ function buildEvent(event, properties, identity) {
961
+ return {
962
+ schema: 1,
963
+ event,
964
+ machineId: identity.machineId,
965
+ projectId: identity.projectId,
966
+ cliVersion: cliVersion(),
967
+ nodeMajor: Number(process.versions.node.split(".")[0]) || 0,
968
+ platform: os.platform(),
969
+ arch: os.arch(),
970
+ at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
971
+ properties: sanitize(properties)
972
+ };
973
+ }
974
+ function configPath() {
975
+ return path.join(os.homedir(), ".rebase", "telemetry.json");
976
+ }
977
+ function readConfig() {
978
+ try {
979
+ const raw = JSON.parse(fs.readFileSync(configPath(), "utf-8"));
980
+ if (raw && typeof raw === "object") return {
981
+ version: typeof raw.version === "number" ? raw.version : 1,
982
+ enabled: typeof raw.enabled === "boolean" ? raw.enabled : void 0,
983
+ machineId: typeof raw.machineId === "string" ? raw.machineId : void 0,
984
+ decidedAt: typeof raw.decidedAt === "string" ? raw.decidedAt : void 0
985
+ };
986
+ } catch {}
987
+ return { version: 1 };
988
+ }
989
+ function writeConfig(config) {
990
+ const file = configPath();
991
+ fs.mkdirSync(path.dirname(file), { recursive: true });
992
+ fs.writeFileSync(file, JSON.stringify({
993
+ ...config,
994
+ version: 1
995
+ }, null, 2), {
996
+ encoding: "utf-8",
997
+ mode: 384
998
+ });
999
+ }
1000
+ /**
1001
+ * The machine's id, generating and persisting one on first use.
1002
+ *
1003
+ * Only called once consent exists — an id written before the user agreed would
1004
+ * be a record we had no right to create, even unsent.
1005
+ */
1006
+ function ensureMachineId() {
1007
+ const config = readConfig();
1008
+ if (config.machineId) return config.machineId;
1009
+ const machineId = crypto.randomUUID();
1010
+ writeConfig({
1011
+ ...config,
1012
+ machineId
1013
+ });
1014
+ return machineId;
1015
+ }
1016
+ /**
1017
+ * The checkout's id, generating one if this project has none.
1018
+ *
1019
+ * The directory is created when it is missing, but **only** where a
1020
+ * `rebase.json` proves this really is a project root. An earlier version
1021
+ * required `.rebase/` to already exist, on the assumption that `rebase init`
1022
+ * created it — it does not. It is written only when a scaffold is linked to
1023
+ * Rebase Cloud, so every self-hosted project reported no `projectId` at all,
1024
+ * for ever. That silently broke the funnel this id exists for, on exactly the
1025
+ * population the telemetry is meant to learn about.
1026
+ *
1027
+ * The `rebase.json` check is what keeps the fix from being "scatter `.rebase/`
1028
+ * wherever a command happens to run": no manifest, no project, no directory,
1029
+ * and the event simply reports no project.
1030
+ */
1031
+ function ensureProjectId(projectRoot) {
1032
+ if (!fs.existsSync(path.join(projectRoot, "rebase.json"))) return void 0;
1033
+ const dir = path.join(projectRoot, ".rebase");
1034
+ try {
1035
+ fs.mkdirSync(dir, { recursive: true });
1036
+ } catch {
1037
+ return;
1038
+ }
1039
+ const file = path.join(dir, "state.json");
1040
+ let state = {};
1041
+ if (fs.existsSync(file)) try {
1042
+ const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
1043
+ if (raw && typeof raw === "object") state = raw;
1044
+ } catch {
1045
+ return;
1046
+ }
1047
+ if (typeof state.telemetryProjectId === "string") return state.telemetryProjectId;
1048
+ const projectId = crypto.randomUUID();
1049
+ try {
1050
+ fs.writeFileSync(file, JSON.stringify({
1051
+ ...state,
1052
+ telemetryProjectId: projectId
1053
+ }, null, 2), "utf-8");
1054
+ } catch {
1055
+ return;
1056
+ }
1057
+ return projectId;
1058
+ }
1059
+ //#endregion
1060
+ //#region src/telemetry/project.ts
1061
+ function readProjectPolicy(startDir = process.cwd()) {
1062
+ try {
1063
+ const root = findProjectRoot(startDir);
1064
+ if (!root) return "unset";
1065
+ const file = path.join(root, MANIFEST_FILENAME);
1066
+ if (!fs.existsSync(file)) return "unset";
1067
+ const raw = fs.readFileSync(file, "utf-8");
1068
+ const match = /"telemetry"\s*:\s*(true|false)/.exec(raw);
1069
+ if (!match) return "unset";
1070
+ return match[1] === "false" ? "opt_out" : "ignored_opt_in";
1071
+ } catch {
1072
+ return "unset";
1073
+ }
1074
+ }
1075
+ function endpoint() {
1076
+ return process.env.REBASE_TELEMETRY_ENDPOINT?.trim() || "https://app.rebase.pro/api/functions/telemetry";
1077
+ }
1078
+ /**
1079
+ * The one function that decides whether anything leaves the machine.
1080
+ *
1081
+ * Every path is a refusal except the last, which is the point: consent is
1082
+ * opt-in, so the default answer at every branch — never asked, unreadable
1083
+ * config, a set env var, a CI runner — is no.
1084
+ *
1085
+ * `DO_NOT_TRACK` is honoured because it is the cross-tool convention
1086
+ * (consoledonottrack.com); a user who has set it globally has already answered
1087
+ * this question and should not be asked again by us.
1088
+ *
1089
+ * `CI` is refused for a different reason: a build runner is not a person, and
1090
+ * counting one is both useless and misleading. A single pipeline re-running on
1091
+ * every push would otherwise outweigh every real developer in the data.
1092
+ *
1093
+ * A project's own `"telemetry": false` is checked *before* the machine setting,
1094
+ * because it has to beat an individual opt-in to be worth anything — see
1095
+ * project.ts on why the reverse is refused.
1096
+ */
1097
+ function suppressionReason(env = process.env, cwd = process.cwd()) {
1098
+ if (env.DO_NOT_TRACK && env.DO_NOT_TRACK !== "0") return "do_not_track";
1099
+ if (env.REBASE_TELEMETRY_DISABLED && env.REBASE_TELEMETRY_DISABLED !== "0") return "rebase_telemetry_disabled";
1100
+ if (env.CI && env.CI !== "false") return "ci";
1101
+ if (readProjectPolicy(cwd) === "opt_out") return "project_opt_out";
1102
+ const config = readConfig();
1103
+ if (config.enabled === void 0) return "not_asked";
1104
+ if (!config.enabled) return "declined";
1105
+ return null;
1106
+ }
1107
+ function isEnabled(env = process.env, cwd = process.cwd()) {
1108
+ return suppressionReason(env, cwd) === null;
1109
+ }
1110
+ /**
1111
+ * Record the user's answer. `false` is final — nothing prompts again.
1112
+ *
1113
+ * Returns whether the choice could actually be persisted. A read-only or full
1114
+ * home directory makes `writeConfig` throw, and the direction of that failure
1115
+ * matters enormously: someone turning sharing **off** who sees a stack trace,
1116
+ * or worse sees nothing, is left sharing. The caller is expected to say so and
1117
+ * point at `REBASE_TELEMETRY_DISABLED`, which needs no disk.
1118
+ */
1119
+ function setConsent(enabled) {
1120
+ const config = readConfig();
1121
+ try {
1122
+ writeConfig({
1123
+ ...config,
1124
+ enabled,
1125
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
1126
+ machineId: enabled ? config.machineId ?? void 0 : void 0
1127
+ });
1128
+ if (enabled) ensureMachineId();
1129
+ return true;
1130
+ } catch {
1131
+ return false;
1132
+ }
1133
+ }
1134
+ /**
1135
+ * Build the event that *would* be sent, without sending it.
1136
+ *
1137
+ * This is what `rebase telemetry show` prints, and it is deliberately the same
1138
+ * function the sender uses — a preview assembled by separate code is a promise
1139
+ * that drifts. Returns `null` when nothing would be sent, so the command can
1140
+ * say why instead of showing a payload that is never going anywhere.
1141
+ */
1142
+ function previewEvent(event, properties = {}, projectRoot) {
1143
+ const config = readConfig();
1144
+ if (!config.machineId) return null;
1145
+ return buildEvent(event, properties, {
1146
+ machineId: config.machineId,
1147
+ projectId: projectRoot ? ensureProjectId(projectRoot) : void 0
1148
+ });
1149
+ }
1150
+ /**
1151
+ * Send one event, or quietly do nothing.
1152
+ *
1153
+ * Three properties this must hold, in order of importance:
1154
+ *
1155
+ * 1. **It never throws.** Telemetry failing is not a reason for `rebase dev`
1156
+ * to fail. Every error is swallowed.
1157
+ * 2. **It never blocks meaningfully.** A two-second ceiling, after which the
1158
+ * command carries on regardless — a collector having a bad day must not
1159
+ * become the CLI hanging.
1160
+ * 3. **It never sends without consent.** Enforced here rather than at the call
1161
+ * sites, so a new call site cannot get it wrong.
1162
+ */
1163
+ async function recordEvent(event, properties = {}, options = {}) {
1164
+ try {
1165
+ if (!isEnabled(process.env, options.projectRoot ?? process.cwd())) return;
1166
+ const payload = buildEvent(event, properties, {
1167
+ machineId: ensureMachineId(),
1168
+ projectId: options.projectRoot ? ensureProjectId(options.projectRoot) : void 0
1169
+ });
1170
+ const abort = new AbortController();
1171
+ const timer = setTimeout(() => abort.abort(), options.timeoutMs ?? 2e3);
1172
+ try {
1173
+ await fetch(endpoint(), {
1174
+ method: "POST",
1175
+ headers: { "Content-Type": "application/json" },
1176
+ body: JSON.stringify(payload),
1177
+ signal: abort.signal
1178
+ });
1179
+ } finally {
1180
+ clearTimeout(timer);
1181
+ }
1182
+ } catch {}
1183
+ }
1184
+ //#endregion
1185
+ //#region src/telemetry/consent.ts
1186
+ /**
1187
+ * Asking, and what the question looks like.
1188
+ *
1189
+ * ## Why the prompt comes *after* the work
1190
+ *
1191
+ * The first `rebase init` on a machine is the event most worth having, and it
1192
+ * is the one where no consent exists yet. Asking before scaffolding puts a
1193
+ * privacy negotiation in front of someone who has not yet seen the tool do
1194
+ * anything — the worst possible moment, and a reliable way to get a reflexive
1195
+ * no.
1196
+ *
1197
+ * So the question is asked once the project exists and the user has seen it
1198
+ * work. The event's data is still in memory at that point, so nothing is lost
1199
+ * by waiting, and — this is the part that matters — **nothing has been
1200
+ * transmitted or written**. Declining leaves no id, no file, no record.
1201
+ *
1202
+ * ## Why the payload is shown rather than described
1203
+ *
1204
+ * "Anonymous usage data" is a phrase that has been used to mean almost
1205
+ * anything. Printing the exact JSON costs four lines of output and replaces a
1206
+ * claim the user has to take on faith with something they can read. It is also
1207
+ * the same builder the sender uses, so it cannot drift into a comfortable
1208
+ * fiction.
1209
+ */
1210
+ /** True when we may ask: no decision recorded, and nothing else forbids it. */
1211
+ function shouldPrompt(env = process.env) {
1212
+ return suppressionReason(env) === "not_asked" && Boolean(process.stdin.isTTY);
1213
+ }
1214
+ function renderPreview(event, properties) {
1215
+ const preview = buildEvent(event, properties, {
1216
+ machineId: "<random uuid, generated only if you say yes>",
1217
+ projectId: "<random uuid, per checkout>"
1218
+ });
1219
+ return JSON.stringify(preview, null, 2);
1220
+ }
1221
+ /**
1222
+ * Ask, record the answer, and report it.
1223
+ *
1224
+ * Never throws and never blocks a non-interactive run: `rebase init --yes` in
1225
+ * CI must behave exactly as it does today, which means not asking and not
1226
+ * sending.
1227
+ */
1228
+ async function promptForConsent(event, properties) {
1229
+ if (!shouldPrompt()) return false;
1230
+ try {
1231
+ console.log("");
1232
+ console.log(chalk.bold("Help improve Rebase?"));
1233
+ console.log("");
1234
+ console.log(chalk.gray(" Rebase is self-hosted, so we have no idea what works and what does not"));
1235
+ console.log(chalk.gray(" unless you tell us. Sharing is entirely optional and off by default."));
1236
+ console.log("");
1237
+ console.log(chalk.gray(" This is exactly what would be sent — nothing more, ever:"));
1238
+ console.log("");
1239
+ console.log(renderPreview(event, properties).split("\n").map((line) => chalk.gray(" " + line)).join("\n"));
1240
+ console.log("");
1241
+ console.log(chalk.gray(" No project names, paths, schemas, URLs or error messages. Change your"));
1242
+ console.log(chalk.gray(` mind any time with ${chalk.cyan("rebase telemetry disable")}.`));
1243
+ console.log("");
1244
+ const { accepted } = await inquirer.prompt([{
1245
+ type: "confirm",
1246
+ name: "accepted",
1247
+ message: "Share anonymous usage data?",
1248
+ default: false
1249
+ }]);
1250
+ setConsent(Boolean(accepted));
1251
+ console.log(accepted ? chalk.green(" Thank you — sharing enabled.") : chalk.gray(" Nothing will be sent. You will not be asked again."));
1252
+ console.log("");
1253
+ return Boolean(accepted);
1254
+ } catch {
1255
+ return false;
1256
+ }
1257
+ }
1258
+ /** Human-readable current state, for `rebase telemetry status`. */
1259
+ function describeState(env = process.env) {
1260
+ const reason = suppressionReason(env);
1261
+ const config = readConfig();
1262
+ switch (reason) {
1263
+ case null: return `${chalk.green("enabled")} — schema v1, machine id ${chalk.gray(config.machineId ?? "unset")}`;
1264
+ case "not_asked": return `${chalk.yellow("not configured")} — nothing has been sent, and you have not been asked yet`;
1265
+ case "declined": return `${chalk.gray("disabled")} — you declined${config.decidedAt ? ` on ${config.decidedAt.slice(0, 10)}` : ""}`;
1266
+ case "do_not_track": return `${chalk.gray("disabled")} — the ${chalk.cyan("DO_NOT_TRACK")} environment variable is set`;
1267
+ case "rebase_telemetry_disabled": return `${chalk.gray("disabled")} — the ${chalk.cyan("REBASE_TELEMETRY_DISABLED")} environment variable is set`;
1268
+ case "ci": return `${chalk.gray("disabled")} — this looks like CI (${chalk.cyan("CI")} is set), which is never counted`;
1269
+ case "project_opt_out": return `${chalk.gray("disabled")} — this project's ${chalk.cyan("rebase.json")} sets ${chalk.cyan("\"telemetry\": false")}`;
1270
+ }
1271
+ }
828
1272
  //#endregion
829
1273
  //#region src/commands/init.ts
830
1274
  var access = promisify(fs.access);
@@ -1163,6 +1607,7 @@ async function linkScaffoldToCloud(options) {
1163
1607
  }
1164
1608
  }
1165
1609
  async function createProject$1(options) {
1610
+ const startedAt = Date.now();
1166
1611
  if (fs.existsSync(options.targetDirectory)) {
1167
1612
  if (fs.readdirSync(options.targetDirectory).length !== 0) {
1168
1613
  console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
@@ -1355,6 +1800,18 @@ async function createProject$1(options) {
1355
1800
  console.log("");
1356
1801
  console.log(` ${chalk.cyan("rebase skills install")} ${chalk.gray("or")} ${chalk.cyan(pmCommands.run("skills:install").join(" "))}`);
1357
1802
  console.log("");
1803
+ const initProperties = {
1804
+ preset: options.headless ? "none" : options.preset,
1805
+ headless: Boolean(options.headless),
1806
+ package_manager: options.pm,
1807
+ installed_deps: Boolean(options.installDeps),
1808
+ introspected,
1809
+ own_database: Boolean(options.databaseUrl),
1810
+ cloud_linked: Boolean(options.cloudProject),
1811
+ git: Boolean(options.git),
1812
+ duration: durationBucket(Date.now() - startedAt)
1813
+ };
1814
+ if (await promptForConsent("cli.init", initProperties)) await recordEvent("cli.init", initProperties, { projectRoot: options.targetDirectory });
1358
1815
  }
1359
1816
  /**
1360
1817
  * Apply a template preset by replacing the default collection files.
@@ -1461,9 +1918,10 @@ async function replacePlaceholders(options) {
1461
1918
  versionToUse = stdout.trim();
1462
1919
  } catch {
1463
1920
  try {
1921
+ const tag = cliVersion.includes("canary") ? "canary" : "latest";
1464
1922
  const { stdout } = await execa(viewBin, [
1465
1923
  "view",
1466
- `${pkgName}@${cliVersion.includes("canary") ? "canary" : "latest"}`,
1924
+ `${pkgName}@${tag}`,
1467
1925
  "version"
1468
1926
  ]);
1469
1927
  if (!stdout.trim()) throw new Error("Not found");
@@ -1501,7 +1959,11 @@ async function replacePlaceholders(options) {
1501
1959
  const prereleasePins = [...unreleased].filter(([, version]) => version === "latest" || version.includes("-"));
1502
1960
  if (cliIsStable && prereleasePins.length > 0) {
1503
1961
  const lines = prereleasePins.map(([name, version]) => ` ${name} → ${version}`).join("\n");
1504
- throw new Error(`Rebase ${cliVersion} is not fully published to npm.\n\nThese packages have no ${cliVersion} release, so the newest thing on the\nregistry is a prerelease:\n\n${lines}\n\nScaffolding would pin those alongside the ${cliVersion} packages and produce\nan app that cannot install or run. That is a release gap in Rebase itself —\nnot a problem with your machine, your network, or your package manager.\n\nStopped before writing dependency versions or installing anything. The\nproject directory ${path.basename(options.targetDirectory)}/ was created and is safe to delete.\nPlease report this with the list above.`);
1962
+ throw new Error(`Rebase ${cliVersion} is not fully published to npm.\n\nThese packages have no ${cliVersion} release, so the newest thing on the\nregistry is a prerelease:\n\n${lines}\n\nScaffolding would pin those alongside the ${cliVersion} packages and produce\nan app that cannot install or run. That is a release gap in Rebase itself
1963
+ not a problem with your machine, your network, or your package manager.
1964
+
1965
+ Stopped before writing dependency versions or installing anything. The
1966
+ project directory ${path.basename(options.targetDirectory)}/ was created and is safe to delete.\nPlease report this with the list above.`);
1505
1967
  }
1506
1968
  for (const [fullPath, originalContent] of fileContents.entries()) {
1507
1969
  let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
@@ -1560,13 +2022,22 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
1560
2022
  const dbPassword = crypto.randomBytes(16).toString("hex");
1561
2023
  const serviceKey = crypto.randomBytes(48).toString("base64");
1562
2024
  let envContent = fs.readFileSync(envPath, "utf-8");
2025
+ envContent = envContent.replace(/^(# ║ {2})(Copy this file to \.env and fill in the values)( +║)$/m, (_match, open, old, close) => {
2026
+ const replacement = "Generated by `rebase init` — the secrets below are already set";
2027
+ const width = old.length + close.length - 1;
2028
+ return open + replacement.slice(0, width).padEnd(width, " ") + "║";
2029
+ });
1563
2030
  envContent = envContent.replace(/^JWT_SECRET=.*$/m, `JWT_SECRET=${jwtSecret}`);
1564
2031
  envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
2032
+ const composeApiPort = /^PORT=(\d+)/m.exec(envContent)?.[1] ?? "3001";
2033
+ envContent = envContent.replace(/^#\s*CORS_ORIGINS=.*$/m, `CORS_ORIGINS=http://localhost:${composeApiPort}`);
1565
2034
  const runtimeVersion = readCliVersion();
1566
2035
  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
2036
  if (databaseUrl) {
1568
2037
  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}`);
2038
+ const { pinSearchPath } = await import("@rebasepro/server-postgres");
2039
+ const pinnedUrl = pinSearchPath(databaseUrl);
2040
+ envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
1570
2041
  } else {
1571
2042
  const dbPort = await findAvailablePort(5432);
1572
2043
  envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public&sslmode=disable\nDATABASE_PASSWORD=${dbPassword}`);
@@ -1896,6 +2367,7 @@ async function schemaCommand(subcommand, rawArgs) {
1896
2367
  return;
1897
2368
  }
1898
2369
  const projectRoot = requireProjectRoot();
2370
+ recordEvent("cli.schema_generate", { subcommand: subcommand ?? "none" }, { projectRoot });
1899
2371
  const backendDir = requireBackendDir(projectRoot);
1900
2372
  const activePlugin = getActiveBackendPlugin(backendDir);
1901
2373
  if (!activePlugin) {
@@ -1964,6 +2436,7 @@ async function dbCommand(subcommand, rawArgs) {
1964
2436
  return;
1965
2437
  }
1966
2438
  const projectRoot = requireProjectRoot();
2439
+ recordEvent("cli.db_push", { subcommand: subcommand ?? "none" }, { projectRoot });
1967
2440
  const backendDir = requireBackendDir(projectRoot);
1968
2441
  const activePlugin = getActiveBackendPlugin(backendDir);
1969
2442
  if (!activePlugin) {
@@ -2084,7 +2557,7 @@ var REMOVED_APP_TYPES = {
2084
2557
  mobile: "mobile apps are no longer declared in the manifest — nothing consumed this type. Remove the entry"
2085
2558
  };
2086
2559
  /** Reserved because they name things in URLs and CLI output. */
2087
- var RESERVED_APP_NAMES = new Set([
2560
+ var RESERVED_APP_NAMES = /* @__PURE__ */ new Set([
2088
2561
  "api",
2089
2562
  "health",
2090
2563
  "metrics",
@@ -2727,6 +3200,11 @@ async function devCommand(rawArgs) {
2727
3200
  return;
2728
3201
  }
2729
3202
  const projectRoot = requireProjectRoot();
3203
+ recordEvent("cli.dev", {
3204
+ backend_only: Boolean(args["--backend-only"]),
3205
+ frontend_only: Boolean(args["--frontend-only"]),
3206
+ generate: Boolean(args["--generate"])
3207
+ }, { projectRoot });
2730
3208
  const backendDir = findBackendDir(projectRoot);
2731
3209
  const frontendDir = findFrontendDir(projectRoot);
2732
3210
  const backendOnly = args["--backend-only"] || false;
@@ -3081,7 +3559,7 @@ ${chalk.green.bold("Description")}
3081
3559
  */
3082
3560
  var DEFAULT_BUNDLE_DIR = "dist-bundle";
3083
3561
  /** Packages whose presence means the bundle cannot run on a stock runtime image. */
3084
- var KNOWN_NATIVE_PACKAGES = new Set([
3562
+ var KNOWN_NATIVE_PACKAGES = /* @__PURE__ */ new Set([
3085
3563
  "sharp",
3086
3564
  "canvas",
3087
3565
  "bcrypt",
@@ -3097,7 +3575,7 @@ var KNOWN_NATIVE_PACKAGES = new Set([
3097
3575
  "pg-native"
3098
3576
  ]);
3099
3577
  /** Dependencies supplied by the runtime image itself, not by the bundle. */
3100
- var RUNTIME_PROVIDED = new Set([
3578
+ var RUNTIME_PROVIDED = /* @__PURE__ */ new Set([
3101
3579
  "@rebasepro/server",
3102
3580
  "@rebasepro/types",
3103
3581
  "@rebasepro/client",
@@ -3506,6 +3984,139 @@ function collectDeclaredDependencies(projectRoot) {
3506
3984
  return declared;
3507
3985
  }
3508
3986
  /**
3987
+ * Lowest version a range could resolve to, or null if it is not a range.
3988
+ *
3989
+ * Kept deliberately tiny and local. The published range grammar here is a caret,
3990
+ * a tilde, an exact version or a `>=` floor, and the alternative — a semver
3991
+ * dependency in the CLI — buys breadth this does not need.
3992
+ */
3993
+ function lowerBoundOf(range) {
3994
+ const raw = range.trim().replace(/^[\^~]/, "").replace(/^>=\s*/, "").replace(/^v/, "");
3995
+ if (!/^\d+(\.\d+){0,2}$/.test(raw)) return null;
3996
+ const [major, minor = 0, patch = 0] = raw.split(".").map(Number);
3997
+ return [
3998
+ major,
3999
+ minor,
4000
+ patch
4001
+ ];
4002
+ }
4003
+ /** Highest version a range could resolve to (exclusive), or null. */
4004
+ function upperBoundOf(range) {
4005
+ const trimmed = range.trim();
4006
+ const min = lowerBoundOf(trimmed);
4007
+ if (!min) return null;
4008
+ if (trimmed.startsWith(">=")) return null;
4009
+ const [major, minor, patch] = min;
4010
+ if (trimmed.startsWith("^")) {
4011
+ if (major > 0) return [
4012
+ major + 1,
4013
+ 0,
4014
+ 0
4015
+ ];
4016
+ if (minor > 0) return [
4017
+ 0,
4018
+ minor + 1,
4019
+ 0
4020
+ ];
4021
+ return [
4022
+ 0,
4023
+ 0,
4024
+ patch + 1
4025
+ ];
4026
+ }
4027
+ if (trimmed.startsWith("~")) return trimmed.replace(/^~v?/, "").split(".").length >= 2 ? [
4028
+ major,
4029
+ minor + 1,
4030
+ 0
4031
+ ] : [
4032
+ major + 1,
4033
+ 0,
4034
+ 0
4035
+ ];
4036
+ const parts = trimmed.replace(/^v/, "").split(".").length;
4037
+ if (parts === 1) return [
4038
+ major + 1,
4039
+ 0,
4040
+ 0
4041
+ ];
4042
+ if (parts === 2) return [
4043
+ major,
4044
+ minor + 1,
4045
+ 0
4046
+ ];
4047
+ return [
4048
+ major,
4049
+ minor,
4050
+ patch + 1
4051
+ ];
4052
+ }
4053
+ function compareTriples(a, b) {
4054
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
4055
+ return 0;
4056
+ }
4057
+ /**
4058
+ * Whether a declared range could EVER resolve at or above `target`.
4059
+ *
4060
+ * The same question the control plane asks at intake, asked here first. Only a
4061
+ * range whose entire span sits below the target is reported — `^0.10.0` can
4062
+ * never cross to 0.12 whatever npm publishes — because a false alarm on a build
4063
+ * that would have worked trains people to ignore the warning that matters.
4064
+ */
4065
+ function canReach(range, target) {
4066
+ const ceiling = upperBoundOf(range);
4067
+ const floor = lowerBoundOf(target);
4068
+ if (!floor || !lowerBoundOf(range)) return null;
4069
+ if (!ceiling) return true;
4070
+ return compareTriples(ceiling, floor) > 0;
4071
+ }
4072
+ /**
4073
+ * Find `@rebasepro/*` dependencies pinned to a version older than this CLI.
4074
+ *
4075
+ * This is the only place a developer can be told. In development, every
4076
+ * `@rebasepro/*` resolves through pnpm's `link:`/`workspace:` overrides to the
4077
+ * checkout, so the version STRINGS in package.json are never exercised — the
4078
+ * project runs fine locally on whatever is on disk, and the declared numbers are
4079
+ * first honoured when the runtime npm-installs them from a bundle in the cloud.
4080
+ * A project scaffolded at 0.10.0 therefore keeps working on a developer's
4081
+ * machine indefinitely while being, in the cloud, a 0.10.0 driver.
4082
+ *
4083
+ * That matters because the image supplies only `@rebasepro/server`; the database
4084
+ * driver comes from these declarations and a newer runtime never updates it.
4085
+ * Every package.json is scanned, `dependencies` and `devDependencies` both,
4086
+ * because they have to be bumped together and the one that gets forgotten is the
4087
+ * one nobody looks at.
4088
+ */
4089
+ function detectFrameworkDepDrift(projectRoot, cliVersion) {
4090
+ const found = [];
4091
+ for (const relative of [
4092
+ "package.json",
4093
+ "backend/package.json",
4094
+ "config/package.json",
4095
+ "frontend/package.json"
4096
+ ]) {
4097
+ const file = path.join(projectRoot, relative);
4098
+ if (!fs.existsSync(file)) continue;
4099
+ try {
4100
+ const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
4101
+ for (const block of [pkg.dependencies, pkg.devDependencies]) for (const [name, range] of Object.entries(block ?? {})) {
4102
+ if (!name.startsWith("@rebasepro/")) continue;
4103
+ if (typeof range !== "string") continue;
4104
+ found.push({
4105
+ name,
4106
+ range,
4107
+ file: relative
4108
+ });
4109
+ }
4110
+ } catch {}
4111
+ }
4112
+ const behind = found.filter((d) => canReach(d.range, cliVersion) === false);
4113
+ const bounds = new Set(found.map((d) => lowerBoundOf(d.range)).filter((b) => b != null).map((b) => b.join(".")));
4114
+ return {
4115
+ behind,
4116
+ disagreeing: bounds.size > 1 ? [...bounds].sort() : []
4117
+ };
4118
+ }
4119
+ /**
3509
4120
  * Rewrite relative import specifiers in emitted JavaScript so Node can resolve them.
3510
4121
  *
3511
4122
  * TypeScript deliberately does not touch specifiers: `moduleResolution: "bundler"`
@@ -3671,7 +4282,7 @@ async function buildBundle(options) {
3671
4282
  console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
3672
4283
  console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
3673
4284
  console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
3674
- console.log(chalk.dim(` or run \`rebase eject\` to make this file the entrypoint and own the image.`));
4285
+ console.log(chalk.dim(" or run `rebase eject` to make this file the entrypoint and own the image."));
3675
4286
  }
3676
4287
  log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
3677
4288
  cleanOutDir(projectRoot, outDir);
@@ -4099,7 +4710,7 @@ async function foldFrontendIntoBundle(options) {
4099
4710
  * entrypoint, falls back to the previous behaviour: run every workspace's own
4100
4711
  * `build` script. Nothing that built before stops building.
4101
4712
  */
4102
- function printHelp$4() {
4713
+ function printHelp$5() {
4103
4714
  console.log(`
4104
4715
  ${chalk.bold("rebase build")} — build the apps declared in rebase.json
4105
4716
 
@@ -4134,7 +4745,7 @@ async function buildCommand(rawArgs = []) {
4134
4745
  permissive: true
4135
4746
  });
4136
4747
  if (args["--help"]) {
4137
- printHelp$4();
4748
+ printHelp$5();
4138
4749
  return;
4139
4750
  }
4140
4751
  const projectRoot = requireProjectRoot();
@@ -4203,6 +4814,16 @@ async function buildCommand(rawArgs = []) {
4203
4814
  console.log(chalk.yellow(` ⚠ native dependencies detected: ${names}`));
4204
4815
  console.log(chalk.dim(" These cannot run on the managed runtime. See `rebase doctor`."));
4205
4816
  }
4817
+ const drift = detectFrameworkDepDrift(projectRoot, resolveCliVersion());
4818
+ if (drift.behind.length > 0) {
4819
+ console.log(chalk.yellow(` ⚠ framework dependencies older than this CLI (${resolveCliVersion()}):`));
4820
+ for (const dep of drift.behind) console.log(chalk.dim(` ${dep.name}@${dep.range} (${dep.file})`));
4821
+ console.log(chalk.dim(" The image supplies the server, but your bundle supplies the database"));
4822
+ console.log(chalk.dim(" driver — a newer runtime does not update it. Bump these and rebuild."));
4823
+ } else if (drift.disagreeing.length > 0) {
4824
+ console.log(chalk.yellow(` ⚠ mixed @rebasepro versions declared: ${drift.disagreeing.join(", ")}`));
4825
+ console.log(chalk.dim(" These are published together and expect to run together; pin them alike."));
4826
+ }
4206
4827
  if (!args["--no-static"]) {
4207
4828
  const folded = await foldFrontendIntoBundle({
4208
4829
  projectRoot,
@@ -4363,7 +4984,7 @@ function projectNameOf(projectRoot) {
4363
4984
  } catch {}
4364
4985
  return path.basename(projectRoot);
4365
4986
  }
4366
- function printHelp$3() {
4987
+ function printHelp$4() {
4367
4988
  console.log(`
4368
4989
  ${chalk.bold("rebase eject")} — take ownership of the server process
4369
4990
 
@@ -4390,7 +5011,7 @@ async function ejectCommand(rawArgs = []) {
4390
5011
  permissive: true
4391
5012
  });
4392
5013
  if (args["--help"]) {
4393
- printHelp$3();
5014
+ printHelp$4();
4394
5015
  return;
4395
5016
  }
4396
5017
  const projectRoot = requireProjectRoot();
@@ -4534,7 +5155,7 @@ function restoreBackendScripts(projectRoot) {
4534
5155
  * `rebase.json`) this falls back to the backend workspace's own `start` script,
4535
5156
  * which is what such a project has always used.
4536
5157
  */
4537
- function printHelp$2() {
5158
+ function printHelp$3() {
4538
5159
  console.log(`
4539
5160
  ${chalk.bold("rebase start")} — run a built bundle
4540
5161
 
@@ -4560,7 +5181,7 @@ async function startCommand(rawArgs = []) {
4560
5181
  permissive: true
4561
5182
  });
4562
5183
  if (args["--help"]) {
4563
- printHelp$2();
5184
+ printHelp$3();
4564
5185
  return;
4565
5186
  }
4566
5187
  const projectRoot = requireProjectRoot();
@@ -5061,7 +5682,7 @@ function parseAgentFlags(rawArgs) {
5061
5682
  return requested;
5062
5683
  }
5063
5684
  async function skillsInstall(rawArgs = []) {
5064
- const projectDir = process.cwd();
5685
+ const projectDir = findProjectRoot() ?? process.cwd();
5065
5686
  let skillsDir;
5066
5687
  try {
5067
5688
  skillsDir = getSkillsSourceDir();
@@ -5105,7 +5726,8 @@ async function skillsInstall(rawArgs = []) {
5105
5726
  for (const agentKey of agents) {
5106
5727
  const agent = AGENTS[agentKey];
5107
5728
  const count = installForAgent(agentKey, skills, projectDir);
5108
- console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} ${count} skills installed to ${chalk.gray(agent.targetDir)}`);
5729
+ const shown = path.relative(process.cwd(), path.join(projectDir, agent.targetDir)) || agent.targetDir;
5730
+ console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} — ${count} skills installed to ${chalk.gray(shown)}`);
5109
5731
  }
5110
5732
  console.log("");
5111
5733
  console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
@@ -5423,6 +6045,107 @@ ${chalk.green.bold("Examples")}
5423
6045
  `);
5424
6046
  }
5425
6047
  //#endregion
6048
+ //#region src/commands/telemetry.ts
6049
+ /**
6050
+ * `rebase telemetry` — the command that makes the rest of it inspectable.
6051
+ *
6052
+ * The whole subsystem asks for trust it cannot otherwise earn, and the cheapest
6053
+ * way to earn it is to stop describing the payload and print it. `show` runs
6054
+ * the same builder the sender uses, so what appears here is what would go, not
6055
+ * a documentation comment that quietly fell out of date two releases ago.
6056
+ */
6057
+ async function telemetryCommand(rawArgs) {
6058
+ switch (rawArgs.slice(3).filter((a) => !a.startsWith("-"))[0]) {
6059
+ case "status":
6060
+ case void 0:
6061
+ printStatus();
6062
+ return;
6063
+ case "show":
6064
+ printPayload();
6065
+ return;
6066
+ case "enable":
6067
+ if (!setConsent(true)) {
6068
+ console.error(chalk.red(`Could not write ${configPath()} — sharing was not enabled.`));
6069
+ process.exitCode = 1;
6070
+ return;
6071
+ }
6072
+ console.log(chalk.green("Anonymous usage sharing enabled."));
6073
+ console.log(chalk.gray(`Inspect what gets sent with ${chalk.cyan("rebase telemetry show")}.`));
6074
+ return;
6075
+ case "disable":
6076
+ if (!setConsent(false)) {
6077
+ console.error(chalk.red(`Could not write ${configPath()} — sharing is still ON.`));
6078
+ console.error(chalk.yellow(`Set ${chalk.cyan("REBASE_TELEMETRY_DISABLED=1")} in your environment instead; it needs no file.`));
6079
+ process.exitCode = 1;
6080
+ return;
6081
+ }
6082
+ console.log(chalk.gray("Anonymous usage sharing disabled. Nothing further will be sent."));
6083
+ return;
6084
+ default:
6085
+ printHelp$2();
6086
+ process.exitCode = 1;
6087
+ }
6088
+ }
6089
+ function printStatus() {
6090
+ console.log("");
6091
+ console.log(` Status: ${describeState()}`);
6092
+ if (readProjectPolicy() === "ignored_opt_in") {
6093
+ console.log("");
6094
+ console.log(chalk.yellow(" Note: this project's rebase.json sets \"telemetry\": true, which is ignored."));
6095
+ console.log(chalk.gray(" A committed file cannot consent on behalf of everyone who clones it."));
6096
+ console.log(chalk.gray(` Only "telemetry": false is honoured there. Use ${chalk.cyan("rebase telemetry enable")}.`));
6097
+ }
6098
+ console.log(` Endpoint: ${chalk.gray(endpoint())}`);
6099
+ console.log(` Config: ${chalk.gray(configPath())}`);
6100
+ console.log("");
6101
+ console.log(chalk.gray(` ${chalk.cyan("rebase telemetry show")} prints the exact payload.`));
6102
+ console.log("");
6103
+ }
6104
+ function printPayload() {
6105
+ if (readConfig().enabled !== true) {
6106
+ console.log("");
6107
+ console.log(` ${describeState()}`);
6108
+ console.log("");
6109
+ console.log(chalk.gray(" Nothing is being sent, so there is no payload to show."));
6110
+ console.log(chalk.gray(` Run ${chalk.cyan("rebase telemetry enable")} first if you want to inspect one.`));
6111
+ console.log("");
6112
+ return;
6113
+ }
6114
+ const event = previewEvent("cli.dev", { first_run: false }, process.cwd());
6115
+ console.log("");
6116
+ console.log(chalk.gray(" Sent to ") + chalk.gray(endpoint()) + chalk.gray(", for example:"));
6117
+ console.log("");
6118
+ console.log(JSON.stringify(event, null, 2).split("\n").map((l) => " " + l).join("\n"));
6119
+ console.log("");
6120
+ console.log(chalk.gray(" Both ids are random. Neither is derived from your machine, your"));
6121
+ console.log(chalk.gray(" hostname, your project name or anything you have typed."));
6122
+ console.log("");
6123
+ }
6124
+ function printHelp$2() {
6125
+ console.log(`
6126
+ ${chalk.bold("rebase telemetry")} — anonymous usage sharing (opt-in, off by default)
6127
+
6128
+ ${chalk.bold("Commands")}
6129
+ ${chalk.blue("status")} Whether anything is being shared, and why ${chalk.gray("(default)")}
6130
+ ${chalk.blue("show")} Print the exact payload that would be sent
6131
+ ${chalk.blue("enable")} Start sharing
6132
+ ${chalk.blue("disable")} Stop sharing, permanently
6133
+
6134
+ ${chalk.bold("Project policy")}
6135
+ ${chalk.blue("\"telemetry\": false")} in ${chalk.blue("rebase.json")} disables sharing for everyone who
6136
+ clones the repository, overriding each developer's own choice.
6137
+ ${chalk.gray("\"telemetry\": true is ignored — a committed file cannot consent for others.")}
6138
+
6139
+ ${chalk.bold("Environment")}
6140
+ ${chalk.blue("DO_NOT_TRACK")} Set to disable, across every tool that honours it
6141
+ ${chalk.blue("REBASE_TELEMETRY_DISABLED")} Set to disable Rebase specifically
6142
+ ${chalk.blue("REBASE_TELEMETRY_ENDPOINT")} Send elsewhere — your own collector, for instance
6143
+
6144
+ ${chalk.gray("Never shared: project names, paths, collection or table names, database")}
6145
+ ${chalk.gray("URLs, hostnames, error messages, stack traces, or exact record counts.")}
6146
+ `);
6147
+ }
6148
+ //#endregion
5426
6149
  //#region src/commands/cloud/auth.ts
5427
6150
  /**
5428
6151
  * `rebase cloud` auth subcommands: login, logout, whoami.
@@ -5715,6 +6438,54 @@ function providerDefaults(provider) {
5715
6438
  };
5716
6439
  }
5717
6440
  }
6441
+ /**
6442
+ * Where this project says it runs.
6443
+ *
6444
+ * `provider`/`region` are a *request*: no code downstream reads them to pick a
6445
+ * deploy target — that comes from the project's cluster record or the ambient
6446
+ * in-cluster context (saas/backend/src/k8s/resolve.ts). So a wrong value here is
6447
+ * never contradicted by a failure; it just sits in the record. The CLI used to
6448
+ * default to `hetzner`/`nbg1` unconditionally, which is how projects running on
6449
+ * our GKE cluster came to describe themselves as Hetzner in the console — and
6450
+ * `provider` is half the Stripe compute lookup key (`compute_<provider>_<vmSize>`),
6451
+ * so that is a mispricing, not a cosmetic slip.
6452
+ *
6453
+ * The control plane already publishes the infrastructure that actually exists,
6454
+ * and the console's create wizard reads it. Ask the same question here.
6455
+ *
6456
+ * Exported for tests: the decision is pure, so it can be pinned without a
6457
+ * control plane. The fetching and the exit live in `resolveRequestedTarget`.
6458
+ *
6459
+ * @param requested `--provider`, if the caller named one. An explicit flag wins:
6460
+ * it is the caller stating intent, and `deploy` corrects the record anyway.
6461
+ * @param targets What the control plane says exists, or `undefined` when it
6462
+ * cannot say — an older deployment with no `platform-config`, or a failed
6463
+ * request. That is different from an empty list, which is a control plane
6464
+ * stating it has no infrastructure at all.
6465
+ * @returns the target to record, or `null` when the control plane answered that
6466
+ * there is none.
6467
+ */
6468
+ function chooseRequestedTarget(requested, targets) {
6469
+ if (requested) return {
6470
+ provider: requested,
6471
+ region: void 0
6472
+ };
6473
+ if (!targets) return {
6474
+ provider: "hetzner",
6475
+ region: void 0
6476
+ };
6477
+ if (targets.length === 0) return null;
6478
+ const [target] = targets;
6479
+ return {
6480
+ provider: target.provider,
6481
+ region: target.region?.trim() || void 0
6482
+ };
6483
+ }
6484
+ async function resolveRequestedTarget(client, url, requested) {
6485
+ const chosen = chooseRequestedTarget(requested, await fetchDeployTargets(client, url));
6486
+ if (!chosen) fail("This control plane has no deploy targets configured.", `Register a cluster, or pass ${chalk.bold("--provider")} and ${chalk.bold("--region")} to record one anyway.`);
6487
+ return chosen;
6488
+ }
5718
6489
  async function createProject(rawArgs) {
5719
6490
  const args = arg({
5720
6491
  "--name": String,
@@ -5750,9 +6521,10 @@ async function createProject(rawArgs) {
5750
6521
  const subdomain = (args["--subdomain"] || a.subdomain || "").trim().toLowerCase();
5751
6522
  const gitRepoUrl = (args["--repo"] || a.repo || "").trim();
5752
6523
  const gitBranch = (args["--branch"] || a.branch || "main").trim();
5753
- const provider = (args["--provider"] || a.provider || "hetzner").trim();
6524
+ const target = await resolveRequestedTarget(client, url, (args["--provider"] || a.provider)?.trim() || void 0);
6525
+ const provider = target.provider;
5754
6526
  const defaults = providerDefaults(provider);
5755
- const region = (args["--region"] || defaults.region).trim();
6527
+ const region = (args["--region"] || target.region || defaults.region).trim();
5756
6528
  const vmSize = (args["--vm-size"] || defaults.vmSize).trim();
5757
6529
  if (!name || !subdomain) fail("Name and subdomain are required.");
5758
6530
  try {
@@ -6085,12 +6857,24 @@ function resolveFrameworkVersion(sourceDir) {
6085
6857
  dir = parent;
6086
6858
  }
6087
6859
  }
6860
+ /**
6861
+ * A progress line for a human — dropped entirely in JSON mode.
6862
+ *
6863
+ * Progress is not a result. In JSON mode stdout carries the one result value
6864
+ * and nothing else, so every unguarded `console.log` on a deploy path was a
6865
+ * line printed in front of the JSON, breaking the parser meant to read it.
6866
+ * Warnings are the other half of this rule and go the other way: they are
6867
+ * `warn`, which prints in every mode, to stderr. See `warn` in `context.ts`.
6868
+ */
6869
+ function progress(line) {
6870
+ if (!isJsonMode()) console.log(line);
6871
+ }
6088
6872
  /** Upload a build-context tarball; returns the opaque `source` ref for deploy. */
6089
6873
  async function uploadSource(url, token, projectId, tarPath) {
6090
6874
  const bytes = fs.readFileSync(tarPath);
6091
6875
  const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
6092
6876
  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)...`));
6877
+ progress(chalk.gray(` Uploading source (${sizeMb} MB)...`));
6094
6878
  const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
6095
6879
  method: "POST",
6096
6880
  headers: {
@@ -6122,7 +6906,7 @@ async function deployBundle(opts) {
6122
6906
  const loaded = loadManifest(projectRoot);
6123
6907
  const backend = findBackendApp(loaded.manifest);
6124
6908
  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..."));
6909
+ progress(chalk.gray(" Building bundle..."));
6126
6910
  bundleDir = (await buildBundle({
6127
6911
  projectRoot,
6128
6912
  appName: backend.name,
@@ -6130,16 +6914,16 @@ async function deployBundle(opts) {
6130
6914
  runtimeRange: loaded.manifest.rebase,
6131
6915
  storage: loaded.manifest.storage,
6132
6916
  skipTypeCheck: opts.skipTypeCheck,
6133
- log: (m) => console.log(chalk.gray(m))
6917
+ log: (m) => progress(chalk.gray(m))
6134
6918
  })).outDir;
6135
6919
  try {
6136
6920
  const folded = await foldFrontendIntoBundle({
6137
6921
  projectRoot,
6138
6922
  manifest: loaded.manifest,
6139
6923
  bundleDir,
6140
- log: (m) => console.log(m)
6924
+ log: (m) => progress(m)
6141
6925
  });
6142
- for (const outcome of folded) console.log(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
6926
+ for (const outcome of folded) progress(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
6143
6927
  } catch (err) {
6144
6928
  fail(err instanceof Error ? err.message : String(err), "Fix the frontend build, or pass --no-static to deploy the API alone.");
6145
6929
  }
@@ -6156,7 +6940,7 @@ async function deployBundle(opts) {
6156
6940
  try {
6157
6941
  await packBundle(bundleDir, tarPath);
6158
6942
  const sizeMb = (fs.statSync(tarPath).size / 1024 / 1024).toFixed(1);
6159
- console.log(chalk.gray(` Uploading bundle (${sizeMb} MB)...`));
6943
+ progress(chalk.gray(` Uploading bundle (${sizeMb} MB)...`));
6160
6944
  bundleId = await uploadBundle(url, token, projectId, tarPath);
6161
6945
  } catch (e) {
6162
6946
  fail(e instanceof Error ? e.message : String(e));
@@ -6164,8 +6948,10 @@ async function deployBundle(opts) {
6164
6948
  } finally {
6165
6949
  fs.rmSync(tarPath, { force: true });
6166
6950
  }
6167
- console.log("");
6168
- console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
6951
+ if (!isJsonMode()) {
6952
+ console.log("");
6953
+ console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
6954
+ }
6169
6955
  let declaredApps = [];
6170
6956
  try {
6171
6957
  declaredApps = declaredAppsFrom(loadManifest(process.cwd()).manifest);
@@ -6253,9 +7039,75 @@ function planBareDeploy(project, latest, now) {
6253
7039
  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
7040
  };
6255
7041
  }
7042
+ /** `code` of the warning below, and the field name it sets in the payload. */
7043
+ var EJECTS_MANAGED_RUNTIME = "ejects_managed_runtime";
6256
7044
  /** The one sentence that says a source build undoes `runtimeMode: managed`. */
6257
7045
  function ejectWarning(projectRef) {
6258
- return `⚠ ${projectRef} runs on the managed runtime — a source build ejects it to a custom container.`;
7046
+ return {
7047
+ code: EJECTS_MANAGED_RUNTIME,
7048
+ message: `${projectRef} runs on the managed runtime — this build ejects it to a custom container.`,
7049
+ hint: "Use `rebase cloud deploy --bundle` to stay on managed."
7050
+ };
7051
+ }
7052
+ /**
7053
+ * Why a container-image deploy of a managed project is refused — or `undefined`
7054
+ * to let it through.
7055
+ *
7056
+ * Every path below this point builds a container image, and a successful one
7057
+ * sets `runtimeMode: "custom"` server-side. So the question is never "which flag
7058
+ * was used" but "did the caller ask to leave the managed runtime", and only
7059
+ * `--force` answers it.
7060
+ *
7061
+ * `--source` used to be read as answering it too, on the theory that uploading a
7062
+ * build context is self-evidently a deliberate eject. It is not: `--source`
7063
+ * picks *which source* gets built — this directory, rather than the stale
7064
+ * archive the control plane is holding — and the eject is a side effect of the
7065
+ * answer. That is exactly how a live project got flipped to `custom` by someone
7066
+ * whose actual intent was "deploy what I have here", and it is the same
7067
+ * ignorance the bare form is refused for. Same ignorance, same refusal.
7068
+ */
7069
+ function ejectRefusal(opts, projectRef) {
7070
+ if (!opts.managed || opts.force) return void 0;
7071
+ const eject = "To eject on purpose, add `--force`.";
7072
+ if (opts.source) return {
7073
+ 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.`,
7074
+ hint: `Deploy this directory to the managed runtime with \`rebase cloud deploy --bundle\`. ${eject}`,
7075
+ code: "managed_project"
7076
+ };
7077
+ return {
7078
+ 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.`,
7079
+ hint: `Redeploy it with \`rebase cloud deploy --bundle\`. ${eject} \`--source . --force\` builds this directory; \`--force\` alone builds what the control plane holds.`,
7080
+ code: "managed_project"
7081
+ };
7082
+ }
7083
+ /**
7084
+ * Which warnings a container-image deploy has earned.
7085
+ *
7086
+ * Pure, and separate from the printing, because the printing is what went
7087
+ * wrong: the eject warning used to be written inline behind `!isJsonMode()`, so
7088
+ * the fact that a deploy ejects a managed project existed only as a side effect
7089
+ * of a TTY being attached. Deciding here, emitting once at the call site, means
7090
+ * the decision cannot be output-mode-dependent again.
7091
+ *
7092
+ * The condition is just `managed`: anything reaching this point is a container
7093
+ * image build that `ejectRefusal` has already let through, and on a managed
7094
+ * project that is an eject however it was spelled. A caller who passed `--force`
7095
+ * knows — the warning is for the transcript and the payload, which is what
7096
+ * anyone reviewing the deploy afterwards actually reads.
7097
+ */
7098
+ function deployWarnings(opts, projectRef) {
7099
+ return opts.managed ? [ejectWarning(projectRef)] : [];
7100
+ }
7101
+ /** The warning half of a deploy's JSON payload — merged into whatever it emits. */
7102
+ function warningPayload(warnings) {
7103
+ return {
7104
+ warnings: warnings.map((w) => ({
7105
+ code: w.code,
7106
+ message: w.message,
7107
+ hint: w.hint ?? null
7108
+ })),
7109
+ ejectsManagedRuntime: warnings.some((w) => w.code === EJECTS_MANAGED_RUNTIME)
7110
+ };
6259
7111
  }
6260
7112
  /**
6261
7113
  * Read the two rows the preflight needs.
@@ -6326,17 +7178,18 @@ async function deployCommand(rawArgs, projectRef) {
6326
7178
  }
6327
7179
  const { project, latest } = await readDeployContext(client, projectId);
6328
7180
  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()) {
7181
+ const eject = {
7182
+ managed: plan.managed,
7183
+ source: Boolean(args["--source"]),
7184
+ force: args["--force"] === true
7185
+ };
7186
+ const refusal = ejectRefusal(eject, projectRef);
7187
+ if (refusal) fail(refusal.message, refusal.hint, refusal.code);
7188
+ const warnings = deployWarnings(eject, projectRef);
7189
+ for (const w of warnings) warn(w.message, w.hint);
7190
+ if (!args["--source"] && !isJsonMode()) {
6337
7191
  console.log("");
6338
- console.log(chalk.yellow(` ${ejectWarning(projectRef)}`));
6339
- console.log(chalk.gray(" Use `rebase cloud deploy --bundle` to stay on managed."));
7192
+ for (const line of plan.lines) console.log(chalk.gray(` ${line}`));
6340
7193
  }
6341
7194
  let source;
6342
7195
  if (args["--source"]) {
@@ -6349,8 +7202,10 @@ async function deployCommand(rawArgs, projectRef) {
6349
7202
  fs.rmSync(tarPath, { force: true });
6350
7203
  }
6351
7204
  }
6352
- console.log("");
6353
- console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectRef)}${source ? " from uploaded source" : ""}...`);
7205
+ if (!isJsonMode()) {
7206
+ console.log("");
7207
+ console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectRef)}${source ? " from uploaded source" : ""}...`);
7208
+ }
6354
7209
  const body = { projectId };
6355
7210
  if (source) body.source = source;
6356
7211
  if (args["--message"]) body.message = args["--message"];
@@ -6378,7 +7233,8 @@ async function deployCommand(rawArgs, projectRef) {
6378
7233
  deploymentId,
6379
7234
  deduplicated,
6380
7235
  frameworkVersion: frameworkVersion ?? null,
6381
- following: false
7236
+ following: false,
7237
+ ...warningPayload(warnings)
6382
7238
  });
6383
7239
  return;
6384
7240
  }
@@ -6392,7 +7248,8 @@ async function deployCommand(rawArgs, projectRef) {
6392
7248
  deduplicated,
6393
7249
  frameworkVersion: frameworkVersion ?? null,
6394
7250
  following: true,
6395
- status
7251
+ status,
7252
+ ...warningPayload(warnings)
6396
7253
  });
6397
7254
  }
6398
7255
  /**
@@ -9006,6 +9863,7 @@ function describeDatabaseState(db) {
9006
9863
  * So both are printed, rather than leaving anyone to infer one from a Docker tag.
9007
9864
  */
9008
9865
  function describeRuntime(project) {
9866
+ if (project.runtimeMode == null || project.runtimeMode.trim() === "") return `not deployed yet ${chalk.gray("· the first deploy decides (`--bundle` keeps it managed)")}`;
9009
9867
  if (project.runtimeMode !== "managed") return `custom ${chalk.gray("· your own image")}`;
9010
9868
  const version = project.runtimeVersion ?? "unknown";
9011
9869
  const framework = project.runtimeFrameworkVersion;
@@ -9404,17 +10262,41 @@ async function billingCommand(rawArgs) {
9404
10262
  * dispatched from here. Individual groups live in sibling modules; this file
9405
10263
  * only routes and prints help.
9406
10264
  */
9407
- /** Positional tokens after `rebase cloud` (group, action, …). */
10265
+ /**
10266
+ * Positional tokens after `rebase cloud` (group, action, …).
10267
+ *
10268
+ * Two things stop a flag being mistaken for the group. `GLOBAL_CLOUD_FLAGS` is
10269
+ * declared so `arg` *consumes* the flags that may precede it — critically
10270
+ * together with their values, which is the half that filtering cannot do. The
10271
+ * leading-`-` skip then covers a flag nobody declared, so an unrecognised
10272
+ * boolean shifts nothing.
10273
+ *
10274
+ * Only leading tokens are skipped: past the group and action, an undeclared
10275
+ * flag and its value are somebody else's positionals and none of our business.
10276
+ * A flag this file has never heard of, that takes a value, placed before the
10277
+ * group, is the one shape still unresolvable here — there is no way to know
10278
+ * whether the token after it is its value or the group, and guessing either way
10279
+ * is worse than the handler reporting an unknown group.
10280
+ *
10281
+ * Exported so its tests can drive the real thing. The dispatch test used to
10282
+ * re-implement it locally as `slice(3).filter(a => !a.startsWith("-"))` — which
10283
+ * filtered flags, while this function did not — so the test asserted the
10284
+ * behaviour we wanted against a copy that had it, and stayed green for as long
10285
+ * as the real dispatcher was broken.
10286
+ */
9408
10287
  function positionals(rawArgs) {
9409
- return arg({}, {
10288
+ const rest = arg(GLOBAL_CLOUD_FLAGS, {
9410
10289
  argv: rawArgs.slice(3),
9411
10290
  permissive: true
9412
10291
  })._;
10292
+ let i = 0;
10293
+ while (i < rest.length && rest[i].startsWith("-")) i++;
10294
+ return rest.slice(i);
9413
10295
  }
9414
10296
  async function cloudCommand(subcommand, rawArgs) {
9415
10297
  initOutputMode(rawArgs);
9416
10298
  const pos = positionals(rawArgs);
9417
- const group = subcommand && subcommand !== "--help" ? subcommand : pos[0];
10299
+ const group = pos[0] ?? (subcommand !== "--help" ? subcommand : void 0);
9418
10300
  const action = pos[1];
9419
10301
  if (!group || subcommand === "--help") {
9420
10302
  printCloudHelp();
@@ -9836,8 +10718,9 @@ async function entry(args) {
9836
10718
  console.log(getVersion());
9837
10719
  return;
9838
10720
  }
9839
- const command = parsedArgs._[0];
9840
- const subcommand = parsedArgs._[1];
10721
+ const words = parsedArgs._.filter((a) => !a.startsWith("-"));
10722
+ const command = words[0];
10723
+ const subcommand = words[1];
9841
10724
  if (!command || parsedArgs["--help"] && ![
9842
10725
  "init",
9843
10726
  "schema",
@@ -9852,7 +10735,8 @@ async function entry(args) {
9852
10735
  "cloud",
9853
10736
  "apps",
9854
10737
  "eject",
9855
- "generate-sdk"
10738
+ "generate-sdk",
10739
+ "telemetry"
9856
10740
  ].includes(command)) {
9857
10741
  printHelp();
9858
10742
  return;
@@ -9876,9 +10760,10 @@ async function entry(args) {
9876
10760
  argv: args.slice(3),
9877
10761
  permissive: true
9878
10762
  });
10763
+ const sdkRoot = sdkArgs["--help"] ? process.cwd() : requireProjectRoot();
9879
10764
  await generateSdkCommand({
9880
- collectionsDir: sdkArgs["--collections-dir"] || "./config/collections",
9881
- output: sdkArgs["--output"] || "./generated/sdk",
10765
+ collectionsDir: sdkArgs["--collections-dir"] || path.join(sdkRoot, "config/collections"),
10766
+ output: sdkArgs["--output"] || path.join(sdkRoot, "generated/sdk"),
9882
10767
  from: sdkArgs["--from"],
9883
10768
  token: sdkArgs["--token"],
9884
10769
  help: sdkArgs["--help"],
@@ -9922,6 +10807,9 @@ async function entry(args) {
9922
10807
  case "cloud":
9923
10808
  await cloudCommand(effectiveSubcommand, args);
9924
10809
  break;
10810
+ case "telemetry":
10811
+ await telemetryCommand(args);
10812
+ break;
9925
10813
  default:
9926
10814
  console.error(chalk.red(`Unknown command: ${command}`));
9927
10815
  console.log("");
@@ -9972,6 +10860,7 @@ ${chalk.green.bold("API Keys")}
9972
10860
  ${chalk.blue.bold("api-keys list")} List all service API keys
9973
10861
  ${chalk.blue.bold("api-keys create")} Create a new scoped API key
9974
10862
  ${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
10863
+ ${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
9975
10864
  ${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
9976
10865
 
9977
10866
  ${chalk.green.bold("Rebase Cloud")}
@@ -9985,9 +10874,25 @@ ${chalk.green.bold("Options")}
9985
10874
  ${chalk.blue("--help, -h")} Show this help message
9986
10875
 
9987
10876
  ${chalk.gray("Documentation: https://rebase.pro/docs")}
9988
- `);
10877
+ ${telemetryNotice()}`);
10878
+ }
10879
+ /**
10880
+ * One line about usage sharing, in the global help.
10881
+ *
10882
+ * Every other tool that collects anything prints a first-run notice. Ours asks
10883
+ * at the end of `rebase init` — but someone who installs the CLI and never runs
10884
+ * `init`, or who joins a project someone else scaffolded, would otherwise never
10885
+ * learn the subsystem exists. This is the cheapest place to close that: the
10886
+ * help is what an unfamiliar user reads first.
10887
+ *
10888
+ * It states the current setting rather than a generic sentence, so it is also
10889
+ * the fastest answer to "is this thing on?".
10890
+ */
10891
+ function telemetryNotice() {
10892
+ const sharing = isEnabled();
10893
+ return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
9989
10894
  }
9990
10895
  //#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 };
10896
+ 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
10897
 
9993
10898
  //# sourceMappingURL=index.es.js.map