@shadow-js/ui 0.0.0-stage → 1.0.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 (190) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -2
  4. package/dist/cli.mjs +2293 -0
  5. package/dist/dev/cli/schema.d.ts +73 -0
  6. package/dist/dev/index.d.ts +6 -0
  7. package/dist/dev/index.js +5 -0
  8. package/dist/dev/locales/ar.d.ts +141 -0
  9. package/dist/dev/locales/ar.js +128 -0
  10. package/dist/dev/locales/ar.js.map +1 -0
  11. package/dist/dev/locales/en.d.ts +141 -0
  12. package/dist/dev/locales/en.js +128 -0
  13. package/dist/dev/locales/en.js.map +1 -0
  14. package/dist/dev/registry/utils/utils.d.ts +29 -0
  15. package/dist/dev/registry/utils/utils.js +27 -0
  16. package/dist/dev/registry/utils/utils.js.map +1 -0
  17. package/dist/dev/theme/theme-logic.d.ts +187 -0
  18. package/dist/dev/theme/theme-logic.js +184 -0
  19. package/dist/dev/theme/theme-logic.js.map +1 -0
  20. package/dist/dev/theme/theme-provider.d.ts +90 -0
  21. package/dist/dev/theme/theme-provider.js +150 -0
  22. package/dist/dev/theme/theme-provider.js.map +1 -0
  23. package/dist/prod/index.js +1 -0
  24. package/dist/prod/locales/ar.js +2 -0
  25. package/dist/prod/locales/ar.js.map +1 -0
  26. package/dist/prod/locales/en.js +2 -0
  27. package/dist/prod/locales/en.js.map +1 -0
  28. package/dist/prod/registry/utils/utils.js +2 -0
  29. package/dist/prod/registry/utils/utils.js.map +1 -0
  30. package/dist/prod/theme/theme-logic.js +2 -0
  31. package/dist/prod/theme/theme-logic.js.map +1 -0
  32. package/dist/prod/theme/theme-provider.js +2 -0
  33. package/dist/prod/theme/theme-provider.js.map +1 -0
  34. package/package.json +102 -4
  35. package/registry/accordion/accordion.tsx +210 -0
  36. package/registry/accordion/item.json +4 -0
  37. package/registry/alert/alert.tsx +200 -0
  38. package/registry/alert/item.json +4 -0
  39. package/registry/alert-dialog/alert-dialog.tsx +530 -0
  40. package/registry/alert-dialog/item.json +4 -0
  41. package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
  42. package/registry/aspect-ratio/item.json +4 -0
  43. package/registry/attachment/attachment.tsx +459 -0
  44. package/registry/attachment/item.json +4 -0
  45. package/registry/avatar/avatar.tsx +385 -0
  46. package/registry/avatar/item.json +4 -0
  47. package/registry/badge/badge.tsx +108 -0
  48. package/registry/badge/item.json +4 -0
  49. package/registry/breadcrumb/breadcrumb.tsx +354 -0
  50. package/registry/breadcrumb/item.json +4 -0
  51. package/registry/bubble/bubble.tsx +260 -0
  52. package/registry/bubble/item.json +4 -0
  53. package/registry/button/button.tsx +125 -0
  54. package/registry/button/item.json +4 -0
  55. package/registry/button-group/button-group.tsx +189 -0
  56. package/registry/button-group/item.json +4 -0
  57. package/registry/calendar/calendar.tsx +348 -0
  58. package/registry/calendar/item.json +4 -0
  59. package/registry/card/card.tsx +311 -0
  60. package/registry/card/item.json +4 -0
  61. package/registry/carousel/carousel.tsx +851 -0
  62. package/registry/carousel/item.json +4 -0
  63. package/registry/chart/chart.tsx +1956 -0
  64. package/registry/chart/item.json +4 -0
  65. package/registry/chart-scale/chart-scale.ts +670 -0
  66. package/registry/chart-scale/item.json +5 -0
  67. package/registry/checkbox/checkbox.tsx +79 -0
  68. package/registry/checkbox/item.json +4 -0
  69. package/registry/code-block/code-block.tsx +470 -0
  70. package/registry/code-block/item.json +4 -0
  71. package/registry/collapsible/collapsible.tsx +117 -0
  72. package/registry/collapsible/item.json +4 -0
  73. package/registry/combobox/combobox.tsx +977 -0
  74. package/registry/combobox/item.json +4 -0
  75. package/registry/command/command.tsx +757 -0
  76. package/registry/command/item.json +4 -0
  77. package/registry/context-menu/context-menu.tsx +697 -0
  78. package/registry/context-menu/item.json +4 -0
  79. package/registry/data-table/data-table.tsx +437 -0
  80. package/registry/data-table/item.json +4 -0
  81. package/registry/date-picker/date-picker.tsx +349 -0
  82. package/registry/date-picker/item.json +4 -0
  83. package/registry/dialog/dialog.tsx +412 -0
  84. package/registry/dialog/item.json +4 -0
  85. package/registry/direction/direction.tsx +88 -0
  86. package/registry/direction/item.json +4 -0
  87. package/registry/drawer/drawer.tsx +787 -0
  88. package/registry/drawer/item.json +4 -0
  89. package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
  90. package/registry/dropdown-menu/item.json +4 -0
  91. package/registry/empty/empty.tsx +240 -0
  92. package/registry/empty/item.json +4 -0
  93. package/registry/field/field.tsx +624 -0
  94. package/registry/field/item.json +4 -0
  95. package/registry/form/form.tsx +1112 -0
  96. package/registry/form/item.json +4 -0
  97. package/registry/hover-card/hover-card.tsx +144 -0
  98. package/registry/hover-card/item.json +4 -0
  99. package/registry/icons/icons.tsx +1691 -0
  100. package/registry/icons/item.json +4 -0
  101. package/registry/input/input.tsx +43 -0
  102. package/registry/input/item.json +4 -0
  103. package/registry/input-group/input-group.tsx +315 -0
  104. package/registry/input-group/item.json +4 -0
  105. package/registry/input-otp/input-otp.tsx +329 -0
  106. package/registry/input-otp/item.json +4 -0
  107. package/registry/item/item.json +4 -0
  108. package/registry/item/item.tsx +439 -0
  109. package/registry/kbd/item.json +4 -0
  110. package/registry/kbd/kbd.tsx +65 -0
  111. package/registry/label/item.json +4 -0
  112. package/registry/label/label.tsx +60 -0
  113. package/registry/marker/item.json +4 -0
  114. package/registry/marker/marker.tsx +175 -0
  115. package/registry/menubar/item.json +4 -0
  116. package/registry/menubar/menubar.tsx +694 -0
  117. package/registry/message/item.json +4 -0
  118. package/registry/message/message.tsx +280 -0
  119. package/registry/message-scroller/item.json +4 -0
  120. package/registry/message-scroller/message-scroller.tsx +262 -0
  121. package/registry/native-select/item.json +4 -0
  122. package/registry/native-select/native-select.tsx +113 -0
  123. package/registry/navigation-menu/item.json +4 -0
  124. package/registry/navigation-menu/navigation-menu.tsx +362 -0
  125. package/registry/pagination/item.json +4 -0
  126. package/registry/pagination/pagination.tsx +460 -0
  127. package/registry/password-input/item.json +5 -0
  128. package/registry/password-input/password-input.tsx +494 -0
  129. package/registry/popover/item.json +4 -0
  130. package/registry/popover/popover.tsx +284 -0
  131. package/registry/progress/item.json +4 -0
  132. package/registry/progress/progress.tsx +190 -0
  133. package/registry/questionnaire/item.json +4 -0
  134. package/registry/questionnaire/questionnaire.tsx +569 -0
  135. package/registry/radio-group/item.json +4 -0
  136. package/registry/radio-group/radio-group.tsx +101 -0
  137. package/registry/resizable/item.json +4 -0
  138. package/registry/resizable/resizable.tsx +455 -0
  139. package/registry/scroll-area/item.json +4 -0
  140. package/registry/scroll-area/scroll-area.tsx +149 -0
  141. package/registry/select/item.json +4 -0
  142. package/registry/select/select.tsx +571 -0
  143. package/registry/separator/item.json +4 -0
  144. package/registry/separator/separator.tsx +52 -0
  145. package/registry/sheet/item.json +4 -0
  146. package/registry/sheet/sheet.tsx +476 -0
  147. package/registry/sidebar/item.json +4 -0
  148. package/registry/sidebar/sidebar.tsx +1870 -0
  149. package/registry/skeleton/item.json +4 -0
  150. package/registry/skeleton/skeleton.tsx +47 -0
  151. package/registry/slider/item.json +4 -0
  152. package/registry/slider/slider.tsx +149 -0
  153. package/registry/spinner/item.json +4 -0
  154. package/registry/spinner/spinner.tsx +51 -0
  155. package/registry/switch/item.json +4 -0
  156. package/registry/switch/switch.tsx +93 -0
  157. package/registry/table/item.json +4 -0
  158. package/registry/table/table.tsx +373 -0
  159. package/registry/tabs/item.json +4 -0
  160. package/registry/tabs/tabs.tsx +209 -0
  161. package/registry/textarea/item.json +4 -0
  162. package/registry/textarea/textarea.tsx +51 -0
  163. package/registry/toast/item.json +4 -0
  164. package/registry/toast/toast.tsx +949 -0
  165. package/registry/toggle/item.json +4 -0
  166. package/registry/toggle/toggle.tsx +110 -0
  167. package/registry/toggle-group/item.json +4 -0
  168. package/registry/toggle-group/toggle-group.tsx +189 -0
  169. package/registry/tooltip/item.json +4 -0
  170. package/registry/tooltip/tooltip.tsx +169 -0
  171. package/registry/trace/item.json +4 -0
  172. package/registry/trace/trace.tsx +356 -0
  173. package/registry/ui-strings/item.json +5 -0
  174. package/registry/ui-strings/ui-strings.tsx +454 -0
  175. package/registry/use-highlighted-code/item.json +5 -0
  176. package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
  177. package/registry/use-mobile/item.json +5 -0
  178. package/registry/use-mobile/use-mobile.ts +46 -0
  179. package/registry/utils/item.json +5 -0
  180. package/registry/utils/utils.ts +38 -0
  181. package/registry.json +3274 -0
  182. package/styles/animations.css +321 -0
  183. package/styles/theme-core.css +55 -0
  184. package/styles/theme.css +12 -0
  185. package/styles/themes/gray.css +69 -0
  186. package/styles/themes/neutral.css +69 -0
  187. package/styles/themes/slate.css +69 -0
  188. package/styles/themes/stone.css +69 -0
  189. package/styles/themes/zinc.css +69 -0
  190. package/styles/typeset.css +251 -0
package/dist/cli.mjs ADDED
@@ -0,0 +1,2293 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
6
+ import { parseArgs } from "node:util";
7
+ import { fileURLToPath } from "node:url";
8
+ //#region src/cli/fs.ts
9
+ /**
10
+ * The filesystem the CLI works on, as an interface so commands are tested against a temp
11
+ * directory or an in-memory tree, and so every path goes through one place.
12
+ * @packageDocumentation
13
+ */
14
+ /**
15
+ * The real filesystem.
16
+ *
17
+ * @returns A {@link ProjectFs} over `node:fs`.
18
+ * @example
19
+ * const fs = nodeFs();
20
+ * @since 1.0.0
21
+ *
22
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
23
+ */
24
+ function nodeFs() {
25
+ return {
26
+ exists: (path) => existsSync(path),
27
+ read(path) {
28
+ try {
29
+ return readFileSync(path, "utf8");
30
+ } catch (error) {
31
+ if (error.code === "ENOENT") return void 0;
32
+ throw error;
33
+ }
34
+ },
35
+ write(path, content) {
36
+ mkdirSync(dirname(path), { recursive: true });
37
+ writeFileSync(path, content);
38
+ },
39
+ remove(path) {
40
+ rmSync(path, { force: true });
41
+ },
42
+ realpath(path) {
43
+ const tail = [];
44
+ let current = path;
45
+ while (!existsSync(current)) {
46
+ const parent = dirname(current);
47
+ if (parent === current) return path;
48
+ tail.unshift(basename(current));
49
+ current = parent;
50
+ }
51
+ return join(realpathSync(current), ...tail);
52
+ }
53
+ };
54
+ }
55
+ /**
56
+ * What a command records while it works, for the document: the facts, the warnings, and the first failure. Present in
57
+ * the context of a `--json` run only.
58
+ * @since 1.0.0
59
+ *
60
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
61
+ */
62
+ var Report = class {
63
+ /** The facts, by key. */
64
+ data = {};
65
+ warnings = [];
66
+ /** The failure; a second one adds its message to the first. */
67
+ error;
68
+ /**
69
+ * Sets a fact.
70
+ * @param key The key of `data`.
71
+ * @param value The value.
72
+ */
73
+ set(key, value) {
74
+ this.data[key] = value;
75
+ }
76
+ /**
77
+ * Adds a value to the list at a key of `data`.
78
+ * @param key The key of `data`; the list is made when it is missing.
79
+ * @param value The value.
80
+ */
81
+ push(key, value) {
82
+ const list = this.data[key];
83
+ if (Array.isArray(list)) list.push(value);
84
+ else this.data[key] = [value];
85
+ }
86
+ /**
87
+ * Adds a warning.
88
+ * @param code The stable code.
89
+ * @param message The sentence.
90
+ */
91
+ warn(code, message) {
92
+ this.warnings.push({
93
+ code,
94
+ message
95
+ });
96
+ }
97
+ /**
98
+ * Records the failure. The first code wins; a later message is added to the first.
99
+ * @param code The stable code.
100
+ * @param message The sentence.
101
+ */
102
+ fail(code, message) {
103
+ this.error = this.error === void 0 ? {
104
+ code,
105
+ message
106
+ } : {
107
+ ...this.error,
108
+ message: `${this.error.message} ${message}`
109
+ };
110
+ }
111
+ };
112
+ /**
113
+ * A successful document.
114
+ * @param command The command that ran.
115
+ * @param data The facts of the command.
116
+ * @param warnings The warnings; none by default.
117
+ * @returns The document.
118
+ * @example
119
+ * jsonSuccess("shadow-ui list", { items: [] }).ok; // true
120
+ * @since 1.0.0
121
+ *
122
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
123
+ */
124
+ function jsonSuccess(command, data, warnings = []) {
125
+ return {
126
+ schema: 1,
127
+ command,
128
+ ok: true,
129
+ data,
130
+ warnings
131
+ };
132
+ }
133
+ /**
134
+ * A failed document.
135
+ * @param command The command that ran.
136
+ * @param error The code and the message.
137
+ * @param data What is known despite the failure.
138
+ * @param warnings The warnings raised before it.
139
+ * @returns The document, with `ok: false`.
140
+ * @example
141
+ * jsonFailure("shadow-ui add", { code: "SHADOW_UI_NO_CONFIG", message: "no components.json" }).ok; // false
142
+ * @since 1.0.0
143
+ *
144
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
145
+ */
146
+ function jsonFailure(command, error, data = {}, warnings = []) {
147
+ return {
148
+ schema: 1,
149
+ command,
150
+ ok: false,
151
+ data,
152
+ warnings,
153
+ error
154
+ };
155
+ }
156
+ /**
157
+ * The document as text, without the final newline (the output adds one).
158
+ * @param document The document.
159
+ * @param compact True for one line.
160
+ * @returns The JSON text.
161
+ * @since 1.0.0
162
+ *
163
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
164
+ */
165
+ function jsonText(document, compact = false) {
166
+ return JSON.stringify(document, null, compact ? void 0 : 2);
167
+ }
168
+ /**
169
+ * Whether the command line asks for JSON: `--json` anywhere before a `--`. Read before the line is parsed, so a refused
170
+ * command line is answered in JSON too.
171
+ * @param argv The arguments after `shadow-ui`.
172
+ * @returns True for `--json`.
173
+ * @example
174
+ * wantsJson(["list", "--json"]); // true
175
+ * @since 1.0.0
176
+ *
177
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
178
+ */
179
+ function wantsJson(argv) {
180
+ for (const arg of argv) {
181
+ if (arg === "--") return false;
182
+ if (arg === "--json") return true;
183
+ }
184
+ return false;
185
+ }
186
+ /**
187
+ * Prints an uncaught exception or a rejected promise as a JSON failure, then exits 1. Installed by the executable when the
188
+ * command line asks for JSON, so a crash is no exception to "stdout carries one document". A reader that went away
189
+ * (`| head -n 1` closed the pipe) is not a crash: the process exits quietly.
190
+ * @param argv The arguments after `shadow-ui`; the first word that is not an option names the command.
191
+ * @since 1.0.0
192
+ *
193
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
194
+ */
195
+ function installCrashHandlers(argv) {
196
+ let crashed = false;
197
+ const crash = (error) => {
198
+ if (typeof error === "object" && error !== null && Reflect.get(error, "code") === "EPIPE") process.exit(0);
199
+ if (crashed) process.exit(1);
200
+ crashed = true;
201
+ const message = error instanceof Error ? error.message : String(error);
202
+ if (error instanceof Error && error.stack !== void 0) process.stderr.write(`${error.stack}\n`);
203
+ const word = argv.find((arg) => !arg.startsWith("-"));
204
+ const document = jsonFailure(word === void 0 ? "shadow-ui" : `shadow-ui ${word}`, {
205
+ code: "SHADOW_UI_UNEXPECTED",
206
+ message: `unexpected error: ${message}`
207
+ });
208
+ process.stdout.write(`${jsonText(document)}\n`, () => process.exit(1));
209
+ };
210
+ process.on("uncaughtException", crash);
211
+ process.on("unhandledRejection", crash);
212
+ }
213
+ /** The edit script turning `a` into `b`; one big replacement when the texts are too large. */
214
+ function edits(a, b) {
215
+ const n = a.length;
216
+ const m = b.length;
217
+ const max = n + m;
218
+ if (max > 2e4) return [...a.map((line) => ({
219
+ kind: "delete",
220
+ line
221
+ })), ...b.map((line) => ({
222
+ kind: "insert",
223
+ line
224
+ }))];
225
+ const offset = max;
226
+ let frontier = new Int32Array(2 * max + 2);
227
+ const trace = [];
228
+ let reached = -1;
229
+ for (let d = 0; d <= max && reached === -1; d += 1) {
230
+ trace.push(frontier.slice());
231
+ const next = frontier.slice();
232
+ for (let k = -d; k <= d; k += 2) {
233
+ let x;
234
+ const below = frontier[offset + k - 1] ?? 0;
235
+ const above = frontier[offset + k + 1] ?? 0;
236
+ if (k === -d || k !== d && below < above) x = above;
237
+ else x = below + 1;
238
+ let y = x - k;
239
+ while (x < n && y < m && a[x] === b[y]) {
240
+ x += 1;
241
+ y += 1;
242
+ }
243
+ next[offset + k] = x;
244
+ if (x >= n && y >= m) {
245
+ reached = d;
246
+ break;
247
+ }
248
+ }
249
+ frontier = next;
250
+ }
251
+ const result = [];
252
+ let x = n;
253
+ let y = m;
254
+ for (let d = reached; d > 0; d -= 1) {
255
+ const previous = trace[d] ?? /* @__PURE__ */ new Int32Array(0);
256
+ const k = x - y;
257
+ const below = previous[offset + k - 1] ?? 0;
258
+ const above = previous[offset + k + 1] ?? 0;
259
+ const previousK = k === -d || k !== d && below < above ? k + 1 : k - 1;
260
+ const previousX = previous[offset + previousK] ?? 0;
261
+ const previousY = previousX - previousK;
262
+ while (x > previousX && y > previousY) {
263
+ x -= 1;
264
+ y -= 1;
265
+ result.push({
266
+ kind: "equal",
267
+ line: a[x] ?? ""
268
+ });
269
+ }
270
+ if (x === previousX) {
271
+ y -= 1;
272
+ result.push({
273
+ kind: "insert",
274
+ line: b[y] ?? ""
275
+ });
276
+ } else {
277
+ x -= 1;
278
+ result.push({
279
+ kind: "delete",
280
+ line: a[x] ?? ""
281
+ });
282
+ }
283
+ }
284
+ while (x > 0 && y > 0) {
285
+ x -= 1;
286
+ y -= 1;
287
+ result.push({
288
+ kind: "equal",
289
+ line: a[x] ?? ""
290
+ });
291
+ }
292
+ return result.reverse();
293
+ }
294
+ /** Text to lines, ignoring the line-ending style and a final newline. */
295
+ function toLines(text) {
296
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
297
+ if (lines[lines.length - 1] === "") lines.pop();
298
+ return lines;
299
+ }
300
+ /**
301
+ * Whether two texts are the same apart from line endings (CRLF vs LF) and a trailing newline.
302
+ *
303
+ * @param a One text.
304
+ * @param b Another text.
305
+ * @returns `true` when they have the same lines.
306
+ * @example
307
+ * sameContent("a\r\nb\r\n", "a\nb"); // true
308
+ * @since 1.0.0
309
+ *
310
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
311
+ */
312
+ function sameContent(a, b) {
313
+ const left = toLines(a);
314
+ const right = toLines(b);
315
+ return left.length === right.length && left.every((line, index) => line === right[index]);
316
+ }
317
+ /**
318
+ * A unified diff (3 lines of context) from `before` to `after`.
319
+ *
320
+ * @param before The old text (what is on disk).
321
+ * @param after The new text (what the registry has).
322
+ * @param labels Names for the `---` and `+++` header lines.
323
+ * @returns The diff, or `""` when the texts have the same lines.
324
+ * @example
325
+ * unifiedDiff("a\n", "b\n", { before: "local", after: "registry" });
326
+ * @since 1.0.0
327
+ *
328
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
329
+ */
330
+ function unifiedDiff(before, after, labels, context = 3) {
331
+ const script = edits(toLines(before), toLines(after));
332
+ if (script.every((operation) => operation.kind === "equal")) return "";
333
+ const hunks = [];
334
+ let index = 0;
335
+ let oldLine = 1;
336
+ let newLine = 1;
337
+ const lineNumbers = [];
338
+ for (const operation of script) {
339
+ lineNumbers.push({
340
+ old: oldLine,
341
+ new: newLine
342
+ });
343
+ if (operation.kind !== "insert") oldLine += 1;
344
+ if (operation.kind !== "delete") newLine += 1;
345
+ }
346
+ while (index < script.length) {
347
+ while (index < script.length && script[index]?.kind === "equal") index += 1;
348
+ if (index >= script.length) break;
349
+ const start = Math.max(0, index - context);
350
+ let end = index;
351
+ let lastChange = index;
352
+ while (end < script.length) {
353
+ if (script[end]?.kind !== "equal") lastChange = end;
354
+ else if (end - lastChange > context * 2) break;
355
+ end += 1;
356
+ }
357
+ const stop = Math.min(script.length, lastChange + context + 1);
358
+ const slice = script.slice(start, stop);
359
+ const oldCount = slice.filter((operation) => operation.kind !== "insert").length;
360
+ const newCount = slice.filter((operation) => operation.kind !== "delete").length;
361
+ const first = lineNumbers[start] ?? {
362
+ old: 1,
363
+ new: 1
364
+ };
365
+ hunks.push(`@@ -${oldCount === 0 ? first.old - 1 : first.old},${oldCount} +${newCount === 0 ? first.new - 1 : first.new},${newCount} @@`, ...slice.map((operation) => `${operation.kind === "equal" ? " " : operation.kind === "insert" ? "+" : "-"}${operation.line}`));
366
+ index = stop;
367
+ }
368
+ return [
369
+ `--- ${labels.before}`,
370
+ `+++ ${labels.after}`,
371
+ ...hunks
372
+ ].join("\n");
373
+ }
374
+ //#endregion
375
+ //#region src/cli/paths.ts
376
+ /**
377
+ * Path handling: reading `tsconfig` path aliases, mapping an import alias to a directory, and the
378
+ * guard that keeps every write inside the project. Pure except for the `ProjectFs` it is handed.
379
+ * @packageDocumentation
380
+ */
381
+ /**
382
+ * Thrown when a path would leave the project or an alias cannot be mapped to a directory.
383
+ * @since 1.0.0
384
+ *
385
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
386
+ */
387
+ var PathError = class extends Error {
388
+ /** The stable code `--json` prints. */
389
+ code = "SHADOW_UI_UNSAFE_PATH";
390
+ constructor(message) {
391
+ super(message);
392
+ this.name = "PathError";
393
+ }
394
+ };
395
+ /**
396
+ * Drops the commas that stand right before a closing `}` or `]`. String literals are copied as they
397
+ * are, so a comma inside a string never changes.
398
+ */
399
+ function dropTrailingCommas(text) {
400
+ let out = "";
401
+ let index = 0;
402
+ while (index < text.length) {
403
+ const char = text[index] ?? "";
404
+ if (char === "\"") {
405
+ let end = index + 1;
406
+ while (end < text.length && text[end] !== "\"") end += text[end] === "\\" ? 2 : 1;
407
+ out += text.slice(index, end + 1);
408
+ index = end + 1;
409
+ continue;
410
+ }
411
+ if (char === ",") {
412
+ const closing = /\s*[}\]]/y;
413
+ closing.lastIndex = index + 1;
414
+ if (closing.test(text)) {
415
+ index += 1;
416
+ continue;
417
+ }
418
+ }
419
+ out += char;
420
+ index += 1;
421
+ }
422
+ return out;
423
+ }
424
+ /**
425
+ * Parses JSON with comments and trailing commas, as tsconfig files are written. Line and block
426
+ * comments are removed, and trailing commas are dropped; the contents of strings are left as they
427
+ * are.
428
+ *
429
+ * @param text JSON with comments.
430
+ * @returns The parsed value.
431
+ * @throws SyntaxError when the cleaned text is not JSON.
432
+ * @example
433
+ * parseJsonc('{ // comment\n "a": 1, }'); // { a: 1 }
434
+ * @since 1.0.0
435
+ *
436
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
437
+ */
438
+ function parseJsonc(text) {
439
+ let out = "";
440
+ let index = 0;
441
+ while (index < text.length) {
442
+ const char = text[index] ?? "";
443
+ const next = text[index + 1] ?? "";
444
+ if (char === "\"") {
445
+ let end = index + 1;
446
+ while (end < text.length && text[end] !== "\"") end += text[end] === "\\" ? 2 : 1;
447
+ out += text.slice(index, end + 1);
448
+ index = end + 1;
449
+ } else if (char === "/" && next === "/") while (index < text.length && text[index] !== "\n") index += 1;
450
+ else if (char === "/" && next === "*") {
451
+ const end = text.indexOf("*/", index + 2);
452
+ index = end === -1 ? text.length : end + 2;
453
+ } else {
454
+ out += char;
455
+ index += 1;
456
+ }
457
+ }
458
+ return JSON.parse(dropTrailingCommas(out));
459
+ }
460
+ function asRecord(value) {
461
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
462
+ }
463
+ /**
464
+ * Reads the `compilerOptions.paths` of the project's tsconfig files (`tsconfig.json`,
465
+ * `tsconfig.app.json`). Targets are resolved against `baseUrl` or the config's directory.
466
+ *
467
+ * @param fs The project filesystem.
468
+ * @param root The project root.
469
+ * @returns The mappings, first file first.
470
+ * @example
471
+ * readPathMappings(fs, "/app"); // [{ pattern: "@/*", targets: ["/app/src/*"] }]
472
+ * @since 1.0.0
473
+ *
474
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
475
+ */
476
+ function readPathMappings(fs, root) {
477
+ const mappings = [];
478
+ for (const name of ["tsconfig.json", "tsconfig.app.json"]) {
479
+ const text = fs.read(join(root, name));
480
+ if (text === void 0) continue;
481
+ const options = asRecord(asRecord(parseJsonc(text))?.["compilerOptions"]);
482
+ const paths = asRecord(options?.["paths"]);
483
+ if (paths === void 0) continue;
484
+ const base = typeof options?.["baseUrl"] === "string" ? resolve(root, options["baseUrl"]) : root;
485
+ for (const [pattern, targets] of Object.entries(paths)) {
486
+ if (!Array.isArray(targets)) continue;
487
+ mappings.push({
488
+ pattern,
489
+ targets: targets.filter((target) => typeof target === "string").map((target) => resolve(base, target))
490
+ });
491
+ }
492
+ }
493
+ return mappings;
494
+ }
495
+ /**
496
+ * Maps an import alias directory (`@/components/ui`) to an absolute directory using the tsconfig
497
+ * mappings.
498
+ *
499
+ * @param alias The alias from `components.json`.
500
+ * @param mappings From {@link readPathMappings}.
501
+ * @returns The absolute directory path.
502
+ * @throws PathError when no mapping covers the alias.
503
+ * @example
504
+ * aliasToDirectory("@/components/ui", [{ pattern: "@/*", targets: ["/app/src/*"] }]);
505
+ * // "/app/src/components/ui"
506
+ * @since 1.0.0
507
+ *
508
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
509
+ */
510
+ function aliasToDirectory(alias, mappings) {
511
+ for (const { pattern, targets } of mappings) {
512
+ const star = pattern.indexOf("*");
513
+ if (star === -1) {
514
+ if (pattern === alias && targets[0] !== void 0) return targets[0];
515
+ continue;
516
+ }
517
+ const prefix = pattern.slice(0, star);
518
+ const suffix = pattern.slice(star + 1);
519
+ if (!alias.startsWith(prefix) || !alias.endsWith(suffix) || alias.length < prefix.length + suffix.length) continue;
520
+ const rest = alias.slice(prefix.length, alias.length - suffix.length);
521
+ const target = targets[0];
522
+ if (target !== void 0) return target.replace("*", rest);
523
+ }
524
+ throw new PathError(`The alias "${alias}" is not covered by "paths" in tsconfig.json. Add, for example:\n "compilerOptions": { "paths": { "@/*": ["./src/*"] } }`);
525
+ }
526
+ /**
527
+ * Resolves `target` against `root` and throws unless the result is inside `root`. This is the
528
+ * single guard every write and delete goes through.
529
+ *
530
+ * @param root The project root (absolute).
531
+ * @param target A path, absolute or relative to `root`.
532
+ * @returns The normalized absolute path.
533
+ * @throws PathError when the path escapes the project.
534
+ * @example
535
+ * resolveInside("/app", "src/a.ts"); // "/app/src/a.ts"
536
+ * resolveInside("/app", "../x"); // throws
537
+ * @since 1.0.0
538
+ *
539
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
540
+ */
541
+ function resolveInside(root, target) {
542
+ const absolute = resolve(root, target);
543
+ const rel = relative(resolve(root), absolute);
544
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new PathError(`Refusing to write outside the project: ${target}`);
545
+ return absolute;
546
+ }
547
+ /**
548
+ * Like {@link resolveInside} but also follows symbolic links of the nearest existing ancestor, so
549
+ * a symlinked directory cannot be used to write outside the project.
550
+ *
551
+ * @param fs The project filesystem.
552
+ * @param root The project root.
553
+ * @param target A path, absolute or relative to `root`.
554
+ * @returns The normalized absolute path.
555
+ * @throws PathError when the path, or its real location, is outside the project.
556
+ * @example
557
+ * resolveInsideReal(fs, "/app", "src/components/ui/button.tsx");
558
+ * @since 1.0.0
559
+ *
560
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
561
+ */
562
+ function resolveInsideReal(fs, root, target) {
563
+ const absolute = resolveInside(root, target);
564
+ const real = fs.realpath(absolute);
565
+ const realRoot = fs.realpath(resolve(root));
566
+ const rel = relative(realRoot, real);
567
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new PathError(`Refusing to write outside the project (symbolic link): ${target}`);
568
+ return absolute;
569
+ }
570
+ //#endregion
571
+ //#region src/cli/pm.ts
572
+ /**
573
+ * Package manager detection and the install command line. npm is the default; pnpm, yarn and bun
574
+ * are used when their lockfile (or the `packageManager` field) says so.
575
+ * @packageDocumentation
576
+ */
577
+ /**
578
+ * Detects the project's package manager: the `packageManager` field of `package.json` first, then
579
+ * lockfiles, otherwise npm.
580
+ *
581
+ * @param fs The project filesystem.
582
+ * @param root The project root.
583
+ * @returns The package manager name.
584
+ * @example
585
+ * detectPackageManager(fs, "/app"); // "pnpm" when /app/pnpm-lock.yaml exists
586
+ * @since 1.0.0
587
+ *
588
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
589
+ */
590
+ function detectPackageManager(fs, root) {
591
+ const manifest = fs.read(join(root, "package.json"));
592
+ if (manifest !== void 0) try {
593
+ const field = JSON.parse(manifest).packageManager;
594
+ if (typeof field === "string") {
595
+ const name = field.split("@")[0];
596
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
597
+ }
598
+ } catch (error) {
599
+ if (!(error instanceof SyntaxError)) throw error;
600
+ }
601
+ if (fs.exists(join(root, "pnpm-lock.yaml"))) return "pnpm";
602
+ if (fs.exists(join(root, "yarn.lock"))) return "yarn";
603
+ if (fs.exists(join(root, "bun.lock")) || fs.exists(join(root, "bun.lockb"))) return "bun";
604
+ return "npm";
605
+ }
606
+ /**
607
+ * The command and arguments that install `packages` as dependencies.
608
+ *
609
+ * @param manager The package manager.
610
+ * @param packages Package specifiers (`clsx@2.1.1`).
611
+ * @returns The executable and its arguments.
612
+ * @example
613
+ * installCommand("npm", ["clsx@2.1.1"]); // { command: "npm", args: ["install", "clsx@2.1.1"] }
614
+ * @since 1.0.0
615
+ *
616
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
617
+ */
618
+ function installCommand(manager, packages) {
619
+ switch (manager) {
620
+ case "npm": return {
621
+ command: "npm",
622
+ args: ["install", ...packages]
623
+ };
624
+ case "pnpm": return {
625
+ command: "pnpm",
626
+ args: ["add", ...packages]
627
+ };
628
+ case "yarn": return {
629
+ command: "yarn",
630
+ args: ["add", ...packages]
631
+ };
632
+ case "bun": return {
633
+ command: "bun",
634
+ args: ["add", ...packages]
635
+ };
636
+ }
637
+ }
638
+ //#endregion
639
+ //#region src/cli/schema.ts
640
+ /**
641
+ * The base colors, in documentation order.
642
+ * @since 1.0.0
643
+ *
644
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
645
+ */
646
+ const BASE_COLORS = [
647
+ "neutral",
648
+ "zinc",
649
+ "slate",
650
+ "stone",
651
+ "gray"
652
+ ];
653
+ /**
654
+ * The default aliases (shadcn's).
655
+ * @since 1.0.0
656
+ *
657
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
658
+ */
659
+ const DEFAULT_ALIASES = {
660
+ components: "@/components",
661
+ ui: "@/components/ui",
662
+ lib: "@/lib",
663
+ utils: "@/lib/utils",
664
+ hooks: "@/hooks"
665
+ };
666
+ /** Registry item names are kebab-case; nothing else may reach a file path. */
667
+ const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
668
+ /** A file name inside `registry/<name>/`. */
669
+ const FILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
670
+ /**
671
+ * Whether `name` is a valid registry item name.
672
+ *
673
+ * @param name Any string.
674
+ * @returns `true` for kebab-case names such as `dropdown-menu`.
675
+ * @example
676
+ * isValidItemName("../etc/passwd"); // false
677
+ * @since 1.0.0
678
+ *
679
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
680
+ */
681
+ function isValidItemName(name) {
682
+ return NAME_PATTERN.test(name);
683
+ }
684
+ /**
685
+ * Thrown for malformed registry or configuration data.
686
+ * @since 1.0.0
687
+ *
688
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
689
+ */
690
+ var SchemaError = class extends Error {
691
+ /** The stable code `--json` prints. */
692
+ code = "SHADOW_UI_INVALID_FILE";
693
+ constructor(message) {
694
+ super(message);
695
+ this.name = "SchemaError";
696
+ }
697
+ };
698
+ function record(value, where) {
699
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new SchemaError(`${where} must be an object`);
700
+ return value;
701
+ }
702
+ function text(value, where) {
703
+ if (typeof value !== "string" || value === "") throw new SchemaError(`${where} must be a string`);
704
+ return value;
705
+ }
706
+ function strings(value, where) {
707
+ if (!Array.isArray(value)) throw new SchemaError(`${where} must be an array`);
708
+ return value.map((entry, index) => text(entry, `${where}[${index}]`));
709
+ }
710
+ function itemType(value, where) {
711
+ if (value === "ui" || value === "lib" || value === "hook") return value;
712
+ throw new SchemaError(`${where} must be "ui", "lib" or "hook"`);
713
+ }
714
+ function parseItem(value, index) {
715
+ const where = `items[${index}]`;
716
+ const item = record(value, where);
717
+ const name = text(item["name"], `${where}.name`);
718
+ if (!isValidItemName(name)) throw new SchemaError(`${where}.name "${name}" is not a valid name`);
719
+ const files = item["files"];
720
+ if (!Array.isArray(files) || files.length === 0) throw new SchemaError(`${where}.files must be a non-empty array`);
721
+ const dependencies = record(item["dependencies"], `${where}.dependencies`);
722
+ const tailwind = record(item["tailwind"], `${where}.tailwind`);
723
+ return {
724
+ name,
725
+ type: itemType(item["type"], `${where}.type`),
726
+ title: text(item["title"], `${where}.title`),
727
+ description: text(item["description"], `${where}.description`),
728
+ files: files.map((file, fileIndex) => {
729
+ const entry = record(file, `${where}.files[${fileIndex}]`);
730
+ const path = text(entry["path"], `${where}.files[${fileIndex}].path`);
731
+ const segments = path.split("/");
732
+ const [root, folder, base] = segments;
733
+ if (segments.length !== 3 || root !== "registry" || folder !== name || base === void 0 || !FILE_PATTERN.test(base)) throw new SchemaError(`${where}.files[${fileIndex}].path "${path}" must be registry/${name}/<file>`);
734
+ return {
735
+ path,
736
+ type: itemType(entry["type"], `${where}.files[${fileIndex}].type`)
737
+ };
738
+ }),
739
+ registryDependencies: strings(item["registryDependencies"], `${where}.registryDependencies`),
740
+ dependencies: Object.fromEntries(Object.entries(dependencies).map(([pkg, version]) => [pkg, text(version, `${where}.dependencies.${pkg}`)])),
741
+ exports: strings(item["exports"], `${where}.exports`),
742
+ primitives: strings(item["primitives"], `${where}.primitives`),
743
+ tailwind: {
744
+ tokens: strings(tailwind["tokens"], `${where}.tailwind.tokens`),
745
+ animations: tailwind["animations"] === true
746
+ }
747
+ };
748
+ }
749
+ /**
750
+ * Validates parsed `registry.json` data.
751
+ *
752
+ * @param value The parsed JSON.
753
+ * @returns The typed index.
754
+ * @throws SchemaError naming the first malformed field.
755
+ * @example
756
+ * const index = parseRegistryIndex(JSON.parse(readFileSync("registry.json", "utf8")));
757
+ * @since 1.0.0
758
+ *
759
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
760
+ */
761
+ function parseRegistryIndex(value) {
762
+ const root = record(value, "registry");
763
+ const items = root["items"];
764
+ if (!Array.isArray(items)) throw new SchemaError("registry.items must be an array");
765
+ const parsed = items.map(parseItem);
766
+ const seen = /* @__PURE__ */ new Set();
767
+ for (const item of parsed) {
768
+ if (seen.has(item.name)) throw new SchemaError(`registry has two items named "${item.name}"`);
769
+ seen.add(item.name);
770
+ }
771
+ return {
772
+ name: text(root["name"], "registry.name"),
773
+ version: text(root["version"], "registry.version"),
774
+ frameworkDependencies: strings(root["frameworkDependencies"], "registry.frameworkDependencies"),
775
+ items: parsed
776
+ };
777
+ }
778
+ /**
779
+ * Validates parsed `components.json` data.
780
+ *
781
+ * @param value The parsed JSON.
782
+ * @returns The typed configuration, with defaults for missing aliases.
783
+ * @throws SchemaError naming the first malformed field.
784
+ * @example
785
+ * const config = parseComponentsConfig(JSON.parse(readFileSync("components.json", "utf8")));
786
+ * @since 1.0.0
787
+ *
788
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
789
+ */
790
+ function parseComponentsConfig(value) {
791
+ const root = record(value, "components.json");
792
+ const tailwind = record(root["tailwind"], "components.json tailwind");
793
+ const baseColor = tailwind["baseColor"] ?? "neutral";
794
+ if (!BASE_COLORS.includes(baseColor)) throw new SchemaError(`components.json tailwind.baseColor must be one of ${BASE_COLORS.join(", ")}`);
795
+ const given = root["aliases"] === void 0 ? {} : record(root["aliases"], "components.json aliases");
796
+ const alias = (key) => {
797
+ const entry = given[key];
798
+ if (entry === void 0) return DEFAULT_ALIASES[key];
799
+ const aliasText = text(entry, `components.json aliases.${key}`);
800
+ if (aliasText.includes("..") || /\s/.test(aliasText)) throw new SchemaError(`components.json aliases.${key} "${aliasText}" is not a valid alias`);
801
+ return aliasText;
802
+ };
803
+ const framework = root["framework"];
804
+ return {
805
+ framework: framework === "vite" || framework === "dusk" ? framework : "unknown",
806
+ tailwind: {
807
+ css: text(tailwind["css"], "components.json tailwind.css"),
808
+ baseColor
809
+ },
810
+ aliases: {
811
+ components: alias("components"),
812
+ ui: alias("ui"),
813
+ lib: alias("lib"),
814
+ utils: alias("utils"),
815
+ hooks: alias("hooks")
816
+ }
817
+ };
818
+ }
819
+ //#endregion
820
+ //#region src/cli/project.ts
821
+ /**
822
+ * The project the CLI works in: `components.json`, `package.json`, the stylesheet that imports
823
+ * Tailwind, and the theme stylesheet `init` generates. Reads through {@link ProjectFs}.
824
+ * @packageDocumentation
825
+ */
826
+ /**
827
+ * The file name of the generated theme stylesheet.
828
+ * @since 1.0.0
829
+ *
830
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
831
+ */
832
+ const THEME_FILE = "shadow-ui.css";
833
+ /**
834
+ * Reads the project's `package.json`.
835
+ *
836
+ * @param fs The project filesystem.
837
+ * @param root The project root.
838
+ * @returns The manifest, or `undefined` when there is no `package.json`.
839
+ * @throws SchemaError when the file is not valid JSON.
840
+ * @example
841
+ * readManifest(fs, "/app")?.packages.has("vite");
842
+ * @since 1.0.0
843
+ *
844
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
845
+ */
846
+ function readManifest(fs, root) {
847
+ const text = fs.read(join(root, "package.json"));
848
+ if (text === void 0) return void 0;
849
+ let parsed;
850
+ try {
851
+ parsed = JSON.parse(text);
852
+ } catch (error) {
853
+ if (error instanceof SyntaxError) throw new SchemaError(`package.json is not valid JSON: ${error.message}`);
854
+ throw error;
855
+ }
856
+ const packages = /* @__PURE__ */ new Set();
857
+ if (typeof parsed === "object" && parsed !== null) for (const field of [
858
+ "dependencies",
859
+ "devDependencies",
860
+ "peerDependencies",
861
+ "optionalDependencies"
862
+ ]) {
863
+ const group = parsed[field];
864
+ if (typeof group === "object" && group !== null) for (const name of Object.keys(group)) packages.add(name);
865
+ }
866
+ return { packages };
867
+ }
868
+ /**
869
+ * Reads and validates `components.json`.
870
+ *
871
+ * @param fs The project filesystem.
872
+ * @param root The project root.
873
+ * @returns The configuration, or `undefined` when the file does not exist.
874
+ * @throws SchemaError when the file is malformed.
875
+ * @example
876
+ * const config = readConfig(fs, "/app");
877
+ * @since 1.0.0
878
+ *
879
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
880
+ */
881
+ function readConfig(fs, root) {
882
+ const text = fs.read(join(root, "components.json"));
883
+ if (text === void 0) return void 0;
884
+ try {
885
+ return parseComponentsConfig(JSON.parse(text));
886
+ } catch (error) {
887
+ if (error instanceof SyntaxError) throw new SchemaError(`components.json is not valid JSON: ${error.message}`);
888
+ throw error;
889
+ }
890
+ }
891
+ /**
892
+ * The text of `components.json` for a configuration.
893
+ *
894
+ * @param config The configuration.
895
+ * @returns Pretty-printed JSON ending in a newline.
896
+ * @example
897
+ * fs.write("components.json", serializeConfig(config));
898
+ * @since 1.0.0
899
+ *
900
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
901
+ */
902
+ function serializeConfig(config) {
903
+ return `${JSON.stringify({
904
+ framework: config.framework,
905
+ tailwind: {
906
+ css: config.tailwind.css,
907
+ baseColor: config.tailwind.baseColor
908
+ },
909
+ aliases: config.aliases
910
+ }, null, 2)}\n`;
911
+ }
912
+ const CSS_CANDIDATES = [
913
+ "src/index.css",
914
+ "src/styles.css",
915
+ "src/app.css",
916
+ "src/global.css",
917
+ "src/globals.css",
918
+ "src/styles/globals.css",
919
+ "src/styles/global.css",
920
+ "src/styles/index.css",
921
+ "app/app.css",
922
+ "app/globals.css"
923
+ ];
924
+ const TAILWIND_IMPORT = /@import\s+["']tailwindcss["']|@import\s+["']tailwindcss\//;
925
+ /**
926
+ * Detects the framework, whether Tailwind is installed, and the stylesheet to extend.
927
+ *
928
+ * @param fs The project filesystem.
929
+ * @param root The project root.
930
+ * @param manifest The project's manifest.
931
+ * @returns What was found.
932
+ * @example
933
+ * detectProject(fs, "/app", manifest).framework; // "vite"
934
+ * @since 1.0.0
935
+ *
936
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
937
+ */
938
+ function detectProject(fs, root, manifest) {
939
+ const framework = manifest.packages.has("@shadow-js/dusk") ? "dusk" : manifest.packages.has("vite") || manifest.packages.has("@shadow-js/vite") ? "vite" : "unknown";
940
+ let existing;
941
+ for (const candidate of CSS_CANDIDATES) {
942
+ const text = fs.read(join(root, candidate));
943
+ if (text === void 0) continue;
944
+ if (TAILWIND_IMPORT.test(text)) {
945
+ existing = candidate;
946
+ break;
947
+ }
948
+ existing ??= candidate;
949
+ }
950
+ return {
951
+ framework,
952
+ hasTailwind: manifest.packages.has("tailwindcss"),
953
+ hasTailwindVite: manifest.packages.has("@tailwindcss/vite"),
954
+ css: {
955
+ path: existing ?? (framework === "dusk" ? "src/app.css" : "src/index.css"),
956
+ exists: existing !== void 0
957
+ }
958
+ };
959
+ }
960
+ /** Vite config files, in the order Vite looks for them. */
961
+ const VITE_CONFIGS = [
962
+ "vite.config.ts",
963
+ "vite.config.mts",
964
+ "vite.config.js",
965
+ "vite.config.mjs",
966
+ "vite.config.cts",
967
+ "vite.config.cjs"
968
+ ];
969
+ /** An `@` alias in a Vite config: `"@": ...`, `find: "@"`, or a plugin that reads tsconfig `paths`. */
970
+ const VITE_ALIAS = /["']@["']\s*:|\bfind\s*:\s*["']@["']|tsconfig-?paths|tsconfigPaths/i;
971
+ /** The Tailwind Vite plugin in a Vite config. */
972
+ const VITE_TAILWIND = /@tailwindcss\/vite|\btailwindcss\s*\(/;
973
+ /**
974
+ * What the project's Vite config already has, so `init` prints only the steps that are missing.
975
+ * The check reads the config as text (it is TypeScript that may import anything): a config that
976
+ * builds its alias or plugin list in a way this does not recognize is reported as missing, which
977
+ * costs a redundant hint, never a wrong edit.
978
+ *
979
+ * @param fs The project filesystem.
980
+ * @param root The project root.
981
+ * @returns `undefined` when no Vite config file exists.
982
+ * @example
983
+ * readViteSetup(fs, "/app")?.alias; // true when `"@"` is aliased
984
+ * @since 1.0.0
985
+ *
986
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
987
+ */
988
+ function readViteSetup(fs, root) {
989
+ for (const file of VITE_CONFIGS) {
990
+ const text = fs.read(join(root, file));
991
+ if (text === void 0) continue;
992
+ return {
993
+ file,
994
+ alias: VITE_ALIAS.test(text),
995
+ tailwind: VITE_TAILWIND.test(text)
996
+ };
997
+ }
998
+ }
999
+ /**
1000
+ * The default `components.json` for a project.
1001
+ *
1002
+ * @param info From {@link detectProject}.
1003
+ * @param baseColor The chosen base color.
1004
+ * @param css Override for the stylesheet path.
1005
+ * @returns A complete configuration.
1006
+ * @example
1007
+ * defaultConfig(info, "zinc");
1008
+ * @since 1.0.0
1009
+ *
1010
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1011
+ */
1012
+ function defaultConfig(info, baseColor, css) {
1013
+ return {
1014
+ framework: info.framework,
1015
+ tailwind: {
1016
+ css: css ?? info.css.path,
1017
+ baseColor
1018
+ },
1019
+ aliases: DEFAULT_ALIASES
1020
+ };
1021
+ }
1022
+ /**
1023
+ * The theme stylesheet `init` writes into the project: the base color variables, the Tailwind v4
1024
+ * token mapping and base layer, and the animation utilities, in one file.
1025
+ *
1026
+ * @param source The registry source.
1027
+ * @param baseColor The base color.
1028
+ * @returns CSS text.
1029
+ * @example
1030
+ * fs.write("src/shadow-ui.css", buildThemeCss(registry, "neutral"));
1031
+ * @since 1.0.0
1032
+ *
1033
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1034
+ */
1035
+ function buildThemeCss(source, baseColor) {
1036
+ return [
1037
+ "/* Generated by `shadow-ui init`. Edit freely: this file is yours. */",
1038
+ source.readStyle(`themes/${baseColor}.css`).trimEnd(),
1039
+ source.readStyle("theme-core.css").trim(),
1040
+ source.readStyle("animations.css").trim(),
1041
+ ""
1042
+ ].join("\n\n");
1043
+ }
1044
+ /**
1045
+ * The `@import` line that pulls the theme stylesheet into the entry stylesheet (the theme file is
1046
+ * written next to it).
1047
+ * @since 1.0.0
1048
+ *
1049
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1050
+ */
1051
+ const THEME_IMPORT_LINE = `@import "./${THEME_FILE}";`;
1052
+ /**
1053
+ * Inserts the theme import after the Tailwind import of a stylesheet.
1054
+ *
1055
+ * @param css The entry stylesheet's text.
1056
+ * @returns The new text, or `undefined` when the import is already there or no Tailwind import exists.
1057
+ * @example
1058
+ * insertThemeImport('@import "tailwindcss";\n'); // '@import "tailwindcss";\n@import "./shadow-ui.css";\n'
1059
+ * @since 1.0.0
1060
+ *
1061
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1062
+ */
1063
+ function insertThemeImport(css) {
1064
+ if (css.includes("shadow-ui.css")) return void 0;
1065
+ const lines = css.split("\n");
1066
+ const at = lines.findIndex((entry) => TAILWIND_IMPORT.test(entry));
1067
+ if (at === -1) return void 0;
1068
+ lines.splice(at + 1, 0, THEME_IMPORT_LINE);
1069
+ return lines.join("\n");
1070
+ }
1071
+ /**
1072
+ * Project-relative path for display (forward slashes).
1073
+ *
1074
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1075
+ */
1076
+ function display(root, absolute) {
1077
+ return relative(root, absolute).replaceAll("\\", "/");
1078
+ }
1079
+ /**
1080
+ * The theme file next to a stylesheet.
1081
+ *
1082
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1083
+ */
1084
+ function themePathFor(cssPath) {
1085
+ return join(dirname(cssPath), THEME_FILE);
1086
+ }
1087
+ //#endregion
1088
+ //#region src/cli/resolve.ts
1089
+ /**
1090
+ * Registry resolution: from the names a user typed to the ordered list of items to copy and the
1091
+ * npm packages they need. Pure: no filesystem, no network.
1092
+ * @packageDocumentation
1093
+ */
1094
+ /**
1095
+ * Thrown for an unknown or invalid item name, a `registryDependencies` entry that is not in the
1096
+ * registry, a dependency cycle, or conflicting versions of one npm package.
1097
+ * @since 1.0.0
1098
+ *
1099
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1100
+ */
1101
+ var ResolveError = class extends Error {
1102
+ /** The stable code `--json` prints: `SHADOW_UI_UNKNOWN_COMPONENT`, `SHADOW_UI_INVALID_NAME` or `SHADOW_UI_REGISTRY_CONFLICT`. */
1103
+ code;
1104
+ constructor(message, code = "SHADOW_UI_UNKNOWN_COMPONENT") {
1105
+ super(message);
1106
+ this.name = "ResolveError";
1107
+ this.code = code;
1108
+ }
1109
+ };
1110
+ /** Levenshtein distance, for "did you mean" hints. Names are short, so the plain table is fine. */
1111
+ function distance(a, b) {
1112
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
1113
+ for (let i = 1; i <= a.length; i += 1) {
1114
+ let diagonal = row[0] ?? 0;
1115
+ row[0] = i;
1116
+ for (let j = 1; j <= b.length; j += 1) {
1117
+ const above = row[j] ?? 0;
1118
+ row[j] = Math.min(above + 1, (row[j - 1] ?? 0) + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
1119
+ diagonal = above;
1120
+ }
1121
+ }
1122
+ return row[b.length] ?? 0;
1123
+ }
1124
+ function suggestion(index, name) {
1125
+ let best;
1126
+ let bestDistance = 3;
1127
+ for (const item of index.items) {
1128
+ const d = distance(name, item.name);
1129
+ if (d < bestDistance) {
1130
+ best = item.name;
1131
+ bestDistance = d;
1132
+ }
1133
+ }
1134
+ return best === void 0 ? "" : ` Did you mean "${best}"?`;
1135
+ }
1136
+ /**
1137
+ * Finds an item by name.
1138
+ *
1139
+ * @param index The registry.
1140
+ * @param name The item name.
1141
+ * @returns The item, or `undefined` when there is none.
1142
+ * @example
1143
+ * findItem(index, "button")?.files;
1144
+ * @since 1.0.0
1145
+ *
1146
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1147
+ */
1148
+ function findItem(index, name) {
1149
+ return index.items.find((item) => item.name === name);
1150
+ }
1151
+ /**
1152
+ * Resolves requested names to a copy plan: transitive `registryDependencies`, dependencies first,
1153
+ * each item once. The order is deterministic: depth-first, in the order of the request and of each
1154
+ * item's `registryDependencies`. Traversal is iterative, so a deep dependency chain cannot overflow
1155
+ * the stack.
1156
+ *
1157
+ * @param index The registry.
1158
+ * @param names Requested item names.
1159
+ * @returns The plan.
1160
+ * @throws ResolveError for an invalid or unknown name, a dependency missing from the registry, a
1161
+ * cycle, or two versions of one npm package.
1162
+ * @example
1163
+ * resolveItems(index, ["dialog"]).items.map((item) => item.name); // ["utils", "icons", "dialog"]
1164
+ * @since 1.0.0
1165
+ *
1166
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1167
+ */
1168
+ function resolveItems(index, names) {
1169
+ const requested = [...new Set(names)];
1170
+ const ordered = [];
1171
+ const done = /* @__PURE__ */ new Set();
1172
+ const visiting = /* @__PURE__ */ new Set();
1173
+ for (const root of requested) {
1174
+ if (!isValidItemName(root)) throw new ResolveError(`"${root}" is not a valid component name.`, "SHADOW_UI_INVALID_NAME");
1175
+ if (done.has(root)) continue;
1176
+ const first = findItem(index, root);
1177
+ if (first === void 0) throw new ResolveError(`Unknown component "${root}".${suggestion(index, root)}`);
1178
+ const stack = [{
1179
+ item: first,
1180
+ next: 0
1181
+ }];
1182
+ visiting.add(root);
1183
+ while (stack.length > 0) {
1184
+ const frame = stack[stack.length - 1];
1185
+ if (frame === void 0) break;
1186
+ const dependency = frame.item.registryDependencies[frame.next];
1187
+ if (dependency === void 0) {
1188
+ stack.pop();
1189
+ visiting.delete(frame.item.name);
1190
+ if (!done.has(frame.item.name)) {
1191
+ done.add(frame.item.name);
1192
+ ordered.push(frame.item);
1193
+ }
1194
+ continue;
1195
+ }
1196
+ frame.next += 1;
1197
+ if (done.has(dependency)) continue;
1198
+ if (visiting.has(dependency)) throw new ResolveError(`Circular registry dependency: ${[...stack.map((entry) => entry.item.name), dependency].join(" -> ")}`, "SHADOW_UI_REGISTRY_CONFLICT");
1199
+ const item = findItem(index, dependency);
1200
+ if (item === void 0) throw new ResolveError(`"${frame.item.name}" depends on "${dependency}", which is not in the registry.`, "SHADOW_UI_REGISTRY_CONFLICT");
1201
+ visiting.add(dependency);
1202
+ stack.push({
1203
+ item,
1204
+ next: 0
1205
+ });
1206
+ }
1207
+ }
1208
+ const dependencies = {};
1209
+ for (const item of ordered) for (const [name, version] of Object.entries(item.dependencies)) {
1210
+ const known = dependencies[name];
1211
+ if (known !== void 0 && known !== version) throw new ResolveError(`"${name}" is required at both ${known} and ${version}.`, "SHADOW_UI_REGISTRY_CONFLICT");
1212
+ dependencies[name] = version;
1213
+ }
1214
+ return {
1215
+ requested,
1216
+ items: ordered,
1217
+ dependencies: Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))),
1218
+ usesPrimitives: ordered.some((item) => item.primitives.length > 0),
1219
+ usesAnimations: ordered.some((item) => item.tailwind.animations)
1220
+ };
1221
+ }
1222
+ /**
1223
+ * The items that import `name`, directly or not, among `candidates` (used by `remove` to refuse
1224
+ * deleting something another installed item needs).
1225
+ *
1226
+ * @param index The registry.
1227
+ * @param name The item to be removed.
1228
+ * @param candidates Names of installed items to check.
1229
+ * @returns The candidates that depend on `name`, sorted.
1230
+ * @example
1231
+ * dependentsOf(index, "utils", ["button", "dialog"]); // ["button", "dialog"]
1232
+ * @since 1.0.0
1233
+ *
1234
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1235
+ */
1236
+ function dependentsOf(index, name, candidates) {
1237
+ const found = [];
1238
+ for (const candidate of candidates) {
1239
+ if (candidate === name || !isValidItemName(candidate)) continue;
1240
+ if (findItem(index, candidate) === void 0) continue;
1241
+ if (resolveItems(index, [candidate]).items.some((entry) => entry.name === name)) found.push(candidate);
1242
+ }
1243
+ return found.sort();
1244
+ }
1245
+ //#endregion
1246
+ //#region src/cli/rewrite.ts
1247
+ /**
1248
+ * Maps one import specifier from the registry layout to the project's aliases.
1249
+ *
1250
+ * @param specifier An import specifier such as `@/components/ui/button`.
1251
+ * @param aliases The aliases from `components.json`.
1252
+ * @returns The rewritten specifier, or the input unchanged when it is not a registry alias.
1253
+ * @example
1254
+ * rewriteSpecifier("@/lib/utils", { ...aliases, utils: "~/helpers/cn" }); // "~/helpers/cn"
1255
+ * @since 1.0.0
1256
+ *
1257
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1258
+ */
1259
+ function rewriteSpecifier(specifier, aliases) {
1260
+ if (specifier === "@/lib/utils") return aliases.utils;
1261
+ if (specifier.startsWith("@/components/ui/")) return `${aliases.ui}/${specifier.slice(16)}`;
1262
+ if (specifier.startsWith("@/hooks/")) return `${aliases.hooks}/${specifier.slice(8)}`;
1263
+ if (specifier.startsWith("@/lib/")) return `${aliases.lib}/${specifier.slice(6)}`;
1264
+ return specifier;
1265
+ }
1266
+ /** `from "x"`, `import "x"`, `import("x")` and `export ... from "x"` specifiers. */
1267
+ const SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(["'])(@\/[^"'\n]+)\2/g;
1268
+ /**
1269
+ * Rewrites every registry-alias import in `source` to the project's aliases. Only module
1270
+ * specifiers are touched (static imports, re-exports, dynamic imports); other strings are not.
1271
+ *
1272
+ * @param source A registry file's text.
1273
+ * @param aliases The aliases from `components.json`.
1274
+ * @returns The text with rewritten specifiers.
1275
+ * @example
1276
+ * rewriteImports('import { cn } from "@/lib/utils";', aliases);
1277
+ * @since 1.0.0
1278
+ *
1279
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1280
+ */
1281
+ function rewriteImports(source, aliases) {
1282
+ return source.replace(SPECIFIER, (_match, lead, quote, specifier) => `${lead}${quote}${rewriteSpecifier(specifier, aliases)}${quote}`);
1283
+ }
1284
+ //#endregion
1285
+ //#region src/cli/commands.ts
1286
+ /**
1287
+ * The CLI commands. Each takes a {@link CliContext} (filesystem, registry, output, prompt and
1288
+ * installer are parameters), so tests run them against a temp directory with a recording
1289
+ * installer. Every command returns the process exit code.
1290
+ * @packageDocumentation
1291
+ */
1292
+ /** Exit codes. */
1293
+ const OK = 0;
1294
+ const FAILED = 1;
1295
+ /** Prints an error and records it for `--json`. @returns The failing exit code. */
1296
+ function problem(ctx, code, message) {
1297
+ ctx.err(`error: ${message}`);
1298
+ ctx.report?.fail(code, message);
1299
+ return FAILED;
1300
+ }
1301
+ /** Prints a warning and records it for `--json`. */
1302
+ function caution(ctx, code, message) {
1303
+ ctx.err(`warning: ${message}`);
1304
+ ctx.report?.warn(code, message);
1305
+ }
1306
+ /** Records one file of the document's `files`. */
1307
+ function recordFile(ctx, path, action, diff) {
1308
+ ctx.report?.push("files", diff === void 0 ? {
1309
+ path,
1310
+ action
1311
+ } : {
1312
+ path,
1313
+ action,
1314
+ diff
1315
+ });
1316
+ }
1317
+ function failed(ctx, error) {
1318
+ if (error instanceof SchemaError || error instanceof ResolveError || error instanceof PathError) return problem(ctx, error.code, error.message);
1319
+ throw error;
1320
+ }
1321
+ function requireProject(ctx) {
1322
+ const manifest = readManifest(ctx.fs, ctx.cwd);
1323
+ if (manifest === void 0) problem(ctx, "SHADOW_UI_NO_PROJECT", "no package.json here. Run this command in the root of your project.");
1324
+ return manifest;
1325
+ }
1326
+ function requireConfig(ctx) {
1327
+ const config = readConfig(ctx.fs, ctx.cwd);
1328
+ if (config === void 0) problem(ctx, "SHADOW_UI_NO_CONFIG", "components.json not found. Run `shadow-ui init` first.");
1329
+ return config;
1330
+ }
1331
+ /** The directory (absolute) an item's files go to. */
1332
+ function directoryFor(item, config, mappings, ctx, pathOverride) {
1333
+ if (item.type === "ui" && pathOverride !== void 0) return resolve(ctx.cwd, pathOverride);
1334
+ if (item.type === "ui") return aliasToDirectory(config.aliases.ui, mappings);
1335
+ if (item.type === "hook") return aliasToDirectory(config.aliases.hooks, mappings);
1336
+ return aliasToDirectory(config.aliases.lib, mappings);
1337
+ }
1338
+ /** The absolute target of one registry file, checked to be inside the project. */
1339
+ function targetFor(item, file, config, mappings, ctx, pathOverride) {
1340
+ const base = file.path.slice(file.path.lastIndexOf("/") + 1);
1341
+ if (item.type === "lib" && item.name === "utils") return resolveInsideReal(ctx.fs, ctx.cwd, `${aliasToDirectory(config.aliases.utils, mappings)}${extname(base)}`);
1342
+ return resolveInsideReal(ctx.fs, ctx.cwd, join(directoryFor(item, config, mappings, ctx, pathOverride), base));
1343
+ }
1344
+ /** Builds the per-file plan for the items, reading what is on disk. */
1345
+ function planFiles(ctx, config, items, pathOverride, mappings = readPathMappings(ctx.fs, ctx.cwd)) {
1346
+ const plans = [];
1347
+ for (const item of items) for (const file of item.files) {
1348
+ const raw = ctx.registry.readFile(file.path);
1349
+ const content = file.path.endsWith(".css") ? raw : rewriteImports(raw, config.aliases);
1350
+ const target = targetFor(item, file, config, mappings, ctx, pathOverride);
1351
+ const current = ctx.fs.read(target);
1352
+ plans.push({
1353
+ item,
1354
+ file,
1355
+ target,
1356
+ content,
1357
+ current,
1358
+ status: current === void 0 ? "create" : sameContent(current, content) ? "unchanged" : "differs"
1359
+ });
1360
+ }
1361
+ return plans;
1362
+ }
1363
+ function diffOf(plan, ctx) {
1364
+ return unifiedDiff(plan.current ?? "", plan.content, {
1365
+ before: `a/${display(ctx.cwd, plan.target)} (yours)`,
1366
+ after: `b/${display(ctx.cwd, plan.target)} (registry)`
1367
+ });
1368
+ }
1369
+ /** Missing packages the plan needs, as install specifiers. */
1370
+ function missingPackages(ctx, needed, framework) {
1371
+ const installed = readManifest(ctx.fs, ctx.cwd)?.packages ?? /* @__PURE__ */ new Set();
1372
+ const missing = [];
1373
+ for (const name of framework) if (!installed.has(name)) missing.push(name);
1374
+ for (const [name, version] of Object.entries(needed)) if (!installed.has(name)) missing.push(`${name}@${version}`);
1375
+ return missing;
1376
+ }
1377
+ async function installMissing(ctx, packages, flags) {
1378
+ if (packages.length === 0) return OK;
1379
+ const manager = detectPackageManager(ctx.fs, ctx.cwd);
1380
+ const { command, args } = installCommand(manager, packages);
1381
+ const line = [command, ...args].join(" ");
1382
+ const record = (status) => ctx.report?.push("installs", {
1383
+ status,
1384
+ manager,
1385
+ packages,
1386
+ command: line
1387
+ });
1388
+ if (flags.dryRun) {
1389
+ ctx.out(`would run: ${line}`);
1390
+ record("dry-run");
1391
+ return OK;
1392
+ }
1393
+ if (flags.skipInstall) {
1394
+ ctx.out(`Install the packages yourself: ${line}`);
1395
+ record("skipped");
1396
+ return OK;
1397
+ }
1398
+ if (!(flags.yes || await ctx.confirm(`Install ${packages.join(", ")} with ${manager}?`))) {
1399
+ ctx.out(`Skipped. Install the packages yourself: ${line}`);
1400
+ record("skipped");
1401
+ return OK;
1402
+ }
1403
+ ctx.out(`running: ${line}`);
1404
+ try {
1405
+ await ctx.install({
1406
+ cwd: ctx.cwd,
1407
+ manager,
1408
+ packages,
1409
+ output: ctx.report === void 0 ? "inherit" : "stderr"
1410
+ });
1411
+ } catch (error) {
1412
+ record("failed");
1413
+ return problem(ctx, "SHADOW_UI_INSTALL_FAILED", `${line} failed: ${error instanceof Error ? error.message : String(error)}`);
1414
+ }
1415
+ record("installed");
1416
+ return OK;
1417
+ }
1418
+ /**
1419
+ * Copies components into the project, with everything they import.
1420
+ *
1421
+ * Resolves `registryDependencies` transitively, rewrites imports to the configured aliases, writes
1422
+ * new files, leaves identical files alone, and shows a diff for files that differ. A differing file
1423
+ * is replaced with `overwrite` or when the user confirms; otherwise it is kept and the exit code is 1.
1424
+ * Installs the npm packages the files import that the project lacks. Every target is checked to be
1425
+ * inside the project.
1426
+ *
1427
+ * @param ctx The CLI context.
1428
+ * @param names Component names (ignored with `flags.all`).
1429
+ * @param flags The flags.
1430
+ * @returns The exit code: 0 on success, 1 on any error or when a differing file was kept.
1431
+ * @example
1432
+ * await runAdd(ctx, ["button", "dialog"], { overwrite: false, dryRun: false, yes: true, skipInstall: false, all: false, path: undefined });
1433
+ * @since 1.0.0
1434
+ *
1435
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1436
+ */
1437
+ async function runAdd(ctx, names, flags) {
1438
+ try {
1439
+ if (requireProject(ctx) === void 0) return FAILED;
1440
+ const config = requireConfig(ctx);
1441
+ if (config === void 0) return FAILED;
1442
+ const requested = flags.all ? ctx.registry.index.items.map((item) => item.name) : names;
1443
+ if (requested.length === 0) return problem(ctx, "SHADOW_UI_NO_COMPONENTS", "name at least one component, e.g. `shadow-ui add button`, or pass --all.");
1444
+ const plan = resolveItems(ctx.registry.index, requested);
1445
+ const extra = plan.items.map((item) => item.name).filter((name) => !plan.requested.includes(name));
1446
+ ctx.report?.set("dryRun", flags.dryRun);
1447
+ ctx.report?.set("requested", plan.requested);
1448
+ ctx.report?.set("dependencies", extra);
1449
+ ctx.report?.set("files", []);
1450
+ ctx.out(`Adding ${plan.requested.join(", ")}${extra.length > 0 ? ` (and ${extra.join(", ")} they depend on)` : ""}`);
1451
+ const files = planFiles(ctx, config, plan.items, flags.path);
1452
+ let kept = 0;
1453
+ for (const entry of files) {
1454
+ const where = display(ctx.cwd, entry.target);
1455
+ if (entry.status === "unchanged") {
1456
+ ctx.out(` unchanged ${where}`);
1457
+ recordFile(ctx, where, "unchanged");
1458
+ continue;
1459
+ }
1460
+ if (entry.status === "create") {
1461
+ ctx.out(` ${flags.dryRun ? "would create" : "create"} ${where}`);
1462
+ if (!flags.dryRun) ctx.fs.write(entry.target, entry.content);
1463
+ recordFile(ctx, where, "create");
1464
+ continue;
1465
+ }
1466
+ ctx.out(` differs ${where}`);
1467
+ const diff = diffOf(entry, ctx);
1468
+ for (const line of diff.split("\n")) ctx.out(` ${line}`);
1469
+ if (flags.dryRun) {
1470
+ recordFile(ctx, where, "differs", diff);
1471
+ continue;
1472
+ }
1473
+ if (flags.overwrite || !flags.yes && await ctx.confirm(`Overwrite ${where}?`)) {
1474
+ ctx.fs.write(entry.target, entry.content);
1475
+ ctx.out(` overwrote ${where}`);
1476
+ recordFile(ctx, where, "overwrite");
1477
+ } else {
1478
+ kept += 1;
1479
+ ctx.out(` kept ${where} (pass --overwrite to replace it)`);
1480
+ recordFile(ctx, where, "kept", diff);
1481
+ }
1482
+ }
1483
+ if (plan.usesAnimations) {
1484
+ const css = ctx.fs.read(join(ctx.cwd, config.tailwind.css));
1485
+ if (!ctx.fs.exists(join(ctx.cwd, themePathFor(config.tailwind.css))) && !(css ?? "").includes("animate-in")) caution(ctx, "SHADOW_UI_NO_ANIMATIONS", `these components animate; run \`shadow-ui init\` to add ${THEME_FILE} (animation utilities).`);
1486
+ }
1487
+ const installFailed = await installMissing(ctx, missingPackages(ctx, plan.dependencies, plan.usesPrimitives ? ctx.registry.index.frameworkDependencies : []), flags);
1488
+ if (kept > 0) {
1489
+ const message = `${kept} file(s) differ from the registry and were kept.`;
1490
+ ctx.err(message);
1491
+ ctx.report?.fail("SHADOW_UI_FILES_DIFFER", message);
1492
+ }
1493
+ return installFailed === OK && kept === 0 ? OK : FAILED;
1494
+ } catch (error) {
1495
+ return failed(ctx, error);
1496
+ }
1497
+ }
1498
+ /**
1499
+ * Sets a project up: writes `components.json`, `lib/utils`, and the theme stylesheet, wires the
1500
+ * stylesheet in, installs the helper packages, and prints what remains to configure by hand.
1501
+ * An existing file is kept unless `overwrite` is set. The one exception is `components.json`: a
1502
+ * changed base color or stylesheet (from the flags) rewrites it, keeping the aliases.
1503
+ *
1504
+ * @param ctx The CLI context.
1505
+ * @param flags The flags.
1506
+ * @returns The exit code.
1507
+ * @example
1508
+ * await runInit(ctx, { overwrite: false, dryRun: false, yes: true, skipInstall: false, baseColor: "zinc", css: undefined });
1509
+ * @since 1.0.0
1510
+ *
1511
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1512
+ */
1513
+ async function runInit(ctx, flags) {
1514
+ try {
1515
+ const manifest = requireProject(ctx);
1516
+ if (manifest === void 0) return FAILED;
1517
+ const info = detectProject(ctx.fs, ctx.cwd, manifest);
1518
+ if (info.framework === "unknown") caution(ctx, "SHADOW_UI_UNKNOWN_FRAMEWORK", "neither Vite nor @shadow-js/dusk found in package.json; continuing with Vite defaults.");
1519
+ const existing = readConfig(ctx.fs, ctx.cwd);
1520
+ const baseColor = flags.baseColor ?? existing?.tailwind.baseColor ?? "neutral";
1521
+ const config = existing === void 0 || flags.overwrite ? defaultConfig(info, baseColor, flags.css) : {
1522
+ ...existing,
1523
+ tailwind: {
1524
+ css: flags.css ?? existing.tailwind.css,
1525
+ baseColor
1526
+ }
1527
+ };
1528
+ const write = (path, content, label) => {
1529
+ const absolute = resolveInsideReal(ctx.fs, ctx.cwd, path);
1530
+ const current = ctx.fs.read(absolute);
1531
+ if (current !== void 0 && !flags.overwrite) {
1532
+ if (!sameContent(current, content)) {
1533
+ ctx.out(` kept ${label} (exists; pass --overwrite to replace it)`);
1534
+ recordFile(ctx, label, "kept");
1535
+ } else {
1536
+ ctx.out(` unchanged ${label}`);
1537
+ recordFile(ctx, label, "unchanged");
1538
+ }
1539
+ return "kept";
1540
+ }
1541
+ ctx.out(` ${flags.dryRun ? "would write" : "write"} ${label}`);
1542
+ if (!flags.dryRun) ctx.fs.write(absolute, content);
1543
+ recordFile(ctx, label, current === void 0 ? "create" : "update");
1544
+ return "created";
1545
+ };
1546
+ ctx.report?.set("dryRun", flags.dryRun);
1547
+ ctx.report?.set("config", {
1548
+ baseColor,
1549
+ css: config.tailwind.css
1550
+ });
1551
+ ctx.report?.set("files", []);
1552
+ ctx.out("Setting up @shadow-js/ui");
1553
+ const configText = serializeConfig(config);
1554
+ const configPath = resolveInsideReal(ctx.fs, ctx.cwd, "components.json");
1555
+ const configNow = ctx.fs.read(configPath);
1556
+ if (existing !== void 0 && !flags.overwrite && configNow !== void 0 && (config.tailwind.baseColor !== existing.tailwind.baseColor || config.tailwind.css !== existing.tailwind.css) && !sameContent(configNow, configText)) {
1557
+ ctx.out(` ${flags.dryRun ? "would update" : "updated"} components.json (base color ${config.tailwind.baseColor}, stylesheet ${config.tailwind.css})`);
1558
+ if (!flags.dryRun) ctx.fs.write(configPath, configText);
1559
+ recordFile(ctx, "components.json", "update");
1560
+ } else write("components.json", configText, "components.json");
1561
+ const themeFile = themePathFor(config.tailwind.css);
1562
+ const themeLabel = themeFile.replaceAll("\\", "/");
1563
+ const themeText = buildThemeCss(ctx.registry, config.tailwind.baseColor);
1564
+ const themeCurrent = ctx.fs.read(resolveInsideReal(ctx.fs, ctx.cwd, themeFile));
1565
+ const recolor = existing !== void 0 && !flags.overwrite && existing.tailwind.baseColor !== config.tailwind.baseColor && themeCurrent !== void 0;
1566
+ if (recolor && sameContent(themeCurrent, buildThemeCss(ctx.registry, existing.tailwind.baseColor))) {
1567
+ ctx.out(` ${flags.dryRun ? "would write" : "write"} ${themeLabel} (${config.tailwind.baseColor})`);
1568
+ if (!flags.dryRun) ctx.fs.write(resolveInsideReal(ctx.fs, ctx.cwd, themeFile), themeText);
1569
+ recordFile(ctx, themeLabel, "update");
1570
+ } else if (recolor) {
1571
+ ctx.out(` kept ${themeLabel} (edited; pass --overwrite to regenerate it for ${config.tailwind.baseColor})`);
1572
+ recordFile(ctx, themeLabel, "kept");
1573
+ } else write(themeFile, themeText, themeLabel);
1574
+ const cssPath = resolveInsideReal(ctx.fs, ctx.cwd, config.tailwind.css);
1575
+ const css = ctx.fs.read(cssPath);
1576
+ if (css === void 0) write(config.tailwind.css, `@import "tailwindcss";\n${THEME_IMPORT_LINE}\n`, config.tailwind.css);
1577
+ else {
1578
+ const next = insertThemeImport(css);
1579
+ if (next === void 0) {
1580
+ ctx.out(` unchanged ${config.tailwind.css} (${css.includes("shadow-ui.css") ? "already imports the theme" : "no `@import \"tailwindcss\"` line; add the import yourself"})`);
1581
+ recordFile(ctx, config.tailwind.css, "unchanged");
1582
+ } else if (flags.dryRun) {
1583
+ ctx.out(` would add ${THEME_IMPORT_LINE} to ${config.tailwind.css}`);
1584
+ recordFile(ctx, config.tailwind.css, "update");
1585
+ } else if (flags.yes || await ctx.confirm(`Add @import "./shadow-ui.css"; to ${config.tailwind.css}?`)) {
1586
+ ctx.fs.write(cssPath, next);
1587
+ ctx.out(` edited ${config.tailwind.css} (added ${THEME_IMPORT_LINE})`);
1588
+ recordFile(ctx, config.tailwind.css, "update");
1589
+ } else {
1590
+ ctx.out(` skipped add this line after the Tailwind import of ${config.tailwind.css}: ${THEME_IMPORT_LINE}`);
1591
+ recordFile(ctx, config.tailwind.css, "skipped");
1592
+ }
1593
+ }
1594
+ const added = await runAddForInit(ctx, config, flags);
1595
+ if (added !== OK) return added;
1596
+ const tooling = [];
1597
+ if (!info.hasTailwind) tooling.push("tailwindcss");
1598
+ if (!info.hasTailwindVite) tooling.push("@tailwindcss/vite");
1599
+ const installed = await installMissing(ctx, tooling, flags);
1600
+ printNextSteps(ctx, config, info.framework);
1601
+ return installed;
1602
+ } catch (error) {
1603
+ return failed(ctx, error);
1604
+ }
1605
+ }
1606
+ /** `init` copies `utils` through the same pipeline as `add`. */
1607
+ async function runAddForInit(ctx, config, flags) {
1608
+ const plan = resolveItems(ctx.registry.index, ["utils"]);
1609
+ const mappings = [...readPathMappings(ctx.fs, ctx.cwd), {
1610
+ pattern: "@/*",
1611
+ targets: [join(ctx.cwd, "src", "*")]
1612
+ }];
1613
+ for (const entry of planFiles(ctx, config, plan.items, void 0, mappings)) {
1614
+ const where = display(ctx.cwd, entry.target);
1615
+ if (entry.status === "unchanged") {
1616
+ ctx.out(` unchanged ${where}`);
1617
+ recordFile(ctx, where, "unchanged");
1618
+ } else if (entry.status === "differs" && !flags.overwrite) {
1619
+ ctx.out(` kept ${where} (exists; pass --overwrite to replace it)`);
1620
+ recordFile(ctx, where, "kept");
1621
+ } else {
1622
+ ctx.out(` ${flags.dryRun ? "would write" : "write"} ${where}`);
1623
+ if (!flags.dryRun) ctx.fs.write(entry.target, entry.content);
1624
+ recordFile(ctx, where, entry.status === "create" ? "create" : "update");
1625
+ }
1626
+ }
1627
+ return installMissing(ctx, missingPackages(ctx, plan.dependencies, []), flags);
1628
+ }
1629
+ function printNextSteps(ctx, config, framework) {
1630
+ const mappings = readPathMappings(ctx.fs, ctx.cwd);
1631
+ const vite = readViteSetup(ctx.fs, ctx.cwd);
1632
+ ctx.out("");
1633
+ ctx.out("Next steps");
1634
+ let step = 1;
1635
+ const say = (text) => {
1636
+ ctx.out(` ${step++}. ${text}`);
1637
+ ctx.report?.push("nextSteps", text);
1638
+ };
1639
+ try {
1640
+ aliasToDirectory(config.aliases.ui, mappings);
1641
+ } catch (error) {
1642
+ if (!(error instanceof PathError)) throw error;
1643
+ say("Add the import alias to tsconfig.json: \"compilerOptions\": { \"paths\": { \"@/*\": [\"./src/*\"] } }");
1644
+ }
1645
+ if (framework !== "unknown" || vite !== void 0) {
1646
+ const file = vite?.file ?? "vite.config.ts";
1647
+ const tailwind = vite?.tailwind === true;
1648
+ if (vite?.alias !== true) say(`Make Vite resolve "@": in ${file} add \`resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } }\` (\`fileURLToPath\` is from "node:url").`);
1649
+ if (!tailwind) say(`Add \`tailwindcss()\` from @tailwindcss/vite to \`plugins\` in ${file}.`);
1650
+ } else say("Make Vite resolve \"@\": in vite.config.ts add `resolve: { alias: { \"@\": fileURLToPath(new URL(\"./src\", import.meta.url)) } }` and `tailwindcss()` from @tailwindcss/vite to `plugins`.");
1651
+ say(`Import ${config.tailwind.css} once in your entry file (it already imports ${THEME_FILE}).`);
1652
+ say("Prevent a flash of the wrong theme: put the text of themeScript() from \"@shadow-js/ui\" in a <script> in <head> (README, \"Dark mode\"), and wrap the app in <ThemeProvider>.");
1653
+ say("Add components: shadow-ui add button dialog");
1654
+ }
1655
+ /**
1656
+ * Lists the registry's components and whether each is already in the project. In a `--json` run
1657
+ * (`ctx.report` is set) the rows are recorded as `items`, with `initialized` saying whether `components.json` exists.
1658
+ *
1659
+ * @param ctx The CLI context.
1660
+ * @returns The exit code.
1661
+ * @example
1662
+ * runList(ctx);
1663
+ * @since 1.0.0
1664
+ *
1665
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1666
+ */
1667
+ function runList(ctx) {
1668
+ try {
1669
+ const config = readConfig(ctx.fs, ctx.cwd);
1670
+ const mappings = config === void 0 ? [] : readPathMappings(ctx.fs, ctx.cwd);
1671
+ const rows = ctx.registry.index.items.map((item) => {
1672
+ let installed = false;
1673
+ if (config !== void 0) {
1674
+ const main = item.files[0];
1675
+ if (main !== void 0) try {
1676
+ installed = ctx.fs.exists(targetFor(item, main, config, mappings, ctx, void 0));
1677
+ } catch (error) {
1678
+ if (!(error instanceof PathError)) throw error;
1679
+ }
1680
+ }
1681
+ return {
1682
+ name: item.name,
1683
+ type: item.type,
1684
+ description: item.description,
1685
+ installed
1686
+ };
1687
+ });
1688
+ ctx.report?.set("initialized", config !== void 0);
1689
+ ctx.report?.set("items", rows);
1690
+ const width = Math.max(...rows.map((row) => row.name.length));
1691
+ for (const row of rows) ctx.out(`${row.installed ? "*" : " "} ${row.name.padEnd(width)} ${row.description}`);
1692
+ ctx.out("");
1693
+ ctx.out(`${rows.length} items${config === void 0 ? "" : "; * = already in your project"}`);
1694
+ return OK;
1695
+ } catch (error) {
1696
+ return failed(ctx, error);
1697
+ }
1698
+ }
1699
+ /**
1700
+ * Compares the project's copy of components with the registry's and prints a unified diff of each
1701
+ * that differs. With no names, checks every component that is in the project.
1702
+ *
1703
+ * @param ctx The CLI context.
1704
+ * @param names Component names, or empty for all installed ones.
1705
+ * @returns 0 when every checked file matches, 1 when a checked file differs or a named component is
1706
+ * not in the project.
1707
+ * @example
1708
+ * await runDiff(ctx, ["button"]);
1709
+ * @since 1.0.0
1710
+ *
1711
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1712
+ */
1713
+ function runDiff(ctx, names) {
1714
+ try {
1715
+ if (requireProject(ctx) === void 0) return FAILED;
1716
+ const config = requireConfig(ctx);
1717
+ if (config === void 0) return FAILED;
1718
+ for (const name of names) {
1719
+ if (!isValidItemName(name)) throw new ResolveError(`"${name}" is not a valid component name.`);
1720
+ if (findItem(ctx.registry.index, name) === void 0) throw new ResolveError(`Unknown component "${name}".`);
1721
+ }
1722
+ const items = (names.length === 0 ? ctx.registry.index.items : names.map((name) => findItem(ctx.registry.index, name))).filter((item) => item !== void 0);
1723
+ let differing = 0;
1724
+ let checked = 0;
1725
+ ctx.report?.set("components", []);
1726
+ const record = (name, path, status, diff) => ctx.report?.push("components", diff === void 0 ? {
1727
+ name,
1728
+ path,
1729
+ status
1730
+ } : {
1731
+ name,
1732
+ path,
1733
+ status,
1734
+ diff
1735
+ });
1736
+ for (const entry of planFiles(ctx, config, items, void 0)) {
1737
+ const where = display(ctx.cwd, entry.target);
1738
+ if (entry.status === "create") {
1739
+ if (names.length > 0) {
1740
+ ctx.out(`${entry.item.name}: not in your project (${where})`);
1741
+ record(entry.item.name, where, "missing");
1742
+ differing += 1;
1743
+ }
1744
+ continue;
1745
+ }
1746
+ checked += 1;
1747
+ if (entry.status === "unchanged") {
1748
+ if (names.length > 0) ctx.out(`${entry.item.name}: up to date (${where})`);
1749
+ record(entry.item.name, where, "up-to-date");
1750
+ continue;
1751
+ }
1752
+ differing += 1;
1753
+ ctx.out(`${entry.item.name}: differs (${where})`);
1754
+ const diff = diffOf(entry, ctx);
1755
+ ctx.out(diff);
1756
+ record(entry.item.name, where, "differs", diff);
1757
+ }
1758
+ if (names.length === 0) ctx.out(`${checked} file(s) checked, ${differing} differ from the registry.`);
1759
+ ctx.report?.set("checked", checked);
1760
+ ctx.report?.set("differing", differing);
1761
+ if (differing > 0) ctx.report?.fail("SHADOW_UI_DIFFERS", `${differing} file(s) differ from the registry or are missing from the project.`);
1762
+ return differing === 0 ? OK : FAILED;
1763
+ } catch (error) {
1764
+ return failed(ctx, error);
1765
+ }
1766
+ }
1767
+ /**
1768
+ * Deletes components' files from the project. Refuses when another component that is in the
1769
+ * project imports one of them (unless `force`), and never removes npm packages.
1770
+ *
1771
+ * @param ctx The CLI context.
1772
+ * @param names Component names.
1773
+ * @param flags The flags.
1774
+ * @returns The exit code.
1775
+ * @example
1776
+ * await runRemove(ctx, ["badge"], { dryRun: false, force: false });
1777
+ * @since 1.0.0
1778
+ *
1779
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1780
+ */
1781
+ function runRemove(ctx, names, flags) {
1782
+ try {
1783
+ if (requireProject(ctx) === void 0) return FAILED;
1784
+ const config = requireConfig(ctx);
1785
+ if (config === void 0) return FAILED;
1786
+ if (names.length === 0) return problem(ctx, "SHADOW_UI_NO_COMPONENTS", "name at least one component to remove.");
1787
+ const mappings = readPathMappings(ctx.fs, ctx.cwd);
1788
+ const installedNames = ctx.registry.index.items.filter((item) => {
1789
+ const main = item.files[0];
1790
+ return main !== void 0 && ctx.fs.exists(targetFor(item, main, config, mappings, ctx, void 0));
1791
+ }).map((item) => item.name);
1792
+ const removing = resolveItems(ctx.registry.index, names).requested;
1793
+ ctx.report?.set("dryRun", flags.dryRun);
1794
+ ctx.report?.set("removed", []);
1795
+ let code = OK;
1796
+ for (const name of removing) {
1797
+ const blockers = dependentsOf(ctx.registry.index, name, installedNames).filter((other) => !removing.includes(other));
1798
+ if (blockers.length > 0 && !flags.force) {
1799
+ problem(ctx, "SHADOW_UI_IN_USE", `${name} is used by ${blockers.join(", ")}; remove those first or pass --force.`);
1800
+ ctx.report?.push("blocked", {
1801
+ name,
1802
+ usedBy: blockers
1803
+ });
1804
+ code = FAILED;
1805
+ continue;
1806
+ }
1807
+ const item = findItem(ctx.registry.index, name);
1808
+ if (item === void 0) continue;
1809
+ for (const file of item.files) {
1810
+ const target = targetFor(item, file, config, mappings, ctx, void 0);
1811
+ if (!ctx.fs.exists(target)) continue;
1812
+ const where = display(ctx.cwd, target);
1813
+ ctx.out(` ${flags.dryRun ? "would remove" : "remove"} ${where}`);
1814
+ if (!flags.dryRun) ctx.fs.remove(target);
1815
+ ctx.report?.push("removed", {
1816
+ name,
1817
+ path: where
1818
+ });
1819
+ }
1820
+ }
1821
+ return code;
1822
+ } catch (error) {
1823
+ return failed(ctx, error);
1824
+ }
1825
+ }
1826
+ //#endregion
1827
+ //#region src/cli/completion.ts
1828
+ /**
1829
+ * Shell completion scripts for the `shadow-ui` command. Each script is static text built from the
1830
+ * commands and flags it is given. It never calls back into the CLI, so it cannot complete
1831
+ * component names.
1832
+ * @packageDocumentation
1833
+ */
1834
+ /**
1835
+ * The shells `shadow-ui completion` writes a script for.
1836
+ * @example
1837
+ * const shells: readonly string[] = SHELLS; // ["bash", "zsh", "fish"]
1838
+ * @since 1.0.0
1839
+ *
1840
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1841
+ */
1842
+ const SHELLS = [
1843
+ "bash",
1844
+ "zsh",
1845
+ "fish"
1846
+ ];
1847
+ /**
1848
+ * The shells as a phrase for messages: `bash, zsh or fish`.
1849
+ * @example
1850
+ * const phrase: string = SHELL_CHOICES; // "bash, zsh or fish"
1851
+ * @since 1.0.0
1852
+ *
1853
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1854
+ */
1855
+ const SHELL_CHOICES = `${SHELLS.slice(0, -1).join(", ")} or ${SHELLS[SHELLS.length - 1]}`;
1856
+ /**
1857
+ * Tells whether a word names one of the shells {@link SHELLS} lists.
1858
+ * @param value The word from the command line, if any.
1859
+ * @returns `true` when `value` is `bash`, `zsh` or `fish`.
1860
+ * @since 1.0.0
1861
+ *
1862
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1863
+ */
1864
+ function isShell(value) {
1865
+ return value !== void 0 && SHELLS.includes(value);
1866
+ }
1867
+ /**
1868
+ * Builds the completion script for one shell. The script has no trailing newline.
1869
+ * @param shell The shell the script is for.
1870
+ * @param spec The commands and long flags the script offers.
1871
+ * @returns The script text, ready to save or to `source`.
1872
+ * @example
1873
+ * const script = completionScript("bash", { commands: ["init", "add"], flags: ["yes"] });
1874
+ * // script begins with "# bash completion for shadow-ui" and its last line is
1875
+ * // "complete -o default -F _shadow_ui shadow-ui"
1876
+ * @since 1.0.0
1877
+ *
1878
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
1879
+ */
1880
+ function completionScript(shell, spec) {
1881
+ const commands = spec.commands.join(" ");
1882
+ switch (shell) {
1883
+ case "bash": return bashScript(commands, spec.flags.map((flag) => `--${flag}`));
1884
+ case "zsh": return zshScript(commands, spec.flags.map((flag) => `--${flag}`));
1885
+ case "fish": return fishScript(commands, spec.flags);
1886
+ }
1887
+ }
1888
+ function bashScript(commands, flags) {
1889
+ return [
1890
+ "# bash completion for shadow-ui. Save it in your bash-completion directory as shadow-ui,",
1891
+ "# or run: source <(shadow-ui completion bash)",
1892
+ "_shadow_ui() {",
1893
+ " local cur",
1894
+ " cur=\"${COMP_WORDS[COMP_CWORD]}\"",
1895
+ " if [ \"$COMP_CWORD\" -eq 1 ]; then",
1896
+ ` COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )`,
1897
+ " elif [[ \"$cur\" == -* ]]; then",
1898
+ ` COMPREPLY=( $(compgen -W "${flags.join(" ")}" -- "$cur") )`,
1899
+ " fi",
1900
+ " return 0",
1901
+ "}",
1902
+ "complete -o default -F _shadow_ui shadow-ui"
1903
+ ].join("\n");
1904
+ }
1905
+ function zshScript(commands, flags) {
1906
+ return [
1907
+ "#compdef shadow-ui",
1908
+ "",
1909
+ "_shadow_ui() {",
1910
+ " local -a commands flags",
1911
+ ` commands=(${commands})`,
1912
+ ` flags=(${flags.join(" ")})`,
1913
+ " if (( CURRENT == 2 )); then",
1914
+ " compadd -- $commands",
1915
+ " elif [[ $PREFIX == -* ]]; then",
1916
+ " compadd -- $flags",
1917
+ " else",
1918
+ " _files",
1919
+ " fi",
1920
+ "}",
1921
+ "",
1922
+ "if [[ $funcstack[1] == _shadow_ui ]]; then",
1923
+ " _shadow_ui \"$@\"",
1924
+ "else",
1925
+ " compdef _shadow_ui shadow-ui",
1926
+ "fi"
1927
+ ].join("\n");
1928
+ }
1929
+ function fishScript(commands, flags) {
1930
+ return [
1931
+ "# fish completion for shadow-ui. Save it as ~/.config/fish/completions/shadow-ui.fish,",
1932
+ "# or run: shadow-ui completion fish | source",
1933
+ `complete -c shadow-ui -f -n __fish_use_subcommand -a "${commands}"`,
1934
+ `complete -c shadow-ui -f -n "__fish_seen_subcommand_from completion" -a "${SHELLS.join(" ")}"`,
1935
+ ...flags.map((flag) => `complete -c shadow-ui -l ${flag}`)
1936
+ ].join("\n");
1937
+ }
1938
+ //#endregion
1939
+ //#region src/cli/main.ts
1940
+ /**
1941
+ * Argument parsing and dispatch for the `shadow-ui` command. `main` takes its context as a
1942
+ * parameter, so it is tested without spawning a process.
1943
+ * @packageDocumentation
1944
+ */
1945
+ /** Subcommand names the shell completion offers, aliases included. */
1946
+ const COMMANDS = [
1947
+ "init",
1948
+ "add",
1949
+ "list",
1950
+ "ls",
1951
+ "diff",
1952
+ "remove",
1953
+ "rm",
1954
+ "completion",
1955
+ "help"
1956
+ ];
1957
+ const HELP = `shadow-ui: add shadcn-style components to a ShadowJS project
1958
+
1959
+ Usage
1960
+ shadow-ui init [options] set up components.json, lib/utils and the theme
1961
+ shadow-ui add <name...> [options] copy components (and what they import) into the project
1962
+ shadow-ui list list the registry; * marks components in your project
1963
+ shadow-ui diff [name...] compare your copy with the registry (exit 1 if it differs)
1964
+ shadow-ui remove <name...> delete components from the project
1965
+ shadow-ui completion <shell> print a bash, zsh or fish completion script
1966
+
1967
+ Options
1968
+ -y, --yes answer yes to confirmations (never overwrites by itself)
1969
+ -o, --overwrite replace files that differ from the registry
1970
+ --dry-run print what would happen, change nothing
1971
+ -a, --all add: every component
1972
+ --path <dir> add: write ui components to <dir> instead of the configured alias
1973
+ --skip-install do not install npm packages; print the command instead
1974
+ --base-color <c> init: ${BASE_COLORS.join(" | ")} (default neutral)
1975
+ --css <file> init: the stylesheet that imports Tailwind
1976
+ --force remove: even if another component uses it
1977
+ --cwd <dir> run in <dir>
1978
+ -h, --help show this help
1979
+ -v, --version show the version of @shadow-js/ui
1980
+ --json print one JSON document on stdout instead of words (every command; nothing is asked)
1981
+ `;
1982
+ const OPTIONS = {
1983
+ yes: {
1984
+ type: "boolean",
1985
+ short: "y"
1986
+ },
1987
+ overwrite: {
1988
+ type: "boolean",
1989
+ short: "o"
1990
+ },
1991
+ "dry-run": { type: "boolean" },
1992
+ all: {
1993
+ type: "boolean",
1994
+ short: "a"
1995
+ },
1996
+ path: { type: "string" },
1997
+ "skip-install": { type: "boolean" },
1998
+ "base-color": { type: "string" },
1999
+ css: { type: "string" },
2000
+ force: { type: "boolean" },
2001
+ json: { type: "boolean" },
2002
+ cwd: { type: "string" },
2003
+ help: {
2004
+ type: "boolean",
2005
+ short: "h"
2006
+ },
2007
+ version: {
2008
+ type: "boolean",
2009
+ short: "v"
2010
+ }
2011
+ };
2012
+ /** The command a document names: `shadow-ui add` (an alias is named by its command). */
2013
+ const CANONICAL = {
2014
+ ls: "list",
2015
+ rm: "remove"
2016
+ };
2017
+ /**
2018
+ * Runs the CLI. With `--json` (anywhere before a `--`) every outcome is one JSON document on stdout, in the contract of
2019
+ * `json.ts`: the command's facts in `data`, its warnings in `warnings`, its failure in `error`. The human lines that
2020
+ * `data` already holds are left out, the warnings and errors still go to stderr, nothing is asked, and an installer's
2021
+ * output goes to stderr. The exit codes are the same with and without `--json`.
2022
+ *
2023
+ * @param argv The arguments after the executable (`["add", "button"]`).
2024
+ * @param context Builds the context for a working directory (`--cwd` overrides the default).
2025
+ * @returns The process exit code.
2026
+ * @example
2027
+ * process.exitCode = await main(process.argv.slice(2), (cwd) => realContext(cwd));
2028
+ * @since 1.0.0
2029
+ *
2030
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
2031
+ */
2032
+ async function main(argv, context) {
2033
+ const json = wantsJson(argv);
2034
+ let parsed;
2035
+ const base = context(void 0);
2036
+ /** Prints a document: the one thing a `--json` run writes on stdout. */
2037
+ const emit = (document) => base.out(jsonText(document));
2038
+ try {
2039
+ parsed = parseArgs({
2040
+ args: [...argv],
2041
+ options: OPTIONS,
2042
+ allowPositionals: true,
2043
+ strict: true
2044
+ });
2045
+ } catch (error) {
2046
+ if (error instanceof TypeError) {
2047
+ if (json) {
2048
+ const message = error.message.split(". ")[0] ?? error.message;
2049
+ emit(jsonFailure("shadow-ui", {
2050
+ code: /^Unknown option/.test(message) ? "SHADOW_UI_UNKNOWN_OPTION" : "SHADOW_UI_INVALID_OPTION",
2051
+ message
2052
+ }));
2053
+ return 1;
2054
+ }
2055
+ base.err(`error: ${error.message}`);
2056
+ base.err("Run `shadow-ui --help` for usage.");
2057
+ return 1;
2058
+ }
2059
+ throw error;
2060
+ }
2061
+ const { values, positionals } = parsed;
2062
+ const [command, ...rest] = positionals;
2063
+ if (values.help === true || command === "help" || command === void 0 && values.version !== true) {
2064
+ const missing = command === void 0 && values.help !== true;
2065
+ if (json) emit(missing ? jsonFailure("shadow-ui", {
2066
+ code: "SHADOW_UI_MISSING_COMMAND",
2067
+ message: "no command given. Run `shadow-ui --help` for usage."
2068
+ }) : jsonSuccess("shadow-ui", { usage: HELP }));
2069
+ else base.out(HELP);
2070
+ return missing ? 1 : 0;
2071
+ }
2072
+ if (values.version === true) {
2073
+ const version = base.version ?? base.registry.index.version;
2074
+ if (json) emit(jsonSuccess("shadow-ui", { version }));
2075
+ else base.out(version);
2076
+ return 0;
2077
+ }
2078
+ const real = values.cwd === void 0 ? base : context(values.cwd);
2079
+ const report = json ? new Report() : void 0;
2080
+ const ctx = report === void 0 ? real : {
2081
+ ...real,
2082
+ out: () => void 0,
2083
+ confirm: async () => false,
2084
+ report
2085
+ };
2086
+ const scope = `shadow-ui ${CANONICAL[command ?? ""] ?? command ?? ""}`.trimEnd();
2087
+ const common = {
2088
+ overwrite: values.overwrite === true,
2089
+ dryRun: values["dry-run"] === true,
2090
+ yes: values.yes === true,
2091
+ skipInstall: values["skip-install"] === true
2092
+ };
2093
+ let code;
2094
+ try {
2095
+ code = await dispatch(ctx, command, rest, values, common);
2096
+ } catch (error) {
2097
+ if (report === void 0) throw error;
2098
+ const message = error instanceof Error ? error.message : String(error);
2099
+ if (error instanceof Error && error.stack !== void 0) ctx.err(error.stack);
2100
+ report.fail("SHADOW_UI_UNEXPECTED", `unexpected error: ${message}`);
2101
+ code = 1;
2102
+ }
2103
+ if (report !== void 0) {
2104
+ const unknown = command === void 0 || !COMMANDS.includes(command);
2105
+ const document = code === 0 ? jsonSuccess(scope, report.data, report.warnings) : jsonFailure(unknown ? "shadow-ui" : scope, report.error ?? {
2106
+ code: "SHADOW_UI_FAILED",
2107
+ message: "the command failed."
2108
+ }, report.data, report.warnings);
2109
+ real.out(jsonText(document));
2110
+ }
2111
+ return code;
2112
+ }
2113
+ /** Runs the named command. */
2114
+ async function dispatch(ctx, command, rest, values, common) {
2115
+ switch (command) {
2116
+ case "init": {
2117
+ const color = values["base-color"];
2118
+ if (color !== void 0 && !BASE_COLORS.includes(color)) {
2119
+ const message = `--base-color must be one of ${BASE_COLORS.join(", ")}`;
2120
+ ctx.err(`error: ${message}`);
2121
+ ctx.report?.fail("SHADOW_UI_INVALID_BASE_COLOR", message);
2122
+ return 1;
2123
+ }
2124
+ return runInit(ctx, {
2125
+ ...common,
2126
+ baseColor: color,
2127
+ css: values.css
2128
+ });
2129
+ }
2130
+ case "add": return runAdd(ctx, rest, {
2131
+ ...common,
2132
+ all: values.all === true,
2133
+ path: values.path
2134
+ });
2135
+ case "list":
2136
+ case "ls": return runList(ctx);
2137
+ case "diff": return runDiff(ctx, rest);
2138
+ case "remove":
2139
+ case "rm": return runRemove(ctx, rest, {
2140
+ dryRun: common.dryRun,
2141
+ force: values.force === true
2142
+ });
2143
+ case "completion": {
2144
+ const shell = rest[0];
2145
+ if (!isShell(shell)) {
2146
+ const message = shell === void 0 ? `completion needs a shell: ${SHELL_CHOICES}` : `unknown shell "${shell}": use ${SHELL_CHOICES}`;
2147
+ ctx.err(`error: ${message}`);
2148
+ ctx.report?.fail("SHADOW_UI_INVALID_SHELL", message);
2149
+ return 1;
2150
+ }
2151
+ const script = completionScript(shell, {
2152
+ commands: COMMANDS,
2153
+ flags: Object.keys(OPTIONS)
2154
+ });
2155
+ if (ctx.report !== void 0) {
2156
+ ctx.report.set("shell", shell);
2157
+ ctx.report.set("script", script);
2158
+ return 0;
2159
+ }
2160
+ for (const line of script.split("\n")) ctx.out(line);
2161
+ return 0;
2162
+ }
2163
+ default: {
2164
+ const message = `unknown command "${command ?? ""}"`;
2165
+ ctx.err(`error: ${message}`);
2166
+ ctx.err("Run `shadow-ui --help` for usage.");
2167
+ ctx.report?.fail("SHADOW_UI_UNKNOWN_COMMAND", message);
2168
+ return 1;
2169
+ }
2170
+ }
2171
+ }
2172
+ //#endregion
2173
+ //#region src/cli/registry-source.ts
2174
+ /**
2175
+ * Where the CLI reads the registry from. The registry ships inside the package, so nothing is
2176
+ * downloaded: `registry.json` plus the files under `registry/` and `styles/`.
2177
+ * @packageDocumentation
2178
+ */
2179
+ /**
2180
+ * Builds a source from an index and two reader functions (tests use in-memory ones).
2181
+ *
2182
+ * @param index The parsed index.
2183
+ * @param read Reads a package-relative file.
2184
+ * @returns The source. Only files named in the index, and the known stylesheets, can be read.
2185
+ * @example
2186
+ * const source = createRegistrySource(index, (path) => files[path]);
2187
+ * @since 1.0.0
2188
+ *
2189
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
2190
+ */
2191
+ function createRegistrySource(index, read) {
2192
+ const listed = new Set(index.items.flatMap((item) => item.files.map((file) => file.path)));
2193
+ const styles = /* @__PURE__ */ new Set([
2194
+ "theme-core.css",
2195
+ "animations.css",
2196
+ ...BASE_COLORS.map((base) => `themes/${base}.css`)
2197
+ ]);
2198
+ const fetch = (path) => {
2199
+ const text = read(path);
2200
+ if (text === void 0) throw new Error(`The registry file "${path}" is missing from the package.`);
2201
+ return text;
2202
+ };
2203
+ return {
2204
+ index,
2205
+ readFile(path) {
2206
+ if (!listed.has(path)) throw new Error(`"${path}" is not a registry file.`);
2207
+ return fetch(path);
2208
+ },
2209
+ readStyle(name) {
2210
+ if (!styles.has(name)) throw new Error(`"${name}" is not a registry stylesheet.`);
2211
+ return fetch(`styles/${name}`);
2212
+ }
2213
+ };
2214
+ }
2215
+ /** The package root: the nearest ancestor of this file with the `@shadow-js/ui` package.json. */
2216
+ function packageRoot() {
2217
+ let directory = dirname(fileURLToPath(import.meta.url));
2218
+ for (;;) {
2219
+ const manifest = join(directory, "package.json");
2220
+ if (existsSync(manifest)) {
2221
+ if (JSON.parse(readFileSync(manifest, "utf8")).name === "@shadow-js/ui") return directory;
2222
+ }
2223
+ const parent = dirname(directory);
2224
+ if (parent === directory) throw new Error("Cannot locate the @shadow-js/ui package.");
2225
+ directory = parent;
2226
+ }
2227
+ }
2228
+ /**
2229
+ * Opens the registry that ships with this package.
2230
+ *
2231
+ * @param root Package root override (for tests).
2232
+ * @returns The source.
2233
+ * @throws SchemaError when `registry.json` is malformed, or an Error when it or a registry file cannot be read.
2234
+ * @example
2235
+ * const registry = loadRegistrySource();
2236
+ * @since 1.0.0
2237
+ *
2238
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
2239
+ */
2240
+ function loadRegistrySource(root = packageRoot()) {
2241
+ return createRegistrySource(parseRegistryIndex(JSON.parse(readFileSync(join(root, "registry.json"), "utf8"))), (path) => {
2242
+ const file = join(root, path);
2243
+ return existsSync(file) ? readFileSync(file, "utf8") : void 0;
2244
+ });
2245
+ }
2246
+ //#endregion
2247
+ //#region src/cli/bin.ts
2248
+ /**
2249
+ * The `shadow-ui` executable: wires the real filesystem, terminal and package manager into
2250
+ * {@link main}. Kept tiny; everything testable lives in the other modules.
2251
+ */
2252
+ function realContext(cwd) {
2253
+ return {
2254
+ cwd: resolve(cwd ?? process.cwd()),
2255
+ fs: nodeFs(),
2256
+ registry: loadRegistrySource(),
2257
+ version: "1.0.0",
2258
+ out: (line) => process.stdout.write(`${line}\n`),
2259
+ err: (line) => process.stderr.write(`${line}\n`),
2260
+ async confirm(question) {
2261
+ if (!process.stdin.isTTY) return false;
2262
+ const prompt = createInterface({
2263
+ input: process.stdin,
2264
+ output: process.stdout
2265
+ });
2266
+ try {
2267
+ const answer = await prompt.question(`${question} (y/N) `);
2268
+ return /^y(es)?$/i.test(answer.trim());
2269
+ } finally {
2270
+ prompt.close();
2271
+ }
2272
+ },
2273
+ install: ({ cwd: directory, manager, packages, output }) => new Promise((done, reject) => {
2274
+ const args = manager === "npm" ? ["install", ...packages] : ["add", ...packages];
2275
+ const child = spawn(manager, args, {
2276
+ cwd: directory,
2277
+ stdio: output === "stderr" ? [
2278
+ "inherit",
2279
+ 2,
2280
+ 2
2281
+ ] : "inherit",
2282
+ shell: process.platform === "win32"
2283
+ });
2284
+ child.on("error", reject);
2285
+ child.on("close", (code) => code === 0 ? done() : reject(/* @__PURE__ */ new Error(`${manager} exited with code ${code}`)));
2286
+ })
2287
+ };
2288
+ }
2289
+ const argv = process.argv.slice(2);
2290
+ if (wantsJson(argv)) installCrashHandlers(argv);
2291
+ process.exitCode = await main(argv, realContext);
2292
+ //#endregion
2293
+ export {};