Files
AstralRinth/packages/assets/build/add-icons.ts
Calum H. 1f21d66140 devex: add icon cmd (#4958)
* feat: icons add cmd

* fix: dep

* Update packages/assets/build/add-icons.ts

Signed-off-by: Calum H. <hendersoncal117@gmail.com>

* fix: lint

---------

Signed-off-by: Calum H. <hendersoncal117@gmail.com>
Signed-off-by: Calum H. <contact@cal.engineer>
2025-12-24 22:30:46 +00:00

214 lines
5.2 KiB
TypeScript

import fs from 'node:fs'
import path from 'node:path'
import readline from 'node:readline'
const packageRoot = path.resolve(__dirname, '..')
const iconsDir = path.join(packageRoot, 'icons')
const lucideIconsDir = path.join(packageRoot, 'node_modules/lucide-static/icons')
function listAvailableIcons(): string[] {
if (!fs.existsSync(lucideIconsDir)) {
return []
}
return fs
.readdirSync(lucideIconsDir)
.filter((file) => file.endsWith('.svg'))
.map((file) => path.basename(file, '.svg'))
.sort()
}
function paginateList(allIcons: string[], pageSize = 20): void {
let page = 0
let search = ''
let filteredIcons = allIcons
const getFilteredIcons = (): string[] => {
if (!search) return allIcons
return allIcons.filter((icon) => icon.includes(search))
}
const renderPage = (): void => {
console.clear()
filteredIcons = getFilteredIcons()
const totalPages = Math.max(1, Math.ceil(filteredIcons.length / pageSize))
if (page >= totalPages) page = Math.max(0, totalPages - 1)
const start = page * pageSize
const end = Math.min(start + pageSize, filteredIcons.length)
const pageIcons = filteredIcons.slice(start, end)
console.log(`\x1b[1mAvailable Lucide Icons\x1b[0m`)
console.log(`\x1b[2mSearch: \x1b[0m${search || '\x1b[2m(type to search)\x1b[0m'}\n`)
if (pageIcons.length === 0) {
console.log(` \x1b[2mNo icons found matching "${search}"\x1b[0m`)
} else {
pageIcons.forEach((icon) => {
if (search) {
const highlighted = icon.replace(search, `\x1b[33m${search}\x1b[0m`)
console.log(` ${highlighted}`)
} else {
console.log(` ${icon}`)
}
})
}
console.log(
`\n\x1b[2m${filteredIcons.length}/${allIcons.length} icons | Page ${page + 1}/${totalPages} | ← → navigate | :q quit\x1b[0m`,
)
}
renderPage()
readline.emitKeypressEvents(process.stdin)
if (process.stdin.isTTY) {
process.stdin.setRawMode(true)
}
process.stdin.on('keypress', (str, key) => {
if (key.ctrl && key.name === 'c') {
console.clear()
process.exit(0)
}
// :q to quit
if (search === ':' && key.name === 'q') {
console.clear()
process.exit(0)
}
// Navigation
if (key.name === 'right') {
const totalPages = Math.max(1, Math.ceil(filteredIcons.length / pageSize))
if (page < totalPages - 1) {
page++
renderPage()
}
return
}
if (key.name === 'left') {
if (page > 0) {
page--
renderPage()
}
return
}
// Backspace
if (key.name === 'backspace') {
search = search.slice(0, -1)
page = 0
renderPage()
return
}
// Escape to clear search
if (key.name === 'escape') {
search = ''
page = 0
renderPage()
return
}
// Type to search
if (str && str.length === 1 && !key.ctrl && !key.meta) {
search += str
page = 0
renderPage()
}
})
}
function addIcon(iconId: string, overwrite: boolean): boolean {
const sourcePath = path.join(lucideIconsDir, `${iconId}.svg`)
const targetPath = path.join(iconsDir, `${iconId}.svg`)
if (!fs.existsSync(sourcePath)) {
console.error(`❌ Icon "${iconId}" not found in lucide-static`)
console.error(` Run with --list to see available icons`)
return false
}
if (fs.existsSync(targetPath) && !overwrite) {
console.log(`⏭️ Skipping "${iconId}" (already exists, use --overwrite to replace)`)
return false
}
fs.copyFileSync(sourcePath, targetPath)
console.log(`✅ Added "${iconId}"`)
return true
}
function main(): void {
const args = process.argv.slice(2)
if (args.includes('--help') || args.includes('-h')) {
console.log(`
Usage: pnpm icons:add [options] <icon_id> [icon_id...]
Options:
--list, -l Browse all available Lucide icons (interactive)
--overwrite, -o Overwrite existing icons
--help, -h Show this help message
Examples:
pnpm icons:add heart star settings-2
pnpm icons:add --overwrite heart
pnpm icons:add --list # Interactive browser
pnpm icons:add --list | grep arrow # Pipe to grep
Interactive controls:
Type Search icons
← → Navigate pages
Escape Clear search
:q Quit
`)
process.exit(0)
}
if (args.includes('--list') || args.includes('-l')) {
const icons = listAvailableIcons()
if (icons.length === 0) {
console.error('❌ lucide-static not installed. Run pnpm install first.')
process.exit(1)
}
if (process.stdout.isTTY) {
paginateList(icons)
} else {
// Non-interactive mode (piped output)
icons.forEach((icon) => console.log(icon))
process.exit(0)
}
return
}
const overwrite = args.includes('--overwrite') || args.includes('-o')
const iconIds = args.filter((arg) => !arg.startsWith('-'))
if (iconIds.length === 0) {
console.error('Usage: pnpm icons:add <icon_id> [icon_id...]')
console.error('Example: pnpm icons:add heart star settings-2')
console.error('Run with --help for more options')
process.exit(1)
}
if (!fs.existsSync(lucideIconsDir)) {
console.error('❌ lucide-static not installed. Run pnpm install first.')
process.exit(1)
}
let added = 0
for (const iconId of iconIds) {
if (addIcon(iconId, overwrite)) {
added++
}
}
if (added > 0) {
console.log(`\n📦 Added ${added} icon(s). Run 'pnpm prepr:frontend:lib' to update exports.`)
}
}
main()