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