@f5xc-salesdemos/xcsh 19.9.1 → 19.10.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": "@f5xc-salesdemos/xcsh",
4
- "version": "19.9.1",
4
+ "version": "19.10.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -50,12 +50,12 @@
50
50
  "dependencies": {
51
51
  "@agentclientprotocol/sdk": "0.16.1",
52
52
  "@mozilla/readability": "^0.6",
53
- "@f5xc-salesdemos/xcsh-stats": "19.9.1",
54
- "@f5xc-salesdemos/pi-agent-core": "19.9.1",
55
- "@f5xc-salesdemos/pi-ai": "19.9.1",
56
- "@f5xc-salesdemos/pi-natives": "19.9.1",
57
- "@f5xc-salesdemos/pi-tui": "19.9.1",
58
- "@f5xc-salesdemos/pi-utils": "19.9.1",
53
+ "@f5xc-salesdemos/xcsh-stats": "19.10.0",
54
+ "@f5xc-salesdemos/pi-agent-core": "19.10.0",
55
+ "@f5xc-salesdemos/pi-ai": "19.10.0",
56
+ "@f5xc-salesdemos/pi-natives": "19.10.0",
57
+ "@f5xc-salesdemos/pi-tui": "19.10.0",
58
+ "@f5xc-salesdemos/pi-utils": "19.10.0",
59
59
  "@sinclair/typebox": "^0.34",
60
60
  "@xterm/headless": "^6.0",
61
61
  "ajv": "^8.20",
@@ -1,29 +1,230 @@
1
- import type { MarketplacePluginEntry } from "./types";
1
+ import { logger } from "@f5xc-salesdemos/pi-utils";
2
2
 
3
- const cache = new Map<string, boolean>();
3
+ // ── Types ────────────────────────────────────────────────────────────────────
4
+
5
+ export interface Prerequisite {
6
+ tool: string;
7
+ installCmd: string;
8
+ detectCmd: string;
9
+ authDetectCmd?: string;
10
+ authLoginCmd?: string;
11
+ }
12
+
13
+ export interface ToolStatus {
14
+ tool: string;
15
+ installed: boolean;
16
+ authenticated: boolean;
17
+ user?: string;
18
+ error?: string;
19
+ }
20
+
21
+ export interface SetupResult {
22
+ tool: string;
23
+ wasInstalled: boolean;
24
+ installAttempted: boolean;
25
+ installSuccess: boolean;
26
+ authenticated: boolean;
27
+ authLoginCmd?: string;
28
+ user?: string;
29
+ error?: string;
30
+ }
31
+
32
+ // ── Cache ────────────────────────────────────────────────────────────────────
33
+
34
+ const detectCache = new Map<string, boolean>();
35
+
36
+ export function clearPrerequisiteCache(): void {
37
+ detectCache.clear();
38
+ }
39
+
40
+ // ── Retry utility ────────────────────────────────────────────────────────────
41
+
42
+ export async function withRetry<T>(
43
+ fn: () => Promise<T>,
44
+ opts: { maxRetries?: number; baseDelayMs?: number; label?: string } = {},
45
+ ): Promise<T> {
46
+ const maxRetries = opts.maxRetries ?? 3;
47
+ const baseDelayMs = opts.baseDelayMs ?? 1000;
48
+ let lastError: unknown;
49
+
50
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
51
+ try {
52
+ return await fn();
53
+ } catch (err) {
54
+ lastError = err;
55
+ if (attempt < maxRetries) {
56
+ const delay = baseDelayMs * 2 ** attempt;
57
+ logger.debug(`Retry ${attempt + 1}/${maxRetries} for ${opts.label ?? "operation"} in ${delay}ms`);
58
+ await new Promise(resolve => setTimeout(resolve, delay));
59
+ }
60
+ }
61
+ }
62
+
63
+ throw lastError;
64
+ }
65
+
66
+ // ── Tool detection ───────────────────────────────────────────────────────────
4
67
 
5
68
  export async function checkPrerequisite(detectCmd: string): Promise<boolean> {
6
- const cached = cache.get(detectCmd);
69
+ const cached = detectCache.get(detectCmd);
7
70
  if (cached !== undefined) return cached;
8
71
 
9
72
  try {
10
73
  const [cmd, ...args] = detectCmd.split(/\s+/);
11
- const proc = Bun.spawn([cmd!, ...args], {
12
- stdout: "ignore",
13
- stderr: "ignore",
14
- });
74
+ const proc = Bun.spawn([cmd!, ...args], { stdout: "ignore", stderr: "ignore" });
15
75
  const exitCode = await proc.exited;
16
76
  const available = exitCode === 0;
17
- cache.set(detectCmd, available);
77
+ detectCache.set(detectCmd, available);
18
78
  return available;
19
79
  } catch {
20
- cache.set(detectCmd, false);
80
+ detectCache.set(detectCmd, false);
21
81
  return false;
22
82
  }
23
83
  }
24
84
 
85
+ // ── Tool installation ────────────────────────────────────────────────────────
86
+
87
+ export async function installTool(
88
+ installCmd: string,
89
+ opts?: { maxRetries?: number; baseDelayMs?: number },
90
+ ): Promise<{ success: boolean; error?: string }> {
91
+ try {
92
+ await withRetry(
93
+ async () => {
94
+ const parts = installCmd.split(/\s+/);
95
+ const proc = Bun.spawn(parts, { stdout: "ignore", stderr: "pipe" });
96
+ const exitCode = await proc.exited;
97
+ if (exitCode !== 0) {
98
+ const stderr = await new Response(proc.stderr).text();
99
+ throw new Error(stderr.trim() || `exit code ${exitCode}`);
100
+ }
101
+ },
102
+ { maxRetries: opts?.maxRetries ?? 2, baseDelayMs: opts?.baseDelayMs ?? 2000, label: installCmd },
103
+ );
104
+ return { success: true };
105
+ } catch (err) {
106
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
107
+ }
108
+ }
109
+
110
+ // ── Auth detection ───────────────────────────────────────────────────────────
111
+
112
+ export async function checkAuth(
113
+ authDetectCmd: string,
114
+ ): Promise<{ authenticated: boolean; user?: string; error?: string }> {
115
+ try {
116
+ const parts = authDetectCmd.split(/\s+/);
117
+ const proc = Bun.spawn(parts, { stdout: "pipe", stderr: "pipe" });
118
+ const exitCode = await proc.exited;
119
+
120
+ if (exitCode !== 0) {
121
+ return { authenticated: false };
122
+ }
123
+
124
+ let user: string | undefined;
125
+ try {
126
+ const stdout = await new Response(proc.stdout).text();
127
+ const parsed = JSON.parse(stdout);
128
+ user = parsed?.user?.name ?? parsed?.Account ?? parsed?.login ?? parsed?.username;
129
+ } catch {
130
+ // Non-JSON output is fine — exit code 0 means authenticated
131
+ }
132
+
133
+ return { authenticated: true, user };
134
+ } catch {
135
+ return { authenticated: false, error: "auth check command failed" };
136
+ }
137
+ }
138
+
139
+ // ── Full tool readiness check ────────────────────────────────────────────────
140
+
141
+ export async function checkToolReady(prereq: Prerequisite): Promise<ToolStatus> {
142
+ const installed = await checkPrerequisite(prereq.detectCmd);
143
+ if (!installed) {
144
+ return { tool: prereq.tool, installed: false, authenticated: false };
145
+ }
146
+
147
+ if (!prereq.authDetectCmd) {
148
+ return { tool: prereq.tool, installed: true, authenticated: true };
149
+ }
150
+
151
+ const auth = await checkAuth(prereq.authDetectCmd);
152
+ return {
153
+ tool: prereq.tool,
154
+ installed: true,
155
+ authenticated: auth.authenticated,
156
+ user: auth.user,
157
+ };
158
+ }
159
+
160
+ // ── Full setup orchestration for one tool ────────────────────────────────────
161
+
162
+ export async function setupTool(prereq: Prerequisite): Promise<SetupResult> {
163
+ // Step 1: Check if already installed
164
+ let installed = await checkPrerequisite(prereq.detectCmd);
165
+ let installAttempted = false;
166
+ let installSuccess = false;
167
+
168
+ if (!installed) {
169
+ // Step 2: Attempt installation with retry
170
+ installAttempted = true;
171
+ detectCache.delete(prereq.detectCmd);
172
+ const result = await installTool(prereq.installCmd);
173
+ installSuccess = result.success;
174
+
175
+ if (!result.success) {
176
+ return {
177
+ tool: prereq.tool,
178
+ wasInstalled: false,
179
+ installAttempted: true,
180
+ installSuccess: false,
181
+ authenticated: false,
182
+ error: `Install failed: ${result.error}`,
183
+ };
184
+ }
185
+
186
+ // Verify installation
187
+ detectCache.delete(prereq.detectCmd);
188
+ installed = await checkPrerequisite(prereq.detectCmd);
189
+ if (!installed) {
190
+ return {
191
+ tool: prereq.tool,
192
+ wasInstalled: false,
193
+ installAttempted: true,
194
+ installSuccess: false,
195
+ authenticated: false,
196
+ error: "Install appeared to succeed but tool not found on PATH",
197
+ };
198
+ }
199
+ }
200
+
201
+ // Step 3: Check auth
202
+ if (!prereq.authDetectCmd) {
203
+ return {
204
+ tool: prereq.tool,
205
+ wasInstalled: !installAttempted,
206
+ installAttempted,
207
+ installSuccess: installAttempted ? true : false,
208
+ authenticated: true,
209
+ };
210
+ }
211
+
212
+ const auth = await checkAuth(prereq.authDetectCmd);
213
+ return {
214
+ tool: prereq.tool,
215
+ wasInstalled: !installAttempted,
216
+ installAttempted,
217
+ installSuccess: installAttempted ? true : false,
218
+ authenticated: auth.authenticated,
219
+ user: auth.user,
220
+ authLoginCmd: auth.authenticated ? undefined : prereq.authLoginCmd,
221
+ };
222
+ }
223
+
224
+ // ── Batch operations ─────────────────────────────────────────────────────────
225
+
25
226
  export async function checkAllPrerequisites(
26
- plugins: MarketplacePluginEntry[],
227
+ plugins: Array<{ name: string; prerequisites?: Prerequisite[] }>,
27
228
  ): Promise<Map<string, { available: boolean; missing: string[] }>> {
28
229
  const results = new Map<string, { available: boolean; missing: string[] }>();
29
230
 
@@ -45,6 +246,10 @@ export async function checkAllPrerequisites(
45
246
  return results;
46
247
  }
47
248
 
48
- export function clearPrerequisiteCache(): void {
49
- cache.clear();
249
+ export async function setupAllTools(prerequisites: Prerequisite[]): Promise<SetupResult[]> {
250
+ const results: SetupResult[] = [];
251
+ for (const prereq of prerequisites) {
252
+ results.push(await setupTool(prereq));
253
+ }
254
+ return results;
50
255
  }
@@ -90,7 +90,13 @@ export interface MarketplacePluginEntry {
90
90
  strict?: boolean;
91
91
  defaultEnabled?: boolean;
92
92
  recommended?: boolean;
93
- prerequisites?: Array<{ tool: string; installCmd: string; detectCmd: string }>;
93
+ prerequisites?: Array<{
94
+ tool: string;
95
+ installCmd: string;
96
+ detectCmd: string;
97
+ authDetectCmd?: string;
98
+ authLoginCmd?: string;
99
+ }>;
94
100
  commands?: string | string[];
95
101
  agents?: string | string[];
96
102
  hooks?: string | Record<string, unknown>;
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.9.1",
21
- "commit": "d3150a05f1d9eb0b91c234b970ed08b8f679ad3f",
22
- "shortCommit": "d3150a0",
20
+ "version": "19.10.0",
21
+ "commit": "d46f8e9c1e3828c21cfa36c04509b3ec562d3d07",
22
+ "shortCommit": "d46f8e9",
23
23
  "branch": "main",
24
- "tag": "v19.9.1",
25
- "commitDate": "2026-06-05T02:56:19Z",
26
- "buildDate": "2026-06-05T03:25:42.133Z",
24
+ "tag": "v19.10.0",
25
+ "commitDate": "2026-06-05T04:49:35Z",
26
+ "buildDate": "2026-06-05T05:11:51.351Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
30
30
  "repoSlug": "f5xc-salesdemos/xcsh",
31
- "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/d3150a05f1d9eb0b91c234b970ed08b8f679ad3f",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.9.1"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/d46f8e9c1e3828c21cfa36c04509b3ec562d3d07",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.10.0"
33
33
  };
@@ -290,6 +290,7 @@ export class PluginDashboard extends Container {
290
290
  }
291
291
 
292
292
  async #installAllRecommended(): Promise<void> {
293
+ const { setupTool } = await import("../../../extensibility/plugins/marketplace/prerequisites");
293
294
  const recommended = this.#state.allPlugins.filter(p => !p.installed && p.recommended && p.marketplace);
294
295
  if (recommended.length === 0) {
295
296
  this.#state.notice = "All recommended plugins are already installed";
@@ -297,12 +298,33 @@ export class PluginDashboard extends Container {
297
298
  return;
298
299
  }
299
300
 
300
- this.#state.notice = `Installing ${recommended.length} recommended plugin(s)...`;
301
- this.#rebuildAndRender();
302
-
303
301
  let installed = 0;
304
302
  let failed = 0;
303
+ const authNeeded: string[] = [];
304
+
305
305
  for (const plugin of recommended) {
306
+ this.#state.notice = `Setting up ${plugin.displayName || plugin.name}... (${installed + failed + 1}/${recommended.length})`;
307
+ this.#rebuildAndRender();
308
+
309
+ // Check and install prerequisites
310
+ if (plugin.prerequisites && plugin.prerequisites.length > 0) {
311
+ let prereqReady = true;
312
+ for (const prereq of plugin.prerequisites) {
313
+ const result = await setupTool(prereq);
314
+ if (!result.installSuccess && result.installAttempted) {
315
+ prereqReady = false;
316
+ break;
317
+ }
318
+ if (!result.authenticated && prereq.authLoginCmd) {
319
+ authNeeded.push(`${prereq.tool}: ${prereq.authLoginCmd}`);
320
+ }
321
+ }
322
+ if (!prereqReady) {
323
+ failed++;
324
+ continue;
325
+ }
326
+ }
327
+
306
328
  try {
307
329
  await this.#mgr.installPlugin(plugin.name, plugin.marketplace!);
308
330
  installed++;
@@ -311,10 +333,10 @@ export class PluginDashboard extends Container {
311
333
  }
312
334
  }
313
335
 
314
- this.#state.notice =
315
- failed > 0
316
- ? `Installed ${installed}/${recommended.length} recommended plugin(s), ${failed} failed`
317
- : `Installed ${installed} recommended plugin(s)`;
336
+ const parts = [`Installed ${installed}/${recommended.length} recommended plugin(s)`];
337
+ if (failed > 0) parts.push(`${failed} failed`);
338
+ if (authNeeded.length > 0) parts.push(`Auth needed: ${authNeeded.join(", ")}`);
339
+ this.#state.notice = parts.join(". ");
318
340
  await this.#reloadData();
319
341
  }
320
342
 
@@ -19,7 +19,13 @@ export interface DashboardPlugin {
19
19
  hasUpdate: boolean;
20
20
  updateVersion?: string;
21
21
  recommended?: boolean;
22
- prerequisites?: Array<{ tool: string; installCmd: string; detectCmd: string }>;
22
+ prerequisites?: Array<{
23
+ tool: string;
24
+ installCmd: string;
25
+ detectCmd: string;
26
+ authDetectCmd?: string;
27
+ authLoginCmd?: string;
28
+ }>;
23
29
  }
24
30
 
25
31
  export type PluginTabId = "installed" | "recommended" | "discover" | "updates";
@@ -1164,7 +1164,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1164
1164
  }
1165
1165
  // ── Setup (guided recommended plugin install) ──
1166
1166
  case "setup": {
1167
- const { checkPrerequisite } = await import("../extensibility/plugins/marketplace/prerequisites");
1167
+ const { setupTool } = await import("../extensibility/plugins/marketplace/prerequisites");
1168
1168
  const allPlugins = await mgr.listAvailablePlugins();
1169
1169
  const recommended = allPlugins.filter(p => p.recommended);
1170
1170
  if (recommended.length === 0) {
@@ -1172,66 +1172,86 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1172
1172
  break;
1173
1173
  }
1174
1174
 
1175
- const installed = await mgr.listInstalledPlugins();
1176
- const installedIds = new Set(installed.map(p => p.id));
1177
- const toInstall = recommended.filter(
1175
+ const installedPlugins = await mgr.listInstalledPlugins();
1176
+ const installedIds = new Set(installedPlugins.map(p => p.id));
1177
+ const toSetup = recommended.filter(
1178
1178
  p => !Array.from(installedIds).some(id => id.startsWith(`${p.name}@`)),
1179
1179
  );
1180
1180
 
1181
- if (toInstall.length === 0) {
1181
+ if (toSetup.length === 0) {
1182
1182
  runtime.ctx.showStatus("All recommended plugins are already installed");
1183
1183
  break;
1184
1184
  }
1185
1185
 
1186
1186
  const lines: string[] = ["Recommended plugins setup:\n"];
1187
- let installedCount = 0;
1187
+ let pluginInstalledCount = 0;
1188
1188
  let skippedCount = 0;
1189
- const skippedReasons: string[] = [];
1190
1189
 
1191
- for (const plugin of toInstall) {
1190
+ for (const plugin of toSetup) {
1191
+ const name = plugin.displayName || plugin.name;
1192
+
1193
+ // Step 1: Setup prerequisites (detect → install → auth)
1192
1194
  if (plugin.prerequisites && plugin.prerequisites.length > 0) {
1193
- const missing: string[] = [];
1195
+ let allReady = true;
1194
1196
  for (const prereq of plugin.prerequisites) {
1195
- const ok = await checkPrerequisite(prereq.detectCmd);
1196
- if (!ok) missing.push(`${prereq.tool} (${prereq.installCmd})`);
1197
+ const result = await setupTool(prereq);
1198
+
1199
+ if (!result.installSuccess && result.installAttempted) {
1200
+ lines.push(` x ${name} — ${prereq.tool}: install failed (${result.error})`);
1201
+ lines.push(` Fix: ${prereq.installCmd}`);
1202
+ allReady = false;
1203
+ break;
1204
+ }
1205
+
1206
+ if (result.installAttempted && result.installSuccess) {
1207
+ lines.push(` + ${name} — ${prereq.tool}: installed`);
1208
+ }
1209
+
1210
+ if (!result.authenticated && prereq.authLoginCmd) {
1211
+ lines.push(` ~ ${name} — ${prereq.tool}: not authenticated`);
1212
+ lines.push(` Run: ${prereq.authLoginCmd}`);
1213
+ } else if (result.authenticated && result.user) {
1214
+ lines.push(` ✓ ${name} — ${prereq.tool}: authenticated as ${result.user}`);
1215
+ } else if (result.authenticated) {
1216
+ lines.push(` ✓ ${name} — ${prereq.tool}: ready`);
1217
+ }
1197
1218
  }
1198
- if (missing.length > 0) {
1199
- lines.push(` ⊘ ${plugin.displayName || plugin.name} — missing: ${missing.join(", ")}`);
1219
+ if (!allReady) {
1200
1220
  skippedCount++;
1201
- skippedReasons.push(...missing);
1202
1221
  continue;
1203
1222
  }
1204
1223
  }
1205
1224
 
1225
+ // Step 2: Install the plugin
1206
1226
  const marketplaces = await mgr.listMarketplaces();
1207
- let installed = false;
1227
+ let didInstall = false;
1208
1228
  for (const mkt of marketplaces) {
1209
1229
  const available = await mgr.listAvailablePlugins(mkt.name);
1210
1230
  if (available.some(a => a.name === plugin.name)) {
1211
1231
  try {
1212
1232
  await mgr.installPlugin(plugin.name, mkt.name);
1213
- lines.push(` + ${plugin.displayName || plugin.name} — installed`);
1214
- installedCount++;
1215
- installed = true;
1233
+ lines.push(` ${name} — plugin installed`);
1234
+ pluginInstalledCount++;
1235
+ didInstall = true;
1216
1236
  } catch (err) {
1217
1237
  lines.push(
1218
- ` ! ${plugin.displayName || plugin.name} — ${err instanceof Error ? err.message : String(err)}`,
1238
+ ` ! ${name} — plugin install failed: ${err instanceof Error ? err.message : String(err)}`,
1219
1239
  );
1220
1240
  skippedCount++;
1221
1241
  }
1222
1242
  break;
1223
1243
  }
1224
1244
  }
1225
- if (!installed && skippedCount === 0) {
1226
- lines.push(` ? ${plugin.displayName || plugin.name} — not found in any marketplace`);
1245
+ if (!didInstall && skippedCount === 0) {
1246
+ lines.push(` ? ${name} — not found in any marketplace`);
1227
1247
  skippedCount++;
1228
1248
  }
1229
1249
  }
1230
1250
 
1231
1251
  lines.push("");
1232
- lines.push(`Installed ${installedCount}/${toInstall.length} recommended plugin(s)`);
1252
+ lines.push(`Installed ${pluginInstalledCount}/${toSetup.length} recommended plugin(s)`);
1233
1253
  if (skippedCount > 0) {
1234
- lines.push(`${skippedCount} skipped — install missing tools and run /plugin setup again`);
1254
+ lines.push(`${skippedCount} skipped — fix issues above and run /plugin setup again (idempotent)`);
1235
1255
  }
1236
1256
  runtime.ctx.showStatus(lines.join("\n"));
1237
1257
  break;