@f5xc-salesdemos/xcsh 19.30.4 → 19.31.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "19.30.4",
4
+ "version": "19.31.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",
@@ -50,13 +50,13 @@
50
50
  "dependencies": {
51
51
  "@agentclientprotocol/sdk": "0.16.1",
52
52
  "@mozilla/readability": "^0.6",
53
- "@f5xc-salesdemos/xcsh-stats": "19.30.4",
54
- "@f5xc-salesdemos/pi-agent-core": "19.30.4",
55
- "@f5xc-salesdemos/pi-ai": "19.30.4",
56
- "@f5xc-salesdemos/pi-natives": "19.30.4",
57
- "@f5xc-salesdemos/pi-resource-management": "19.30.4",
58
- "@f5xc-salesdemos/pi-tui": "19.30.4",
59
- "@f5xc-salesdemos/pi-utils": "19.30.4",
53
+ "@f5xc-salesdemos/xcsh-stats": "19.31.0",
54
+ "@f5xc-salesdemos/pi-agent-core": "19.31.0",
55
+ "@f5xc-salesdemos/pi-ai": "19.31.0",
56
+ "@f5xc-salesdemos/pi-natives": "19.31.0",
57
+ "@f5xc-salesdemos/pi-resource-management": "19.31.0",
58
+ "@f5xc-salesdemos/pi-tui": "19.31.0",
59
+ "@f5xc-salesdemos/pi-utils": "19.31.0",
60
60
  "@sinclair/typebox": "^0.34",
61
61
  "@xterm/headless": "^6.0",
62
62
  "ajv": "^8.20",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.30.4",
21
- "commit": "4a5bc4f09ff018233f64855d9db988cb328ee146",
22
- "shortCommit": "4a5bc4f",
20
+ "version": "19.31.0",
21
+ "commit": "f162b28f59b6db856a5c429de1dc9041767fc341",
22
+ "shortCommit": "f162b28",
23
23
  "branch": "main",
24
- "tag": "v19.30.4",
25
- "commitDate": "2026-06-15T02:13:30Z",
26
- "buildDate": "2026-06-15T02:43:45.871Z",
24
+ "tag": "v19.31.0",
25
+ "commitDate": "2026-06-15T02:42:18Z",
26
+ "buildDate": "2026-06-15T03:07:55.063Z",
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/4a5bc4f09ff018233f64855d9db988cb328ee146",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.30.4"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/f162b28f59b6db856a5c429de1dc9041767fc341",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.31.0"
33
33
  };
@@ -185,6 +185,32 @@ Use these values when constructing API payloads and resource names.
185
185
  Available F5 XC documentation topics: {{knowledgeTopics}}.
186
186
  {{/if}}
187
187
  {{/if}}
188
+
189
+ ## Resource Manifest Format
190
+
191
+ When a user asks you to write, export, or save a resource manifest, produce a clean `{kind, metadata, spec}` JSON file that is compatible with the `/apply` slash command.
192
+
193
+ **Format:**
194
+ ```json
195
+ {
196
+ "kind": "<resource_kind>",
197
+ "metadata": {
198
+ "name": "<resource_name>",
199
+ "namespace": "<namespace>"
200
+ },
201
+ "spec": { ... }
202
+ }
203
+ ```
204
+
205
+ **Rules:**
206
+ - `kind` — the resource type in snake_case (e.g., `http_loadbalancer`, `origin_pool`, `app_firewall`)
207
+ - `metadata` — include only: `name`, `namespace`, `labels`, `annotations`, `description`, `disable`
208
+ - `spec` — include the full spec from the API response
209
+ - **Exclude:** `system_metadata`, `status`, and any other server-managed fields
210
+ - The output must round-trip through `/apply -f` without errors
211
+
212
+ When fetching a resource from the API, strip the server-added metadata fields and inject the `kind` field (which the API response does not include). The `/manifest` slash command does this automatically.
213
+
188
214
  {{#if userProfile}}
189
215
  ## Primary Human
190
216
 
@@ -1475,6 +1475,24 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1475
1475
  await handleResourceCommand("get", command, runtime.ctx);
1476
1476
  },
1477
1477
  },
1478
+ {
1479
+ name: "manifest",
1480
+ description: "Export live F5 XC resources as {kind, metadata, spec} manifest files",
1481
+ inlineHint: "<kind> [name] [-n namespace] [-o json|yaml] [-f output-path] [--all]",
1482
+ allowArgs: true,
1483
+ getArgumentCompletions(prefix: string) {
1484
+ try {
1485
+ const { getExportKindCompletions } = require("./export-command") as typeof import("./export-command");
1486
+ return getExportKindCompletions(prefix);
1487
+ } catch {
1488
+ return null;
1489
+ }
1490
+ },
1491
+ handle: async (command, runtime) => {
1492
+ const { handleExportResourceCommand } = await import("./export-command");
1493
+ await handleExportResourceCommand(command, runtime.ctx);
1494
+ },
1495
+ },
1478
1496
  {
1479
1497
  name: "context",
1480
1498
  description: t("commands.context.description"),
@@ -0,0 +1,170 @@
1
+ import type { AutocompleteItem } from "@f5xc-salesdemos/pi-tui";
2
+ import type { InteractiveModeContext } from "../modes/types";
3
+
4
+ interface ParsedBuiltinSlashCommand {
5
+ name: string;
6
+ args: string;
7
+ text: string;
8
+ }
9
+
10
+ function getExportKindCompletions(prefix: string): AutocompleteItem[] | null {
11
+ try {
12
+ const { kindResolver } = require("../resource-management/index") as typeof import("../resource-management/index");
13
+ const kinds = kindResolver.getKindsWithApiPaths();
14
+ const lower = prefix.toLowerCase();
15
+ const items = kinds
16
+ .filter(k => k.toLowerCase().startsWith(lower))
17
+ .slice(0, 20)
18
+ .map(k => ({ value: `${k} `, label: k }));
19
+ return items.length > 0 ? items : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ export async function handleExportResourceCommand(
26
+ command: ParsedBuiltinSlashCommand,
27
+ ctx: InteractiveModeContext,
28
+ ): Promise<void> {
29
+ ctx.editor.addToHistory(command.text);
30
+ ctx.editor.setText("");
31
+
32
+ const { parseExportArgs, ResourceClient, KindResolutionError, toManifest, formatManifestOutput } = await import(
33
+ "@f5xc-salesdemos/pi-resource-management"
34
+ );
35
+ const { kindResolver } = await import("../resource-management/index");
36
+
37
+ const parsed = parseExportArgs(command.args);
38
+ if ("error" in parsed) {
39
+ ctx.showStatus(parsed.error);
40
+ return;
41
+ }
42
+
43
+ const { ContextService } = await import("../services/f5xc-context");
44
+ const { createContextEnv } = await import("../services/context-env");
45
+ const { settings } = await import("../config/settings");
46
+
47
+ let svc: typeof ContextService.prototype;
48
+ try {
49
+ svc = ContextService.instance;
50
+ } catch {
51
+ ctx.showStatus("No F5 XC context active. Run /context create to configure one first.");
52
+ return;
53
+ }
54
+
55
+ const status = svc.getStatus();
56
+ if (!status.isConfigured) {
57
+ ctx.showStatus("No F5 XC context active. Run /context create to configure one first.");
58
+ return;
59
+ }
60
+
61
+ const contextEnv = createContextEnv(settings);
62
+ const apiUrl = contextEnv.get("F5XC_API_URL");
63
+ const apiToken = contextEnv.get("F5XC_API_TOKEN");
64
+ const defaultNamespace = contextEnv.get("F5XC_NAMESPACE") ?? "";
65
+
66
+ if (!apiUrl || !apiToken) {
67
+ ctx.showStatus("Missing API credentials. Check your context configuration.");
68
+ return;
69
+ }
70
+
71
+ const ns = parsed.namespace ?? defaultNamespace;
72
+ const client = new ResourceClient({
73
+ apiUrl,
74
+ apiToken,
75
+ namespace: ns,
76
+ });
77
+
78
+ if (parsed.outputFormat === "hcl") {
79
+ ctx.showStatus(
80
+ 'HCL export requires AI-assisted transformation. Use a conversational request instead:\n "Export my-lb as Terraform HCL"',
81
+ );
82
+ return;
83
+ }
84
+
85
+ const fmt = parsed.outputFormat as "json" | "yaml";
86
+
87
+ try {
88
+ const manifests: Array<{ kind: string; metadata: Record<string, unknown>; spec: Record<string, unknown> }> = [];
89
+
90
+ if (parsed.all) {
91
+ const result = await client.exportAll(kindResolver, ns, (kind, count) => {
92
+ ctx.showStatus(`Exporting ${kind} (${count} found)...`);
93
+ });
94
+
95
+ for (const err of result.errors) {
96
+ ctx.showStatus(`Warning: ${err.kind}: ${err.error.message}`);
97
+ }
98
+
99
+ manifests.push(...result.manifests);
100
+ } else if (parsed.kind && parsed.name) {
101
+ const resolved = kindResolver.resolveKind(parsed.kind);
102
+ const result = await client.exportOne(parsed.kind, resolved, parsed.name, ns);
103
+ if (result.error) {
104
+ ctx.showStatus(`Error: ${result.error.message}`);
105
+ return;
106
+ }
107
+ manifests.push(result.manifest!);
108
+ } else if (parsed.kind) {
109
+ const resolved = kindResolver.resolveKind(parsed.kind);
110
+ const getResult = await client.get(resolved, undefined, ns);
111
+ if (getResult.error) {
112
+ ctx.showStatus(`Error: ${getResult.error.message}`);
113
+ return;
114
+ }
115
+ if (!getResult.items || getResult.items.length === 0) {
116
+ ctx.showStatus(`No ${parsed.kind} resources found.`);
117
+ return;
118
+ }
119
+ for (const item of getResult.items) {
120
+ manifests.push(toManifest(item, parsed.kind));
121
+ }
122
+ }
123
+
124
+ if (manifests.length === 0) {
125
+ ctx.showStatus("No resources found to export.");
126
+ return;
127
+ }
128
+
129
+ const output = formatManifestOutput(manifests, fmt);
130
+
131
+ if (parsed.outputFile) {
132
+ const { writeFile, mkdir } = await import("node:fs/promises");
133
+ const { dirname, resolve, sep } = await import("node:path");
134
+ const isDir = parsed.outputFile.endsWith("/");
135
+
136
+ if (isDir) {
137
+ await mkdir(parsed.outputFile, { recursive: true });
138
+ const baseDir = resolve(parsed.outputFile);
139
+ const ext = fmt === "yaml" ? "yaml" : "json";
140
+ for (const m of manifests) {
141
+ const safeKind = m.kind.replace(/[/\\]/g, "_");
142
+ const safeName = ((m.metadata.name as string) ?? "unknown").replace(/[/\\]/g, "_");
143
+ const filename = `${safeKind}-${safeName}.${ext}`;
144
+ const filepath = resolve(parsed.outputFile, filename);
145
+ if (!filepath.startsWith(baseDir + sep) && filepath !== baseDir) {
146
+ ctx.showStatus(`Skipping unsafe resource name: ${m.metadata.name}`);
147
+ continue;
148
+ }
149
+ const content = formatManifestOutput([m], fmt);
150
+ await writeFile(filepath, content, "utf-8");
151
+ }
152
+ ctx.showStatus(`Exported ${manifests.length} resource(s) to ${parsed.outputFile}`);
153
+ } else {
154
+ await mkdir(dirname(parsed.outputFile), { recursive: true });
155
+ await writeFile(parsed.outputFile, output, "utf-8");
156
+ ctx.showStatus(`Exported ${manifests.length} resource(s) to ${parsed.outputFile}`);
157
+ }
158
+ } else {
159
+ ctx.showStatus(output);
160
+ }
161
+ } catch (err) {
162
+ if (err instanceof KindResolutionError) {
163
+ ctx.showStatus(`Error: ${err.message}`);
164
+ } else {
165
+ ctx.showStatus(`Unexpected error: ${(err as Error).message}`);
166
+ }
167
+ }
168
+ }
169
+
170
+ export { getExportKindCompletions };