@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.0",
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.0",
61
- "@f5-sales-demo/pi-agent-core": "19.101.0",
62
- "@f5-sales-demo/pi-ai": "19.101.0",
63
- "@f5-sales-demo/pi-natives": "19.101.0",
64
- "@f5-sales-demo/pi-resource-management": "19.101.0",
65
- "@f5-sales-demo/pi-tui": "19.101.0",
66
- "@f5-sales-demo/pi-utils": "19.101.0",
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
- // Co-build refresh: prefer the sibling extension's freshly-built contract.
24
- const siblingPath =
25
- process.env.XCSH_EXTENSION_CAPABILITIES ?? path.resolve(here, "../../../../xcsh-chrome-extension/capabilities.json");
26
- if (fs.existsSync(siblingPath) && path.resolve(siblingPath) !== vendoredPath) {
27
- fs.copyFileSync(siblingPath, vendoredPath);
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
- const manifest = JSON.parse(fs.readFileSync(vendoredPath, "utf8")) as {
31
- contractVersion: string;
32
- tools: Array<{ name: string }>;
33
- };
34
- const toolNames = manifest.tools.map(t => t.name);
35
-
36
- const output = [
37
- "// Auto-generated by scripts/generate-extension-capabilities.ts - DO NOT EDIT",
38
- "// Source: src/browser/capabilities.json (the Chrome extension's published contract).",
39
- "",
40
- "export interface ExtensionToolFlags {",
41
- "\treadonly readOnly?: boolean;",
42
- "\treadonly mutates?: boolean;",
43
- "\treadonly requiresExplainMode?: boolean;",
44
- "}",
45
- "",
46
- "export interface ExtensionToolDef {",
47
- "\treadonly name: string;",
48
- "\treadonly summary: string;",
49
- "\treadonly category: string;",
50
- "\treadonly params: Record<string, unknown>;",
51
- "\treadonly flags?: ExtensionToolFlags;",
52
- "}",
53
- "",
54
- "export interface ExtensionCapabilities {",
55
- "\treadonly version: string;",
56
- "\treadonly contractVersion: string;",
57
- "\treadonly multiPortDiscovery?: boolean;",
58
- "\treadonly protocol: string;",
59
- "\treadonly tools: readonly ExtensionToolDef[];",
60
- "\treadonly features: Record<string, unknown>;",
61
- "}",
62
- "",
63
- `export const EXTENSION_CAPABILITIES: ExtensionCapabilities = ${JSON.stringify(manifest, null, "\t")};`,
64
- "",
65
- `export const EXTENSION_CONTRACT_VERSION = ${JSON.stringify(manifest.contractVersion)};`,
66
- "",
67
- `export const EXTENSION_TOOL_NAMES: readonly string[] = ${JSON.stringify(toolNames)};`,
68
- "",
69
- ].join("\n");
70
-
71
- await Bun.write(generatedPath, output);
72
- console.log(
73
- `Generated ${path.relative(process.cwd(), generatedPath)} (${toolNames.length} tools, contract ${manifest.contractVersion})`,
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.0",
21
- "commit": "389279d7ee7ae16a82af6b03d9f6ea3920e8925e",
22
- "shortCommit": "389279d",
20
+ "version": "19.101.1",
21
+ "commit": "fbb716396d667153d37600b5c654893f7cdb3385",
22
+ "shortCommit": "fbb7163",
23
23
  "branch": "main",
24
- "tag": "v19.101.0",
25
- "commitDate": "2026-07-29T21:37:59Z",
26
- "buildDate": "2026-07-29T22:01:44.081Z",
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/389279d7ee7ae16a82af6b03d9f6ea3920e8925e",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.101.0"
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
  };