@taurgis/bonsai 3.2.0 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +7 -4
  2. package/agents/agents/salesforce-docs-researcher.agent.md +14 -1
  3. package/agents/instructions/salesforce-research.instructions.md +2 -1
  4. package/bin/cli.mjs +9 -17
  5. package/dist/base-command.d.ts +22 -12
  6. package/dist/base-command.js +86 -49
  7. package/dist/commands/config/base.js +11 -2
  8. package/dist/commands/config/get.js +8 -12
  9. package/dist/commands/config/index.js +2 -2
  10. package/dist/commands/config/list.js +10 -16
  11. package/dist/commands/config/set.d.ts +2 -1
  12. package/dist/commands/config/set.js +34 -27
  13. package/dist/commands/config/unset.d.ts +2 -1
  14. package/dist/commands/config/unset.js +25 -24
  15. package/dist/commands/fetch.d.ts +7 -15
  16. package/dist/commands/fetch.js +57 -401
  17. package/dist/commands/import.d.ts +4 -9
  18. package/dist/commands/import.js +56 -284
  19. package/dist/commands/inspect.d.ts +7 -1
  20. package/dist/commands/inspect.js +103 -52
  21. package/dist/commands/list.d.ts +13 -1
  22. package/dist/commands/list.js +79 -32
  23. package/dist/commands/prune.d.ts +4 -1
  24. package/dist/commands/prune.js +55 -75
  25. package/dist/commands/status.d.ts +3 -2
  26. package/dist/commands/status.js +52 -79
  27. package/dist/hooks/command-not-found/suggest.d.ts +1 -0
  28. package/dist/hooks/command-not-found/suggest.js +66 -39
  29. package/dist/lib/argv.d.ts +24 -3
  30. package/dist/lib/argv.js +124 -53
  31. package/dist/lib/batch.d.ts +41 -0
  32. package/dist/lib/batch.js +39 -0
  33. package/dist/lib/cache-view.d.ts +21 -0
  34. package/dist/lib/cache-view.js +30 -0
  35. package/dist/lib/cli-emit.d.ts +28 -0
  36. package/dist/lib/cli-emit.js +44 -0
  37. package/dist/lib/cli-error-policy.d.ts +57 -0
  38. package/dist/lib/cli-error-policy.js +74 -0
  39. package/dist/lib/cli-flag-manifest.d.ts +5 -0
  40. package/dist/lib/cli-flag-manifest.js +8 -0
  41. package/dist/lib/cli-presentation.d.ts +39 -0
  42. package/dist/lib/cli-presentation.js +50 -0
  43. package/dist/lib/cli-result-types.d.ts +88 -0
  44. package/dist/lib/cli-result-types.js +2 -0
  45. package/dist/lib/config/index.d.ts +21 -3
  46. package/dist/lib/config/index.js +58 -3
  47. package/dist/lib/config-persist.d.ts +28 -0
  48. package/dist/lib/config-persist.js +29 -0
  49. package/dist/lib/envelope.d.ts +41 -3
  50. package/dist/lib/envelope.js +147 -4
  51. package/dist/lib/help-preflight.d.ts +5 -8
  52. package/dist/lib/help-preflight.js +13 -32
  53. package/dist/lib/json-meta.d.ts +0 -2
  54. package/dist/lib/json-meta.js +21 -25
  55. package/dist/lib/limit-flag.d.ts +11 -1
  56. package/dist/lib/limit-flag.js +19 -6
  57. package/dist/lib/parse-error-ux.d.ts +5 -0
  58. package/dist/lib/parse-error-ux.js +73 -0
  59. package/dist/lib/prune-flags.d.ts +28 -0
  60. package/dist/lib/prune-flags.js +74 -0
  61. package/dist/lib/research/artifact-index.d.ts +11 -2
  62. package/dist/lib/research/artifact-index.js +7 -4
  63. package/dist/lib/research/browser.d.ts +41 -2
  64. package/dist/lib/research/browser.js +139 -50
  65. package/dist/lib/research/capture.d.ts +24 -2
  66. package/dist/lib/research/capture.js +24 -4
  67. package/dist/lib/research/cli-io.d.ts +33 -0
  68. package/dist/lib/research/cli-io.js +27 -0
  69. package/dist/lib/research/compress.d.ts +7 -0
  70. package/dist/lib/research/compress.js +7 -3
  71. package/dist/lib/research/docs/capabilities.d.ts +15 -0
  72. package/dist/lib/research/docs/capabilities.js +5 -0
  73. package/dist/lib/research/docs/clean-dom.d.ts +2 -0
  74. package/dist/lib/research/docs/clean-dom.js +2 -0
  75. package/dist/lib/research/docs/code-blocks.d.ts +2 -0
  76. package/dist/lib/research/docs/code-blocks.js +2 -0
  77. package/dist/lib/research/docs/detect.d.ts +4 -0
  78. package/dist/lib/research/docs/detect.js +4 -0
  79. package/dist/lib/research/docs/machine-readable.d.ts +18 -0
  80. package/dist/lib/research/docs/machine-readable.js +10 -0
  81. package/dist/lib/research/docs/markdown-source.d.ts +20 -0
  82. package/dist/lib/research/docs/markdown-source.js +19 -2
  83. package/dist/lib/research/docs/quality-gates.d.ts +5 -0
  84. package/dist/lib/research/docs/quality-gates.js +4 -0
  85. package/dist/lib/research/docs/rsc.d.ts +9 -2
  86. package/dist/lib/research/docs/rsc.js +5 -2
  87. package/dist/lib/research/docs/section-artifacts.d.ts +12 -0
  88. package/dist/lib/research/docs/section-artifacts.js +12 -0
  89. package/dist/lib/research/docs/sections.d.ts +18 -0
  90. package/dist/lib/research/docs/sections.js +17 -1
  91. package/dist/lib/research/docs/source-map.d.ts +28 -0
  92. package/dist/lib/research/docs/source-map.js +27 -0
  93. package/dist/lib/research/docs/validate.d.ts +35 -2
  94. package/dist/lib/research/docs/validate.js +47 -2
  95. package/dist/lib/research/extract.d.ts +16 -0
  96. package/dist/lib/research/extract.js +12 -1
  97. package/dist/lib/research/fetch-command-service.d.ts +36 -0
  98. package/dist/lib/research/fetch-command-service.js +310 -0
  99. package/dist/lib/research/fetch-result.d.ts +114 -0
  100. package/dist/lib/research/fetch-result.js +212 -0
  101. package/dist/lib/research/fetcher.d.ts +46 -8
  102. package/dist/lib/research/fetcher.js +58 -30
  103. package/dist/lib/research/freshness.d.ts +54 -11
  104. package/dist/lib/research/freshness.js +91 -31
  105. package/dist/lib/research/import-command-service.d.ts +56 -0
  106. package/dist/lib/research/import-command-service.js +381 -0
  107. package/dist/lib/research/keywords.js +5 -1
  108. package/dist/lib/research/markdown.d.ts +12 -1
  109. package/dist/lib/research/markdown.js +13 -5
  110. package/dist/lib/research/persist-artifact.d.ts +23 -0
  111. package/dist/lib/research/persist-artifact.js +27 -0
  112. package/dist/lib/research/prompt-injection.d.ts +8 -0
  113. package/dist/lib/research/prompt-injection.js +42 -13
  114. package/dist/lib/research/proxy.d.ts +14 -8
  115. package/dist/lib/research/proxy.js +14 -10
  116. package/dist/lib/research/revalidate.d.ts +49 -20
  117. package/dist/lib/research/revalidate.js +80 -15
  118. package/dist/lib/research/schema.d.ts +7 -0
  119. package/dist/lib/research/schema.js +2 -0
  120. package/dist/lib/research/secret-scan.d.ts +6 -0
  121. package/dist/lib/research/secret-scan.js +8 -0
  122. package/dist/lib/research/secure-write.d.ts +4 -0
  123. package/dist/lib/research/secure-write.js +31 -2
  124. package/dist/lib/research/storage.d.ts +5 -1
  125. package/dist/lib/research/storage.js +8 -3
  126. package/dist/lib/research/summarize.d.ts +15 -0
  127. package/dist/lib/research/summarize.js +11 -0
  128. package/dist/lib/research/url.d.ts +9 -6
  129. package/dist/lib/research/url.js +33 -21
  130. package/dist/lib/text.d.ts +7 -2
  131. package/dist/lib/text.js +13 -2
  132. package/dist/lib/value-flags.d.ts +14 -0
  133. package/dist/lib/value-flags.js +28 -0
  134. package/dist/sites/coveo.d.ts +10 -0
  135. package/dist/sites/coveo.js +10 -0
  136. package/dist/sites/index.d.ts +16 -0
  137. package/dist/sites/index.js +16 -4
  138. package/dist/sites/markdown-twin.d.ts +31 -0
  139. package/dist/sites/markdown-twin.js +56 -0
  140. package/dist/sites/salesforce/fetch-page.d.ts +8 -2
  141. package/dist/sites/salesforce/fetch-page.js +14 -3
  142. package/dist/sites/salesforce/help-tooling-markdown.d.ts +30 -0
  143. package/dist/sites/salesforce/help-tooling-markdown.js +74 -0
  144. package/dist/sites/salesforce/index.d.ts +1 -0
  145. package/dist/sites/salesforce/index.js +1 -0
  146. package/dist/sites/salesforce-developer/fetch-page.d.ts +6 -1
  147. package/dist/sites/salesforce-developer/fetch-page.js +6 -1
  148. package/dist/sites/salesforce-developer/index.d.ts +1 -0
  149. package/dist/sites/salesforce-developer/index.js +1 -0
  150. package/dist/sites/salesforce-developer/route-markdown.d.ts +18 -6
  151. package/dist/sites/salesforce-developer/route-markdown.js +29 -67
  152. package/dist/sites/salesforce-doc-fetch.d.ts +39 -5
  153. package/dist/sites/salesforce-doc-fetch.js +52 -7
  154. package/dist/sites/salesforce-dom-probe.d.ts +20 -2
  155. package/dist/sites/salesforce-dom-probe.js +20 -2
  156. package/dist/sites/types.d.ts +14 -8
  157. package/dist/sites/types.js +3 -0
  158. package/oclif.manifest.json +104 -98
  159. package/package.json +19 -7
  160. package/agents/agents/official-docs-researcher.agent.md +0 -87
  161. package/agents/instructions/web-research.instructions.md +0 -44
  162. package/agents/skills/web-research/SKILL.md +0 -118
@@ -0,0 +1,29 @@
1
+ import { writeUserConfig, writeProjectConfig, } from './config/index.js';
2
+ /**
3
+ * Persist a config patch to user or project scope. Pure of oclif — callers map failures to
4
+ * this.error(...). Shared by config set and unset so the write branch cannot drift.
5
+ */
6
+ export function persistConfigPatch(opts) {
7
+ if (opts.scope === 'project') {
8
+ writeProjectConfig(opts.cwd, opts.patch);
9
+ return { ok: true };
10
+ }
11
+ if (!opts.configDir) {
12
+ const verb = opts.action === 'set' ? 'write' : 'remove';
13
+ const example = opts.action === 'set'
14
+ ? `${opts.bin} config set ${opts.key} <value> --local`
15
+ : `${opts.bin} config unset ${opts.key} --local`;
16
+ return {
17
+ ok: false,
18
+ code: 'CONFIG_DIR_UNAVAILABLE',
19
+ message: `Could not determine user config directory. Use --local to ${verb} project config.`,
20
+ suggestions: [
21
+ opts.action === 'set'
22
+ ? `Write project config instead: ${example}`
23
+ : `Remove project config instead: ${example}`,
24
+ ],
25
+ };
26
+ }
27
+ writeUserConfig(opts.configDir, opts.patch);
28
+ return { ok: true };
29
+ }
@@ -7,8 +7,12 @@ export interface CliErrorShape {
7
7
  }
8
8
  /** Map oclif parse-time errors to stable Bonsai codes when the throw site did not set one. */
9
9
  export declare function stableErrorCodeFrom(err: unknown): string | undefined;
10
- /** Strip oclif's generic help suffix from JSON stderr — agents already have structured codes. */
10
+ /**
11
+ * Strip oclif's generic help suffix and the `Parsing --flag` wrapper so agents and humans see the
12
+ * actionable message we threw (e.g. `Limit must be between 1 and 100.`) instead of the wrapper.
13
+ */
11
14
  export declare function normalizeCliErrorMessage(message: string): string;
15
+ /** Machine-readable envelope fields for `--json` success and error paths. */
12
16
  export interface EnvelopeParts {
13
17
  command: string;
14
18
  ok: boolean;
@@ -25,12 +29,46 @@ export interface EnvelopeParts {
25
29
  * without ANSI or the leading "Error:" prefix (the message is already the primary line).
26
30
  */
27
31
  export declare function formatErrorForJson(err: CliErrorShape): string;
32
+ /** Stable CLIError fields for batch failure rows (status/inspect/fetch). */
33
+ export declare function cliErrorFields(err: {
34
+ code?: string;
35
+ message?: string;
36
+ suggestions?: string[];
37
+ ref?: string;
38
+ }, fallbackCode?: string): {
39
+ code: string;
40
+ message: string;
41
+ suggestions?: string[];
42
+ ref?: string;
43
+ };
44
+ /**
45
+ * CACHE_MISS overlay. Message wording is fixed so status/inspect cannot drift.
46
+ */
47
+ export declare function enrichCacheMissEnvelope(envelope: Record<string, unknown>, data: unknown, bin: string): Record<string, unknown>;
48
+ /**
49
+ * Per-row `.error` overlay for multi-URL batches (fetch, status, inspect).
50
+ * Keeps prior successes in `data` and surfaces the first row's code/message.
51
+ */
52
+ export declare function enrichRowErrorEnvelope(envelope: Record<string, unknown>, data: unknown): Record<string, unknown>;
53
+ /**
54
+ * Partial prune unlink failure: candidates remain in `data`, envelope reports the stable code.
55
+ */
56
+ export declare function enrichPrunePartialEnvelope(envelope: Record<string, unknown>, data: unknown): Record<string, unknown>;
28
57
  /**
29
58
  * Builds the standard Bonsai CLI JSON envelope structure.
30
59
  * This is the single source of truth for the output envelope schema.
31
60
  */
32
61
  export declare function buildEnvelope(parts: EnvelopeParts): Record<string, unknown>;
33
62
  /**
34
- * Formats a JSON envelope to a string with indentation.
63
+ * Usage/not-found style error envelope: message + Code (+ Try this) in `stderr`, stable `code`,
64
+ * and top-level `suggestions` when present. Shared by argv preflight, help-preflight, and the
65
+ * command_not_found hook so those paths cannot drift.
35
66
  */
36
- export declare function formatEnvelope(parts: EnvelopeParts): string;
67
+ export declare function buildCliErrorEnvelope(opts: {
68
+ command: string;
69
+ message: string;
70
+ code: string;
71
+ suggestions?: string[];
72
+ exitCode?: number;
73
+ ref?: string;
74
+ }): Record<string, unknown>;
@@ -1,3 +1,4 @@
1
+ import { EXIT_RUNTIME_FAILURE } from './cli-error-policy.js';
1
2
  /** Map oclif parse-time errors to stable Bonsai codes when the throw site did not set one. */
2
3
  export function stableErrorCodeFrom(err) {
3
4
  const e = err;
@@ -21,10 +22,23 @@ export function stableErrorCodeFrom(err) {
21
22
  /^Flag --\S+ expects (a value$|one of these values:)/.test(e.message)) {
22
23
  return 'MISSING_FLAG_VALUE';
23
24
  }
25
+ // Repeating a single-value flag (e.g. `--topic a --topic b`) is also a plain CLIError with no
26
+ // dedicated class — oclif's parser rejects it before any flag-specific validation runs.
27
+ if (typeof e?.message === 'string' && /^Flag --\S+ can only be specified once$/.test(e.message)) {
28
+ return 'DUPLICATE_FLAG';
29
+ }
24
30
  return undefined;
25
31
  }
26
- /** Strip oclif's generic help suffix from JSON stderr — agents already have structured codes. */
32
+ /** oclif wraps custom flag parse failures as `Parsing --name \n\t…\nSee more help with --help`. */
33
+ const PARSING_WRAPPER = /^Parsing --\S+ \n\t([\s\S]*?)(?:\nSee more help with --help)?$/;
34
+ /**
35
+ * Strip oclif's generic help suffix and the `Parsing --flag` wrapper so agents and humans see the
36
+ * actionable message we threw (e.g. `Limit must be between 1 and 100.`) instead of the wrapper.
37
+ */
27
38
  export function normalizeCliErrorMessage(message) {
39
+ const wrapped = message.match(PARSING_WRAPPER);
40
+ if (wrapped?.[1])
41
+ return wrapped[1];
28
42
  const suffix = '\nSee more help with --help';
29
43
  return message.endsWith(suffix) ? message.slice(0, -suffix.length) : message;
30
44
  }
@@ -52,6 +66,119 @@ export function formatErrorForJson(err) {
52
66
  lines.push(`Reference: ${err.ref}`);
53
67
  return lines.join('\n');
54
68
  }
69
+ /** Stable CLIError fields for batch failure rows (status/inspect/fetch). */
70
+ export function cliErrorFields(err, fallbackCode = 'INVALID_URL') {
71
+ return {
72
+ code: typeof err.code === 'string' && err.code ? err.code : fallbackCode,
73
+ message: typeof err.message === 'string' ? err.message : String(err),
74
+ suggestions: err.suggestions,
75
+ ref: err.ref,
76
+ };
77
+ }
78
+ /**
79
+ * Overlay a batch failure onto a success envelope while keeping per-row `data`.
80
+ * Shared by multi-URL status/inspect (CACHE_MISS) and fetch/row errors.
81
+ */
82
+ function enrichBatchFailureEnvelope(envelope, data, opts) {
83
+ const list = Array.isArray(data) ? data : [data];
84
+ const failures = list.map(opts.pick).filter(Boolean);
85
+ if (failures.length === 0)
86
+ return envelope;
87
+ const first = failures[0];
88
+ const code = typeof opts.code === 'function' ? opts.code(first, failures) : opts.code;
89
+ const suggestions = opts.suggestions?.(failures);
90
+ const ref = opts.ref?.(first);
91
+ const stderr = formatErrorForJson({
92
+ message: opts.message(first, failures),
93
+ code,
94
+ suggestions,
95
+ ref,
96
+ });
97
+ return {
98
+ ...envelope,
99
+ ok: false,
100
+ exitCode: opts.exitCode ?? 1,
101
+ stderr,
102
+ code,
103
+ suggestions,
104
+ ref,
105
+ };
106
+ }
107
+ /**
108
+ * CACHE_MISS overlay. Message wording is fixed so status/inspect cannot drift.
109
+ */
110
+ export function enrichCacheMissEnvelope(envelope, data, bin) {
111
+ return enrichBatchFailureEnvelope(envelope, data, {
112
+ pick: (row) => {
113
+ const r = row;
114
+ return r?.status === 'miss' ? r : null;
115
+ },
116
+ code: 'CACHE_MISS',
117
+ exitCode: EXIT_RUNTIME_FAILURE,
118
+ message: (first, failures) => {
119
+ const url = first.normalizedUrl ?? '';
120
+ return failures.length > 1
121
+ ? `Cache miss for ${url} and ${failures.length - 1} other URLs`
122
+ : `Cache miss for ${url}`;
123
+ },
124
+ // A URL that is already a secondary source of a different cached artifact must not be pointed
125
+ // at a plain fetch — that would create an unrelated duplicate entry instead of finding the
126
+ // note that already covers it.
127
+ suggestions: (failures) => failures.map((m) => m.partOfExistingNote
128
+ ? `Find it with: ${bin} list --url "${m.normalizedUrl}"`
129
+ : `Fetch and cache it first: ${bin} ${m.normalizedUrl}`),
130
+ });
131
+ }
132
+ /**
133
+ * Per-row `.error` overlay for multi-URL batches (fetch, status, inspect).
134
+ * Keeps prior successes in `data` and surfaces the first row's code/message.
135
+ */
136
+ export function enrichRowErrorEnvelope(envelope, data) {
137
+ return enrichBatchFailureEnvelope(envelope, data, {
138
+ pick: (row) => {
139
+ if (!row || typeof row !== 'object')
140
+ return null;
141
+ return row.error ?? null;
142
+ },
143
+ code: (first) => first.code ?? 'FETCH_FAILED',
144
+ message: (first, failures) => (first.message ?? '') +
145
+ (failures.length > 1 ? `\n…and ${failures.length - 1} other URL failure(s)` : ''),
146
+ suggestions: (failures) => failures[0]?.suggestions,
147
+ ref: (first) => first.ref,
148
+ exitCode: EXIT_RUNTIME_FAILURE,
149
+ });
150
+ }
151
+ /**
152
+ * Partial prune unlink failure: candidates remain in `data`, envelope reports the stable code.
153
+ */
154
+ export function enrichPrunePartialEnvelope(envelope, data) {
155
+ if (!isPruneCounts(data))
156
+ return envelope;
157
+ // A dry run never deletes anything, so prunedCount is always 0 by design — that is a preview,
158
+ // not a partial failure, and must never be reported as one.
159
+ if (data.dryRun)
160
+ return envelope;
161
+ if (data.candidateCount <= 0 || data.prunedCount >= data.candidateCount)
162
+ return envelope;
163
+ const failed = data.candidateCount - data.prunedCount;
164
+ const code = 'PRUNE_PARTIAL_FAILURE';
165
+ const message = `Failed to delete ${failed} of ${data.candidateCount} cache ${failed === 1 ? 'entry' : 'entries'}.`;
166
+ return {
167
+ ...envelope,
168
+ ok: false,
169
+ exitCode: EXIT_RUNTIME_FAILURE,
170
+ code,
171
+ stderr: formatErrorForJson({ message, code }),
172
+ };
173
+ }
174
+ function isPruneCounts(data) {
175
+ if (!data || typeof data !== 'object')
176
+ return false;
177
+ const d = data;
178
+ return (typeof d.prunedCount === 'number' &&
179
+ typeof d.candidateCount === 'number' &&
180
+ typeof d.dryRun === 'boolean');
181
+ }
55
182
  /**
56
183
  * Builds the standard Bonsai CLI JSON envelope structure.
57
184
  * This is the single source of truth for the output envelope schema.
@@ -75,8 +202,24 @@ export function buildEnvelope(parts) {
75
202
  return envelope;
76
203
  }
77
204
  /**
78
- * Formats a JSON envelope to a string with indentation.
205
+ * Usage/not-found style error envelope: message + Code (+ Try this) in `stderr`, stable `code`,
206
+ * and top-level `suggestions` when present. Shared by argv preflight, help-preflight, and the
207
+ * command_not_found hook so those paths cannot drift.
79
208
  */
80
- export function formatEnvelope(parts) {
81
- return JSON.stringify(buildEnvelope(parts), null, 2);
209
+ export function buildCliErrorEnvelope(opts) {
210
+ return buildEnvelope({
211
+ command: opts.command,
212
+ ok: false,
213
+ exitCode: opts.exitCode ?? 2,
214
+ stderr: formatErrorForJson({
215
+ message: opts.message,
216
+ code: opts.code,
217
+ suggestions: opts.suggestions,
218
+ ref: opts.ref,
219
+ }),
220
+ data: null,
221
+ code: opts.code,
222
+ suggestions: opts.suggestions?.length ? opts.suggestions : undefined,
223
+ ref: opts.ref,
224
+ });
82
225
  }
@@ -1,15 +1,12 @@
1
- type UnknownHelpResult = {
2
- exitCode: 2;
3
- kind: 'human';
4
- message: string;
5
- } | {
1
+ import { EXIT_USAGE } from './cli-error-policy.js';
2
+ /** Always an envelope — human and JSON share one formatted stderr (Code + Try this). */
3
+ export type UnknownHelpResult = {
4
+ exitCode: typeof EXIT_USAGE;
6
5
  envelope: Record<string, unknown>;
7
- exitCode: 2;
8
- kind: 'json';
6
+ json: boolean;
9
7
  };
10
8
  /**
11
9
  * oclif resolves help before its command_not_found hook, so `bonsai config gett --help` otherwise
12
10
  * falls back to a terse framework error. Catch that narrow case and reuse the normal typo guidance.
13
11
  */
14
12
  export declare function tryUnknownHelpOutput(argv: readonly string[], root: string): Promise<UnknownHelpResult | null>;
15
- export {};
@@ -1,22 +1,12 @@
1
1
  import { Config } from '@oclif/core';
2
- import { buildEnvelope } from './envelope.js';
2
+ import { positionalArgvTokens } from './argv.js';
3
+ import { EXIT_USAGE } from './cli-error-policy.js';
4
+ import { VALUE_TAKING_FLAG_TOKENS } from './cli-flag-manifest.js';
5
+ import { buildCliErrorEnvelope } from './envelope.js';
3
6
  import { buildCommandNotFoundDetails } from '../hooks/command-not-found/suggest.js';
4
7
  function commandId(parts) {
5
8
  return parts.join(':');
6
9
  }
7
- function positionalTokens(argv) {
8
- const tokens = [];
9
- for (const arg of argv) {
10
- if (arg === '--')
11
- break;
12
- if (arg === '--help' || arg === '--json')
13
- continue;
14
- if (arg.startsWith('-'))
15
- continue;
16
- tokens.push(arg);
17
- }
18
- return tokens;
19
- }
20
10
  function commandPrefixLength(tokens, config) {
21
11
  let longest = 0;
22
12
  for (let n = 1; n <= tokens.length; n++) {
@@ -51,7 +41,7 @@ function hasValidHelpTarget(tokens, config) {
51
41
  export async function tryUnknownHelpOutput(argv, root) {
52
42
  if (!argv.includes('--help'))
53
43
  return null;
54
- const tokens = positionalTokens(argv);
44
+ const tokens = positionalArgvTokens(argv, VALUE_TAKING_FLAG_TOKENS);
55
45
  if (tokens.length === 0)
56
46
  return null;
57
47
  const config = await Config.load({ root });
@@ -59,23 +49,14 @@ export async function tryUnknownHelpOutput(argv, root) {
59
49
  return null;
60
50
  const attemptedId = commandId(tokens);
61
51
  const details = buildCommandNotFoundDetails(attemptedId, [...argv], config);
62
- if (argv.includes('--json')) {
63
- return {
64
- kind: 'json',
65
- exitCode: 2,
66
- envelope: buildEnvelope({
67
- command: details.command,
68
- ok: false,
69
- exitCode: 2,
70
- stderr: `${details.message}\nCode: ${details.code}`,
71
- data: null,
72
- code: details.code,
73
- }),
74
- };
75
- }
76
52
  return {
77
- kind: 'human',
78
- exitCode: 2,
79
- message: `${details.message}\nCode: ${details.code}`,
53
+ exitCode: EXIT_USAGE,
54
+ json: argv.includes('--json'),
55
+ envelope: buildCliErrorEnvelope({
56
+ command: details.command,
57
+ message: details.message,
58
+ code: details.code,
59
+ suggestions: details.suggestions,
60
+ }),
80
61
  };
81
62
  }
@@ -1,5 +1,3 @@
1
- /** True when argv asks for help or version together with --json. */
2
- export declare function isJsonMetaRequest(argv: readonly string[]): boolean;
3
1
  /**
4
2
  * When `--json` is combined with `--help` or `--version`, oclif would print human text to stdout.
5
3
  * Render the same content inside the standard Bonsai JSON envelope instead.
@@ -1,12 +1,14 @@
1
1
  import { Config, Help, toConfiguredId } from '@oclif/core';
2
+ import { positionalArgvTokens } from './argv.js';
3
+ import { EXIT_OK, EXIT_RUNTIME_FAILURE } from './cli-error-policy.js';
4
+ import { VALUE_TAKING_FLAG_TOKENS } from './cli-flag-manifest.js';
2
5
  import { buildEnvelope } from './envelope.js';
3
6
  /** Strip ANSI color codes so JSON help text stays machine-stable. */
4
7
  function stripAnsi(text) {
5
- // eslint-disable-next-line no-control-regex
6
8
  return text.replace(/\x1b\[[0-9;]*m/g, '');
7
9
  }
8
10
  /** True when argv asks for help or version together with --json. */
9
- export function isJsonMetaRequest(argv) {
11
+ function isJsonMetaRequest(argv) {
10
12
  if (!argv.includes('--json'))
11
13
  return false;
12
14
  return argv.includes('--help') || argv.includes('--version');
@@ -21,27 +23,21 @@ class CaptureHelp extends Help {
21
23
  }
22
24
  }
23
25
  }
24
- function helpSubject(argv) {
25
- for (const arg of argv) {
26
- if (arg === '--' || arg === '--help' || arg === 'help')
27
- continue;
28
- if (arg.startsWith('-'))
29
- return;
30
- return arg;
31
- }
32
- return undefined;
33
- }
34
26
  function envelopeCommandId(config, argv) {
35
- const subject = helpSubject(argv);
36
- if (!subject)
27
+ const tokens = positionalArgvTokens(argv, VALUE_TAKING_FLAG_TOKENS);
28
+ if (tokens.length === 0)
37
29
  return config.bin;
38
- const command = config.findCommand(subject);
39
- if (command?.id)
40
- return toConfiguredId(command.id, config);
41
- const topic = config.findTopic(subject);
30
+ // Longest matching command id wins (`config:get` over topic `config`).
31
+ for (let n = tokens.length; n >= 1; n--) {
32
+ const id = tokens.slice(0, n).join(':');
33
+ const command = config.findCommand(id);
34
+ if (command?.id)
35
+ return toConfiguredId(command.id, config);
36
+ }
37
+ const topic = config.findTopic(tokens[0]);
42
38
  if (topic)
43
39
  return topic.name;
44
- return subject;
40
+ return tokens[0];
45
41
  }
46
42
  /**
47
43
  * When `--json` is combined with `--help` or `--version`, oclif would print human text to stdout.
@@ -54,11 +50,11 @@ export async function tryJsonMetaOutput(argv, root) {
54
50
  const config = await Config.load({ root });
55
51
  if (argv.includes('--version')) {
56
52
  return {
57
- exitCode: 0,
53
+ exitCode: EXIT_OK,
58
54
  envelope: buildEnvelope({
59
55
  command: config.bin,
60
56
  ok: true,
61
- exitCode: 0,
57
+ exitCode: EXIT_OK,
62
58
  stderr: '',
63
59
  data: {
64
60
  version: config.version,
@@ -71,11 +67,11 @@ export async function tryJsonMetaOutput(argv, root) {
71
67
  await help.showHelp([...argv]);
72
68
  const helpText = stripAnsi(help.lines.join('\n').trimEnd());
73
69
  return {
74
- exitCode: 0,
70
+ exitCode: EXIT_OK,
75
71
  envelope: buildEnvelope({
76
72
  command: envelopeCommandId(config, argv),
77
73
  ok: true,
78
- exitCode: 0,
74
+ exitCode: EXIT_OK,
79
75
  stderr: '',
80
76
  data: { help: helpText },
81
77
  }),
@@ -84,11 +80,11 @@ export async function tryJsonMetaOutput(argv, root) {
84
80
  catch (err) {
85
81
  const message = err instanceof Error ? err.message : String(err);
86
82
  return {
87
- exitCode: 1,
83
+ exitCode: EXIT_RUNTIME_FAILURE,
88
84
  envelope: buildEnvelope({
89
85
  command: 'bonsai',
90
86
  ok: false,
91
- exitCode: 1,
87
+ exitCode: EXIT_RUNTIME_FAILURE,
92
88
  stderr: message,
93
89
  data: null,
94
90
  code: 'META_RENDER_FAILED',
@@ -1 +1,11 @@
1
- export declare function limitFlag(max: number, defaultValue: number, description: string): import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
1
+ /**
2
+ * Build an oclif integer `--limit` flag capped at `maxLimit`.
3
+ *
4
+ * @param maxLimit - Inclusive upper bound for accepted values.
5
+ * @param defaultValue - Default when the flag is omitted.
6
+ * @param description - Help text for the flag.
7
+ * @returns An oclif custom flag definition.
8
+ * @throws {Errors.CLIError} With code `INVALID_LIMIT` and exit {@link EXIT_USAGE} on bad input
9
+ * (thrown by oclif during parse, not by this factory itself).
10
+ */
11
+ export declare function limitFlag(maxLimit: number, defaultValue: number, description: string): import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,18 +1,31 @@
1
1
  import { Errors, Flags } from '@oclif/core';
2
- export function limitFlag(max, defaultValue, description) {
2
+ import { EXIT_USAGE } from './cli-error-policy.js';
3
+ /**
4
+ * Build an oclif integer `--limit` flag capped at `maxLimit`.
5
+ *
6
+ * @param maxLimit - Inclusive upper bound for accepted values.
7
+ * @param defaultValue - Default when the flag is omitted.
8
+ * @param description - Help text for the flag.
9
+ * @returns An oclif custom flag definition.
10
+ * @throws {Errors.CLIError} With code `INVALID_LIMIT` and exit {@link EXIT_USAGE} on bad input
11
+ * (thrown by oclif during parse, not by this factory itself).
12
+ */
13
+ export function limitFlag(maxLimit, defaultValue, description) {
3
14
  return Flags.custom({
4
15
  parse: async (input) => {
5
16
  if (!/^\d+$/.test(input)) {
6
- throw new Errors.CLIError(`Limit must be an integer between 1 and ${max}.`, {
7
- exit: 2,
17
+ throw new Errors.CLIError(`Limit must be an integer between 1 and ${maxLimit}.`, {
18
+ exit: EXIT_USAGE,
8
19
  code: 'INVALID_LIMIT',
20
+ suggestions: [`Use an integer from 1 to ${maxLimit}.`],
9
21
  });
10
22
  }
11
23
  const value = Number(input);
12
- if (value < 1 || value > max) {
13
- throw new Errors.CLIError(`Limit must be between 1 and ${max}.`, {
14
- exit: 2,
24
+ if (value < 1 || value > maxLimit) {
25
+ throw new Errors.CLIError(`Limit must be between 1 and ${maxLimit}.`, {
26
+ exit: EXIT_USAGE,
15
27
  code: 'INVALID_LIMIT',
28
+ suggestions: [`Use a value from 1 to ${maxLimit}.`],
16
29
  });
17
30
  }
18
31
  return value;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Improve oclif parse-time errors in place: unwrap wrapper text, then suggest flag/option typos.
3
+ * Accepts `unknown` because `catch` can receive non-Error throws (bare strings, etc.).
4
+ */
5
+ export declare function enrichParseError(err: unknown): void;
@@ -0,0 +1,73 @@
1
+ import { normalizeCliErrorMessage } from './envelope.js';
2
+ import { closestMatch, closestOptionValues, maxFuzzyDistance } from './text.js';
3
+ /** FlagInvalidOptionError body after {@link normalizeCliErrorMessage}. */
4
+ const EXPECTED_OPTION = /^Expected --([^=]+)=(\S+) to be one of: (.+)$/;
5
+ function asEnrichable(err) {
6
+ if (!err || typeof err !== 'object')
7
+ return null;
8
+ if (typeof err.message !== 'string')
9
+ return null;
10
+ return err;
11
+ }
12
+ function suggestableFlagNames(err) {
13
+ const defined = err.parse?.input?.flags;
14
+ if (!defined)
15
+ return [];
16
+ const names = [];
17
+ for (const [name, flag] of Object.entries(defined)) {
18
+ names.push(`--${name}`);
19
+ for (const alias of flag.aliases ?? [])
20
+ names.push(`--${alias}`);
21
+ if (flag.char)
22
+ names.push(`-${flag.char}`);
23
+ }
24
+ // enableJsonFlag is framework-level — it may be absent from parse.input.flags.
25
+ if (!names.includes('--json'))
26
+ names.push('--json');
27
+ return names;
28
+ }
29
+ function collectTips(err) {
30
+ const lines = [];
31
+ const suggestions = [];
32
+ if (err.flags?.length) {
33
+ const candidates = suggestableFlagNames(err);
34
+ for (const bad of err.flags) {
35
+ const match = closestMatch(bad, candidates, maxFuzzyDistance(bad.replace(/^-*/, '')));
36
+ if (!match || match === bad)
37
+ continue;
38
+ lines.push(`Did you mean ${match}?`);
39
+ suggestions.push(match);
40
+ }
41
+ }
42
+ const option = err.message.match(EXPECTED_OPTION);
43
+ if (option) {
44
+ const [, flagName, input, optionsCsv] = option;
45
+ if (flagName && input && optionsCsv) {
46
+ const options = optionsCsv
47
+ .split(', ')
48
+ .map((part) => part.trim())
49
+ .filter(Boolean);
50
+ const values = closestOptionValues(input, options);
51
+ if (values.length > 0) {
52
+ lines.push(`Did you mean ${values.join(' or ')}?`);
53
+ suggestions.push(...values.map((value) => `--${flagName} ${value}`));
54
+ }
55
+ }
56
+ }
57
+ return { lines, suggestions };
58
+ }
59
+ /**
60
+ * Improve oclif parse-time errors in place: unwrap wrapper text, then suggest flag/option typos.
61
+ * Accepts `unknown` because `catch` can receive non-Error throws (bare strings, etc.).
62
+ */
63
+ export function enrichParseError(err) {
64
+ const enrichable = asEnrichable(err);
65
+ if (!enrichable)
66
+ return;
67
+ enrichable.message = normalizeCliErrorMessage(enrichable.message);
68
+ const { lines, suggestions } = collectTips(enrichable);
69
+ if (lines.length === 0)
70
+ return;
71
+ enrichable.message = [enrichable.message, ...lines].join('\n');
72
+ enrichable.suggestions = [...(enrichable.suggestions ?? []), ...suggestions];
73
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Pure prune-flag policy. Returns a usage/safety error descriptor, or null when flags are valid.
3
+ * Kept outside the command class so the branching stays testable without oclif and so
4
+ * `validatePruneFlags` stays a thin apply-error loop.
5
+ */
6
+ export interface PruneFlagError {
7
+ message: string;
8
+ code: 'INVALID_FLAG_VALUE' | 'MISSING_FILTER' | 'READ_ONLY_MODE' | 'CONFLICTING_FLAGS' | 'SAFETY_CHECK_REQUIRED' | 'INVALID_DURATION';
9
+ suggestions?: string[];
10
+ }
11
+ /** Flag bag passed into {@link pruneFlagError}. */
12
+ export interface PruneFlagInput {
13
+ olderThan?: string;
14
+ inactive?: string;
15
+ artifactType?: string;
16
+ url?: string;
17
+ dryRun: boolean;
18
+ yes: boolean;
19
+ readOnly: boolean;
20
+ bin: string;
21
+ }
22
+ /**
23
+ * Return the first prune usage/safety error, or `null` when flags are valid.
24
+ *
25
+ * @param input - Parsed prune flags and binary name.
26
+ * @returns Error descriptor for `this.error`, or `null`.
27
+ */
28
+ export declare function pruneFlagError(input: PruneFlagInput): PruneFlagError | null;