@kora-platform/cli 0.7.0-rc1 → 0.8.0-rc2

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 (38) hide show
  1. package/README.md +21 -0
  2. package/dist/api-client.d.ts +250 -93
  3. package/dist/api-client.js +187 -163
  4. package/dist/api-types.d.ts +280 -162
  5. package/dist/artifact-api-client.d.ts +28 -1
  6. package/dist/artifact-api-client.js +33 -0
  7. package/dist/artifact-commands.d.ts +5 -0
  8. package/dist/artifact-commands.js +172 -1
  9. package/dist/audit-commands.d.ts +12 -0
  10. package/dist/audit-commands.js +74 -0
  11. package/dist/auth-commands.d.ts +1 -0
  12. package/dist/auth-commands.js +116 -29
  13. package/dist/command-builders.d.ts +1 -0
  14. package/dist/command-builders.js +1 -0
  15. package/dist/command-groups.js +10 -12
  16. package/dist/command-registry.js +548 -243
  17. package/dist/commands.js +652 -602
  18. package/dist/environment-context.d.ts +9 -0
  19. package/dist/environment-context.js +32 -0
  20. package/dist/{integration-commands.d.ts → extension-commands.d.ts} +3 -2
  21. package/dist/extension-commands.js +446 -0
  22. package/dist/files.d.ts +33 -0
  23. package/dist/files.js +247 -12
  24. package/dist/format.d.ts +5 -0
  25. package/dist/format.js +78 -1
  26. package/dist/runner.js +14 -5
  27. package/dist/schema-registry-data.d.ts +272 -569
  28. package/dist/schema-registry-data.js +307 -700
  29. package/dist/session.d.ts +1 -0
  30. package/dist/transport.d.ts +10 -0
  31. package/dist/transport.js +22 -0
  32. package/dist/types.d.ts +2 -1
  33. package/dist/workspace-source.d.ts +1 -0
  34. package/dist/workspace-source.js +13 -0
  35. package/package.json +2 -1
  36. package/dist/integration-api-client.d.ts +0 -29
  37. package/dist/integration-api-client.js +0 -50
  38. package/dist/integration-commands.js +0 -208
package/dist/files.js CHANGED
@@ -1,8 +1,13 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { readdir, readFile, stat } from "node:fs/promises";
3
- import { basename, join, relative, resolve } from "node:path";
2
+ import { lstat, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { usageProblem } from "./cli-errors.js";
5
5
  import { parseDotEnv } from "./dotenv.js";
6
+ import { shouldIgnoreWorkspacePath } from "./workspace-source.js";
7
+ const MAX_IMPORT_FILE_COUNT = 500;
8
+ const MAX_IMPORT_FILE_BYTES = 1_000_000;
9
+ const MAX_PACKAGE_FILE_BYTES = 2_000_000;
10
+ const MAX_IMPORT_TOTAL_BYTES = 10_000_000;
6
11
  export async function readJsonInputSpecifier(specifier, stdin, instance) {
7
12
  if (specifier === "-") {
8
13
  return readJsonObject(await readStream(stdin), instance);
@@ -22,16 +27,146 @@ export async function readTextInputSpecifier(specifier, stdin, instance) {
22
27
  return readFile(specifier.slice(1), "utf8");
23
28
  }
24
29
  export async function readImportEntries(pathValue) {
30
+ return await readUtf8Entries(pathValue, {
31
+ instance: "org.import",
32
+ maxFileBytes: MAX_IMPORT_FILE_BYTES
33
+ });
34
+ }
35
+ export function isZipArchivePath(pathValue) {
36
+ return extname(pathValue).toLowerCase() === ".zip";
37
+ }
38
+ export async function readArchiveBytes(pathValue, instance) {
39
+ const absolutePath = resolve(pathValue);
40
+ const pathStat = await lstat(absolutePath);
41
+ if (pathStat.isSymbolicLink() || !pathStat.isFile()) {
42
+ throw usageProblem("Archive path must be a regular .zip file, not a directory or symbolic link.", instance);
43
+ }
44
+ return await readFile(absolutePath);
45
+ }
46
+ export async function readWorkspaceTestEntries(pathValue) {
47
+ return await readUtf8Entries(pathValue, {
48
+ instance: "test node",
49
+ maxFileBytes: MAX_PACKAGE_FILE_BYTES
50
+ });
51
+ }
52
+ async function readUtf8Entries(pathValue, options) {
25
53
  const absolutePath = resolve(pathValue);
26
- const pathStat = await stat(absolutePath);
54
+ const pathStat = await lstat(absolutePath);
55
+ if (pathStat.isSymbolicLink()) {
56
+ throw usageProblem("Import path must be a regular file or directory, not a symbolic link.", options.instance);
57
+ }
27
58
  if (pathStat.isFile()) {
59
+ const content = await readLimitedUtf8File(absolutePath, basename(absolutePath), {
60
+ fileCount: 0,
61
+ totalBytes: 0
62
+ }, options);
28
63
  return [{
29
- content: await readFile(absolutePath, "utf8"),
64
+ content: content.content,
30
65
  path: basename(absolutePath)
31
66
  }];
32
67
  }
33
68
  const entries = [];
34
- await walkImportDirectory(absolutePath, absolutePath, entries);
69
+ await walkImportDirectory(absolutePath, absolutePath, entries, {
70
+ fileCount: 0,
71
+ totalBytes: 0
72
+ }, options);
73
+ entries.sort((left, right) => left.path.localeCompare(right.path));
74
+ return entries;
75
+ }
76
+ export async function readWorkspaceExportMetadata(pathValue) {
77
+ const absolutePath = resolve(pathValue);
78
+ try {
79
+ const metadata = await readFile(join(absolutePath, ".kora", "export.json"), "utf8");
80
+ const parsed = JSON.parse(metadata);
81
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
82
+ ? parsed
83
+ : null;
84
+ }
85
+ catch (error) {
86
+ if (isNodeErrorWithCode(error, "ENOENT") || isNodeErrorWithCode(error, "ENOTDIR")) {
87
+ return null;
88
+ }
89
+ throw error;
90
+ }
91
+ }
92
+ export async function writeWorkspaceExport(outPath, envelope) {
93
+ const absolutePath = resolve(outPath);
94
+ await assertExportOutputIsEmpty(absolutePath, "org.export");
95
+ await mkdir(absolutePath, { recursive: true });
96
+ for (const file of envelope.files) {
97
+ const targetPath = resolveExportTargetPath(absolutePath, file.path, "org.export");
98
+ await mkdir(dirname(targetPath), { recursive: true });
99
+ await writeFile(targetPath, file.content, "utf8");
100
+ }
101
+ const metadataPath = join(absolutePath, ".kora", "export.json");
102
+ await mkdir(dirname(metadataPath), { recursive: true });
103
+ await writeFile(metadataPath, `${JSON.stringify(envelope.metadata, null, 2)}\n`, "utf8");
104
+ }
105
+ export async function writeReleaseSourceFiles(outPath, envelope) {
106
+ const absolutePath = resolve(outPath);
107
+ await assertExportOutputIsEmpty(absolutePath, "release source", "Release source");
108
+ await mkdir(absolutePath, { recursive: true });
109
+ const seenPaths = new Set();
110
+ for (const file of envelope.files) {
111
+ if (seenPaths.has(file.path)) {
112
+ throw usageProblem(`Release source contains duplicate file path '${file.path}'.`, "release source");
113
+ }
114
+ seenPaths.add(file.path);
115
+ const targetPath = resolveExportTargetPath(absolutePath, file.path, "release source");
116
+ await mkdir(dirname(targetPath), { recursive: true });
117
+ await writeFile(targetPath, file.content, "utf8");
118
+ }
119
+ const metadataPath = join(absolutePath, ".kora", "release-source.json");
120
+ await mkdir(dirname(metadataPath), { recursive: true });
121
+ await writeFile(metadataPath, `${JSON.stringify(envelope.metadata, null, 2)}\n`, "utf8");
122
+ }
123
+ export async function writeArchiveExport(outPath, archive, instance) {
124
+ const absolutePath = resolve(outPath);
125
+ await mkdir(dirname(absolutePath), { recursive: true });
126
+ try {
127
+ await writeFile(absolutePath, archive, { flag: "wx" });
128
+ }
129
+ catch (error) {
130
+ if (isNodeErrorWithCode(error, "EEXIST")) {
131
+ throw usageProblem("Export output file already exists.", instance);
132
+ }
133
+ throw error;
134
+ }
135
+ }
136
+ export async function writePackageExport(outPath, envelope) {
137
+ const absolutePath = resolve(outPath);
138
+ await assertExportOutputIsEmpty(absolutePath, "extensions.export");
139
+ await mkdir(absolutePath, { recursive: true });
140
+ for (const file of envelope.files) {
141
+ const targetPath = resolveExportTargetPath(absolutePath, file.path, "extensions.export");
142
+ await mkdir(dirname(targetPath), { recursive: true });
143
+ await writeFile(targetPath, Buffer.from(file.contentBase64, "base64"));
144
+ }
145
+ const metadataPath = join(absolutePath, ".kora", "export.json");
146
+ await mkdir(dirname(metadataPath), { recursive: true });
147
+ await writeFile(metadataPath, `${JSON.stringify(envelope.metadata, null, 2)}\n`, "utf8");
148
+ }
149
+ export async function readPackageFileEntries(pathValue, instance) {
150
+ const absolutePath = resolve(pathValue);
151
+ const pathStat = await lstat(absolutePath);
152
+ if (pathStat.isSymbolicLink()) {
153
+ throw usageProblem("Package path must be a regular file or directory, not a symbolic link.", instance);
154
+ }
155
+ if (pathStat.isFile()) {
156
+ assertPackageFileCanBeRead(pathStat.size, basename(absolutePath), {
157
+ fileCount: 0,
158
+ totalBytes: 0
159
+ }, instance);
160
+ return [{
161
+ contentBase64: (await readFile(absolutePath)).toString("base64"),
162
+ path: basename(absolutePath)
163
+ }];
164
+ }
165
+ const entries = [];
166
+ await walkPackageDirectory(absolutePath, absolutePath, entries, {
167
+ fileCount: 0,
168
+ totalBytes: 0
169
+ }, instance);
35
170
  entries.sort((left, right) => left.path.localeCompare(right.path));
36
171
  return entries;
37
172
  }
@@ -62,24 +197,124 @@ async function readStream(stream) {
62
197
  }
63
198
  return Buffer.concat(chunks).toString("utf8");
64
199
  }
65
- async function walkImportDirectory(root, current, entries) {
200
+ async function walkImportDirectory(root, current, entries, counters, options) {
66
201
  const children = await readdir(current, { withFileTypes: true });
67
202
  children.sort((left, right) => left.name.localeCompare(right.name));
68
203
  for (const child of children) {
69
- if (shouldIgnoreImportSegment(child.name)) {
204
+ const childPath = join(current, child.name);
205
+ const relativePath = relative(root, childPath).replaceAll("\\", "/");
206
+ if (shouldIgnoreImportPath(relativePath)) {
70
207
  continue;
71
208
  }
209
+ if (child.isDirectory()) {
210
+ await walkImportDirectory(root, childPath, entries, counters, options);
211
+ continue;
212
+ }
213
+ if (!child.isFile()) {
214
+ continue;
215
+ }
216
+ const content = await readLimitedUtf8File(childPath, relativePath, counters, options);
217
+ entries.push({
218
+ content: content.content,
219
+ path: relativePath
220
+ });
221
+ }
222
+ }
223
+ async function walkPackageDirectory(root, current, entries, counters, instance) {
224
+ const children = await readdir(current, { withFileTypes: true });
225
+ children.sort((left, right) => left.name.localeCompare(right.name));
226
+ for (const child of children) {
72
227
  const childPath = join(current, child.name);
228
+ const relativePath = relative(root, childPath).replaceAll("\\", "/");
229
+ if (shouldIgnoreImportPath(relativePath)) {
230
+ continue;
231
+ }
73
232
  if (child.isDirectory()) {
74
- await walkImportDirectory(root, childPath, entries);
233
+ await walkPackageDirectory(root, childPath, entries, counters, instance);
75
234
  continue;
76
235
  }
236
+ if (!child.isFile()) {
237
+ continue;
238
+ }
239
+ const childStat = await lstat(childPath);
240
+ assertPackageFileCanBeRead(childStat.size, relativePath, counters, instance);
77
241
  entries.push({
78
- content: await readFile(childPath, "utf8"),
79
- path: relative(root, childPath).replaceAll("\\", "/")
242
+ contentBase64: (await readFile(childPath)).toString("base64"),
243
+ path: relativePath
80
244
  });
81
245
  }
82
246
  }
83
- function shouldIgnoreImportSegment(segment) {
84
- return new Set([".git", ".kora", "coverage", "dist", "node_modules"]).has(segment);
247
+ function shouldIgnoreImportPath(pathValue) {
248
+ return shouldIgnoreWorkspacePath(pathValue);
249
+ }
250
+ async function assertExportOutputIsEmpty(absolutePath, instance, outputLabel = "Export") {
251
+ try {
252
+ const pathStat = await lstat(absolutePath);
253
+ if (pathStat.isSymbolicLink()) {
254
+ throw usageProblem(`${outputLabel} output path must not be a symbolic link.`, instance);
255
+ }
256
+ if (!pathStat.isDirectory()) {
257
+ throw usageProblem(`${outputLabel} output path must be a directory.`, instance);
258
+ }
259
+ const children = await readdir(absolutePath);
260
+ if (children.length > 0) {
261
+ throw usageProblem(`${outputLabel} output directory must be empty or not exist.`, instance);
262
+ }
263
+ }
264
+ catch (error) {
265
+ if (isNodeErrorWithCode(error, "ENOENT")) {
266
+ return;
267
+ }
268
+ throw error;
269
+ }
270
+ }
271
+ function resolveExportTargetPath(root, filePath, instance) {
272
+ const normalizedPath = filePath.replaceAll("\\", "/");
273
+ const segments = normalizedPath.split("/");
274
+ if (normalizedPath.trim().length === 0 ||
275
+ isAbsolute(normalizedPath) ||
276
+ segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
277
+ throw usageProblem("Export file paths must be relative paths inside the output directory.", instance);
278
+ }
279
+ const targetPath = resolve(root, normalizedPath);
280
+ const relativeTarget = relative(root, targetPath);
281
+ if (relativeTarget === "" || relativeTarget.startsWith("..") || isAbsolute(relativeTarget)) {
282
+ throw usageProblem("Export file paths must be relative paths inside the output directory.", instance);
283
+ }
284
+ return targetPath;
285
+ }
286
+ async function readLimitedUtf8File(filePath, displayPath, counters, options) {
287
+ if (counters.fileCount >= MAX_IMPORT_FILE_COUNT) {
288
+ throw usageProblem(`Import has more than ${String(MAX_IMPORT_FILE_COUNT)} files.`, options.instance);
289
+ }
290
+ const content = await readFile(filePath, "utf8");
291
+ const bytes = Buffer.byteLength(content, "utf8");
292
+ if (bytes > options.maxFileBytes) {
293
+ throw usageProblem(`Import file ${displayPath} exceeds the per-file byte limit.`, options.instance);
294
+ }
295
+ if (counters.totalBytes + bytes > MAX_IMPORT_TOTAL_BYTES) {
296
+ throw usageProblem(`Import exceeds the ${String(MAX_IMPORT_TOTAL_BYTES)} byte limit.`, options.instance);
297
+ }
298
+ counters.fileCount += 1;
299
+ counters.totalBytes += bytes;
300
+ return { content };
301
+ }
302
+ function assertPackageFileCanBeRead(bytes, displayPath, counters, instance) {
303
+ if (counters.fileCount >= MAX_IMPORT_FILE_COUNT) {
304
+ throw usageProblem(`Package has more than ${String(MAX_IMPORT_FILE_COUNT)} files.`, instance);
305
+ }
306
+ if (bytes > MAX_PACKAGE_FILE_BYTES) {
307
+ throw usageProblem(`Package file ${displayPath} exceeds the per-file byte limit.`, instance);
308
+ }
309
+ if (counters.totalBytes + bytes > MAX_IMPORT_TOTAL_BYTES) {
310
+ throw usageProblem(`Package exceeds the ${String(MAX_IMPORT_TOTAL_BYTES)} byte limit.`, instance);
311
+ }
312
+ counters.fileCount += 1;
313
+ counters.totalBytes += bytes;
314
+ }
315
+ function isNodeErrorWithCode(error, code) {
316
+ return typeof error === "object" &&
317
+ error !== null &&
318
+ "code" in error &&
319
+ error.code === code;
85
320
  }
package/dist/format.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export interface CommandOutput {
2
+ exitCode?: number;
2
3
  human: string;
3
4
  kind: string;
4
5
  meta?: Record<string, unknown>;
@@ -10,6 +11,7 @@ export declare function renderJsonEnvelope(input: {
10
11
  }): string;
11
12
  export declare function renderProblemJson(input: {
12
13
  detail: string;
14
+ details?: Record<string, unknown>;
13
15
  instance: string;
14
16
  status: number;
15
17
  title: string;
@@ -27,12 +29,15 @@ export declare function renderPrettyJson(value: unknown): string;
27
29
  export declare function renderVerboseHuman(base: string, data: unknown): string;
28
30
  export declare function renderVerboseProblem(input: {
29
31
  detail: string;
32
+ details?: Record<string, unknown>;
30
33
  instance: string;
31
34
  rawDetail?: string;
32
35
  title: string;
33
36
  type: string;
34
37
  }): string;
35
38
  export declare function renderSuccess(message: string): string;
39
+ export declare function formatProblemDetail(detail: string, details?: Record<string, unknown>): string;
40
+ export declare function formatProblemDetailsSummary(details: Record<string, unknown> | undefined): string | null;
36
41
  export declare function renderDiffSummary(input: {
37
42
  added: string[];
38
43
  changed: string[];
package/dist/format.js CHANGED
@@ -6,7 +6,14 @@ export function renderJsonEnvelope(input) {
6
6
  }, null, 2)}\n`;
7
7
  }
8
8
  export function renderProblemJson(input) {
9
- return `${JSON.stringify(input, null, 2)}\n`;
9
+ return `${JSON.stringify({
10
+ detail: input.detail,
11
+ ...(input.details ? { details: input.details } : {}),
12
+ instance: input.instance,
13
+ status: input.status,
14
+ title: input.title,
15
+ type: input.type
16
+ }, null, 2)}\n`;
10
17
  }
11
18
  export function renderTable(rows, columns) {
12
19
  if (rows.length === 0) {
@@ -38,8 +45,10 @@ export function renderVerboseHuman(base, data) {
38
45
  return `${base}\n\nDetails:\n${detail}`;
39
46
  }
40
47
  export function renderVerboseProblem(input) {
48
+ const detailsSummary = formatProblemDetailsSummary(input.details);
41
49
  const lines = [
42
50
  `${input.title}: ${input.detail}`,
51
+ ...(detailsSummary ? ["", detailsSummary] : []),
43
52
  "",
44
53
  "Details:",
45
54
  `Type: ${input.type}`,
@@ -48,11 +57,79 @@ export function renderVerboseProblem(input) {
48
57
  if (input.rawDetail && input.rawDetail !== input.detail) {
49
58
  lines.push(`Raw backend message: ${input.rawDetail}`);
50
59
  }
60
+ if (input.details) {
61
+ lines.push(`Structured details: ${renderPrettyJson(input.details)}`);
62
+ }
51
63
  return lines.join("\n");
52
64
  }
53
65
  export function renderSuccess(message) {
54
66
  return message;
55
67
  }
68
+ export function formatProblemDetail(detail, details) {
69
+ const detailsSummary = formatProblemDetailsSummary(details);
70
+ return detailsSummary ? `${detail}\n${detailsSummary}` : detail;
71
+ }
72
+ export function formatProblemDetailsSummary(details) {
73
+ if (!details) {
74
+ return null;
75
+ }
76
+ const diagnostics = formatProblemDetailsList(details.diagnostics, "Diagnostics", formatDiagnosticLikeEntry);
77
+ if (diagnostics) {
78
+ return diagnostics;
79
+ }
80
+ const errors = formatProblemDetailsList(details.errors, "Errors", formatUnknownDetailEntry);
81
+ if (errors) {
82
+ return errors;
83
+ }
84
+ return formatProblemDetailsList(details.issues, "Issues", formatUnknownDetailEntry);
85
+ }
86
+ function formatProblemDetailsList(value, label, formatter) {
87
+ if (!Array.isArray(value) || value.length === 0) {
88
+ return null;
89
+ }
90
+ const lines = value
91
+ .map(formatter)
92
+ .filter((entry) => typeof entry === "string" && entry.trim().length > 0);
93
+ if (lines.length === 0) {
94
+ return null;
95
+ }
96
+ return [label, ...lines.slice(0, 5).map((entry) => `- ${entry}`)].join("\n");
97
+ }
98
+ function formatDiagnosticLikeEntry(entry) {
99
+ if (!isRecord(entry)) {
100
+ return formatUnknownDetailEntry(entry);
101
+ }
102
+ const message = readString(entry.message);
103
+ if (!message) {
104
+ return formatUnknownDetailEntry(entry);
105
+ }
106
+ const path = readString(entry.path) ?? readString(entry.filePath) ?? readString(entry.instancePath);
107
+ const code = readString(entry.code);
108
+ return [
109
+ path ? `${path}:` : null,
110
+ message,
111
+ code && !path ? `(${code})` : null
112
+ ].filter((part) => typeof part === "string" && part.length > 0).join(" ");
113
+ }
114
+ function formatUnknownDetailEntry(entry) {
115
+ if (typeof entry === "string") {
116
+ return entry.trim() || null;
117
+ }
118
+ if (isRecord(entry)) {
119
+ const message = readString(entry.message);
120
+ if (message) {
121
+ const path = readString(entry.path) ?? readString(entry.instancePath);
122
+ return path ? `${path}: ${message}` : message;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ function isRecord(value) {
128
+ return typeof value === "object" && value !== null && !Array.isArray(value);
129
+ }
130
+ function readString(value) {
131
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
132
+ }
56
133
  export function renderDiffSummary(input) {
57
134
  const lines = [
58
135
  `Add: ${input.added.length}`,
package/dist/runner.js CHANGED
@@ -2,7 +2,7 @@ import { defaultGlobalConfigPath, loadCliConfig } from "./config.js";
2
2
  import { authProblem, exitCodeFromError, toProblem, usageProblem } from "./cli-errors.js";
3
3
  import { buildHelpJson, findCommandDefinition, listSubcommands, resolveCommandAliases } from "./command-registry.js";
4
4
  import { executeParsedCommand } from "./commands.js";
5
- import { renderJsonEnvelope, renderProblemJson, renderVerboseHuman, renderVerboseProblem } from "./format.js";
5
+ import { formatProblemDetail, renderJsonEnvelope, renderProblemJson, renderVerboseHuman, renderVerboseProblem } from "./format.js";
6
6
  import { createFileSessionStore, createMemorySessionStore } from "./session-store.js";
7
7
  import { defaultSessionPath, readSessionFromEnv } from "./session.js";
8
8
  const VISIBLE_COMMAND_LABELS_ENV = "KORA_VISIBLE_COMMAND_LABELS";
@@ -57,7 +57,7 @@ export async function runCli(argv, input = {}) {
57
57
  stdout
58
58
  });
59
59
  return {
60
- exitCode: 0,
60
+ exitCode: executed.exitCode ?? 0,
61
61
  stderr: "",
62
62
  stdout: parsed.json
63
63
  ? renderJsonEnvelope({
@@ -72,6 +72,7 @@ export async function runCli(argv, input = {}) {
72
72
  const wantsJson = tokens.includes("--json");
73
73
  const wantsVerbose = tokens.includes("--verbose");
74
74
  const problem = toProblem(error, tokens.join(" "));
75
+ const details = hasProblemDetails(problem) ? problem.details : undefined;
75
76
  return {
76
77
  exitCode: exitCodeFromError(problem),
77
78
  stderr: wantsJson
@@ -79,15 +80,17 @@ export async function runCli(argv, input = {}) {
79
80
  : `${wantsVerbose
80
81
  ? renderVerboseProblem({
81
82
  detail: problem.detail,
83
+ ...(details ? { details } : {}),
82
84
  instance: problem.instance,
83
85
  ...(hasRawDetail(problem) ? { rawDetail: problem.rawDetail } : {}),
84
86
  title: problem.title,
85
87
  type: problem.type
86
88
  })
87
- : `${problem.title}: ${problem.detail}`}\n`,
89
+ : `${problem.title}: ${formatProblemDetail(problem.detail, details)}`}\n`,
88
90
  stdout: wantsJson
89
91
  ? renderProblemJson({
90
92
  detail: problem.detail,
93
+ ...(details ? { details } : {}),
91
94
  instance: problem.instance,
92
95
  status: problem.status,
93
96
  title: problem.title,
@@ -137,6 +140,9 @@ function resolveHelpInvocation(tokens, commandFilter) {
137
140
  function hasRawDetail(problem) {
138
141
  return "rawDetail" in problem && typeof problem.rawDetail === "string";
139
142
  }
143
+ function hasProblemDetails(problem) {
144
+ return "details" in problem && typeof problem.details === "object" && problem.details !== null && !Array.isArray(problem.details);
145
+ }
140
146
  function normalizeHelpPath(tokens, commandFilter) {
141
147
  if (tokens.length === 0) {
142
148
  return [];
@@ -213,7 +219,10 @@ function parseInvocation(tokens, commandFilter) {
213
219
  throw usageProblem(withHelpSuggestion(`Unknown command: ${tokens.join(" ")}.`, resolved.commandPath, commandFilter), tokens.join(" "));
214
220
  }
215
221
  const definition = resolved.definition;
216
- const flagDefinitions = new Map(definition.flags.map((entry) => [entry.name, entry]));
222
+ const visibleFlags = commandFilter
223
+ ? definition.flags.filter((entry) => !entry.hiddenWhenCommandFiltered)
224
+ : definition.flags;
225
+ const flagDefinitions = new Map(visibleFlags.map((entry) => [entry.name, entry]));
217
226
  const positionals = [];
218
227
  const flags = {};
219
228
  for (let index = 0; index < resolved.remainder.length; index += 1) {
@@ -254,7 +263,7 @@ function parseInvocation(tokens, commandFilter) {
254
263
  if (positionals.length > definition.args.length) {
255
264
  throw usageProblem(`Too many positional arguments for '${definition.path.join(" ")}'.`, definition.path.join(" "));
256
265
  }
257
- for (const flag of definition.flags) {
266
+ for (const flag of visibleFlags) {
258
267
  if (flag.required && flags[flag.name] === undefined) {
259
268
  throw usageProblem(`Missing required flag '--${flag.name}'.`, definition.path.join(" "));
260
269
  }