@keyline-icons/cli 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyline-icons/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Search Keyline Icons and copy them into your project from the terminal.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
package/src/index.mjs CHANGED
@@ -22,9 +22,9 @@ import { existsSync } from "node:fs"
22
22
  const data = JSON.parse(
23
23
  await readFile(new URL("../icons.json", import.meta.url), "utf8")
24
24
  )
25
- const { icons, styles } = data
25
+ const { icons, styles, keywords = {} } = data
26
26
  const NAMES = Object.keys(icons)
27
- const VERSION = "0.1.0"
27
+ const VERSION = "0.1.2"
28
28
 
29
29
  /** Colour only when a human is looking. Piped output stays clean. */
30
30
  const tty = process.stdout.isTTY
@@ -66,7 +66,15 @@ function parseArgs(argv) {
66
66
  const key = alias[k] ?? k
67
67
  if (inline !== undefined) flags[key] = inline
68
68
  else if (key === "help" || key === "version") flags[key] = true
69
- else if (argv[i + 1] && !argv[i + 1].startsWith("-"))
69
+ else if (
70
+ argv[i + 1] &&
71
+ // A bare negative number is a value, not a flag. Without this `-l -3`
72
+ // read `-3` as its own short flag, left `limit` set to `true`, and fell
73
+ // back to the default, so it showed 25 results while `--limit=-3`
74
+ // clamped to 1. No name in the set starts with a digit, so nothing else
75
+ // can be caught by it.
76
+ (!argv[i + 1].startsWith("-") || /^-\d+(\.\d+)?$/.test(argv[i + 1]))
77
+ )
70
78
  flags[key] = argv[++i]
71
79
  else flags[key] = true
72
80
  } else positional.push(a)
@@ -94,7 +102,8 @@ function svgFor(name, style) {
94
102
 
95
103
  /** Kept in step with `@keyline-icons/mcp`, which carries the reasoning. */
96
104
  function wordsOf(query) {
97
- const identifier = /[a-z][A-Z]/.test(query)
105
+ const identifier =
106
+ /[a-z][A-Z]/.test(query) || /^[A-Z][A-Za-z]*\d+$/.test(query)
98
107
  const split = identifier
99
108
  ? query
100
109
  .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
@@ -112,6 +121,27 @@ function wordsOf(query) {
112
121
  * for "check-circle" when the icon is `circle-check`. Kept in step with the
113
122
  * copy in `@keyline-icons/mcp`, which carries the full reasoning.
114
123
  */
124
+ /**
125
+ * The words a name can be found by: the name itself, plus whatever is written
126
+ * about it.
127
+ *
128
+ * Keywords are keyed by base name, because one component set in Figma covers
129
+ * all three containers, so `square-arrow-down` reads `arrow-down`'s words. The
130
+ * prefix only counts when that base exists: `circle-half` is a shape in its own
131
+ * right and there is no `half` for it to contain.
132
+ *
133
+ * Searched as one string rather than as a separate tier, which is what lets a
134
+ * query take a word from each: "help circle" finds `circle-question` because
135
+ * `circle` is in the name and `help` is in the words. Matching the whole query
136
+ * against one keyword, which is what this used to do, could never do that.
137
+ */
138
+ function keywordsFor(name) {
139
+ const m = /^(square|circle)-(.+)$/.exec(name)
140
+ return keywords[m && icons[m[2]] ? m[2] : name] || []
141
+ }
142
+
143
+ const haystackFor = (name) => `${name} ${keywordsFor(name).join(" ")}`
144
+
115
145
  function search(query, style, limit) {
116
146
  const q = query.toLowerCase().trim()
117
147
  const words = wordsOf(query)
@@ -120,13 +150,17 @@ function search(query, style, limit) {
120
150
  return NAMES.filter(
121
151
  (n) =>
122
152
  (!style || icons[n][style]) &&
123
- (n.includes(q) || words.every((w) => n.includes(w)))
153
+ (n.includes(q) ||
154
+ words.every((w) => n.includes(w)) ||
155
+ words.every((w) => haystackFor(n).includes(w)))
124
156
  )
125
157
  .map((n) => {
126
158
  const i = n.indexOf(q)
127
159
  const rank =
128
160
  i === -1
129
- ? 4
161
+ ? words.every((w) => n.includes(w))
162
+ ? 4
163
+ : 5
130
164
  : n === q
131
165
  ? 0
132
166
  : n.startsWith(q)
@@ -144,8 +178,16 @@ function search(query, style, limit) {
144
178
  .map((h) => h.n)
145
179
  }
146
180
 
181
+ /**
182
+ * The longest query worth answering. Kept in step with `@keyline-icons/mcp`,
183
+ * which carries the reasoning: `nearest` costs O(query x name) per name, so the
184
+ * caller sets the price, and a 200KB query took ten seconds here too.
185
+ */
186
+ const MAX_QUERY = 200
187
+
147
188
  /** Only reached when a name misses, so it can afford to walk every name. */
148
189
  function nearest(name) {
190
+ if (name.length > MAX_QUERY) return []
149
191
  const q = name.toLowerCase()
150
192
  const hit = search(q, null, 5)
151
193
  if (hit.length) return hit
@@ -236,11 +278,18 @@ const commands = {
236
278
  search({ flags, positional }) {
237
279
  const query = positional.join(" ")
238
280
  if (!query) die("search needs a query. Try: keyline-icons search arrow")
281
+ if (query.length > MAX_QUERY)
282
+ die(
283
+ `that query is ${query.length} characters. The longest name in the set ` +
284
+ `is under 40, so anything over ${MAX_QUERY} is refused.`
285
+ )
239
286
  const style = flags.style ? styleOf(flags) : null
240
- const limit = Math.min(
241
- Math.max(parseInt(flags.limit ?? "25", 10) || 25, 1),
242
- 200
243
- )
287
+ /* `|| 25` turned every falsy limit into the default, which swallowed `0`
288
+ along with the genuinely unusable ones: `--limit=0` should clamp up to 1,
289
+ the way `--limit=-3` does, rather than quietly show 25. Only a value that
290
+ is not a number at all earns the default. */
291
+ const raw = parseInt(flags.limit ?? "25", 10)
292
+ const limit = Math.min(Math.max(Number.isNaN(raw) ? 25 : raw, 1), 200)
244
293
  const hits = search(query, style, limit)
245
294
  if (!hits.length) {
246
295
  const near = nearest(query)
@@ -298,7 +347,10 @@ if (flags.version) {
298
347
  }
299
348
  if (flags.help || !positional.length) {
300
349
  commands.help()
301
- process.exit(positional.length ? 0 : 1)
350
+ // Both print the same text and only one of them is a failure. Asking for help
351
+ // succeeded; running with no command did not. `--help` exiting non-zero stops
352
+ // any `set -e` script that offers its own usage line.
353
+ process.exit(flags.help ? 0 : 1)
302
354
  }
303
355
 
304
356
  const name = positional.shift()