@kidd-cli/core 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +23 -8
  2. package/dist/{config-D8e5qxLp.js → config-BiEi8RG2.js} +2 -2
  3. package/dist/{config-D8e5qxLp.js.map → config-BiEi8RG2.js.map} +1 -1
  4. package/dist/{create-store-OHdkm_Yt.js → create-store-CGeHrTcl.js} +2 -2
  5. package/dist/{create-store-OHdkm_Yt.js.map → create-store-CGeHrTcl.js.map} +1 -1
  6. package/dist/index.d.ts +8 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +265 -95
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/config.js +2 -2
  11. package/dist/lib/format.d.ts +73 -0
  12. package/dist/lib/format.d.ts.map +1 -0
  13. package/dist/lib/format.js +20 -0
  14. package/dist/lib/format.js.map +1 -0
  15. package/dist/lib/logger.d.ts +1 -1
  16. package/dist/lib/logger.js +10 -0
  17. package/dist/lib/logger.js.map +1 -1
  18. package/dist/lib/project.d.ts +1 -1
  19. package/dist/lib/project.js +1 -1
  20. package/dist/lib/store.d.ts +1 -1
  21. package/dist/lib/store.js +2 -2
  22. package/dist/{logger-9j49T5da.d.ts → logger-Bm-LRSeQ.d.ts} +17 -1
  23. package/dist/logger-Bm-LRSeQ.d.ts.map +1 -0
  24. package/dist/middleware/auth.d.ts +15 -3
  25. package/dist/middleware/auth.d.ts.map +1 -1
  26. package/dist/middleware/auth.js +48 -9
  27. package/dist/middleware/auth.js.map +1 -1
  28. package/dist/middleware/http.d.ts +1 -1
  29. package/dist/middleware/http.js +1 -1
  30. package/dist/middleware/icons.d.ts +119 -0
  31. package/dist/middleware/icons.d.ts.map +1 -0
  32. package/dist/middleware/icons.js +824 -0
  33. package/dist/middleware/icons.js.map +1 -0
  34. package/dist/{middleware-BWnPSRWR.js → middleware-BewRXb2G.js} +1 -1
  35. package/dist/{middleware-BWnPSRWR.js.map → middleware-BewRXb2G.js.map} +1 -1
  36. package/dist/{project-D0g84bZY.js → project-CoWHMVc8.js} +1 -1
  37. package/dist/{project-D0g84bZY.js.map → project-CoWHMVc8.js.map} +1 -1
  38. package/dist/tally-ioa20iGw.js +220 -0
  39. package/dist/tally-ioa20iGw.js.map +1 -0
  40. package/dist/{types-D-BxshYM.d.ts → types-Boe_1EjY.d.ts} +1 -1
  41. package/dist/{types-D-BxshYM.d.ts.map → types-Boe_1EjY.d.ts.map} +1 -1
  42. package/dist/types-Cp8_uIil.d.ts +160 -0
  43. package/dist/types-Cp8_uIil.d.ts.map +1 -0
  44. package/dist/{types-CTvrsrnD.d.ts → types-s-yUj9Zj.d.ts} +104 -44
  45. package/dist/types-s-yUj9Zj.d.ts.map +1 -0
  46. package/package.json +12 -3
  47. package/dist/logger-9j49T5da.d.ts.map +0 -1
  48. package/dist/types-CTvrsrnD.d.ts.map +0 -1
@@ -0,0 +1,824 @@
1
+ import { n as decorateContext, t as middleware } from "../middleware-BewRXb2G.js";
2
+ import { join } from "node:path";
3
+ import { attemptAsync, ok } from "@kidd-cli/utils/fp";
4
+ import { z } from "zod";
5
+ import { match as match$1 } from "ts-pattern";
6
+ import { mkdir, rm } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { exec } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import { getFonts } from "font-list";
11
+ //#region src/middleware/icons/definitions.ts
12
+ /**
13
+ * Predefined icon definitions organized by category.
14
+ *
15
+ * Each icon has a Nerd Font glyph and an emoji fallback. The middleware
16
+ * resolves to one or the other based on font detection.
17
+ *
18
+ * @module
19
+ */
20
+ /**
21
+ * Git-related icons for version control operations.
22
+ *
23
+ * Emoji values use Unicode escape sequences rather than literal emoji
24
+ * characters to avoid encoding issues across editors, terminals, and
25
+ * build tools that may not handle multi-byte characters correctly.
26
+ *
27
+ * Nerd Font sources: nf-dev (Devicons), nf-fa (Font Awesome)
28
+ */
29
+ const GIT_ICONS = Object.freeze({
30
+ branch: {
31
+ emoji: "🔀",
32
+ nerdFont: ""
33
+ },
34
+ clone: {
35
+ emoji: "📋",
36
+ nerdFont: ""
37
+ },
38
+ commit: {
39
+ emoji: "📝",
40
+ nerdFont: ""
41
+ },
42
+ compare: {
43
+ emoji: "🔄",
44
+ nerdFont: ""
45
+ },
46
+ fetch: {
47
+ emoji: "⬇️",
48
+ nerdFont: ""
49
+ },
50
+ fork: {
51
+ emoji: "🔀",
52
+ nerdFont: ""
53
+ },
54
+ git: {
55
+ emoji: "💻",
56
+ nerdFont: ""
57
+ },
58
+ merge: {
59
+ emoji: "🔀",
60
+ nerdFont: ""
61
+ },
62
+ pr: {
63
+ emoji: "📥",
64
+ nerdFont: ""
65
+ },
66
+ tag: {
67
+ emoji: "🏷️",
68
+ nerdFont: ""
69
+ },
70
+ worktree: {
71
+ emoji: "🌳",
72
+ nerdFont: ""
73
+ }
74
+ });
75
+ /**
76
+ * DevOps and infrastructure icons.
77
+ *
78
+ * Nerd Font sources: nf-dev (Devicons), nf-fa (Font Awesome)
79
+ */
80
+ const DEVOPS_ICONS = Object.freeze({
81
+ ci: {
82
+ emoji: "⚙️",
83
+ nerdFont: ""
84
+ },
85
+ cloud: {
86
+ emoji: "☁️",
87
+ nerdFont: ""
88
+ },
89
+ deploy: {
90
+ emoji: "🚀",
91
+ nerdFont: ""
92
+ },
93
+ docker: {
94
+ emoji: "🐳",
95
+ nerdFont: ""
96
+ },
97
+ kubernetes: {
98
+ emoji: "☸️",
99
+ nerdFont: ""
100
+ },
101
+ server: {
102
+ emoji: "🖥️",
103
+ nerdFont: ""
104
+ },
105
+ terminal: {
106
+ emoji: "💻",
107
+ nerdFont: ""
108
+ }
109
+ });
110
+ /**
111
+ * Status indicator icons.
112
+ *
113
+ * Nerd Font sources: nf-fa (Font Awesome)
114
+ */
115
+ const STATUS_ICONS = Object.freeze({
116
+ error: {
117
+ emoji: "❌",
118
+ nerdFont: ""
119
+ },
120
+ info: {
121
+ emoji: "ℹ️",
122
+ nerdFont: ""
123
+ },
124
+ pending: {
125
+ emoji: "⏳",
126
+ nerdFont: ""
127
+ },
128
+ running: {
129
+ emoji: "▶️",
130
+ nerdFont: ""
131
+ },
132
+ stopped: {
133
+ emoji: "⏹️",
134
+ nerdFont: ""
135
+ },
136
+ success: {
137
+ emoji: "✅",
138
+ nerdFont: ""
139
+ },
140
+ warning: {
141
+ emoji: "⚠️",
142
+ nerdFont: ""
143
+ }
144
+ });
145
+ /**
146
+ * File type and filesystem icons.
147
+ *
148
+ * Nerd Font sources: nf-fa (Font Awesome), nf-dev (Devicons)
149
+ */
150
+ const FILES_ICONS = Object.freeze({
151
+ config: {
152
+ emoji: "⚙️",
153
+ nerdFont: ""
154
+ },
155
+ file: {
156
+ emoji: "📄",
157
+ nerdFont: ""
158
+ },
159
+ folder: {
160
+ emoji: "📁",
161
+ nerdFont: ""
162
+ },
163
+ javascript: {
164
+ emoji: "📄",
165
+ nerdFont: ""
166
+ },
167
+ json: {
168
+ emoji: "📄",
169
+ nerdFont: ""
170
+ },
171
+ lock: {
172
+ emoji: "🔒",
173
+ nerdFont: ""
174
+ },
175
+ markdown: {
176
+ emoji: "📄",
177
+ nerdFont: ""
178
+ },
179
+ typescript: {
180
+ emoji: "📄",
181
+ nerdFont: ""
182
+ }
183
+ });
184
+ /**
185
+ * Merge all category icon records into a single definitions record.
186
+ *
187
+ * @returns A frozen record of all predefined icons.
188
+ */
189
+ function createDefaultIcons() {
190
+ return Object.freeze({
191
+ ...GIT_ICONS,
192
+ ...DEVOPS_ICONS,
193
+ ...STATUS_ICONS,
194
+ ...FILES_ICONS
195
+ });
196
+ }
197
+ /**
198
+ * Retrieve the icon definitions for a specific category.
199
+ *
200
+ * @param category - The icon category to retrieve.
201
+ * @returns The frozen record of icons for that category.
202
+ */
203
+ function getIconsByCategory(category) {
204
+ return match$1(category).with("git", () => GIT_ICONS).with("devops", () => DEVOPS_ICONS).with("status", () => STATUS_ICONS).with("files", () => FILES_ICONS).exhaustive();
205
+ }
206
+ //#endregion
207
+ //#region src/middleware/icons/install.ts
208
+ /**
209
+ * Nerd Font installation for macOS and Linux.
210
+ *
211
+ * Detects installed system fonts, matches them to available Nerd Font
212
+ * equivalents, and lets the user choose which to install. Supports
213
+ * Homebrew on macOS and direct download on Linux.
214
+ *
215
+ * All shell commands run asynchronously so the spinner can animate
216
+ * and ctrl+c remains responsive.
217
+ *
218
+ * @module
219
+ */
220
+ const execAsync = promisify(exec);
221
+ /**
222
+ * Zod schema for validating font names before shell interpolation.
223
+ *
224
+ * Restricts to alphanumeric characters and hyphens to prevent command injection.
225
+ *
226
+ * @private
227
+ */
228
+ const fontNameSchema = z.string().regex(/^[A-Za-z0-9-]+$/, "Font name must be alphanumeric or hyphen");
229
+ /**
230
+ * Maps base font family name patterns to their Nerd Font release names.
231
+ *
232
+ * Keys are lowercase patterns matched against installed font names.
233
+ * Values are the exact release archive names on GitHub.
234
+ *
235
+ * @private
236
+ */
237
+ const FONT_MAP = Object.freeze([
238
+ ["jetbrains mono", "JetBrainsMono"],
239
+ ["fira code", "FiraCode"],
240
+ ["fira mono", "FiraMono"],
241
+ ["cascadia code", "CascadiaCode"],
242
+ ["cascadia mono", "CascadiaMono"],
243
+ ["hack", "Hack"],
244
+ ["source code pro", "SourceCodePro"],
245
+ ["meslo", "Meslo"],
246
+ ["inconsolata", "Inconsolata"],
247
+ ["dejavu sans mono", "DejaVuSansMono"],
248
+ ["droid sans mono", "DroidSansMono"],
249
+ ["ubuntu mono", "UbuntuMono"],
250
+ ["ubuntu sans", "UbuntuSans"],
251
+ ["roboto mono", "RobotoMono"],
252
+ ["ibm plex mono", "IBMPlexMono"],
253
+ ["victor mono", "VictorMono"],
254
+ ["iosevka", "Iosevka"],
255
+ ["mononoki", "Mononoki"],
256
+ ["geist mono", "GeistMono"],
257
+ ["space mono", "SpaceMono"],
258
+ ["anonymous pro", "AnonymousPro"],
259
+ ["overpass", "Overpass"],
260
+ ["go mono", "Go-Mono"],
261
+ ["noto", "Noto"],
262
+ ["commit mono", "CommitMono"],
263
+ ["monaspace", "Monaspace"],
264
+ ["intel one mono", "IntelOneMono"],
265
+ ["zed mono", "ZedMono"],
266
+ ["comic shanns", "ComicShannsMono"],
267
+ ["lilex", "Lilex"],
268
+ ["recursive", "Recursive"],
269
+ ["hermit", "Hermit"],
270
+ ["hasklig", "Hasklig"],
271
+ ["martian mono", "MartianMono"],
272
+ ["0xproto", "0xProto"],
273
+ ["departure mono", "DepartureMono"],
274
+ ["atkinson hyperlegible", "AtkinsonHyperlegibleMono"]
275
+ ]);
276
+ /**
277
+ * Popular Nerd Fonts shown as fallback options when no matches are found.
278
+ *
279
+ * @private
280
+ */
281
+ const POPULAR_FONTS = Object.freeze([
282
+ "JetBrainsMono",
283
+ "FiraCode",
284
+ "Hack",
285
+ "CascadiaCode",
286
+ "Meslo",
287
+ "SourceCodePro",
288
+ "Iosevka",
289
+ "VictorMono"
290
+ ]);
291
+ /**
292
+ * Interactively install a Nerd Font on the user's system.
293
+ *
294
+ * Detects installed system fonts, matches them against available Nerd Font
295
+ * equivalents, and presents a selection prompt. If a `font` option is
296
+ * provided, skips detection and installs that font directly after confirmation.
297
+ *
298
+ * @param options - Installation options including context and font name.
299
+ * @returns A Result with true on success or an IconsError on failure.
300
+ */
301
+ async function installNerdFont(options) {
302
+ const { ctx, font } = options;
303
+ if (font !== void 0) {
304
+ const parsed = fontNameSchema.safeParse(font);
305
+ if (!parsed.success) return iconsError({
306
+ message: `Invalid font name: ${parsed.error.message}`,
307
+ type: "install_failed"
308
+ });
309
+ return installWithConfirmation({
310
+ ctx,
311
+ fontName: parsed.data
312
+ });
313
+ }
314
+ return installWithSelection(ctx);
315
+ }
316
+ /**
317
+ * Run the font selection flow: detect installed fonts, match to Nerd Fonts,
318
+ * and let the user pick.
319
+ *
320
+ * @private
321
+ * @param ctx - The icons context with prompts, spinner, and logger.
322
+ * @returns A Result with true on success or an IconsError on failure.
323
+ */
324
+ async function installWithSelection(ctx) {
325
+ ctx.spinner.start("Detecting installed fonts...");
326
+ const matches = await detectMatchingFonts();
327
+ ctx.spinner.stop("Font detection complete");
328
+ const choices = buildFontChoices(matches);
329
+ const selected = await ctx.prompts.select({
330
+ message: "Select a Nerd Font to install",
331
+ options: choices
332
+ });
333
+ if (selected === void 0 || typeof selected === "symbol") return ok(false);
334
+ const fontName = String(selected);
335
+ const parsed = fontNameSchema.safeParse(fontName);
336
+ if (!parsed.success) return iconsError({
337
+ message: `Invalid font name: ${parsed.error.message}`,
338
+ type: "install_failed"
339
+ });
340
+ const action = await ctx.prompts.select({
341
+ message: "How would you like to install?",
342
+ options: [{
343
+ label: "Auto install",
344
+ value: "auto"
345
+ }, {
346
+ label: "Show install commands",
347
+ value: "commands"
348
+ }]
349
+ });
350
+ if (action === void 0 || typeof action === "symbol") return ok(false);
351
+ return match$1(String(action)).with("auto", () => installFontWithSpinner({
352
+ ctx,
353
+ fontName: parsed.data
354
+ })).with("commands", () => showInstallCommands({
355
+ ctx,
356
+ fontName: parsed.data
357
+ })).otherwise(() => ok(false));
358
+ }
359
+ /**
360
+ * Confirm and install a specific font by name.
361
+ *
362
+ * @private
363
+ * @param params - The icons context and font name.
364
+ * @returns A Result with true on success or an IconsError on failure.
365
+ */
366
+ async function installWithConfirmation({ ctx, fontName }) {
367
+ if (!await ctx.prompts.confirm({ message: `Nerd Fonts not detected. Install ${fontName} Nerd Font?` })) return ok(false);
368
+ return installFontWithSpinner({
369
+ ctx,
370
+ fontName
371
+ });
372
+ }
373
+ /**
374
+ * Detect system fonts and match them to available Nerd Font equivalents.
375
+ *
376
+ * @private
377
+ * @returns An array of matched Nerd Font release names.
378
+ */
379
+ async function detectMatchingFonts() {
380
+ const [error, systemFonts] = await attemptAsync(() => getFonts({ disableQuoting: true }));
381
+ if (error || systemFonts === null) return [];
382
+ const lowerFonts = systemFonts.map((f) => f.toLowerCase());
383
+ return FONT_MAP.filter(([pattern]) => lowerFonts.some((f) => f.includes(pattern))).map(([, nerdName]) => nerdName);
384
+ }
385
+ /**
386
+ * Build the select prompt choices from matched and popular fonts.
387
+ *
388
+ * Matched fonts (based on what's installed) appear first with a hint,
389
+ * followed by popular alternatives that weren't already matched.
390
+ *
391
+ * @private
392
+ * @param matches - Nerd Font names matched from installed system fonts.
393
+ * @returns An array of select options.
394
+ */
395
+ function buildFontChoices(matches) {
396
+ const matchedSet = new Set(matches);
397
+ const matchedChoices = matches.map((name) => ({
398
+ hint: "detected on your system",
399
+ label: `${name} Nerd Font`,
400
+ value: name
401
+ }));
402
+ const popularChoices = POPULAR_FONTS.filter((name) => !matchedSet.has(name)).map((name) => ({
403
+ label: `${name} Nerd Font`,
404
+ value: name
405
+ }));
406
+ return [...matchedChoices, ...popularChoices];
407
+ }
408
+ /**
409
+ * Print the install commands for the user to run manually.
410
+ *
411
+ * @private
412
+ * @param params - The icons context and font name.
413
+ * @returns A Result with false since nothing was installed.
414
+ */
415
+ async function showInstallCommands({ ctx, fontName }) {
416
+ const slug = fontNameToSlug(fontName);
417
+ const url = `https://github.com/ryanoasis/nerd-fonts/releases/latest/download/${fontName}.zip`;
418
+ const fontDir = match$1(process.platform).with("darwin", () => join(homedir(), "Library", "Fonts")).otherwise(() => join(homedir(), ".local", "share", "fonts"));
419
+ const hasBrew = await checkBrewAvailable();
420
+ return ok(match$1(process.platform).with("darwin", () => match$1(hasBrew).with(true, () => [
421
+ "",
422
+ "Run the following command to install via Homebrew:",
423
+ "",
424
+ ` brew install --cask font-${slug}-nerd-font`,
425
+ ""
426
+ ]).with(false, () => [
427
+ "",
428
+ "Run the following commands to install manually:",
429
+ "",
430
+ ` curl -fsSL -o "${fontDir}/${fontName}.zip" "${url}"`,
431
+ ` unzip -o "${fontDir}/${fontName}.zip" -d "${fontDir}"`,
432
+ ` rm -f "${fontDir}/${fontName}.zip"`,
433
+ ""
434
+ ]).exhaustive()).with("linux", () => [
435
+ "",
436
+ "Run the following commands to install:",
437
+ "",
438
+ ` mkdir -p "${fontDir}"`,
439
+ ` curl -fsSL -o "${fontDir}/${fontName}.zip" "${url}"`,
440
+ ` unzip -o "${fontDir}/${fontName}.zip" -d "${fontDir}"`,
441
+ ` rm -f "${fontDir}/${fontName}.zip"`,
442
+ " fc-cache -fv",
443
+ ""
444
+ ]).otherwise(() => [
445
+ "",
446
+ `Download the font from: ${url}`,
447
+ ""
448
+ ]).reduce((_acc, line) => {
449
+ ctx.logger.info(line);
450
+ return false;
451
+ }, false));
452
+ }
453
+ /**
454
+ * Install a Nerd Font with spinner feedback.
455
+ *
456
+ * @private
457
+ * @param params - The icons context and font name.
458
+ * @returns A Result with true on success or an IconsError on failure.
459
+ */
460
+ async function installFontWithSpinner({ ctx, fontName }) {
461
+ ctx.spinner.start(`Installing ${fontName} Nerd Font...`);
462
+ const result = await installFont({
463
+ ctx,
464
+ fontName
465
+ });
466
+ const [error] = result;
467
+ if (error) {
468
+ ctx.spinner.stop(`Failed to install ${fontName} Nerd Font`);
469
+ return result;
470
+ }
471
+ ctx.spinner.stop(`${fontName} Nerd Font installed successfully`);
472
+ return result;
473
+ }
474
+ /**
475
+ * Install a Nerd Font by name, dispatching to the platform-appropriate method.
476
+ *
477
+ * @private
478
+ * @param params - The icons context and font name.
479
+ * @returns A Result with true on success or an IconsError on failure.
480
+ */
481
+ async function installFont({ ctx, fontName }) {
482
+ return match$1(process.platform).with("darwin", () => installDarwin({
483
+ ctx,
484
+ fontName
485
+ })).with("linux", () => installLinux({
486
+ ctx,
487
+ fontName
488
+ })).otherwise(() => Promise.resolve(iconsError({
489
+ message: `Unsupported platform: ${process.platform}`,
490
+ type: "install_failed"
491
+ })));
492
+ }
493
+ /**
494
+ * Install a Nerd Font on macOS via Homebrew or direct download.
495
+ *
496
+ * @private
497
+ * @param params - The icons context and font name.
498
+ * @returns A Result with true on success or an IconsError on failure.
499
+ */
500
+ async function installDarwin({ ctx, fontName }) {
501
+ const slug = fontNameToSlug(fontName);
502
+ if (await checkBrewAvailable()) return installViaBrew({
503
+ ctx,
504
+ slug
505
+ });
506
+ return installViaDownload({
507
+ ctx,
508
+ fontName
509
+ });
510
+ }
511
+ /**
512
+ * Install a Nerd Font on Linux via direct download.
513
+ *
514
+ * @private
515
+ * @param params - The icons context and font name.
516
+ * @returns A Result with true on success or an IconsError on failure.
517
+ */
518
+ async function installLinux({ ctx, fontName }) {
519
+ return installViaDownload({
520
+ ctx,
521
+ fontName
522
+ });
523
+ }
524
+ /**
525
+ * Check whether Homebrew is available on the system.
526
+ *
527
+ * @private
528
+ * @returns A promise resolving to true when the `brew` command is found.
529
+ */
530
+ async function checkBrewAvailable() {
531
+ const [error] = await attemptAsync(() => execAsync("command -v brew"));
532
+ return error === null;
533
+ }
534
+ /**
535
+ * Install a Nerd Font via Homebrew cask.
536
+ *
537
+ * @private
538
+ * @param params - The icons context and cask slug.
539
+ * @returns A Result with true on success or an IconsError on failure.
540
+ */
541
+ async function installViaBrew({ ctx, slug }) {
542
+ try {
543
+ ctx.spinner.message(`Installing font-${slug}-nerd-font via Homebrew...`);
544
+ await execAsync(`brew install --cask font-${slug}-nerd-font`);
545
+ return ok(true);
546
+ } catch {
547
+ return iconsError({
548
+ message: `Homebrew installation failed for font-${slug}-nerd-font`,
549
+ type: "install_failed"
550
+ });
551
+ }
552
+ }
553
+ /**
554
+ * Install a Nerd Font by downloading from GitHub releases.
555
+ *
556
+ * Downloads the zip archive, extracts it to the appropriate font directory,
557
+ * and refreshes the font cache on Linux.
558
+ *
559
+ * @private
560
+ * @param params - The icons context and font name.
561
+ * @returns A Result with true on success or an IconsError on failure.
562
+ */
563
+ async function installViaDownload({ ctx, fontName }) {
564
+ const fontDir = match$1(process.platform).with("darwin", () => join(homedir(), "Library", "Fonts")).otherwise(() => join(homedir(), ".local", "share", "fonts"));
565
+ try {
566
+ await mkdir(fontDir, { recursive: true });
567
+ const url = `https://github.com/ryanoasis/nerd-fonts/releases/latest/download/${fontName}.zip`;
568
+ const tmpZip = join(fontDir, `${fontName}.zip`);
569
+ ctx.spinner.message(`Downloading ${fontName} Nerd Font...`);
570
+ await execAsync(`curl -fsSL -o "${tmpZip}" "${url}"`, { timeout: 12e4 });
571
+ ctx.spinner.message(`Extracting ${fontName} Nerd Font...`);
572
+ await execAsync(`unzip -o "${tmpZip}" -d "${fontDir}"`);
573
+ await rm(tmpZip, { force: true });
574
+ if (process.platform === "linux") {
575
+ ctx.spinner.message("Refreshing font cache...");
576
+ await execAsync("fc-cache -fv");
577
+ }
578
+ return ok(true);
579
+ } catch {
580
+ return iconsError({
581
+ message: `Failed to download and install ${fontName} Nerd Font`,
582
+ type: "install_failed"
583
+ });
584
+ }
585
+ }
586
+ /**
587
+ * Canonical mapping of Nerd Font release names to Homebrew cask slugs.
588
+ *
589
+ * The generic regex-based conversion produces incorrect slugs for
590
+ * abbreviations (e.g. IBM, DejaVu) and compound names. This map
591
+ * provides the correct slugs for all fonts in {@link FONT_MAP}.
592
+ *
593
+ * @private
594
+ */
595
+ const BREW_SLUG_MAP = Object.freeze({
596
+ "0xProto": "0xproto",
597
+ AnonymousPro: "anonymous-pro",
598
+ AtkinsonHyperlegibleMono: "atkinson-hyperlegible-mono",
599
+ CascadiaCode: "cascadia-code",
600
+ CascadiaMono: "cascadia-mono",
601
+ ComicShannsMono: "comic-shanns-mono",
602
+ CommitMono: "commit-mono",
603
+ DejaVuSansMono: "dejavu-sans-mono",
604
+ DepartureMono: "departure-mono",
605
+ DroidSansMono: "droid-sans-mono",
606
+ FiraCode: "fira-code",
607
+ FiraMono: "fira-mono",
608
+ GeistMono: "geist-mono",
609
+ "Go-Mono": "go-mono",
610
+ Hack: "hack",
611
+ Hasklig: "hasklig",
612
+ Hermit: "hermit",
613
+ IBMPlexMono: "ibm-plex-mono",
614
+ Inconsolata: "inconsolata",
615
+ IntelOneMono: "intone-mono",
616
+ Iosevka: "iosevka",
617
+ JetBrainsMono: "jetbrains-mono",
618
+ Lilex: "lilex",
619
+ MartianMono: "martian-mono",
620
+ Meslo: "meslo-lg",
621
+ Monaspace: "monaspace",
622
+ Mononoki: "mononoki",
623
+ Noto: "noto",
624
+ Overpass: "overpass",
625
+ Recursive: "recursive",
626
+ RobotoMono: "roboto-mono",
627
+ SourceCodePro: "sauce-code-pro",
628
+ SpaceMono: "space-mono",
629
+ UbuntuMono: "ubuntu-mono",
630
+ UbuntuSans: "ubuntu-sans",
631
+ VictorMono: "victor-mono",
632
+ ZedMono: "zed-mono"
633
+ });
634
+ /**
635
+ * Convert a font family name to a Homebrew cask slug.
636
+ *
637
+ * Uses the canonical {@link BREW_SLUG_MAP} when available, falling
638
+ * back to a regex-based conversion for unknown font names.
639
+ *
640
+ * @private
641
+ * @param name - The font family name (e.g. 'JetBrainsMono').
642
+ * @returns The slug (e.g. 'jetbrains-mono').
643
+ */
644
+ function fontNameToSlug(name) {
645
+ const mapped = BREW_SLUG_MAP[name];
646
+ if (mapped !== void 0) return mapped;
647
+ return name.replaceAll(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
648
+ }
649
+ /**
650
+ * Construct a failure Result tuple with an {@link IconsError}.
651
+ *
652
+ * @private
653
+ * @param error - The icons error.
654
+ * @returns A synchronous Result tuple `[IconsError, null]`.
655
+ */
656
+ function iconsError(error) {
657
+ return [error, null];
658
+ }
659
+ //#endregion
660
+ //#region src/middleware/icons/context.ts
661
+ /**
662
+ * Create an {@link IconsContext} value for `ctx.icons`.
663
+ *
664
+ * The returned object exposes methods for resolving icons (`get`, `has`,
665
+ * `installed`, `setup`, `category`).
666
+ *
667
+ * @param options - Factory options.
668
+ * @returns An IconsContext instance.
669
+ */
670
+ function createIconsContext(options) {
671
+ const { ctx, icons, font, forceSetup } = options;
672
+ const state = { isInstalled: options.isInstalled };
673
+ return Object.freeze({
674
+ category: (cat) => {
675
+ const categoryIcons = getIconsByCategory(cat);
676
+ return Object.freeze(Object.fromEntries(Object.entries(categoryIcons).map(([name, def]) => [name, resolveIcon(icons, name, state.isInstalled, def)])));
677
+ },
678
+ get: (name) => resolveIcon(icons, name, state.isInstalled),
679
+ has: (name) => name in icons,
680
+ installed: () => match$1(forceSetup).with(true, () => false).otherwise(() => state.isInstalled),
681
+ setup: async () => {
682
+ const [error, result] = await installNerdFont({
683
+ ctx,
684
+ font
685
+ });
686
+ if (error) return [error, null];
687
+ if (result) state.isInstalled = true;
688
+ return [null, result];
689
+ }
690
+ });
691
+ }
692
+ /**
693
+ * Resolve a single icon to its appropriate glyph string.
694
+ *
695
+ * @private
696
+ * @param icons - The full icon definitions record.
697
+ * @param name - The icon name to resolve.
698
+ * @param nerdFontsInstalled - Whether Nerd Fonts are available.
699
+ * @param fallbackDef - Optional fallback definition (used for category resolution).
700
+ * @returns The resolved glyph string, or empty string if not found.
701
+ */
702
+ function resolveIcon(icons, name, nerdFontsInstalled, fallbackDef) {
703
+ const def = icons[name] ?? fallbackDef;
704
+ if (def === void 0) return "";
705
+ return match$1(nerdFontsInstalled).with(true, () => def.nerdFont).with(false, () => def.emoji).exhaustive();
706
+ }
707
+ //#endregion
708
+ //#region src/middleware/icons/detect.ts
709
+ /**
710
+ * Nerd Font detection using the `font-list` package.
711
+ *
712
+ * Queries the system font catalog and checks whether any installed
713
+ * font family name contains "Nerd".
714
+ *
715
+ * @module
716
+ */
717
+ /**
718
+ * Detect whether Nerd Fonts are installed on the system.
719
+ *
720
+ * Uses the `font-list` package to query installed font families and
721
+ * checks for any family name containing "Nerd".
722
+ *
723
+ * @returns A promise that resolves to true when at least one Nerd Font is found.
724
+ */
725
+ async function detectNerdFonts() {
726
+ const [error, fonts] = await attemptAsync(() => getFonts({ disableQuoting: true }));
727
+ if (error || fonts === null) return false;
728
+ return fonts.some((font) => /nerd/i.test(font));
729
+ }
730
+ //#endregion
731
+ //#region src/middleware/icons/icons.ts
732
+ /**
733
+ * Icons middleware factory.
734
+ *
735
+ * Detects Nerd Font availability, optionally prompts for installation,
736
+ * and decorates `ctx.icons` with an icon resolver.
737
+ *
738
+ * @module
739
+ */
740
+ /**
741
+ * Create an icons middleware that decorates `ctx.icons`.
742
+ *
743
+ * Detects whether Nerd Fonts are installed. When `autoSetup` is enabled
744
+ * and fonts are missing, prompts the user to install them. Merges any
745
+ * custom icon definitions with the built-in defaults.
746
+ *
747
+ * @param options - Optional middleware configuration.
748
+ * @returns A Middleware instance.
749
+ *
750
+ * @example
751
+ * ```ts
752
+ * import { icons } from '@kidd-cli/core/icons'
753
+ *
754
+ * cli({
755
+ * middleware: [
756
+ * icons({ autoSetup: true, font: 'JetBrainsMono' }),
757
+ * ],
758
+ * })
759
+ * ```
760
+ */
761
+ function icons(options) {
762
+ const resolved = resolveOptions(options);
763
+ const frozenIcons = Object.freeze({
764
+ ...createDefaultIcons(),
765
+ ...resolved.icons
766
+ });
767
+ return middleware(async (ctx, next) => {
768
+ const isInstalled = await resolveInstallStatus({
769
+ ctx,
770
+ isDetected: await detectNerdFonts(),
771
+ resolved
772
+ });
773
+ decorateContext(ctx, "icons", createIconsContext({
774
+ ctx,
775
+ font: resolved.font,
776
+ forceSetup: resolved.forceSetup,
777
+ icons: frozenIcons,
778
+ isInstalled
779
+ }));
780
+ return next();
781
+ });
782
+ }
783
+ /**
784
+ * Extract options into a resolved shape, avoiding optional chaining.
785
+ *
786
+ * @private
787
+ * @param options - Raw middleware options.
788
+ * @returns Resolved options with defaults applied.
789
+ */
790
+ function resolveOptions(options) {
791
+ if (options === void 0) return {
792
+ autoSetup: false,
793
+ font: void 0,
794
+ forceSetup: false,
795
+ icons: void 0
796
+ };
797
+ return {
798
+ autoSetup: options.autoSetup === true,
799
+ font: options.font,
800
+ forceSetup: options.forceSetup === true,
801
+ icons: options.icons
802
+ };
803
+ }
804
+ /**
805
+ * Determine final install status, triggering auto-setup if configured.
806
+ *
807
+ * @private
808
+ * @param params - Detection state, resolved options, and middleware context.
809
+ * @returns Whether Nerd Fonts should be considered installed.
810
+ */
811
+ async function resolveInstallStatus({ isDetected, resolved, ctx }) {
812
+ if (isDetected) return true;
813
+ if (!resolved.autoSetup) return false;
814
+ const [error, result] = await installNerdFont({
815
+ ctx,
816
+ font: resolved.font
817
+ });
818
+ if (error) ctx.logger.warn(`Auto-setup failed: ${error.message}`);
819
+ return result === true;
820
+ }
821
+ //#endregion
822
+ export { icons };
823
+
824
+ //# sourceMappingURL=icons.js.map