@kidd-cli/core 0.4.0 → 0.5.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.
- package/README.md +21 -5
- package/dist/{config-D8e5qxLp.js → config-BiEi8RG2.js} +2 -2
- package/dist/{config-D8e5qxLp.js.map → config-BiEi8RG2.js.map} +1 -1
- package/dist/{create-store-OHdkm_Yt.js → create-store-CGeHrTcl.js} +2 -2
- package/dist/{create-store-OHdkm_Yt.js.map → create-store-CGeHrTcl.js.map} +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -56
- package/dist/index.js.map +1 -1
- package/dist/lib/config.js +2 -2
- package/dist/lib/format.d.ts +73 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +20 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/logger.d.ts +1 -1
- package/dist/lib/logger.js +10 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/project.d.ts +1 -1
- package/dist/lib/project.js +1 -1
- package/dist/lib/store.d.ts +1 -1
- package/dist/lib/store.js +2 -2
- package/dist/{logger-9j49T5da.d.ts → logger-Bm-LRSeQ.d.ts} +17 -1
- package/dist/logger-Bm-LRSeQ.d.ts.map +1 -0
- package/dist/middleware/auth.d.ts +1 -1
- package/dist/middleware/auth.js +3 -3
- package/dist/middleware/http.d.ts +1 -1
- package/dist/middleware/http.js +1 -1
- package/dist/middleware/icons.d.ts +119 -0
- package/dist/middleware/icons.d.ts.map +1 -0
- package/dist/middleware/icons.js +824 -0
- package/dist/middleware/icons.js.map +1 -0
- package/dist/{middleware-BWnPSRWR.js → middleware-BewRXb2G.js} +1 -1
- package/dist/{middleware-BWnPSRWR.js.map → middleware-BewRXb2G.js.map} +1 -1
- package/dist/{project-D0g84bZY.js → project-CoWHMVc8.js} +1 -1
- package/dist/{project-D0g84bZY.js.map → project-CoWHMVc8.js.map} +1 -1
- package/dist/tally-ioa20iGw.js +220 -0
- package/dist/tally-ioa20iGw.js.map +1 -0
- package/dist/{types-D-BxshYM.d.ts → types-Boe_1EjY.d.ts} +1 -1
- package/dist/{types-D-BxshYM.d.ts.map → types-Boe_1EjY.d.ts.map} +1 -1
- package/dist/types-Cp8_uIil.d.ts +160 -0
- package/dist/types-Cp8_uIil.d.ts.map +1 -0
- package/dist/{types-U73X_oQ_.d.ts → types-s-yUj9Zj.d.ts} +47 -37
- package/dist/types-s-yUj9Zj.d.ts.map +1 -0
- package/package.json +14 -5
- package/dist/logger-9j49T5da.d.ts.map +0 -1
- package/dist/types-U73X_oQ_.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
|