@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 +129 -23
- package/dist/index.es.js.map +1 -1
- package/dist/telemetry/identity.d.ts +11 -3
- package/dist/telemetry/index.d.ts +19 -5
- package/dist/telemetry/payload.d.ts +0 -8
- package/dist/telemetry/project.d.ts +34 -0
- package/package.json +7 -7
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
|
-
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
|
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(
|
|
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 };
|