@f5-sales-demo/xcsh 22.2.0 → 22.3.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 +7 -0
- package/package.json +8 -8
- package/src/cli/plugin-cli.ts +17 -7
- package/src/cli/update-cli.ts +85 -21
- package/src/cli/update-recommendation.ts +31 -11
- 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/src/internal-urls/documentation-repository.ts +5 -1
- package/src/internal-urls/documentation-resolve.ts +1 -1
- package/src/internal-urls/documentation-snapshot.ts +28 -1
- package/src/internal-urls/knowledge-classifier.ts +19 -5
- package/src/modes/components/model-selector.ts +148 -40
- package/src/modes/controllers/model-selection.ts +35 -4
- package/src/prompts/rules/llms-search.md +3 -1
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/qmd-smoke.ts +58 -15
package/CHANGELOG.md
CHANGED
|
@@ -42,6 +42,13 @@
|
|
|
42
42
|
|
|
43
43
|
### Changed
|
|
44
44
|
|
|
45
|
+
- Allowed explicit `xcsh plugin setup <plugin> --force` runs to refresh dynamic setup plans even
|
|
46
|
+
when the current integration receipt is already ready ([#4516](https://github.com/f5-sales-demo/xcsh/issues/4516)).
|
|
47
|
+
|
|
48
|
+
- Made standalone executable and schema-v2 receipt updates transactional, canonicalized receipt paths,
|
|
49
|
+
removed unused adjacent native payloads from standalone installs, and added published installer
|
|
50
|
+
qualification on Linux, macOS, and Windows ([#4510](https://github.com/f5-sales-demo/xcsh/issues/4510)).
|
|
51
|
+
|
|
45
52
|
- 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
53
|
|
|
47
54
|
- 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.
|
|
4
|
+
"version": "22.3.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.
|
|
71
|
-
"@f5-sales-demo/pi-ai": "22.
|
|
72
|
-
"@f5-sales-demo/pi-natives": "22.
|
|
73
|
-
"@f5-sales-demo/pi-resource-management": "22.
|
|
74
|
-
"@f5-sales-demo/pi-tui": "22.
|
|
75
|
-
"@f5-sales-demo/pi-utils": "22.
|
|
76
|
-
"@f5-sales-demo/xcsh-stats": "22.
|
|
70
|
+
"@f5-sales-demo/pi-agent-core": "22.3.1",
|
|
71
|
+
"@f5-sales-demo/pi-ai": "22.3.1",
|
|
72
|
+
"@f5-sales-demo/pi-natives": "22.3.1",
|
|
73
|
+
"@f5-sales-demo/pi-resource-management": "22.3.1",
|
|
74
|
+
"@f5-sales-demo/pi-tui": "22.3.1",
|
|
75
|
+
"@f5-sales-demo/pi-utils": "22.3.1",
|
|
76
|
+
"@f5-sales-demo/xcsh-stats": "22.3.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/plugin-cli.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
executeInstallAuthorizedSetup,
|
|
29
29
|
executeReviewedSetup,
|
|
30
30
|
} from "../integrations/setup";
|
|
31
|
-
import type { IntegrationHandle } from "../integrations/types";
|
|
31
|
+
import type { IntegrationHandle, IntegrationState } from "../integrations/types";
|
|
32
32
|
import { theme } from "../modes/theme/theme";
|
|
33
33
|
import { personProfileService } from "../person-profile/service";
|
|
34
34
|
import { ContextError, ContextService, type XCSHContext } from "../services/xcsh-context";
|
|
@@ -336,11 +336,18 @@ async function handleIntegrationStatus(args: string[], flags: { json?: boolean;
|
|
|
336
336
|
});
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
export
|
|
339
|
+
export function shouldSkipIntegrationSetup(state: IntegrationState, force = false): boolean {
|
|
340
|
+
return state === "ready" && !force;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function reviewAndExecuteIntegrationSetup(
|
|
344
|
+
handle: IntegrationHandle<unknown>,
|
|
345
|
+
options: { force?: boolean } = {},
|
|
346
|
+
) {
|
|
340
347
|
const plan = handle.setupPlan;
|
|
341
348
|
if (!plan) throw new Error(`Integration ${handle.id} does not declare setup`);
|
|
342
349
|
const current = await handle.get();
|
|
343
|
-
if (current.state
|
|
350
|
+
if (shouldSkipIntegrationSetup(current.state, options.force)) return current;
|
|
344
351
|
if (plan.guidedAction?.kind === "context_wizard")
|
|
345
352
|
throw new Error(
|
|
346
353
|
`Plugin ${handle.plugin ?? handle.id} setup requires interactive xcsh; run /plugin setup ${handle.plugin ?? handle.id} in the xcsh TUI.`,
|
|
@@ -358,19 +365,22 @@ export async function reviewAndExecuteIntegrationSetup(handle: IntegrationHandle
|
|
|
358
365
|
return result;
|
|
359
366
|
}
|
|
360
367
|
|
|
361
|
-
async function handleIntegrationSetup(
|
|
368
|
+
async function handleIntegrationSetup(
|
|
369
|
+
args: string[],
|
|
370
|
+
flags: { json?: boolean; context?: string; force?: boolean },
|
|
371
|
+
): Promise<void> {
|
|
362
372
|
if (flags.json) throw new CliUsageError("plugin setup is human-only and does not accept --json");
|
|
363
|
-
if (args.length !== 1) throw new Error(`Usage: ${APP_NAME} plugin setup <plugin> [--context <name>]`);
|
|
373
|
+
if (args.length !== 1) throw new Error(`Usage: ${APP_NAME} plugin setup <plugin> [--context <name>] [--force]`);
|
|
364
374
|
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Plugin setup requires an interactive terminal");
|
|
365
375
|
const handles = await loadIntegrationHandles();
|
|
366
376
|
await withPluginContext(flags.context, async () => {
|
|
367
377
|
const handle = selectSetupIntegration(handles, args[0]);
|
|
368
378
|
const current = await handle.get();
|
|
369
|
-
if (current.state
|
|
379
|
+
if (shouldSkipIntegrationSetup(current.state, flags.force)) {
|
|
370
380
|
process.stdout.write(`${handle.plugin ?? handle.id}: ready (setup is not required)\n`);
|
|
371
381
|
return;
|
|
372
382
|
}
|
|
373
|
-
const result = await reviewAndExecuteIntegrationSetup(handle);
|
|
383
|
+
const result = await reviewAndExecuteIntegrationSetup(handle, { force: flags.force });
|
|
374
384
|
process.stdout.write(
|
|
375
385
|
`${handle.plugin ?? handle.id}: ${result.state}${result.reason ? ` (${result.reason})` : ""}\n`,
|
|
376
386
|
);
|
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,24 +105,21 @@ 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;
|
|
94
120
|
}
|
|
95
121
|
if (receipt.channel === "standalone") return deps.platform !== "win32";
|
|
96
|
-
|
|
97
|
-
const localAppData = deps.env.LOCALAPPDATA;
|
|
98
|
-
if (!localAppData) return false;
|
|
99
|
-
return (
|
|
100
|
-
comparablePath(resolvedExecPath, deps.platform) ===
|
|
101
|
-
comparablePath(path.win32.join(localAppData, "xcsh", "xcsh.exe"), deps.platform)
|
|
102
|
-
);
|
|
122
|
+
return deps.platform === "win32";
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
async function defaultRun(command: string, args: string[]): Promise<CommandResult> {
|
|
@@ -233,7 +253,7 @@ export async function resolveInstallChannel(deps: InstallChannelDependencies): P
|
|
|
233
253
|
if (receiptIsValid(parsed, deps, resolvedExecPath)) {
|
|
234
254
|
signals.push({
|
|
235
255
|
channel: parsed.channel,
|
|
236
|
-
evidence: `valid schema-
|
|
256
|
+
evidence: `valid schema-v2 ${parsed.channel} receipt at ${receiptPath}`,
|
|
237
257
|
});
|
|
238
258
|
} else {
|
|
239
259
|
diagnostics.push(`invalid or stale install receipt at ${receiptPath}`);
|