@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.6.2",
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.6.2",
50
- "@f5xc-salesdemos/pi-agent-core": "15.6.2",
51
- "@f5xc-salesdemos/pi-ai": "15.6.2",
52
- "@f5xc-salesdemos/pi-natives": "15.6.2",
53
- "@f5xc-salesdemos/pi-tui": "15.6.2",
54
- "@f5xc-salesdemos/pi-utils": "15.6.2",
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",
@@ -2,7 +2,8 @@
2
2
  * Update CLI command handler.
3
3
  *
4
4
  * Handles `xcsh update` to check for and install updates.
5
- * Uses bun if available, otherwise downloads binary from GitHub releases.
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 UpdateTarget = { method: "bun" } | { method: "binary"; path: string };
64
+ export type InstallMethod = "npm" | "brew" | "bun" | "binary";
64
65
 
65
- function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "bun" | "binary" {
66
- if (!bunBinDir) return "binary";
67
- return isPathInDirectory(ompPath, bunBinDir) ? "bun" : "binary";
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): "bun" | "binary" {
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
- if (target.method === "bun") {
314
- await updateViaBun(release.version);
315
- } else {
316
- await updateViaBinaryAt(target.path, release.version);
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
@@ -791,7 +791,7 @@ export const SETTINGS_SCHEMA = {
791
791
 
792
792
  "compaction.handoffSaveToDisk": {
793
793
  type: "boolean",
794
- default: false,
794
+ default: true,
795
795
  ui: {
796
796
  tab: "context",
797
797
  label: "Save Handoff Docs",
@@ -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-thin",
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-thin",
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-thin",
98
+ separator: "powerline",
99
99
  segmentOptions: {},
100
100
  },
101
101
  };