@rebasepro/cli 0.12.1-canary.gdd7172e → 0.12.1-canary.ge191fb7

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
@@ -904,10 +904,26 @@ function durationBucket(ms) {
904
904
  * turning into an incident. Strings are capped at 64 characters because no
905
905
  * legitimate enumerated value is longer, and a path or a message always is.
906
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
+ ]);
907
923
  function sanitize(properties) {
908
924
  const out = {};
909
925
  for (const [key, value] of Object.entries(properties)) {
910
- if (!/^[a-z][a-z0-9_]{0,31}$/.test(key)) continue;
926
+ if (!/^[a-z][a-z0-9_]{0,31}$/.test(key) || RESERVED_KEYS.has(key.toLowerCase())) continue;
911
927
  if (typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) out[key] = value;
912
928
  else if (typeof value === "string" && value.length > 0 && value.length <= 64) {
913
929
  if (!/[/\\@:\s]/.test(value)) out[key] = value;
@@ -982,16 +998,29 @@ function ensureMachineId() {
982
998
  /**
983
999
  * The checkout's id, generating one if this project has none.
984
1000
  *
985
- * Returns `undefined` when there is nowhere to put it — `.rebase/` is created
986
- * by `rebase init`, so a command run outside a project simply reports no
987
- * project, rather than scattering directories across the filesystem.
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.
988
1012
  */
989
1013
  function ensureProjectId(projectRoot) {
1014
+ if (!fs.existsSync(path.join(projectRoot, "rebase.json"))) return void 0;
990
1015
  const dir = path.join(projectRoot, ".rebase");
991
- if (!fs.existsSync(dir)) return void 0;
1016
+ try {
1017
+ fs.mkdirSync(dir, { recursive: true });
1018
+ } catch {
1019
+ return;
1020
+ }
992
1021
  const file = path.join(dir, "state.json");
993
1022
  let state = {};
994
- try {
1023
+ if (fs.existsSync(file)) try {
995
1024
  const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
996
1025
  if (raw && typeof raw === "object") state = raw;
997
1026
  } catch {
@@ -1009,6 +1038,22 @@ function ensureProjectId(projectRoot) {
1009
1038
  }
1010
1039
  return projectId;
1011
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
+ }
1012
1057
  function endpoint() {
1013
1058
  return process.env.REBASE_TELEMETRY_ENDPOINT?.trim() || "https://app.rebase.pro/api/functions/telemetry";
1014
1059
  }
@@ -1026,29 +1071,47 @@ function endpoint() {
1026
1071
  * `CI` is refused for a different reason: a build runner is not a person, and
1027
1072
  * counting one is both useless and misleading. A single pipeline re-running on
1028
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.
1029
1078
  */
1030
- function suppressionReason(env = process.env) {
1079
+ function suppressionReason(env = process.env, cwd = process.cwd()) {
1031
1080
  if (env.DO_NOT_TRACK && env.DO_NOT_TRACK !== "0") return "do_not_track";
1032
1081
  if (env.REBASE_TELEMETRY_DISABLED && env.REBASE_TELEMETRY_DISABLED !== "0") return "rebase_telemetry_disabled";
1033
1082
  if (env.CI && env.CI !== "false") return "ci";
1083
+ if (readProjectPolicy(cwd) === "opt_out") return "project_opt_out";
1034
1084
  const config = readConfig();
1035
1085
  if (config.enabled === void 0) return "not_asked";
1036
1086
  if (!config.enabled) return "declined";
1037
1087
  return null;
1038
1088
  }
1039
- function isEnabled(env = process.env) {
1040
- return suppressionReason(env) === null;
1089
+ function isEnabled(env = process.env, cwd = process.cwd()) {
1090
+ return suppressionReason(env, cwd) === null;
1041
1091
  }
1042
- /** Record the user's answer. `false` is final — nothing prompts again. */
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
+ */
1043
1101
  function setConsent(enabled) {
1044
1102
  const config = readConfig();
1045
- writeConfig({
1046
- ...config,
1047
- enabled,
1048
- decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
1049
- machineId: enabled ? config.machineId ?? void 0 : void 0
1050
- });
1051
- if (enabled) ensureMachineId();
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
+ }
1052
1115
  }
1053
1116
  /**
1054
1117
  * Build the event that *would* be sent, without sending it.
@@ -1081,7 +1144,7 @@ function previewEvent(event, properties = {}, projectRoot) {
1081
1144
  */
1082
1145
  async function recordEvent(event, properties = {}, options = {}) {
1083
1146
  try {
1084
- if (!isEnabled()) return;
1147
+ if (!isEnabled(process.env, options.projectRoot ?? process.cwd())) return;
1085
1148
  const payload = buildEvent(event, properties, {
1086
1149
  machineId: ensureMachineId(),
1087
1150
  projectId: options.projectRoot ? ensureProjectId(options.projectRoot) : void 0
@@ -1185,6 +1248,7 @@ function describeState(env = process.env) {
1185
1248
  case "do_not_track": return `${chalk.gray("disabled")} — the ${chalk.cyan("DO_NOT_TRACK")} environment variable is set`;
1186
1249
  case "rebase_telemetry_disabled": return `${chalk.gray("disabled")} — the ${chalk.cyan("REBASE_TELEMETRY_DISABLED")} environment variable is set`;
1187
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")}`;
1188
1252
  }
1189
1253
  }
1190
1254
  //#endregion
@@ -1877,7 +1941,11 @@ async function replacePlaceholders(options) {
1877
1941
  const prereleasePins = [...unreleased].filter(([, version]) => version === "latest" || version.includes("-"));
1878
1942
  if (cliIsStable && prereleasePins.length > 0) {
1879
1943
  const lines = prereleasePins.map(([name, version]) => ` ${name} → ${version}`).join("\n");
1880
- 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.`);
1881
1949
  }
1882
1950
  for (const [fullPath, originalContent] of fileContents.entries()) {
1883
1951
  let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
@@ -2281,6 +2349,7 @@ async function schemaCommand(subcommand, rawArgs) {
2281
2349
  return;
2282
2350
  }
2283
2351
  const projectRoot = requireProjectRoot();
2352
+ recordEvent("cli.schema_generate", { subcommand: subcommand ?? "none" }, { projectRoot });
2284
2353
  const backendDir = requireBackendDir(projectRoot);
2285
2354
  const activePlugin = getActiveBackendPlugin(backendDir);
2286
2355
  if (!activePlugin) {
@@ -2349,6 +2418,7 @@ async function dbCommand(subcommand, rawArgs) {
2349
2418
  return;
2350
2419
  }
2351
2420
  const projectRoot = requireProjectRoot();
2421
+ recordEvent("cli.db_push", { subcommand: subcommand ?? "none" }, { projectRoot });
2352
2422
  const backendDir = requireBackendDir(projectRoot);
2353
2423
  const activePlugin = getActiveBackendPlugin(backendDir);
2354
2424
  if (!activePlugin) {
@@ -4194,7 +4264,7 @@ async function buildBundle(options) {
4194
4264
  console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
4195
4265
  console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
4196
4266
  console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
4197
- 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."));
4198
4268
  }
4199
4269
  log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
4200
4270
  cleanOutDir(projectRoot, outDir);
@@ -5976,12 +6046,21 @@ async function telemetryCommand(rawArgs) {
5976
6046
  printPayload();
5977
6047
  return;
5978
6048
  case "enable":
5979
- setConsent(true);
6049
+ if (!setConsent(true)) {
6050
+ console.error(chalk.red(`Could not write ${configPath()} — sharing was not enabled.`));
6051
+ process.exitCode = 1;
6052
+ return;
6053
+ }
5980
6054
  console.log(chalk.green("Anonymous usage sharing enabled."));
5981
6055
  console.log(chalk.gray(`Inspect what gets sent with ${chalk.cyan("rebase telemetry show")}.`));
5982
6056
  return;
5983
6057
  case "disable":
5984
- setConsent(false);
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
+ }
5985
6064
  console.log(chalk.gray("Anonymous usage sharing disabled. Nothing further will be sent."));
5986
6065
  return;
5987
6066
  default:
@@ -5992,6 +6071,12 @@ async function telemetryCommand(rawArgs) {
5992
6071
  function printStatus() {
5993
6072
  console.log("");
5994
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
+ }
5995
6080
  console.log(` Endpoint: ${chalk.gray(endpoint())}`);
5996
6081
  console.log(` Config: ${chalk.gray(configPath())}`);
5997
6082
  console.log("");
@@ -6028,6 +6113,11 @@ ${chalk.bold("Commands")}
6028
6113
  ${chalk.blue("enable")} Start sharing
6029
6114
  ${chalk.blue("disable")} Stop sharing, permanently
6030
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
+
6031
6121
  ${chalk.bold("Environment")}
6032
6122
  ${chalk.blue("DO_NOT_TRACK")} Set to disable, across every tool that honours it
6033
6123
  ${chalk.blue("REBASE_TELEMETRY_DISABLED")} Set to disable Rebase specifically
@@ -10717,7 +10807,23 @@ ${chalk.green.bold("Options")}
10717
10807
  ${chalk.blue("--help, -h")} Show this help message
10718
10808
 
10719
10809
  ${chalk.gray("Documentation: https://rebase.pro/docs")}
10720
- `);
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`);
10721
10827
  }
10722
10828
  //#endregion
10723
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 };