@f5-sales-demo/xcsh 19.101.0 → 19.101.1
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": "19.101.
|
|
4
|
+
"version": "19.101.1",
|
|
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": "19.101.
|
|
61
|
-
"@f5-sales-demo/pi-agent-core": "19.101.
|
|
62
|
-
"@f5-sales-demo/pi-ai": "19.101.
|
|
63
|
-
"@f5-sales-demo/pi-natives": "19.101.
|
|
64
|
-
"@f5-sales-demo/pi-resource-management": "19.101.
|
|
65
|
-
"@f5-sales-demo/pi-tui": "19.101.
|
|
66
|
-
"@f5-sales-demo/pi-utils": "19.101.
|
|
60
|
+
"@f5-sales-demo/xcsh-stats": "19.101.1",
|
|
61
|
+
"@f5-sales-demo/pi-agent-core": "19.101.1",
|
|
62
|
+
"@f5-sales-demo/pi-ai": "19.101.1",
|
|
63
|
+
"@f5-sales-demo/pi-natives": "19.101.1",
|
|
64
|
+
"@f5-sales-demo/pi-resource-management": "19.101.1",
|
|
65
|
+
"@f5-sales-demo/pi-tui": "19.101.1",
|
|
66
|
+
"@f5-sales-demo/pi-utils": "19.101.1",
|
|
67
67
|
"@sinclair/typebox": "^0.34",
|
|
68
68
|
"@xterm/headless": "^6.0",
|
|
69
69
|
"ajv": "^8.20",
|
|
@@ -20,55 +20,182 @@ const here = import.meta.dir; // packages/coding-agent/scripts
|
|
|
20
20
|
const vendoredPath = path.resolve(here, "../src/browser/capabilities.json");
|
|
21
21
|
const generatedPath = path.resolve(here, "../src/browser/capabilities.generated.ts");
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
// Guarded so the regression checks below can be imported by tests without regenerating files.
|
|
24
|
+
// Importing this module used to run the whole generator as a side effect (#2578).
|
|
25
|
+
if (import.meta.main) {
|
|
26
|
+
// Co-build refresh: adopt the sibling extension's freshly-built contract — but only when it really is
|
|
27
|
+
// fresher. "The directory exists" is not evidence of that, and treating it as evidence silently rewrote a
|
|
28
|
+
// committed contract from whatever happened to be on the developer's disk (#2578).
|
|
29
|
+
//
|
|
30
|
+
// Measured on a real machine: a sibling checkout left at contractVersion 1.8.0 overwrote the committed
|
|
31
|
+
// 1.12.0 and deleted the whole `handshake` feature block, on every `bun run check`, `bun test` and
|
|
32
|
+
// `bun run build`. `release.ts` runs `git commit -a`, so the downgrade was one release away from shipping.
|
|
33
|
+
// The guard applies to the path found by *proximity*, which is the accidental one. An explicitly set
|
|
34
|
+
// XCSH_EXTENSION_CAPABILITIES is a deliberate statement about which manifest to compile in — including a
|
|
35
|
+
// major-version co-build that removes tools on purpose — so it is honoured and merely announced.
|
|
36
|
+
const explicitPath = process.env.XCSH_EXTENSION_CAPABILITIES;
|
|
37
|
+
const siblingPath = explicitPath ?? path.resolve(here, "../../../../xcsh-chrome-extension/capabilities.json");
|
|
38
|
+
if (explicitPath !== undefined && fs.existsSync(explicitPath) && path.resolve(explicitPath) !== vendoredPath) {
|
|
39
|
+
console.log(`Adopting ${explicitPath} (XCSH_EXTENSION_CAPABILITIES set explicitly).`);
|
|
40
|
+
fs.copyFileSync(explicitPath, vendoredPath);
|
|
41
|
+
} else if (explicitPath === undefined && fs.existsSync(siblingPath) && path.resolve(siblingPath) !== vendoredPath) {
|
|
42
|
+
adoptSiblingIfNotARegression(siblingPath, vendoredPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const manifest = JSON.parse(fs.readFileSync(vendoredPath, "utf8")) as {
|
|
46
|
+
contractVersion: string;
|
|
47
|
+
tools: Array<{ name: string }>;
|
|
48
|
+
};
|
|
49
|
+
const toolNames = manifest.tools.map(t => t.name);
|
|
50
|
+
|
|
51
|
+
const output = [
|
|
52
|
+
"// Auto-generated by scripts/generate-extension-capabilities.ts - DO NOT EDIT",
|
|
53
|
+
"// Source: src/browser/capabilities.json (the Chrome extension's published contract).",
|
|
54
|
+
"",
|
|
55
|
+
"export interface ExtensionToolFlags {",
|
|
56
|
+
"\treadonly readOnly?: boolean;",
|
|
57
|
+
"\treadonly mutates?: boolean;",
|
|
58
|
+
"\treadonly requiresExplainMode?: boolean;",
|
|
59
|
+
"}",
|
|
60
|
+
"",
|
|
61
|
+
"export interface ExtensionToolDef {",
|
|
62
|
+
"\treadonly name: string;",
|
|
63
|
+
"\treadonly summary: string;",
|
|
64
|
+
"\treadonly category: string;",
|
|
65
|
+
"\treadonly params: Record<string, unknown>;",
|
|
66
|
+
"\treadonly flags?: ExtensionToolFlags;",
|
|
67
|
+
"}",
|
|
68
|
+
"",
|
|
69
|
+
"export interface ExtensionCapabilities {",
|
|
70
|
+
"\treadonly version: string;",
|
|
71
|
+
"\treadonly contractVersion: string;",
|
|
72
|
+
"\treadonly multiPortDiscovery?: boolean;",
|
|
73
|
+
"\treadonly protocol: string;",
|
|
74
|
+
"\treadonly tools: readonly ExtensionToolDef[];",
|
|
75
|
+
"\treadonly features: Record<string, unknown>;",
|
|
76
|
+
"}",
|
|
77
|
+
"",
|
|
78
|
+
`export const EXTENSION_CAPABILITIES: ExtensionCapabilities = ${JSON.stringify(manifest, null, "\t")};`,
|
|
79
|
+
"",
|
|
80
|
+
`export const EXTENSION_CONTRACT_VERSION = ${JSON.stringify(manifest.contractVersion)};`,
|
|
81
|
+
"",
|
|
82
|
+
`export const EXTENSION_TOOL_NAMES: readonly string[] = ${JSON.stringify(toolNames)};`,
|
|
83
|
+
"",
|
|
84
|
+
].join("\n");
|
|
85
|
+
|
|
86
|
+
await Bun.write(generatedPath, output);
|
|
87
|
+
console.log(
|
|
88
|
+
`Generated ${path.relative(process.cwd(), generatedPath)} (${toolNames.length} tools, contract ${manifest.contractVersion})`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A capability manifest, as far as the regression check needs to understand one. */
|
|
93
|
+
interface CapabilityManifest {
|
|
94
|
+
contractVersion?: string;
|
|
95
|
+
tools?: Array<{ name?: string }>;
|
|
96
|
+
features?: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Decide whether a sibling manifest may replace the committed one.
|
|
101
|
+
*
|
|
102
|
+
* Exported for tests. Returns the reason to refuse, or undefined when adoption is safe.
|
|
103
|
+
*
|
|
104
|
+
* Two independent guards, because they catch different mistakes:
|
|
105
|
+
* - a lower `contractVersion` is a stale checkout, the case that actually happened;
|
|
106
|
+
* - a missing tool or feature key at the same-or-higher version is a broken build, which a version
|
|
107
|
+
* comparison alone would wave through.
|
|
108
|
+
*/
|
|
109
|
+
export function regressionReason(sibling: CapabilityManifest, vendored: CapabilityManifest): string | undefined {
|
|
110
|
+
const siblingVersion = sibling.contractVersion ?? "";
|
|
111
|
+
const vendoredVersion = vendored.contractVersion ?? "";
|
|
112
|
+
const versionOrder = compareContractVersions(siblingVersion, vendoredVersion);
|
|
113
|
+
if (versionOrder < 0) {
|
|
114
|
+
return `contractVersion would go backwards, ${vendoredVersion} -> ${siblingVersion}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const lostTools = missingKeys(
|
|
118
|
+
(vendored.tools ?? []).map(tool => tool.name ?? ""),
|
|
119
|
+
(sibling.tools ?? []).map(tool => tool.name ?? ""),
|
|
120
|
+
);
|
|
121
|
+
if (lostTools.length > 0) return `tools would be lost: ${lostTools.join(", ")}`;
|
|
122
|
+
|
|
123
|
+
const lostFeatures = missingKeys(Object.keys(vendored.features ?? {}), Object.keys(sibling.features ?? {}));
|
|
124
|
+
if (lostFeatures.length > 0) return `features would be lost: ${lostFeatures.join(", ")}`;
|
|
125
|
+
|
|
126
|
+
// Equal versions with different content mean one side changed without bumping, so the version says
|
|
127
|
+
// nothing about which is fresher. That is not hypothetical: a tool was once added while contractVersion
|
|
128
|
+
// stayed put, which is exactly how a stale same-version sibling could regress nested tool params or
|
|
129
|
+
// feature internals past a check that only compares names. Refuse and let the operator decide, via
|
|
130
|
+
// XCSH_EXTENSION_CAPABILITIES if they mean it.
|
|
131
|
+
if (versionOrder === 0 && !sameContent(sibling, vendored)) {
|
|
132
|
+
return `contractVersion is unchanged at ${vendoredVersion} but the content differs, so neither copy is provably fresher`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return undefined;
|
|
28
136
|
}
|
|
29
137
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Whole-manifest equality, by canonical JSON.
|
|
140
|
+
*
|
|
141
|
+
* Deliberately not a deep field-by-field comparison: the question is only "are these the same document",
|
|
142
|
+
* and any difference at an equal version is enough to refuse.
|
|
143
|
+
*/
|
|
144
|
+
function sameContent(left: CapabilityManifest, right: CapabilityManifest): boolean {
|
|
145
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** JSON with object keys sorted, so key order alone does not read as a difference. */
|
|
149
|
+
function canonicalJson(value: unknown): string {
|
|
150
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
151
|
+
if (value !== null && typeof value === "object") {
|
|
152
|
+
const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
153
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
154
|
+
}
|
|
155
|
+
return JSON.stringify(value) ?? "null";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function missingKeys(expected: readonly string[], actual: readonly string[]): string[] {
|
|
159
|
+
const present = new Set(actual.filter(Boolean));
|
|
160
|
+
return expected.filter(key => key !== "" && !present.has(key));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Numeric-segment comparison. Enough for a dotted contract version, and no dependency. */
|
|
164
|
+
export function compareContractVersions(left: string, right: string): number {
|
|
165
|
+
const parse = (value: string): number[] => value.split(".").map(part => Number.parseInt(part, 10) || 0);
|
|
166
|
+
const a = parse(left);
|
|
167
|
+
const b = parse(right);
|
|
168
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
169
|
+
const diff = (a[index] ?? 0) - (b[index] ?? 0);
|
|
170
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
171
|
+
}
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function adoptSiblingIfNotARegression(from: string, to: string): void {
|
|
176
|
+
let sibling: CapabilityManifest;
|
|
177
|
+
let vendored: CapabilityManifest;
|
|
178
|
+
try {
|
|
179
|
+
sibling = JSON.parse(fs.readFileSync(from, "utf8")) as CapabilityManifest;
|
|
180
|
+
vendored = JSON.parse(fs.readFileSync(to, "utf8")) as CapabilityManifest;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// Unreadable input must not silently leave the committed copy in place either — say so.
|
|
183
|
+
console.warn(`Keeping the committed capabilities: could not compare with ${from} (${String(error)}).`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const reason = regressionReason(sibling, vendored);
|
|
188
|
+
if (reason === undefined) {
|
|
189
|
+
fs.copyFileSync(from, to);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Refused rather than fatal: this generator runs from `check:types`, `test` and `build`, so failing
|
|
194
|
+
// here would block every command on any machine with a stale sibling checkout. Skipping keeps the
|
|
195
|
+
// committed contract — the correct one — and names what was ignored so it is fixable.
|
|
196
|
+
console.warn(
|
|
197
|
+
`Ignoring ${from}: ${reason}.\n` +
|
|
198
|
+
" The committed capabilities.json is being kept. Update or remove that checkout, or set\n" +
|
|
199
|
+
" XCSH_EXTENSION_CAPABILITIES to the manifest you actually want compiled in.",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.101.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.101.1",
|
|
21
|
+
"commit": "fbb716396d667153d37600b5c654893f7cdb3385",
|
|
22
|
+
"shortCommit": "fbb7163",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.101.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.101.1",
|
|
25
|
+
"commitDate": "2026-07-30T02:09:00Z",
|
|
26
|
+
"buildDate": "2026-07-30T02:36:13.835Z",
|
|
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/v19.101.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/fbb716396d667153d37600b5c654893f7cdb3385",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.101.1"
|
|
33
33
|
};
|