@f5-sales-demo/xcsh 21.32.0 → 21.33.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.
Files changed (32) hide show
  1. package/package.json +8 -8
  2. package/src/cli/plugin-cli.ts +161 -5
  3. package/src/commands/plugin.ts +2 -0
  4. package/src/extensibility/extensions/loader.ts +46 -6
  5. package/src/extensibility/extensions/runner.ts +7 -7
  6. package/src/extensibility/extensions/types.ts +9 -18
  7. package/src/extensibility/plugins/marketplace/builtin-marketplace-snapshot.ts +2 -443
  8. package/src/extensibility/plugins/marketplace/builtin-marketplace.provenance.json +3 -3
  9. package/src/extensibility/plugins/marketplace/fetcher.ts +74 -51
  10. package/src/extensibility/plugins/marketplace/types.ts +12 -7
  11. package/src/integrations/index.ts +3 -0
  12. package/src/integrations/registry.ts +290 -0
  13. package/src/integrations/setup.ts +49 -0
  14. package/src/integrations/types.ts +89 -0
  15. package/src/internal-urls/build-info.generated.ts +9 -9
  16. package/src/internal-urls/sitecli-index.generated.ts +57 -3
  17. package/src/modes/components/plugins/plugin-dashboard.ts +22 -43
  18. package/src/modes/components/plugins/state-manager.ts +2 -2
  19. package/src/modes/components/plugins/types.ts +4 -8
  20. package/src/modes/components/welcome-checks.ts +11 -1
  21. package/src/modes/rpc/rpc-mode.ts +17 -9
  22. package/src/person-profile/collectors.ts +3 -169
  23. package/src/person-profile/private-store.ts +3 -1
  24. package/src/person-profile/schema.ts +48 -3
  25. package/src/person-profile/service.ts +137 -18
  26. package/src/remote-control/lifecycle-state.ts +39 -3
  27. package/src/remote-control/startup-manager.ts +21 -5
  28. package/src/slash-commands/builtin-registry.ts +51 -25
  29. package/src/slash-commands/plugin-reviewed-actions.ts +7 -31
  30. package/src/extensibility/plugins/marketplace/prerequisites.ts +0 -255
  31. package/src/person-profile/account-collectors.ts +0 -209
  32. package/src/person-profile/salesforce-discovery.ts +0 -163
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "21.32.0",
4
+ "version": "21.33.0",
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",
@@ -63,13 +63,13 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@agentclientprotocol/sdk": "1.4.0",
66
- "@f5-sales-demo/pi-agent-core": "21.32.0",
67
- "@f5-sales-demo/pi-ai": "21.32.0",
68
- "@f5-sales-demo/pi-natives": "21.32.0",
69
- "@f5-sales-demo/pi-resource-management": "21.32.0",
70
- "@f5-sales-demo/pi-tui": "21.32.0",
71
- "@f5-sales-demo/pi-utils": "21.32.0",
72
- "@f5-sales-demo/xcsh-stats": "21.32.0",
66
+ "@f5-sales-demo/pi-agent-core": "21.33.0",
67
+ "@f5-sales-demo/pi-ai": "21.33.0",
68
+ "@f5-sales-demo/pi-natives": "21.33.0",
69
+ "@f5-sales-demo/pi-resource-management": "21.33.0",
70
+ "@f5-sales-demo/pi-tui": "21.33.0",
71
+ "@f5-sales-demo/pi-utils": "21.33.0",
72
+ "@f5-sales-demo/xcsh-stats": "21.33.0",
73
73
  "@mozilla/readability": "^0.6",
74
74
  "@sinclair/typebox": "0.34.52",
75
75
  "@xterm/headless": "^6.0",
@@ -4,9 +4,11 @@
4
4
  * Handles `xcsh plugin <command>` subcommands for plugin lifecycle management.
5
5
  */
6
6
 
7
+ import { createInterface } from "node:readline/promises";
7
8
  import { APP_NAME, getProjectDir } from "@f5-sales-demo/pi-utils";
8
9
  import chalk from "chalk";
9
10
  import { resolveOrDefaultProjectRegistryPath } from "../discovery/helpers";
11
+ import { discoverAndLoadExtensions } from "../extensibility/extensions";
10
12
  import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins";
11
13
  import {
12
14
  formatMarketplaceRefreshWarning,
@@ -16,7 +18,10 @@ import {
16
18
  getPluginsCacheDir,
17
19
  MarketplaceManager,
18
20
  } from "../extensibility/plugins/marketplace/index.js";
21
+ import { describeSetupPlan, executeReviewedSetup } from "../integrations/setup";
22
+ import type { IntegrationHandle } from "../integrations/types";
19
23
  import { theme } from "../modes/theme/theme";
24
+ import { personProfileService } from "../person-profile/service";
20
25
 
21
26
  // =============================================================================
22
27
  // Types
@@ -34,7 +39,9 @@ export type PluginAction =
34
39
  | "disable"
35
40
  | "marketplace"
36
41
  | "discover"
37
- | "upgrade";
42
+ | "upgrade"
43
+ | "status"
44
+ | "setup";
38
45
 
39
46
  export interface PluginCommandArgs {
40
47
  action: PluginAction;
@@ -69,6 +76,8 @@ const VALID_ACTIONS: PluginAction[] = [
69
76
  "marketplace",
70
77
  "discover",
71
78
  "upgrade",
79
+ "status",
80
+ "setup",
72
81
  ];
73
82
 
74
83
  /**
@@ -187,6 +196,149 @@ export async function runPluginCommand(cmd: PluginCommandArgs): Promise<void> {
187
196
  case "upgrade":
188
197
  await handleUpgrade(cmd.args, cmd.flags);
189
198
  break;
199
+ case "status":
200
+ await handleIntegrationStatus(cmd.args, cmd.flags);
201
+ break;
202
+ case "setup":
203
+ await handleIntegrationSetup(cmd.args, cmd.flags);
204
+ break;
205
+ }
206
+ }
207
+
208
+ async function loadIntegrationHandles(): Promise<IntegrationHandle<unknown>[]> {
209
+ const loaded = await discoverAndLoadExtensions([], process.cwd());
210
+ if (loaded.errors.length) throw new Error(`Unable to load ${loaded.errors.length} plugin extension(s)`);
211
+ return loaded.extensions.flatMap(extension => [...extension.integrations.values()]);
212
+ }
213
+
214
+ function matchesIntegration(handle: IntegrationHandle<unknown>, target: string): boolean {
215
+ return handle.id === target || handle.plugin === target || handle.plugin?.split("@")[0] === target;
216
+ }
217
+
218
+ export function selectSetupIntegration(
219
+ handles: readonly IntegrationHandle<unknown>[],
220
+ target: string,
221
+ ): IntegrationHandle<unknown> {
222
+ const matches = handles.filter(handle => matchesIntegration(handle, target));
223
+ if (!matches.length) throw new Error(`No enabled integration is registered for ${target}`);
224
+ const setupMatches = matches.filter(handle => handle.setupPlan !== undefined);
225
+ if (!setupMatches.length) throw new Error(`No setup plan is registered for ${target}`);
226
+ if (setupMatches.length > 1)
227
+ throw new Error(`Plugin ${target} registers multiple setup plans; use an integration id`);
228
+ return setupMatches[0];
229
+ }
230
+
231
+ async function handleIntegrationStatus(args: string[], flags: { json?: boolean }): Promise<void> {
232
+ if (args.length > 1) throw new Error(`Usage: ${APP_NAME} plugin status [plugin] [--json]`);
233
+ const handles = await loadIntegrationHandles();
234
+ const selected = args[0] ? handles.filter(handle => matchesIntegration(handle, args[0])) : handles;
235
+ if (args[0] && !selected.length) throw new Error(`No enabled integration is registered for ${args[0]}`);
236
+ const statuses = await Promise.all(
237
+ selected.map(async handle => {
238
+ const { value: _, ...status } = await handle.get();
239
+ return {
240
+ ...status,
241
+ nextAction:
242
+ status.state === "setup_required" ? `${APP_NAME} plugin setup ${handle.plugin ?? handle.id}` : undefined,
243
+ };
244
+ }),
245
+ );
246
+ if (flags.json) {
247
+ process.stdout.write(`${JSON.stringify({ integrations: statuses })}\n`);
248
+ return;
249
+ }
250
+ if (!statuses.length) {
251
+ process.stdout.write("No enabled plugin integrations are registered.\n");
252
+ return;
253
+ }
254
+ for (const status of statuses) {
255
+ process.stdout.write(
256
+ `${status.plugin ?? status.id}: ${status.state}${status.reason ? ` (${status.reason})` : ""}\n`,
257
+ );
258
+ if (status.nextAction) process.stdout.write(` next: ${status.nextAction}\n`);
259
+ }
260
+ }
261
+
262
+ export async function reviewAndExecuteIntegrationSetup(handle: IntegrationHandle<unknown>) {
263
+ const plan = handle.setupPlan;
264
+ if (!plan) throw new Error(`Integration ${handle.id} does not declare setup`);
265
+ const current = await handle.get();
266
+ if (current.state === "ready") return current;
267
+ process.stdout.write(`${describeSetupPlan(handle)}\n`);
268
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
269
+ try {
270
+ const answer = await prompt.question('Type "setup" to run this exact plan: ');
271
+ if (answer !== "setup") throw new Error("Plugin setup cancelled; the installed plugin remains setup_required");
272
+ } finally {
273
+ prompt.close();
274
+ }
275
+ const result = await executeReviewedSetup(handle, plan);
276
+ await personProfileService.reconcileFromCollectors(undefined, 0);
277
+ return result;
278
+ }
279
+
280
+ async function handleIntegrationSetup(args: string[], flags: { json?: boolean }): Promise<void> {
281
+ if (flags.json) throw new Error("plugin setup is human-only and does not accept --json");
282
+ if (args.length !== 1) throw new Error(`Usage: ${APP_NAME} plugin setup <plugin>`);
283
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Plugin setup requires an interactive terminal");
284
+ const handle = selectSetupIntegration(await loadIntegrationHandles(), args[0]);
285
+ const current = await handle.get();
286
+ if (current.state === "ready") {
287
+ process.stdout.write(`${handle.plugin ?? handle.id}: ready (setup is not required)\n`);
288
+ return;
289
+ }
290
+ const result = await reviewAndExecuteIntegrationSetup(handle);
291
+ process.stdout.write(
292
+ `${handle.plugin ?? handle.id}: ${result.state}${result.reason ? ` (${result.reason})` : ""}\n`,
293
+ );
294
+ }
295
+
296
+ async function reportMarketplaceInstallation(
297
+ plugin: string,
298
+ marketplace: string,
299
+ entry: { version: string; scope: string; setupRequired: boolean },
300
+ flags: { json?: boolean },
301
+ ): Promise<void> {
302
+ let handles: IntegrationHandle<unknown>[] = [];
303
+ try {
304
+ handles = (await loadIntegrationHandles()).filter(handle => matchesIntegration(handle, plugin));
305
+ } catch {
306
+ // Installation remains successful even when extension loading cannot establish readiness.
307
+ }
308
+ const statuses = await Promise.all(
309
+ handles.map(async handle => {
310
+ const { value: _, ...status } = await handle.get();
311
+ return status;
312
+ }),
313
+ );
314
+ const setupHandles = handles.filter(handle => handle.setupPlan !== undefined);
315
+ const setupHandle = setupHandles.length === 1 ? setupHandles[0] : undefined;
316
+ const setupStatus = setupHandle ? statuses.find(status => status.id === setupHandle.id) : undefined;
317
+ const nextAction =
318
+ entry.setupRequired && setupStatus?.state !== "ready" ? `${APP_NAME} plugin setup ${plugin}` : undefined;
319
+ if (flags.json || !process.stdin.isTTY || !process.stdout.isTTY) {
320
+ const result = {
321
+ installation: { state: "installed", plugin, marketplace, version: entry.version, scope: entry.scope },
322
+ readiness: { integrations: statuses },
323
+ ...(nextAction ? { nextAction } : {}),
324
+ };
325
+ if (flags.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
326
+ else {
327
+ process.stdout.write(`${theme.status.success} Installed ${plugin} from ${marketplace} (${entry.version})\n`);
328
+ for (const status of statuses)
329
+ process.stdout.write(` ${status.id}: ${status.state}${status.reason ? ` (${status.reason})` : ""}\n`);
330
+ if (nextAction) process.stdout.write(` next: ${nextAction}\n`);
331
+ }
332
+ return;
333
+ }
334
+ process.stdout.write(`${theme.status.success} Installed ${plugin} from ${marketplace} (${entry.version})\n`);
335
+ if (!setupHandle || setupStatus?.state === "ready") return;
336
+ try {
337
+ const result = await reviewAndExecuteIntegrationSetup(setupHandle);
338
+ process.stdout.write(`${plugin}: ${result.state}${result.reason ? ` (${result.reason})` : ""}\n`);
339
+ } catch (error) {
340
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
341
+ process.stderr.write(`Next: ${APP_NAME} plugin setup ${plugin}\n`);
190
342
  }
191
343
  }
192
344
 
@@ -461,6 +613,9 @@ async function handleInstall(
461
613
  }
462
614
 
463
615
  if (target.type === "marketplace") {
616
+ const catalogEntry = listings.find(
617
+ item => item.marketplace === target.marketplace && item.plugin.name === target.name,
618
+ )?.plugin;
464
619
  if (flags.dryRun) {
465
620
  const preview = {
466
621
  action: "install",
@@ -477,10 +632,11 @@ async function handleInstall(
477
632
  force: flags.force,
478
633
  scope: flags.scope,
479
634
  });
480
- console.log(
481
- chalk.green(
482
- `${theme.status.success} Installed ${target.name} from ${target.marketplace} (${entry.version})`,
483
- ),
635
+ await reportMarketplaceInstallation(
636
+ target.name,
637
+ target.marketplace,
638
+ { ...entry, setupRequired: catalogEntry?.lifecycle.setupRequired ?? false },
639
+ flags,
484
640
  );
485
641
  } catch (err) {
486
642
  console.error(chalk.red(`${theme.status.error} Failed to install ${spec}: ${err}`));
@@ -18,6 +18,8 @@ const ACTIONS: PluginAction[] = [
18
18
  "marketplace",
19
19
  "discover",
20
20
  "upgrade",
21
+ "status",
22
+ "setup",
21
23
  ];
22
24
 
23
25
  export default class Plugin extends Command {
@@ -15,6 +15,8 @@ import { loadCapability } from "../../discovery";
15
15
  import { getExtensionNameFromPath, getPreloadedPluginRoots } from "../../discovery/helpers";
16
16
  import type { ExecOptions } from "../../exec/exec";
17
17
  import { execCommand } from "../../exec/exec";
18
+ import { integrationRegistry } from "../../integrations/registry";
19
+ import type { IntegrationDefinition, IntegrationHandle } from "../../integrations/types";
18
20
  import { personProfileService } from "../../person-profile/service";
19
21
  import type { CustomMessage } from "../../session/messages";
20
22
  import { EventBus } from "../../utils/event-bus";
@@ -33,7 +35,6 @@ import type {
33
35
  LoadExtensionsResult,
34
36
  MessageRenderer,
35
37
  RegisteredCommand,
36
- ServiceStatusContribution,
37
38
  ToolDefinition,
38
39
  } from "./types";
39
40
 
@@ -122,6 +123,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
122
123
  sourceId: string;
123
124
  }> = [];
124
125
  readonly personProfile: ExtensionAPI["personProfile"];
126
+ readonly integrations: ExtensionAPI["integrations"];
125
127
 
126
128
  constructor(
127
129
  public readonly pi: typeof import("@f5-sales-demo/xcsh"),
@@ -131,6 +133,8 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
131
133
  public readonly events: EventBus,
132
134
  ) {
133
135
  const registrant = extension.resolvedPath;
136
+ integrationRegistry.unregisterOwner(registrant);
137
+ personProfileService.unregisterProfileCollectorsByRegistrant(registrant);
134
138
  this.personProfile = Object.freeze({
135
139
  get: () => personProfileService.get(),
136
140
  registerCollector: collector => {
@@ -140,6 +144,46 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
140
144
  },
141
145
  unregisterCollector: (id: string) => personProfileService.unregisterProfileCollector(id, registrant),
142
146
  });
147
+ this.integrations = Object.freeze({
148
+ register: <T>(definition: IntegrationDefinition<T>): IntegrationHandle<T> => {
149
+ const handle = integrationRegistry.register(registrant, definition);
150
+ extension.integrations.set(definition.id, handle as IntegrationHandle<unknown>);
151
+ if (definition.profile) {
152
+ const profile = definition.profile;
153
+ personProfileService.registerProfileCollector(
154
+ {
155
+ id: definition.id,
156
+ name: definition.name,
157
+ dependsOn: definition.dependencies,
158
+ available: async () => true,
159
+ collect: async signal => {
160
+ const snapshot = await handle.get(signal);
161
+ if (snapshot.state !== "ready" || snapshot.value === undefined)
162
+ return {
163
+ facts: {},
164
+ observations: [],
165
+ sourceState: {
166
+ state: snapshot.state,
167
+ ...(snapshot.reason ? { reason: snapshot.reason } : {}),
168
+ ...(snapshot.retryAt ? { retryAt: new Date(snapshot.retryAt).toISOString() } : {}),
169
+ },
170
+ };
171
+ return { ...profile(snapshot.value), sourceState: { state: snapshot.state } };
172
+ },
173
+ },
174
+ registrant,
175
+ );
176
+ }
177
+ return handle;
178
+ },
179
+ unregister: (id: string): boolean => {
180
+ const removed = integrationRegistry.unregister(registrant, id);
181
+ if (!removed) return false;
182
+ extension.integrations.delete(id);
183
+ personProfileService.unregisterProfileCollector(id, registrant);
184
+ return true;
185
+ },
186
+ });
143
187
  }
144
188
 
145
189
  on<F extends HandlerFn>(event: string, handler: F): void {
@@ -194,10 +238,6 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
194
238
  this.extension.messageRenderers.set(customType, renderer as MessageRenderer);
195
239
  }
196
240
 
197
- registerServiceStatus(contribution: ServiceStatusContribution): void {
198
- this.extension.serviceStatuses.set(contribution.name, contribution);
199
- }
200
-
201
241
  getFlag(name: string): boolean | string | undefined {
202
242
  if (!this.extension.flags.has(name)) return undefined;
203
243
  return this.runtime.flagValues.get(name);
@@ -279,7 +319,7 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
279
319
  commands: new Map(),
280
320
  flags: new Map(),
281
321
  shortcuts: new Map(),
282
- serviceStatuses: new Map(),
322
+ integrations: new Map(),
283
323
  };
284
324
  }
285
325
 
@@ -6,6 +6,7 @@ import type { ImageContent, Model } from "@f5-sales-demo/pi-ai";
6
6
  import type { KeyId } from "@f5-sales-demo/pi-tui";
7
7
  import { logger } from "@f5-sales-demo/pi-utils";
8
8
  import type { ModelRegistry } from "../../config/model-registry";
9
+ import type { IntegrationHandle } from "../../integrations/types";
9
10
  import { type Theme, theme } from "../../modes/theme/theme";
10
11
  import type { SessionManager } from "../../session/session-manager";
11
12
  import type {
@@ -36,7 +37,6 @@ import type {
36
37
  RegisteredTool,
37
38
  ResourcesDiscoverEvent,
38
39
  ResourcesDiscoverResult,
39
- ServiceStatusContribution,
40
40
  SessionBeforeBranchResult,
41
41
  SessionBeforeCompactResult,
42
42
  SessionBeforeSwitchResult,
@@ -263,15 +263,15 @@ export class ExtensionRunner {
263
263
  return tools;
264
264
  }
265
265
 
266
- /** Get all registered service status contributions from all extensions. */
267
- getAllRegisteredServiceStatuses(): ServiceStatusContribution[] {
268
- const statuses: ServiceStatusContribution[] = [];
266
+ /** Get all integration handles registered by the loaded extensions. */
267
+ getAllRegisteredIntegrations(): IntegrationHandle<unknown>[] {
268
+ const integrations: IntegrationHandle<unknown>[] = [];
269
269
  for (const ext of this.extensions) {
270
- for (const contribution of ext.serviceStatuses.values()) {
271
- statuses.push(contribution);
270
+ for (const handle of ext.integrations.values()) {
271
+ integrations.push(handle);
272
272
  }
273
273
  }
274
- return statuses;
274
+ return integrations;
275
275
  }
276
276
 
277
277
  getFlags(): Map<string, ExtensionFlag> {
@@ -974,20 +974,6 @@ export interface RegisteredCommand {
974
974
  handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
975
975
  }
976
976
 
977
- // ============================================================================
978
- // Service Status Contributions
979
- // ============================================================================
980
-
981
- export interface ServiceStatusContribution {
982
- name: string;
983
- group?: string;
984
- check: () => Promise<{ state: "connected" | "unauthenticated" | "unavailable"; hint?: string }>;
985
- fix?: {
986
- prompt: string;
987
- command: string[];
988
- };
989
- }
990
-
991
977
  // ============================================================================
992
978
  // Extension API
993
979
  // ============================================================================
@@ -1020,6 +1006,14 @@ export interface ExtensionAPI {
1020
1006
  unregisterCollector(id: string): boolean;
1021
1007
  };
1022
1008
 
1009
+ /** Process-wide provider readiness, setup and profile-discovery lifecycle. */
1010
+ readonly integrations: {
1011
+ register<T>(
1012
+ definition: import("../../integrations/types").IntegrationDefinition<T>,
1013
+ ): import("../../integrations/types").IntegrationHandle<T>;
1014
+ unregister(id: string): boolean;
1015
+ };
1016
+
1023
1017
  // =========================================================================
1024
1018
  // Event Subscription
1025
1019
  // =========================================================================
@@ -1129,9 +1123,6 @@ export interface ExtensionAPI {
1129
1123
  /** Register a custom renderer for CustomMessageEntry. */
1130
1124
  registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
1131
1125
 
1132
- /** Register a service status check for the welcome screen. */
1133
- registerServiceStatus(contribution: ServiceStatusContribution): void;
1134
-
1135
1126
  // =========================================================================
1136
1127
  // Actions
1137
1128
  // =========================================================================
@@ -1422,7 +1413,7 @@ export interface Extension {
1422
1413
  commands: Map<string, RegisteredCommand>;
1423
1414
  flags: Map<string, ExtensionFlag>;
1424
1415
  shortcuts: Map<KeyId, ExtensionShortcut>;
1425
- serviceStatuses: Map<string, ServiceStatusContribution>;
1416
+ integrations: Map<string, import("../../integrations/types").IntegrationHandle<unknown>>;
1426
1417
  }
1427
1418
 
1428
1419
  /** Result of loading extensions. */