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 +22 -0
- package/package.json +5 -5
- package/src/commands/auto-update.ts +1 -1
- package/src/commands/cache.ts +1 -1
- package/src/commands/history.ts +1 -1
- package/src/commands/install.ts +1 -1
- package/src/commands/registry.ts +2 -2
- package/src/commands/types.ts +1 -1
- package/src/index.ts +1 -1
- package/src/packages/catalog.ts +1 -1
- package/src/packages/discovery.ts +1 -1
- package/src/packages/extensions.ts +20 -12
- package/src/packages/install.ts +1 -1
- package/src/packages/management.ts +1 -1
- package/src/ui/async-task.ts +9 -2
- package/src/ui/help.ts +1 -1
- package/src/ui/package-config.ts +2 -2
- package/src/ui/remote.ts +10 -10
- package/src/ui/theme.ts +1 -1
- package/src/ui/unified.ts +10 -10
- package/src/utils/auto-update.ts +1 -1
- package/src/utils/history.ts +1 -1
- package/src/utils/mode.ts +4 -1
- package/src/utils/notify.ts +4 -1
- package/src/utils/npm-exec.ts +180 -3
- package/src/utils/settings.ts +1 -1
- package/src/utils/status.ts +1 -1
- package/src/utils/ui-helpers.ts +11 -5
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.
|
|
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
|
-
"@
|
|
48
|
-
"@
|
|
47
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
48
|
+
"@earendil-works/pi-tui": "*"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@biomejs/biome": "^2.4.9",
|
|
52
|
-
"@
|
|
53
|
-
"@
|
|
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",
|
package/src/commands/cache.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ExtensionAPI, type ExtensionCommandContext } from "@
|
|
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";
|
package/src/commands/history.ts
CHANGED
package/src/commands/install.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ExtensionAPI, type ExtensionCommandContext } from "@
|
|
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
|
|
package/src/commands/registry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type ExtensionAPI, type ExtensionCommandContext } from "@
|
|
2
|
-
import { type AutocompleteItem } from "@
|
|
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,
|
package/src/commands/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ExtensionAPI,
|
|
8
8
|
type ExtensionCommandContext,
|
|
9
9
|
type ExtensionContext,
|
|
10
|
-
} from "@
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { createAutoUpdateNotificationHandler } from "./commands/auto-update.js";
|
|
12
12
|
import {
|
|
13
13
|
getExtensionsAutocompleteItems,
|
package/src/packages/catalog.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type PackageSource,
|
|
5
5
|
type ProgressEvent,
|
|
6
6
|
SettingsManager,
|
|
7
|
-
} from "@
|
|
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 "@
|
|
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 "@
|
|
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 {
|
|
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
|
-
|
|
63
|
-
|
|
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 =
|
|
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
|
}
|
package/src/packages/install.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type ExtensionAPI,
|
|
9
9
|
type ExtensionCommandContext,
|
|
10
10
|
type ProgressEvent,
|
|
11
|
-
} from "@
|
|
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 "@
|
|
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";
|
package/src/ui/async-task.ts
CHANGED
|
@@ -3,8 +3,15 @@ import {
|
|
|
3
3
|
type ExtensionCommandContext,
|
|
4
4
|
type ExtensionContext,
|
|
5
5
|
type Theme,
|
|
6
|
-
} from "@
|
|
7
|
-
import {
|
|
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 "@
|
|
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 {
|
package/src/ui/package-config.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ExtensionCommandContext,
|
|
8
8
|
getSettingsListTheme,
|
|
9
9
|
type Theme,
|
|
10
|
-
} from "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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 (
|
|
608
|
+
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
610
609
|
this.moveSelection(-1);
|
|
611
610
|
return true;
|
|
612
611
|
}
|
|
613
612
|
|
|
614
|
-
if (
|
|
613
|
+
if (this.keybindings.matches(data, "tui.select.down")) {
|
|
615
614
|
this.moveSelection(1);
|
|
616
615
|
return true;
|
|
617
616
|
}
|
|
618
617
|
|
|
619
|
-
if (
|
|
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 (
|
|
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,
|
|
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
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 "@
|
|
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 "@
|
|
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,
|
|
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 (
|
|
748
|
+
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
749
749
|
this.moveSelection(-1);
|
|
750
750
|
return true;
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
-
if (
|
|
753
|
+
if (this.keybindings.matches(data, "tui.select.down")) {
|
|
754
754
|
this.moveSelection(1);
|
|
755
755
|
return true;
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
-
if (
|
|
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 (
|
|
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
|
}
|
package/src/utils/auto-update.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type ExtensionAPI,
|
|
6
6
|
type ExtensionCommandContext,
|
|
7
7
|
type ExtensionContext,
|
|
8
|
-
} from "@
|
|
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";
|
package/src/utils/history.ts
CHANGED
|
@@ -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 "@
|
|
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 {
|
|
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;
|
package/src/utils/notify.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized notification handling for UI and non-UI modes
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
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
|
|
package/src/utils/npm-exec.ts
CHANGED
|
@@ -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 "@
|
|
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
|
-
):
|
|
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 =
|
|
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,
|
package/src/utils/settings.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type ExtensionAPI,
|
|
11
11
|
type ExtensionCommandContext,
|
|
12
12
|
type ExtensionContext,
|
|
13
|
-
} from "@
|
|
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";
|
package/src/utils/status.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ExtensionCommandContext,
|
|
7
7
|
type ExtensionContext,
|
|
8
8
|
getAgentDir,
|
|
9
|
-
} from "@
|
|
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";
|
package/src/utils/ui-helpers.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Common UI helper patterns
|
|
3
3
|
*/
|
|
4
|
-
import { type ExtensionCommandContext } from "@
|
|
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
|
/**
|