@f5-sales-demo/xcsh 19.72.0 → 19.73.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": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.73.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",
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
58
58
|
"@mozilla/readability": "^0.6",
|
|
59
|
-
"@f5-sales-demo/xcsh-stats": "19.
|
|
60
|
-
"@f5-sales-demo/pi-agent-core": "19.
|
|
61
|
-
"@f5-sales-demo/pi-ai": "19.
|
|
62
|
-
"@f5-sales-demo/pi-natives": "19.
|
|
63
|
-
"@f5-sales-demo/pi-resource-management": "19.
|
|
64
|
-
"@f5-sales-demo/pi-tui": "19.
|
|
65
|
-
"@f5-sales-demo/pi-utils": "19.
|
|
59
|
+
"@f5-sales-demo/xcsh-stats": "19.73.0",
|
|
60
|
+
"@f5-sales-demo/pi-agent-core": "19.73.0",
|
|
61
|
+
"@f5-sales-demo/pi-ai": "19.73.0",
|
|
62
|
+
"@f5-sales-demo/pi-natives": "19.73.0",
|
|
63
|
+
"@f5-sales-demo/pi-resource-management": "19.73.0",
|
|
64
|
+
"@f5-sales-demo/pi-tui": "19.73.0",
|
|
65
|
+
"@f5-sales-demo/pi-utils": "19.73.0",
|
|
66
66
|
"@sinclair/typebox": "^0.34",
|
|
67
67
|
"@xterm/headless": "^6.0",
|
|
68
68
|
"ajv": "^8.20",
|
|
@@ -69,3 +69,31 @@ export function createExtensionBridgeTools(bridge: BridgeServer): CustomTool<TSc
|
|
|
69
69
|
export const EXTENSION_AGENT_TOOL_NAMES: readonly string[] = EXTENSION_CAPABILITIES.tools
|
|
70
70
|
.filter(def => !INTERNAL_TOOLS.has(def.name))
|
|
71
71
|
.map(def => def.name);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Builtin agent tools scoped into a headless browser-bridge session (the Chrome
|
|
75
|
+
* extension worker and the Office `serve` bridge). Shared so both bootstraps use
|
|
76
|
+
* one list. Office document tools are NOT here — the pane advertises those over
|
|
77
|
+
* the bridge via `set_host_tools`, registered at runtime by the ChatHandler.
|
|
78
|
+
*/
|
|
79
|
+
export const BROWSER_TOOL_NAMES: readonly string[] = [
|
|
80
|
+
"catalog_workflow_runner",
|
|
81
|
+
"navigate",
|
|
82
|
+
"click",
|
|
83
|
+
"click_element",
|
|
84
|
+
"fill",
|
|
85
|
+
"type_text",
|
|
86
|
+
"screenshot",
|
|
87
|
+
"login",
|
|
88
|
+
"read_ax",
|
|
89
|
+
"get_page_context",
|
|
90
|
+
"query_dom",
|
|
91
|
+
"find",
|
|
92
|
+
"wait_for",
|
|
93
|
+
"key_press",
|
|
94
|
+
"select_option",
|
|
95
|
+
"label_select",
|
|
96
|
+
"scroll_to",
|
|
97
|
+
"annotate",
|
|
98
|
+
"set_explain_mode",
|
|
99
|
+
];
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless chat bridge — the no-TUI extension-bridge host used by `xcsh office
|
|
3
|
+
* serve` so one command yields a working task pane (pane files + a live chat/
|
|
4
|
+
* host-tool bridge). It mirrors the proven `xcsh worker` bootstrap
|
|
5
|
+
* (`commands/worker.ts`) MINUS the fleet concerns (manager keepalive, pre-warm
|
|
6
|
+
* IPC bind, TTFT spans): set the browser-provider env, init Settings + Context,
|
|
7
|
+
* quiet startup, provision the wss cert, start the bridge, publish session info,
|
|
8
|
+
* create ONE agent session scoped to the browser tools, and attach the
|
|
9
|
+
* `ChatHandler`. Office document tools (Excel/Word/PPT) are advertised by the
|
|
10
|
+
* pane at runtime over the bridge (`set_host_tools`), so they need no scoping here.
|
|
11
|
+
*
|
|
12
|
+
* The heavy / socket / network calls are injected (defaulting to the real ones)
|
|
13
|
+
* so the wiring is unit-testable without opening real listeners or a session.
|
|
14
|
+
*
|
|
15
|
+
* NOT browser-safe (node/bun): runs inside the full xcsh binary, never the pane.
|
|
16
|
+
*/
|
|
17
|
+
import { getProjectDir, getXCSHConfigDir } from "@f5-sales-demo/pi-utils";
|
|
18
|
+
import { createAgentSession } from "../sdk";
|
|
19
|
+
import { ContextService } from "../services/xcsh-context";
|
|
20
|
+
import { deriveTenantEnv } from "../services/xcsh-env";
|
|
21
|
+
import { resolveBridgeTls } from "./bridge-cert";
|
|
22
|
+
import { ChatHandler } from "./chat-handler";
|
|
23
|
+
import { type BridgeServer, startBridgeServer } from "./extension-bridge";
|
|
24
|
+
import { BROWSER_TOOL_NAMES, createExtensionBridgeTools, EXTENSION_AGENT_TOOL_NAMES } from "./extension-bridge-tools";
|
|
25
|
+
import { setSharedBridgeServer } from "./provider";
|
|
26
|
+
|
|
27
|
+
/** A running headless bridge + a teardown that disposes the chat handler and
|
|
28
|
+
* closes the bridge (both ws + wss listeners). */
|
|
29
|
+
export interface HeadlessChatBridge {
|
|
30
|
+
bridge: BridgeServer;
|
|
31
|
+
dispose: () => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Tenant identity for the `hello` handshake, contextless-friendly: the active
|
|
36
|
+
* `/context` wins (its apiUrl + name), otherwise fall back to the `XCSH_API_URL`/
|
|
37
|
+
* `XCSH_SESSION_TENANT` env so the pane still learns which tenant this process
|
|
38
|
+
* serves. Sync — the bridge calls it while answering `hello`. Mirrors the
|
|
39
|
+
* interactive path in `main.ts` (there is no per-tab session id here).
|
|
40
|
+
*/
|
|
41
|
+
export function sessionInfoForOfficeServe(): {
|
|
42
|
+
tenant: string | null;
|
|
43
|
+
env: string | null;
|
|
44
|
+
apiUrl: string | null;
|
|
45
|
+
contextBound: boolean;
|
|
46
|
+
sessionId: string | null;
|
|
47
|
+
} {
|
|
48
|
+
let apiUrl: string | null = null;
|
|
49
|
+
let contextBound = false;
|
|
50
|
+
try {
|
|
51
|
+
apiUrl = ContextService.instance.activeApiUrl;
|
|
52
|
+
contextBound = ContextService.instance.getStatus().activeContextName != null;
|
|
53
|
+
} catch {
|
|
54
|
+
/* ContextService not initialized — env-only mode. */
|
|
55
|
+
}
|
|
56
|
+
apiUrl = apiUrl ?? process.env.XCSH_API_URL ?? null;
|
|
57
|
+
const tenantKey = process.env.XCSH_SESSION_TENANT ?? null;
|
|
58
|
+
const { tenant, env } = deriveTenantEnv(apiUrl, tenantKey);
|
|
59
|
+
return { tenant, env, apiUrl, contextBound, sessionId: null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Injectable seams (defaulted to the real ones) so the bootstrap is testable. */
|
|
63
|
+
export interface HeadlessBridgeDeps {
|
|
64
|
+
/** Set the browser-provider env, init Settings + ContextService + provider
|
|
65
|
+
* persistence, and quiet startup; returns the project cwd for the session. */
|
|
66
|
+
initEnv: () => Promise<{ cwd: string }>;
|
|
67
|
+
resolveBridgeTls: typeof resolveBridgeTls;
|
|
68
|
+
startBridgeServer: typeof startBridgeServer;
|
|
69
|
+
setSharedBridgeServer: typeof setSharedBridgeServer;
|
|
70
|
+
createExtensionBridgeTools: typeof createExtensionBridgeTools;
|
|
71
|
+
createAgentSession: typeof createAgentSession;
|
|
72
|
+
ChatHandlerCtor: typeof ChatHandler;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const defaultDeps: HeadlessBridgeDeps = {
|
|
76
|
+
initEnv: async () => {
|
|
77
|
+
process.env.XCSH_BROWSER_PROVIDER = "extension";
|
|
78
|
+
const cwd = getProjectDir();
|
|
79
|
+
const { Settings, settings } = await import("../config/settings");
|
|
80
|
+
await Settings.init({ cwd });
|
|
81
|
+
// Init the ContextService singleton so sessionInfoForOfficeServe can read the
|
|
82
|
+
// active apiUrl once a /context is bound.
|
|
83
|
+
try {
|
|
84
|
+
ContextService.init(getXCSHConfigDir());
|
|
85
|
+
} catch {
|
|
86
|
+
/* already initialized / unavailable — continue. */
|
|
87
|
+
}
|
|
88
|
+
// Provider persistence for model discovery (parity with main.ts / worker.ts).
|
|
89
|
+
const { initializeWithSettings } = await import("../discovery");
|
|
90
|
+
initializeWithSettings(settings);
|
|
91
|
+
// Quiet startup: skip the welcome screen + blocking plugin "Fix now?" prompts.
|
|
92
|
+
settings.override("startup.quiet", true);
|
|
93
|
+
return { cwd };
|
|
94
|
+
},
|
|
95
|
+
resolveBridgeTls,
|
|
96
|
+
startBridgeServer,
|
|
97
|
+
setSharedBridgeServer,
|
|
98
|
+
createExtensionBridgeTools,
|
|
99
|
+
createAgentSession,
|
|
100
|
+
ChatHandlerCtor: ChatHandler,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Start the headless chat bridge and return it with a teardown. Fully awaits the
|
|
105
|
+
* session + ChatHandler.attach() before resolving, so once this resolves the pane
|
|
106
|
+
* can connect and chat immediately (no warm-up race). A `configure`-less pane
|
|
107
|
+
* chats over xcsh's already-configured provider.
|
|
108
|
+
*/
|
|
109
|
+
export async function startHeadlessChatBridge(deps: HeadlessBridgeDeps = defaultDeps): Promise<HeadlessChatBridge> {
|
|
110
|
+
const { cwd } = await deps.initEnv();
|
|
111
|
+
|
|
112
|
+
// Provision the wss cert before binding (warm boot = on-disk cache hit);
|
|
113
|
+
// `undefined` (offline) → the bridge starts ws-only.
|
|
114
|
+
const tls = await deps.resolveBridgeTls();
|
|
115
|
+
const bridge = await deps.startBridgeServer(undefined, tls ? { tls } : undefined);
|
|
116
|
+
// Reuse this bridge for any in-process selectProvider() (no conflicting second bridge).
|
|
117
|
+
deps.setSharedBridgeServer(bridge);
|
|
118
|
+
bridge.setSessionInfo(sessionInfoForOfficeServe);
|
|
119
|
+
// Re-announce the tenant when the active context changes (best-effort).
|
|
120
|
+
try {
|
|
121
|
+
ContextService.onContextChange(() => bridge.broadcastTenantChanged());
|
|
122
|
+
} catch {
|
|
123
|
+
/* ContextService not initialized (tests) — the tenant is static. */
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Everything past the bind can throw (createAgentSession on a misconfigured
|
|
127
|
+
// provider, etc.). If it does, close the already-bound bridge and clear the
|
|
128
|
+
// shared-bridge global before rethrowing — otherwise the ws/wss listeners leak
|
|
129
|
+
// (keeping the event loop alive so Ctrl+C can't exit) and a later in-process
|
|
130
|
+
// selectProvider() reuses a dead bridge. The caller (startOfficeServe) treats
|
|
131
|
+
// the rethrow as a non-fatal "pane only" fallback.
|
|
132
|
+
try {
|
|
133
|
+
// Turn the extension's browser actions into bridge-proxying CustomTools, then
|
|
134
|
+
// create ONE headless session scoped to the browser tools (Office document
|
|
135
|
+
// tools arrive at runtime via set_host_tools).
|
|
136
|
+
const extensionTools = deps.createExtensionBridgeTools(bridge);
|
|
137
|
+
const { session } = await deps.createAgentSession({
|
|
138
|
+
cwd,
|
|
139
|
+
hasUI: false,
|
|
140
|
+
toolNames: [...new Set([...BROWSER_TOOL_NAMES, ...EXTENSION_AGENT_TOOL_NAMES])],
|
|
141
|
+
customTools: extensionTools,
|
|
142
|
+
// Headless: no MCP/LSP/extension discovery — lean, no network/blocking prompts.
|
|
143
|
+
enableMCP: false,
|
|
144
|
+
enableLsp: false,
|
|
145
|
+
disableExtensionDiscovery: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const chatHandler = new deps.ChatHandlerCtor(bridge, session);
|
|
149
|
+
chatHandler.attach();
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
bridge,
|
|
153
|
+
dispose: async () => {
|
|
154
|
+
chatHandler.dispose();
|
|
155
|
+
deps.setSharedBridgeServer(null);
|
|
156
|
+
await bridge.close();
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
} catch (err) {
|
|
160
|
+
deps.setSharedBridgeServer(null);
|
|
161
|
+
await bridge.close();
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/browser/provider.ts
CHANGED
|
@@ -115,7 +115,10 @@ export class CdpBrowserProvider implements BrowserProvider {
|
|
|
115
115
|
* instead of starting a conflicting second one on the same port.
|
|
116
116
|
*/
|
|
117
117
|
let _sharedBridgeServer: import("./extension-bridge").BridgeServer | null = null;
|
|
118
|
-
|
|
118
|
+
/** Publish (or, with `null`, clear) the process-shared bridge. Clearing is
|
|
119
|
+
* required when a partially-started bridge is torn down (e.g. session bootstrap
|
|
120
|
+
* failed after bind) so a later `selectProvider()` never reuses a closed bridge. */
|
|
121
|
+
export function setSharedBridgeServer(server: import("./extension-bridge").BridgeServer | null): void {
|
|
119
122
|
_sharedBridgeServer = server;
|
|
120
123
|
}
|
|
121
124
|
|
package/src/cli/office-cli.ts
CHANGED
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { LOCALIP_HOST } from "../browser/bridge-cert";
|
|
13
|
+
import { type HeadlessChatBridge, startHeadlessChatBridge } from "../browser/headless-bridge";
|
|
14
|
+
import {
|
|
15
|
+
getOfficePaneDir,
|
|
16
|
+
type OfficePaneServer,
|
|
17
|
+
readManifest,
|
|
18
|
+
startOfficePaneServer,
|
|
19
|
+
} from "../browser/office-pane-server";
|
|
13
20
|
|
|
14
21
|
/** The subcommands `xcsh office` accepts (also the Args `options` constraint). */
|
|
15
22
|
export const OFFICE_ACTIONS = ["serve", "manifest", "sideload"] as const;
|
|
@@ -39,9 +46,53 @@ export async function writeManifest(outPath?: string): Promise<string> {
|
|
|
39
46
|
return text;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
/**
|
|
49
|
+
/** Injectable seams for {@link startOfficeServe} (defaulted to the real ones). */
|
|
50
|
+
export interface OfficeServeDeps {
|
|
51
|
+
startOfficePaneServer: typeof startOfficePaneServer;
|
|
52
|
+
startHeadlessChatBridge: typeof startHeadlessChatBridge;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const defaultServeDeps: OfficeServeDeps = { startOfficePaneServer, startHeadlessChatBridge };
|
|
56
|
+
|
|
57
|
+
/** A running `office serve`: the pane file server, the (optional) chat bridge, and
|
|
58
|
+
* a teardown that disposes both. */
|
|
59
|
+
export interface OfficeServeHandle {
|
|
60
|
+
server: OfficePaneServer;
|
|
61
|
+
chat: HeadlessChatBridge | null;
|
|
62
|
+
dispose: () => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Start BOTH the :8444 pane file server AND the headless chat bridge, so one
|
|
67
|
+
* `xcsh office serve` yields a working pane (no separately-run bridge). A bridge
|
|
68
|
+
* failure is NON-fatal: the pane still serves and we warn, so `serve` degrades to
|
|
69
|
+
* "pane only" rather than failing outright. Returns a teardown that disposes both.
|
|
70
|
+
* Extracted from {@link runServe} so the start/teardown wiring is unit-testable.
|
|
71
|
+
*/
|
|
72
|
+
export async function startOfficeServe(deps: OfficeServeDeps = defaultServeDeps): Promise<OfficeServeHandle> {
|
|
73
|
+
const server = await deps.startOfficePaneServer();
|
|
74
|
+
let chat: HeadlessChatBridge | null = null;
|
|
75
|
+
try {
|
|
76
|
+
chat = await deps.startHeadlessChatBridge();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`Warning: the chat bridge could not start (${err instanceof Error ? err.message : String(err)}). ` +
|
|
80
|
+
"The task pane will load but chat is unavailable until a bridge is running.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
server,
|
|
85
|
+
chat,
|
|
86
|
+
dispose: async () => {
|
|
87
|
+
if (chat) await chat.dispose();
|
|
88
|
+
server.stop();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Start the pane server + chat bridge, print status, and block until killed. */
|
|
43
94
|
async function runServe(): Promise<void> {
|
|
44
|
-
const server = await
|
|
95
|
+
const { server, chat, dispose } = await startOfficeServe();
|
|
45
96
|
console.log(`Serving the xcsh Office task pane at ${server.taskpaneUrl}`);
|
|
46
97
|
if (!server.trusted) {
|
|
47
98
|
console.warn(
|
|
@@ -49,9 +100,26 @@ async function runServe(): Promise<void> {
|
|
|
49
100
|
"Office's WebView may refuse to load the page until the cert is trusted.",
|
|
50
101
|
);
|
|
51
102
|
}
|
|
103
|
+
if (chat) {
|
|
104
|
+
if (chat.bridge.wssPort) {
|
|
105
|
+
console.log(
|
|
106
|
+
`Chat bridge ready on wss://${LOCALIP_HOST}:${chat.bridge.wssPort} — the pane connects automatically.`,
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
console.warn(
|
|
110
|
+
"Warning: the chat bridge is ws-only (no wss cert). The pane connects over wss and may not reach it.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
52
114
|
console.log("Press Ctrl+C to stop.");
|
|
53
|
-
//
|
|
54
|
-
await new Promise<
|
|
115
|
+
// Block until a signal tears us down, then dispose the bridge + pane server.
|
|
116
|
+
await new Promise<void>(resolve => {
|
|
117
|
+
const shutdown = (): void => {
|
|
118
|
+
void dispose().finally(resolve);
|
|
119
|
+
};
|
|
120
|
+
process.once("SIGINT", shutdown);
|
|
121
|
+
process.once("SIGTERM", shutdown);
|
|
122
|
+
});
|
|
55
123
|
}
|
|
56
124
|
|
|
57
125
|
/** Run the Office sideload against the embedded bundle (best-effort). */
|
package/src/commands/worker.ts
CHANGED
|
@@ -20,7 +20,11 @@ import { Command } from "@f5-sales-demo/pi-utils/cli";
|
|
|
20
20
|
import { LOCALIP_HOST, resolveBridgeTls } from "../browser/bridge-cert";
|
|
21
21
|
import { ChatHandler } from "../browser/chat-handler";
|
|
22
22
|
import { startBridgeServer } from "../browser/extension-bridge";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
BROWSER_TOOL_NAMES,
|
|
25
|
+
createExtensionBridgeTools,
|
|
26
|
+
EXTENSION_AGENT_TOOL_NAMES,
|
|
27
|
+
} from "../browser/extension-bridge-tools";
|
|
24
28
|
import { setSharedBridgeServer } from "../browser/provider";
|
|
25
29
|
import { coldStartSpans, type SpanFrame, sessionBuildSpan } from "../browser/ttft-spans";
|
|
26
30
|
import { initializeWithSettings } from "../discovery";
|
|
@@ -94,28 +98,6 @@ function managerSockPath(): string {
|
|
|
94
98
|
/** Browser-automation tool set — identical scoping to `main.ts`'s extension path.
|
|
95
99
|
* With scoped tools the ONLY way to create a resource is the form-driven workflow
|
|
96
100
|
* runner, which is exactly what the human watching the browser wants. */
|
|
97
|
-
const BROWSER_TOOL_NAMES = [
|
|
98
|
-
"catalog_workflow_runner",
|
|
99
|
-
"navigate",
|
|
100
|
-
"click",
|
|
101
|
-
"click_element",
|
|
102
|
-
"fill",
|
|
103
|
-
"type_text",
|
|
104
|
-
"screenshot",
|
|
105
|
-
"login",
|
|
106
|
-
"read_ax",
|
|
107
|
-
"get_page_context",
|
|
108
|
-
"query_dom",
|
|
109
|
-
"find",
|
|
110
|
-
"wait_for",
|
|
111
|
-
"key_press",
|
|
112
|
-
"select_option",
|
|
113
|
-
"label_select",
|
|
114
|
-
"scroll_to",
|
|
115
|
-
"annotate",
|
|
116
|
-
"set_explain_mode",
|
|
117
|
-
];
|
|
118
|
-
|
|
119
101
|
export default class Worker extends Command {
|
|
120
102
|
static description = "Run a headless extension-bridge worker (no TUI); blocks until SIGTERM";
|
|
121
103
|
|
|
@@ -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.73.0",
|
|
21
|
+
"commit": "3f32c968ef8b2a0c9fabbc1be3fbe5c7395a7dc9",
|
|
22
|
+
"shortCommit": "3f32c96",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.73.0",
|
|
25
|
+
"commitDate": "2026-07-22T04:58:36Z",
|
|
26
|
+
"buildDate": "2026-07-22T05:24:54.248Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/3f32c968ef8b2a0c9fabbc1be3fbe5c7395a7dc9",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.73.0"
|
|
33
33
|
};
|