@f5-sales-demo/xcsh 20.2.2 → 20.2.4
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
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [20.2.4] - 2026-08-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Preserved command exit status while evaluating assignment-only shell commands so canonical captures such as `rc=$?` retain failures ([#2837](https://github.com/f5-sales-demo/xcsh/issues/2837))
|
|
10
|
+
|
|
11
|
+
## [20.2.3] - 2026-08-02
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Made sandbox launch-flag scope errors consistent in either argument position, report usage exit 2 without parser exceptions, and direct operators to the flag-free conformance check ([#2834](https://github.com/f5-sales-demo/xcsh/issues/2834))
|
|
16
|
+
|
|
5
17
|
## [20.2.1] - 2026-08-01
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "20.2.
|
|
4
|
+
"version": "20.2.4",
|
|
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",
|
|
@@ -57,13 +57,13 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
59
59
|
"@mozilla/readability": "^0.6",
|
|
60
|
-
"@f5-sales-demo/xcsh-stats": "20.2.
|
|
61
|
-
"@f5-sales-demo/pi-agent-core": "20.2.
|
|
62
|
-
"@f5-sales-demo/pi-ai": "20.2.
|
|
63
|
-
"@f5-sales-demo/pi-natives": "20.2.
|
|
64
|
-
"@f5-sales-demo/pi-resource-management": "20.2.
|
|
65
|
-
"@f5-sales-demo/pi-tui": "20.2.
|
|
66
|
-
"@f5-sales-demo/pi-utils": "20.2.
|
|
60
|
+
"@f5-sales-demo/xcsh-stats": "20.2.4",
|
|
61
|
+
"@f5-sales-demo/pi-agent-core": "20.2.4",
|
|
62
|
+
"@f5-sales-demo/pi-ai": "20.2.4",
|
|
63
|
+
"@f5-sales-demo/pi-natives": "20.2.4",
|
|
64
|
+
"@f5-sales-demo/pi-resource-management": "20.2.4",
|
|
65
|
+
"@f5-sales-demo/pi-tui": "20.2.4",
|
|
66
|
+
"@f5-sales-demo/pi-utils": "20.2.4",
|
|
67
67
|
"@sinclair/typebox": "^0.34",
|
|
68
68
|
"@xterm/headless": "^6.0",
|
|
69
69
|
"ajv": "^8.20",
|
package/src/cli/flag-spec.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `cli/args.ts` can both depend on it without a cycle.
|
|
14
14
|
*/
|
|
15
15
|
import { THINKING_EFFORTS } from "@f5-sales-demo/pi-ai";
|
|
16
|
-
import { type FlagDescriptor, Flags } from "@f5-sales-demo/pi-utils/cli";
|
|
16
|
+
import { CliUsageError, type FlagDescriptor, Flags } from "@f5-sales-demo/pi-utils/cli";
|
|
17
17
|
|
|
18
18
|
export type FlagArity = "boolean" | "value" | "optional-value" | "repeatable-value";
|
|
19
19
|
|
|
@@ -157,8 +157,6 @@ export interface UnrecognizedFlag {
|
|
|
157
157
|
name: string;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
export class FlagUsageError extends Error {}
|
|
161
|
-
|
|
162
160
|
/**
|
|
163
161
|
* Rewrite `--name=value` into `["--name", "value"]` for every flag that takes a value.
|
|
164
162
|
*
|
|
@@ -197,7 +195,7 @@ export function normalizeFlagTokens(
|
|
|
197
195
|
const spec = flagSpec(name);
|
|
198
196
|
if (spec) {
|
|
199
197
|
if (!takesValue(spec)) {
|
|
200
|
-
throw new
|
|
198
|
+
throw new CliUsageError(`--${name} is a boolean flag and does not take a value`);
|
|
201
199
|
}
|
|
202
200
|
normalized.push(`--${name}`, value);
|
|
203
201
|
continue;
|
|
@@ -206,7 +204,7 @@ export function normalizeFlagTokens(
|
|
|
206
204
|
const extension = extensionFlags?.get(name);
|
|
207
205
|
if (extension) {
|
|
208
206
|
if (extension.type === "boolean") {
|
|
209
|
-
throw new
|
|
207
|
+
throw new CliUsageError(`--${name} is a boolean flag and does not take a value`);
|
|
210
208
|
}
|
|
211
209
|
normalized.push(`--${name}`, value);
|
|
212
210
|
continue;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FlagDescriptor } from "@f5-sales-demo/pi-utils/cli";
|
|
1
2
|
import { flagNameForChar, flagSpec, type LaunchFlagName } from "./flag-spec";
|
|
2
3
|
|
|
3
4
|
export interface PrefixedCommandRoute {
|
|
@@ -6,6 +7,67 @@ export interface PrefixedCommandRoute {
|
|
|
6
7
|
prefixFlags: LaunchFlagName[];
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
function uniqueFlags(flags: readonly LaunchFlagName[]): LaunchFlagName[] {
|
|
11
|
+
return [...new Set(flags)];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find agent-launch flags in a registered command's argv while preserving that command's own flags.
|
|
16
|
+
*
|
|
17
|
+
* `--` ends option parsing. Long and short command flags both take precedence over identically named
|
|
18
|
+
* launch flags, so `sandbox check -v` remains the sandbox check's verbose mode.
|
|
19
|
+
*/
|
|
20
|
+
export function findCommandLaunchFlags(
|
|
21
|
+
argv: readonly string[],
|
|
22
|
+
commandFlags: Readonly<Record<string, FlagDescriptor>>,
|
|
23
|
+
): LaunchFlagName[] {
|
|
24
|
+
const shortCommandFlags = new Set(
|
|
25
|
+
Object.values(commandFlags)
|
|
26
|
+
.map(descriptor => descriptor.char)
|
|
27
|
+
.filter((char): char is string => char !== undefined),
|
|
28
|
+
);
|
|
29
|
+
const found: LaunchFlagName[] = [];
|
|
30
|
+
|
|
31
|
+
for (const token of argv) {
|
|
32
|
+
if (token === "--") break;
|
|
33
|
+
if (token.startsWith("--")) {
|
|
34
|
+
const name = token.slice(2).split("=", 1)[0];
|
|
35
|
+
if (commandFlags[name] !== undefined) continue;
|
|
36
|
+
if (flagSpec(name) !== undefined) found.push(name as LaunchFlagName);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!token.startsWith("-") || token === "-") continue;
|
|
40
|
+
const char = token.slice(1);
|
|
41
|
+
if (shortCommandFlags.has(char)) continue;
|
|
42
|
+
const name = flagNameForChar(char);
|
|
43
|
+
if (name !== undefined) found.push(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return uniqueFlags(found);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Render the single scope diagnostic used for launch flags on either side of a subcommand. */
|
|
50
|
+
export function launchFlagScopeMessage(
|
|
51
|
+
flags: readonly LaunchFlagName[],
|
|
52
|
+
command: string,
|
|
53
|
+
commandArgs: readonly string[],
|
|
54
|
+
bin: string,
|
|
55
|
+
): string {
|
|
56
|
+
const unique = uniqueFlags(flags);
|
|
57
|
+
const names = unique.map(flag => `--${flag}`).join(", ");
|
|
58
|
+
const singular = unique.length === 1;
|
|
59
|
+
if (command === "sandbox" && commandArgs[0] === "check") {
|
|
60
|
+
return (
|
|
61
|
+
`Launch ${singular ? "flag" : "flags"} ${names} ${singular ? "applies" : "apply"} to an agent session, not to \`sandbox check\`. ` +
|
|
62
|
+
`Run \`${bin} sandbox check\` without launch flags; its \`explicit grant restores parent enumeration\` probe verifies grant behavior.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
`Launch ${singular ? "flag" : "flags"} ${names} ${singular ? "applies" : "apply"} to an agent session, not to the ` +
|
|
67
|
+
`\`${command}\` subcommand. Start an agent session with launch flags before its prompt.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
9
71
|
function optionalValue(token: string | undefined): token is string {
|
|
10
72
|
return token !== undefined && !token.startsWith("-") && !token.startsWith("@");
|
|
11
73
|
}
|
package/src/cli.ts
CHANGED
|
@@ -5,8 +5,7 @@ import { APP_NAME, initI18n, MIN_BUN_VERSION, registerLocales, t, VERSION } from
|
|
|
5
5
|
* lightweight CLI runner from pi-utils.
|
|
6
6
|
*/
|
|
7
7
|
import { type CommandEntry, run } from "@f5-sales-demo/pi-utils/cli";
|
|
8
|
-
import {
|
|
9
|
-
import { findPrefixedCommand } from "./cli/root-command-routing";
|
|
8
|
+
import { findPrefixedCommand, launchFlagScopeMessage } from "./cli/root-command-routing";
|
|
10
9
|
import { locales } from "./locales/index";
|
|
11
10
|
|
|
12
11
|
registerLocales(locales);
|
|
@@ -118,10 +117,13 @@ export function runCli(argv: string[]): Promise<void> {
|
|
|
118
117
|
help: showHelp,
|
|
119
118
|
});
|
|
120
119
|
}
|
|
121
|
-
const flags = [...new Set(prefixedCommand.prefixFlags)].map(flag => `--${flag}`).join(", ");
|
|
122
120
|
process.stderr.write(
|
|
123
|
-
`Error:
|
|
124
|
-
|
|
121
|
+
`Error: ${launchFlagScopeMessage(
|
|
122
|
+
prefixedCommand.prefixFlags,
|
|
123
|
+
prefixedCommand.command,
|
|
124
|
+
prefixedCommand.commandArgs,
|
|
125
|
+
APP_NAME,
|
|
126
|
+
)}\n`,
|
|
125
127
|
);
|
|
126
128
|
process.exitCode = 2;
|
|
127
129
|
return Promise.resolve();
|
|
@@ -152,10 +154,4 @@ if (process.env.XCSH_SMOKE_TEST_SPECS === "1") {
|
|
|
152
154
|
process.exit(domainCount > 0 && categoryCount > 0 ? 0 : 1);
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
|
|
156
|
-
await runCli(process.argv.slice(2));
|
|
157
|
-
} catch (error) {
|
|
158
|
-
if (!(error instanceof FlagUsageError)) throw error;
|
|
159
|
-
process.stderr.write(`Error: ${error.message}\n`);
|
|
160
|
-
process.exitCode = 2;
|
|
161
|
-
}
|
|
157
|
+
await runCli(process.argv.slice(2));
|
package/src/commands/sandbox.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Run installed-binary sandbox diagnostics. */
|
|
2
|
-
import {
|
|
2
|
+
import { APP_NAME } from "@f5-sales-demo/pi-utils";
|
|
3
|
+
import { Args, CliUsageError, Command, Flags } from "@f5-sales-demo/pi-utils/cli";
|
|
4
|
+
import { findCommandLaunchFlags, launchFlagScopeMessage } from "../cli/root-command-routing";
|
|
3
5
|
import { runSandboxCheck } from "../cli/sandbox-check";
|
|
4
6
|
|
|
5
7
|
export default class Sandbox extends Command {
|
|
@@ -19,6 +21,10 @@ export default class Sandbox extends Command {
|
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
async run(): Promise<void> {
|
|
24
|
+
const launchFlags = findCommandLaunchFlags(this.argv, Sandbox.flags);
|
|
25
|
+
if (launchFlags.length > 0) {
|
|
26
|
+
throw new CliUsageError(launchFlagScopeMessage(launchFlags, "sandbox", this.argv, APP_NAME));
|
|
27
|
+
}
|
|
22
28
|
const { flags } = await this.parse(Sandbox);
|
|
23
29
|
const report = await runSandboxCheck({ json: flags.json, verbose: flags.verbose });
|
|
24
30
|
if (report.summary.failed > 0 || report.summary.errors > 0) process.exitCode = 1;
|
|
@@ -120,11 +120,9 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
120
120
|
const CWD_SENTINEL_END = ":__XCSH_CWD_END__";
|
|
121
121
|
// Exit-code capture sentinel — the persistent shell's own exit code reflects
|
|
122
122
|
// the trailing printf (always 0), so the user command's actual exit status
|
|
123
|
-
// must be captured in-band
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// this sentinel, subprocess failures like `false`, `ls /nonexistent`, or
|
|
127
|
-
// `(exit 3)` silently report success.
|
|
123
|
+
// must be captured in-band before another command replaces it. Without this
|
|
124
|
+
// sentinel, subprocess failures like `false`, `ls /nonexistent`, or `(exit 3)`
|
|
125
|
+
// silently report success.
|
|
128
126
|
const EXIT_SENTINEL_START = "__XCSH_EXIT__:";
|
|
129
127
|
const EXIT_SENTINEL_END = ":__XCSH_EXIT_END__";
|
|
130
128
|
|
|
@@ -167,12 +165,9 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
// Append CWD + exit-code sentinels only for persistent shell sessions.
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
// shell's winner.exitCode always reflects the printf's return (0), so
|
|
174
|
-
// the EXIT sentinel is the authoritative source for the user command's
|
|
175
|
-
// actual exit status.
|
|
168
|
+
// The persistent shell's winner.exitCode reflects the trailing printf's
|
|
169
|
+
// return (0), so the EXIT sentinel is the authoritative source for the user
|
|
170
|
+
// command's actual exit status.
|
|
176
171
|
const finalCommand = shellSession
|
|
177
172
|
? `${prefixedCommand}\nprintf '${CWD_SENTINEL_START}%s${CWD_SENTINEL_END}\\n${EXIT_SENTINEL_START}%s${EXIT_SENTINEL_END}\\n' "$PWD" "$?"`
|
|
178
173
|
: prefixedCommand;
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "20.2.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "20.2.4",
|
|
21
|
+
"commit": "b47f63ed7ea202dbcc870fbbcc39c20f17814143",
|
|
22
|
+
"shortCommit": "b47f63e",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v20.2.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
24
|
+
"tag": "v20.2.4",
|
|
25
|
+
"commitDate": "2026-08-02T04:49:38Z",
|
|
26
|
+
"buildDate": "2026-08-02T05:15:27.513Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.2.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/b47f63ed7ea202dbcc870fbbcc39c20f17814143",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.2.4"
|
|
33
33
|
};
|