@oh-my-pi/pi-coding-agent 13.6.0 → 13.6.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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.6.1] - 2026-03-03
6
+
7
+ ### Fixed
8
+
9
+ - Fixed `omp update` silently succeeding without actually updating the binary when the update channel (bun global vs compiled binary) doesn't match the installation method ([#247](https://github.com/can1357/oh-my-pi/issues/247))
10
+ - Added post-update verification that checks the resolved `omp` binary reports the expected version, with actionable warnings on mismatch
11
+ - `omp update` now detects when the `omp` in PATH is not managed by bun and falls back to binary replacement instead of updating the wrong location
5
12
  ## [13.6.0] - 2026-03-03
6
13
  ### Added
7
14
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.6.0",
4
+ "version": "13.6.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.6.0",
45
- "@oh-my-pi/pi-agent-core": "13.6.0",
46
- "@oh-my-pi/pi-ai": "13.6.0",
47
- "@oh-my-pi/pi-natives": "13.6.0",
48
- "@oh-my-pi/pi-tui": "13.6.0",
49
- "@oh-my-pi/pi-utils": "13.6.0",
44
+ "@oh-my-pi/omp-stats": "13.6.1",
45
+ "@oh-my-pi/pi-agent-core": "13.6.1",
46
+ "@oh-my-pi/pi-ai": "13.6.1",
47
+ "@oh-my-pi/pi-natives": "13.6.1",
48
+ "@oh-my-pi/pi-tui": "13.6.1",
49
+ "@oh-my-pi/pi-utils": "13.6.1",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -4,29 +4,20 @@
4
4
  * Handles `omp update` to check for and install updates.
5
5
  * Uses bun if available, otherwise downloads binary from GitHub releases.
6
6
  */
7
- import { execSync, spawnSync } from "node:child_process";
8
7
  import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
9
  import { pipeline } from "node:stream/promises";
10
10
  import { APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
11
+ import { $ } from "bun";
11
12
  import chalk from "chalk";
12
13
  import { theme } from "../modes/theme/theme";
13
14
 
14
- /**
15
- * Detect if we're running as a Bun compiled binary.
16
- */
17
- const isBunBinary =
18
- Bun.env.PI_COMPILED ||
19
- import.meta.url.includes("$bunfs") ||
20
- import.meta.url.includes("~BUN") ||
21
- import.meta.url.includes("%7EBUN");
22
-
23
15
  const REPO = "can1357/oh-my-pi";
24
16
  const PACKAGE = "@oh-my-pi/pi-coding-agent";
25
17
 
26
18
  interface ReleaseInfo {
27
19
  tag: string;
28
20
  version: string;
29
- assets: Array<{ name: string; url: string }>;
30
21
  }
31
22
 
32
23
  /**
@@ -44,18 +35,59 @@ export function parseUpdateArgs(args: string[]): { force: boolean; check: boolea
44
35
  };
45
36
  }
46
37
 
47
- /**
48
- * Check if bun is available in PATH.
49
- */
50
- function hasBun(): boolean {
38
+ async function getBunGlobalBinDir(): Promise<string | undefined> {
39
+ if (!Bun.which("bun")) return undefined;
51
40
  try {
52
- const result = spawnSync("bun", ["--version"], { encoding: "utf-8", stdio: "pipe" });
53
- return result.status === 0;
41
+ const result = await $`bun pm bin -g`.quiet().nothrow();
42
+ if (result.exitCode !== 0) return undefined;
43
+ const output = result.text().trim();
44
+ return output.length > 0 ? output : undefined;
54
45
  } catch {
55
- return false;
46
+ return undefined;
56
47
  }
57
48
  }
58
49
 
50
+ function getRealPathOrOriginal(filePath: string): string {
51
+ try {
52
+ return fs.realpathSync(filePath);
53
+ } catch {
54
+ return filePath;
55
+ }
56
+ }
57
+
58
+ function normalizePathForComparison(filePath: string): string {
59
+ const normalized = path.normalize(filePath);
60
+ if (process.platform === "win32") return normalized.toLowerCase();
61
+ return normalized;
62
+ }
63
+
64
+ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
65
+ const normalizedPath = normalizePathForComparison(getRealPathOrOriginal(filePath));
66
+ const normalizedDirectory = normalizePathForComparison(getRealPathOrOriginal(directoryPath));
67
+ const relativePath = path.relative(normalizedDirectory, normalizedPath);
68
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
69
+ }
70
+
71
+ interface UpdateTarget {
72
+ method: "bun" | "binary";
73
+ path: string;
74
+ }
75
+
76
+ function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "bun" | "binary" {
77
+ if (!bunBinDir) return "binary";
78
+ return isPathInDirectory(ompPath, bunBinDir) ? "bun" : "binary";
79
+ }
80
+
81
+ export function _resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "binary" {
82
+ return resolveUpdateMethod(ompPath, bunBinDir);
83
+ }
84
+ async function resolveUpdateTarget(): Promise<UpdateTarget> {
85
+ const ompPath = resolveOmpPath() ?? process.execPath;
86
+ const bunBinDir = await getBunGlobalBinDir();
87
+ const method = resolveUpdateMethod(ompPath, bunBinDir);
88
+ return { method, path: ompPath };
89
+ }
90
+
59
91
  /**
60
92
  * Get the latest release info from the npm registry.
61
93
  * Uses npm instead of GitHub API to avoid unauthenticated rate limiting.
@@ -70,15 +102,9 @@ async function getLatestRelease(): Promise<ReleaseInfo> {
70
102
  const version = data.version;
71
103
  const tag = `v${version}`;
72
104
 
73
- // Construct deterministic GitHub release download URLs for the current platform
74
- const makeAsset = (name: string) => ({
75
- name,
76
- url: `https://github.com/${REPO}/releases/download/${tag}/${name}`,
77
- });
78
105
  return {
79
106
  tag,
80
107
  version,
81
- assets: [makeAsset(getBinaryName())],
82
108
  };
83
109
  }
84
110
 
@@ -140,60 +166,93 @@ function getBinaryName(): string {
140
166
  return `${APP_NAME}-${os}-${archName}`;
141
167
  }
142
168
 
169
+ /**
170
+ * Resolve the path that `omp` maps to in the user's PATH.
171
+ */
172
+ function resolveOmpPath(): string | undefined {
173
+ return Bun.which(APP_NAME) ?? undefined;
174
+ }
175
+
176
+ /**
177
+ * Run the resolved omp binary and check if it reports the expected version.
178
+ */
179
+ async function verifyInstalledVersion(
180
+ expectedVersion: string,
181
+ ): Promise<{ ok: boolean; actual?: string; path?: string }> {
182
+ const ompPath = resolveOmpPath();
183
+ if (!ompPath) return { ok: false };
184
+ try {
185
+ const result = await $`${ompPath} --version`.quiet().nothrow();
186
+ if (result.exitCode !== 0) return { ok: false, path: ompPath };
187
+ const output = result.text().trim();
188
+ // Output format: "omp/X.Y.Z"
189
+ const match = output.match(/\/(\d+\.\d+\.\d+)/);
190
+ const actual = match?.[1];
191
+ return { ok: actual === expectedVersion, actual, path: ompPath };
192
+ } catch {
193
+ return { ok: false, path: ompPath };
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Print post-update verification result.
199
+ */
200
+ async function printVerification(expectedVersion: string): Promise<void> {
201
+ const result = await verifyInstalledVersion(expectedVersion);
202
+ if (result.ok) {
203
+ console.log(chalk.green(`\n${theme.status.success} Updated to ${expectedVersion}`));
204
+ return;
205
+ }
206
+ if (result.actual) {
207
+ console.log(
208
+ chalk.yellow(
209
+ `\nWarning: ${APP_NAME} at ${result.path} still reports ${result.actual} (expected ${expectedVersion})`,
210
+ ),
211
+ );
212
+ } else {
213
+ console.log(
214
+ chalk.yellow(`\nWarning: could not verify updated version${result.path ? ` at ${result.path}` : ""}`),
215
+ );
216
+ }
217
+ console.log(
218
+ chalk.yellow(
219
+ `You may need to reinstall: curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`,
220
+ ),
221
+ );
222
+ }
223
+
143
224
  /**
144
225
  * Update via bun package manager.
145
226
  */
146
227
  async function updateViaBun(expectedVersion: string): Promise<void> {
147
228
  console.log(chalk.dim("Updating via bun..."));
148
- try {
149
- execSync(`bun install -g ${PACKAGE}@${expectedVersion}`, { stdio: "inherit" });
150
- } catch (error) {
151
- throw new Error("bun install failed", { cause: error });
229
+ const result = await $`bun install -g ${PACKAGE}@${expectedVersion}`.nothrow();
230
+ if (result.exitCode !== 0) {
231
+ throw new Error(`bun install failed with exit code ${result.exitCode}`);
152
232
  }
153
233
 
154
- // Verify the update actually took effect
155
- try {
156
- const result = spawnSync("bun", ["pm", "ls", "-g"], { encoding: "utf-8", stdio: "pipe" });
157
- const output = result.stdout || "";
158
- const match = output.match(new RegExp(`${PACKAGE.replace("/", "\\/")}@(\\S+)`));
159
- if (match) {
160
- const installedVersion = match[1];
161
- if (compareVersions(installedVersion, expectedVersion) < 0) {
162
- console.log(
163
- chalk.yellow(`\nWarning: bun reports ${installedVersion} installed, expected ${expectedVersion}`),
164
- );
165
- console.log(chalk.yellow(`Try: bun install -g ${PACKAGE}@latest`));
166
- return;
167
- }
168
- }
169
- } catch {
170
- // Verification is best-effort, don't fail the update
171
- }
172
- console.log(chalk.green(`\n${theme.status.success} Update complete`));
234
+ await printVerification(expectedVersion);
173
235
  }
174
236
 
175
237
  /**
176
- * Update by downloading binary from GitHub releases.
238
+ * Download a release binary to a target path, replacing an existing file.
177
239
  */
178
- async function updateViaBinary(release: ReleaseInfo): Promise<void> {
240
+ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): Promise<void> {
179
241
  const binaryName = getBinaryName();
180
- const asset = release.assets.find(a => a.name === binaryName);
181
- if (!asset) {
182
- throw new Error(`No binary found for ${binaryName}`);
183
- }
184
- const execPath = process.execPath;
185
- const tempPath = `${execPath}.new`;
186
- const backupPath = `${execPath}.bak`;
242
+ const tag = `v${expectedVersion}`;
243
+ const url = `https://github.com/${REPO}/releases/download/${tag}/${binaryName}`;
244
+
245
+ const tempPath = `${targetPath}.new`;
246
+ const backupPath = `${targetPath}.bak`;
187
247
  console.log(chalk.dim(`Downloading ${binaryName}…`));
188
248
 
189
- // Download binary to temp file
190
- const response = await fetch(asset.url, { redirect: "follow" });
249
+ const response = await fetch(url, { redirect: "follow" });
191
250
  if (!response.ok || !response.body) {
192
251
  throw new Error(`Download failed: ${response.statusText}`);
193
252
  }
194
253
  const fileStream = fs.createWriteStream(tempPath, { mode: 0o755 });
195
254
  await pipeline(response.body, fileStream);
196
- // Replace current binary
255
+
197
256
  console.log(chalk.dim("Installing update..."));
198
257
  try {
199
258
  try {
@@ -201,15 +260,15 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
201
260
  } catch (err) {
202
261
  if (!isEnoent(err)) throw err;
203
262
  }
204
- await fs.promises.rename(execPath, backupPath);
205
- await fs.promises.rename(tempPath, execPath);
263
+ await fs.promises.rename(targetPath, backupPath);
264
+ await fs.promises.rename(tempPath, targetPath);
206
265
  await fs.promises.unlink(backupPath);
207
266
 
208
- console.log(chalk.green(`\n${theme.status.success} Updated to ${release.version}`));
267
+ await printVerification(expectedVersion);
209
268
  console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
210
269
  } catch (err) {
211
- if (fs.existsSync(backupPath) && !fs.existsSync(execPath)) {
212
- await fs.promises.rename(backupPath, execPath);
270
+ if (fs.existsSync(backupPath) && !fs.existsSync(targetPath)) {
271
+ await fs.promises.rename(backupPath, targetPath);
213
272
  }
214
273
  if (fs.existsSync(tempPath)) {
215
274
  await fs.promises.unlink(tempPath);
@@ -251,12 +310,13 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
251
310
  return;
252
311
  }
253
312
 
254
- // Choose update method
313
+ // Choose update method based on the prioritized omp binary in PATH
255
314
  try {
256
- if (!isBunBinary && hasBun()) {
315
+ const target = await resolveUpdateTarget();
316
+ if (target.method === "bun") {
257
317
  await updateViaBun(release.version);
258
318
  } else {
259
- await updateViaBinary(release);
319
+ await updateViaBinaryAt(target.path, release.version);
260
320
  }
261
321
  } catch (err) {
262
322
  console.error(chalk.red(`Update failed: ${err}`));
@@ -6,13 +6,11 @@ import {
6
6
  DEFAULT_LOCAL_TOKEN,
7
7
  getBundledModels,
8
8
  getBundledProviders,
9
- getGitHubCopilotBaseUrl,
10
9
  googleAntigravityModelManagerOptions,
11
10
  googleGeminiCliModelManagerOptions,
12
11
  type Model,
13
12
  type ModelManagerOptions,
14
13
  type ModelRefreshStrategy,
15
- normalizeDomain,
16
14
  type OAuthCredentials,
17
15
  type OAuthLoginCallbacks,
18
16
  openaiCodexModelManagerOptions,
@@ -539,17 +537,7 @@ export class ModelRegistry {
539
537
  const builtInModels = this.#loadBuiltInModels(overrides, modelOverrides);
540
538
  const combined = this.#mergeCustomModels(builtInModels, customModels);
541
539
 
542
- // Update github-copilot base URL based on OAuth credentials
543
- const copilotCred = this.authStorage.getOAuthCredential("github-copilot");
544
- if (copilotCred) {
545
- const domain = copilotCred.enterpriseUrl
546
- ? (normalizeDomain(copilotCred.enterpriseUrl) ?? undefined)
547
- : undefined;
548
- const baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);
549
- this.#models = combined.map(m => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
550
- } else {
551
- this.#models = combined;
552
- }
540
+ this.#models = combined;
553
541
  }
554
542
 
555
543
  /** Load built-in models, applying provider and per-model overrides */