@rebasepro/cli 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +2 -1
  2. package/bin/rebase.js +143 -4
  3. package/dist/{agent-setup-BYb2Tbdz.js → agent-setup-D9mHRZ5R.js} +125 -3
  4. package/dist/agent-setup-D9mHRZ5R.js.map +1 -0
  5. package/dist/{api-keys-CSMbsmAn.js → api-keys-CyJ26KEJ.js} +41 -9
  6. package/dist/api-keys-CyJ26KEJ.js.map +1 -0
  7. package/dist/{apps-iBXNZhVN.js → apps-CnLDV5b6.js} +6 -6
  8. package/dist/{apps-iBXNZhVN.js.map → apps-CnLDV5b6.js.map} +1 -1
  9. package/dist/{atlas-dev-database-BusbHP6n.js → atlas-dev-database-BODqdCWt.js} +2 -2
  10. package/dist/{atlas-dev-database-BusbHP6n.js.map → atlas-dev-database-BODqdCWt.js.map} +1 -1
  11. package/dist/{auth-DRsTWPky.js → auth-dII2jCO1.js} +10 -7
  12. package/dist/auth-dII2jCO1.js.map +1 -0
  13. package/dist/{branch-pointer-Bn_FjNld.js → branch-pointer-BQUF6lYa.js} +1 -1
  14. package/dist/{build-et7Rccxi.js → build-Cza7ZQrp.js} +6 -6
  15. package/dist/{build-et7Rccxi.js.map → build-Cza7ZQrp.js.map} +1 -1
  16. package/dist/{bundle-DlqkDYSe.js → bundle-C6yEtMJ-.js} +53 -5
  17. package/dist/bundle-C6yEtMJ-.js.map +1 -0
  18. package/dist/bundle.d.ts +14 -0
  19. package/dist/{cloud-B-ZIjHrA.js → cloud-BdnmqQaZ.js} +8527 -4340
  20. package/dist/cloud-BdnmqQaZ.js.map +1 -0
  21. package/dist/collections-dir-DlvDhime.js +69 -0
  22. package/dist/collections-dir-DlvDhime.js.map +1 -0
  23. package/dist/commands/agent-setup.d.ts +58 -0
  24. package/dist/commands/api-keys.d.ts +13 -0
  25. package/dist/commands/cloud/activity.d.ts +25 -0
  26. package/dist/commands/cloud/app-key.d.ts +18 -0
  27. package/dist/commands/cloud/auth.d.ts +1 -0
  28. package/dist/commands/cloud/billing-invoices.d.ts +39 -0
  29. package/dist/commands/cloud/claim.d.ts +7 -0
  30. package/dist/commands/cloud/context.d.ts +115 -6
  31. package/dist/commands/cloud/deploy-summary.d.ts +162 -0
  32. package/dist/commands/cloud/deploy.d.ts +64 -0
  33. package/dist/commands/cloud/env.d.ts +24 -0
  34. package/dist/commands/cloud/index.d.ts +13 -1
  35. package/dist/commands/cloud/init.d.ts +143 -0
  36. package/dist/commands/cloud/login-flow.d.ts +51 -0
  37. package/dist/commands/cloud/notifications.d.ts +28 -0
  38. package/dist/commands/cloud/orgs.d.ts +13 -0
  39. package/dist/commands/cloud/previews.d.ts +123 -0
  40. package/dist/commands/cloud/resources.d.ts +68 -14
  41. package/dist/commands/cloud/runtime-logs.d.ts +176 -0
  42. package/dist/commands/cloud/service-key.d.ts +15 -0
  43. package/dist/commands/cloud/token-capabilities.d.ts +61 -8
  44. package/dist/commands/cloud/tokens.d.ts +18 -3
  45. package/dist/commands/cloud/usage.d.ts +63 -0
  46. package/dist/commands/dev.d.ts +10 -0
  47. package/dist/commands/doctor.d.ts +1 -1
  48. package/dist/commands/generate-view.d.ts +94 -0
  49. package/dist/commands/generate_sdk.d.ts +15 -0
  50. package/dist/commands/skills.d.ts +4 -3
  51. package/dist/commands/storage.d.ts +4 -0
  52. package/dist/commands/verify.d.ts +96 -0
  53. package/dist/{consent-BaB-eQKG.js → consent-BD7F29T3.js} +2 -2
  54. package/dist/{consent-BaB-eQKG.js.map → consent-BD7F29T3.js.map} +1 -1
  55. package/dist/{console-BlD9kjFW.js → console-BXB6-8GT.js} +12 -6
  56. package/dist/console-BXB6-8GT.js.map +1 -0
  57. package/dist/{context-DHiiiJ5C.js → context-GUxkZm-S.js} +481 -24
  58. package/dist/context-GUxkZm-S.js.map +1 -0
  59. package/dist/{daemon-DDMRbz4T.js → daemon-Cp7oNXth.js} +2 -2
  60. package/dist/{daemon-DDMRbz4T.js.map → daemon-Cp7oNXth.js.map} +1 -1
  61. package/dist/{db-9P6MnmdC.js → db-KzlqddGD.js} +37 -30
  62. package/dist/db-KzlqddGD.js.map +1 -0
  63. package/dist/{derive-MTdClnkc.js → derive-ae9UOoOB.js} +2 -2
  64. package/dist/{derive-MTdClnkc.js.map → derive-ae9UOoOB.js.map} +1 -1
  65. package/dist/{dev-BDN_OKXA.js → dev-DTqVMgB3.js} +59 -32
  66. package/dist/dev-DTqVMgB3.js.map +1 -0
  67. package/dist/{dev-preflight-FT9GbJVj.js → dev-preflight-Cxv3TiD1.js} +2 -2
  68. package/dist/{dev-preflight-FT9GbJVj.js.map → dev-preflight-Cxv3TiD1.js.map} +1 -1
  69. package/dist/{dev-server-CIZwAfZa.js → dev-server-OJsUGZEB.js} +2 -2
  70. package/dist/{dev-server-CIZwAfZa.js.map → dev-server-OJsUGZEB.js.map} +1 -1
  71. package/dist/{doctor-D8YXz7df.js → doctor-BRWOlqeG.js} +13 -11
  72. package/dist/doctor-BRWOlqeG.js.map +1 -0
  73. package/dist/{eject-izG2KjMo.js → eject-DABcDnPN.js} +3 -3
  74. package/dist/{eject-izG2KjMo.js.map → eject-DABcDnPN.js.map} +1 -1
  75. package/dist/{fold-static-CybSay_W.js → fold-static-BnAK-anQ.js} +3 -3
  76. package/dist/{fold-static-CybSay_W.js.map → fold-static-BnAK-anQ.js.map} +1 -1
  77. package/dist/generate-view-IFwCADvb.js +662 -0
  78. package/dist/generate-view-IFwCADvb.js.map +1 -0
  79. package/dist/{generate_sdk-B4R65oUV.js → generate_sdk-BvcWvoUh.js} +33 -7
  80. package/dist/generate_sdk-BvcWvoUh.js.map +1 -0
  81. package/dist/index.es.js +57 -38
  82. package/dist/index.es.js.map +1 -1
  83. package/dist/{init-DYaeppEN.js → init-CaU6GhwI.js} +8 -8
  84. package/dist/{init-DYaeppEN.js.map → init-CaU6GhwI.js.map} +1 -1
  85. package/dist/last-dev-database-DpW5dfoX.js +2 -0
  86. package/dist/{manifest-nvPdLlJY.js → manifest-BtzDy7KQ.js} +36 -3
  87. package/dist/{manifest-nvPdLlJY.js.map → manifest-BtzDy7KQ.js.map} +1 -1
  88. package/dist/{normalize-imports-BYw8ad87.js → normalize-imports-D0zXlM6g.js} +3 -3
  89. package/dist/{normalize-imports-BYw8ad87.js.map → normalize-imports-D0zXlM6g.js.map} +1 -1
  90. package/dist/{path-flags-BFgG9V6y.js → path-flags-DNvtEi2-.js} +5 -4
  91. package/dist/path-flags-DNvtEi2-.js.map +1 -0
  92. package/dist/{prepare-1rVNd2PF.js → prepare-CTw9NEkh.js} +4 -4
  93. package/dist/{prepare-1rVNd2PF.js.map → prepare-CTw9NEkh.js.map} +1 -1
  94. package/dist/{project-g4K6kV1t.js → project-CasK56vN.js} +2 -2
  95. package/dist/{project-g4K6kV1t.js.map → project-CasK56vN.js.map} +1 -1
  96. package/dist/resolve-D647F1LA.js +2 -0
  97. package/dist/{resources-CovP8AZ-.js → resources-CPSC5Kcu.js} +4 -4
  98. package/dist/{resources-CovP8AZ-.js.map → resources-CPSC5Kcu.js.map} +1 -1
  99. package/dist/{schema-nuxvvc50.js → schema-BSXwZSiI.js} +9 -7
  100. package/dist/schema-BSXwZSiI.js.map +1 -0
  101. package/dist/{skills-DeUIsZcT.js → skills-BU2flMOy.js} +52 -7
  102. package/dist/skills-BU2flMOy.js.map +1 -0
  103. package/dist/{start-RzZqDroW.js → start-mVb6upwY.js} +5 -5
  104. package/dist/{start-RzZqDroW.js.map → start-mVb6upwY.js.map} +1 -1
  105. package/dist/static-syntax-check.d.ts +64 -0
  106. package/dist/{status-D9Zm88NZ.js → status-BREvtzJm.js} +7 -7
  107. package/dist/{status-D9Zm88NZ.js.map → status-BREvtzJm.js.map} +1 -1
  108. package/dist/{storage-BB3umRfw.js → storage-DtOXGflp.js} +159 -15
  109. package/dist/storage-DtOXGflp.js.map +1 -0
  110. package/dist/{telemetry-HfU5uAh1.js → telemetry-CgwJPVMO.js} +2 -2
  111. package/dist/{telemetry-HfU5uAh1.js.map → telemetry-CgwJPVMO.js.map} +1 -1
  112. package/dist/{telemetry-TseNLk6X.js → telemetry-DwK5RQJl.js} +4 -4
  113. package/dist/{telemetry-TseNLk6X.js.map → telemetry-DwK5RQJl.js.map} +1 -1
  114. package/dist/{upgrade-apiPKKE_.js → upgrade-B7sVwL8S.js} +4 -4
  115. package/dist/{upgrade-apiPKKE_.js.map → upgrade-B7sVwL8S.js.map} +1 -1
  116. package/dist/utils/collections-dir.d.ts +14 -0
  117. package/dist/utils/path-flags.d.ts +2 -1
  118. package/dist/utils/tool-stdio.d.ts +1 -1
  119. package/dist/verify-D7UsV_qe.js +730 -0
  120. package/dist/verify-D7UsV_qe.js.map +1 -0
  121. package/package.json +9 -7
  122. package/templates/eject/backend/src/index.ts +9 -1
  123. package/templates/generated-schemas/ecommerce.ts +3 -0
  124. package/templates/overlays/baas/ai-instructions.md +13 -0
  125. package/templates/template/.claude/hooks/post-edit-collection.sh +99 -0
  126. package/templates/template/.claude/settings.json +17 -0
  127. package/templates/template/.env.example +10 -1
  128. package/templates/template/ai-instructions.md +21 -1
  129. package/templates/template/config/collections/presets/ecommerce/index.ts +23 -0
  130. package/templates/template/docker-compose.yml +4 -1
  131. package/templates/template/frontend/package.json +1 -0
  132. package/templates/views/AppView.tsx.tmpl +138 -0
  133. package/templates/views/EntityView.tsx.tmpl +64 -0
  134. package/templates/views/index.ts.tmpl +8 -0
  135. package/dist/agent-setup-BYb2Tbdz.js.map +0 -1
  136. package/dist/api-keys-CSMbsmAn.js.map +0 -1
  137. package/dist/auth-DRsTWPky.js.map +0 -1
  138. package/dist/bundle-DlqkDYSe.js.map +0 -1
  139. package/dist/cloud-B-ZIjHrA.js.map +0 -1
  140. package/dist/console-BlD9kjFW.js.map +0 -1
  141. package/dist/context-DHiiiJ5C.js.map +0 -1
  142. package/dist/db-9P6MnmdC.js.map +0 -1
  143. package/dist/dev-BDN_OKXA.js.map +0 -1
  144. package/dist/doctor-D8YXz7df.js.map +0 -1
  145. package/dist/generate_sdk-B4R65oUV.js.map +0 -1
  146. package/dist/last-dev-database-B_75fxZC.js +0 -2
  147. package/dist/path-flags-BFgG9V6y.js.map +0 -1
  148. package/dist/resolve-BQUAC3Wi.js +0 -2
  149. package/dist/schema-nuxvvc50.js.map +0 -1
  150. package/dist/skills-DeUIsZcT.js.map +0 -1
  151. package/dist/storage-BB3umRfw.js.map +0 -1
@@ -0,0 +1,662 @@
1
+ import { b as UsageError, p as requireProjectRoot, w as wantsHelp, x as parseCommandArgs } from "./project-CasK56vN.js";
2
+ import { l as cmsMountOf, p as loadManifest } from "./manifest-BtzDy7KQ.js";
3
+ import { resolveCollectionsDir } from "./collections-dir-DlvDhime.js";
4
+ import chalk from "chalk";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+ //#region src/commands/generate-view.ts
9
+ /**
10
+ * `rebase generate view <slug>` — scaffold a custom admin screen and register it.
11
+ *
12
+ * Most back-office screens are a table, a form or a board, and the admin draws
13
+ * those from the collection. The rest — a dispatch board, a reconciliation
14
+ * screen, a record's audit trail — are a hundred lines of React. Written inside
15
+ * the admin they inherit the sign-in, the security rules, the sidebar, the UI
16
+ * kit and the data layer; written as a separate app they have to rebuild every
17
+ * one of those. This command writes the first version of that file where the
18
+ * admin will find it, so the screen starts native and registered.
19
+ *
20
+ * Two places, one file each:
21
+ *
22
+ * - **A screen** (default): `<frontend>/src/views/<Name>View.tsx`, listed in
23
+ * `<frontend>/src/views/index.ts` as an `AppView`, which `App.tsx` hands to
24
+ * `<RebaseCMS views>`. The first run wires `App.tsx`; later runs only append
25
+ * to the list.
26
+ * - **A record tab** (`--collection <slug>`): the same file location, with a
27
+ * component that takes `EntityCustomViewParams`, added to that collection's
28
+ * `admin.entityViews` *by path*. The collection file is shared with the
29
+ * backend, and a path is the one form of component reference the backend
30
+ * never evaluates — the admin's Vite plugin turns it into a lazy import.
31
+ *
32
+ * The frontend is where `rebase.json` says the CMS is (the static app with a
33
+ * `cms` mount), which is `frontend/` in everything `rebase init` writes. A
34
+ * project with no admin panel (`rebase init --headless`) is refused: there is
35
+ * nowhere for a view to render.
36
+ *
37
+ * Edits to existing files are textual and conservative. When `App.tsx` already
38
+ * passes its own `views`, or a collection's shape is not one this can read, the
39
+ * view file is still written and the command prints the one line to add by
40
+ * hand instead of guessing.
41
+ */
42
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
43
+ /** What `rebase generate` dispatches. Read by `cli.ts` to name the subcommand. */
44
+ var GENERATE_SUBCOMMANDS = ["view"];
45
+ var VIEW_FLAGS = {
46
+ "--collection": String,
47
+ "--name": String,
48
+ "--icon": String,
49
+ "--dry-run": Boolean,
50
+ "--force": Boolean
51
+ };
52
+ /** A slug is a URL segment and an entity view key: kebab-case, starting with a letter. */
53
+ var SLUG_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
54
+ /**
55
+ * Top-level paths the admin's own routes answer (`RebaseRouteDefs` in
56
+ * `@rebasepro/cms`): `/c/*` the collections, `/s` Studio, `/settings`. They are
57
+ * declared before custom views, so a screen at one of them was listed in the
58
+ * sidebar and opened the built-in page instead.
59
+ */
60
+ var RESERVED_SCREEN_SLUGS = [
61
+ "c",
62
+ "s",
63
+ "settings"
64
+ ];
65
+ /** `orders-board` → `OrdersBoardView`; `sales-view` → `SalesView`, not `SalesViewView`. */
66
+ function viewComponentName(slug) {
67
+ const pascal = slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
68
+ return pascal.endsWith("View") ? pascal : `${pascal}View`;
69
+ }
70
+ /** `orders-board` → `Orders board`: labels are sentence case in the admin. */
71
+ function viewLabel(slug) {
72
+ const words = slug.split("-").join(" ");
73
+ return words.charAt(0).toUpperCase() + words.slice(1);
74
+ }
75
+ /**
76
+ * The source with every string, template literal, regular expression and
77
+ * comment blanked out, character for character, so brackets and keys can be
78
+ * found by position without being fooled by a `{` inside a string. String
79
+ * contents become `_` (they are still "something"), comments become spaces.
80
+ *
81
+ * Exported for its tests.
82
+ */
83
+ function maskCode(source) {
84
+ const out = source.split("");
85
+ let i = 0;
86
+ const blank = (from, to, fill) => {
87
+ for (let k = from; k < to; k++) if (out[k] !== "\n") out[k] = fill;
88
+ };
89
+ const previousSignificant = (at) => {
90
+ for (let k = at - 1; k >= 0; k--) {
91
+ const ch = out[k];
92
+ if (ch !== " " && ch !== " " && ch !== "\n" && ch !== "\r") return ch;
93
+ }
94
+ return "";
95
+ };
96
+ while (i < source.length) {
97
+ const ch = source[i];
98
+ const next = source[i + 1];
99
+ if (ch === "/" && next === "/") {
100
+ const end = source.indexOf("\n", i);
101
+ const stop = end === -1 ? source.length : end;
102
+ blank(i, stop, " ");
103
+ i = stop;
104
+ } else if (ch === "/" && next === "*") {
105
+ const end = source.indexOf("*/", i + 2);
106
+ const stop = end === -1 ? source.length : end + 2;
107
+ blank(i, stop, " ");
108
+ i = stop;
109
+ } else if (ch === "\"" || ch === "'" || ch === "`") {
110
+ let k = i + 1;
111
+ while (k < source.length && source[k] !== ch) {
112
+ if (source[k] === "\\") k++;
113
+ k++;
114
+ }
115
+ blank(i + 1, k, "_");
116
+ i = k + 1;
117
+ } else if (ch === "/" && (previousSignificant(i) === "" || "(,=:[!&|?{;".includes(previousSignificant(i)))) {
118
+ let k = i + 1;
119
+ let inClass = false;
120
+ while (k < source.length && source[k] !== "\n") {
121
+ if (source[k] === "\\") k++;
122
+ else if (source[k] === "[") inClass = true;
123
+ else if (source[k] === "]") inClass = false;
124
+ else if (source[k] === "/" && !inClass) break;
125
+ k++;
126
+ }
127
+ blank(i + 1, k, "_");
128
+ i = k + 1;
129
+ } else i++;
130
+ }
131
+ return out.join("");
132
+ }
133
+ var CLOSERS = {
134
+ "{": "}",
135
+ "[": "]",
136
+ "(": ")"
137
+ };
138
+ /** The index of the bracket closing the one at `open`, in masked source. */
139
+ function matchClose(masked, open) {
140
+ const stack = [];
141
+ for (let i = open; i < masked.length; i++) {
142
+ const ch = masked[i];
143
+ if (ch in CLOSERS) stack.push(CLOSERS[ch]);
144
+ else if (ch === "}" || ch === "]" || ch === ")") {
145
+ if (stack.pop() !== ch) return -1;
146
+ if (stack.length === 0) return i;
147
+ }
148
+ }
149
+ return -1;
150
+ }
151
+ /** The index of the innermost `{` still open at `at`, in masked source. */
152
+ function enclosingBrace(masked, at) {
153
+ const stack = [];
154
+ for (let i = 0; i < at; i++) {
155
+ const ch = masked[i];
156
+ if (ch === "{" || ch === "[" || ch === "(") stack.push(i);
157
+ else if (ch === "}" || ch === "]" || ch === ")") stack.pop();
158
+ }
159
+ for (let k = stack.length - 1; k >= 0; k--) if (masked[stack[k]] === "{") return stack[k];
160
+ return -1;
161
+ }
162
+ /**
163
+ * The position of `key:` as a direct property of the object whose `{` is at
164
+ * `open`, followed by `opener` (`{` or `[`) — or -1.
165
+ */
166
+ function directProperty(masked, open, key, opener) {
167
+ const close = matchClose(masked, open);
168
+ if (close === -1) return void 0;
169
+ const pattern = new RegExp(`(^|[\\s,{])${key}\\s*:\\s*\\${opener}`, "g");
170
+ const body = masked.slice(open + 1, close);
171
+ for (const match of body.matchAll(pattern)) {
172
+ const keyAt = open + 1 + (match.index ?? 0) + match[1].length;
173
+ if (enclosingBrace(masked, keyAt) !== open) continue;
174
+ return {
175
+ keyAt,
176
+ valueOpen: open + 1 + (match.index ?? 0) + match[0].length - 1
177
+ };
178
+ }
179
+ }
180
+ function lineStart(source, at) {
181
+ return source.lastIndexOf("\n", at - 1) + 1;
182
+ }
183
+ function indentAt(source, at) {
184
+ const start = lineStart(source, at);
185
+ return /^[ \t]*/.exec(source.slice(start))?.[0] ?? "";
186
+ }
187
+ /**
188
+ * The indentation step a file uses: the smallest non-zero leading run of
189
+ * spaces, not counting the ` * ` lines of a block comment.
190
+ */
191
+ function indentUnit(source) {
192
+ if (/^\t/m.test(source)) return " ";
193
+ let unit = Infinity;
194
+ for (const match of source.matchAll(/^( +)[^\s*]/gm)) unit = Math.min(unit, match[1].length);
195
+ return " ".repeat(Number.isFinite(unit) ? unit : 4);
196
+ }
197
+ function reindent(block, indent) {
198
+ return block.split("\n").map((line) => line ? indent + line : line).join("\n");
199
+ }
200
+ /**
201
+ * Insert `element` (already indented one level past `indent`) as the last item
202
+ * of the array whose `[` is at `open`.
203
+ */
204
+ function appendToArray(source, masked, open, element) {
205
+ const close = matchClose(masked, open);
206
+ const inner = masked.slice(open + 1, close);
207
+ const indent = indentAt(source, open);
208
+ if (inner.trim() === "") return `${source.slice(0, open + 1)}\n${element}\n${indent}${source.slice(close)}`;
209
+ let last = close - 1;
210
+ while (last > open && /\s/.test(masked[last])) last--;
211
+ const hasTrailingComma = masked[last] === ",";
212
+ const insertAt = last + 1;
213
+ return `${source.slice(0, insertAt)}${hasTrailingComma ? "" : ","}\n${element}${source.slice(insertAt)}`;
214
+ }
215
+ /** Add an import line after the file's last import statement. */
216
+ function addImport(source, line) {
217
+ const masked = maskCode(source);
218
+ let end = 0;
219
+ for (const match of masked.matchAll(/^import\b[\s\S]*?["'][^"'\n]*["'];?[ \t]*$/gm)) end = (match.index ?? 0) + match[0].length;
220
+ if (end === 0) return `${line}\n${source}`;
221
+ return `${source.slice(0, end)}\n${line}${source.slice(end)}`;
222
+ }
223
+ /** A path import from `fromDir` to `toFile`, without extension, always starting with `.`. */
224
+ function relativeImport(fromDir, toFile) {
225
+ const relative = path.relative(fromDir, toFile).split(path.sep).join("/").replace(/\.tsx?$/, "");
226
+ return relative.startsWith(".") ? relative : `./${relative}`;
227
+ }
228
+ /** The admin's frontend directory: the static app that mounts the CMS, or `frontend/`. */
229
+ function resolveFrontendDir(projectRoot) {
230
+ try {
231
+ const cms = cmsMountOf(loadManifest(projectRoot).manifest);
232
+ if (cms) return path.join(projectRoot, cms.app.root);
233
+ } catch {}
234
+ const fallback = path.join(projectRoot, "frontend");
235
+ return fs.existsSync(path.join(fallback, "src")) ? fallback : void 0;
236
+ }
237
+ function collectionFiles(dir) {
238
+ if (!fs.existsSync(dir)) return [];
239
+ const out = [];
240
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
241
+ for (const entry of entries) {
242
+ const full = path.join(dir, entry.name);
243
+ if (entry.isDirectory()) {
244
+ if (entry.name !== "node_modules" && entry.name !== "presets") out.push(...collectionFiles(full));
245
+ } else if (/\.tsx?$/.test(entry.name) && !/\.(test|spec|d)\.tsx?$/.test(entry.name)) out.push(full);
246
+ }
247
+ return out;
248
+ }
249
+ var SLUG_PROPERTY = /(^|[\s,{])slug\s*:\s*(["'])([^"'\n]+)\2/g;
250
+ /** Every `slug: "…"` a collection file declares, with where. */
251
+ function declaredSlugs(dir) {
252
+ const found = [];
253
+ for (const file of collectionFiles(dir)) {
254
+ const source = fs.readFileSync(file, "utf8");
255
+ const masked = maskCode(source);
256
+ for (const match of source.matchAll(SLUG_PROPERTY)) {
257
+ const at = (match.index ?? 0) + match[1].length;
258
+ if (masked[at] !== "s") continue;
259
+ found.push({
260
+ slug: match[3],
261
+ file,
262
+ at
263
+ });
264
+ }
265
+ }
266
+ return found;
267
+ }
268
+ /**
269
+ * The scaffolds live in `templates/views/` rather than in template literals
270
+ * here: they import React and the admin packages, and this package's sources
271
+ * must not name those, even inside a string (`pnpm check:types-headless`).
272
+ */
273
+ function viewTemplate(name) {
274
+ const cliRoot = findCliRoot(__dirname);
275
+ if (!cliRoot) throw new Error(`The CLI's templates/views folder is missing from ${__dirname}.`);
276
+ return fs.readFileSync(path.join(cliRoot, "templates", "views", name), "utf8");
277
+ }
278
+ /** Walk up to the package root, which holds `templates/views`. */
279
+ function findCliRoot(from) {
280
+ const root = path.parse(from).root;
281
+ let dir = from;
282
+ while (dir && dir !== root) {
283
+ if (fs.existsSync(path.join(dir, "templates", "views"))) return dir;
284
+ dir = path.dirname(dir);
285
+ }
286
+ return null;
287
+ }
288
+ /**
289
+ * A label as JSX text: written as is when it is plain, as a string expression
290
+ * when it holds a character JSX reads — `{`, `}`, `<`, `>`, `&`. Pasted raw,
291
+ * `--name "KPIs {beta}"` wrote `<Typography>KPIs {beta}</Typography>`, and the
292
+ * view did not compile ("Cannot find name 'beta'").
293
+ */
294
+ function jsxText(text) {
295
+ return /[{}<>&]/.test(text) ? `{${JSON.stringify(text)}}` : text;
296
+ }
297
+ /** A label inside the leading `/** … *\/` comment: one line, and no `*\/` to close it early. */
298
+ function commentText(text) {
299
+ return text.replace(/[\r\n]+/g, " ").replaceAll("*/", "*\\/");
300
+ }
301
+ function fillViewTemplate(template, options) {
302
+ const { slug, componentName, label, collection } = options;
303
+ return template.replaceAll("\"__COLLECTION__\"", JSON.stringify(collection)).replaceAll("__COLLECTION__", collection).replaceAll("__COMPONENT__", componentName).replaceAll("__label_jsx__", jsxText(label.toLowerCase())).replaceAll("__LABEL_JSX__", jsxText(label)).replaceAll("__label__", commentText(label.toLowerCase())).replaceAll("__LABEL__", commentText(label)).replaceAll("__SLUG__", slug);
304
+ }
305
+ function appViewSource(options) {
306
+ return fillViewTemplate(viewTemplate("AppView.tsx.tmpl"), options);
307
+ }
308
+ function entityViewSource(options) {
309
+ return fillViewTemplate(viewTemplate("EntityView.tsx.tmpl"), options);
310
+ }
311
+ function viewsIndexHeader() {
312
+ return viewTemplate("index.ts.tmpl");
313
+ }
314
+ function appViewEntry(options, unit) {
315
+ return [
316
+ "{",
317
+ `${unit}slug: ${JSON.stringify(options.slug)},`,
318
+ `${unit}name: ${JSON.stringify(options.label)},`,
319
+ `${unit}icon: ${JSON.stringify(options.icon)},`,
320
+ `${unit}view: ${options.componentName}`,
321
+ "}"
322
+ ].join("\n");
323
+ }
324
+ function readIfExists(file) {
325
+ return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : void 0;
326
+ }
327
+ /**
328
+ * The frontend's package.json gains `@rebasepro/cms-types` when it lacks it.
329
+ *
330
+ * The view files import their prop types from it, and under pnpm a package the
331
+ * frontend does not declare does not resolve from the frontend — the frontend's
332
+ * `tsc` would fail on the first generated view. The version is the one the
333
+ * frontend already uses for `@rebasepro/cms`, so the two cannot drift.
334
+ */
335
+ function planCmsTypesDependency(frontendDir, files, manualSteps) {
336
+ const manifestFile = path.join(frontendDir, "package.json");
337
+ const raw = readIfExists(manifestFile);
338
+ if (raw === void 0) return;
339
+ let manifest;
340
+ try {
341
+ manifest = JSON.parse(raw);
342
+ } catch {
343
+ return;
344
+ }
345
+ const declared = {
346
+ ...manifest.devDependencies,
347
+ ...manifest.dependencies
348
+ };
349
+ if (declared["@rebasepro/cms-types"]) return;
350
+ const version = declared["@rebasepro/cms"] ?? declared["@rebasepro/app"];
351
+ if (!version) return;
352
+ const dependencies = {
353
+ ...manifest.dependencies,
354
+ "@rebasepro/cms-types": version
355
+ };
356
+ const sorted = Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)));
357
+ const indent = /^(\s+)"/m.exec(raw)?.[1] ?? " ";
358
+ files.push({
359
+ path: manifestFile,
360
+ contents: `${JSON.stringify({
361
+ ...manifest,
362
+ dependencies: sorted
363
+ }, null, indent)}\n`,
364
+ action: "update"
365
+ });
366
+ manualSteps.push(`Run your package manager's install: ${path.basename(frontendDir)}/package.json now depends on @rebasepro/cms-types, which the view imports its types from.`);
367
+ }
368
+ function defaultCollectionFor(projectRoot) {
369
+ const slugs = declaredSlugs(resolveCollectionsDir(projectRoot)).map((entry) => entry.slug);
370
+ return slugs.find((slug) => slug !== "users") ?? slugs[0] ?? "users";
371
+ }
372
+ /**
373
+ * Everything `rebase generate view` would write, without writing it.
374
+ *
375
+ * @throws UsageError when the slug is not a slug, there is no admin panel, the
376
+ * view already exists (without `force`), or `--collection` names nothing.
377
+ */
378
+ function planGenerateView(options) {
379
+ const { projectRoot, slug } = options;
380
+ if (!SLUG_PATTERN.test(slug)) throw new UsageError(`"${slug}" is not a view slug: use lowercase letters, digits and dashes, starting with a letter (e.g. orders-board).`);
381
+ if (!options.collection && RESERVED_SCREEN_SLUGS.includes(slug)) throw new UsageError(`"${slug}" is reserved: the admin's own routes answer /${slug}, so a screen there would never show. Pick another slug (e.g. ${slug === "settings" ? "app-settings" : `${slug}-view`}).`);
382
+ const frontendDir = resolveFrontendDir(projectRoot);
383
+ if (!frontendDir) throw new UsageError("This project has no admin panel to add a view to: rebase.json declares no app with a `cms` mount, and there is no frontend/src. A project created with `rebase init --headless` has no admin.");
384
+ const componentName = viewComponentName(slug);
385
+ const label = options.name?.trim() || viewLabel(slug);
386
+ const viewsDir = path.join(frontendDir, "src", "views");
387
+ const viewFile = path.join(viewsDir, `${componentName}.tsx`);
388
+ const files = [];
389
+ const manualSteps = [];
390
+ const viewExists = fs.existsSync(viewFile);
391
+ if (viewExists && !options.force) throw new UsageError(`${path.relative(projectRoot, viewFile)} already exists. Pass --force to replace it; its registration is kept.`);
392
+ const viewAction = viewExists ? "replace" : "create";
393
+ if (options.collection) {
394
+ const kind = "entity";
395
+ const collectionsDir = resolveCollectionsDir(projectRoot);
396
+ const declared = declaredSlugs(collectionsDir);
397
+ const matches = declared.filter((entry) => entry.slug === options.collection);
398
+ if (matches.length === 0) {
399
+ const known = [...new Set(declared.map((entry) => entry.slug))].sort();
400
+ throw new UsageError(`No collection with slug "${options.collection}" under ${path.relative(projectRoot, collectionsDir) || "."}.` + (known.length ? ` Declared: ${known.join(", ")}.` : ""));
401
+ }
402
+ if (matches.length > 1) throw new UsageError(`More than one collection declares slug "${options.collection}": ${matches.map((entry) => path.relative(projectRoot, entry.file)).join(", ")}.`);
403
+ const [{ file: collectionFile, at: slugAt }] = matches;
404
+ files.push({
405
+ path: viewFile,
406
+ contents: entityViewSource({
407
+ slug,
408
+ componentName,
409
+ label,
410
+ collection: options.collection
411
+ }),
412
+ action: viewAction
413
+ });
414
+ const builder = relativeImport(path.dirname(collectionFile), viewFile);
415
+ const updated = registerEntityView(fs.readFileSync(collectionFile, "utf8"), slugAt, {
416
+ key: slug,
417
+ name: label,
418
+ builder
419
+ });
420
+ const where = `${path.relative(projectRoot, collectionFile)}`;
421
+ if (updated === "already") {} else if (updated === void 0) manualSteps.push(`Add this to admin.entityViews in ${where}: { key: ${JSON.stringify(slug)}, name: ${JSON.stringify(label)}, Builder: ${JSON.stringify(builder)} }`);
422
+ else files.push({
423
+ path: collectionFile,
424
+ contents: updated,
425
+ action: "update"
426
+ });
427
+ planCmsTypesDependency(frontendDir, files, manualSteps);
428
+ return {
429
+ kind,
430
+ componentName,
431
+ viewFile,
432
+ files,
433
+ manualSteps
434
+ };
435
+ }
436
+ files.push({
437
+ path: viewFile,
438
+ contents: appViewSource({
439
+ slug,
440
+ componentName,
441
+ label,
442
+ collection: defaultCollectionFor(projectRoot)
443
+ }),
444
+ action: viewAction
445
+ });
446
+ const indexFile = path.join(viewsDir, "index.ts");
447
+ const existingIndex = readIfExists(indexFile);
448
+ const icon = options.icon ?? "LayoutDashboard";
449
+ if (existingIndex === void 0) {
450
+ const unit = " ";
451
+ files.push({
452
+ path: indexFile,
453
+ contents: `${viewsIndexHeader()}import { ${componentName} } from "./${componentName}";\n\nexport const views: AppView[] = [\n${reindent(appViewEntry({
454
+ slug,
455
+ label,
456
+ icon,
457
+ componentName
458
+ }, unit), unit)}\n];\n`,
459
+ action: "create"
460
+ });
461
+ } else {
462
+ const updated = registerAppView(existingIndex, {
463
+ slug,
464
+ label,
465
+ icon,
466
+ componentName
467
+ });
468
+ if (updated === void 0) manualSteps.push(`Add { slug: ${JSON.stringify(slug)}, name: ${JSON.stringify(label)}, view: ${componentName} } to the views in ${path.relative(projectRoot, indexFile)}.`);
469
+ else if (updated !== "already") files.push({
470
+ path: indexFile,
471
+ contents: updated,
472
+ action: "update"
473
+ });
474
+ }
475
+ const appFile = path.join(frontendDir, "src", "App.tsx");
476
+ const app = readIfExists(appFile);
477
+ const wired = app === void 0 ? void 0 : wireViewsIntoApp(app);
478
+ if (wired === void 0) manualSteps.push(`Pass the screens to the admin: import { views } from "./views" and add views={views} to <RebaseCMS> in ${path.relative(projectRoot, appFile)} (or spread ...views into the views it already passes).`);
479
+ else if (wired !== "already") files.push({
480
+ path: appFile,
481
+ contents: wired,
482
+ action: "update"
483
+ });
484
+ planCmsTypesDependency(frontendDir, files, manualSteps);
485
+ return {
486
+ kind: "app",
487
+ componentName,
488
+ viewFile,
489
+ files,
490
+ manualSteps
491
+ };
492
+ }
493
+ /**
494
+ * `views/index.ts` with one more entry, `"already"` when the slug is listed, or
495
+ * `undefined` when the file has no `export const views … = [`.
496
+ *
497
+ * Exported for its tests.
498
+ */
499
+ function registerAppView(source, options) {
500
+ const masked = maskCode(source);
501
+ const declaration = /export\s+const\s+views\b[^=]*=\s*\[/.exec(masked);
502
+ if (!declaration) return void 0;
503
+ const open = declaration.index + declaration[0].length - 1;
504
+ if (matchClose(masked, open) === -1) return void 0;
505
+ const close = matchClose(masked, open);
506
+ const listed = source.slice(open, close);
507
+ if (new RegExp(`slug\\s*:\\s*["']${options.slug}["']`).test(listed)) return "already";
508
+ const unit = indentUnit(source);
509
+ const indent = indentAt(source, open) + unit;
510
+ return addImport(appendToArray(source, masked, open, reindent(appViewEntry(options, unit), indent)), `import { ${options.componentName} } from "./${options.componentName}";`);
511
+ }
512
+ /**
513
+ * `App.tsx` with `views={views}` on `<RebaseCMS>` and its import, `"already"`
514
+ * when it passes exactly that, or `undefined` when it cannot be done safely:
515
+ * no `<RebaseCMS`, a `views` prop of its own, or a `views` name already in scope.
516
+ *
517
+ * Exported for its tests.
518
+ */
519
+ function wireViewsIntoApp(source) {
520
+ const masked = maskCode(source);
521
+ const tag = /<RebaseCMS\b/.exec(masked);
522
+ if (!tag) return void 0;
523
+ const tagEnd = masked.indexOf(">", tag.index);
524
+ if (tagEnd === -1) return void 0;
525
+ const attributes = masked.slice(tag.index, tagEnd);
526
+ const importsViews = /import\s*\{\s*views\s*\}\s*from\s*["']\.\/views(?:\/index)?["']/.test(source);
527
+ if (/\sviews\s*=/.test(attributes)) return importsViews && /\sviews\s*=\s*\{\s*views\s*\}/.test(attributes) ? "already" : void 0;
528
+ if (/\bviews\b/.test(masked)) return void 0;
529
+ const insertAt = tag.index + 10;
530
+ const afterTag = source.slice(insertAt);
531
+ const nextLine = /^[ \t]*\r?\n([ \t]*)\S/.exec(afterTag);
532
+ const attribute = nextLine ? `\n${nextLine[1]}views={views}` : " views={views}";
533
+ return addImport(source.slice(0, insertAt) + attribute + source.slice(insertAt), "import { views } from \"./views\";");
534
+ }
535
+ /**
536
+ * The collection file with a `{ key, name, Builder }` entry in the
537
+ * `admin.entityViews` of the collection whose `slug:` is at `slugAt`;
538
+ * `"already"` when that key is there; `undefined` when the shape cannot be read.
539
+ *
540
+ * Exported for its tests.
541
+ */
542
+ function registerEntityView(source, slugAt, view) {
543
+ const masked = maskCode(source);
544
+ const collectionOpen = enclosingBrace(masked, slugAt);
545
+ if (collectionOpen === -1) return void 0;
546
+ const unit = indentUnit(source);
547
+ const entry = [
548
+ "{",
549
+ `${unit}key: ${JSON.stringify(view.key)},`,
550
+ `${unit}name: ${JSON.stringify(view.name)},`,
551
+ `${unit}Builder: ${JSON.stringify(view.builder)}`,
552
+ "}"
553
+ ].join("\n");
554
+ const admin = directProperty(masked, collectionOpen, "admin", "{");
555
+ if (admin) {
556
+ const entityViews = directProperty(masked, admin.valueOpen, "entityViews", "[");
557
+ if (entityViews) {
558
+ const close = matchClose(masked, entityViews.valueOpen);
559
+ if (close === -1) return void 0;
560
+ const listed = source.slice(entityViews.valueOpen, close);
561
+ if (new RegExp(`key\\s*:\\s*["']${view.key}["']|["']${view.key}["']`).test(listed)) return "already";
562
+ const indent = indentAt(source, entityViews.keyAt) + unit;
563
+ return appendToArray(source, masked, entityViews.valueOpen, reindent(entry, indent));
564
+ }
565
+ const indent = indentAt(source, admin.keyAt) + unit;
566
+ const block = `${indent}entityViews: [\n${reindent(entry, indent + unit)}\n${indent}],`;
567
+ const adminClose = matchClose(masked, admin.valueOpen);
568
+ const empty = masked.slice(admin.valueOpen + 1, adminClose).trim() === "";
569
+ const insertAt = admin.valueOpen + 1;
570
+ return empty ? `${source.slice(0, insertAt)}\n${block.replace(/,$/, "")}\n${indentAt(source, admin.keyAt)}${source.slice(adminClose)}` : `${source.slice(0, insertAt)}\n${block}${source.slice(insertAt)}`;
571
+ }
572
+ const slugLineEnd = source.indexOf("\n", slugAt);
573
+ if (slugLineEnd === -1) return void 0;
574
+ const slugLine = masked.slice(slugAt, slugLineEnd);
575
+ if (!/,\s*$/.test(slugLine)) return void 0;
576
+ const indent = indentAt(source, slugAt);
577
+ const block = [
578
+ `${indent}admin: {`,
579
+ `${indent}${unit}entityViews: [`,
580
+ reindent(entry, indent + unit + unit),
581
+ `${indent}${unit}]`,
582
+ `${indent}},`
583
+ ].join("\n");
584
+ return `${source.slice(0, slugLineEnd + 1)}${block}\n${source.slice(slugLineEnd + 1)}`;
585
+ }
586
+ function printHelp() {
587
+ console.log(`
588
+ ${chalk.bold("rebase generate")} — scaffold code into this project
589
+
590
+ ${chalk.bold("Usage")}
591
+ rebase generate view <slug> A screen of the admin, in the sidebar
592
+ rebase generate view <slug> --collection <slug> A tab on every record of a collection
593
+
594
+ ${chalk.bold("What it writes")}
595
+ <frontend>/src/views/<Name>View.tsx The component, drawn with @rebasepro/ui
596
+ <frontend>/src/views/index.ts The screens list (AppView[]), wired into
597
+ <RebaseCMS views> in App.tsx on first run
598
+ With --collection, the collection file instead gains an admin.entityViews
599
+ entry that points at the component by path.
600
+
601
+ ${chalk.bold("Options")}
602
+ --collection <slug> Register as a record tab of this collection
603
+ --name <label> Sidebar or tab label ${chalk.gray("(default: the slug in sentence case)")}
604
+ --icon <Lucide name> Sidebar icon for a screen ${chalk.gray("(default: LayoutDashboard)")}
605
+ --dry-run List what would change, and change nothing
606
+ --force Replace an existing view file
607
+ -h, --help Show this help
608
+ `.trim());
609
+ }
610
+ async function generateCommand(subcommand, rawArgs = []) {
611
+ if (subcommand === void 0 || subcommand === "--help" || subcommand !== "view" && wantsHelp(rawArgs)) {
612
+ printHelp();
613
+ return;
614
+ }
615
+ switch (subcommand) {
616
+ case "view":
617
+ await generateViewCommand(rawArgs);
618
+ return;
619
+ default: throw new UsageError(`Unknown generate command "${subcommand}". Available: ${GENERATE_SUBCOMMANDS.join(", ")}. See \`rebase generate --help\`.`);
620
+ }
621
+ }
622
+ async function generateViewCommand(rawArgs) {
623
+ if (wantsHelp(rawArgs)) {
624
+ printHelp();
625
+ return;
626
+ }
627
+ const { flags, positionals } = parseCommandArgs({
628
+ spec: VIEW_FLAGS,
629
+ rawArgs,
630
+ commandWords: 2,
631
+ command: "generate view",
632
+ maxPositionals: 1
633
+ });
634
+ const slug = positionals[0];
635
+ if (!slug) throw new UsageError("Name the view: `rebase generate view <slug>`, e.g. `rebase generate view orders-board`.");
636
+ if (flags["--icon"] && flags["--collection"]) throw new UsageError("--icon is for a screen in the sidebar; a record tab (--collection) has no icon here.");
637
+ const projectRoot = requireProjectRoot();
638
+ const plan = planGenerateView({
639
+ projectRoot,
640
+ slug,
641
+ collection: flags["--collection"],
642
+ name: flags["--name"],
643
+ icon: flags["--icon"],
644
+ force: Boolean(flags["--force"])
645
+ });
646
+ const dryRun = Boolean(flags["--dry-run"]);
647
+ for (const file of plan.files) {
648
+ if (!dryRun) {
649
+ fs.mkdirSync(path.dirname(file.path), { recursive: true });
650
+ fs.writeFileSync(file.path, file.contents);
651
+ }
652
+ const verb = file.action === "create" ? "created" : file.action === "replace" ? "replaced" : "updated";
653
+ console.log(`${chalk.green("✓")} ${dryRun ? `would be ${verb}` : verb} ${path.relative(projectRoot, file.path)}`);
654
+ }
655
+ for (const step of plan.manualSteps) console.log(`${chalk.yellow("!")} ${step}`);
656
+ const where = plan.kind === "app" ? `/${slug} in the admin` : `a "${flags["--name"]?.trim() || viewLabel(slug)}" tab on each ${flags["--collection"]} record`;
657
+ console.log(chalk.gray(` ${plan.componentName} renders at ${where}. Edit ${path.relative(projectRoot, plan.viewFile)} to build the screen.`));
658
+ }
659
+ //#endregion
660
+ export { GENERATE_SUBCOMMANDS, generateCommand };
661
+
662
+ //# sourceMappingURL=generate-view-IFwCADvb.js.map