@taurgis/bonsai 3.2.0 → 3.2.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/README.md +7 -4
- package/agents/agents/salesforce-docs-researcher.agent.md +14 -1
- package/agents/instructions/salesforce-research.instructions.md +2 -1
- package/bin/cli.mjs +9 -17
- package/dist/base-command.d.ts +22 -12
- package/dist/base-command.js +86 -49
- package/dist/commands/config/base.js +11 -2
- package/dist/commands/config/get.js +8 -12
- package/dist/commands/config/index.js +2 -2
- package/dist/commands/config/list.js +10 -16
- package/dist/commands/config/set.d.ts +2 -1
- package/dist/commands/config/set.js +34 -27
- package/dist/commands/config/unset.d.ts +2 -1
- package/dist/commands/config/unset.js +25 -24
- package/dist/commands/fetch.d.ts +7 -15
- package/dist/commands/fetch.js +57 -401
- package/dist/commands/import.d.ts +4 -9
- package/dist/commands/import.js +56 -284
- package/dist/commands/inspect.d.ts +7 -1
- package/dist/commands/inspect.js +103 -52
- package/dist/commands/list.d.ts +13 -1
- package/dist/commands/list.js +79 -32
- package/dist/commands/prune.d.ts +4 -1
- package/dist/commands/prune.js +55 -75
- package/dist/commands/status.d.ts +3 -2
- package/dist/commands/status.js +52 -79
- package/dist/hooks/command-not-found/suggest.d.ts +1 -0
- package/dist/hooks/command-not-found/suggest.js +66 -39
- package/dist/lib/argv.d.ts +24 -3
- package/dist/lib/argv.js +124 -53
- package/dist/lib/batch.d.ts +41 -0
- package/dist/lib/batch.js +39 -0
- package/dist/lib/cache-view.d.ts +21 -0
- package/dist/lib/cache-view.js +30 -0
- package/dist/lib/cli-emit.d.ts +28 -0
- package/dist/lib/cli-emit.js +44 -0
- package/dist/lib/cli-error-policy.d.ts +57 -0
- package/dist/lib/cli-error-policy.js +74 -0
- package/dist/lib/cli-flag-manifest.d.ts +5 -0
- package/dist/lib/cli-flag-manifest.js +8 -0
- package/dist/lib/cli-presentation.d.ts +39 -0
- package/dist/lib/cli-presentation.js +50 -0
- package/dist/lib/cli-result-types.d.ts +88 -0
- package/dist/lib/cli-result-types.js +2 -0
- package/dist/lib/config/index.d.ts +21 -3
- package/dist/lib/config/index.js +58 -3
- package/dist/lib/config-persist.d.ts +28 -0
- package/dist/lib/config-persist.js +29 -0
- package/dist/lib/envelope.d.ts +41 -3
- package/dist/lib/envelope.js +147 -4
- package/dist/lib/help-preflight.d.ts +5 -8
- package/dist/lib/help-preflight.js +13 -32
- package/dist/lib/json-meta.d.ts +0 -2
- package/dist/lib/json-meta.js +21 -25
- package/dist/lib/limit-flag.d.ts +11 -1
- package/dist/lib/limit-flag.js +19 -6
- package/dist/lib/parse-error-ux.d.ts +5 -0
- package/dist/lib/parse-error-ux.js +73 -0
- package/dist/lib/prune-flags.d.ts +28 -0
- package/dist/lib/prune-flags.js +74 -0
- package/dist/lib/research/artifact-index.d.ts +11 -2
- package/dist/lib/research/artifact-index.js +7 -4
- package/dist/lib/research/browser.d.ts +41 -2
- package/dist/lib/research/browser.js +139 -50
- package/dist/lib/research/capture.d.ts +24 -2
- package/dist/lib/research/capture.js +24 -4
- package/dist/lib/research/cli-io.d.ts +33 -0
- package/dist/lib/research/cli-io.js +27 -0
- package/dist/lib/research/compress.d.ts +7 -0
- package/dist/lib/research/compress.js +7 -3
- package/dist/lib/research/docs/capabilities.d.ts +15 -0
- package/dist/lib/research/docs/capabilities.js +5 -0
- package/dist/lib/research/docs/clean-dom.d.ts +2 -0
- package/dist/lib/research/docs/clean-dom.js +2 -0
- package/dist/lib/research/docs/code-blocks.d.ts +2 -0
- package/dist/lib/research/docs/code-blocks.js +2 -0
- package/dist/lib/research/docs/detect.d.ts +4 -0
- package/dist/lib/research/docs/detect.js +4 -0
- package/dist/lib/research/docs/machine-readable.d.ts +18 -0
- package/dist/lib/research/docs/machine-readable.js +10 -0
- package/dist/lib/research/docs/markdown-source.d.ts +20 -0
- package/dist/lib/research/docs/markdown-source.js +19 -2
- package/dist/lib/research/docs/quality-gates.d.ts +5 -0
- package/dist/lib/research/docs/quality-gates.js +4 -0
- package/dist/lib/research/docs/rsc.d.ts +9 -2
- package/dist/lib/research/docs/rsc.js +5 -2
- package/dist/lib/research/docs/section-artifacts.d.ts +12 -0
- package/dist/lib/research/docs/section-artifacts.js +12 -0
- package/dist/lib/research/docs/sections.d.ts +18 -0
- package/dist/lib/research/docs/sections.js +17 -1
- package/dist/lib/research/docs/source-map.d.ts +28 -0
- package/dist/lib/research/docs/source-map.js +27 -0
- package/dist/lib/research/docs/validate.d.ts +35 -2
- package/dist/lib/research/docs/validate.js +47 -2
- package/dist/lib/research/extract.d.ts +16 -0
- package/dist/lib/research/extract.js +12 -1
- package/dist/lib/research/fetch-command-service.d.ts +36 -0
- package/dist/lib/research/fetch-command-service.js +310 -0
- package/dist/lib/research/fetch-result.d.ts +114 -0
- package/dist/lib/research/fetch-result.js +212 -0
- package/dist/lib/research/fetcher.d.ts +46 -8
- package/dist/lib/research/fetcher.js +58 -30
- package/dist/lib/research/freshness.d.ts +54 -11
- package/dist/lib/research/freshness.js +91 -31
- package/dist/lib/research/import-command-service.d.ts +56 -0
- package/dist/lib/research/import-command-service.js +381 -0
- package/dist/lib/research/keywords.js +5 -1
- package/dist/lib/research/markdown.d.ts +12 -1
- package/dist/lib/research/markdown.js +13 -5
- package/dist/lib/research/persist-artifact.d.ts +23 -0
- package/dist/lib/research/persist-artifact.js +27 -0
- package/dist/lib/research/prompt-injection.d.ts +8 -0
- package/dist/lib/research/prompt-injection.js +42 -13
- package/dist/lib/research/proxy.d.ts +14 -8
- package/dist/lib/research/proxy.js +14 -10
- package/dist/lib/research/revalidate.d.ts +49 -20
- package/dist/lib/research/revalidate.js +80 -15
- package/dist/lib/research/schema.d.ts +7 -0
- package/dist/lib/research/schema.js +2 -0
- package/dist/lib/research/secret-scan.d.ts +6 -0
- package/dist/lib/research/secret-scan.js +8 -0
- package/dist/lib/research/secure-write.d.ts +4 -0
- package/dist/lib/research/secure-write.js +31 -2
- package/dist/lib/research/storage.d.ts +5 -1
- package/dist/lib/research/storage.js +8 -3
- package/dist/lib/research/summarize.d.ts +15 -0
- package/dist/lib/research/summarize.js +11 -0
- package/dist/lib/research/url.d.ts +9 -6
- package/dist/lib/research/url.js +33 -21
- package/dist/lib/text.d.ts +7 -2
- package/dist/lib/text.js +13 -2
- package/dist/lib/value-flags.d.ts +14 -0
- package/dist/lib/value-flags.js +28 -0
- package/dist/sites/coveo.d.ts +10 -0
- package/dist/sites/coveo.js +10 -0
- package/dist/sites/index.d.ts +16 -0
- package/dist/sites/index.js +16 -4
- package/dist/sites/markdown-twin.d.ts +31 -0
- package/dist/sites/markdown-twin.js +56 -0
- package/dist/sites/salesforce/fetch-page.d.ts +8 -2
- package/dist/sites/salesforce/fetch-page.js +14 -3
- package/dist/sites/salesforce/help-tooling-markdown.d.ts +30 -0
- package/dist/sites/salesforce/help-tooling-markdown.js +74 -0
- package/dist/sites/salesforce/index.d.ts +1 -0
- package/dist/sites/salesforce/index.js +1 -0
- package/dist/sites/salesforce-developer/fetch-page.d.ts +6 -1
- package/dist/sites/salesforce-developer/fetch-page.js +6 -1
- package/dist/sites/salesforce-developer/index.d.ts +1 -0
- package/dist/sites/salesforce-developer/index.js +1 -0
- package/dist/sites/salesforce-developer/route-markdown.d.ts +18 -6
- package/dist/sites/salesforce-developer/route-markdown.js +29 -67
- package/dist/sites/salesforce-doc-fetch.d.ts +39 -5
- package/dist/sites/salesforce-doc-fetch.js +52 -7
- package/dist/sites/salesforce-dom-probe.d.ts +20 -2
- package/dist/sites/salesforce-dom-probe.js +20 -2
- package/dist/sites/types.d.ts +14 -8
- package/dist/sites/types.js +3 -0
- package/oclif.manifest.json +104 -98
- package/package.json +19 -7
- package/agents/agents/official-docs-researcher.agent.md +0 -87
- package/agents/instructions/web-research.instructions.md +0 -44
- package/agents/skills/web-research/SKILL.md +0 -118
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ Common flags:
|
|
|
110
110
|
* `--max-age`: Read-time freshness threshold.
|
|
111
111
|
* `--force`: Force a fresh fetch.
|
|
112
112
|
* `--dry-run`: Scrape and validate without writing to cache.
|
|
113
|
-
* `--allow-stale`:
|
|
113
|
+
* `--allow-stale`: Suppress the exit `5` signal when a within-grace stale entry is served after failed revalidation (see [Freshness and Cache Rules](#freshness-and-cache-rules) below). Stale content is served within the grace window regardless of this flag; it does not extend serving beyond the grace window.
|
|
114
114
|
* `--rendered`: Use browser-rendered extraction for SPA pages.
|
|
115
115
|
* `--read-only` (alias `--plan`): Block all filesystem writes/deletes across every command; network fetches still run. See [Read-only / plan mode](#read-only--plan-mode) below.
|
|
116
116
|
* `--json`: Return structured machine-readable output.
|
|
@@ -181,7 +181,7 @@ npx @taurgis/bonsai import https://example.com/docs --file notes.md --read-only
|
|
|
181
181
|
npx @taurgis/bonsai prune --older-than 90d --plan
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
It can also be set via the `BONSAI_READ_ONLY` or `BONSAI_PLAN_MODE` environment variables (both are equivalent). Composition is OR, not override: read-only mode is active if *either* env var or the flag is set, and there is no way to force writes back on once one of them is — this is a safety gate, not a preference. Commands that
|
|
184
|
+
It can also be set via the `BONSAI_READ_ONLY` or `BONSAI_PLAN_MODE` environment variables (both are equivalent). Composition is OR, not override: read-only mode is active if *either* env var or the flag is set, and there is no way to force writes back on once one of them is — this is a safety gate, not a preference. Commands that expose `--dry-run` (`fetch`, `import`, `prune`, `config set`/`unset`) treat global read-only the same as an explicit `--dry-run` and report `dryRun: true` (with `would_*` cache statuses where applicable) instead of persisting.
|
|
185
185
|
|
|
186
186
|
---
|
|
187
187
|
|
|
@@ -235,7 +235,10 @@ If no `--ttl` is specified, freshness is computed from the tier:
|
|
|
235
235
|
| `standard` | 30 days | 14 days | General API docs and developer guides |
|
|
236
236
|
| `stable` | 180 days | 60 days | RFCs, standards, long-lived references |
|
|
237
237
|
|
|
238
|
-
When stale entries still have `ETag` or `Last-Modified` metadata, Bonsai attempts cheap revalidation before a full refetch. If revalidation fails
|
|
238
|
+
When stale entries still have `ETag` or `Last-Modified` metadata, Bonsai attempts cheap revalidation before a full refetch. If revalidation fails:
|
|
239
|
+
|
|
240
|
+
* **Within the grace window**, the stale content is served either way. By default this exits with code `5` so a caller can detect "this result may be outdated"; `--allow-stale` suppresses that signal and exits `0` instead, since the caller has already said outdated content is acceptable.
|
|
241
|
+
* **Past the grace window**, the entry is expired and revalidation failure is always a hard error (exit `1`) — `--allow-stale` has no effect here, since there is no fresher fallback to serve.
|
|
239
242
|
|
|
240
243
|
---
|
|
241
244
|
|
|
@@ -303,7 +306,7 @@ When run with `--json`, commands return a stable envelope:
|
|
|
303
306
|
| `0` | Success. |
|
|
304
307
|
| `1` | Runtime failure (network error, fetch failed, file read error). |
|
|
305
308
|
| `2` | Usage error (invalid flag, bad URL, missing required argument). |
|
|
306
|
-
| `5` | Stale content served —
|
|
309
|
+
| `5` | Stale content served — revalidation failed within the grace window and `--allow-stale` was omitted. The returned content is still usable. |
|
|
307
310
|
|
|
308
311
|
---
|
|
309
312
|
|
|
@@ -5,7 +5,7 @@ model: 'Auto'
|
|
|
5
5
|
tools: ['vscode/askQuestions', 'execute', 'read', 'search', 'web', 'vscode/memory']
|
|
6
6
|
argument-hint: 'What Salesforce topic should I research in official docs?'
|
|
7
7
|
metadata:
|
|
8
|
-
version: '2.
|
|
8
|
+
version: '2.1.0'
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Salesforce Docs Researcher Agent
|
|
@@ -22,6 +22,8 @@ Do not research from memory alone. Training-data knowledge does not satisfy this
|
|
|
22
22
|
|
|
23
23
|
Run Bonsai as `npx @taurgis/bonsai ...`. Add `--json` when you need structured output.
|
|
24
24
|
|
|
25
|
+
If you are operating in a read-only/plan mode (no filesystem writes allowed), add `--read-only` (alias `--plan`) to every Bonsai call, or export `BONSAI_READ_ONLY=1` (or `BONSAI_PLAN_MODE=1`) once for the session — fetches still run, but nothing is persisted to the local cache or config. See "Read-only / Plan Mode" below.
|
|
26
|
+
|
|
25
27
|
## Default Workflow
|
|
26
28
|
|
|
27
29
|
1. Identify the product, version, edition, and source authority needed for the request.
|
|
@@ -68,6 +70,17 @@ Choose the freshness tier based on the source:
|
|
|
68
70
|
|
|
69
71
|
Stale volatile sources must be revalidated before they are trusted. Never overwrite a good cache entry with an error response.
|
|
70
72
|
|
|
73
|
+
## Read-only / Plan Mode
|
|
74
|
+
|
|
75
|
+
Bonsai supports a global `--read-only` flag (alias `--plan`), also honored via the `BONSAI_READ_ONLY`/`BONSAI_PLAN_MODE` environment variables, that blocks all filesystem writes and deletes (cache persistence, config writes, `prune` deletions) while still allowing network fetches to run. Use it whenever you are invoked under a read-only/plan-mode harness so this research workflow stays non-destructive:
|
|
76
|
+
|
|
77
|
+
- Research fetches (`npx @taurgis/bonsai <url>`), including the rendered Salesforce site-module fetches above, still hit the network and return content normally; they just skip writing the result to the local cache.
|
|
78
|
+
- `import` previews the write and reports `dryRun: true` instead of persisting.
|
|
79
|
+
- `config set` / `config unset` preview the write and report `would_set` / `would_unset`.
|
|
80
|
+
- `prune` treats `--read-only` like an implicit dry run; combining it with `--yes` is rejected.
|
|
81
|
+
|
|
82
|
+
Composition is OR: once read-only mode is active (flag or either env var), there is no per-call way to force writes back on.
|
|
83
|
+
|
|
71
84
|
## Fallbacks
|
|
72
85
|
|
|
73
86
|
If direct web access was unavoidable because Bonsai could not fetch the content, import the synthesized notes back into Bonsai before returning:
|
|
@@ -4,7 +4,7 @@ applyTo: '**'
|
|
|
4
4
|
skills:
|
|
5
5
|
- web-research
|
|
6
6
|
metadata:
|
|
7
|
-
version: '2.
|
|
7
|
+
version: '2.1.0'
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Web Research Requirement (Salesforce)
|
|
@@ -29,6 +29,7 @@ metadata:
|
|
|
29
29
|
- **Browse cached entries**: Use `npx @taurgis/bonsai list` when you need to see what is already cached by topic, tags, or freshness.
|
|
30
30
|
- If Bonsai is configured for project storage and `.bonsai/research/` is not ignored by git, treat those cache artifacts as intentional shared project files. It is OK to check them in, and agents must not delete them as incidental generated output without an explicit request.
|
|
31
31
|
- Re-running on a recent topic is cheap — research the topic rather than skipping it to "save" a fetch.
|
|
32
|
+
- If you are operating under a read-only/plan-mode harness, add `--read-only` (alias `--plan`) to Bonsai calls, or rely on `BONSAI_READ_ONLY`/`BONSAI_PLAN_MODE` if the harness sets it — fetches still run, but nothing is persisted to the local cache or config; `config set` / `config unset` preview with `would_set` / `would_unset`.
|
|
32
33
|
|
|
33
34
|
## Working with Salesforce Sites
|
|
34
35
|
|
package/bin/cli.mjs
CHANGED
|
@@ -7,6 +7,8 @@ import updateNotifier from 'update-notifier';
|
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
|
|
9
9
|
import { normalizeArgv } from '../dist/lib/argv.js';
|
|
10
|
+
import { VALUE_TAKING_FLAG_TOKENS } from '../dist/lib/cli-flag-manifest.js';
|
|
11
|
+
import { exitWithPreflight } from '../dist/lib/cli-emit.js';
|
|
10
12
|
import { tryUnknownHelpOutput } from '../dist/lib/help-preflight.js';
|
|
11
13
|
import { tryJsonMetaOutput } from '../dist/lib/json-meta.js';
|
|
12
14
|
|
|
@@ -21,35 +23,25 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
21
23
|
// trying to resolve the bare URL as a command — which crashes with a stack trace. Rewrite
|
|
22
24
|
// process.argv itself so both the run path and the help/error path agree.
|
|
23
25
|
const rawArgv = process.argv.slice(2);
|
|
24
|
-
const result = normalizeArgv(rawArgv);
|
|
26
|
+
const result = normalizeArgv(rawArgv, { valueTakingFlags: VALUE_TAKING_FLAG_TOKENS });
|
|
25
27
|
const root = __dirname + '/../';
|
|
26
28
|
|
|
27
|
-
if (result.
|
|
28
|
-
|
|
29
|
-
console.log(JSON.stringify(result.exitWithJson.envelope, null, 2));
|
|
30
|
-
process.exit();
|
|
29
|
+
if (result.earlyExit) {
|
|
30
|
+
exitWithPreflight(result.earlyExit);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const unknownHelp = await tryUnknownHelpOutput(result.argv, root);
|
|
34
34
|
if (unknownHelp) {
|
|
35
|
-
|
|
36
|
-
if (unknownHelp.kind === 'json') {
|
|
37
|
-
console.log(JSON.stringify(unknownHelp.envelope, null, 2));
|
|
38
|
-
} else {
|
|
39
|
-
console.error(` › Error: ${unknownHelp.message.replaceAll('\n', '\n › ')}`);
|
|
40
|
-
}
|
|
41
|
-
process.exit();
|
|
35
|
+
exitWithPreflight(unknownHelp);
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
const jsonMeta = await tryJsonMetaOutput(result.argv, root);
|
|
45
39
|
if (jsonMeta) {
|
|
46
|
-
|
|
47
|
-
console.log(JSON.stringify(jsonMeta.envelope, null, 2));
|
|
48
|
-
process.exit();
|
|
40
|
+
exitWithPreflight({ ...jsonMeta, json: true });
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
// Only notify after the
|
|
52
|
-
// a
|
|
43
|
+
// Only notify after the earlyExit fast-path so the notifier never fires before
|
|
44
|
+
// a usage-error envelope exits. Also skip under --json so agent callers never see stderr noise.
|
|
53
45
|
if (!process.argv.includes('--json')) {
|
|
54
46
|
updateNotifier({ pkg }).notify();
|
|
55
47
|
}
|
package/dist/base-command.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Command, Interfaces } from '@oclif/core';
|
|
1
|
+
import { Command, Errors, Interfaces } from '@oclif/core';
|
|
2
2
|
import { type ResolveResearchTargetOptions, type ResolvedResearchTarget } from './lib/research/resolve-target.js';
|
|
3
|
+
import { type CliIo } from './lib/research/cli-io.js';
|
|
3
4
|
/**
|
|
4
5
|
* Shared base for every Bonsai command. Enables oclif's native `--json` flag,
|
|
5
6
|
* parses args/flags once in `init()` so commands read `this.args`/`this.flags`
|
|
@@ -36,12 +37,6 @@ export declare abstract class BaseCommand<T extends typeof Command> extends Comm
|
|
|
36
37
|
* write, so a skipped write is never a silent surprise to the caller.
|
|
37
38
|
*/
|
|
38
39
|
protected effectiveDryRun(explicitDryRun: boolean): boolean;
|
|
39
|
-
/**
|
|
40
|
-
* The exit code an error maps to. oclif `CLIError`s carry it in `err.oclif.exit`; other errors may
|
|
41
|
-
* expose `err.exitCode`. Defined once so the process exit code (`catch`) and the JSON envelope
|
|
42
|
-
* (`toErrorJson`) can never disagree — a mismatch between these two was itself the bug this fixes.
|
|
43
|
-
*/
|
|
44
|
-
private static exitCodeOf;
|
|
45
40
|
/**
|
|
46
41
|
* Align the process exit code with the code reported in the JSON envelope. oclif's default
|
|
47
42
|
* `catch` sets `process.exitCode = err.exitCode ?? 1`, but oclif `CLIError`s carry their code in
|
|
@@ -56,6 +51,7 @@ export declare abstract class BaseCommand<T extends typeof Command> extends Comm
|
|
|
56
51
|
};
|
|
57
52
|
exitCode?: number;
|
|
58
53
|
code?: string;
|
|
54
|
+
suggestions?: string[];
|
|
59
55
|
}): Promise<any>;
|
|
60
56
|
/** Command id for the JSON envelope; falls back to the binary name when a command has no id. */
|
|
61
57
|
protected envelopeCommandId(): string;
|
|
@@ -66,16 +62,30 @@ export declare abstract class BaseCommand<T extends typeof Command> extends Comm
|
|
|
66
62
|
* stays INVALID_URL.
|
|
67
63
|
*/
|
|
68
64
|
protected resolveResearchTargetOrFail(url: string, extra?: Pick<ResolveResearchTargetOptions, 'flagOverride' | 'lookup'>): ResolvedResearchTarget;
|
|
69
|
-
/**
|
|
70
|
-
* Single exit point for a URL that failed normalization, shared by every URL-accepting command.
|
|
71
|
-
* The scheme-less message is already self-contained and actionable, so it is surfaced as-is;
|
|
72
|
-
* other parse failures keep the "Invalid URL:" prefix for context.
|
|
73
|
-
*/
|
|
65
|
+
/** Exit for a URL that failed normalization; see {@link failInvalidUrl} for the code contract. */
|
|
74
66
|
protected failInvalidUrl(url: string, message: string): never;
|
|
67
|
+
/** Command context handed to command services (fetch/import) so they stay oclif-free. */
|
|
68
|
+
protected cliIo(): CliIo;
|
|
75
69
|
/** Single source of truth for the `--json` envelope shape, shared by success and error output. */
|
|
76
70
|
private envelope;
|
|
71
|
+
/**
|
|
72
|
+
* Base success envelope from `process.exitCode` (including exit 5 = served stale / ok).
|
|
73
|
+
* Subclasses that overlay batch failures must call this — not `toSuccessJson` — to avoid
|
|
74
|
+
* recursing through their own override.
|
|
75
|
+
*/
|
|
76
|
+
protected baseSuccessJson(data: unknown): Record<string, unknown>;
|
|
77
77
|
/** Wrap a command's return value in the machine-readable envelope emitted under `--json`. */
|
|
78
78
|
protected toSuccessJson(data: unknown): Record<string, unknown>;
|
|
79
|
+
/**
|
|
80
|
+
* Success overlay for multi-URL read commands (`status`, `inspect`). Apply CACHE_MISS first,
|
|
81
|
+
* then row `.error` so validation failures win when both appear in one batch.
|
|
82
|
+
*/
|
|
83
|
+
protected batchReadSuccessJson(data: unknown): Record<string, unknown>;
|
|
84
|
+
/**
|
|
85
|
+
* Map each URL through `fn`. In a multi-URL batch, per-URL CLIErrors become failure rows so
|
|
86
|
+
* prior hits are kept — matching fetch's INVALID_URL / MISSING_URL_SCHEME batch contract.
|
|
87
|
+
*/
|
|
88
|
+
protected mapUrlsAllowingBatchErrors<T, F>(urls: string[], fn: (url: string) => T, failureRow: (url: string, err: InstanceType<typeof Errors.CLIError>) => F): Array<T | F>;
|
|
79
89
|
/** Mirror the success envelope for failures so JSON consumers get one consistent shape. */
|
|
80
90
|
protected toErrorJson(err: unknown): Record<string, unknown>;
|
|
81
91
|
}
|
package/dist/base-command.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Command, Errors, Flags, toConfiguredId } from '@oclif/core';
|
|
2
2
|
import { invalidEnvOverrideWarnings, resolveReadOnly } from './lib/config/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { CLI_FLAG_DESCRIPTIONS } from './lib/cli-presentation.js';
|
|
4
|
+
import { enrichErrorForDisplay, resolveExitCode, prepareCliError, EXIT_OK, EXIT_STALE_SERVED, } from './lib/cli-error-policy.js';
|
|
5
|
+
import { buildEnvelope, enrichCacheMissEnvelope, enrichRowErrorEnvelope, formatErrorForJson, } from './lib/envelope.js';
|
|
6
|
+
import { enrichParseError } from './lib/parse-error-ux.js';
|
|
4
7
|
import { resolveResearchTarget, } from './lib/research/resolve-target.js';
|
|
5
|
-
import {
|
|
8
|
+
import { failInvalidUrl } from './lib/research/cli-io.js';
|
|
6
9
|
/**
|
|
7
10
|
* Shared base for every Bonsai command. Enables oclif's native `--json` flag,
|
|
8
11
|
* parses args/flags once in `init()` so commands read `this.args`/`this.flags`
|
|
@@ -21,8 +24,7 @@ export class BaseCommand extends Command {
|
|
|
21
24
|
'read-only': Flags.boolean({
|
|
22
25
|
aliases: ['plan'],
|
|
23
26
|
default: false,
|
|
24
|
-
description:
|
|
25
|
-
'Also honored via BONSAI_READ_ONLY/BONSAI_PLAN_MODE — enable for agent read-only/plan modes.',
|
|
27
|
+
description: CLI_FLAG_DESCRIPTIONS.readOnly,
|
|
26
28
|
}),
|
|
27
29
|
};
|
|
28
30
|
flags;
|
|
@@ -71,14 +73,6 @@ export class BaseCommand extends Command {
|
|
|
71
73
|
}
|
|
72
74
|
return explicitDryRun || this.readOnly;
|
|
73
75
|
}
|
|
74
|
-
/**
|
|
75
|
-
* The exit code an error maps to. oclif `CLIError`s carry it in `err.oclif.exit`; other errors may
|
|
76
|
-
* expose `err.exitCode`. Defined once so the process exit code (`catch`) and the JSON envelope
|
|
77
|
-
* (`toErrorJson`) can never disagree — a mismatch between these two was itself the bug this fixes.
|
|
78
|
-
*/
|
|
79
|
-
static exitCodeOf(err) {
|
|
80
|
-
return err?.oclif?.exit ?? err?.exitCode ?? 1;
|
|
81
|
-
}
|
|
82
76
|
/**
|
|
83
77
|
* Align the process exit code with the code reported in the JSON envelope. oclif's default
|
|
84
78
|
* `catch` sets `process.exitCode = err.exitCode ?? 1`, but oclif `CLIError`s carry their code in
|
|
@@ -92,16 +86,10 @@ export class BaseCommand extends Command {
|
|
|
92
86
|
// [UnparsedCommand] warning to stderr even under `--json`. CLIParseError subclasses carry `parse`.
|
|
93
87
|
if (err && typeof err === 'object' && 'parse' in err)
|
|
94
88
|
this.parsed = true;
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (err && typeof err === 'object' && !err.code) {
|
|
100
|
-
const code = stableErrorCodeFrom(err);
|
|
101
|
-
if (code)
|
|
102
|
-
err.code = code;
|
|
103
|
-
}
|
|
104
|
-
process.exitCode = process.exitCode ?? BaseCommand.exitCodeOf(err);
|
|
89
|
+
// Unwrap / fuzzy tips are no-ops when not applicable (including non-Error throws with no message).
|
|
90
|
+
enrichParseError(err);
|
|
91
|
+
enrichErrorForDisplay(err, { bin: this.config.bin, command: this.envelopeCommandId() });
|
|
92
|
+
process.exitCode = process.exitCode ?? resolveExitCode(err);
|
|
105
93
|
return super.catch(err);
|
|
106
94
|
}
|
|
107
95
|
/** Command id for the JSON envelope; falls back to the binary name when a command has no id. */
|
|
@@ -128,16 +116,22 @@ export class BaseCommand extends Command {
|
|
|
128
116
|
this.failInvalidUrl(url, err.message);
|
|
129
117
|
}
|
|
130
118
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Single exit point for a URL that failed normalization, shared by every URL-accepting command.
|
|
133
|
-
* The scheme-less message is already self-contained and actionable, so it is surfaced as-is;
|
|
134
|
-
* other parse failures keep the "Invalid URL:" prefix for context.
|
|
135
|
-
*/
|
|
119
|
+
/** Exit for a URL that failed normalization; see {@link failInvalidUrl} for the code contract. */
|
|
136
120
|
failInvalidUrl(url, message) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
121
|
+
failInvalidUrl((msg, opts) => this.error(msg, opts), url, message);
|
|
122
|
+
}
|
|
123
|
+
/** Command context handed to command services (fetch/import) so they stay oclif-free. */
|
|
124
|
+
cliIo() {
|
|
125
|
+
return {
|
|
126
|
+
bin: this.config.bin,
|
|
127
|
+
configDir: this.config.configDir,
|
|
128
|
+
dataDir: this.config.dataDir,
|
|
129
|
+
cwd: process.cwd(),
|
|
130
|
+
json: this.jsonEnabled(),
|
|
131
|
+
warn: (msg) => void this.warn(msg),
|
|
132
|
+
log: (msg) => this.log(msg),
|
|
133
|
+
error: (msg, opts) => this.error(msg, opts),
|
|
134
|
+
};
|
|
141
135
|
}
|
|
142
136
|
/** Single source of truth for the `--json` envelope shape, shared by success and error output. */
|
|
143
137
|
envelope(parts) {
|
|
@@ -149,33 +143,76 @@ export class BaseCommand extends Command {
|
|
|
149
143
|
data: parts.data,
|
|
150
144
|
});
|
|
151
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Base success envelope from `process.exitCode` (including exit 5 = served stale / ok).
|
|
148
|
+
* Subclasses that overlay batch failures must call this — not `toSuccessJson` — to avoid
|
|
149
|
+
* recursing through their own override.
|
|
150
|
+
*/
|
|
151
|
+
baseSuccessJson(data) {
|
|
152
|
+
// ponytail: the stale-serve path signals EXIT_STALE_SERVED by setting process.exitCode inside
|
|
153
|
+
// run() (e.g. fetch's handleStaleRevalidationResult); this reads it back to mark the envelope.
|
|
154
|
+
// If that signalling moves off process.exitCode, update here too. Number() normalizes Node's
|
|
155
|
+
// string codes.
|
|
156
|
+
const exitCode = Number(process.exitCode ?? EXIT_OK);
|
|
157
|
+
// EXIT_STALE_SERVED means "served stale" — a successful, usable result, so it reports ok.
|
|
158
|
+
return this.envelope({
|
|
159
|
+
ok: exitCode === EXIT_OK || exitCode === EXIT_STALE_SERVED,
|
|
160
|
+
exitCode,
|
|
161
|
+
stderr: '',
|
|
162
|
+
data,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
152
165
|
/** Wrap a command's return value in the machine-readable envelope emitted under `--json`. */
|
|
153
166
|
toSuccessJson(data) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
return this.baseSuccessJson(data);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Success overlay for multi-URL read commands (`status`, `inspect`). Apply CACHE_MISS first,
|
|
171
|
+
* then row `.error` so validation failures win when both appear in one batch.
|
|
172
|
+
*/
|
|
173
|
+
batchReadSuccessJson(data) {
|
|
174
|
+
return enrichRowErrorEnvelope(enrichCacheMissEnvelope(this.baseSuccessJson(data), data, this.config.bin), data);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Map each URL through `fn`. In a multi-URL batch, per-URL CLIErrors become failure rows so
|
|
178
|
+
* prior hits are kept — matching fetch's INVALID_URL / MISSING_URL_SCHEME batch contract.
|
|
179
|
+
*/
|
|
180
|
+
mapUrlsAllowingBatchErrors(urls, fn, failureRow) {
|
|
181
|
+
const batch = urls.length > 1;
|
|
182
|
+
const results = [];
|
|
183
|
+
for (const url of urls) {
|
|
184
|
+
try {
|
|
185
|
+
results.push(fn(url));
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
if (!batch || !(err instanceof Errors.CLIError))
|
|
189
|
+
throw err;
|
|
190
|
+
// Mirror the single-URL error format (message + Code: + Try this:) rather than a bare
|
|
191
|
+
// message, so a row failure in a batch is exactly as actionable as it is standalone.
|
|
192
|
+
if (!this.jsonEnabled())
|
|
193
|
+
this.warn(formatErrorForJson(err));
|
|
194
|
+
results.push(failureRow(url, err));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return results;
|
|
160
198
|
}
|
|
161
199
|
/** Mirror the success envelope for failures so JSON consumers get one consistent shape. */
|
|
162
200
|
toErrorJson(err) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
: String(err);
|
|
201
|
+
const prepared = prepareCliError(err, {
|
|
202
|
+
bin: this.config.bin,
|
|
203
|
+
command: this.envelopeCommandId(),
|
|
204
|
+
});
|
|
205
|
+
// oclif prints the returned object to stdout; error text lives in envelope.stderr only
|
|
206
|
+
// so process stderr stays clean under `--json` (CACHE_MISS / batch overlays + oclif JSON log suppression).
|
|
170
207
|
return buildEnvelope({
|
|
171
208
|
command: this.envelopeCommandId(),
|
|
172
209
|
ok: false,
|
|
173
|
-
exitCode,
|
|
174
|
-
stderr,
|
|
210
|
+
exitCode: prepared.exitCode,
|
|
211
|
+
stderr: prepared.stderr,
|
|
175
212
|
data: null,
|
|
176
|
-
code,
|
|
177
|
-
suggestions:
|
|
178
|
-
ref:
|
|
213
|
+
code: prepared.code,
|
|
214
|
+
suggestions: prepared.suggestions,
|
|
215
|
+
ref: prepared.ref,
|
|
179
216
|
});
|
|
180
217
|
}
|
|
181
218
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Flags } from '@oclif/core';
|
|
1
|
+
import { Flags, toConfiguredId } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../base-command.js';
|
|
3
3
|
import { isKnownKey, suggestKey, validKeysHint } from '../../lib/config/index.js';
|
|
4
4
|
/**
|
|
@@ -30,15 +30,24 @@ export class ConfigCommand extends BaseCommand {
|
|
|
30
30
|
if (!isKnownKey(keyArg)) {
|
|
31
31
|
const suggestion = suggestKey(keyArg);
|
|
32
32
|
const hint = suggestion ? ` Did you mean: ${suggestion}?` : '';
|
|
33
|
+
const command = this.ctor.id ? toConfiguredId(this.ctor.id, this.config) : 'config';
|
|
33
34
|
this.error(`Unknown config key: "${keyArg}".${hint} Valid keys: ${validKeysHint()}.`, {
|
|
34
35
|
exit: 2,
|
|
35
36
|
code: 'UNKNOWN_KEY',
|
|
37
|
+
suggestions: suggestion
|
|
38
|
+
? [`Use "${suggestion}": ${this.config.bin} ${command} ${suggestion}`]
|
|
39
|
+
: [`Use one of: ${validKeysHint()}`],
|
|
36
40
|
});
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
requireConfigKey(key) {
|
|
40
44
|
if (!key) {
|
|
41
|
-
this.
|
|
45
|
+
const command = this.ctor.id ? toConfiguredId(this.ctor.id, this.config) : 'config';
|
|
46
|
+
this.error('Missing required argument: key', {
|
|
47
|
+
exit: 2,
|
|
48
|
+
code: 'MISSING_ARGUMENT',
|
|
49
|
+
suggestions: [`Choose a key: ${this.config.bin} ${command} storage`],
|
|
50
|
+
});
|
|
42
51
|
}
|
|
43
52
|
this.assertKnownKey(key);
|
|
44
53
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { ConfigCommand, configScopeFlags } from './base.js';
|
|
3
|
-
import {
|
|
3
|
+
import { formatConfigEntry, resolveConfigEntry, validKeysHint } from '../../lib/config/index.js';
|
|
4
4
|
export default class ConfigGet extends ConfigCommand {
|
|
5
5
|
static id = 'config get';
|
|
6
|
-
static summary = 'Get
|
|
7
|
-
static description = `Print a config value. Without --global/--local, shows the merged effective value
|
|
6
|
+
static summary = 'Get a research configuration value';
|
|
7
|
+
static description = `Print a config value. Without --global/--local, shows the merged effective value, in precedence order: a per-command override flag (e.g. \`fetch --storage\`) > env var > project file > user file > built-in default.\n\nValid keys: ${validKeysHint()}.`;
|
|
8
8
|
static examples = [
|
|
9
9
|
{
|
|
10
10
|
description: 'get the effective storage mode',
|
|
@@ -20,8 +20,8 @@ export default class ConfigGet extends ConfigCommand {
|
|
|
20
20
|
};
|
|
21
21
|
static flags = {
|
|
22
22
|
...configScopeFlags({
|
|
23
|
-
global: '
|
|
24
|
-
local: '
|
|
23
|
+
global: 'read user-level config only',
|
|
24
|
+
local: 'read project-level config only',
|
|
25
25
|
}),
|
|
26
26
|
};
|
|
27
27
|
static stdoutIsPrimaryData = true;
|
|
@@ -29,13 +29,9 @@ export default class ConfigGet extends ConfigCommand {
|
|
|
29
29
|
const key = this.args.key;
|
|
30
30
|
this.validateConfigKeyAndScope(key, this.flags.global, this.flags.local);
|
|
31
31
|
const scope = this.readScope(this.flags.global, this.flags.local);
|
|
32
|
-
const
|
|
33
|
-
const meta = KEY_META[key];
|
|
34
|
-
const displayValue = value !== undefined ? value : BUILT_IN_DEFAULTS[key];
|
|
35
|
-
const formatted = meta.format(displayValue);
|
|
36
|
-
const suffix = value === undefined ? ' (not configured)' : '';
|
|
32
|
+
const entry = resolveConfigEntry(key, scope, this.config.configDir, process.cwd());
|
|
37
33
|
if (!this.jsonEnabled())
|
|
38
|
-
this.log(
|
|
39
|
-
return
|
|
34
|
+
this.log(formatConfigEntry(entry, scope));
|
|
35
|
+
return entry;
|
|
40
36
|
}
|
|
41
37
|
}
|
|
@@ -2,7 +2,7 @@ import { Help, toConfiguredId } from '@oclif/core';
|
|
|
2
2
|
import { BaseCommand } from '../../base-command.js';
|
|
3
3
|
export default class ConfigIndex extends BaseCommand {
|
|
4
4
|
static id = 'config';
|
|
5
|
-
static summary = 'Manage research cache
|
|
5
|
+
static summary = 'Manage research cache configuration';
|
|
6
6
|
static description = 'Show config subcommands for reading and writing Bonsai research cache configuration.';
|
|
7
7
|
static examples = [
|
|
8
8
|
{
|
|
@@ -10,7 +10,7 @@ export default class ConfigIndex extends BaseCommand {
|
|
|
10
10
|
command: '<%= config.bin %> <%= command.id %> get storage',
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
|
-
description: 'set
|
|
13
|
+
description: 'set project storage mode',
|
|
14
14
|
command: '<%= config.bin %> <%= command.id %> set storage project --local',
|
|
15
15
|
},
|
|
16
16
|
];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ConfigCommand, configScopeFlags } from './base.js';
|
|
2
|
-
import {
|
|
2
|
+
import { formatConfigEntry, resolveConfigEntries, validKeysHint } from '../../lib/config/index.js';
|
|
3
3
|
export default class ConfigList extends ConfigCommand {
|
|
4
4
|
static id = 'config list';
|
|
5
|
-
static summary = 'List
|
|
5
|
+
static summary = 'List research configuration values';
|
|
6
6
|
static description = `Show every configuration key with its current value. Use --global/--local to show a single scope.\n\nValid keys: ${validKeysHint()}.`;
|
|
7
7
|
static examples = [
|
|
8
8
|
{
|
|
@@ -16,28 +16,22 @@ export default class ConfigList extends ConfigCommand {
|
|
|
16
16
|
];
|
|
17
17
|
static flags = {
|
|
18
18
|
...configScopeFlags({
|
|
19
|
-
global: '
|
|
20
|
-
local: '
|
|
19
|
+
global: 'show user-level config only',
|
|
20
|
+
local: 'show project-level config only',
|
|
21
21
|
}),
|
|
22
22
|
};
|
|
23
23
|
static stdoutIsPrimaryData = true;
|
|
24
24
|
async run() {
|
|
25
25
|
this.assertScopeFlagsExclusive(this.flags.global, this.flags.local);
|
|
26
26
|
const scope = this.readScope(this.flags.global, this.flags.local);
|
|
27
|
-
const
|
|
27
|
+
const entries = resolveConfigEntries(scope, this.config.configDir, process.cwd());
|
|
28
28
|
if (!this.jsonEnabled()) {
|
|
29
|
-
const width = Math.max(...
|
|
30
|
-
for (const
|
|
31
|
-
|
|
32
|
-
const formatted = raw !== undefined ? KEY_META[key].format(raw) : '(not configured)';
|
|
33
|
-
this.log(`${key.padEnd(width)}${formatted}`);
|
|
29
|
+
const width = Math.max(...entries.map((e) => e.key.length)) + 2;
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
this.log(`${entry.key.padEnd(width)}${formatConfigEntry(entry, scope)}`);
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const raw = scoped[key];
|
|
39
|
-
values[key] = raw !== undefined ? raw : BUILT_IN_DEFAULTS[key];
|
|
40
|
-
}
|
|
41
|
-
return values;
|
|
34
|
+
// Bare array — same shape as `list`'s data payload; no `{ entries }` wrapper.
|
|
35
|
+
return entries;
|
|
42
36
|
}
|
|
43
37
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConfigCommand } from './base.js';
|
|
2
|
+
import type { ConfigWriteResult } from '../../lib/cli-result-types.js';
|
|
2
3
|
export default class ConfigSet extends ConfigCommand<typeof ConfigSet> {
|
|
3
4
|
static id: string;
|
|
4
5
|
static summary: string;
|
|
@@ -17,5 +18,5 @@ export default class ConfigSet extends ConfigCommand<typeof ConfigSet> {
|
|
|
17
18
|
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
19
|
};
|
|
19
20
|
static stdoutIsPrimaryData: boolean;
|
|
20
|
-
run(): Promise<
|
|
21
|
+
run(): Promise<ConfigWriteResult>;
|
|
21
22
|
}
|