@rebasepro/cli 0.23.0 → 0.24.1-canary.g19596ef

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.
Files changed (186) hide show
  1. package/README.md +19 -3
  2. package/bin/rebase.js +4 -1
  3. package/dist/agent-setup-B31F00DC.js +324 -0
  4. package/dist/agent-setup-B31F00DC.js.map +1 -0
  5. package/dist/api-keys-Bl1I9q7M.js +525 -0
  6. package/dist/api-keys-Bl1I9q7M.js.map +1 -0
  7. package/dist/apps-CNgQJjg5.js +340 -0
  8. package/dist/apps-CNgQJjg5.js.map +1 -0
  9. package/dist/args-BnmsEXvu.js +109 -0
  10. package/dist/args-BnmsEXvu.js.map +1 -0
  11. package/dist/atlas-dev-database-D_LP_Woq.js +62 -0
  12. package/dist/atlas-dev-database-D_LP_Woq.js.map +1 -0
  13. package/dist/auth-aySHEF45.js +414 -0
  14. package/dist/auth-aySHEF45.js.map +1 -0
  15. package/dist/branch-pointer-DvSHBjjz.js +2 -0
  16. package/dist/build-DIHWIqO4.js +336 -0
  17. package/dist/build-DIHWIqO4.js.map +1 -0
  18. package/dist/bundle-CqURVN-K.js +1621 -0
  19. package/dist/bundle-CqURVN-K.js.map +1 -0
  20. package/dist/bundle.d.ts +12 -3
  21. package/dist/cli.d.ts +23 -2
  22. package/dist/cloud-DjByQlkW.js +9667 -0
  23. package/dist/cloud-DjByQlkW.js.map +1 -0
  24. package/dist/collection-drift-BKj_BoMq.js +106 -0
  25. package/dist/collection-drift-BKj_BoMq.js.map +1 -0
  26. package/dist/commands/__test_support__/parser-specs.d.ts +13 -0
  27. package/dist/commands/agent-setup.d.ts +116 -0
  28. package/dist/commands/api-keys.d.ts +86 -23
  29. package/dist/commands/apps.d.ts +17 -0
  30. package/dist/commands/auth.d.ts +7 -0
  31. package/dist/commands/build.d.ts +5 -1
  32. package/dist/commands/cloud/auth.d.ts +4 -3
  33. package/dist/commands/cloud/bundle-deploy.d.ts +45 -0
  34. package/dist/commands/cloud/context.d.ts +44 -5
  35. package/dist/commands/cloud/cron.d.ts +53 -0
  36. package/dist/commands/cloud/debug.d.ts +134 -0
  37. package/dist/commands/cloud/index.d.ts +89 -12
  38. package/dist/commands/cloud/token-capabilities.d.ts +76 -0
  39. package/dist/commands/cloud/tokens.d.ts +23 -0
  40. package/dist/commands/console.d.ts +12 -0
  41. package/dist/commands/db.d.ts +79 -25
  42. package/dist/commands/dev.d.ts +181 -13
  43. package/dist/commands/doctor.d.ts +5 -0
  44. package/dist/commands/generate_sdk.d.ts +29 -2
  45. package/dist/commands/init.d.ts +115 -14
  46. package/dist/commands/schema.d.ts +6 -0
  47. package/dist/commands/skills.d.ts +126 -7
  48. package/dist/commands/start.d.ts +44 -0
  49. package/dist/commands/storage.d.ts +46 -0
  50. package/dist/commands/upgrade.d.ts +1 -0
  51. package/dist/consent-ClRHmcxf.js +76 -0
  52. package/dist/consent-ClRHmcxf.js.map +1 -0
  53. package/dist/console-D6SJf8k_.js +102 -0
  54. package/dist/console-D6SJf8k_.js.map +1 -0
  55. package/dist/context-BqhRydBg.js +1234 -0
  56. package/dist/context-BqhRydBg.js.map +1 -0
  57. package/dist/{daemon-entry-LTFKtpHy.js → daemon-entry-Da-D3rmH.js} +3 -4
  58. package/dist/daemon-entry-Da-D3rmH.js.map +1 -0
  59. package/dist/daemon-pzsQwDHa.js +2 -0
  60. package/dist/db-B7RuNuaQ.js +1312 -0
  61. package/dist/db-B7RuNuaQ.js.map +1 -0
  62. package/dist/derive-Dl0z2OWi.js +484 -0
  63. package/dist/derive-Dl0z2OWi.js.map +1 -0
  64. package/dist/dev-CYr_WXan.js +1599 -0
  65. package/dist/dev-CYr_WXan.js.map +1 -0
  66. package/dist/dev-db/atlas-dev-database.d.ts +32 -0
  67. package/dist/dev-db/daemon-entry.d.ts +13 -0
  68. package/dist/dev-db/daemon.d.ts +10 -0
  69. package/dist/dev-db/last-dev-database.d.ts +41 -0
  70. package/dist/dev-db/prepare.d.ts +29 -3
  71. package/dist/dev-db/pull.d.ts +90 -5
  72. package/dist/dev-db/resolve.d.ts +26 -2
  73. package/dist/dev-preflight-B-LYyBf1.js +2 -0
  74. package/dist/dev-server-DG8Tt-uf.js +96 -0
  75. package/dist/dev-server-DG8Tt-uf.js.map +1 -0
  76. package/dist/doctor-BP8cZ-7F.js +771 -0
  77. package/dist/doctor-BP8cZ-7F.js.map +1 -0
  78. package/dist/eject-DizdoecV.js +335 -0
  79. package/dist/eject-DizdoecV.js.map +1 -0
  80. package/dist/fold-static-BC9CYicw.js +201 -0
  81. package/dist/fold-static-BC9CYicw.js.map +1 -0
  82. package/dist/fold-static.d.ts +26 -3
  83. package/dist/function-portability-BIvlvR9q.js +265 -0
  84. package/dist/function-portability-BIvlvR9q.js.map +1 -0
  85. package/dist/generate_sdk-BPqdJc3Q.js +489 -0
  86. package/dist/generate_sdk-BPqdJc3Q.js.map +1 -0
  87. package/dist/index.es.js +199 -22095
  88. package/dist/index.es.js.map +1 -1
  89. package/dist/init-Utstvh2Q.js +1149 -0
  90. package/dist/init-Utstvh2Q.js.map +1 -0
  91. package/dist/last-dev-database-B58g89uA.js +2 -0
  92. package/dist/manifest-DRp0UVTj.js +880 -0
  93. package/dist/manifest-DRp0UVTj.js.map +1 -0
  94. package/dist/manifest.d.ts +56 -4
  95. package/dist/normalize-imports-X0Tgze9y.js +107 -0
  96. package/dist/normalize-imports-X0Tgze9y.js.map +1 -0
  97. package/dist/package-manager-DSEOwTqM.js +193 -0
  98. package/dist/package-manager-DSEOwTqM.js.map +1 -0
  99. package/dist/path-flags-BFgG9V6y.js +91 -0
  100. package/dist/path-flags-BFgG9V6y.js.map +1 -0
  101. package/dist/precompress.d.ts +17 -0
  102. package/dist/prepare-CDN6lSpz.js +1236 -0
  103. package/dist/prepare-CDN6lSpz.js.map +1 -0
  104. package/dist/{project-3WCbrrfW.js → project-Wp2zoyeN.js} +34 -184
  105. package/dist/project-Wp2zoyeN.js.map +1 -0
  106. package/dist/pull-CrGYMEsy.js +458 -0
  107. package/dist/pull-CrGYMEsy.js.map +1 -0
  108. package/dist/resolve-B2O-wKKk.js +2 -0
  109. package/dist/resources-FD7AfZx8.js +183 -0
  110. package/dist/resources-FD7AfZx8.js.map +1 -0
  111. package/dist/schema-DMdr431S.js +125 -0
  112. package/dist/schema-DMdr431S.js.map +1 -0
  113. package/dist/skills-DTFibT1m.js +642 -0
  114. package/dist/skills-DTFibT1m.js.map +1 -0
  115. package/dist/start-DEa-hOBh.js +267 -0
  116. package/dist/start-DEa-hOBh.js.map +1 -0
  117. package/dist/{state-C59Elrnt.js → state-Do7ZJYkF.js} +49 -4
  118. package/dist/state-Do7ZJYkF.js.map +1 -0
  119. package/dist/status-DxnJTekv.js +352 -0
  120. package/dist/status-DxnJTekv.js.map +1 -0
  121. package/dist/storage-DyVq-f_F.js +303 -0
  122. package/dist/storage-DyVq-f_F.js.map +1 -0
  123. package/dist/telemetry/identity.d.ts +10 -1
  124. package/dist/telemetry-C8Uq8phb.js +129 -0
  125. package/dist/telemetry-C8Uq8phb.js.map +1 -0
  126. package/dist/telemetry-EMFRVRQi.js +351 -0
  127. package/dist/telemetry-EMFRVRQi.js.map +1 -0
  128. package/dist/unknown-command-DNwDcX3w.js +98 -0
  129. package/dist/unknown-command-DNwDcX3w.js.map +1 -0
  130. package/dist/upgrade-CtZtRkTE.js +1332 -0
  131. package/dist/upgrade-CtZtRkTE.js.map +1 -0
  132. package/dist/upgrade.d.ts +38 -5
  133. package/dist/utils/dev-preflight.d.ts +47 -5
  134. package/dist/utils/dev-server.d.ts +30 -0
  135. package/dist/utils/path-flags.d.ts +31 -0
  136. package/dist/utils/project.d.ts +16 -0
  137. package/dist/utils/temp-archive.d.ts +9 -0
  138. package/dist/version-Buyyo1ut.js +58 -0
  139. package/dist/version-Buyyo1ut.js.map +1 -0
  140. package/package.json +7 -7
  141. package/templates/eject/Dockerfile +4 -2
  142. package/templates/eject/backend/src/env.ts +1 -1
  143. package/templates/eject/backend/src/index.ts +42 -4
  144. package/templates/eject/docker-compose.custom.yml +8 -4
  145. package/templates/generated-schemas/blank.ts +40 -0
  146. package/templates/generated-schemas/blog.ts +127 -0
  147. package/templates/generated-schemas/ecommerce.ts +93 -0
  148. package/templates/overlays/baas/ai-instructions.md +1 -1
  149. package/templates/overlays/baas/config/package.json +1 -0
  150. package/templates/overlays/baas/config/storage.ts +5 -3
  151. package/templates/overlays/baas/package.json +1 -0
  152. package/templates/template/.cursorrules +1 -1
  153. package/templates/template/.env.example +20 -10
  154. package/templates/template/.github/copilot-instructions.md +1 -1
  155. package/templates/template/.mcp.json +2 -2
  156. package/templates/template/.windsurfrules +1 -1
  157. package/templates/template/AGENTS.md +1 -1
  158. package/templates/template/CLAUDE.md +1 -1
  159. package/templates/template/README.md +21 -15
  160. package/templates/template/ai-instructions.md +5 -4
  161. package/templates/template/backend/functions/hello.ts +8 -0
  162. package/templates/template/backend/package.json +1 -1
  163. package/templates/template/config/resources.ts +7 -6
  164. package/templates/template/config/storage.ts +5 -3
  165. package/templates/template/docker-compose.yml +15 -9
  166. package/templates/template/frontend/vite.config.ts +13 -3
  167. package/templates/template/gitignore +6 -1
  168. package/templates/template/package.json +5 -3
  169. package/templates/template/scripts/example.ts +6 -4
  170. package/templates/template/tsconfig.json +25 -0
  171. package/dist/branch-pointer-dBiClJ0p.js +0 -111
  172. package/dist/branch-pointer-dBiClJ0p.js.map +0 -1
  173. package/dist/constraints-DKGbNfUW.js +0 -48
  174. package/dist/constraints-DKGbNfUW.js.map +0 -1
  175. package/dist/daemon-DR9Tk0La.js +0 -355
  176. package/dist/daemon-DR9Tk0La.js.map +0 -1
  177. package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
  178. package/dist/dev-preflight-CNLl4rdo.js +0 -367
  179. package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
  180. package/dist/project-3WCbrrfW.js.map +0 -1
  181. package/dist/pull-Dywm5FWP.js +0 -232
  182. package/dist/pull-Dywm5FWP.js.map +0 -1
  183. package/dist/resolve-BAeRUBhg.js +0 -74
  184. package/dist/resolve-BAeRUBhg.js.map +0 -1
  185. package/dist/state-C59Elrnt.js.map +0 -1
  186. package/templates/template/backend/src/schema.generated.ts +0 -13
@@ -0,0 +1,1234 @@
1
+ import { c as findProjectRoot } from "./project-Wp2zoyeN.js";
2
+ import { n as parseCommandArgs } from "./args-BnmsEXvu.js";
3
+ import { t as cliUserAgent } from "./version-Buyyo1ut.js";
4
+ import chalk from "chalk";
5
+ import arg from "arg";
6
+ import fs from "fs";
7
+ import os from "os";
8
+ import path from "path";
9
+ import { spawn } from "child_process";
10
+ import { parseEnvBoolean } from "@rebasepro/types";
11
+ import inquirer from "inquirer";
12
+ import { createMemoryStorage, createRebaseClient } from "@rebasepro/client";
13
+ //#region src/commands/cloud/errors.ts
14
+ /**
15
+ * Turning a control-plane failure into something the caller can act on.
16
+ *
17
+ * The control plane talks to Kubernetes, and when Kubernetes refuses it, the
18
+ * refusal travels back verbatim: a whole `Status` object, the request headers,
19
+ * an `audit-id`, an `x-kubernetes-pf-flowschema-uid`. That reached the user's
20
+ * terminal unedited. Two things are wrong with it beyond the noise.
21
+ *
22
+ * The first is that the one sentence that matters is buried in the middle of a
23
+ * JSON blob, so the remedy — if there is one — is the hardest part to find.
24
+ *
25
+ * The second is worse, and is the reason this file exists rather than a
26
+ * `slice(0, 200)`. A `403` naming a `system:serviceaccount:` is a statement
27
+ * about the PLATFORM's own credentials: some role the control plane runs as is
28
+ * missing a grant. Nothing in the user's project can change that — not the
29
+ * collections, not `rebase.json`, not the deploy flags — and the error, printed
30
+ * raw in the middle of `rebase cloud deploy`, reads exactly like a project
31
+ * fault. Someone acting on that reading deletes working code looking for the
32
+ * cause. (That is not hypothetical: three cron jobs were removed from a project
33
+ * to test whether they caused a `cronjobs.batch` 403. They did not.)
34
+ *
35
+ * So this classifies before it summarises, and says whose problem it is.
36
+ */
37
+ /**
38
+ * The service accounts a Kubernetes 403 can name, and what each means.
39
+ *
40
+ * `system:serviceaccount:` is the platform's own identity — the control plane's
41
+ * pod, or a tenant's operator. A project cannot grant it anything, because the
42
+ * grant lives in a cluster the project does not own. `system:anonymous` is the
43
+ * same fact with the credential missing entirely.
44
+ */
45
+ var PLATFORM_PRINCIPAL_RE = /system:(?:serviceaccount:|anonymous|node:)/;
46
+ /**
47
+ * A Kubernetes `Status` object embedded anywhere in a message.
48
+ *
49
+ * Scanned for rather than parsed off the front: the control plane wraps it
50
+ * ("Failed to create the tenant namespace: …"), the client library appends
51
+ * headers after it, and both halves are worth keeping out of the summary.
52
+ * Balanced-brace scanning rather than a regex, because `details.causes` nests
53
+ * and a lazy `\{.*?\}` truncates the object at the first inner brace — which
54
+ * parses to nothing and silently falls through to the raw message.
55
+ */
56
+ function extractKubernetesStatus(text) {
57
+ for (let start = text.indexOf("{"); start !== -1; start = text.indexOf("{", start + 1)) {
58
+ let depth = 0;
59
+ let inString = false;
60
+ let escaped = false;
61
+ for (let i = start; i < text.length; i++) {
62
+ const ch = text[i];
63
+ if (escaped) {
64
+ escaped = false;
65
+ continue;
66
+ }
67
+ if (ch === "\\") {
68
+ escaped = true;
69
+ continue;
70
+ }
71
+ if (ch === "\"") {
72
+ inString = !inString;
73
+ continue;
74
+ }
75
+ if (inString) continue;
76
+ if (ch === "{") depth++;
77
+ else if (ch === "}") {
78
+ depth--;
79
+ if (depth !== 0) continue;
80
+ const candidate = text.slice(start, i + 1);
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(candidate);
84
+ } catch {
85
+ break;
86
+ }
87
+ const obj = parsed;
88
+ if (obj?.kind === "Status" || obj?.status === "Failure" && typeof obj.code === "number") return {
89
+ message: typeof obj.message === "string" ? obj.message : void 0,
90
+ reason: typeof obj.reason === "string" ? obj.reason : void 0,
91
+ code: typeof obj.code === "number" ? obj.code : void 0,
92
+ details: obj.details ?? void 0
93
+ };
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ }
99
+ /**
100
+ * Strip the transport noise a Kubernetes client appends to its own message.
101
+ *
102
+ * Only ever applied to the fallback path — when no `Status` was found, the
103
+ * message is all there is, and cutting it at the first header keeps the
104
+ * sentence while dropping the `audit-id` and the flowschema uid nobody outside
105
+ * the cluster can use.
106
+ */
107
+ function trimTransportNoise(message) {
108
+ const cut = message.search(/\s*(?:headers:|HTTP request failed|audit-id|x-kubernetes-pf-|\{"kind":"Status")/i);
109
+ return (cut > 0 ? message.slice(0, cut) : message).trim().replace(/[\s:,-]+$/, "");
110
+ }
111
+ /** The longest a summary may be before it stops being one. */
112
+ var MAX_SUMMARY = 300;
113
+ /**
114
+ * One actionable line (plus a hint) from whatever the control plane returned.
115
+ *
116
+ * `status` is the HTTP status of the control-plane call itself, which is a
117
+ * different number from the `code` inside an embedded Kubernetes `Status` — the
118
+ * control plane routinely answers 500 while the cluster answered 403, and it is
119
+ * the inner one that says what happened.
120
+ */
121
+ function summarizeError(error, context) {
122
+ const err = error;
123
+ const raw = err?.message ?? String(error);
124
+ const k8s = extractKubernetesStatus(raw);
125
+ if (k8s) {
126
+ const inner = k8s.message?.trim() ?? "";
127
+ if ((k8s.code === 403 || k8s.reason === "Forbidden") && PLATFORM_PRINCIPAL_RE.test(inner)) return {
128
+ message: `${context}: the platform's own cluster credentials were refused by Kubernetes.`,
129
+ hint: `${truncate(inner)}\n This is a platform-side permission, not something your project can grant. Nothing in your code, collections or deploy flags will change it — report it with the message above rather than retrying or changing the project.`,
130
+ code: "platform_permission_denied",
131
+ platform: true,
132
+ raw
133
+ };
134
+ return {
135
+ message: `${context}: ${truncate(inner || k8s.reason || "the cluster refused the request")}`,
136
+ hint: k8s.code ? `Kubernetes answered ${k8s.code}${k8s.reason ? ` (${k8s.reason})` : ""}.` : void 0,
137
+ code: k8s.reason ? `k8s_${k8s.reason.toLowerCase()}` : "k8s_error",
138
+ platform: false,
139
+ raw
140
+ };
141
+ }
142
+ const trimmed = trimTransportNoise(raw);
143
+ return {
144
+ message: `${context}${err?.status ? ` (${err.status})` : ""}: ${truncate(trimmed || raw)}`,
145
+ code: err?.code ?? (err?.status ? `http_${err.status}` : "request_failed"),
146
+ platform: false,
147
+ raw
148
+ };
149
+ }
150
+ function truncate(text) {
151
+ const flat = text.replace(/\s+/g, " ").trim();
152
+ return flat.length > MAX_SUMMARY ? `${flat.slice(0, MAX_SUMMARY - 1)}…` : flat;
153
+ }
154
+ /**
155
+ * Whether the caller asked for the untouched body.
156
+ *
157
+ * `--debug` is already what `bin/rebase.js` prints after every failure as the
158
+ * thing to add, so the raw payload hangs off the flag people are told to reach
159
+ * for rather than off one invented here.
160
+ *
161
+ * The one reader of `REBASE_DEBUG` in the commands, spelled as the bin spells
162
+ * it. It used to be `=== "1"` here while the quote fallbacks in `resources.ts`
163
+ * tested the raw string for truthiness, so `=true` hid the body and `=0`
164
+ * printed the fallback.
165
+ */
166
+ function wantsRawError(argv = process.argv) {
167
+ return argv.includes("--debug") || parseEnvBoolean(process.env.REBASE_DEBUG) === true;
168
+ }
169
+ //#endregion
170
+ //#region src/commands/cloud/context.ts
171
+ /**
172
+ * Shared foundation for the `rebase cloud` command family.
173
+ *
174
+ * Everything cloud subcommands need in common lives here:
175
+ * - credential storage (~/.rebase/credentials.json, keyed per control-plane host)
176
+ * - project link file (.rebase/cloud.json in the project dir)
177
+ * - control-plane URL resolution
178
+ * - an authenticated `@rebasepro/client` instance (createCloudClient / requireClient)
179
+ * - small output helpers shared across subcommands
180
+ *
181
+ * The control plane is itself a Rebase app, so we reuse the same SDK the web
182
+ * console uses (`@rebasepro/client`). Auth, token refresh, the data REST client
183
+ * and function invocation all come from the SDK — the CLI only supplies a
184
+ * file-backed AuthStorage so a login persists across invocations.
185
+ */
186
+ /** Default hosted control plane (the Rebase Cloud console origin). */
187
+ var DEFAULT_CLOUD_URL = "https://app.rebase.pro";
188
+ /** The storage key the SDK's auth module reads/writes the session under. */
189
+ var SDK_SESSION_KEY = "rebase_auth";
190
+ /** ~/.rebase/credentials.json — one file, many hosts. */
191
+ function credentialsPath() {
192
+ return path.join(os.homedir(), ".rebase", "credentials.json");
193
+ }
194
+ /** Project-local link file: <project>/.rebase/cloud.json */
195
+ /**
196
+ * The billing account an organization row points at, whichever key it arrived under.
197
+ *
198
+ * REST serves every column under its own property name, so the relation comes
199
+ * back as `billingAccountId`. Both readers in this CLI checked
200
+ * `billing_account_id` and `billingAccount` — neither is what arrives — so
201
+ * `rebase cloud billing` answered "no billing account" for every organization,
202
+ * and the deploy pre-check never saw an internal plan. The other two spellings
203
+ * are still accepted: older servers and fixtures send them.
204
+ */
205
+ function billingAccountIdOf(org) {
206
+ if (typeof org !== "object" || org === null) return void 0;
207
+ for (const key of [
208
+ "billingAccountId",
209
+ "billing_account_id",
210
+ "billingAccount"
211
+ ]) {
212
+ const value = org[key];
213
+ if (typeof value === "string" || typeof value === "number") return value;
214
+ }
215
+ }
216
+ function projectLinkPath(cwd = process.cwd()) {
217
+ const root = findProjectRoot(cwd) || cwd;
218
+ return path.join(root, ".rebase", "cloud.json");
219
+ }
220
+ function readCredentials() {
221
+ try {
222
+ const raw = fs.readFileSync(credentialsPath(), "utf-8");
223
+ const parsed = JSON.parse(raw);
224
+ if (!parsed.contexts) parsed.contexts = {};
225
+ return parsed;
226
+ } catch {
227
+ return { contexts: {} };
228
+ }
229
+ }
230
+ function writeCredentials(data) {
231
+ const file = credentialsPath();
232
+ fs.mkdirSync(path.dirname(file), { recursive: true });
233
+ fs.writeFileSync(file, JSON.stringify(data, null, 2), { mode: 384 });
234
+ try {
235
+ fs.chmodSync(file, 384);
236
+ } catch {}
237
+ }
238
+ /** Host that a bare `rebase cloud` command should target, if any. */
239
+ function currentContextUrl() {
240
+ return readCredentials().current;
241
+ }
242
+ /** Persist the active organization id for a host. */
243
+ function setContextOrg(url, org) {
244
+ const creds = readCredentials();
245
+ const entry = creds.contexts[url] || {};
246
+ if (org) entry.org = org;
247
+ else delete entry.org;
248
+ creds.contexts[url] = entry;
249
+ writeCredentials(creds);
250
+ }
251
+ function getContextOrg(url) {
252
+ return readCredentials().contexts[url]?.org;
253
+ }
254
+ function createFileAuthStorage(url) {
255
+ return {
256
+ getItem(key) {
257
+ if (key !== SDK_SESSION_KEY) return null;
258
+ return readCredentials().contexts[url]?.auth ?? null;
259
+ },
260
+ setItem(key, value) {
261
+ if (key !== SDK_SESSION_KEY) return;
262
+ const creds = readCredentials();
263
+ const entry = creds.contexts[url] || {};
264
+ entry.auth = value;
265
+ creds.contexts[url] = entry;
266
+ if (!creds.current) creds.current = url;
267
+ writeCredentials(creds);
268
+ },
269
+ removeItem(key) {
270
+ if (key !== SDK_SESSION_KEY) return;
271
+ const creds = readCredentials();
272
+ if (creds.contexts[url]) {
273
+ delete creds.contexts[url].auth;
274
+ delete creds.contexts[url].org;
275
+ }
276
+ writeCredentials(creds);
277
+ }
278
+ };
279
+ }
280
+ /** Mark a host as the active context (called on login). */
281
+ function setCurrentContext(url) {
282
+ const creds = readCredentials();
283
+ creds.current = url;
284
+ if (!creds.contexts[url]) creds.contexts[url] = {};
285
+ writeCredentials(creds);
286
+ }
287
+ function resolveCloudUrl(rawArgs) {
288
+ return resolveCloudTarget(rawArgs).url;
289
+ }
290
+ /** The control-plane URL, and which rung of the ladder above supplied it. */
291
+ function resolveCloudTarget(rawArgs) {
292
+ const parsed = arg({ "--url": String }, {
293
+ argv: rawArgs.slice(2),
294
+ permissive: true
295
+ });
296
+ if (parsed["--url"]) return {
297
+ url: normalizeUrl(parsed["--url"]),
298
+ source: "flag"
299
+ };
300
+ if (process.env.REBASE_CLOUD_URL) return {
301
+ url: normalizeUrl(process.env.REBASE_CLOUD_URL),
302
+ source: "env"
303
+ };
304
+ const link = readLink();
305
+ if (link?.url && link.mode !== "direct") return {
306
+ url: normalizeUrl(link.url),
307
+ source: "link"
308
+ };
309
+ const current = currentContextUrl();
310
+ if (current) return {
311
+ url: normalizeUrl(current),
312
+ source: "context"
313
+ };
314
+ return {
315
+ url: DEFAULT_CLOUD_URL,
316
+ source: "default"
317
+ };
318
+ }
319
+ /**
320
+ * Whether a control-plane host is one the user chose: the platform's own, or a
321
+ * host already in their credentials file because they signed in to it.
322
+ *
323
+ * A link file is part of the repository, and a cloned repository is not the
324
+ * user's to trust with their password. `login` asks before sending one to a
325
+ * linked host that is neither.
326
+ */
327
+ function isKnownControlPlane(url) {
328
+ return url === DEFAULT_CLOUD_URL || Object.hasOwn(readCredentials().contexts, url);
329
+ }
330
+ /**
331
+ * Refuse to run a control-plane command in a directory linked straight at a
332
+ * backend.
333
+ *
334
+ * The alternative — quietly using the default control plane — would be worse
335
+ * than the bug it replaces: the user asked for `https://api.example.com` and
336
+ * would get an answer about `app.rebase.pro` with nothing saying so. Naming the
337
+ * link, and both ways out of it, is the whole message.
338
+ *
339
+ * `--project` and `--url` are the ways out, so a line carrying either passes
340
+ * through: both say, explicitly, which control-plane subject the caller means.
341
+ */
342
+ function refuseDirectLink(rawArgs) {
343
+ const link = readLink();
344
+ if (link?.mode !== "direct") return;
345
+ const parsed = arg({
346
+ "--project": String,
347
+ "-p": "--project",
348
+ "--url": String
349
+ }, {
350
+ argv: rawArgs.slice(2),
351
+ permissive: true
352
+ });
353
+ if (parsed["--project"] || parsed["--url"]) return;
354
+ fail(`This directory is linked directly to ${chalk.cyan(link.url)} — ${chalk.bold("rebase cloud unlink")} first, or pass ${chalk.bold("--project")}.`, "A direct link is one backend, not a control plane: it has no projects, no organizations and no session.", "direct_link");
355
+ }
356
+ function normalizeUrl(url) {
357
+ let u = url.trim().replace(/\/+$/, "");
358
+ if (!/^https?:\/\//.test(u)) u = `https://${u}`;
359
+ return u;
360
+ }
361
+ /**
362
+ * Build an SDK client bound to a control-plane host, backed by the on-disk
363
+ * credential store. `autoRefresh` is disabled so we never leave a dangling
364
+ * setTimeout that keeps the CLI process alive; token refresh is done on demand
365
+ * by `requireClient`.
366
+ */
367
+ function createCloudClient(url) {
368
+ return createRebaseClient({
369
+ baseUrl: url,
370
+ realtime: false,
371
+ headers: { "User-Agent": cliUserAgent() },
372
+ auth: {
373
+ storage: createFileAuthStorage(url),
374
+ persistSession: true,
375
+ autoRefresh: false
376
+ }
377
+ });
378
+ }
379
+ /**
380
+ * The environment variable that authenticates the cloud family with a token
381
+ * instead of a `cloud login` session — for CI and agents, which have no
382
+ * terminal to log in at and should not hold a person's password.
383
+ */
384
+ var TOKEN_ENV = "REBASE_TOKEN";
385
+ /** `REBASE_TOKEN`, trimmed, or undefined when it is unset or blank. */
386
+ function envToken() {
387
+ const value = process.env[TOKEN_ENV]?.trim();
388
+ return value ? value : void 0;
389
+ }
390
+ /** The token each token-authenticated client was built with — see {@link freshAccessToken}. */
391
+ var STATIC_TOKENS = /* @__PURE__ */ new WeakMap();
392
+ /**
393
+ * A client that sends `token` as its bearer and nothing else.
394
+ *
395
+ * No session: the token is not refreshed, and the credentials file is neither
396
+ * read nor written — a CI job's token must not become, or replace, the login
397
+ * of whoever owns the machine. A 401 is answered by {@link explainRefusedToken}
398
+ * rather than by the SDK's session refresh, because there is no session to
399
+ * refresh and "log in again" is the wrong remedy for a token.
400
+ */
401
+ function createTokenClient(url, token) {
402
+ const client = createRebaseClient({
403
+ baseUrl: url,
404
+ realtime: false,
405
+ headers: { "User-Agent": cliUserAgent() },
406
+ token,
407
+ auth: {
408
+ storage: createMemoryStorage(),
409
+ persistSession: false,
410
+ autoRefresh: false
411
+ }
412
+ });
413
+ client.setOnUnauthorized(() => explainRefusedToken(url, token));
414
+ STATIC_TOKENS.set(client, token);
415
+ return client;
416
+ }
417
+ /**
418
+ * Why the control plane answered a token-authenticated request with 401, and
419
+ * what to do about it. Never returns.
420
+ *
421
+ * Two different causes reach the same status. Usually the token itself was
422
+ * refused — it expired, was revoked, or belongs to another control plane — and
423
+ * the remedy is a new token. But a few endpoints take only a person's session
424
+ * (`/auth/me` among them), and those refuse a perfectly good token too. One
425
+ * request to `/api/auth/scopes`, which accepts a token, tells the two apart, so
426
+ * neither is answered with the other's remedy.
427
+ */
428
+ async function explainRefusedToken(url, token) {
429
+ let tokenAccepted = false;
430
+ try {
431
+ tokenAccepted = (await fetch(`${url}/api/auth/scopes`, { headers: {
432
+ Authorization: `Bearer ${token}`,
433
+ "User-Agent": cliUserAgent()
434
+ } })).ok;
435
+ } catch {}
436
+ if (tokenAccepted) fail(`This command needs a signed-in session, and ${TOKEN_ENV} is an API token.`, `Unset ${TOKEN_ENV} and run ${chalk.bold("rebase cloud login")} to run it.`, "session_required");
437
+ fail(`${TOKEN_ENV} was rejected by ${url}: it has expired, been revoked, or was not issued by this control plane.`, `Create a new one with ${chalk.bold("rebase cloud tokens create")} and put it in ${TOKEN_ENV}.`, "token_rejected");
438
+ }
439
+ /** Two minutes of head-room before a token is treated as expired. */
440
+ var EXPIRY_BUFFER_MS = 12e4;
441
+ /**
442
+ * An access token with time left on it, refreshed first if it is close to
443
+ * expiry. Rejects when there is no session, or it cannot be refreshed.
444
+ *
445
+ * For the requests the SDK does not make itself. Its own calls refresh and
446
+ * retry on a 401; a raw `fetch` or a tunnel handshake presents whatever token
447
+ * it was handed, and `createCloudClient` never refreshes in the background. A
448
+ * token read once at the start of a command that runs for longer than it
449
+ * lives — a deploy's build, a `db connect` left open — is refused by the time
450
+ * it is used, so it is read here at the moment it is sent.
451
+ *
452
+ * A client built from `REBASE_TOKEN` has no session; its token is returned
453
+ * as it is, since there is nothing to refresh it with.
454
+ */
455
+ async function freshAccessToken(client) {
456
+ const fixed = STATIC_TOKENS.get(client);
457
+ if (fixed) return fixed;
458
+ const session = client.auth.getSession();
459
+ if (session?.accessToken && session.expiresAt > Date.now() + EXPIRY_BUFFER_MS) return session.accessToken;
460
+ return (await client.auth.refreshSession()).accessToken;
461
+ }
462
+ /**
463
+ * Return an authenticated client for the resolved host.
464
+ *
465
+ * With `REBASE_TOKEN` set, that token is the credential for every request and
466
+ * the stored login is not consulted at all. Otherwise it is the `cloud login`
467
+ * session — see {@link requireSessionClient}.
468
+ */
469
+ async function requireClient(rawArgs) {
470
+ const token = envToken();
471
+ if (!token) return requireSessionClient(rawArgs);
472
+ refuseDirectLink(rawArgs);
473
+ const url = resolveCloudUrl(rawArgs);
474
+ return {
475
+ client: createTokenClient(url, token),
476
+ url
477
+ };
478
+ }
479
+ /**
480
+ * Return a client holding the `cloud login` session for the resolved host,
481
+ * refreshing the access token if it is close to expiry, whether or not
482
+ * `REBASE_TOKEN` is set. Exits with a helpful message when there is no usable
483
+ * session (never logged in, or the refresh token was revoked).
484
+ *
485
+ * For the commands a token cannot run — minting and revoking tokens: a key that
486
+ * could mint keys could mint its own successor, so the control plane refuses
487
+ * every key on those routes.
488
+ */
489
+ async function requireSessionClient(rawArgs) {
490
+ refuseDirectLink(rawArgs);
491
+ const url = resolveCloudUrl(rawArgs);
492
+ const client = createCloudClient(url);
493
+ const session = client.auth.getSession();
494
+ if (!session || !session.accessToken) {
495
+ if (envToken()) fail(`This command needs a signed-in session for ${chalk.cyan(url)}, and ${TOKEN_ENV} is an API token.`, `A token cannot manage tokens. Run ${chalk.bold("rebase cloud login")} first.`, "session_required");
496
+ fail(`Not logged in to ${chalk.cyan(url)}.`, `Run ${chalk.bold("rebase cloud login")} first.`, "not_logged_in");
497
+ }
498
+ if (session.expiresAt <= Date.now() + EXPIRY_BUFFER_MS) try {
499
+ await client.auth.refreshSession();
500
+ } catch {
501
+ fail(`Your session for ${chalk.cyan(url)} has expired.`, `Run ${chalk.bold("rebase cloud login")} to sign in again.`, "session_expired");
502
+ }
503
+ return {
504
+ client,
505
+ url
506
+ };
507
+ }
508
+ /**
509
+ * The control plane's public, non-secret self-description (`platform-config`).
510
+ *
511
+ * None of it is knowable from the CLI side: it is per-deployment configuration —
512
+ * production serves tenants at `rebase.website` on GKE, a dev control plane at
513
+ * `localhost` on Docker. Guessing produced two separate lies: a congratulation
514
+ * URL that resolved nowhere near the app, and a `provider` the project does not
515
+ * run on (see `createProject`).
516
+ *
517
+ * Cached per host for the process: it is fixed for a control plane's lifetime,
518
+ * and `projects list` formats one host per row off a single fetch.
519
+ *
520
+ * @returns the config, or `undefined` if the control plane doesn't serve
521
+ * `platform-config` (an older deployment) or the request failed. A failure is
522
+ * cached too — a short-lived CLI should not retry once per row. Note the
523
+ * distinction callers rely on: `undefined` means "this control plane cannot
524
+ * tell us", whereas `deployTargets: []` is a control plane stating that it has
525
+ * no infrastructure configured.
526
+ */
527
+ var platformConfigCache = /* @__PURE__ */ new Map();
528
+ function fetchPlatformConfig(client, url) {
529
+ let pending = platformConfigCache.get(url);
530
+ if (!pending) {
531
+ pending = client.functions.invoke("platform-config", void 0, { method: "GET" }).then((cfg) => cfg ?? void 0).catch(() => void 0);
532
+ platformConfigCache.set(url, pending);
533
+ }
534
+ return pending;
535
+ }
536
+ /**
537
+ * The base domain tenant projects are served at, derived from the same
538
+ * TENANT_BASE_DOMAIN the ingress and the console read (see
539
+ * saas/backend/src/utils/tenant-domain.ts).
540
+ */
541
+ function fetchTenantBaseDomain(client, url) {
542
+ return fetchPlatformConfig(client, url).then((cfg) => cfg?.tenantBaseDomain?.trim() || void 0);
543
+ }
544
+ /**
545
+ * The infrastructure a deploy for this control plane would ACTUALLY use, in the
546
+ * resolver's own preference order (saas/backend/src/k8s/resolve.ts).
547
+ *
548
+ * @returns the targets, or `undefined` when the control plane cannot say.
549
+ */
550
+ function fetchDeployTargets(client, url) {
551
+ return fetchPlatformConfig(client, url).then((cfg) => Array.isArray(cfg?.deployTargets) ? cfg.deployTargets : void 0);
552
+ }
553
+ /**
554
+ * Public host for a project — `<subdomain>.<base>`, or the bare subdomain when
555
+ * the base domain is unknown.
556
+ *
557
+ * It deliberately never falls back to a guessed domain. The user copies this
558
+ * string into a browser, so a plausible-but-wrong hostname is worse than an
559
+ * obviously incomplete one: `acme.rebase.pro` looks reachable and isn't, while
560
+ * `acme` reads as "the subdomain is acme" and prompts no wasted debugging.
561
+ */
562
+ function formatTenantHost(subdomain, baseDomain) {
563
+ if (!subdomain) return void 0;
564
+ return baseDomain ? `${subdomain}.${baseDomain}` : subdomain;
565
+ }
566
+ /**
567
+ * The host to display for a project.
568
+ *
569
+ * Prefers `host` off the record: the control plane resolves it through the same
570
+ * `tenantHost()` the ingress uses, so it accounts for the project's *cluster*
571
+ * base domain. The CLI cannot compute that itself — `clusters` is admin-only
572
+ * under RLS, so a normal user's token cannot read `baseDomain`, and a project on
573
+ * a second cluster is served somewhere the platform default does not name.
574
+ *
575
+ * `baseDomain` (from `platform-config`) remains the fallback for a control plane
576
+ * that predates the hook — right for the single-cluster case, which is every
577
+ * project today.
578
+ */
579
+ function projectHost(project, baseDomain) {
580
+ return project.host || formatTenantHost(project.subdomain, baseDomain);
581
+ }
582
+ function readLink(cwd = process.cwd()) {
583
+ try {
584
+ return JSON.parse(fs.readFileSync(projectLinkPath(cwd), "utf-8"));
585
+ } catch {
586
+ return null;
587
+ }
588
+ }
589
+ function writeLink(link, cwd = process.cwd()) {
590
+ const file = projectLinkPath(cwd);
591
+ fs.mkdirSync(path.dirname(file), { recursive: true });
592
+ fs.writeFileSync(file, JSON.stringify(link, null, 2));
593
+ }
594
+ function removeLink(cwd = process.cwd()) {
595
+ const file = projectLinkPath(cwd);
596
+ if (fs.existsSync(file)) {
597
+ fs.rmSync(file);
598
+ return true;
599
+ }
600
+ return false;
601
+ }
602
+ /**
603
+ * Flags that may appear anywhere on a `rebase cloud` line, including *before*
604
+ * the resource group.
605
+ *
606
+ * They have to be declared wherever positionals are resolved, because `arg`'s
607
+ * `permissive: true` does not merely tolerate an undeclared flag — it pushes it
608
+ * into `_` alongside the positionals, and for a flag that takes a value it
609
+ * pushes the value in too. So `cloud --project acme storage create` parsed
610
+ * without this spec yields `_` of `["--project", "acme", "storage", "create"]`,
611
+ * and the group reads as `"acme"`: a real project name, in the group position,
612
+ * dispatching to nothing. Skipping tokens that start with `-` does not save you
613
+ * there — the damage is the orphaned value, which looks exactly like a
614
+ * positional.
615
+ *
616
+ * Only genuinely global flags belong here. Group-specific ones (`--bucket`,
617
+ * `--region`, …) are declared by the handler that owns them. They can still
618
+ * precede the action — or stand in for it, on a group with a default — so the
619
+ * dispatcher also declares every one that takes a value: `CLOUD_VALUE_FLAGS`
620
+ * in `index.ts`.
621
+ *
622
+ * `-p` is `--project` in eighteen places and `--password` in `login`. That
623
+ * ambiguity does not matter to the one caller that reads this spec: it resolves
624
+ * positionals and never looks at a flag's value, so all it needs to know is
625
+ * that `-p` takes one. Anything that wants the value must keep declaring it
626
+ * itself, with the meaning its own command gives it.
627
+ */
628
+ var GLOBAL_CLOUD_FLAGS = {
629
+ "--json": Boolean,
630
+ "--yes": Boolean,
631
+ "--help": Boolean,
632
+ "--project": String,
633
+ "--url": String,
634
+ "-p": "--project",
635
+ "-y": "--yes",
636
+ "-h": "--help"
637
+ };
638
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
639
+ /**
640
+ * The raw project reference to operate on: explicit `--project` flag wins,
641
+ * otherwise the linked project. Exits with guidance when neither is present.
642
+ * The value is a slug (the project's subdomain, as shown in console URLs) or,
643
+ * for old scripts and link files, a raw project UUID.
644
+ */
645
+ function requireProjectRef(rawArgs) {
646
+ const parsed = arg({
647
+ "--project": String,
648
+ "-p": "--project"
649
+ }, {
650
+ argv: rawArgs.slice(2),
651
+ permissive: true
652
+ });
653
+ if (parsed["--project"]) return parsed["--project"];
654
+ const link = readLink();
655
+ if (link?.projectId) return link.projectId;
656
+ fail("No project specified and this directory is not linked.", `Pass ${chalk.bold("--project <slug>")} or run ${chalk.bold("rebase cloud link")}.`, "no_project");
657
+ }
658
+ /**
659
+ * Resolve a project reference — slug or UUID — to the internal id the API
660
+ * takes, or undefined when no such project is visible. Slugs cost one lookup;
661
+ * UUIDs pass through untouched so linked directories and old scripts skip the
662
+ * round-trip.
663
+ */
664
+ async function lookupProjectId(ref, client) {
665
+ if (UUID_RE.test(ref)) return ref;
666
+ const row = (await client.data.collection("projects").find({
667
+ where: { subdomain: ["==", ref] },
668
+ limit: 1
669
+ })).data[0];
670
+ return row?.id === void 0 ? void 0 : String(row.id);
671
+ }
672
+ /** Like `lookupProjectId`, but exits with guidance when the ref matches nothing. */
673
+ async function resolveProjectRef(ref, client) {
674
+ const id = await lookupProjectId(ref, client);
675
+ if (id === void 0) fail(`No project with slug ${chalk.bold(ref)}.`, `List yours with ${chalk.bold("rebase cloud projects")}.`, "project_not_found");
676
+ return id;
677
+ }
678
+ /** `requireProjectRef` + `resolveProjectRef` in one step. */
679
+ async function requireProject(rawArgs, client) {
680
+ return resolveProjectRef(requireProjectRef(rawArgs), client);
681
+ }
682
+ /**
683
+ * The project reference to SHOW: the slug the user typed or the linked slug.
684
+ * Never resolves — for human output only. Old link files predate `slug` and
685
+ * fall back to the stored id.
686
+ */
687
+ function displayProjectRef(rawArgs) {
688
+ const parsed = arg({
689
+ "--project": String,
690
+ "-p": "--project"
691
+ }, {
692
+ argv: rawArgs.slice(2),
693
+ permissive: true
694
+ });
695
+ if (parsed["--project"]) return parsed["--project"];
696
+ const link = readLink();
697
+ return link?.slug || link?.projectId || "";
698
+ }
699
+ var JSON_MODE = false;
700
+ /**
701
+ * Resolve and latch the output mode for this invocation. Call once at the top of
702
+ * `cloudCommand`, before anything can print or `fail`. Returns the resolved mode
703
+ * (handy for tests, which otherwise leave it at its `false` default).
704
+ */
705
+ function initOutputMode(rawArgs) {
706
+ if (arg({ "--json": Boolean }, {
707
+ argv: rawArgs.slice(2),
708
+ permissive: true
709
+ })["--json"]) JSON_MODE = true;
710
+ else JSON_MODE = parseEnvBoolean(process.env.REBASE_JSON) ?? process.stdout.isTTY !== true;
711
+ return JSON_MODE;
712
+ }
713
+ /** Whether the current invocation is emitting machine-readable JSON. */
714
+ function isJsonMode() {
715
+ return JSON_MODE;
716
+ }
717
+ /** Strip ANSI colour codes — JSON output must never carry terminal escapes. */
718
+ var ANSI_RE = /\[[0-9;]*m/g;
719
+ function stripAnsi(s) {
720
+ return s.replace(ANSI_RE, "");
721
+ }
722
+ /**
723
+ * Write one JSON value to stdout, followed by a newline.
724
+ *
725
+ * Indented, because the overwhelmingly common reader is a person or an agent
726
+ * looking at a terminal — JSON mode is entered automatically whenever stdout is
727
+ * not a TTY, so `rebase cloud deployments list` piped anywhere at all produced
728
+ * a project's entire deployment history as one unwrapped line. `JSON.parse`
729
+ * does not care about the whitespace; everything else does.
730
+ */
731
+ function printJson(value) {
732
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
733
+ }
734
+ /**
735
+ * The one output primitive every new command uses: in JSON mode emit `json`
736
+ * (and nothing else); otherwise run `human`. Keeping the two behind a single
737
+ * call is what guarantees a command can never print a table AND a JSON blob.
738
+ */
739
+ function emit(human, json) {
740
+ if (JSON_MODE) printJson(json);
741
+ else human();
742
+ }
743
+ /**
744
+ * Print a help page — the human one, or a machine-readable description of the
745
+ * same command in JSON mode.
746
+ *
747
+ * `--help` is the one place where "stdout is not a TTY" is a weak signal: a
748
+ * person runs `rebase cloud db --help | less` and wants the page. But the rule
749
+ * this family promises is that stdout carries one JSON value whenever it is not
750
+ * a terminal, and a help page is the easiest possible thing to describe
751
+ * structurally — so rather than carve out an exception, help answers the same
752
+ * question in the reader's own language. For an agent, `--help` piped is then a
753
+ * discovery call rather than 60 lines of ANSI to scrape.
754
+ *
755
+ * `env` shipped this shape first, alone; this generalises it so every group
756
+ * answers the same way.
757
+ */
758
+ function emitHelp(command, actions, human, extra = {}) {
759
+ emit(human, {
760
+ command,
761
+ actions: actions.map((entry) => describeAction(entry, command)),
762
+ ...extra
763
+ });
764
+ }
765
+ /**
766
+ * Normalise an action for the JSON form. Strings stay legal — the index page
767
+ * lists group names, which are not actions of the command being described.
768
+ *
769
+ * `command` is the page's own command (`cloud env`), so the usage line a caller
770
+ * reads is a line it can run: without it the answer said `rebase cloud set
771
+ * KEY=VALUE`, which is a command that does not exist.
772
+ */
773
+ function describeAction(entry, command) {
774
+ if (typeof entry === "string") return {
775
+ action: entry,
776
+ description: "",
777
+ flags: []
778
+ };
779
+ return {
780
+ action: entry.action,
781
+ usage: `rebase ${command} ${entry.action}${entry.args ? ` ${entry.args}` : ""}`,
782
+ description: entry.description,
783
+ flags: (entry.flags ?? []).map(([flag, description]) => ({
784
+ flag,
785
+ description
786
+ }))
787
+ };
788
+ }
789
+ /**
790
+ * Flags every cloud command accepts, documented once.
791
+ *
792
+ * Lives here rather than in `action-help.ts` because both help renderers print
793
+ * it and `action-help` imports this module, not the other way round.
794
+ */
795
+ var GLOBAL_HELP_FLAGS = [
796
+ ["--project, -p <slug>", "Operate on a project without linking this directory"],
797
+ ["--json", "Machine-readable output (also when piped, or REBASE_JSON=1)"],
798
+ ["--url <origin>", "Target a specific control plane (or REBASE_CLOUD_URL)"],
799
+ ["--yes, -y", "Skip confirmation prompts"],
800
+ ["--debug", "Print the untouched error body after a failure"]
801
+ ];
802
+ [...Object.keys(GLOBAL_CLOUD_FLAGS)];
803
+ /** Column width for an action word (+ its args) before its description. */
804
+ var HELP_COLUMN = 28;
805
+ /**
806
+ * The gap after `text` in a `width`-wide column, never less than one space.
807
+ *
808
+ * `padEnd` returns the string unchanged when it is already at the width, which
809
+ * glues the description onto the flag: `--connection-string <url>The external…`.
810
+ */
811
+ function pad(text, width) {
812
+ return " ".repeat(Math.max(1, width - text.length));
813
+ }
814
+ /**
815
+ * Print a group's page — the human one, and the same content as JSON when piped.
816
+ */
817
+ function printGroupHelp(page) {
818
+ emitHelp(page.command, page.actions, () => {
819
+ console.log("");
820
+ console.log(`${chalk.bold(`rebase ${page.command}`)} — ${page.title}`);
821
+ console.log("");
822
+ console.log(chalk.green.bold("Usage"));
823
+ console.log(` rebase ${page.command} ${chalk.blue("<action>")} [options]`);
824
+ let section;
825
+ for (const entry of page.actions) {
826
+ const heading = entry.section ?? "Commands";
827
+ if (heading !== section) {
828
+ section = heading;
829
+ console.log("");
830
+ console.log(chalk.green.bold(heading));
831
+ }
832
+ const word = `${entry.action}${entry.args ? ` ${entry.args}` : ""}`;
833
+ console.log(` ${chalk.blue.bold(entry.action)}${entry.args ? ` ${chalk.gray(entry.args)}` : ""}${pad(word, HELP_COLUMN)}${entry.description}`);
834
+ for (const [flag, description] of entry.flags ?? []) console.log(` ${chalk.blue(flag)}${pad(flag, HELP_COLUMN - 4)}${chalk.gray(description)}`);
835
+ }
836
+ if (page.options?.length) {
837
+ console.log("");
838
+ console.log(chalk.green.bold("Options"));
839
+ for (const [flag, description] of page.options) console.log(` ${chalk.blue(flag)}${pad(flag, HELP_COLUMN)}${description}`);
840
+ }
841
+ console.log("");
842
+ console.log(chalk.green.bold("Global options"));
843
+ for (const [flag, description] of GLOBAL_HELP_FLAGS) console.log(` ${chalk.blue(flag)}${pad(flag, HELP_COLUMN)}${chalk.gray(description)}`);
844
+ if (page.notes?.length) {
845
+ console.log("");
846
+ for (const note of page.notes) console.log(chalk.gray(` ${note}`));
847
+ }
848
+ console.log("");
849
+ }, {
850
+ usage: `rebase ${page.command} <action> [options]`,
851
+ summary: page.title,
852
+ options: (page.options ?? []).map(([flag, description]) => ({
853
+ flag,
854
+ description
855
+ })),
856
+ globalFlags: GLOBAL_HELP_FLAGS.map(([flag, description]) => ({
857
+ flag,
858
+ description
859
+ })),
860
+ notes: page.notes ?? []
861
+ });
862
+ }
863
+ /**
864
+ * Print a warning (+ optional hint) — in every output mode, always to stderr.
865
+ *
866
+ * `emit` is for a command's *result*, and JSON mode legitimately replaces the
867
+ * human rendering of one. A warning is not a result: it says the command is
868
+ * about to do something the caller may not have meant, and that is exactly as
869
+ * true when the output is piped. Gating one on `!isJsonMode()` deleted it
870
+ * precisely where nobody was watching the terminal — a `--source` deploy ejected
871
+ * a live project off the managed runtime and said so only to a TTY that wasn't
872
+ * there.
873
+ *
874
+ * stdout carries the JSON value and nothing else, so warnings go to stderr:
875
+ * a machine parser reading stdout cannot be corrupted by one. Only the
876
+ * *formatting* may depend on the mode — colour and indentation for a terminal,
877
+ * plain ASCII otherwise. Whether a warning is emitted at all may not.
878
+ *
879
+ * Anything a caller might branch on belongs in the JSON payload as well; stderr
880
+ * is for whoever reads the transcript afterwards.
881
+ */
882
+ function warn(message, hint) {
883
+ if (JSON_MODE) {
884
+ process.stderr.write(`warning: ${stripAnsi(message)}\n`);
885
+ if (hint) process.stderr.write(` ${stripAnsi(hint)}\n`);
886
+ return;
887
+ }
888
+ console.error("");
889
+ console.error(chalk.yellow(` ⚠ ${message}`));
890
+ if (hint) console.error(chalk.gray(` ${hint}`));
891
+ }
892
+ /**
893
+ * Print an error (+ optional hint) and exit non-zero. Never returns.
894
+ *
895
+ * `code` is the field a caller branches on, and it defaults to `"error"` rather
896
+ * than `null`. An envelope whose only machine-readable field is null is not
897
+ * machine-readable — `{"error":{"message":"No project specified…","code":null}}`
898
+ * forced the very substring-matching on `message` that the envelope exists to
899
+ * make unnecessary, and `message` is the field most likely to be reworded.
900
+ *
901
+ * `"error"` is deliberately a poor code: it says "this refusal has not been
902
+ * classified yet" without ever being absent. Anything a caller might plausibly
903
+ * want to distinguish — `usage`, `not_found`, `unauthenticated` — passes a real
904
+ * one. Codes are part of the CLI's contract once shipped; see
905
+ * `cloud-reporting.test.ts`, which pins the ones commands are documented to
906
+ * return.
907
+ */
908
+ function fail(message, hint, code) {
909
+ if (JSON_MODE) {
910
+ printJson({ error: {
911
+ message: stripAnsi(message),
912
+ code: code ?? "error",
913
+ hint: hint ? stripAnsi(hint) : void 0
914
+ } });
915
+ process.exit(1);
916
+ }
917
+ console.error("");
918
+ console.error(chalk.red(` ✗ ${message}`));
919
+ if (hint) console.error(chalk.gray(` ${hint}`));
920
+ console.error("");
921
+ process.exit(1);
922
+ }
923
+ /**
924
+ * Confirm a destructive/irreversible action, respecting non-interactive use.
925
+ *
926
+ * With `--yes`/`-y` it proceeds silently. In JSON mode or a non-TTY it REFUSES
927
+ * to prompt — a prompt that can hang is a known repo landmine — and fails,
928
+ * telling the caller to pass `--yes`. Only an interactive terminal gets a real
929
+ * confirm prompt; declining there aborts cleanly (exit 0).
930
+ */
931
+ async function confirmDestructive(opts) {
932
+ if (opts.yes) return;
933
+ if (JSON_MODE || process.stdin.isTTY !== true) fail("This action is destructive and needs confirmation.", `Re-run with ${chalk.bold("--yes")} to proceed.`, "confirmation_required");
934
+ const { confirmed } = await inquirer.prompt([{
935
+ type: "confirm",
936
+ name: "confirmed",
937
+ default: false,
938
+ message: opts.prompt
939
+ }]);
940
+ if (!confirmed) {
941
+ console.error(chalk.gray(" Aborted."));
942
+ process.exit(0);
943
+ }
944
+ }
945
+ /**
946
+ * Refuse, rather than prompt, when there is nobody to answer.
947
+ *
948
+ * `confirmDestructive` has always done this for yes/no confirmations. The
949
+ * *value* prompts had no such guard: `cloud login`, `cloud link`, `cloud use`,
950
+ * `cloud orgs create` and `cloud db create` all called `inquirer.prompt`
951
+ * unconditionally, so piping any of them — which is how an agent runs every
952
+ * command in this family — parked the process on a prompt reading from a stdin
953
+ * that was never going to produce a line. A hang is the worst failure mode
954
+ * available here: no output, no exit code, nothing to retry on.
955
+ *
956
+ * @param what what the prompt would have asked for, e.g. "an email and password"
957
+ * @param flags the flags that supply it non-interactively
958
+ */
959
+ function requireInteractive(what, flags) {
960
+ if (JSON_MODE || process.stdin.isTTY !== true) fail(`This command needs ${what}, and there is no terminal to ask on.`, `Pass ${chalk.bold(flags)}.`, "input_required");
961
+ }
962
+ /**
963
+ * Resolve one cloud command's flags and ARGUMENTS from the full `process.argv`.
964
+ *
965
+ * This replaces `cloudPositionals`, which was `rawArgs.slice(3).filter(a =>
966
+ * !a.startsWith("-"))`. Dropping `-`-prefixed tokens looks like it solves the
967
+ * permissive-parse problem and does not: a flag that takes a VALUE leaves the
968
+ * value behind, an ordinary word in the argument position that no filter can
969
+ * tell from a real one. `--project` is the flag every one of these commands
970
+ * documents, so the failure was reachable from the help page:
971
+ *
972
+ * rebase cloud env unset -p acme → removed the variable "acme"
973
+ * rebase cloud env set KEY -p acme → stored the value "acme"
974
+ * rebase cloud domains add -p acme → registered the domain "acme"
975
+ * rebase cloud webhooks delete -p acme 42 → deleted webhook "acme", not 42
976
+ * rebase cloud cancel -p acme → cancelled deployment id "acme"
977
+ *
978
+ * The filter's other half is quieter. A flag nobody declared *is* dropped by
979
+ * it — but only from the operands, never from the run: nothing rejects it, so
980
+ * the command proceeds with the argument missing or defaulted. `db backup
981
+ * --dry-run` listed backups, `domains remove --dry-run` detached the domain,
982
+ * and `env set KEY=v --secrett` stored the value as an ordinary variable that
983
+ * `env reveal` will hand back. The one place an undeclared flag became the
984
+ * argument outright is `projects info|delete`, which resolved its id through
985
+ * `positionals()` instead — that skips only LEADING `-` tokens, so `projects
986
+ * delete --force` looked up a project named "--force".
987
+ *
988
+ * So: parse the whole line strictly, through the same `parseCommandArgs` the
989
+ * non-cloud commands use — `arg` then consumes each declared flag *with its
990
+ * value* wherever it appears, and rejects the undeclared, leaving `_` holding
991
+ * the command words followed by the real arguments. `commandWords` counts from
992
+ * `cloud` itself (`cloud env set` ⇒ 3), and is applied to the parsed
993
+ * positionals, so a flag written before the group shifts nothing.
994
+ *
995
+ * Two things this adds over calling `parseCommandArgs` directly, and the reason
996
+ * it is worth a wrapper:
997
+ *
998
+ * - `GLOBAL_CLOUD_FLAGS` is merged in. `--json`, `--yes` and `--project` may
999
+ * appear anywhere on a cloud line including before the group, so a strict
1000
+ * parse that did not declare them would reject the CLI's own documented
1001
+ * usage. (`parseCommandArgs` adds `--debug`/`--help` on top of that.)
1002
+ * - A parse error is reported through `fail`, not thrown. A throw reaches
1003
+ * `bin/rebase.js`, which prints `✗ …` to stderr — which is right for every
1004
+ * other command and wrong here: `rebase cloud` is in JSON mode whenever
1005
+ * stdout is not a TTY, i.e. always for the agents this family is built for,
1006
+ * and it promises them exactly one JSON value. `fail` keeps that promise,
1007
+ * with the same `usage` code the other refusals in this family use.
1008
+ */
1009
+ function parseCloudArgs(opts) {
1010
+ const spec = {
1011
+ ...GLOBAL_CLOUD_FLAGS,
1012
+ ...opts.spec
1013
+ };
1014
+ try {
1015
+ const parsed = parseCommandArgs({
1016
+ ...opts,
1017
+ spec
1018
+ });
1019
+ return {
1020
+ flags: parsed.flags,
1021
+ positionals: parsed.positionals
1022
+ };
1023
+ } catch (err) {
1024
+ fail(err instanceof Error ? err.message : String(err), void 0, "usage");
1025
+ }
1026
+ }
1027
+ /**
1028
+ * Refuse an action word its group does not dispatch. Returns for `undefined`,
1029
+ * which is every group's default action.
1030
+ *
1031
+ * The groups that switch on their action already do this in a `default:` case.
1032
+ * The groups written as a chain of `if (action === "x") return …` did not: a
1033
+ * word that matched nothing fell out of the chain into the *default* action, so
1034
+ * `rebase cloud storage creat` listed the buckets and exited 0, and
1035
+ * `rebase cloud billing usage` printed the account. Reporting a typo as a
1036
+ * successful run of a different command is the failure mode this family exists
1037
+ * to not have — an agent branching on the exit code learns nothing, and a person
1038
+ * reads the output of a command they did not ask for.
1039
+ *
1040
+ * One spelling, so the code is `unknown_command` everywhere rather than the
1041
+ * default `"error"` half of them used, and the hint always names the group's own
1042
+ * `--help` rather than the index page.
1043
+ */
1044
+ function requireKnownAction(group, action, known) {
1045
+ if (action === void 0 || known.includes(action)) return;
1046
+ fail(`Unknown ${group} command: ${action}`, `Run \`rebase cloud ${group} --help\`. Actions: ${known.join(", ")}.`, "unknown_command");
1047
+ }
1048
+ /**
1049
+ * `--timeout <seconds>` as milliseconds, or `fallbackMs` when it was not given.
1050
+ *
1051
+ * One function rather than one per command, because two commands take this flag
1052
+ * and a second copy is where the two would come to disagree about what
1053
+ * `--timeout 0` means.
1054
+ *
1055
+ * A value this cannot read is a refusal, not a fall back to the default. The
1056
+ * whole reason a caller passes a timeout is that it has a deadline of its own;
1057
+ * quietly substituting a different one is how a fifteen-minute wait turns up
1058
+ * inside a five-minute CI step, having been asked for `--timeout 30s`.
1059
+ */
1060
+ function resolveTimeoutMs(value, opts) {
1061
+ if (value === void 0) return opts.fallbackMs;
1062
+ const seconds = Number(value);
1063
+ if (!Number.isFinite(seconds) || seconds <= 0) fail(`--timeout takes a number of seconds (got "${value}").`, `Run \`rebase ${opts.command} --help\`.`, "usage");
1064
+ return seconds * 1e3;
1065
+ }
1066
+ /**
1067
+ * Announce an outcome — "Logged in as …", "Deleted project …".
1068
+ *
1069
+ * On **stderr**, in both modes. It reads like a result and is not one: the
1070
+ * result is the JSON value (or the table) on stdout, and every JSON payload in
1071
+ * this family already carries `success: true`. Leaving this on stdout meant a
1072
+ * successful `rebase cloud link | jq` was handed a green tick followed by an
1073
+ * object — one stream, two syntaxes, and only the second parseable.
1074
+ *
1075
+ * It stays visible in JSON mode, unlike `note`: an agent that got a `success`
1076
+ * line on a command it expected to refuse has learned something.
1077
+ */
1078
+ function success(message) {
1079
+ if (JSON_MODE) {
1080
+ process.stderr.write(`${stripAnsi(message)}\n`);
1081
+ return;
1082
+ }
1083
+ console.error("");
1084
+ console.error(chalk.bold.green(` ✓ ${message}`));
1085
+ console.error("");
1086
+ }
1087
+ /**
1088
+ * Narrate progress, or point at the next step — "Signing in to …", "Redeploy
1089
+ * for the tenant to pick this up".
1090
+ *
1091
+ * stderr, and **suppressed entirely in JSON mode**. This is the one helper that
1092
+ * a mode may silence, and the distinction from `warn` is worth keeping sharp:
1093
+ *
1094
+ * - A warning is a *condition*. It is as true when piped as when watched, so
1095
+ * silencing it hides something the caller would want to know. `warn` never
1096
+ * silences.
1097
+ * - A note is *hand-holding*. "Next: run `rebase generate-sdk`" tells a person
1098
+ * what to type; the agent reading the JSON already has the same information
1099
+ * structurally, or does not need it. Printing it anyway is transcript noise.
1100
+ *
1101
+ * When in doubt it is a warning. The cost of a needless warning is a line; the
1102
+ * cost of a swallowed one is the deploy that ejected a project off the managed
1103
+ * runtime and said so only to a terminal nobody was looking at.
1104
+ */
1105
+ function note(message, indent = " ") {
1106
+ if (JSON_MODE) return;
1107
+ console.error(`${indent}${message}`);
1108
+ }
1109
+ /** A blank spacer line on the narration stream. No-op in JSON mode. */
1110
+ function noteBlank() {
1111
+ if (JSON_MODE) return;
1112
+ console.error("");
1113
+ }
1114
+ /** Colorize a deployment / resource status token. */
1115
+ function colorStatus(status) {
1116
+ switch (status) {
1117
+ case "active":
1118
+ case "success":
1119
+ case "connected": return chalk.green(status);
1120
+ case "deploying":
1121
+ case "provisioning":
1122
+ case "pending_billing":
1123
+ case "untested": return chalk.yellow(status ?? "");
1124
+ case "failed": return chalk.red(status);
1125
+ case "stopped": return chalk.gray(status);
1126
+ default: return chalk.gray(status ?? "unknown");
1127
+ }
1128
+ }
1129
+ /**
1130
+ * Render a two-column key/value block with aligned keys. Empty rows are skipped
1131
+ * — including `null`, which the API sends for an unset column and which used to
1132
+ * print the literal string "null" (e.g. `Custom domain: null`).
1133
+ */
1134
+ function keyValues(rows) {
1135
+ const width = Math.max(...rows.map(([k]) => k.length));
1136
+ for (const [k, v] of rows) {
1137
+ if (v === void 0 || v === null || v === "") continue;
1138
+ console.log(` ${chalk.gray(`${k}:`.padEnd(width + 1))} ${v}`);
1139
+ }
1140
+ }
1141
+ /**
1142
+ * Surface an SDK/HTTP error consistently. The SDK throws RebaseApiError with
1143
+ * a `.status` and `.message`; anything else falls back to its string form.
1144
+ *
1145
+ * The message is summarised rather than printed — see `summarizeError`. What
1146
+ * arrives here is routinely a whole Kubernetes `Status` object with the request
1147
+ * headers appended, and the one sentence worth reading is inside it. The
1148
+ * untouched body is still available, behind `--debug`, on stderr where it
1149
+ * cannot corrupt the JSON value on stdout.
1150
+ */
1151
+ function reportError(e, context) {
1152
+ const err = e;
1153
+ const summary = summarizeError(e, context);
1154
+ if (wantsRawError()) process.stderr.write(`\n${summary.raw}\n\n`);
1155
+ if (JSON_MODE) {
1156
+ printJson({ error: {
1157
+ message: stripAnsi(summary.message),
1158
+ code: summary.code,
1159
+ status: err?.status ?? null,
1160
+ hint: summary.hint ? stripAnsi(summary.hint) : void 0,
1161
+ platform: summary.platform,
1162
+ context
1163
+ } });
1164
+ process.exit(1);
1165
+ }
1166
+ fail(summary.message, summary.hint, summary.code);
1167
+ }
1168
+ /**
1169
+ * Open a URL in the user's default browser (best effort). Always announces the
1170
+ * URL first so it stays usable over SSH or when no browser is available.
1171
+ *
1172
+ * The announcement is narration, not the result — it goes to stderr, and in
1173
+ * JSON mode it is silent. Every caller `emit`s the same URL in its payload, so
1174
+ * a machine reader gets it from the one place it is guaranteed to be parseable
1175
+ * rather than from a line that happens to end in a URL.
1176
+ */
1177
+ function openUrl(target, label = "Opening") {
1178
+ noteBlank();
1179
+ note(`${label} ${chalk.cyan(target)}`);
1180
+ noteBlank();
1181
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1182
+ try {
1183
+ const child = spawn(opener, [target], {
1184
+ stdio: "ignore",
1185
+ detached: true,
1186
+ shell: process.platform === "win32"
1187
+ });
1188
+ child.on("error", () => {});
1189
+ child.unref();
1190
+ } catch {}
1191
+ }
1192
+ /**
1193
+ * Read the control plane's rows as a subcommand's row shape.
1194
+ *
1195
+ * `client.data.collection(...)` is typed against the *generated* schema of the
1196
+ * project the SDK is pointed at, and the control-plane collections this CLI
1197
+ * reads are not in one — so every row arrives as an open
1198
+ * `Record<string, unknown>`. A declared `interface` gets no implicit index
1199
+ * signature, so it does not overlap that, and a direct `as` is refused: which
1200
+ * is how ten call sites came to write `as unknown as XRow[]`, an assertion
1201
+ * about a wire payload with nothing checking it in either direction.
1202
+ *
1203
+ * There is exactly one invariant to check and this checks it. Everything else
1204
+ * the shapes declare is optional and already read as such, so there is nothing
1205
+ * further to verify — but a row with no usable `id` is not a row any of these
1206
+ * commands can act on, and passing it through was how a listing came to print
1207
+ * `[undefined]` and a lookup came to compare against the string `"undefined"`.
1208
+ */
1209
+ function cloudRows(rows) {
1210
+ return (rows ?? []).filter((row) => typeof row?.id === "string" || typeof row?.id === "number");
1211
+ }
1212
+ /** {@link cloudRows} for an endpoint that returns a single row. */
1213
+ function cloudRow(row) {
1214
+ return cloudRows(row ? [row] : [])[0];
1215
+ }
1216
+ /**
1217
+ * {@link cloudRows} for a write that must have produced a row.
1218
+ *
1219
+ * `create()` returning something with no usable `id` means the control plane
1220
+ * accepted the write and then described it in a way this CLI cannot act on.
1221
+ * The callers all go straight on to use that id — `setContextOrg(url,
1222
+ * String(created.id))` — so asserting the shape, which is what stood here,
1223
+ * turned a control-plane fault into an organization whose active id is the
1224
+ * seven-letter string `"undefined"`, stored in the user's config file.
1225
+ */
1226
+ function requireCloudRow(row, what) {
1227
+ const parsed = cloudRow(row);
1228
+ if (!parsed) throw new Error(`The control plane accepted the ${what} but returned no id for it.`);
1229
+ return parsed;
1230
+ }
1231
+ //#endregion
1232
+ export { projectHost as A, requireProject as B, lookupProjectId as C, parseCloudArgs as D, openUrl as E, reportError as F, resolveProjectRef as G, requireSessionClient as H, requireClient as I, setCurrentContext as J, resolveTimeoutMs as K, requireCloudRow as L, readLink as M, refuseDirectLink as N, printGroupHelp as O, removeLink as P, wantsRawError as Q, requireInteractive as R, keyValues as S, noteBlank as T, resolveCloudTarget as U, requireProjectRef as V, resolveCloudUrl as W, warn as X, success as Y, writeLink as Z, freshAccessToken as _, cloudRow as a, isJsonMode as b, confirmDestructive as c, emit as d, emitHelp as f, fetchTenantBaseDomain as g, fetchDeployTargets as h, billingAccountIdOf as i, projectLinkPath as j, printJson as k, createCloudClient as l, fail as m, GLOBAL_HELP_FLAGS as n, cloudRows as o, envToken as p, setContextOrg as q, TOKEN_ENV as r, colorStatus as s, GLOBAL_CLOUD_FLAGS as t, displayProjectRef as u, getContextOrg as v, note as w, isKnownControlPlane as x, initOutputMode as y, requireKnownAction as z };
1233
+
1234
+ //# sourceMappingURL=context-BqhRydBg.js.map