@taurgis/bonsai 1.0.5 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -12
- package/agents/agents/official-docs-researcher.agent.md +15 -26
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -35
- package/agents/hooks/README.md +2 -9
- package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/github-copilot/bonsai-web-fetch.sh +1 -1
- package/agents/instructions/salesforce-research.instructions.md +7 -6
- package/agents/instructions/web-research.instructions.md +3 -2
- package/agents/skills/web-research/SKILL.md +24 -32
- package/bin/cli.mjs +44 -14
- package/dist/base-command.d.ts +17 -1
- package/dist/base-command.js +70 -9
- package/dist/commands/config/base.d.ts +2 -0
- package/dist/commands/config/base.js +15 -1
- package/dist/commands/config/get.d.ts +1 -1
- package/dist/commands/config/get.js +4 -5
- package/dist/commands/config/index.d.ts +12 -0
- package/dist/commands/config/index.js +34 -0
- package/dist/commands/config/list.js +2 -2
- package/dist/commands/config/set.js +18 -12
- package/dist/commands/config/unset.d.ts +1 -1
- package/dist/commands/config/unset.js +9 -6
- package/dist/commands/fetch.d.ts +2 -0
- package/dist/commands/fetch.js +69 -45
- package/dist/commands/import.d.ts +3 -0
- package/dist/commands/import.js +91 -53
- package/dist/commands/inspect.d.ts +8 -0
- package/dist/commands/inspect.js +59 -30
- package/dist/commands/list.d.ts +5 -3
- package/dist/commands/list.js +60 -35
- package/dist/commands/prune.d.ts +2 -1
- package/dist/commands/prune.js +62 -19
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.js +84 -26
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +2 -2
- package/dist/hooks/command-not-found/suggest.d.ts +7 -1
- package/dist/hooks/command-not-found/suggest.js +100 -46
- package/dist/lib/argv.d.ts +10 -0
- package/dist/lib/argv.js +97 -0
- package/dist/lib/color.d.ts +20 -0
- package/dist/lib/color.js +36 -0
- package/dist/lib/envelope.d.ts +36 -0
- package/dist/lib/envelope.js +82 -0
- package/dist/lib/help-preflight.d.ts +15 -0
- package/dist/lib/help-preflight.js +81 -0
- package/dist/lib/json-meta.d.ts +10 -0
- package/dist/lib/json-meta.js +98 -0
- package/dist/lib/limit-flag.d.ts +1 -0
- package/dist/lib/limit-flag.js +24 -0
- package/dist/lib/research/{search-index.d.ts → artifact-index.d.ts} +5 -5
- package/dist/lib/research/{search-index.js → artifact-index.js} +10 -10
- package/dist/lib/research/browser.d.ts +0 -8
- package/dist/lib/research/browser.js +20 -43
- package/dist/lib/research/regression.d.ts +18 -0
- package/dist/lib/research/regression.js +47 -0
- package/dist/lib/research/revalidate.js +3 -1
- package/dist/lib/research/schema.d.ts +5 -2
- package/dist/lib/research/schema.js +12 -1
- package/dist/lib/research/storage.d.ts +0 -1
- package/dist/lib/research/storage.js +15 -13
- package/dist/lib/research/url.d.ts +22 -0
- package/dist/lib/research/url.js +63 -1
- package/dist/lib/text.d.ts +5 -1
- package/dist/lib/text.js +11 -1
- package/dist/sites/coveo.d.ts +7 -0
- package/dist/sites/coveo.js +33 -0
- package/dist/sites/index.js +1 -2
- package/dist/sites/salesforce/fetch-page.js +1 -1
- package/dist/sites/salesforce/index.js +3 -8
- package/dist/sites/salesforce-developer/fetch-page.js +3 -1
- package/dist/sites/salesforce-developer/index.js +1 -2
- package/dist/sites/salesforce-doc-fetch.d.ts +11 -0
- package/dist/sites/salesforce-doc-fetch.js +117 -76
- package/dist/sites/salesforce-dom-probe.d.ts +16 -0
- package/dist/sites/salesforce-dom-probe.js +114 -0
- package/dist/sites/types.d.ts +0 -6
- package/oclif.manifest.json +153 -155
- package/package.json +38 -25
- package/dist/commands/index.d.ts +0 -35
- package/dist/commands/index.js +0 -306
- package/dist/commands/research/config/base.d.ts +0 -23
- package/dist/commands/research/config/base.js +0 -46
- package/dist/commands/research/config/get.d.ts +0 -20
- package/dist/commands/research/config/get.js +0 -45
- package/dist/commands/research/config/list.d.ts +0 -17
- package/dist/commands/research/config/list.js +0 -42
- package/dist/commands/research/config/set.d.ts +0 -31
- package/dist/commands/research/config/set.js +0 -94
- package/dist/commands/research/config/unset.d.ts +0 -21
- package/dist/commands/research/config/unset.js +0 -59
- package/dist/commands/research/import.d.ts +0 -38
- package/dist/commands/research/import.js +0 -313
- package/dist/commands/research/inspect.d.ts +0 -13
- package/dist/commands/research/inspect.js +0 -86
- package/dist/commands/research/list.d.ts +0 -25
- package/dist/commands/research/list.js +0 -143
- package/dist/commands/research/prune.d.ts +0 -24
- package/dist/commands/research/prune.js +0 -141
- package/dist/commands/research/search.d.ts +0 -32
- package/dist/commands/research/search.js +0 -334
- package/dist/commands/research/status.d.ts +0 -17
- package/dist/commands/research/status.js +0 -108
- package/dist/commands/research.d.ts +0 -36
- package/dist/commands/research.js +0 -304
- package/dist/commands/search.d.ts +0 -31
- package/dist/commands/search.js +0 -329
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.d.ts +0 -1
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.js +0 -2
- package/dist/lib/research/docs/discovery-index.d.ts +0 -14
- package/dist/lib/research/docs/discovery-index.js +0 -112
- package/dist/lib/research/docs/remote-search-runner.d.ts +0 -21
- package/dist/lib/research/docs/remote-search-runner.js +0 -114
- package/dist/lib/research/docs/remote-search.d.ts +0 -14
- package/dist/lib/research/docs/remote-search.js +0 -101
- package/dist/lib/research/docs/search-index.d.ts +0 -13
- package/dist/lib/research/docs/search-index.js +0 -176
- package/dist/sites/salesforce/browser-search.d.ts +0 -7
- package/dist/sites/salesforce/browser-search.js +0 -82
- package/dist/sites/salesforce/coveo.d.ts +0 -23
- package/dist/sites/salesforce/coveo.js +0 -161
- package/dist/sites/salesforce/token.d.ts +0 -26
- package/dist/sites/salesforce/token.js +0 -154
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { toConfiguredId } from '@oclif/core';
|
|
2
|
-
import { closestMatch } from '../../lib/text.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// command for input that resembles nothing.
|
|
6
|
-
const MAX_SUGGESTION_DISTANCE = 3;
|
|
2
|
+
import { closestMatch, maxFuzzyDistance } from '../../lib/text.js';
|
|
3
|
+
import { buildEnvelope, formatErrorForJson } from '../../lib/envelope.js';
|
|
4
|
+
import { looksLikeSchemelessUrl } from '../../lib/research/url.js';
|
|
7
5
|
/**
|
|
8
6
|
* The nearest visible command to a typo, plus how many leading segments of the attempted id name
|
|
9
7
|
* the command. With `topicSeparator: ' '`, oclif folds every leading token of an unknown command
|
|
10
|
-
* into one colon-delimited id, so `bonsai
|
|
8
|
+
* into one colon-delimited id, so `bonsai lisst extra` arrives as `lisst:extra`. Matching that whole
|
|
11
9
|
* string never finds a short command, so walk prefixes longest→shortest and suggest off the closest:
|
|
12
|
-
* `
|
|
10
|
+
* `lisst:extra` resolves to nothing, but its prefix `lisst` resolves to `list`. The matched prefix
|
|
13
11
|
* length doubles as the command-segment count, so a folded-in positional arg is dropped from display.
|
|
14
12
|
*/
|
|
15
13
|
function findSuggestion(segments, visibleIds) {
|
|
16
14
|
for (let n = segments.length; n >= 1; n--) {
|
|
17
|
-
const
|
|
15
|
+
const attempted = segments.slice(0, n).join(':');
|
|
16
|
+
const suggestion = closestMatch(attempted, visibleIds, maxFuzzyDistance(attempted));
|
|
17
|
+
if (suggestion === attempted &&
|
|
18
|
+
n < segments.length &&
|
|
19
|
+
visibleIds.some((id) => id.startsWith(`${attempted}:`))) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
18
22
|
if (suggestion)
|
|
19
23
|
return { suggestion, commandSegments: n };
|
|
20
24
|
}
|
|
@@ -31,63 +35,113 @@ function topicChainLength(segments, config) {
|
|
|
31
35
|
i++;
|
|
32
36
|
return i + 1;
|
|
33
37
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
function exactZeroArgCommandPrefix(segments, visibleIds, config) {
|
|
39
|
+
for (let n = segments.length - 1; n >= 1; n--) {
|
|
40
|
+
const candidate = segments.slice(0, n).join(':');
|
|
41
|
+
if (!visibleIds.includes(candidate))
|
|
42
|
+
continue;
|
|
43
|
+
if (visibleIds.some((id) => id.startsWith(`${candidate}:`)))
|
|
44
|
+
continue;
|
|
45
|
+
const command = config.commands.find((entry) => entry.id === candidate);
|
|
46
|
+
const args = Object.keys(command?.args ?? {});
|
|
47
|
+
if (args.length > 0)
|
|
48
|
+
continue;
|
|
49
|
+
const extra = segments[n];
|
|
50
|
+
if (extra === undefined)
|
|
51
|
+
continue;
|
|
52
|
+
return { commandId: candidate, extra };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
39
56
|
/**
|
|
40
57
|
* The input as-typed when it looks like a URL missing its scheme (`example.com`,
|
|
41
58
|
* `docs.nestjs.com/guide`), else null. `bonsai <url>` is the headline command, but bin/cli.mjs only
|
|
42
59
|
* routes args carrying a `://` scheme to the fetch shorthand — so a scheme-less URL falls through to
|
|
43
60
|
* `command_not_found`. A domain-shaped host separates a forgotten-scheme URL from an ordinary command
|
|
44
61
|
* typo: `statuss` has no dot and never matches, while `config:frobnicate` fails to parse (the second
|
|
45
|
-
* segment becomes a non-numeric port), so neither is misread as a URL.
|
|
62
|
+
* segment becomes a non-numeric port), so neither is misread as a URL. The same domain-shape test
|
|
63
|
+
* powers normalizeUrl's "missing scheme" hint, so the two entry paths stay in lockstep.
|
|
46
64
|
*/
|
|
47
65
|
function bareUrlInput(id) {
|
|
48
|
-
|
|
49
|
-
return null;
|
|
50
|
-
let hostname;
|
|
51
|
-
try {
|
|
52
|
-
hostname = new URL(`https://${id}`).hostname;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
return DOMAIN_OR_IPV4.test(hostname) ? id : null;
|
|
66
|
+
return looksLikeSchemelessUrl(id) ? id : null;
|
|
58
67
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
function isJsonMode(argv) {
|
|
69
|
+
return argv?.includes('--json') ?? false;
|
|
70
|
+
}
|
|
71
|
+
function emitJsonError(command, message, code) {
|
|
72
|
+
const envelope = buildEnvelope({
|
|
73
|
+
command,
|
|
74
|
+
ok: false,
|
|
75
|
+
exitCode: 2,
|
|
76
|
+
stderr: formatErrorForJson({ message, code }),
|
|
77
|
+
data: null,
|
|
78
|
+
code,
|
|
79
|
+
});
|
|
80
|
+
process.exitCode = 2;
|
|
81
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
82
|
+
return envelope;
|
|
83
|
+
}
|
|
84
|
+
export function buildCommandNotFoundDetails(id, argv, config) {
|
|
85
|
+
const hiddenIds = new Set(config.commands.filter((c) => c.hidden).map((c) => c.id));
|
|
86
|
+
const visibleIds = config.commandIDs.filter((commandId) => !hiddenIds.has(commandId));
|
|
68
87
|
// A scheme-less URL is the most common "not a command" mistake for this CLI, so steer the user to
|
|
69
88
|
// the `bonsai <url>` shorthand with a scheme before falling back to nearest-command matching (which
|
|
70
89
|
// never finds a command for a hostname). The correction is shown, never auto-run (clig.dev).
|
|
71
|
-
const bareUrl = bareUrlInput(
|
|
90
|
+
const bareUrl = bareUrlInput(id);
|
|
72
91
|
if (bareUrl) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
return {
|
|
93
|
+
code: 'MISSING_URL_SCHEME',
|
|
94
|
+
command: bareUrl,
|
|
95
|
+
jsonMode: isJsonMode(argv),
|
|
96
|
+
message: [
|
|
97
|
+
`${bareUrl} is not a ${config.bin} command.`,
|
|
98
|
+
`Run ${config.bin} help for a list of available commands.`,
|
|
99
|
+
`Did you mean ${config.bin} https://${bareUrl}? URLs need an http:// or https:// scheme.`,
|
|
100
|
+
].join('\n'),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const segments = id.split(':');
|
|
104
|
+
const unexpected = exactZeroArgCommandPrefix(segments, visibleIds, config);
|
|
105
|
+
if (unexpected) {
|
|
106
|
+
const command = toConfiguredId(unexpected.commandId, config);
|
|
107
|
+
return {
|
|
108
|
+
code: 'UNEXPECTED_ARGUMENT',
|
|
109
|
+
command,
|
|
110
|
+
jsonMode: isJsonMode(argv),
|
|
111
|
+
message: [
|
|
112
|
+
`Unexpected argument: ${unexpected.extra}`,
|
|
113
|
+
`Run ${config.bin} ${command} --help for usage.`,
|
|
114
|
+
].join('\n'),
|
|
115
|
+
};
|
|
78
116
|
}
|
|
79
|
-
const segments = opts.id.split(':');
|
|
80
117
|
const { suggestion, commandSegments } = findSuggestion(segments, visibleIds);
|
|
81
118
|
// Show only the segments that name the command, never the positional args oclif folded into the id
|
|
82
119
|
// (which would otherwise glue the arg on and turn `://` into ` //`).
|
|
83
|
-
const displaySegments = commandSegments || topicChainLength(segments,
|
|
84
|
-
const attempted = toConfiguredId(segments.slice(0, displaySegments).join(':'),
|
|
120
|
+
const displaySegments = commandSegments || topicChainLength(segments, config);
|
|
121
|
+
const attempted = toConfiguredId(segments.slice(0, displaySegments).join(':'), config);
|
|
85
122
|
// Order so the most actionable line lands last, where the eye rests (clig.dev): the help pointer
|
|
86
123
|
// is the floor, and a concrete "Did you mean …?" correction — when we have one — goes below it.
|
|
87
|
-
const lines = [`${attempted} is not a ${
|
|
88
|
-
lines.push(`Run ${
|
|
124
|
+
const lines = [`${attempted} is not a ${config.bin} command.`];
|
|
125
|
+
lines.push(`Run ${config.bin} help for a list of available commands.`);
|
|
89
126
|
if (suggestion)
|
|
90
|
-
lines.push(`Did you mean ${toConfiguredId(suggestion,
|
|
91
|
-
|
|
127
|
+
lines.push(`Did you mean ${toConfiguredId(suggestion, config)}?`);
|
|
128
|
+
return {
|
|
129
|
+
code: 'COMMAND_NOT_FOUND',
|
|
130
|
+
command: attempted,
|
|
131
|
+
jsonMode: isJsonMode(argv),
|
|
132
|
+
message: lines.join('\n'),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Typo-aware "command not found" handler. oclif fires this before printing its own terse error;
|
|
137
|
+
* throwing here (via `this.error`) replaces that default with a more helpful message. A correction
|
|
138
|
+
* is offered only when the nearest command is a close match, and it is never auto-run — the user
|
|
139
|
+
* re-runs the corrected command themselves, so they learn the right syntax (clig.dev guidance).
|
|
140
|
+
*/
|
|
141
|
+
const hook = async function (opts) {
|
|
142
|
+
const details = buildCommandNotFoundDetails(opts.id, opts.argv, opts.config);
|
|
143
|
+
if (details.jsonMode)
|
|
144
|
+
return emitJsonError(details.command, details.message, details.code);
|
|
145
|
+
this.error(details.message, { exit: 2, code: details.code });
|
|
92
146
|
};
|
|
93
147
|
export default hook;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface NormalizationResult {
|
|
2
|
+
/** The normalized argv array to set on process.argv. */
|
|
3
|
+
argv: string[];
|
|
4
|
+
/** Optional metadata indicating the shim should exit early with a JSON envelope. */
|
|
5
|
+
exitWithJson?: {
|
|
6
|
+
exitCode: number;
|
|
7
|
+
envelope: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function normalizeArgv(rawArgv: string[]): NormalizationResult;
|
package/dist/lib/argv.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { buildEnvelope } from './envelope.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes process.argv (excluding Node executable and script path) so the
|
|
4
|
+
* oclif pipeline sees one consistent command structure.
|
|
5
|
+
*/
|
|
6
|
+
function missingUsageJsonExit() {
|
|
7
|
+
return {
|
|
8
|
+
exitCode: 2,
|
|
9
|
+
envelope: buildEnvelope({
|
|
10
|
+
command: 'bonsai',
|
|
11
|
+
ok: false,
|
|
12
|
+
exitCode: 2,
|
|
13
|
+
stderr: 'Missing URL or command. Run bonsai --help for usage.',
|
|
14
|
+
data: null,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const FLAGS_WITH_VALUES = new Set([
|
|
19
|
+
'--topic',
|
|
20
|
+
'-t',
|
|
21
|
+
'--tags',
|
|
22
|
+
'-g',
|
|
23
|
+
'--format',
|
|
24
|
+
'--tier',
|
|
25
|
+
'--ttl',
|
|
26
|
+
'--max-age',
|
|
27
|
+
'--storage',
|
|
28
|
+
'--file',
|
|
29
|
+
'-f',
|
|
30
|
+
'--input-format',
|
|
31
|
+
'--source-url',
|
|
32
|
+
'--freshness',
|
|
33
|
+
'--artifact-type',
|
|
34
|
+
'--capture-method',
|
|
35
|
+
'--older-than',
|
|
36
|
+
'--inactive',
|
|
37
|
+
'--domain',
|
|
38
|
+
'--remote',
|
|
39
|
+
'--limit',
|
|
40
|
+
]);
|
|
41
|
+
export function normalizeArgv(rawArgv) {
|
|
42
|
+
const onlyJsonFlags = rawArgv.length > 0 && rawArgv.every((arg) => arg === '--json');
|
|
43
|
+
if (onlyJsonFlags) {
|
|
44
|
+
return {
|
|
45
|
+
// bin/cli.mjs exits before applying argv when exitWithJson is set.
|
|
46
|
+
argv: ['--json'],
|
|
47
|
+
exitWithJson: missingUsageJsonExit(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// oclif's JSON flag is command-scoped, so `bonsai list --json` works but
|
|
51
|
+
// `bonsai --json list` is otherwise parsed as an unknown command named
|
|
52
|
+
// "--json". Collect every --json, append one copy after the command/URL, and
|
|
53
|
+
// dedupe repeats like `bonsai --json --json list`.
|
|
54
|
+
const jsonMode = rawArgv.includes('--json');
|
|
55
|
+
let tokens = rawArgv.filter((arg) => arg !== '--json');
|
|
56
|
+
// clig.dev: -h and --help must always work at any level.
|
|
57
|
+
tokens = tokens.map((arg) => (arg === '-h' ? '--help' : arg));
|
|
58
|
+
if (tokens[0] === 'help') {
|
|
59
|
+
tokens = [...tokens.slice(1), '--help'];
|
|
60
|
+
}
|
|
61
|
+
const helpRequested = tokens.includes('--help');
|
|
62
|
+
let core = tokens.filter((arg) => arg !== '--help');
|
|
63
|
+
// Treat URL-shaped tokens as the `fetch` shorthand. Match both `https://...` and scheme-only
|
|
64
|
+
// forms like `javascript:` or `data:` so fetch can reject unsupported protocols instead of oclif
|
|
65
|
+
// reporting a misleading "command not found". If the invocation begins with a flag, allow common
|
|
66
|
+
// flag-before-argument usage such as `bonsai --format detailed https://example.com`.
|
|
67
|
+
let firstNonFlagArgIndex = -1;
|
|
68
|
+
for (let i = 0; i < core.length; i++) {
|
|
69
|
+
const token = core[i];
|
|
70
|
+
if (token.startsWith('-')) {
|
|
71
|
+
if (FLAGS_WITH_VALUES.has(token)) {
|
|
72
|
+
i++; // skip the value
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
firstNonFlagArgIndex = i;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
const rootFetchShape = firstNonFlagArgIndex !== -1 && looksLikeUrl(core[firstNonFlagArgIndex]);
|
|
80
|
+
if (rootFetchShape) {
|
|
81
|
+
const url = core[firstNonFlagArgIndex];
|
|
82
|
+
core = [
|
|
83
|
+
'fetch',
|
|
84
|
+
url,
|
|
85
|
+
...core.slice(0, firstNonFlagArgIndex),
|
|
86
|
+
...core.slice(firstNonFlagArgIndex + 1),
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
if (helpRequested)
|
|
90
|
+
core.push('--help');
|
|
91
|
+
if (jsonMode)
|
|
92
|
+
core.push('--json');
|
|
93
|
+
return { argv: core };
|
|
94
|
+
}
|
|
95
|
+
function looksLikeUrl(arg) {
|
|
96
|
+
return arg.includes('://') || /^[a-z][a-z0-9+.-]*:/i.test(arg);
|
|
97
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether ANSI color should be emitted, per the clig.dev / no-color.org rules:
|
|
3
|
+
* - `FORCE_COLOR` (set to any value other than an explicit off) forces color on, ignoring detection.
|
|
4
|
+
* - `NO_COLOR` set to a non-empty value disables color, regardless of that value. An empty
|
|
5
|
+
* `NO_COLOR` is treated as unset (the spec keys off "present and not empty").
|
|
6
|
+
* - `TERM=dumb` cannot render ANSI, so color is disabled.
|
|
7
|
+
* - Otherwise color follows whether stdout is an interactive terminal.
|
|
8
|
+
* See https://clig.dev/#output and https://no-color.org/.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isColorEnabled(): boolean;
|
|
11
|
+
export declare function color(text: string, colorCode: string): string;
|
|
12
|
+
export declare const colors: {
|
|
13
|
+
bold: (t: string) => string;
|
|
14
|
+
green: (t: string) => string;
|
|
15
|
+
yellow: (t: string) => string;
|
|
16
|
+
red: (t: string) => string;
|
|
17
|
+
cyan: (t: string) => string;
|
|
18
|
+
gray: (t: string) => string;
|
|
19
|
+
magenta: (t: string) => string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether ANSI color should be emitted, per the clig.dev / no-color.org rules:
|
|
3
|
+
* - `FORCE_COLOR` (set to any value other than an explicit off) forces color on, ignoring detection.
|
|
4
|
+
* - `NO_COLOR` set to a non-empty value disables color, regardless of that value. An empty
|
|
5
|
+
* `NO_COLOR` is treated as unset (the spec keys off "present and not empty").
|
|
6
|
+
* - `TERM=dumb` cannot render ANSI, so color is disabled.
|
|
7
|
+
* - Otherwise color follows whether stdout is an interactive terminal.
|
|
8
|
+
* See https://clig.dev/#output and https://no-color.org/.
|
|
9
|
+
*/
|
|
10
|
+
export function isColorEnabled() {
|
|
11
|
+
if (typeof process === 'undefined' || !process.env)
|
|
12
|
+
return false;
|
|
13
|
+
const env = process.env;
|
|
14
|
+
const force = env.FORCE_COLOR;
|
|
15
|
+
if (force !== undefined && force !== '' && force !== '0' && force !== 'false')
|
|
16
|
+
return true;
|
|
17
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
|
|
18
|
+
return false;
|
|
19
|
+
if (env.TERM === 'dumb')
|
|
20
|
+
return false;
|
|
21
|
+
return Boolean(process.stdout && process.stdout.isTTY);
|
|
22
|
+
}
|
|
23
|
+
export function color(text, colorCode) {
|
|
24
|
+
if (!isColorEnabled())
|
|
25
|
+
return text;
|
|
26
|
+
return `${colorCode}${text}\x1b[0m`;
|
|
27
|
+
}
|
|
28
|
+
export const colors = {
|
|
29
|
+
bold: (t) => color(t, '\x1b[1m'),
|
|
30
|
+
green: (t) => color(t, '\x1b[32m'),
|
|
31
|
+
yellow: (t) => color(t, '\x1b[33m'),
|
|
32
|
+
red: (t) => color(t, '\x1b[31m'),
|
|
33
|
+
cyan: (t) => color(t, '\x1b[36m'),
|
|
34
|
+
gray: (t) => color(t, '\x1b[90m'),
|
|
35
|
+
magenta: (t) => color(t, '\x1b[35m'),
|
|
36
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Fields oclif attaches to CLIError that human pretty-print surfaces but JSON mode must mirror. */
|
|
2
|
+
export interface CliErrorShape {
|
|
3
|
+
message?: string;
|
|
4
|
+
code?: string;
|
|
5
|
+
suggestions?: string[];
|
|
6
|
+
ref?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Map oclif parse-time errors to stable Bonsai codes when the throw site did not set one. */
|
|
9
|
+
export declare function stableErrorCodeFrom(err: unknown): string | undefined;
|
|
10
|
+
/** Strip oclif's generic help suffix from JSON stderr — agents already have structured codes. */
|
|
11
|
+
export declare function normalizeCliErrorMessage(message: string): string;
|
|
12
|
+
export interface EnvelopeParts {
|
|
13
|
+
command: string;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
exitCode: number;
|
|
16
|
+
stderr: string;
|
|
17
|
+
data: unknown;
|
|
18
|
+
/** Stable machine-readable error code when the failure is a CLIError with `code`. */
|
|
19
|
+
code?: string;
|
|
20
|
+
suggestions?: string[];
|
|
21
|
+
ref?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Formats a CLI error for the JSON envelope `stderr` field — same lines as human pretty-print,
|
|
25
|
+
* without ANSI or the leading "Error:" prefix (the message is already the primary line).
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatErrorForJson(err: CliErrorShape): string;
|
|
28
|
+
/**
|
|
29
|
+
* Builds the standard Bonsai CLI JSON envelope structure.
|
|
30
|
+
* This is the single source of truth for the output envelope schema.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildEnvelope(parts: EnvelopeParts): Record<string, unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* Formats a JSON envelope to a string with indentation.
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatEnvelope(parts: EnvelopeParts): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Map oclif parse-time errors to stable Bonsai codes when the throw site did not set one. */
|
|
2
|
+
export function stableErrorCodeFrom(err) {
|
|
3
|
+
const e = err;
|
|
4
|
+
if (typeof e?.code === 'string' && e.code)
|
|
5
|
+
return e.code;
|
|
6
|
+
switch (e?.constructor?.name) {
|
|
7
|
+
case 'RequiredArgsError':
|
|
8
|
+
return 'MISSING_ARGUMENT';
|
|
9
|
+
case 'FlagInvalidOptionError':
|
|
10
|
+
case 'ArgInvalidOptionError':
|
|
11
|
+
return 'INVALID_FLAG_VALUE';
|
|
12
|
+
case 'NonExistentFlagsError':
|
|
13
|
+
return 'UNKNOWN_FLAG';
|
|
14
|
+
case 'UnexpectedArgsError':
|
|
15
|
+
return 'UNEXPECTED_ARGUMENT';
|
|
16
|
+
}
|
|
17
|
+
// A flag supplied without its value throws a plain CLIError with no dedicated class to switch on,
|
|
18
|
+
// so it would otherwise reach agents with no stable code. oclif phrases it two ways: "expects a
|
|
19
|
+
// value" for a free-form flag, "expects one of these values: …" for an options-constrained flag.
|
|
20
|
+
if (typeof e?.message === 'string' &&
|
|
21
|
+
/^Flag --\S+ expects (a value$|one of these values:)/.test(e.message)) {
|
|
22
|
+
return 'MISSING_FLAG_VALUE';
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/** Strip oclif's generic help suffix from JSON stderr — agents already have structured codes. */
|
|
27
|
+
export function normalizeCliErrorMessage(message) {
|
|
28
|
+
const suffix = '\nSee more help with --help';
|
|
29
|
+
return message.endsWith(suffix) ? message.slice(0, -suffix.length) : message;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Formats a CLI error for the JSON envelope `stderr` field — same lines as human pretty-print,
|
|
33
|
+
* without ANSI or the leading "Error:" prefix (the message is already the primary line).
|
|
34
|
+
*/
|
|
35
|
+
export function formatErrorForJson(err) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
if (err.message)
|
|
38
|
+
lines.push(err.message);
|
|
39
|
+
if (err.code)
|
|
40
|
+
lines.push(`Code: ${err.code}`);
|
|
41
|
+
if (err.suggestions?.length) {
|
|
42
|
+
if (err.suggestions.length === 1) {
|
|
43
|
+
lines.push(`Try this: ${err.suggestions[0]}`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
lines.push('Try this:');
|
|
47
|
+
for (const suggestion of err.suggestions)
|
|
48
|
+
lines.push(`* ${suggestion}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (err.ref)
|
|
52
|
+
lines.push(`Reference: ${err.ref}`);
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Builds the standard Bonsai CLI JSON envelope structure.
|
|
57
|
+
* This is the single source of truth for the output envelope schema.
|
|
58
|
+
*/
|
|
59
|
+
export function buildEnvelope(parts) {
|
|
60
|
+
const envelope = {
|
|
61
|
+
schemaVersion: 1,
|
|
62
|
+
command: parts.command,
|
|
63
|
+
ok: parts.ok,
|
|
64
|
+
exitCode: parts.exitCode,
|
|
65
|
+
stdout: '',
|
|
66
|
+
stderr: parts.stderr,
|
|
67
|
+
data: parts.data,
|
|
68
|
+
};
|
|
69
|
+
if (parts.code)
|
|
70
|
+
envelope.code = parts.code;
|
|
71
|
+
if (parts.suggestions?.length)
|
|
72
|
+
envelope.suggestions = parts.suggestions;
|
|
73
|
+
if (parts.ref)
|
|
74
|
+
envelope.ref = parts.ref;
|
|
75
|
+
return envelope;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Formats a JSON envelope to a string with indentation.
|
|
79
|
+
*/
|
|
80
|
+
export function formatEnvelope(parts) {
|
|
81
|
+
return JSON.stringify(buildEnvelope(parts), null, 2);
|
|
82
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type UnknownHelpResult = {
|
|
2
|
+
exitCode: 2;
|
|
3
|
+
kind: 'human';
|
|
4
|
+
message: string;
|
|
5
|
+
} | {
|
|
6
|
+
envelope: Record<string, unknown>;
|
|
7
|
+
exitCode: 2;
|
|
8
|
+
kind: 'json';
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* oclif resolves help before its command_not_found hook, so `bonsai config gett --help` otherwise
|
|
12
|
+
* falls back to a terse framework error. Catch that narrow case and reuse the normal typo guidance.
|
|
13
|
+
*/
|
|
14
|
+
export declare function tryUnknownHelpOutput(argv: readonly string[], root: string): Promise<UnknownHelpResult | null>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Config } from '@oclif/core';
|
|
2
|
+
import { buildEnvelope } from './envelope.js';
|
|
3
|
+
import { buildCommandNotFoundDetails } from '../hooks/command-not-found/suggest.js';
|
|
4
|
+
function commandId(parts) {
|
|
5
|
+
return parts.join(':');
|
|
6
|
+
}
|
|
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
|
+
function commandPrefixLength(tokens, config) {
|
|
21
|
+
let longest = 0;
|
|
22
|
+
for (let n = 1; n <= tokens.length; n++) {
|
|
23
|
+
if (config.commandIDs.includes(commandId(tokens.slice(0, n))))
|
|
24
|
+
longest = n;
|
|
25
|
+
}
|
|
26
|
+
return longest;
|
|
27
|
+
}
|
|
28
|
+
function branchPrefixLength(tokens, config) {
|
|
29
|
+
let longest = 0;
|
|
30
|
+
for (let n = 1; n <= tokens.length; n++) {
|
|
31
|
+
const id = commandId(tokens.slice(0, n));
|
|
32
|
+
if (config.commandIDs.some((command) => command.startsWith(`${id}:`)))
|
|
33
|
+
longest = n;
|
|
34
|
+
}
|
|
35
|
+
return longest;
|
|
36
|
+
}
|
|
37
|
+
function hasValidHelpTarget(tokens, config) {
|
|
38
|
+
const commandLength = commandPrefixLength(tokens, config);
|
|
39
|
+
const branchLength = branchPrefixLength(tokens, config);
|
|
40
|
+
if (commandLength === tokens.length || branchLength === tokens.length)
|
|
41
|
+
return true;
|
|
42
|
+
if (branchLength > 0 && tokens.length > branchLength) {
|
|
43
|
+
return commandLength > branchLength;
|
|
44
|
+
}
|
|
45
|
+
return commandLength > 0;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* oclif resolves help before its command_not_found hook, so `bonsai config gett --help` otherwise
|
|
49
|
+
* falls back to a terse framework error. Catch that narrow case and reuse the normal typo guidance.
|
|
50
|
+
*/
|
|
51
|
+
export async function tryUnknownHelpOutput(argv, root) {
|
|
52
|
+
if (!argv.includes('--help'))
|
|
53
|
+
return null;
|
|
54
|
+
const tokens = positionalTokens(argv);
|
|
55
|
+
if (tokens.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
const config = await Config.load({ root });
|
|
58
|
+
if (hasValidHelpTarget(tokens, config))
|
|
59
|
+
return null;
|
|
60
|
+
const attemptedId = commandId(tokens);
|
|
61
|
+
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
|
+
return {
|
|
77
|
+
kind: 'human',
|
|
78
|
+
exitCode: 2,
|
|
79
|
+
message: `${details.message}\nCode: ${details.code}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** True when argv asks for help or version together with --json. */
|
|
2
|
+
export declare function isJsonMetaRequest(argv: readonly string[]): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* When `--json` is combined with `--help` or `--version`, oclif would print human text to stdout.
|
|
5
|
+
* Render the same content inside the standard Bonsai JSON envelope instead.
|
|
6
|
+
*/
|
|
7
|
+
export declare function tryJsonMetaOutput(argv: readonly string[], root: string): Promise<{
|
|
8
|
+
exitCode: number;
|
|
9
|
+
envelope: Record<string, unknown>;
|
|
10
|
+
} | null>;
|