@f5-sales-demo/xcsh 22.2.0 → 22.2.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 +4 -0
- package/package.json +8 -8
- package/src/cli/update-cli.ts +85 -21
- package/src/cli/update-recommendation.ts +30 -4
- package/src/internal-urls/api-catalog-qmd-index.generated.ts +2 -2
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/docs-index.generated.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
|
|
43
43
|
### Changed
|
|
44
44
|
|
|
45
|
+
- Made standalone executable and schema-v2 receipt updates transactional, canonicalized receipt paths,
|
|
46
|
+
removed unused adjacent native payloads from standalone installs, and added published installer
|
|
47
|
+
qualification on Linux, macOS, and Windows ([#4510](https://github.com/f5-sales-demo/xcsh/issues/4510)).
|
|
48
|
+
|
|
45
49
|
- Standardized executable updates on `xcsh self-update`; `xcsh update` is now reserved for strict manifest resource updates ([#4487](https://github.com/f5-sales-demo/xcsh/issues/4487)).
|
|
46
50
|
|
|
47
51
|
- Added `power.sleepPrevention` with an `idle` default and cumulative `off`, `idle`, `display`, and
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "22.2.
|
|
4
|
+
"version": "22.2.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",
|
|
@@ -67,13 +67,13 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@agentclientprotocol/sdk": "1.4.0",
|
|
70
|
-
"@f5-sales-demo/pi-agent-core": "22.2.
|
|
71
|
-
"@f5-sales-demo/pi-ai": "22.2.
|
|
72
|
-
"@f5-sales-demo/pi-natives": "22.2.
|
|
73
|
-
"@f5-sales-demo/pi-resource-management": "22.2.
|
|
74
|
-
"@f5-sales-demo/pi-tui": "22.2.
|
|
75
|
-
"@f5-sales-demo/pi-utils": "22.2.
|
|
76
|
-
"@f5-sales-demo/xcsh-stats": "22.2.
|
|
70
|
+
"@f5-sales-demo/pi-agent-core": "22.2.1",
|
|
71
|
+
"@f5-sales-demo/pi-ai": "22.2.1",
|
|
72
|
+
"@f5-sales-demo/pi-natives": "22.2.1",
|
|
73
|
+
"@f5-sales-demo/pi-resource-management": "22.2.1",
|
|
74
|
+
"@f5-sales-demo/pi-tui": "22.2.1",
|
|
75
|
+
"@f5-sales-demo/pi-utils": "22.2.1",
|
|
76
|
+
"@f5-sales-demo/xcsh-stats": "22.2.1",
|
|
77
77
|
"@mozilla/readability": "^0.6",
|
|
78
78
|
"@resvg/resvg-wasm": "2.6.2",
|
|
79
79
|
"@sinclair/typebox": "0.34.52",
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** Context-aware executable update policy and standalone replacement. */
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { realpath } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
4
5
|
import { pipeline } from "node:stream/promises";
|
|
5
6
|
import { APP_NAME, isEnoent, VERSION } from "@f5-sales-demo/pi-utils";
|
|
6
7
|
import chalk from "chalk";
|
|
7
8
|
import {
|
|
9
|
+
createInstallReceipt,
|
|
8
10
|
defaultInstallChannelDependencies,
|
|
9
11
|
formatUpdateRecommendation,
|
|
10
12
|
resolveInstallChannel,
|
|
@@ -70,23 +72,49 @@ export interface StandaloneReplacementOptions {
|
|
|
70
72
|
arch?: NodeJS.Architecture;
|
|
71
73
|
fetchImpl?(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
72
74
|
rename?(source: string, destination: string): Promise<void>;
|
|
75
|
+
readFile?(file: string): Promise<string>;
|
|
76
|
+
writeFile?(file: string, contents: string): Promise<void>;
|
|
77
|
+
unlink?(file: string): Promise<void>;
|
|
73
78
|
validate?(executable: string, expectedVersion: string): Promise<void>;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
async function ignoreMissing(unlink: (file: string) => Promise<void>, file: string): Promise<void> {
|
|
82
|
+
try {
|
|
83
|
+
await unlink(file);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!isEnoent(error)) throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Replace a proven standalone executable and receipt as one rollback-safe transaction. */
|
|
77
90
|
export async function replaceStandaloneExecutable(
|
|
78
91
|
expectedVersion: string,
|
|
79
92
|
options: StandaloneReplacementOptions,
|
|
80
93
|
): Promise<void> {
|
|
81
94
|
const targetPath = options.targetPath;
|
|
82
|
-
const
|
|
95
|
+
const platform = options.platform ?? process.platform;
|
|
96
|
+
const arch = options.arch ?? process.arch;
|
|
97
|
+
const binaryName = getBinaryName(platform, arch);
|
|
83
98
|
const url = `https://github.com/${REPO}/releases/download/v${expectedVersion}/${binaryName}`;
|
|
84
99
|
const tempPath = `${targetPath}.new-${process.pid}`;
|
|
85
|
-
const
|
|
100
|
+
const receiptPath = path.join(path.dirname(targetPath), "xcsh-install.json");
|
|
101
|
+
const tempReceiptPath = `${receiptPath}.new-${process.pid}`;
|
|
102
|
+
const backupPath = `${targetPath}.bak-${process.pid}`;
|
|
103
|
+
const backupReceiptPath = `${receiptPath}.bak-${process.pid}`;
|
|
86
104
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
87
105
|
const rename = options.rename ?? fs.promises.rename;
|
|
106
|
+
const readFile = options.readFile ?? (file => fs.promises.readFile(file, "utf8"));
|
|
107
|
+
const writeFile =
|
|
108
|
+
options.writeFile ??
|
|
109
|
+
(async (file, contents) => fs.promises.writeFile(file, contents, { flag: "wx", mode: 0o644 }));
|
|
110
|
+
const unlink = options.unlink ?? fs.promises.unlink;
|
|
88
111
|
const validate = options.validate ?? validateExecutableVersion;
|
|
89
|
-
|
|
112
|
+
const receiptContents = `${JSON.stringify(createInstallReceipt("standalone", expectedVersion, targetPath, platform, arch))}\n`;
|
|
113
|
+
let originalBinaryMoved = false;
|
|
114
|
+
let originalReceiptMoved = false;
|
|
115
|
+
let replacementBinaryInstalled = false;
|
|
116
|
+
let replacementReceiptInstalled = false;
|
|
117
|
+
let committed = false;
|
|
90
118
|
|
|
91
119
|
try {
|
|
92
120
|
const response = await fetchImpl(url, { redirect: "follow" });
|
|
@@ -94,33 +122,69 @@ export async function replaceStandaloneExecutable(
|
|
|
94
122
|
await pipeline(response.body, fs.createWriteStream(tempPath, { mode: 0o755, flags: "wx" }));
|
|
95
123
|
await fs.promises.chmod(tempPath, 0o755);
|
|
96
124
|
await validate(tempPath, expectedVersion);
|
|
125
|
+
await writeFile(tempReceiptPath, receiptContents);
|
|
126
|
+
if ((await readFile(tempReceiptPath)) !== receiptContents)
|
|
127
|
+
throw new Error("Staged install receipt validation failed");
|
|
97
128
|
|
|
98
|
-
try {
|
|
99
|
-
await fs.promises.unlink(backupPath);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (!isEnoent(error)) throw error;
|
|
102
|
-
}
|
|
103
129
|
await rename(targetPath, backupPath);
|
|
104
|
-
|
|
130
|
+
originalBinaryMoved = true;
|
|
131
|
+
await rename(receiptPath, backupReceiptPath);
|
|
132
|
+
originalReceiptMoved = true;
|
|
105
133
|
await rename(tempPath, targetPath);
|
|
134
|
+
replacementBinaryInstalled = true;
|
|
135
|
+
await rename(tempReceiptPath, receiptPath);
|
|
136
|
+
replacementReceiptInstalled = true;
|
|
106
137
|
await validate(targetPath, expectedVersion);
|
|
107
|
-
await
|
|
108
|
-
|
|
138
|
+
if ((await readFile(receiptPath)) !== receiptContents) throw new Error("Installed receipt validation failed");
|
|
139
|
+
committed = true;
|
|
140
|
+
await ignoreMissing(unlink, backupPath).catch(() => {});
|
|
141
|
+
originalBinaryMoved = false;
|
|
142
|
+
await ignoreMissing(unlink, backupReceiptPath).catch(() => {});
|
|
143
|
+
originalReceiptMoved = false;
|
|
109
144
|
} catch (error) {
|
|
110
|
-
|
|
145
|
+
const rollbackErrors: unknown[] = [];
|
|
146
|
+
if (replacementReceiptInstalled) {
|
|
147
|
+
try {
|
|
148
|
+
await ignoreMissing(unlink, receiptPath);
|
|
149
|
+
replacementReceiptInstalled = false;
|
|
150
|
+
} catch (rollbackError) {
|
|
151
|
+
rollbackErrors.push(rollbackError);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (originalReceiptMoved) {
|
|
155
|
+
try {
|
|
156
|
+
await rename(backupReceiptPath, receiptPath);
|
|
157
|
+
originalReceiptMoved = false;
|
|
158
|
+
} catch (rollbackError) {
|
|
159
|
+
rollbackErrors.push(rollbackError);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (replacementBinaryInstalled) {
|
|
111
163
|
try {
|
|
112
|
-
await
|
|
113
|
-
|
|
114
|
-
|
|
164
|
+
await ignoreMissing(unlink, targetPath);
|
|
165
|
+
replacementBinaryInstalled = false;
|
|
166
|
+
} catch (rollbackError) {
|
|
167
|
+
rollbackErrors.push(rollbackError);
|
|
115
168
|
}
|
|
116
|
-
await rename(backupPath, targetPath);
|
|
117
|
-
originalMoved = false;
|
|
118
169
|
}
|
|
170
|
+
if (originalBinaryMoved) {
|
|
171
|
+
try {
|
|
172
|
+
await rename(backupPath, targetPath);
|
|
173
|
+
originalBinaryMoved = false;
|
|
174
|
+
} catch (rollbackError) {
|
|
175
|
+
rollbackErrors.push(rollbackError);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (rollbackErrors.length > 0)
|
|
179
|
+
throw new AggregateError([error, ...rollbackErrors], "Self-update rollback failed");
|
|
119
180
|
throw error;
|
|
120
181
|
} finally {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
182
|
+
await ignoreMissing(unlink, tempPath).catch(() => {});
|
|
183
|
+
await ignoreMissing(unlink, tempReceiptPath).catch(() => {});
|
|
184
|
+
if (committed) {
|
|
185
|
+
await ignoreMissing(unlink, backupPath).catch(() => {});
|
|
186
|
+
await ignoreMissing(unlink, backupReceiptPath).catch(() => {});
|
|
187
|
+
}
|
|
124
188
|
}
|
|
125
189
|
}
|
|
126
190
|
|
|
@@ -37,8 +37,10 @@ export interface InstallChannelDependencies {
|
|
|
37
37
|
run(command: string, args: string[]): Promise<CommandResult>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
export const INSTALL_RECEIPT_SCHEMA_VERSION = 2;
|
|
41
|
+
|
|
42
|
+
export interface InstallReceipt {
|
|
43
|
+
schemaVersion: typeof INSTALL_RECEIPT_SCHEMA_VERSION;
|
|
42
44
|
channel: "standalone" | "windows-installer";
|
|
43
45
|
version: string;
|
|
44
46
|
executablePath: string;
|
|
@@ -46,6 +48,8 @@ interface InstallReceipt {
|
|
|
46
48
|
arch: string;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
const INSTALL_RECEIPT_KEYS = ["arch", "channel", "executablePath", "platform", "schemaVersion", "version"];
|
|
52
|
+
|
|
49
53
|
function platformPath(platform: NodeJS.Platform): typeof path.posix | typeof path.win32 {
|
|
50
54
|
return platform === "win32" ? path.win32 : path.posix;
|
|
51
55
|
}
|
|
@@ -55,6 +59,25 @@ function comparablePath(value: string, platform: NodeJS.Platform): string {
|
|
|
55
59
|
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
export function createInstallReceipt(
|
|
63
|
+
channel: InstallReceipt["channel"],
|
|
64
|
+
version: string,
|
|
65
|
+
executablePath: string,
|
|
66
|
+
platform: NodeJS.Platform,
|
|
67
|
+
arch: string,
|
|
68
|
+
): InstallReceipt {
|
|
69
|
+
const pathApi = platformPath(platform);
|
|
70
|
+
if (!pathApi.isAbsolute(executablePath)) throw new Error("Install receipt executable path must be absolute");
|
|
71
|
+
return {
|
|
72
|
+
schemaVersion: INSTALL_RECEIPT_SCHEMA_VERSION,
|
|
73
|
+
channel,
|
|
74
|
+
version,
|
|
75
|
+
executablePath: pathApi.normalize(executablePath),
|
|
76
|
+
platform,
|
|
77
|
+
arch,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
function releaseAsset(platform: NodeJS.Platform, arch: string): string | undefined {
|
|
59
82
|
if (platform === "linux" && (arch === "x64" || arch === "arm64")) return `xcsh-linux-${arch}`;
|
|
60
83
|
if (platform === "darwin" && (arch === "x64" || arch === "arm64")) return `xcsh-darwin-${arch}`;
|
|
@@ -82,12 +105,15 @@ function receiptIsValid(
|
|
|
82
105
|
if (!value || typeof value !== "object") return false;
|
|
83
106
|
const receipt = value as Partial<InstallReceipt>;
|
|
84
107
|
if (
|
|
85
|
-
receipt.
|
|
108
|
+
JSON.stringify(Object.keys(receipt).sort()) !== JSON.stringify(INSTALL_RECEIPT_KEYS) ||
|
|
109
|
+
receipt.schemaVersion !== INSTALL_RECEIPT_SCHEMA_VERSION ||
|
|
86
110
|
(receipt.channel !== "standalone" && receipt.channel !== "windows-installer") ||
|
|
87
111
|
receipt.version !== deps.version ||
|
|
88
112
|
receipt.platform !== deps.platform ||
|
|
89
113
|
receipt.arch !== deps.arch ||
|
|
90
114
|
typeof receipt.executablePath !== "string" ||
|
|
115
|
+
!platformPath(deps.platform).isAbsolute(receipt.executablePath) ||
|
|
116
|
+
platformPath(deps.platform).normalize(receipt.executablePath) !== receipt.executablePath ||
|
|
91
117
|
comparablePath(receipt.executablePath, deps.platform) !== comparablePath(resolvedExecPath, deps.platform)
|
|
92
118
|
) {
|
|
93
119
|
return false;
|
|
@@ -233,7 +259,7 @@ export async function resolveInstallChannel(deps: InstallChannelDependencies): P
|
|
|
233
259
|
if (receiptIsValid(parsed, deps, resolvedExecPath)) {
|
|
234
260
|
signals.push({
|
|
235
261
|
channel: parsed.channel,
|
|
236
|
-
evidence: `valid schema-
|
|
262
|
+
evidence: `valid schema-v2 ${parsed.channel} receipt at ${receiptPath}`,
|
|
237
263
|
});
|
|
238
264
|
} else {
|
|
239
265
|
diagnostics.push(`invalid or stale install receipt at ${receiptPath}`);
|