@f5-sales-demo/xcsh 19.85.2 → 19.85.3

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.85.2",
4
+ "version": "19.85.3",
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",
@@ -56,13 +56,13 @@
56
56
  "dependencies": {
57
57
  "@agentclientprotocol/sdk": "0.16.1",
58
58
  "@mozilla/readability": "^0.6",
59
- "@f5-sales-demo/xcsh-stats": "19.85.2",
60
- "@f5-sales-demo/pi-agent-core": "19.85.2",
61
- "@f5-sales-demo/pi-ai": "19.85.2",
62
- "@f5-sales-demo/pi-natives": "19.85.2",
63
- "@f5-sales-demo/pi-resource-management": "19.85.2",
64
- "@f5-sales-demo/pi-tui": "19.85.2",
65
- "@f5-sales-demo/pi-utils": "19.85.2",
59
+ "@f5-sales-demo/xcsh-stats": "19.85.3",
60
+ "@f5-sales-demo/pi-agent-core": "19.85.3",
61
+ "@f5-sales-demo/pi-ai": "19.85.3",
62
+ "@f5-sales-demo/pi-natives": "19.85.3",
63
+ "@f5-sales-demo/pi-resource-management": "19.85.3",
64
+ "@f5-sales-demo/pi-tui": "19.85.3",
65
+ "@f5-sales-demo/pi-utils": "19.85.3",
66
66
  "@sinclair/typebox": "^0.34",
67
67
  "@xterm/headless": "^6.0",
68
68
  "ajv": "^8.20",
@@ -205,12 +205,16 @@ export class ChatHandler {
205
205
  return;
206
206
  }
207
207
  if (event.type === "tool_execution_end" && "toolName" in event) {
208
+ // ToolExecutionEndEvent carries `isError` (NOT `error`) — checking the wrong
209
+ // field made this always ok:true, so errored tools rendered ✓ and "failed"
210
+ // never fired. Read the real field.
211
+ const failed = "isError" in event && Boolean(event.isError);
208
212
  this.#server.send({
209
213
  type: "chat_tool_notice",
210
214
  id: chat.id,
211
215
  tool: String(event.toolName),
212
- ok: !("error" in event && event.error),
213
- detail: `${event.toolName}: ${"error" in event && event.error ? "failed" : "done"}`,
216
+ ok: !failed,
217
+ detail: `${event.toolName}: ${failed ? "failed" : "done"}`,
214
218
  });
215
219
  return;
216
220
  }
@@ -2,11 +2,17 @@
2
2
  * Classify an install spec as a marketplace plugin reference or a plain npm package.
3
3
  *
4
4
  * Rules (applied in order):
5
+ * 0. `npm:<pkg>` prefix -> always npm (explicit escape hatch; the prefix is stripped).
5
6
  * 1. Starts with `@` (scoped npm) -> always npm.
6
7
  * 2. Contains `@` after the first character -> split on the LAST `@`.
7
8
  * If the right-hand side is a known marketplace name, it's a marketplace ref.
8
9
  * Otherwise it's an npm spec (e.g. `pkg@1.2.3`).
9
- * 3. No `@` -> npm.
10
+ * 3. Bare name (no `@`) -> look it up against the marketplace catalog index:
11
+ * - exactly one marketplace publishes it -> marketplace ref;
12
+ * - more than one -> ambiguous (caller errors and asks for `name@marketplace`);
13
+ * - none (or no index supplied) -> npm.
14
+ * This is what makes `xcsh plugin install azure` resolve to the marketplace
15
+ * `azure` plugin rather than the public npm package of the same name.
10
16
  */
11
17
  // Common npm dist-tags that should never be interpreted as marketplace names
12
18
  const NPM_DIST_TAGS = new Set([
@@ -25,10 +31,19 @@ const NPM_DIST_TAGS = new Set([
25
31
  // Semver-like: starts with digit, or contains version range prefixes
26
32
  const LOOKS_LIKE_VERSION = /^[\d~^>=<]/;
27
33
 
34
+ export type InstallTarget =
35
+ | { type: "marketplace"; name: string; marketplace: string }
36
+ | { type: "ambiguous"; name: string; marketplaces: string[] }
37
+ | { type: "npm"; spec: string };
38
+
28
39
  export function classifyInstallTarget(
29
40
  spec: string,
30
41
  knownMarketplaces: Set<string>,
31
- ): { type: "marketplace"; name: string; marketplace: string } | { type: "npm"; spec: string } {
42
+ /** plugin name -> marketplaces whose catalog publishes a plugin with that name */
43
+ catalogIndex?: Map<string, string[]>,
44
+ ): InstallTarget {
45
+ // Rule 0: explicit npm escape hatch — force npm even if a marketplace shares the name.
46
+ if (spec.startsWith("npm:")) return { type: "npm", spec: spec.slice("npm:".length) };
32
47
  // Rule 1: scoped npm package — @ at position 0 is never a marketplace separator.
33
48
  if (spec.startsWith("@")) return { type: "npm", spec };
34
49
  // Rule 2: @ somewhere after the first character.
@@ -45,6 +60,14 @@ export function classifyInstallTarget(
45
60
  // Not a known marketplace — treat as npm version specifier.
46
61
  return { type: "npm", spec };
47
62
  }
48
- // Rule 3: no @ at all.
63
+ // Rule 3: bare name resolve against the marketplace catalog index before npm.
64
+ const sources = catalogIndex?.get(spec);
65
+ if (sources && sources.length === 1) {
66
+ return { type: "marketplace", name: spec, marketplace: sources[0] };
67
+ }
68
+ if (sources && sources.length > 1) {
69
+ return { type: "ambiguous", name: spec, marketplaces: sources };
70
+ }
71
+ // Rule 4: no marketplace publishes this name — plain npm package.
49
72
  return { type: "npm", spec };
50
73
  }
@@ -311,10 +311,10 @@ async function handleUpgrade(args: string[], flags: PluginCommandArgs["flags"]):
311
311
  try {
312
312
  if (pluginId) {
313
313
  if (flags.scope) {
314
- const result = await manager.upgradePlugin(pluginId, flags.scope);
314
+ const result = await manager.upgradePlugin(pluginId, flags.scope, { refresh: true });
315
315
  console.log(chalk.green(`Upgraded ${pluginId} (${flags.scope}) to ${result.version}`));
316
316
  } else {
317
- const entries = await manager.upgradePluginAcrossScopes(pluginId);
317
+ const entries = await manager.upgradePluginAcrossScopes(pluginId, { refresh: true });
318
318
  for (const entry of entries) {
319
319
  console.log(chalk.green(`Upgraded ${pluginId} (${entry.scope}) to ${entry.version}`));
320
320
  }
@@ -327,7 +327,7 @@ async function handleUpgrade(args: string[], flags: PluginCommandArgs["flags"]):
327
327
  ),
328
328
  );
329
329
  }
330
- const results = await manager.upgradeAllPlugins();
330
+ const results = await manager.upgradeAllPlugins({ refresh: true });
331
331
  if (results.length === 0) {
332
332
  console.log("All marketplace plugins are up to date.");
333
333
  } else {
@@ -355,12 +355,39 @@ async function handleInstall(
355
355
  process.exit(1);
356
356
  }
357
357
 
358
- // Build known marketplace set for classification
358
+ // Build classification inputs: known marketplace names, plus a catalog index
359
+ // (plugin name -> marketplaces that publish it) so a bare name resolves to the
360
+ // marketplace plugin instead of a same-named public npm package.
359
361
  const mktMgr = await makeMarketplaceManager();
360
- const knownMarketplaces = new Set((await mktMgr.listMarketplaces()).map(m => m.name));
362
+ const marketplaces = await mktMgr.listMarketplaces();
363
+ const knownMarketplaces = new Set(marketplaces.map(m => m.name));
364
+ const catalogIndex = new Map<string, string[]>();
365
+ for (const mp of marketplaces) {
366
+ let plugins: Awaited<ReturnType<typeof mktMgr.listAvailablePlugins>>;
367
+ try {
368
+ plugins = await mktMgr.listAvailablePlugins(mp.name);
369
+ } catch {
370
+ continue; // a broken/uncloned catalog must not block installs from others
371
+ }
372
+ for (const p of plugins) {
373
+ const sources = catalogIndex.get(p.name) ?? [];
374
+ if (!sources.includes(mp.name)) sources.push(mp.name);
375
+ catalogIndex.set(p.name, sources);
376
+ }
377
+ }
361
378
 
362
379
  for (const spec of packages) {
363
- const target = classifyInstallTarget(spec, knownMarketplaces);
380
+ const target = classifyInstallTarget(spec, knownMarketplaces, catalogIndex);
381
+
382
+ if (target.type === "ambiguous") {
383
+ console.error(
384
+ chalk.red(
385
+ `${theme.status.error} "${target.name}" is published by multiple marketplaces: ${target.marketplaces.join(", ")}.`,
386
+ ),
387
+ );
388
+ console.error(chalk.dim(`Disambiguate with ${target.name}@<marketplace>.`));
389
+ process.exit(1);
390
+ }
364
391
 
365
392
  if (target.type === "marketplace") {
366
393
  try {
@@ -563,9 +563,13 @@ export class MarketplaceManager {
563
563
  // Compare installed plugin versions against their catalog entries.
564
564
  // Returns one entry per (pluginId, scope) pair where the catalog declares a newer version.
565
565
  // Catalog entries without a version field are skipped.
566
- async checkForUpdates(): Promise<
567
- Array<{ pluginId: string; scope: "user" | "project" | "local"; from: string; to: string }>
568
- > {
566
+ async checkForUpdates(opts?: {
567
+ refresh?: boolean;
568
+ }): Promise<Array<{ pluginId: string; scope: "user" | "project" | "local"; from: string; to: string }>> {
569
+ // Explicit upgrade flows pass refresh:true to re-fetch catalogs from source before
570
+ // comparing, so freshly-published versions are seen. Passive callers (startup notify,
571
+ // dashboard poll) omit it and rely on the 24h TTL (refreshStaleMarketplaces).
572
+ if (opts?.refresh) await this.updateAllMarketplaces();
569
573
  const mktReg = await readMarketplacesRegistry(this.#opts.marketplacesRegistryPath);
570
574
  const updates: Array<{ pluginId: string; scope: "user" | "project" | "local"; from: string; to: string }> = [];
571
575
 
@@ -620,7 +624,12 @@ export class MarketplaceManager {
620
624
  }
621
625
 
622
626
  // Re-install a specific plugin at the latest catalog version (force-overwrites).
623
- async upgradePlugin(pluginId: string, scope?: "user" | "project" | "local"): Promise<InstalledPluginEntry> {
627
+ async upgradePlugin(
628
+ pluginId: string,
629
+ scope?: "user" | "project" | "local",
630
+ opts?: { refresh?: boolean },
631
+ ): Promise<InstalledPluginEntry> {
632
+ if (opts?.refresh) await this.updateAllMarketplaces();
624
633
  const parsed = parsePluginId(pluginId);
625
634
  if (!parsed) {
626
635
  throw new Error(`Invalid plugin ID: "${pluginId}". Expected "name@marketplace".`);
@@ -656,7 +665,8 @@ export class MarketplaceManager {
656
665
 
657
666
  // Upgrade a plugin across all scopes where it is installed.
658
667
  // Returns one entry per scope upgraded (0–2 entries).
659
- async upgradePluginAcrossScopes(pluginId: string): Promise<InstalledPluginEntry[]> {
668
+ async upgradePluginAcrossScopes(pluginId: string, opts?: { refresh?: boolean }): Promise<InstalledPluginEntry[]> {
669
+ if (opts?.refresh) await this.updateAllMarketplaces();
660
670
  const parsed = parsePluginId(pluginId);
661
671
  if (!parsed) {
662
672
  throw new Error(`Invalid plugin ID: "${pluginId}". Expected "name@marketplace".`);
@@ -688,10 +698,10 @@ export class MarketplaceManager {
688
698
  // Upgrade every (pluginId, scope) pair that checkForUpdates reports as outdated.
689
699
  // Only stale scopes are touched; a current user install is not re-installed when only
690
700
  // the project scope is stale. Per-entry failures are skipped — partial success is returned.
691
- async upgradeAllPlugins(): Promise<
692
- Array<{ pluginId: string; scope: "user" | "project" | "local"; from: string; to: string }>
693
- > {
694
- const updates = await this.checkForUpdates();
701
+ async upgradeAllPlugins(opts?: {
702
+ refresh?: boolean;
703
+ }): Promise<Array<{ pluginId: string; scope: "user" | "project" | "local"; from: string; to: string }>> {
704
+ const updates = await this.checkForUpdates(opts);
695
705
  const results: Array<{ pluginId: string; scope: "user" | "project" | "local"; from: string; to: string }> = [];
696
706
  for (const update of updates) {
697
707
  try {
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.85.2",
21
- "commit": "a068b469d7c985c518847bf2862acabc1605f490",
22
- "shortCommit": "a068b46",
20
+ "version": "19.85.3",
21
+ "commit": "a3a789009c484dbdf11840b17a59588794b8d37f",
22
+ "shortCommit": "a3a7890",
23
23
  "branch": "main",
24
- "tag": "v19.85.2",
25
- "commitDate": "2026-07-23T18:30:49Z",
26
- "buildDate": "2026-07-23T18:54:23.656Z",
24
+ "tag": "v19.85.3",
25
+ "commitDate": "2026-07-23T20:38:04Z",
26
+ "buildDate": "2026-07-23T21:04:16.479Z",
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/a068b469d7c985c518847bf2862acabc1605f490",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.85.2"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/a3a789009c484dbdf11840b17a59588794b8d37f",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.85.3"
33
33
  };
@@ -353,7 +353,7 @@ export class PluginDashboard extends Container {
353
353
  this.#rebuildAndRender();
354
354
 
355
355
  try {
356
- await this.#mgr.upgradePlugin(plugin.id, plugin.scope);
356
+ await this.#mgr.upgradePlugin(plugin.id, plugin.scope, { refresh: true });
357
357
  this.#state.notice = t("plugins.dashboard.upgraded", { name: plugin.name });
358
358
  await this.#reloadData();
359
359
  } catch (error) {
@@ -1144,12 +1144,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1144
1144
  runtime.ctx.showStatus(upArgs.error);
1145
1145
  return;
1146
1146
  }
1147
- const result = await mgr.upgradePlugin(upArgs.pluginId, upArgs.scope);
1147
+ const result = await mgr.upgradePlugin(upArgs.pluginId, upArgs.scope, { refresh: true });
1148
1148
  runtime.ctx.showStatus(
1149
1149
  t("commands.plugin.upgraded", { pluginId: upArgs.pluginId, version: result.version }),
1150
1150
  );
1151
1151
  } else {
1152
- const results = await mgr.upgradeAllPlugins();
1152
+ const results = await mgr.upgradeAllPlugins({ refresh: true });
1153
1153
  if (results.length === 0) {
1154
1154
  runtime.ctx.showStatus(t("commands.plugin.allUpToDate"));
1155
1155
  } else {