@f5-sales-demo/xcsh 21.5.3 → 21.5.6
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 +3 -0
- package/bin/xcsh.ts +19 -0
- package/package.json +10 -9
- package/src/cli/update-cli.ts +12 -3
- package/src/cli.ts +18 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/npm-binary-bootstrap.ts +185 -0
package/CHANGELOG.md
CHANGED
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
|
|
37
37
|
### Fixed
|
|
38
38
|
|
|
39
|
+
- Made npm, Bun, and default installer commands execute the exact matching compiled release so
|
|
40
|
+
licensed Corporate Vertex authentication is available without exposing build credentials in
|
|
41
|
+
published source ([#3557](https://github.com/f5-sales-demo/xcsh/issues/3557)).
|
|
39
42
|
- Isolated post-publication npm verification in a job-local writable prefix and failed immediately
|
|
40
43
|
for non-registry installation errors
|
|
41
44
|
([#3375](https://github.com/f5-sales-demo/xcsh/issues/3375)).
|
package/bin/xcsh.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
4
|
+
import { ensureReleaseBinary, getPackageDistributionChannel, runReleaseBinary } from "../src/npm-binary-bootstrap";
|
|
5
|
+
|
|
6
|
+
async function main(): Promise<void> {
|
|
7
|
+
const binaryPath = await ensureReleaseBinary({ version: packageJson.version });
|
|
8
|
+
const result = await runReleaseBinary(binaryPath, process.argv.slice(2), getPackageDistributionChannel(import.meta.dir));
|
|
9
|
+
if (result.signalCode) {
|
|
10
|
+
process.kill(process.pid, result.signalCode);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
process.exitCode = result.exitCode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
main().catch(error => {
|
|
17
|
+
console.error(`xcsh: ${error instanceof Error ? error.message : String(error)}`);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "21.5.
|
|
4
|
+
"version": "21.5.6",
|
|
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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"main": "./src/index.ts",
|
|
29
29
|
"types": "./src/index.ts",
|
|
30
30
|
"bin": {
|
|
31
|
-
"xcsh": "
|
|
31
|
+
"xcsh": "bin/xcsh.ts"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"prebuild": "bun ../../scripts/check-installed-dependencies.ts",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
64
|
-
"@f5-sales-demo/pi-agent-core": "21.5.
|
|
65
|
-
"@f5-sales-demo/pi-ai": "21.5.
|
|
66
|
-
"@f5-sales-demo/pi-natives": "21.5.
|
|
67
|
-
"@f5-sales-demo/pi-resource-management": "21.5.
|
|
68
|
-
"@f5-sales-demo/pi-tui": "21.5.
|
|
69
|
-
"@f5-sales-demo/pi-utils": "21.5.
|
|
70
|
-
"@f5-sales-demo/xcsh-stats": "21.5.
|
|
64
|
+
"@f5-sales-demo/pi-agent-core": "21.5.6",
|
|
65
|
+
"@f5-sales-demo/pi-ai": "21.5.6",
|
|
66
|
+
"@f5-sales-demo/pi-natives": "21.5.6",
|
|
67
|
+
"@f5-sales-demo/pi-resource-management": "21.5.6",
|
|
68
|
+
"@f5-sales-demo/pi-tui": "21.5.6",
|
|
69
|
+
"@f5-sales-demo/pi-utils": "21.5.6",
|
|
70
|
+
"@f5-sales-demo/xcsh-stats": "21.5.6",
|
|
71
71
|
"@mozilla/readability": "^0.6",
|
|
72
72
|
"@sinclair/typebox": "^0.34",
|
|
73
73
|
"@xterm/headless": "^6.0",
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"bun": ">=1.3.7"
|
|
91
91
|
},
|
|
92
92
|
"files": [
|
|
93
|
+
"bin",
|
|
93
94
|
"src",
|
|
94
95
|
"README.md",
|
|
95
96
|
"CHANGELOG.md"
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -124,12 +124,21 @@ function detectInstallMethod(binPath: string, bunBinDir: string | undefined): In
|
|
|
124
124
|
return "binary";
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
function resolveUpdateMethod(
|
|
127
|
+
function resolveUpdateMethod(
|
|
128
|
+
ompPath: string,
|
|
129
|
+
bunBinDir: string | undefined,
|
|
130
|
+
distributionChannel = process.env.XCSH_DISTRIBUTION_CHANNEL,
|
|
131
|
+
): InstallMethod {
|
|
132
|
+
if (distributionChannel === "bun" || distributionChannel === "npm") return distributionChannel;
|
|
128
133
|
return detectInstallMethod(ompPath, bunBinDir);
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
export function _resolveUpdateMethodForTest(
|
|
132
|
-
|
|
136
|
+
export function _resolveUpdateMethodForTest(
|
|
137
|
+
ompPath: string,
|
|
138
|
+
bunBinDir: string | undefined,
|
|
139
|
+
distributionChannel?: string,
|
|
140
|
+
): InstallMethod {
|
|
141
|
+
return resolveUpdateMethod(ompPath, bunBinDir, distributionChannel);
|
|
133
142
|
}
|
|
134
143
|
async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
135
144
|
const bunBinDir = await getBunGlobalBinDir();
|
package/src/cli.ts
CHANGED
|
@@ -181,4 +181,22 @@ if (process.env.XCSH_SMOKE_TEST_SPECS === "1") {
|
|
|
181
181
|
process.exit(domainCount > 0 && categoryCount > 0 ? 0 : 1);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// Post-publication verification exercises the licensed build inputs without
|
|
185
|
+
// printing the client, authorization URL, state, challenge, or any credential.
|
|
186
|
+
if (process.env.XCSH_SMOKE_TEST_VERTEX_AUTH === "1") {
|
|
187
|
+
try {
|
|
188
|
+
const { createVertexAuthorizationUrl } = await import("@f5-sales-demo/pi-ai/utils/oauth/google-antigravity");
|
|
189
|
+
const url = new URL(createVertexAuthorizationUrl("release-verification-state", "release-verification-challenge"));
|
|
190
|
+
const ready =
|
|
191
|
+
url.protocol === "https:" &&
|
|
192
|
+
url.searchParams.get("code_challenge_method") === "S256" &&
|
|
193
|
+
Boolean(url.searchParams.get("client_id"));
|
|
194
|
+
console.log(ready ? "vertex-auth: ready" : "vertex-auth: unavailable");
|
|
195
|
+
process.exit(ready ? 0 : 1);
|
|
196
|
+
} catch {
|
|
197
|
+
console.log("vertex-auth: unavailable");
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
184
202
|
await runCli(process.argv.slice(2));
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "21.5.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "21.5.6",
|
|
21
|
+
"commit": "d3cadfbd84e1ec85fc4f48619ffdabe334164c90",
|
|
22
|
+
"shortCommit": "d3cadfb",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v21.5.
|
|
25
|
-
"commitDate": "2026-09-
|
|
26
|
-
"buildDate": "2026-09-
|
|
24
|
+
"tag": "v21.5.6",
|
|
25
|
+
"commitDate": "2026-09-02T00:45:22+00:00",
|
|
26
|
+
"buildDate": "2026-09-02T01:15:05.987Z",
|
|
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/v21.5.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/d3cadfbd84e1ec85fc4f48619ffdabe334164c90",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.5.6"
|
|
33
33
|
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { chmod, mkdir, open, rename, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const RELEASE_REPOSITORY = "f5-sales-demo/xcsh";
|
|
6
|
+
const LOCK_STALE_AFTER_MS = 5 * 60_000;
|
|
7
|
+
const LOCK_WAIT_TIMEOUT_MS = 3 * 60_000;
|
|
8
|
+
|
|
9
|
+
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
10
|
+
|
|
11
|
+
export interface EnsureReleaseBinaryOptions {
|
|
12
|
+
version: string;
|
|
13
|
+
platform?: NodeJS.Platform;
|
|
14
|
+
arch?: string;
|
|
15
|
+
cacheRoot?: string;
|
|
16
|
+
fetchImpl?: FetchLike;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ReleaseBinaryResult {
|
|
20
|
+
exitCode: number;
|
|
21
|
+
signalCode?: NodeJS.Signals;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function unsupportedPlatform(platform: string, arch: string): never {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Unsupported xcsh platform: ${platform}/${arch}. Install a supported compiled release from https://github.com/${RELEASE_REPOSITORY}/releases.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getReleaseAsset(platform: string, arch: string): string {
|
|
31
|
+
if (platform === "linux" && (arch === "x64" || arch === "arm64")) return `xcsh-linux-${arch}`;
|
|
32
|
+
if (platform === "darwin" && (arch === "x64" || arch === "arm64")) return `xcsh-darwin-${arch}`;
|
|
33
|
+
if (platform === "win32" && arch === "x64") return "xcsh-windows-x64.exe";
|
|
34
|
+
return unsupportedPlatform(platform, arch);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getReleaseDownloadUrl(version: string, asset: string): string {
|
|
38
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
39
|
+
throw new Error(`Invalid xcsh package version: ${version}`);
|
|
40
|
+
}
|
|
41
|
+
return `https://github.com/${RELEASE_REPOSITORY}/releases/download/v${version}/${asset}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getPackageDistributionChannel(moduleDirectory: string): "bun" | "npm" {
|
|
45
|
+
const normalized = moduleDirectory.replaceAll("\\", "/").toLowerCase();
|
|
46
|
+
return normalized.includes("/.bun/") ? "bun" : "npm";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getDefaultReleaseCacheRoot(platform: NodeJS.Platform = process.platform): string {
|
|
50
|
+
if (process.env.XCSH_RELEASE_CACHE_DIR) return process.env.XCSH_RELEASE_CACHE_DIR;
|
|
51
|
+
if (platform === "win32") {
|
|
52
|
+
return join(process.env.LOCALAPPDATA || process.env.TEMP || tmpdir(), "xcsh", "releases");
|
|
53
|
+
}
|
|
54
|
+
return join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "xcsh", "releases");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function releaseValidationEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
58
|
+
const validationEnvironment = { ...environment };
|
|
59
|
+
delete validationEnvironment.XCSH_SMOKE_TEST_SPECS;
|
|
60
|
+
delete validationEnvironment.XCSH_SMOKE_TEST_VERTEX_AUTH;
|
|
61
|
+
return validationEnvironment;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function validateReleaseBinary(
|
|
65
|
+
binaryPath: string,
|
|
66
|
+
version: string,
|
|
67
|
+
): Promise<{ valid: boolean; actual?: string }> {
|
|
68
|
+
try {
|
|
69
|
+
if (process.platform !== "win32") await chmod(binaryPath, 0o755);
|
|
70
|
+
const child = Bun.spawn([binaryPath, "--version"], {
|
|
71
|
+
stdin: "ignore",
|
|
72
|
+
stdout: "pipe",
|
|
73
|
+
stderr: "pipe",
|
|
74
|
+
env: releaseValidationEnvironment(process.env),
|
|
75
|
+
});
|
|
76
|
+
const [exitCode, stdout] = await Promise.all([child.exited, new Response(child.stdout).text()]);
|
|
77
|
+
if (exitCode !== 0) return { valid: false };
|
|
78
|
+
const actual = stdout.match(/(?:xcsh\/)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/)?.[1];
|
|
79
|
+
return { valid: actual === version, actual };
|
|
80
|
+
} catch {
|
|
81
|
+
return { valid: false };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function cachedBinaryIsValid(binaryPath: string, version: string): Promise<boolean> {
|
|
86
|
+
return (await validateReleaseBinary(binaryPath, version)).valid;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function lockIsStale(lockPath: string): Promise<boolean> {
|
|
90
|
+
try {
|
|
91
|
+
const lockStat = await stat(lockPath);
|
|
92
|
+
return Date.now() - lockStat.mtimeMs > LOCK_STALE_AFTER_MS;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function ensureReleaseBinary(options: EnsureReleaseBinaryOptions): Promise<string> {
|
|
99
|
+
const platform = options.platform ?? process.platform;
|
|
100
|
+
const arch = options.arch ?? process.arch;
|
|
101
|
+
const asset = getReleaseAsset(platform, arch);
|
|
102
|
+
const cacheRoot = options.cacheRoot ?? getDefaultReleaseCacheRoot(platform);
|
|
103
|
+
const cacheDirectory = join(cacheRoot, `v${options.version}`, `${platform}-${arch}`);
|
|
104
|
+
const binaryPath = join(cacheDirectory, asset);
|
|
105
|
+
const lockPath = `${binaryPath}.lock`;
|
|
106
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
107
|
+
|
|
108
|
+
await mkdir(cacheDirectory, { recursive: true });
|
|
109
|
+
if (await cachedBinaryIsValid(binaryPath, options.version)) return binaryPath;
|
|
110
|
+
await rm(binaryPath, { force: true });
|
|
111
|
+
|
|
112
|
+
const waitStartedAt = Date.now();
|
|
113
|
+
let lockHandle: Awaited<ReturnType<typeof open>> | undefined;
|
|
114
|
+
while (!lockHandle) {
|
|
115
|
+
try {
|
|
116
|
+
lockHandle = await open(lockPath, "wx", 0o600);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
119
|
+
if (await cachedBinaryIsValid(binaryPath, options.version)) return binaryPath;
|
|
120
|
+
if (await lockIsStale(lockPath)) {
|
|
121
|
+
await rm(lockPath, { force: true });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (Date.now() - waitStartedAt >= LOCK_WAIT_TIMEOUT_MS) {
|
|
125
|
+
throw new Error(`Timed out waiting for the xcsh v${options.version} binary download lock`);
|
|
126
|
+
}
|
|
127
|
+
await Bun.sleep(100);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const partialPath = `${binaryPath}.partial-${process.pid}-${crypto.randomUUID()}`;
|
|
132
|
+
try {
|
|
133
|
+
if (await cachedBinaryIsValid(binaryPath, options.version)) return binaryPath;
|
|
134
|
+
|
|
135
|
+
const downloadUrl = getReleaseDownloadUrl(options.version, asset);
|
|
136
|
+
const response = await fetchImpl(downloadUrl, { redirect: "follow" });
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(`Failed to download compiled xcsh v${options.version}: HTTP ${response.status}`);
|
|
139
|
+
}
|
|
140
|
+
const bytes = await response.arrayBuffer();
|
|
141
|
+
if (bytes.byteLength === 0) throw new Error(`Downloaded compiled xcsh v${options.version} was empty`);
|
|
142
|
+
await Bun.write(partialPath, bytes);
|
|
143
|
+
if (platform !== "win32") await chmod(partialPath, 0o755);
|
|
144
|
+
|
|
145
|
+
const validation = await validateReleaseBinary(partialPath, options.version);
|
|
146
|
+
if (!validation.valid) {
|
|
147
|
+
const actual = validation.actual ? ` reported version ${validation.actual}` : " could not execute";
|
|
148
|
+
throw new Error(`Downloaded xcsh${actual}; expected ${options.version}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await rename(partialPath, binaryPath);
|
|
152
|
+
return binaryPath;
|
|
153
|
+
} finally {
|
|
154
|
+
await rm(partialPath, { force: true });
|
|
155
|
+
await lockHandle.close();
|
|
156
|
+
await rm(lockPath, { force: true });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function runReleaseBinary(
|
|
161
|
+
binaryPath: string,
|
|
162
|
+
args: string[],
|
|
163
|
+
distributionChannel: "bun" | "npm" = "npm",
|
|
164
|
+
): Promise<ReleaseBinaryResult> {
|
|
165
|
+
const child = Bun.spawn([binaryPath, ...args], {
|
|
166
|
+
stdin: "inherit",
|
|
167
|
+
stdout: "inherit",
|
|
168
|
+
stderr: "inherit",
|
|
169
|
+
env: { ...process.env, XCSH_DISTRIBUTION_CHANNEL: distributionChannel },
|
|
170
|
+
});
|
|
171
|
+
const forwardedSignals: NodeJS.Signals[] = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
172
|
+
const handlers = new Map<NodeJS.Signals, () => void>();
|
|
173
|
+
for (const signal of forwardedSignals) {
|
|
174
|
+
const handler = () => child.kill(signal);
|
|
175
|
+
handlers.set(signal, handler);
|
|
176
|
+
process.on(signal, handler);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const exitCode = await child.exited;
|
|
181
|
+
return { exitCode, signalCode: (child.signalCode as NodeJS.Signals | null) ?? undefined };
|
|
182
|
+
} finally {
|
|
183
|
+
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
184
|
+
}
|
|
185
|
+
}
|