@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,880 @@
1
+ import { n as getPMCommands, t as detectPackageManager } from "./package-manager-DSEOwTqM.js";
2
+ import { t as MANIFEST_FILENAME } from "./project-Wp2zoyeN.js";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { parseAppAddress, reservedPrefixFor } from "@rebasepro/types";
6
+ //#region src/manifest.ts
7
+ /**
8
+ * Loading, validating and synthesizing `rebase.json`.
9
+ *
10
+ * The manifest declares *topology*: which runtime major a project targets and
11
+ * which apps this repository contributes. It is deliberately small — schema,
12
+ * security rules, hooks and functions stay in TypeScript, where a type system
13
+ * can check them.
14
+ *
15
+ * Two properties matter more than the file format itself:
16
+ *
17
+ * - **A missing manifest is never an error.** Every project that exists today
18
+ * predates this file. One is synthesized from the conventions the template
19
+ * already follows, so nothing breaks and nobody is forced to migrate.
20
+ * - **Validation reports every problem at once**, with the path to each. A
21
+ * config file that surfaces its mistakes one run at a time is a bad config
22
+ * file.
23
+ */
24
+ /** Runtime range written into new manifests. */
25
+ var CURRENT_RUNTIME_RANGE = "^1";
26
+ /** Conventional locations, matching what `rebase init` scaffolds. */
27
+ var DEFAULT_CONFIG_DIR = "config";
28
+ var DEFAULT_FUNCTIONS_DIR = "backend/functions";
29
+ var DEFAULT_CRONS_DIR = "backend/crons";
30
+ var DEFAULT_SCHEMA_FILE = "backend/src/schema.generated.ts";
31
+ var ManifestError = class extends Error {
32
+ issues;
33
+ constructor(message, issues = []) {
34
+ super(message);
35
+ this.issues = issues;
36
+ this.name = "ManifestError";
37
+ }
38
+ };
39
+ var APP_TYPES = ["backend", "static"];
40
+ /**
41
+ * App types that used to exist, and what replaced each one.
42
+ *
43
+ * Kept so a manifest written against the old vocabulary fails with the fix in
44
+ * hand rather than with `must be one of: backend, static`, which tells a reader
45
+ * what is wrong but not what to write instead.
46
+ */
47
+ var REMOVED_APP_TYPES = {
48
+ admin: "the admin panel is an ordinary static app now — declare it as { \"type\": \"static\", \"root\": \"frontend\", \"output\": \"frontend/dist\", \"path\": \"/admin\" }",
49
+ custom: "who owns the server is a property of the backend now — declare { \"type\": \"backend\", \"runtime\": \"custom\" } instead of a separate app",
50
+ mobile: "mobile apps are no longer declared in the manifest — nothing consumed this type. Remove the entry"
51
+ };
52
+ /** Reserved because they name things in URLs and CLI output. */
53
+ var RESERVED_APP_NAMES = /* @__PURE__ */ new Set([
54
+ "api",
55
+ "health",
56
+ "metrics",
57
+ "livez",
58
+ "_rebase"
59
+ ]);
60
+ /**
61
+ * Validate a URL path an app is served at: a static app's `path`, or the path
62
+ * part of it when `path` is a URL, and `cms`.
63
+ *
64
+ * Normalizes to "no trailing slash, except for the root" so that mounting and
65
+ * the `REBASE_APP_BASE` build variable have one shape to reason about.
66
+ *
67
+ * `origin` is `https://<host>` when the path came out of a URL, and is only
68
+ * used to phrase the fix in the form the author wrote: telling someone who
69
+ * wrote `"https://admin.example.com/cms/"` to write `"/cms"` would be telling
70
+ * them to drop the hostname.
71
+ */
72
+ function checkAppPath(value, fieldPath, issues, origin = "") {
73
+ if (value === void 0) return void 0;
74
+ if (typeof value !== "string" || !value.startsWith("/")) {
75
+ issues.push({
76
+ path: fieldPath,
77
+ message: "must be an absolute path like \"/admin\""
78
+ });
79
+ return;
80
+ }
81
+ if (!origin && value.startsWith("//")) {
82
+ issues.push({
83
+ path: fieldPath,
84
+ message: `looks like a URL without its scheme — write "https:${value}" to give this app its own hostname`
85
+ });
86
+ return;
87
+ }
88
+ if (value.includes("..") || value.includes("//")) {
89
+ issues.push({
90
+ path: fieldPath,
91
+ message: origin ? `must have a plain path after the hostname, with no ".." and no empty segment — like "${origin}/admin"` : "must be an absolute path like \"/admin\", with no \"..\" and no empty segment"
92
+ });
93
+ return;
94
+ }
95
+ if (value !== "/" && value.endsWith("/")) {
96
+ issues.push({
97
+ path: fieldPath,
98
+ message: `must not end with a slash — write "${origin}${value.slice(0, -1)}", not "${origin}${value}"`
99
+ });
100
+ return;
101
+ }
102
+ const reserved = reservedPrefixFor(value);
103
+ if (reserved) {
104
+ issues.push({
105
+ path: fieldPath,
106
+ message: `cannot be "${reserved}" or nest under it — the backend serves that path` + (origin ? " on every hostname, this one included" : "") + ", and an app mounted there would answer the API's requests with its own index.html"
107
+ });
108
+ return;
109
+ }
110
+ return value;
111
+ }
112
+ /**
113
+ * Validate a static app's `path`, which is either a path or an https URL that
114
+ * also gives the app a hostname of its own.
115
+ *
116
+ * The split is {@link parseAppAddress}, shared with the control plane and the
117
+ * runtime so that all three read one value the same way. What it leaves to the
118
+ * caller — the path rules — are {@link checkAppPath}'s, applied to the path
119
+ * part of either form.
120
+ */
121
+ function checkAppAddress(value, fieldPath, issues) {
122
+ if (value === void 0) return void 0;
123
+ if (typeof value !== "string") {
124
+ issues.push({
125
+ path: fieldPath,
126
+ message: "must be an absolute path like \"/admin\", or a URL like \"https://admin.example.com\""
127
+ });
128
+ return;
129
+ }
130
+ const parsed = parseAppAddress(value);
131
+ if (!parsed.ok) {
132
+ issues.push({
133
+ path: fieldPath,
134
+ message: parsed.reason
135
+ });
136
+ return;
137
+ }
138
+ const { host } = parsed.address;
139
+ const appPath = checkAppPath(parsed.address.path, fieldPath, issues, host ? `https://${host}` : "");
140
+ if (appPath === void 0) return void 0;
141
+ return host ? {
142
+ host,
143
+ path: appPath
144
+ } : { path: appPath };
145
+ }
146
+ /**
147
+ * Where a validated static app answers: its hostname, if its `path` names one,
148
+ * and the path under it.
149
+ *
150
+ * The manifest keeps `path` as it was written, URL and all, because it is also
151
+ * what `writeManifest` puts back: storing the split pieces instead would have
152
+ * `rebase eject` rewrite `"https://admin.example.com"` to `"/"` and drop the
153
+ * hostname from a file the developer never asked it to touch. Everything that
154
+ * mounts, builds or prints an app reads it through here instead, so there is
155
+ * one reading of the field.
156
+ *
157
+ * Throws on a value `validateManifest` would have refused. Every caller holds a
158
+ * manifest that came through it, so reaching the throw means one did not — and
159
+ * guessing an address for it would build or mount the app somewhere it was
160
+ * never declared.
161
+ */
162
+ function staticAppAddress(name, app) {
163
+ if (app.path === void 0) return { path: "/" };
164
+ const parsed = parseAppAddress(app.path);
165
+ if (!parsed.ok) throw new ManifestError(`apps.${name}.path ${parsed.reason}`, [{
166
+ path: `apps.${name}.path`,
167
+ message: parsed.reason
168
+ }]);
169
+ return parsed.address;
170
+ }
171
+ /**
172
+ * An address as a developer would type it: the path alone for an app on every
173
+ * hostname, the https URL for one on its own. The root of a hostname prints as
174
+ * the bare origin — `https://admin.example.com`, which is how it is declared.
175
+ */
176
+ function formatAppAddress(address) {
177
+ if (!address.host) return address.path;
178
+ return `https://${address.host}${address.path === "/" ? "" : address.path}`;
179
+ }
180
+ /**
181
+ * The order static apps are mounted in, and the order a bundle lists them.
182
+ *
183
+ * Longest path first, so an app at `/` is registered after the apps beneath it
184
+ * and its catch-all cannot claim their URLs. At an equal path, the app bound to
185
+ * a hostname goes first: on that hostname it is the one that answers, and the
186
+ * app on every hostname answers the rest. That is the runtime's matching rule,
187
+ * restated as a sort so a bundle reads in the order it is served.
188
+ */
189
+ function compareMountOrder(a, b) {
190
+ if (a.path.length !== b.path.length) return b.path.length - a.path.length;
191
+ return (a.host ? 0 : 1) - (b.host ? 0 : 1);
192
+ }
193
+ /**
194
+ * Is `candidate` the path `parent`, or something beneath it?
195
+ *
196
+ * Segment-aware for the same reason `isForbiddenStaticPath` is: a plain
197
+ * `startsWith` reads `/admin` as living under `/adm`.
198
+ */
199
+ function isUnderPath(candidate, parent) {
200
+ if (parent === "/") return true;
201
+ return candidate === parent || candidate.startsWith(`${parent}/`);
202
+ }
203
+ /**
204
+ * Validate where an app says it mounts the Rebase CMS.
205
+ *
206
+ * The same shape as {@link checkAppPath} — it is a URL path and ends up in the
207
+ * same places — plus one rule of its own: it has to be inside the app declaring
208
+ * it. The app's SPA fallback is what answers that URL, so a `cms` outside its
209
+ * `path` names an address this app will never serve. That is a link the console
210
+ * would then offer to a 404, which is worse than the missing link it replaces.
211
+ *
212
+ * Always a path, even on an app whose `path` is a URL. The app's address
213
+ * already says which hostname it answers on, and a second hostname here could
214
+ * only disagree with it; `cms` says where on that hostname the CMS is.
215
+ */
216
+ function checkCmsPath(value, app, fieldPath, issues) {
217
+ if (value === void 0) return void 0;
218
+ if (typeof value === "string" && (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("//"))) {
219
+ const parsed = parseAppAddress(value.startsWith("//") ? `https:${value}` : value);
220
+ issues.push({
221
+ path: fieldPath,
222
+ message: "must be a path, not a URL — the CMS is a route of this app, which is served where its `path` says" + (parsed.ok ? `. Write "${parsed.address.path}"` : "")
223
+ });
224
+ return;
225
+ }
226
+ const cms = checkAppPath(value, fieldPath, issues);
227
+ if (cms === void 0) return void 0;
228
+ if (!isUnderPath(cms, app.path)) {
229
+ issues.push({
230
+ path: fieldPath,
231
+ message: `must be inside this app's path — it is at "${formatAppAddress(app)}", so it cannot serve "${cms}". The CMS is a route of this app, not a separate deployment; if it really lives elsewhere, declare that app and put \`cms\` on it instead`
232
+ });
233
+ return;
234
+ }
235
+ return cms;
236
+ }
237
+ function isRecord(value) {
238
+ return typeof value === "object" && value !== null && !Array.isArray(value);
239
+ }
240
+ /**
241
+ * Reject paths that escape the repository.
242
+ *
243
+ * A manifest is committed and reviewed, so this is not a security boundary so
244
+ * much as a guard against `../../` typos that would otherwise have `rebase build`
245
+ * writing outside the project.
246
+ *
247
+ * `mayEscape` is for `context`, and only for `context`. Every other path here
248
+ * names something Rebase *reads* — collections, functions, the generated
249
+ * schema, the built assets to serve — and those have to be inside the project
250
+ * or the bundle cannot carry them. A Docker build context is the one field that
251
+ * names something Rebase never opens: it is handed to `docker build`, and in
252
+ * any workspace repository the thing it has to name is the workspace root,
253
+ * above the app. That is not exotic. It is what pnpm, turbo and nx layouts all
254
+ * look like, and it is what this repository's own reference project needs — its
255
+ * Dockerfile's first instruction copies `pnpm-lock.yaml`, which does not exist
256
+ * beside `rebase.json` and never will.
257
+ *
258
+ * Refusing it did not prevent the escape; it only stopped anyone declaring it.
259
+ * `app/rebase.json` built from the monorepo root the whole time — via
260
+ * `infra/cloudbuild.yaml`, which says `-f app/backend/Dockerfile .` — while the
261
+ * manifest said the context was `app/` and `rebase build` printed a command
262
+ * that dies on its first `COPY`.
263
+ */
264
+ function checkRelativePath(value, fieldPath, issues, { required, mayEscape = false }) {
265
+ if (value === void 0) {
266
+ if (required) issues.push({
267
+ path: fieldPath,
268
+ message: "is required"
269
+ });
270
+ return;
271
+ }
272
+ if (typeof value !== "string" || value.trim() === "") {
273
+ issues.push({
274
+ path: fieldPath,
275
+ message: "must be a non-empty string"
276
+ });
277
+ return;
278
+ }
279
+ if (path.isAbsolute(value)) {
280
+ issues.push({
281
+ path: fieldPath,
282
+ message: "must be a relative path, not absolute"
283
+ });
284
+ return;
285
+ }
286
+ const normalized = path.normalize(value);
287
+ if (!mayEscape && (normalized === ".." || normalized.startsWith(`..${path.sep}`))) {
288
+ issues.push({
289
+ path: fieldPath,
290
+ message: "must stay inside the project directory"
291
+ });
292
+ return;
293
+ }
294
+ return value;
295
+ }
296
+ /** Fields each app type understands. Anything else is a typo or a newer CLI. */
297
+ var KNOWN_APP_FIELDS = {
298
+ backend: [
299
+ "type",
300
+ "runtime",
301
+ "config",
302
+ "functions",
303
+ "crons",
304
+ "schema",
305
+ "usersCollection",
306
+ "registerDatabase",
307
+ "dockerfile",
308
+ "context",
309
+ "port"
310
+ ],
311
+ static: [
312
+ "type",
313
+ "root",
314
+ "build",
315
+ "output",
316
+ "path",
317
+ "spa",
318
+ "cms"
319
+ ]
320
+ };
321
+ /**
322
+ * Fields people reach for to give a static app a hostname, which is spelled as
323
+ * a URL in `path` instead. Named so the warning says that, rather than "your
324
+ * CLI may be older than this manifest" — which is false here and sends the
325
+ * reader to upgrade a CLI that already does what they wanted.
326
+ */
327
+ var HOSTNAME_FIELDS = /* @__PURE__ */ new Set([
328
+ "host",
329
+ "hostname",
330
+ "domain"
331
+ ]);
332
+ /**
333
+ * Report a field this CLI does not recognise.
334
+ *
335
+ * A warning rather than an error, and the distinction matters in both
336
+ * directions. `"pathh": "/admin"` is a typo that would otherwise be silently
337
+ * dropped — the app builds for `/`, mounts at `/`, and the only symptom is that
338
+ * it is not where you put it. But an unknown field is also what an *older* CLI
339
+ * sees when it opens a manifest written for a newer one, and refusing to build
340
+ * over a field that is simply from the future would make every manifest addition
341
+ * a breaking change.
342
+ *
343
+ * So: name it, suggest the near-miss, and carry on.
344
+ */
345
+ function warnUnknownFields(name, raw, type) {
346
+ const known = KNOWN_APP_FIELDS[type];
347
+ if (!known) return;
348
+ for (const field of Object.keys(raw)) {
349
+ if (known.includes(field)) continue;
350
+ if (type === "static" && HOSTNAME_FIELDS.has(field)) {
351
+ const value = raw[field];
352
+ const host = typeof value === "string" && value.trim() !== "" ? value.trim().replace(/^[a-z]+:\/\//i, "").replace(/\/+$/, "") : "admin.example.com";
353
+ const under = typeof raw.path === "string" && raw.path.startsWith("/") && raw.path !== "/" ? raw.path : "";
354
+ console.warn(`⚠ rebase.json: apps.${name}.${field} is not a field — it is ignored. To serve this app on its own hostname, write its path as a URL: "path": "https://${host}${under}"`);
355
+ continue;
356
+ }
357
+ const suggestion = known.find((candidate) => isNearMiss(candidate, field));
358
+ console.warn(`⚠ rebase.json: apps.${name}.${field} is not a field this CLI knows.` + (suggestion ? ` Did you mean "${suggestion}"?` : " It is ignored — your CLI may be older than this manifest."));
359
+ }
360
+ }
361
+ /**
362
+ * One edit apart, ignoring case: enough for a typo, tight enough to stay quiet.
363
+ *
364
+ * The same rule as `isNearMiss` in `@rebasepro/utils`, which the collection
365
+ * validator uses for unknown `admin` keys. It is a copy rather than an import
366
+ * because the CLI does not depend on `@rebasepro/utils`, and adding the
367
+ * dependency is a lockfile change; collapsing the two is worth doing the next
368
+ * time this package's dependencies are touched.
369
+ */
370
+ function isNearMiss(a, b) {
371
+ const x = a.toLowerCase();
372
+ const y = b.toLowerCase();
373
+ if (x === y) return true;
374
+ if (Math.abs(x.length - y.length) > 1) return false;
375
+ const [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];
376
+ let i = 0;
377
+ let j = 0;
378
+ let edits = 0;
379
+ while (i < shorter.length && j < longer.length) {
380
+ if (shorter[i] === longer[j]) {
381
+ i++;
382
+ j++;
383
+ continue;
384
+ }
385
+ if (++edits > 1) return false;
386
+ if (shorter.length === longer.length) i++;
387
+ j++;
388
+ }
389
+ return edits + (longer.length - j) + (shorter.length - i) <= 1;
390
+ }
391
+ function validateApp(name, raw, issues) {
392
+ const base = `apps.${name}`;
393
+ if (!isRecord(raw)) {
394
+ issues.push({
395
+ path: base,
396
+ message: "must be an object"
397
+ });
398
+ return;
399
+ }
400
+ const type = raw.type;
401
+ if (typeof type === "string" && REMOVED_APP_TYPES[type]) {
402
+ issues.push({
403
+ path: `${base}.type`,
404
+ message: `"${type}" is no longer an app type — ${REMOVED_APP_TYPES[type]}`
405
+ });
406
+ return;
407
+ }
408
+ if (typeof type !== "string" || !APP_TYPES.includes(type)) {
409
+ issues.push({
410
+ path: `${base}.type`,
411
+ message: `must be one of: ${APP_TYPES.join(", ")}`
412
+ });
413
+ return;
414
+ }
415
+ warnUnknownFields(name, raw, type);
416
+ switch (type) {
417
+ case "backend": {
418
+ checkRelativePath(raw.config, `${base}.config`, issues, { required: false });
419
+ checkRelativePath(raw.functions, `${base}.functions`, issues, { required: false });
420
+ checkRelativePath(raw.crons, `${base}.crons`, issues, { required: false });
421
+ checkRelativePath(raw.schema, `${base}.schema`, issues, { required: false });
422
+ checkRelativePath(raw.usersCollection, `${base}.usersCollection`, issues, { required: false });
423
+ if (raw.mode !== void 0) issues.push({
424
+ path: `${base}.mode`,
425
+ message: "is no longer a field — collections come from the config directory when it exists, and are introspected from the database when it does not"
426
+ });
427
+ const custom = raw.runtime === "custom";
428
+ if (raw.runtime !== "managed" && !custom) issues.push({
429
+ path: `${base}.runtime`,
430
+ message: "is required, and must be \"managed\" or \"custom\""
431
+ });
432
+ for (const field of [
433
+ "dockerfile",
434
+ "context",
435
+ "port"
436
+ ]) if (raw[field] !== void 0 && !custom) issues.push({
437
+ path: `${base}.${field}`,
438
+ message: "only applies to a custom runtime — set \"runtime\": \"custom\" to build your own image"
439
+ });
440
+ checkRelativePath(raw.dockerfile, `${base}.dockerfile`, issues, { required: false });
441
+ checkRelativePath(raw.context, `${base}.context`, issues, {
442
+ required: false,
443
+ mayEscape: true
444
+ });
445
+ if (raw.port !== void 0 && (typeof raw.port !== "number" || !Number.isInteger(raw.port))) issues.push({
446
+ path: `${base}.port`,
447
+ message: "must be an integer"
448
+ });
449
+ if (raw.registerDatabase !== void 0 && typeof raw.registerDatabase !== "boolean") issues.push({
450
+ path: `${base}.registerDatabase`,
451
+ message: "must be a boolean"
452
+ });
453
+ return {
454
+ type: "backend",
455
+ runtime: custom ? "custom" : "managed",
456
+ config: raw.config,
457
+ functions: raw.functions,
458
+ crons: raw.crons,
459
+ schema: raw.schema,
460
+ usersCollection: raw.usersCollection,
461
+ registerDatabase: raw.registerDatabase,
462
+ dockerfile: raw.dockerfile,
463
+ context: raw.context,
464
+ port: raw.port
465
+ };
466
+ }
467
+ case "static": {
468
+ checkRelativePath(raw.root, `${base}.root`, issues, { required: true });
469
+ checkRelativePath(raw.output, `${base}.output`, issues, { required: true });
470
+ if (raw.build !== void 0 && typeof raw.build !== "string") issues.push({
471
+ path: `${base}.build`,
472
+ message: "must be a string command"
473
+ });
474
+ if (raw.spa !== void 0 && typeof raw.spa !== "boolean") issues.push({
475
+ path: `${base}.spa`,
476
+ message: "must be a boolean"
477
+ });
478
+ const address = checkAppAddress(raw.path, `${base}.path`, issues);
479
+ checkCmsPath(raw.cms, address ?? { path: "/" }, `${base}.cms`, issues);
480
+ return {
481
+ type: "static",
482
+ root: raw.root,
483
+ build: raw.build,
484
+ output: raw.output,
485
+ path: address ? raw.path : void 0,
486
+ spa: raw.spa,
487
+ cms: raw.cms
488
+ };
489
+ }
490
+ default: return;
491
+ }
492
+ }
493
+ /**
494
+ * Validate a parsed manifest, collecting every problem.
495
+ */
496
+ function validateManifest(raw) {
497
+ const issues = [];
498
+ if (!isRecord(raw)) return { issues: [{
499
+ path: "",
500
+ message: `${MANIFEST_FILENAME} must contain a JSON object`
501
+ }] };
502
+ if (typeof raw.rebase !== "string" || raw.rebase.trim() === "") {
503
+ const message = typeof raw.runtime === "string" ? `is required, e.g. "^1" — this is the top-level "runtime" key renamed, because "runtime" now means "managed" or "custom" on the backend app` : `is required, e.g. "^1"`;
504
+ issues.push({
505
+ path: "rebase",
506
+ message
507
+ });
508
+ }
509
+ if (!isRecord(raw.apps)) {
510
+ issues.push({
511
+ path: "apps",
512
+ message: "is required and must be an object"
513
+ });
514
+ return { issues };
515
+ }
516
+ const apps = {};
517
+ let backendCount = 0;
518
+ for (const [name, value] of Object.entries(raw.apps)) {
519
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
520
+ issues.push({
521
+ path: `apps.${name}`,
522
+ message: "name must be lowercase alphanumeric with dashes (it appears in URLs)"
523
+ });
524
+ continue;
525
+ }
526
+ if (RESERVED_APP_NAMES.has(name)) {
527
+ issues.push({
528
+ path: `apps.${name}`,
529
+ message: "name is reserved"
530
+ });
531
+ continue;
532
+ }
533
+ const app = validateApp(name, value, issues);
534
+ if (!app) continue;
535
+ if (app.type === "backend") backendCount++;
536
+ apps[name] = app;
537
+ }
538
+ if (backendCount > 1) issues.push({
539
+ path: "apps",
540
+ message: "a project may declare at most one backend app"
541
+ });
542
+ const byAddress = /* @__PURE__ */ new Map();
543
+ for (const [name, app] of Object.entries(apps)) {
544
+ if (app.type !== "static") continue;
545
+ const address = staticAppAddress(name, app);
546
+ const key = `${address.host ?? ""} ${address.path}`;
547
+ const owner = byAddress.get(key);
548
+ if (owner) {
549
+ issues.push({
550
+ path: `apps.${name}.path`,
551
+ message: `two apps cannot serve the same path — "${owner}" is already at "${formatAppAddress(address)}"`
552
+ });
553
+ continue;
554
+ }
555
+ byAddress.set(key, name);
556
+ }
557
+ const withCms = Object.entries(apps).filter(([, app]) => app.type === "static" && typeof app.cms === "string");
558
+ if (withCms.length > 1) for (const [name] of withCms.slice(1)) issues.push({
559
+ path: `apps.${name}.cms`,
560
+ message: `a project has one CMS — "${withCms[0][0]}" already declares one`
561
+ });
562
+ refuseStorageBlock(raw.storage, issues);
563
+ let telemetry;
564
+ if (raw.telemetry !== void 0) if (typeof raw.telemetry === "boolean") telemetry = raw.telemetry;
565
+ else issues.push({
566
+ path: "telemetry",
567
+ message: "must be a boolean — only `false` does anything, and it opts this repository out of usage sharing"
568
+ });
569
+ if (issues.length > 0) return { issues };
570
+ return {
571
+ manifest: {
572
+ $schema: typeof raw.$schema === "string" ? raw.$schema : void 0,
573
+ rebase: raw.rebase,
574
+ apps,
575
+ ...telemetry !== void 0 ? { telemetry } : {}
576
+ },
577
+ issues
578
+ };
579
+ }
580
+ /**
581
+ * Refuse a hand-written `storage` block.
582
+ *
583
+ * Buckets are declared in config code now — `bucket("media")` — and the graph
584
+ * is generated from those declarations into `rebase.resources.json`. Two homes
585
+ * for one concept is what this replaced, and the reason it had to go is that
586
+ * the runtime *merged* them: a bucket declared in both had one engine kept and
587
+ * the other silently discarded.
588
+ *
589
+ * Refused rather than ignored. A key that still parses and no longer does
590
+ * anything is the same failure wearing a nicer hat — the author sees valid
591
+ * JSON, the platform sees a bucket, and the runtime has neither.
592
+ */
593
+ function refuseStorageBlock(raw, issues) {
594
+ if (raw === void 0) return void 0;
595
+ issues.push({
596
+ path: "storage",
597
+ message: "is no longer read. Declare each bucket in your config's resources.ts — bucket(\"media\", { engine: \"s3\" }) — then run `rebase resources --write`, which generates rebase.resources.json. Refused rather than ignored: this block used to be merged with the code's declarations and silently win."
598
+ });
599
+ }
600
+ /**
601
+ * Infer a manifest from a directory that does not have one.
602
+ *
603
+ * This mirrors what the template scaffolds, which is what makes adopting the
604
+ * manifest a no-op for existing projects: the synthesized result is what they
605
+ * would have written by hand. A field is written only when it says something
606
+ * its default does not, as the scaffold's own `rebase.json` does — `spa`,
607
+ * `functions` and `crons` restate their defaults, so they are left out.
608
+ *
609
+ * The static app's build command is the project's package manager, spelled the
610
+ * way `rebase init` spells it, so a pnpm project is never built with npm.
611
+ *
612
+ * The static app is named `web`, where the scaffold says `admin`. The name is an
613
+ * app's identity on a deployed project: Rebase Cloud registered `web` for every
614
+ * project that deployed without a manifest, and its next deploy has to name the
615
+ * same app. Nor can an inference know where an app mounts the CMS (`cms`), which
616
+ * is a client-side route. `rebase apps init --force` therefore keeps the name,
617
+ * `path` and `cms` a manifest already declares for the app it infers again.
618
+ *
619
+ * The backend's `runtime` is inferred from whether the repository declares a
620
+ * **Dockerfile** — the thing that actually builds an image — and from nothing
621
+ * else. It used to be inferred from the presence of `backend/src/index.ts`,
622
+ * which every scaffolded project had whether or not it wanted its own server, so
623
+ * projects predating the manifest silently landed on the custom runtime and paid
624
+ * for it.
625
+ */
626
+ function synthesizeManifest(projectRoot) {
627
+ const exists = (relative) => fs.existsSync(path.join(projectRoot, relative));
628
+ const apps = {};
629
+ const hasConfig = exists(DEFAULT_CONFIG_DIR);
630
+ const hasBackend = exists("backend");
631
+ const dockerfile = ["Dockerfile", "backend/Dockerfile"].find(exists);
632
+ if (hasBackend || hasConfig) {
633
+ apps.backend = dockerfile ? {
634
+ type: "backend",
635
+ runtime: "custom",
636
+ dockerfile,
637
+ context: "."
638
+ } : {
639
+ type: "backend",
640
+ runtime: "managed"
641
+ };
642
+ if (!dockerfile && exists("backend/src/index.ts")) console.warn("⚠ backend/src/index.ts exists but this project's backend is managed — it is\n never loaded. Delete it, or move it aside and run `rebase eject`, which\n writes an entrypoint of its own and owns the image. Eject does not adopt\n this file: it refuses to replace it unless you pass --force.");
643
+ }
644
+ if (exists("frontend")) apps.web = {
645
+ type: "static",
646
+ root: "frontend",
647
+ build: getPMCommands(detectPackageManager(projectRoot)).runWorkspace("frontend", "build").join(" "),
648
+ output: "frontend/dist",
649
+ path: "/"
650
+ };
651
+ return {
652
+ rebase: "^1",
653
+ apps
654
+ };
655
+ }
656
+ function manifestPath(projectRoot) {
657
+ return path.join(projectRoot, MANIFEST_FILENAME);
658
+ }
659
+ function manifestExists(projectRoot) {
660
+ return fs.existsSync(manifestPath(projectRoot));
661
+ }
662
+ /**
663
+ * Read the manifest, falling back to a synthesized one.
664
+ *
665
+ * A malformed manifest throws — unlike a missing one. Silently ignoring a file
666
+ * the developer wrote, and building something else instead, is the worst
667
+ * available behaviour.
668
+ */
669
+ function loadManifest(projectRoot) {
670
+ const filePath = manifestPath(projectRoot);
671
+ if (!fs.existsSync(filePath)) return {
672
+ manifest: synthesizeManifest(projectRoot),
673
+ source: "synthesized"
674
+ };
675
+ let parsed;
676
+ try {
677
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
678
+ } catch (err) {
679
+ throw new ManifestError(`${MANIFEST_FILENAME} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
680
+ }
681
+ const { manifest, issues } = validateManifest(parsed);
682
+ if (!manifest) throw new ManifestError(`${MANIFEST_FILENAME} is invalid`, issues);
683
+ return {
684
+ manifest,
685
+ source: "file",
686
+ filePath
687
+ };
688
+ }
689
+ /** The keys `writeManifest` knows how to write. Everything else is carried through. */
690
+ var MODELLED_MANIFEST_KEYS = [
691
+ "$schema",
692
+ "rebase",
693
+ "apps",
694
+ "storage",
695
+ "telemetry"
696
+ ];
697
+ /**
698
+ * What is on disk right now, or `{}` — this is a *rewrite*, so the file that is
699
+ * about to be replaced is the only record of the keys the caller did not model.
700
+ * Unparseable is treated as absent: `loadManifest` refuses malformed JSON long
701
+ * before anything gets here, and a writer is not the place to fail on it.
702
+ */
703
+ function readManifestObject(filePath) {
704
+ try {
705
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
706
+ return isRecord(parsed) ? parsed : {};
707
+ } catch {
708
+ return {};
709
+ }
710
+ }
711
+ /**
712
+ * Write a manifest, with a trailing newline so it plays well with other tools.
713
+ *
714
+ * **Every key on disk survives.** This used to emit exactly `$schema`, `rebase`
715
+ * and `apps`, so a rewrite deleted the rest of the file — and two commands with
716
+ * no visible relationship to either key rewrite it: `rebase eject` and
717
+ * `rebase apps init --force`. A repository that had committed
718
+ * `"telemetry": false` lost its opt-out, and a multi-bucket project lost its
719
+ * whole `storage` block, in a commit whose stated change was `runtime: custom`.
720
+ *
721
+ * So: the caller's manifest wins for what it models, the file supplies the rest.
722
+ * `storage` and `telemetry` fall back to the file because the callers that
723
+ * synthesize a manifest (`apps init --force`) cannot know them — they are
724
+ * authored, not inferred — and unknown top-level keys are copied verbatim
725
+ * rather than listed, since a hand-listed set loses the next key too.
726
+ */
727
+ function writeManifest(projectRoot, manifest) {
728
+ const filePath = manifestPath(projectRoot);
729
+ const existing = readManifestObject(filePath);
730
+ const carried = {};
731
+ for (const [key, value] of Object.entries(existing)) if (!MODELLED_MANIFEST_KEYS.includes(key)) carried[key] = value;
732
+ const schema = manifest.$schema ?? (typeof existing.$schema === "string" ? existing.$schema : void 0) ?? "https://rebase.pro/schemas/rebase.json";
733
+ const telemetry = manifest.telemetry ?? (typeof existing.telemetry === "boolean" ? existing.telemetry : void 0);
734
+ const ordered = {
735
+ $schema: schema,
736
+ rebase: manifest.rebase,
737
+ apps: manifest.apps,
738
+ ...telemetry !== void 0 ? { telemetry } : {},
739
+ ...carried
740
+ };
741
+ fs.writeFileSync(filePath, `${JSON.stringify(ordered, null, 4)}\n`, "utf8");
742
+ return filePath;
743
+ }
744
+ /** Find the single backend app, if this repository declares one. */
745
+ function findBackendApp(manifest) {
746
+ for (const [name, app] of Object.entries(manifest.apps)) if (app.type === "backend") return {
747
+ name,
748
+ app
749
+ };
750
+ }
751
+ /**
752
+ * Where this project mounts the Rebase CMS, if it says.
753
+ *
754
+ * The CMS is a component inside a developer's own app, so its address is a
755
+ * client-side route: no build artifact, no running server and no control plane
756
+ * can observe it. {@link RebaseStaticAppConfig.cms} is the only place that fact
757
+ * is ever written down, and this is the one reader of it — so that "the
758
+ * project's CMS" means the same thing to `rebase dev`, `rebase apps list`, the
759
+ * bundle it builds and the console that shows it.
760
+ *
761
+ * Returns the *serving* app alongside the path, because a caller with a base URL
762
+ * needs to know which app answers there — and because the path alone cannot say
763
+ * whether the CMS is the whole of an app or one route of it.
764
+ *
765
+ * `path` is always a path. `host` is the serving app's hostname, when its `path`
766
+ * gives it one: the CMS is then at `https://<host><path>` and nowhere else, and
767
+ * a caller holding a local or project base URL must not put the path on that.
768
+ */
769
+ function cmsMountOf(manifest) {
770
+ for (const [name, app] of Object.entries(manifest.apps)) {
771
+ if (app.type !== "static" || typeof app.cms !== "string") continue;
772
+ const { host } = staticAppAddress(name, app);
773
+ return {
774
+ appName: name,
775
+ app,
776
+ path: app.cms,
777
+ ...host ? { host } : {}
778
+ };
779
+ }
780
+ }
781
+ /**
782
+ * The app a deploy targets: the one named, or the obvious one.
783
+ *
784
+ * A repository declares apps; a project owns them. So "which app" is a question
785
+ * a multi-repo setup asks routinely and a single-app repository should never
786
+ * have to answer — hence the fallbacks, in the order that makes each of them
787
+ * unambiguous:
788
+ *
789
+ * - A name, when one is given. Wrong names fail loudly, listing what is there;
790
+ * guessing past a typo would deploy the wrong app to a live project.
791
+ * - The backend, when this repository declares one. It is the app whose deploy
792
+ * people mean when they do not say, and it is what every existing invocation
793
+ * already did.
794
+ * - The only app, when there is exactly one. This is what makes a static-only
795
+ * repository — an admin panel, a marketing site — deploy with no argument.
796
+ * - Otherwise a refusal that lists the choices. A repository with two static
797
+ * apps and no backend has no obvious default, and picking one would publish
798
+ * somebody's admin panel at their marketing domain.
799
+ */
800
+ function selectDeployApp(manifest, requested) {
801
+ const entries = Object.entries(manifest.apps).map(([name, app]) => ({
802
+ name,
803
+ app
804
+ }));
805
+ if (requested) {
806
+ const match = entries.find((entry) => entry.name === requested);
807
+ if (!match) throw new ManifestError(`This repository declares no app named "${requested}". It declares: ${entries.map((e) => e.name).join(", ") || "none"}.`);
808
+ return match;
809
+ }
810
+ const backend = findBackendApp(manifest);
811
+ if (backend) return {
812
+ name: backend.name,
813
+ app: backend.app
814
+ };
815
+ if (entries.length === 1) return entries[0];
816
+ if (entries.length === 0) throw new ManifestError(`${MANIFEST_FILENAME} declares no apps, so there is nothing to deploy.`);
817
+ throw new ManifestError(`This repository declares several apps and no backend, so there is no obvious one to deploy. Name one: ${entries.map((e) => e.name).join(", ")}.`);
818
+ }
819
+ /** Apps that produce build output, in the order they should be built. */
820
+ function buildableApps(manifest) {
821
+ const entries = Object.entries(manifest.apps).map(([name, app]) => ({
822
+ name,
823
+ app
824
+ }));
825
+ const rank = (app) => app.type === "backend" ? 0 : 1;
826
+ return entries.sort((a, b) => rank(a.app) - rank(b.app));
827
+ }
828
+ /**
829
+ * Decide whether a project can run on the managed runtime, and say why not.
830
+ *
831
+ * "Not eligible" is never a dead end — it selects the custom-runtime path, which
832
+ * still deploys. The reasons exist so the answer is actionable rather than a
833
+ * verdict.
834
+ */
835
+ function assessManagedCompatibility(manifest) {
836
+ const backend = findBackendApp(manifest);
837
+ if (!backend) return {
838
+ eligible: false,
839
+ reasons: ["No backend app is declared in this repository. Only the repository that declares the backend selects the runtime."]
840
+ };
841
+ if (backend.app.runtime === "custom") return {
842
+ eligible: false,
843
+ reasons: [`App "${backend.name}" declares runtime: "custom", so it builds and runs its own image. The managed runtime boots the platform image with your bundle; the custom runtime deploys exactly the same way, from your Dockerfile.`]
844
+ };
845
+ return {
846
+ eligible: true,
847
+ reasons: []
848
+ };
849
+ }
850
+ /**
851
+ * Resolve a backend app's directories against the conventions it omits.
852
+ *
853
+ * `hasCollections` replaces the old `mode: "cms" | "baas"` field. Where the
854
+ * collections come from was never an independent choice: either they are
855
+ * declared in code and the bundle ships them, or they are not and the runtime
856
+ * introspects the live database at boot. Declaring it separately only created
857
+ * the contradictory state — code-first declared, no collections anywhere.
858
+ *
859
+ * `hasConfig` is deliberately a **separate** question. A headless project has no
860
+ * `config/collections`, but it may still ship a config package — that is where
861
+ * the `storageAuthorize` hook lives, and storage is not under row-level
862
+ * security, so without one the server refuses to boot with storage enabled.
863
+ * Collapsing the two would leave a headless project nowhere to put it.
864
+ */
865
+ function resolveBackendPaths(app, projectRoot) {
866
+ const config = app.config ?? "config";
867
+ return {
868
+ config,
869
+ functions: app.functions ?? "backend/functions",
870
+ crons: app.crons ?? "backend/crons",
871
+ schema: app.schema ?? "backend/src/schema.generated.ts",
872
+ usersCollection: app.usersCollection ?? "collections/users",
873
+ hasConfig: fs.existsSync(path.join(projectRoot, config)),
874
+ hasCollections: fs.existsSync(path.join(projectRoot, config, "collections"))
875
+ };
876
+ }
877
+ //#endregion
878
+ export { selectDeployApp as _, DEFAULT_SCHEMA_FILE as a, validateManifest as b, buildableApps as c, findBackendApp as d, formatAppAddress as f, resolveBackendPaths as g, manifestPath as h, DEFAULT_FUNCTIONS_DIR as i, cmsMountOf as l, manifestExists as m, DEFAULT_CONFIG_DIR as n, ManifestError as o, loadManifest as p, DEFAULT_CRONS_DIR as r, assessManagedCompatibility as s, CURRENT_RUNTIME_RANGE as t, compareMountOrder as u, staticAppAddress as v, writeManifest as x, synthesizeManifest as y };
879
+
880
+ //# sourceMappingURL=manifest-DRp0UVTj.js.map