@rebasepro/cli 0.12.1-canary.g4e7bcbf → 0.12.1-canary.g6861540

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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `rebase telemetry` — the command that makes the rest of it inspectable.
3
+ *
4
+ * The whole subsystem asks for trust it cannot otherwise earn, and the cheapest
5
+ * way to earn it is to stop describing the payload and print it. `show` runs
6
+ * the same builder the sender uses, so what appears here is what would go, not
7
+ * a documentation comment that quietly fell out of date two releases ago.
8
+ */
9
+ export declare function telemetryCommand(rawArgs: string[]): Promise<void>;
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.
@@ -888,6 +888,369 @@ function openUrl(target, label = "Opening") {
888
888
  child.unref();
889
889
  } catch {}
890
890
  }
891
+ /** Duration in coarse bands — enough to see "slow", not enough to fingerprint. */
892
+ function durationBucket(ms) {
893
+ if (!Number.isFinite(ms) || ms < 0) return "unknown";
894
+ if (ms < 5e3) return "<5s";
895
+ if (ms < 3e4) return "5-30s";
896
+ if (ms < 12e4) return "30-120s";
897
+ return "120s+";
898
+ }
899
+ /**
900
+ * Drop anything that is not a permitted value type, and clamp strings.
901
+ *
902
+ * The last line of defence rather than the first. Every call site is supposed
903
+ * to pass enumerated values; this is what stops a future one that forgets from
904
+ * turning into an incident. Strings are capped at 64 characters because no
905
+ * legitimate enumerated value is longer, and a path or a message always is.
906
+ */
907
+ /**
908
+ * Key names that shadow an `Object.prototype` member.
909
+ *
910
+ * `__proto__` is already excluded by the pattern (it starts with an
911
+ * underscore), and assigning any of these to an object literal shadows rather
912
+ * than pollutes — so nothing is exploitable here. They are refused because a
913
+ * stored property called `constructor` is a trap for every consumer downstream
914
+ * that reaches for `properties.constructor` and gets a string.
915
+ */
916
+ var RESERVED_KEYS = /* @__PURE__ */ new Set([
917
+ "constructor",
918
+ "prototype",
919
+ "hasownproperty",
920
+ "tostring",
921
+ "valueof"
922
+ ]);
923
+ function sanitize(properties) {
924
+ const out = {};
925
+ for (const [key, value] of Object.entries(properties)) {
926
+ if (!/^[a-z][a-z0-9_]{0,31}$/.test(key) || RESERVED_KEYS.has(key.toLowerCase())) continue;
927
+ if (typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) out[key] = value;
928
+ else if (typeof value === "string" && value.length > 0 && value.length <= 64) {
929
+ if (!/[/\\@:\s]/.test(value)) out[key] = value;
930
+ }
931
+ }
932
+ return out;
933
+ }
934
+ function cliVersion() {
935
+ try {
936
+ const pkg = createRequire(import.meta.url)("../../package.json");
937
+ return typeof pkg?.version === "string" ? pkg.version : "unknown";
938
+ } catch {
939
+ return "unknown";
940
+ }
941
+ }
942
+ function buildEvent(event, properties, identity) {
943
+ return {
944
+ schema: 1,
945
+ event,
946
+ machineId: identity.machineId,
947
+ projectId: identity.projectId,
948
+ cliVersion: cliVersion(),
949
+ nodeMajor: Number(process.versions.node.split(".")[0]) || 0,
950
+ platform: os.platform(),
951
+ arch: os.arch(),
952
+ at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
953
+ properties: sanitize(properties)
954
+ };
955
+ }
956
+ function configPath() {
957
+ return path.join(os.homedir(), ".rebase", "telemetry.json");
958
+ }
959
+ function readConfig() {
960
+ try {
961
+ const raw = JSON.parse(fs.readFileSync(configPath(), "utf-8"));
962
+ if (raw && typeof raw === "object") return {
963
+ version: typeof raw.version === "number" ? raw.version : 1,
964
+ enabled: typeof raw.enabled === "boolean" ? raw.enabled : void 0,
965
+ machineId: typeof raw.machineId === "string" ? raw.machineId : void 0,
966
+ decidedAt: typeof raw.decidedAt === "string" ? raw.decidedAt : void 0
967
+ };
968
+ } catch {}
969
+ return { version: 1 };
970
+ }
971
+ function writeConfig(config) {
972
+ const file = configPath();
973
+ fs.mkdirSync(path.dirname(file), { recursive: true });
974
+ fs.writeFileSync(file, JSON.stringify({
975
+ ...config,
976
+ version: 1
977
+ }, null, 2), {
978
+ encoding: "utf-8",
979
+ mode: 384
980
+ });
981
+ }
982
+ /**
983
+ * The machine's id, generating and persisting one on first use.
984
+ *
985
+ * Only called once consent exists — an id written before the user agreed would
986
+ * be a record we had no right to create, even unsent.
987
+ */
988
+ function ensureMachineId() {
989
+ const config = readConfig();
990
+ if (config.machineId) return config.machineId;
991
+ const machineId = crypto.randomUUID();
992
+ writeConfig({
993
+ ...config,
994
+ machineId
995
+ });
996
+ return machineId;
997
+ }
998
+ /**
999
+ * The checkout's id, generating one if this project has none.
1000
+ *
1001
+ * The directory is created when it is missing, but **only** where a
1002
+ * `rebase.json` proves this really is a project root. An earlier version
1003
+ * required `.rebase/` to already exist, on the assumption that `rebase init`
1004
+ * created it — it does not. It is written only when a scaffold is linked to
1005
+ * Rebase Cloud, so every self-hosted project reported no `projectId` at all,
1006
+ * for ever. That silently broke the funnel this id exists for, on exactly the
1007
+ * population the telemetry is meant to learn about.
1008
+ *
1009
+ * The `rebase.json` check is what keeps the fix from being "scatter `.rebase/`
1010
+ * wherever a command happens to run": no manifest, no project, no directory,
1011
+ * and the event simply reports no project.
1012
+ */
1013
+ function ensureProjectId(projectRoot) {
1014
+ if (!fs.existsSync(path.join(projectRoot, "rebase.json"))) return void 0;
1015
+ const dir = path.join(projectRoot, ".rebase");
1016
+ try {
1017
+ fs.mkdirSync(dir, { recursive: true });
1018
+ } catch {
1019
+ return;
1020
+ }
1021
+ const file = path.join(dir, "state.json");
1022
+ let state = {};
1023
+ if (fs.existsSync(file)) try {
1024
+ const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
1025
+ if (raw && typeof raw === "object") state = raw;
1026
+ } catch {
1027
+ return;
1028
+ }
1029
+ if (typeof state.telemetryProjectId === "string") return state.telemetryProjectId;
1030
+ const projectId = crypto.randomUUID();
1031
+ try {
1032
+ fs.writeFileSync(file, JSON.stringify({
1033
+ ...state,
1034
+ telemetryProjectId: projectId
1035
+ }, null, 2), "utf-8");
1036
+ } catch {
1037
+ return;
1038
+ }
1039
+ return projectId;
1040
+ }
1041
+ //#endregion
1042
+ //#region src/telemetry/project.ts
1043
+ function readProjectPolicy(startDir = process.cwd()) {
1044
+ try {
1045
+ const root = findProjectRoot(startDir);
1046
+ if (!root) return "unset";
1047
+ const file = path.join(root, MANIFEST_FILENAME);
1048
+ if (!fs.existsSync(file)) return "unset";
1049
+ const raw = fs.readFileSync(file, "utf-8");
1050
+ const match = /"telemetry"\s*:\s*(true|false)/.exec(raw);
1051
+ if (!match) return "unset";
1052
+ return match[1] === "false" ? "opt_out" : "ignored_opt_in";
1053
+ } catch {
1054
+ return "unset";
1055
+ }
1056
+ }
1057
+ function endpoint() {
1058
+ return process.env.REBASE_TELEMETRY_ENDPOINT?.trim() || "https://app.rebase.pro/api/functions/telemetry";
1059
+ }
1060
+ /**
1061
+ * The one function that decides whether anything leaves the machine.
1062
+ *
1063
+ * Every path is a refusal except the last, which is the point: consent is
1064
+ * opt-in, so the default answer at every branch — never asked, unreadable
1065
+ * config, a set env var, a CI runner — is no.
1066
+ *
1067
+ * `DO_NOT_TRACK` is honoured because it is the cross-tool convention
1068
+ * (consoledonottrack.com); a user who has set it globally has already answered
1069
+ * this question and should not be asked again by us.
1070
+ *
1071
+ * `CI` is refused for a different reason: a build runner is not a person, and
1072
+ * counting one is both useless and misleading. A single pipeline re-running on
1073
+ * every push would otherwise outweigh every real developer in the data.
1074
+ *
1075
+ * A project's own `"telemetry": false` is checked *before* the machine setting,
1076
+ * because it has to beat an individual opt-in to be worth anything — see
1077
+ * project.ts on why the reverse is refused.
1078
+ */
1079
+ function suppressionReason(env = process.env, cwd = process.cwd()) {
1080
+ if (env.DO_NOT_TRACK && env.DO_NOT_TRACK !== "0") return "do_not_track";
1081
+ if (env.REBASE_TELEMETRY_DISABLED && env.REBASE_TELEMETRY_DISABLED !== "0") return "rebase_telemetry_disabled";
1082
+ if (env.CI && env.CI !== "false") return "ci";
1083
+ if (readProjectPolicy(cwd) === "opt_out") return "project_opt_out";
1084
+ const config = readConfig();
1085
+ if (config.enabled === void 0) return "not_asked";
1086
+ if (!config.enabled) return "declined";
1087
+ return null;
1088
+ }
1089
+ function isEnabled(env = process.env, cwd = process.cwd()) {
1090
+ return suppressionReason(env, cwd) === null;
1091
+ }
1092
+ /**
1093
+ * Record the user's answer. `false` is final — nothing prompts again.
1094
+ *
1095
+ * Returns whether the choice could actually be persisted. A read-only or full
1096
+ * home directory makes `writeConfig` throw, and the direction of that failure
1097
+ * matters enormously: someone turning sharing **off** who sees a stack trace,
1098
+ * or worse sees nothing, is left sharing. The caller is expected to say so and
1099
+ * point at `REBASE_TELEMETRY_DISABLED`, which needs no disk.
1100
+ */
1101
+ function setConsent(enabled) {
1102
+ const config = readConfig();
1103
+ try {
1104
+ writeConfig({
1105
+ ...config,
1106
+ enabled,
1107
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
1108
+ machineId: enabled ? config.machineId ?? void 0 : void 0
1109
+ });
1110
+ if (enabled) ensureMachineId();
1111
+ return true;
1112
+ } catch {
1113
+ return false;
1114
+ }
1115
+ }
1116
+ /**
1117
+ * Build the event that *would* be sent, without sending it.
1118
+ *
1119
+ * This is what `rebase telemetry show` prints, and it is deliberately the same
1120
+ * function the sender uses — a preview assembled by separate code is a promise
1121
+ * that drifts. Returns `null` when nothing would be sent, so the command can
1122
+ * say why instead of showing a payload that is never going anywhere.
1123
+ */
1124
+ function previewEvent(event, properties = {}, projectRoot) {
1125
+ const config = readConfig();
1126
+ if (!config.machineId) return null;
1127
+ return buildEvent(event, properties, {
1128
+ machineId: config.machineId,
1129
+ projectId: projectRoot ? ensureProjectId(projectRoot) : void 0
1130
+ });
1131
+ }
1132
+ /**
1133
+ * Send one event, or quietly do nothing.
1134
+ *
1135
+ * Three properties this must hold, in order of importance:
1136
+ *
1137
+ * 1. **It never throws.** Telemetry failing is not a reason for `rebase dev`
1138
+ * to fail. Every error is swallowed.
1139
+ * 2. **It never blocks meaningfully.** A two-second ceiling, after which the
1140
+ * command carries on regardless — a collector having a bad day must not
1141
+ * become the CLI hanging.
1142
+ * 3. **It never sends without consent.** Enforced here rather than at the call
1143
+ * sites, so a new call site cannot get it wrong.
1144
+ */
1145
+ async function recordEvent(event, properties = {}, options = {}) {
1146
+ try {
1147
+ if (!isEnabled(process.env, options.projectRoot ?? process.cwd())) return;
1148
+ const payload = buildEvent(event, properties, {
1149
+ machineId: ensureMachineId(),
1150
+ projectId: options.projectRoot ? ensureProjectId(options.projectRoot) : void 0
1151
+ });
1152
+ const abort = new AbortController();
1153
+ const timer = setTimeout(() => abort.abort(), options.timeoutMs ?? 2e3);
1154
+ try {
1155
+ await fetch(endpoint(), {
1156
+ method: "POST",
1157
+ headers: { "Content-Type": "application/json" },
1158
+ body: JSON.stringify(payload),
1159
+ signal: abort.signal
1160
+ });
1161
+ } finally {
1162
+ clearTimeout(timer);
1163
+ }
1164
+ } catch {}
1165
+ }
1166
+ //#endregion
1167
+ //#region src/telemetry/consent.ts
1168
+ /**
1169
+ * Asking, and what the question looks like.
1170
+ *
1171
+ * ## Why the prompt comes *after* the work
1172
+ *
1173
+ * The first `rebase init` on a machine is the event most worth having, and it
1174
+ * is the one where no consent exists yet. Asking before scaffolding puts a
1175
+ * privacy negotiation in front of someone who has not yet seen the tool do
1176
+ * anything — the worst possible moment, and a reliable way to get a reflexive
1177
+ * no.
1178
+ *
1179
+ * So the question is asked once the project exists and the user has seen it
1180
+ * work. The event's data is still in memory at that point, so nothing is lost
1181
+ * by waiting, and — this is the part that matters — **nothing has been
1182
+ * transmitted or written**. Declining leaves no id, no file, no record.
1183
+ *
1184
+ * ## Why the payload is shown rather than described
1185
+ *
1186
+ * "Anonymous usage data" is a phrase that has been used to mean almost
1187
+ * anything. Printing the exact JSON costs four lines of output and replaces a
1188
+ * claim the user has to take on faith with something they can read. It is also
1189
+ * the same builder the sender uses, so it cannot drift into a comfortable
1190
+ * fiction.
1191
+ */
1192
+ /** True when we may ask: no decision recorded, and nothing else forbids it. */
1193
+ function shouldPrompt(env = process.env) {
1194
+ return suppressionReason(env) === "not_asked" && Boolean(process.stdin.isTTY);
1195
+ }
1196
+ function renderPreview(event, properties) {
1197
+ const preview = buildEvent(event, properties, {
1198
+ machineId: "<random uuid, generated only if you say yes>",
1199
+ projectId: "<random uuid, per checkout>"
1200
+ });
1201
+ return JSON.stringify(preview, null, 2);
1202
+ }
1203
+ /**
1204
+ * Ask, record the answer, and report it.
1205
+ *
1206
+ * Never throws and never blocks a non-interactive run: `rebase init --yes` in
1207
+ * CI must behave exactly as it does today, which means not asking and not
1208
+ * sending.
1209
+ */
1210
+ async function promptForConsent(event, properties) {
1211
+ if (!shouldPrompt()) return false;
1212
+ try {
1213
+ console.log("");
1214
+ console.log(chalk.bold("Help improve Rebase?"));
1215
+ console.log("");
1216
+ console.log(chalk.gray(" Rebase is self-hosted, so we have no idea what works and what does not"));
1217
+ console.log(chalk.gray(" unless you tell us. Sharing is entirely optional and off by default."));
1218
+ console.log("");
1219
+ console.log(chalk.gray(" This is exactly what would be sent — nothing more, ever:"));
1220
+ console.log("");
1221
+ console.log(renderPreview(event, properties).split("\n").map((line) => chalk.gray(" " + line)).join("\n"));
1222
+ console.log("");
1223
+ console.log(chalk.gray(" No project names, paths, schemas, URLs or error messages. Change your"));
1224
+ console.log(chalk.gray(` mind any time with ${chalk.cyan("rebase telemetry disable")}.`));
1225
+ console.log("");
1226
+ const { accepted } = await inquirer.prompt([{
1227
+ type: "confirm",
1228
+ name: "accepted",
1229
+ message: "Share anonymous usage data?",
1230
+ default: false
1231
+ }]);
1232
+ setConsent(Boolean(accepted));
1233
+ console.log(accepted ? chalk.green(" Thank you — sharing enabled.") : chalk.gray(" Nothing will be sent. You will not be asked again."));
1234
+ console.log("");
1235
+ return Boolean(accepted);
1236
+ } catch {
1237
+ return false;
1238
+ }
1239
+ }
1240
+ /** Human-readable current state, for `rebase telemetry status`. */
1241
+ function describeState(env = process.env) {
1242
+ const reason = suppressionReason(env);
1243
+ const config = readConfig();
1244
+ switch (reason) {
1245
+ case null: return `${chalk.green("enabled")} — schema v1, machine id ${chalk.gray(config.machineId ?? "unset")}`;
1246
+ case "not_asked": return `${chalk.yellow("not configured")} — nothing has been sent, and you have not been asked yet`;
1247
+ case "declined": return `${chalk.gray("disabled")} — you declined${config.decidedAt ? ` on ${config.decidedAt.slice(0, 10)}` : ""}`;
1248
+ case "do_not_track": return `${chalk.gray("disabled")} — the ${chalk.cyan("DO_NOT_TRACK")} environment variable is set`;
1249
+ case "rebase_telemetry_disabled": return `${chalk.gray("disabled")} — the ${chalk.cyan("REBASE_TELEMETRY_DISABLED")} environment variable is set`;
1250
+ case "ci": return `${chalk.gray("disabled")} — this looks like CI (${chalk.cyan("CI")} is set), which is never counted`;
1251
+ case "project_opt_out": return `${chalk.gray("disabled")} — this project's ${chalk.cyan("rebase.json")} sets ${chalk.cyan("\"telemetry\": false")}`;
1252
+ }
1253
+ }
891
1254
  //#endregion
892
1255
  //#region src/commands/init.ts
893
1256
  var access = promisify(fs.access);
@@ -1226,6 +1589,7 @@ async function linkScaffoldToCloud(options) {
1226
1589
  }
1227
1590
  }
1228
1591
  async function createProject$1(options) {
1592
+ const startedAt = Date.now();
1229
1593
  if (fs.existsSync(options.targetDirectory)) {
1230
1594
  if (fs.readdirSync(options.targetDirectory).length !== 0) {
1231
1595
  console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
@@ -1418,6 +1782,18 @@ async function createProject$1(options) {
1418
1782
  console.log("");
1419
1783
  console.log(` ${chalk.cyan("rebase skills install")} ${chalk.gray("or")} ${chalk.cyan(pmCommands.run("skills:install").join(" "))}`);
1420
1784
  console.log("");
1785
+ const initProperties = {
1786
+ preset: options.headless ? "none" : options.preset,
1787
+ headless: Boolean(options.headless),
1788
+ package_manager: options.pm,
1789
+ installed_deps: Boolean(options.installDeps),
1790
+ introspected,
1791
+ own_database: Boolean(options.databaseUrl),
1792
+ cloud_linked: Boolean(options.cloudProject),
1793
+ git: Boolean(options.git),
1794
+ duration: durationBucket(Date.now() - startedAt)
1795
+ };
1796
+ if (await promptForConsent("cli.init", initProperties)) await recordEvent("cli.init", initProperties, { projectRoot: options.targetDirectory });
1421
1797
  }
1422
1798
  /**
1423
1799
  * Apply a template preset by replacing the default collection files.
@@ -1565,7 +1941,11 @@ async function replacePlaceholders(options) {
1565
1941
  const prereleasePins = [...unreleased].filter(([, version]) => version === "latest" || version.includes("-"));
1566
1942
  if (cliIsStable && prereleasePins.length > 0) {
1567
1943
  const lines = prereleasePins.map(([name, version]) => ` ${name} → ${version}`).join("\n");
1568
- 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.`);
1944
+ 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
1945
+ not a problem with your machine, your network, or your package manager.
1946
+
1947
+ Stopped before writing dependency versions or installing anything. The
1948
+ project directory ${path.basename(options.targetDirectory)}/ was created and is safe to delete.\nPlease report this with the list above.`);
1569
1949
  }
1570
1950
  for (const [fullPath, originalContent] of fileContents.entries()) {
1571
1951
  let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
@@ -1969,6 +2349,7 @@ async function schemaCommand(subcommand, rawArgs) {
1969
2349
  return;
1970
2350
  }
1971
2351
  const projectRoot = requireProjectRoot();
2352
+ recordEvent("cli.schema_generate", { subcommand: subcommand ?? "none" }, { projectRoot });
1972
2353
  const backendDir = requireBackendDir(projectRoot);
1973
2354
  const activePlugin = getActiveBackendPlugin(backendDir);
1974
2355
  if (!activePlugin) {
@@ -2037,6 +2418,7 @@ async function dbCommand(subcommand, rawArgs) {
2037
2418
  return;
2038
2419
  }
2039
2420
  const projectRoot = requireProjectRoot();
2421
+ recordEvent("cli.db_push", { subcommand: subcommand ?? "none" }, { projectRoot });
2040
2422
  const backendDir = requireBackendDir(projectRoot);
2041
2423
  const activePlugin = getActiveBackendPlugin(backendDir);
2042
2424
  if (!activePlugin) {
@@ -2800,6 +3182,11 @@ async function devCommand(rawArgs) {
2800
3182
  return;
2801
3183
  }
2802
3184
  const projectRoot = requireProjectRoot();
3185
+ recordEvent("cli.dev", {
3186
+ backend_only: Boolean(args["--backend-only"]),
3187
+ frontend_only: Boolean(args["--frontend-only"]),
3188
+ generate: Boolean(args["--generate"])
3189
+ }, { projectRoot });
2803
3190
  const backendDir = findBackendDir(projectRoot);
2804
3191
  const frontendDir = findFrontendDir(projectRoot);
2805
3192
  const backendOnly = args["--backend-only"] || false;
@@ -3877,7 +4264,7 @@ async function buildBundle(options) {
3877
4264
  console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
3878
4265
  console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
3879
4266
  console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
3880
- console.log(chalk.dim(` or run \`rebase eject\` to make this file the entrypoint and own the image.`));
4267
+ console.log(chalk.dim(" or run `rebase eject` to make this file the entrypoint and own the image."));
3881
4268
  }
3882
4269
  log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
3883
4270
  cleanOutDir(projectRoot, outDir);
@@ -4305,7 +4692,7 @@ async function foldFrontendIntoBundle(options) {
4305
4692
  * entrypoint, falls back to the previous behaviour: run every workspace's own
4306
4693
  * `build` script. Nothing that built before stops building.
4307
4694
  */
4308
- function printHelp$4() {
4695
+ function printHelp$5() {
4309
4696
  console.log(`
4310
4697
  ${chalk.bold("rebase build")} — build the apps declared in rebase.json
4311
4698
 
@@ -4340,7 +4727,7 @@ async function buildCommand(rawArgs = []) {
4340
4727
  permissive: true
4341
4728
  });
4342
4729
  if (args["--help"]) {
4343
- printHelp$4();
4730
+ printHelp$5();
4344
4731
  return;
4345
4732
  }
4346
4733
  const projectRoot = requireProjectRoot();
@@ -4579,7 +4966,7 @@ function projectNameOf(projectRoot) {
4579
4966
  } catch {}
4580
4967
  return path.basename(projectRoot);
4581
4968
  }
4582
- function printHelp$3() {
4969
+ function printHelp$4() {
4583
4970
  console.log(`
4584
4971
  ${chalk.bold("rebase eject")} — take ownership of the server process
4585
4972
 
@@ -4606,7 +4993,7 @@ async function ejectCommand(rawArgs = []) {
4606
4993
  permissive: true
4607
4994
  });
4608
4995
  if (args["--help"]) {
4609
- printHelp$3();
4996
+ printHelp$4();
4610
4997
  return;
4611
4998
  }
4612
4999
  const projectRoot = requireProjectRoot();
@@ -4750,7 +5137,7 @@ function restoreBackendScripts(projectRoot) {
4750
5137
  * `rebase.json`) this falls back to the backend workspace's own `start` script,
4751
5138
  * which is what such a project has always used.
4752
5139
  */
4753
- function printHelp$2() {
5140
+ function printHelp$3() {
4754
5141
  console.log(`
4755
5142
  ${chalk.bold("rebase start")} — run a built bundle
4756
5143
 
@@ -4776,7 +5163,7 @@ async function startCommand(rawArgs = []) {
4776
5163
  permissive: true
4777
5164
  });
4778
5165
  if (args["--help"]) {
4779
- printHelp$2();
5166
+ printHelp$3();
4780
5167
  return;
4781
5168
  }
4782
5169
  const projectRoot = requireProjectRoot();
@@ -5640,6 +6027,107 @@ ${chalk.green.bold("Examples")}
5640
6027
  `);
5641
6028
  }
5642
6029
  //#endregion
6030
+ //#region src/commands/telemetry.ts
6031
+ /**
6032
+ * `rebase telemetry` — the command that makes the rest of it inspectable.
6033
+ *
6034
+ * The whole subsystem asks for trust it cannot otherwise earn, and the cheapest
6035
+ * way to earn it is to stop describing the payload and print it. `show` runs
6036
+ * the same builder the sender uses, so what appears here is what would go, not
6037
+ * a documentation comment that quietly fell out of date two releases ago.
6038
+ */
6039
+ async function telemetryCommand(rawArgs) {
6040
+ switch (rawArgs.slice(3).filter((a) => !a.startsWith("-"))[0]) {
6041
+ case "status":
6042
+ case void 0:
6043
+ printStatus();
6044
+ return;
6045
+ case "show":
6046
+ printPayload();
6047
+ return;
6048
+ case "enable":
6049
+ if (!setConsent(true)) {
6050
+ console.error(chalk.red(`Could not write ${configPath()} — sharing was not enabled.`));
6051
+ process.exitCode = 1;
6052
+ return;
6053
+ }
6054
+ console.log(chalk.green("Anonymous usage sharing enabled."));
6055
+ console.log(chalk.gray(`Inspect what gets sent with ${chalk.cyan("rebase telemetry show")}.`));
6056
+ return;
6057
+ case "disable":
6058
+ if (!setConsent(false)) {
6059
+ console.error(chalk.red(`Could not write ${configPath()} — sharing is still ON.`));
6060
+ console.error(chalk.yellow(`Set ${chalk.cyan("REBASE_TELEMETRY_DISABLED=1")} in your environment instead; it needs no file.`));
6061
+ process.exitCode = 1;
6062
+ return;
6063
+ }
6064
+ console.log(chalk.gray("Anonymous usage sharing disabled. Nothing further will be sent."));
6065
+ return;
6066
+ default:
6067
+ printHelp$2();
6068
+ process.exitCode = 1;
6069
+ }
6070
+ }
6071
+ function printStatus() {
6072
+ console.log("");
6073
+ console.log(` Status: ${describeState()}`);
6074
+ if (readProjectPolicy() === "ignored_opt_in") {
6075
+ console.log("");
6076
+ console.log(chalk.yellow(" Note: this project's rebase.json sets \"telemetry\": true, which is ignored."));
6077
+ console.log(chalk.gray(" A committed file cannot consent on behalf of everyone who clones it."));
6078
+ console.log(chalk.gray(` Only "telemetry": false is honoured there. Use ${chalk.cyan("rebase telemetry enable")}.`));
6079
+ }
6080
+ console.log(` Endpoint: ${chalk.gray(endpoint())}`);
6081
+ console.log(` Config: ${chalk.gray(configPath())}`);
6082
+ console.log("");
6083
+ console.log(chalk.gray(` ${chalk.cyan("rebase telemetry show")} prints the exact payload.`));
6084
+ console.log("");
6085
+ }
6086
+ function printPayload() {
6087
+ if (readConfig().enabled !== true) {
6088
+ console.log("");
6089
+ console.log(` ${describeState()}`);
6090
+ console.log("");
6091
+ console.log(chalk.gray(" Nothing is being sent, so there is no payload to show."));
6092
+ console.log(chalk.gray(` Run ${chalk.cyan("rebase telemetry enable")} first if you want to inspect one.`));
6093
+ console.log("");
6094
+ return;
6095
+ }
6096
+ const event = previewEvent("cli.dev", { first_run: false }, process.cwd());
6097
+ console.log("");
6098
+ console.log(chalk.gray(" Sent to ") + chalk.gray(endpoint()) + chalk.gray(", for example:"));
6099
+ console.log("");
6100
+ console.log(JSON.stringify(event, null, 2).split("\n").map((l) => " " + l).join("\n"));
6101
+ console.log("");
6102
+ console.log(chalk.gray(" Both ids are random. Neither is derived from your machine, your"));
6103
+ console.log(chalk.gray(" hostname, your project name or anything you have typed."));
6104
+ console.log("");
6105
+ }
6106
+ function printHelp$2() {
6107
+ console.log(`
6108
+ ${chalk.bold("rebase telemetry")} — anonymous usage sharing (opt-in, off by default)
6109
+
6110
+ ${chalk.bold("Commands")}
6111
+ ${chalk.blue("status")} Whether anything is being shared, and why ${chalk.gray("(default)")}
6112
+ ${chalk.blue("show")} Print the exact payload that would be sent
6113
+ ${chalk.blue("enable")} Start sharing
6114
+ ${chalk.blue("disable")} Stop sharing, permanently
6115
+
6116
+ ${chalk.bold("Project policy")}
6117
+ ${chalk.blue("\"telemetry\": false")} in ${chalk.blue("rebase.json")} disables sharing for everyone who
6118
+ clones the repository, overriding each developer's own choice.
6119
+ ${chalk.gray("\"telemetry\": true is ignored — a committed file cannot consent for others.")}
6120
+
6121
+ ${chalk.bold("Environment")}
6122
+ ${chalk.blue("DO_NOT_TRACK")} Set to disable, across every tool that honours it
6123
+ ${chalk.blue("REBASE_TELEMETRY_DISABLED")} Set to disable Rebase specifically
6124
+ ${chalk.blue("REBASE_TELEMETRY_ENDPOINT")} Send elsewhere — your own collector, for instance
6125
+
6126
+ ${chalk.gray("Never shared: project names, paths, collection or table names, database")}
6127
+ ${chalk.gray("URLs, hostnames, error messages, stack traces, or exact record counts.")}
6128
+ `);
6129
+ }
6130
+ //#endregion
5643
6131
  //#region src/commands/cloud/auth.ts
5644
6132
  /**
5645
6133
  * `rebase cloud` auth subcommands: login, logout, whoami.
@@ -10180,7 +10668,8 @@ async function entry(args) {
10180
10668
  "cloud",
10181
10669
  "apps",
10182
10670
  "eject",
10183
- "generate-sdk"
10671
+ "generate-sdk",
10672
+ "telemetry"
10184
10673
  ].includes(command)) {
10185
10674
  printHelp();
10186
10675
  return;
@@ -10251,6 +10740,9 @@ async function entry(args) {
10251
10740
  case "cloud":
10252
10741
  await cloudCommand(effectiveSubcommand, args);
10253
10742
  break;
10743
+ case "telemetry":
10744
+ await telemetryCommand(args);
10745
+ break;
10254
10746
  default:
10255
10747
  console.error(chalk.red(`Unknown command: ${command}`));
10256
10748
  console.log("");
@@ -10301,6 +10793,7 @@ ${chalk.green.bold("API Keys")}
10301
10793
  ${chalk.blue.bold("api-keys list")} List all service API keys
10302
10794
  ${chalk.blue.bold("api-keys create")} Create a new scoped API key
10303
10795
  ${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
10796
+ ${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
10304
10797
  ${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
10305
10798
 
10306
10799
  ${chalk.green.bold("Rebase Cloud")}
@@ -10314,7 +10807,23 @@ ${chalk.green.bold("Options")}
10314
10807
  ${chalk.blue("--help, -h")} Show this help message
10315
10808
 
10316
10809
  ${chalk.gray("Documentation: https://rebase.pro/docs")}
10317
- `);
10810
+ ${telemetryNotice()}`);
10811
+ }
10812
+ /**
10813
+ * One line about usage sharing, in the global help.
10814
+ *
10815
+ * Every other tool that collects anything prints a first-run notice. Ours asks
10816
+ * at the end of `rebase init` — but someone who installs the CLI and never runs
10817
+ * `init`, or who joins a project someone else scaffolded, would otherwise never
10818
+ * learn the subsystem exists. This is the cheapest place to close that: the
10819
+ * help is what an unfamiliar user reads first.
10820
+ *
10821
+ * It states the current setting rather than a generic sentence, so it is also
10822
+ * the fastest answer to "is this thing on?".
10823
+ */
10824
+ function telemetryNotice() {
10825
+ const sharing = isEnabled();
10826
+ return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
10318
10827
  }
10319
10828
  //#endregion
10320
10829
  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 };