@nexusbloom/cli 0.3.4 → 0.9.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 CHANGED
@@ -11,23 +11,33 @@ npm install -g @nexusbloom/cli
11
11
  ## Quick Start
12
12
 
13
13
  ```bash
14
- # List available tools (interactive pagination, 10 per page)
14
+ # List available tools (interactive pagination, 20 per page)
15
15
  nxb list
16
16
 
17
- # Search with pagination
17
+ # Search with pagination — every long list has a search option
18
18
  nxb list -s ssl
19
+ nxb search "ssl"
20
+ nxb abbr -s ssl
21
+ nxb cache list -s json
22
+
23
+ # Bare `nxb run` shows the tool list (and prompts to search when large)
24
+ nxb run
25
+ nxb run -s gradient
26
+
27
+ # Build a pipeline interactively, wiring against real tool output
28
+ nxb pipe --build
29
+
30
+ # Check whether a tool's declared output_schema matches what it returns
31
+ nxb doctor data-generator
32
+
33
+ # Exits non-zero on drift, so it works as a CI gate
34
+ nxb doctor data-generator || exit 1
19
35
 
20
36
  # Non-interactive mode (single page dump)
21
37
  nxb list --no-interactive
22
38
 
23
39
  # JSON output (all commands)
24
40
  nxb --json list
25
-
26
- # Show tool abbreviations (paginated)
27
- nxb abbr
28
-
29
- # Search tools
30
- nxb search "ssl"
31
41
  ```
32
42
 
33
43
  ## Running Tools
@@ -48,6 +58,9 @@ nxb run ssl-certificate-checker domain=example.com --local
48
58
  # Interactive runner (select tool → pick inputs → auto-build command)
49
59
  nxb run
50
60
 
61
+ # Narrow the picker first
62
+ nxb run -s gradient
63
+
51
64
  # Pipe stdin through a tool
52
65
  echo '{"domain":"example.com"}' | nxb transform ssl-certificate-checker
53
66
  ```
@@ -61,53 +74,175 @@ nxb info ssl-certificate-checker
61
74
  # Show abbreviations (useful for quick tool references)
62
75
  nxb abbr
63
76
  nxb abbr ip # Filter abbreviations by query
77
+ nxb abbr -s ip # Same, explicit flag
64
78
 
65
79
  # Interactive pagination for lists
66
80
  nxb list # Browse pages with prev / next / done
67
- nxb list -s validate # Filtered + paginated
81
+ nxb list -s validate # Filtered + paginated
82
+ nxb list --refresh # Bypass cache, re-fetch the catalogue
83
+ nxb list --offline # Cache only, no network
68
84
 
69
- # Search (full-text across slugs, names, descriptions, tags)
85
+ # Search (full-text across slugs, names, descriptions, categories, tags)
70
86
  nxb search "validate"
71
87
  ```
72
88
 
89
+ Search matches case-insensitively across slug, name, description, category and
90
+ tags. Multi-word queries narrow the result set — `nxb search "json format"`
91
+ matches tools mentioning **both** words.
92
+
73
93
  ## Configuration
74
94
 
75
95
  ```bash
76
- # Set config values
77
- nxb config set key YOUR_KEY # Set an API key
78
- nxb config set default-tool ssl-scanner # Set default tool
96
+ # Set config values (secrets are stored but never echoed back)
97
+ nxb config set key YOUR_KEY
98
+ nxb config set supabaseKey YOUR_SUPABASE_ANON_KEY
99
+ nxb config set telemetry false
100
+
101
+ # Remove a stored value (e.g. rotate away an old API key)
102
+ nxb config unset key
79
103
 
80
- # Get config values
81
- nxb config get key # Get specific value
82
- nxb config get # List all config values
104
+ # Get config values — secret values are always redacted
105
+ nxb config get
106
+ nxb config get key
83
107
 
84
108
  # Environment variables (alternative to config)
85
109
  # NEXUSBLOOM_API_KEY — API key (can also use `--key`)
86
110
  # NEXUSBLOOM_API_URL — API base URL (default: https://nexusbloom.dev)
87
111
  # NEXUSBLOOM_SUPABASE_URL — Supabase URL (for fallback fetching)
88
- # NEXUSBLOOM_SUPABASE_ANON_KEY — Supabase anon key (for fallback fetching)
112
+ # NEXUSBLOOM_SUPABASE_ANON_KEY — Supabase anon key (enables the fallback fetcher)
113
+ # NEXUSBLOOM_TELEMETRY — set to `false` to disable the run counter
114
+ # NXB_NO_INTERACTIVE — force non-interactive output
89
115
  ```
90
116
 
91
- ## Local Tool Cache
117
+ No credentials are bundled with the CLI. The Supabase fallback stays disabled
118
+ until you supply your own key.
119
+
120
+ ## Pipelines
121
+
122
+ Chain tools by piping one tool's output into the next tool's input.
92
123
 
93
124
  ```bash
94
- # List cached tools
95
- nxb cache list
125
+ # Frequency -> how many rows to generate -> encode the first one
126
+ nxb pipe 'frequency-analyzer text=@in | data-generator count=$total_words format=json | base64-tool text=$data|first.id|str action=encode'
127
+
128
+ # Read the pipeline's input from stdin
129
+ echo "some text to analyse" | nxb pipe 'frequency-analyzer text=@in | base64-tool text=$total_words|str action=encode'
130
+
131
+ # Check the wiring and types without running anything
132
+ nxb pipe --dry-run 'frequency-analyzer text=@in | data-generator count=$word_frequency format=json'
96
133
 
97
- # Clear the local tool cache
98
- nxb cache clear
134
+ # List the available transforms
135
+ nxb pipe --transforms
99
136
  ```
100
137
 
101
- ## Scaffolding
138
+ ### Connections
139
+
140
+ | Form | Meaning |
141
+ |------|---------|
142
+ | `key=value` | literal |
143
+ | `key=@in` | the pipeline's standard input |
144
+ | `key=$field` | a field from the previous step |
145
+ | `key=$` | the whole previous output |
146
+ | `key=$data[0].email` | array index + nested field |
147
+ | `key=$text|len` | apply a transform |
148
+ | `key=$max|int(0)` | transform with an argument |
149
+ | `key=$data|first.email` | transform, then keep going into the result |
150
+ | `key=$missing|default(7)` | fall back when the path is absent |
151
+
152
+ A step separator is written with surrounding whitespace (` | `); a `|` inside a
153
+ `key=$...` token is always a transform.
154
+
155
+ ### Type safety
156
+
157
+ Conversions that cannot lose information happen silently. Conversions that can
158
+ lose information require an explicit transform:
159
+
160
+ | From → To | Behaviour |
161
+ |-----------|-----------|
162
+ | `integer → number` | automatic (widening) |
163
+ | `number → integer` | requires `\|int()` |
164
+ | `integer/number/boolean → string` | automatic |
165
+ | `string → integer/number/boolean` | requires a cast |
166
+ | `array → scalar` | requires `\|first()`, `\|last()`, `\|get(i)` |
167
+ | `* → enum` | validated against the allowed values |
168
+
169
+ The wiring is checked **before** anything runs, so a broken pipe is reported
170
+ before any side effects:
102
171
 
103
172
  ```bash
104
- # Interactive tool creation
105
- nxb create
173
+ $ nxb pipe 'frequency-analyzer text=hi | data-generator count=$word_frequency format=json'
174
+ ✗ step 2: frequency-analyzer.word_frequency is array but data-generator.count
175
+ wants integer — add |first()
176
+ ```
177
+
178
+ Everything is re-checked against real values at runtime, because declared
179
+ schemas can be wrong. `nxb doctor` finds that drift ahead of time:
106
180
 
107
- # Non-interactive tool creation
108
- nxb create --slug my-tool --name "My Tool" --desc "Does something useful"
181
+ ```bash
182
+ $ nxb doctor some-tool
183
+ ! undeclared output fields: summary, meta
184
+ ✗ score: declared integer, actually returns string
109
185
  ```
110
186
 
187
+ ### Interactive builder
188
+
189
+ `nxb pipe --build` walks you through building a pipeline. It runs each tool as
190
+ you go and lists **what it actually returned** — including fields its schema
191
+ never mentioned — before asking what to send onward. A browser node builder can
192
+ only trust `output_schema`; this can execute the tool and look.
193
+
194
+ ```bash
195
+ $ nxb pipe --build
196
+ Step 1
197
+ Tool › frequency-analyzer
198
+ text hello world
199
+ mode words
200
+ ✓ ran frequency-analyzer · 12ms
201
+ Output
202
+ total_words integer 2
203
+ character_frequency array [9]
204
+ Send this output into another tool? yes
205
+ Step 2
206
+ frequency-analyzer → count integer
207
+ ✔ data_generator... array → integer needs a transform
208
+ · set by hand
209
+ frequency-analyzer.total_words integer 2
210
+ ```
211
+
212
+ ### Saved pipelines
213
+
214
+ ```bash
215
+ nxb pipeline list
216
+ nxb pipeline run my-pipeline
217
+ nxb pipeline delete my-pipeline
218
+ ```
219
+
220
+ Pipelines are JSON files in `~/.config/nexusbloom/pipelines/` — plain text, so
221
+ they diff, sync and live in git. No database, no migrations.
222
+
223
+ ## Local Tool Cache
224
+
225
+ Tool catalogues and downloaded tool source are cached on disk so `list`,
226
+ `abbr` and `--local` runs work offline and stay fast.
227
+
228
+ ```bash
229
+ nxb cache path # show exactly where config + cache live
230
+ nxb cache list # list cached tools (-s to filter)
231
+ nxb cache clear # delete everything, including the legacy cache location
232
+ ```
233
+
234
+ Locations follow the XDG spec:
235
+
236
+ | What | Where |
237
+ |------|-------|
238
+ | Config (contains your API key) | `$XDG_CONFIG_HOME/nexusbloom/config.json` — mode `0600`, dir `0700` |
239
+ | Tool catalogue | `$XDG_CACHE_HOME/nexusbloom/tools-list.json` |
240
+ | Tool source | `$XDG_CACHE_HOME/nexusbloom/tools/<slug>.json` |
241
+
242
+ Earlier versions cached inside the installed package directory, which meant the
243
+ cache was wiped by every `npm update -g` and could fail on root-owned global
244
+ installs. `nxb cache clear` removes that leftover directory too.
245
+
111
246
  ## JSON Input Generation
112
247
 
113
248
  ```bash
@@ -126,23 +261,98 @@ nxb json --template data-generator -s count=3 | nxb run data-generator --input @
126
261
 
127
262
  ## Options
128
263
 
129
- | Flag | Description |
130
- |------|-------------|
131
- | `--json` | Output results in JSON format (global flag, place before subcommand) |
132
- | `--no-interactive` | Disable interactive pagination for `list`/`abbr` |
264
+ | Flag | Commands | Description |
265
+ |------|----------|-------------|
266
+ | `--json` | all | Output results in JSON (global — place before the subcommand) |
267
+ | `-s, --search <query>` | `list`, `run`, `abbr` | Filter a list by name, slug, category or tags |
268
+ | `--no-interactive` | `list`, `search`, `abbr` | Disable interactive pagination |
269
+ | `-r, --refresh` | `list` | Bypass the catalogue cache and re-fetch |
270
+ | `--offline` | `list` | Use only cached data, never the network |
271
+ | `-k, --key <key>` | `run`, `transform` | API key (or `NEXUSBLOOM_API_KEY`) |
272
+ | `-i, --input <json>` | `run` | Inline JSON input, or `@-` for stdin |
273
+ | `-f, --file <path>` | `run` | Read input from a JSON file |
274
+ | `--local` | `run`, `transform` | Execute `coreLogic` on this machine |
275
+ | `--dry-run` | `pipe`, `pipeline run` | Check wiring and types without running |
276
+ | `--remote` | `pipe`, `pipeline run` | Run every step against the API |
277
+ | `--transforms` | `pipe` | List the available connection transforms |
278
+ | `--build` | `pipe` | Build a pipeline interactively |
279
+ | `-t, --template <slug>` | `json` | Generate a JSON template for a tool |
280
+ | `-i, --interactive` | `json` | Prompt for each field |
281
+ | `-s, --set <key=value>` | `json` | Set specific field values |
282
+ | `-a, --add <json>` | `json` | Merge additional JSON into the output |
283
+
284
+ Subcommands: `config list|set|get|unset`, `cache path|clear`,
285
+ `pipeline list|run|delete`.
286
+
287
+ Exit codes are meaningful for scripting: `0` success, `1` anything the CLI
288
+ diagnosed — unknown tool, broken wiring, type mismatch, empty catalogue, or an
289
+ unreachable API. `--json` puts machine-readable results on stdout and keeps
290
+ progress, warnings and errors on stderr, so `nxb --json list | jq` is always
291
+ safe.
133
292
 
134
293
  ## Interactive Mode Features
135
294
 
136
- - **Paginated browsing**: `nxb list` and `nxb abbr` show 10 items per page with prev/next navigation
295
+ - **Paginated browsing**: `nxb list` and `nxb abbr` show 20 items per page with prev/next navigation
296
+ - **Search-first**: any list long enough to page through offers `-s/--search`
137
297
  - **Interactive runner**: `nxb run` without a slug opens a tool selector, then prompts for each input field, then builds and executes the command
138
298
  - **Abbreviation resolution**: Tool slugs can be typed using auto-generated abbreviations (e.g. `scc` for `ssl-certificate-checker`)
299
+ - **Script-safe**: when stdout/stdin is piped (or `CI` is set), prompts are never opened. `nxb run` prints a searchable tool list and exits instead of hanging
139
300
 
140
301
  ## Local Execution
141
302
 
142
- The `--local` flag fetches the tool's v2 source code from the NexusBloom API, caches it locally, and executes the `coreLogic()` function directly on your machine. This is useful for:
303
+ The `--local` flag fetches the tool's v2 source code from the NexusBloom API,
304
+ caches it locally, and executes the `coreLogic()` function directly on your
305
+ machine. This is useful for:
143
306
 
144
307
  - Offline use (after initial fetch)
145
308
  - Faster repeated execution (cached)
146
309
  - Privacy-sensitive inputs (data stays on your machine)
147
310
 
148
- Use `nxb cache clear` to clear the local source cache.
311
+ ****This executes JavaScript downloaded from the network.** Tool code runs in a
312
+ disposable child process with a hard `SIGKILL` deadline, so a tool that loops
313
+ forever, allocates without bound, or calls `process.exit()` cannot take the CLI
314
+ with it. Run `nxb cache clear` to remove downloaded source and stop trusting it.
315
+
316
+ Note that this is isolation, not a security sandbox — the child runs with your
317
+ user's permissions. Prefer remote execution for tools you do not trust.
318
+
319
+ A tool's `coreLogic` must be **self-contained**: local execution strips module
320
+ syntax and evaluates the function in isolation, so `import` statements are
321
+ removed rather than resolved. Relative imports like `import { helper } from
322
+ "./util"` will fail at runtime. `export async function coreLogic`,
323
+ `export const coreLogic = async () => {}` and plain `function coreLogic` are all
324
+ supported.
325
+
326
+ ## Development
327
+
328
+ ```bash
329
+ npm test # 500+ tests, no network required
330
+ npm run test:coverage # with a coverage report
331
+ npm run test:watch # re-run on change
332
+ ```
333
+
334
+ The suite runs fully offline and is isolated from your real config, API key and
335
+ network — both properties are enforced by guard tests, not assumed. See
336
+ [TESTING.md](./TESTING.md) for the harness and its gotchas.
337
+
338
+ ## Security Notes
339
+
340
+ - No API keys or Supabase keys are shipped inside the package.
341
+ - Manifest text fetched from the network is **parsed, never evaluated**; the
342
+ literal parser rejects anything that is not plain data.
343
+ - Tool code runs in a child process with a hard timeout, a memory cap, and
344
+ captured stdio, so it cannot hang, OOM or corrupt the CLI's output.
345
+ - Pipeline slugs are validated before being used as cache filenames.
346
+ - `config.json` is written with mode `0600`; secret values are redacted by
347
+ `nxb config get` and never echoed by `nxb config set` or `config unset`.
348
+ - `nxb config unset` exists so a rotated API key can actually be removed.
349
+ - `nxb transform` writes the payload to stdout exactly once and nothing else, so
350
+ `nxb transform <tool> | jq` is safe.
351
+ - Tool slugs are validated against a strict pattern before being used as cache
352
+ filenames or query strings, which also blocks PostgREST filter injection.
353
+ - Run telemetry sends only the tool slug, is bounded by a 1.5s abort timeout, and
354
+ is skipped entirely when disabled.
355
+ - Test isolation is itself guarded: the suite cannot read your real config or
356
+ spend your API key, and canary tests fail if a secret is ever printed.
357
+ - The run counter sends only the tool slug. Disable it with
358
+ `nxb config set telemetry false`.
package/TESTING.md ADDED
@@ -0,0 +1,125 @@
1
+ # Testing the CLI
2
+
3
+ The test suite runs entirely offline and never touches a developer's real
4
+ config, credentials or network. Both of those properties are enforced, not
5
+ assumed — see [Leak guards](#leak-guards).
6
+
7
+ ```bash
8
+ npm test # run everything
9
+ npm run test:coverage # run everything with a coverage report
10
+ npm run test:watch # re-run on change
11
+ node --test --import ./test/setup.mjs test/pipe.test.js # one file
12
+ ```
13
+
14
+ ## The isolation harness
15
+
16
+ Every test file is launched with `--import ./test/setup.mjs`. That module runs
17
+ **before any `src/` file is loaded**, which is the only moment it can work:
18
+ `paths.js` and `API_BASE` are evaluated at import time, so isolation set up
19
+ inside a test file would already be too late.
20
+
21
+ `test/setup.mjs` gives each test file:
22
+
23
+ | | |
24
+ |---|---|
25
+ | `XDG_CONFIG_HOME` / `XDG_CACHE_HOME` | a fresh `mkdtemp` directory, so tests read and write a throwaway config instead of yours |
26
+ | `HOME` | masked, so any fallback path cannot reach your real home |
27
+ | `NEXUSBLOOM_API_URL` | `https://api.nxb.invalid` — `.invalid` is reserved by RFC 2606, so a request that escapes a stub fails instead of reaching production |
28
+ | `NEXUSBLOOM_API_KEY`, `NEXUSBLOOM_SUPABASE_ANON_KEY` | deleted, so a test can never spend your key |
29
+ | `NXB_NO_INTERACTIVE`, `CI` | set, so no prompt can block a run |
30
+ | `NEXUSBLOOM_TELEMETRY` | deleted, so no test can phone home |
31
+ | `process.stdin` | replaced with a double that is **not** a TTY but does emit `end` |
32
+
33
+ That last row is subtle and load-bearing. `readStdin()` resolves on `end`, or
34
+ immediately when stdin is a TTY. Under `node --test` stdin is a pipe that is
35
+ never closed, so `end` never arrives and any test reaching `readStdin()` waits
36
+ forever — which surfaces as an 800-second file timeout rather than a failing
37
+ assertion. The double has to satisfy three things at once: stay non-TTY so
38
+ `isInteractive()` is correctly false, deliver `end` to a listener that attaches
39
+ later, and hold no OS handle so the file can still exit. A test that wants piped
40
+ input uses `withStdin()`.
41
+
42
+ ## Helpers
43
+
44
+ | helper | use |
45
+ |---|---|
46
+ | `helpers/net.js` | `resetNetwork()` · `route/json/text/status` · `calledWith` · `calls` |
47
+ | `helpers/output.js` | `captureOutput(fn)` · `captureBoth(fn)` → `{value, stdout, stderr}` |
48
+ | `helpers/cli.js` | `withExitCode(fn)` → `{value, exitCode}` |
49
+ | `helpers/stdin.js` | `withStdin(text, fn)` |
50
+ | `helpers/capture.js` | `captureIO` — string chunks only |
51
+ | `helpers/ui-double.mjs` | scripted stand-in for the interactive prompts |
52
+ | `helpers/ui-hook.mjs` | `module.register` hook that redirects only `run.js`'s `./ui.js` import |
53
+
54
+ ## Gotchas
55
+
56
+ Each of these cost real debugging time. They are properties of the runner and
57
+ the harness, not opinions.
58
+
59
+ **`node --test` writes its own protocol to stdout as Buffers.** A capture helper
60
+ that decodes Buffers into the captured text corrupts it, and a
61
+ `JSON.parse(captured)` fails intermittently with `Unexpected token` depending on
62
+ timing. Record string chunks only and pass binary straight through. Both
63
+ `helpers/output.js` and `helpers/capture.js` do this.
64
+
65
+ **Do not capture stdout around anything that awaits a real timer or async gap.**
66
+ The runner's own reporting written during that window is swallowed, and the file
67
+ reports a wrong final test count with no error at all.
68
+
69
+ **Wrap anything that reports a failure in `withExitCode`.** The CLI signals
70
+ failure by setting `process.exitCode = 1` and returning, so the caller can keep
71
+ working. That is right for the CLI and wrong for a test: a perfectly correct test
72
+ of a failure path would make the whole file exit non-zero.
73
+
74
+ **`resetNetwork()` between cases, not just between tests.** Routes only append
75
+ and the first match wins, so registering two responses for one pattern leaves
76
+ the second as dead code and every assertion after it passes for the wrong reason.
77
+
78
+ **`await import("node:fs")` yields a namespace, not a live binding.** A stub
79
+ assigned to the *default* export is invisible to a named-export read from inside
80
+ the module. To exercise a path that checks the filesystem, change the
81
+ filesystem — e.g. `sandbox-missing.test.js` copies `sandbox.js` into a temp
82
+ directory with no `runner.js` beside it, which is a real broken install.
83
+
84
+ **Module-level state latches.** `runnerMissing` in `sandbox.js` stays true once
85
+ set, so that behaviour lives in its own test file; `node --test` gives each file
86
+ a separate process, which is the isolation it needs.
87
+
88
+ **Interactive code is tested by injection, not by a terminal.** Inquirer cannot
89
+ be driven headlessly and a pending prompt keeps the process alive forever. The
90
+ wizard takes a `prompts` bundle (`wizard-flow.test.js`); `run.js`'s interactive
91
+ half is reached through a `module.register` hook that swaps only its `./ui.js`
92
+ import (`run-interactive.test.js`). Both keep the real decision logic in `src`
93
+ and replace only the presentation.
94
+
95
+ **`index.js` is spawned, not imported.** It parses argv on import. Spawning it
96
+ is also the honest way to assert the two contracts users depend on: exit codes,
97
+ and the stdout/stderr split.
98
+
99
+ ## Writing a test for a new module
100
+
101
+ 1. `import` it normally at the top of the file. The `--import` hook has already
102
+ run, so isolation is in place.
103
+ 2. Network: `beforeEach(() => resetNetwork())`, `after(() => restoreNetwork())`.
104
+ 3. Failure paths: wrap in `withExitCode` and assert `exitCode`.
105
+ 4. Anything reading stdin: `withStdin`, or just let the default resolve `""`.
106
+ 5. Assert behaviour, not coverage. A test that exists only to execute a line is
107
+ a maintenance liability that fails for the wrong reasons.
108
+
109
+ ## Leak guards
110
+
111
+ `test/isolation.test.js` is not an ordinary unit test file. Each test fails if a
112
+ future change lets the suite read the developer's real config, reuse a real API
113
+ key, or reach the real network:
114
+
115
+ - config and cache resolve inside the temp sandbox, never under the real home
116
+ - no inherited credentials or telemetry opt-in survive into the process
117
+ - the API base is unroutable, so an unstubbed request fails rather than escaping
118
+ - a canary secret never appears in captured stdout or stderr
119
+ - redacted config leaks neither the value nor a fragment of it
120
+ - `config.json` is `0600` inside a `0700` directory
121
+ - no source file contains a hardcoded credential
122
+ - no inlined JWT grants more than the `anon` role
123
+
124
+ If you add a code path that could print a secret, add a canary test for it in the
125
+ same change.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexusbloom/cli",
3
- "version": "0.3.4",
3
+ "version": "0.9.0",
4
4
  "description": "NexusBloom CLI — run tools and workflows from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "files": [
10
10
  "src/",
11
11
  "package.json",
12
- "README.md"
12
+ "README.md",
13
+ "TESTING.md"
13
14
  ],
14
15
  "publishConfig": {
15
16
  "access": "public"
@@ -22,5 +23,11 @@
22
23
  "chalk": "^5.3.0",
23
24
  "commander": "^12.0.0",
24
25
  "qrcode": "^1.5.4"
26
+ },
27
+ "scripts": {
28
+ "test": "node --test --import ./test/setup.mjs test/*.test.js",
29
+ "test:coverage": "node --test --experimental-test-coverage --import ./test/setup.mjs test/*.test.js",
30
+ "test:watch": "node --test --watch --import ./test/setup.mjs test/*.test.js",
31
+ "lint": "node --check src/index.js"
25
32
  }
26
33
  }
package/src/abbr.js CHANGED
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * abbr.js — `nxb abbr` command.
3
3
  *
4
- * Simple paginated abbreviation view.
4
+ * Paginated abbreviation view.
5
5
  *
6
- * nxb abbr — interactive pagination (10/page)
7
- * nxb abbr ip — filter by query (matches slug, abbr, or name)
8
- * nxb abbr --json — raw JSON
9
- * nxb abbr --no-interactive — single dump
6
+ * nxb abbr — interactive pagination (20/page)
7
+ * nxb abbr ip — filter by query (matches slug, abbr, or name)
8
+ * nxb abbr -s ip — same, explicit flag
9
+ * nxb abbr --json — raw JSON
10
+ * nxb abbr --no-interactive— single dump
10
11
  */
11
12
 
12
13
  import chalk from "chalk";
13
- import { fetchTools, generateAbbreviations } from "./tools.js";
14
+ import { fetchTools, generateAbbreviations, filterTools } from "./tools.js";
14
15
  import { paginate, paginateInteractive, PAGE_SIZE } from "./ui.js";
16
+ import { diagnoseEmptyCatalogue } from "./diagnostics.js";
15
17
 
16
18
  function renderAbbrPage(result, abbrMap) {
17
19
  if (result.total === 0) {
@@ -30,22 +32,27 @@ function renderAbbrPage(result, abbrMap) {
30
32
 
31
33
  export async function abbrAction(opts = {}) {
32
34
  const { query, json: jsonMode, interactive = true, force = false, offline = false } = opts;
35
+ const search = query || opts.search;
33
36
 
34
37
  const tools = await fetchTools({ force, offline });
38
+ if (!tools || tools.length === 0) {
39
+ return diagnoseEmptyCatalogue({ offline, jsonMode, source: "catalogue" });
40
+ }
35
41
  const abbrMap = generateAbbreviations(tools.map((t) => t.slug));
36
42
 
37
- let filtered = tools;
38
- if (query) {
39
- const q = query.toLowerCase();
40
- filtered = tools.filter((t) => {
41
- const abbr = abbrMap[t.slug];
42
- return (
43
- abbr?.toLowerCase().includes(q) ||
44
- t.slug.toLowerCase().includes(q) ||
45
- (t.name || "").toLowerCase().includes(q)
46
- );
47
- });
48
- }
43
+ // Match slug / name / description / category, plus the abbreviation itself —
44
+ // the abbreviation is what you actually type, so it has to be searchable.
45
+ const filtered = search
46
+ ? (() => {
47
+ const byFields = filterTools(tools, search);
48
+ const seen = new Set(byFields.map((t) => t.slug));
49
+ const q = search.toLowerCase();
50
+ const byAbbr = tools.filter(
51
+ (t) => !seen.has(t.slug) && (abbrMap[t.slug] || "").toLowerCase().includes(q)
52
+ );
53
+ return [...byFields, ...byAbbr];
54
+ })()
55
+ : tools;
49
56
 
50
57
  if (jsonMode) {
51
58
  const output = filtered.reduce((acc, t) => { acc[t.slug] = abbrMap[t.slug]; return acc; }, {});
@@ -55,7 +62,7 @@ export async function abbrAction(opts = {}) {
55
62
 
56
63
  if (!interactive) {
57
64
  if (filtered.length === 0) {
58
- console.log(chalk.yellow(` No abbreviations${query ? ` matching "${query}"` : ""}.`));
65
+ console.log(chalk.yellow(` No abbreviations${search ? ` matching "${search}"` : ""}.`));
59
66
  return;
60
67
  }
61
68
  const result = paginate(filtered, filtered.length, 1);
@@ -73,7 +80,7 @@ export async function abbrAction(opts = {}) {
73
80
  console.log(chalk.bold(`\n Tool Abbreviations (${result.total} · page ${result.page}/${result.totalPages})\n`));
74
81
  renderAbbrPage(result, abbrMap);
75
82
  },
76
- {}
83
+ { interactive }
77
84
  );
78
85
  }
79
86