@f5-sales-demo/xcsh 22.14.6 → 22.15.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": "@f5-sales-demo/xcsh",
4
- "version": "22.14.6",
4
+ "version": "22.15.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",
@@ -68,13 +68,13 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@agentclientprotocol/sdk": "1.4.0",
71
- "@f5-sales-demo/pi-agent-core": "22.14.6",
72
- "@f5-sales-demo/pi-ai": "22.14.6",
73
- "@f5-sales-demo/pi-natives": "22.14.6",
74
- "@f5-sales-demo/pi-resource-management": "22.14.6",
75
- "@f5-sales-demo/pi-tui": "22.14.6",
76
- "@f5-sales-demo/pi-utils": "22.14.6",
77
- "@f5-sales-demo/xcsh-stats": "22.14.6",
71
+ "@f5-sales-demo/pi-agent-core": "22.15.0",
72
+ "@f5-sales-demo/pi-ai": "22.15.0",
73
+ "@f5-sales-demo/pi-natives": "22.15.0",
74
+ "@f5-sales-demo/pi-resource-management": "22.15.0",
75
+ "@f5-sales-demo/pi-tui": "22.15.0",
76
+ "@f5-sales-demo/pi-utils": "22.15.0",
77
+ "@f5-sales-demo/xcsh-stats": "22.15.0",
78
78
  "@mozilla/readability": "^0.6",
79
79
  "@resvg/resvg-wasm": "2.6.2",
80
80
  "@sinclair/typebox": "0.34.52",
package/src/cli.ts CHANGED
@@ -54,6 +54,7 @@ process.title = APP_NAME;
54
54
 
55
55
  const commands: CommandEntry[] = [
56
56
  { name: "context", load: () => import("./commands/context").then(m => m.default) },
57
+ { name: "request", load: () => import("./commands/request").then(m => m.default) },
57
58
  { name: "blindfold", load: () => import("./commands/blindfold").then(m => m.default) },
58
59
  { name: "remote-control", load: () => import("./commands/remote-control").then(m => m.default) },
59
60
  { name: "apply", load: () => import("./commands/apply").then(m => m.default) },
@@ -125,6 +126,17 @@ function requestsHelp(args: readonly string[]): boolean {
125
126
 
126
127
  /** Run the CLI with the given argv (no `process.argv` prefix). */
127
128
  export function runCli(argv: string[]): Promise<void> {
129
+ if (["blindfold", "request"].includes(argv[0] ?? "")) {
130
+ if (requestsHelp(argv.slice(1)))
131
+ return import("./commands/blindfold-help").then(m => m.showBlindfoldHelp(argv, APP_NAME));
132
+ return import("./commands/blindfold")
133
+ .then(m => m.runBlindfold(argv.slice(1), argv[0] === "request"))
134
+ .catch(error => {
135
+ if (!(error instanceof CliUsageError)) throw error;
136
+ writeCliOutput(process.stderr, `Error: ${error.message}\n`);
137
+ process.exitCode = 2;
138
+ });
139
+ }
128
140
  // --help and --version are handled by run() directly, don't rewrite those.
129
141
  // Everything else that isn't a known subcommand routes to "launch".
130
142
  // Keeping this routing boundary explicit makes the CLI fallback easy to audit.
@@ -0,0 +1,106 @@
1
+ import { resolve } from "node:path";
2
+ import { Args, CliUsageError, Flags, parseCommandArgv } from "@f5-sales-demo/pi-utils/cli";
3
+ import type { BlindfoldArgs, BlindfoldOperation } from "../services/blindfold";
4
+
5
+ export const blindfoldFlags = {
6
+ input: Flags.string({ description: "Secret filename or - for stdin" }),
7
+ "public-key": Flags.string({ description: "Public key JSON/YAML file; requires --policy-document (offline)" }),
8
+ "policy-document": Flags.string({ description: "Policy JSON/YAML file; requires --public-key (offline)" }),
9
+ cert: Flags.string({ description: "PEM certificate chain" }),
10
+ key: Flags.string({ description: "PEM private key" }),
11
+ bundle: Flags.string({ description: "PKCS#12 bundle" }),
12
+ name: Flags.string({ description: "Certificate name or secret policy name" }),
13
+ namespace: Flags.string({ char: "n", description: "Certificate namespace or secret policy namespace" }),
14
+ policy: Flags.string({ description: "Secret policy namespace/name (default shared/ves-io-allow-volterra)" }),
15
+ "passphrase-env": Flags.string({ description: "Passphrase environment variable name" }),
16
+ "context-name": Flags.string({ description: "Assert selected native context (online only)" }),
17
+ output: Flags.string({ description: "Public material format", options: ["json", "yaml"] }),
18
+ "output-file": Flags.string({ description: "New artifact file (0600)" }),
19
+ "result-file": Flags.string({ description: "New public JSON report file (0600)" }),
20
+ "dry-run": Flags.string({ description: "Validate without tenant writes", options: ["client"] }),
21
+ json: Flags.boolean({ description: "Print public JSON report" }),
22
+ };
23
+ const aliases = { "get-public-key": "public-key", "get-policy-document": "policy", encrypt: "encrypt" } as const;
24
+ const supported = ["public-key", "policy", "encrypt", "certificate", "create", "replace"];
25
+ const common = ["context-name", "output-file", "result-file", "json"];
26
+ export function flagsForBlindfold(operation: string) {
27
+ const names =
28
+ operation === "public-key"
29
+ ? [...common, "output"]
30
+ : operation === "policy"
31
+ ? [...common, "output", "policy", "namespace", "name"]
32
+ : operation === "encrypt"
33
+ ? [...common, "input", "policy", "public-key", "policy-document"]
34
+ : [...common, "cert", "key", "bundle", "name", "namespace", "policy", "passphrase-env", "dry-run"];
35
+ return Object.fromEntries(names.map(name => [name, blindfoldFlags[name as keyof typeof blindfoldFlags]]));
36
+ }
37
+ export function parseBlindfoldCli(
38
+ argv: readonly string[],
39
+ compatibility: boolean,
40
+ stdinIsTTY: boolean,
41
+ ): BlindfoldArgs & { json?: boolean } {
42
+ const parts = [...argv];
43
+ if (compatibility && parts.shift() !== "secrets") throw new CliUsageError("Only request secrets is supported");
44
+ const selected = parts.shift();
45
+ const operation = compatibility ? aliases[selected as keyof typeof aliases] : selected;
46
+ if (!operation || !supported.includes(operation)) throw new CliUsageError("Unknown Blindfold operation");
47
+ for (const arg of parts.slice(0, parts.includes("--") ? parts.indexOf("--") : undefined)) {
48
+ if (/^(--(?:config|server-urls|p12-bundle|cacert|cert|key|hw-key)(?:=|$)|-[acku])/.test(arg) && compatibility)
49
+ throw new CliUsageError("Legacy authentication is unsupported; configure xcsh context with xcsh context");
50
+ }
51
+ const parsed = parseCommandArgv(parts, {
52
+ flags: flagsForBlindfold(operation),
53
+ args: operation === "encrypt" ? { file: Args.string() } : {},
54
+ });
55
+ if (parsed.argv.length > (operation === "encrypt" ? 1 : 0)) throw new CliUsageError("Surplus positional arguments");
56
+ const f = parsed.flags;
57
+ if (f.input !== undefined && parsed.args.file !== undefined)
58
+ throw new CliUsageError("Select one secret input source");
59
+ const args: BlindfoldArgs & { json?: boolean } = {
60
+ operation: operation as BlindfoldOperation,
61
+ input: (f.input ?? parsed.args.file) as string | undefined,
62
+ publicKey: f["public-key"] as string | undefined,
63
+ policyDocument: f["policy-document"] as string | undefined,
64
+ cert: f.cert as string | undefined,
65
+ key: f.key as string | undefined,
66
+ bundle: f.bundle as string | undefined,
67
+ name: f.name as string | undefined,
68
+ namespace: f.namespace as string | undefined,
69
+ policy: f.policy as string | undefined,
70
+ passphraseEnv: f["passphrase-env"] as string | undefined,
71
+ contextName: f["context-name"] as string | undefined,
72
+ output: f.output as "json" | "yaml" | undefined,
73
+ outputFile: f["output-file"] as string | undefined,
74
+ resultFile: f["result-file"] as string | undefined,
75
+ dryRun: f["dry-run"] as "client" | undefined,
76
+ json: f.json as boolean | undefined,
77
+ };
78
+ if (Boolean(args.publicKey) !== Boolean(args.policyDocument))
79
+ throw new CliUsageError("--public-key and --policy-document must be supplied together");
80
+ if (args.publicKey && args.contextName)
81
+ throw new CliUsageError("Offline encryption does not select a context; omit --context-name");
82
+ if (args.json && (args.outputFile || args.output)) throw new CliUsageError("Conflicting output selections");
83
+ if (operation === "policy" && args.policy && (args.namespace !== undefined || args.name !== undefined))
84
+ throw new CliUsageError("Cannot combine --policy with --namespace or --name");
85
+
86
+ const label = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
87
+ if (args.policy && (args.policy.split("/").length !== 2 || args.policy.split("/").some(v => !label.test(v))))
88
+ throw new CliUsageError("Policy must be namespace/name");
89
+ if ([args.name, args.namespace].some(v => v !== undefined && !label.test(v)))
90
+ throw new CliUsageError("Invalid resource namespace or name");
91
+ if ([args.input, args.publicKey, args.policyDocument, args.outputFile, args.resultFile].some(v => v === ""))
92
+ throw new CliUsageError("File paths must be nonempty");
93
+ if (["certificate", "create", "replace"].includes(operation)) {
94
+ if (!args.name || (args.bundle ? args.cert || args.key : !args.cert || !args.key))
95
+ throw new CliUsageError("Use --name and either --bundle or --cert with --key");
96
+ }
97
+ if (args.outputFile && args.resultFile && resolve(args.outputFile) === resolve(args.resultFile))
98
+ throw new CliUsageError("Artifact and report destinations must differ");
99
+ if (operation === "encrypt" && args.input === undefined) {
100
+ if (stdinIsTTY) throw new CliUsageError("Encrypt requires an input filename or redirected stdin");
101
+ args.input = "-";
102
+ }
103
+ if (operation === "encrypt" && args.input === "-" && stdinIsTTY)
104
+ throw new CliUsageError("Encrypt stdin must be redirected");
105
+ return args;
106
+ }
@@ -0,0 +1,51 @@
1
+ import { type ArgDescriptor, Args, CliUsageError, Command, renderCommandHelp } from "@f5-sales-demo/pi-utils/cli";
2
+ import { writeCliOutput } from "@f5-sales-demo/pi-utils/cli-output";
3
+ import { flagsForBlindfold } from "./blindfold-args";
4
+ export function showBlindfoldHelp(argv: string[], bin: string): void {
5
+ const compatibility = argv[0] === "request";
6
+ const leaf = compatibility ? argv[2] : argv[1];
7
+ const operation =
8
+ (
9
+ { "get-public-key": "public-key", "get-policy-document": "policy", encrypt: "encrypt" } as Record<
10
+ string,
11
+ string
12
+ >
13
+ )[leaf ?? ""] ?? (!compatibility ? leaf : undefined);
14
+ if (compatibility && argv[1] && !["secrets", "-h", "--help"].includes(argv[1])) {
15
+ writeCliOutput(process.stderr, "Error: Only request secrets is supported\n");
16
+ process.exitCode = 2;
17
+ return;
18
+ }
19
+ if (!operation || ["--help", "-h"].includes(operation)) {
20
+ writeCliOutput(
21
+ process.stdout,
22
+ `${bin} ${compatibility ? "request secrets" : "blindfold"} ${compatibility ? "get-public-key | get-policy-document | encrypt" : "public-key | policy | encrypt | certificate | create | replace"}\nUse an operation followed by --help. Authentication uses xcsh context.\n`,
23
+ );
24
+ return;
25
+ }
26
+ try {
27
+ if (
28
+ !["public-key", "policy", "encrypt", "certificate", "create", "replace"].includes(operation) ||
29
+ (compatibility && !["get-public-key", "get-policy-document", "encrypt"].includes(leaf!))
30
+ )
31
+ throw new CliUsageError("Unknown Blindfold operation");
32
+ class Help extends Command {
33
+ static description =
34
+ operation === "encrypt"
35
+ ? "Encrypt an input file or redirected stdin; paired public material files select offline operation"
36
+ : operation === "policy"
37
+ ? "Retrieve a secret policy document; namespace/name select the policy"
38
+ : "Native Blindfold operation";
39
+ static flags = flagsForBlindfold(operation!);
40
+ static args: Record<string, ArgDescriptor> =
41
+ operation === "encrypt"
42
+ ? { file: Args.string({ description: "Filename or -; omit for redirected stdin" }) }
43
+ : {};
44
+ async run() {}
45
+ }
46
+ renderCommandHelp(bin, argv.slice(0, compatibility ? 3 : 2).join(" "), Help);
47
+ } catch (error) {
48
+ writeCliOutput(process.stderr, `Error: ${error instanceof Error ? error.message : "invalid operation"}\n`);
49
+ process.exitCode = 2;
50
+ }
51
+ }
@@ -1,80 +1,55 @@
1
- import { Args, Command, Flags } from "@f5-sales-demo/pi-utils/cli";
2
- import { Settings } from "../config/settings";
3
- import { BLINDFOLD_OPERATIONS, type BlindfoldOperation, BlindfoldService } from "../services/blindfold";
4
- import { ContextService } from "../services/xcsh-context";
1
+ import { Args, Command } from "@f5-sales-demo/pi-utils/cli";
2
+ import { writeCliOutput } from "@f5-sales-demo/pi-utils/cli-output";
5
3
 
4
+ import { BlindfoldService } from "../services/blindfold";
5
+ import type { ContextService } from "../services/xcsh-context";
6
+ import { blindfoldFlags, parseBlindfoldCli } from "./blindfold-args";
7
+ export async function runBlindfold(argv: string[], compatibility = false): Promise<void> {
8
+ const args = parseBlindfoldCli(argv, compatibility, Boolean(process.stdin.isTTY));
9
+ let context: ContextService | undefined;
10
+ const controller = new AbortController();
11
+ const cancel = () => controller.abort();
12
+ process.once("SIGINT", cancel);
13
+ try {
14
+ let env: Record<string, string | undefined> = {};
15
+ if (!args.publicKey) {
16
+ const { Settings } = await import("../config/settings");
17
+ const { ContextService } = await import("../services/xcsh-context");
18
+ await Settings.init({ cwd: process.cwd() });
19
+ context = await ContextService.getOrInit(undefined, process.cwd());
20
+ env = { ...(Settings.instance.get("bash.environment") as Record<string, string> | undefined), ...process.env };
21
+ }
22
+ const service = new BlindfoldService({
23
+ env,
24
+ emit: content => {
25
+ if (!args.json && !args.outputFile) writeCliOutput(process.stdout, content);
26
+ },
27
+ });
28
+ const report = await service.run(args, { signal: controller.signal });
29
+ if (args.json || args.outputFile || ["create", "replace"].includes(report.operation))
30
+ writeCliOutput(process.stdout, `${JSON.stringify(report, null, 2)}\n`);
31
+ } catch (error) {
32
+ writeCliOutput(
33
+ process.stderr,
34
+ `Blindfold: ${controller.signal.aborted ? "operation cancelled" : error instanceof Error ? error.message : "operation failed"}\n`,
35
+ );
36
+ process.exitCode = controller.signal.aborted ? 130 : 1;
37
+ } finally {
38
+ context?.stopRevalidation();
39
+ process.removeListener("SIGINT", cancel);
40
+ }
41
+ }
6
42
  export default class Blindfold extends Command {
7
43
  static description = "Encrypt secrets natively and create or rotate tenant TLS certificates";
8
44
  static args = {
9
45
  operation: Args.string({
10
46
  required: true,
11
- options: [...BLINDFOLD_OPERATIONS],
12
- description: "public-key, policy, encrypt, certificate, create, or replace",
47
+ description: "public-key, policy, encrypt, certificate, create, replace",
13
48
  }),
49
+ file: Args.string({ description: "Encryption input file or - for redirected stdin" }),
14
50
  };
15
- static flags = {
16
- input: Flags.string({ description: "Secret file to encrypt" }),
17
- cert: Flags.string({ description: "PEM certificate chain" }),
18
- key: Flags.string({ description: "PEM private key" }),
19
- bundle: Flags.string({ description: "PKCS#12 .p12/.pfx bundle" }),
20
- name: Flags.string({ description: "Certificate resource name" }),
21
- namespace: Flags.string({ char: "n", description: "Certificate namespace; defaults to XCSH_NAMESPACE" }),
22
- policy: Flags.string({ description: "Secret policy namespace/name", default: "shared/ves-io-allow-volterra" }),
23
- "passphrase-env": Flags.string({
24
- description: "Name of an environment variable containing the input passphrase",
25
- }),
26
- "context-name": Flags.string({ description: "Expected active context; fail if different" }),
27
- "output-file": Flags.string({ description: "Create a new encrypted artifact or public material file (0600)" }),
28
- "result-file": Flags.string({ description: "Create a new JSON report (0600)" }),
29
- "dry-run": Flags.string({ description: "Validate without tenant writes", options: ["client"] }),
30
- json: Flags.boolean({ description: "Print the public JSON report" }),
31
- };
51
+ static flags = blindfoldFlags;
32
52
  async run(): Promise<void> {
33
- const { args, flags } = await this.parse(Blindfold);
34
- let context: ContextService | undefined;
35
- const controller = new AbortController();
36
- const cancel = () => controller.abort();
37
- process.once("SIGINT", cancel);
38
- try {
39
- await Settings.init({ cwd: process.cwd() });
40
- context = await ContextService.getOrInit(undefined, process.cwd());
41
- const env = {
42
- ...(Settings.instance.get("bash.environment") as Record<string, string> | undefined),
43
- ...process.env,
44
- };
45
- const service = new BlindfoldService({
46
- env,
47
- emit: content => {
48
- if (!flags.json && !flags["output-file"]) process.stdout.write(content);
49
- },
50
- });
51
- const report = await service.run(
52
- {
53
- operation: args.operation as BlindfoldOperation,
54
- input: flags.input,
55
- cert: flags.cert,
56
- key: flags.key,
57
- bundle: flags.bundle,
58
- name: flags.name,
59
- namespace: flags.namespace,
60
- policy: flags.policy,
61
- passphraseEnv: flags["passphrase-env"],
62
- contextName: flags["context-name"],
63
- outputFile: flags["output-file"],
64
- resultFile: flags["result-file"],
65
- dryRun: flags["dry-run"] as "client" | undefined,
66
- },
67
- { signal: controller.signal },
68
- );
69
- if (flags.json || flags["output-file"] || ["create", "replace"].includes(report.operation))
70
- process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
71
- context.stopRevalidation();
72
- } catch (error) {
73
- process.stderr.write(`Blindfold: ${error instanceof Error ? error.message : "operation failed"}\n`);
74
- process.exitCode = 1;
75
- } finally {
76
- context?.stopRevalidation();
77
- process.removeListener("SIGINT", cancel);
78
- }
53
+ await runBlindfold(this.argv);
79
54
  }
80
55
  }
@@ -0,0 +1,12 @@
1
+ import { Args, Command } from "@f5-sales-demo/pi-utils/cli";
2
+ import { runBlindfold } from "./blindfold";
3
+ export default class Request extends Command {
4
+ static description = "Native compatibility for request secrets get-public-key, get-policy-document, encrypt";
5
+ static args = {
6
+ secrets: Args.string({ required: true, options: ["secrets"] }),
7
+ operation: Args.string({ required: true, options: ["get-public-key", "get-policy-document", "encrypt"] }),
8
+ };
9
+ async run(): Promise<void> {
10
+ await runBlindfold(this.argv, true);
11
+ }
12
+ }