@shadow-js/ui 0.0.0-stage → 1.0.1

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