@f5xc-salesdemos/xcsh 18.64.1 → 18.65.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/CHANGELOG.md +6 -0
- package/package.json +7 -7
- package/scripts/generate-api-spec-index.ts +129 -1
- package/src/internal-urls/api-spec-resolve.ts +204 -20
- package/src/internal-urls/api-spec-types.ts +75 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/xcsh-protocol.ts +17 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.64.2] - 2026-05-16
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Welcome banner cloud provider hints: AWS SSO token expiry now correctly suggests `aws sso login` instead of `aws configure`; Google Cloud check replaced `gcloud auth list` (false positives on expired tokens) with `gcloud auth print-access-token`; F5 XC Context surfaces `errorClass` (network/URL) in hints; GitLab `project_inaccessible` gets its own hint; Salesforce differentiates `session_expired` and `not_configured` hints ([#825](https://github.com/f5xc-salesdemos/xcsh/issues/825))
|
|
10
|
+
|
|
5
11
|
## [18.64.0] - 2026-05-13
|
|
6
12
|
|
|
7
13
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.65.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
50
50
|
"@mozilla/readability": "^0.6",
|
|
51
|
-
"@f5xc-salesdemos/xcsh-stats": "18.
|
|
52
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
53
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
56
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
51
|
+
"@f5xc-salesdemos/xcsh-stats": "18.65.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-agent-core": "18.65.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-ai": "18.65.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-natives": "18.65.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-tui": "18.65.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-utils": "18.65.0",
|
|
57
57
|
"@sinclair/typebox": "^0.34",
|
|
58
58
|
"@xterm/headless": "^6.0",
|
|
59
59
|
"ajv": "^8.18",
|
|
@@ -64,6 +64,16 @@ interface IndexEntry {
|
|
|
64
64
|
"x-f5xc-use-cases"?: string[];
|
|
65
65
|
"x-f5xc-related-domains"?: string[];
|
|
66
66
|
"x-f5xc-primary-resources"?: IndexEntryResource[];
|
|
67
|
+
"x-f5xc-description-long"?: string;
|
|
68
|
+
"x-f5xc-summary"?: string;
|
|
69
|
+
"x-f5xc-logo-svg"?: string;
|
|
70
|
+
"x-f5xc-cli-domain"?: string;
|
|
71
|
+
"x-f5xc-cli-metadata"?: {
|
|
72
|
+
quick_start: { command: string; description: string; expected_output: string };
|
|
73
|
+
common_workflows: Array<{ name: string; commands: string[] }>;
|
|
74
|
+
troubleshooting: Array<{ symptom: string; fix: string }>;
|
|
75
|
+
icon?: string;
|
|
76
|
+
};
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
interface RawIndex {
|
|
@@ -224,6 +234,8 @@ for (const entry of rawIndex.specifications) {
|
|
|
224
234
|
const specContent = fs.readFileSync(specFile, "utf-8");
|
|
225
235
|
const specJson = JSON.parse(specContent) as {
|
|
226
236
|
paths?: Record<string, Record<string, SpecPathOperation>>;
|
|
237
|
+
info?: Record<string, unknown>;
|
|
238
|
+
components?: { schemas?: Record<string, Record<string, unknown>> };
|
|
227
239
|
[k: string]: unknown;
|
|
228
240
|
};
|
|
229
241
|
|
|
@@ -255,6 +267,14 @@ for (const entry of rawIndex.specifications) {
|
|
|
255
267
|
|
|
256
268
|
const useCases = entry["x-f5xc-use-cases"];
|
|
257
269
|
const relatedDomains = entry["x-f5xc-related-domains"];
|
|
270
|
+
const rawBp = specJson.info?.["x-f5xc-best-practices"] as Record<string, unknown> | undefined;
|
|
271
|
+
const bpData = rawBp
|
|
272
|
+
? {
|
|
273
|
+
commonErrors: rawBp.common_errors ?? [],
|
|
274
|
+
securityNotes: rawBp.security_notes ?? [],
|
|
275
|
+
performanceTips: rawBp.performance_tips ?? [],
|
|
276
|
+
}
|
|
277
|
+
: undefined;
|
|
258
278
|
|
|
259
279
|
domainEntries.push(
|
|
260
280
|
[
|
|
@@ -280,6 +300,35 @@ for (const entry of rawIndex.specifications) {
|
|
|
280
300
|
entry["x-f5xc-requires-tier"]
|
|
281
301
|
? `\t\t\trequiresTier: ${JSON.stringify(entry["x-f5xc-requires-tier"])},`
|
|
282
302
|
: undefined,
|
|
303
|
+
entry["x-f5xc-description-long"]
|
|
304
|
+
? `\t\t\tdescriptionLong: ${JSON.stringify(entry["x-f5xc-description-long"])},`
|
|
305
|
+
: undefined,
|
|
306
|
+
entry["x-f5xc-summary"] ? `\t\t\tsummary: ${JSON.stringify(entry["x-f5xc-summary"])},` : undefined,
|
|
307
|
+
entry["x-f5xc-logo-svg"] ? `\t\t\tlogoSvg: ${JSON.stringify(entry["x-f5xc-logo-svg"])},` : undefined,
|
|
308
|
+
entry["x-f5xc-cli-domain"] ? `\t\t\tcliDomain: ${JSON.stringify(entry["x-f5xc-cli-domain"])},` : undefined,
|
|
309
|
+
entry["x-f5xc-cli-metadata"]
|
|
310
|
+
? (() => {
|
|
311
|
+
const raw = entry["x-f5xc-cli-metadata"]!;
|
|
312
|
+
const qs = raw.quick_start;
|
|
313
|
+
return `\t\t\tcliMetadata: ${JSON.stringify({
|
|
314
|
+
quickStart: {
|
|
315
|
+
command: qs.command,
|
|
316
|
+
description: qs.description,
|
|
317
|
+
expectedOutput: qs.expected_output,
|
|
318
|
+
},
|
|
319
|
+
commonWorkflows: (raw.common_workflows ?? []).map((w: { name: string; commands: string[] }) => ({
|
|
320
|
+
name: w.name,
|
|
321
|
+
commands: w.commands,
|
|
322
|
+
})),
|
|
323
|
+
troubleshooting: (raw.troubleshooting ?? []).map((t: { symptom: string; fix: string }) => ({
|
|
324
|
+
symptom: t.symptom,
|
|
325
|
+
fix: t.fix,
|
|
326
|
+
})),
|
|
327
|
+
icon: raw.icon,
|
|
328
|
+
})},`;
|
|
329
|
+
})()
|
|
330
|
+
: undefined,
|
|
331
|
+
bpData ? `\t\t\tbestPractices: ${JSON.stringify(bpData)},` : undefined,
|
|
283
332
|
"\t\t},",
|
|
284
333
|
]
|
|
285
334
|
.filter(Boolean)
|
|
@@ -295,10 +344,85 @@ const guidedWorkflows = rawIndex["x-f5xc-guided-workflows"];
|
|
|
295
344
|
const errorResolution = rawIndex["x-f5xc-error-resolution"];
|
|
296
345
|
const acronyms = rawIndex["x-f5xc-acronyms"];
|
|
297
346
|
|
|
347
|
+
// Extract operation-level and schema-level enrichments per domain
|
|
348
|
+
const enrichmentEntries: string[] = [];
|
|
349
|
+
|
|
350
|
+
for (const entry of rawIndex.specifications) {
|
|
351
|
+
const specFile = path.join(specsDir, entry.file);
|
|
352
|
+
if (!fs.existsSync(specFile)) continue;
|
|
353
|
+
|
|
354
|
+
const enrichSpecContent = fs.readFileSync(specFile, "utf-8");
|
|
355
|
+
const enrichSpecJson = JSON.parse(enrichSpecContent) as {
|
|
356
|
+
paths?: Record<string, Record<string, Record<string, unknown>>>;
|
|
357
|
+
components?: { schemas?: Record<string, Record<string, unknown>> };
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const operationMeta: Record<string, Record<string, unknown>> = {};
|
|
361
|
+
for (const methods of Object.values(enrichSpecJson.paths ?? {})) {
|
|
362
|
+
for (const op of Object.values(methods)) {
|
|
363
|
+
if (typeof op !== "object" || !op) continue;
|
|
364
|
+
const opId = op.operationId as string | undefined;
|
|
365
|
+
if (!opId) continue;
|
|
366
|
+
const enrichment: Record<string, unknown> = {};
|
|
367
|
+
if (op["x-f5xc-danger-level"]) enrichment.dangerLevel = op["x-f5xc-danger-level"];
|
|
368
|
+
if (op["x-f5xc-confirmation-required"] != null)
|
|
369
|
+
enrichment.confirmationRequired = op["x-f5xc-confirmation-required"];
|
|
370
|
+
if (op["x-f5xc-side-effects"]) enrichment.sideEffects = op["x-f5xc-side-effects"];
|
|
371
|
+
if (op["x-f5xc-discovered-response-time"]) {
|
|
372
|
+
const rt = op["x-f5xc-discovered-response-time"] as Record<string, unknown>;
|
|
373
|
+
enrichment.discoveredResponseTime = {
|
|
374
|
+
p50Ms: rt.p50_ms,
|
|
375
|
+
p95Ms: rt.p95_ms,
|
|
376
|
+
p99Ms: rt.p99_ms,
|
|
377
|
+
sampleCount: rt.sample_count,
|
|
378
|
+
source: rt.source,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (op["x-f5xc-required-fields"]) enrichment.requiredFields = op["x-f5xc-required-fields"];
|
|
382
|
+
if (op["x-f5xc-operation-metadata"]) {
|
|
383
|
+
const om = op["x-f5xc-operation-metadata"] as Record<string, unknown>;
|
|
384
|
+
const mapped: Record<string, unknown> = { purpose: om.purpose };
|
|
385
|
+
if (om.conditions) {
|
|
386
|
+
const cond = om.conditions as Record<string, unknown>;
|
|
387
|
+
if (cond.prerequisites) mapped.prerequisites = cond.prerequisites;
|
|
388
|
+
if (cond.postconditions) mapped.postconditions = cond.postconditions;
|
|
389
|
+
}
|
|
390
|
+
if (om.common_errors) {
|
|
391
|
+
mapped.commonErrors = (om.common_errors as Array<Record<string, unknown>>).map(e => ({
|
|
392
|
+
code: e.code,
|
|
393
|
+
message: e.message,
|
|
394
|
+
resolution: e.resolution ?? e.solution ?? "",
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
if (om.performance_impact) {
|
|
398
|
+
const pi = om.performance_impact as Record<string, unknown>;
|
|
399
|
+
mapped.performanceImpact = { latency: pi.latency, resourceUsage: pi.resource_usage };
|
|
400
|
+
}
|
|
401
|
+
enrichment.operationMetadata = mapped;
|
|
402
|
+
}
|
|
403
|
+
if (Object.keys(enrichment).length > 0) operationMeta[opId] = enrichment;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const schemaEnrichments: Record<string, Record<string, unknown>> = {};
|
|
408
|
+
for (const [schemaName, schemaDef] of Object.entries(enrichSpecJson.components?.schemas ?? {})) {
|
|
409
|
+
const rec = schemaDef["x-f5xc-recommended-oneof-variant"] as Record<string, string> | undefined;
|
|
410
|
+
if (rec) {
|
|
411
|
+
schemaEnrichments[schemaName] = { recommendedOneofVariant: rec };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (Object.keys(operationMeta).length > 0 || Object.keys(schemaEnrichments).length > 0) {
|
|
416
|
+
enrichmentEntries.push(
|
|
417
|
+
`\t${JSON.stringify(entry.domain)}: { operationMeta: ${JSON.stringify(operationMeta)}, schemaEnrichments: ${JSON.stringify(schemaEnrichments)} },`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
298
422
|
const output = [
|
|
299
423
|
"// Auto-generated by scripts/generate-api-spec-index.ts - DO NOT EDIT",
|
|
300
424
|
"",
|
|
301
|
-
`import type { ApiSpecIndex } from "./api-spec-types";`,
|
|
425
|
+
`import type { ApiSpecDomainEnrichments, ApiSpecIndex } from "./api-spec-types";`,
|
|
302
426
|
"",
|
|
303
427
|
`export const API_SPEC_VERSION = ${JSON.stringify(rawIndex.version)};`,
|
|
304
428
|
"",
|
|
@@ -318,6 +442,10 @@ const output = [
|
|
|
318
442
|
...specDataEntries,
|
|
319
443
|
`};`,
|
|
320
444
|
"",
|
|
445
|
+
`export const API_SPEC_ENRICHMENTS: Readonly<Record<string, ApiSpecDomainEnrichments>> = {`,
|
|
446
|
+
...enrichmentEntries,
|
|
447
|
+
`};`,
|
|
448
|
+
"",
|
|
321
449
|
]
|
|
322
450
|
.filter(l => l !== undefined)
|
|
323
451
|
.join("\n");
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ApiSpecDomainEnrichments,
|
|
3
|
+
ApiSpecDomainEntry,
|
|
4
|
+
ApiSpecIndex,
|
|
5
|
+
OpenAPIPathOperation,
|
|
6
|
+
OpenAPISpec,
|
|
7
|
+
} from "./api-spec-types";
|
|
2
8
|
import type { InternalResource, InternalUrl } from "./types";
|
|
3
9
|
|
|
4
10
|
const SCHEMA_RENDER_MAX_DEPTH = 3;
|
|
@@ -21,6 +27,7 @@ export interface ApiSpecResolver {
|
|
|
21
27
|
export function createApiSpecResolver(
|
|
22
28
|
index: ApiSpecIndex,
|
|
23
29
|
data: Readonly<Record<string, OpenAPISpec>>,
|
|
30
|
+
enrichments?: Readonly<Record<string, ApiSpecDomainEnrichments>>,
|
|
24
31
|
): ApiSpecResolver {
|
|
25
32
|
function lookup(domain: string): OpenAPISpec {
|
|
26
33
|
const spec = data[domain];
|
|
@@ -68,7 +75,10 @@ export function createApiSpecResolver(
|
|
|
68
75
|
if (Object.keys(matchingPaths).length === 0) {
|
|
69
76
|
return makeResource(url, renderUnknownResource(resource, entry, spec));
|
|
70
77
|
}
|
|
71
|
-
return makeResource(
|
|
78
|
+
return makeResource(
|
|
79
|
+
url,
|
|
80
|
+
renderResourceSpec(domain, resource, spec, entry, { crudOnly: crud }, enrichments?.[domain]),
|
|
81
|
+
);
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
if (pathFilter) {
|
|
@@ -138,8 +148,9 @@ function renderDomainDetail(domain: string, entry: ApiSpecDomainEntry, spec: Ope
|
|
|
138
148
|
sections.push("", `> ${tags.join(" | ")}`);
|
|
139
149
|
}
|
|
140
150
|
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
const desc = entry.descriptionLong ?? entry.descriptionMedium;
|
|
152
|
+
if (desc) {
|
|
153
|
+
sections.push("", desc);
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
sections.push("", "## Resources", "");
|
|
@@ -181,6 +192,52 @@ function renderDomainDetail(domain: string, entry: ApiSpecDomainEntry, spec: Ope
|
|
|
181
192
|
sections.push("", "## Related Domains", `- ${entry.relatedDomains.join(", ")}`);
|
|
182
193
|
}
|
|
183
194
|
|
|
195
|
+
if (entry.bestPractices) {
|
|
196
|
+
const bp = entry.bestPractices;
|
|
197
|
+
sections.push("", "## Best Practices");
|
|
198
|
+
if (bp.commonErrors.length > 0) {
|
|
199
|
+
sections.push("", "### Common Errors", "");
|
|
200
|
+
sections.push("| Code | Message | Resolution | Prevention |");
|
|
201
|
+
sections.push("|------|---------|------------|------------|");
|
|
202
|
+
for (const e of bp.commonErrors) {
|
|
203
|
+
sections.push(`| ${e.code} | ${e.message} | ${e.resolution} | ${e.prevention} |`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (bp.securityNotes.length > 0) {
|
|
207
|
+
sections.push("", "### Security Notes", ...bp.securityNotes.map(n => `- ${n}`));
|
|
208
|
+
}
|
|
209
|
+
if (bp.performanceTips.length > 0) {
|
|
210
|
+
sections.push("", "### Performance Tips", ...bp.performanceTips.map(t => `- ${t}`));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (entry.cliMetadata?.quickStart?.command) {
|
|
215
|
+
const cli = entry.cliMetadata;
|
|
216
|
+
sections.push("", "## CLI Quick Start", "");
|
|
217
|
+
sections.push(`\`${cli.quickStart.command}\` — ${cli.quickStart.description}`);
|
|
218
|
+
const validWorkflows = cli.commonWorkflows?.filter(wf => wf.name) ?? [];
|
|
219
|
+
if (validWorkflows.length > 0) {
|
|
220
|
+
sections.push("", "### Common Workflows");
|
|
221
|
+
for (const wf of validWorkflows) {
|
|
222
|
+
if (wf.commands?.length) {
|
|
223
|
+
sections.push("", `**${wf.name}:**`);
|
|
224
|
+
for (const cmd of wf.commands) {
|
|
225
|
+
sections.push(`- \`${cmd}\``);
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
sections.push(`- ${wf.name}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const validTroubleshooting = cli.troubleshooting?.filter(ts => ts.symptom) ?? [];
|
|
233
|
+
if (validTroubleshooting.length > 0) {
|
|
234
|
+
sections.push("", "### Troubleshooting");
|
|
235
|
+
for (const ts of validTroubleshooting) {
|
|
236
|
+
sections.push(`- **${ts.symptom}:** ${ts.fix}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
184
241
|
sections.push("", `Read \`xcsh://api-spec/${domain}?resource={name}\` for full endpoint specification.`, "");
|
|
185
242
|
|
|
186
243
|
return sections.join("\n");
|
|
@@ -297,6 +354,7 @@ function renderResourceSpec(
|
|
|
297
354
|
spec: OpenAPISpec,
|
|
298
355
|
entry?: ApiSpecDomainEntry,
|
|
299
356
|
options?: { crudOnly?: boolean },
|
|
357
|
+
domainEnrichments?: ApiSpecDomainEnrichments,
|
|
300
358
|
): string {
|
|
301
359
|
const matchingPaths = filterPathsByResource(spec, resource, entry);
|
|
302
360
|
const label = options?.crudOnly ? "CRUD Operations" : "Full API Specification";
|
|
@@ -313,6 +371,52 @@ function renderResourceSpec(
|
|
|
313
371
|
sections.push(`## ${method.toUpperCase()} ${pathKey}`, "");
|
|
314
372
|
if (operation.summary) sections.push(String(operation.summary), "");
|
|
315
373
|
|
|
374
|
+
const opEnrichment = domainEnrichments?.operationMeta[operation.operationId ?? ""];
|
|
375
|
+
if (opEnrichment) {
|
|
376
|
+
const badges: string[] = [];
|
|
377
|
+
if (opEnrichment.dangerLevel) badges.push(`Danger: **${opEnrichment.dangerLevel}**`);
|
|
378
|
+
if (opEnrichment.confirmationRequired) badges.push("**Confirmation required**");
|
|
379
|
+
if (badges.length > 0) sections.push(badges.join(" | "), "");
|
|
380
|
+
|
|
381
|
+
if (opEnrichment.sideEffects) {
|
|
382
|
+
const fx = opEnrichment.sideEffects;
|
|
383
|
+
const parts: string[] = [];
|
|
384
|
+
if (fx.creates?.length) parts.push(`Creates: ${fx.creates.join(", ")}`);
|
|
385
|
+
if (fx.deletes?.length) parts.push(`Deletes: ${fx.deletes.join(", ")}`);
|
|
386
|
+
if (fx.modifies?.length) parts.push(`Modifies: ${fx.modifies.join(", ")}`);
|
|
387
|
+
if (parts.length > 0) sections.push(`Side effects: ${parts.join("; ")}`, "");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (opEnrichment.discoveredResponseTime) {
|
|
391
|
+
const rt = opEnrichment.discoveredResponseTime;
|
|
392
|
+
sections.push(`Response time: p50=${rt.p50Ms}ms, p95=${rt.p95Ms}ms, p99=${rt.p99Ms}ms`, "");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (opEnrichment.requiredFields?.length) {
|
|
396
|
+
sections.push(`Required fields: ${opEnrichment.requiredFields.join(", ")}`, "");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (opEnrichment.operationMetadata) {
|
|
400
|
+
const meta = opEnrichment.operationMetadata;
|
|
401
|
+
if (meta.prerequisites?.length) {
|
|
402
|
+
sections.push("**Prerequisites:**", ...meta.prerequisites.map(p => `- ${p}`), "");
|
|
403
|
+
}
|
|
404
|
+
if (meta.commonErrors?.length) {
|
|
405
|
+
sections.push("**Common Errors:**");
|
|
406
|
+
for (const err of meta.commonErrors) {
|
|
407
|
+
sections.push(`- ${err.code}: ${err.message} — ${err.resolution}`);
|
|
408
|
+
}
|
|
409
|
+
sections.push("");
|
|
410
|
+
}
|
|
411
|
+
if (meta.performanceImpact) {
|
|
412
|
+
sections.push(
|
|
413
|
+
`Performance: ${meta.performanceImpact.latency}, ${meta.performanceImpact.resourceUsage}`,
|
|
414
|
+
"",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
316
420
|
const params = operation.parameters;
|
|
317
421
|
if (params?.length) {
|
|
318
422
|
sections.push("### Parameters", "");
|
|
@@ -333,10 +437,13 @@ function renderResourceSpec(
|
|
|
333
437
|
const content = reqBody.content as Record<string, Record<string, unknown>> | undefined;
|
|
334
438
|
const jsonContent = content?.["application/json"];
|
|
335
439
|
if (jsonContent?.schema) {
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
440
|
+
const rawSchema = jsonContent.schema as Record<string, unknown>;
|
|
441
|
+
const schema = resolveSchemaRef(rawSchema, spec);
|
|
442
|
+
const schemaName = extractSchemaName(rawSchema);
|
|
443
|
+
const schemaRec = schemaName
|
|
444
|
+
? domainEnrichments?.schemaEnrichments[schemaName]?.recommendedOneofVariant
|
|
445
|
+
: undefined;
|
|
446
|
+
sections.push(renderSchemaAsTable(schema, spec, 0, "", schemaRec));
|
|
340
447
|
}
|
|
341
448
|
}
|
|
342
449
|
|
|
@@ -350,7 +457,8 @@ function renderResourceSpec(
|
|
|
350
457
|
const jsonResp = respContent?.["application/json"];
|
|
351
458
|
if (jsonResp?.schema) {
|
|
352
459
|
sections.push(`### Response ${status}`, "");
|
|
353
|
-
const
|
|
460
|
+
const rawRespSchema = jsonResp.schema as Record<string, unknown>;
|
|
461
|
+
const schema = resolveSchemaRef(rawRespSchema, spec);
|
|
354
462
|
sections.push(renderSchemaAsTable(schema, spec));
|
|
355
463
|
}
|
|
356
464
|
}
|
|
@@ -385,7 +493,17 @@ function renderPathSpec(_domain: string, pathKey: string, spec: OpenAPISpec): st
|
|
|
385
493
|
}
|
|
386
494
|
|
|
387
495
|
function resolveSchemaRef(schema: Record<string, unknown>, spec: OpenAPISpec): Record<string, unknown> {
|
|
388
|
-
|
|
496
|
+
let ref = schema.$ref as string | undefined;
|
|
497
|
+
|
|
498
|
+
if (!ref && Array.isArray(schema.allOf)) {
|
|
499
|
+
for (const item of schema.allOf as Record<string, unknown>[]) {
|
|
500
|
+
if (typeof item === "object" && item !== null && typeof item.$ref === "string") {
|
|
501
|
+
ref = item.$ref as string;
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
389
507
|
if (!ref) return schema;
|
|
390
508
|
|
|
391
509
|
const match = ref.match(/^#\/components\/schemas\/(.+)$/);
|
|
@@ -393,7 +511,35 @@ function resolveSchemaRef(schema: Record<string, unknown>, spec: OpenAPISpec): R
|
|
|
393
511
|
|
|
394
512
|
const schemaName = match[1];
|
|
395
513
|
const resolved = spec.components?.schemas?.[schemaName];
|
|
396
|
-
|
|
514
|
+
if (!resolved) return schema;
|
|
515
|
+
|
|
516
|
+
const siblings: Record<string, unknown> = {};
|
|
517
|
+
for (const [key, val] of Object.entries(schema)) {
|
|
518
|
+
if (key !== "$ref" && key !== "allOf") {
|
|
519
|
+
siblings[key] = val;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (Object.keys(siblings).length > 0) {
|
|
524
|
+
return { ...(resolved as Record<string, unknown>), ...siblings };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return resolved as Record<string, unknown>;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function extractSchemaName(schema: Record<string, unknown>): string | null {
|
|
531
|
+
let ref = schema.$ref as string | undefined;
|
|
532
|
+
if (!ref && Array.isArray(schema.allOf)) {
|
|
533
|
+
for (const item of schema.allOf as Record<string, unknown>[]) {
|
|
534
|
+
if (typeof item === "object" && item !== null && typeof item.$ref === "string") {
|
|
535
|
+
ref = item.$ref as string;
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!ref) return null;
|
|
541
|
+
const match = ref.match(/^#\/components\/schemas\/(.+)$/);
|
|
542
|
+
return match ? match[1] : null;
|
|
397
543
|
}
|
|
398
544
|
|
|
399
545
|
function formatFieldConstraints(prop: Record<string, unknown>): string {
|
|
@@ -434,19 +580,27 @@ function parseOneOfOptions(val: unknown): string[] {
|
|
|
434
580
|
return [String(val)];
|
|
435
581
|
}
|
|
436
582
|
|
|
437
|
-
function renderOneOfGroups(schema: Record<string, unknown
|
|
583
|
+
function renderOneOfGroups(schema: Record<string, unknown>, recommended?: Readonly<Record<string, string>>): string {
|
|
438
584
|
const groups: string[] = [];
|
|
439
585
|
for (const [key, val] of Object.entries(schema)) {
|
|
440
586
|
if (!key.startsWith("x-ves-oneof-field-")) continue;
|
|
441
587
|
const groupName = key.slice("x-ves-oneof-field-".length);
|
|
442
588
|
const options = parseOneOfOptions(val).join(" | ");
|
|
443
|
-
|
|
589
|
+
const rec = recommended?.[groupName];
|
|
590
|
+
const recStr = rec ? ` (recommended: **${rec}**)` : "";
|
|
591
|
+
groups.push(`- **${groupName}**: ${options}${recStr}`);
|
|
444
592
|
}
|
|
445
593
|
if (groups.length === 0) return "";
|
|
446
594
|
return ["**Mutually exclusive — choose one per group:**", ...groups, ""].join("\n");
|
|
447
595
|
}
|
|
448
596
|
|
|
449
|
-
function renderSchemaAsTable(
|
|
597
|
+
function renderSchemaAsTable(
|
|
598
|
+
schema: Record<string, unknown>,
|
|
599
|
+
spec: OpenAPISpec,
|
|
600
|
+
depth = 0,
|
|
601
|
+
prefix = "",
|
|
602
|
+
schemaRecommended?: Readonly<Record<string, string>>,
|
|
603
|
+
): string {
|
|
450
604
|
if (depth > SCHEMA_RENDER_MAX_DEPTH) return "";
|
|
451
605
|
|
|
452
606
|
const resolved = resolveSchemaRef(schema, spec);
|
|
@@ -459,11 +613,11 @@ function renderSchemaAsTable(schema: Record<string, unknown>, spec: OpenAPISpec,
|
|
|
459
613
|
const required = (resolved.required as string[]) ?? [];
|
|
460
614
|
const rows: string[] = [];
|
|
461
615
|
|
|
462
|
-
const oneOfStr = renderOneOfGroups(resolved);
|
|
616
|
+
const oneOfStr = renderOneOfGroups(resolved, schemaRecommended);
|
|
463
617
|
if (depth === 0) {
|
|
464
618
|
if (oneOfStr) rows.push(oneOfStr);
|
|
465
|
-
rows.push("| Field | Type | Required | Constraints | Example | Description |");
|
|
466
|
-
rows.push("
|
|
619
|
+
rows.push("| Field | Type | Required | Default | Constraints | Example | Description |");
|
|
620
|
+
rows.push("|-------|------|----------|---------|-------------|---------|-------------|");
|
|
467
621
|
}
|
|
468
622
|
|
|
469
623
|
for (const [name, prop] of Object.entries(properties)) {
|
|
@@ -476,12 +630,42 @@ function renderSchemaAsTable(schema: Record<string, unknown>, spec: OpenAPISpec,
|
|
|
476
630
|
const rawExample = (fieldProp["x-ves-example"] as string) ?? (fieldProp["x-f5xc-example"] as string) ?? "";
|
|
477
631
|
const example = rawExample.length > 40 ? `${rawExample.slice(0, 37)}…` : rawExample;
|
|
478
632
|
|
|
479
|
-
|
|
633
|
+
const serverDefault = fieldProp["x-f5xc-server-default"];
|
|
634
|
+
const recommendedValue = fieldProp["x-f5xc-recommended-value"];
|
|
635
|
+
let defaultCol = "";
|
|
636
|
+
if (serverDefault != null) defaultCol = `${serverDefault} (server)`;
|
|
637
|
+
else if (recommendedValue != null) defaultCol = `${recommendedValue} (rec)`;
|
|
638
|
+
|
|
639
|
+
const reqFor = fieldProp["x-f5xc-required-for"] as Record<string, boolean> | undefined;
|
|
640
|
+
let reqStr = isRequired;
|
|
641
|
+
if (reqFor) {
|
|
642
|
+
const ops = Object.entries(reqFor)
|
|
643
|
+
.filter(([, v]) => v)
|
|
644
|
+
.map(([k]) => k);
|
|
645
|
+
if (ops.length > 0) reqStr = ops.join(", ");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
rows.push(`| ${fieldName} | ${type} | ${reqStr} | ${defaultCol} | ${constraints} | ${example} | ${desc} |`);
|
|
649
|
+
|
|
650
|
+
const conflictsWith = fieldProp["x-f5xc-conflicts-with"] as string[] | undefined;
|
|
651
|
+
if (conflictsWith?.length) {
|
|
652
|
+
rows.push(`| └─ ${fieldName} | | **conflicts with** ${conflictsWith.join(", ")} | | | | |`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const requires = fieldProp["x-f5xc-requires"] as
|
|
656
|
+
| Array<{ field: string; required?: boolean; reason?: string; min_items?: number }>
|
|
657
|
+
| undefined;
|
|
658
|
+
if (requires?.length) {
|
|
659
|
+
for (const dep of requires) {
|
|
660
|
+
const note = dep.min_items != null ? ` (min: ${dep.min_items})` : "";
|
|
661
|
+
rows.push(`| └─ ${fieldName} | | **requires** ${dep.field}${note} | | ${dep.reason ?? ""} | | |`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
480
664
|
|
|
481
665
|
if (type === "object" && fieldProp.properties && depth < SCHEMA_RENDER_MAX_DEPTH) {
|
|
482
|
-
const nestedOneOf = renderOneOfGroups(fieldProp);
|
|
666
|
+
const nestedOneOf = renderOneOfGroups(fieldProp, schemaRecommended);
|
|
483
667
|
if (nestedOneOf) rows.push("", nestedOneOf);
|
|
484
|
-
const nested = renderSchemaAsTable(fieldProp, spec, depth + 1, fieldName);
|
|
668
|
+
const nested = renderSchemaAsTable(fieldProp, spec, depth + 1, fieldName, schemaRecommended);
|
|
485
669
|
const nestedLines = nested.split("\n").filter(l => l.startsWith("|") && !l.startsWith("| Field"));
|
|
486
670
|
rows.push(...nestedLines);
|
|
487
671
|
}
|
|
@@ -23,6 +23,34 @@ export interface ApiSpecDomainResource {
|
|
|
23
23
|
readonly catalogCategories?: readonly string[];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface ApiSpecCliMetadata {
|
|
27
|
+
readonly quickStart: {
|
|
28
|
+
readonly command: string;
|
|
29
|
+
readonly description: string;
|
|
30
|
+
readonly expectedOutput: string;
|
|
31
|
+
};
|
|
32
|
+
readonly commonWorkflows: readonly {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly commands?: readonly string[];
|
|
35
|
+
}[];
|
|
36
|
+
readonly troubleshooting: readonly {
|
|
37
|
+
readonly symptom?: string;
|
|
38
|
+
readonly fix?: string;
|
|
39
|
+
}[];
|
|
40
|
+
readonly icon?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ApiSpecBestPractices {
|
|
44
|
+
readonly commonErrors: readonly {
|
|
45
|
+
readonly code: number;
|
|
46
|
+
readonly message: string;
|
|
47
|
+
readonly resolution: string;
|
|
48
|
+
readonly prevention: string;
|
|
49
|
+
}[];
|
|
50
|
+
readonly securityNotes: readonly string[];
|
|
51
|
+
readonly performanceTips: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
export interface ApiSpecDomainEntry {
|
|
27
55
|
readonly domain: string;
|
|
28
56
|
readonly title: string;
|
|
@@ -39,6 +67,12 @@ export interface ApiSpecDomainEntry {
|
|
|
39
67
|
readonly descriptionMedium?: string;
|
|
40
68
|
readonly isPreview?: boolean;
|
|
41
69
|
readonly requiresTier?: string;
|
|
70
|
+
readonly descriptionLong?: string;
|
|
71
|
+
readonly summary?: string;
|
|
72
|
+
readonly logoSvg?: string;
|
|
73
|
+
readonly cliDomain?: string;
|
|
74
|
+
readonly cliMetadata?: ApiSpecCliMetadata;
|
|
75
|
+
readonly bestPractices?: ApiSpecBestPractices;
|
|
42
76
|
}
|
|
43
77
|
|
|
44
78
|
export interface ApiSpecGuidedWorkflows {
|
|
@@ -111,6 +145,47 @@ export interface ApiSpecAcronyms {
|
|
|
111
145
|
readonly acronyms: readonly ApiSpecAcronym[];
|
|
112
146
|
}
|
|
113
147
|
|
|
148
|
+
export interface ApiSpecOperationEnrichment {
|
|
149
|
+
readonly dangerLevel?: "low" | "medium" | "high";
|
|
150
|
+
readonly confirmationRequired?: boolean;
|
|
151
|
+
readonly sideEffects?: {
|
|
152
|
+
readonly creates?: readonly string[];
|
|
153
|
+
readonly deletes?: readonly string[];
|
|
154
|
+
readonly modifies?: readonly string[];
|
|
155
|
+
};
|
|
156
|
+
readonly discoveredResponseTime?: {
|
|
157
|
+
readonly p50Ms: number;
|
|
158
|
+
readonly p95Ms: number;
|
|
159
|
+
readonly p99Ms: number;
|
|
160
|
+
readonly sampleCount: number;
|
|
161
|
+
readonly source: string;
|
|
162
|
+
};
|
|
163
|
+
readonly requiredFields?: readonly string[];
|
|
164
|
+
readonly operationMetadata?: {
|
|
165
|
+
readonly purpose: string;
|
|
166
|
+
readonly prerequisites?: readonly string[];
|
|
167
|
+
readonly postconditions?: readonly string[];
|
|
168
|
+
readonly commonErrors?: readonly {
|
|
169
|
+
readonly code: number;
|
|
170
|
+
readonly message: string;
|
|
171
|
+
readonly resolution: string;
|
|
172
|
+
}[];
|
|
173
|
+
readonly performanceImpact?: {
|
|
174
|
+
readonly latency: string;
|
|
175
|
+
readonly resourceUsage: string;
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface ApiSpecSchemaEnrichment {
|
|
181
|
+
readonly recommendedOneofVariant?: Readonly<Record<string, string>>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface ApiSpecDomainEnrichments {
|
|
185
|
+
readonly operationMeta: Readonly<Record<string, ApiSpecOperationEnrichment>>;
|
|
186
|
+
readonly schemaEnrichments: Readonly<Record<string, ApiSpecSchemaEnrichment>>;
|
|
187
|
+
}
|
|
188
|
+
|
|
114
189
|
export interface ApiSpecIndex {
|
|
115
190
|
readonly version: string;
|
|
116
191
|
readonly timestamp: string;
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "18.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.65.0",
|
|
21
|
+
"commit": "35c77b050317137efe631106d543647195229000",
|
|
22
|
+
"shortCommit": "35c77b0",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
24
|
+
"tag": "v18.65.0",
|
|
25
|
+
"commitDate": "2026-05-17T08:48:25Z",
|
|
26
|
+
"buildDate": "2026-05-17T09:14:22.007Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
|
|
30
30
|
"repoSlug": "f5xc-salesdemos/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/35c77b050317137efe631106d543647195229000",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.65.0"
|
|
33
33
|
};
|
|
@@ -25,7 +25,7 @@ import type { ContextStatus } from "../services/f5xc-context";
|
|
|
25
25
|
import { type ApiCatalogResolver, createApiCatalogResolver } from "./api-catalog-resolve";
|
|
26
26
|
import type { ApiCatalogCategory, ApiCatalogCategorySummary, ApiCatalogIndex } from "./api-catalog-types";
|
|
27
27
|
import { type ApiSpecResolver, createApiSpecResolver } from "./api-spec-resolve";
|
|
28
|
-
import type { ApiSpecIndex, OpenAPISpec } from "./api-spec-types";
|
|
28
|
+
import type { ApiSpecDomainEnrichments, ApiSpecIndex, OpenAPISpec } from "./api-spec-types";
|
|
29
29
|
import { getRuntimeBuildInfo, type RuntimeBuildInfo, renderAboutDoc } from "./build-info-runtime";
|
|
30
30
|
import { loadComputerProfile, renderComputerProfileMarkdown, seedComputerProfile } from "./computer-profile";
|
|
31
31
|
import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
|
|
@@ -51,15 +51,26 @@ const EMPTY_CATALOG_INDEX: ApiCatalogIndex = {
|
|
|
51
51
|
defaults: {},
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
let _apiSpecCache: {
|
|
54
|
+
let _apiSpecCache: {
|
|
55
|
+
index: ApiSpecIndex;
|
|
56
|
+
data: Readonly<Record<string, OpenAPISpec>>;
|
|
57
|
+
enrichments: Readonly<Record<string, ApiSpecDomainEnrichments>>;
|
|
58
|
+
version: string;
|
|
59
|
+
} | null = null;
|
|
55
60
|
|
|
56
|
-
function loadApiSpecs(): {
|
|
61
|
+
function loadApiSpecs(): {
|
|
62
|
+
index: ApiSpecIndex;
|
|
63
|
+
data: Readonly<Record<string, OpenAPISpec>>;
|
|
64
|
+
enrichments: Readonly<Record<string, ApiSpecDomainEnrichments>>;
|
|
65
|
+
version: string;
|
|
66
|
+
} {
|
|
57
67
|
if (_apiSpecCache) return _apiSpecCache;
|
|
58
68
|
try {
|
|
59
69
|
const mod = require("./api-spec-index.generated") as {
|
|
60
70
|
API_SPEC_INDEX?: ApiSpecIndex;
|
|
61
71
|
API_SPEC_DATA?: Readonly<Record<string, unknown>>;
|
|
62
72
|
API_SPEC_VERSION?: string;
|
|
73
|
+
API_SPEC_ENRICHMENTS?: Readonly<Record<string, ApiSpecDomainEnrichments>>;
|
|
63
74
|
};
|
|
64
75
|
const index = mod.API_SPEC_INDEX ?? EMPTY_INDEX;
|
|
65
76
|
const version = mod.API_SPEC_VERSION ?? "unknown";
|
|
@@ -69,13 +80,14 @@ function loadApiSpecs(): { index: ApiSpecIndex; data: Readonly<Record<string, Op
|
|
|
69
80
|
_apiSpecCache = {
|
|
70
81
|
index,
|
|
71
82
|
data: (mod.API_SPEC_DATA ?? {}) as Readonly<Record<string, OpenAPISpec>>,
|
|
83
|
+
enrichments: mod.API_SPEC_ENRICHMENTS ?? {},
|
|
72
84
|
version,
|
|
73
85
|
};
|
|
74
86
|
} catch (err) {
|
|
75
87
|
logger.warn("api-spec index unavailable, embedded specs disabled", {
|
|
76
88
|
error: err instanceof Error ? err.message : String(err),
|
|
77
89
|
});
|
|
78
|
-
_apiSpecCache = { index: EMPTY_INDEX, data: {}, version: "unavailable" };
|
|
90
|
+
_apiSpecCache = { index: EMPTY_INDEX, data: {}, enrichments: {}, version: "unavailable" };
|
|
79
91
|
}
|
|
80
92
|
return _apiSpecCache;
|
|
81
93
|
}
|
|
@@ -141,7 +153,7 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
141
153
|
#getApiSpecResolver(): ApiSpecResolver {
|
|
142
154
|
if (!this.#apiSpecResolver) {
|
|
143
155
|
const specs = loadApiSpecs();
|
|
144
|
-
this.#apiSpecResolver = createApiSpecResolver(specs.index, specs.data);
|
|
156
|
+
this.#apiSpecResolver = createApiSpecResolver(specs.index, specs.data, specs.enrichments);
|
|
145
157
|
}
|
|
146
158
|
return this.#apiSpecResolver;
|
|
147
159
|
}
|