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 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();