@nexusbloom/cli 0.9.1 → 0.9.3

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
@@ -323,18 +323,6 @@ removed rather than resolved. Relative imports like `import { helper } from
323
323
  `export const coreLogic = async () => {}` and plain `function coreLogic` are all
324
324
  supported.
325
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
326
  ## Security Notes
339
327
 
340
328
  - No API keys or Supabase keys are shipped inside the package.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexusbloom/cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "NexusBloom CLI — run tools and workflows from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,8 +9,7 @@
9
9
  "files": [
10
10
  "src/",
11
11
  "package.json",
12
- "README.md",
13
- "TESTING.md"
12
+ "README.md"
14
13
  ],
15
14
  "publishConfig": {
16
15
  "access": "public"
package/src/wizard.js CHANGED
@@ -97,8 +97,16 @@ function renderObservedOutput(fields) {
97
97
 
98
98
  // ─── Prompts ─────────────────────────────────────────────────────────────────
99
99
 
100
+ /**
101
+ * Ask for a tool to add as the next step.
102
+ *
103
+ * A tool may be chosen more than once: the pipeline engine addresses steps
104
+ * positionally, so `base64-tool … | base64-tool text=$result action=decode`
105
+ * is a legal round-trip. (It was verified working before this restriction was
106
+ * lifted.) `exclude` remains for callers that genuinely need a subset.
107
+ */
100
108
  async function pickTool(tools, exclude = [], P = defaultPrompts) {
101
- const pool = tools.filter((t) => !exclude.includes(t.slug));
109
+ const pool = exclude.length ? tools.filter((t) => !exclude.includes(t.slug)) : tools;
102
110
  const source = async (term) => {
103
111
  const hits = term ? rankTools(pool, term) : pool;
104
112
  return hits.slice(0, 50).map((t) => ({
@@ -246,7 +254,6 @@ export async function buildWizard(opts = {}) {
246
254
 
247
255
  const steps = [];
248
256
  const outputs = [];
249
- const usedSlugs = [];
250
257
 
251
258
  for (;;) {
252
259
  renderHeader(
@@ -254,7 +261,7 @@ export async function buildWizard(opts = {}) {
254
261
  { subtitle: steps.length ? "Pick the next tool to feed" : "Start with any tool" }
255
262
  );
256
263
 
257
- const tool = await pickTool(tools, usedSlugs, P);
264
+ const tool = await pickTool(tools, [], P);
258
265
  if (!tool) break;
259
266
 
260
267
  const info = await getTool(tool.slug);
@@ -280,7 +287,6 @@ export async function buildWizard(opts = {}) {
280
287
 
281
288
  const spec = `${tool.slug} ${assignments.map((a) => a.text).join(" ")}`.trim();
282
289
  steps.push({ slug: tool.slug, spec, assignments });
283
- usedSlugs.push(tool.slug);
284
290
 
285
291
  console.log(chalk.gray(`\n Running ${spec}\n`));
286
292
  const started = Date.now();
@@ -294,7 +300,6 @@ export async function buildWizard(opts = {}) {
294
300
  // Drop the step either way: only steps that actually ran belong in the
295
301
  // replay command we hand back.
296
302
  steps.pop();
297
- usedSlugs.pop();
298
303
  const again = await P.confirm({ message: "Change the inputs and retry?", default: true });
299
304
  if (again) continue;
300
305
  break;
package/TESTING.md DELETED
@@ -1,125 +0,0 @@
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.