search-solar 0.1.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.
- package/README.md +77 -0
- package/dist/cli.js +168 -0
- package/dist/data/icons.json +17820 -0
- package/dist/scripts/parse-icons.js +38 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# search-solar
|
|
2
|
+
|
|
3
|
+
A dead simple CLI to search [Solar icons (`react-perf`)](https://www.npmjs.com/package/@solar-icons/react-perf). Built for agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g search-solar
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Recommended: install the skill as well.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx skills add bittere/search-solar
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `query <text>` — global search
|
|
20
|
+
Searches across name, category, and tags at once. Results ranked by relevance.
|
|
21
|
+
```bash
|
|
22
|
+
search-solar query chat
|
|
23
|
+
search-solar query "arrow left" --limit 5
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### `search` — filtered search
|
|
27
|
+
Filter by any combination of name, tag, and category (all filters are ANDed).
|
|
28
|
+
```bash
|
|
29
|
+
search-solar search -n chat
|
|
30
|
+
search-solar search -t chat
|
|
31
|
+
search-solar search -n chat -c messages
|
|
32
|
+
search-solar search --category messages --tag video
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### `list` — browse icons
|
|
36
|
+
```bash
|
|
37
|
+
search-solar list # all icons
|
|
38
|
+
search-solar list -c arrows # scoped to a category
|
|
39
|
+
search-solar list -c arrows --limit 20
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `categories` — list all categories
|
|
43
|
+
Useful before using `search -c` or `list -c`.
|
|
44
|
+
```bash
|
|
45
|
+
search-solar categories
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `tags` — list all tags
|
|
49
|
+
```bash
|
|
50
|
+
search-solar tags # all tags
|
|
51
|
+
search-solar tags --category arrows # tags within a category
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Output format
|
|
55
|
+
|
|
56
|
+
Every matched icon shows:
|
|
57
|
+
```
|
|
58
|
+
name: ChatRound
|
|
59
|
+
category: Messages
|
|
60
|
+
tags: chat, message, bubble, communication
|
|
61
|
+
import: import { ChatRound } from "@solar-icons/react-perf/<Broken|Outline|Linear|Bold|LineDuotone|BoldDuotone>"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Global options
|
|
65
|
+
|
|
66
|
+
| Flag | Description | Commands |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `--limit <n>` | Max results to return | `query`, `search`, `list` |
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
You will need to grab the Solar Icons list from their website ([https://solar-icons.vercel.app/](https://solar-icons.vercel.app/)). Open DevTools, do some digging around, find the list of all icons and their tags, categories, etc. Save it to a file, run the `parse` script on that file to generate `data/icons.json`.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run build # Build and copy data file
|
|
76
|
+
npm run parse # Parse icons data
|
|
77
|
+
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const dataPath = join(__dirname, "data", "icons.json");
|
|
8
|
+
const STYLES = ["Broken", "Outline", "Linear", "Bold", "LineDuotone", "BoldDuotone"];
|
|
9
|
+
// ── Output helpers ────────────────────────────────────────────────────────────
|
|
10
|
+
const SEP = "─".repeat(60);
|
|
11
|
+
function formatIcon(icon) {
|
|
12
|
+
return [
|
|
13
|
+
`name: ${icon.name}`,
|
|
14
|
+
`category: ${icon.category}`,
|
|
15
|
+
`tags: ${[...icon.tags, ...icon.categoryTags].join(", ")}`,
|
|
16
|
+
`import: import { ${icon.name} } from "@solar-icons/react-perf/<${STYLES.join("|")}>"`,
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
function printResults(results) {
|
|
20
|
+
if (results.length === 0) {
|
|
21
|
+
console.log("No icons found.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
console.log(`Found ${results.length} icon(s):\n`);
|
|
25
|
+
results.forEach((icon, i) => {
|
|
26
|
+
console.log(formatIcon(icon));
|
|
27
|
+
if (i < results.length - 1)
|
|
28
|
+
console.log(SEP);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function fail(message, exitCode = 1) {
|
|
32
|
+
console.error(`Error: ${message}`);
|
|
33
|
+
process.exit(exitCode);
|
|
34
|
+
}
|
|
35
|
+
function loadIcons() {
|
|
36
|
+
try {
|
|
37
|
+
const data = readFileSync(dataPath, "utf-8");
|
|
38
|
+
return JSON.parse(data);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
fail("Could not read data/icons.json — make sure the file exists.");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ── Search logic ──────────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Global text search across name, category, and all tags.
|
|
47
|
+
* Ranked: name match (3pts) > category match (2pts) > tag match (1pt).
|
|
48
|
+
*/
|
|
49
|
+
function globalSearch(icons, query) {
|
|
50
|
+
const q = query.toLowerCase();
|
|
51
|
+
return icons
|
|
52
|
+
.map((icon) => {
|
|
53
|
+
let score = 0;
|
|
54
|
+
if (icon.name.toLowerCase().includes(q))
|
|
55
|
+
score += 3;
|
|
56
|
+
if (icon.category.toLowerCase().includes(q))
|
|
57
|
+
score += 2;
|
|
58
|
+
const allTags = [...icon.tags, ...icon.categoryTags].map((t) => t.toLowerCase());
|
|
59
|
+
if (allTags.some((t) => t.includes(q)))
|
|
60
|
+
score += 1;
|
|
61
|
+
return { icon, score };
|
|
62
|
+
})
|
|
63
|
+
.filter(({ score }) => score > 0)
|
|
64
|
+
.sort((a, b) => b.score - a.score)
|
|
65
|
+
.map(({ icon }) => icon);
|
|
66
|
+
}
|
|
67
|
+
function filterIcons(icons, options) {
|
|
68
|
+
return icons.filter((icon) => {
|
|
69
|
+
if (options.name) {
|
|
70
|
+
if (!icon.name.toLowerCase().includes(options.name.toLowerCase()))
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (options.category) {
|
|
74
|
+
if (!icon.category.toLowerCase().includes(options.category.toLowerCase()))
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (options.tag) {
|
|
78
|
+
const q = options.tag.toLowerCase();
|
|
79
|
+
const allTags = [...icon.tags, ...icon.categoryTags].map((t) => t.toLowerCase());
|
|
80
|
+
if (!allTags.some((t) => t.includes(q)))
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// ── CLI ───────────────────────────────────────────────────────────────────────
|
|
87
|
+
const program = new Command();
|
|
88
|
+
program
|
|
89
|
+
.name("search-solar")
|
|
90
|
+
.description("CLI tool for searching Solar icons.")
|
|
91
|
+
.version("1.0.0");
|
|
92
|
+
// ── query (global text search) ────────────────────────────────────────────────
|
|
93
|
+
program
|
|
94
|
+
.command("query <text>")
|
|
95
|
+
.description("Global text search across name, category, and all tags.\n" +
|
|
96
|
+
"Results ranked: name match > category match > tag match.")
|
|
97
|
+
.option("--limit <n>", "Max results to return (default: 20)", parseInt)
|
|
98
|
+
.action((text, options) => {
|
|
99
|
+
if (!text?.trim())
|
|
100
|
+
fail("Query text must not be empty.");
|
|
101
|
+
const icons = loadIcons();
|
|
102
|
+
const limit = options.limit ?? 20;
|
|
103
|
+
const results = globalSearch(icons, text).slice(0, limit);
|
|
104
|
+
printResults(results);
|
|
105
|
+
});
|
|
106
|
+
// ── search (filtered) ─────────────────────────────────────────────────────────
|
|
107
|
+
program
|
|
108
|
+
.command("search")
|
|
109
|
+
.description("Search icons by name, tag, and/or category. All filters are ANDed.")
|
|
110
|
+
.option("-n, --name <n>", "Filter by icon name (substring match)")
|
|
111
|
+
.option("-t, --tag <tag>", "Filter by tag or categoryTag (substring match)")
|
|
112
|
+
.option("-c, --category <category>", "Filter by category (substring match)")
|
|
113
|
+
.option("--limit <n>", "Max results to return", parseInt)
|
|
114
|
+
.action((options) => {
|
|
115
|
+
if (!options.name && !options.tag && !options.category) {
|
|
116
|
+
fail("Provide at least one of --name, --tag, or --category.");
|
|
117
|
+
}
|
|
118
|
+
const icons = loadIcons();
|
|
119
|
+
let results = filterIcons(icons, options);
|
|
120
|
+
if (options.limit)
|
|
121
|
+
results = results.slice(0, options.limit);
|
|
122
|
+
printResults(results);
|
|
123
|
+
});
|
|
124
|
+
// ── list ──────────────────────────────────────────────────────────────────────
|
|
125
|
+
program
|
|
126
|
+
.command("list")
|
|
127
|
+
.description("List all icons, optionally filtered by category.")
|
|
128
|
+
.option("-c, --category <category>", "Filter by category (substring match)")
|
|
129
|
+
.option("--limit <n>", "Max results to return", parseInt)
|
|
130
|
+
.action((options) => {
|
|
131
|
+
const icons = loadIcons();
|
|
132
|
+
let results = options.category
|
|
133
|
+
? icons.filter((icon) => icon.category.toLowerCase().includes(options.category.toLowerCase()))
|
|
134
|
+
: icons;
|
|
135
|
+
if (options.limit)
|
|
136
|
+
results = results.slice(0, options.limit);
|
|
137
|
+
printResults(results);
|
|
138
|
+
});
|
|
139
|
+
// ── categories ────────────────────────────────────────────────────────────────
|
|
140
|
+
program
|
|
141
|
+
.command("categories")
|
|
142
|
+
.description("List all unique categories.")
|
|
143
|
+
.action(() => {
|
|
144
|
+
const icons = loadIcons();
|
|
145
|
+
const categories = [...new Set(icons.map((icon) => icon.category))].sort();
|
|
146
|
+
console.log(`Categories (${categories.length}):\n`);
|
|
147
|
+
categories.forEach((cat) => console.log(` ${cat}`));
|
|
148
|
+
});
|
|
149
|
+
// ── tags ──────────────────────────────────────────────────────────────────────
|
|
150
|
+
program
|
|
151
|
+
.command("tags")
|
|
152
|
+
.description("List all unique tags (icon tags + category tags combined).")
|
|
153
|
+
.option("--category <category>", "Limit to tags within a category")
|
|
154
|
+
.action((options) => {
|
|
155
|
+
const icons = loadIcons();
|
|
156
|
+
const source = options.category
|
|
157
|
+
? icons.filter((i) => i.category.toLowerCase().includes(options.category.toLowerCase()))
|
|
158
|
+
: icons;
|
|
159
|
+
const tags = new Set();
|
|
160
|
+
source.forEach((icon) => {
|
|
161
|
+
icon.tags.forEach((t) => tags.add(t));
|
|
162
|
+
icon.categoryTags.forEach((t) => tags.add(t));
|
|
163
|
+
});
|
|
164
|
+
const sorted = [...tags].sort();
|
|
165
|
+
console.log(`Tags (${sorted.length}):\n`);
|
|
166
|
+
sorted.forEach((tag) => console.log(` ${tag}`));
|
|
167
|
+
});
|
|
168
|
+
program.parse();
|