@taurgis/bonsai 3.2.0 → 4.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 +9 -6
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -4
- package/agents/instructions/salesforce-research.instructions.md +2 -1
- package/bin/cli.mjs +13 -18
- package/dist/base-command.d.ts +59 -12
- package/dist/base-command.js +179 -51
- package/dist/commands/config/base.js +12 -2
- package/dist/commands/config/get.js +8 -12
- package/dist/commands/config/index.js +2 -2
- package/dist/commands/config/list.js +11 -17
- package/dist/commands/config/set.d.ts +2 -1
- package/dist/commands/config/set.js +35 -28
- package/dist/commands/config/unset.d.ts +2 -1
- package/dist/commands/config/unset.js +26 -25
- package/dist/commands/fetch.d.ts +7 -15
- package/dist/commands/fetch.js +61 -401
- package/dist/commands/import.d.ts +4 -9
- package/dist/commands/import.js +56 -284
- package/dist/commands/inspect.d.ts +7 -1
- package/dist/commands/inspect.js +112 -52
- package/dist/commands/list.d.ts +18 -3
- package/dist/commands/list.js +121 -46
- package/dist/commands/prune.d.ts +6 -1
- package/dist/commands/prune.js +89 -78
- package/dist/commands/status.d.ts +3 -2
- package/dist/commands/status.js +62 -79
- package/dist/hooks/command-not-found/suggest.d.ts +2 -1
- package/dist/hooks/command-not-found/suggest.js +78 -41
- package/dist/lib/argv.d.ts +29 -3
- package/dist/lib/argv.js +147 -54
- 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 +12 -0
- package/dist/lib/cli-flag-manifest.js +15 -0
- package/dist/lib/cli-presentation.d.ts +41 -0
- package/dist/lib/cli-presentation.js +53 -0
- package/dist/lib/cli-result-types.d.ts +112 -0
- package/dist/lib/cli-result-types.js +2 -0
- package/dist/lib/config/index.d.ts +23 -5
- package/dist/lib/config/index.js +60 -5
- package/dist/lib/config/io.d.ts +9 -0
- package/dist/lib/config/io.js +71 -21
- package/dist/lib/config/schema.d.ts +2 -0
- package/dist/lib/config/schema.js +7 -0
- 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 +154 -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 +30 -0
- package/dist/lib/prune-flags.js +86 -0
- package/dist/lib/research/artifact-index.d.ts +11 -2
- package/dist/lib/research/artifact-index.js +14 -5
- package/dist/lib/research/artifact.js +20 -2
- package/dist/lib/research/browser.d.ts +53 -2
- package/dist/lib/research/browser.js +202 -58
- 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 +40 -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 +367 -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 +61 -31
- 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 +402 -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/metadata-filters.d.ts +18 -0
- package/dist/lib/research/metadata-filters.js +37 -0
- package/dist/lib/research/metadata-flags.d.ts +34 -0
- package/dist/lib/research/metadata-flags.js +45 -0
- 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 +9 -0
- package/dist/lib/research/prompt-injection.js +59 -14
- package/dist/lib/research/proxy.d.ts +14 -8
- package/dist/lib/research/proxy.js +14 -10
- package/dist/lib/research/resolve-target.d.ts +2 -0
- package/dist/lib/research/resolve-target.js +1 -1
- package/dist/lib/research/revalidate.d.ts +49 -20
- package/dist/lib/research/revalidate.js +80 -15
- package/dist/lib/research/schema.d.ts +7 -0
- package/dist/lib/research/schema.js +2 -0
- package/dist/lib/research/secret-scan.d.ts +6 -0
- package/dist/lib/research/secret-scan.js +8 -0
- package/dist/lib/research/secure-write.d.ts +4 -0
- package/dist/lib/research/secure-write.js +31 -2
- package/dist/lib/research/storage.d.ts +9 -8
- package/dist/lib/research/storage.js +34 -28
- 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 +19 -8
- package/dist/lib/text.js +32 -9
- 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 +216 -102
- package/package.json +20 -7
- package/agents/agents/official-docs-researcher.agent.md +0 -87
- package/agents/instructions/web-research.instructions.md +0 -44
- package/agents/skills/web-research/SKILL.md +0 -118
package/dist/commands/status.js
CHANGED
|
@@ -1,77 +1,67 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../base-command.js';
|
|
3
|
-
import {
|
|
3
|
+
import { finalizeBatch, isBatchReadFailure, urlValidationErrorRow } from '../lib/batch.js';
|
|
4
4
|
import { getArtifactPath } from '../lib/research/storage.js';
|
|
5
|
-
import {
|
|
5
|
+
import { durationFlagError, evaluateFreshnessWithMaxAge } from '../lib/research/freshness.js';
|
|
6
|
+
import { CLI_FLAG_DESCRIPTIONS } from '../lib/cli-presentation.js';
|
|
7
|
+
import { batchSeparator, cacheMissHint, formatCacheTargetHeader } from '../lib/cache-view.js';
|
|
6
8
|
import { colors } from '../lib/color.js';
|
|
7
|
-
function resolveFreshness(cached, currentTime, ttl, maxAge) {
|
|
8
|
-
|
|
9
|
-
return isMaxAgeExpired
|
|
10
|
-
? 'stale_expired'
|
|
11
|
-
: evaluateFreshness(cached.metadata, currentTime, ttl || cached.metadata.ttl);
|
|
9
|
+
function resolveFreshness(cached, currentTime, ttl, maxAge, tier) {
|
|
10
|
+
return evaluateFreshnessWithMaxAge(cached, currentTime, { ttl, maxAge, tier });
|
|
12
11
|
}
|
|
13
|
-
function describeCacheStatus(cached, currentTime, ttl, maxAge) {
|
|
12
|
+
function describeCacheStatus(cached, currentTime, ttl, maxAge, tier) {
|
|
14
13
|
if (!cached) {
|
|
15
14
|
return { status: 'miss', freshness: 'none', action: 'would_fetch' };
|
|
16
15
|
}
|
|
17
|
-
const freshness = resolveFreshness(cached, currentTime, ttl, maxAge);
|
|
16
|
+
const freshness = resolveFreshness(cached, currentTime, ttl, maxAge, tier);
|
|
18
17
|
return freshness === 'fresh'
|
|
19
18
|
? { status: 'hit', freshness, action: 'would_return_cached' }
|
|
20
19
|
: { status: 'stale', freshness, action: 'would_revalidate' };
|
|
21
20
|
}
|
|
21
|
+
/** Report cache hit/miss and freshness without writing. */
|
|
22
22
|
export default class ResearchStatus extends BaseCommand {
|
|
23
23
|
static id = 'status';
|
|
24
|
-
static summary = 'Check
|
|
25
|
-
static description = '
|
|
24
|
+
static summary = 'Check cache status without fetching or writing';
|
|
25
|
+
static description = 'Report cache hit/miss state, freshness, and the action the URL shorthand would take.';
|
|
26
26
|
static examples = [
|
|
27
27
|
{
|
|
28
|
-
description: 'check
|
|
28
|
+
description: 'check whether a URL is cached',
|
|
29
29
|
command: '<%= config.bin %> <%= command.id %> https://example.com/docs',
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
|
-
description: '
|
|
32
|
+
description: 'evaluate status with a specific tier and TTL',
|
|
33
33
|
command: '<%= config.bin %> <%= command.id %> https://example.com/docs --tier volatile --ttl 2h',
|
|
34
34
|
},
|
|
35
|
+
{
|
|
36
|
+
description: 'check multiple URLs in one batch (space-separated)',
|
|
37
|
+
command: '<%= config.bin %> <%= command.id %> https://example.com/docs https://example.com/api',
|
|
38
|
+
},
|
|
35
39
|
];
|
|
36
40
|
static strict = false;
|
|
37
41
|
static args = {
|
|
38
42
|
url: Args.string({
|
|
39
43
|
required: true,
|
|
40
|
-
description: '
|
|
44
|
+
description: 'URL to check',
|
|
41
45
|
}),
|
|
42
46
|
};
|
|
43
47
|
static flags = {
|
|
48
|
+
// No default: omitting --tier evaluates against the artifact's stored tier. A default of
|
|
49
|
+
// `standard` would silently re-grade stable/volatile entries and lie about planned action.
|
|
44
50
|
tier: Flags.option({
|
|
45
|
-
description:
|
|
51
|
+
description: CLI_FLAG_DESCRIPTIONS.statusFreshnessTierPolicy,
|
|
46
52
|
options: ['stable', 'standard', 'volatile'],
|
|
47
|
-
default: 'standard',
|
|
48
53
|
})(),
|
|
49
54
|
ttl: Flags.string({
|
|
50
55
|
char: 'l',
|
|
51
|
-
description:
|
|
56
|
+
description: CLI_FLAG_DESCRIPTIONS.statusTtl,
|
|
52
57
|
}),
|
|
53
58
|
'max-age': Flags.string({
|
|
54
|
-
description:
|
|
59
|
+
description: CLI_FLAG_DESCRIPTIONS.maxAge,
|
|
55
60
|
}),
|
|
56
61
|
};
|
|
57
62
|
static stdoutIsPrimaryData = true;
|
|
58
|
-
/** Enrich cache-miss envelopes with the same CACHE_MISS code and suggestions as inspect. */
|
|
59
63
|
toSuccessJson(data) {
|
|
60
|
-
|
|
61
|
-
const list = Array.isArray(data) ? data : [data];
|
|
62
|
-
const misses = list.filter((d) => d && d.status === 'miss');
|
|
63
|
-
if (misses.length === 0)
|
|
64
|
-
return envelope;
|
|
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}`);
|
|
68
|
-
const stderr = formatErrorForJson({
|
|
69
|
-
message: `Cache miss for ${normalizedUrl}` +
|
|
70
|
-
(misses.length > 1 ? ` and ${misses.length - 1} other URLs` : ''),
|
|
71
|
-
code: 'CACHE_MISS',
|
|
72
|
-
suggestions,
|
|
73
|
-
});
|
|
74
|
-
return { ...envelope, ok: false, exitCode: 1, stderr, code: 'CACHE_MISS', suggestions };
|
|
64
|
+
return this.batchReadSuccessJson(data);
|
|
75
65
|
}
|
|
76
66
|
async run() {
|
|
77
67
|
const urls = this.parsedArgv;
|
|
@@ -82,59 +72,27 @@ export default class ResearchStatus extends BaseCommand {
|
|
|
82
72
|
if (msg)
|
|
83
73
|
this.error(msg, { exit: 2, code: 'INVALID_DURATION' });
|
|
84
74
|
}
|
|
85
|
-
const results = [];
|
|
86
|
-
let hasMiss = false;
|
|
87
75
|
const currentTime = new Date();
|
|
88
|
-
|
|
89
|
-
|
|
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;
|
|
76
|
+
const results = this.mapUrlsAllowingBatchErrors(urls, (url) => this.checkSingleStatus(url, currentTime, { ttl, maxAge, tier }), urlValidationErrorRow);
|
|
77
|
+
return finalizeBatch(results, isBatchReadFailure);
|
|
99
78
|
}
|
|
100
|
-
checkSingleStatus(url, currentTime,
|
|
79
|
+
checkSingleStatus(url, currentTime, policy) {
|
|
101
80
|
const target = this.resolveResearchTargetOrFail(url);
|
|
102
81
|
const { cacheKey, located, normalizedUrl, roots } = target;
|
|
103
82
|
const cached = located?.artifact ?? null;
|
|
104
|
-
const result = describeCacheStatus(cached, currentTime, ttl, maxAge);
|
|
83
|
+
const result = describeCacheStatus(cached, currentTime, policy.ttl, policy.maxAge, policy.tier);
|
|
105
84
|
const artifactPath = located?.path ?? getArtifactPath(roots.writeRoot, cacheKey);
|
|
106
85
|
if (!this.jsonEnabled()) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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));
|
|
86
|
+
this.logStatusTable(target, artifactPath, result);
|
|
87
|
+
if (result.status === 'miss') {
|
|
88
|
+
this.warn(cacheMissHint(this.config.bin, normalizedUrl));
|
|
89
|
+
}
|
|
90
|
+
else if (result.action === 'would_revalidate') {
|
|
91
|
+
this.tip(`${this.config.bin} ${normalizedUrl} to revalidate.`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.tip(`${this.config.bin} inspect ${normalizedUrl} for full metadata.`);
|
|
134
95
|
}
|
|
135
|
-
}
|
|
136
|
-
if (result.status === 'miss' && !this.jsonEnabled()) {
|
|
137
|
-
this.warn(`Cache miss — run: ${this.config.bin} ${normalizedUrl}`);
|
|
138
96
|
}
|
|
139
97
|
return {
|
|
140
98
|
cacheKey,
|
|
@@ -145,4 +103,29 @@ export default class ResearchStatus extends BaseCommand {
|
|
|
145
103
|
action: result.action,
|
|
146
104
|
};
|
|
147
105
|
}
|
|
106
|
+
logStatusTable(target, artifactPath, result) {
|
|
107
|
+
const colorOf = (map, key) => map[key] ?? ((t) => t);
|
|
108
|
+
const statusColor = colorOf({ hit: colors.green, stale: colors.yellow, miss: colors.red }, result.status);
|
|
109
|
+
const freshnessColor = colorOf({
|
|
110
|
+
fresh: colors.green,
|
|
111
|
+
stale_grace: colors.yellow,
|
|
112
|
+
stale_expired: colors.red,
|
|
113
|
+
none: colors.gray,
|
|
114
|
+
}, result.freshness);
|
|
115
|
+
const actionColor = colorOf({
|
|
116
|
+
would_return_cached: colors.green,
|
|
117
|
+
would_revalidate: colors.yellow,
|
|
118
|
+
would_fetch: colors.red,
|
|
119
|
+
}, result.action);
|
|
120
|
+
for (const line of formatCacheTargetHeader(target, [
|
|
121
|
+
['Status', statusColor(result.status)],
|
|
122
|
+
['Freshness', freshnessColor(result.freshness)],
|
|
123
|
+
['Action', actionColor(result.action)],
|
|
124
|
+
], artifactPath)) {
|
|
125
|
+
this.log(line);
|
|
126
|
+
}
|
|
127
|
+
const sep = batchSeparator(this.parsedArgv.length > 1);
|
|
128
|
+
if (sep)
|
|
129
|
+
this.log(sep);
|
|
130
|
+
}
|
|
148
131
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type Hook, type Interfaces } from '@oclif/core';
|
|
2
|
-
export declare function buildCommandNotFoundDetails(
|
|
2
|
+
export declare function buildCommandNotFoundDetails(rawId: string, argv: string[] | undefined, config: Interfaces.Config): {
|
|
3
3
|
code: 'COMMAND_NOT_FOUND' | 'MISSING_URL_SCHEME' | 'UNEXPECTED_ARGUMENT';
|
|
4
4
|
command: string;
|
|
5
5
|
jsonMode: boolean;
|
|
6
6
|
message: string;
|
|
7
|
+
suggestions?: string[];
|
|
7
8
|
};
|
|
8
9
|
/**
|
|
9
10
|
* Typo-aware "command not found" handler. oclif fires this before printing its own terse error;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { toConfiguredId } from '@oclif/core';
|
|
2
|
-
import { closestMatch, maxFuzzyDistance } from '../../lib/text.js';
|
|
3
|
-
import {
|
|
2
|
+
import { closestMatch, maxFuzzyDistance, sanitizeForTerminal } from '../../lib/text.js';
|
|
3
|
+
import { writeCommandNotFoundJson } from '../../lib/cli-emit.js';
|
|
4
4
|
import { looksLikeSchemelessUrl } from '../../lib/research/url.js';
|
|
5
5
|
/**
|
|
6
6
|
* The nearest visible command to a typo, plus how many leading segments of the attempted id name
|
|
@@ -10,13 +10,13 @@ import { looksLikeSchemelessUrl } from '../../lib/research/url.js';
|
|
|
10
10
|
* `lisst:extra` resolves to nothing, but its prefix `lisst` resolves to `list`. The matched prefix
|
|
11
11
|
* length doubles as the command-segment count, so a folded-in positional arg is dropped from display.
|
|
12
12
|
*/
|
|
13
|
-
function findSuggestion(segments,
|
|
13
|
+
function findSuggestion(segments, commandIds) {
|
|
14
14
|
for (let n = segments.length; n >= 1; n--) {
|
|
15
15
|
const attempted = segments.slice(0, n).join(':');
|
|
16
|
-
const suggestion = closestMatch(attempted,
|
|
16
|
+
const suggestion = closestMatch(attempted, commandIds, maxFuzzyDistance(attempted));
|
|
17
17
|
if (suggestion === attempted &&
|
|
18
18
|
n < segments.length &&
|
|
19
|
-
|
|
19
|
+
commandIds.some((id) => id.startsWith(`${attempted}:`))) {
|
|
20
20
|
continue;
|
|
21
21
|
}
|
|
22
22
|
if (suggestion)
|
|
@@ -35,12 +35,12 @@ function topicChainLength(segments, config) {
|
|
|
35
35
|
i++;
|
|
36
36
|
return i + 1;
|
|
37
37
|
}
|
|
38
|
-
function exactZeroArgCommandPrefix(segments,
|
|
38
|
+
function exactZeroArgCommandPrefix(segments, commandIds, config) {
|
|
39
39
|
for (let n = segments.length - 1; n >= 1; n--) {
|
|
40
40
|
const candidate = segments.slice(0, n).join(':');
|
|
41
|
-
if (!
|
|
41
|
+
if (!commandIds.includes(candidate))
|
|
42
42
|
continue;
|
|
43
|
-
if (
|
|
43
|
+
if (commandIds.some((id) => id.startsWith(`${candidate}:`)))
|
|
44
44
|
continue;
|
|
45
45
|
const command = config.commands.find((entry) => entry.id === candidate);
|
|
46
46
|
const args = Object.keys(command?.args ?? {});
|
|
@@ -65,43 +65,64 @@ function exactZeroArgCommandPrefix(segments, visibleIds, config) {
|
|
|
65
65
|
function bareUrlInput(id) {
|
|
66
66
|
return looksLikeSchemelessUrl(id) ? id : null;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The id's colon-joined segments when every one individually looks like a URL missing its scheme,
|
|
70
|
+
* else null. Covers the multi-URL batch-fetch typo (`bonsai example.com example.org` folds into one
|
|
71
|
+
* id, "example.com:example.org") that {@link bareUrlInput} misses: parsed as a single URL, the colon
|
|
72
|
+
* between two hostnames reads as an invalid port, so the whole-string check fails even though each
|
|
73
|
+
* segment on its own is a plain scheme-less host.
|
|
74
|
+
*/
|
|
75
|
+
function bareUrlSegments(id) {
|
|
76
|
+
const segments = id.split(':');
|
|
77
|
+
return segments.length > 1 && segments.every((segment) => looksLikeSchemelessUrl(segment))
|
|
78
|
+
? segments
|
|
79
|
+
: null;
|
|
80
|
+
}
|
|
68
81
|
function isJsonMode(argv) {
|
|
69
|
-
return argv?.includes('--json') ?? false;
|
|
82
|
+
return (argv?.includes('--json') ?? false) || process.argv.includes('--json');
|
|
70
83
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
/** Shared "did you mean https://…?" response for one or several scheme-less URLs. */
|
|
85
|
+
function missingUrlSchemeDetails(urls, argv, config) {
|
|
86
|
+
const attempted = urls.join(' ');
|
|
87
|
+
const suggestion = `${config.bin} ${urls.map((url) => `https://${url}`).join(' ')}`;
|
|
88
|
+
return {
|
|
89
|
+
code: 'MISSING_URL_SCHEME',
|
|
90
|
+
command: attempted,
|
|
91
|
+
jsonMode: isJsonMode(argv),
|
|
92
|
+
message: [
|
|
93
|
+
`${attempted} is not a ${config.bin} command.`,
|
|
94
|
+
`Run ${config.bin} help for a list of available commands.`,
|
|
95
|
+
`Did you mean ${suggestion}? URLs need an http:// or https:// scheme.`,
|
|
96
|
+
].join('\n'),
|
|
97
|
+
suggestions: [suggestion],
|
|
98
|
+
};
|
|
83
99
|
}
|
|
84
|
-
export function buildCommandNotFoundDetails(
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
export function buildCommandNotFoundDetails(rawId, argv, config) {
|
|
101
|
+
// The attempted id is raw user input echoed straight back into this error's message and
|
|
102
|
+
// suggestions — untrusted per the repo's trust-boundary rules, same reasoning as
|
|
103
|
+
// BaseCommand.error(). Sanitize once, up front, so every derived value below (segments, the
|
|
104
|
+
// display-only "attempted" string, a bare-URL correction) is already safe; a real command/topic id
|
|
105
|
+
// never contains control bytes, so this only affects input that could never have matched one
|
|
106
|
+
// anyway. Gate on jsonMode exactly like BaseCommand.error() does, so `--json` keeps
|
|
107
|
+
// byte-for-byte fidelity — this hook fires before a Command instance exists, so there's no
|
|
108
|
+
// `jsonEnabled()` to call, but the same boolean is cheap to compute directly from argv.
|
|
109
|
+
const jsonMode = isJsonMode(argv);
|
|
110
|
+
const id = jsonMode ? rawId : sanitizeForTerminal(rawId);
|
|
111
|
+
// Match against every loaded command id — including hidden ones that stay invokable
|
|
112
|
+
// (`fetch`, plugin internals). Root `--help` hides them; typo recovery should not.
|
|
113
|
+
const commandIds = config.commandIDs;
|
|
87
114
|
// A scheme-less URL is the most common "not a command" mistake for this CLI, so steer the user to
|
|
88
115
|
// the `bonsai <url>` shorthand with a scheme before falling back to nearest-command matching (which
|
|
89
116
|
// never finds a command for a hostname). The correction is shown, never auto-run (clig.dev).
|
|
117
|
+
// Flag-only argv never reaches here — normalizeArgv early-exits those as MISSING_COMMAND.
|
|
90
118
|
const bareUrl = bareUrlInput(id);
|
|
91
|
-
if (bareUrl)
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
}
|
|
119
|
+
if (bareUrl)
|
|
120
|
+
return missingUrlSchemeDetails([bareUrl], argv, config);
|
|
121
|
+
const bareUrls = bareUrlSegments(id);
|
|
122
|
+
if (bareUrls)
|
|
123
|
+
return missingUrlSchemeDetails(bareUrls, argv, config);
|
|
103
124
|
const segments = id.split(':');
|
|
104
|
-
const unexpected = exactZeroArgCommandPrefix(segments,
|
|
125
|
+
const unexpected = exactZeroArgCommandPrefix(segments, commandIds, config);
|
|
105
126
|
if (unexpected) {
|
|
106
127
|
const command = toConfiguredId(unexpected.commandId, config);
|
|
107
128
|
return {
|
|
@@ -114,7 +135,7 @@ export function buildCommandNotFoundDetails(id, argv, config) {
|
|
|
114
135
|
].join('\n'),
|
|
115
136
|
};
|
|
116
137
|
}
|
|
117
|
-
const { suggestion, commandSegments } = findSuggestion(segments,
|
|
138
|
+
const { suggestion, commandSegments } = findSuggestion(segments, commandIds);
|
|
118
139
|
// Show only the segments that name the command, never the positional args oclif folded into the id
|
|
119
140
|
// (which would otherwise glue the arg on and turn `://` into ` //`).
|
|
120
141
|
const displaySegments = commandSegments || topicChainLength(segments, config);
|
|
@@ -123,6 +144,9 @@ export function buildCommandNotFoundDetails(id, argv, config) {
|
|
|
123
144
|
// is the floor, and a concrete "Did you mean …?" correction — when we have one — goes below it.
|
|
124
145
|
const lines = [`${attempted} is not a ${config.bin} command.`];
|
|
125
146
|
lines.push(`Run ${config.bin} help for a list of available commands.`);
|
|
147
|
+
const suggestions = suggestion
|
|
148
|
+
? [`${config.bin} ${toConfiguredId(suggestion, config)}`]
|
|
149
|
+
: undefined;
|
|
126
150
|
if (suggestion)
|
|
127
151
|
lines.push(`Did you mean ${toConfiguredId(suggestion, config)}?`);
|
|
128
152
|
return {
|
|
@@ -130,6 +154,7 @@ export function buildCommandNotFoundDetails(id, argv, config) {
|
|
|
130
154
|
command: attempted,
|
|
131
155
|
jsonMode: isJsonMode(argv),
|
|
132
156
|
message: lines.join('\n'),
|
|
157
|
+
suggestions,
|
|
133
158
|
};
|
|
134
159
|
}
|
|
135
160
|
/**
|
|
@@ -140,8 +165,20 @@ export function buildCommandNotFoundDetails(id, argv, config) {
|
|
|
140
165
|
*/
|
|
141
166
|
const hook = async function (opts) {
|
|
142
167
|
const details = buildCommandNotFoundDetails(opts.id, opts.argv, opts.config);
|
|
143
|
-
if (details.jsonMode)
|
|
144
|
-
return
|
|
145
|
-
|
|
168
|
+
if (details.jsonMode) {
|
|
169
|
+
return writeCommandNotFoundJson({
|
|
170
|
+
command: details.command,
|
|
171
|
+
message: details.message,
|
|
172
|
+
code: details.code,
|
|
173
|
+
suggestions: details.suggestions,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Hook context typings omit `suggestions` even though CLIError accepts them; cast keeps the
|
|
177
|
+
// human pretty-print "Try this:" lines aligned with the JSON envelope.
|
|
178
|
+
this.error(details.message, {
|
|
179
|
+
exit: 2,
|
|
180
|
+
code: details.code,
|
|
181
|
+
suggestions: details.suggestions,
|
|
182
|
+
});
|
|
146
183
|
};
|
|
147
184
|
export default hook;
|
package/dist/lib/argv.d.ts
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
|
+
/** Result of argv normalization before oclif command dispatch. */
|
|
1
2
|
export interface NormalizationResult {
|
|
2
3
|
/** The normalized argv array to set on process.argv. */
|
|
3
4
|
argv: string[];
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Exit before oclif runs. Used for flag-only / bare `--json` usage errors so human and JSON
|
|
7
|
+
* callers share one path (no "leave a flag token for command_not_found" hack).
|
|
8
|
+
*/
|
|
9
|
+
earlyExit?: {
|
|
6
10
|
exitCode: number;
|
|
7
11
|
envelope: Record<string, unknown>;
|
|
12
|
+
json: boolean;
|
|
8
13
|
};
|
|
9
14
|
}
|
|
10
|
-
|
|
15
|
+
/** Options for {@link normalizeArgv}. */
|
|
16
|
+
export interface ArgvNormalizeOptions {
|
|
17
|
+
/** Tokens that consume the next argv value (`--topic`, `-t`, …). From cli-flag-manifest. */
|
|
18
|
+
valueTakingFlags: ReadonlySet<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Root segment of every registered command id (`config:get` → `config`). From cli-flag-manifest.
|
|
21
|
+
* A `word:` token whose root is in this set is a command (`config:get`), never a URL scheme.
|
|
22
|
+
*/
|
|
23
|
+
knownCommandRoots: ReadonlySet<string>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Normalize raw process argv for oclif. `valueTakingFlags` must come from the composition root
|
|
27
|
+
* (`cli-flag-manifest`) — this module stays free of command registry imports.
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeArgv(rawArgv: string[], options: ArgvNormalizeOptions): NormalizationResult;
|
|
30
|
+
/**
|
|
31
|
+
* Non-flag positional tokens from a (typically already-normalized) argv. Skips `--help`/`--json`
|
|
32
|
+
* and any dash-prefixed flags so multi-segment command ids (`config get`) stay intact. Shared by
|
|
33
|
+
* help-preflight and json-meta — both run after `normalizeArgv` has already folded `help X` →
|
|
34
|
+
* `X --help` and `-h` → `--help`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function positionalArgvTokens(argv: readonly string[], valueTakingFlags: ReadonlySet<string>): string[];
|