@f5-sales-demo/xcsh 19.104.0 → 19.105.1
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
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [19.105.1] - 2026-07-31
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed GPT-5.6 Sol inference through LiteLLM when deterministic print mode requests an unsupported temperature ([#2698](https://github.com/f5-sales-demo/xcsh/issues/2698))
|
|
10
|
+
|
|
5
11
|
## [19.104.0] - 2026-07-31
|
|
6
12
|
|
|
7
13
|
### Added
|
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.105.1",
|
|
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",
|
|
@@ -57,13 +57,13 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
59
59
|
"@mozilla/readability": "^0.6",
|
|
60
|
-
"@f5-sales-demo/xcsh-stats": "19.
|
|
61
|
-
"@f5-sales-demo/pi-agent-core": "19.
|
|
62
|
-
"@f5-sales-demo/pi-ai": "19.
|
|
63
|
-
"@f5-sales-demo/pi-natives": "19.
|
|
64
|
-
"@f5-sales-demo/pi-resource-management": "19.
|
|
65
|
-
"@f5-sales-demo/pi-tui": "19.
|
|
66
|
-
"@f5-sales-demo/pi-utils": "19.
|
|
60
|
+
"@f5-sales-demo/xcsh-stats": "19.105.1",
|
|
61
|
+
"@f5-sales-demo/pi-agent-core": "19.105.1",
|
|
62
|
+
"@f5-sales-demo/pi-ai": "19.105.1",
|
|
63
|
+
"@f5-sales-demo/pi-natives": "19.105.1",
|
|
64
|
+
"@f5-sales-demo/pi-resource-management": "19.105.1",
|
|
65
|
+
"@f5-sales-demo/pi-tui": "19.105.1",
|
|
66
|
+
"@f5-sales-demo/pi-utils": "19.105.1",
|
|
67
67
|
"@sinclair/typebox": "^0.34",
|
|
68
68
|
"@xterm/headless": "^6.0",
|
|
69
69
|
"ajv": "^8.20",
|
|
@@ -43,8 +43,58 @@ const IS_BUN_COMPILED =
|
|
|
43
43
|
|
|
44
44
|
// Dev-mode assets: packages/coding-agent/src/browser → packages/office-pane/dist.
|
|
45
45
|
const DEV_DIST_DIR = path.resolve(import.meta.dir, "..", "..", "..", "office-pane", "dist");
|
|
46
|
+
/** The office-pane package itself. Present in a checkout, absent from a published tarball. */
|
|
47
|
+
const DEV_PACKAGE_DIR = path.dirname(DEV_DIST_DIR);
|
|
46
48
|
const COMPILED_DIR_ROOT = path.join(os.tmpdir(), "xcsh-office-pane");
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The files a usable pane must have regardless of what its manifest says.
|
|
52
|
+
*
|
|
53
|
+
* Not one marker. The build writes `taskpane.html` first and `manifest.json` three steps later, so a
|
|
54
|
+
* single marker accepts an interrupted build: `serve` would answer the page, then `manifest` and
|
|
55
|
+
* `sideload` would fail with the same ENOENT this check exists to replace.
|
|
56
|
+
*/
|
|
57
|
+
const PANE_REQUIRED_FILES = ["taskpane.html", "taskpane.js", "manifest.json"] as const;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The assets a manifest says it needs, as dist-relative paths.
|
|
61
|
+
*
|
|
62
|
+
* Read out of the manifest rather than listed in this file, so adding an icon there cannot leave this
|
|
63
|
+
* check behind — the same "one map, or the copies drift" rule the rest of the pane follows.
|
|
64
|
+
*
|
|
65
|
+
* They are NOT cosmetic, which an earlier version of this guard assumed by grouping them with the
|
|
66
|
+
* fonts. `office sideload` shells out to office-addin-debugging, whose zip step fails outright on a
|
|
67
|
+
* missing `assets/color.png`; the comment in `runSideload` records exactly that. Since the build copies
|
|
68
|
+
* `assets/` after `manifest.json`, an interruption between the two leaves every other required file in
|
|
69
|
+
* place — and `runSideload` deletes the existing WEF registration before it discovers the problem.
|
|
70
|
+
*
|
|
71
|
+
* The manifest carries both forms, `assets/color.png` and an absolute `…:8444/assets/icon-16.png`, and
|
|
72
|
+
* both normalise to the same dist-relative path.
|
|
73
|
+
*/
|
|
74
|
+
export function manifestAssetPaths(manifestText: string): string[] {
|
|
75
|
+
return [...new Set(manifestText.match(/assets\/[\w.-]+/g) ?? [])];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Which supply route was expected to provide the pane, and therefore what the remedy is. */
|
|
79
|
+
export type PaneSource = "compiled" | "dev" | "packaged";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* There is no pane to serve, and it is the environment's doing rather than a bug.
|
|
83
|
+
*
|
|
84
|
+
* Its own class so the command boundary can print the remedy and exit 1, while a real defect keeps its
|
|
85
|
+
* stack trace. String-matching the message would couple the two and quietly swallow the next genuine
|
|
86
|
+
* failure whose text happened to look similar.
|
|
87
|
+
*/
|
|
88
|
+
export class OfficePaneUnavailableError extends Error {
|
|
89
|
+
constructor(
|
|
90
|
+
message: string,
|
|
91
|
+
readonly source: PaneSource,
|
|
92
|
+
) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "OfficePaneUnavailableError";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
48
98
|
const getEmbeddedArchive = (() => {
|
|
49
99
|
const txt = embeddedPaneArchiveTxt.replaceAll(/[\s\r\n]/g, "").trim();
|
|
50
100
|
if (!txt) return null;
|
|
@@ -81,31 +131,166 @@ async function extractEmbeddedArchive(archiveBytes: Buffer, outputDir: string):
|
|
|
81
131
|
}
|
|
82
132
|
}
|
|
83
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Why there is no pane to serve, and what to do about it.
|
|
136
|
+
*
|
|
137
|
+
* Pure, and separate from the check, because the wording is the whole value of this path and the two
|
|
138
|
+
* audiences need opposite advice. Telling a developer to install a binary hides that they have simply
|
|
139
|
+
* not built yet; telling somebody who installed from npm to run a build names a directory they do not
|
|
140
|
+
* have. Getting that backwards is worse than the ENOENT it replaces.
|
|
141
|
+
*/
|
|
142
|
+
export function paneUnavailableMessage(distDir: string, source: PaneSource): string {
|
|
143
|
+
if (source === "compiled") {
|
|
144
|
+
return (
|
|
145
|
+
"The Office pane is missing from this binary: office-pane.generated.txt carries no embedded " +
|
|
146
|
+
"archive. Rebuild the binary with the office-pane assets baked in."
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (source === "dev") {
|
|
150
|
+
return (
|
|
151
|
+
`The Office pane has not been built: ${distDir} does not exist. ` +
|
|
152
|
+
"Run `bun run build` in packages/office-pane, then try again."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return (
|
|
156
|
+
"The Office pane is not included in the npm package — it ships as a build-time asset of the " +
|
|
157
|
+
"compiled binary, which is also what provides the sideload and the trusted certificate. " +
|
|
158
|
+
"Install it with `brew install f5-sales-demo/tap/xcsh` and run `xcsh office` from there."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Which supply route a non-compiled run was relying on, from whether the office-pane package is here.
|
|
164
|
+
*
|
|
165
|
+
* A checkout has `packages/office-pane` and may simply not have built it; a published tarball has no
|
|
166
|
+
* `packages/` directory at all. One line, but the line that chooses between two opposite remedies, so
|
|
167
|
+
* it is named and tested rather than inlined as a ternary nothing covers.
|
|
168
|
+
*/
|
|
169
|
+
export function paneSourceForLayout(officePanePackagePresent: boolean): PaneSource {
|
|
170
|
+
return officePanePackagePresent ? "dev" : "packaged";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The manifest's text if it parses as JSON, else null — presence alone says too little. */
|
|
174
|
+
async function readValidManifest(manifestPath: string): Promise<string | null> {
|
|
175
|
+
try {
|
|
176
|
+
const text = await Bun.file(manifestPath).text();
|
|
177
|
+
JSON.parse(text);
|
|
178
|
+
return text;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Which of the files a usable pane needs are absent from `dir`. Empty means complete. */
|
|
185
|
+
export async function missingPaneFiles(dir: string): Promise<string[]> {
|
|
186
|
+
const missing: string[] = [];
|
|
187
|
+
for (const name of PANE_REQUIRED_FILES) {
|
|
188
|
+
if (!(await Bun.file(path.join(dir, name)).exists())) missing.push(name);
|
|
189
|
+
}
|
|
190
|
+
// Only worth asking once the manifest is there to be read — and a manifest that is present but
|
|
191
|
+
// unreadable is not a manifest. A truncated one has no asset references, so an existence-only check
|
|
192
|
+
// finds nothing missing, calls the pane complete, and lets `office manifest` print invalid JSON.
|
|
193
|
+
if (!missing.includes("manifest.json")) {
|
|
194
|
+
const manifestText = await readValidManifest(path.join(dir, "manifest.json"));
|
|
195
|
+
if (manifestText === null) {
|
|
196
|
+
missing.push("manifest.json (present but not valid JSON)");
|
|
197
|
+
} else {
|
|
198
|
+
for (const asset of manifestAssetPaths(manifestText)) {
|
|
199
|
+
if (!(await Bun.file(path.join(dir, asset)).exists())) missing.push(asset);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return missing;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** True when `dir` holds every file a pane needs. */
|
|
207
|
+
export async function isCompletePane(dir: string): Promise<boolean> {
|
|
208
|
+
return (await missingPaneFiles(dir)).length === 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Return `dir` only if it actually holds a built pane, else refuse with {@link paneUnavailableMessage}.
|
|
213
|
+
*
|
|
214
|
+
* The dev branch used to return its path unchecked, which is how a published npm install came to bind
|
|
215
|
+
* :8444 and answer 404 to every request — Office rendered "Not Found" and nothing reported an error.
|
|
216
|
+
* Serving nothing quietly is the failure this exists to prevent.
|
|
217
|
+
*/
|
|
218
|
+
export async function resolvePaneDir(dir: string, source: PaneSource): Promise<string> {
|
|
219
|
+
const missing = await missingPaneFiles(dir);
|
|
220
|
+
if (missing.length === 0) return dir;
|
|
221
|
+
// Name what is absent. "The pane is missing" sends someone looking at the whole directory; "no
|
|
222
|
+
// manifest.json" points at the step of the build that did not finish.
|
|
223
|
+
throw new OfficePaneUnavailableError(
|
|
224
|
+
`${paneUnavailableMessage(dir, source)} (missing: ${missing.join(", ")})`,
|
|
225
|
+
source,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Extract into a staging directory and move it into place only once it validates.
|
|
231
|
+
*
|
|
232
|
+
* Extraction used to write straight into the final hash-addressed directory, so an interruption or a
|
|
233
|
+
* full disk left a partial bundle exactly where the next run looks for a finished one — and every run
|
|
234
|
+
* after that accepted it, serving a pane whose JS 404s or whose script is half a file.
|
|
235
|
+
*
|
|
236
|
+
* Validating file CONTENTS would be an endless chase: a truncated `taskpane.js` is still valid text, so
|
|
237
|
+
* each new file type invites its own check. Publishing atomically closes the whole class instead. A
|
|
238
|
+
* crash leaves a `.incoming-*` directory, which the cache lookup ignores because it is not the name it
|
|
239
|
+
* asks for.
|
|
240
|
+
*
|
|
241
|
+
* The staging name carries the pid so two processes racing a first run cannot rename over each other
|
|
242
|
+
* mid-extraction.
|
|
243
|
+
*/
|
|
244
|
+
export async function publishCompletePane(
|
|
245
|
+
outputDir: string,
|
|
246
|
+
extractInto: (stagingDir: string) => Promise<void>,
|
|
247
|
+
): Promise<string> {
|
|
248
|
+
const stagingDir = `${outputDir}.incoming-${process.pid}`;
|
|
249
|
+
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
250
|
+
await fs.mkdir(stagingDir, { recursive: true });
|
|
251
|
+
try {
|
|
252
|
+
await extractInto(stagingDir);
|
|
253
|
+
// Validate BEFORE publishing, so an incomplete extraction never becomes the cache.
|
|
254
|
+
await resolvePaneDir(stagingDir, "compiled");
|
|
255
|
+
await fs.rm(outputDir, { recursive: true, force: true });
|
|
256
|
+
await fs.rename(stagingDir, outputDir);
|
|
257
|
+
return outputDir;
|
|
258
|
+
} finally {
|
|
259
|
+
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
84
263
|
/**
|
|
85
264
|
* Resolve the directory the assets are served from: the extracted embedded
|
|
86
265
|
* bundle in a compiled binary, else `packages/office-pane/dist` in dev.
|
|
87
266
|
*/
|
|
88
267
|
export async function getOfficePaneDir(): Promise<string> {
|
|
89
|
-
if (!IS_BUN_COMPILED)
|
|
268
|
+
if (!IS_BUN_COMPILED) {
|
|
269
|
+
const present = await Bun.file(path.join(DEV_PACKAGE_DIR, "package.json")).exists();
|
|
270
|
+
return resolvePaneDir(DEV_DIST_DIR, paneSourceForLayout(present));
|
|
271
|
+
}
|
|
90
272
|
if (compiledDirPromise) return compiledDirPromise;
|
|
91
273
|
|
|
92
274
|
const archiveBytes = getEmbeddedArchive?.();
|
|
93
275
|
if (!archiveBytes) {
|
|
94
|
-
throw new
|
|
276
|
+
throw new OfficePaneUnavailableError(paneUnavailableMessage(COMPILED_DIR_ROOT, "compiled"), "compiled");
|
|
95
277
|
}
|
|
96
278
|
|
|
97
279
|
compiledDirPromise = (async () => {
|
|
98
280
|
const bundleHash = Bun.hash(archiveBytes).toString(16);
|
|
99
281
|
const outputDir = path.join(COMPILED_DIR_ROOT, bundleHash);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
282
|
+
// A cached extraction is reused only if it is COMPLETE. The old check was `taskpane.html` alone,
|
|
283
|
+
// so an extraction interrupted after the page but before its bundle left a hash-addressed
|
|
284
|
+
// directory that every later run accepted — superseding a working server to serve a pane whose
|
|
285
|
+
// JS 404s, until somebody deleted a temp directory they had no reason to suspect.
|
|
286
|
+
//
|
|
287
|
+
// Incomplete is not fatal here, unlike in the dev case: the archive is in hand, so the remedy is
|
|
288
|
+
// to extract it again rather than to tell anyone anything.
|
|
289
|
+
if (await isCompletePane(outputDir)) return outputDir;
|
|
104
290
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return outputDir;
|
|
291
|
+
// Still incomplete after a fresh extraction means the baked archive is itself short, which no
|
|
292
|
+
// amount of retrying fixes — publishCompletePane refuses rather than serving a pane with holes.
|
|
293
|
+
return publishCompletePane(outputDir, staging => extractEmbeddedArchive(archiveBytes, staging));
|
|
109
294
|
})();
|
|
110
295
|
|
|
111
296
|
return compiledDirPromise;
|
|
@@ -151,9 +336,13 @@ export interface OfficePaneServer {
|
|
|
151
336
|
* Start the fixed :8444 HTTPS listener serving the embedded/dev assets. Resolves
|
|
152
337
|
* TLS via {@link resolveBridgeTls} (the shared local-ip.sh cert). Serves only GET
|
|
153
338
|
* (405 otherwise); unknown paths return 404.
|
|
339
|
+
*
|
|
340
|
+
* The asset directory is resolved BEFORE the port is bound and before TLS is fetched, so a run with
|
|
341
|
+
* no pane available refuses without holding :8444 and without reaching the network. `assetDir` is the
|
|
342
|
+
* seam that lets a test assert exactly that, in the same spirit as the pure `handleAssetRequest`.
|
|
154
343
|
*/
|
|
155
|
-
export async function startOfficePaneServer(port = OFFICE_PANE_PORT): Promise<OfficePaneServer> {
|
|
156
|
-
const dir = await getOfficePaneDir();
|
|
344
|
+
export async function startOfficePaneServer(port = OFFICE_PANE_PORT, assetDir?: string): Promise<OfficePaneServer> {
|
|
345
|
+
const dir = assetDir === undefined ? await getOfficePaneDir() : await resolvePaneDir(assetDir, "dev");
|
|
157
346
|
const tls = await resolveBridgeTls();
|
|
158
347
|
|
|
159
348
|
const server = Bun.serve({
|
package/src/cli/office-cli.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getOfficePaneDir,
|
|
18
18
|
OFFICE_PANE_PORT,
|
|
19
19
|
type OfficePaneServer,
|
|
20
|
+
OfficePaneUnavailableError,
|
|
20
21
|
readManifest,
|
|
21
22
|
startOfficePaneServer,
|
|
22
23
|
} from "../browser/office-pane-server";
|
|
@@ -55,9 +56,15 @@ export interface OfficeServeDeps {
|
|
|
55
56
|
startOfficePaneServer: typeof startOfficePaneServer;
|
|
56
57
|
startHeadlessChatBridge: typeof startHeadlessChatBridge;
|
|
57
58
|
supersedeStaleServe: typeof supersedeStaleServe;
|
|
59
|
+
getOfficePaneDir: typeof getOfficePaneDir;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
const defaultServeDeps: OfficeServeDeps = {
|
|
62
|
+
const defaultServeDeps: OfficeServeDeps = {
|
|
63
|
+
startOfficePaneServer,
|
|
64
|
+
startHeadlessChatBridge,
|
|
65
|
+
supersedeStaleServe,
|
|
66
|
+
getOfficePaneDir,
|
|
67
|
+
};
|
|
61
68
|
|
|
62
69
|
/** A running `office serve`: the pane file server, the (optional) chat bridge, and
|
|
63
70
|
* a teardown that disposes both. */
|
|
@@ -75,6 +82,12 @@ export interface OfficeServeHandle {
|
|
|
75
82
|
* Extracted from {@link runServe} so the start/teardown wiring is unit-testable.
|
|
76
83
|
*/
|
|
77
84
|
export async function startOfficeServe(deps: OfficeServeDeps = defaultServeDeps): Promise<OfficeServeHandle> {
|
|
85
|
+
// Prove there is a pane to serve BEFORE anything is torn down. supersedeStaleServe SIGTERMs a
|
|
86
|
+
// running serve to take the port, so checking afterwards let an install with no pane at all stop
|
|
87
|
+
// the operator's working one and then exit with nothing in its place. Refusing loudly is only an
|
|
88
|
+
// improvement if it refuses before it breaks something.
|
|
89
|
+
await deps.getOfficePaneDir();
|
|
90
|
+
|
|
78
91
|
// Step down a stale serve squatting :8444 (e.g. left over from before a
|
|
79
92
|
// `brew upgrade`) so this start binds cleanly instead of "port 8444 in use".
|
|
80
93
|
const superseded = await deps.supersedeStaleServe(OFFICE_PANE_PORT);
|
|
@@ -238,6 +251,21 @@ export async function runOfficeCommand(
|
|
|
238
251
|
args: OfficeCommandArgs,
|
|
239
252
|
deps: OfficeCommandDeps = defaultCommandDeps,
|
|
240
253
|
): Promise<void> {
|
|
254
|
+
try {
|
|
255
|
+
await dispatchOfficeCommand(args, deps);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
// Only this one class. The published npm form carries `office` and no pane bundle, so every
|
|
258
|
+
// action here can legitimately have nothing to work with — that is a fact about the install,
|
|
259
|
+
// not a defect, and it deserves the remedy rather than a stack trace. Anything else rethrows
|
|
260
|
+
// with its stack intact, because swallowing a real failure here is how the original silent
|
|
261
|
+
// 404 survived.
|
|
262
|
+
if (!(err instanceof OfficePaneUnavailableError)) throw err;
|
|
263
|
+
console.error(err.message);
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function dispatchOfficeCommand(args: OfficeCommandArgs, deps: OfficeCommandDeps): Promise<void> {
|
|
241
269
|
switch (args.action) {
|
|
242
270
|
case "serve":
|
|
243
271
|
await deps.serve();
|
|
@@ -140,6 +140,7 @@ const ReasoningEffortMapSchema = Type.Object({
|
|
|
140
140
|
|
|
141
141
|
const OpenAICompatSchema = Type.Object({
|
|
142
142
|
supportsStore: Type.Optional(Type.Boolean()),
|
|
143
|
+
supportsTemperature: Type.Optional(Type.Boolean()),
|
|
143
144
|
supportsDeveloperRole: Type.Optional(Type.Boolean()),
|
|
144
145
|
supportsReasoningEffort: Type.Optional(Type.Boolean()),
|
|
145
146
|
reasoningEffortMap: Type.Optional(ReasoningEffortMapSchema),
|
|
@@ -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.105.1",
|
|
21
|
+
"commit": "9f750089164f53de345f753a606ba28ff4cd446b",
|
|
22
|
+
"shortCommit": "9f75008",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.105.1",
|
|
25
|
+
"commitDate": "2026-07-31T15:37:23Z",
|
|
26
|
+
"buildDate": "2026-07-31T16:02:58.879Z",
|
|
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/9f750089164f53de345f753a606ba28ff4cd446b",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.105.1"
|
|
33
33
|
};
|