@rebasepro/cli 0.12.1-canary.g4e7bcbf → 0.12.1-canary.g52d71ee
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/commands/cloud/context.d.ts +25 -0
- package/dist/commands/cloud/projects.d.ts +32 -1
- package/dist/commands/telemetry.d.ts +9 -0
- package/dist/index.es.js +605 -29
- package/dist/index.es.js.map +1 -1
- package/dist/telemetry/consent.d.ts +38 -0
- package/dist/telemetry/identity.d.ts +69 -0
- package/dist/telemetry/index.d.ts +72 -0
- package/dist/telemetry/payload.d.ts +78 -0
- package/dist/telemetry/project.d.ts +34 -0
- package/package.json +7 -7
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.
|
|
@@ -528,33 +528,51 @@ async function requireClient(rawArgs) {
|
|
|
528
528
|
};
|
|
529
529
|
}
|
|
530
530
|
/**
|
|
531
|
-
* The
|
|
532
|
-
* plane (`platform-config`, which derives it from the same TENANT_BASE_DOMAIN
|
|
533
|
-
* 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`).
|
|
534
532
|
*
|
|
535
|
-
*
|
|
536
|
-
* serves tenants at `rebase.website
|
|
537
|
-
*
|
|
538
|
-
*
|
|
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`).
|
|
539
538
|
*
|
|
540
539
|
* Cached per host for the process: it is fixed for a control plane's lifetime,
|
|
541
540
|
* and `projects list` formats one host per row off a single fetch.
|
|
542
541
|
*
|
|
543
|
-
* @returns the
|
|
542
|
+
* @returns the config, or `undefined` if the control plane doesn't serve
|
|
544
543
|
* `platform-config` (an older deployment) or the request failed. A failure is
|
|
545
|
-
* cached too —
|
|
546
|
-
*
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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);
|
|
551
552
|
if (!pending) {
|
|
552
|
-
pending = client.functions.invoke("platform-config", void 0, { method: "GET" }).then((cfg) => cfg
|
|
553
|
-
|
|
553
|
+
pending = client.functions.invoke("platform-config", void 0, { method: "GET" }).then((cfg) => cfg ?? void 0).catch(() => void 0);
|
|
554
|
+
platformConfigCache.set(url, pending);
|
|
554
555
|
}
|
|
555
556
|
return pending;
|
|
556
557
|
}
|
|
557
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
|
+
/**
|
|
558
576
|
* Public host for a project — `<subdomain>.<base>`, or the bare subdomain when
|
|
559
577
|
* the base domain is unknown.
|
|
560
578
|
*
|
|
@@ -888,6 +906,369 @@ function openUrl(target, label = "Opening") {
|
|
|
888
906
|
child.unref();
|
|
889
907
|
} catch {}
|
|
890
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
|
+
}
|
|
891
1272
|
//#endregion
|
|
892
1273
|
//#region src/commands/init.ts
|
|
893
1274
|
var access = promisify(fs.access);
|
|
@@ -1226,6 +1607,7 @@ async function linkScaffoldToCloud(options) {
|
|
|
1226
1607
|
}
|
|
1227
1608
|
}
|
|
1228
1609
|
async function createProject$1(options) {
|
|
1610
|
+
const startedAt = Date.now();
|
|
1229
1611
|
if (fs.existsSync(options.targetDirectory)) {
|
|
1230
1612
|
if (fs.readdirSync(options.targetDirectory).length !== 0) {
|
|
1231
1613
|
console.error(`${chalk.red.bold("ERROR")} Directory "${options.projectName}" already exists and is not empty`);
|
|
@@ -1418,6 +1800,18 @@ async function createProject$1(options) {
|
|
|
1418
1800
|
console.log("");
|
|
1419
1801
|
console.log(` ${chalk.cyan("rebase skills install")} ${chalk.gray("or")} ${chalk.cyan(pmCommands.run("skills:install").join(" "))}`);
|
|
1420
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 });
|
|
1421
1815
|
}
|
|
1422
1816
|
/**
|
|
1423
1817
|
* Apply a template preset by replacing the default collection files.
|
|
@@ -1565,7 +1959,11 @@ async function replacePlaceholders(options) {
|
|
|
1565
1959
|
const prereleasePins = [...unreleased].filter(([, version]) => version === "latest" || version.includes("-"));
|
|
1566
1960
|
if (cliIsStable && prereleasePins.length > 0) {
|
|
1567
1961
|
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
|
|
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.`);
|
|
1569
1967
|
}
|
|
1570
1968
|
for (const [fullPath, originalContent] of fileContents.entries()) {
|
|
1571
1969
|
let content = originalContent.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
|
|
@@ -1969,6 +2367,7 @@ async function schemaCommand(subcommand, rawArgs) {
|
|
|
1969
2367
|
return;
|
|
1970
2368
|
}
|
|
1971
2369
|
const projectRoot = requireProjectRoot();
|
|
2370
|
+
recordEvent("cli.schema_generate", { subcommand: subcommand ?? "none" }, { projectRoot });
|
|
1972
2371
|
const backendDir = requireBackendDir(projectRoot);
|
|
1973
2372
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
1974
2373
|
if (!activePlugin) {
|
|
@@ -2037,6 +2436,7 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
2037
2436
|
return;
|
|
2038
2437
|
}
|
|
2039
2438
|
const projectRoot = requireProjectRoot();
|
|
2439
|
+
recordEvent("cli.db_push", { subcommand: subcommand ?? "none" }, { projectRoot });
|
|
2040
2440
|
const backendDir = requireBackendDir(projectRoot);
|
|
2041
2441
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
2042
2442
|
if (!activePlugin) {
|
|
@@ -2800,6 +3200,11 @@ async function devCommand(rawArgs) {
|
|
|
2800
3200
|
return;
|
|
2801
3201
|
}
|
|
2802
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 });
|
|
2803
3208
|
const backendDir = findBackendDir(projectRoot);
|
|
2804
3209
|
const frontendDir = findFrontendDir(projectRoot);
|
|
2805
3210
|
const backendOnly = args["--backend-only"] || false;
|
|
@@ -3877,7 +4282,7 @@ async function buildBundle(options) {
|
|
|
3877
4282
|
console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
|
|
3878
4283
|
console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
|
|
3879
4284
|
console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
|
|
3880
|
-
console.log(chalk.dim(
|
|
4285
|
+
console.log(chalk.dim(" or run `rebase eject` to make this file the entrypoint and own the image."));
|
|
3881
4286
|
}
|
|
3882
4287
|
log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
|
|
3883
4288
|
cleanOutDir(projectRoot, outDir);
|
|
@@ -4305,7 +4710,7 @@ async function foldFrontendIntoBundle(options) {
|
|
|
4305
4710
|
* entrypoint, falls back to the previous behaviour: run every workspace's own
|
|
4306
4711
|
* `build` script. Nothing that built before stops building.
|
|
4307
4712
|
*/
|
|
4308
|
-
function printHelp$
|
|
4713
|
+
function printHelp$5() {
|
|
4309
4714
|
console.log(`
|
|
4310
4715
|
${chalk.bold("rebase build")} — build the apps declared in rebase.json
|
|
4311
4716
|
|
|
@@ -4340,7 +4745,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
4340
4745
|
permissive: true
|
|
4341
4746
|
});
|
|
4342
4747
|
if (args["--help"]) {
|
|
4343
|
-
printHelp$
|
|
4748
|
+
printHelp$5();
|
|
4344
4749
|
return;
|
|
4345
4750
|
}
|
|
4346
4751
|
const projectRoot = requireProjectRoot();
|
|
@@ -4579,7 +4984,7 @@ function projectNameOf(projectRoot) {
|
|
|
4579
4984
|
} catch {}
|
|
4580
4985
|
return path.basename(projectRoot);
|
|
4581
4986
|
}
|
|
4582
|
-
function printHelp$
|
|
4987
|
+
function printHelp$4() {
|
|
4583
4988
|
console.log(`
|
|
4584
4989
|
${chalk.bold("rebase eject")} — take ownership of the server process
|
|
4585
4990
|
|
|
@@ -4606,7 +5011,7 @@ async function ejectCommand(rawArgs = []) {
|
|
|
4606
5011
|
permissive: true
|
|
4607
5012
|
});
|
|
4608
5013
|
if (args["--help"]) {
|
|
4609
|
-
printHelp$
|
|
5014
|
+
printHelp$4();
|
|
4610
5015
|
return;
|
|
4611
5016
|
}
|
|
4612
5017
|
const projectRoot = requireProjectRoot();
|
|
@@ -4750,7 +5155,7 @@ function restoreBackendScripts(projectRoot) {
|
|
|
4750
5155
|
* `rebase.json`) this falls back to the backend workspace's own `start` script,
|
|
4751
5156
|
* which is what such a project has always used.
|
|
4752
5157
|
*/
|
|
4753
|
-
function printHelp$
|
|
5158
|
+
function printHelp$3() {
|
|
4754
5159
|
console.log(`
|
|
4755
5160
|
${chalk.bold("rebase start")} — run a built bundle
|
|
4756
5161
|
|
|
@@ -4776,7 +5181,7 @@ async function startCommand(rawArgs = []) {
|
|
|
4776
5181
|
permissive: true
|
|
4777
5182
|
});
|
|
4778
5183
|
if (args["--help"]) {
|
|
4779
|
-
printHelp$
|
|
5184
|
+
printHelp$3();
|
|
4780
5185
|
return;
|
|
4781
5186
|
}
|
|
4782
5187
|
const projectRoot = requireProjectRoot();
|
|
@@ -5640,6 +6045,107 @@ ${chalk.green.bold("Examples")}
|
|
|
5640
6045
|
`);
|
|
5641
6046
|
}
|
|
5642
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
|
|
5643
6149
|
//#region src/commands/cloud/auth.ts
|
|
5644
6150
|
/**
|
|
5645
6151
|
* `rebase cloud` auth subcommands: login, logout, whoami.
|
|
@@ -5932,6 +6438,54 @@ function providerDefaults(provider) {
|
|
|
5932
6438
|
};
|
|
5933
6439
|
}
|
|
5934
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
|
+
}
|
|
5935
6489
|
async function createProject(rawArgs) {
|
|
5936
6490
|
const args = arg({
|
|
5937
6491
|
"--name": String,
|
|
@@ -5967,9 +6521,10 @@ async function createProject(rawArgs) {
|
|
|
5967
6521
|
const subdomain = (args["--subdomain"] || a.subdomain || "").trim().toLowerCase();
|
|
5968
6522
|
const gitRepoUrl = (args["--repo"] || a.repo || "").trim();
|
|
5969
6523
|
const gitBranch = (args["--branch"] || a.branch || "main").trim();
|
|
5970
|
-
const
|
|
6524
|
+
const target = await resolveRequestedTarget(client, url, (args["--provider"] || a.provider)?.trim() || void 0);
|
|
6525
|
+
const provider = target.provider;
|
|
5971
6526
|
const defaults = providerDefaults(provider);
|
|
5972
|
-
const region = (args["--region"] || defaults.region).trim();
|
|
6527
|
+
const region = (args["--region"] || target.region || defaults.region).trim();
|
|
5973
6528
|
const vmSize = (args["--vm-size"] || defaults.vmSize).trim();
|
|
5974
6529
|
if (!name || !subdomain) fail("Name and subdomain are required.");
|
|
5975
6530
|
try {
|
|
@@ -10180,7 +10735,8 @@ async function entry(args) {
|
|
|
10180
10735
|
"cloud",
|
|
10181
10736
|
"apps",
|
|
10182
10737
|
"eject",
|
|
10183
|
-
"generate-sdk"
|
|
10738
|
+
"generate-sdk",
|
|
10739
|
+
"telemetry"
|
|
10184
10740
|
].includes(command)) {
|
|
10185
10741
|
printHelp();
|
|
10186
10742
|
return;
|
|
@@ -10251,6 +10807,9 @@ async function entry(args) {
|
|
|
10251
10807
|
case "cloud":
|
|
10252
10808
|
await cloudCommand(effectiveSubcommand, args);
|
|
10253
10809
|
break;
|
|
10810
|
+
case "telemetry":
|
|
10811
|
+
await telemetryCommand(args);
|
|
10812
|
+
break;
|
|
10254
10813
|
default:
|
|
10255
10814
|
console.error(chalk.red(`Unknown command: ${command}`));
|
|
10256
10815
|
console.log("");
|
|
@@ -10301,6 +10860,7 @@ ${chalk.green.bold("API Keys")}
|
|
|
10301
10860
|
${chalk.blue.bold("api-keys list")} List all service API keys
|
|
10302
10861
|
${chalk.blue.bold("api-keys create")} Create a new scoped API key
|
|
10303
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)
|
|
10304
10864
|
${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
|
|
10305
10865
|
|
|
10306
10866
|
${chalk.green.bold("Rebase Cloud")}
|
|
@@ -10314,7 +10874,23 @@ ${chalk.green.bold("Options")}
|
|
|
10314
10874
|
${chalk.blue("--help, -h")} Show this help message
|
|
10315
10875
|
|
|
10316
10876
|
${chalk.gray("Documentation: https://rebase.pro/docs")}
|
|
10317
|
-
`);
|
|
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`);
|
|
10318
10894
|
}
|
|
10319
10895
|
//#endregion
|
|
10320
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 };
|