@taurgis/bonsai 3.1.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 +21 -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 +37 -10
- package/dist/base-command.js +113 -46
- 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 +58 -401
- package/dist/commands/import.d.ts +4 -9
- package/dist/commands/import.js +56 -280
- 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 +56 -65
- 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 +22 -4
- package/dist/lib/config/index.js +59 -4
- package/dist/lib/config/resolve.d.ts +21 -0
- package/dist/lib/config/resolve.js +55 -6
- 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 +11 -1
- package/dist/lib/research/secure-write.js +38 -7
- 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 +192 -87
- 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
|
@@ -2,6 +2,26 @@ import { BUILT_IN_DEFAULTS, STORAGE_MODES, SUMMARY_LEVELS } from './schema.js';
|
|
|
2
2
|
/** Scoped env override, following oclif's `<BIN>_*` env-var convention for this CLI. */
|
|
3
3
|
export const STORAGE_ENV_VAR = 'BONSAI_STORAGE';
|
|
4
4
|
export const SUMMARY_ENV_VAR = 'BONSAI_SUMMARY';
|
|
5
|
+
/** Two names for the same safety switch: `PLAN_MODE` mirrors agent-harness "plan mode" terminology. */
|
|
6
|
+
export const READ_ONLY_ENV_VAR = 'BONSAI_READ_ONLY';
|
|
7
|
+
export const PLAN_MODE_ENV_VAR = 'BONSAI_PLAN_MODE';
|
|
8
|
+
const BOOLEAN_TRUE_VALUES = ['1', 'true', 'yes'];
|
|
9
|
+
const BOOLEAN_FALSE_VALUES = ['0', 'false', 'no'];
|
|
10
|
+
/**
|
|
11
|
+
* Parses a boolean-ish env value. Empty/unset is `undefined` (not present); an unrecognized
|
|
12
|
+
* non-empty value is also `undefined` (invalid — see {@link invalidEnvOverrideWarnings}), so a
|
|
13
|
+
* typo never silently reads as `true`.
|
|
14
|
+
*/
|
|
15
|
+
export function parseEnvBoolean(raw) {
|
|
16
|
+
const v = raw?.trim().toLowerCase();
|
|
17
|
+
if (!v)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (BOOLEAN_TRUE_VALUES.includes(v))
|
|
20
|
+
return true;
|
|
21
|
+
if (BOOLEAN_FALSE_VALUES.includes(v))
|
|
22
|
+
return false;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* Warnings for Bonsai env overrides that are set but hold an unrecognized value. Such a value is
|
|
7
27
|
* dropped during resolution (falling back to config/default), which would otherwise silently mask
|
|
@@ -9,18 +29,36 @@ export const SUMMARY_ENV_VAR = 'BONSAI_SUMMARY';
|
|
|
9
29
|
* is visible without breaking the run. Returns one message per offending variable, or empty.
|
|
10
30
|
*/
|
|
11
31
|
export function invalidEnvOverrideWarnings(env) {
|
|
32
|
+
// Each check names the var, how to tell a value is invalid, and the hint to show when it is.
|
|
12
33
|
const checks = [
|
|
13
|
-
[
|
|
14
|
-
|
|
34
|
+
[
|
|
35
|
+
STORAGE_ENV_VAR,
|
|
36
|
+
(raw) => STORAGE_MODES.includes(raw),
|
|
37
|
+
`not one of ${STORAGE_MODES.join(', ')}. Using the configured value or default instead.`,
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
SUMMARY_ENV_VAR,
|
|
41
|
+
(raw) => SUMMARY_LEVELS.includes(raw),
|
|
42
|
+
`not one of ${SUMMARY_LEVELS.join(', ')}. Using the configured value or default instead.`,
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
READ_ONLY_ENV_VAR,
|
|
46
|
+
(raw) => parseEnvBoolean(raw) !== undefined,
|
|
47
|
+
'expected 1/true/yes or 0/false/no. Read-only mode was not toggled by this value.',
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
PLAN_MODE_ENV_VAR,
|
|
51
|
+
(raw) => parseEnvBoolean(raw) !== undefined,
|
|
52
|
+
'expected 1/true/yes or 0/false/no. Read-only mode was not toggled by this value.',
|
|
53
|
+
],
|
|
15
54
|
];
|
|
16
55
|
const warnings = [];
|
|
17
|
-
for (const [name,
|
|
56
|
+
for (const [name, isValid, hint] of checks) {
|
|
18
57
|
// After trim, an empty/whitespace-only value is falsy and treated as "unset" (no warning),
|
|
19
58
|
// matching parseEnv*; matching is case-sensitive, so a wrong-case value is invalid and warns.
|
|
20
59
|
const raw = env[name]?.trim();
|
|
21
|
-
if (raw && !
|
|
22
|
-
warnings.push(`Ignoring ${name}="${raw}":
|
|
23
|
-
`Using the configured value or default instead.`);
|
|
60
|
+
if (raw && !isValid(raw)) {
|
|
61
|
+
warnings.push(`Ignoring ${name}="${raw}": ${hint}`);
|
|
24
62
|
}
|
|
25
63
|
}
|
|
26
64
|
return warnings;
|
|
@@ -59,3 +97,14 @@ export function resolveSummaryLevel(input) {
|
|
|
59
97
|
input.userConfig.summary ??
|
|
60
98
|
BUILT_IN_DEFAULTS.summary);
|
|
61
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve whether read-only (plan) mode is active. Unlike storage/summary, this is a safety gate
|
|
102
|
+
* rather than a preference: composition is OR, not override-by-precedence — if the flag or either
|
|
103
|
+
* env var says read-only, the run is read-only. There is no way to force writes back on when an
|
|
104
|
+
* ambient `BONSAI_READ_ONLY`/`BONSAI_PLAN_MODE` is set, by design.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveReadOnly(input) {
|
|
107
|
+
return Boolean(input.flag ||
|
|
108
|
+
parseEnvBoolean(input.env[READ_ONLY_ENV_VAR]) ||
|
|
109
|
+
parseEnvBoolean(input.env[PLAN_MODE_ENV_VAR]));
|
|
110
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ConfigKey, type ConfigValues } from './config/index.js';
|
|
2
|
+
/** Scope for config writes (`user` = XDG config dir; `project` = cwd `.bonsai`). */
|
|
3
|
+
export type ConfigWriteScope = 'user' | 'project';
|
|
4
|
+
/** Successful config persist outcome. */
|
|
5
|
+
export interface ConfigPersistResult {
|
|
6
|
+
ok: true;
|
|
7
|
+
}
|
|
8
|
+
/** Failed config persist outcome with stable code and recovery tips. */
|
|
9
|
+
export interface ConfigPersistFailure {
|
|
10
|
+
ok: false;
|
|
11
|
+
code: 'CONFIG_DIR_UNAVAILABLE';
|
|
12
|
+
message: string;
|
|
13
|
+
suggestions: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Persist a config patch to user or project scope. Pure of oclif — callers map failures to
|
|
17
|
+
* this.error(...). Shared by config set and unset so the write branch cannot drift.
|
|
18
|
+
*/
|
|
19
|
+
export declare function persistConfigPatch(opts: {
|
|
20
|
+
scope: ConfigWriteScope;
|
|
21
|
+
cwd: string;
|
|
22
|
+
configDir: string | undefined;
|
|
23
|
+
patch: Partial<ConfigValues>;
|
|
24
|
+
/** Used in failure suggestions (set vs unset). */
|
|
25
|
+
action: 'set' | 'unset';
|
|
26
|
+
bin: string;
|
|
27
|
+
key: ConfigKey;
|
|
28
|
+
}): ConfigPersistResult | ConfigPersistFailure;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { writeUserConfig, writeProjectConfig, } from './config/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Persist a config patch to user or project scope. Pure of oclif — callers map failures to
|
|
4
|
+
* this.error(...). Shared by config set and unset so the write branch cannot drift.
|
|
5
|
+
*/
|
|
6
|
+
export function persistConfigPatch(opts) {
|
|
7
|
+
if (opts.scope === 'project') {
|
|
8
|
+
writeProjectConfig(opts.cwd, opts.patch);
|
|
9
|
+
return { ok: true };
|
|
10
|
+
}
|
|
11
|
+
if (!opts.configDir) {
|
|
12
|
+
const verb = opts.action === 'set' ? 'write' : 'remove';
|
|
13
|
+
const example = opts.action === 'set'
|
|
14
|
+
? `${opts.bin} config set ${opts.key} <value> --local`
|
|
15
|
+
: `${opts.bin} config unset ${opts.key} --local`;
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
code: 'CONFIG_DIR_UNAVAILABLE',
|
|
19
|
+
message: `Could not determine user config directory. Use --local to ${verb} project config.`,
|
|
20
|
+
suggestions: [
|
|
21
|
+
opts.action === 'set'
|
|
22
|
+
? `Write project config instead: ${example}`
|
|
23
|
+
: `Remove project config instead: ${example}`,
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
writeUserConfig(opts.configDir, opts.patch);
|
|
28
|
+
return { ok: true };
|
|
29
|
+
}
|
package/dist/lib/envelope.d.ts
CHANGED
|
@@ -7,8 +7,12 @@ export interface CliErrorShape {
|
|
|
7
7
|
}
|
|
8
8
|
/** Map oclif parse-time errors to stable Bonsai codes when the throw site did not set one. */
|
|
9
9
|
export declare function stableErrorCodeFrom(err: unknown): string | undefined;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Strip oclif's generic help suffix and the `Parsing --flag` wrapper so agents and humans see the
|
|
12
|
+
* actionable message we threw (e.g. `Limit must be between 1 and 100.`) instead of the wrapper.
|
|
13
|
+
*/
|
|
11
14
|
export declare function normalizeCliErrorMessage(message: string): string;
|
|
15
|
+
/** Machine-readable envelope fields for `--json` success and error paths. */
|
|
12
16
|
export interface EnvelopeParts {
|
|
13
17
|
command: string;
|
|
14
18
|
ok: boolean;
|
|
@@ -25,12 +29,46 @@ export interface EnvelopeParts {
|
|
|
25
29
|
* without ANSI or the leading "Error:" prefix (the message is already the primary line).
|
|
26
30
|
*/
|
|
27
31
|
export declare function formatErrorForJson(err: CliErrorShape): string;
|
|
32
|
+
/** Stable CLIError fields for batch failure rows (status/inspect/fetch). */
|
|
33
|
+
export declare function cliErrorFields(err: {
|
|
34
|
+
code?: string;
|
|
35
|
+
message?: string;
|
|
36
|
+
suggestions?: string[];
|
|
37
|
+
ref?: string;
|
|
38
|
+
}, fallbackCode?: string): {
|
|
39
|
+
code: string;
|
|
40
|
+
message: string;
|
|
41
|
+
suggestions?: string[];
|
|
42
|
+
ref?: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* CACHE_MISS overlay. Message wording is fixed so status/inspect cannot drift.
|
|
46
|
+
*/
|
|
47
|
+
export declare function enrichCacheMissEnvelope(envelope: Record<string, unknown>, data: unknown, bin: string): Record<string, unknown>;
|
|
48
|
+
/**
|
|
49
|
+
* Per-row `.error` overlay for multi-URL batches (fetch, status, inspect).
|
|
50
|
+
* Keeps prior successes in `data` and surfaces the first row's code/message.
|
|
51
|
+
*/
|
|
52
|
+
export declare function enrichRowErrorEnvelope(envelope: Record<string, unknown>, data: unknown): Record<string, unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Partial prune unlink failure: candidates remain in `data`, envelope reports the stable code.
|
|
55
|
+
*/
|
|
56
|
+
export declare function enrichPrunePartialEnvelope(envelope: Record<string, unknown>, data: unknown): Record<string, unknown>;
|
|
28
57
|
/**
|
|
29
58
|
* Builds the standard Bonsai CLI JSON envelope structure.
|
|
30
59
|
* This is the single source of truth for the output envelope schema.
|
|
31
60
|
*/
|
|
32
61
|
export declare function buildEnvelope(parts: EnvelopeParts): Record<string, unknown>;
|
|
33
62
|
/**
|
|
34
|
-
*
|
|
63
|
+
* Usage/not-found style error envelope: message + Code (+ Try this) in `stderr`, stable `code`,
|
|
64
|
+
* and top-level `suggestions` when present. Shared by argv preflight, help-preflight, and the
|
|
65
|
+
* command_not_found hook so those paths cannot drift.
|
|
35
66
|
*/
|
|
36
|
-
export declare function
|
|
67
|
+
export declare function buildCliErrorEnvelope(opts: {
|
|
68
|
+
command: string;
|
|
69
|
+
message: string;
|
|
70
|
+
code: string;
|
|
71
|
+
suggestions?: string[];
|
|
72
|
+
exitCode?: number;
|
|
73
|
+
ref?: string;
|
|
74
|
+
}): Record<string, unknown>;
|
package/dist/lib/envelope.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EXIT_RUNTIME_FAILURE } from './cli-error-policy.js';
|
|
1
2
|
/** Map oclif parse-time errors to stable Bonsai codes when the throw site did not set one. */
|
|
2
3
|
export function stableErrorCodeFrom(err) {
|
|
3
4
|
const e = err;
|
|
@@ -21,10 +22,23 @@ export function stableErrorCodeFrom(err) {
|
|
|
21
22
|
/^Flag --\S+ expects (a value$|one of these values:)/.test(e.message)) {
|
|
22
23
|
return 'MISSING_FLAG_VALUE';
|
|
23
24
|
}
|
|
25
|
+
// Repeating a single-value flag (e.g. `--topic a --topic b`) is also a plain CLIError with no
|
|
26
|
+
// dedicated class — oclif's parser rejects it before any flag-specific validation runs.
|
|
27
|
+
if (typeof e?.message === 'string' && /^Flag --\S+ can only be specified once$/.test(e.message)) {
|
|
28
|
+
return 'DUPLICATE_FLAG';
|
|
29
|
+
}
|
|
24
30
|
return undefined;
|
|
25
31
|
}
|
|
26
|
-
/**
|
|
32
|
+
/** oclif wraps custom flag parse failures as `Parsing --name \n\t…\nSee more help with --help`. */
|
|
33
|
+
const PARSING_WRAPPER = /^Parsing --\S+ \n\t([\s\S]*?)(?:\nSee more help with --help)?$/;
|
|
34
|
+
/**
|
|
35
|
+
* Strip oclif's generic help suffix and the `Parsing --flag` wrapper so agents and humans see the
|
|
36
|
+
* actionable message we threw (e.g. `Limit must be between 1 and 100.`) instead of the wrapper.
|
|
37
|
+
*/
|
|
27
38
|
export function normalizeCliErrorMessage(message) {
|
|
39
|
+
const wrapped = message.match(PARSING_WRAPPER);
|
|
40
|
+
if (wrapped?.[1])
|
|
41
|
+
return wrapped[1];
|
|
28
42
|
const suffix = '\nSee more help with --help';
|
|
29
43
|
return message.endsWith(suffix) ? message.slice(0, -suffix.length) : message;
|
|
30
44
|
}
|
|
@@ -52,6 +66,119 @@ export function formatErrorForJson(err) {
|
|
|
52
66
|
lines.push(`Reference: ${err.ref}`);
|
|
53
67
|
return lines.join('\n');
|
|
54
68
|
}
|
|
69
|
+
/** Stable CLIError fields for batch failure rows (status/inspect/fetch). */
|
|
70
|
+
export function cliErrorFields(err, fallbackCode = 'INVALID_URL') {
|
|
71
|
+
return {
|
|
72
|
+
code: typeof err.code === 'string' && err.code ? err.code : fallbackCode,
|
|
73
|
+
message: typeof err.message === 'string' ? err.message : String(err),
|
|
74
|
+
suggestions: err.suggestions,
|
|
75
|
+
ref: err.ref,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Overlay a batch failure onto a success envelope while keeping per-row `data`.
|
|
80
|
+
* Shared by multi-URL status/inspect (CACHE_MISS) and fetch/row errors.
|
|
81
|
+
*/
|
|
82
|
+
function enrichBatchFailureEnvelope(envelope, data, opts) {
|
|
83
|
+
const list = Array.isArray(data) ? data : [data];
|
|
84
|
+
const failures = list.map(opts.pick).filter(Boolean);
|
|
85
|
+
if (failures.length === 0)
|
|
86
|
+
return envelope;
|
|
87
|
+
const first = failures[0];
|
|
88
|
+
const code = typeof opts.code === 'function' ? opts.code(first, failures) : opts.code;
|
|
89
|
+
const suggestions = opts.suggestions?.(failures);
|
|
90
|
+
const ref = opts.ref?.(first);
|
|
91
|
+
const stderr = formatErrorForJson({
|
|
92
|
+
message: opts.message(first, failures),
|
|
93
|
+
code,
|
|
94
|
+
suggestions,
|
|
95
|
+
ref,
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
...envelope,
|
|
99
|
+
ok: false,
|
|
100
|
+
exitCode: opts.exitCode ?? 1,
|
|
101
|
+
stderr,
|
|
102
|
+
code,
|
|
103
|
+
suggestions,
|
|
104
|
+
ref,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* CACHE_MISS overlay. Message wording is fixed so status/inspect cannot drift.
|
|
109
|
+
*/
|
|
110
|
+
export function enrichCacheMissEnvelope(envelope, data, bin) {
|
|
111
|
+
return enrichBatchFailureEnvelope(envelope, data, {
|
|
112
|
+
pick: (row) => {
|
|
113
|
+
const r = row;
|
|
114
|
+
return r?.status === 'miss' ? r : null;
|
|
115
|
+
},
|
|
116
|
+
code: 'CACHE_MISS',
|
|
117
|
+
exitCode: EXIT_RUNTIME_FAILURE,
|
|
118
|
+
message: (first, failures) => {
|
|
119
|
+
const url = first.normalizedUrl ?? '';
|
|
120
|
+
return failures.length > 1
|
|
121
|
+
? `Cache miss for ${url} and ${failures.length - 1} other URLs`
|
|
122
|
+
: `Cache miss for ${url}`;
|
|
123
|
+
},
|
|
124
|
+
// A URL that is already a secondary source of a different cached artifact must not be pointed
|
|
125
|
+
// at a plain fetch — that would create an unrelated duplicate entry instead of finding the
|
|
126
|
+
// note that already covers it.
|
|
127
|
+
suggestions: (failures) => failures.map((m) => m.partOfExistingNote
|
|
128
|
+
? `Find it with: ${bin} list --url "${m.normalizedUrl}"`
|
|
129
|
+
: `Fetch and cache it first: ${bin} ${m.normalizedUrl}`),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Per-row `.error` overlay for multi-URL batches (fetch, status, inspect).
|
|
134
|
+
* Keeps prior successes in `data` and surfaces the first row's code/message.
|
|
135
|
+
*/
|
|
136
|
+
export function enrichRowErrorEnvelope(envelope, data) {
|
|
137
|
+
return enrichBatchFailureEnvelope(envelope, data, {
|
|
138
|
+
pick: (row) => {
|
|
139
|
+
if (!row || typeof row !== 'object')
|
|
140
|
+
return null;
|
|
141
|
+
return row.error ?? null;
|
|
142
|
+
},
|
|
143
|
+
code: (first) => first.code ?? 'FETCH_FAILED',
|
|
144
|
+
message: (first, failures) => (first.message ?? '') +
|
|
145
|
+
(failures.length > 1 ? `\n…and ${failures.length - 1} other URL failure(s)` : ''),
|
|
146
|
+
suggestions: (failures) => failures[0]?.suggestions,
|
|
147
|
+
ref: (first) => first.ref,
|
|
148
|
+
exitCode: EXIT_RUNTIME_FAILURE,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Partial prune unlink failure: candidates remain in `data`, envelope reports the stable code.
|
|
153
|
+
*/
|
|
154
|
+
export function enrichPrunePartialEnvelope(envelope, data) {
|
|
155
|
+
if (!isPruneCounts(data))
|
|
156
|
+
return envelope;
|
|
157
|
+
// A dry run never deletes anything, so prunedCount is always 0 by design — that is a preview,
|
|
158
|
+
// not a partial failure, and must never be reported as one.
|
|
159
|
+
if (data.dryRun)
|
|
160
|
+
return envelope;
|
|
161
|
+
if (data.candidateCount <= 0 || data.prunedCount >= data.candidateCount)
|
|
162
|
+
return envelope;
|
|
163
|
+
const failed = data.candidateCount - data.prunedCount;
|
|
164
|
+
const code = 'PRUNE_PARTIAL_FAILURE';
|
|
165
|
+
const message = `Failed to delete ${failed} of ${data.candidateCount} cache ${failed === 1 ? 'entry' : 'entries'}.`;
|
|
166
|
+
return {
|
|
167
|
+
...envelope,
|
|
168
|
+
ok: false,
|
|
169
|
+
exitCode: EXIT_RUNTIME_FAILURE,
|
|
170
|
+
code,
|
|
171
|
+
stderr: formatErrorForJson({ message, code }),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function isPruneCounts(data) {
|
|
175
|
+
if (!data || typeof data !== 'object')
|
|
176
|
+
return false;
|
|
177
|
+
const d = data;
|
|
178
|
+
return (typeof d.prunedCount === 'number' &&
|
|
179
|
+
typeof d.candidateCount === 'number' &&
|
|
180
|
+
typeof d.dryRun === 'boolean');
|
|
181
|
+
}
|
|
55
182
|
/**
|
|
56
183
|
* Builds the standard Bonsai CLI JSON envelope structure.
|
|
57
184
|
* This is the single source of truth for the output envelope schema.
|
|
@@ -75,8 +202,24 @@ export function buildEnvelope(parts) {
|
|
|
75
202
|
return envelope;
|
|
76
203
|
}
|
|
77
204
|
/**
|
|
78
|
-
*
|
|
205
|
+
* Usage/not-found style error envelope: message + Code (+ Try this) in `stderr`, stable `code`,
|
|
206
|
+
* and top-level `suggestions` when present. Shared by argv preflight, help-preflight, and the
|
|
207
|
+
* command_not_found hook so those paths cannot drift.
|
|
79
208
|
*/
|
|
80
|
-
export function
|
|
81
|
-
return
|
|
209
|
+
export function buildCliErrorEnvelope(opts) {
|
|
210
|
+
return buildEnvelope({
|
|
211
|
+
command: opts.command,
|
|
212
|
+
ok: false,
|
|
213
|
+
exitCode: opts.exitCode ?? 2,
|
|
214
|
+
stderr: formatErrorForJson({
|
|
215
|
+
message: opts.message,
|
|
216
|
+
code: opts.code,
|
|
217
|
+
suggestions: opts.suggestions,
|
|
218
|
+
ref: opts.ref,
|
|
219
|
+
}),
|
|
220
|
+
data: null,
|
|
221
|
+
code: opts.code,
|
|
222
|
+
suggestions: opts.suggestions?.length ? opts.suggestions : undefined,
|
|
223
|
+
ref: opts.ref,
|
|
224
|
+
});
|
|
82
225
|
}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} | {
|
|
1
|
+
import { EXIT_USAGE } from './cli-error-policy.js';
|
|
2
|
+
/** Always an envelope — human and JSON share one formatted stderr (Code + Try this). */
|
|
3
|
+
export type UnknownHelpResult = {
|
|
4
|
+
exitCode: typeof EXIT_USAGE;
|
|
6
5
|
envelope: Record<string, unknown>;
|
|
7
|
-
|
|
8
|
-
kind: 'json';
|
|
6
|
+
json: boolean;
|
|
9
7
|
};
|
|
10
8
|
/**
|
|
11
9
|
* oclif resolves help before its command_not_found hook, so `bonsai config gett --help` otherwise
|
|
12
10
|
* falls back to a terse framework error. Catch that narrow case and reuse the normal typo guidance.
|
|
13
11
|
*/
|
|
14
12
|
export declare function tryUnknownHelpOutput(argv: readonly string[], root: string): Promise<UnknownHelpResult | null>;
|
|
15
|
-
export {};
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { Config } from '@oclif/core';
|
|
2
|
-
import {
|
|
2
|
+
import { positionalArgvTokens } from './argv.js';
|
|
3
|
+
import { EXIT_USAGE } from './cli-error-policy.js';
|
|
4
|
+
import { VALUE_TAKING_FLAG_TOKENS } from './cli-flag-manifest.js';
|
|
5
|
+
import { buildCliErrorEnvelope } from './envelope.js';
|
|
3
6
|
import { buildCommandNotFoundDetails } from '../hooks/command-not-found/suggest.js';
|
|
4
7
|
function commandId(parts) {
|
|
5
8
|
return parts.join(':');
|
|
6
9
|
}
|
|
7
|
-
function positionalTokens(argv) {
|
|
8
|
-
const tokens = [];
|
|
9
|
-
for (const arg of argv) {
|
|
10
|
-
if (arg === '--')
|
|
11
|
-
break;
|
|
12
|
-
if (arg === '--help' || arg === '--json')
|
|
13
|
-
continue;
|
|
14
|
-
if (arg.startsWith('-'))
|
|
15
|
-
continue;
|
|
16
|
-
tokens.push(arg);
|
|
17
|
-
}
|
|
18
|
-
return tokens;
|
|
19
|
-
}
|
|
20
10
|
function commandPrefixLength(tokens, config) {
|
|
21
11
|
let longest = 0;
|
|
22
12
|
for (let n = 1; n <= tokens.length; n++) {
|
|
@@ -51,7 +41,7 @@ function hasValidHelpTarget(tokens, config) {
|
|
|
51
41
|
export async function tryUnknownHelpOutput(argv, root) {
|
|
52
42
|
if (!argv.includes('--help'))
|
|
53
43
|
return null;
|
|
54
|
-
const tokens =
|
|
44
|
+
const tokens = positionalArgvTokens(argv, VALUE_TAKING_FLAG_TOKENS);
|
|
55
45
|
if (tokens.length === 0)
|
|
56
46
|
return null;
|
|
57
47
|
const config = await Config.load({ root });
|
|
@@ -59,23 +49,14 @@ export async function tryUnknownHelpOutput(argv, root) {
|
|
|
59
49
|
return null;
|
|
60
50
|
const attemptedId = commandId(tokens);
|
|
61
51
|
const details = buildCommandNotFoundDetails(attemptedId, [...argv], config);
|
|
62
|
-
if (argv.includes('--json')) {
|
|
63
|
-
return {
|
|
64
|
-
kind: 'json',
|
|
65
|
-
exitCode: 2,
|
|
66
|
-
envelope: buildEnvelope({
|
|
67
|
-
command: details.command,
|
|
68
|
-
ok: false,
|
|
69
|
-
exitCode: 2,
|
|
70
|
-
stderr: `${details.message}\nCode: ${details.code}`,
|
|
71
|
-
data: null,
|
|
72
|
-
code: details.code,
|
|
73
|
-
}),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
52
|
return {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
53
|
+
exitCode: EXIT_USAGE,
|
|
54
|
+
json: argv.includes('--json'),
|
|
55
|
+
envelope: buildCliErrorEnvelope({
|
|
56
|
+
command: details.command,
|
|
57
|
+
message: details.message,
|
|
58
|
+
code: details.code,
|
|
59
|
+
suggestions: details.suggestions,
|
|
60
|
+
}),
|
|
80
61
|
};
|
|
81
62
|
}
|
package/dist/lib/json-meta.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** True when argv asks for help or version together with --json. */
|
|
2
|
-
export declare function isJsonMetaRequest(argv: readonly string[]): boolean;
|
|
3
1
|
/**
|
|
4
2
|
* When `--json` is combined with `--help` or `--version`, oclif would print human text to stdout.
|
|
5
3
|
* Render the same content inside the standard Bonsai JSON envelope instead.
|
package/dist/lib/json-meta.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Config, Help, toConfiguredId } from '@oclif/core';
|
|
2
|
+
import { positionalArgvTokens } from './argv.js';
|
|
3
|
+
import { EXIT_OK, EXIT_RUNTIME_FAILURE } from './cli-error-policy.js';
|
|
4
|
+
import { VALUE_TAKING_FLAG_TOKENS } from './cli-flag-manifest.js';
|
|
2
5
|
import { buildEnvelope } from './envelope.js';
|
|
3
6
|
/** Strip ANSI color codes so JSON help text stays machine-stable. */
|
|
4
7
|
function stripAnsi(text) {
|
|
5
|
-
// eslint-disable-next-line no-control-regex
|
|
6
8
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
7
9
|
}
|
|
8
10
|
/** True when argv asks for help or version together with --json. */
|
|
9
|
-
|
|
11
|
+
function isJsonMetaRequest(argv) {
|
|
10
12
|
if (!argv.includes('--json'))
|
|
11
13
|
return false;
|
|
12
14
|
return argv.includes('--help') || argv.includes('--version');
|
|
@@ -21,27 +23,21 @@ class CaptureHelp extends Help {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
|
-
function helpSubject(argv) {
|
|
25
|
-
for (const arg of argv) {
|
|
26
|
-
if (arg === '--' || arg === '--help' || arg === 'help')
|
|
27
|
-
continue;
|
|
28
|
-
if (arg.startsWith('-'))
|
|
29
|
-
return;
|
|
30
|
-
return arg;
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
26
|
function envelopeCommandId(config, argv) {
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
27
|
+
const tokens = positionalArgvTokens(argv, VALUE_TAKING_FLAG_TOKENS);
|
|
28
|
+
if (tokens.length === 0)
|
|
37
29
|
return config.bin;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
// Longest matching command id wins (`config:get` over topic `config`).
|
|
31
|
+
for (let n = tokens.length; n >= 1; n--) {
|
|
32
|
+
const id = tokens.slice(0, n).join(':');
|
|
33
|
+
const command = config.findCommand(id);
|
|
34
|
+
if (command?.id)
|
|
35
|
+
return toConfiguredId(command.id, config);
|
|
36
|
+
}
|
|
37
|
+
const topic = config.findTopic(tokens[0]);
|
|
42
38
|
if (topic)
|
|
43
39
|
return topic.name;
|
|
44
|
-
return
|
|
40
|
+
return tokens[0];
|
|
45
41
|
}
|
|
46
42
|
/**
|
|
47
43
|
* When `--json` is combined with `--help` or `--version`, oclif would print human text to stdout.
|
|
@@ -54,11 +50,11 @@ export async function tryJsonMetaOutput(argv, root) {
|
|
|
54
50
|
const config = await Config.load({ root });
|
|
55
51
|
if (argv.includes('--version')) {
|
|
56
52
|
return {
|
|
57
|
-
exitCode:
|
|
53
|
+
exitCode: EXIT_OK,
|
|
58
54
|
envelope: buildEnvelope({
|
|
59
55
|
command: config.bin,
|
|
60
56
|
ok: true,
|
|
61
|
-
exitCode:
|
|
57
|
+
exitCode: EXIT_OK,
|
|
62
58
|
stderr: '',
|
|
63
59
|
data: {
|
|
64
60
|
version: config.version,
|
|
@@ -71,11 +67,11 @@ export async function tryJsonMetaOutput(argv, root) {
|
|
|
71
67
|
await help.showHelp([...argv]);
|
|
72
68
|
const helpText = stripAnsi(help.lines.join('\n').trimEnd());
|
|
73
69
|
return {
|
|
74
|
-
exitCode:
|
|
70
|
+
exitCode: EXIT_OK,
|
|
75
71
|
envelope: buildEnvelope({
|
|
76
72
|
command: envelopeCommandId(config, argv),
|
|
77
73
|
ok: true,
|
|
78
|
-
exitCode:
|
|
74
|
+
exitCode: EXIT_OK,
|
|
79
75
|
stderr: '',
|
|
80
76
|
data: { help: helpText },
|
|
81
77
|
}),
|
|
@@ -84,11 +80,11 @@ export async function tryJsonMetaOutput(argv, root) {
|
|
|
84
80
|
catch (err) {
|
|
85
81
|
const message = err instanceof Error ? err.message : String(err);
|
|
86
82
|
return {
|
|
87
|
-
exitCode:
|
|
83
|
+
exitCode: EXIT_RUNTIME_FAILURE,
|
|
88
84
|
envelope: buildEnvelope({
|
|
89
85
|
command: 'bonsai',
|
|
90
86
|
ok: false,
|
|
91
|
-
exitCode:
|
|
87
|
+
exitCode: EXIT_RUNTIME_FAILURE,
|
|
92
88
|
stderr: message,
|
|
93
89
|
data: null,
|
|
94
90
|
code: 'META_RENDER_FAILED',
|
package/dist/lib/limit-flag.d.ts
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Build an oclif integer `--limit` flag capped at `maxLimit`.
|
|
3
|
+
*
|
|
4
|
+
* @param maxLimit - Inclusive upper bound for accepted values.
|
|
5
|
+
* @param defaultValue - Default when the flag is omitted.
|
|
6
|
+
* @param description - Help text for the flag.
|
|
7
|
+
* @returns An oclif custom flag definition.
|
|
8
|
+
* @throws {Errors.CLIError} With code `INVALID_LIMIT` and exit {@link EXIT_USAGE} on bad input
|
|
9
|
+
* (thrown by oclif during parse, not by this factory itself).
|
|
10
|
+
*/
|
|
11
|
+
export declare function limitFlag(maxLimit: number, defaultValue: number, description: string): import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
package/dist/lib/limit-flag.js
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
import { Errors, Flags } from '@oclif/core';
|
|
2
|
-
|
|
2
|
+
import { EXIT_USAGE } from './cli-error-policy.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build an oclif integer `--limit` flag capped at `maxLimit`.
|
|
5
|
+
*
|
|
6
|
+
* @param maxLimit - Inclusive upper bound for accepted values.
|
|
7
|
+
* @param defaultValue - Default when the flag is omitted.
|
|
8
|
+
* @param description - Help text for the flag.
|
|
9
|
+
* @returns An oclif custom flag definition.
|
|
10
|
+
* @throws {Errors.CLIError} With code `INVALID_LIMIT` and exit {@link EXIT_USAGE} on bad input
|
|
11
|
+
* (thrown by oclif during parse, not by this factory itself).
|
|
12
|
+
*/
|
|
13
|
+
export function limitFlag(maxLimit, defaultValue, description) {
|
|
3
14
|
return Flags.custom({
|
|
4
15
|
parse: async (input) => {
|
|
5
16
|
if (!/^\d+$/.test(input)) {
|
|
6
|
-
throw new Errors.CLIError(`Limit must be an integer between 1 and ${
|
|
7
|
-
exit:
|
|
17
|
+
throw new Errors.CLIError(`Limit must be an integer between 1 and ${maxLimit}.`, {
|
|
18
|
+
exit: EXIT_USAGE,
|
|
8
19
|
code: 'INVALID_LIMIT',
|
|
20
|
+
suggestions: [`Use an integer from 1 to ${maxLimit}.`],
|
|
9
21
|
});
|
|
10
22
|
}
|
|
11
23
|
const value = Number(input);
|
|
12
|
-
if (value < 1 || value >
|
|
13
|
-
throw new Errors.CLIError(`Limit must be between 1 and ${
|
|
14
|
-
exit:
|
|
24
|
+
if (value < 1 || value > maxLimit) {
|
|
25
|
+
throw new Errors.CLIError(`Limit must be between 1 and ${maxLimit}.`, {
|
|
26
|
+
exit: EXIT_USAGE,
|
|
15
27
|
code: 'INVALID_LIMIT',
|
|
28
|
+
suggestions: [`Use a value from 1 to ${maxLimit}.`],
|
|
16
29
|
});
|
|
17
30
|
}
|
|
18
31
|
return value;
|