@taurgis/bonsai 1.0.6 → 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 +7 -13
- package/agents/agents/official-docs-researcher.agent.md +15 -35
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -44
- 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 -8
- package/agents/instructions/web-research.instructions.md +3 -4
- package/agents/skills/web-research/SKILL.md +24 -37
- package/bin/cli.mjs +15 -2
- package/dist/base-command.d.ts +15 -2
- package/dist/base-command.js +32 -4
- package/dist/commands/fetch.d.ts +2 -0
- package/dist/commands/fetch.js +43 -29
- package/dist/commands/import.js +11 -3
- package/dist/commands/inspect.d.ts +4 -0
- package/dist/commands/inspect.js +48 -24
- package/dist/commands/list.d.ts +3 -1
- package/dist/commands/list.js +42 -25
- package/dist/commands/prune.d.ts +1 -0
- package/dist/commands/prune.js +35 -9
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +57 -25
- package/dist/commands.d.ts +0 -2
- package/dist/commands.js +0 -2
- package/dist/hooks/command-not-found/suggest.d.ts +7 -1
- package/dist/hooks/command-not-found/suggest.js +82 -57
- package/dist/lib/argv.js +51 -7
- package/dist/lib/color.d.ts +20 -0
- package/dist/lib/color.js +36 -0
- package/dist/lib/envelope.js +8 -2
- package/dist/lib/help-preflight.d.ts +15 -0
- package/dist/lib/help-preflight.js +81 -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/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 +3 -1
- package/dist/lib/text.js +9 -1
- package/dist/sites/coveo.d.ts +7 -0
- package/dist/sites/coveo.js +33 -0
- package/dist/sites/index.js +1 -18
- 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 +31 -106
- package/package.json +32 -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 -35
- package/dist/commands/search.js +0 -391
- 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/algolia.d.ts +0 -9
- package/dist/sites/algolia.js +0 -26
- package/dist/sites/auto.d.ts +0 -8
- package/dist/sites/auto.js +0 -30
- package/dist/sites/nextjs/index.d.ts +0 -2
- package/dist/sites/nextjs/index.js +0 -52
- package/dist/sites/react/index.d.ts +0 -2
- package/dist/sites/react/index.js +0 -17
- 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
- package/dist/sites/tailwind/index.d.ts +0 -2
- package/dist/sites/tailwind/index.js +0 -17
- package/dist/sites/vue/index.d.ts +0 -2
- package/dist/sites/vue/index.js +0 -17
|
@@ -7,6 +7,7 @@ export default class ResearchStatus extends BaseCommand<typeof ResearchStatus> {
|
|
|
7
7
|
description: string;
|
|
8
8
|
command: string;
|
|
9
9
|
}[];
|
|
10
|
+
static strict: boolean;
|
|
10
11
|
static args: {
|
|
11
12
|
url: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
12
13
|
};
|
|
@@ -19,4 +20,5 @@ export default class ResearchStatus extends BaseCommand<typeof ResearchStatus> {
|
|
|
19
20
|
/** Enrich cache-miss envelopes with the same CACHE_MISS code and suggestions as inspect. */
|
|
20
21
|
protected toSuccessJson(data: unknown): Record<string, unknown>;
|
|
21
22
|
run(): Promise<unknown>;
|
|
23
|
+
private checkSingleStatus;
|
|
22
24
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -3,6 +3,7 @@ import { BaseCommand } from '../base-command.js';
|
|
|
3
3
|
import { formatErrorForJson } from '../lib/envelope.js';
|
|
4
4
|
import { getArtifactPath } from '../lib/research/storage.js';
|
|
5
5
|
import { evaluateFreshness, checkMaxAgeExpired, durationFlagError, } from '../lib/research/freshness.js';
|
|
6
|
+
import { colors } from '../lib/color.js';
|
|
6
7
|
function resolveFreshness(cached, currentTime, ttl, maxAge) {
|
|
7
8
|
const isMaxAgeExpired = maxAge ? checkMaxAgeExpired(cached, currentTime, maxAge) : false;
|
|
8
9
|
return isMaxAgeExpired
|
|
@@ -32,6 +33,7 @@ export default class ResearchStatus extends BaseCommand {
|
|
|
32
33
|
command: '<%= config.bin %> <%= command.id %> https://example.com/docs --tier volatile --ttl 2h',
|
|
33
34
|
},
|
|
34
35
|
];
|
|
36
|
+
static strict = false;
|
|
35
37
|
static args = {
|
|
36
38
|
url: Args.string({
|
|
37
39
|
required: true,
|
|
@@ -56,20 +58,23 @@ export default class ResearchStatus extends BaseCommand {
|
|
|
56
58
|
/** Enrich cache-miss envelopes with the same CACHE_MISS code and suggestions as inspect. */
|
|
57
59
|
toSuccessJson(data) {
|
|
58
60
|
const envelope = super.toSuccessJson(data);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
+
const list = Array.isArray(data) ? data : [data];
|
|
62
|
+
const misses = list.filter((d) => d && d.status === 'miss');
|
|
63
|
+
if (misses.length === 0)
|
|
61
64
|
return envelope;
|
|
62
|
-
const
|
|
63
|
-
const
|
|
65
|
+
const firstMiss = misses[0];
|
|
66
|
+
const normalizedUrl = firstMiss.normalizedUrl ?? '';
|
|
67
|
+
const suggestions = misses.map((m) => `Fetch and cache it first: ${this.config.bin} ${m.normalizedUrl}`);
|
|
64
68
|
const stderr = formatErrorForJson({
|
|
65
|
-
message: `Cache miss for ${normalizedUrl}
|
|
69
|
+
message: `Cache miss for ${normalizedUrl}` +
|
|
70
|
+
(misses.length > 1 ? ` and ${misses.length - 1} other URLs` : ''),
|
|
66
71
|
code: 'CACHE_MISS',
|
|
67
72
|
suggestions,
|
|
68
73
|
});
|
|
69
74
|
return { ...envelope, ok: false, exitCode: 1, stderr, code: 'CACHE_MISS', suggestions };
|
|
70
75
|
}
|
|
71
76
|
async run() {
|
|
72
|
-
const
|
|
77
|
+
const urls = this.parsedArgv;
|
|
73
78
|
const { ttl, tier, 'max-age': maxAge } = this.flags;
|
|
74
79
|
// Validate the duration flags up front so a malformed value reports the exact flag that is
|
|
75
80
|
// wrong, rather than later surfacing as a misattributed parse failure.
|
|
@@ -77,32 +82,59 @@ export default class ResearchStatus extends BaseCommand {
|
|
|
77
82
|
if (msg)
|
|
78
83
|
this.error(msg, { exit: 2, code: 'INVALID_DURATION' });
|
|
79
84
|
}
|
|
85
|
+
const results = [];
|
|
86
|
+
let hasMiss = false;
|
|
87
|
+
const currentTime = new Date();
|
|
88
|
+
for (const url of urls) {
|
|
89
|
+
const res = this.checkSingleStatus(url, currentTime, ttl, maxAge, urls.length > 1);
|
|
90
|
+
if (res.status === 'miss') {
|
|
91
|
+
hasMiss = true;
|
|
92
|
+
}
|
|
93
|
+
results.push(res);
|
|
94
|
+
}
|
|
95
|
+
if (hasMiss) {
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
}
|
|
98
|
+
return urls.length === 1 ? results[0] : results;
|
|
99
|
+
}
|
|
100
|
+
checkSingleStatus(url, currentTime, ttl, maxAge, showSeparator) {
|
|
80
101
|
const target = this.resolveResearchTargetOrFail(url);
|
|
81
102
|
const { cacheKey, located, normalizedUrl, roots } = target;
|
|
82
103
|
const cached = located?.artifact ?? null;
|
|
83
|
-
const currentTime = new Date();
|
|
84
|
-
// Both duration flags were validated above, so freshness resolution cannot throw on a parse error.
|
|
85
104
|
const result = describeCacheStatus(cached, currentTime, ttl, maxAge);
|
|
86
|
-
// On a hit, report where it actually lives; on a miss, where a fetch would write it.
|
|
87
105
|
const artifactPath = located?.path ?? getArtifactPath(roots.writeRoot, cacheKey);
|
|
88
106
|
if (!this.jsonEnabled()) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
const statusColorMap = {
|
|
108
|
+
hit: colors.green,
|
|
109
|
+
stale: colors.yellow,
|
|
110
|
+
miss: colors.red,
|
|
111
|
+
};
|
|
112
|
+
const freshnessColorMap = {
|
|
113
|
+
fresh: colors.green,
|
|
114
|
+
stale_grace: colors.yellow,
|
|
115
|
+
stale_expired: colors.red,
|
|
116
|
+
none: colors.gray,
|
|
117
|
+
};
|
|
118
|
+
const actionColorMap = {
|
|
119
|
+
would_return_cached: colors.green,
|
|
120
|
+
would_revalidate: colors.yellow,
|
|
121
|
+
would_fetch: colors.red,
|
|
122
|
+
};
|
|
123
|
+
const statusColor = statusColorMap[result.status] || ((t) => t);
|
|
124
|
+
const freshnessColor = freshnessColorMap[result.freshness] || ((t) => t);
|
|
125
|
+
const actionColor = actionColorMap[result.action] || ((t) => t);
|
|
126
|
+
this.log(`${colors.cyan('URL:'.padEnd(25))} ${colors.bold(normalizedUrl)}`);
|
|
127
|
+
this.log(`${colors.cyan('Cache Key:'.padEnd(25))} ${colors.bold(cacheKey)}`);
|
|
128
|
+
this.log(`${colors.cyan('Cache Path:'.padEnd(25))} ${colors.gray(artifactPath)}`);
|
|
129
|
+
this.log(`${colors.cyan('Status:'.padEnd(25))} ${statusColor(result.status)}`);
|
|
130
|
+
this.log(`${colors.cyan('Freshness:'.padEnd(25))} ${freshnessColor(result.freshness)}`);
|
|
131
|
+
this.log(`${colors.cyan('Action:'.padEnd(25))} ${actionColor(result.action)}`);
|
|
132
|
+
if (showSeparator) {
|
|
133
|
+
this.log('-'.repeat(40));
|
|
104
134
|
}
|
|
105
|
-
|
|
135
|
+
}
|
|
136
|
+
if (result.status === 'miss' && !this.jsonEnabled()) {
|
|
137
|
+
this.warn(`Cache miss — run: ${this.config.bin} ${normalizedUrl}`);
|
|
106
138
|
}
|
|
107
139
|
return {
|
|
108
140
|
cacheKey,
|
package/dist/commands.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ import ImportCommand from './commands/import.js';
|
|
|
8
8
|
import InspectCommand from './commands/inspect.js';
|
|
9
9
|
import ListCommand from './commands/list.js';
|
|
10
10
|
import PruneCommand from './commands/prune.js';
|
|
11
|
-
import SearchCommand from './commands/search.js';
|
|
12
11
|
import StatusCommand from './commands/status.js';
|
|
13
12
|
export declare const commands: {
|
|
14
13
|
fetch: typeof FetchCommand;
|
|
@@ -16,7 +15,6 @@ export declare const commands: {
|
|
|
16
15
|
inspect: typeof InspectCommand;
|
|
17
16
|
list: typeof ListCommand;
|
|
18
17
|
prune: typeof PruneCommand;
|
|
19
|
-
search: typeof SearchCommand;
|
|
20
18
|
status: typeof StatusCommand;
|
|
21
19
|
config: typeof ConfigIndex;
|
|
22
20
|
'config:get': typeof ConfigGet;
|
package/dist/commands.js
CHANGED
|
@@ -8,7 +8,6 @@ import ImportCommand from './commands/import.js';
|
|
|
8
8
|
import InspectCommand from './commands/inspect.js';
|
|
9
9
|
import ListCommand from './commands/list.js';
|
|
10
10
|
import PruneCommand from './commands/prune.js';
|
|
11
|
-
import SearchCommand from './commands/search.js';
|
|
12
11
|
import StatusCommand from './commands/status.js';
|
|
13
12
|
export const commands = {
|
|
14
13
|
fetch: FetchCommand,
|
|
@@ -16,7 +15,6 @@ export const commands = {
|
|
|
16
15
|
inspect: InspectCommand,
|
|
17
16
|
list: ListCommand,
|
|
18
17
|
prune: PruneCommand,
|
|
19
|
-
search: SearchCommand,
|
|
20
18
|
status: StatusCommand,
|
|
21
19
|
config: ConfigIndex,
|
|
22
20
|
'config:get': ConfigGet,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { type Hook } from '@oclif/core';
|
|
1
|
+
import { type Hook, type Interfaces } from '@oclif/core';
|
|
2
|
+
export declare function buildCommandNotFoundDetails(id: string, argv: string[] | undefined, config: Interfaces.Config): {
|
|
3
|
+
code: 'COMMAND_NOT_FOUND' | 'MISSING_URL_SCHEME' | 'UNEXPECTED_ARGUMENT';
|
|
4
|
+
command: string;
|
|
5
|
+
jsonMode: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
2
8
|
/**
|
|
3
9
|
* Typo-aware "command not found" handler. oclif fires this before printing its own terse error;
|
|
4
10
|
* throwing here (via `this.error`) replaces that default with a more helpful message. A correction
|
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
import { toConfiguredId } from '@oclif/core';
|
|
2
|
-
import { closestMatch } from '../../lib/text.js';
|
|
2
|
+
import { closestMatch, maxFuzzyDistance } from '../../lib/text.js';
|
|
3
3
|
import { buildEnvelope, formatErrorForJson } from '../../lib/envelope.js';
|
|
4
|
-
|
|
5
|
-
// very short inputs (`wat` should not suggest `list`), while still reasonable for longer topic ids.
|
|
6
|
-
function maxSuggestionDistance(input) {
|
|
7
|
-
if (input.length <= 3)
|
|
8
|
-
return 1;
|
|
9
|
-
if (input.length <= 5)
|
|
10
|
-
return 2;
|
|
11
|
-
return 3;
|
|
12
|
-
}
|
|
4
|
+
import { looksLikeSchemelessUrl } from '../../lib/research/url.js';
|
|
13
5
|
/**
|
|
14
6
|
* The nearest visible command to a typo, plus how many leading segments of the attempted id name
|
|
15
7
|
* the command. With `topicSeparator: ' '`, oclif folds every leading token of an unknown command
|
|
16
|
-
* into one colon-delimited id, so `bonsai
|
|
8
|
+
* into one colon-delimited id, so `bonsai lisst extra` arrives as `lisst:extra`. Matching that whole
|
|
17
9
|
* string never finds a short command, so walk prefixes longest→shortest and suggest off the closest:
|
|
18
|
-
* `
|
|
10
|
+
* `lisst:extra` resolves to nothing, but its prefix `lisst` resolves to `list`. The matched prefix
|
|
19
11
|
* length doubles as the command-segment count, so a folded-in positional arg is dropped from display.
|
|
20
12
|
*/
|
|
21
13
|
function findSuggestion(segments, visibleIds) {
|
|
22
14
|
for (let n = segments.length; n >= 1; n--) {
|
|
23
15
|
const attempted = segments.slice(0, n).join(':');
|
|
24
|
-
const suggestion = closestMatch(attempted, visibleIds,
|
|
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
|
+
}
|
|
25
22
|
if (suggestion)
|
|
26
23
|
return { suggestion, commandSegments: n };
|
|
27
24
|
}
|
|
@@ -38,30 +35,35 @@ function topicChainLength(segments, config) {
|
|
|
38
35
|
i++;
|
|
39
36
|
return i + 1;
|
|
40
37
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
}
|
|
46
56
|
/**
|
|
47
57
|
* The input as-typed when it looks like a URL missing its scheme (`example.com`,
|
|
48
58
|
* `docs.nestjs.com/guide`), else null. `bonsai <url>` is the headline command, but bin/cli.mjs only
|
|
49
59
|
* routes args carrying a `://` scheme to the fetch shorthand — so a scheme-less URL falls through to
|
|
50
60
|
* `command_not_found`. A domain-shaped host separates a forgotten-scheme URL from an ordinary command
|
|
51
61
|
* typo: `statuss` has no dot and never matches, while `config:frobnicate` fails to parse (the second
|
|
52
|
-
* 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.
|
|
53
64
|
*/
|
|
54
65
|
function bareUrlInput(id) {
|
|
55
|
-
|
|
56
|
-
return null;
|
|
57
|
-
let hostname;
|
|
58
|
-
try {
|
|
59
|
-
hostname = new URL(`https://${id}`).hostname;
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
return DOMAIN_OR_IPV4.test(hostname) ? id : null;
|
|
66
|
+
return looksLikeSchemelessUrl(id) ? id : null;
|
|
65
67
|
}
|
|
66
68
|
function isJsonMode(argv) {
|
|
67
69
|
return argv?.includes('--json') ?? false;
|
|
@@ -79,44 +81,67 @@ function emitJsonError(command, message, code) {
|
|
|
79
81
|
console.log(JSON.stringify(envelope, null, 2));
|
|
80
82
|
return envelope;
|
|
81
83
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* is offered only when the nearest command is a close match, and it is never auto-run — the user
|
|
86
|
-
* re-runs the corrected command themselves, so they learn the right syntax (clig.dev guidance).
|
|
87
|
-
*/
|
|
88
|
-
const hook = async function (opts) {
|
|
89
|
-
const hiddenIds = new Set(opts.config.commands.filter((c) => c.hidden).map((c) => c.id));
|
|
90
|
-
const visibleIds = opts.config.commandIDs.filter((id) => !hiddenIds.has(id));
|
|
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));
|
|
91
87
|
// A scheme-less URL is the most common "not a command" mistake for this CLI, so steer the user to
|
|
92
88
|
// the `bonsai <url>` shorthand with a scheme before falling back to nearest-command matching (which
|
|
93
89
|
// never finds a command for a hostname). The correction is shown, never auto-run (clig.dev).
|
|
94
|
-
const bareUrl = bareUrlInput(
|
|
90
|
+
const bareUrl = bareUrlInput(id);
|
|
95
91
|
if (bareUrl) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
};
|
|
104
116
|
}
|
|
105
|
-
const segments = opts.id.split(':');
|
|
106
117
|
const { suggestion, commandSegments } = findSuggestion(segments, visibleIds);
|
|
107
118
|
// Show only the segments that name the command, never the positional args oclif folded into the id
|
|
108
119
|
// (which would otherwise glue the arg on and turn `://` into ` //`).
|
|
109
|
-
const displaySegments = commandSegments || topicChainLength(segments,
|
|
110
|
-
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);
|
|
111
122
|
// Order so the most actionable line lands last, where the eye rests (clig.dev): the help pointer
|
|
112
123
|
// is the floor, and a concrete "Did you mean …?" correction — when we have one — goes below it.
|
|
113
|
-
const lines = [`${attempted} is not a ${
|
|
114
|
-
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.`);
|
|
115
126
|
if (suggestion)
|
|
116
|
-
lines.push(`Did you mean ${toConfiguredId(suggestion,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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 });
|
|
121
146
|
};
|
|
122
147
|
export default hook;
|
package/dist/lib/argv.js
CHANGED
|
@@ -15,6 +15,29 @@ function missingUsageJsonExit() {
|
|
|
15
15
|
}),
|
|
16
16
|
};
|
|
17
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
|
+
]);
|
|
18
41
|
export function normalizeArgv(rawArgv) {
|
|
19
42
|
const onlyJsonFlags = rawArgv.length > 0 && rawArgv.every((arg) => arg === '--json');
|
|
20
43
|
if (onlyJsonFlags) {
|
|
@@ -37,13 +60,31 @@ export function normalizeArgv(rawArgv) {
|
|
|
37
60
|
}
|
|
38
61
|
const helpRequested = tokens.includes('--help');
|
|
39
62
|
let core = tokens.filter((arg) => arg !== '--help');
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
];
|
|
47
88
|
}
|
|
48
89
|
if (helpRequested)
|
|
49
90
|
core.push('--help');
|
|
@@ -51,3 +92,6 @@ export function normalizeArgv(rawArgv) {
|
|
|
51
92
|
core.push('--json');
|
|
52
93
|
return { argv: core };
|
|
53
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
|
+
};
|
package/dist/lib/envelope.js
CHANGED
|
@@ -13,9 +13,15 @@ export function stableErrorCodeFrom(err) {
|
|
|
13
13
|
return 'UNKNOWN_FLAG';
|
|
14
14
|
case 'UnexpectedArgsError':
|
|
15
15
|
return 'UNEXPECTED_ARGUMENT';
|
|
16
|
-
default:
|
|
17
|
-
return undefined;
|
|
18
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;
|
|
19
25
|
}
|
|
20
26
|
/** Strip oclif's generic help suffix from JSON stderr — agents already have structured codes. */
|
|
21
27
|
export function normalizeCliErrorMessage(message) {
|
|
@@ -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 @@
|
|
|
1
|
+
export declare function limitFlag(max: number, defaultValue: number, description: string): import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Errors, Flags } from '@oclif/core';
|
|
2
|
+
export function limitFlag(max, defaultValue, description) {
|
|
3
|
+
return Flags.custom({
|
|
4
|
+
parse: async (input) => {
|
|
5
|
+
if (!/^\d+$/.test(input)) {
|
|
6
|
+
throw new Errors.CLIError(`Limit must be an integer between 1 and ${max}.`, {
|
|
7
|
+
exit: 2,
|
|
8
|
+
code: 'INVALID_LIMIT',
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
const value = Number(input);
|
|
12
|
+
if (value < 1 || value > max) {
|
|
13
|
+
throw new Errors.CLIError(`Limit must be between 1 and ${max}.`, {
|
|
14
|
+
exit: 2,
|
|
15
|
+
code: 'INVALID_LIMIT',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
},
|
|
20
|
+
})({
|
|
21
|
+
default: defaultValue,
|
|
22
|
+
description,
|
|
23
|
+
});
|
|
24
|
+
}
|