@f5xc-salesdemos/xcsh 15.6.2 → 15.7.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": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.7.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
48
48
|
"@mozilla/readability": "^0.6",
|
|
49
|
-
"@f5xc-salesdemos/xcsh-stats": "15.
|
|
50
|
-
"@f5xc-salesdemos/pi-agent-core": "15.
|
|
51
|
-
"@f5xc-salesdemos/pi-ai": "15.
|
|
52
|
-
"@f5xc-salesdemos/pi-natives": "15.
|
|
53
|
-
"@f5xc-salesdemos/pi-tui": "15.
|
|
54
|
-
"@f5xc-salesdemos/pi-utils": "15.
|
|
49
|
+
"@f5xc-salesdemos/xcsh-stats": "15.7.0",
|
|
50
|
+
"@f5xc-salesdemos/pi-agent-core": "15.7.0",
|
|
51
|
+
"@f5xc-salesdemos/pi-ai": "15.7.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-natives": "15.7.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-tui": "15.7.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-utils": "15.7.0",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Update CLI command handler.
|
|
3
3
|
*
|
|
4
4
|
* Handles `xcsh update` to check for and install updates.
|
|
5
|
-
*
|
|
5
|
+
* Auto-detects the installation method (npm, brew, bun, or standalone binary)
|
|
6
|
+
* and updates through the appropriate channel.
|
|
6
7
|
*/
|
|
7
8
|
import * as fs from "node:fs";
|
|
8
9
|
import * as path from "node:path";
|
|
@@ -60,14 +61,74 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
|
|
|
60
61
|
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
type
|
|
64
|
+
export type InstallMethod = "npm" | "brew" | "bun" | "binary";
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
type UpdateTarget =
|
|
67
|
+
| { method: "npm"; path: string }
|
|
68
|
+
| { method: "brew"; path: string }
|
|
69
|
+
| { method: "bun" }
|
|
70
|
+
| { method: "binary"; path: string };
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detect how xcsh was installed by examining the binary path.
|
|
74
|
+
*
|
|
75
|
+
* Detection order:
|
|
76
|
+
* 1. bun — binary is inside bun's global bin directory
|
|
77
|
+
* 2. npm — binary is a symlink whose resolution chain contains "node_modules"
|
|
78
|
+
* 3. brew — binary path or realpath contains "Cellar" or "homebrew"
|
|
79
|
+
* 4. binary — fallback for standalone installs
|
|
80
|
+
*/
|
|
81
|
+
function detectInstallMethod(binPath: string, bunBinDir: string | undefined): InstallMethod {
|
|
82
|
+
// 1. Bun: binary lives inside bun's global bin dir
|
|
83
|
+
if (bunBinDir && isPathInDirectory(binPath, bunBinDir)) {
|
|
84
|
+
return "bun";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. npm: binary is a symlink whose target chain contains node_modules
|
|
88
|
+
try {
|
|
89
|
+
const stats = fs.lstatSync(binPath);
|
|
90
|
+
if (stats.isSymbolicLink()) {
|
|
91
|
+
const linkTarget = fs.readlinkSync(binPath);
|
|
92
|
+
const resolvedTarget = path.resolve(path.dirname(binPath), linkTarget);
|
|
93
|
+
if (linkTarget.includes("node_modules") || resolvedTarget.includes("node_modules")) {
|
|
94
|
+
return "npm";
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const realPath = fs.realpathSync(binPath);
|
|
98
|
+
if (realPath.includes("node_modules")) {
|
|
99
|
+
return "npm";
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// realpath may fail if target doesn't exist
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// lstat/readlink may fail; fall through
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. brew: path or realpath contains Cellar or homebrew
|
|
110
|
+
const lowerBinPath = binPath.toLowerCase();
|
|
111
|
+
if (lowerBinPath.includes("/cellar/") || lowerBinPath.includes("/homebrew/")) {
|
|
112
|
+
return "brew";
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const realPath = fs.realpathSync(binPath).toLowerCase();
|
|
116
|
+
if (realPath.includes("/cellar/") || realPath.includes("/homebrew/")) {
|
|
117
|
+
return "brew";
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// realpath may fail; fall through
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 4. Standalone binary (fallback)
|
|
124
|
+
return "binary";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): InstallMethod {
|
|
128
|
+
return detectInstallMethod(ompPath, bunBinDir);
|
|
68
129
|
}
|
|
69
130
|
|
|
70
|
-
export function _resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined):
|
|
131
|
+
export function _resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): InstallMethod {
|
|
71
132
|
return resolveUpdateMethod(ompPath, bunBinDir);
|
|
72
133
|
}
|
|
73
134
|
async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
@@ -175,8 +236,9 @@ function resolveOmpPath(): string | undefined {
|
|
|
175
236
|
*/
|
|
176
237
|
async function verifyInstalledVersion(
|
|
177
238
|
expectedVersion: string,
|
|
239
|
+
explicitPath?: string,
|
|
178
240
|
): Promise<{ ok: boolean; actual?: string; path?: string }> {
|
|
179
|
-
const ompPath = resolveOmpPath();
|
|
241
|
+
const ompPath = explicitPath ?? resolveOmpPath();
|
|
180
242
|
if (!ompPath) return { ok: false };
|
|
181
243
|
try {
|
|
182
244
|
const result = await $`${ompPath} --version`.quiet().nothrow();
|
|
@@ -194,8 +256,8 @@ async function verifyInstalledVersion(
|
|
|
194
256
|
/**
|
|
195
257
|
* Print post-update verification result.
|
|
196
258
|
*/
|
|
197
|
-
async function printVerification(expectedVersion: string): Promise<void> {
|
|
198
|
-
const result = await verifyInstalledVersion(expectedVersion);
|
|
259
|
+
async function printVerification(expectedVersion: string, explicitPath?: string): Promise<void> {
|
|
260
|
+
const result = await verifyInstalledVersion(expectedVersion, explicitPath);
|
|
199
261
|
if (result.ok) {
|
|
200
262
|
console.log(chalk.green(`\n${theme.status.success} Updated to ${expectedVersion}`));
|
|
201
263
|
return;
|
|
@@ -231,6 +293,30 @@ async function updateViaBun(expectedVersion: string): Promise<void> {
|
|
|
231
293
|
await printVerification(expectedVersion);
|
|
232
294
|
}
|
|
233
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Update via npm package manager.
|
|
298
|
+
*/
|
|
299
|
+
async function updateViaNpm(expectedVersion: string): Promise<void> {
|
|
300
|
+
console.log(chalk.dim("Updating via npm..."));
|
|
301
|
+
const result = await $`npm install -g ${PACKAGE}@${expectedVersion}`.nothrow();
|
|
302
|
+
if (result.exitCode !== 0) {
|
|
303
|
+
throw new Error(`npm install failed with exit code ${result.exitCode}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Handle brew-installed xcsh.
|
|
309
|
+
*
|
|
310
|
+
* For corporate environments, brew-managed software must not be bypassed.
|
|
311
|
+
* Prints instructions instead of running brew upgrade automatically.
|
|
312
|
+
*/
|
|
313
|
+
function updateViaBrew(targetPath: string, expectedVersion: string): void {
|
|
314
|
+
console.log(chalk.yellow(`\n${APP_NAME} at ${targetPath} was installed via Homebrew.`));
|
|
315
|
+
console.log(chalk.yellow(`To update to ${expectedVersion}, run:`));
|
|
316
|
+
console.log(chalk.cyan(` brew upgrade ${APP_NAME}`));
|
|
317
|
+
console.log(chalk.dim("\nThis ensures the update goes through your organization's Homebrew tap."));
|
|
318
|
+
}
|
|
319
|
+
|
|
234
320
|
/**
|
|
235
321
|
* Download a release binary to a target path, replacing an existing file.
|
|
236
322
|
*/
|
|
@@ -261,7 +347,7 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
|
|
|
261
347
|
await fs.promises.rename(tempPath, targetPath);
|
|
262
348
|
await fs.promises.unlink(backupPath);
|
|
263
349
|
|
|
264
|
-
await printVerification(expectedVersion);
|
|
350
|
+
await printVerification(expectedVersion, targetPath);
|
|
265
351
|
console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
|
|
266
352
|
} catch (err) {
|
|
267
353
|
if (fs.existsSync(backupPath) && !fs.existsSync(targetPath)) {
|
|
@@ -310,10 +396,22 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
|
|
|
310
396
|
// Choose update method based on the prioritized xcsh binary in PATH
|
|
311
397
|
try {
|
|
312
398
|
const target = await resolveUpdateTarget();
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
399
|
+
console.log(chalk.dim(`Install method: ${target.method}`));
|
|
400
|
+
|
|
401
|
+
switch (target.method) {
|
|
402
|
+
case "bun":
|
|
403
|
+
await updateViaBun(release.version);
|
|
404
|
+
break;
|
|
405
|
+
case "npm":
|
|
406
|
+
await updateViaNpm(release.version);
|
|
407
|
+
await printVerification(release.version);
|
|
408
|
+
break;
|
|
409
|
+
case "brew":
|
|
410
|
+
updateViaBrew(target.path, release.version);
|
|
411
|
+
return;
|
|
412
|
+
case "binary":
|
|
413
|
+
await updateViaBinaryAt(target.path, release.version);
|
|
414
|
+
break;
|
|
317
415
|
}
|
|
318
416
|
} catch (err) {
|
|
319
417
|
console.error(chalk.red(`Update failed: ${err}`));
|
|
@@ -334,6 +432,12 @@ ${chalk.bold("Options:")}
|
|
|
334
432
|
-c, --check Check for updates without installing
|
|
335
433
|
-f, --force Force reinstall even if up to date
|
|
336
434
|
|
|
435
|
+
${chalk.bold("Install methods (auto-detected):")}
|
|
436
|
+
npm Installed via npm install -g
|
|
437
|
+
brew Installed via Homebrew (prints upgrade instructions)
|
|
438
|
+
bun Installed via bun install -g
|
|
439
|
+
binary Standalone binary (direct download)
|
|
440
|
+
|
|
337
441
|
${chalk.bold("Examples:")}
|
|
338
442
|
${APP_NAME} update Update to latest version
|
|
339
443
|
${APP_NAME} update --check Check if updates are available
|
|
@@ -4,7 +4,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
4
4
|
default: {
|
|
5
5
|
leftSegments: ["pi", "model", "plan_mode", "path", "git", "pr", "context_pct", "token_total", "cost"],
|
|
6
6
|
rightSegments: [],
|
|
7
|
-
separator: "powerline
|
|
7
|
+
separator: "powerline",
|
|
8
8
|
segmentOptions: {
|
|
9
9
|
model: { showThinkingLevel: true },
|
|
10
10
|
path: { abbreviate: true, maxLength: 40, stripWorkPrefix: true },
|
|
@@ -25,7 +25,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
25
25
|
compact: {
|
|
26
26
|
leftSegments: ["model", "plan_mode", "git", "pr"],
|
|
27
27
|
rightSegments: ["cost", "context_pct"],
|
|
28
|
-
separator: "powerline
|
|
28
|
+
separator: "powerline",
|
|
29
29
|
segmentOptions: {
|
|
30
30
|
model: { showThinkingLevel: false },
|
|
31
31
|
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: false },
|
|
@@ -95,7 +95,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
95
95
|
// User-defined - these are just defaults that get overridden
|
|
96
96
|
leftSegments: ["model", "plan_mode", "path", "git", "pr"],
|
|
97
97
|
rightSegments: ["token_total", "cost", "context_pct"],
|
|
98
|
-
separator: "powerline
|
|
98
|
+
separator: "powerline",
|
|
99
99
|
segmentOptions: {},
|
|
100
100
|
},
|
|
101
101
|
};
|