pi-extmgr 0.2.1 → 0.3.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/README.md CHANGED
@@ -20,6 +20,28 @@ If Pi is already running, use `/reload`.
20
20
 
21
21
  Requires Node.js `>=22.20.0`.
22
22
 
23
+ ### npm prefix permissions
24
+
25
+ Pi global package installs use `npm install -g`. If `npm_config_prefix` points at an
26
+ unwritable prefix such as `/usr/local`, install or reload can fail with `EACCES`.
27
+ Use a writable npm prefix, configure Pi's `npmCommand` setting for your Node
28
+ version manager, or install project-local with `pi install npm:pi-extmgr -l`.
29
+
30
+ `npmCommand` examples for `~/.pi/agent/settings.json`:
31
+
32
+ ```jsonc
33
+ {
34
+ // mise: run npm through Node 22
35
+ // "npmCommand": ["mise", "exec", "node@22", "--", "npm"]
36
+
37
+ // bun: use Bun as Pi's package-manager command
38
+ // "npmCommand": ["bun"]
39
+
40
+ // nvm: start Pi from an nvm-enabled shell and use npm on PATH
41
+ // "npmCommand": ["npm"]
42
+ }
43
+ ```
44
+
23
45
  ## Features
24
46
 
25
47
  - **Unified manager UI**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extmgr",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Enhanced UX for managing local Pi extensions and community packages",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -44,13 +44,13 @@
44
44
  "image": "https://i.imgur.com/Ce513Br.png"
45
45
  },
46
46
  "peerDependencies": {
47
- "@mariozechner/pi-coding-agent": "*",
48
- "@mariozechner/pi-tui": "*"
47
+ "@earendil-works/pi-coding-agent": "*",
48
+ "@earendil-works/pi-tui": "*"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@biomejs/biome": "^2.4.9",
52
- "@mariozechner/pi-coding-agent": "^0.70.6",
53
- "@mariozechner/pi-tui": "^0.70.6",
52
+ "@earendil-works/pi-coding-agent": "^0.74.0",
53
+ "@earendil-works/pi-tui": "^0.74.0",
54
54
  "@release-it/conventional-changelog": "^10.0.5",
55
55
  "@types/node": "^22.19.10",
56
56
  "husky": "^9.1.7",
@@ -2,7 +2,7 @@ import {
2
2
  type ExtensionAPI,
3
3
  type ExtensionCommandContext,
4
4
  type ExtensionContext,
5
- } from "@mariozechner/pi-coding-agent";
5
+ } from "@earendil-works/pi-coding-agent";
6
6
  import {
7
7
  disableAutoUpdate,
8
8
  enableAutoUpdate,
@@ -1,4 +1,4 @@
1
- import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
1
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { clearSearchCache } from "../packages/discovery.js";
3
3
  import { clearRemotePackageInfoCache } from "../ui/remote.js";
4
4
  import { clearCache } from "../utils/cache.js";
@@ -1,4 +1,4 @@
1
- import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
1
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  type ChangeAction,
4
4
  formatChangeEntry,
@@ -1,4 +1,4 @@
1
- import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
1
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { type InstallScope, installPackage } from "../packages/install.js";
3
3
  import { notify } from "../utils/notify.js";
4
4
 
@@ -1,5 +1,5 @@
1
- import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
- import { type AutocompleteItem } from "@mariozechner/pi-tui";
1
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import { type AutocompleteItem } from "@earendil-works/pi-tui";
3
3
  import {
4
4
  promptRemove,
5
5
  removePackage,
@@ -1,4 +1,4 @@
1
- import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
1
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  export type CommandId =
4
4
  | "local"
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  type ExtensionAPI,
8
8
  type ExtensionCommandContext,
9
9
  type ExtensionContext,
10
- } from "@mariozechner/pi-coding-agent";
10
+ } from "@earendil-works/pi-coding-agent";
11
11
  import { createAutoUpdateNotificationHandler } from "./commands/auto-update.js";
12
12
  import {
13
13
  getExtensionsAutocompleteItems,
@@ -4,7 +4,7 @@ import {
4
4
  type PackageSource,
5
5
  type ProgressEvent,
6
6
  SettingsManager,
7
- } from "@mariozechner/pi-coding-agent";
7
+ } from "@earendil-works/pi-coding-agent";
8
8
  import { type InstalledPackage, type Scope } from "../types/index.js";
9
9
  import { normalizePackageIdentity, parsePackageNameAndVersion } from "../utils/package-source.js";
10
10
 
@@ -8,7 +8,7 @@ import {
8
8
  type ExtensionCommandContext,
9
9
  type ExtensionContext,
10
10
  getAgentDir,
11
- } from "@mariozechner/pi-coding-agent";
11
+ } from "@earendil-works/pi-coding-agent";
12
12
  import { CACHE_TTL, TIMEOUTS } from "../constants.js";
13
13
  import { type InstalledPackage, type NpmPackage, type SearchCache } from "../types/index.js";
14
14
  import { parseNpmSource } from "../utils/format.js";
@@ -5,7 +5,7 @@ import { homedir } from "node:os";
5
5
  import { dirname, join, relative, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { promisify } from "node:util";
8
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
8
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
9
9
  import {
10
10
  type InstalledPackage,
11
11
  type PackageExtensionEntry,
@@ -19,7 +19,7 @@ import {
19
19
  normalizeRelativePath,
20
20
  resolveRelativePathSelection,
21
21
  } from "../utils/relative-path-selection.js";
22
- import { resolveNpmCommand } from "../utils/npm-exec.js";
22
+ import { resolveConfiguredNpmRootCommand } from "../utils/npm-exec.js";
23
23
 
24
24
  interface PackageSettingsObject {
25
25
  source: string;
@@ -39,7 +39,7 @@ export interface PackageManifest {
39
39
  }
40
40
 
41
41
  const execFileAsync = promisify(execFile);
42
- let globalNpmRootCache: string | null | undefined;
42
+ let globalNpmRootCache: { key: string; root: string | null } | undefined;
43
43
 
44
44
  function normalizeSource(source: string): string {
45
45
  return source
@@ -58,24 +58,32 @@ function normalizePackageRootCandidate(candidate: string): string {
58
58
  return resolved;
59
59
  }
60
60
 
61
- async function getGlobalNpmRoot(): Promise<string | undefined> {
62
- if (globalNpmRootCache !== undefined) {
63
- return globalNpmRootCache ?? undefined;
61
+ async function getGlobalNpmRoot(cwd: string): Promise<string | undefined> {
62
+ let npmCommand: ReturnType<typeof resolveConfiguredNpmRootCommand>;
63
+ try {
64
+ npmCommand = resolveConfiguredNpmRootCommand(cwd);
65
+ } catch {
66
+ return undefined;
67
+ }
68
+
69
+ const cacheKey = [npmCommand.command, ...npmCommand.args].join("\0");
70
+
71
+ if (globalNpmRootCache?.key === cacheKey) {
72
+ return globalNpmRootCache.root ?? undefined;
64
73
  }
65
74
 
66
75
  try {
67
- const npmCommand = resolveNpmCommand(["root", "-g"]);
68
76
  const { stdout } = await execFileAsync(npmCommand.command, npmCommand.args, {
69
77
  timeout: 2_000,
70
78
  windowsHide: true,
71
79
  });
72
- const root = stdout.trim();
73
- globalNpmRootCache = root || null;
80
+ const root = npmCommand.getRoot(stdout);
81
+ globalNpmRootCache = { key: cacheKey, root: root || null };
74
82
  } catch {
75
- globalNpmRootCache = null;
83
+ globalNpmRootCache = { key: cacheKey, root: null };
76
84
  }
77
85
 
78
- return globalNpmRootCache ?? undefined;
86
+ return globalNpmRootCache.root ?? undefined;
79
87
  }
80
88
 
81
89
  async function resolveNpmPackageRoot(
@@ -96,7 +104,7 @@ async function resolveNpmPackageRoot(
96
104
  const packageDir = process.env.PI_PACKAGE_DIR || join(homedir(), ".pi", "agent");
97
105
  const globalCandidates = [join(packageDir, "npm", "node_modules", packageName)];
98
106
 
99
- const npmGlobalRoot = await getGlobalNpmRoot();
107
+ const npmGlobalRoot = await getGlobalNpmRoot(cwd);
100
108
  if (npmGlobalRoot) {
101
109
  globalCandidates.unshift(join(npmGlobalRoot, packageName));
102
110
  }
@@ -8,7 +8,7 @@ import {
8
8
  type ExtensionAPI,
9
9
  type ExtensionCommandContext,
10
10
  type ProgressEvent,
11
- } from "@mariozechner/pi-coding-agent";
11
+ } from "@earendil-works/pi-coding-agent";
12
12
  import { TIMEOUTS } from "../constants.js";
13
13
  import { runTaskWithLoader } from "../ui/async-task.js";
14
14
  import { parseChoiceByLabel } from "../utils/command.js";
@@ -6,7 +6,7 @@ import {
6
6
  type ExtensionCommandContext,
7
7
  getAgentDir,
8
8
  type ProgressEvent,
9
- } from "@mariozechner/pi-coding-agent";
9
+ } from "@earendil-works/pi-coding-agent";
10
10
  import { UI } from "../constants.js";
11
11
  import { type InstalledPackage } from "../types/index.js";
12
12
  import { runTaskWithLoader } from "../ui/async-task.js";
@@ -3,8 +3,15 @@ import {
3
3
  type ExtensionCommandContext,
4
4
  type ExtensionContext,
5
5
  type Theme,
6
- } from "@mariozechner/pi-coding-agent";
7
- import { CancellableLoader, Container, Loader, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ CancellableLoader,
9
+ Container,
10
+ Loader,
11
+ Spacer,
12
+ Text,
13
+ type TUI,
14
+ } from "@earendil-works/pi-tui";
8
15
  import { hasCustomUI } from "../utils/mode.js";
9
16
 
10
17
  type AnyContext = ExtensionCommandContext | ExtensionContext;
package/src/ui/help.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Help display
3
3
  */
4
- import { type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
4
+ import { type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
5
5
  import { notify } from "../utils/notify.js";
6
6
 
7
7
  export function showHelp(ctx: ExtensionCommandContext): void {
@@ -7,7 +7,7 @@ import {
7
7
  type ExtensionCommandContext,
8
8
  getSettingsListTheme,
9
9
  type Theme,
10
- } from "@mariozechner/pi-coding-agent";
10
+ } from "@earendil-works/pi-coding-agent";
11
11
  import {
12
12
  Container,
13
13
  Key,
@@ -16,7 +16,7 @@ import {
16
16
  SettingsList,
17
17
  Spacer,
18
18
  Text,
19
- } from "@mariozechner/pi-tui";
19
+ } from "@earendil-works/pi-tui";
20
20
  import { UI } from "../constants.js";
21
21
  import {
22
22
  applyPackageExtensionStateChanges,
package/src/ui/remote.ts CHANGED
@@ -5,13 +5,13 @@ import {
5
5
  DynamicBorder,
6
6
  type ExtensionAPI,
7
7
  type ExtensionCommandContext,
8
+ type KeybindingsManager,
8
9
  type Theme,
9
- } from "@mariozechner/pi-coding-agent";
10
+ } from "@earendil-works/pi-coding-agent";
10
11
  import {
11
12
  Container,
12
13
  fuzzyMatch,
13
14
  type Focusable,
14
- getKeybindings,
15
15
  Input,
16
16
  Key,
17
17
  matchesKey,
@@ -19,7 +19,7 @@ import {
19
19
  Text,
20
20
  truncateToWidth,
21
21
  wrapTextWithAnsi,
22
- } from "@mariozechner/pi-tui";
22
+ } from "@earendil-works/pi-tui";
23
23
  import { CACHE_LIMITS, PAGE_SIZE, TIMEOUTS, UI } from "../constants.js";
24
24
  import {
25
25
  clearSearchCache,
@@ -552,6 +552,7 @@ class RemotePackageBrowser implements Focusable {
552
552
  constructor(
553
553
  private readonly packages: NpmPackage[],
554
554
  private readonly theme: Theme,
555
+ private readonly keybindings: KeybindingsManager,
555
556
  private readonly browseSource: RemoteBrowseSource,
556
557
  private readonly queryLabel: string,
557
558
  private readonly totalResults: number,
@@ -578,8 +579,6 @@ class RemotePackageBrowser implements Focusable {
578
579
  }
579
580
 
580
581
  handleBrowseInput(data: string): boolean {
581
- const kb = getKeybindings();
582
-
583
582
  if (this.searchActive) {
584
583
  if (matchesKey(data, Key.enter)) {
585
584
  this.searchActive = false;
@@ -606,22 +605,22 @@ class RemotePackageBrowser implements Focusable {
606
605
  return true;
607
606
  }
608
607
 
609
- if (kb.matches(data, "tui.select.up")) {
608
+ if (this.keybindings.matches(data, "tui.select.up")) {
610
609
  this.moveSelection(-1);
611
610
  return true;
612
611
  }
613
612
 
614
- if (kb.matches(data, "tui.select.down")) {
613
+ if (this.keybindings.matches(data, "tui.select.down")) {
615
614
  this.moveSelection(1);
616
615
  return true;
617
616
  }
618
617
 
619
- if (kb.matches(data, "tui.select.pageUp")) {
618
+ if (this.keybindings.matches(data, "tui.select.pageUp")) {
620
619
  this.moveSelection(-Math.max(1, this.maxVisibleItems - 1));
621
620
  return true;
622
621
  }
623
622
 
624
- if (kb.matches(data, "tui.select.pageDown")) {
623
+ if (this.keybindings.matches(data, "tui.select.pageDown")) {
625
624
  this.moveSelection(Math.max(1, this.maxVisibleItems - 1));
626
625
  return true;
627
626
  }
@@ -811,12 +810,13 @@ async function selectBrowseAction(
811
810
  if (!ctx.hasUI) return undefined;
812
811
 
813
812
  return runCustomUI(ctx, "Remote package browsing", () =>
814
- ctx.ui.custom<BrowseAction>((tui, theme, _keybindings, done) => {
813
+ ctx.ui.custom<BrowseAction>((tui, theme, keybindings, done) => {
815
814
  const container = new Container();
816
815
  const title = new Text("", 2, 0);
817
816
  const browser = new RemotePackageBrowser(
818
817
  packages,
819
818
  theme,
819
+ keybindings,
820
820
  browseSource,
821
821
  plan.displayQuery,
822
822
  totalResults,
package/src/ui/theme.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Theme utilities for consistent UI styling across dark/light themes
3
3
  */
4
- import { type Theme } from "@mariozechner/pi-coding-agent";
4
+ import { type Theme } from "@earendil-works/pi-coding-agent";
5
5
 
6
6
  /**
7
7
  * Status icons that work across themes
package/src/ui/unified.ts CHANGED
@@ -8,13 +8,13 @@ import {
8
8
  DynamicBorder,
9
9
  type ExtensionAPI,
10
10
  type ExtensionCommandContext,
11
+ type KeybindingsManager,
11
12
  type Theme,
12
- } from "@mariozechner/pi-coding-agent";
13
+ } from "@earendil-works/pi-coding-agent";
13
14
  import {
14
15
  Container,
15
16
  type Focusable,
16
17
  fuzzyMatch,
17
- getKeybindings,
18
18
  Input,
19
19
  Key,
20
20
  matchesKey,
@@ -22,7 +22,7 @@ import {
22
22
  Text,
23
23
  truncateToWidth,
24
24
  wrapTextWithAnsi,
25
- } from "@mariozechner/pi-tui";
25
+ } from "@earendil-works/pi-tui";
26
26
  import { UI } from "../constants.js";
27
27
  import {
28
28
  discoverExtensions,
@@ -164,7 +164,7 @@ async function showInteractiveOnce(
164
164
  ctx,
165
165
  "The unified extensions manager",
166
166
  () =>
167
- ctx.ui.custom<UnifiedAction>((tui, theme, _keybindings, done) => {
167
+ ctx.ui.custom<UnifiedAction>((tui, theme, keybindings, done) => {
168
168
  const container = new Container();
169
169
 
170
170
  const titleText = new Text("", 2, 0);
@@ -179,6 +179,7 @@ async function showInteractiveOnce(
179
179
  items,
180
180
  staged,
181
181
  theme,
182
+ keybindings,
182
183
  ctx.cwd,
183
184
  Math.max(4, Math.min(UI.maxListHeight, tui.terminal.rows - 12)),
184
185
  complete,
@@ -637,6 +638,7 @@ class UnifiedManagerBrowser implements Focusable {
637
638
  private readonly items: UnifiedItem[],
638
639
  private readonly staged: Map<string, State>,
639
640
  private readonly theme: Theme,
641
+ private readonly keybindings: KeybindingsManager,
640
642
  private readonly cwd: string,
641
643
  private readonly maxVisibleItems: number,
642
644
  private readonly onAction: (action: UnifiedAction) => void,
@@ -695,8 +697,6 @@ class UnifiedManagerBrowser implements Focusable {
695
697
  }
696
698
 
697
699
  handleManagerInput(data: string): boolean {
698
- const kb = getKeybindings();
699
-
700
700
  if (this.searchActive) {
701
701
  if (matchesKey(data, Key.enter)) {
702
702
  this.searchActive = false;
@@ -745,22 +745,22 @@ class UnifiedManagerBrowser implements Focusable {
745
745
  return true;
746
746
  }
747
747
 
748
- if (kb.matches(data, "tui.select.up")) {
748
+ if (this.keybindings.matches(data, "tui.select.up")) {
749
749
  this.moveSelection(-1);
750
750
  return true;
751
751
  }
752
752
 
753
- if (kb.matches(data, "tui.select.down")) {
753
+ if (this.keybindings.matches(data, "tui.select.down")) {
754
754
  this.moveSelection(1);
755
755
  return true;
756
756
  }
757
757
 
758
- if (kb.matches(data, "tui.select.pageUp")) {
758
+ if (this.keybindings.matches(data, "tui.select.pageUp")) {
759
759
  this.moveSelection(-Math.max(1, this.maxVisibleItems - 1));
760
760
  return true;
761
761
  }
762
762
 
763
- if (kb.matches(data, "tui.select.pageDown")) {
763
+ if (this.keybindings.matches(data, "tui.select.pageDown")) {
764
764
  this.moveSelection(Math.max(1, this.maxVisibleItems - 1));
765
765
  return true;
766
766
  }
@@ -5,7 +5,7 @@ import {
5
5
  type ExtensionAPI,
6
6
  type ExtensionCommandContext,
7
7
  type ExtensionContext,
8
- } from "@mariozechner/pi-coding-agent";
8
+ } from "@earendil-works/pi-coding-agent";
9
9
  import { getPackageCatalog } from "../packages/catalog.js";
10
10
  import { parseChoiceByLabel } from "./command.js";
11
11
  import { logAutoUpdateConfig } from "./history.js";
@@ -7,7 +7,7 @@ import { type Dirent } from "node:fs";
7
7
  import { readdir, readFile } from "node:fs/promises";
8
8
  import { homedir } from "node:os";
9
9
  import { join } from "node:path";
10
- import { type ExtensionAPI, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
10
+ import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
11
11
 
12
12
  export type ChangeAction =
13
13
  | "extension_toggle"
package/src/utils/mode.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * UI capability helpers
3
3
  */
4
- import { type ExtensionCommandContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
4
+ import {
5
+ type ExtensionCommandContext,
6
+ type ExtensionContext,
7
+ } from "@earendil-works/pi-coding-agent";
5
8
  import { notify } from "./notify.js";
6
9
 
7
10
  type AnyContext = ExtensionCommandContext | ExtensionContext;
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Centralized notification handling for UI and non-UI modes
3
3
  */
4
- import { type ExtensionCommandContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
4
+ import {
5
+ type ExtensionCommandContext,
6
+ type ExtensionContext,
7
+ } from "@earendil-works/pi-coding-agent";
5
8
 
6
9
  export type NotifyLevel = "info" | "warning" | "error";
7
10
 
@@ -1,10 +1,23 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
1
3
  import path from "node:path";
2
4
  import { execPath, platform } from "node:process";
3
- import { type ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+ import { type ExtensionAPI, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
4
6
 
5
7
  interface NpmCommandResolutionOptions {
6
8
  platform?: NodeJS.Platform;
7
9
  nodeExecPath?: string;
10
+ npmCommand?: readonly string[] | undefined;
11
+ cwd?: string;
12
+ }
13
+
14
+ interface ResolvedNpmCommand {
15
+ command: string;
16
+ args: string[];
17
+ }
18
+
19
+ interface ResolvedNpmRootCommand extends ResolvedNpmCommand {
20
+ getRoot(stdout: string): string;
8
21
  }
9
22
 
10
23
  interface NpmExecOptions {
@@ -12,15 +25,148 @@ interface NpmExecOptions {
12
25
  signal?: AbortSignal;
13
26
  }
14
27
 
28
+ const settingsManagersByPath = new Map<string, SettingsManager>();
29
+ let warnedAboutBunGlobalDirHeuristic = false;
30
+
15
31
  function getNpmCliPath(nodeExecPath: string, runtimePlatform: NodeJS.Platform): string {
16
32
  const pathImpl = runtimePlatform === "win32" ? path.win32 : path;
17
33
  return pathImpl.join(pathImpl.dirname(nodeExecPath), "node_modules", "npm", "bin", "npm-cli.js");
18
34
  }
19
35
 
36
+ function getConfiguredNpmBase(
37
+ npmCommand?: readonly string[] | undefined
38
+ ): ResolvedNpmCommand | undefined {
39
+ if (!npmCommand || npmCommand.length === 0) {
40
+ return undefined;
41
+ }
42
+
43
+ const [command, ...args] = npmCommand;
44
+ if (!command?.trim()) {
45
+ throw new Error("Invalid npmCommand: first array entry must be a non-empty command");
46
+ }
47
+
48
+ return { command, args: [...args] };
49
+ }
50
+
51
+ function getSettingsNpmCommand(cwd: string): string[] | undefined {
52
+ const agentDir = getAgentDir();
53
+ const cacheKey = `${agentDir}\0${cwd}`;
54
+ const cached = settingsManagersByPath.get(cacheKey);
55
+ if (cached) {
56
+ return cached.getNpmCommand();
57
+ }
58
+
59
+ const settingsManager = SettingsManager.create(cwd, agentDir);
60
+ settingsManagersByPath.set(cacheKey, settingsManager);
61
+ return settingsManager.getNpmCommand();
62
+ }
63
+
64
+ function getCommandName(command: string): string {
65
+ return (command.split(/[\\/]/).pop() ?? "").replace(/\.(cmd|exe)$/i, "");
66
+ }
67
+
68
+ function expandHome(input: string): string {
69
+ if (input === "~") return homedir();
70
+ if (input.startsWith("~/")) return path.join(homedir(), input.slice(2));
71
+ return input;
72
+ }
73
+
74
+ function resolveBunConfigPath(value: string, baseDir: string): string {
75
+ const expanded = expandHome(value.trim());
76
+ return path.isAbsolute(expanded) ? expanded : path.resolve(baseDir, expanded);
77
+ }
78
+
79
+ function stripTomlComment(line: string): string {
80
+ return line.replace(/\s+#.*$/, "").trim();
81
+ }
82
+
83
+ function readBunGlobalDirFromBunfig(configPath: string): string | undefined {
84
+ if (!existsSync(configPath)) return undefined;
85
+
86
+ let content: string;
87
+ try {
88
+ content = readFileSync(configPath, "utf8");
89
+ } catch {
90
+ return undefined;
91
+ }
92
+
93
+ let inInstallSection = false;
94
+ for (const rawLine of content.split(/\r?\n/)) {
95
+ const line = stripTomlComment(rawLine);
96
+ if (!line) continue;
97
+
98
+ const sectionMatch = line.match(/^\[([^\]]+)]$/);
99
+ if (sectionMatch) {
100
+ inInstallSection = sectionMatch[1]?.trim() === "install";
101
+ continue;
102
+ }
103
+
104
+ const keyMatch = line.match(/^(install\.)?globalDir\s*=\s*(["'])(.*?)\2/);
105
+ if (!keyMatch) continue;
106
+ if (!inInstallSection && !keyMatch[1]) continue;
107
+
108
+ const value = keyMatch[3]?.trim();
109
+ return value ? resolveBunConfigPath(value, path.dirname(configPath)) : undefined;
110
+ }
111
+
112
+ return undefined;
113
+ }
114
+
115
+ function getBunGlobalDir(cwd?: string): string | undefined {
116
+ const envGlobalDir = process.env.BUN_INSTALL_GLOBAL_DIR?.trim();
117
+ if (envGlobalDir) {
118
+ return resolveBunConfigPath(envGlobalDir, process.cwd());
119
+ }
120
+
121
+ const candidates = [
122
+ path.join(homedir(), ".bunfig.toml"),
123
+ ...(process.env.XDG_CONFIG_HOME
124
+ ? [path.join(process.env.XDG_CONFIG_HOME, ".bunfig.toml")]
125
+ : []),
126
+ ...(cwd ? [path.join(cwd, "bunfig.toml")] : []),
127
+ ];
128
+
129
+ let globalDir: string | undefined;
130
+ for (const configPath of candidates) {
131
+ globalDir = readBunGlobalDirFromBunfig(configPath) ?? globalDir;
132
+ }
133
+ return globalDir;
134
+ }
135
+
136
+ function warnAboutBunGlobalDirHeuristic(): void {
137
+ if (warnedAboutBunGlobalDirHeuristic) return;
138
+ warnedAboutBunGlobalDirHeuristic = true;
139
+ console.warn(
140
+ "[extmgr] Could not read Bun globalDir from BUN_INSTALL_GLOBAL_DIR or bunfig.toml; " +
141
+ "guessing from `bun pm bin -g`. If Bun's globalDir is customized, set BUN_INSTALL_GLOBAL_DIR."
142
+ );
143
+ }
144
+
145
+ function getBunNodeModulesRoot(globalBinDir: string, cwd?: string): string {
146
+ const globalDir = getBunGlobalDir(cwd);
147
+ if (globalDir) {
148
+ return path.join(globalDir, "node_modules");
149
+ }
150
+
151
+ // Best-effort fallback for Bun's default layout: globalBinDir is usually
152
+ // ~/.bun/bin and globalDir is usually ~/.bun/install/global. This may be
153
+ // wrong when [install].globalDir is customized in bunfig.toml.
154
+ warnAboutBunGlobalDirHeuristic();
155
+ return path.join(path.dirname(globalBinDir), "install", "global", "node_modules");
156
+ }
157
+
20
158
  export function resolveNpmCommand(
21
159
  npmArgs: string[],
22
160
  options?: NpmCommandResolutionOptions
23
- ): { command: string; args: string[] } {
161
+ ): ResolvedNpmCommand {
162
+ const configured = getConfiguredNpmBase(options?.npmCommand);
163
+ if (configured) {
164
+ return {
165
+ command: configured.command,
166
+ args: [...configured.args, ...npmArgs],
167
+ };
168
+ }
169
+
24
170
  const runtimePlatform = options?.platform ?? platform;
25
171
 
26
172
  if (runtimePlatform === "win32") {
@@ -34,13 +180,44 @@ export function resolveNpmCommand(
34
180
  return { command: "npm", args: npmArgs };
35
181
  }
36
182
 
183
+ export function resolveConfiguredNpmCommand(npmArgs: string[], cwd: string): ResolvedNpmCommand {
184
+ return resolveNpmCommand(npmArgs, { npmCommand: getSettingsNpmCommand(cwd) });
185
+ }
186
+
187
+ export function resolveNpmRootCommand(
188
+ options?: NpmCommandResolutionOptions
189
+ ): ResolvedNpmRootCommand {
190
+ const configured = getConfiguredNpmBase(options?.npmCommand);
191
+
192
+ if (configured && getCommandName(configured.command) === "bun") {
193
+ return {
194
+ command: configured.command,
195
+ args: [...configured.args, "pm", "bin", "-g"],
196
+ getRoot: (stdout) => {
197
+ const binDir = stdout.trim();
198
+ return binDir ? getBunNodeModulesRoot(binDir, options?.cwd) : "";
199
+ },
200
+ };
201
+ }
202
+
203
+ const resolved = resolveNpmCommand(["root", "-g"], options);
204
+ return {
205
+ ...resolved,
206
+ getRoot: (stdout) => stdout.trim(),
207
+ };
208
+ }
209
+
210
+ export function resolveConfiguredNpmRootCommand(cwd: string): ResolvedNpmRootCommand {
211
+ return resolveNpmRootCommand({ npmCommand: getSettingsNpmCommand(cwd), cwd });
212
+ }
213
+
37
214
  export async function execNpm(
38
215
  pi: ExtensionAPI,
39
216
  npmArgs: string[],
40
217
  ctx: { cwd: string },
41
218
  options: NpmExecOptions
42
219
  ): Promise<{ code: number; stdout: string; stderr: string; killed: boolean }> {
43
- const resolved = resolveNpmCommand(npmArgs);
220
+ const resolved = resolveConfiguredNpmCommand(npmArgs, ctx.cwd);
44
221
  return pi.exec(resolved.command, resolved.args, {
45
222
  timeout: options.timeout,
46
223
  cwd: ctx.cwd,
@@ -10,7 +10,7 @@ import {
10
10
  type ExtensionAPI,
11
11
  type ExtensionCommandContext,
12
12
  type ExtensionContext,
13
- } from "@mariozechner/pi-coding-agent";
13
+ } from "@earendil-works/pi-coding-agent";
14
14
  import { parseScheduleDuration } from "./duration.js";
15
15
  import { fileExists } from "./fs.js";
16
16
  import { normalizePackageIdentity } from "./package-source.js";
@@ -6,7 +6,7 @@ import {
6
6
  type ExtensionCommandContext,
7
7
  type ExtensionContext,
8
8
  getAgentDir,
9
- } from "@mariozechner/pi-coding-agent";
9
+ } from "@earendil-works/pi-coding-agent";
10
10
  import { getPackageCatalog, type PackageCatalog } from "../packages/catalog.js";
11
11
  import { getAutoUpdateStatus } from "./auto-update.js";
12
12
  import { normalizePackageIdentity } from "./package-source.js";
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Common UI helper patterns
3
3
  */
4
- import { type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
4
+ import { type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
5
5
  import { UI } from "../constants.js";
6
- import { notify } from "./notify.js";
6
+ import { error as notifyError, notify } from "./notify.js";
7
7
 
8
8
  /**
9
9
  * Confirm and trigger reload
@@ -20,12 +20,18 @@ export async function confirmReload(
20
20
 
21
21
  const confirmed = await ctx.ui.confirm("Reload Required", `${reason}\nReload pi now?`);
22
22
 
23
- if (confirmed) {
23
+ if (!confirmed) {
24
+ return false;
25
+ }
26
+
27
+ try {
24
28
  await ctx.reload();
25
29
  return true;
30
+ } catch (error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ notifyError(ctx, `Reload failed: ${message}`);
33
+ return false;
26
34
  }
27
-
28
- return false;
29
35
  }
30
36
 
31
37
  /**