@f5xc-salesdemos/xcsh 19.9.1 → 19.11.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 +7 -7
- package/src/extensibility/plugins/marketplace/prerequisites.ts +217 -12
- package/src/extensibility/plugins/marketplace/types.ts +7 -1
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/modes/components/plugins/plugin-dashboard.ts +29 -7
- package/src/modes/components/plugins/state-manager.ts +3 -6
- package/src/modes/components/plugins/types.ts +7 -1
- package/src/modes/components/plugins/utils.ts +6 -0
- package/src/modes/components/welcome-checks.ts +50 -0
- package/src/modes/components/welcome.ts +35 -2
- package/src/modes/interactive-mode.ts +4 -0
- package/src/slash-commands/builtin-registry.ts +43 -23
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.11.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.
|
|
54
|
-
"@f5xc-salesdemos/pi-agent-core": "19.
|
|
55
|
-
"@f5xc-salesdemos/pi-ai": "19.
|
|
56
|
-
"@f5xc-salesdemos/pi-natives": "19.
|
|
57
|
-
"@f5xc-salesdemos/pi-tui": "19.
|
|
58
|
-
"@f5xc-salesdemos/pi-utils": "19.
|
|
53
|
+
"@f5xc-salesdemos/xcsh-stats": "19.11.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-agent-core": "19.11.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-ai": "19.11.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-natives": "19.11.0",
|
|
57
|
+
"@f5xc-salesdemos/pi-tui": "19.11.0",
|
|
58
|
+
"@f5xc-salesdemos/pi-utils": "19.11.0",
|
|
59
59
|
"@sinclair/typebox": "^0.34",
|
|
60
60
|
"@xterm/headless": "^6.0",
|
|
61
61
|
"ajv": "^8.20",
|
|
@@ -1,29 +1,230 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { logger } from "@f5xc-salesdemos/pi-utils";
|
|
2
2
|
|
|
3
|
-
|
|
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 =
|
|
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
|
-
|
|
77
|
+
detectCache.set(detectCmd, available);
|
|
18
78
|
return available;
|
|
19
79
|
} catch {
|
|
20
|
-
|
|
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:
|
|
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
|
|
49
|
-
|
|
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<{
|
|
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.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.11.0",
|
|
21
|
+
"commit": "f1aa8804fd9d391462a8c13d4ff0de70b01e20b8",
|
|
22
|
+
"shortCommit": "f1aa880",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-06-
|
|
26
|
-
"buildDate": "2026-06-
|
|
24
|
+
"tag": "v19.11.0",
|
|
25
|
+
"commitDate": "2026-06-05T12:59:46Z",
|
|
26
|
+
"buildDate": "2026-06-05T13:30:16.635Z",
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/f1aa8804fd9d391462a8c13d4ff0de70b01e20b8",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.11.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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
|
@@ -3,12 +3,9 @@ import type { MarketplaceManager } from "../../../extensibility/plugins/marketpl
|
|
|
3
3
|
import type { InstalledPluginSummary, MarketplacePluginEntry } from "../../../extensibility/plugins/marketplace/types";
|
|
4
4
|
import type { DashboardPlugin, PluginDashboardState, PluginTab, PluginTabId } from "./types";
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (result.length > 0 && result.endsWith("-status")) result = result.slice(0, -7);
|
|
10
|
-
return result || name;
|
|
11
|
-
}
|
|
6
|
+
export { normalizePluginDisplayName } from "./utils";
|
|
7
|
+
|
|
8
|
+
import { normalizePluginDisplayName } from "./utils";
|
|
12
9
|
|
|
13
10
|
function npmToDashboard(npm: { name: string; version: string; enabled: boolean }): DashboardPlugin {
|
|
14
11
|
return {
|
|
@@ -19,7 +19,13 @@ export interface DashboardPlugin {
|
|
|
19
19
|
hasUpdate: boolean;
|
|
20
20
|
updateVersion?: string;
|
|
21
21
|
recommended?: boolean;
|
|
22
|
-
prerequisites?: Array<{
|
|
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";
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { Model } from "@f5xc-salesdemos/pi-ai";
|
|
2
2
|
import { validateApiKeyAgainstModelsEndpoint } from "@f5xc-salesdemos/pi-ai/utils/oauth/api-key-validation";
|
|
3
3
|
import { logger } from "@f5xc-salesdemos/pi-utils";
|
|
4
|
+
import { MarketplaceManager } from "../../extensibility/plugins/marketplace";
|
|
5
|
+
import {
|
|
6
|
+
getInstalledPluginsRegistryPath,
|
|
7
|
+
getMarketplacesCacheDir,
|
|
8
|
+
getMarketplacesRegistryPath,
|
|
9
|
+
getPluginsCacheDir,
|
|
10
|
+
} from "../../extensibility/plugins/marketplace/registry";
|
|
4
11
|
import { type AuthStatus, ContextService } from "../../services/f5xc-context";
|
|
5
12
|
import { deriveTenantFromUrl } from "../../services/f5xc-env";
|
|
6
13
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
14
|
+
import { normalizePluginDisplayName } from "./plugins/utils";
|
|
7
15
|
|
|
8
16
|
// Startup validation budget. These are longer than validateToken's 3000ms default because
|
|
9
17
|
// the welcome path runs during TLS/DNS cold-start — a single 3s shot races against warm-up
|
|
@@ -214,3 +222,45 @@ export interface FixableService {
|
|
|
214
222
|
command: string[];
|
|
215
223
|
recheck: () => Promise<ServiceStatus>;
|
|
216
224
|
}
|
|
225
|
+
|
|
226
|
+
export interface RecommendedPluginStatus {
|
|
227
|
+
name: string;
|
|
228
|
+
installed: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function checkRecommendedPlugins(): Promise<RecommendedPluginStatus[]> {
|
|
232
|
+
try {
|
|
233
|
+
const mgr = new MarketplaceManager({
|
|
234
|
+
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
235
|
+
installedRegistryPath: getInstalledPluginsRegistryPath(),
|
|
236
|
+
marketplacesCacheDir: getMarketplacesCacheDir(),
|
|
237
|
+
pluginsCacheDir: getPluginsCacheDir(),
|
|
238
|
+
clearPluginRootsCache: () => {},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const [marketplaces, installedSummaries] = await Promise.all([
|
|
242
|
+
mgr.listMarketplaces(),
|
|
243
|
+
mgr.listInstalledPlugins(),
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
const installedIds = new Set(installedSummaries.map(s => s.id));
|
|
247
|
+
const results: RecommendedPluginStatus[] = [];
|
|
248
|
+
|
|
249
|
+
for (const mkt of marketplaces) {
|
|
250
|
+
const available = await mgr.listAvailablePlugins(mkt.name).catch(() => []);
|
|
251
|
+
for (const entry of available) {
|
|
252
|
+
if (!entry.recommended) continue;
|
|
253
|
+
const pluginId = `${entry.name}@${mkt.name}`;
|
|
254
|
+
results.push({
|
|
255
|
+
name: normalizePluginDisplayName(entry.name),
|
|
256
|
+
installed: installedIds.has(pluginId),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
262
|
+
} catch (err) {
|
|
263
|
+
logger.debug("checkRecommendedPlugins failed", { error: String(err) });
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -2,7 +2,7 @@ import { type Component, padding, truncateToWidth, visibleWidth } from "@f5xc-sa
|
|
|
2
2
|
import { APP_NAME } from "@f5xc-salesdemos/pi-utils";
|
|
3
3
|
import { theme } from "../../modes/theme/theme";
|
|
4
4
|
import { formatStatusIcon } from "../../services/f5xc-context-indicators";
|
|
5
|
-
import type { ModelStatus, ServiceStatus } from "./welcome-checks";
|
|
5
|
+
import type { ModelStatus, RecommendedPluginStatus, ServiceStatus } from "./welcome-checks";
|
|
6
6
|
|
|
7
7
|
export interface UpdateStatus {
|
|
8
8
|
available: boolean;
|
|
@@ -15,6 +15,7 @@ export class WelcomeComponent implements Component {
|
|
|
15
15
|
private modelStatus: ModelStatus,
|
|
16
16
|
private services: ServiceStatus[] = [],
|
|
17
17
|
private updateStatus?: UpdateStatus,
|
|
18
|
+
private recommendedPlugins: RecommendedPluginStatus[] = [],
|
|
18
19
|
) {}
|
|
19
20
|
invalidate(): void {}
|
|
20
21
|
setModelStatus(status: ModelStatus): void {
|
|
@@ -26,6 +27,9 @@ export class WelcomeComponent implements Component {
|
|
|
26
27
|
setUpdateStatus(status: UpdateStatus | undefined): void {
|
|
27
28
|
this.updateStatus = status;
|
|
28
29
|
}
|
|
30
|
+
setRecommendedPlugins(plugins: RecommendedPluginStatus[]): void {
|
|
31
|
+
this.recommendedPlugins = plugins;
|
|
32
|
+
}
|
|
29
33
|
|
|
30
34
|
render(termWidth: number): string[] {
|
|
31
35
|
const minLeftCol = 48;
|
|
@@ -136,6 +140,12 @@ export class WelcomeComponent implements Component {
|
|
|
136
140
|
lines.push(this.#renderServiceLine(svc));
|
|
137
141
|
}
|
|
138
142
|
}
|
|
143
|
+
if (this.recommendedPlugins.length > 0) {
|
|
144
|
+
lines.push(" Recommended Plugins");
|
|
145
|
+
for (const p of this.recommendedPlugins) {
|
|
146
|
+
lines.push(this.#renderRecommendedLine(p));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
139
149
|
if (this.#showUpdateSection()) {
|
|
140
150
|
lines.push(this.#renderUpdateLine());
|
|
141
151
|
}
|
|
@@ -163,7 +173,11 @@ export class WelcomeComponent implements Component {
|
|
|
163
173
|
lines.push("");
|
|
164
174
|
const coreServices = this.services.filter(s => !s._isPlugin);
|
|
165
175
|
const pluginServices = this.services.filter(s => s._isPlugin);
|
|
166
|
-
const hasContent =
|
|
176
|
+
const hasContent =
|
|
177
|
+
coreServices.length > 0 ||
|
|
178
|
+
pluginServices.length > 0 ||
|
|
179
|
+
this.recommendedPlugins.length > 0 ||
|
|
180
|
+
this.#showUpdateSection();
|
|
167
181
|
if (hasContent) {
|
|
168
182
|
lines.push(separator);
|
|
169
183
|
for (const svc of coreServices) {
|
|
@@ -179,6 +193,18 @@ export class WelcomeComponent implements Component {
|
|
|
179
193
|
}
|
|
180
194
|
}
|
|
181
195
|
}
|
|
196
|
+
if (this.recommendedPlugins.length > 0) {
|
|
197
|
+
lines.push("");
|
|
198
|
+
lines.push(` ${theme.fg("dim", "Recommended Plugins")}`);
|
|
199
|
+
for (const p of this.recommendedPlugins) {
|
|
200
|
+
lines.push(this.#renderRecommendedLine(p));
|
|
201
|
+
}
|
|
202
|
+
const missing = this.recommendedPlugins.filter(p => !p.installed);
|
|
203
|
+
if (missing.length > 0) {
|
|
204
|
+
lines.push(` ${theme.fg("dim", "run: /plugin setup")}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
182
208
|
if (this.#showUpdateSection()) {
|
|
183
209
|
lines.push(this.#renderUpdateLine());
|
|
184
210
|
}
|
|
@@ -187,6 +213,13 @@ export class WelcomeComponent implements Component {
|
|
|
187
213
|
return lines;
|
|
188
214
|
}
|
|
189
215
|
|
|
216
|
+
#renderRecommendedLine(plugin: RecommendedPluginStatus): string {
|
|
217
|
+
if (plugin.installed) {
|
|
218
|
+
return ` ${formatStatusIcon("connected")} ${theme.fg("muted", plugin.name)}`;
|
|
219
|
+
}
|
|
220
|
+
return ` ${theme.fg("dim", "·")} ${theme.fg("dim", plugin.name)}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
190
223
|
#showUpdateSection(): boolean {
|
|
191
224
|
return this.updateStatus?.available === true;
|
|
192
225
|
}
|
|
@@ -53,6 +53,7 @@ import { StatusLineComponent } from "./components/status-line";
|
|
|
53
53
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
54
54
|
import { type UpdateStatus, WelcomeComponent } from "./components/welcome";
|
|
55
55
|
import {
|
|
56
|
+
checkRecommendedPlugins,
|
|
56
57
|
type FixableService,
|
|
57
58
|
mapContextStatus,
|
|
58
59
|
runWelcomeChecks,
|
|
@@ -386,12 +387,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
386
387
|
}
|
|
387
388
|
}
|
|
388
389
|
|
|
390
|
+
const recommendedPlugins = !startupQuiet ? await checkRecommendedPlugins().catch(() => []) : [];
|
|
391
|
+
|
|
389
392
|
if (!startupQuiet) {
|
|
390
393
|
this.#welcomeComponent = new WelcomeComponent(
|
|
391
394
|
this.#version,
|
|
392
395
|
welcomeResult.model,
|
|
393
396
|
services,
|
|
394
397
|
this.#initialUpdateStatus,
|
|
398
|
+
recommendedPlugins,
|
|
395
399
|
);
|
|
396
400
|
|
|
397
401
|
// Setup UI layout
|
|
@@ -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 {
|
|
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
|
|
1176
|
-
const installedIds = new Set(
|
|
1177
|
-
const
|
|
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 (
|
|
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
|
|
1187
|
+
let pluginInstalledCount = 0;
|
|
1188
1188
|
let skippedCount = 0;
|
|
1189
|
-
const skippedReasons: string[] = [];
|
|
1190
1189
|
|
|
1191
|
-
for (const plugin of
|
|
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
|
-
|
|
1195
|
+
let allReady = true;
|
|
1194
1196
|
for (const prereq of plugin.prerequisites) {
|
|
1195
|
-
const
|
|
1196
|
-
|
|
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 (
|
|
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
|
|
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(`
|
|
1214
|
-
|
|
1215
|
-
|
|
1233
|
+
lines.push(` ✓ ${name} — plugin installed`);
|
|
1234
|
+
pluginInstalledCount++;
|
|
1235
|
+
didInstall = true;
|
|
1216
1236
|
} catch (err) {
|
|
1217
1237
|
lines.push(
|
|
1218
|
-
` ! ${
|
|
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 (!
|
|
1226
|
-
lines.push(` ? ${
|
|
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 ${
|
|
1252
|
+
lines.push(`Installed ${pluginInstalledCount}/${toSetup.length} recommended plugin(s)`);
|
|
1233
1253
|
if (skippedCount > 0) {
|
|
1234
|
-
lines.push(`${skippedCount} skipped —
|
|
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;
|