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

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 (49) hide show
  1. package/README.md +21 -0
  2. package/dist/api-client.d.ts +274 -106
  3. package/dist/api-client.js +192 -167
  4. package/dist/api-types.d.ts +301 -163
  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 +177 -4
  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 +195 -32
  13. package/dist/cli-errors.d.ts +7 -1
  14. package/dist/cli-errors.js +12 -1
  15. package/dist/command-builders.d.ts +1 -0
  16. package/dist/command-builders.js +1 -0
  17. package/dist/command-flags.d.ts +1 -0
  18. package/dist/command-flags.js +7 -0
  19. package/dist/command-groups.js +10 -12
  20. package/dist/command-registry.js +595 -277
  21. package/dist/commands.js +728 -636
  22. package/dist/environment-context.d.ts +9 -0
  23. package/dist/environment-context.js +32 -0
  24. package/dist/error-code.d.ts +2 -0
  25. package/dist/error-code.js +9 -0
  26. package/dist/{integration-commands.d.ts → extension-commands.d.ts} +3 -2
  27. package/dist/extension-commands.js +446 -0
  28. package/dist/files.d.ts +44 -4
  29. package/dist/files.js +349 -26
  30. package/dist/format.d.ts +6 -0
  31. package/dist/format.js +83 -1
  32. package/dist/runner.js +28 -10
  33. package/dist/schema-registry-data.d.ts +318 -571
  34. package/dist/schema-registry-data.js +356 -698
  35. package/dist/session-store.js +80 -0
  36. package/dist/session.d.ts +1 -0
  37. package/dist/transport-refresh.d.ts +10 -0
  38. package/dist/transport-refresh.js +51 -0
  39. package/dist/transport.d.ts +31 -0
  40. package/dist/transport.js +102 -36
  41. package/dist/types.d.ts +2 -1
  42. package/dist/workspace-source.d.ts +1 -0
  43. package/dist/workspace-source.js +13 -0
  44. package/package.json +2 -1
  45. package/dist/dotenv.d.ts +0 -1
  46. package/dist/dotenv.js +0 -26
  47. package/dist/integration-api-client.d.ts +0 -29
  48. package/dist/integration-api-client.js +0 -50
  49. package/dist/integration-commands.js +0 -208
package/dist/files.js CHANGED
@@ -1,16 +1,44 @@
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
- import { parseDotEnv } from "./dotenv.js";
6
- export async function readJsonInputSpecifier(specifier, stdin, instance) {
5
+ import { shouldIgnoreWorkspacePath } from "./workspace-source.js";
6
+ const MAX_IMPORT_FILE_COUNT = 500;
7
+ const MAX_IMPORT_FILE_BYTES = 1_000_000;
8
+ const MAX_PACKAGE_FILE_BYTES = 2_000_000;
9
+ const MAX_IMPORT_TOTAL_BYTES = 10_000_000;
10
+ export async function readJsonInputSpecifier(specifier, stdin, instance, options = {}) {
7
11
  if (specifier === "-") {
8
- return readJsonObject(await readStream(stdin), instance);
12
+ return readJsonObject(await readStream(stdin), instance, {
13
+ source: "stdin",
14
+ ...options
15
+ });
9
16
  }
10
17
  if (!specifier.startsWith("@")) {
11
- throw usageProblem("Structured JSON input must use @file.json or - for stdin.", instance);
18
+ throw usageProblem("Structured JSON input must use @file.json or - for stdin.", instance, {
19
+ ...(options.flag ? { flag: options.flag } : {})
20
+ });
21
+ }
22
+ const filePath = resolve(specifier.slice(1));
23
+ let source;
24
+ try {
25
+ source = await readFile(filePath, "utf8");
12
26
  }
13
- return readJsonObject(await readFile(specifier.slice(1), "utf8"), instance);
27
+ catch (error) {
28
+ const nativeCode = readNodeErrorCode(error);
29
+ if (nativeCode && isLocalPathReadErrorCode(nativeCode)) {
30
+ throw usageProblem(`Structured JSON input file ${filePath} could not be read: ${nativeCode}.`, instance, {
31
+ filePath,
32
+ ...(options.flag ? { flag: options.flag } : {}),
33
+ nativeCode
34
+ });
35
+ }
36
+ throw error;
37
+ }
38
+ return readJsonObject(source, instance, {
39
+ filePath,
40
+ ...options
41
+ });
14
42
  }
15
43
  export async function readTextInputSpecifier(specifier, stdin, instance) {
16
44
  if (specifier === "-") {
@@ -22,36 +50,187 @@ export async function readTextInputSpecifier(specifier, stdin, instance) {
22
50
  return readFile(specifier.slice(1), "utf8");
23
51
  }
24
52
  export async function readImportEntries(pathValue) {
53
+ return await readUtf8Entries(pathValue, {
54
+ instance: "org.import",
55
+ maxFileBytes: MAX_IMPORT_FILE_BYTES
56
+ });
57
+ }
58
+ export function isZipArchivePath(pathValue) {
59
+ return extname(pathValue).toLowerCase() === ".zip";
60
+ }
61
+ export async function readArchiveBytes(pathValue, instance) {
62
+ const file = await readLocalFileBytes(pathValue, instance, {
63
+ regularFileMessage: "Archive path must be a regular .zip file, not a directory or symbolic link."
64
+ });
65
+ return file.bytes;
66
+ }
67
+ export async function readLocalFileBytes(pathValue, instance, options = {}) {
25
68
  const absolutePath = resolve(pathValue);
26
- const pathStat = await stat(absolutePath);
69
+ const pathStat = await readLocalPathStat(absolutePath, instance);
70
+ if (pathStat.isSymbolicLink() || !pathStat.isFile()) {
71
+ throw usageProblem(options.regularFileMessage ?? "Path must be a regular file, not a directory or symbolic link.", instance);
72
+ }
73
+ try {
74
+ return {
75
+ absolutePath,
76
+ bytes: await readFile(absolutePath)
77
+ };
78
+ }
79
+ catch (error) {
80
+ throwLocalPathReadProblem(error, absolutePath, instance);
81
+ }
82
+ }
83
+ export async function readWorkspaceTestEntries(pathValue) {
84
+ return await readUtf8Entries(pathValue, {
85
+ instance: "test node",
86
+ maxFileBytes: MAX_PACKAGE_FILE_BYTES
87
+ });
88
+ }
89
+ async function readUtf8Entries(pathValue, options) {
90
+ const absolutePath = resolve(pathValue);
91
+ const pathStat = await readLocalPathStat(absolutePath, options.instance);
92
+ if (pathStat.isSymbolicLink()) {
93
+ throw usageProblem("Import path must be a regular file or directory, not a symbolic link.", options.instance);
94
+ }
27
95
  if (pathStat.isFile()) {
96
+ const pathName = basename(absolutePath);
97
+ if (shouldIgnoreSourceImportPath(pathName)) {
98
+ return [];
99
+ }
100
+ const content = await readLimitedUtf8File(absolutePath, pathName, {
101
+ fileCount: 0,
102
+ totalBytes: 0
103
+ }, options);
28
104
  return [{
29
- content: await readFile(absolutePath, "utf8"),
105
+ content: content.content,
30
106
  path: basename(absolutePath)
31
107
  }];
32
108
  }
33
109
  const entries = [];
34
- await walkImportDirectory(absolutePath, absolutePath, entries);
110
+ await walkImportDirectory(absolutePath, absolutePath, entries, {
111
+ fileCount: 0,
112
+ totalBytes: 0
113
+ }, options);
35
114
  entries.sort((left, right) => left.path.localeCompare(right.path));
36
115
  return entries;
37
116
  }
38
- export async function parseEnvFile(pathValue) {
39
- const content = await readFile(pathValue, "utf8");
40
- return Object.entries(parseDotEnv(content)).map(([name, value]) => ({
41
- name,
42
- value
43
- }));
117
+ export async function readWorkspaceExportMetadata(pathValue) {
118
+ const absolutePath = resolve(pathValue);
119
+ try {
120
+ const metadata = await readFile(join(absolutePath, ".kora", "export.json"), "utf8");
121
+ const parsed = JSON.parse(metadata);
122
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
123
+ ? parsed
124
+ : null;
125
+ }
126
+ catch (error) {
127
+ if (isNodeErrorWithCode(error, "ENOENT") || isNodeErrorWithCode(error, "ENOTDIR")) {
128
+ return null;
129
+ }
130
+ throw error;
131
+ }
132
+ }
133
+ export async function writeWorkspaceExport(outPath, envelope) {
134
+ const absolutePath = resolve(outPath);
135
+ await assertExportOutputIsEmpty(absolutePath, "org.export");
136
+ await mkdir(absolutePath, { recursive: true });
137
+ for (const file of envelope.files) {
138
+ const targetPath = resolveExportTargetPath(absolutePath, file.path, "org.export");
139
+ await mkdir(dirname(targetPath), { recursive: true });
140
+ await writeFile(targetPath, file.content, "utf8");
141
+ }
142
+ const metadataPath = join(absolutePath, ".kora", "export.json");
143
+ await mkdir(dirname(metadataPath), { recursive: true });
144
+ await writeFile(metadataPath, `${JSON.stringify(envelope.metadata, null, 2)}\n`, "utf8");
145
+ }
146
+ export async function writeReleaseSourceFiles(outPath, envelope) {
147
+ const absolutePath = resolve(outPath);
148
+ await assertExportOutputIsEmpty(absolutePath, "release source", "Release source");
149
+ await mkdir(absolutePath, { recursive: true });
150
+ const seenPaths = new Set();
151
+ for (const file of envelope.files) {
152
+ if (seenPaths.has(file.path)) {
153
+ throw usageProblem(`Release source contains duplicate file path '${file.path}'.`, "release source");
154
+ }
155
+ seenPaths.add(file.path);
156
+ const targetPath = resolveExportTargetPath(absolutePath, file.path, "release source");
157
+ await mkdir(dirname(targetPath), { recursive: true });
158
+ await writeFile(targetPath, file.content, "utf8");
159
+ }
160
+ const metadataPath = join(absolutePath, ".kora", "release-source.json");
161
+ await mkdir(dirname(metadataPath), { recursive: true });
162
+ await writeFile(metadataPath, `${JSON.stringify(envelope.metadata, null, 2)}\n`, "utf8");
44
163
  }
45
- function readJsonObject(source, instance) {
164
+ export async function writeArchiveExport(outPath, archive, instance) {
165
+ const absolutePath = resolve(outPath);
166
+ await mkdir(dirname(absolutePath), { recursive: true });
167
+ try {
168
+ await writeFile(absolutePath, archive, { flag: "wx" });
169
+ }
170
+ catch (error) {
171
+ if (isNodeErrorWithCode(error, "EEXIST")) {
172
+ throw usageProblem("Export output file already exists.", instance);
173
+ }
174
+ throw error;
175
+ }
176
+ }
177
+ export async function writePackageExport(outPath, envelope) {
178
+ const absolutePath = resolve(outPath);
179
+ await assertExportOutputIsEmpty(absolutePath, "extensions.export");
180
+ await mkdir(absolutePath, { recursive: true });
181
+ for (const file of envelope.files) {
182
+ const targetPath = resolveExportTargetPath(absolutePath, file.path, "extensions.export");
183
+ await mkdir(dirname(targetPath), { recursive: true });
184
+ await writeFile(targetPath, Buffer.from(file.contentBase64, "base64"));
185
+ }
186
+ const metadataPath = join(absolutePath, ".kora", "export.json");
187
+ await mkdir(dirname(metadataPath), { recursive: true });
188
+ await writeFile(metadataPath, `${JSON.stringify(envelope.metadata, null, 2)}\n`, "utf8");
189
+ }
190
+ export async function readPackageFileEntries(pathValue, instance) {
191
+ const absolutePath = resolve(pathValue);
192
+ const pathStat = await readLocalPathStat(absolutePath, instance);
193
+ if (pathStat.isSymbolicLink()) {
194
+ throw usageProblem("Package path must be a regular file or directory, not a symbolic link.", instance);
195
+ }
196
+ if (pathStat.isFile()) {
197
+ assertPackageFileCanBeRead(pathStat.size, basename(absolutePath), {
198
+ fileCount: 0,
199
+ totalBytes: 0
200
+ }, instance);
201
+ return [{
202
+ contentBase64: (await readLocalFileBytes(absolutePath, instance, {
203
+ regularFileMessage: "Package path must be a regular file or directory, not a symbolic link."
204
+ })).bytes.toString("base64"),
205
+ path: basename(absolutePath)
206
+ }];
207
+ }
208
+ const entries = [];
209
+ await walkPackageDirectory(absolutePath, absolutePath, entries, {
210
+ fileCount: 0,
211
+ totalBytes: 0
212
+ }, instance);
213
+ entries.sort((left, right) => left.path.localeCompare(right.path));
214
+ return entries;
215
+ }
216
+ function readJsonObject(source, instance, options) {
46
217
  let parsed;
47
218
  try {
48
219
  parsed = JSON.parse(source);
49
220
  }
50
221
  catch {
51
- throw usageProblem("Structured JSON input must be valid JSON.", instance);
222
+ const sourceLabel = "filePath" in options ? `file ${options.filePath}` : "from stdin";
223
+ throw usageProblem(`Structured JSON input ${sourceLabel} must be valid JSON.`, instance, {
224
+ ...("filePath" in options ? { filePath: options.filePath } : { source: options.source }),
225
+ ...(options.flag ? { flag: options.flag } : {})
226
+ });
52
227
  }
53
228
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
54
- throw usageProblem("Structured JSON input must be a JSON object.", instance);
229
+ const sourceLabel = "filePath" in options ? `file ${options.filePath}` : "from stdin";
230
+ throw usageProblem(`Structured JSON input ${sourceLabel} must be a JSON object.`, instance, {
231
+ ...("filePath" in options ? { filePath: options.filePath } : { source: options.source }),
232
+ ...(options.flag ? { flag: options.flag } : {})
233
+ });
55
234
  }
56
235
  return parsed;
57
236
  }
@@ -62,24 +241,168 @@ async function readStream(stream) {
62
241
  }
63
242
  return Buffer.concat(chunks).toString("utf8");
64
243
  }
65
- async function walkImportDirectory(root, current, entries) {
244
+ async function walkImportDirectory(root, current, entries, counters, options) {
66
245
  const children = await readdir(current, { withFileTypes: true });
67
246
  children.sort((left, right) => left.name.localeCompare(right.name));
68
247
  for (const child of children) {
69
- if (shouldIgnoreImportSegment(child.name)) {
248
+ const childPath = join(current, child.name);
249
+ const relativePath = relative(root, childPath).replaceAll("\\", "/");
250
+ if (shouldIgnoreSourceImportPath(relativePath)) {
251
+ continue;
252
+ }
253
+ if (child.isDirectory()) {
254
+ await walkImportDirectory(root, childPath, entries, counters, options);
255
+ continue;
256
+ }
257
+ if (!child.isFile()) {
70
258
  continue;
71
259
  }
260
+ const content = await readLimitedUtf8File(childPath, relativePath, counters, options);
261
+ entries.push({
262
+ content: content.content,
263
+ path: relativePath
264
+ });
265
+ }
266
+ }
267
+ async function walkPackageDirectory(root, current, entries, counters, instance) {
268
+ const children = await readdir(current, { withFileTypes: true });
269
+ children.sort((left, right) => left.name.localeCompare(right.name));
270
+ for (const child of children) {
72
271
  const childPath = join(current, child.name);
272
+ const relativePath = relative(root, childPath).replaceAll("\\", "/");
273
+ if (shouldIgnoreImportPath(relativePath)) {
274
+ continue;
275
+ }
73
276
  if (child.isDirectory()) {
74
- await walkImportDirectory(root, childPath, entries);
277
+ await walkPackageDirectory(root, childPath, entries, counters, instance);
75
278
  continue;
76
279
  }
280
+ if (!child.isFile()) {
281
+ continue;
282
+ }
283
+ const childStat = await readLocalPathStat(childPath, instance);
284
+ assertPackageFileCanBeRead(childStat.size, relativePath, counters, instance);
77
285
  entries.push({
78
- content: await readFile(childPath, "utf8"),
79
- path: relative(root, childPath).replaceAll("\\", "/")
286
+ contentBase64: (await readLocalFileBytes(childPath, instance)).bytes.toString("base64"),
287
+ path: relativePath
80
288
  });
81
289
  }
82
290
  }
83
- function shouldIgnoreImportSegment(segment) {
84
- return new Set([".git", ".kora", "coverage", "dist", "node_modules"]).has(segment);
291
+ function shouldIgnoreImportPath(pathValue) {
292
+ return shouldIgnoreWorkspacePath(pathValue);
293
+ }
294
+ function shouldIgnoreSourceImportPath(pathValue) {
295
+ return shouldIgnoreImportPath(pathValue) || isRemovedRuntimeEnvironmentPath(pathValue);
296
+ }
297
+ function isRemovedRuntimeEnvironmentPath(pathValue) {
298
+ return pathValue === ".env" || pathValue === ".env.example";
299
+ }
300
+ async function assertExportOutputIsEmpty(absolutePath, instance, outputLabel = "Export") {
301
+ try {
302
+ const pathStat = await lstat(absolutePath);
303
+ if (pathStat.isSymbolicLink()) {
304
+ throw usageProblem(`${outputLabel} output path must not be a symbolic link.`, instance);
305
+ }
306
+ if (!pathStat.isDirectory()) {
307
+ throw usageProblem(`${outputLabel} output path must be a directory.`, instance);
308
+ }
309
+ const children = await readdir(absolutePath);
310
+ if (children.length > 0) {
311
+ throw usageProblem(`${outputLabel} output directory must be empty or not exist.`, instance);
312
+ }
313
+ }
314
+ catch (error) {
315
+ if (isNodeErrorWithCode(error, "ENOENT")) {
316
+ return;
317
+ }
318
+ throw error;
319
+ }
320
+ }
321
+ function resolveExportTargetPath(root, filePath, instance) {
322
+ const normalizedPath = filePath.replaceAll("\\", "/");
323
+ const segments = normalizedPath.split("/");
324
+ if (normalizedPath.trim().length === 0 ||
325
+ isAbsolute(normalizedPath) ||
326
+ segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
327
+ throw usageProblem("Export file paths must be relative paths inside the output directory.", instance);
328
+ }
329
+ const targetPath = resolve(root, normalizedPath);
330
+ const relativeTarget = relative(root, targetPath);
331
+ if (relativeTarget === "" || relativeTarget.startsWith("..") || isAbsolute(relativeTarget)) {
332
+ throw usageProblem("Export file paths must be relative paths inside the output directory.", instance);
333
+ }
334
+ return targetPath;
335
+ }
336
+ async function readLimitedUtf8File(filePath, displayPath, counters, options) {
337
+ if (counters.fileCount >= MAX_IMPORT_FILE_COUNT) {
338
+ throw usageProblem(`Import has more than ${String(MAX_IMPORT_FILE_COUNT)} files.`, options.instance);
339
+ }
340
+ let content;
341
+ try {
342
+ content = await readFile(filePath, "utf8");
343
+ }
344
+ catch (error) {
345
+ throwLocalPathReadProblem(error, filePath, options.instance);
346
+ }
347
+ const bytes = Buffer.byteLength(content, "utf8");
348
+ if (bytes > options.maxFileBytes) {
349
+ throw usageProblem(`Import file ${displayPath} exceeds the per-file byte limit.`, options.instance);
350
+ }
351
+ if (counters.totalBytes + bytes > MAX_IMPORT_TOTAL_BYTES) {
352
+ throw usageProblem(`Import exceeds the ${String(MAX_IMPORT_TOTAL_BYTES)} byte limit.`, options.instance);
353
+ }
354
+ counters.fileCount += 1;
355
+ counters.totalBytes += bytes;
356
+ return { content };
357
+ }
358
+ function assertPackageFileCanBeRead(bytes, displayPath, counters, instance) {
359
+ if (counters.fileCount >= MAX_IMPORT_FILE_COUNT) {
360
+ throw usageProblem(`Package has more than ${String(MAX_IMPORT_FILE_COUNT)} files.`, instance);
361
+ }
362
+ if (bytes > MAX_PACKAGE_FILE_BYTES) {
363
+ throw usageProblem(`Package file ${displayPath} exceeds the per-file byte limit.`, instance);
364
+ }
365
+ if (counters.totalBytes + bytes > MAX_IMPORT_TOTAL_BYTES) {
366
+ throw usageProblem(`Package exceeds the ${String(MAX_IMPORT_TOTAL_BYTES)} byte limit.`, instance);
367
+ }
368
+ counters.fileCount += 1;
369
+ counters.totalBytes += bytes;
370
+ }
371
+ function isNodeErrorWithCode(error, code) {
372
+ return typeof error === "object" &&
373
+ error !== null &&
374
+ "code" in error &&
375
+ error.code === code;
376
+ }
377
+ function readNodeErrorCode(error) {
378
+ if (typeof error !== "object" || error === null || !("code" in error)) {
379
+ return undefined;
380
+ }
381
+ const code = error.code;
382
+ return typeof code === "string" && code.length > 0 ? code : undefined;
383
+ }
384
+ function isLocalPathReadErrorCode(code) {
385
+ return code === "EACCES" ||
386
+ code === "EISDIR" ||
387
+ code === "ENOENT" ||
388
+ code === "ENOTDIR" ||
389
+ code === "EPERM";
390
+ }
391
+ async function readLocalPathStat(absolutePath, instance) {
392
+ try {
393
+ return await lstat(absolutePath);
394
+ }
395
+ catch (error) {
396
+ throwLocalPathReadProblem(error, absolutePath, instance);
397
+ }
398
+ }
399
+ function throwLocalPathReadProblem(error, absolutePath, instance) {
400
+ const nativeCode = readNodeErrorCode(error);
401
+ if (nativeCode && isLocalPathReadErrorCode(nativeCode)) {
402
+ throw usageProblem(`Local path ${absolutePath} could not be read: ${nativeCode}.`, instance, {
403
+ nativeCode,
404
+ path: absolutePath
405
+ });
406
+ }
407
+ throw error;
85
408
  }
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>;
@@ -9,7 +10,9 @@ export declare function renderJsonEnvelope(input: {
9
10
  meta?: Record<string, unknown>;
10
11
  }): string;
11
12
  export declare function renderProblemJson(input: {
13
+ code: string;
12
14
  detail: string;
15
+ details?: Record<string, unknown>;
13
16
  instance: string;
14
17
  status: number;
15
18
  title: string;
@@ -27,12 +30,15 @@ export declare function renderPrettyJson(value: unknown): string;
27
30
  export declare function renderVerboseHuman(base: string, data: unknown): string;
28
31
  export declare function renderVerboseProblem(input: {
29
32
  detail: string;
33
+ details?: Record<string, unknown>;
30
34
  instance: string;
31
35
  rawDetail?: string;
32
36
  title: string;
33
37
  type: string;
34
38
  }): string;
35
39
  export declare function renderSuccess(message: string): string;
40
+ export declare function formatProblemDetail(detail: string, details?: Record<string, unknown>): string;
41
+ export declare function formatProblemDetailsSummary(details: Record<string, unknown> | undefined): string | null;
36
42
  export declare function renderDiffSummary(input: {
37
43
  added: string[];
38
44
  changed: string[];
package/dist/format.js CHANGED
@@ -6,7 +6,19 @@ 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
+ error: {
11
+ code: input.code,
12
+ details: {
13
+ ...(input.details ?? {}),
14
+ instance: input.instance,
15
+ status: input.status,
16
+ title: input.title,
17
+ type: input.type
18
+ },
19
+ message: input.detail
20
+ }
21
+ }, null, 2)}\n`;
10
22
  }
11
23
  export function renderTable(rows, columns) {
12
24
  if (rows.length === 0) {
@@ -38,8 +50,10 @@ export function renderVerboseHuman(base, data) {
38
50
  return `${base}\n\nDetails:\n${detail}`;
39
51
  }
40
52
  export function renderVerboseProblem(input) {
53
+ const detailsSummary = formatProblemDetailsSummary(input.details);
41
54
  const lines = [
42
55
  `${input.title}: ${input.detail}`,
56
+ ...(detailsSummary ? ["", detailsSummary] : []),
43
57
  "",
44
58
  "Details:",
45
59
  `Type: ${input.type}`,
@@ -48,11 +62,79 @@ export function renderVerboseProblem(input) {
48
62
  if (input.rawDetail && input.rawDetail !== input.detail) {
49
63
  lines.push(`Raw backend message: ${input.rawDetail}`);
50
64
  }
65
+ if (input.details) {
66
+ lines.push(`Structured details: ${renderPrettyJson(input.details)}`);
67
+ }
51
68
  return lines.join("\n");
52
69
  }
53
70
  export function renderSuccess(message) {
54
71
  return message;
55
72
  }
73
+ export function formatProblemDetail(detail, details) {
74
+ const detailsSummary = formatProblemDetailsSummary(details);
75
+ return detailsSummary ? `${detail}\n${detailsSummary}` : detail;
76
+ }
77
+ export function formatProblemDetailsSummary(details) {
78
+ if (!details) {
79
+ return null;
80
+ }
81
+ const diagnostics = formatProblemDetailsList(details.diagnostics, "Diagnostics", formatDiagnosticLikeEntry);
82
+ if (diagnostics) {
83
+ return diagnostics;
84
+ }
85
+ const errors = formatProblemDetailsList(details.errors, "Errors", formatUnknownDetailEntry);
86
+ if (errors) {
87
+ return errors;
88
+ }
89
+ return formatProblemDetailsList(details.issues, "Issues", formatUnknownDetailEntry);
90
+ }
91
+ function formatProblemDetailsList(value, label, formatter) {
92
+ if (!Array.isArray(value) || value.length === 0) {
93
+ return null;
94
+ }
95
+ const lines = value
96
+ .map(formatter)
97
+ .filter((entry) => typeof entry === "string" && entry.trim().length > 0);
98
+ if (lines.length === 0) {
99
+ return null;
100
+ }
101
+ return [label, ...lines.slice(0, 5).map((entry) => `- ${entry}`)].join("\n");
102
+ }
103
+ function formatDiagnosticLikeEntry(entry) {
104
+ if (!isRecord(entry)) {
105
+ return formatUnknownDetailEntry(entry);
106
+ }
107
+ const message = readString(entry.message);
108
+ if (!message) {
109
+ return formatUnknownDetailEntry(entry);
110
+ }
111
+ const path = readString(entry.path) ?? readString(entry.filePath) ?? readString(entry.instancePath);
112
+ const code = readString(entry.code);
113
+ return [
114
+ path ? `${path}:` : null,
115
+ message,
116
+ code && !path ? `(${code})` : null
117
+ ].filter((part) => typeof part === "string" && part.length > 0).join(" ");
118
+ }
119
+ function formatUnknownDetailEntry(entry) {
120
+ if (typeof entry === "string") {
121
+ return entry.trim() || null;
122
+ }
123
+ if (isRecord(entry)) {
124
+ const message = readString(entry.message);
125
+ if (message) {
126
+ const path = readString(entry.path) ?? readString(entry.instancePath);
127
+ return path ? `${path}: ${message}` : message;
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+ function isRecord(value) {
133
+ return typeof value === "object" && value !== null && !Array.isArray(value);
134
+ }
135
+ function readString(value) {
136
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
137
+ }
56
138
  export function renderDiffSummary(input) {
57
139
  const lines = [
58
140
  `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,18 @@ 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({
92
+ code: problem.code,
90
93
  detail: problem.detail,
94
+ ...(details ? { details } : {}),
91
95
  instance: problem.instance,
92
96
  status: problem.status,
93
97
  title: problem.title,
@@ -137,6 +141,9 @@ function resolveHelpInvocation(tokens, commandFilter) {
137
141
  function hasRawDetail(problem) {
138
142
  return "rawDetail" in problem && typeof problem.rawDetail === "string";
139
143
  }
144
+ function hasProblemDetails(problem) {
145
+ return "details" in problem && typeof problem.details === "object" && problem.details !== null && !Array.isArray(problem.details);
146
+ }
140
147
  function normalizeHelpPath(tokens, commandFilter) {
141
148
  if (tokens.length === 0) {
142
149
  return [];
@@ -213,7 +220,10 @@ function parseInvocation(tokens, commandFilter) {
213
220
  throw usageProblem(withHelpSuggestion(`Unknown command: ${tokens.join(" ")}.`, resolved.commandPath, commandFilter), tokens.join(" "));
214
221
  }
215
222
  const definition = resolved.definition;
216
- const flagDefinitions = new Map(definition.flags.map((entry) => [entry.name, entry]));
223
+ const visibleFlags = commandFilter
224
+ ? definition.flags.filter((entry) => !entry.hiddenWhenCommandFiltered)
225
+ : definition.flags;
226
+ const flagDefinitions = new Map(visibleFlags.map((entry) => [entry.name, entry]));
217
227
  const positionals = [];
218
228
  const flags = {};
219
229
  for (let index = 0; index < resolved.remainder.length; index += 1) {
@@ -222,7 +232,10 @@ function parseInvocation(tokens, commandFilter) {
222
232
  positionals.push(token);
223
233
  continue;
224
234
  }
225
- const [rawName = "", maybeValue] = token.slice(2).split("=", 2);
235
+ const rawFlag = token.slice(2);
236
+ const valueSeparatorIndex = rawFlag.indexOf("=");
237
+ const rawName = valueSeparatorIndex === -1 ? rawFlag : rawFlag.slice(0, valueSeparatorIndex);
238
+ const maybeValue = valueSeparatorIndex === -1 ? undefined : rawFlag.slice(valueSeparatorIndex + 1);
226
239
  const flag = flagDefinitions.get(rawName);
227
240
  if (!flag) {
228
241
  const detail = rawName === "output"
@@ -237,11 +250,16 @@ function parseInvocation(tokens, commandFilter) {
237
250
  flags[rawName] = true;
238
251
  continue;
239
252
  }
240
- const valueSource = maybeValue ?? resolved.remainder[index + 1];
241
- if (!valueSource || valueSource.startsWith("--")) {
242
- throw usageProblem(`Flag '--${rawName}' requires a value.`, definition.path.join(" "));
253
+ let valueSource;
254
+ if (maybeValue !== undefined) {
255
+ valueSource = maybeValue;
243
256
  }
244
- if (maybeValue === undefined) {
257
+ else {
258
+ const nextValue = resolved.remainder[index + 1];
259
+ if (!nextValue || nextValue.startsWith("--")) {
260
+ throw usageProblem(`Flag '--${rawName}' requires a value.`, definition.path.join(" "));
261
+ }
262
+ valueSource = nextValue;
245
263
  index += 1;
246
264
  }
247
265
  const coercedValue = coerceFlagValue(flag.valueType ?? "string", valueSource, rawName, definition.path.join(" "));
@@ -254,7 +272,7 @@ function parseInvocation(tokens, commandFilter) {
254
272
  if (positionals.length > definition.args.length) {
255
273
  throw usageProblem(`Too many positional arguments for '${definition.path.join(" ")}'.`, definition.path.join(" "));
256
274
  }
257
- for (const flag of definition.flags) {
275
+ for (const flag of visibleFlags) {
258
276
  if (flag.required && flags[flag.name] === undefined) {
259
277
  throw usageProblem(`Missing required flag '--${flag.name}'.`, definition.path.join(" "));
260
278
  }