@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 +0 -12
- package/package.json +2 -3
- package/src/wizard.js +10 -5
- package/TESTING.md +0 -125
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.
|
|
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,
|
|
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.
|