@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 CHANGED
@@ -88,4 +88,30 @@ if (!existsSync(distEntry)) {
88
88
 
89
89
  const { entry } = await import("../dist/index.es.js");
90
90
 
91
- entry(process.argv);
91
+ /**
92
+ * The CLI's last line of defence.
93
+ *
94
+ * `entry()` returns a promise and nothing was awaiting it, so anything a
95
+ * command threw surfaced as an unhandled rejection: Node's own stack trace,
96
+ * rooted in `dist/index.es.js`, with the CLI's bundled line numbers and no
97
+ * exit code of its own. "Collections directory not found" is a sentence a
98
+ * developer can act on; the same sentence under ten frames of bundle internals
99
+ * reads as a crash in Rebase.
100
+ *
101
+ * The message is the error's own — commands that already print something
102
+ * friendly and exit never reach here. The stack is available behind
103
+ * `--debug`/`REBASE_DEBUG`, because when the message is *not* enough that is
104
+ * the only thing that helps.
105
+ */
106
+ const wantsStack = process.argv.includes("--debug") || process.env.REBASE_DEBUG === "1";
107
+
108
+ entry(process.argv).catch((error) => {
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ process.stderr.write(`\x1b[31m✗ ${message}\x1b[0m\n`);
111
+ if (wantsStack && error instanceof Error && error.stack) {
112
+ process.stderr.write(`\n${error.stack}\n`);
113
+ } else {
114
+ process.stderr.write("\x1b[90m Re-run with --debug for the stack trace.\x1b[0m\n");
115
+ }
116
+ process.exit(1);
117
+ });
package/dist/bundle.d.ts CHANGED
@@ -68,6 +68,41 @@ export declare function detectNativeDependencies(projectRoot: string, declared:
68
68
  * own config package already travels inside the bundle.
69
69
  */
70
70
  export declare function collectDeclaredDependencies(projectRoot: string): Record<string, string>;
71
+ /** One `@rebasepro/*` dependency as some package.json in the project declares it. */
72
+ export interface DeclaredFrameworkDep {
73
+ name: string;
74
+ range: string;
75
+ /** Project-relative package.json it was declared in. */
76
+ file: string;
77
+ }
78
+ export interface FrameworkDepDrift {
79
+ /** Declared at a version that can never reach the CLI's own. */
80
+ behind: DeclaredFrameworkDep[];
81
+ /**
82
+ * The distinct lower bounds found across all declared `@rebasepro/*`, when
83
+ * there is more than one — the project is pinning mixed-era framework
84
+ * packages against each other.
85
+ */
86
+ disagreeing: string[];
87
+ }
88
+ /**
89
+ * Find `@rebasepro/*` dependencies pinned to a version older than this CLI.
90
+ *
91
+ * This is the only place a developer can be told. In development, every
92
+ * `@rebasepro/*` resolves through pnpm's `link:`/`workspace:` overrides to the
93
+ * checkout, so the version STRINGS in package.json are never exercised — the
94
+ * project runs fine locally on whatever is on disk, and the declared numbers are
95
+ * first honoured when the runtime npm-installs them from a bundle in the cloud.
96
+ * A project scaffolded at 0.10.0 therefore keeps working on a developer's
97
+ * machine indefinitely while being, in the cloud, a 0.10.0 driver.
98
+ *
99
+ * That matters because the image supplies only `@rebasepro/server`; the database
100
+ * driver comes from these declarations and a newer runtime never updates it.
101
+ * Every package.json is scanned, `dependencies` and `devDependencies` both,
102
+ * because they have to be bumped together and the one that gets forgotten is the
103
+ * one nobody looks at.
104
+ */
105
+ export declare function detectFrameworkDepDrift(projectRoot: string, cliVersion: string): FrameworkDepDrift;
71
106
  /**
72
107
  * Rewrite relative import specifiers in emitted JavaScript so Node can resolve them.
73
108
  *
@@ -182,3 +217,4 @@ export declare function buildStaticBundle(options: {
182
217
  manifest: RebaseBundleManifest;
183
218
  fileCount: number;
184
219
  };
220
+ export declare function resolveCliVersion(): string;
@@ -24,7 +24,32 @@ export declare function requireClient(rawArgs: string[]): Promise<{
24
24
  client: CloudClient;
25
25
  url: string;
26
26
  }>;
27
+ /** One place a deploy can actually land, as the control plane describes it. */
28
+ export interface DeployTarget {
29
+ clusterId?: string | null;
30
+ provider: string;
31
+ region?: string;
32
+ label?: string;
33
+ baseDomain?: string;
34
+ }
35
+ export interface PlatformConfig {
36
+ tenantBaseDomain?: string;
37
+ deployTargets?: DeployTarget[];
38
+ }
39
+ export declare function fetchPlatformConfig(client: CloudClient, url: string): Promise<PlatformConfig | undefined>;
40
+ /**
41
+ * The base domain tenant projects are served at, derived from the same
42
+ * TENANT_BASE_DOMAIN the ingress and the console read (see
43
+ * saas/backend/src/utils/tenant-domain.ts).
44
+ */
27
45
  export declare function fetchTenantBaseDomain(client: CloudClient, url: string): Promise<string | undefined>;
46
+ /**
47
+ * The infrastructure a deploy for this control plane would ACTUALLY use, in the
48
+ * resolver's own preference order (saas/backend/src/k8s/resolve.ts).
49
+ *
50
+ * @returns the targets, or `undefined` when the control plane cannot say.
51
+ */
52
+ export declare function fetchDeployTargets(client: CloudClient, url: string): Promise<DeployTarget[] | undefined>;
28
53
  /**
29
54
  * Public host for a project — `<subdomain>.<base>`, or the bare subdomain when
30
55
  * the base domain is unknown.
@@ -84,6 +109,39 @@ export interface ProjectLink {
84
109
  export declare function readLink(cwd?: string): ProjectLink | null;
85
110
  export declare function writeLink(link: ProjectLink, cwd?: string): void;
86
111
  export declare function removeLink(cwd?: string): boolean;
112
+ /**
113
+ * Flags that may appear anywhere on a `rebase cloud` line, including *before*
114
+ * the resource group.
115
+ *
116
+ * They have to be declared wherever positionals are resolved, because `arg`'s
117
+ * `permissive: true` does not merely tolerate an undeclared flag — it pushes it
118
+ * into `_` alongside the positionals, and for a flag that takes a value it
119
+ * pushes the value in too. So `cloud --project acme storage create` parsed
120
+ * without this spec yields `_` of `["--project", "acme", "storage", "create"]`,
121
+ * and the group reads as `"acme"`: a real project name, in the group position,
122
+ * dispatching to nothing. Skipping tokens that start with `-` does not save you
123
+ * there — the damage is the orphaned value, which looks exactly like a
124
+ * positional.
125
+ *
126
+ * Only genuinely global flags belong here. Group-specific ones (`--bucket`,
127
+ * `--region`, …) are declared by the handler that owns them and always follow
128
+ * the group, so they cannot shift the group or action.
129
+ *
130
+ * `-p` is `--project` in eighteen places and `--password` in `login`. That
131
+ * ambiguity does not matter to the one caller that reads this spec: it resolves
132
+ * positionals and never looks at a flag's value, so all it needs to know is
133
+ * that `-p` takes one. Anything that wants the value must keep declaring it
134
+ * itself, with the meaning its own command gives it.
135
+ */
136
+ export declare const GLOBAL_CLOUD_FLAGS: {
137
+ readonly "--json": BooleanConstructor;
138
+ readonly "--yes": BooleanConstructor;
139
+ readonly "--help": BooleanConstructor;
140
+ readonly "--project": StringConstructor;
141
+ readonly "-p": "--project";
142
+ readonly "-y": "--yes";
143
+ readonly "-h": "--help";
144
+ };
87
145
  /**
88
146
  * The raw project reference to operate on: explicit `--project` flag wins,
89
147
  * otherwise the linked project. Exits with guidance when neither is present.
@@ -134,6 +192,26 @@ export declare function printJson(value: unknown): void;
134
192
  * call is what guarantees a command can never print a table AND a JSON blob.
135
193
  */
136
194
  export declare function emit(human: () => void, json: unknown): void;
195
+ /**
196
+ * Print a warning (+ optional hint) — in every output mode, always to stderr.
197
+ *
198
+ * `emit` is for a command's *result*, and JSON mode legitimately replaces the
199
+ * human rendering of one. A warning is not a result: it says the command is
200
+ * about to do something the caller may not have meant, and that is exactly as
201
+ * true when the output is piped. Gating one on `!isJsonMode()` deleted it
202
+ * precisely where nobody was watching the terminal — a `--source` deploy ejected
203
+ * a live project off the managed runtime and said so only to a TTY that wasn't
204
+ * there.
205
+ *
206
+ * stdout carries the JSON value and nothing else, so warnings go to stderr:
207
+ * a machine parser reading stdout cannot be corrupted by one. Only the
208
+ * *formatting* may depend on the mode — colour and indentation for a terminal,
209
+ * plain ASCII otherwise. Whether a warning is emitted at all may not.
210
+ *
211
+ * Anything a caller might branch on belongs in the JSON payload as well; stderr
212
+ * is for whoever reads the transcript afterwards.
213
+ */
214
+ export declare function warn(message: string, hint?: string): void;
137
215
  /** Print an error (+ optional hint) and exit non-zero. Never returns. */
138
216
  export declare function fail(message: string, hint?: string, code?: string): never;
139
217
  /**
@@ -46,5 +46,69 @@ export declare function timeAgo(value: string | Date | undefined, now: Date): st
46
46
  export declare function isManagedProject(project: DeployProjectRow | undefined, latest: DeploySourceRow | undefined): boolean;
47
47
  /** What a `deploy` with nothing attached will build, in the words to print. */
48
48
  export declare function planBareDeploy(project: DeployProjectRow | undefined, latest: DeploySourceRow | undefined, now: Date): BareDeployPlan;
49
+ /**
50
+ * A warning attached to a deploy: printed for the human, carried in the JSON.
51
+ *
52
+ * `code` is the stable half — the message is prose and will be reworded, so it
53
+ * is the code that CI or an agent branches on.
54
+ */
55
+ export interface DeployWarning {
56
+ code: string;
57
+ message: string;
58
+ hint?: string;
59
+ }
60
+ /** `code` of the warning below, and the field name it sets in the payload. */
61
+ export declare const EJECTS_MANAGED_RUNTIME = "ejects_managed_runtime";
62
+ /** The one sentence that says a source build undoes `runtimeMode: managed`. */
63
+ export declare function ejectWarning(projectRef: string): DeployWarning;
64
+ /** How a container-image deploy was asked for — the input to both rules below. */
65
+ export interface EjectContext {
66
+ /** The project currently runs on the managed runtime. */
67
+ managed: boolean;
68
+ /** `--source` was passed: build this directory. */
69
+ source: boolean;
70
+ /** `--force` was passed: eject on purpose. */
71
+ force: boolean;
72
+ }
73
+ /**
74
+ * Why a container-image deploy of a managed project is refused — or `undefined`
75
+ * to let it through.
76
+ *
77
+ * Every path below this point builds a container image, and a successful one
78
+ * sets `runtimeMode: "custom"` server-side. So the question is never "which flag
79
+ * was used" but "did the caller ask to leave the managed runtime", and only
80
+ * `--force` answers it.
81
+ *
82
+ * `--source` used to be read as answering it too, on the theory that uploading a
83
+ * build context is self-evidently a deliberate eject. It is not: `--source`
84
+ * picks *which source* gets built — this directory, rather than the stale
85
+ * archive the control plane is holding — and the eject is a side effect of the
86
+ * answer. That is exactly how a live project got flipped to `custom` by someone
87
+ * whose actual intent was "deploy what I have here", and it is the same
88
+ * ignorance the bare form is refused for. Same ignorance, same refusal.
89
+ */
90
+ export declare function ejectRefusal(opts: EjectContext, projectRef: string): {
91
+ message: string;
92
+ hint: string;
93
+ code: string;
94
+ } | undefined;
95
+ /**
96
+ * Which warnings a container-image deploy has earned.
97
+ *
98
+ * Pure, and separate from the printing, because the printing is what went
99
+ * wrong: the eject warning used to be written inline behind `!isJsonMode()`, so
100
+ * the fact that a deploy ejects a managed project existed only as a side effect
101
+ * of a TTY being attached. Deciding here, emitting once at the call site, means
102
+ * the decision cannot be output-mode-dependent again.
103
+ *
104
+ * The condition is just `managed`: anything reaching this point is a container
105
+ * image build that `ejectRefusal` has already let through, and on a managed
106
+ * project that is an eject however it was spelled. A caller who passed `--force`
107
+ * knows — the warning is for the transcript and the payload, which is what
108
+ * anyone reviewing the deploy afterwards actually reads.
109
+ */
110
+ export declare function deployWarnings(opts: EjectContext, projectRef: string): DeployWarning[];
111
+ /** The warning half of a deploy's JSON payload — merged into whatever it emits. */
112
+ export declare function warningPayload(warnings: DeployWarning[]): Record<string, unknown>;
49
113
  export declare function deployCommand(rawArgs: string[], projectRef: string): Promise<void>;
50
114
  export declare function logsCommand(rawArgs: string[], projectRef: string): Promise<void>;
@@ -1 +1,24 @@
1
+ /**
2
+ * Positional tokens after `rebase cloud` (group, action, …).
3
+ *
4
+ * Two things stop a flag being mistaken for the group. `GLOBAL_CLOUD_FLAGS` is
5
+ * declared so `arg` *consumes* the flags that may precede it — critically
6
+ * together with their values, which is the half that filtering cannot do. The
7
+ * leading-`-` skip then covers a flag nobody declared, so an unrecognised
8
+ * boolean shifts nothing.
9
+ *
10
+ * Only leading tokens are skipped: past the group and action, an undeclared
11
+ * flag and its value are somebody else's positionals and none of our business.
12
+ * A flag this file has never heard of, that takes a value, placed before the
13
+ * group, is the one shape still unresolvable here — there is no way to know
14
+ * whether the token after it is its value or the group, and guessing either way
15
+ * is worse than the handler reporting an unknown group.
16
+ *
17
+ * Exported so its tests can drive the real thing. The dispatch test used to
18
+ * re-implement it locally as `slice(3).filter(a => !a.startsWith("-"))` — which
19
+ * filtered flags, while this function did not — so the test asserted the
20
+ * behaviour we wanted against a copy that had it, and stayed green for as long
21
+ * as the real dispatcher was broken.
22
+ */
23
+ export declare function positionals(rawArgs: string[]): string[];
1
24
  export declare function cloudCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void>;
@@ -1,5 +1,36 @@
1
- import { type CloudClient } from "./context";
1
+ import { type DeployTarget, type CloudClient } from "./context";
2
2
  export declare function listProjects(rawArgs: string[]): Promise<void>;
3
+ /**
4
+ * Where this project says it runs.
5
+ *
6
+ * `provider`/`region` are a *request*: no code downstream reads them to pick a
7
+ * deploy target — that comes from the project's cluster record or the ambient
8
+ * in-cluster context (saas/backend/src/k8s/resolve.ts). So a wrong value here is
9
+ * never contradicted by a failure; it just sits in the record. The CLI used to
10
+ * default to `hetzner`/`nbg1` unconditionally, which is how projects running on
11
+ * our GKE cluster came to describe themselves as Hetzner in the console — and
12
+ * `provider` is half the Stripe compute lookup key (`compute_<provider>_<vmSize>`),
13
+ * so that is a mispricing, not a cosmetic slip.
14
+ *
15
+ * The control plane already publishes the infrastructure that actually exists,
16
+ * and the console's create wizard reads it. Ask the same question here.
17
+ *
18
+ * Exported for tests: the decision is pure, so it can be pinned without a
19
+ * control plane. The fetching and the exit live in `resolveRequestedTarget`.
20
+ *
21
+ * @param requested `--provider`, if the caller named one. An explicit flag wins:
22
+ * it is the caller stating intent, and `deploy` corrects the record anyway.
23
+ * @param targets What the control plane says exists, or `undefined` when it
24
+ * cannot say — an older deployment with no `platform-config`, or a failed
25
+ * request. That is different from an empty list, which is a control plane
26
+ * stating it has no infrastructure at all.
27
+ * @returns the target to record, or `null` when the control plane answered that
28
+ * there is none.
29
+ */
30
+ export declare function chooseRequestedTarget(requested: string | undefined, targets: DeployTarget[] | undefined): {
31
+ provider: string;
32
+ region?: string;
33
+ } | null;
3
34
  export declare function createProject(rawArgs: string[]): Promise<void>;
4
35
  export declare function projectInfo(rawArgs: string[], projectRef: string): Promise<void>;
5
36
  export declare function deleteProject(rawArgs: string[], projectRef: string): Promise<void>;
@@ -1 +1,17 @@
1
+ /** Well-known filename the backend writes its actual port to. */
2
+ export declare const DEV_PORT_FILENAME = ".rebase-dev-port";
3
+ /**
4
+ * Compute a deterministic port from the project root path.
5
+ * Range: 3001–3999 (avoids privileged ports and common services).
6
+ * Two different project directories will almost always get different ports.
7
+ */
8
+ export declare function getProjectPort(projectRoot: string): number;
9
+ /**
10
+ * Resolve the best starting port for this project:
11
+ * 1. Explicit --port flag (highest priority)
12
+ * 2. PORT env var
13
+ * 3. Previously used port from .rebase-dev-port (port affinity across restarts)
14
+ * 4. Deterministic hash from project path (unique per project)
15
+ */
16
+ export declare function resolveStartPort(projectRoot: string, explicitPort?: number): number;
1
17
  export declare function devCommand(rawArgs: string[]): Promise<void>;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `rebase telemetry` — the command that makes the rest of it inspectable.
3
+ *
4
+ * The whole subsystem asks for trust it cannot otherwise earn, and the cheapest
5
+ * way to earn it is to stop describing the payload and print it. `show` runs
6
+ * the same builder the sender uses, so what appears here is what would go, not
7
+ * a documentation comment that quietly fell out of date two releases ago.
8
+ */
9
+ export declare function telemetryCommand(rawArgs: string[]): Promise<void>;