@f5xc-salesdemos/xcsh 18.87.3 → 18.89.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/CHANGELOG.md +6 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +0 -11
- package/src/extensibility/extensions/loader.ts +6 -0
- package/src/extensibility/extensions/runner.ts +12 -0
- package/src/extensibility/extensions/types.ts +17 -0
- package/src/internal-urls/build-info-runtime.ts +0 -1
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/index.ts +0 -1
- package/src/internal-urls/profile-collectors.ts +1 -104
- package/src/internal-urls/user-profile.ts +7 -11
- package/src/internal-urls/xcsh-protocol.ts +4 -26
- package/src/modes/components/welcome-checks.ts +0 -121
- package/src/modes/interactive-mode.ts +51 -23
- package/src/modes/rpc/rpc-mode.ts +17 -7
- package/src/prompts/system/system-prompt.md +0 -8
- package/src/sdk.ts +1 -13
- package/src/system-prompt.ts +0 -20
- package/src/tools/index.ts +0 -7
- package/src/tools/renderers.ts +0 -5
- package/src/internal-urls/salesforce-context.ts +0 -745
- package/src/pipeline-report/benchmark.ts +0 -405
- package/src/pipeline-report/generator.ts +0 -684
- package/src/pipeline-report/index.ts +0 -3
- package/src/pipeline-report/renderer.ts +0 -306
- package/src/pipeline-report/types.ts +0 -166
- package/src/prompts/tools/sf-org-display.md +0 -7
- package/src/prompts/tools/sf-pipeline-report.md +0 -25
- package/src/prompts/tools/sf-query.md +0 -122
- package/src/prompts/tools/sf-setup.md +0 -10
- package/src/tools/sf/exec.ts +0 -104
- package/src/tools/sf/formatters.ts +0 -150
- package/src/tools/sf/types.ts +0 -40
- package/src/tools/sf-pipeline-report.ts +0 -175
- package/src/tools/sf-renderer.ts +0 -332
- package/src/tools/sf.ts +0 -391
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.88.0] - 2026-05-31
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Salesforce tools extracted to marketplace plugin: sf_setup, sf_query, sf_org_display, and sf_pipeline_report are now available as an installable plugin (`@f5xc-salesdemos/xcsh-salesforce`) via the Extension API instead of built-in tools. Install with `xcsh plugin install salesforce`. Context discovery, pipeline reporting, and container-adapted authentication are preserved with full feature parity. ([#1059](https://github.com/f5xc-salesdemos/xcsh/issues/1059))
|
|
10
|
+
|
|
5
11
|
## [18.75.0] - 2026-05-23
|
|
6
12
|
|
|
7
13
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.89.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": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
56
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
57
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
58
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
53
|
+
"@f5xc-salesdemos/xcsh-stats": "18.89.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-agent-core": "18.89.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-ai": "18.89.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-natives": "18.89.0",
|
|
57
|
+
"@f5xc-salesdemos/pi-tui": "18.89.0",
|
|
58
|
+
"@f5xc-salesdemos/pi-utils": "18.89.0",
|
|
59
59
|
"@sinclair/typebox": "^0.34",
|
|
60
60
|
"@xterm/headless": "^6.0",
|
|
61
61
|
"ajv": "^8.18",
|
|
@@ -1360,17 +1360,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
1360
1360
|
},
|
|
1361
1361
|
},
|
|
1362
1362
|
|
|
1363
|
-
"salesforce.enabled": {
|
|
1364
|
-
type: "boolean",
|
|
1365
|
-
default: true,
|
|
1366
|
-
ui: {
|
|
1367
|
-
tab: "tools",
|
|
1368
|
-
label: "Salesforce CLI",
|
|
1369
|
-
description:
|
|
1370
|
-
"Enable sf_* tools for Salesforce org management, SOQL queries, and pipeline reporting via sf CLI",
|
|
1371
|
-
},
|
|
1372
|
-
},
|
|
1373
|
-
|
|
1374
1363
|
"web_search.enabled": {
|
|
1375
1364
|
type: "boolean",
|
|
1376
1365
|
default: true,
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
LoadExtensionsResult,
|
|
29
29
|
MessageRenderer,
|
|
30
30
|
RegisteredCommand,
|
|
31
|
+
ServiceStatusContribution,
|
|
31
32
|
ToolDefinition,
|
|
32
33
|
} from "./types";
|
|
33
34
|
|
|
@@ -176,6 +177,10 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
176
177
|
this.extension.messageRenderers.set(customType, renderer as MessageRenderer);
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
registerServiceStatus(contribution: ServiceStatusContribution): void {
|
|
181
|
+
this.extension.serviceStatuses.set(contribution.name, contribution);
|
|
182
|
+
}
|
|
183
|
+
|
|
179
184
|
getFlag(name: string): boolean | string | undefined {
|
|
180
185
|
if (!this.extension.flags.has(name)) return undefined;
|
|
181
186
|
return this.runtime.flagValues.get(name);
|
|
@@ -257,6 +262,7 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
|
|
|
257
262
|
commands: new Map(),
|
|
258
263
|
flags: new Map(),
|
|
259
264
|
shortcuts: new Map(),
|
|
265
|
+
serviceStatuses: new Map(),
|
|
260
266
|
};
|
|
261
267
|
}
|
|
262
268
|
|
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
RegisteredTool,
|
|
37
37
|
ResourcesDiscoverEvent,
|
|
38
38
|
ResourcesDiscoverResult,
|
|
39
|
+
ServiceStatusContribution,
|
|
39
40
|
SessionBeforeBranchResult,
|
|
40
41
|
SessionBeforeCompactResult,
|
|
41
42
|
SessionBeforeSwitchResult,
|
|
@@ -251,6 +252,17 @@ export class ExtensionRunner {
|
|
|
251
252
|
return tools;
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
/** Get all registered service status contributions from all extensions. */
|
|
256
|
+
getAllRegisteredServiceStatuses(): ServiceStatusContribution[] {
|
|
257
|
+
const statuses: ServiceStatusContribution[] = [];
|
|
258
|
+
for (const ext of this.extensions) {
|
|
259
|
+
for (const contribution of ext.serviceStatuses.values()) {
|
|
260
|
+
statuses.push(contribution);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return statuses;
|
|
264
|
+
}
|
|
265
|
+
|
|
254
266
|
getFlags(): Map<string, ExtensionFlag> {
|
|
255
267
|
const allFlags = new Map<string, ExtensionFlag>();
|
|
256
268
|
for (const ext of this.extensions) {
|
|
@@ -933,6 +933,19 @@ export interface RegisteredCommand {
|
|
|
933
933
|
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
|
|
934
934
|
}
|
|
935
935
|
|
|
936
|
+
// ============================================================================
|
|
937
|
+
// Service Status Contributions
|
|
938
|
+
// ============================================================================
|
|
939
|
+
|
|
940
|
+
export interface ServiceStatusContribution {
|
|
941
|
+
name: string;
|
|
942
|
+
check: () => Promise<{ state: "connected" | "unauthenticated" | "unavailable"; hint?: string }>;
|
|
943
|
+
fix?: {
|
|
944
|
+
prompt: string;
|
|
945
|
+
command: string[];
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
936
949
|
// ============================================================================
|
|
937
950
|
// Extension API
|
|
938
951
|
// ============================================================================
|
|
@@ -1064,6 +1077,9 @@ export interface ExtensionAPI {
|
|
|
1064
1077
|
/** Register a custom renderer for CustomMessageEntry. */
|
|
1065
1078
|
registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
|
|
1066
1079
|
|
|
1080
|
+
/** Register a service status check for the welcome screen. */
|
|
1081
|
+
registerServiceStatus(contribution: ServiceStatusContribution): void;
|
|
1082
|
+
|
|
1067
1083
|
// =========================================================================
|
|
1068
1084
|
// Actions
|
|
1069
1085
|
// =========================================================================
|
|
@@ -1351,6 +1367,7 @@ export interface Extension {
|
|
|
1351
1367
|
commands: Map<string, RegisteredCommand>;
|
|
1352
1368
|
flags: Map<string, ExtensionFlag>;
|
|
1353
1369
|
shortcuts: Map<KeyId, ExtensionShortcut>;
|
|
1370
|
+
serviceStatuses: Map<string, ServiceStatusContribution>;
|
|
1354
1371
|
}
|
|
1355
1372
|
|
|
1356
1373
|
/** Result of loading extensions. */
|
|
@@ -209,7 +209,6 @@ export function renderAboutDoc(info: RuntimeBuildInfo, context: ContextStatus |
|
|
|
209
209
|
"SSH remote execution, image generation and analysis.",
|
|
210
210
|
"",
|
|
211
211
|
"SE specialization: F5 XC API integration (xcsh_api, api-catalog, api-spec),",
|
|
212
|
-
"Salesforce pipeline intelligence (sf_query, xcsh://salesforce),",
|
|
213
212
|
"F5 XC federated product docs (llms.txt hierarchy),",
|
|
214
213
|
"user/computer profiling (xcsh://user, xcsh://computer),",
|
|
215
214
|
"SE-specific subagents (deal-analyst, status-operator, cli-operator, github-ops).",
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "18.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.89.0",
|
|
21
|
+
"commit": "e4462e8d079ea1a120c1f9253c79f78eb4d01e65",
|
|
22
|
+
"shortCommit": "e4462e8",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-
|
|
24
|
+
"tag": "v18.89.0",
|
|
25
|
+
"commitDate": "2026-05-31T23:55:12Z",
|
|
26
|
+
"buildDate": "2026-06-01T00:31:02.396Z",
|
|
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/v18.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/e4462e8d079ea1a120c1f9253c79f78eb4d01e65",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.89.0"
|
|
33
33
|
};
|
|
@@ -36,7 +36,6 @@ export * from "./parse";
|
|
|
36
36
|
export * from "./profile-collectors";
|
|
37
37
|
export * from "./router";
|
|
38
38
|
export * from "./rule-protocol";
|
|
39
|
-
export * from "./salesforce-context";
|
|
40
39
|
export * from "./skill-protocol";
|
|
41
40
|
export * from "./terraform-resolve";
|
|
42
41
|
export type * from "./terraform-types";
|
|
@@ -14,109 +14,6 @@ export interface ProfileCollector {
|
|
|
14
14
|
collect(): Promise<Partial<UserProfile>>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Salesforce
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
const salesforceCollector: ProfileCollector = {
|
|
22
|
-
id: "salesforce",
|
|
23
|
-
name: "Salesforce",
|
|
24
|
-
|
|
25
|
-
async available(): Promise<boolean> {
|
|
26
|
-
if (!$which("sf")) return false;
|
|
27
|
-
try {
|
|
28
|
-
const proc = await $`sf org display --json`.quiet().nothrow();
|
|
29
|
-
if (proc.exitCode !== 0) return false;
|
|
30
|
-
const parsed = JSON.parse(proc.stdout.toString()) as Record<string, unknown>;
|
|
31
|
-
const result = parsed.result as Record<string, unknown> | undefined;
|
|
32
|
-
return typeof result?.username === "string" && result.username.length > 0;
|
|
33
|
-
} catch {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
async collect(): Promise<Partial<UserProfile>> {
|
|
39
|
-
try {
|
|
40
|
-
// Get username
|
|
41
|
-
const orgProc = await $`sf org display --json`.quiet().nothrow();
|
|
42
|
-
if (orgProc.exitCode !== 0) return {};
|
|
43
|
-
const orgData = JSON.parse(orgProc.stdout.toString()) as Record<string, unknown>;
|
|
44
|
-
const orgResult = orgData.result as Record<string, unknown> | undefined;
|
|
45
|
-
const username = orgResult?.username as string | undefined;
|
|
46
|
-
if (!username) return {};
|
|
47
|
-
|
|
48
|
-
// Build and run SOQL
|
|
49
|
-
const soql = `SELECT Id, Username, FirstName, LastName, Email, Title, Department, Division, CompanyName, AboutMe, ManagerId, Manager.Name, Manager.Email, UserRole.Name, Profile.Name, Street, City, State, PostalCode, Country, Phone, MobilePhone FROM User WHERE Username = '${username}'`;
|
|
50
|
-
const queryProc = await $`sf data query --query ${soql} --json`.quiet().nothrow();
|
|
51
|
-
if (queryProc.exitCode !== 0) return {};
|
|
52
|
-
|
|
53
|
-
const queryData = JSON.parse(queryProc.stdout.toString()) as Record<string, unknown>;
|
|
54
|
-
const queryResult = queryData.result as Record<string, unknown> | undefined;
|
|
55
|
-
const records = queryResult?.records as Record<string, unknown>[] | undefined;
|
|
56
|
-
const rec = records?.[0];
|
|
57
|
-
if (!rec) return {};
|
|
58
|
-
|
|
59
|
-
// Map fields
|
|
60
|
-
const profile: Partial<UserProfile> = {};
|
|
61
|
-
|
|
62
|
-
if (rec.FirstName) profile.givenName = rec.FirstName as string;
|
|
63
|
-
if (rec.LastName) profile.familyName = rec.LastName as string;
|
|
64
|
-
if (rec.Email) profile.email = rec.Email as string;
|
|
65
|
-
|
|
66
|
-
const phone = (rec.Phone || rec.MobilePhone) as string | undefined;
|
|
67
|
-
if (phone) profile.telephone = phone;
|
|
68
|
-
|
|
69
|
-
if (rec.Title) profile.jobTitle = rec.Title as string;
|
|
70
|
-
if (rec.Department) profile.department = rec.Department as string;
|
|
71
|
-
if (rec.Division) profile.division = rec.Division as string;
|
|
72
|
-
|
|
73
|
-
const companyName = (rec.CompanyName as string) || "F5";
|
|
74
|
-
profile.worksFor = { name: companyName };
|
|
75
|
-
|
|
76
|
-
// Manager
|
|
77
|
-
const mgr = rec.Manager as Record<string, unknown> | undefined;
|
|
78
|
-
if (mgr) {
|
|
79
|
-
const mgrName = mgr.Name as string | undefined;
|
|
80
|
-
const mgrEmail = mgr.Email as string | undefined;
|
|
81
|
-
if (mgrName || mgrEmail) {
|
|
82
|
-
profile.manager = {};
|
|
83
|
-
if (mgrName) {
|
|
84
|
-
const parts = mgrName.split(" ");
|
|
85
|
-
profile.manager.givenName = parts[0];
|
|
86
|
-
if (parts.length > 1) profile.manager.familyName = parts.slice(1).join(" ");
|
|
87
|
-
}
|
|
88
|
-
if (mgrEmail) profile.manager.email = mgrEmail;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Address
|
|
93
|
-
const street = rec.Street as string | undefined;
|
|
94
|
-
const city = rec.City as string | undefined;
|
|
95
|
-
const state = rec.State as string | undefined;
|
|
96
|
-
const postalCode = rec.PostalCode as string | undefined;
|
|
97
|
-
const country = rec.Country as string | undefined;
|
|
98
|
-
if (street || city || state || postalCode || country) {
|
|
99
|
-
profile.address = {};
|
|
100
|
-
if (street) profile.address.streetAddress = street;
|
|
101
|
-
if (city) profile.address.addressLocality = city;
|
|
102
|
-
if (state) profile.address.addressRegion = state;
|
|
103
|
-
if (postalCode) profile.address.postalCode = postalCode;
|
|
104
|
-
if (country) profile.address.addressCountry = country;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Identifiers
|
|
108
|
-
if (rec.Id) {
|
|
109
|
-
profile.identifiers = { salesforceId: rec.Id as string };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return profile;
|
|
113
|
-
} catch (err: unknown) {
|
|
114
|
-
logger.debug("salesforce collector failed", { error: err });
|
|
115
|
-
return {};
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
|
|
120
17
|
// ---------------------------------------------------------------------------
|
|
121
18
|
// GitHub
|
|
122
19
|
// ---------------------------------------------------------------------------
|
|
@@ -225,4 +122,4 @@ const systemCollector: ProfileCollector = {
|
|
|
225
122
|
// Registry
|
|
226
123
|
// ---------------------------------------------------------------------------
|
|
227
124
|
|
|
228
|
-
export const PROFILE_COLLECTORS: readonly ProfileCollector[] = [
|
|
125
|
+
export const PROFILE_COLLECTORS: readonly ProfileCollector[] = [githubCollector, systemCollector];
|
|
@@ -45,27 +45,27 @@ export interface UserProfile {
|
|
|
45
45
|
description?: string;
|
|
46
46
|
image?: string;
|
|
47
47
|
sameAs?: string[];
|
|
48
|
-
identifiers?: { github?: string; twitter?: string
|
|
49
|
-
/** User-authored: short role label, e.g. 'SE', 'AE', 'CSM', 'SA'. Set manually
|
|
48
|
+
identifiers?: { github?: string; twitter?: string };
|
|
49
|
+
/** User-authored: short role label, e.g. 'SE', 'AE', 'CSM', 'SA'. Set manually. */
|
|
50
50
|
role?: string;
|
|
51
51
|
/**
|
|
52
52
|
* User-authored: confirmed partner (AE/SE counterpart, CSM, etc.).
|
|
53
|
-
* Set manually in user-profile.json.
|
|
53
|
+
* Set manually in user-profile.json.
|
|
54
54
|
*/
|
|
55
55
|
partner?: {
|
|
56
|
-
/**
|
|
56
|
+
/** Partner user ID — used to scope pipeline queries */
|
|
57
57
|
id?: string;
|
|
58
58
|
name: string;
|
|
59
59
|
title?: string;
|
|
60
60
|
/** Short role label, e.g. 'AE', 'SE', 'CSM' */
|
|
61
61
|
role?: string;
|
|
62
62
|
};
|
|
63
|
-
/** User-authored: primary territory names.
|
|
63
|
+
/** User-authored: primary territory names. Scopes pipeline reports. */
|
|
64
64
|
territories?: string[];
|
|
65
65
|
/** User-authored: quarterly quota target in dollars. Used for coverage ratio calculations. */
|
|
66
66
|
quota?: number;
|
|
67
67
|
observations?: UserProfileObservation[];
|
|
68
|
-
sources?: {
|
|
68
|
+
sources?: { github?: string; system?: string; conversation?: string };
|
|
69
69
|
updatedAt?: string;
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -164,9 +164,7 @@ export function renderProfileMarkdown(profile: UserProfile): string {
|
|
|
164
164
|
|
|
165
165
|
const isEmpty = !profile.givenName && !profile.familyName && !profile.email && !profile.jobTitle;
|
|
166
166
|
if (isEmpty) {
|
|
167
|
-
sections.push(
|
|
168
|
-
"No profile data yet. Use `xcsh://user?seed=true` to populate from Salesforce, GitHub, and system sources.\n",
|
|
169
|
-
);
|
|
167
|
+
sections.push("No profile data yet. Use `xcsh://user?seed=true` to populate from GitHub and system sources.\n");
|
|
170
168
|
sections.push("Profile facts can also be added progressively during conversation.\n");
|
|
171
169
|
return sections.join("\n");
|
|
172
170
|
}
|
|
@@ -267,7 +265,6 @@ export function renderProfileMarkdown(profile: UserProfile): string {
|
|
|
267
265
|
if (profile.description) onlineLines.push(`- **Bio:** ${profile.description}`);
|
|
268
266
|
if (profile.identifiers?.github) onlineLines.push(`- **GitHub:** ${profile.identifiers.github}`);
|
|
269
267
|
if (profile.identifiers?.twitter) onlineLines.push(`- **Twitter/X:** ${profile.identifiers.twitter}`);
|
|
270
|
-
if (profile.identifiers?.salesforceId) onlineLines.push(`- **Salesforce ID:** ${profile.identifiers.salesforceId}`);
|
|
271
268
|
if (profile.sameAs && profile.sameAs.length > 0) {
|
|
272
269
|
for (const link of profile.sameAs) {
|
|
273
270
|
onlineLines.push(`- **Profile:** ${link}`);
|
|
@@ -293,7 +290,6 @@ export function renderProfileMarkdown(profile: UserProfile): string {
|
|
|
293
290
|
sections.push("\n---\n");
|
|
294
291
|
sections.push("**Sources:**");
|
|
295
292
|
const srcLines: string[] = [];
|
|
296
|
-
if (profile.sources.salesforce) srcLines.push(`Salesforce: ${profile.sources.salesforce}`);
|
|
297
293
|
if (profile.sources.github) srcLines.push(`GitHub: ${profile.sources.github}`);
|
|
298
294
|
if (profile.sources.system) srcLines.push(`System: ${profile.sources.system}`);
|
|
299
295
|
if (profile.sources.conversation) srcLines.push(`Conversation: ${profile.sources.conversation}`);
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* - xcsh://terraform/{category}/{resource} - Self-contained resource doc
|
|
22
22
|
* - xcsh://user - Human user profile
|
|
23
23
|
* - xcsh://user?seed=true - Seed profile from sources and render
|
|
24
|
+
*
|
|
25
|
+
* Note: Salesforce context (xcsh://salesforce) has been extracted to the
|
|
26
|
+
* salesforce plugin. See packages/salesforce/ for the standalone implementation.
|
|
24
27
|
*/
|
|
25
28
|
import * as path from "node:path";
|
|
26
29
|
import { logger } from "@f5xc-salesdemos/pi-utils";
|
|
@@ -37,7 +40,6 @@ import type {
|
|
|
37
40
|
import { getRuntimeBuildInfo, type RuntimeBuildInfo, renderAboutDoc } from "./build-info-runtime";
|
|
38
41
|
import { loadComputerProfile, renderComputerProfileMarkdown, seedComputerProfile } from "./computer-profile";
|
|
39
42
|
import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
|
|
40
|
-
import { loadSalesforceContext, renderSalesforceContextMarkdown, seedSalesforceContext } from "./salesforce-context";
|
|
41
43
|
import { createTerraformResolver, type TerraformResolver } from "./terraform-resolve";
|
|
42
44
|
import type { TerraformIndex } from "./terraform-types";
|
|
43
45
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
@@ -51,8 +53,6 @@ const BRANDING_HOST = "branding";
|
|
|
51
53
|
const TERRAFORM_HOST = "terraform";
|
|
52
54
|
const USER_ROUTE = "user";
|
|
53
55
|
const COMPUTER_ROUTE = "computer";
|
|
54
|
-
const SALESFORCE_ROUTE = "salesforce";
|
|
55
|
-
|
|
56
56
|
const EMPTY_INDEX: ApiSpecIndex = { version: "unavailable", timestamp: "", domains: [] };
|
|
57
57
|
const EMPTY_CATALOG_INDEX: ApiCatalogIndex = {
|
|
58
58
|
version: "unavailable",
|
|
@@ -311,10 +311,6 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
311
311
|
return this.#resolveComputerProfile(url);
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
if (host === SALESFORCE_ROUTE) {
|
|
315
|
-
return this.#resolveSalesforceContext(url);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
314
|
const pathname = url.rawPathname ?? url.pathname;
|
|
319
315
|
const filename = host ? (pathname && pathname !== "/" ? host + pathname : host) : "";
|
|
320
316
|
|
|
@@ -357,22 +353,6 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
357
353
|
};
|
|
358
354
|
}
|
|
359
355
|
|
|
360
|
-
async #resolveSalesforceContext(url: InternalUrl): Promise<InternalResource> {
|
|
361
|
-
const params = new URLSearchParams(url.search);
|
|
362
|
-
const shouldRefresh = params.get("refresh") === "true";
|
|
363
|
-
|
|
364
|
-
const ctx = shouldRefresh ? await seedSalesforceContext() : await loadSalesforceContext();
|
|
365
|
-
const content = renderSalesforceContextMarkdown(ctx);
|
|
366
|
-
|
|
367
|
-
return {
|
|
368
|
-
url: url.href,
|
|
369
|
-
content,
|
|
370
|
-
contentType: "text/markdown",
|
|
371
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
372
|
-
sourcePath: `xcsh://${SALESFORCE_ROUTE}`,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
356
|
async #listDocs(url: InternalUrl): Promise<InternalResource> {
|
|
377
357
|
if (EMBEDDED_DOC_FILENAMES.length === 0) {
|
|
378
358
|
throw new Error("No documentation files found");
|
|
@@ -387,7 +367,6 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
387
367
|
const brandingEntry = `- [${BRANDING_HOST}](${SCHEME_PREFIX}${BRANDING_HOST}) — F5 XC branding and legacy name mapping (v${branding.version})`;
|
|
388
368
|
const userEntry = `- [${USER_ROUTE}](${SCHEME_PREFIX}${USER_ROUTE}) — human user profile`;
|
|
389
369
|
const computerEntry = `- [${COMPUTER_ROUTE}](${SCHEME_PREFIX}${COMPUTER_ROUTE}) — machine hardware and environment profile`;
|
|
390
|
-
const salesforceEntry = `- [${SALESFORCE_ROUTE}](${SCHEME_PREFIX}${SALESFORCE_ROUTE}) — Salesforce pipeline context and team discovery`;
|
|
391
370
|
const tf = loadTerraformIndex();
|
|
392
371
|
const terraformEntry = `- [${TERRAFORM_HOST}/](${SCHEME_PREFIX}${TERRAFORM_HOST}/) — F5 XC Terraform provider (${Object.keys(tf.resources).length} resources, v${tf.version})`;
|
|
393
372
|
const listing = [
|
|
@@ -398,10 +377,9 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
398
377
|
terraformEntry,
|
|
399
378
|
userEntry,
|
|
400
379
|
computerEntry,
|
|
401
|
-
salesforceEntry,
|
|
402
380
|
...EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](${SCHEME_PREFIX}${f})`),
|
|
403
381
|
].join("\n");
|
|
404
|
-
const totalCount = EMBEDDED_DOC_FILENAMES.length +
|
|
382
|
+
const totalCount = EMBEDDED_DOC_FILENAMES.length + 7;
|
|
405
383
|
const content = `# Documentation\n\n${totalCount} files available:\n\n${listing}\n`;
|
|
406
384
|
|
|
407
385
|
return {
|
|
@@ -234,104 +234,6 @@ export async function checkGitLabStatus(cwd: string): Promise<WelcomeGitLabStatu
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
export type SalesforceCheckState = "connected" | "auth_error" | "session_expired" | "not_configured";
|
|
238
|
-
|
|
239
|
-
export interface WelcomeSalesforceStatus {
|
|
240
|
-
state: SalesforceCheckState;
|
|
241
|
-
username?: string;
|
|
242
|
-
orgAlias?: string;
|
|
243
|
-
instanceUrl?: string;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Idempotent startup check: sf installed -> org list -> default org -> display status. */
|
|
247
|
-
export async function checkSalesforceStatus(_cwd: string): Promise<WelcomeSalesforceStatus | undefined> {
|
|
248
|
-
try {
|
|
249
|
-
if (!$which("sf")) return undefined;
|
|
250
|
-
|
|
251
|
-
// Suppress telemetry consent nag (idempotent)
|
|
252
|
-
await $`sf config set disable-telemetry true --global`.quiet().nothrow();
|
|
253
|
-
|
|
254
|
-
// Step 1: Get org list
|
|
255
|
-
const listResult = await $`sf org list --json`.quiet().nothrow();
|
|
256
|
-
if (listResult.exitCode !== 0) return { state: "auth_error" };
|
|
257
|
-
|
|
258
|
-
let listData: {
|
|
259
|
-
result?: {
|
|
260
|
-
nonScratchOrgs?: unknown[];
|
|
261
|
-
sandboxes?: unknown[];
|
|
262
|
-
scratchOrgs?: unknown[];
|
|
263
|
-
devHubs?: unknown[];
|
|
264
|
-
other?: unknown[];
|
|
265
|
-
};
|
|
266
|
-
};
|
|
267
|
-
try {
|
|
268
|
-
listData = JSON.parse(listResult.text());
|
|
269
|
-
} catch {
|
|
270
|
-
return { state: "auth_error" };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const r = listData.result ?? {};
|
|
274
|
-
const seen = new Set<string>();
|
|
275
|
-
const allRawOrgs = (
|
|
276
|
-
[
|
|
277
|
-
...(r.nonScratchOrgs ?? []),
|
|
278
|
-
...(r.sandboxes ?? []),
|
|
279
|
-
...(r.scratchOrgs ?? []),
|
|
280
|
-
...(r.devHubs ?? []),
|
|
281
|
-
...(r.other ?? []),
|
|
282
|
-
] as Record<string, unknown>[]
|
|
283
|
-
).filter(org => {
|
|
284
|
-
const id = String(org.orgId ?? org.orgid ?? "");
|
|
285
|
-
if (!id || seen.has(id)) return false;
|
|
286
|
-
seen.add(id);
|
|
287
|
-
return true;
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
if (allRawOrgs.length === 0) return { state: "auth_error" };
|
|
291
|
-
|
|
292
|
-
// Step 2: Find default org (normalize raw CLI fields)
|
|
293
|
-
const defaultRaw = allRawOrgs.find(
|
|
294
|
-
org =>
|
|
295
|
-
(typeof org.defaultMarker === "string" && org.defaultMarker.includes("(U)")) ||
|
|
296
|
-
org.isDefaultUsername === true,
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
if (!defaultRaw) {
|
|
300
|
-
return { state: "not_configured", username: allRawOrgs[0]?.username as string | undefined };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const alias = (defaultRaw.alias ?? defaultRaw.username) as string;
|
|
304
|
-
|
|
305
|
-
// Step 3: Display org details
|
|
306
|
-
const displayResult = await $`sf org display --target-org ${alias} --json`.quiet().nothrow();
|
|
307
|
-
if (displayResult.exitCode !== 0) {
|
|
308
|
-
return { state: "session_expired", username: defaultRaw.username as string | undefined, orgAlias: alias };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
let displayData: { result?: Record<string, unknown> };
|
|
312
|
-
try {
|
|
313
|
-
displayData = JSON.parse(displayResult.text());
|
|
314
|
-
} catch {
|
|
315
|
-
return { state: "session_expired", username: defaultRaw.username as string | undefined, orgAlias: alias };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const result = displayData.result;
|
|
319
|
-
if (!result || result.connectedStatus !== "Connected") {
|
|
320
|
-
return { state: "session_expired", username: defaultRaw.username as string | undefined, orgAlias: alias };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
state: "connected",
|
|
325
|
-
username: result.username as string | undefined,
|
|
326
|
-
orgAlias: alias,
|
|
327
|
-
instanceUrl: result.instanceUrl as string | undefined,
|
|
328
|
-
};
|
|
329
|
-
} catch (err) {
|
|
330
|
-
logger.warn("Salesforce startup check failed", { error: String(err) });
|
|
331
|
-
return { state: "auth_error" };
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
237
|
export type GitHubCheckState = "connected" | "auth_error";
|
|
336
238
|
|
|
337
239
|
export interface WelcomeGitHubStatus {
|
|
@@ -391,20 +293,6 @@ export function mapGitLabStatus(status: WelcomeGitLabStatus | undefined): Servic
|
|
|
391
293
|
}
|
|
392
294
|
}
|
|
393
295
|
|
|
394
|
-
export function mapSalesforceStatus(status: WelcomeSalesforceStatus | undefined): ServiceStatus {
|
|
395
|
-
if (!status) return { name: "Salesforce", state: "unavailable", hint: "not installed" };
|
|
396
|
-
switch (status.state) {
|
|
397
|
-
case "connected":
|
|
398
|
-
return { name: "Salesforce", state: "connected" };
|
|
399
|
-
case "session_expired":
|
|
400
|
-
return { name: "Salesforce", state: "unauthenticated", hint: "session expired, run: sf org login web" };
|
|
401
|
-
case "not_configured":
|
|
402
|
-
return { name: "Salesforce", state: "unauthenticated", hint: "run: sf org login web --set-default" };
|
|
403
|
-
default:
|
|
404
|
-
return { name: "Salesforce", state: "unauthenticated", hint: "run: sf org login web" };
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
296
|
export function mapGitHubStatus(status: WelcomeGitHubStatus | undefined): ServiceStatus {
|
|
409
297
|
if (!status) return { name: "GitHub", state: "unavailable", hint: "not installed" };
|
|
410
298
|
switch (status.state) {
|
|
@@ -609,7 +497,6 @@ export function getFixableServices(statuses: {
|
|
|
609
497
|
gcloud: WelcomeGcloudStatus | undefined;
|
|
610
498
|
github: WelcomeGitHubStatus | undefined;
|
|
611
499
|
gitlab: WelcomeGitLabStatus | undefined;
|
|
612
|
-
salesforce: WelcomeSalesforceStatus | undefined;
|
|
613
500
|
}): FixableService[] {
|
|
614
501
|
const fixable: FixableService[] = [];
|
|
615
502
|
|
|
@@ -629,14 +516,6 @@ export function getFixableServices(statuses: {
|
|
|
629
516
|
recheck: async () => mapGitHubStatus(await checkGitHubStatus()),
|
|
630
517
|
});
|
|
631
518
|
}
|
|
632
|
-
if (statuses.salesforce?.state === "session_expired") {
|
|
633
|
-
fixable.push({
|
|
634
|
-
name: "Salesforce",
|
|
635
|
-
prompt: "Salesforce session expired",
|
|
636
|
-
command: ["sf", "org", "login", "web"],
|
|
637
|
-
recheck: async () => mapSalesforceStatus(await checkSalesforceStatus(getProjectDir())),
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
519
|
if (statuses.azure?.state === "auth_error") {
|
|
641
520
|
fixable.push({
|
|
642
521
|
name: "Azure",
|