@f5-sales-demo/xcsh 20.3.2 → 20.4.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +8 -8
  3. package/src/cli/resource-cli.ts +134 -0
  4. package/src/cli/sandbox-check.ts +84 -21
  5. package/src/cli/update-cli.ts +1 -1
  6. package/src/cli.ts +8 -0
  7. package/src/commands/apply.ts +21 -0
  8. package/src/commands/create.ts +21 -0
  9. package/src/commands/delete.ts +32 -0
  10. package/src/commands/diff.ts +20 -0
  11. package/src/commands/export.ts +36 -0
  12. package/src/commands/get.ts +30 -0
  13. package/src/commands/resource-flags.ts +30 -0
  14. package/src/commands/self-update.ts +18 -0
  15. package/src/commands/update.ts +14 -14
  16. package/src/commands/validate.ts +20 -0
  17. package/src/internal-urls/build-info.generated.ts +8 -8
  18. package/src/locales/ar.json +1 -1
  19. package/src/locales/de.json +1 -1
  20. package/src/locales/en.json +1 -1
  21. package/src/locales/es.json +1 -1
  22. package/src/locales/fr.json +1 -1
  23. package/src/locales/hi.json +1 -1
  24. package/src/locales/it.json +1 -1
  25. package/src/locales/ja.json +1 -1
  26. package/src/locales/ko.json +1 -1
  27. package/src/locales/pt-br.json +1 -1
  28. package/src/locales/th.json +1 -1
  29. package/src/locales/zh-cn.json +1 -1
  30. package/src/locales/zh-tw.json +1 -1
  31. package/src/main.ts +1 -1
  32. package/src/prompts/internal-urls/containment.md +16 -17
  33. package/src/resource-management/index.ts +8 -0
  34. package/src/sandbox/command-operands.ts +20 -290
  35. package/src/sandbox/containment.ts +41 -42
  36. package/src/sandbox/enforce.ts +45 -280
  37. package/src/sandbox/session-fence.ts +7 -0
  38. package/src/slash-commands/resource-commands.ts +63 -93
  39. package/src/tools/bash.ts +9 -3
  40. package/src/tools/shell-lex.ts +8 -10
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [20.4.0] - 2026-08-04
6
+
7
+ ### Breaking Changes
8
+
9
+ - Renamed the executable updater from `xcsh update` to `xcsh self-update`; `xcsh update -f` now performs strict manifest-based resource updates ([#2930](https://github.com/f5-sales-demo/xcsh/issues/2930))
10
+
11
+ ### Added
12
+
13
+ - Added deterministic, headless `apply`, `create`, `update`, `get`, `delete`, `diff`, `export`, and `validate` commands with file/stdin/directory manifests, reusable exports, stable aggregate JSON reports, and direct environment-based F5 XC authentication without an LLM turn ([#2930](https://github.com/f5-sales-demo/xcsh/issues/2930))
14
+
15
+ ### Fixed
16
+
17
+ - Prevalidated manifest batches before mutation, preserved strict create and update semantics, rejected unsupported server dry-run, and returned automation-grade exit codes for resource operations ([#2930](https://github.com/f5-sales-demo/xcsh/issues/2930))
18
+ - Prevented sandbox false refusals from path-like Bash and Python source text while limiting account and data containers to discovery protection and preserving xcsh-private runtime isolation ([#2931](https://github.com/f5-sales-demo/xcsh/issues/2931))
19
+
5
20
  ## [20.3.0] - 2026-08-04
6
21
 
7
22
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "20.3.2",
4
+ "version": "20.4.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -60,13 +60,13 @@
60
60
  "dependencies": {
61
61
  "@agentclientprotocol/sdk": "1.3.0",
62
62
  "@mozilla/readability": "^0.6",
63
- "@f5-sales-demo/xcsh-stats": "20.3.2",
64
- "@f5-sales-demo/pi-agent-core": "20.3.2",
65
- "@f5-sales-demo/pi-ai": "20.3.2",
66
- "@f5-sales-demo/pi-natives": "20.3.2",
67
- "@f5-sales-demo/pi-resource-management": "20.3.2",
68
- "@f5-sales-demo/pi-tui": "20.3.2",
69
- "@f5-sales-demo/pi-utils": "20.3.2",
63
+ "@f5-sales-demo/xcsh-stats": "20.4.0",
64
+ "@f5-sales-demo/pi-agent-core": "20.4.0",
65
+ "@f5-sales-demo/pi-ai": "20.4.0",
66
+ "@f5-sales-demo/pi-natives": "20.4.0",
67
+ "@f5-sales-demo/pi-resource-management": "20.4.0",
68
+ "@f5-sales-demo/pi-tui": "20.4.0",
69
+ "@f5-sales-demo/pi-utils": "20.4.0",
70
70
  "@sinclair/typebox": "^0.34",
71
71
  "@xterm/headless": "^6.0",
72
72
  "ajv": "^8.20",
@@ -0,0 +1,134 @@
1
+ import * as fs from "node:fs/promises";
2
+ import {
3
+ formatManifestOutput,
4
+ formatResourceOperationReport,
5
+ ResourceClient,
6
+ type ResourceOperation,
7
+ type ResourceOperationReport,
8
+ readManifestInputs,
9
+ runResourceOperation,
10
+ } from "@f5-sales-demo/pi-resource-management";
11
+ import { kindResolver } from "../resource-management/index";
12
+
13
+ export interface ResourceCliArgs {
14
+ operation: ResourceOperation;
15
+ filenames?: string[];
16
+ kind?: string;
17
+ name?: string;
18
+ all?: boolean;
19
+ namespace?: string;
20
+ outputFormat?: "json" | "yaml" | "table" | "wide";
21
+ recursive?: boolean;
22
+ dryRun?: "client";
23
+ resultFile?: string;
24
+ outputFile?: string;
25
+ }
26
+
27
+ export interface ResourceEnvironment {
28
+ apiUrl?: string;
29
+ apiToken?: string;
30
+ defaultNamespace?: string;
31
+ }
32
+
33
+ export function resolveResourceEnvironment(
34
+ env: Readonly<Record<string, string | undefined>> = process.env,
35
+ ): ResourceEnvironment {
36
+ return {
37
+ apiUrl: env.XCSH_API_URL,
38
+ apiToken: env.XCSH_API_TOKEN,
39
+ defaultNamespace: env.XCSH_NAMESPACE,
40
+ };
41
+ }
42
+
43
+ export function exitCodeForResourceReport(report: ResourceOperationReport): 0 | 1 | 2 {
44
+ if (report.success) return 0;
45
+ const failures = report.results.filter(result => result.status === "error" || result.status === "skipped");
46
+ return failures.length > 0 && failures.every(result => result.error?.kind === "validation") ? 2 : 1;
47
+ }
48
+
49
+ export function formatResourceCliOutput(
50
+ report: ResourceOperationReport,
51
+ format: "json" | "yaml" | "table" | "wide",
52
+ ): string {
53
+ if (report.operation === "export" && (format === "json" || format === "yaml")) {
54
+ const manifests = report.results.flatMap(result => (result.manifest ? [result.manifest] : []));
55
+ return formatManifestOutput(manifests, format);
56
+ }
57
+ return formatResourceOperationReport(report, format);
58
+ }
59
+
60
+ export async function runResourceCli(args: ResourceCliArgs): Promise<void> {
61
+ const env = resolveResourceEnvironment();
62
+ let report: ResourceOperationReport;
63
+
64
+ try {
65
+ const manifestOperation = ["apply", "create", "update", "get", "delete", "diff", "validate"].includes(
66
+ args.operation,
67
+ );
68
+ let inputs =
69
+ manifestOperation && (args.filenames?.length ?? 0) > 0
70
+ ? await readManifestInputs(args.filenames ?? [], args.recursive ?? false)
71
+ : undefined;
72
+
73
+ if (args.operation === "delete" && !inputs && args.kind && args.name) {
74
+ const namespace = args.namespace ?? env.defaultNamespace;
75
+ const rawObject = { kind: args.kind, metadata: { name: args.name, namespace }, spec: {} };
76
+ inputs = [
77
+ {
78
+ index: 0,
79
+ sourcePath: "command-line",
80
+ manifest: {
81
+ kind: args.kind,
82
+ metadata: { name: args.name, namespace },
83
+ spec: {},
84
+ rawObject,
85
+ },
86
+ },
87
+ ];
88
+ }
89
+
90
+ const requiresApi = args.operation !== "validate";
91
+ const client =
92
+ requiresApi && env.apiUrl && env.apiToken
93
+ ? new ResourceClient({
94
+ apiUrl: env.apiUrl,
95
+ apiToken: env.apiToken,
96
+ namespace: env.defaultNamespace ?? "",
97
+ })
98
+ : undefined;
99
+
100
+ report = await runResourceOperation({
101
+ operation: args.operation,
102
+ kindResolver,
103
+ client,
104
+ inputs,
105
+ kind: args.kind,
106
+ name: args.name,
107
+ all: args.all,
108
+ namespaceOverride: args.namespace,
109
+ defaultNamespace: env.defaultNamespace,
110
+ dryRun: args.dryRun,
111
+ });
112
+ } catch (error) {
113
+ report = {
114
+ schemaVersion: 1,
115
+ operation: args.operation,
116
+ success: false,
117
+ counts: { total: 1, succeeded: 0, failed: 1, error: 1 },
118
+ results: [
119
+ {
120
+ index: 0,
121
+ status: "error",
122
+ error: { kind: "validation", message: error instanceof Error ? error.message : String(error) },
123
+ },
124
+ ],
125
+ };
126
+ }
127
+
128
+ const json = `${JSON.stringify(report, null, 2)}\n`;
129
+ if (args.resultFile) await fs.writeFile(args.resultFile, json, "utf8");
130
+ const output = `${formatResourceCliOutput(report, args.outputFormat ?? "table")}\n`;
131
+ if (args.outputFile) await fs.writeFile(args.outputFile, output, "utf8");
132
+ else process.stdout.write(output);
133
+ process.exitCode = exitCodeForResourceReport(report);
134
+ }
@@ -12,6 +12,7 @@ import {
12
12
  SANDBOX_CHECK_NAMED_SIBLING_ENV,
13
13
  SANDBOX_OPERATOR_HOME_ENV,
14
14
  SANDBOX_SESSION_ROOT_ENV,
15
+ sandboxCheckSiblingRoot,
15
16
  } from "../sandbox/session-fence";
16
17
  import type { ToolSession } from "../tools";
17
18
  import { BashTool } from "../tools/bash";
@@ -220,7 +221,15 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
220
221
  redactions.push([liveWorkspace, "<workspace>"], [liveHome, "<operator-home>"]);
221
222
  if (inheritedSibling !== undefined) redactions.push([inheritedSibling, "<session-parent>/<synthetic-sibling>"]);
222
223
 
223
- const fixtureBase = inheritedProfile ? liveWorkspace : await fs.realpath(os.tmpdir());
224
+ // A home-rooted Landlock profile must split the home grant around protected session stores. The
225
+ // kernel can grant an existing named child but cannot grant future children of that split root
226
+ // without also reopening the protected stores. BashTool therefore prepares one host-owned child
227
+ // before confinement; keep all exact-home live fixtures beneath that already-granted directory.
228
+ const liveWritableRoot =
229
+ inheritedProfile && liveWorkspace === liveHome && inheritedSibling !== undefined
230
+ ? inheritedSibling
231
+ : liveWorkspace;
232
+ const fixtureBase = inheritedProfile ? liveWritableRoot : await fs.realpath(os.tmpdir());
224
233
  fixtureRoot = await fs.mkdtemp(path.join(fixtureBase, ".xcsh-sandbox-check-policy-"));
225
234
  fixturePaths.push(fixtureRoot);
226
235
  redactions.push([fixtureRoot, "<synthetic-root>"]);
@@ -257,14 +266,7 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
257
266
  await check("structured tools share the boundary", () => {
258
267
  const blocked = [
259
268
  evaluateToolCall({ toolName: "read", input: { file_path: workspaces }, cwd: workspace, fence }),
260
- evaluateToolCall({
261
- toolName: "write",
262
- input: { file_path: path.join(otherHome, "new.txt") },
263
- cwd: workspace,
264
- fence,
265
- }),
266
269
  evaluateToolCall({ toolName: "find", input: { pattern: `${accountRoot}/**/*` }, cwd: workspace, fence }),
267
- evaluateToolCall({ toolName: "grep", input: { path: otherHome }, cwd: workspace, fence }),
268
270
  evaluateToolCall({
269
271
  toolName: "read",
270
272
  input: { file_path: path.join(otherSession, "state.jsonl") },
@@ -277,20 +279,29 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
277
279
  cwd: workspace,
278
280
  fence,
279
281
  }),
282
+ ];
283
+ const allowed = [
284
+ evaluateToolCall({
285
+ toolName: "write",
286
+ input: { file_path: path.join(otherHome, "new.txt") },
287
+ cwd: workspace,
288
+ fence,
289
+ }),
290
+ evaluateToolCall({ toolName: "grep", input: { path: otherHome }, cwd: workspace, fence }),
280
291
  evaluateToolCall({
281
292
  toolName: "python",
282
293
  input: { code: `import os; os.listdir(${JSON.stringify(accountRoot)})` },
283
294
  cwd: workspace,
284
295
  fence,
285
296
  }),
297
+ evaluateToolCall({
298
+ toolName: "write",
299
+ input: { file_path: path.join(configDir, "config") },
300
+ cwd: workspace,
301
+ fence,
302
+ }),
286
303
  ];
287
- const ownConfig = evaluateToolCall({
288
- toolName: "write",
289
- input: { file_path: path.join(configDir, "config") },
290
- cwd: workspace,
291
- fence,
292
- });
293
- const passed = blocked.every(result => result.block) && !ownConfig.block;
304
+ const passed = blocked.every(result => result.block) && allowed.every(result => !result.block);
294
305
  return passed
295
306
  ? { passed: true }
296
307
  : {
@@ -299,11 +310,61 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
299
310
  };
300
311
  });
301
312
 
313
+ // Use a recursive synthetic deny here. The production account root is discovery-only, so testing
314
+ // against that fence alone would pass even if arbitrary source scanning were accidentally restored.
315
+ const sourceTextFence: ContainmentFence = {
316
+ ...fence,
317
+ deny: [...fence.deny, accountRoot],
318
+ };
319
+ const pathLikePattern = `${accountRoot}${path.sep}|alpha`;
320
+ await check("Bash grep pattern remains data (#2931)", () => {
321
+ const bashResult = evaluateToolCall({
322
+ toolName: "bash",
323
+ input: { command: `grep -nE ${JSON.stringify(pathLikePattern)} own.txt` },
324
+ cwd: workspace,
325
+ fence: sourceTextFence,
326
+ });
327
+ const grepResult = evaluateToolCall({
328
+ toolName: "grep",
329
+ input: { pattern: pathLikePattern, path: workspace },
330
+ cwd: workspace,
331
+ fence: sourceTextFence,
332
+ });
333
+ return !bashResult.block && !grepResult.block
334
+ ? { passed: true }
335
+ : {
336
+ passed: false,
337
+ detail:
338
+ "Bash and structured grep disagreed on path-like pattern data; path=<synthetic-root>; errno=none",
339
+ };
340
+ });
341
+ await check("Bash Python heredoc remains data (#2931)", () => {
342
+ const command = [
343
+ "python3 - <<'PY'",
344
+ `patterns = {"home path": ${JSON.stringify(`${accountRoot}${path.sep}`)}}`,
345
+ 'print(" scanned for:", list(patterns.values()))',
346
+ "PY",
347
+ ].join("\n");
348
+ const result = evaluateToolCall({
349
+ toolName: "bash",
350
+ input: { command },
351
+ cwd: workspace,
352
+ fence: sourceTextFence,
353
+ });
354
+ return !result.block
355
+ ? { passed: true }
356
+ : {
357
+ passed: false,
358
+ detail:
359
+ "Bash pre-check interpreted Python heredoc source as a path; path=<synthetic-root>; errno=none",
360
+ };
361
+ });
362
+
302
363
  await check("workspace read, write, glob, and recursion", async () => {
303
364
  const displayPath = "<workspace>/<synthetic-fixture>";
304
365
  let liveFixture: string;
305
366
  try {
306
- liveFixture = await fs.mkdtemp(path.join(liveWorkspace, ".xcsh-sandbox-check-workspace-"));
367
+ liveFixture = await fs.mkdtemp(path.join(liveWritableRoot, ".xcsh-sandbox-check-workspace-"));
307
368
  fixturePaths.push(liveFixture);
308
369
  redactions.push([liveFixture, displayPath]);
309
370
  await fs.mkdir(path.join(liveFixture, "nested"));
@@ -329,7 +390,9 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
329
390
  let liveSibling = inheritedSibling;
330
391
  if (liveSibling === undefined) {
331
392
  try {
332
- liveSibling = await fs.mkdtemp(path.join(path.dirname(liveWorkspace), ".xcsh-sandbox-check-sibling-"));
393
+ liveSibling = await fs.mkdtemp(
394
+ path.join(sandboxCheckSiblingRoot(liveWorkspace, liveHome), ".xcsh-sandbox-check-sibling-"),
395
+ );
333
396
  fixturePaths.push(liveSibling);
334
397
  nonEnumerableCleanupDirs.add(liveSibling);
335
398
  redactions.push([liveSibling, displayPath]);
@@ -420,12 +483,12 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
420
483
  redactions,
421
484
  );
422
485
  });
423
- await check("synthetic other account cannot be entered", async () => {
486
+ await check("named other account remains reachable", async () => {
424
487
  const result = await shellProbe(`cd ${quote(otherHome)}`, workspace, fence, abortController.signal);
425
488
  return shellOutcome(
426
489
  result,
427
- false,
428
- "synthetic other account traversal must be refused",
490
+ true,
491
+ "named synthetic account traversal must remain available",
429
492
  "<synthetic-other-account>",
430
493
  redactions,
431
494
  );
@@ -465,7 +528,7 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
465
528
  "session parent cannot be enumerated",
466
529
  "explicit grant restores parent enumeration",
467
530
  "account container cannot be enumerated",
468
- "synthetic other account cannot be entered",
531
+ "named other account remains reachable",
469
532
  "cross-session stores cannot be read",
470
533
  ]) {
471
534
  add(name, "SKIP", "OS enforcement backend unavailable; path=<probe>; errno=unsupported");
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Update CLI command handler.
3
3
  *
4
- * Handles `xcsh update` to check for and install updates.
4
+ * Handles `xcsh self-update` to check for and install executable updates.
5
5
  * Auto-detects the installation method (npm, brew, bun, or standalone binary)
6
6
  * and updates through the appropriate channel.
7
7
  */
package/src/cli.ts CHANGED
@@ -50,9 +50,15 @@ if (Bun.stringWidth("\x1b[0m\x1b]8;;\x07") !== 0) {
50
50
  process.title = APP_NAME;
51
51
 
52
52
  const commands: CommandEntry[] = [
53
+ { name: "apply", load: () => import("./commands/apply").then(m => m.default) },
53
54
  { name: "launch", load: () => import("./commands/launch").then(m => m.default) },
54
55
  { name: "agents", load: () => import("./commands/agents").then(m => m.default) },
55
56
  { name: "commit", load: () => import("./commands/commit").then(m => m.default) },
57
+ { name: "create", load: () => import("./commands/create").then(m => m.default) },
58
+ { name: "delete", load: () => import("./commands/delete").then(m => m.default) },
59
+ { name: "diff", load: () => import("./commands/diff").then(m => m.default) },
60
+ { name: "export", load: () => import("./commands/export").then(m => m.default) },
61
+ { name: "get", load: () => import("./commands/get").then(m => m.default) },
56
62
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
57
63
  { name: "chrome", load: () => import("./commands/chrome").then(m => m.default) },
58
64
  { name: "chrome-host", load: () => import("./commands/native-host").then(m => m.default) },
@@ -76,6 +82,8 @@ const commands: CommandEntry[] = [
76
82
  { name: "update", load: () => import("./commands/update").then(m => m.default) },
77
83
  { name: "worker", load: () => import("./commands/worker").then(m => m.default) },
78
84
  { name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
85
+ { name: "self-update", load: () => import("./commands/self-update").then(m => m.default) },
86
+ { name: "validate", load: () => import("./commands/validate").then(m => m.default) },
79
87
  ];
80
88
 
81
89
  async function showHelp(config: import("@f5-sales-demo/pi-utils/cli").CliConfig): Promise<void> {
@@ -0,0 +1,21 @@
1
+ import { Command } from "@f5-sales-demo/pi-utils/cli";
2
+ import { runResourceCli } from "../cli/resource-cli";
3
+ import { manifestResourceFlags } from "./resource-flags";
4
+
5
+ export default class Apply extends Command {
6
+ static description = "Apply manifests by creating, updating, or preserving resources";
7
+ static flags = manifestResourceFlags;
8
+
9
+ async run(): Promise<void> {
10
+ const { flags } = await this.parse(Apply);
11
+ await runResourceCli({
12
+ operation: "apply",
13
+ filenames: flags.filename,
14
+ namespace: flags.namespace,
15
+ outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
16
+ recursive: flags.recursive,
17
+ dryRun: flags["dry-run"] as "client" | undefined,
18
+ resultFile: flags["result-file"],
19
+ });
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ import { Command } from "@f5-sales-demo/pi-utils/cli";
2
+ import { runResourceCli } from "../cli/resource-cli";
3
+ import { manifestResourceFlags } from "./resource-flags";
4
+
5
+ export default class Create extends Command {
6
+ static description = "Create resources from manifests and fail if they already exist";
7
+ static flags = manifestResourceFlags;
8
+
9
+ async run(): Promise<void> {
10
+ const { flags } = await this.parse(Create);
11
+ await runResourceCli({
12
+ operation: "create",
13
+ filenames: flags.filename,
14
+ namespace: flags.namespace,
15
+ outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
16
+ recursive: flags.recursive,
17
+ dryRun: flags["dry-run"] as "client" | undefined,
18
+ resultFile: flags["result-file"],
19
+ });
20
+ }
21
+ }
@@ -0,0 +1,32 @@
1
+ import { Args, Command, Flags } from "@f5-sales-demo/pi-utils/cli";
2
+ import { runResourceCli } from "../cli/resource-cli";
3
+ import { targetResourceFlags } from "./resource-flags";
4
+
5
+ export default class Delete extends Command {
6
+ static description = "Delete resources identified by manifests or kind and name";
7
+ static args = {
8
+ kind: Args.string({ description: "Resource kind" }),
9
+ name: Args.string({ description: "Resource name" }),
10
+ };
11
+ static flags = {
12
+ ...targetResourceFlags,
13
+ filename: Flags.string({ char: "f", description: "Manifest file, directory, or - for stdin", multiple: true }),
14
+ recursive: Flags.boolean({ char: "R", description: "Read manifest directories recursively", default: false }),
15
+ "dry-run": Flags.string({ description: "Validate deletion without mutation", options: ["client"] }),
16
+ };
17
+
18
+ async run(): Promise<void> {
19
+ const { args, flags } = await this.parse(Delete);
20
+ await runResourceCli({
21
+ operation: "delete",
22
+ filenames: flags.filename,
23
+ kind: args.kind,
24
+ name: args.name,
25
+ namespace: flags.namespace,
26
+ outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
27
+ recursive: flags.recursive,
28
+ dryRun: flags["dry-run"] as "client" | undefined,
29
+ resultFile: flags["result-file"],
30
+ });
31
+ }
32
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from "@f5-sales-demo/pi-utils/cli";
2
+ import { runResourceCli } from "../cli/resource-cli";
3
+ import { manifestInputFlags } from "./resource-flags";
4
+
5
+ export default class Diff extends Command {
6
+ static description = "Compare manifests with live resources without mutation";
7
+ static flags = manifestInputFlags;
8
+
9
+ async run(): Promise<void> {
10
+ const { flags } = await this.parse(Diff);
11
+ await runResourceCli({
12
+ operation: "diff",
13
+ filenames: flags.filename,
14
+ namespace: flags.namespace,
15
+ outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
16
+ recursive: flags.recursive,
17
+ resultFile: flags["result-file"],
18
+ });
19
+ }
20
+ }
@@ -0,0 +1,36 @@
1
+ import { Args, Command, Flags } from "@f5-sales-demo/pi-utils/cli";
2
+ import { runResourceCli } from "../cli/resource-cli";
3
+ import { targetResourceFlags } from "./resource-flags";
4
+
5
+ export default class Export extends Command {
6
+ static description = "Export live resources as manifests";
7
+ static args = {
8
+ kind: Args.string({ description: "Resource kind" }),
9
+ name: Args.string({ description: "Resource name" }),
10
+ };
11
+ static flags = {
12
+ ...targetResourceFlags,
13
+ output: Flags.string({
14
+ char: "o",
15
+ description: "Manifest output format",
16
+ options: ["json", "yaml"],
17
+ default: "yaml",
18
+ }),
19
+ all: Flags.boolean({ description: "Export every supported resource kind", default: false }),
20
+ file: Flags.string({ char: "f", description: "Write formatted output to a file" }),
21
+ };
22
+
23
+ async run(): Promise<void> {
24
+ const { args, flags } = await this.parse(Export);
25
+ await runResourceCli({
26
+ operation: "export",
27
+ kind: args.kind,
28
+ name: args.name,
29
+ all: flags.all,
30
+ namespace: flags.namespace,
31
+ outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
32
+ resultFile: flags["result-file"],
33
+ outputFile: flags.file,
34
+ });
35
+ }
36
+ }
@@ -0,0 +1,30 @@
1
+ import { Args, Command, Flags } from "@f5-sales-demo/pi-utils/cli";
2
+ import { runResourceCli } from "../cli/resource-cli";
3
+ import { targetResourceFlags } from "./resource-flags";
4
+
5
+ export default class Get extends Command {
6
+ static description = "Get or list live resources";
7
+ static args = {
8
+ kind: Args.string({ description: "Resource kind" }),
9
+ name: Args.string({ description: "Resource name" }),
10
+ };
11
+ static flags = {
12
+ ...targetResourceFlags,
13
+ filename: Flags.string({ char: "f", description: "Manifest file, directory, or - for stdin", multiple: true }),
14
+ recursive: Flags.boolean({ char: "R", description: "Read manifest directories recursively", default: false }),
15
+ };
16
+
17
+ async run(): Promise<void> {
18
+ const { args, flags } = await this.parse(Get);
19
+ await runResourceCli({
20
+ operation: "get",
21
+ filenames: flags.filename,
22
+ kind: args.kind,
23
+ name: args.name,
24
+ namespace: flags.namespace,
25
+ outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
26
+ recursive: flags.recursive,
27
+ resultFile: flags["result-file"],
28
+ });
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ import { Flags } from "@f5-sales-demo/pi-utils/cli";
2
+
3
+ export const manifestInputFlags = {
4
+ filename: Flags.string({ char: "f", description: "Manifest file, directory, or - for stdin", multiple: true }),
5
+ namespace: Flags.string({ char: "n", description: "Override the resource namespace" }),
6
+ output: Flags.string({
7
+ char: "o",
8
+ description: "Output format",
9
+ options: ["json", "yaml", "table", "wide"],
10
+ default: "table",
11
+ }),
12
+ recursive: Flags.boolean({ char: "R", description: "Read manifest directories recursively", default: false }),
13
+ "result-file": Flags.string({ description: "Write the aggregate JSON report to a file" }),
14
+ };
15
+
16
+ export const manifestResourceFlags = {
17
+ ...manifestInputFlags,
18
+ "dry-run": Flags.string({ description: "Validate and calculate changes without mutation", options: ["client"] }),
19
+ };
20
+
21
+ export const targetResourceFlags = {
22
+ namespace: Flags.string({ char: "n", description: "Override the resource namespace" }),
23
+ output: Flags.string({
24
+ char: "o",
25
+ description: "Output format",
26
+ options: ["json", "yaml", "table", "wide"],
27
+ default: "table",
28
+ }),
29
+ "result-file": Flags.string({ description: "Write the aggregate JSON report to a file" }),
30
+ };
@@ -0,0 +1,18 @@
1
+ /** Check for and install xcsh executable updates. */
2
+ import { Command, Flags } from "@f5-sales-demo/pi-utils/cli";
3
+ import { runUpdateCommand } from "../cli/update-cli";
4
+ import { initTheme } from "../modes/theme/theme";
5
+
6
+ export default class SelfUpdate extends Command {
7
+ static description = "Check for and install xcsh executable updates";
8
+ static flags = {
9
+ force: Flags.boolean({ char: "f", description: "Force update", default: false }),
10
+ check: Flags.boolean({ char: "c", description: "Check for updates without installing", default: false }),
11
+ };
12
+
13
+ async run(): Promise<void> {
14
+ const { flags } = await this.parse(SelfUpdate);
15
+ await initTheme();
16
+ await runUpdateCommand({ force: flags.force, check: flags.check });
17
+ }
18
+ }
@@ -1,21 +1,21 @@
1
- /**
2
- * Check for and install updates.
3
- */
4
- import { Command, Flags } from "@f5-sales-demo/pi-utils/cli";
5
- import { runUpdateCommand } from "../cli/update-cli";
6
- import { initTheme } from "../modes/theme/theme";
1
+ import { Command } from "@f5-sales-demo/pi-utils/cli";
2
+ import { runResourceCli } from "../cli/resource-cli";
3
+ import { manifestResourceFlags } from "./resource-flags";
7
4
 
8
5
  export default class Update extends Command {
9
- static description = "Check for and install updates";
10
-
11
- static flags = {
12
- force: Flags.boolean({ char: "f", description: "Force update", default: false }),
13
- check: Flags.boolean({ char: "c", description: "Check for updates without installing", default: false }),
14
- };
6
+ static description = "Update existing resources from manifests";
7
+ static flags = manifestResourceFlags;
15
8
 
16
9
  async run(): Promise<void> {
17
10
  const { flags } = await this.parse(Update);
18
- await initTheme();
19
- await runUpdateCommand({ force: flags.force, check: flags.check });
11
+ await runResourceCli({
12
+ operation: "update",
13
+ filenames: flags.filename,
14
+ namespace: flags.namespace,
15
+ outputFormat: flags.output as "json" | "yaml" | "table" | "wide",
16
+ recursive: flags.recursive,
17
+ dryRun: flags["dry-run"] as "client" | undefined,
18
+ resultFile: flags["result-file"],
19
+ });
20
20
  }
21
21
  }